Files
lislgosms/tools/testing/verify-protocol-fields.mjs
T
hectorzhao 001d5f2cbd
CSS quality / css-quality (push) Has been cancelled
fix: 修复 CMPP 协议字段容量与版本兼容性
2026-09-20 15:49:37 +08:00

320 lines
13 KiB
JavaScript

// Local-only real PostgreSQL, Redis and Nest callback validation. No supplier connection or SMS submitter.
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { randomUUID, randomBytes } from 'node:crypto';
import { readFileSync } from 'node:fs';
const url = new URL(process.env.PROTOCOL_TEST_DATABASE_URL || '');
assert(['127.0.0.1', 'localhost'].includes(url.hostname) && url.pathname.startsWith('/cmpp_qa_'));
const redisUrl = new URL(process.env.PROTOCOL_TEST_REDIS_URL || 'redis://127.0.0.1:16441');
assert(['127.0.0.1', 'localhost'].includes(redisUrl.hostname));
Object.assign(process.env, {
NODE_ENV: 'test',
DATABASE_URL: url.href,
REDIS_URL: redisUrl.href,
HTTP_API_MASTER_KEY: randomBytes(32).toString('hex'),
CMPP_PROCESS_ROLE: 'gateway-callback',
GATEWAY_CONTROL_URL: 'http://127.0.0.1:1',
GATEWAY_CONNECTION_RECONCILER_DISABLED: 'true',
GATEWAY_CONNECTING_TIMEOUT_SCANNER_DISABLED: 'true',
SMS_RECEIPT_TIMEOUT_SCAN_ENABLED: 'false',
SMS_SCHEDULED_DISPATCH_SCAN_ENABLED: 'false',
CMPP_INBOUND_LONG_MESSAGE_SCAN_ENABLED: 'false',
UPSTREAM_RECEIPT_INBOX_SCAN_ENABLED: 'false',
CMPP_DOWNSTREAM_REQUEUE_TASK_SCAN_ENABLED: 'false',
API_ENABLE_SEND_WORKER: 'false',
CMPP_INBOUND_WORKFLOW_WORKER_ENABLED: 'false',
});
const require = createRequire(new URL('../../api/package.json', import.meta.url));
require('reflect-metadata');
// Match the existing bootstrap money serializer. Protocol field conversion is separately asserted below.
Object.defineProperty(BigInt.prototype, 'toJSON', {
configurable: true,
value() {
assert(this <= BigInt(Number.MAX_SAFE_INTEGER));
return Number(this);
},
});
const { NestFactory } = require('@nestjs/core');
const { GatewayCallbackModule } = require('./dist/gateway-callback.module');
const { PrismaService } = require('./dist/prisma/prisma.service');
const { SendChainService } = require('./dist/send-chain/send-chain.service');
const { protocolFieldsToJson } = require('./dist/common/protocol-uint32');
const { Client } = require('pg');
const Redis = require('ioredis');
const key = () => randomUUID();
const pass = (name, detail) => console.log(JSON.stringify({ passed: name, ...detail }));
const app = await NestFactory.create(GatewayCallbackModule, { logger: ['error'] });
app.setGlobalPrefix('api');
const redis = new Redis(redisUrl.href);
try {
await app.listen(0, '127.0.0.1');
const base = await app.getUrl(),
db = app.get(PrismaService),
chain = app.get(SendChainService);
assert.equal(
await db.smsMessageRecord.count(),
0,
'Use a fresh isolated database: fixed maximum Msg_Id fixtures must not collide with earlier runs',
);
const post = async (path, body, expected = 201) => {
const r = await fetch(`${base}/api/gateway/events/${path}`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
});
const json = await r.json();
assert.equal(r.status, expected, JSON.stringify(json));
return json;
};
const tenant = await db.tenant.create({ data: { name: 'protocol QA', code: key() } });
const application = await db.smsApplication.create({
data: {
tenantId: tenant.id,
name: 'protocol QA',
cmppAccount: key(),
cmppEnterpriseCode: '000001',
secretHash: 'disabled',
interfaceEnabled: true,
},
});
const channel = await db.smsChannel.create({
data: {
name: 'protocol QA disabled',
code: key(),
gatewayHost: '127.0.0.1',
gatewayPort: 1,
account: key(),
passwordCipher: 'unused',
srcId: '1069',
carriers: ['mobile'],
status: 'disabled',
},
});
const maxMsgId = '18446744073709551615';
for (const [index, sequenceId] of [0, 2147483647, 2147483648, 4294967295].entries()) {
const gatewayMessageId = (BigInt(maxMsgId) - BigInt(index)).toString();
const m = await db.smsMessageRecord.create({
data: {
messageId: key(),
phoneNumber: `1380013800${index}`,
content: 'isolated',
billingUnits: 1,
status: 'submitted',
channelId: channel.id,
submitId: key(),
tenantId: tenant.id,
applicationId: application.id,
cmppSubmitSequenceId: String(sequenceId),
cmppRegisteredDelivery: true,
},
});
await db.smsSubmitRecord.create({
data: {
messageRecordId: m.id,
tenantId: tenant.id,
channelId: channel.id,
submitId: m.submitId,
submitStatus: 'accepted',
},
});
await post('submit-segment-result', {
messageId: m.messageId,
channelId: channel.id,
submitId: m.submitId,
gatewayMessageId,
sequenceId,
segmentIndex: 1,
segmentTotal: 1,
submitStatus: 'accepted',
});
const receipt = {
messageId: m.messageId,
channelId: channel.id,
sequenceId,
gatewayMessageId,
phoneNumber: m.phoneNumber,
receiptStatus: 'delivered',
rawStatus: 'DELIVRD',
deliveredAt: new Date().toISOString(),
};
const stream = `qa:protocol:${key()}`;
try {
await redis.xadd(stream, '*', 'payload', JSON.stringify(receipt));
const entries = await redis.xrange(stream, '-', '+');
const wire = JSON.parse(entries[0][1][1]);
assert.equal(wire.gatewayMessageId, gatewayMessageId);
assert.equal(wire.sequenceId, sequenceId);
const [intake, concurrent] = await Promise.all([post('receipt/intake', wire), post('receipt/intake', wire)]);
assert.equal(intake.inboxId, concurrent.inboxId);
for (let i = 0; i < 100; i++) {
const inbox = await db.upstreamReceiptInbox.findUnique({ where: { id: intake.inboxId } });
if (inbox.status === 'matched') break;
await new Promise((resolve) => setTimeout(resolve, 30));
}
const batch = await post('batch', {
batchId: key(),
gatewayInstanceId: 'qa',
events: [{ eventId: key(), type: 'receipt_intake', payload: wire }],
});
assert(
batch.results.every((r) => r.accepted),
JSON.stringify(batch),
);
const inbox = await db.upstreamReceiptInbox.findUnique({ where: { id: intake.inboxId } });
assert.equal(inbox.status, 'matched');
assert.equal(inbox.sequenceId, BigInt(sequenceId));
const receipts = await db.smsReceiptRecord.findMany({ where: { messageRecordId: m.id } });
assert.equal(receipts.length, 1);
assert.equal(receipts[0].sequenceId, BigInt(sequenceId));
const [submit, segment] = await Promise.all([
db.smsSubmitRecord.findFirst({ where: { messageRecordId: m.id } }),
db.smsMessageSegmentAudit.findFirst({ where: { messageRecordId: m.id } }),
]);
assert.equal(submit.sequenceId, BigInt(sequenceId));
assert.equal(segment.sequenceId, BigInt(sequenceId));
assert.equal((await db.smsMessageRecord.findUnique({ where: { id: m.id } })).status, 'delivered');
const delivery = await db.cmppDownstreamDelivery.findFirst({ where: { messageRecordId: m.id } });
assert(delivery);
assert.equal(delivery.payload.submitSequenceId, sequenceId);
await chain.acknowledgeDownstreamDelivery({
id: delivery.id,
claimId: key(),
connectionId: 'qa',
sequenceId: String(sequenceId),
messageId: gatewayMessageId,
result: sequenceId,
});
const ack = await db.cmppDownstreamDelivery.findUnique({
where: { id: delivery.id },
include: { attempts: true },
});
assert.equal(ack.ackResult, BigInt(sequenceId));
assert.equal(ack.attempts[0].ackResult, BigInt(sequenceId));
assert.equal(ack.status === 'delivered', sequenceId === 0);
assert.equal(protocolFieldsToJson(ack).ackResult, sequenceId);
const uplink = {
eventId: key(),
channelId: channel.id,
sequenceId,
gatewayMessageId,
phoneNumber: m.phoneNumber,
destId: '1069',
content: 'QA',
receivedAt: new Date().toISOString(),
};
const first = await post('uplink', uplink),
duplicate = await post('uplink', uplink);
assert.equal(first.id, duplicate.id);
assert.equal(first.sequenceId, sequenceId);
assert.equal(first.gatewayMessageId, gatewayMessageId);
assert.equal((await db.smsUplinkMessage.findUnique({ where: { id: first.id } })).sequenceId, BigInt(sequenceId));
pass('uint32_real_chain', {
sequenceId,
gatewayMessageId,
deduplicated: true,
sevenColumns: true,
notificationQueued: true,
});
if (sequenceId === 4294967295) {
// Model the emergency legacy case: receipt fact exists but optional sequence was omitted.
const notices = await db.cmppDownstreamDelivery.count({ where: { messageRecordId: m.id } });
await db.upstreamReceiptInbox.update({ where: { id: inbox.id }, data: { sequenceId: null } });
await db.smsReceiptRecord.update({ where: { id: receipts[0].id }, data: { sequenceId: null } });
await post('receipt/intake', wire);
await post('receipt', { ...wire, receiptStatus: 'undelivered', rawStatus: 'REJECTD' });
assert.equal((await db.upstreamReceiptInbox.findUnique({ where: { id: inbox.id } })).sequenceId, null);
assert.equal((await db.smsReceiptRecord.findUnique({ where: { id: receipts[0].id } })).sequenceId, null);
assert.equal((await db.smsMessageRecord.findUnique({ where: { id: m.id } })).status, 'delivered');
assert.equal(await db.cmppDownstreamDelivery.count({ where: { messageRecordId: m.id } }), notices);
pass('historical_null_and_contradictory_high_sequence_no_reopen');
}
} finally {
await redis.del(stream);
}
}
const count = await db.upstreamReceiptInbox.count();
for (const bad of [-1, 4294967296, 1.5, '', '0'])
await post('receipt/intake', { channelId: channel.id, sequenceId: bad }, 400);
assert.equal(await db.upstreamReceiptInbox.count(), count);
const invalid = await post('batch', {
batchId: key(),
gatewayInstanceId: 'qa',
events: [{ eventId: 'bad', type: 'receipt_intake', payload: { channelId: channel.id, sequenceId: -1 } }],
});
assert.equal(invalid.results[0].accepted, false);
assert.equal(invalid.results[0].retryable, false);
pass('invalid_events_no_writes');
const models = [
'UpstreamReceiptInbox',
'SmsReceiptRecord',
'SmsUplinkMessage',
'SmsSubmitRecord',
'SmsMessageSegmentAudit',
'CmppDownstreamDelivery',
'CmppDownstreamDeliveryAttempt',
];
for (const [i, table] of models.entries()) {
const col = i < 5 ? 'sequenceId' : 'ackResult';
for (const bad of [-1, 4294967296])
await assert.rejects(db.$executeRawUnsafe(`UPDATE "${table}" SET "${col}" = ${bad}`));
}
pass('database_constraints_all_seven');
// Rehearse the exact migration with old values and an unrelated index, then a bad historical value and lock contention.
const sql = readFileSync(
new URL('../../api/prisma/migrations/20260920160000_cmpp_protocol_uint32/migration.sql', import.meta.url),
'utf8',
);
const pg = new Client({ connectionString: url.href }),
blocker = new Client({ connectionString: url.href });
await pg.connect();
await blocker.connect();
try {
const schema = 'qa_' + key().replaceAll('-', '');
await pg.query(`CREATE SCHEMA "${schema}"; SET search_path TO "${schema}"`);
for (const [i, table] of models.entries()) {
const col = i < 5 ? 'sequenceId' : 'ackResult';
await pg.query(
`CREATE TABLE "${table}" (id int, "${col}" integer); CREATE INDEX "${table}_qa" ON "${table}"(id); INSERT INTO "${table}" VALUES (1,NULL),(2,0),(3,2147483647)`,
);
}
await pg.query('UPDATE "CmppDownstreamDeliveryAttempt" SET "ackResult"=-1 WHERE id=2');
await assert.rejects(pg.query(sql));
await pg.query('ROLLBACK');
assert.equal(
(await pg.query('SELECT pg_typeof("sequenceId")::text AS typ FROM "UpstreamReceiptInbox" LIMIT 1')).rows[0].typ,
'integer',
);
await pg.query('UPDATE "CmppDownstreamDeliveryAttempt" SET "ackResult"=0 WHERE id=2');
await blocker.query(`BEGIN; LOCK TABLE "${schema}"."UpstreamReceiptInbox" IN ACCESS EXCLUSIVE MODE`);
await assert.rejects(pg.query(sql), (e) => e.code === '55P03');
await pg.query('ROLLBACK');
await blocker.query('ROLLBACK');
const before = await pg.query('SELECT pg_current_wal_lsn() AS lsn');
const started = Date.now();
await pg.query(sql);
const wal = await pg.query('SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), $1) AS bytes', [before.rows[0].lsn]);
for (const [i, table] of models.entries()) {
const col = i < 5 ? 'sequenceId' : 'ackResult';
assert.deepEqual(
(await pg.query(`SELECT "${col}" FROM "${table}" ORDER BY id`)).rows.map((r) => r[col]),
[null, '0', '2147483647'],
);
}
assert.equal(
Number((await pg.query('SELECT count(*) AS n FROM pg_indexes WHERE schemaname=$1', [schema])).rows[0].n),
7,
);
pass('migration_rollback_lock_timeout_and_preservation', {
durationMs: Date.now() - started,
walBytes: wal.rows[0].bytes,
fixtureRows: 21,
});
} finally {
await pg.end();
await blocker.end();
}
} finally {
await redis.quit();
await app.close();
}