Tear down what a test opens in t.after so a failing assertion ends the run

This commit is contained in:
2026-08-30 21:35:09 +02:00
parent fa720e244a
commit 427565ed3e
8 changed files with 317 additions and 415 deletions
+65 -93
View File
@@ -11,10 +11,12 @@ import type { SendSmsResult } from '../src/send-sms.ts';
import type { SmppLog } from '../src/log.ts';
import type { Sms } from '../src/sms.ts';
import type { SmppServer } from '../src/server.ts';
import type { TestContext } from 'node:test';
import { Reassembler, decodeSegments } from '../src/reassembly.ts';
import { Session } from '../src/session.ts';
import { DlrMerger } from '../src/dlr-merger.ts';
import { client } from '../src/client.ts';
import { closeAfter, endListenerAfter } from './teardown.ts';
import { consts } from '../src/defs/constants.ts';
import { errors } from '../src/defs/errors.ts';
import { objToPdu } from '../src/pdu.ts';
@@ -22,15 +24,31 @@ 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> {
async function startServer(
t: TestContext,
options: Parameters<typeof server>[0] = {},
): Promise<SmppServer> {
const { err, server: smpp } = await server({ ...options, port: 0 });
assert.equal(err, undefined);
assert.ok(smpp);
closeAfter(t, smpp);
return smpp;
}
async function connect(
t: TestContext,
smpp: SmppServer,
options: Parameters<typeof client>[0] = {},
) {
const connected = await client({ port: smpp.port, ...options });
if (connected.session) closeAfter(t, connected.session);
return connected;
}
/** 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) => {
@@ -61,12 +79,12 @@ function gate(): Gate {
describe('merged delivery reports', () => {
// 0.4.0 allocated a longSmsDlrs store to do exactly this and then never used it.
test('reports once on a whole multipart message', async () => {
const smpp = await startServer();
test('reports once on a whole multipart message', async t => {
const smpp = await startServer(t);
const incoming = once<Sms>(resolve => {
smpp.on('session', session => session.on('sms', resolve));
});
const { session } = await client({ port: smpp.port });
const { session } = await connect(t, smpp);
assert.ok(session);
@@ -97,17 +115,14 @@ describe('merged delivery reports', () => {
assert.equal(report.segments.length, 3);
assert.equal(report.statusMsg, 'DELIVERED');
assert.deepEqual(perSegment, ['merge-me-1', 'merge-me-2', 'merge-me-3']);
await session.close();
await smpp.close();
});
test('reports the worst status across the segments', async () => {
const smpp = await startServer();
test('reports the worst status across the segments', async t => {
const smpp = await startServer(t);
const incoming = once<Sms>(resolve => {
smpp.on('session', session => session.on('sms', resolve));
});
const { session } = await client({ port: smpp.port });
const { session } = await connect(t, smpp);
assert.ok(session);
@@ -133,9 +148,6 @@ describe('merged delivery reports', () => {
assert.equal(report.statusMsg, 'UNDELIVERABLE');
assert.equal(report.segments.length, 3);
await session.close();
await smpp.close();
});
});
@@ -201,12 +213,12 @@ describe('sendSms()', () => {
);
}
test('reports a submit_sm the peer refused instead of an empty message id', async () => {
const smpp = await startServer();
test('reports a submit_sm the peer refused instead of an empty message id', async t => {
const smpp = await startServer(t);
const incoming = once<Sms>(resolve => {
smpp.on('session', session => session.on('sms', resolve));
});
const { session } = await client({ port: smpp.port });
const { session } = await connect(t, smpp);
assert.ok(session);
@@ -217,9 +229,6 @@ describe('sendSms()', () => {
assert.ok(sent.err instanceof Error);
assert.match(sent.err.message, /ESME_RMSGQFUL/);
await session.close();
await smpp.close();
});
// A retry that repeats the segments the SMSC already took bills the recipient twice.
@@ -315,8 +324,8 @@ describe('reconnect', () => {
assert.equal(sent.err, undefined);
}
test('re-binds after the connection drops, keeping the same session object', async () => {
const smpp = await startServer();
test('re-binds after the connection drops, keeping the same session object', async t => {
const smpp = await startServer(t);
const messages: string[] = [];
// Registered up front so the session created by the reconnect is covered too.
@@ -327,10 +336,7 @@ describe('reconnect', () => {
});
});
const { err, session } = await client({
port: smpp.port,
reconnect: { maxDelay: 100, minDelay: 20 },
});
const { err, session } = await connect(t, smpp, { reconnect: { maxDelay: 100, minDelay: 20 } });
assert.equal(err, undefined);
assert.ok(session);
@@ -355,22 +361,16 @@ describe('reconnect', () => {
assert.equal(sent.err, undefined);
assert.deepEqual(messages, ['after reconnect']);
await session.close();
await smpp.close();
});
test('merges the receipts of a multipart message across a drop', async () => {
const smpp = await startServer();
test('merges the receipts of a multipart message across a drop', async t => {
const smpp = await startServer(t);
smpp.on('session', bound => {
bound.on('sms', sms => { void sms.sendResp({ smsId: 'across-the-drop' }); });
});
const { session } = await client({
port: smpp.port,
reconnect: { maxDelay: 100, minDelay: 20 },
});
const { session } = await connect(t, smpp, { reconnect: { maxDelay: 100, minDelay: 20 } });
assert.ok(session);
@@ -400,17 +400,11 @@ describe('reconnect', () => {
assert.equal(report.smsId, 'across-the-drop');
assert.equal(report.segments.length, 3);
await session.close();
await smpp.close();
});
test('does not reconnect after an explicit close', async () => {
const smpp = await startServer();
const { session } = await client({
port: smpp.port,
reconnect: { maxDelay: 50, minDelay: 10 },
});
test('does not reconnect after an explicit close', async t => {
const smpp = await startServer(t);
const { session } = await connect(t, smpp, { reconnect: { maxDelay: 50, minDelay: 10 } });
assert.ok(session);
@@ -422,7 +416,6 @@ describe('reconnect', () => {
await delay(150);
assert.equal(reconnects, 0);
await smpp.close();
});
});
@@ -572,7 +565,7 @@ describe('reassembly bounds', () => {
});
describe('AbortSignal on a send', () => {
test('gives up on an in-flight request when the signal fires', async () => {
test('gives up on an in-flight request when the signal fires', async t => {
const accepted: net.Socket[] = [];
const silent = net.createServer(sock => {
accepted.push(sock);
@@ -582,6 +575,7 @@ describe('AbortSignal on a send', () => {
});
await new Promise<void>(resolve => silent.listen(0, resolve));
endListenerAfter(t, silent, accepted);
const address = silent.address();
const port = typeof address === 'object' && address !== null ? address.port : 0;
@@ -589,6 +583,7 @@ describe('AbortSignal on a send', () => {
assert.equal(err, undefined);
assert.ok(session);
closeAfter(t, session);
const controller = new AbortController();
@@ -600,22 +595,16 @@ describe('AbortSignal on a send', () => {
);
assert.ok(sent.err instanceof Error);
await session.close();
for (const sock of accepted) sock.destroy();
await new Promise<void>(resolve => silent.close(() => { resolve(); }));
});
});
describe('graceful shutdown', () => {
async function submitInFlight(options: Parameters<typeof client>[0] = {}) {
const smpp = await startServer();
async function submitInFlight(t: TestContext, options: Parameters<typeof client>[0] = {}) {
const smpp = await startServer(t);
const incoming = once<Sms>(resolve => {
smpp.on('session', bound => bound.on('sms', resolve));
});
const { session } = await client({ port: smpp.port, ...options });
const { session } = await connect(t, smpp, options);
assert.ok(session);
@@ -624,8 +613,8 @@ describe('graceful shutdown', () => {
return { sent, session, sms: await incoming, smpp };
}
test('close() waits out a submit already on the wire and refuses new ones', async () => {
const { sent, session, sms, smpp } = await submitInFlight();
test('close() waits out a submit already on the wire and refuses new ones', async t => {
const { sent, session, sms } = await submitInFlight(t);
const closed = session.close();
const refused = await session.sendSms({
from: '46701113311',
@@ -640,24 +629,20 @@ describe('graceful shutdown', () => {
assert.deepEqual((await sent).smsIds, ['answered-while-draining']);
assert.deepEqual(await closed, {});
await smpp.close();
});
test('unbind() waits out a submit already on the wire before it unbinds', async () => {
const { sent, session, sms, smpp } = await submitInFlight();
test('unbind() waits out a submit already on the wire before it unbinds', async t => {
const { sent, session, sms } = await submitInFlight(t);
const unbound = session.unbind();
await sms.sendResp({ smsId: 'answered-before-unbind' });
assert.deepEqual((await sent).smsIds, ['answered-before-unbind']);
assert.deepEqual(await unbound, {});
await smpp.close();
});
test('gives up on a request that outlasts shutdownTimeout', async () => {
const { sent, session, smpp } = await submitInFlight({ shutdownTimeout: 50 });
test('gives up on a request that outlasts shutdownTimeout', async t => {
const { sent, session } = await submitInFlight(t, { shutdownTimeout: 50 });
const closed = await session.close();
assert.ok(closed.err instanceof Error);
@@ -667,13 +652,11 @@ describe('graceful shutdown', () => {
assert.ok(result.err instanceof Error);
assert.equal(result.err.message, 'Session closed before a response arrived');
await smpp.close();
});
// The window empties on a drop as well as on an answer, so it cannot be what the result reads.
test('reports a link that dropped mid-drain rather than calling it a clean shutdown', async () => {
const { sent, session, smpp } = await submitInFlight();
test('reports a link that dropped mid-drain rather than calling it a clean shutdown', async t => {
const { sent, session, smpp } = await submitInFlight(t);
const closing = session.close();
for (const bound of smpp.sessions) {
@@ -685,17 +668,15 @@ describe('graceful shutdown', () => {
assert.ok(closed.err instanceof Error);
assert.match(closed.err.message, /closed before the drain finished/);
assert.ok((await sent).err instanceof Error);
await smpp.close();
});
// The queued segments are the whole reason the drain waits on the window and not on the pending map.
test('counts the segments still queued behind a full window', async () => {
const smpp = await startServer();
test('counts the segments still queued behind a full window', async t => {
const smpp = await startServer(t);
const onWire = once<PduObject>(resolve => {
smpp.on('session', bound => bound.on('incomingPduObj', resolve));
});
const { session } = await client({ maxOutstanding: 1, port: smpp.port, shutdownTimeout: 50 });
const { session } = await connect(t, smpp, { maxOutstanding: 1, shutdownTimeout: 50 });
assert.ok(session);
@@ -712,12 +693,10 @@ describe('graceful shutdown', () => {
assert.ok(closed.err instanceof Error);
assert.match(closed.err.message, /3 request\(s\)/);
assert.ok((await sent).err instanceof Error);
await smpp.close();
});
test('an aborted close tears down at once instead of waiting out the drain', async () => {
const { sent, session, smpp } = await submitInFlight({ shutdownTimeout: 30_000 });
test('an aborted close tears down at once instead of waiting out the drain', async t => {
const { sent, session } = await submitInFlight(t, { shutdownTimeout: 30_000 });
const controller = new AbortController();
const started = Date.now();
@@ -729,15 +708,14 @@ describe('graceful shutdown', () => {
assert.ok(closed.err instanceof Error);
assert.ok(session.sock.destroyed);
assert.ok((await sent).err instanceof Error);
await smpp.close();
});
test('stops accepting the moment close() is called, not when the drain ends', async () => {
const smpp = await startServer({ shutdownTimeout: 30_000 });
test('stops accepting the moment close() is called, not when the drain ends', async t => {
const smpp = await startServer(t, { shutdownTimeout: 30_000 });
const arrived = once<Session>(resolve => { smpp.on('session', resolve); });
const silent = net.connect({ port: smpp.port });
t.after(() => { silent.destroy(); });
silent.resume();
const bound = await arrived;
@@ -759,11 +737,12 @@ describe('graceful shutdown', () => {
});
// Waiting on a peer that has just declared itself finished is dead time, unbounded at 0.
test('tears down at once when the peer unbinds rather than draining for it', async () => {
const smpp = await startServer({ shutdownTimeout: 0 });
test('tears down at once when the peer unbinds rather than draining for it', async t => {
const smpp = await startServer(t, { shutdownTimeout: 0 });
const arrived = once<Session>(resolve => { smpp.on('session', resolve); });
const peer = net.connect({ port: smpp.port });
t.after(() => { peer.destroy(); });
peer.resume();
const bound = await arrived;
@@ -776,12 +755,9 @@ describe('graceful shutdown', () => {
assert.ok(await ended);
assert.ok((await unanswered).err instanceof Error);
peer.destroy();
await smpp.close();
});
test('does not report a reconnect on a session closed while it was coming back up', async () => {
test('does not report a reconnect on a session closed while it was coming back up', async t => {
const listener = net.createServer(sock => { sock.resume(); });
await new Promise<void>(resolve => { listener.listen(0, resolve); });
@@ -816,6 +792,8 @@ describe('graceful shutdown', () => {
});
const reported: string[] = [];
closeAfter(t, session);
endListenerAfter(t, listener, opened);
session.on('reconnected', () => reported.push('reconnected'));
first.sock.destroy();
@@ -828,11 +806,5 @@ describe('graceful shutdown', () => {
await delay(50);
assert.deepEqual(reported, []);
for (const sock of opened) {
sock.destroy();
}
await new Promise<void>(resolve => { listener.close(() => { resolve(); }); });
});
});