feat: harden CMPP delivery and platform workflows

This commit is contained in:
hectorzhao
2026-07-20 18:07:29 +08:00
parent 80fb5a8f53
commit f02c33cbb7
61 changed files with 1834 additions and 281 deletions
+179 -7
View File
@@ -163,6 +163,8 @@ function createPrismaMock() {
},
smsReceiptRecord: {
create: jest.fn().mockResolvedValue({ id: 'receipt-1' }),
upsert: jest.fn().mockResolvedValue({ id: 'receipt-1', createdAt: new Date('2026-07-01T10:01:00.000Z') }),
findUnique: jest.fn().mockResolvedValue(null),
findFirst: jest.fn().mockResolvedValue(null),
findMany: jest.fn(),
},
@@ -355,6 +357,28 @@ describe('SendChainService', () => {
expect(service.enqueueBatchTask).toHaveBeenCalledWith('task-1');
});
it('recognizes an approved template for public HTTP content and reads back the api task', async () => {
const { service, prisma, riskReview } = createService();
service.enqueueBatchTask = jest.fn().mockResolvedValue({ taskId: 'task-1', enqueued: 1 });
prisma.smsTemplate.findFirst.mockResolvedValue({
id: 'tpl-http', tenantId: 'tenant-1', applicationId: 'app-1', content: '【签名】验证码${code}',
auditStatus: 'approved', signatureId: 'sig-1', signature: { id: 'sig-1', auditStatus: 'approved' },
});
await service.createHttpBatchTask({
tenantId: 'tenant-1', applicationId: 'app-1', content: '【签名】验证码123456', phones: ['13800000001'],
sourceIp: '127.0.0.1', clientMessageId: 'client-http-1',
});
expect(riskReview.evaluateTask).toHaveBeenCalledWith(expect.objectContaining({
templateId: 'tpl-http',
variables: { code: '123456' },
}));
expect(prisma.smsBatchTask.findFirst).toHaveBeenCalledWith(expect.objectContaining({
where: expect.objectContaining({ id: 'task-1', sourceType: 'api' }),
}));
});
it('persists the unique longest approved drainage URL match on new message records', async () => {
const { service, prisma } = createService();
service.enqueueBatchTask = jest.fn().mockResolvedValue({ taskId: 'task-1', enqueued: 1 });
@@ -576,6 +600,70 @@ describe('SendChainService', () => {
});
});
it('splits every destination in one inbound CMPP Submit into an independent real message record', async () => {
const { service, prisma } = createService();
prisma.smsApplication.findFirst.mockResolvedValue({
id: 'app-1',
tenantId: 'tenant-1',
cmppAccount: '100001',
secretHash: 'secret-hash',
status: 'active',
interfaceEnabled: false,
queuePriority: 'normal',
ipAllowlist: [{ ipCidr: '127.0.0.1/32' }],
tenant: { id: 'tenant-1', status: 'active', certificationStatus: 'approved' },
});
let taskIndex = 0;
prisma.smsBatchTask.create.mockImplementation(({ data }) => {
taskIndex += 1;
return Promise.resolve({ id: `task-${taskIndex}`, ...data });
});
let messageIndex = 0;
prisma.smsMessageRecord.create.mockImplementation(({ data }) => {
messageIndex += 1;
return Promise.resolve({ id: `record-${messageIndex}`, messageId: data.messageId, ...data });
});
const result = await service.submitInboundMessage({
account: '100001',
phoneNumbers: ['13800000001', '13900000002'],
content: 'hello',
sequenceId: 777823876,
remoteIp: '127.0.0.1',
});
expect(result).toEqual(expect.objectContaining({
accepted: true,
phoneCount: 2,
messages: [
expect.objectContaining({ phoneNumber: '13800000001', messageRecordId: 'record-1' }),
expect.objectContaining({ phoneNumber: '13900000002', messageRecordId: 'record-2' }),
],
}));
expect(result.messageId).toBe(result.messages[0].messageId);
expect(prisma.smsBatchTask.create).toHaveBeenCalledTimes(2);
expect(prisma.smsMessageRecord.create).toHaveBeenCalledTimes(2);
expect(prisma.smsMessageRecord.create).toHaveBeenCalledWith({ data: expect.objectContaining({ phoneNumber: '13800000001', cmppSubmitSequenceId: '777823876', cmppSubmitGroupMessageId: result.messageId }) });
expect(prisma.smsMessageRecord.create).toHaveBeenCalledWith({ data: expect.objectContaining({ phoneNumber: '13900000002', cmppSubmitSequenceId: '777823876', cmppSubmitGroupMessageId: result.messageId }) });
expect(prisma.smsReceiptRecord.create).toHaveBeenCalledTimes(2);
expect(prisma.cmppDownstreamDelivery.create).toHaveBeenCalledTimes(2);
});
it('rejects a multi-destination CMPP Submit before persistence when any destination is invalid', async () => {
const { service, prisma } = createService();
await expect(service.submitInboundMessage({
account: '100001',
phoneNumbers: ['13800000001', 'invalid'],
content: 'hello',
remoteIp: '127.0.0.1',
})).rejects.toThrow('CMPP submit phone number is invalid');
expect(prisma.smsApplication.findFirst).not.toHaveBeenCalled();
expect(prisma.smsBatchTask.create).not.toHaveBeenCalled();
expect(prisma.smsMessageRecord.create).not.toHaveBeenCalled();
});
it('accepts only the filled client Src_Id and snapshots the real application extension', async () => {
const { service, prisma } = createService();
prisma.smsApplication.findFirst.mockResolvedValue({
@@ -684,7 +772,10 @@ describe('SendChainService', () => {
})).resolves.toEqual(expect.objectContaining({ accepted: true, messageRecordId: 'record-1' }));
expect(prisma.smsTemplate.findMany).toHaveBeenCalledWith({
where: { applicationId: 'app-1', content: { contains: '${' } },
where: {
applicationId: 'app-1', content: { contains: '${' }, auditStatus: 'approved',
signature: { auditStatus: 'approved' },
},
include: { signature: true },
orderBy: { updatedAt: 'desc' },
});
@@ -1238,8 +1329,10 @@ describe('SendChainService', () => {
it('matches receipt to a unique timed-out submit attempt when the upstream submit response was lost', async () => {
const { service, prisma } = createService();
prisma.smsMessageRecord.findFirst.mockResolvedValue(null);
prisma.smsSubmitRecord.findMany.mockResolvedValue([
prisma.smsMessageRecord.findUnique.mockResolvedValue(null);
prisma.smsSubmitRecord.findMany
.mockResolvedValueOnce([])
.mockResolvedValueOnce([
{
id: 'submit-timeout-1',
channelId: 'channel-1',
@@ -1258,7 +1351,7 @@ describe('SendChainService', () => {
status: 'timeout',
},
},
]);
]);
await service.handleReceipt({
messageId: 'receipt-123456789',
@@ -1290,10 +1383,89 @@ describe('SendChainService', () => {
});
});
it('matches identical upstream Msg_Id values by channel and destination instead of another channel record', async () => {
const { service, prisma } = createService();
prisma.smsMessageRecord.findUnique.mockResolvedValue(null);
prisma.smsSubmitRecord.findMany.mockResolvedValueOnce([
{
id: 'submit-channel-b',
channelId: 'channel-b',
gatewayMessageId: 'SHARED-UPSTREAM-ID',
messageRecord: {
id: 'record-channel-b',
tenantId: 'tenant-1',
batchTaskId: 'task-1',
applicationId: 'app-1',
messageId: 'MSG-B',
phoneNumber: '15601992925',
channelId: 'channel-b',
gatewayMessageId: 'SHARED-UPSTREAM-ID',
status: 'submitted',
},
},
]);
await service.handleReceipt({
messageId: 'receipt-SHARED-UPSTREAM-ID',
channelId: 'channel-b',
gatewayMessageId: 'SHARED-UPSTREAM-ID',
phoneNumber: '15601992925',
receiptStatus: 'delivered',
rawStatus: 'DELIVRD',
deliveredAt: '2026-07-01T10:01:00.000Z',
});
expect(prisma.smsSubmitRecord.findMany).toHaveBeenCalledWith(expect.objectContaining({
where: expect.objectContaining({
channelId: 'channel-b',
gatewayMessageId: 'SHARED-UPSTREAM-ID',
messageRecord: { phoneNumber: '15601992925' },
}),
}));
expect(prisma.smsMessageRecord.update).toHaveBeenCalledWith({
where: { id: 'record-channel-b' },
data: expect.objectContaining({
status: 'delivered',
receiptStatus: 'delivered',
channelId: 'channel-b',
gatewayMessageId: 'SHARED-UPSTREAM-ID',
receiptRawStatus: 'DELIVRD',
}),
});
});
it('treats a repeated DELIVRD event as idempotent and does not redeliver it downstream', async () => {
const { service, prisma } = createService();
prisma.smsReceiptRecord.findUnique
.mockResolvedValueOnce(null)
.mockResolvedValueOnce({
id: 'receipt-existing',
messageRecordId: 'record-1',
messageRecord: { id: 'record-1', messageId: 'MSG-1', status: 'delivered' },
});
const receipt = {
messageId: 'MSG-1',
channelId: 'channel-1',
gatewayMessageId: 'GW-1',
phoneNumber: '13800000001',
receiptStatus: 'delivered' as const,
rawStatus: 'DELIVRD',
deliveredAt: '2026-07-01T10:01:00.000Z',
};
await service.handleReceipt(receipt);
await service.handleReceipt(receipt);
expect(prisma.smsReceiptRecord.create).toHaveBeenCalledTimes(1);
expect(prisma.cmppDownstreamDelivery.create).toHaveBeenCalledTimes(1);
});
it('rejects ambiguous receipt heuristic matches to avoid binding to the wrong message', async () => {
const { service, prisma } = createService();
prisma.smsMessageRecord.findFirst.mockResolvedValue(null);
prisma.smsSubmitRecord.findMany.mockResolvedValue([
prisma.smsMessageRecord.findUnique.mockResolvedValue(null);
prisma.smsSubmitRecord.findMany
.mockResolvedValueOnce([])
.mockResolvedValueOnce([
{
id: 'submit-timeout-1',
messageRecord: { id: 'record-1', messageId: 'MSG-1', phoneNumber: '13800000001' },
@@ -1302,7 +1474,7 @@ describe('SendChainService', () => {
id: 'submit-timeout-2',
messageRecord: { id: 'record-2', messageId: 'MSG-2', phoneNumber: '13800000001' },
},
]);
]);
await expect(
service.handleReceipt({