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
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
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));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test, { describe } from 'node:test';
|
||||
import { detect, encodingByDataCoding, encodings, unencodable } from '../src/defs/encodings.ts';
|
||||
import { dataCodingByEncoding, detect, encodingByDataCoding, encodings, isEncodingName, unencodable } from '../src/defs/encodings.ts';
|
||||
|
||||
describe('ASCII (GSM 03.38)', () => {
|
||||
const samples: [string, number[]][] = [
|
||||
@@ -188,6 +188,14 @@ describe('encodingByDataCoding()', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('reads every coding dataCodingByEncoding writes back as the alphabet that wrote it', () => {
|
||||
for (const name of Object.keys(dataCodingByEncoding)) {
|
||||
if (!isEncodingName(name)) return assert.fail(`${name} names no alphabet`);
|
||||
|
||||
assert.equal(encodingByDataCoding(dataCodingByEncoding[name]), name);
|
||||
}
|
||||
});
|
||||
|
||||
test('falls back to ASCII for alphabets it has no codec for', () => {
|
||||
assert.equal(encodingByDataCoding(0x05), 'ASCII');
|
||||
assert.equal(encodingByDataCoding(0x0E), 'ASCII');
|
||||
|
||||
@@ -171,7 +171,7 @@ describe('sendSms() flash', () => {
|
||||
}
|
||||
|
||||
// 0.4.0 forced 0x10 whatever the alphabet was, which mangled every non-GSM flash message.
|
||||
assert.deepEqual(dataCodingsOf(smsc.octets), [0x01, 0x10, 0x08, 0x18, 0x03]);
|
||||
assert.deepEqual(dataCodingsOf(smsc.octets), [0x00, 0x10, 0x08, 0x18, 0x03]);
|
||||
});
|
||||
|
||||
test('refuses a flash Latin-1 message, which no coding group carrying a class can spell', async () => {
|
||||
|
||||
@@ -19,13 +19,13 @@ const to = '46709771337';
|
||||
const longMessage = 'Segments of a long message, counted in a user data header. '.repeat(7);
|
||||
|
||||
/** A freshly bound session's first submit_sm for `Hello world`: sequence number 2. */
|
||||
const singleSegmentOctets = '0000004200000004000000000000000200010034363730313131333331310001003436373039373731333337000000000000000001000b48656c6c6f20776f726c64';
|
||||
const singleSegmentOctets = '0000004200000004000000000000000200010034363730313131333331310001003436373039373731333337000000000000000000000b48656c6c6f20776f726c64';
|
||||
|
||||
/** The same for `longMessage`: concatenation reference 1, sequence numbers 2 to 4. */
|
||||
const threeSegmentOctets = [
|
||||
'000000d600000004000000000000000200010034363730313131333331310001003436373039373731333337004000000000000001009f0500030103015365676d656e7473206f662061206c6f6e67206d6573736167652c20636f756e74656420696e206120757365722064617461206865616465722e205365676d656e7473206f662061206c6f6e67206d6573736167652c20636f756e74656420696e206120757365722064617461206865616465722e205365676d656e7473206f662061206c6f6e67206d6573736167652c20636f756e746564',
|
||||
'000000d600000004000000000000000300010034363730313131333331310001003436373039373731333337004000000000000001009f05000301030220696e206120757365722064617461206865616465722e205365676d656e7473206f662061206c6f6e67206d6573736167652c20636f756e74656420696e206120757365722064617461206865616465722e205365676d656e7473206f662061206c6f6e67206d6573736167652c20636f756e74656420696e206120757365722064617461206865616465722e205365676d656e7473206f66',
|
||||
'000000a80000000400000000000000040001003436373031313133333131000100343637303937373133333700400000000000000100710500030103032061206c6f6e67206d6573736167652c20636f756e74656420696e206120757365722064617461206865616465722e205365676d656e7473206f662061206c6f6e67206d6573736167652c20636f756e74656420696e206120757365722064617461206865616465722e20',
|
||||
'000000d600000004000000000000000200010034363730313131333331310001003436373039373731333337004000000000000000009f0500030103015365676d656e7473206f662061206c6f6e67206d6573736167652c20636f756e74656420696e206120757365722064617461206865616465722e205365676d656e7473206f662061206c6f6e67206d6573736167652c20636f756e74656420696e206120757365722064617461206865616465722e205365676d656e7473206f662061206c6f6e67206d6573736167652c20636f756e746564',
|
||||
'000000d600000004000000000000000300010034363730313131333331310001003436373039373731333337004000000000000000009f05000301030220696e206120757365722064617461206865616465722e205365676d656e7473206f662061206c6f6e67206d6573736167652c20636f756e74656420696e206120757365722064617461206865616465722e205365676d656e7473206f662061206c6f6e67206d6573736167652c20636f756e74656420696e206120757365722064617461206865616465722e205365676d656e7473206f66',
|
||||
'000000a80000000400000000000000040001003436373031313133333131000100343637303937373133333700400000000000000000710500030103032061206c6f6e67206d6573736167652c20636f756e74656420696e206120757365722064617461206865616465722e205365676d656e7473206f662061206c6f6e67206d6573736167652c20636f756e74656420696e206120757365722064617461206865616465722e20',
|
||||
];
|
||||
|
||||
type BoundPeer = {
|
||||
|
||||
+3
-2
@@ -130,7 +130,8 @@ describe('parsing real PDUs', () => {
|
||||
});
|
||||
|
||||
describe('encoding submit_sm', () => {
|
||||
test('produces the same bytes as 0.4.0 for a GSM message', () => {
|
||||
// 0.4.0 declared IA5 for the GSM octets it wrote; that one field is the whole difference.
|
||||
test('produces the 0.4.0 bytes for a GSM message, under the alphabet those octets are in', () => {
|
||||
const pdu = encode({
|
||||
cmdName: 'submit_sm',
|
||||
cmdStatus: 'ESME_ROK',
|
||||
@@ -144,7 +145,7 @@ describe('encoding submit_sm', () => {
|
||||
|
||||
assert.equal(
|
||||
pdu.toString('hex'),
|
||||
'0000004200000004000000000000000c00000034363730313131333331310000003436373039373731333337000000000000000001000b48656c6c6f20776f726c64',
|
||||
'0000004200000004000000000000000c00000034363730313131333331310000003436373039373731333337000000000000000000000b48656c6c6f20776f726c64',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ describe('an alphabet the caller named that cannot carry the message', () => {
|
||||
assert.equal((await session.sendSms({ from, message, to })).err, undefined, message);
|
||||
}
|
||||
|
||||
assert.deepEqual(sentAs(smsc.octets), [[0x01, 'Hello world'], [0x08, 'Åsa naïve'], [0x08, 'あいう']]);
|
||||
assert.deepEqual(sentAs(smsc.octets), [[0x00, 'Hello world'], [0x08, 'Åsa naïve'], [0x08, 'あいう']]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user