// node --test js/fetch.test.mjs // The iroh: fetcher against fake-bin/ssh (relay.mjs to a unix socket) and http-stub.mjs, no network: URL // parsing, keep-alive reuse per (ref, socket), one relay per concurrent request, chunked and // connection: close responses, a relay that dies before answering retried once, abort, and an idle relay // never holding a host process up. import assert from "node:assert/strict"; import { execFile, spawn } from "node:child_process"; import { mkdtemp, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { after, before, test } from "node:test"; import { promisify } from "node:util"; import { SHELL_SOCKET, fetchers, irohFetch, irohUrl, relayCommand } from "./index.js"; const FAKE = new URL("./fake-bin/", import.meta.url).pathname; const dir = await mkdtemp(join(tmpdir(), "atlibs-iroh-fetch-")); const key = join(dir, "key"); await writeFile(key, "not a real key\n", { mode: 0o600 }); const sock = join(dir, "it's.sock"); // a quote: the relay command's quoting const hostKeys = [{ type: "ssh-ed25519", publicKey: "AAAAC3Bare" }]; const ref = (r) => Buffer.from(JSON.stringify(r)).toString("base64url"); const base = `iroh:///${ref({ ticket: "abc123", hostKeys })}/${encodeURIComponent(sock)}`; const spawned = []; const fetch = irohFetch({ key, spawn: (cmd, args, options) => (spawned.push(args), spawn(join(FAKE, cmd), args, options)) }); const json = async (request) => { const r = await fetch(request); return { ...r, json: JSON.parse(r.body) }; }; let stub; before(async () => { stub = spawn(process.execPath, [join(FAKE, "http-stub.mjs"), sock], { stdio: ["ignore", "pipe", "inherit"] }); await new Promise((resolve) => stub.stdout.once("data", resolve)); }); after(() => (fetch.close(), stub.kill())); test("irohUrl: ref, socket (shell alias or a percent-encoded path), target", () => { assert.deepEqual(irohUrl(`iroh:///${ref({ ticket: "iroh://abc", hostKeys })}/shell/mcp?x=1#f`), { ticket: "abc", hostKeys, socket: SHELL_SOCKET, target: "/mcp?x=1" }); assert.deepEqual(irohUrl(`iroh://${ref({ ticket: "abc", hostKeys })}/%2Ftmp%2Fs.sock`), { ticket: "abc", hostKeys, socket: "/tmp/s.sock", target: "/" }); assert.throws(() => irohUrl("iroh:///abc"), /not an iroh/); assert.throws(() => irohUrl(`iroh:///${ref({ ticket: "abc" })}/tmp/x`), /absolute unix path/); assert.throws(() => irohUrl(`iroh:///${ref({ ticket: "ABC" })}/shell/mcp`), /not an iroh ticket/); assert.throws(() => irohUrl(`iroh:///e30/shell/a b`), /spaces/); assert.throws(() => irohUrl("iroh:///!!/shell/mcp"), /not an iroh/); assert.equal(relayCommand("/a'b"), "socialweb-guest-agent relay '/a'\\''b'"); }); test("a POST through the relay; the next request reuses its connection", async () => { const r = await json({ method: "POST", url: `${base}/mcp`, headers: { "content-type": "application/json", accept: "application/json, text/event-stream", host: "evil" }, body: '{"jsonrpc":"2.0"}' }); assert.equal(r.status, 200); assert.equal(r.headers["content-type"], "application/json"); assert.deepEqual([r.json.method, r.json.url, r.json.body, r.json.headers.host, r.json.headers.accept, r.json.n], ["POST", "/mcp", '{"jsonrpc":"2.0"}', "guest", "application/json, text/event-stream", 1]); assert.deepEqual(spawned[0].slice(-3), ["--", "root@guest", relayCommand(sock)]); assert.ok(spawned[0].includes("StrictHostKeyChecking=yes") && spawned[0].includes("ProxyCommand=dumbpipe connect abc123")); const again = await json({ url: `${base}/x` }); assert.deepEqual([again.json.method, again.json.n, spawned.length], ["GET", 2, 1], "keep-alive: the same relay"); }); test("concurrent requests get a relay each; chunked bodies; connection: close is not reused", async () => { const before = spawned.length; const [slow, fast] = await Promise.all([json({ url: `${base}/slow?ms=300` }), json({ url: `${base}/fast?chunked=1` })]); assert.deepEqual([slow.json.url, fast.json.url, fast.headers["transfer-encoding"]], ["/slow?ms=300", "/fast?chunked=1", "chunked"]); assert.equal(spawned.length, before + 1, "one more relay for the second in-flight request"); await json({ url: `${base}/c?close=1` }); await json({ url: `${base}/c?close=1` }); await json({ url: `${base}/c?close=1` }); assert.equal(spawned.length, before + 2, "the two idle relays, then a fresh one for the third"); }); test("a relay that dies before answering is retried once on a fresh one", async () => { const before = spawned.length; const r = await json({ method: "POST", url: `${base}/mcp?die=${encodeURIComponent(join(dir, "died"))}`, body: "{}" }); assert.equal(r.json.n, 1, "answered on a new connection"); assert.ok(spawned.length >= before + 1); await assert.rejects(fetch({ url: `iroh:///${ref({ ticket: "abc", hostKeys })}/${encodeURIComponent(join(dir, "absent.sock"))}/` }), /relay ended .*relay: connect ENOENT/s); }); test("abort, bad input, a missing key", async () => { await assert.rejects(fetch({ url: `${base}/s?ms=2000`, signal: AbortSignal.timeout(200) }), { name: "TimeoutError" }); await assert.rejects(fetch({ url: `iroh:///${ref({ ticket: "abc" })}/shell/mcp` }), /no host keys/); await assert.rejects(fetch({ url: `${base}/`, headers: { "x-a": "b\r\nx: y" } }), /bad header/); await assert.rejects(fetch({ method: "GE T", url: `${base}/` }), /bad method/); await assert.rejects(irohFetch({ key: join(dir, "absent") })({ url: `${base}/` }), /ATLIBS_IROH_KEY/); assert.equal(typeof fetchers({ env: {}, stateDir: dir })["iroh:"], "function"); }); test("an idle relay never keeps the host process up", async () => { const script = `import { spawn } from "node:child_process"; import { irohFetch } from ${JSON.stringify(new URL("./index.js", import.meta.url).href)}; const fetch = irohFetch({ key: ${JSON.stringify(key)}, spawn: (cmd, args, o) => spawn(${JSON.stringify(FAKE)} + cmd, args, o) }); console.log((await fetch({ url: ${JSON.stringify(`${base}/idle`)} })).status);`; const started = Date.now(); const { stdout } = await promisify(execFile)(process.execPath, ["--input-type=module", "-e", script], { timeout: 10000 }); assert.equal(stdout.trim(), "200"); assert.ok(Date.now() - started < 5000, "exited without waiting on the relay"); });