feat: 完成全系统30个功能点开发与E2E全链路验收 (8轮调度32个子代理闭环)
- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
这个提交包含在:
@@ -0,0 +1,228 @@
|
||||
import { getMetadataArgsStorage } from 'typeorm';
|
||||
import { BaseEntity } from '../../../src/modules/base/entity/base';
|
||||
import { BaseAuditLogEntity } from '../../../src/modules/base/entity/audit';
|
||||
import { BaseAuditLogService } from '../../../src/modules/base/service/audit';
|
||||
import { BaseAuditLogController } from '../../../src/modules/base/controller/admin/audit';
|
||||
|
||||
describe('Point 20: 后台系统安全审计日志与行为追踪页面 单元测试', () => {
|
||||
const metadata = getMetadataArgsStorage();
|
||||
|
||||
// ==========================================
|
||||
// 1. 实体定义与数据库字段映射测试
|
||||
// ==========================================
|
||||
describe('1. 实体定义与数据库表映射测试', () => {
|
||||
it('BaseAuditLogEntity 应映射至 base_audit_log 表且继承 BaseEntity', () => {
|
||||
const table = metadata.tables.find((t) => t.target === BaseAuditLogEntity);
|
||||
expect(table).toBeDefined();
|
||||
expect(table?.name).toBe('base_audit_log');
|
||||
expect(BaseAuditLogEntity.prototype instanceof BaseEntity).toBe(true);
|
||||
});
|
||||
|
||||
it('BaseAuditLogEntity 应包含所有合规审计字段映射', () => {
|
||||
const columns = metadata.columns.filter((c) => c.target === BaseAuditLogEntity);
|
||||
const expected: Record<string, string> = {
|
||||
module: 'module',
|
||||
action: 'action',
|
||||
operatorId: 'operator_id',
|
||||
operatorName: 'operator_name',
|
||||
ip: 'ip',
|
||||
detail: 'detail',
|
||||
status: 'status',
|
||||
};
|
||||
|
||||
for (const [prop, dbCol] of Object.entries(expected)) {
|
||||
const col = columns.find((c) => c.propertyName === prop);
|
||||
expect(col).toBeDefined();
|
||||
expect(col?.options.name || col?.propertyName).toBe(dbCol);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// 2. 服务核心业务与自动落库测试 (BaseAuditLogService)
|
||||
// ==========================================
|
||||
describe('2. 审计日志自动记录与多条件过滤测试 (BaseAuditLogService)', () => {
|
||||
let service: BaseAuditLogService;
|
||||
let logs: BaseAuditLogEntity[];
|
||||
let nextId = 1;
|
||||
|
||||
beforeEach(() => {
|
||||
logs = [];
|
||||
nextId = 1;
|
||||
|
||||
// Mock Repository
|
||||
const mockRepo = {
|
||||
save: jest.fn(async (entity: BaseAuditLogEntity) => {
|
||||
const saved = Object.assign(new BaseAuditLogEntity(), entity, {
|
||||
id: entity.id || nextId++,
|
||||
createTime: entity.createTime || new Date(),
|
||||
updateTime: new Date(),
|
||||
status: entity.status !== undefined ? entity.status : 1,
|
||||
});
|
||||
const existingIdx = logs.findIndex((l) => l.id === saved.id);
|
||||
if (existingIdx >= 0) {
|
||||
logs[existingIdx] = saved;
|
||||
} else {
|
||||
logs.push(saved);
|
||||
}
|
||||
return saved;
|
||||
}),
|
||||
insert: jest.fn(async (entity: BaseAuditLogEntity) => {
|
||||
const saved = Object.assign(new BaseAuditLogEntity(), entity, {
|
||||
id: nextId++,
|
||||
createTime: new Date(),
|
||||
updateTime: new Date(),
|
||||
status: entity.status !== undefined ? entity.status : 1,
|
||||
});
|
||||
logs.push(saved);
|
||||
return { identifiers: [{ id: saved.id }] };
|
||||
}),
|
||||
findAndCount: jest.fn(async (options?: {
|
||||
where?: Record<string, unknown>;
|
||||
skip?: number;
|
||||
take?: number;
|
||||
order?: Record<string, 'ASC' | 'DESC'>;
|
||||
}) => {
|
||||
let filtered = [...logs];
|
||||
if (options?.where) {
|
||||
filtered = filtered.filter((item) => {
|
||||
const record = item as unknown as Record<string, unknown>;
|
||||
for (const [k, v] of Object.entries(options.where!)) {
|
||||
if (v !== undefined && record[k] !== v) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
const skip = options?.skip || 0;
|
||||
const take = options?.take || 20;
|
||||
return [filtered.slice(skip, skip + take), filtered.length];
|
||||
}),
|
||||
};
|
||||
|
||||
service = new BaseAuditLogService();
|
||||
service.baseAuditLogEntity = mockRepo as unknown as BaseAuditLogService['baseAuditLogEntity'];
|
||||
});
|
||||
|
||||
it('record 应能自动落库审计日志,默认 status 为 1', async () => {
|
||||
const created = await service.record({
|
||||
module: 'auth',
|
||||
action: 'login',
|
||||
operatorId: 10,
|
||||
operatorName: 'SecAdmin',
|
||||
ip: '192.168.1.100',
|
||||
detail: JSON.stringify({ browser: 'Chrome', os: 'Windows' }),
|
||||
});
|
||||
|
||||
expect(created.id).toBeDefined();
|
||||
expect(created.module).toBe('auth');
|
||||
expect(created.action).toBe('login');
|
||||
expect(created.operatorId).toBe(10);
|
||||
expect(created.operatorName).toBe('SecAdmin');
|
||||
expect(created.ip).toBe('192.168.1.100');
|
||||
expect(created.status).toBe(1);
|
||||
expect(logs.length).toBe(1);
|
||||
});
|
||||
|
||||
it('record 支持显式指定失败状态 status=0', async () => {
|
||||
const created = await service.record({
|
||||
module: 'nfc',
|
||||
action: 'batch_export',
|
||||
operatorId: 12,
|
||||
operatorName: 'TestUser',
|
||||
ip: '10.0.0.1',
|
||||
detail: 'Permission denied for batch NFC export',
|
||||
status: 0,
|
||||
});
|
||||
|
||||
expect(created.status).toBe(0);
|
||||
expect(created.module).toBe('nfc');
|
||||
expect(logs[0].status).toBe(0);
|
||||
});
|
||||
|
||||
it('filterLogs 应支持按 module, status 及分页多条件过滤', async () => {
|
||||
// 预置多条审计日志
|
||||
await service.record({
|
||||
module: 'auth',
|
||||
action: 'login',
|
||||
operatorId: 1,
|
||||
operatorName: 'Alice',
|
||||
ip: '127.0.0.1',
|
||||
status: 1,
|
||||
});
|
||||
await service.record({
|
||||
module: 'auth',
|
||||
action: 'logout',
|
||||
operatorId: 1,
|
||||
operatorName: 'Alice',
|
||||
ip: '127.0.0.1',
|
||||
status: 1,
|
||||
});
|
||||
await service.record({
|
||||
module: 'nfc',
|
||||
action: 'format_card',
|
||||
operatorId: 2,
|
||||
operatorName: 'Bob',
|
||||
ip: '192.168.1.20',
|
||||
status: 0,
|
||||
});
|
||||
|
||||
// 1. 按 module 过滤
|
||||
const authLogs = await service.filterLogs({ module: 'auth', page: 1, size: 10 });
|
||||
expect(authLogs.total).toBe(2);
|
||||
expect(authLogs.list.every((l) => l.module === 'auth')).toBe(true);
|
||||
|
||||
// 2. 按 status 过滤
|
||||
const failedLogs = await service.filterLogs({ status: 0, page: 1, size: 10 });
|
||||
expect(failedLogs.total).toBe(1);
|
||||
expect(failedLogs.list[0].action).toBe('format_card');
|
||||
|
||||
// 3. 关键字模糊搜索
|
||||
const bobLogs = await service.filterLogs({ keyWord: 'Bob', page: 1, size: 10 });
|
||||
expect(bobLogs.total).toBe(1);
|
||||
expect(bobLogs.list[0].operatorName).toBe('Bob');
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// 3. 控制器契约与接口测试 (BaseAuditLogController)
|
||||
// ==========================================
|
||||
describe('3. 控制器路由与接口测试 (BaseAuditLogController)', () => {
|
||||
let controller: BaseAuditLogController;
|
||||
let auditService: BaseAuditLogService;
|
||||
|
||||
beforeEach(() => {
|
||||
auditService = new BaseAuditLogService();
|
||||
auditService.record = jest.fn(async (params) => {
|
||||
const entity = new BaseAuditLogEntity();
|
||||
Object.assign(entity, params, { id: 99, createTime: new Date() });
|
||||
return entity;
|
||||
});
|
||||
|
||||
controller = new BaseAuditLogController();
|
||||
controller.baseAuditLogService = auditService;
|
||||
controller.ok = (data: unknown) => ({
|
||||
code: 1000,
|
||||
message: 'success',
|
||||
data,
|
||||
});
|
||||
});
|
||||
|
||||
it('控制器应提供 record 方法并返回 standard 1000 格式', async () => {
|
||||
expect(typeof controller.record).toBe('function');
|
||||
const res = await controller.record({
|
||||
module: 'team',
|
||||
action: 'role_update',
|
||||
operatorName: 'SuperAdmin',
|
||||
});
|
||||
expect(res.code).toBe(1000);
|
||||
expect(auditService.record).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
module: 'team',
|
||||
action: 'role_update',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户