Read a peer's submit and receipt message ids into one notation before comparing them

This commit is contained in:
2026-08-30 22:35:09 +02:00
parent 098891bb84
commit 8e5b9bb556
14 changed files with 245 additions and 47 deletions
+27
View File
@@ -206,6 +206,33 @@ describe('dlrFromPdu()', () => {
assert.equal(dlr.receipt.sub, 1);
assert.equal(dlr.receipt.text, 'hello');
});
test('reads the id in the notation the peer writes receipts in', () => {
const hex = dlrFromPdu(deliverSm('id:1a2B stat:DELIVRD err:000 text:'), 'hex');
assert.ok(hex);
assert.equal(hex.smsId, '6699');
assert.equal(hex.receipt?.id, '1a2B', 'the receipt itself keeps the id as it arrived');
assert.equal(dlrFromPdu(deliverSm('id:0000123 stat:DELIVRD'), 'decimal')?.smsId, '123');
assert.equal(dlrFromPdu(deliverSm('nothing scrapable here', {
receipted_message_id: { tagValue: 'FF' },
}, 0), 'hex')?.smsId, '255');
});
test('leaves an id the notation cannot read as it arrived', () => {
assert.equal(dlrFromPdu(deliverSm('id:beef-1 stat:DELIVRD'), 'hex')?.smsId, 'beef-1');
assert.equal(dlrFromPdu(deliverSm('id:1a2b stat:DELIVRD'), 'decimal')?.smsId, '1a2b');
assert.equal(dlrFromPdu(deliverSm('id:0195f0c7 stat:DELIVRD'))?.smsId, '0195f0c7');
});
// Number() reads 9007199254740993 as ...92, which correlates a receipt to the wrong send.
test('reads an id past the safe integer range without losing a digit', () => {
assert.equal(
dlrFromPdu(deliverSm('id:9007199254740993 stat:DELIVRD'), 'decimal')?.smsId,
'9007199254740993',
);
});
});
describe('receiptCodes', () => {
+20
View File
@@ -95,6 +95,26 @@ describe('README: Client', () => {
assert.equal((await reported).smsId, smsIds[0]);
});
test('naming the notation the SMSC writes message ids in', async t => {
await answeringServer(t);
const { err, session } = await client({ smsIdFormat: { receipt: 'decimal', submitResp: 'hex' } });
if (err) throw err;
closeAfter(t, session);
const reported = once<Dlr>(resolve => { session.on('dlr', resolve); });
const { smsIds } = await session.sendSms({
dlr: true,
from: '46701113311',
message: 'Hello world',
to: '46709771337',
});
// The generated ids the server answers with read as no notation, so they arrive untouched.
assert.equal((await reported).smsId, smsIds[0]);
});
test('the documented sending options', async t => {
const smpp = await answeringServer(t);
const incoming = once<Sms>(resolve => {
+93 -28
View File
@@ -15,11 +15,13 @@ import type { TestContext } from 'node:test';
import { Reassembler, decodeSegments } from '../src/reassembly.ts';
import { Session } from '../src/session.ts';
import { DlrMerger } from '../src/dlr-merger.ts';
import { checkSessionOptions } from '../src/session-options.ts';
import { client } from '../src/client.ts';
import { closeAfter, closeListenerAfter } from './teardown.ts';
import { consts } from '../src/defs/constants.ts';
import { errors } from '../src/defs/errors.ts';
import { objToPdu } from '../src/pdu.ts';
import { paramText } from '../src/defs/types.ts';
import { server } from '../src/server.ts';
import { silentLog } from '../src/log.ts';
import { submitSms } from '../src/send-sms.ts';
@@ -67,6 +69,34 @@ function delay(ms: number): Promise<void> {
return new Promise(resolve => { setTimeout(resolve, ms); });
}
/** The server's side of the one connection under test. */
function peerOf(smpp: SmppServer): Session {
const [peer] = smpp.sessions;
assert.equal(smpp.sessions.size, 1);
assert.ok(peer);
return peer;
}
async function sendReceipt(peer: Session, smsId: string): Promise<void> {
const sent = await peer.send({
cmdName: 'deliver_sm',
params: {
destination_addr: '46701113311',
esm_class: consts.ESM_CLASS.MC_DELIVERY_RECEIPT,
short_message: `id:${smsId} stat:DELIVRD err:000 text:`,
source_addr: '46709771337',
},
tlvs: {
message_state: { tagValue: consts.MESSAGE_STATE.DELIVERED },
receipted_message_id: { tagValue: smsId },
},
});
assert.equal(sent.err, undefined);
}
type Gate = { open: () => void; passed: Promise<true> };
/** A promise the test opens by hand, guarded by once() against waiting on one it never does. */
@@ -296,34 +326,6 @@ describe('sendSms()', () => {
});
describe('reconnect', () => {
/** The server's side of the one connection under test, replaced by every reconnect. */
function peerOf(smpp: SmppServer): Session {
const [peer] = smpp.sessions;
assert.equal(smpp.sessions.size, 1);
assert.ok(peer);
return peer;
}
async function sendReceipt(peer: Session, smsId: string): Promise<void> {
const sent = await peer.send({
cmdName: 'deliver_sm',
params: {
destination_addr: '46701113311',
esm_class: consts.ESM_CLASS.MC_DELIVERY_RECEIPT,
short_message: `id:${smsId} stat:DELIVRD err:000 text:`,
source_addr: '46709771337',
},
tlvs: {
message_state: { tagValue: consts.MESSAGE_STATE.DELIVERED },
receipted_message_id: { tagValue: smsId },
},
});
assert.equal(sent.err, undefined);
}
test('re-binds after the connection drops, keeping the same session object', async t => {
const smpp = await startServer(t);
const messages: string[] = [];
@@ -810,3 +812,66 @@ describe('graceful shutdown', () => {
assert.deepEqual(reported, []);
});
});
describe('message id notation', () => {
async function sendOne(session: Session, message: string): Promise<SendSmsResult> {
return session.sendSms({ dlr: true, from: '46701113311', message, to: '46709771337' });
}
test('correlates a hex submit_sm_resp against a decimal receipt', async t => {
const smpp = await startServer(t);
smpp.on('session', bound => {
bound.on('sms', sms => { void sms.sendResp({ smsId: '1a2b' }); });
});
const { session } = await connect(t, smpp, {
smsIdFormat: { receipt: 'decimal', submitResp: 'hex' },
});
assert.ok(session);
const reported = once<Dlr>(resolve => { session.on('dlr', resolve); });
const sent = await sendOne(session, 'one segment');
assert.deepEqual(sent.smsIds, ['6699']);
assert.equal(paramText(sent.pduObjs[0]?.params.message_id), '1a2b', 'the PDU keeps the id it carried');
await sendReceipt(peerOf(smpp), '6699');
assert.equal((await reported).smsId, sent.smsIds[0]);
});
test('leaves the segment ids of a multipart send to merge as they are', async t => {
const smpp = await startServer(t);
smpp.on('session', bound => {
bound.on('sms', sms => { void sms.sendResp({ smsId: 'beef' }); });
});
const { session } = await connect(t, smpp, {
smsIdFormat: { receipt: 'decimal', submitResp: 'hex' },
});
assert.ok(session);
const merged = once<MessageDlr>(resolve => { session.on('messageDlr', resolve); });
const sent = await sendOne(session, 'x'.repeat(200));
assert.deepEqual(sent.smsIds, ['beef-1', 'beef-2']);
for (const smsId of sent.smsIds) {
await sendReceipt(peerOf(smpp), smsId);
}
assert.equal((await merged).smsId, 'beef');
});
test('refuses a notation it cannot apply', () => {
const checked = checkSessionOptions({ smsIdFormat: { receipt: 'octal' } });
assert.ok(checked.err instanceof Error);
assert.match(checked.err.message, /smsIdFormat\.receipt/);
assert.equal(checkSessionOptions({ smsIdFormat: { submitResp: 'hex' } }).err, undefined);
});
});