Fixtures for the receipt bodies operator documentation publishes (#93)

* Fixtures for the receipt bodies operator documentation publishes

* Read the stat:FAILED several operators write as UNDELIVERABLE

* Cover the 16-bit UDH concatenation element with a fixture

* Record phase 9 of the interoperability plan

* Share the dummy SMSC and tighten the operator fixtures

* Correct what the test conventions claim about the doubles and the tree

* Read a receipt date that carries its century

* Read the stat spelling CM.com publishes

* Say what the receipt status table actually holds
This commit is contained in:
2026-09-08 18:56:57 +02:00
committed by GitHub
parent 83176f53b0
commit 5a0cf56754
11 changed files with 946 additions and 58 deletions
+24
View File
@@ -151,6 +151,30 @@ describe('dlrFromPdu()', () => {
}
});
// CM.com documents its receipt dates as yyyyMMddHHmmss; 10, 12 and 14 are three distinct widths.
test('reads a receipt date whichever of the three widths the peer writes it in', () => {
const at = (date: string): string | undefined =>
dlrFromPdu(deliverSm(`id:x stat:DELIVRD done date:${date}`))?.doneDate?.toISOString();
assert.equal(at('2508251431'), '2025-08-25T14:31:00.000Z');
assert.equal(at('250825143145'), '2025-08-25T14:31:45.000Z');
assert.equal(at('20250825143145'), '2025-08-25T14:31:45.000Z');
assert.equal(at('202508251431'), undefined, 'twelve digits is YYMMDDhhmmss, not a year and no seconds');
assert.equal(at('00250825143145'), undefined, 'Date.UTC would read year 25 as 1925');
});
// message_state 9 is Telesign's SKIPPED, which the seven-character stat field has no code for.
test('names a state only the TLV can spell', () => {
const dlr = dlrFromPdu(deliverSm('id:x stat:UNKNOWN err:000 text:', {
message_state: { tagValue: consts.MESSAGE_STATE.SKIPPED },
}));
assert.ok(dlr);
assert.equal(dlr.statusMsg, 'SKIPPED');
assert.equal(dlr.statusId, 9);
assert.equal(dlr.intermediate, false);
});
test('leaves an impossible receipt date undefined rather than rolling it over', () => {
const rolled = dlrFromPdu(deliverSm('id:x stat:DELIVRD done date:9902310000'));
+104
View File
@@ -0,0 +1,104 @@
import assert from 'node:assert/strict';
import net from 'node:net';
import type { Session } from '../src/session.ts';
import type { TestContext } from 'node:test';
import { PduFramer } from '../src/pdu-framer.ts';
import { client } from '../src/client.ts';
import { closeAfter, closeListenerAfter } from './teardown.ts';
import { consts } from '../src/defs/constants.ts';
import { objToPdu, pduReturn, pduToObj } from '../src/pdu.ts';
import { uuidv7 } from '../src/uuid.ts';
export type DummySmsc = {
/** Writes a delivery receipt to the ESME, its body spelled as the test names it. */
deliver: (body: string) => void;
/** Every submit_sm the ESME wrote, exactly as it arrived on the socket. */
octets: Buffer[];
port: number;
};
export type DummySmscOptions = {
/** The id each submit is answered with, in order; a spent list answers with none, as Telesign does. */
messageIds?: readonly string[];
};
/**
* An SMSC that answers every request the ESME sends and starts nothing of its own. It exists for the
* peers `server()` cannot be — one whose message ids the test chooses, or one that answers a request
* differently from how this library would. A test that answers the peer's requests itself wants
* `smscPeer()` in `session.test.ts` instead, which answers the bind and hands over the rest.
*/
export async function dummySmsc(t: TestContext, options: DummySmscOptions = {}): Promise<DummySmsc> {
const octets: Buffer[] = [];
const sockets: net.Socket[] = [];
let answered = 0;
let delivered = 0;
const nextId = (): string => (options.messageIds ? options.messageIds[answered++] ?? '' : uuidv7());
const listener = net.createServer(sock => {
const framer = new PduFramer();
sockets.push(sock);
sock.on('data', chunk => {
framer.push(chunk);
for (const pdu of framer.next().pdus ?? []) {
const { pduObj } = pduToObj(pdu);
// A response answers nothing; the ESME's deliver_sm_resp is the one that arrives here.
if (!pduObj || pduObj.cmdName.endsWith('_resp')) continue;
// Only a submit takes an id from the list; a bind answered off it shifts every fixture.
const submitted = pduObj.cmdName === 'submit_sm';
const answer = submitted
? pduReturn(pduObj, 'ESME_ROK', { message_id: nextId() })
: pduReturn(pduObj, 'ESME_ROK', { system_id: 'dummy' });
if (submitted) octets.push(pdu);
// Writing nothing leaves the test waiting out its own timeout with nothing naming why.
assert.ok(answer.buffer, `the dummy SMSC has no answer for ${pduObj.cmdName}`);
sock.write(answer.buffer);
}
});
});
closeListenerAfter(t, listener, sockets);
await new Promise<void>(resolve => { listener.listen(0, resolve); });
const address = listener.address();
return {
deliver: (body: string) => {
const { buffer } = objToPdu({
cmdName: 'deliver_sm',
params: {
destination_addr: '46701113311',
esm_class: consts.ESM_CLASS.MC_DELIVERY_RECEIPT,
short_message: body,
source_addr: '46709771337',
},
seqNr: ++delivered,
});
assert.ok(buffer);
sockets[sockets.length - 1]?.write(buffer);
},
octets,
port: typeof address === 'object' && address !== null ? address.port : 0,
};
}
/** A client bound to one of the above, torn down with the test. */
export async function bindToSmsc(
t: TestContext,
port: number,
options: Parameters<typeof client>[0] = {},
): Promise<Session> {
const { err, session } = await client({ ...options, port });
assert.equal(err, undefined);
assert.ok(session);
closeAfter(t, session);
return session;
}
+5 -43
View File
@@ -1,20 +1,16 @@
import assert from 'node:assert/strict';
import net from 'node:net';
import test, { describe } from 'node:test';
import type { PduObjectInput } from '../src/pdu.ts';
import type { SendSmsDeps } from '../src/send-sms.ts';
import type { Session } from '../src/session.ts';
import type { SubmitMessagingMode } from '../src/defs/constants.ts';
import type { TestContext } from 'node:test';
import { PduFramer } from '../src/pdu-framer.ts';
import { client } from '../src/client.ts';
import { closeAfter, closeListenerAfter } from './teardown.ts';
import { bindToSmsc, dummySmsc } from './dummy-smsc.ts';
import { consts, submitMessagingModes } from '../src/defs/constants.ts';
import { paramNumber } from '../src/defs/types.ts';
import { pduReturn, pduToObj } from '../src/pdu.ts';
import { pduToObj } from '../src/pdu.ts';
import { silentLog } from '../src/log.ts';
import { submitSms } from '../src/send-sms.ts';
import { uuidv7 } from '../src/uuid.ts';
const from = '46701113311';
const to = '46709771337';
@@ -38,45 +34,11 @@ type BoundPeer = {
session: Session;
};
/** An SMSC that answers a bind and every submit, keeping the octets each submit arrived as. */
async function boundToPeer(t: TestContext): Promise<BoundPeer> {
const octets: Buffer[] = [];
const sockets: net.Socket[] = [];
const listener = net.createServer(sock => {
const framer = new PduFramer();
const smsc = await dummySmsc(t);
const session = await bindToSmsc(t, smsc.port, { reconnect: false });
sockets.push(sock);
sock.on('data', chunk => {
framer.push(chunk);
for (const pdu of framer.next().pdus ?? []) {
const { pduObj } = pduToObj(pdu);
if (!pduObj) continue;
if (pduObj.cmdName === 'submit_sm') octets.push(pdu);
const answered = pduReturn(pduObj, 'ESME_ROK', pduObj.cmdName === 'submit_sm'
? { message_id: uuidv7() }
: { system_id: 'byte-peer' });
if (answered.buffer) sock.write(answered.buffer);
}
});
});
closeListenerAfter(t, listener, sockets);
await new Promise<void>(resolve => { listener.listen(0, resolve); });
const address = listener.address();
const port = typeof address === 'object' && address !== null ? address.port : 0;
const { err, session } = await client({ port, reconnect: false });
assert.equal(err, undefined);
assert.ok(session);
closeAfter(t, session);
return { octets, session };
return { octets: smsc.octets, session };
}
function hexOf(octets: Buffer[]): string[] {
+494
View File
@@ -0,0 +1,494 @@
import assert from 'node:assert/strict';
import test, { describe } from 'node:test';
import type { Dlr, Receipt } from '../src/dlr.ts';
import type { MessageDlr } from '../src/session.ts';
import type { PduObject, TlvInput } from '../src/pdu.ts';
import { bindToSmsc, dummySmsc } from './dummy-smsc.ts';
import { consts } from '../src/defs/constants.ts';
import { dlrFromPdu, parseReceipt, receiptCodes, transientStates } from '../src/dlr.ts';
import { objToPdu, pduToObj } from '../src/pdu.ts';
/**
* Receipt bodies as commercial operators document them, from `interop-tests/research/operator-quirks.md`
* topics 4 and 5. Every peer the interop suite can run is open source; these shapes are the ones only
* an operator writes, so each fixture carries the URL it was read from.
*/
function deliverSm(
body: string,
tlvs?: Record<string, TlvInput>,
esmClass: number = consts.ESM_CLASS.MC_DELIVERY_RECEIPT,
): PduObject {
const { buffer } = objToPdu({
cmdName: 'deliver_sm',
params: {
destination_addr: '46701113311',
esm_class: esmClass,
short_message: body,
source_addr: '46709771337',
},
seqNr: 1,
tlvs,
});
assert.ok(buffer);
const { pduObj } = pduToObj(buffer);
assert.ok(pduObj);
return pduObj;
}
type ReceiptFixture = {
body: string;
dlr: {
doneDate: string | undefined;
errorCode: string | undefined;
intermediate: boolean;
smsId: string | undefined;
statusId: number;
statusMsg: Dlr['statusMsg'];
};
name: string;
receipt: Receipt;
source: string;
tlvs?: Record<string, TlvInput>;
};
const fixtures: readonly ReceiptFixture[] = [
{
body: 'id:e731049e9fc84e61 sub:000 dlvrd:000 submit date:2609051430 done date:2609051431 stat:DELIVRD err:000 text:',
dlr: {
doneDate: '2026-09-05T14:31:00.000Z',
errorCode: '000',
intermediate: false,
smsId: 'e731049e9fc84e61',
statusId: consts.MESSAGE_STATE.DELIVERED,
statusMsg: 'DELIVERED',
},
name: 'LINK Mobility, whose sub and dlvrd are always 000 and whose text is always empty',
receipt: {
dlvrd: 0,
doneDate: '2609051431',
err: '000',
id: 'e731049e9fc84e61',
stat: 'DELIVRD',
sub: 0,
submitDate: '2609051430',
text: '',
},
source: 'https://www.linkmobility.com/resources/developer/SMSC-SMPP-User-Guide-1.5.pdf',
},
{
body: 'id:2a1f0f1d sub:001 dlvrd:000 submit date:2609051430 done date:2609051447 stat:FAILED err:051 text:none',
dlr: {
doneDate: '2026-09-05T14:47:00.000Z',
errorCode: '051',
intermediate: false,
smsId: '2a1f0f1d',
statusId: consts.MESSAGE_STATE.UNDELIVERABLE,
statusMsg: 'UNDELIVERABLE',
},
name: 'Vonage, whose stat:FAILED is six characters and outside Appendix B',
receipt: {
dlvrd: 0,
doneDate: '2609051447',
err: '051',
id: '2a1f0f1d',
stat: 'FAILED',
sub: 1,
submitDate: '2609051430',
text: 'none',
},
source: 'https://api.support.vonage.com/hc/en-us/articles/204015663',
},
{
body: 'id:44191696 sub:001 dlvrd:000 submit date:2609051430 done date:2609051430 stat:ENROUTE err:000',
dlr: {
doneDate: '2026-09-05T14:30:00.000Z',
errorCode: '000',
intermediate: true,
smsId: '44191696',
statusId: consts.MESSAGE_STATE.ENROUTE,
statusMsg: 'ENROUTE',
},
name: 'Infobip, reporting ENROUTE in an ordinary receipt that carries no text field at all',
receipt: {
dlvrd: 0,
doneDate: '2609051430',
err: '000',
id: '44191696',
stat: 'ENROUTE',
sub: 1,
submitDate: '2609051430',
text: undefined,
},
source: 'https://www.infobip.com/docs/essentials/api-essentials/smpp-specification',
},
{
body: 'id:7d94e772 sub:001 dlvrd:001 submit date:260905143012 done date:260905143145 stat:DELIVRD err:000 text:',
dlr: {
doneDate: '2026-09-05T14:31:45.000Z',
errorCode: '000',
intermediate: false,
smsId: '7d94e772',
statusId: consts.MESSAGE_STATE.DELIVERED,
statusMsg: 'DELIVERED',
},
name: 'Clickatell, whose dates carry seconds',
receipt: {
dlvrd: 1,
doneDate: '260905143145',
err: '000',
id: '7d94e772',
stat: 'DELIVRD',
sub: 1,
submitDate: '260905143012',
text: '',
},
source: 'https://archive.clickatell.com/developers/api-docs/pdu-details/',
},
{
body: 'id:5be9f816f19992a78c8e26442f8afa50 submit date:20260905143012 done date:20260905143345 stat:DELIVERD err:000',
dlr: {
doneDate: '2026-09-05T14:33:45.000Z',
errorCode: '000',
intermediate: false,
smsId: '5be9f816f19992a78c8e26442f8afa50',
statusId: consts.MESSAGE_STATE.DELIVERED,
statusMsg: 'DELIVERED',
},
name: 'CM.com, which writes a four-digit year, an eight-character stat, and the status twice',
receipt: {
dlvrd: undefined,
doneDate: '20260905143345',
err: '000',
id: '5be9f816f19992a78c8e26442f8afa50',
stat: 'DELIVERD',
sub: undefined,
submitDate: '20260905143012',
text: undefined,
},
source: 'https://developers.cm.com/messaging/docs/smpp',
tlvs: { message_state: { tagValue: consts.MESSAGE_STATE.DELIVERED } },
},
{
body: 'id:e9ca671b2497d778d771938333dc0c52 sub:001 dlvrd:000 submit date:260905143000 done date:260905143010 stat:UNDELIV err:4A6 text:',
dlr: {
doneDate: '2026-09-05T14:30:10.000Z',
errorCode: '4A6',
intermediate: false,
smsId: 'e9ca671b2497d778d771938333dc0c52',
statusId: consts.MESSAGE_STATE.UNDELIVERABLE,
statusMsg: 'UNDELIVERABLE',
},
name: 'Telesign, whose err is hexadecimal and whose status is stated in the body and the TLVs alike',
receipt: {
dlvrd: 0,
doneDate: '260905143010',
err: '4A6',
id: 'e9ca671b2497d778d771938333dc0c52',
stat: 'UNDELIV',
sub: 1,
submitDate: '260905143000',
text: '',
},
source: 'https://developer.telesign.com/enterprise/docs/smpp-protocol',
tlvs: {
message_state: { tagValue: consts.MESSAGE_STATE.UNDELIVERABLE },
receipted_message_id: { tagValue: 'e9ca671b2497d778d771938333dc0c52' },
},
},
];
describe('receipt bodies operators document', () => {
for (const fixture of fixtures) {
test(fixture.name, () => {
const dlr = dlrFromPdu(deliverSm(fixture.body, fixture.tlvs));
assert.ok(dlr, fixture.source);
assert.deepEqual(dlr.receipt, fixture.receipt, fixture.source);
assert.deepEqual({
doneDate: dlr.doneDate?.toISOString(),
errorCode: dlr.errorCode,
intermediate: dlr.intermediate,
smsId: dlr.smsId,
statusId: dlr.statusId,
statusMsg: dlr.statusMsg,
}, fixture.dlr, fixture.source);
});
}
});
/** Every `stat:` code the researched operators list, per operator, with the page it is on. */
const documentedCodes: readonly { codes: readonly string[]; operator: string; source: string }[] = [
{
codes: ['ACCEPTD', 'DELIVRD', 'REJECTD', 'UNDELIV'],
operator: 'Clickatell',
source: 'https://archive.clickatell.com/developers/api-docs/pdu-details/',
},
{
codes: ['ACCEPTD', 'DELETED', 'DELIVERD', 'EXPIRED', 'REJECTD', 'UNDELIV', 'UNKNOWN'],
operator: 'CM.com',
source: 'https://developers.cm.com/messaging/docs/smpp',
},
{
codes: ['ACCEPTD', 'DELIVRD', 'ENROUTE', 'EXPIRED', 'REJECTD', 'UNDELIV', 'UNKNOWN'],
operator: 'Infobip',
source: 'https://www.infobip.com/docs/essentials/api-essentials/smpp-specification',
},
{
codes: ['DELIVRD', 'EXPIRED', 'FAILED', 'UNDELIV'],
operator: 'Kaleyra',
source: 'https://messaging.kaleyra.com/support/solutions/articles/3000091798-delivery-reports',
},
{
codes: ['DELETED', 'DELIVRD', 'EXPIRED', 'REJECTD', 'UNDELIV'],
operator: 'LINK Mobility',
source: 'https://www.linkmobility.com/resources/developer/SMSC-SMPP-User-Guide-1.5.pdf',
},
{
codes: ['DELIVRD', 'EXPIRED', 'FAILED', 'REJECTD', 'UNDELIV'],
operator: 'Route Mobile',
source: 'https://routemobile.com/pdf_files/developer/api/routemobilesmpp.pdf',
},
{
codes: ['ACCEPTD', 'DELETED', 'DELIVRD', 'EXPIRED', 'FAILED', 'REJECTD', 'UNDELIV', 'UNKNOWN'],
operator: 'Vonage',
source: 'https://api.support.vonage.com/hc/en-us/articles/204015663',
},
];
describe('the status codes operators publish', () => {
test('names a state of its own for every one of them', () => {
for (const { codes, operator, source } of documentedCodes) {
for (const code of codes) {
const dlr = dlrFromPdu(deliverSm(`id:ec421e62 stat:${code} err:000 text:`));
assert.ok(dlr);
assert.equal(
dlr.statusMsg === 'UNKNOWN',
code === 'UNKNOWN',
`${operator} documents stat:${code}, read as ${dlr.statusMsg}${source}`,
);
assert.equal(dlr.intermediate, code === 'ENROUTE', `${operator} stat:${code}${source}`);
}
}
});
// A code the reader cannot name leaves an unmarked deliver_sm arriving as an inbound message.
test('reads an unmarked deliver_sm reporting one of them as a report, not as a message', () => {
for (const code of ['DELIVERD', 'FAILED']) {
const dlr = dlrFromPdu(deliverSm(`id:2a1f0f1d stat:${code} err:051 text:none`, undefined, 0));
assert.ok(dlr, `stat:${code} marks a report even where esm_class does not`);
assert.equal(dlr.smsId, '2a1f0f1d');
}
});
// "Only none or final delivery ... are supported" — the SMSC-SMPP User Guide 1.5, sourced below.
test('finds none of LINK Mobility\'s among the transient ones', () => {
const link = documentedCodes.find(one => one.operator === 'LINK Mobility');
const transient = transientStates.map(state => receiptCodes[state]);
assert.ok(link);
assert.deepEqual(link.codes.filter(code => transient.includes(code)), [], link.source);
});
});
describe('a receipt body that is not the shape the spec fixes', () => {
const id = 'f5c98a862c8d6014';
const ordered = `id:${id} sub:001 dlvrd:001 submit date:2609051430 done date:2609051431 stat:DELIVRD err:000 text:`;
test('reads the same fields whatever order they arrive in', () => {
const shuffled = `stat:DELIVRD err:000 done date:2609051431 dlvrd:001 submit date:2609051430 sub:001 id:${id} text:`;
assert.deepEqual(parseReceipt(shuffled), parseReceipt(ordered));
});
// text: is the one field that may hold spaces, so it can only end where the line does.
test('reads the rest of the line as the text where a peer does not write text last', () => {
const early = `id:${id} text: stat:DELIVRD err:000`;
assert.equal(parseReceipt(early).text, ' stat:DELIVRD err:000');
assert.equal(parseReceipt(early).stat, 'DELIVRD', 'the other fields are still read');
});
test('settles a status against no message where a marked receipt names no id', () => {
const bodyless = 'sub:001 dlvrd:001 submit date:2609051430 done date:2609051431 stat:DELIVRD err:000 text:';
const marked = dlrFromPdu(deliverSm(bodyless));
assert.ok(marked);
assert.equal(marked.smsId, undefined);
assert.equal(marked.statusMsg, 'DELIVERED');
assert.equal(marked.receipt?.id, undefined);
assert.equal(dlrFromPdu(deliverSm(bodyless, undefined, 0)), undefined, 'unmarked, it is a message');
});
// Telesign's page calls err a 3-octet hex code and then gives eight-digit examples of it.
test('hands the err field over as it arrived, whichever width the operator writes', () => {
for (const err of ['4A6', '000004A6']) {
assert.equal(dlrFromPdu(deliverSm(`id:x stat:UNDELIV err:${err}`))?.errorCode, err);
}
});
test('takes the id from the TLV where the body names none', () => {
const id54 = '08472259999bf99e679376b52ebbb685';
const dlr = dlrFromPdu(deliverSm('sub:001 stat:DELIVRD err:000 text:', {
receipted_message_id: { tagValue: id54 },
}, 0));
assert.ok(dlr);
assert.equal(dlr.smsId, id54);
assert.equal(dlr.statusMsg, 'DELIVERED');
});
});
/** Resolves once `count` of them have arrived, so a run short of that fails rather than hangs. */
function collect<T>(count: number, register: (push: (value: T) => void) => void): Promise<T[]> {
const values: T[] = [];
return new Promise<T[]>((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`waited 5000 ms for ${String(count)} events, ${String(values.length)} arrived`));
}, 5000);
register(value => {
values.push(value);
if (values.length < count) return;
clearTimeout(timer);
resolve(values);
});
});
}
const message = { from: '46701113311', to: '46709771337' };
function receiptBody(id: string): string {
return `id:${id} sub:001 dlvrd:001 submit date:2609051430 done date:2609051431 stat:DELIVRD err:000 text:`;
}
describe('an SMSC that writes its message ids in two notations', () => {
// Vonage answers a submit in hex and writes the receipt's id: in decimal off the same number.
const hex = '33647f6c';
const decimal = '862224236';
test('correlates the receipt against the send once both notations are named', async t => {
const smsc = await dummySmsc(t, { messageIds: [hex] });
const session = await bindToSmsc(t, smsc.port, {
smsIdFormat: { receipt: 'decimal', submitResp: 'hex' },
});
const reported = collect<Dlr>(1, push => { session.on('dlr', push); });
const sent = await session.sendSms({ dlr: true, message: 'operator receipt', ...message });
assert.equal(sent.err, undefined);
assert.deepEqual(sent.smsIds, [decimal]);
smsc.deliver(receiptBody(decimal));
const [dlr] = await reported;
assert.ok(dlr);
assert.equal(dlr.smsId, sent.smsIds[0]);
assert.equal(dlr.receipt?.id, decimal, 'the receipt itself keeps what the operator wrote');
});
test('leaves the two incomparable where neither notation is named', async t => {
const smsc = await dummySmsc(t, { messageIds: [hex] });
const session = await bindToSmsc(t, smsc.port);
const reported = collect<Dlr>(1, push => { session.on('dlr', push); });
const sent = await session.sendSms({ dlr: true, message: 'operator receipt', ...message });
assert.deepEqual(sent.smsIds, [hex]);
smsc.deliver(receiptBody(decimal));
const [dlr] = await reported;
assert.ok(dlr);
assert.equal(dlr.smsId, decimal);
assert.notEqual(dlr.smsId, sent.smsIds[0]);
});
test('strips the padding an operator writes the same number with', async t => {
const smsc = await dummySmsc(t, { messageIds: ['706678557'] });
const session = await bindToSmsc(t, smsc.port, {
smsIdFormat: { receipt: 'decimal', submitResp: 'decimal' },
});
const reported = collect<Dlr>(1, push => { session.on('dlr', push); });
const sent = await session.sendSms({ dlr: true, message: 'operator receipt', ...message });
assert.deepEqual(sent.smsIds, ['706678557']);
smsc.deliver(receiptBody('0000706678557'));
const [dlr] = await reported;
assert.ok(dlr);
assert.equal(dlr.smsId, sent.smsIds[0]);
});
});
describe('an SMSC that reports one message more than once', () => {
// tyntec sends a buffered receipt shortly after submission and a final one later.
test('hands both receipts to the application rather than taking the second for a duplicate', async t => {
const id = 'd91518bd27c1018d';
const smsc = await dummySmsc(t, { messageIds: [id] });
const session = await bindToSmsc(t, smsc.port);
const reported = collect<Dlr>(2, push => { session.on('dlr', push); });
const sent = await session.sendSms({ dlr: true, message: 'buffered then delivered', ...message });
assert.deepEqual(sent.smsIds, [id]);
smsc.deliver(`id:${id} sub:001 dlvrd:000 submit date:2609051430 done date:2609051430 stat:ENROUTE err:000 text:`);
smsc.deliver(receiptBody(id));
const [buffered, final] = await reported;
assert.ok(buffered);
assert.ok(final);
assert.equal(buffered.smsId, id);
assert.equal(final.smsId, id);
assert.deepEqual([buffered.intermediate, final.intermediate], [true, false]);
assert.deepEqual([buffered.statusMsg, final.statusMsg], ['ENROUTE', 'DELIVERED']);
});
});
describe('an SMSC that reports each segment under an id of its own', () => {
// Vonage sends one receipt per segment, and its ids carry no <base>-<n> to merge them by.
test('reports every segment and merges nothing', async t => {
const ids = ['bf53ad8b', '40ccdce2', 'b64bf122'];
const smsc = await dummySmsc(t, { messageIds: ids });
const session = await bindToSmsc(t, smsc.port);
const merged: MessageDlr[] = [];
const reported = collect<Dlr>(3, push => { session.on('dlr', push); });
session.on('messageDlr', report => merged.push(report));
const sent = await session.sendSms({ dlr: true, message: 'x'.repeat(400), ...message });
assert.equal(sent.err, undefined);
assert.deepEqual(sent.smsIds, ids);
for (const id of ids) {
smsc.deliver(receiptBody(id));
}
const dlrs = await reported;
assert.deepEqual(dlrs.map(one => one.smsId), ids);
assert.deepEqual(merged, [], 'unrelated ids spell out no message to merge');
});
// Telesign answers only the first part of a concatenated submit with a message id.
test('hands back what landed where only the first segment is answered with one', async t => {
const id = '5cb0ea53b5d61093529174ca44e23871';
const smsc = await dummySmsc(t, { messageIds: [id] });
const session = await bindToSmsc(t, smsc.port);
const sent = await session.sendSms({ dlr: true, message: 'x'.repeat(400), ...message });
assert.equal(sent.err, undefined);
assert.equal(smsc.octets.length, 3, 'every segment goes out whatever the peer answers');
assert.deepEqual(sent.smsIds, [id, '', '']);
});
});
+31 -2
View File
@@ -1471,8 +1471,10 @@ describe('sendDlr()', () => {
});
});
function segment(reference: number, part: number, total: number): PduObject {
const udh = Buffer.from([0x05, 0x00, 0x03, reference, total, part]);
function segment(reference: number, part: number, total: number, width: 8 | 16 = 8): PduObject {
const udh = width === 8
? Buffer.from([0x05, 0x00, 0x03, reference, total, part])
: Buffer.from([0x06, 0x08, 0x04, reference >>> 8, reference & 0xff, total, part]);
const body = Buffer.concat([udh, Buffer.from('fragment')]);
return {
@@ -1544,6 +1546,11 @@ describe('where a segment says it is concatenated', () => {
test('names the spelling a segment was numbered by, alongside the reference', () => {
assert.deepEqual(concatOf(sarSegment(5, 2, 3)), { part: 2, reference: 5, spelling: 'sar', total: 3 });
assert.deepEqual(concatOf(segment(5, 2, 3)), { part: 2, reference: 5, spelling: 'udh', total: 3 });
assert.deepEqual(
concatOf(segment(0x2af1, 2, 3, 16)),
{ part: 2, reference: 0x2af1, spelling: 'udh', total: 3 },
'GSM 03.40 element 0x08 numbers a segment as element 0x00 does, two octets wider',
);
});
test('reads a segment carrying both spellings from its UDH', () => {
@@ -1632,6 +1639,28 @@ describe('reassembly bounds', () => {
assert.equal(third.smsId, collected.smsId);
});
// The header is stripped by its own declared length, so the wider element assembles identically.
test('assembles a message numbered by a 16-bit UDH reference', () => {
const reassembler = new Reassembler({
log: silentLog,
max: 10,
now: () => 0,
onLost: () => undefined,
timeout: 60_000,
});
const first = collectPdu(reassembler, segment(0x2af1, 1, 2, 16));
assert.ok(first.kept);
assert.equal(first.whole, undefined);
const collected = collectPdu(reassembler, segment(0x2af1, 2, 2, 16));
assert.ok(collected.kept);
assert.ok(collected.whole);
assert.equal(decodeSegments(collected.whole), 'fragmentfragment');
});
// A group the store cannot hold at all is refused, not accepted and then thrown away.
test('refuses a lone segment whose own arrival overruns the octet cap', () => {
const lost: LostGroup[] = [];
+11
View File
@@ -2471,6 +2471,17 @@ describe('merged delivery report bounds', () => {
assert.equal(dlrMerger.size, 0);
});
// Telesign answers only the first segment of a concatenated submit with a message id.
test('arms nothing for a send whose ids do not number one message', () => {
const dlrMerger = merger();
dlrMerger.expect(['5cb0ea53b5d61093529174ca44e23871', '', '']);
assert.equal(dlrMerger.size, 0, 'an id the peer never named numbers nothing');
dlrMerger.expect(['bf53ad8b-1', '40ccdce2-2']);
assert.equal(dlrMerger.size, 0, 'nor do ids numbered off a base each');
});
// A receipt for whole-3 would otherwise fill the slot whole-2 was registered for, truncating the report.
test('ignores a receipt for a part the send never registered', () => {
const dlrMerger = merger();