feat: harden platform workflows and UI governance
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user