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