feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口
补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
这个提交包含在:
@@ -0,0 +1,132 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { CoolCommException } from '@cool-midway/core';
|
||||
import { CardInfoEntity } from '../../../src/modules/card/entity/info';
|
||||
import { CardInfoService } from '../../../src/modules/card/service/info';
|
||||
import { AdminCardInfoController } from '../../../src/modules/card/controller/admin/info';
|
||||
|
||||
/**
|
||||
* 内存 Repository,仅覆盖 findOneBy
|
||||
*/
|
||||
class MemoryCardInfoRepo {
|
||||
private data: CardInfoEntity[] = [];
|
||||
|
||||
constructor(initialData: Array<Partial<CardInfoEntity>> = []) {
|
||||
this.data = initialData.map(item => {
|
||||
const entity = new CardInfoEntity();
|
||||
Object.assign(entity, item);
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
async findOneBy(
|
||||
where: Partial<Pick<CardInfoEntity, 'id'>>
|
||||
): Promise<CardInfoEntity | null> {
|
||||
const found = this.data.find(item => {
|
||||
if (where.id === undefined) {
|
||||
return false;
|
||||
}
|
||||
return Number(item.id) === Number(where.id);
|
||||
});
|
||||
return found ? ({ ...found } as CardInfoEntity) : null;
|
||||
}
|
||||
}
|
||||
|
||||
describe('后台名片 vCard 导出 API', () => {
|
||||
const controllerFile = path.join(
|
||||
__dirname,
|
||||
'../../../src/modules/card/controller/admin/info.ts'
|
||||
);
|
||||
const serviceFile = path.join(
|
||||
__dirname,
|
||||
'../../../src/modules/card/service/info.ts'
|
||||
);
|
||||
|
||||
describe('1. 路由与方法约定', () => {
|
||||
it('admin info 控制器应声明 /vcard 导出接口', () => {
|
||||
expect(fs.existsSync(controllerFile)).toBe(true);
|
||||
const content = fs.readFileSync(controllerFile, 'utf-8');
|
||||
expect(content).toMatch(/@Post\(\s*['"]\/vcard['"]/);
|
||||
expect(content).toMatch(/exportVCard|exportVcard/);
|
||||
});
|
||||
|
||||
it('CardInfoService 应提供 exportVCard 方法', () => {
|
||||
expect(fs.existsSync(serviceFile)).toBe(true);
|
||||
const content = fs.readFileSync(serviceFile, 'utf-8');
|
||||
expect(content).toContain('exportVCard');
|
||||
expect(content).toContain('generateVCard');
|
||||
expect(content).not.toContain("FN:假");
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. exportVCard 业务', () => {
|
||||
let service: CardInfoService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new CardInfoService();
|
||||
service.cardInfoEntity = new MemoryCardInfoRepo([
|
||||
{
|
||||
id: 42,
|
||||
name: 'Alex Morgan',
|
||||
title: 'CTO',
|
||||
company: 'OmniCard AI',
|
||||
phone: '+86-13800138000',
|
||||
email: 'alex@omnicard.ai',
|
||||
slug: 'alex-morgan',
|
||||
status: 'published',
|
||||
},
|
||||
]) as unknown as CardInfoService['cardInfoEntity'];
|
||||
});
|
||||
|
||||
it('名片不存在时应抛出 CoolCommException,不得返回写死姓名', async () => {
|
||||
await expect(service.exportVCard(99999)).rejects.toThrow(CoolCommException);
|
||||
await expect(service.exportVCard(99999)).rejects.toThrow(/不存在|not found/i);
|
||||
});
|
||||
|
||||
it('名片存在时返回的 vcf 必须含 BEGIN:VCARD 且包含真实姓名', async () => {
|
||||
const vcf = await service.exportVCard(42);
|
||||
expect(vcf).toContain('BEGIN:VCARD');
|
||||
expect(vcf).toContain('END:VCARD');
|
||||
expect(vcf).toContain('FN:Alex Morgan');
|
||||
expect(vcf).not.toContain('假姓名');
|
||||
});
|
||||
});
|
||||
|
||||
describe('3. 控制器委派', () => {
|
||||
it('vcard 接口应按 id 调用 service.exportVCard 并 ok 返回', async () => {
|
||||
const controller = new AdminCardInfoController();
|
||||
const exportVCard = jest.fn().mockResolvedValue(
|
||||
'BEGIN:VCARD\r\nVERSION:4.0\r\nFN:Alex Morgan\r\nEND:VCARD\r\n'
|
||||
);
|
||||
controller.cardInfoService = {
|
||||
exportVCard,
|
||||
} as unknown as CardInfoService;
|
||||
controller.ok = jest.fn((data: unknown) => ({ code: 1000, data })) as never;
|
||||
|
||||
const result = await controller.vcard({ id: 42 });
|
||||
|
||||
expect(exportVCard).toHaveBeenCalledWith(42);
|
||||
expect(controller.ok).toHaveBeenCalled();
|
||||
const okArg = (controller.ok as jest.Mock).mock.calls[0][0] as {
|
||||
vcard?: string;
|
||||
filename?: string;
|
||||
};
|
||||
expect(okArg.vcard).toContain('BEGIN:VCARD');
|
||||
expect(okArg.filename).toMatch(/\.vcf$/);
|
||||
expect(result).toEqual({
|
||||
code: 1000,
|
||||
data: okArg,
|
||||
});
|
||||
});
|
||||
|
||||
it('缺少 id 时应抛 CoolCommException', async () => {
|
||||
const controller = new AdminCardInfoController();
|
||||
controller.cardInfoService = {
|
||||
exportVCard: jest.fn(),
|
||||
} as unknown as CardInfoService;
|
||||
|
||||
await expect(controller.vcard({})).rejects.toThrow(CoolCommException);
|
||||
await expect(controller.vcard({ id: 0 })).rejects.toThrow(CoolCommException);
|
||||
});
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户