Files
suaveplan/packages/networking/drive/drive-transport-https/tests/lifecycle.test.ts
2026-05-27 23:47:53 +01:00

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();
});
});