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:
+72
-2
@@ -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
@@ -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' };
|
||||
|
||||
|
||||
@@ -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: {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user