文件
OmniCard/backend/test/modules/card/entity.test.ts

167 行
6.8 KiB
TypeScript

import { getMetadataArgsStorage } from 'typeorm';
import { BaseEntity, transformerTime } from '../../../src/comm/entity/base';
import { CardInfoEntity } from '../../../src/modules/card/entity/info';
import { CardProfile, CardStatus } from '../../../src/comm/types/omni';
describe('后端统一实体基类与名片实体 (Entity & BaseEntity)', () => {
const metadata = getMetadataArgsStorage();
const matchEntity = (target: any) =>
target === BaseEntity || target === CardInfoEntity || (target && target.prototype instanceof BaseEntity);
describe('1. 通用实体基类 (comm/entity/base.ts)', () => {
it('BaseEntity 应定义标准字段: id, tenantId, createTime, updateTime, deletedAt', () => {
const columns = metadata.columns.filter(c => matchEntity(c.target));
const columnNames = columns.map(c => c.propertyName);
expect(columnNames).toContain('id');
expect(columnNames).toContain('tenantId');
expect(columnNames).toContain('createTime');
expect(columnNames).toContain('updateTime');
expect(columnNames).toContain('deletedAt');
});
it('基类字段的数据库列名应严格遵循下划线命名规范', () => {
const columns = metadata.columns.filter(c => matchEntity(c.target));
const tenantIdCol = columns.find(c => c.propertyName === 'tenantId');
expect(tenantIdCol?.options.name).toBe('tenant_id');
const createTimeCol = columns.find(c => c.propertyName === 'createTime');
expect(createTimeCol?.options.name).toBe('create_time');
const updateTimeCol = columns.find(c => c.propertyName === 'updateTime');
expect(updateTimeCol?.options.name).toBe('update_time');
const deletedAtCol = columns.find(c => c.propertyName === 'deletedAt');
expect(deletedAtCol?.options.name).toBe('deleted_at');
});
it('deletedAt 必须被标记为软删除列或具备软删除特性', () => {
const deleteDateCols = metadata.columns.filter(
c => matchEntity(c.target) && c.propertyName === 'deletedAt'
);
expect(deleteDateCols.length).toBeGreaterThan(0);
expect(deleteDateCols[0].mode).toBe('deleteDate');
});
it('基类索引应正确声明在 tenantId 上以保障多租户隔离查询性能', () => {
const indexes = metadata.indices.filter(i => matchEntity(i.target));
const tenantIndex = indexes.find(i => {
if (Array.isArray(i.columns)) {
return i.columns.includes('tenantId') || i.columns.includes('tenant_id');
}
if (typeof i.columns === 'function') {
return (i.columns as any)({ tenantId: true });
}
return false;
});
expect(tenantIndex).toBeDefined();
});
it('transformerTime 应保证时间格式化正确', () => {
const formatted = transformerTime.to('2026-09-20 12:00:00');
expect(formatted).toBe('2026-09-20 12:00:00');
const currentFormatted = transformerTime.to(undefined);
expect(currentFormatted).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
});
});
describe('2. 名片实体 (modules/card/entity/info.ts)', () => {
it('实体表名必须为 card_info', () => {
const table = metadata.tables.find(t => t.target === CardInfoEntity);
expect(table).toBeDefined();
expect(table?.name).toBe('card_info');
});
it('应完整包含 CardProfile 契约与扩展字段,且全部使用下划线数据库列名', () => {
const columns = metadata.columns.filter(c => c.target === CardInfoEntity);
const expectedColumnMappings: Record<string, string> = {
userId: 'user_id',
name: 'name',
title: 'title',
company: 'company',
slogan: 'slogan',
avatarUrl: 'avatar_url',
phone: 'phone',
email: 'email',
whatsapp: 'whatsapp',
linkedin: 'linkedin',
timezone: 'timezone',
audioPronunciationUrl: 'audio_pronunciation_url',
status: 'status',
themeCode: 'theme_code',
slug: 'slug',
};
for (const [prop, dbCol] of Object.entries(expectedColumnMappings)) {
const col = columns.find(c => c.propertyName === prop);
expect(col).toBeDefined();
expect(col?.options.name || col?.propertyName).toBe(dbCol);
}
});
it('slug 字段必须设置唯一索引 (unique: true) 用于短链与 NFC 极速路由', () => {
const indexes = metadata.indices.filter(i => i.target === CardInfoEntity);
const slugIndex = indexes.find(i => {
const matchProp = Array.isArray(i.columns) && (i.columns.includes('slug') || (i.columns as any[])[0] === 'slug');
return matchProp && i.unique === true;
});
expect(slugIndex).toBeDefined();
expect(slugIndex?.unique).toBe(true);
});
it('userId 必须设置索引以优化个人名片列表查询', () => {
const indexes = metadata.indices.filter(i => i.target === CardInfoEntity);
const userIndex = indexes.find(i => {
if (Array.isArray(i.columns)) {
return i.columns.includes('userId') || i.columns.includes('user_id');
}
return false;
});
expect(userIndex).toBeDefined();
});
it('默认值设置应合理且符合规范 (status=\'draft\', themeCode=\'corporate_navy\', timezone=\'UTC+8\')', () => {
const columns = metadata.columns.filter(c => c.target === CardInfoEntity);
const statusCol = columns.find(c => c.propertyName === 'status');
expect(statusCol?.options.default).toBe('draft');
const themeCol = columns.find(c => c.propertyName === 'themeCode');
expect(themeCol?.options.default).toBe('corporate_navy');
const tzCol = columns.find(c => c.propertyName === 'timezone');
expect(tzCol?.options.default).toBe('UTC+8');
});
it('CardInfoEntity 实例应能赋值并完全满足 CardProfile 契约接口', () => {
const card = new CardInfoEntity();
card.id = 1;
card.tenantId = 100;
card.userId = 10;
card.name = 'Alex Zhang';
card.title = 'Global BD Director';
card.company = 'OmniCard Global Ltd.';
card.slogan = 'Connecting Global Business Seamlessly';
card.avatarUrl = 'https://cdn.omnicard.ai/avatar/alex.jpg';
card.phone = '+86 13800138000';
card.email = 'alex@omnicard.ai';
card.whatsapp = '+8613800138000';
card.linkedin = 'https://linkedin.com/in/alex-zhang';
card.timezone = 'UTC+8';
card.audioPronunciationUrl = 'https://cdn.omnicard.ai/audio/alex.mp3';
card.status = 'published';
card.themeCode = 'tech_dark';
card.slug = 'alex-zhang-bd';
// 类型断言与赋值检查
const profile: CardProfile = card;
expect(profile.name).toBe('Alex Zhang');
expect(profile.status).toBe('published');
expect(profile.themeCode).toBe('tech_dark');
expect(card.slug).toBe('alex-zhang-bd');
});
});
});