feat: harden platform workflows and UI governance

This commit is contained in:
hectorzhao
2026-07-22 14:14:55 +08:00
parent ef957f7daa
commit 0f223f7f91
80 changed files with 4958 additions and 764 deletions
@@ -2,6 +2,9 @@ import { OperationsService } from './operations.service';
function createPrismaMock() {
return {
user: {
findFirst: jest.fn().mockResolvedValue({ tenantId: 'tenant-1' }),
},
smsBatchTask: {
findMany: jest.fn().mockResolvedValue([{ id: 'task-1', taskNo: 'BATCH-1' }]),
count: jest.fn().mockResolvedValue(3),
@@ -250,6 +253,64 @@ describe('OperationsService', () => {
});
});
it('returns client message views without supplier channel, submit, tenant, or gateway internals', async () => {
const prisma = createPrismaMock();
prisma.smsMessageRecord.findMany.mockResolvedValue([{
id: 'record-1',
tenantId: 'tenant-1',
batchTaskId: 'task-1',
applicationId: 'app-1',
channelId: 'channel-1',
messageId: 'MSG-1',
phoneNumber: '13800000001',
carrier: 'mobile',
province: '上海',
content: '验证码1234',
billingUnits: 1,
amountCents: 352,
status: 'delivered',
queuedAt: new Date('2026-07-21T01:00:00.000Z'),
application: { id: 'app-1', name: '应用A', secretHash: 'secret' },
tenant: { id: 'tenant-1', name: '企业A' },
channel: { id: 'channel-1', account: 'supplier', passwordCipher: 'cipher', unitPrice: 200 },
submitRecords: [{ id: 'submit-1', gatewayMessageId: 'GW-1', channel: { passwordCipher: 'cipher' } }],
receiptRecords: [{
id: 'receipt-1', messageId: 'MSG-1', gatewayMessageId: 'GW-1', receiptStatus: 'delivered', rawStatus: 'DELIVRD',
errorCode: null, errorMessage: null, deliveredAt: new Date('2026-07-21T01:00:05.000Z'), createdAt: new Date('2026-07-21T01:00:05.000Z'),
}],
}]);
const service = new OperationsService(prisma as never);
const [message] = await service.listClientMessages({ tenantId: 'tenant-1' });
expect(message).toMatchObject({
id: 'record-1', messageId: 'MSG-1', carrier: 'mobile', province: '上海', application: { id: 'app-1', name: '应用A' },
receiptRecords: [expect.objectContaining({ receiptStatus: 'delivered', rawStatus: 'DELIVRD' })],
});
expect(message).not.toHaveProperty('tenant');
expect(message).not.toHaveProperty('tenantId');
expect(message).not.toHaveProperty('channel');
expect(message).not.toHaveProperty('channelId');
expect(message).not.toHaveProperty('submitRecords');
expect(JSON.stringify(message)).not.toMatch(/passwordCipher|gatewayMessageId|unitPrice|supplier|cipher/);
});
it('returns a client dashboard without gateway state or supplier channel secrets', async () => {
const prisma = createPrismaMock();
prisma.smsBatchTask.findMany.mockResolvedValue([{
id: 'task-1', taskNo: 'BATCH-1', tenantId: 'tenant-1', applicationId: 'app-1', phoneTotal: 1, status: 'finished',
createdAt: new Date('2026-07-21T01:00:00.000Z'), application: { id: 'app-1', name: '应用A' },
messages: [{ channel: { account: 'supplier', passwordCipher: 'cipher', unitPrice: 200 } }],
}]);
const service = new OperationsService(prisma as never);
const dashboard = await service.clientDashboard({ tenantId: 'tenant-1' });
expect(dashboard.gatewayConnections).toEqual([]);
expect(dashboard.recentTasks).toEqual([expect.objectContaining({ id: 'task-1', taskNo: 'BATCH-1' })]);
expect(JSON.stringify(dashboard)).not.toMatch(/passwordCipher|supplier|cipher|unitPrice/);
});
it('builds dashboard and statistics aggregates', async () => {
const prisma = createPrismaMock();
prisma.cmppDownstreamDelivery.count = jest.fn()
@@ -390,6 +451,32 @@ describe('OperationsService', () => {
});
});
it('exports filtered operation logs with a traceable operation id', async () => {
const prisma = createPrismaMock();
const service = new OperationsService(prisma as never);
await expect(service.exportSystemLogs({ tenantId: 'tenant-1', range: '7d' })).resolves.toEqual(expect.objectContaining({
operationId: expect.any(String),
status: 'completed',
recordCount: 1,
truncated: false,
content: expect.stringContaining('billing.manual_recharge'),
}));
expect(prisma.operationLog.findMany).toHaveBeenCalledWith(expect.objectContaining({ take: 10_001 }));
});
it('derives client log export tenant from session user and removes internal detail and IP columns', async () => {
const prisma = createPrismaMock();
const service = new OperationsService(prisma as never);
const exported = await service.exportSystemLogs({ tenantId: 'spoofed-tenant' }, 'client-user');
expect(prisma.user.findFirst).toHaveBeenCalledWith(expect.objectContaining({ where: expect.objectContaining({ id: 'client-user' }) }));
expect(prisma.operationLog.findMany).toHaveBeenCalledWith(expect.objectContaining({ where: expect.objectContaining({ tenantId: 'tenant-1' }) }));
expect(exported.content.split('\n')[0]).toBe('时间,级别,模块,操作人,动作,资源ID');
expect(exported.content).not.toContain('amountCents');
});
it('caps legacy audit-log reads with pagination', async () => {
const prisma = createPrismaMock();
const service = new OperationsService(prisma as never);