fix: audit submissions disabled after bind
This commit is contained in:
@@ -1205,7 +1205,7 @@ describe('SendChainService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects new submissions synchronously when the application interface was disabled after bind', async () => {
|
||||
it('accepts Submit after bind and emits an auditable REJECTD receipt when the interface was disabled', async () => {
|
||||
const { service, prisma } = createService();
|
||||
prisma.smsApplication.findFirst.mockResolvedValue({
|
||||
id: 'app-1',
|
||||
@@ -1222,6 +1222,7 @@ describe('SendChainService', () => {
|
||||
id: 'app-1',
|
||||
tenantId: 'tenant-1',
|
||||
cmppAccount: '100001',
|
||||
status: 'active',
|
||||
interfaceEnabled: false,
|
||||
downstreamReceiptRetryEnabled: true,
|
||||
downstreamUplinkRetryEnabled: true,
|
||||
@@ -1232,12 +1233,34 @@ describe('SendChainService', () => {
|
||||
account: '100001',
|
||||
phoneNumber: '13800000001',
|
||||
content: 'hello',
|
||||
sequenceId: 701,
|
||||
remoteIp: '127.0.0.1',
|
||||
})).rejects.toThrow('CMPP account is disabled for new submissions');
|
||||
})).resolves.toEqual(expect.objectContaining({
|
||||
accepted: true,
|
||||
messageRecordId: 'record-1',
|
||||
status: 'accepted',
|
||||
}));
|
||||
|
||||
expect(prisma.smsMessageRecord.create).not.toHaveBeenCalled();
|
||||
expect(prisma.smsReceiptRecord.create).not.toHaveBeenCalled();
|
||||
expect(prisma.cmppDownstreamDelivery.create).not.toHaveBeenCalled();
|
||||
expect(prisma.smsMessageRecord.create).toHaveBeenCalledTimes(1);
|
||||
expect(prisma.smsMessageRecord.update).toHaveBeenCalledWith(expect.objectContaining({
|
||||
where: { id: 'record-1' },
|
||||
data: expect.objectContaining({
|
||||
status: 'failed',
|
||||
receiptStatus: 'undelivered',
|
||||
receiptRawStatus: 'REJECTD',
|
||||
errorCode: 'INTERFACE',
|
||||
}),
|
||||
}));
|
||||
expect(prisma.smsReceiptRecord.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
data: expect.objectContaining({ rawStatus: 'REJECTD', errorCode: 'INTERFACE' }),
|
||||
}));
|
||||
expect(prisma.cmppDownstreamDelivery.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
data: expect.objectContaining({ status: 'pending', deliveryType: 'receipt' }),
|
||||
}));
|
||||
expect(service['postGatewayControl']).toHaveBeenCalledWith(
|
||||
'/downstream/receipt',
|
||||
expect.objectContaining({ rawStatus: 'REJECTD', errorCode: 'INTERFACE' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('queues an HTTP webhook but not CMPP delivery for an HTTP-only application', async () => {
|
||||
@@ -2376,23 +2399,44 @@ describe('SendChainService', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects a disabled application before the merged Inbox statement can insert', async () => {
|
||||
it('persists fast-path Submit before evaluating application or tenant business state', async () => {
|
||||
const previous = process.env.CMPP_INBOUND_FAST_PATH_ENABLED;
|
||||
process.env.CMPP_INBOUND_FAST_PATH_ENABLED = 'true';
|
||||
try {
|
||||
const { service, prisma } = createService();
|
||||
prisma.$queryRaw.mockResolvedValueOnce([{
|
||||
validationError: 'CMPP account is disabled for new submissions',
|
||||
payloadHash: null,
|
||||
response: null,
|
||||
}]);
|
||||
prisma.$queryRaw.mockImplementationOnce((query) => {
|
||||
const payloadHash = query.values.find((value: unknown) => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value));
|
||||
return Promise.resolve([{
|
||||
validationError: null,
|
||||
payloadHash,
|
||||
response: {
|
||||
accepted: true,
|
||||
tenantId: 'tenant-1',
|
||||
applicationId: 'app-1',
|
||||
taskId: '',
|
||||
messageId: 'MSG-disabled-after-bind',
|
||||
messageRecordId: '',
|
||||
status: 'accepted_pending',
|
||||
phoneCount: 1,
|
||||
messages: [],
|
||||
},
|
||||
}]);
|
||||
});
|
||||
|
||||
await expect(service.submitInboundMessage({
|
||||
requestId: 'cmpp-inbound:disabled',
|
||||
account: '100001',
|
||||
phoneNumber: '13800000001',
|
||||
content: 'hello',
|
||||
})).rejects.toThrow('CMPP account is disabled for new submissions');
|
||||
})).resolves.toEqual(expect.objectContaining({
|
||||
accepted: true,
|
||||
status: 'accepted_pending',
|
||||
}));
|
||||
const sql = prisma.$queryRaw.mock.calls[0][0].strings.join(' ');
|
||||
expect(sql).not.toContain("application.status <> 'active'");
|
||||
expect(sql).not.toContain('NOT application."interfaceEnabled"');
|
||||
expect(sql).toContain('CMPP source IP is not in application allowlist');
|
||||
expect(sql).toContain('CMPP Src_Id must equal the access number assigned to this application');
|
||||
expect(prisma.cmppInboundSubmissionInbox.findUnique).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
if (previous == null) delete process.env.CMPP_INBOUND_FAST_PATH_ENABLED;
|
||||
@@ -3928,7 +3972,7 @@ describe('SendChainService', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it('allows a disabling application to reconnect for receipt draining but rejects new submissions', async () => {
|
||||
it('allows a disabling application to reconnect for receipt draining and audits later Submit as REJECTD', async () => {
|
||||
const { service, prisma } = createService();
|
||||
prisma.smsApplication.findFirst.mockResolvedValue({
|
||||
id: 'app-1',
|
||||
@@ -3943,6 +3987,16 @@ describe('SendChainService', () => {
|
||||
ipAllowlist: [{ ipCidr: '127.0.0.1/32' }],
|
||||
tenant: { id: 'tenant-1', status: 'active', certificationStatus: 'approved' },
|
||||
});
|
||||
prisma.smsApplication.findUnique.mockResolvedValue({
|
||||
id: 'app-1',
|
||||
tenantId: 'tenant-1',
|
||||
cmppAccount: '100001',
|
||||
status: 'deleted',
|
||||
interfaceEnabled: false,
|
||||
downstreamReceiptRetryEnabled: true,
|
||||
downstreamUplinkRetryEnabled: true,
|
||||
httpConfig: { enabled: false },
|
||||
});
|
||||
|
||||
await expect(service.authenticateInboundApplication({
|
||||
account: '100001',
|
||||
@@ -3955,9 +4009,20 @@ describe('SendChainService', () => {
|
||||
account: '100001',
|
||||
phoneNumber: '13800000001',
|
||||
content: 'hello',
|
||||
sequenceId: 702,
|
||||
remoteIp: '127.0.0.1',
|
||||
})).rejects.toThrow('disabled for new submissions');
|
||||
expect(prisma.smsMessageRecord.create).not.toHaveBeenCalled();
|
||||
})).resolves.toEqual(expect.objectContaining({ accepted: true, status: 'accepted' }));
|
||||
expect(prisma.smsMessageRecord.create).toHaveBeenCalledTimes(1);
|
||||
expect(prisma.smsReceiptRecord.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
data: expect.objectContaining({ rawStatus: 'REJECTD', errorCode: 'ACCOUNT' }),
|
||||
}));
|
||||
expect(prisma.cmppDownstreamDelivery.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
data: expect.objectContaining({ status: 'pending', deliveryType: 'receipt' }),
|
||||
}));
|
||||
expect(service['postGatewayControl']).toHaveBeenCalledWith(
|
||||
'/downstream/receipt',
|
||||
expect.objectContaining({ rawStatus: 'REJECTD', errorCode: 'ACCOUNT' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('lets Gateway read historical pending receipts after an application or enterprise is disabled', async () => {
|
||||
|
||||
Reference in New Issue
Block a user