import { BadRequestException, ConflictException } from '@nestjs/common'; import { ReviewGovernanceService } from './review-governance.service'; function signature(overrides: Record = {}) { return { id: 'sig-1', tenantId: 'tenant-1', applicationId: 'app-1', name: '签名A', purpose: null, drainageInfo: { signatureReportValues: { companyName: '企业A', credentialFile: { fileObjectId: 'file-1' } }, reportRequirementSnapshot: { fields: [ { code: 'companyName', name: '公司名称', required: true, reportTypes: ['signature'] }, { code: 'credentialFile', name: '资质文件', required: true, reportTypes: ['signature'] }, { code: 'siteOwner', name: '网站主体', required: true, reportTypes: ['drainage'] }, ], }, }, auditStatus: 'pending', reportStatus: 'waiting_material', rejectReason: null, materialVersion: 1, pendingReport: true, reportChangedAt: new Date(), createdAt: new Date(), updatedAt: new Date('2026-07-21T08:00:00.000Z'), tenant: { id: 'tenant-1', name: '企业A' }, application: { id: 'app-1', name: '应用A' }, materials: [], ...overrides, }; } function prismaMock() { const current = signature(); const prisma: Record = { smsSignature: { findUnique: jest.fn().mockResolvedValue(current), updateMany: jest.fn().mockResolvedValue({ count: 1 }), }, smsTemplate: { findUnique: jest.fn(), updateMany: jest.fn() }, auditRecord: { findFirst: jest.fn().mockResolvedValue(null), create: jest.fn().mockResolvedValue({ id: 'audit-1', statusAfter: 'approved' }), }, }; prisma.$transaction = jest.fn(async (callback: (tx: typeof prisma) => unknown) => callback(prisma)); return prisma; } describe('ReviewGovernanceService', () => { it('blocks approval when required signature qualification is incomplete', async () => { const prisma = prismaMock(); prisma.smsSignature.findUnique.mockResolvedValue(signature({ applicationId: null, application: null, drainageInfo: { signatureReportValues: {}, reportRequirementSnapshot: { fields: [ { code: 'companyName', name: '公司名称', required: true, reportTypes: ['signature'] }, { code: 'credentialFile', name: '资质文件', required: true, reportTypes: ['both'] }, { code: 'siteOwner', name: '网站主体', required: true, reportTypes: ['drainage'] }, ], }, }, materials: [], })); const service = new ReviewGovernanceService(prisma as never); const result = await service.preflight('signature', 'sig-1'); expect(result.allowedActions).toEqual(['reject']); expect(result.blockedReasons).toEqual(expect.arrayContaining(['未绑定短信应用', '缺少公司名称', '缺少资质文件'])); expect(result.blockedReasons).not.toContain('缺少网站主体'); }); it('does not invent legacy qualification requirements when the configured channel has no signature fields', async () => { const prisma = prismaMock(); prisma.smsSignature.findUnique.mockResolvedValue(signature({ drainageInfo: { signatureReportValues: {}, reportRequirementSnapshot: { fields: [{ code: 'icpNo', name: 'ICP备案号', required: true, reportTypes: ['drainage'] }], }, }, })); const service = new ReviewGovernanceService(prisma as never); const result = await service.preflight('signature', 'sig-1'); expect(result.allowedActions).toEqual(['approve', 'reject']); expect(result.blockedReasons).toEqual([]); }); it('atomically approves the expected version and returns an audit operation id', async () => { const prisma = prismaMock(); const service = new ReviewGovernanceService(prisma as never); await expect(service.decide('signature', 'sig-1', { decision: 'approve', expectedUpdatedAt: '2026-07-21T08:00:00.000Z', idempotencyKey: 'review:sig-1:key-1', reviewerId: 'admin-1', })).resolves.toEqual(expect.objectContaining({ operationId: 'audit-1', replayed: false, status: 'approved' })); expect(prisma.smsSignature.updateMany).toHaveBeenCalledWith({ where: { id: 'sig-1', auditStatus: 'pending', updatedAt: new Date('2026-07-21T08:00:00.000Z') }, data: { auditStatus: 'approved', rejectReason: null }, }); expect(prisma.auditRecord.create).toHaveBeenCalledWith({ data: expect.objectContaining({ reviewerId: 'admin-1', action: 'approve', statusBefore: 'pending', statusAfter: 'approved' }) }); }); it('rejects a concurrent stale decision without overwriting the winner', async () => { const prisma = prismaMock(); prisma.smsSignature.updateMany.mockResolvedValue({ count: 0 }); const service = new ReviewGovernanceService(prisma as never); await expect(service.decide('signature', 'sig-1', { decision: 'approve', expectedUpdatedAt: '2026-07-21T08:00:00.000Z', idempotencyKey: 'review:sig-1:key-2', reviewerId: 'admin-1', })).rejects.toBeInstanceOf(ConflictException); expect(prisma.auditRecord.create).not.toHaveBeenCalled(); }); it('replays a completed idempotency key without a second status update', async () => { const prisma = prismaMock(); prisma.auditRecord.findFirst.mockResolvedValue({ id: 'audit-existing', action: 'approve', statusAfter: 'approved' }); const service = new ReviewGovernanceService(prisma as never); await expect(service.decide('signature', 'sig-1', { decision: 'approve', expectedUpdatedAt: '2026-07-21T08:00:00.000Z', idempotencyKey: 'review:sig-1:same', reviewerId: 'admin-1', })).resolves.toEqual(expect.objectContaining({ operationId: 'audit-existing', replayed: true })); expect(prisma.smsSignature.updateMany).not.toHaveBeenCalled(); }); it('requires a valid server session reviewer and rejects malformed idempotency keys', async () => { const service = new ReviewGovernanceService(prismaMock() as never); await expect(service.decide('signature', 'sig-1', { decision: 'approve', expectedUpdatedAt: new Date().toISOString(), idempotencyKey: '../bad' })) .rejects.toBeInstanceOf(BadRequestException); }); });