196 lines
5.9 KiB
TypeScript
196 lines
5.9 KiB
TypeScript
import assert from "assert";
|
||
|
||
import { ReconnectingSocket } from "./reconnectingsocket";
|
||
|
||
/** @see https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback */
|
||
type Exec = (command: string, callback: (error: null | (Error & { readonly code: number })) => void) => void;
|
||
|
||
let exec: Exec | undefined;
|
||
let childProcessAvailable: boolean;
|
||
|
||
try {
|
||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||
exec = require("child_process").exec;
|
||
assert.strict(typeof exec === "function");
|
||
childProcessAvailable = true;
|
||
} catch {
|
||
childProcessAvailable = false;
|
||
}
|
||
|
||
function pendingWithoutSocketServer(): void {
|
||
if (!process.env.SOCKETSERVER_ENABLED) {
|
||
pending("Set SOCKETSERVER_ENABLED to enable socket tests");
|
||
}
|
||
}
|
||
|
||
function pendingWithoutChildProcess(): void {
|
||
if (!childProcessAvailable) {
|
||
pending("Run test in an environment which supports child processes to enable socket tests");
|
||
}
|
||
}
|
||
|
||
describe("ReconnectingSocket", () => {
|
||
const socketServerUrl = "ws://localhost:4444/websocket";
|
||
|
||
it("can be constructed", () => {
|
||
const socket = new ReconnectingSocket(socketServerUrl);
|
||
expect(socket).toBeTruthy();
|
||
});
|
||
|
||
describe("connect", () => {
|
||
it("cannot connect after being connected", (done) => {
|
||
pendingWithoutSocketServer();
|
||
const socket = new ReconnectingSocket(socketServerUrl);
|
||
// Necessary otherwise the producer doesn’t start
|
||
socket.events.subscribe({});
|
||
|
||
socket.connect();
|
||
|
||
setTimeout(() => {
|
||
expect(() => socket.connect()).toThrowError(/cannot connect/i);
|
||
done();
|
||
}, 1000);
|
||
});
|
||
});
|
||
|
||
describe("disconnect", () => {
|
||
it("ends the events stream", (done) => {
|
||
pendingWithoutSocketServer();
|
||
const socket = new ReconnectingSocket(socketServerUrl);
|
||
socket.events.subscribe({
|
||
complete: done,
|
||
});
|
||
|
||
socket.connect();
|
||
|
||
setTimeout(() => socket.disconnect(), 1000);
|
||
});
|
||
|
||
it("cannot connect after being disconnected", (done) => {
|
||
pendingWithoutSocketServer();
|
||
const socket = new ReconnectingSocket(socketServerUrl);
|
||
// Necessary otherwise the producer doesn’t start
|
||
socket.events.subscribe({});
|
||
|
||
socket.connect();
|
||
|
||
setTimeout(() => {
|
||
socket.disconnect();
|
||
expect(() => socket.connect()).toThrowError(/cannot connect/i);
|
||
done();
|
||
}, 1000);
|
||
});
|
||
|
||
it("can disconnect without waiting for open", () => {
|
||
pendingWithoutSocketServer();
|
||
const socket = new ReconnectingSocket(socketServerUrl);
|
||
expect(() => {
|
||
socket.connect();
|
||
socket.disconnect();
|
||
}).not.toThrow();
|
||
});
|
||
});
|
||
|
||
describe("reconnection", () => {
|
||
const dirPath = "../../scripts/socketserver";
|
||
const codePkillNoProcessesMatched = 1;
|
||
const startServerCmd = `${dirPath}/start.sh`;
|
||
const stopServerCmd = `${dirPath}/stop.sh`;
|
||
|
||
it("automatically reconnects if no connection can be established at init", (done) => {
|
||
pendingWithoutChildProcess();
|
||
pendingWithoutSocketServer();
|
||
|
||
exec!(stopServerCmd, (stopError) => {
|
||
if (stopError && stopError.code !== codePkillNoProcessesMatched) {
|
||
done.fail(stopError);
|
||
}
|
||
|
||
const socket = new ReconnectingSocket(socketServerUrl);
|
||
const requests = ["request 1", "request 2", "request 3"] as const;
|
||
let eventsSeen = 0;
|
||
socket.events.subscribe({
|
||
next: ({ data }) => {
|
||
expect(data).toEqual(requests[eventsSeen++]);
|
||
if (eventsSeen === requests.length) {
|
||
socket.disconnect();
|
||
}
|
||
},
|
||
complete: () => {
|
||
// Make sure we don't get a completion unexpectedly
|
||
expect(eventsSeen).toEqual(requests.length);
|
||
done();
|
||
},
|
||
});
|
||
|
||
socket.connect();
|
||
requests.forEach((request) => socket.queueRequest(request));
|
||
|
||
setTimeout(
|
||
() =>
|
||
exec!(startServerCmd, (startError) => {
|
||
if (startError) {
|
||
done.fail(startError);
|
||
}
|
||
}),
|
||
2000,
|
||
);
|
||
});
|
||
});
|
||
|
||
it("automatically reconnects if the connection is broken off", (done) => {
|
||
pendingWithoutChildProcess();
|
||
pendingWithoutSocketServer();
|
||
|
||
const socket = new ReconnectingSocket(socketServerUrl);
|
||
const requests = ["request 1", "request 2", "request 3"] as const;
|
||
let eventsSeen = 0;
|
||
socket.events.subscribe({
|
||
next: ({ data }) => {
|
||
expect(data).toEqual(requests[eventsSeen++]);
|
||
if (eventsSeen === requests.length) {
|
||
socket.disconnect();
|
||
}
|
||
},
|
||
complete: () => {
|
||
// Make sure we don't get a completion unexpectedly
|
||
expect(eventsSeen).toEqual(requests.length);
|
||
done();
|
||
},
|
||
});
|
||
|
||
socket.connect();
|
||
socket.queueRequest(requests[0]);
|
||
|
||
setTimeout(
|
||
() =>
|
||
exec!(stopServerCmd, (stopError) => {
|
||
if (stopError && stopError.code !== codePkillNoProcessesMatched) {
|
||
done.fail(stopError);
|
||
}
|
||
|
||
// TODO: This timeout is here to avoid an edge case where if a request
|
||
// is sent just as a disconnection occurs, then the websocket’s `send`
|
||
// method may not error even though the request is never sent.
|
||
// Ideally we would have a way to cover this edge case and the timeout
|
||
// would not be necessary for this test to pass.
|
||
setTimeout(() => {
|
||
requests.slice(1).forEach((request) => socket.queueRequest(request));
|
||
|
||
setTimeout(
|
||
() =>
|
||
exec!(startServerCmd, (startError) => {
|
||
if (startError) {
|
||
done.fail(startError);
|
||
}
|
||
}),
|
||
2000,
|
||
);
|
||
}, 2000);
|
||
}),
|
||
1000,
|
||
);
|
||
});
|
||
});
|
||
});
|