Give server() consumers the onRequest hook the docs promise (#86)
* Regression tests for an onRequest hook on server() * Compose the application's onRequest hook into server() past bind * Document the server's onRequest hook and record the composition decision * Pin that a re-bind and a pre-bind unbind never reach the hook, and that a non-function hook is refused at startup * Keep every bind and all pre-bind traffic out of the application's hook * Make the hook's published promises exact, and record the src layout deferral * Trim the record to what the tests do not already forbid * Pin that nothing answers a request whose hook failed * Answer nothing for a request whose hook failed, on both surfaces alike * State the hook's failure policy as it now behaves * Name the request a failed handler left unanswered, and say what a wedged hook reports
This commit is contained in:
+228
-1
@@ -4,8 +4,8 @@ import test, { describe } from 'node:test';
|
||||
import type { Dlr } from '../src/dlr.ts';
|
||||
import type { PduObject, PduObjectInput } from '../src/pdu.ts';
|
||||
import type { Sms } from '../src/sms.ts';
|
||||
import type { ServerOptions, SmppServer } from '../src/server.ts';
|
||||
import type { SmppLog } from '../src/log.ts';
|
||||
import type { SmppServer } from '../src/server.ts';
|
||||
import type { TestContext } from 'node:test';
|
||||
import type { VoidResult } from '../src/result.ts';
|
||||
import { DlrMerger } from '../src/dlr-merger.ts';
|
||||
@@ -1924,6 +1924,216 @@ describe('application hooks that throw or reject', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('the server\'s onRequest hook', () => {
|
||||
const answeredId = '01a07501-b609-7d27-ab98-d3b29bd78e7e';
|
||||
|
||||
function submitTo(session: Session, to: string, message = 'screened by the hook') {
|
||||
return session.send({
|
||||
cmdName: 'submit_sm',
|
||||
params: { destination_addr: to, short_message: message, source_addr: '46701113311' },
|
||||
});
|
||||
}
|
||||
|
||||
test('refuses an inbound submit_sm with the status the hook chose', async t => {
|
||||
const smpp = await startServer(t, {
|
||||
onRequest: async (bound, pduObj) => {
|
||||
if (!isCommand(pduObj, 'submit_sm')) return false;
|
||||
|
||||
await bound.sendReturn(pduObj, 'ESME_RINVDSTADR');
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
let messages = 0;
|
||||
|
||||
smpp.on('session', bound => bound.on('sms', () => { messages++; }));
|
||||
|
||||
const { session } = await connect(t, smpp);
|
||||
|
||||
assert.ok(session);
|
||||
|
||||
const answered = await submitTo(session, '46700000000');
|
||||
|
||||
assert.equal(answered.err, undefined);
|
||||
assert.ok(answered.pduObj);
|
||||
assert.equal(answered.pduObj.cmdStatus, 'ESME_RINVDSTADR');
|
||||
assert.equal(messages, 0, 'a request the hook answered never reaches the sms event');
|
||||
});
|
||||
|
||||
test('answers a bind itself, so a hook that claims every request cannot intercept one', async t => {
|
||||
const seen: string[] = [];
|
||||
const smpp = await startServer(t, {
|
||||
authenticate: ({ password, systemId }) => systemId === 'user' && password === 'pass',
|
||||
onRequest: (_bound, pduObj) => { seen.push(pduObj.cmdName); return true; },
|
||||
});
|
||||
const peer = rawPeer(t, smpp.port);
|
||||
|
||||
peer.write(bindOf(0x34));
|
||||
|
||||
const accepted = await peer.next();
|
||||
|
||||
peer.write(bindOf(0x34, 2));
|
||||
|
||||
const rebound = await peer.next();
|
||||
const wrong = rawPeer(t, smpp.port);
|
||||
|
||||
wrong.write({
|
||||
cmdName: 'bind_transceiver',
|
||||
params: { interface_version: 0x34, password: 'wrong', system_id: 'user' },
|
||||
seqNr: 1,
|
||||
});
|
||||
|
||||
const refused = await wrong.next();
|
||||
const early = rawPeer(t, smpp.port);
|
||||
|
||||
early.write({ cmdName: 'unbind', seqNr: 1 });
|
||||
|
||||
const unbound = await early.next();
|
||||
|
||||
assert.equal(accepted.cmdName, 'bind_transceiver_resp');
|
||||
assert.equal(accepted.cmdStatus, 'ESME_ROK');
|
||||
assert.equal(rebound.cmdStatus, 'ESME_RALYBND');
|
||||
assert.equal(refused.cmdStatus, 'ESME_RBINDFAIL');
|
||||
assert.equal(unbound.cmdName, 'unbind_resp');
|
||||
assert.deepEqual(seen, [], 'a bind, and everything a peer sends before one, is never the hook\'s');
|
||||
});
|
||||
|
||||
test('passes a request the hook declines through to the sms event', async t => {
|
||||
const seen: string[] = [];
|
||||
const smpp = await startServer(t, {
|
||||
onRequest: (_bound, pduObj) => { seen.push(pduObj.cmdName); return false; },
|
||||
});
|
||||
const incoming = once<Sms>(resolve => {
|
||||
smpp.on('session', bound => bound.on('sms', async sms => {
|
||||
resolve(sms);
|
||||
await sms.sendResp({ smsId: answeredId });
|
||||
}));
|
||||
});
|
||||
const { session } = await connect(t, smpp);
|
||||
|
||||
assert.ok(session);
|
||||
|
||||
const answered = await submitTo(session, '46709771337', 'declined by the hook');
|
||||
const sms = await incoming;
|
||||
|
||||
assert.ok(answered.pduObj);
|
||||
assert.equal(answered.pduObj.cmdStatus, 'ESME_ROK');
|
||||
assert.equal(paramText(answered.pduObj.params.message_id), answeredId);
|
||||
assert.equal(sms.message, 'declined by the hook');
|
||||
assert.deepEqual(seen, ['submit_sm']);
|
||||
});
|
||||
|
||||
test('reports a hook that throws and answers nothing for it', async t => {
|
||||
const smpp = await startServer(t, {
|
||||
onRequest: () => { throw new Error('the onRequest hook exploded'); },
|
||||
});
|
||||
const failed = once<Error>(resolve => {
|
||||
smpp.on('session', bound => { bound.on('sessionError', resolve); });
|
||||
});
|
||||
let messages = 0;
|
||||
|
||||
smpp.on('session', bound => bound.on('sms', () => { messages++; }));
|
||||
|
||||
const { session } = await connect(t, smpp, { responseTimeout: 300 });
|
||||
|
||||
assert.ok(session);
|
||||
|
||||
const answered = await submitTo(session, '46709771337');
|
||||
const reported = await failed;
|
||||
|
||||
assert.ok(answered.err instanceof Error);
|
||||
assert.equal(reported.message, 'the onRequest hook exploded');
|
||||
assert.equal(messages, 0, 'a hook that failed decided nothing, so nothing may decide for it');
|
||||
});
|
||||
|
||||
test('reports a hook that rejects and answers nothing for it', async t => {
|
||||
const smpp = await startServer(t, {
|
||||
onRequest: () => Promise.reject(new Error('the onRequest hook rejected')),
|
||||
});
|
||||
const failed = once<Error>(resolve => {
|
||||
smpp.on('session', bound => { bound.on('sessionError', resolve); });
|
||||
});
|
||||
let messages = 0;
|
||||
|
||||
smpp.on('session', bound => bound.on('sms', () => { messages++; }));
|
||||
|
||||
const { session } = await connect(t, smpp, { responseTimeout: 300 });
|
||||
|
||||
assert.ok(session);
|
||||
|
||||
const answered = await submitTo(session, '46709771337');
|
||||
const reported = await failed;
|
||||
|
||||
assert.ok(answered.err instanceof Error);
|
||||
assert.equal(reported.message, 'the onRequest hook rejected');
|
||||
assert.equal(messages, 0);
|
||||
});
|
||||
|
||||
test('writes nothing more for a segment the hook answered before it failed', async t => {
|
||||
const smpp = await startServer(t, {
|
||||
onRequest: async (bound, pduObj) => {
|
||||
await bound.sendReturn(pduObj, 'ESME_RINVDSTADR');
|
||||
|
||||
throw new Error('the onRequest hook exploded after answering');
|
||||
},
|
||||
});
|
||||
const failed = once<Error>(resolve => {
|
||||
smpp.on('session', bound => { bound.on('sessionError', resolve); });
|
||||
});
|
||||
const peer = rawPeer(t, smpp.port);
|
||||
const segment = splitMessage('one segment of a longer message. '.repeat(8), { reference: 0x6D })[0];
|
||||
|
||||
assert.ok(segment);
|
||||
peer.write(bindOf(0x34));
|
||||
await peer.next();
|
||||
peer.write({
|
||||
cmdName: 'submit_sm',
|
||||
params: {
|
||||
destination_addr: '46709771337',
|
||||
esm_class: consts.ESM_CLASS.UDH_INDICATOR,
|
||||
short_message: segment,
|
||||
source_addr: '46701113311',
|
||||
},
|
||||
seqNr: 2,
|
||||
});
|
||||
|
||||
const refused = await peer.next();
|
||||
const reported = await failed;
|
||||
|
||||
assert.equal(refused.cmdStatus, 'ESME_RINVDSTADR');
|
||||
assert.equal(await raceWithin(200, peer.next()), false, 'the peer gets one answer, not two');
|
||||
assert.equal(reported.message, 'the onRequest hook exploded after answering');
|
||||
});
|
||||
|
||||
test('closes without waiting out the shutdown for a message the hook answered itself', async t => {
|
||||
const smpp = await startServer(t, {
|
||||
onRequest: async (bound, pduObj) => {
|
||||
if (!isCommand(pduObj, 'submit_sm')) return false;
|
||||
|
||||
await bound.sendReturn(pduObj, 'ESME_ROK', { message_id: answeredId });
|
||||
|
||||
return true;
|
||||
},
|
||||
shutdownTimeout: 2000,
|
||||
});
|
||||
const bound = once<Session>(resolve => { smpp.on('session', resolve); });
|
||||
const { session } = await connect(t, smpp);
|
||||
|
||||
assert.ok(session);
|
||||
|
||||
const answered = await submitTo(session, '46709771337');
|
||||
const serverSide = await bound;
|
||||
const started = Date.now();
|
||||
const closed = await serverSide.close();
|
||||
const waited = Date.now() - started;
|
||||
|
||||
assert.ok(answered.pduObj);
|
||||
assert.equal(paramText(answered.pduObj.params.message_id), answeredId);
|
||||
assert.deepEqual(closed, {});
|
||||
assert.ok(waited < 1000, `close() waited ${String(waited)} ms on a message nothing was holding`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('link timers', () => {
|
||||
test('closes a client link the peer has stopped answering', async t => {
|
||||
const peer = await smscPeer(t);
|
||||
@@ -2116,6 +2326,23 @@ describe('option validation', () => {
|
||||
assert.ok(err instanceof Error);
|
||||
});
|
||||
|
||||
test('refuses a hook that is not a function at startup, rather than once per PDU', async () => {
|
||||
const badAuth: ServerOptions = { port: 0 };
|
||||
const badHook: ServerOptions = { port: 0 };
|
||||
|
||||
Object.assign(badAuth, { authenticate: 'yes please' });
|
||||
Object.assign(badHook, { onRequest: 'refuse them all' });
|
||||
|
||||
const authRefused = await server(badAuth);
|
||||
const hookRefused = await server(badHook);
|
||||
|
||||
if (authRefused.server) await authRefused.server.close();
|
||||
if (hookRefused.server) await hookRefused.server.close();
|
||||
|
||||
assert.match(authRefused.err?.message ?? '', /authenticate must be a function/);
|
||||
assert.match(hookRefused.err?.message ?? '', /onRequest must be a function/);
|
||||
});
|
||||
|
||||
test('returns an error rather than rejecting on an impossible port', async () => {
|
||||
const listening = await server({ port: 70_000 });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user