Files
smpp-js/test/declared-alphabet.test.ts
lilleman ca1a7473ed Declare the alphabet a GSM message is actually written in (#100)
* Regression tests for the alphabet a GSM message declares

* Declare the alphabet a GSM message is actually written in

* Reflow the composing-by-hand paragraph

* Drop the ASCII alias SMPP's flat table shares with the option's own

* Bound the receipt test's wait and tighten the notes around it
2026-09-09 21:30:55 +02:00

196 lines
7.0 KiB
TypeScript

import assert from 'node:assert/strict';
import test, { describe } from 'node:test';
import { bindToSmsc, dummySmsc } from './dummy-smsc.ts';
import { client } from '../src/client.ts';
import { closeAfter } from './teardown.ts';
import { consts } from '../src/defs/constants.ts';
import { decodeMessage } from '../src/message.ts';
import { dlrFromPdu } from '../src/dlr.ts';
import { encodingByDataCoding, encodings } from '../src/defs/encodings.ts';
import { objToPdu, pduToObj } from '../src/pdu.ts';
import { paramNumber } from '../src/defs/types.ts';
import { server } from '../src/server.ts';
import type { PduObject } from '../src/pdu.ts';
const from = '46701113311';
const to = '46709771337';
/** GSM 03.38 puts $ at 0x02 and @ at 0x00, where IA5 has STX and NUL. */
const bothTables = 'Cost 5$ @home';
/** What SMPP 3.4 5.2.19 assigns each coding, read the way a peer honouring the field reads it. */
const byTheSpecsTable: Record<number, (octets: Buffer) => string> = {
// The SMSC default alphabet, which every peer in interop-tests/ runs as GSM 03.38.
0x00: octets => encodings.ASCII.decode(octets),
// IA5 (CCITT T.50), whose whole range is what Latin-1 reads below 0x80.
[consts.ENCODING.IA5]: octets => octets.toString('latin1'),
};
/** An event that never fires would otherwise block until the CI job limit, asserting nothing. */
function once<T>(register: (resolve: (value: T) => void) => void): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error('waited 5000 ms for an event that never fired'));
}, 5000);
register(value => {
clearTimeout(timer);
resolve(value);
});
});
}
function submitted(octets: Buffer[]): PduObject[] {
return octets.map(pdu => {
const { pduObj } = pduToObj(pdu);
assert.ok(pduObj);
return pduObj;
});
}
function declaredBy(pduObj: PduObject): number {
return paramNumber(pduObj.params.data_coding, 0);
}
/** A deliver_sm carrying `body` under `dataCoding`, as a peer answering our own send would write it. */
function delivered(body: Buffer | string, dataCoding: number, esmClass: number): PduObject {
const { buffer } = objToPdu({
cmdName: 'deliver_sm',
params: {
data_coding: dataCoding,
destination_addr: from,
esm_class: esmClass,
short_message: body,
source_addr: to,
},
seqNr: 1,
});
assert.ok(buffer);
const { pduObj } = pduToObj(buffer);
assert.ok(pduObj);
return pduObj;
}
describe('the alphabet a message declares is the one its octets are written in', () => {
test('sends GSM 03.38 under data_coding 0x00, the SMSC default alphabet', async t => {
const smsc = await dummySmsc(t, { messageIds: ['01a08779-de97-7caa-9d26-e6d50f5c4888'] });
const session = await bindToSmsc(t, smsc.port, { reconnect: false });
const sent = await session.sendSms({ from, message: bothTables, to });
assert.equal(sent.err, undefined);
assert.deepEqual(submitted(smsc.octets).map(declaredBy), [0x00]);
});
test('keeps $ and @ for a peer that honours the declaration, where IA5 read STX and NUL', async t => {
const smsc = await dummySmsc(t, { messageIds: ['01a08779-de98-7d24-9542-e652e0d3761c'] });
const session = await bindToSmsc(t, smsc.port, { reconnect: false });
assert.equal((await session.sendSms({ from, message: bothTables, to })).err, undefined);
const [pduObj] = submitted(smsc.octets);
assert.ok(pduObj);
const octets = pduObj.shortMessageOctets;
assert.ok(octets);
assert.equal(octets.toString('hex'), '436f73742035022000686f6d65');
const read = byTheSpecsTable[declaredBy(pduObj)];
assert.ok(read, 'the coding declared must be one SMPP 3.4 5.2.19 names an alphabet for');
assert.equal(read(octets), bothTables);
});
test('leaves Latin-1 at 0x03 and UCS2 at 0x08, the codings those alphabets always had', async t => {
const smsc = await dummySmsc(t, {
messageIds: ['01a08779-de99-7fa5-bcac-18feef55aeee', '01a08779-de99-72ec-8dbb-9365a00158c3'],
});
const session = await bindToSmsc(t, smsc.port, { reconnect: false });
assert.equal((await session.sendSms({ encoding: 'LATIN1', from, message: 'Räksmörgås', to })).err, undefined);
assert.equal((await session.sendSms({ from, message: 'あいう', to })).err, undefined);
assert.deepEqual(submitted(smsc.octets).map(declaredBy), [0x03, 0x08]);
});
// sendDlr() writes its body as a string with no data_coding, so it takes the detected branch too.
test('declares 0x00 on a receipt it writes itself', async t => {
const { err, server: smpp } = await server({ port: 0 });
assert.equal(err, undefined);
assert.ok(smpp);
closeAfter(t, smpp);
smpp.on('session', peer => peer.on('sms', async sms => {
await sms.sendResp();
await sms.sendDlr('DELIVERED');
}));
const connected = await client({ port: smpp.port, reconnect: false });
assert.equal(connected.err, undefined);
assert.ok(connected.session);
closeAfter(t, connected.session);
const session = connected.session;
const reported = once<PduObject>(resolve => {
session.on('dlr', (_report, pduObj) => { resolve(pduObj); });
});
assert.equal((await session.sendSms({ dlr: true, from, message: bothTables, to })).err, undefined);
assert.equal(declaredBy(await reported), 0x00);
});
// The low-level surface settles data_coding off the same detection, so it carried the same defect.
test('settles a detected string body at 0x00 where the caller named no data_coding', () => {
const built = objToPdu({
cmdName: 'submit_sm',
params: { destination_addr: to, short_message: bothTables, source_addr: from },
});
assert.ok(built.buffer);
const { pduObj } = pduToObj(built.buffer);
assert.ok(pduObj);
assert.equal(declaredBy(pduObj), 0x00);
assert.equal(pduObj.shortMessageOctets?.toString('hex'), '436f73742035022000686f6d65');
});
});
describe('what a peer declares is read as generously as it was before', () => {
test('reads data_coding 0x01 as GSM 03.38, as 0x00 is read', () => {
assert.equal(encodingByDataCoding(0x01), 'ASCII');
const octets = Buffer.from('436f73742035022000686f6d65', 'hex');
for (const dataCoding of [0x00, 0x01]) {
assert.equal(decodeMessage(octets, dataCoding).message, bothTables, String(dataCoding));
}
});
test('leaves a receipt and an inbound message reading the same under either coding', () => {
const receiptId = '01a08779-de97-7caa-9d26-e6d50f5c4888';
const body = `id:${receiptId} sub:001 dlvrd:001 submit date:2509091430 done date:2509091431 stat:DELIVRD err:000 text:${bothTables}`;
for (const dataCoding of [0x00, 0x01]) {
const receipt = dlrFromPdu(delivered(body, dataCoding, consts.ESM_CLASS.MC_DELIVERY_RECEIPT));
assert.ok(receipt, String(dataCoding));
assert.equal(receipt.smsId, receiptId, String(dataCoding));
assert.equal(receipt.statusMsg, 'DELIVERED', String(dataCoding));
const inbound = delivered(Buffer.from('436f73742035022000686f6d65', 'hex'), dataCoding, 0);
assert.equal(dlrFromPdu(inbound), undefined, String(dataCoding));
assert.equal(inbound.params.short_message, bothTables, String(dataCoding));
}
});
});