feat: strengthen risk controls and review workflows

This commit is contained in:
hectorzhao
2026-07-26 13:08:12 +08:00
parent b461532075
commit 2ce682c3fc
34 changed files with 2167 additions and 390 deletions
+76 -1
View File
@@ -43,6 +43,7 @@ function createPrismaMock() {
tenant: { id: 'tenant-1', name: '租户A', code: 'TENANT-A' },
}),
update: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'app-1', tenantId: 'tenant-1', ...data })),
updateMany: jest.fn().mockResolvedValue({ count: 1 }),
create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'app-new', ...data })),
},
smsApplicationIpAllowlist: {
@@ -147,12 +148,22 @@ function createPrismaMock() {
update: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'downstream-1', ...data })),
delete: jest.fn().mockResolvedValue({ id: 'downstream-1' }),
deleteMany: jest.fn().mockResolvedValue({ count: 0 }),
count: jest.fn().mockResolvedValue(1),
},
smsMessageRecord: {
groupBy: jest.fn().mockResolvedValue([
{ applicationId: 'app-1', status: 'delivered', _count: { _all: 1 } },
{ applicationId: 'app-1', status: 'undelivered', _count: { _all: 1 } },
]),
count: jest.fn().mockResolvedValue(0),
},
cmppDownstreamDelivery: {
count: jest.fn().mockResolvedValue(0),
findMany: jest.fn().mockResolvedValue([]),
updateMany: jest.fn().mockResolvedValue({ count: 0 }),
},
cmppDownstreamDeliveryAttempt: {
updateMany: jest.fn().mockResolvedValue({ count: 0 }),
},
smsChannel: {
findFirst: jest.fn().mockResolvedValue({
@@ -236,6 +247,71 @@ describe('SmsConfigService', () => {
}));
});
it('moves an application with outstanding receipts into disabling for 72 hours', async () => {
const prisma = createPrismaMock();
prisma.smsMessageRecord.count.mockResolvedValue(1);
const service = new SmsConfigService(prisma as never);
const result = await service.changeApplicationStatus('app-1', {
status: 'disabled',
reason: '运营端停用',
});
expect(result).toEqual(expect.objectContaining({
status: 'disabling',
autoDisableAt: expect.any(Date),
deactivation: expect.objectContaining({ awaitingSupplierReceipt: 1 }),
}));
expect(prisma.smsApplication.update).toHaveBeenCalledWith(expect.objectContaining({
where: { id: 'app-1' },
data: expect.objectContaining({
status: 'disabling',
disablingAt: expect.any(Date),
autoDisableAt: expect.any(Date),
}),
}));
});
it('automatically disables and abandons outstanding deliveries after 72 hours', async () => {
const prisma = createPrismaMock();
prisma.smsApplication.findMany.mockResolvedValue([{
id: 'app-1',
tenantId: 'tenant-1',
cmppAccount: '100001',
status: 'disabling',
autoDisableAt: new Date(Date.now() - 1_000),
}]);
prisma.smsApplication.findUnique.mockResolvedValue({
id: 'app-1',
tenantId: 'tenant-1',
cmppAccount: '100001',
status: 'disabling',
disablingAt: new Date(Date.now() - 73 * 60 * 60 * 1_000),
autoDisableAt: new Date(Date.now() - 1_000),
disableReason: '等待清算',
});
prisma.smsMessageRecord.count.mockResolvedValue(1);
prisma.cmppDownstreamDelivery.findMany.mockResolvedValue([{ id: 'delivery-1' }]);
prisma.cmppDownstreamDelivery.updateMany.mockResolvedValue({ count: 1 });
const originalFetch = global.fetch;
global.fetch = jest.fn().mockResolvedValue({
ok: true,
text: jest.fn().mockResolvedValue('{"account":"100001","disconnected":2}'),
}) as never;
const service = new SmsConfigService(prisma as never);
await service['runApplicationDisableScan']();
expect(prisma.smsApplication.updateMany).toHaveBeenCalledWith(expect.objectContaining({
where: expect.objectContaining({ id: 'app-1', status: 'disabling' }),
data: expect.objectContaining({ status: 'disabled' }),
}));
expect(prisma.cmppDownstreamDelivery.updateMany).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({ status: 'abandoned', retryEnabled: false }),
}));
global.fetch = originalFetch;
});
it('sorts enterprise applications by today send count descending with a stable name tie-breaker', async () => {
const prisma = createPrismaMock();
const baseApplication = {
@@ -324,7 +400,6 @@ describe('SmsConfigService', () => {
interfaceType: 'cmpp20',
queuePriority: 'priority',
dailyLimit: 100000,
maxPhonesPerTask: 10000,
downstreamReceiptRetryEnabled: true,
downstreamUplinkRetryEnabled: true,
ipAllowlist: { create: [{ ipCidr: '10.0.0.1/32' }] },