Read a delivery receipt's body as the octets that arrived (#81)

* Regression tests for a receipt body read under a data_coding it is not written in

* Read a delivery receipt's body as the octets that arrived, not by its data_coding

* Assert SMPPSim's UCS2 receipt body parses like every other segment's

* Record the receipt-body defect as fixed

* Retain a multipart segment's octets once, not twice

* Sort the new type import into its file's order
This commit is contained in:
2026-09-05 22:37:05 +02:00
committed by GitHub
parent ea42bc6d20
commit 45d2f5548e
19 changed files with 300 additions and 162 deletions
+3 -1
View File
@@ -52,7 +52,7 @@ tests passing, `malformed: 0`, `expert errors: 0` in both.
| C5 (single-state variants) | pass | `smppsim single-state variants - C5 …`; UNDELIVERABLE/REJECTED/ACCEPTED each map correctly; 2-segment message's shared status confirmed, `messageDlr` confirmed absent (see Open questions) |
| C6 (`smppsim-delayed`) | pass | `smppsim-delayed - C6 …`; disconnect+reconnect observed, delayed receipt still reaches `dlr` on the new link |
| C7 (loopback, GSM 1/2/3/10-segment) | pass | same `C3+C7` suite; one id per segment, receipt per id, loopback reassembles the exact text including € and \[ \] |
| C7 (loopback, UCS2 2-segment) | pass with a documented defect | `… 2-segment UCS2 …: TLVs stay right, the receipt body is corrupted`; TLVs and loopback reassembly both correct, receipt body unreadable - `@larvit/smpp` defect below |
| C7 (loopback, UCS2 2-segment) | pass with a defect, since fixed | `… 2-segment UCS2 …`; TLVs and loopback reassembly both correct, receipt body unreadable at this commit - `@larvit/smpp` defect below |
| C11 (bind refusal + backoff) | pass | `smppsim - C11 …`, three sub-tests, see below for what each shows |
| C12 (`smppsim-queuefull`) | pass | `smppsim-queuefull - C12 …`; `ESME_RMSGQFUL` returned, session stays bound, later send succeeds once the one-slot queue drains |
| C13 (`maxOutstanding: 1`, 10 parallel) | pass | `smppsim - C13 …`; 10 distinct, strictly-increasing ids, none lost |
@@ -96,6 +96,8 @@ left to fall back on. Not reproduced against `smppsim-textdlr` here (that varian
only sends ASCII), so this is inferred from the mechanism, not independently confirmed there - see
Open questions.
**Fixed** in PR #81: a receipt body is read as the octets that arrived, never by its `data_coding`.
### `reconnect` never retries the very first connect or bind attempt
**What happened.** `client()` performs the socket connect and the initial bind directly, not
+21 -67
View File
@@ -4,7 +4,6 @@ import type { Dlr } from '../src/dlr.ts';
import type { EncodingName } from '../src/defs/encodings.ts';
import type { MessageDlr } from '../src/dlr-merger.ts';
import type { PduObject } from '../src/pdu.ts';
import type { SendSmsResult } from '../src/send-sms.ts';
import type { Session } from '../src/session.ts';
import type { Sms } from '../src/sms.ts';
import { client } from '../src/client.ts';
@@ -164,11 +163,20 @@ async function sendUntilComplete(
}
describe('smppsim - C3+C7 long MT, receipts and loopback reassembly', () => {
const cases: { expectedSegments: number; label: string; message: string }[] = [
const cases: { encoding?: EncodingName; expectedSegments: number; label: string; message: string }[] = [
{ expectedSegments: 1, label: 'single-segment GSM with extension chars', message: gsmFiller(100) },
{ expectedSegments: 2, label: '2-segment GSM with extension chars', message: gsmFiller(200) },
{ expectedSegments: 3, label: '3-segment GSM with extension chars', message: gsmFiller(400) },
{ expectedSegments: 10, label: '10-segment GSM with extension chars', message: gsmFiller(1450) },
// SMPPSim writes this one's receipt body as text under the data_coding of the submit it
// reports on (8, UCS2), so it holds to the same bar as the GSM cases only if the body is
// read as the octets that arrived.
{
encoding: 'UCS2',
expectedSegments: 2,
label: '2-segment UCS2 with 一 and an emoji',
message: `一😀${'x'.repeat(70)}`,
},
];
for (const testCase of cases) {
@@ -182,7 +190,13 @@ describe('smppsim - C3+C7 long MT, receipts and loopback reassembly', () => {
const dlrs = collectDlrs(session);
const sms = collectSms(session);
const { reassembled, smsIds } = await sendUntilComplete(session, dlrs, sms, testCase.message);
const { reassembled, smsIds } = await sendUntilComplete(
session,
dlrs,
sms,
testCase.message,
testCase.encoding,
);
assert.equal(smsIds.length, testCase.expectedSegments);
@@ -195,75 +209,15 @@ describe('smppsim - C3+C7 long MT, receipts and loopback reassembly', () => {
// match, which is what a well-behaved SMSC gives you (C3).
assert.equal(received.pduObj.tlvs.receipted_message_id?.tagValue, id);
assert.equal(received.pduObj.tlvs.message_state?.tagValue, consts.MESSAGE_STATE.DELIVERED);
assert.equal(received.dlr.receipt?.stat, 'DELIVRD');
assert.ok(received.dlr.receipt);
assert.equal(received.dlr.receipt.stat, 'DELIVRD');
assert.equal(received.dlr.receipt.id, id);
assert.equal(received.dlr.receipt.err, '000');
}
assert.equal((await reassembled.sendResp()).err, undefined);
});
}
test('2-segment UCS2 with 一 and an emoji: TLVs stay right, the receipt body is corrupted', async t => {
const { err, session } = await bind(PEER_HOST);
assert.equal(err, undefined);
assert.ok(session);
closeAfter(t, session);
const dlrs = collectDlrs(session);
const sms = collectSms(session);
const message = `一😀${'x'.repeat(70)}`;
let smsIds: string[] | undefined;
let reassembled: Sms | undefined;
for (let attempt = 0; attempt < DLR_MAX_ATTEMPTS && !reassembled; attempt++) {
const sent: SendSmsResult = await session.sendSms(
{ dlr: true, encoding: 'UCS2', from: FROM, message, to: TO },
);
assert.equal(sent.err, undefined);
assert.equal(sent.smsIds.length, 2);
const complete = await waitFor(() => {
const tlvsIntact = sent.smsIds.every(id => {
const received = dlrs.find(r => r.dlr.smsId === id);
return received?.pduObj.tlvs.receipted_message_id?.tagValue !== undefined
&& received.pduObj.tlvs.message_state?.tagValue !== undefined;
});
const found = sms.find(s => s.message === message);
return tlvsIntact && found ? { found } : undefined;
}, DLR_RETRY_BUDGET_MS);
if (complete) {
smsIds = sent.smsIds;
reassembled = complete.found;
}
}
assert.ok(smsIds);
assert.ok(reassembled, 'expected the loopback deliver_sm(s), unaffected by the defect, to reassemble');
for (const id of smsIds) {
const received = dlrs.find(r => r.dlr.smsId === id);
assert.ok(received);
// The TLVs are typed fields, unaffected by the defect below.
assert.equal(received.dlr.statusMsg, 'DELIVERED');
assert.equal(received.pduObj.tlvs.receipted_message_id?.tagValue, id);
assert.equal(received.pduObj.tlvs.message_state?.tagValue, consts.MESSAGE_STATE.DELIVERED);
// Defect (see findings/02-smppsim.md): SMPPSim's delivery receipt echoes the
// original submit_sm's data_coding (8, UCS2) but writes its short_message as plain
// ASCII text. pdu.ts decodes short_message for any non-UDH PDU using that same
// data_coding at parse time, so the ASCII receipt bytes are read back as UCS2 -
// every "stat:"/"id:" field becomes unrecoverable CJK-range garbage.
assert.equal(received.dlr.receipt?.stat, undefined);
}
assert.equal((await reassembled.sendResp()).err, undefined);
});
});
describe('smppsim-textdlr - C2 text-only receipts', () => {