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
+72 -2
View File
@@ -2,19 +2,26 @@ import assert from 'node:assert/strict';
import test, { describe } from 'node:test';
import { consts } from '../src/defs/constants.ts';
import { dlrFromPdu, parseReceipt, receiptCodes } from '../src/dlr.ts';
import { encodeMessage } from '../src/message.ts';
import { objToPdu, pduToObj } from '../src/pdu.ts';
import type { PduObject, TlvInput } from '../src/pdu.ts';
const receiptText = 'id:0195f0c7 sub:001 dlvrd:001 submit date:2508251430 done date:2508251431 stat:DELIVRD err:000 text:hello there';
/** SMPPSim writes this body as plain text under the data_coding of the message it reports on. */
const textReceiptId = '01a072f9-30f0-7807-945f-3412d4d5b8c3';
const textReceipt = `id:${textReceiptId} sub:001 dlvrd:001 submit date:2509051430 done date:2509051431 stat:DELIVRD err:000 Text:hello there`;
function deliverSm(
message: Buffer | string,
tlvs?: Record<string, TlvInput>,
esmClass: number = consts.ESM_CLASS.MC_DELIVERY_RECEIPT,
dataCoding = 0,
): PduObject {
const { buffer } = objToPdu({
cmdName: 'deliver_sm',
params: {
data_coding: dataCoding,
destination_addr: '46701113311',
esm_class: esmClass,
short_message: message,
@@ -149,8 +156,8 @@ describe('dlrFromPdu()', () => {
assert.equal(dlr.intermediate, false);
});
// pduToObj leaves a UDH-carrying short_message a buffer, so the body needs decoding before it
// can be read at all — and the message type sits under the UDH indicator in the same octet.
// The message type sits under the UDH indicator in the same octet, and the header has to come
// off the body before any of it can be read.
test('reads the body of a receipt that carries a UDH', () => {
const udh = Buffer.from([0x05, 0x00, 0x03, 0x2a, 0x01, 0x01]);
const body = Buffer.concat([udh, Buffer.from(receiptText, 'ascii')]);
@@ -158,6 +165,7 @@ describe('dlrFromPdu()', () => {
body,
undefined,
consts.ESM_CLASS.MC_DELIVERY_RECEIPT | consts.ESM_CLASS.UDH_INDICATOR,
consts.ENCODING.UCS2,
));
assert.ok(dlr);
@@ -165,6 +173,68 @@ describe('dlrFromPdu()', () => {
assert.equal(dlr.statusMsg, 'DELIVERED');
});
test('reads a text receipt out of a PDU whose data_coding declares UCS2', () => {
const dlr = dlrFromPdu(deliverSm(
Buffer.from(textReceipt, 'ascii'),
{
message_state: { tagValue: consts.MESSAGE_STATE.DELIVERED },
receipted_message_id: { tagValue: textReceiptId },
},
consts.ESM_CLASS.MC_DELIVERY_RECEIPT,
consts.ENCODING.UCS2,
));
assert.ok(dlr?.receipt);
assert.equal(dlr.receipt.id, textReceiptId);
assert.equal(dlr.receipt.sub, 1);
assert.equal(dlr.receipt.dlvrd, 1);
assert.equal(dlr.receipt.submitDate, '2509051430');
assert.equal(dlr.receipt.doneDate, '2509051431');
assert.equal(dlr.receipt.stat, 'DELIVRD');
assert.equal(dlr.receipt.err, '000');
assert.equal(dlr.receipt.text, 'hello there');
assert.equal(dlr.smsId, textReceiptId);
assert.equal(dlr.statusMsg, 'DELIVERED');
});
test('reads that same receipt with no TLVs to fall back on', () => {
const dlr = dlrFromPdu(deliverSm(
Buffer.from(textReceipt, 'ascii'),
undefined,
consts.ESM_CLASS.MC_DELIVERY_RECEIPT,
consts.ENCODING.UCS2,
));
assert.ok(dlr);
assert.equal(dlr.smsId, textReceiptId);
assert.equal(dlr.statusMsg, 'DELIVERED');
assert.equal(dlr.statusId, consts.MESSAGE_STATE.DELIVERED);
});
// A receipt a peer really did write in UCS2 costs exactly this: undetermined, never guessed at.
test('reports a receipt body it cannot read either way as undetermined', () => {
const dlr = dlrFromPdu(deliverSm(
encodeMessage(`id:${textReceiptId} stat:DELIVRD err:000`, 'UCS2').buffer,
undefined,
consts.ESM_CLASS.MC_DELIVERY_RECEIPT,
consts.ENCODING.UCS2,
));
assert.ok(dlr);
assert.equal(dlr.receipt?.id, undefined);
assert.equal(dlr.receipt?.stat, undefined);
assert.equal(dlr.smsId, undefined);
assert.equal(dlr.statusMsg, 'UNKNOWN');
assert.equal(dlr.statusId, consts.MESSAGE_STATE.UNKNOWN);
});
test('leaves a UCS2 message to arrive as an SMS, decoded by its data_coding', () => {
const pduObj = deliverSm(encodeMessage('hej 一', 'UCS2').buffer, undefined, 0, consts.ENCODING.UCS2);
assert.equal(dlrFromPdu(pduObj), undefined);
assert.equal(pduObj.params.short_message, 'hej 一');
});
// message_state 0x80-0xFF is reserved for MC-vendor-specific values, which we cannot name.
test('falls back to the body when the state TLV carries a value it cannot name', () => {
const dlr = dlrFromPdu(deliverSm(receiptText, { message_state: { tagValue: 0x84 } }));
+37 -1
View File
@@ -1,6 +1,8 @@
import assert from 'node:assert/strict';
import test, { describe } from 'node:test';
import { PduRefusedError, isCommand, isResp, objToPdu, pduReturn, pduToObj, refusalAnswer } from '../src/pdu.ts';
import { PduRefusedError, refusalAnswer } from '../src/pdu-refusal.ts';
import { isCommand, isResp, objToPdu, pduReturn, pduToObj } from '../src/pdu.ts';
import { paramText } from '../src/defs/types.ts';
function encode(...args: Parameters<typeof objToPdu>): Buffer {
const { buffer, err } = objToPdu(...args);
@@ -206,6 +208,40 @@ describe('encoding submit_sm', () => {
});
});
describe('decoding short_message', () => {
function deliverSm(dataCoding: number, message: Buffer | string) {
return decode(encode({
cmdName: 'deliver_sm',
params: {
data_coding: dataCoding,
destination_addr: '46709771337',
short_message: message,
source_addr: '46701113311',
},
seqNr: 3,
}));
}
test('reads an inbound message with the data_coding the PDU declares', () => {
const binary = Buffer.from([0x00, 0x1B, 0x60, 0x80, 0xFF]);
assert.equal(deliverSm(0x08, 'hej 一').params.short_message, 'hej 一');
assert.equal(deliverSm(0x03, Buffer.from([0xE1, 0xE7, 0xDA])).params.short_message, 'áçÚ');
assert.deepEqual(
Buffer.from(paramText(deliverSm(0x04, binary).params.short_message), 'latin1'),
binary,
);
});
// A delivery receipt is read from these rather than from the text above, since its body is
// Appendix B's fixed format whatever the data_coding the peer inherited onto it says.
test('keeps the octets that arrived alongside the text they decoded to', () => {
const pduObj = deliverSm(0x08, 'hej 一');
assert.deepEqual(pduObj.shortMessageOctets, Buffer.from('hej 一', 'utf16le').swap16());
});
});
describe('encoding submit_multi', () => {
const dest = { dest_addr_npi: 1, dest_addr_ton: 1, destination_addr: '46709771337' };
+5 -1
View File
@@ -83,6 +83,7 @@ function submitPdu(seqNr: number, cmdStatus: ErrorName = 'ESME_ROK'): PduObject
cmdStatusId: 0,
params: { destination_addr: '46709771337', short_message: 'held', source_addr: '46701113311' },
seqNr,
shortMessageOctets: undefined,
tlvs: {},
};
}
@@ -313,6 +314,7 @@ describe('sendSms()', () => {
cmdStatusId: errors[status],
params: { message_id: messageId },
seqNr,
shortMessageOctets: undefined,
tlvs: {},
};
}
@@ -1153,6 +1155,7 @@ describe('sendDlr()', () => {
describe('reassembly bounds', () => {
function segment(reference: number, part: number, total: number): PduObject {
const udh = Buffer.from([0x05, 0x00, 0x03, reference, total, part]);
const body = Buffer.concat([udh, Buffer.from('fragment')]);
return {
cmdId: 0x00000004,
@@ -1164,10 +1167,11 @@ describe('reassembly bounds', () => {
data_coding: 0,
destination_addr: '46709771337',
esm_class: 0x40,
short_message: Buffer.concat([udh, Buffer.from('fragment')]),
short_message: body,
source_addr: '46701113311',
},
seqNr: part,
shortMessageOctets: body,
tlvs: {},
};
}
+30
View File
@@ -168,6 +168,7 @@ function enquireLink(seqNr: number): PduObject {
cmdStatusId: 0,
params: {},
seqNr,
shortMessageOctets: undefined,
tlvs: {},
};
}
@@ -749,6 +750,35 @@ describe('receiving', () => {
assert.equal(answeredNotification.pduObj.cmdName, 'deliver_sm_resp');
});
// SMPPSim's receipt for a UCS2 message inherits its data_coding and writes the body as text.
test('parses a receipt written as text under a data_coding that says UCS2', async t => {
const { peer, session } = await inbound(t);
const reported = once<{ dlr: Dlr; pduObj: PduObject }>(resolve => {
session.on('dlr', (dlr, pduObj) => { resolve({ dlr, pduObj }); });
});
const smsId = '01a072f9-30f2-71b0-87cd-f5032df3a8e0';
const body = `id:${smsId} sub:001 dlvrd:001 submit date:2509051430 done date:2509051431 stat:DELIVRD err:000 text:`;
const delivered = peer.send({
cmdName: 'deliver_sm',
params: {
data_coding: consts.ENCODING.UCS2,
destination_addr: '46709771337',
esm_class: consts.ESM_CLASS.MC_DELIVERY_RECEIPT,
short_message: Buffer.from(body, 'ascii'),
source_addr: '46701113311',
},
});
const received = await raceWithin(2000, reported);
assert.ok(received);
assert.equal(received.dlr.smsId, smsId);
assert.equal(received.dlr.statusMsg, 'DELIVERED');
assert.equal(received.dlr.receipt?.doneDate, '2509051431');
assert.equal(received.pduObj.params.data_coding, consts.ENCODING.UCS2);
assert.equal(received.pduObj.shortMessageOctets?.toString('latin1'), body);
assert.ok((await delivered).pduObj);
});
test('reassembles a multipart inbound SMS before the sms event', async t => {
const message = 'Inbound lorem ipsum dolor sit amet consectetur, '.repeat(6);
const { peer, session } = await inbound(t);