Validate the wire format against node-smpp and add CI

This commit is contained in:
2026-08-25 16:52:33 +02:00
parent fd2f98464f
commit 2e4699944b
10 changed files with 486 additions and 5 deletions
+284
View File
@@ -0,0 +1,284 @@
import assert from 'node:assert/strict';
import test, { describe } from 'node:test';
import reference from 'smpp';
import type { ReferenceSession } from 'smpp';
import type { Sms } from '../src/sms.ts';
import { client } from '../src/client.ts';
import { concatInfo } from '../src/udh.ts';
import { objToPdu, pduToObj } from '../src/pdu.ts';
import { server } from '../src/server.ts';
import { splitMessage } from '../src/message.ts';
/**
* Cross-checks against farhadi/node-smpp, an independent SMPP implementation. This is what backs
* the claim that the corrected framing is right rather than differently wrong.
*/
function ourBuffer(...args: Parameters<typeof objToPdu>): Buffer {
const { buffer, err } = objToPdu(...args);
assert.equal(err, undefined);
assert.ok(buffer);
return buffer;
}
function referenceParse(buffer: Buffer) {
const parsed = reference.PDU.fromBuffer(buffer);
assert.ok(parsed, 'the reference implementation could not parse our PDU');
return parsed;
}
describe('our encoder against the reference parser', () => {
test('a GSM submit_sm', () => {
const parsed = referenceParse(ourBuffer({
cmdName: 'submit_sm',
params: {
destination_addr: '46709771337',
short_message: 'Hello world',
source_addr: '46701113311',
},
seqNr: 12,
}));
assert.equal(parsed.command, 'submit_sm');
assert.equal(parsed.sequence_number, 12);
assert.equal(parsed.source_addr, '46701113311');
assert.equal(parsed.destination_addr, '46709771337');
assert.equal(String(parsed.short_message?.message), 'Hello world');
});
test('a UCS2 submit_sm, including a character whose low byte is zero', () => {
const parsed = referenceParse(ourBuffer({
cmdName: 'submit_sm',
params: {
destination_addr: '46709771337',
short_message: 'hej 一',
source_addr: '46701113311',
},
seqNr: 3,
}));
assert.equal(parsed.data_coding, 8);
assert.equal(String(parsed.short_message?.message), 'hej 一');
});
test('a bind_transceiver', () => {
const parsed = referenceParse(ourBuffer({
cmdName: 'bind_transceiver',
params: {
interface_version: 0x50,
password: 'bar',
system_id: 'foo',
system_type: 'smpp',
},
seqNr: 1,
}));
assert.equal(parsed.command, 'bind_transceiver');
assert.equal(parsed.system_id, 'foo');
assert.equal(parsed.password, 'bar');
assert.equal(parsed.interface_version, 0x50);
});
test('a deliver_sm carrying delivery receipt TLVs', () => {
const parsed = referenceParse(ourBuffer({
cmdName: 'deliver_sm',
params: {
destination_addr: '46701113311',
esm_class: 4,
short_message: 'id:abc123 sub:001 dlvrd:001 stat:DELIVRD err:000 text:',
source_addr: '46709771337',
},
seqNr: 77,
tlvs: {
message_state: { tagId: 0x0427, tagValue: 2 },
receipted_message_id: { tagId: 0x001E, tagValue: 'abc123' },
},
}));
assert.equal(parsed.command, 'deliver_sm');
assert.equal(parsed.message_state, 2);
assert.equal(parsed.receipted_message_id, 'abc123');
});
// The segment sizes and UDH are the wire change most worth an independent opinion.
test('every segment of a long message, with its concatenation header', () => {
const message = 'a'.repeat(400);
const segments = splitMessage(message, { reference: 0x2A });
assert.equal(segments.length, 3);
let rebuilt = '';
for (const [index, segment] of segments.entries()) {
const parsed = referenceParse(ourBuffer({
cmdName: 'submit_sm',
params: {
data_coding: 0,
destination_addr: '46709771337',
esm_class: 0x40,
short_message: segment,
sm_length: segment.length,
source_addr: '46701113311',
},
seqNr: index + 1,
}));
const body = String(parsed.short_message?.message);
assert.deepEqual(concatInfo(segment), { part: index + 1, reference: 0x2A, total: 3 });
// Both sides must agree on where the 6-octet UDH ends and the text begins.
assert.equal(body.length, segment.length - 6);
rebuilt += body;
}
assert.equal(rebuilt, message);
});
});
describe('the reference encoder against our parser', () => {
test('a submit_sm', () => {
const built = new reference.PDU('submit_sm', {
destination_addr: '46709771337',
sequence_number: 5,
short_message: 'Hello from the reference',
source_addr: '46701113311',
}).toBuffer();
const { err, pduObj } = pduToObj(built);
assert.equal(err, undefined);
assert.ok(pduObj);
assert.equal(pduObj.cmdName, 'submit_sm');
assert.equal(pduObj.seqNr, 5);
assert.equal(pduObj.params.source_addr, '46701113311');
assert.equal(pduObj.params.short_message, 'Hello from the reference');
});
test('a UCS2 submit_sm', () => {
const built = new reference.PDU('submit_sm', {
destination_addr: '46709771337',
sequence_number: 6,
short_message: 'تست 一',
source_addr: '46701113311',
}).toBuffer();
const { err, pduObj } = pduToObj(built);
assert.equal(err, undefined);
assert.ok(pduObj);
assert.equal(pduObj.params.short_message, 'تست 一');
});
test('a submit_sm_resp', () => {
const built = new reference.PDU('submit_sm_resp', {
message_id: 'ref-123',
sequence_number: 7,
}).toBuffer();
const { err, pduObj } = pduToObj(built);
assert.equal(err, undefined);
assert.ok(pduObj);
assert.equal(pduObj.cmdName, 'submit_sm_resp');
assert.equal(pduObj.params.message_id, 'ref-123');
});
test('a deliver_sm with TLVs', () => {
const built = new reference.PDU('deliver_sm', {
destination_addr: '46701113311',
esm_class: 4,
message_state: 2,
receipted_message_id: 'ref-456',
sequence_number: 8,
short_message: 'id:ref-456 stat:DELIVRD err:000',
source_addr: '46709771337',
}).toBuffer();
const { err, pduObj } = pduToObj(built);
assert.equal(err, undefined);
assert.ok(pduObj);
assert.equal(pduObj.tlvs.receipted_message_id?.tagValue, 'ref-456');
assert.equal(pduObj.tlvs.message_state?.tagValue, 2);
});
});
describe('a live session against the reference implementation', () => {
test('our client binds to a reference server and delivers an SMS', async () => {
const received: { from: string; message: string }[] = [];
const refServer = reference.createServer({}, (session: ReferenceSession) => {
session.on('bind_transceiver', pdu => {
session.bind_transceiver_resp({
sequence_number: pdu.sequence_number,
system_id: 'ref',
});
});
session.on('submit_sm', pdu => {
received.push({
from: String(pdu.source_addr),
message: String(pdu.short_message?.message),
});
session.submit_sm_resp({
message_id: 'ref-id',
sequence_number: pdu.sequence_number,
});
});
});
await new Promise<void>(resolve => { refServer.listen(0, () => { resolve(); }); });
const port = refServer.address()?.port ?? 0;
const { err, session } = await client({ port });
assert.equal(err, undefined);
assert.ok(session);
const sent = await session.sendSms({
from: 'MyBrand',
message: 'interop check',
to: '46709771337',
});
assert.equal(sent.err, undefined);
assert.deepEqual(sent.smsIds, ['ref-id']);
assert.deepEqual(received, [{ from: 'MyBrand', message: 'interop check' }]);
session.close();
await new Promise<void>(resolve => { refServer.close(() => { resolve(); }); });
});
test('a reference client binds to our server and delivers an SMS', async () => {
const { err: serverErr, server: smpp } = await server({ port: 0 });
assert.equal(serverErr, undefined);
assert.ok(smpp);
const incoming = new Promise<Sms>(resolve => {
smpp.on('session', session => session.on('sms', resolve));
});
const refSession = reference.connect({
url: `smpp://localhost:${String(smpp.port)}`,
});
await new Promise<void>(resolve => {
refSession.bind_transceiver({ password: 'bar', system_id: 'foo' }, () => { resolve(); });
});
refSession.submit_sm({
destination_addr: '46709771337',
short_message: 'from the reference client',
source_addr: '46701113311',
});
const sms = await incoming;
assert.equal(sms.from, '46701113311');
assert.equal(sms.message, 'from the reference client');
await sms.sendResp();
refSession.close();
await smpp.close();
});
});