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 = { 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; skip?: number; take?: number; order?: Record; }) => { let filtered = [...logs]; if (options?.where) { filtered = filtered.filter((item) => { const record = item as unknown as Record; 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', }) ); }); }); });