212 lines
6.4 KiB
TypeScript
212 lines
6.4 KiB
TypeScript
/**
|
|
* Lifecycle and option-validation tests for `HttpsTransport`.
|
|
*/
|
|
|
|
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import {
|
|
HTTPS_TRANSPORT_CODES,
|
|
HttpsTransport,
|
|
type HttpsTransportError,
|
|
} from "../src/index.js";
|
|
import { jsonResponse, makeFakeFetch } from "./_helpers.js";
|
|
|
|
const RELAY = "https://relay.test";
|
|
|
|
describe("HttpsTransport — lifecycle", () => {
|
|
it("constructor rejects an invalid relayUrl", { timeout: 5000 }, () => {
|
|
expect(
|
|
() =>
|
|
new HttpsTransport({
|
|
relayUrl: "not-a-url",
|
|
ourPeerId: "peer-a",
|
|
})
|
|
).toThrow();
|
|
});
|
|
|
|
it("constructor rejects empty ourPeerId", { timeout: 5000 }, () => {
|
|
expect(
|
|
() =>
|
|
new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "",
|
|
})
|
|
).toThrow();
|
|
});
|
|
|
|
it("constructor rejects backoffBaseMs > backoffMaxMs", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(
|
|
() =>
|
|
new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
backoffBaseMs: 1000,
|
|
backoffMaxMs: 100,
|
|
})
|
|
).toThrow();
|
|
});
|
|
|
|
it("start is idempotent while running", { timeout: 5000 }, async () => {
|
|
const fetchFn = makeFakeFetch({
|
|
responder: () => jsonResponse({ envelopes: [], nextCursor: "0" }),
|
|
});
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
fetchFn,
|
|
longPollTimeoutMs: 50,
|
|
pollIdleDelayMs: 5,
|
|
});
|
|
t.start();
|
|
t.start();
|
|
expect(t.state).toBe("running");
|
|
await t.stop();
|
|
});
|
|
|
|
it("start after stop throws CLOSED", { timeout: 5000 }, async () => {
|
|
const fetchFn = makeFakeFetch({
|
|
responder: () => jsonResponse({ envelopes: [], nextCursor: "0" }),
|
|
});
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
fetchFn,
|
|
});
|
|
t.start();
|
|
await t.stop();
|
|
try {
|
|
t.start();
|
|
throw new Error("expected CLOSED");
|
|
} catch (err) {
|
|
expect((err as HttpsTransportError).code).toBe(
|
|
HTTPS_TRANSPORT_CODES.CLOSED
|
|
);
|
|
}
|
|
});
|
|
|
|
it("stop is idempotent", { timeout: 5000 }, async () => {
|
|
const fetchFn = makeFakeFetch({
|
|
responder: () => jsonResponse({ envelopes: [], nextCursor: "0" }),
|
|
});
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
fetchFn,
|
|
});
|
|
t.start();
|
|
await t.stop();
|
|
await t.stop();
|
|
expect(t.state).toBe("stopped");
|
|
});
|
|
|
|
it("stop before start does not throw", { timeout: 5000 }, async () => {
|
|
const fetchFn = makeFakeFetch({
|
|
responder: () => jsonResponse({ envelopes: [], nextCursor: "0" }),
|
|
});
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
fetchFn,
|
|
});
|
|
await t.stop();
|
|
expect(t.state).toBe("stopped");
|
|
});
|
|
|
|
it("connect/disconnect track peers and getConnectedPeers reports them", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const fetchFn = makeFakeFetch({
|
|
responder: () => jsonResponse({ envelopes: [], nextCursor: "0" }),
|
|
});
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
fetchFn,
|
|
longPollTimeoutMs: 50,
|
|
pollIdleDelayMs: 5,
|
|
});
|
|
t.start();
|
|
await t.connect("peer-b", "ignored");
|
|
await t.connect("peer-c", "ignored");
|
|
expect(t.getConnectedPeers()).toEqual(["peer-b", "peer-c"]);
|
|
await t.disconnect("peer-b");
|
|
expect(t.getConnectedPeers()).toEqual(["peer-c"]);
|
|
// disconnect on an unknown peer is a no-op
|
|
await t.disconnect("peer-x");
|
|
expect(t.getConnectedPeers()).toEqual(["peer-c"]);
|
|
await t.stop();
|
|
});
|
|
|
|
it("connect before start throws NOT_CONNECTED", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const fetchFn = makeFakeFetch({
|
|
responder: () => jsonResponse({ ok: true }),
|
|
});
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
fetchFn,
|
|
});
|
|
await expect(t.connect("peer-b", "x")).rejects.toMatchObject({
|
|
code: HTTPS_TRANSPORT_CODES.NOT_CONNECTED,
|
|
});
|
|
});
|
|
|
|
it("cursor starts empty and updates after a successful /recv", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
let recvCalls = 0;
|
|
const fetchFn = makeFakeFetch({
|
|
responder: (call) => {
|
|
if (call.url.includes("/recv")) {
|
|
recvCalls++;
|
|
return jsonResponse({
|
|
envelopes: [],
|
|
nextCursor: "c-1",
|
|
});
|
|
}
|
|
return jsonResponse({ ok: true });
|
|
},
|
|
});
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
fetchFn,
|
|
longPollTimeoutMs: 25,
|
|
pollIdleDelayMs: 5,
|
|
});
|
|
expect(t.cursor).toBe("");
|
|
expect(t.state).toBe("idle");
|
|
t.start();
|
|
// Wait for at least one successful recv cycle to complete.
|
|
const start = Date.now();
|
|
while (t.cursor === "" && Date.now() - start < 2000) {
|
|
await new Promise<void>((r) => setTimeout(r, 5));
|
|
}
|
|
expect(t.cursor).toBe("c-1");
|
|
expect(recvCalls).toBeGreaterThan(0);
|
|
await t.stop();
|
|
});
|
|
|
|
it("clears handlers and peers on stop", { timeout: 5000 }, async () => {
|
|
const fetchFn = makeFakeFetch({
|
|
responder: () => jsonResponse({ envelopes: [], nextCursor: "0" }),
|
|
});
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
fetchFn,
|
|
longPollTimeoutMs: 25,
|
|
});
|
|
t.start();
|
|
await t.connect("peer-b", "x");
|
|
const off = t.onMessage(() => undefined);
|
|
await t.stop();
|
|
expect(t.getConnectedPeers()).toEqual([]);
|
|
// Calling unsubscribe after stop is a no-op (set already cleared).
|
|
off();
|
|
});
|
|
});
|