fix: 修复上行归属并实现签名质量日报优化
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { createRequire } from 'node:module';
|
||||
const url = new URL(process.env.SIGNATURE_TEST_DATABASE_URL || '');
|
||||
assert(['localhost', '127.0.0.1'].includes(url.hostname) && url.pathname.startsWith('/cmpp_qa_signature_'));
|
||||
process.env.DATABASE_URL = url.toString();
|
||||
process.env.NODE_ENV = 'test';
|
||||
const require = createRequire(new URL('../../api/package.json', import.meta.url));
|
||||
require('reflect-metadata');
|
||||
const { PrismaService } = require('./dist/prisma/prisma.service');
|
||||
const { completionDatabase } = require('./dist/send-chain/completion-context');
|
||||
const { SendDownstreamDeliveryService } = require('./dist/send-chain/send-downstream-delivery.service');
|
||||
const { OpenApiService } = require('./dist/open-api/open-api.service');
|
||||
const { resolveUplinkMatch } = require('./dist/send-chain/uplink-matching');
|
||||
const db = new PrismaService(),
|
||||
proxy = completionDatabase(db),
|
||||
prefix = randomUUID().slice(0, 8);
|
||||
function delivery(openApi) {
|
||||
let service;
|
||||
const facade = {
|
||||
resolveUplinkMatch: (event, channel) => resolveUplinkMatch(proxy, event, channel),
|
||||
queueAndTryDownstreamDelivery: (data) => service.queueAndTryDownstreamDelivery(data),
|
||||
postGatewayControl: () => {
|
||||
throw new Error('External delivery is forbidden in QA');
|
||||
},
|
||||
};
|
||||
service = new SendDownstreamDeliveryService(proxy, undefined, openApi, facade, {});
|
||||
return service;
|
||||
}
|
||||
try {
|
||||
const tenant = await db.tenant.create({ data: { name: '上行验收', code: prefix } });
|
||||
const apps = [];
|
||||
for (const suffix of ['a', 'b'])
|
||||
apps.push(
|
||||
await db.smsApplication.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
name: '应用' + suffix,
|
||||
cmppAccount: prefix + suffix,
|
||||
cmppEnterpriseCode: '000001',
|
||||
secretHash: 'isolated',
|
||||
interfaceEnabled: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const channel = await db.smsChannel.create({
|
||||
data: {
|
||||
name: '仅入库隔离通道',
|
||||
code: prefix,
|
||||
srcId: '1069',
|
||||
gatewayHost: '127.0.0.1',
|
||||
gatewayPort: 1,
|
||||
account: 'isolated',
|
||||
passwordCipher: 'not-a-secret',
|
||||
carriers: ['mobile'],
|
||||
status: 'disabled',
|
||||
},
|
||||
});
|
||||
const received = new Date(),
|
||||
sent = new Date(received.getTime() - 60_000);
|
||||
for (const [phone, index] of [
|
||||
['13800000101', 0],
|
||||
['13800000101', 0],
|
||||
['13800000102', 0],
|
||||
['13800000102', 1],
|
||||
]) {
|
||||
const m = await db.smsMessageRecord.create({
|
||||
data: {
|
||||
messageId: randomUUID(),
|
||||
tenantId: tenant.id,
|
||||
applicationId: apps[index].id,
|
||||
phoneNumber: phone,
|
||||
content: '隔离上行匹配测试',
|
||||
},
|
||||
});
|
||||
await db.smsSubmitRecord.create({
|
||||
data: {
|
||||
messageRecordId: m.id,
|
||||
channelId: channel.id,
|
||||
submitId: randomUUID(),
|
||||
submitStatus: 'accepted',
|
||||
submittedAt: sent,
|
||||
},
|
||||
});
|
||||
}
|
||||
for (const app of apps) {
|
||||
await db.smsApplicationHttpConfig.create({ data: { applicationId: app.id, enabled: true, sendEnabled: false } });
|
||||
await db.httpWebhookEndpoint.create({
|
||||
data: {
|
||||
applicationId: app.id,
|
||||
eventType: 'uplink',
|
||||
url: 'http://127.0.0.1:1/never-called',
|
||||
secretEncrypted: 'isolated-no-delivery',
|
||||
secretLast4: 'test',
|
||||
},
|
||||
});
|
||||
}
|
||||
const service = delivery(new OpenApiService(db, undefined));
|
||||
const event = {
|
||||
eventId: randomUUID(),
|
||||
channelId: channel.id,
|
||||
phoneNumber: '13800000101',
|
||||
destId: '10690001',
|
||||
content: 'TD',
|
||||
receivedAt: received.toISOString(),
|
||||
gatewayMessageId: 'supplier-mo-id',
|
||||
};
|
||||
const copies = await Promise.all([service.handleUplink(event), service.handleUplink(event)]);
|
||||
assert.equal(copies[0].id, copies[1].id);
|
||||
assert.equal(copies[0].messageId, null);
|
||||
assert.equal(copies[0].messageRecordId, null);
|
||||
assert.equal(copies[0].gatewayMessageId, 'supplier-mo-id');
|
||||
assert.equal(await db.cmppDownstreamDelivery.count({ where: { dedupeKey: 'uplink:' + copies[0].id } }), 1);
|
||||
assert.equal(await db.httpWebhookEvent.count({ where: { uplinkMessageId: copies[0].id } }), 1);
|
||||
console.log('PASS 重复事件原子入库、无唯一原短信不伪造编号、CMPP与HTTP通知意图各一份');
|
||||
const ambiguous = await service.handleUplink({ ...event, eventId: randomUUID(), phoneNumber: '13800000102' });
|
||||
assert.equal(ambiguous.matchStatus, 'ambiguous');
|
||||
const candidates = await db.smsUplinkMatchCandidate.findMany({ where: { uplinkMessageId: ambiguous.id } });
|
||||
assert.equal(candidates.length, 2);
|
||||
const claims = await Promise.allSettled(candidates.map((c) => service.claimUplinkMatchCandidate(ambiguous.id, c.id)));
|
||||
assert.equal(claims.filter((r) => r.status === 'fulfilled').length, 1);
|
||||
assert.equal(await db.cmppDownstreamDelivery.count({ where: { dedupeKey: 'uplink:' + ambiguous.id } }), 1);
|
||||
assert.equal(
|
||||
await db.smsUplinkMatchCandidate.count({ where: { uplinkMessageId: ambiguous.id, status: 'claimed' } }),
|
||||
1,
|
||||
);
|
||||
console.log('PASS 并发认领仅一个应用成功、候选及通知同事务');
|
||||
const failedEvent = { ...event, eventId: randomUUID() };
|
||||
await assert.rejects(
|
||||
delivery({
|
||||
queueWebhookEvent: async () => {
|
||||
throw new Error('injected notification storage failure');
|
||||
},
|
||||
}).handleUplink(failedEvent),
|
||||
/injected/,
|
||||
);
|
||||
assert.equal(await db.smsUplinkMessage.count({ where: { eventId: failedEvent.eventId } }), 0);
|
||||
await service.handleUplink(failedEvent);
|
||||
assert.equal(await db.smsUplinkMessage.count({ where: { eventId: failedEvent.eventId } }), 1);
|
||||
console.log('PASS 通知存储故障回滚上行、重试恢复,不调用外部发送');
|
||||
} finally {
|
||||
await db.onModuleDestroy();
|
||||
}
|
||||
Reference in New Issue
Block a user