文件
OmniCard/backend/test/modules/card/vcard-export-api.test.ts
wikiboss 62e0ae4435 feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口
补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 18:01:47 +08:00

133 行
4.4 KiB
TypeScript

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);
});
});
});