Close the stability findings and settle the sendResp, sendSms and unbind contracts

This commit is contained in:
2026-08-27 00:17:48 +02:00
parent 435fa42708
commit 5af75a3c57
23 changed files with 1262 additions and 202 deletions
+5
View File
@@ -73,6 +73,11 @@ describe('splitMessage()', () => {
assert.deepEqual(segments[1]?.subarray(0, 6), Buffer.from([0x05, 0x00, 0x03, 0x2A, 2, 2]));
});
test('produces no segments at all for a message no UDH can number', () => {
assert.equal(splitMessage('a'.repeat(153 * 255), { reference: 1 }).length, 255);
assert.equal(splitMessage('a'.repeat(153 * 255 + 1), { reference: 1 }).length, 0);
});
test('splits on characters, never inside an escape sequence', () => {
const segments = splitMessage('€'.repeat(100), { reference: 1 });
const rejoined = segments
+51
View File
@@ -157,6 +157,23 @@ describe('encoding submit_sm', () => {
assert.equal(pduObj.params.short_message.toString('hex'), '05000301010168656a2076c3a4726c64656e');
});
test('derives sm_length from a Buffer short_message the caller gave no length for', () => {
const message = Buffer.concat([Buffer.from('050003010201', 'hex'), Buffer.from('hej')]);
const pduObj = decode(encode({
cmdName: 'submit_sm',
params: {
destination_addr: '46709771337',
esm_class: 0x40,
short_message: message,
source_addr: '46701113311',
},
seqNr: 12,
}));
assert.equal(pduObj.params.sm_length, message.length);
assert.deepEqual(pduObj.params.short_message, message);
});
test('accepts a number for a C-string parameter', () => {
const pduObj = decode(encode({
cmdName: 'submit_sm_resp',
@@ -185,6 +202,40 @@ describe('encoding submit_sm', () => {
});
});
describe('encoding submit_multi', () => {
const dest = { dest_addr_npi: 1, dest_addr_ton: 1, destination_addr: '46709771337' };
test('reports a value the destination structures cannot hold instead of throwing', () => {
const badTon = objToPdu({
cmdName: 'submit_multi',
params: {
dest_address: [{ ...dest, dest_addr_ton: 999 }],
short_message: 'hi',
source_addr: '46701113311',
},
});
const tooMany = objToPdu({
cmdName: 'submit_multi',
params: {
dest_address: Array.from({ length: 300 }, () => dest),
short_message: 'hi',
source_addr: '46701113311',
},
});
const badStatus = objToPdu({
cmdName: 'submit_multi_resp',
params: {
message_id: '01a03ff4-737f-7c01-91db-cb14aa779bcf',
unsuccess_sme: [{ ...dest, error_status_code: 0x1FFFFFFFF }],
},
});
assert.ok(badTon.err instanceof Error);
assert.ok(tooMany.err instanceof Error);
assert.ok(badStatus.err instanceof Error);
});
});
describe('TLVs', () => {
test('extracts TLVs from a delivery receipt captured from an SMSC', () => {
const pduObj = decode(Buffer.from(
+141 -6
View File
@@ -1,14 +1,19 @@
import assert from 'node:assert/strict';
import net from 'node:net';
import test, { describe } from 'node:test';
import type { ErrorName } from '../src/defs/errors.ts';
import type { MessageDlr } from '../src/session.ts';
import type { PduObject } from '../src/pdu.ts';
import type { PduObject, PduObjectInput } from '../src/pdu.ts';
import type { Result } from '../src/result.ts';
import type { SendSmsResult } from '../src/send-sms.ts';
import type { Sms } from '../src/sms.ts';
import type { SmppServer } from '../src/server.ts';
import { Reassembler } from '../src/reassembly.ts';
import { Reassembler, decodeSegments } from '../src/reassembly.ts';
import { client } from '../src/client.ts';
import { errors } from '../src/defs/errors.ts';
import { server } from '../src/server.ts';
import { silentLog } from '../src/log.ts';
import { submitSms } from '../src/send-sms.ts';
async function startServer(options: Parameters<typeof server>[0] = {}): Promise<SmppServer> {
const { err, server: smpp } = await server({ ...options, port: 0 });
@@ -41,8 +46,7 @@ describe('merged delivery reports', () => {
const [sms] = await Promise.all([
incoming.then(async received => {
received.smsId = 'merge-me';
await received.sendResp();
await received.sendResp({ smsId: 'merge-me' });
return received;
}),
@@ -80,8 +84,7 @@ describe('merged delivery reports', () => {
const [sms] = await Promise.all([
incoming.then(async received => {
received.smsId = 'partly-failed';
await received.sendResp();
await received.sendResp({ smsId: 'partly-failed' });
return received;
}),
@@ -105,6 +108,100 @@ describe('merged delivery reports', () => {
});
});
describe('sendSms()', () => {
function submitResp(seqNr: number, messageId: string, status: ErrorName = 'ESME_ROK'): PduObject {
return {
cmdId: 0x80000004,
cmdLength: 0,
cmdName: 'submit_sm_resp',
cmdStatus: status,
cmdStatusId: errors[status],
params: { message_id: messageId },
seqNr,
tlvs: {},
};
}
/** Three segments, each answered by whatever the caller decides for that part. */
function sendSegments(
answer: (part: number) => Result<{ pduObj: PduObject }>,
): Promise<SendSmsResult> {
let part = 0;
return submitSms(
{
log: silentLog,
reference: 7,
send: () => {
part++;
return Promise.resolve(answer(part));
},
},
{ from: '46701113311', message: 'x'.repeat(400), to: '46709771337' },
);
}
test('reports a submit_sm the peer refused instead of an empty message id', async () => {
const smpp = await startServer();
const incoming = once<Sms>(resolve => {
smpp.on('session', session => session.on('sms', resolve));
});
const { session } = await client({ port: smpp.port });
assert.ok(session);
const [, sent] = await Promise.all([
incoming.then(received => received.sendResp({ status: 'ESME_RMSGQFUL' })),
session.sendSms({ from: '46701113311', message: 'the queue is full', to: '46709771337' }),
]);
assert.ok(sent.err instanceof Error);
assert.match(sent.err.message, /ESME_RMSGQFUL/);
session.close();
await smpp.close();
});
// A retry that repeats the segments the SMSC already took bills the recipient twice.
test('hands back the ids that landed when a segment fails', async () => {
const refused = await sendSegments(part => part === 2
? { pduObj: submitResp(part, '', 'ESME_RMSGQFUL') }
: { pduObj: submitResp(part, `landed-${String(part)}`) });
assert.ok(refused.err instanceof Error);
assert.match(refused.err.message, /ESME_RMSGQFUL/);
assert.equal(refused.pduObjs.length, 2);
assert.deepEqual(refused.smsIds, ['landed-1', 'landed-3']);
const unanswered = await sendSegments(part => part === 2
? { err: new Error('No response to seqNr 2') }
: { pduObj: submitResp(part, `landed-${String(part)}`) });
assert.ok(unanswered.err instanceof Error);
assert.deepEqual(unanswered.smsIds, ['landed-1', 'landed-3']);
});
test('refuses a message needing more segments than a UDH can number', async () => {
const attempts: PduObjectInput[] = [];
const sent = await submitSms(
{
log: silentLog,
reference: 1,
send: input => {
attempts.push(input);
return Promise.resolve({ err: new Error('nothing should reach the wire') });
},
},
{ from: '46701113311', message: 'a'.repeat(153 * 256), to: '46709771337' },
);
assert.ok(sent.err instanceof Error);
assert.equal(attempts.length, 0);
});
});
describe('reconnect', () => {
test('re-binds after the connection drops, keeping the same session object', async () => {
const smpp = await startServer();
@@ -231,6 +328,44 @@ describe('reassembly bounds', () => {
reassembler.clear();
});
test('drops the oldest incomplete message once the retained octets exceed the cap', () => {
const reassembler = new Reassembler({
log: silentLog,
max: 10,
maxOctets: 30,
now: () => 0,
timeout: 60_000,
});
for (const reference of [1, 2, 3]) {
assert.equal(collect(reassembler, reference, 1, 2), undefined);
}
assert.equal(reassembler.size, 2);
assert.equal(collect(reassembler, 1, 2, 2), undefined);
reassembler.clear();
});
// A retained subarray keeps its whole framed PDU alive, up to maxPduLength per segment.
test('copies a segment out of the buffer it arrived in', () => {
const reassembler = new Reassembler({ log: silentLog, max: 10, now: () => 0, timeout: 60_000 });
const framed = Buffer.alloc(1024);
const first = segment(6, 1, 2);
Buffer.concat([Buffer.from([0x05, 0x00, 0x03, 6, 2, 1]), Buffer.from('fragment')]).copy(framed);
first.params.short_message = framed.subarray(0, 14);
assert.equal(reassembler.collect(first, { part: 1, reference: 6, total: 2 }), undefined);
framed.fill(0x00);
const whole = collect(reassembler, 6, 2, 2);
assert.ok(whole);
assert.equal(decodeSegments(whole), 'fragmentfragment');
});
test('expires an incomplete message once its timeout has passed', () => {
let now = 0;
const reassembler = new Reassembler({ log: silentLog, max: 10, now: () => now, timeout: 60 });
+458 -10
View File
@@ -1,15 +1,19 @@
import assert from 'node:assert/strict';
import net from 'node:net';
import test, { describe } from 'node:test';
import type { Dlr } from '../src/dlr.ts';
import type { PduObject, PduObjectInput } from '../src/pdu.ts';
import type { Session } from '../src/session.ts';
import type { Sms } from '../src/sms.ts';
import type { SmppServer } from '../src/server.ts';
import { DlrMerger } from '../src/dlr-merger.ts';
import { PduFramer } from '../src/pdu-framer.ts';
import { ReconnectLoop } from '../src/reconnect-loop.ts';
import { Session, bindCommands } from '../src/session.ts';
import { client } from '../src/client.ts';
import { isCommand, objToPdu, pduToObj } from '../src/pdu.ts';
import { isCommand, objToPdu, pduReturn, pduToObj } from '../src/pdu.ts';
import { paramText } from '../src/defs/types.ts';
import { server } from '../src/server.ts';
import { silentLog } from '../src/log.ts';
async function startServer(options: Parameters<typeof server>[0] = {}): Promise<SmppServer> {
const { err, server: smpp } = await server({ ...options, port: 0 });
@@ -28,6 +32,86 @@ function once<T>(register: (resolve: (value: T) => void) => void): Promise<T> {
return new Promise<T>(resolve => { register(resolve); });
}
function delay(ms: number): Promise<void> {
return new Promise(resolve => { setTimeout(resolve, ms); });
}
/** Polls until the condition holds; false means it never did within the budget. */
async function waitFor(condition: () => boolean, budget = 2000): Promise<boolean> {
const deadline = Date.now() + budget;
while (!condition()) {
if (Date.now() > deadline) return false;
await delay(5);
}
return true;
}
function raceWithin<T>(ms: number, promise: Promise<T>): Promise<T | false> {
return Promise.race([promise, delay(ms).then((): false => false)]);
}
type Peer = { close: () => Promise<void>; port: number };
/** Answers binds and nothing else, which is what a link the peer has stopped serving looks like. */
async function bindOnlyPeer(options: { dropOn?: string } = {}): Promise<Peer> {
const sockets: net.Socket[] = [];
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);
if (pduObj && pduObj.cmdName === options.dropOn) {
sock.destroy();
return;
}
if (!pduObj || !bindCommands.includes(pduObj.cmdName)) continue;
const { buffer } = pduReturn(pduObj, 'ESME_ROK', { system_id: 'silent' });
if (buffer) sock.write(buffer);
}
});
});
await new Promise<void>(resolve => { listener.listen(0, resolve); });
const address = listener.address();
return {
close: async () => {
for (const sock of sockets) {
sock.destroy();
}
await new Promise<void>(resolve => { listener.close(() => { resolve(); }); });
},
port: typeof address === 'object' && address !== null ? address.port : 0,
};
}
function enquireLink(seqNr: number): PduObject {
return {
cmdId: 0x00000015,
cmdLength: 16,
cmdName: 'enquire_link',
cmdStatus: 'ESME_ROK',
cmdStatusId: 0,
params: {},
seqNr,
tlvs: {},
};
}
type RawPeer = {
close: () => void;
/** The next PDU the server sends, queued so none is missed between reads. */
@@ -110,6 +194,42 @@ describe('bind', () => {
await smpp.close();
});
// Plenty of SMSCs drop the connection on unbind instead of answering it.
test('takes a close that follows our unbind as a clean unbind', async () => {
const peer = await bindOnlyPeer({ dropOn: 'unbind' });
const { session } = await client({ port: peer.port, responseTimeout: 2000 });
assert.ok(session);
assert.deepEqual(await session.unbind(), {});
await peer.close();
});
test('still reports a close that lands on another in-flight request', async () => {
const peer = await bindOnlyPeer({ dropOn: 'enquire_link' });
const { session } = await client({ port: peer.port, responseTimeout: 2000 });
assert.ok(session);
const sent = await session.send({ cmdName: 'enquire_link' });
assert.ok(sent.err instanceof Error);
assert.equal(sent.err.message, 'Session closed before a response arrived');
session.close();
await peer.close();
});
test('reports an unbind the peer left unanswered on a link that stays up', async () => {
const peer = await bindOnlyPeer();
const { session } = await client({ port: peer.port, responseTimeout: 150 });
assert.ok(session);
assert.ok((await session.unbind()).err instanceof Error);
await peer.close();
});
test('reports the resolved port when 0 was requested', async () => {
const smpp = await startServer();
@@ -283,8 +403,10 @@ describe('sending', () => {
const [sms, sent] = await Promise.all([
incoming.then(async received => {
received.smsId = 'fixed-id';
await received.sendResp();
const refused = await received.sendResp({ smsId: '' });
assert.ok(refused.err instanceof Error);
await received.sendResp({ smsId: 'fixed-id' });
return received;
}),
@@ -295,6 +417,7 @@ describe('sending', () => {
assert.equal(sms.to, '46709771337');
assert.equal(sms.message, 'hello world');
assert.equal(sms.dlr, false);
assert.equal(sms.smsId, 'fixed-id');
assert.equal(sent.err, undefined);
assert.deepEqual(sent.smsIds, ['fixed-id']);
@@ -321,8 +444,7 @@ describe('sending', () => {
const [sms, sent] = await Promise.all([
incoming.then(async received => {
received.smsId = 'long-id';
await received.sendResp();
await received.sendResp({ smsId: 'long-id' });
return received;
}),
@@ -332,6 +454,7 @@ describe('sending', () => {
assert.equal(sms.message, message);
assert.ok(sms.pduObjs.length > 1);
assert.equal(sent.err, undefined);
assert.equal(sent.pduObjs.length, 4);
assert.deepEqual(sent.smsIds, ['long-id-1', 'long-id-2', 'long-id-3', 'long-id-4']);
session.close();
@@ -358,6 +481,7 @@ describe('sending', () => {
]);
assert.equal(sms.message, message);
assert.match(sms.smsId, /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
session.close();
await smpp.close();
@@ -407,8 +531,7 @@ describe('delivery reports', () => {
const [sms] = await Promise.all([
incoming.then(async received => {
received.smsId = 'dlr-id';
await received.sendResp();
await received.sendResp({ smsId: 'dlr-id' });
return received;
}),
@@ -449,8 +572,7 @@ describe('delivery reports', () => {
const [sms] = await Promise.all([
incoming.then(async received => {
received.smsId = 'fail-id';
await received.sendResp();
await received.sendResp({ smsId: 'fail-id' });
return received;
}),
@@ -506,6 +628,39 @@ describe('delivery reports', () => {
peer.close();
await smpp.close();
});
test('merges nothing for a message that asked for no receipt', async () => {
const smpp = await startServer();
const incoming = once<Sms>(resolve => {
smpp.on('session', session => session.on('sms', resolve));
});
const { session } = await connect(smpp);
assert.ok(session);
const perSegment: string[] = [];
let merged = 0;
session.on('dlr', dlr => perSegment.push(dlr.smsId));
session.on('messageDlr', () => { merged++; });
const [sms] = await Promise.all([
incoming.then(async received => {
await received.sendResp({ smsId: 'unrequested' });
return received;
}),
session.sendSms({ from: '46701113311', message: 'x'.repeat(400), to: '46709771337' }),
]);
await sms.sendDlr();
assert.deepEqual(perSegment, ['unrequested-1', 'unrequested-2', 'unrequested-3']);
assert.equal(merged, 0);
session.close();
await smpp.close();
});
});
describe('a session captured from Kannel', () => {
@@ -632,4 +787,297 @@ describe('robustness', () => {
session.close();
await smpp.close();
});
test('reports a response it could not send', async () => {
const sock = new net.Socket();
sock.destroy();
const session = new Session({ sock });
const failed = once<Error>(resolve => { session.on('sessionError', resolve); });
const sent = await session.sendReturn(enquireLink(7));
const reported = await raceWithin(500, failed);
assert.ok(sent.err instanceof Error);
assert.ok(reported instanceof Error, 'a response that never reached the wire should be reported');
assert.equal(reported.message, sent.err.message);
session.close();
});
test('ignores events from the socket it left behind on a reconnect', async () => {
const smpp = await startServer();
smpp.on('session', bound => {
bound.on('sms', sms => { void sms.sendResp(); });
});
const { session } = await connect(smpp, { reconnect: { maxDelay: 50, minDelay: 10 } });
assert.ok(session);
const reconnected = once<true>(resolve => { session.on('reconnected', () => { resolve(true); }); });
const dead = session.sock;
for (const serverSession of smpp.sessions) {
serverSession.close();
}
await reconnected;
let closes = 0;
session.on('close', () => { closes++; });
dead.emit('close');
const sent = await session.sendSms({
from: '46701113311',
message: 'still up',
to: '46709771337',
});
assert.equal(closes, 0);
assert.equal(sent.err, undefined);
session.close();
await smpp.close();
});
test('closes the session when the signal aborts after the bind', async () => {
const smpp = await startServer();
const controller = new AbortController();
const { err, session } = await connect(smpp, { signal: controller.signal });
assert.equal(err, undefined);
assert.ok(session);
const closed = once<true>(resolve => { session.on('close', () => { resolve(true); }); });
controller.abort();
assert.ok(await closed);
await smpp.close();
});
});
describe('application hooks that throw', () => {
test('turns a throwing authenticate into a session error', async () => {
const smpp = await startServer({
authenticate: () => { throw new Error('authenticate exploded'); },
});
const failed = once<Error>(resolve => {
smpp.on('session', session => { session.on('sessionError', resolve); });
});
const { err } = await connect(smpp, { responseTimeout: 200 });
const reported = await raceWithin(500, failed);
assert.ok(err instanceof Error);
assert.ok(reported instanceof Error, 'a throwing authenticate should reach the session');
assert.equal(reported.message, 'authenticate exploded');
await smpp.close();
});
test('turns a throwing sms listener into a session error', async () => {
const smpp = await startServer();
const failed = once<Error>(resolve => {
smpp.on('session', session => {
session.on('sessionError', resolve);
session.on('sms', () => { throw new Error('listener exploded'); });
});
});
const { session } = await connect(smpp, { responseTimeout: 200 });
assert.ok(session);
const sent = await session.sendSms({
from: '46701113311',
message: 'blows up the listener',
to: '46709771337',
});
const reported = await raceWithin(500, failed);
assert.ok(sent.err instanceof Error);
assert.ok(reported instanceof Error, 'a throwing sms listener should reach the session');
assert.equal(reported.message, 'listener exploded');
session.close();
await smpp.close();
});
test('keeps the reconnect loop alive when connect throws', async () => {
let attempts = 0;
const loop = new ReconnectLoop({
connect: () => {
attempts++;
throw new Error('connect exploded');
},
log: silentLog,
maxDelay: 10,
minDelay: 1,
onConnected: () => Promise.resolve({}),
});
loop.schedule();
const retried = await waitFor(() => attempts >= 2);
loop.stop();
assert.ok(retried, 'a throwing connect should be retried, not left for the process to die on');
});
test('starts only one reconnect attempt at a time', async () => {
let attempts = 0;
let finish: (() => void) | undefined;
const loop = new ReconnectLoop({
connect: () => {
attempts++;
return new Promise(resolve => {
finish = () => { resolve({ err: new Error('no socket') }); };
});
},
log: silentLog,
maxDelay: 5,
minDelay: 1,
onConnected: () => Promise.resolve({}),
});
loop.schedule();
assert.ok(await waitFor(() => attempts === 1));
// A second drop landing while the first attempt is still inside connect().
loop.schedule();
await delay(30);
assert.equal(attempts, 1);
loop.stop();
finish?.();
});
});
describe('link timers', () => {
test('closes a client link the peer has stopped answering', async () => {
const peer = await bindOnlyPeer();
const { err, session } = await client({ enquireLinkInterval: 50, port: peer.port });
assert.equal(err, undefined);
assert.ok(session);
const closed = once<true>(resolve => { session.on('close', () => { resolve(true); }); });
assert.ok(
await raceWithin(1000, closed),
'a peer that answers nothing should time the link out',
);
session.close();
await peer.close();
});
test('reconnects a link that timed out', async () => {
const peer = await bindOnlyPeer();
const { session } = await client({
enquireLinkInterval: 40,
port: peer.port,
reconnect: { maxDelay: 20, minDelay: 10 },
});
assert.ok(session);
const back = once<true>(resolve => { session.on('reconnected', () => { resolve(true); }); });
assert.ok(await raceWithin(2000, back), 'a link that timed out should be reconnected');
session.close();
await peer.close();
});
});
describe('merged delivery report bounds', () => {
function receipt(smsId: string): Dlr {
return {
doneDate: undefined,
errorCode: undefined,
receipt: undefined,
smsId,
statusId: 2,
statusMsg: 'DELIVERED',
};
}
function merger(options: { max?: number; now?: () => number } = {}): DlrMerger {
return new DlrMerger({
log: silentLog,
max: options.max ?? 10,
now: options.now ?? (() => 0),
timeout: 60,
});
}
test('merges the receipts of one message and forgets the group', () => {
const dlrMerger = merger();
dlrMerger.expect(['whole-1', 'whole-2']);
assert.equal(dlrMerger.collect(receipt('whole-1')), undefined);
const merged = dlrMerger.collect(receipt('whole-2'));
assert.ok(merged);
assert.equal(merged.smsId, 'whole');
assert.equal(merged.segments.length, 2);
assert.equal(dlrMerger.size, 0);
});
// Every multipart send registered a group, and only a complete set of receipts ever removed it.
test('drops the oldest group once the cap is reached', () => {
const dlrMerger = merger({ max: 2 });
for (const base of ['first', 'second', 'third']) {
dlrMerger.expect([`${base}-1`, `${base}-2`]);
}
assert.equal(dlrMerger.size, 2);
assert.equal(dlrMerger.collect(receipt('first-1')), undefined);
assert.equal(dlrMerger.collect(receipt('first-2')), undefined);
dlrMerger.clear();
assert.equal(dlrMerger.size, 0);
});
test('expires a group whose receipts never all arrived', () => {
let now = 0;
const dlrMerger = merger({ now: () => now });
dlrMerger.expect(['late-1', 'late-2']);
now = 61;
assert.equal(dlrMerger.collect(receipt('late-1')), undefined);
assert.equal(dlrMerger.size, 0);
});
});
describe('option validation', () => {
test('refuses an interface version that cannot go on the wire', async () => {
const { err, server: smpp } = await server({ interfaceVersion: 0x100, port: 0 });
if (smpp) await smpp.close();
assert.ok(err instanceof Error);
});
test('returns an error rather than rejecting on an impossible port', async () => {
const listening = await server({ port: 70_000 });
assert.ok(listening.err instanceof Error);
const connected = await client({ port: 70_000 });
assert.ok(connected.err instanceof Error);
});
});
+22 -16
View File
@@ -142,8 +142,7 @@ describe('tls', () => {
const [sms, sent] = await Promise.all([
incoming.then(async received => {
received.smsId = 'tls-id';
await received.sendResp();
await received.sendResp({ smsId: 'tls-id' });
return received;
}),
@@ -193,22 +192,29 @@ describe('tls', () => {
});
test('logs a handshake the server turned away', async () => {
const warned = once<string>(resolve => {
const log = new Log({ logLevel: 'warn', stderr: resolve, stdout: resolve });
void server({ log, port: 0, tls: { cert: certificate.cert, key: certificate.key } })
.then(({ server: smpp }) => {
assert.ok(smpp);
const sock = net.connect({ port: smpp.port }, () => {
sock.end('not a client hello');
});
sock.on('close', () => { void smpp.close(); });
sock.resume();
});
let onWarning: ((message: string) => void) | undefined;
const warned = once<string>(resolve => { onWarning = resolve; });
const log = new Log({
logLevel: 'warn',
stderr: message => onWarning?.(message),
stdout: message => onWarning?.(message),
});
const { err, server: smpp } = await server({
log,
port: 0,
tls: { cert: certificate.cert, key: certificate.key },
});
assert.equal(err, undefined);
assert.ok(smpp);
const sock = net.connect({ port: smpp.port }, () => { sock.end('not a client hello'); });
sock.resume();
assert.match(await warned, /client handshake failed/);
sock.destroy();
await smpp.close();
});
});
+20
View File
@@ -155,6 +155,14 @@ describe('dest_address_array', () => {
assert.deepEqual(target, encoded);
});
test('refuses a field value the wire cannot hold instead of throwing', () => {
const badTon: DestAddress[] = [{ dest_addr_npi: 0, dest_addr_ton: 999, destination_addr: '123' }];
const tooMany: DestAddress[] = Array.from({ length: 300 }, () => ({ dl_name: 'a' }));
assert.ok(types.dest_address_array.write(badTon, Buffer.alloc(8), 0).err instanceof Error);
assert.ok(types.dest_address_array.write(tooMany, Buffer.alloc(901), 0).err instanceof Error);
});
});
describe('unsuccess_sme_array', () => {
@@ -186,6 +194,18 @@ describe('unsuccess_sme_array', () => {
assert.deepEqual(target, encoded);
});
test('refuses a field value the wire cannot hold instead of throwing', () => {
const badStatus: UnsuccessSme[] = [
{ dest_addr_npi: 0, dest_addr_ton: 0, destination_addr: 'abc', error_status_code: 0x1FFFFFFFF },
];
const badTon: UnsuccessSme[] = [
{ dest_addr_npi: 0, dest_addr_ton: 999, destination_addr: 'abc', error_status_code: 0 },
];
assert.ok(types.unsuccess_sme_array.write(badStatus, Buffer.alloc(11), 0).err instanceof Error);
assert.ok(types.unsuccess_sme_array.write(badTon, Buffer.alloc(11), 0).err instanceof Error);
});
});
describe('bounds checking', () => {