Initial Commit
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* `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,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user