feat: strengthen risk controls and review workflows
This commit is contained in:
@@ -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' }] },
|
||||
|
||||
Reference in New Issue
Block a user