Enforce bind direction, cap segments per message and pin the README examples
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test, { describe } from 'node:test';
|
||||
import type { Dlr } from '../src/dlr.ts';
|
||||
import type { Session } from '../src/session.ts';
|
||||
import type { Sms } from '../src/sms.ts';
|
||||
import type { SmppLog } from '../src/log.ts';
|
||||
import type { SmppServer } from '../src/server.ts';
|
||||
import type { TestContext } from 'node:test';
|
||||
import { client } from '../src/client.ts';
|
||||
import { server } from '../src/server.ts';
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** The README's examples listen on the documented default port, so one server runs at a time. */
|
||||
async function answeringServer(t: TestContext): Promise<SmppServer> {
|
||||
const { err, server: smpp } = await server();
|
||||
|
||||
assert.equal(err, undefined);
|
||||
assert.ok(smpp);
|
||||
t.after(() => smpp.close());
|
||||
|
||||
smpp.on('session', session => {
|
||||
session.on('sms', async sms => {
|
||||
await sms.sendResp();
|
||||
|
||||
if (sms.dlr) await sms.sendDlr();
|
||||
});
|
||||
});
|
||||
|
||||
return smpp;
|
||||
}
|
||||
|
||||
describe('README: Client', () => {
|
||||
test('the simplest possible client', async t => {
|
||||
await answeringServer(t);
|
||||
|
||||
const { err, session } = await client();
|
||||
if (err) throw err;
|
||||
|
||||
await session.sendSms({
|
||||
from: '46701113311',
|
||||
message: 'Hello world',
|
||||
to: '46709771337',
|
||||
});
|
||||
|
||||
await session.unbind();
|
||||
});
|
||||
|
||||
test('with connection parameters, a delivery report and logging', async t => {
|
||||
await answeringServer(t);
|
||||
|
||||
const log: SmppLog = {
|
||||
debug: () => undefined,
|
||||
error: () => undefined,
|
||||
info: () => undefined,
|
||||
verbose: () => undefined,
|
||||
warn: () => undefined,
|
||||
};
|
||||
|
||||
const { err, session } = await client({
|
||||
host: 'localhost',
|
||||
log,
|
||||
password: 'bar',
|
||||
port: 2775,
|
||||
username: 'foo',
|
||||
});
|
||||
if (err) throw err;
|
||||
|
||||
t.after(() => { session.close(); });
|
||||
|
||||
const reported = once<Dlr>(resolve => { session.on('dlr', resolve); });
|
||||
const { err: sendErr, smsIds } = await session.sendSms({
|
||||
dlr: true,
|
||||
from: '46701113311',
|
||||
message: '«baff»',
|
||||
to: '46709771337',
|
||||
});
|
||||
|
||||
assert.equal(sendErr, undefined);
|
||||
assert.equal(smsIds.length, 1);
|
||||
assert.equal((await reported).smsId, smsIds[0]);
|
||||
});
|
||||
|
||||
test('the documented sending options', async t => {
|
||||
const smpp = await answeringServer(t);
|
||||
const incoming = once<Sms>(resolve => {
|
||||
smpp.on('session', session => session.on('sms', resolve));
|
||||
});
|
||||
const { err, session } = await client();
|
||||
if (err) throw err;
|
||||
|
||||
t.after(() => { session.close(); });
|
||||
|
||||
const { signal } = new AbortController();
|
||||
const [sms, sent] = await Promise.all([
|
||||
incoming,
|
||||
session.sendSms({
|
||||
dlr: true,
|
||||
encoding: 'UCS2',
|
||||
flash: false,
|
||||
from: 'MyBrand',
|
||||
message: 'Hello world',
|
||||
scheduleDeliveryTime: new Date(Date.now() + 3600_000),
|
||||
to: '46709771337',
|
||||
validityPeriod: 3600,
|
||||
}, { signal }),
|
||||
]);
|
||||
|
||||
assert.equal(sent.err, undefined);
|
||||
assert.equal(sms.from, 'MyBrand');
|
||||
assert.equal(sms.message, 'Hello world');
|
||||
});
|
||||
|
||||
test('receiving an inbound message on a client session', async t => {
|
||||
const smpp = await answeringServer(t);
|
||||
const bound = once<Session>(resolve => { smpp.on('session', resolve); });
|
||||
const { err, session } = await client();
|
||||
if (err) throw err;
|
||||
|
||||
t.after(() => { session.close(); });
|
||||
|
||||
const incoming = once<Sms>(resolve => { session.on('sms', resolve); });
|
||||
const peer = await bound;
|
||||
|
||||
void peer.send({
|
||||
cmdName: 'deliver_sm',
|
||||
params: {
|
||||
destination_addr: '46709771337',
|
||||
short_message: 'inbound hello',
|
||||
source_addr: '46701113311',
|
||||
},
|
||||
});
|
||||
|
||||
const sms = await incoming;
|
||||
|
||||
await sms.sendResp();
|
||||
|
||||
assert.equal(sms.message, 'inbound hello');
|
||||
});
|
||||
});
|
||||
|
||||
describe('README: Server', () => {
|
||||
test('the simplest possible server', async t => {
|
||||
const { err, server: smpp } = await server();
|
||||
if (err) throw err;
|
||||
|
||||
t.after(() => smpp.close());
|
||||
|
||||
const received: string[] = [];
|
||||
|
||||
smpp.on('session', session => {
|
||||
session.on('sms', async sms => {
|
||||
received.push(sms.message);
|
||||
await sms.sendResp();
|
||||
});
|
||||
});
|
||||
|
||||
const { err: clientErr, session } = await client();
|
||||
|
||||
assert.equal(clientErr, undefined);
|
||||
assert.ok(session);
|
||||
|
||||
await session.sendSms({ from: '46701113311', message: 'Hello world', to: '46709771337' });
|
||||
await session.unbind();
|
||||
|
||||
assert.deepEqual(received, ['Hello world']);
|
||||
});
|
||||
|
||||
test('with authentication and delivery reports', async t => {
|
||||
const { err, server: smpp } = await server({
|
||||
authenticate: ({ password, systemId }) => {
|
||||
if (systemId !== 'foo' || password !== 'bar') return false;
|
||||
|
||||
return { userData: { userId: 123 } };
|
||||
},
|
||||
});
|
||||
if (err) throw err;
|
||||
|
||||
t.after(() => smpp.close());
|
||||
|
||||
smpp.on('session', session => {
|
||||
session.on('sms', async sms => {
|
||||
await sms.sendResp();
|
||||
|
||||
if (sms.dlr) {
|
||||
await sms.sendDlr();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
assert.equal(smpp.port, 2775);
|
||||
|
||||
const refused = await client({ password: 'wrong', username: 'foo' });
|
||||
|
||||
assert.ok(refused.err instanceof Error);
|
||||
|
||||
const { err: clientErr, session } = await client({ password: 'bar', username: 'foo' });
|
||||
|
||||
assert.equal(clientErr, undefined);
|
||||
assert.ok(session);
|
||||
|
||||
const reported = once<Dlr>(resolve => { session.on('dlr', resolve); });
|
||||
const sent = await session.sendSms({
|
||||
dlr: true,
|
||||
from: '46701113311',
|
||||
message: 'with a receipt',
|
||||
to: '46709771337',
|
||||
});
|
||||
|
||||
assert.equal(sent.err, undefined);
|
||||
assert.equal((await reported).statusMsg, 'DELIVERED');
|
||||
|
||||
await session.unbind();
|
||||
});
|
||||
});
|
||||
|
||||
describe('README: Errors', () => {
|
||||
test('a refused connection reports err instead of throwing', async () => {
|
||||
const { err, session } = await client({ port: 1 });
|
||||
|
||||
assert.ok(err instanceof Error);
|
||||
assert.equal(session, undefined);
|
||||
});
|
||||
});
|
||||
@@ -243,6 +243,31 @@ describe('sendSms()', () => {
|
||||
assert.ok(sent.err instanceof Error);
|
||||
assert.equal(attempts.length, 0);
|
||||
});
|
||||
|
||||
// Most handsets and SMSCs stop well short of the 255 a UDH can number.
|
||||
test('refuses a message needing more segments than the caller allows', async () => {
|
||||
const attempts: PduObjectInput[] = [];
|
||||
const deps = {
|
||||
log: silentLog,
|
||||
reference: 3,
|
||||
send: (input: PduObjectInput) => {
|
||||
attempts.push(input);
|
||||
|
||||
return Promise.resolve({ pduObj: submitResp(attempts.length, `landed-${String(attempts.length)}`) });
|
||||
},
|
||||
};
|
||||
const message = 'a'.repeat(153 * 4);
|
||||
|
||||
const refused = await submitSms(deps, { from: '46701113311', maxSegments: 3, message, to: '46709771337' });
|
||||
|
||||
assert.ok(refused.err instanceof Error);
|
||||
assert.equal(attempts.length, 0);
|
||||
|
||||
const sent = await submitSms(deps, { from: '46701113311', maxSegments: 4, message, to: '46709771337' });
|
||||
|
||||
assert.equal(sent.err, undefined);
|
||||
assert.equal(attempts.length, 4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reconnect', () => {
|
||||
|
||||
@@ -424,6 +424,93 @@ describe('bind', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('bind direction', () => {
|
||||
// A receiver-bound ESME sends no submit_sm and a transmitter-bound one is sent no deliver_sm.
|
||||
test('refuses a submit_sm from a peer that bound as a receiver', async () => {
|
||||
const smpp = await startServer();
|
||||
const { session } = await connect(smpp, { bindType: 'receiver' });
|
||||
|
||||
assert.ok(session);
|
||||
|
||||
const sent = await session.send({
|
||||
cmdName: 'submit_sm',
|
||||
params: { destination_addr: '46709771337', short_message: 'nope', source_addr: '46701113311' },
|
||||
});
|
||||
|
||||
assert.ok(sent.pduObj);
|
||||
assert.equal(sent.pduObj.cmdStatus, 'ESME_RINVBNDSTS');
|
||||
|
||||
session.close();
|
||||
await smpp.close();
|
||||
});
|
||||
|
||||
test('refuses sendSms() on a receiver-bound session before it reaches the wire', async () => {
|
||||
const smpp = await startServer();
|
||||
const arrived: Sms[] = [];
|
||||
|
||||
smpp.on('session', peer => peer.on('sms', sms => arrived.push(sms)));
|
||||
|
||||
const { session } = await connect(smpp, { bindType: 'receiver' });
|
||||
|
||||
assert.ok(session);
|
||||
|
||||
const sent = await session.sendSms({ from: '46701113311', message: 'nope', to: '46709771337' });
|
||||
|
||||
assert.ok(sent.err instanceof Error);
|
||||
assert.match(sent.err.message, /receiver-bound/);
|
||||
assert.deepEqual(sent.smsIds, []);
|
||||
assert.equal(arrived.length, 0);
|
||||
|
||||
session.close();
|
||||
await smpp.close();
|
||||
});
|
||||
|
||||
test('refuses a deliver_sm sent to a peer that bound as a transmitter', async () => {
|
||||
const smpp = await startServer();
|
||||
const bound = once<Session>(resolve => { smpp.on('session', resolve); });
|
||||
const { session } = await connect(smpp, { bindType: 'transmitter' });
|
||||
|
||||
assert.ok(session);
|
||||
|
||||
const peer = await bound;
|
||||
const sent = await peer.send({
|
||||
cmdName: 'deliver_sm',
|
||||
params: { destination_addr: '46709771337', short_message: 'nope', source_addr: '46701113311' },
|
||||
});
|
||||
|
||||
assert.ok(sent.pduObj);
|
||||
assert.equal(sent.pduObj.cmdStatus, 'ESME_RINVBNDSTS');
|
||||
|
||||
session.close();
|
||||
await smpp.close();
|
||||
});
|
||||
|
||||
test('refuses sendDlr() to a transmitter-bound peer before it reaches the wire', async () => {
|
||||
const smpp = await startServer();
|
||||
const incoming = once<Sms>(resolve => {
|
||||
smpp.on('session', peer => peer.on('sms', resolve));
|
||||
});
|
||||
const { session } = await connect(smpp, { bindType: 'transmitter' });
|
||||
|
||||
assert.ok(session);
|
||||
|
||||
const [sms] = await Promise.all([
|
||||
incoming,
|
||||
session.sendSms({ dlr: true, from: '46701113311', message: 'one way', to: '46709771337' }),
|
||||
]);
|
||||
|
||||
await sms.sendResp();
|
||||
|
||||
const report = await sms.sendDlr();
|
||||
|
||||
assert.ok(report.err instanceof Error);
|
||||
assert.match(report.err.message, /transmitter-bound/);
|
||||
|
||||
session.close();
|
||||
await smpp.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sending', () => {
|
||||
test('delivers a simple SMS with the sender TON derived from the address', async () => {
|
||||
const smpp = await startServer();
|
||||
@@ -546,6 +633,40 @@ describe('sending', () => {
|
||||
session.close();
|
||||
await smpp.close();
|
||||
});
|
||||
|
||||
test('puts the address TON and NPI the caller chose on the wire', async () => {
|
||||
const smpp = await startServer();
|
||||
const incoming = once<Sms>(resolve => {
|
||||
smpp.on('session', peer => peer.on('sms', resolve));
|
||||
});
|
||||
const { session } = await connect(smpp);
|
||||
|
||||
assert.ok(session);
|
||||
|
||||
const [sms] = await Promise.all([
|
||||
incoming,
|
||||
session.sendSms({
|
||||
destinationAddrNpi: consts.NPI.ISDN,
|
||||
destinationAddrTon: consts.TON.NATIONAL,
|
||||
from: '46701113311',
|
||||
message: 'addressed by hand',
|
||||
sourceAddrNpi: consts.NPI.PRIVATE,
|
||||
sourceAddrTon: consts.TON.ABBREVIATED,
|
||||
to: '46709771337',
|
||||
}),
|
||||
]);
|
||||
|
||||
const params = sms.pduObjs[0]?.params;
|
||||
|
||||
assert.ok(params);
|
||||
assert.equal(params.dest_addr_npi, consts.NPI.ISDN);
|
||||
assert.equal(params.dest_addr_ton, consts.TON.NATIONAL);
|
||||
assert.equal(params.source_addr_npi, consts.NPI.PRIVATE);
|
||||
assert.equal(params.source_addr_ton, consts.TON.ABBREVIATED);
|
||||
|
||||
session.close();
|
||||
await smpp.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiving', () => {
|
||||
|
||||
Reference in New Issue
Block a user