feat: harden platform workflows and UI governance
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { BadRequestException, ConflictException } from '@nestjs/common';
|
||||
import { ReviewGovernanceService } from './review-governance.service';
|
||||
|
||||
function signature(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: 'sig-1', tenantId: 'tenant-1', applicationId: 'app-1', name: '签名A', purpose: null,
|
||||
drainageInfo: { signatureProfile: { companyName: '企业A', creditCode: '9133', legalPersonName: '法人', responsibleName: '责任人', responsiblePhone: '13800000000', credentialFile: { fileObjectId: 'file-1' } } },
|
||||
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<string, any> = {
|
||||
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: {}, 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(['未绑定短信应用', '缺少公司名称', '缺少资质文件']));
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user