167 行
6.8 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|