384 lines
13 KiB
TypeScript
384 lines
13 KiB
TypeScript
/**
|
|
* `HttpsTransport.send` — covers happy path, 4xx, 5xx, malformed
|
|
* response bodies, ok:false rejections, fetch-throws, and lifecycle
|
|
* preconditions.
|
|
*/
|
|
|
|
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import {
|
|
HTTPS_TRANSPORT_CODES,
|
|
HttpsTransport,
|
|
HttpsTransportError,
|
|
} from "../src/index.js";
|
|
import {
|
|
type FakeFetchCall,
|
|
jsonResponse,
|
|
makeEnvelope,
|
|
makeFakeFetch,
|
|
TEST_PEER,
|
|
} from "./_helpers.js";
|
|
|
|
const RELAY = "https://relay.test";
|
|
|
|
describe("HttpsTransport.send — wire flow", () => {
|
|
it("posts to /send with bearer auth and the envelope body", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const history: FakeFetchCall[] = [];
|
|
const fetchFn = makeFakeFetch({
|
|
responder: (call) =>
|
|
call.url.endsWith("/send")
|
|
? jsonResponse({ ok: true })
|
|
: jsonResponse({ envelopes: [], nextCursor: "0" }),
|
|
history,
|
|
});
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
authToken: "secret",
|
|
fetchFn,
|
|
longPollTimeoutMs: 100,
|
|
});
|
|
t.start();
|
|
await t.connect(TEST_PEER, "ignored");
|
|
await t.send(TEST_PEER, makeEnvelope());
|
|
|
|
const sendCall = history.find((c) => c.url === `${RELAY}/send`);
|
|
expect(sendCall).toBeDefined();
|
|
expect(sendCall?.method).toBe("POST");
|
|
expect(sendCall?.headers.authorization).toBe("Bearer secret");
|
|
const parsed = JSON.parse(sendCall?.body ?? "{}");
|
|
expect(parsed.to).toBe(TEST_PEER);
|
|
expect(parsed.envelope.id).toBe("msg-1");
|
|
|
|
await t.stop();
|
|
});
|
|
|
|
it("strips trailing slashes from relayUrl", { timeout: 5000 }, async () => {
|
|
const history: FakeFetchCall[] = [];
|
|
const fetchFn = makeFakeFetch({
|
|
responder: () => jsonResponse({ ok: true }),
|
|
history,
|
|
});
|
|
const t = new HttpsTransport({
|
|
relayUrl: `${RELAY}//`,
|
|
ourPeerId: "peer-a",
|
|
fetchFn,
|
|
longPollTimeoutMs: 100,
|
|
});
|
|
t.start();
|
|
await t.connect(TEST_PEER, "ignored");
|
|
await t.send(TEST_PEER, makeEnvelope());
|
|
const sendCall = history.find((c) => c.url.endsWith("/send"));
|
|
expect(sendCall?.url).toBe(`${RELAY}/send`);
|
|
await t.stop();
|
|
});
|
|
|
|
it("omits the Authorization header when no token is set", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const history: FakeFetchCall[] = [];
|
|
const fetchFn = makeFakeFetch({
|
|
responder: () => jsonResponse({ ok: true }),
|
|
history,
|
|
});
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
fetchFn,
|
|
longPollTimeoutMs: 100,
|
|
});
|
|
t.start();
|
|
await t.connect(TEST_PEER, "ignored");
|
|
await t.send(TEST_PEER, makeEnvelope());
|
|
const sendCall = history.find((c) => c.url.endsWith("/send"));
|
|
expect(sendCall?.headers.authorization).toBeUndefined();
|
|
await t.stop();
|
|
});
|
|
|
|
it("maps 401 to AUTH_FAILED", { timeout: 5000 }, async () => {
|
|
const fetchFn = makeFakeFetch({
|
|
responder: (call) =>
|
|
call.url.endsWith("/send")
|
|
? new Response("nope", { status: 401 })
|
|
: jsonResponse({ envelopes: [], nextCursor: "0" }),
|
|
});
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
fetchFn,
|
|
longPollTimeoutMs: 100,
|
|
});
|
|
t.start();
|
|
await t.connect(TEST_PEER, "x");
|
|
await expect(t.send(TEST_PEER, makeEnvelope())).rejects.toMatchObject({
|
|
code: HTTPS_TRANSPORT_CODES.AUTH_FAILED,
|
|
});
|
|
await t.stop();
|
|
});
|
|
|
|
it("maps 503 to RELAY_UNAVAILABLE", { timeout: 5000 }, async () => {
|
|
const fetchFn = makeFakeFetch({
|
|
responder: (call) =>
|
|
call.url.endsWith("/send")
|
|
? new Response("down", { status: 503 })
|
|
: jsonResponse({ envelopes: [], nextCursor: "0" }),
|
|
});
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
fetchFn,
|
|
longPollTimeoutMs: 100,
|
|
});
|
|
t.start();
|
|
await t.connect(TEST_PEER, "x");
|
|
await expect(t.send(TEST_PEER, makeEnvelope())).rejects.toMatchObject({
|
|
code: HTTPS_TRANSPORT_CODES.RELAY_UNAVAILABLE,
|
|
});
|
|
await t.stop();
|
|
});
|
|
|
|
it("maps 400 (other 4xx) to SEND_FAILED", { timeout: 5000 }, async () => {
|
|
const fetchFn = makeFakeFetch({
|
|
responder: (call) =>
|
|
call.url.endsWith("/send")
|
|
? new Response("bad", { status: 400 })
|
|
: jsonResponse({ envelopes: [], nextCursor: "0" }),
|
|
});
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
fetchFn,
|
|
longPollTimeoutMs: 100,
|
|
});
|
|
t.start();
|
|
await t.connect(TEST_PEER, "x");
|
|
await expect(t.send(TEST_PEER, makeEnvelope())).rejects.toMatchObject({
|
|
code: HTTPS_TRANSPORT_CODES.SEND_FAILED,
|
|
});
|
|
await t.stop();
|
|
});
|
|
|
|
it("treats fetch-throws as SEND_FAILED with cause", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
fetchFn: async (url) => {
|
|
if (String(url).endsWith("/send")) {
|
|
throw new Error("ECONNRESET");
|
|
}
|
|
return jsonResponse({ envelopes: [], nextCursor: "0" });
|
|
},
|
|
longPollTimeoutMs: 100,
|
|
});
|
|
t.start();
|
|
await t.connect(TEST_PEER, "x");
|
|
try {
|
|
await t.send(TEST_PEER, makeEnvelope());
|
|
throw new Error("expected SEND_FAILED");
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(HttpsTransportError);
|
|
const e = err as HttpsTransportError;
|
|
expect(e.code).toBe(HTTPS_TRANSPORT_CODES.SEND_FAILED);
|
|
expect(e.message).toContain("ECONNRESET");
|
|
expect(e.cause).toBeInstanceOf(Error);
|
|
}
|
|
await t.stop();
|
|
});
|
|
|
|
it("treats fetch-throws of non-Error as SEND_FAILED with stringified cause", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const fetchSubstitute = async (
|
|
url: string | URL
|
|
): Promise<Response> => {
|
|
if (String(url).endsWith("/send")) {
|
|
// Throw a non-Error value to exercise the toError(cause)
|
|
// branch that wraps strings.
|
|
const literal: unknown = "boom-string";
|
|
throw literal;
|
|
}
|
|
return jsonResponse({ envelopes: [], nextCursor: "0" });
|
|
};
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
fetchFn: fetchSubstitute,
|
|
longPollTimeoutMs: 100,
|
|
});
|
|
t.start();
|
|
await t.connect(TEST_PEER, "x");
|
|
try {
|
|
await t.send(TEST_PEER, makeEnvelope());
|
|
throw new Error("expected SEND_FAILED");
|
|
} catch (err) {
|
|
const e = err as HttpsTransportError;
|
|
expect(e.code).toBe(HTTPS_TRANSPORT_CODES.SEND_FAILED);
|
|
expect(e.message).toContain("boom-string");
|
|
}
|
|
await t.stop();
|
|
});
|
|
|
|
it("MALFORMED_RESPONSE when /send returns non-JSON 200", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const fetchFn = makeFakeFetch({
|
|
responder: (call) =>
|
|
call.url.endsWith("/send")
|
|
? new Response("not-json{{", {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
})
|
|
: jsonResponse({ envelopes: [], nextCursor: "0" }),
|
|
});
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
fetchFn,
|
|
longPollTimeoutMs: 100,
|
|
});
|
|
t.start();
|
|
await t.connect(TEST_PEER, "x");
|
|
await expect(t.send(TEST_PEER, makeEnvelope())).rejects.toMatchObject({
|
|
code: HTTPS_TRANSPORT_CODES.MALFORMED_RESPONSE,
|
|
});
|
|
await t.stop();
|
|
});
|
|
|
|
it("MALFORMED_RESPONSE when /send body is unexpected JSON shape", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const fetchFn = makeFakeFetch({
|
|
responder: (call) =>
|
|
call.url.endsWith("/send")
|
|
? jsonResponse({ unexpected: true })
|
|
: jsonResponse({ envelopes: [], nextCursor: "0" }),
|
|
});
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
fetchFn,
|
|
longPollTimeoutMs: 100,
|
|
});
|
|
t.start();
|
|
await t.connect(TEST_PEER, "x");
|
|
await expect(t.send(TEST_PEER, makeEnvelope())).rejects.toMatchObject({
|
|
code: HTTPS_TRANSPORT_CODES.MALFORMED_RESPONSE,
|
|
});
|
|
await t.stop();
|
|
});
|
|
|
|
it("SEND_FAILED when relay returns { ok: false, code: 'POLICY' }", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const fetchFn = makeFakeFetch({
|
|
responder: (call) =>
|
|
call.url.endsWith("/send")
|
|
? jsonResponse({ ok: false, code: "POLICY" })
|
|
: jsonResponse({ envelopes: [], nextCursor: "0" }),
|
|
});
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
fetchFn,
|
|
longPollTimeoutMs: 100,
|
|
});
|
|
t.start();
|
|
await t.connect(TEST_PEER, "x");
|
|
try {
|
|
await t.send(TEST_PEER, makeEnvelope());
|
|
throw new Error("expected");
|
|
} catch (err) {
|
|
const e = err as HttpsTransportError;
|
|
expect(e.code).toBe(HTTPS_TRANSPORT_CODES.SEND_FAILED);
|
|
expect(e.message).toContain("POLICY");
|
|
}
|
|
await t.stop();
|
|
});
|
|
|
|
it("SEND_FAILED when relay returns { ok: false } without a code", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const fetchFn = makeFakeFetch({
|
|
responder: (call) =>
|
|
call.url.endsWith("/send")
|
|
? jsonResponse({ ok: false })
|
|
: jsonResponse({ envelopes: [], nextCursor: "0" }),
|
|
});
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
fetchFn,
|
|
longPollTimeoutMs: 100,
|
|
});
|
|
t.start();
|
|
await t.connect(TEST_PEER, "x");
|
|
try {
|
|
await t.send(TEST_PEER, makeEnvelope());
|
|
throw new Error("expected");
|
|
} catch (err) {
|
|
const e = err as HttpsTransportError;
|
|
expect(e.code).toBe(HTTPS_TRANSPORT_CODES.SEND_FAILED);
|
|
expect(e.message).toContain("unknown");
|
|
}
|
|
await t.stop();
|
|
});
|
|
|
|
it("PEER_UNKNOWN when sending to a peer that has not connected", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const fetchFn = makeFakeFetch({
|
|
responder: () => jsonResponse({ envelopes: [], nextCursor: "0" }),
|
|
});
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
fetchFn,
|
|
longPollTimeoutMs: 100,
|
|
});
|
|
t.start();
|
|
await expect(
|
|
t.send("not-connected", makeEnvelope())
|
|
).rejects.toMatchObject({
|
|
code: HTTPS_TRANSPORT_CODES.PEER_UNKNOWN,
|
|
});
|
|
await t.stop();
|
|
});
|
|
|
|
it("NOT_CONNECTED when send() is called before start()", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const fetchFn = makeFakeFetch({
|
|
responder: () => jsonResponse({ ok: true }),
|
|
});
|
|
const t = new HttpsTransport({
|
|
relayUrl: RELAY,
|
|
ourPeerId: "peer-a",
|
|
fetchFn,
|
|
});
|
|
await expect(t.send(TEST_PEER, makeEnvelope())).rejects.toMatchObject({
|
|
code: HTTPS_TRANSPORT_CODES.NOT_CONNECTED,
|
|
});
|
|
});
|
|
|
|
it("CLOSED when send() is called after stop()", {
|
|
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 expect(t.send(TEST_PEER, makeEnvelope())).rejects.toMatchObject({
|
|
code: HTTPS_TRANSPORT_CODES.CLOSED,
|
|
});
|
|
});
|
|
});
|