import assert from "node:assert/strict"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { test } from "node:test"; import { nextPageUrl, parseArgs, requestJson, resolveRequestUrl, runRequest, selectConnection, } from "./atlassian-request.mjs"; function response(body, status = 200, headers = { "content-type": "application/json" }) { return new Response(typeof body === "string" ? body : JSON.stringify(body), { status, headers }); } test("parses GET defaults and pagination options", () => { assert.deepEqual(parseArgs(["/wiki/api/v2/pages", "--paginate", "--max-pages", "4", "--profile", "work"]), { method: "GET", path: "/wiki/api/v2/pages", bodyFile: undefined, profile: "work", paginate: true, maxPages: 4, allowMutation: false, }); }); test("requires explicit mutation permission", () => { assert.throws(() => parseArgs(["POST", "/wiki/api/v2/pages"]), /requires --allow-mutation/); assert.equal(parseArgs(["POST", "/wiki/api/v2/pages", "--allow-mutation"]).method, "POST"); assert.throws(() => parseArgs(["GET", "/x", "--body", "body.json"]), /GET does not accept --body/); assert.throws(() => parseArgs(["PATCH", "/x", "--allow-mutation", "--paginate"]), /--paginate requires GET/); }); test("selects profiles with environment overrides", () => { const profiles = [ { name: "one", base_url: "https://one.example/", email: "one@example.com" }, { name: "two", base_url: "https://two.example/", email: "two@example.com", is_default: true }, ]; assert.deepEqual(selectConnection(profiles), { baseUrl: "https://two.example/", email: "two@example.com" }); assert.deepEqual(selectConnection(profiles, "one"), { baseUrl: "https://one.example/", email: "one@example.com" }); assert.deepEqual(selectConnection([], undefined, { ATLASSIAN_BASE_URL: "https://env.example/", ATLASSIAN_EMAIL: "env@example.com", }), { baseUrl: "https://env.example/", email: "env@example.com" }); assert.throws(() => selectConnection(profiles, "missing"), /Unknown Atlassian profile/); }); test("keeps credentials on the configured HTTPS origin", () => { assert.equal(resolveRequestUrl("/wiki/api/v2/pages", "https://site.example/").href, "https://site.example/wiki/api/v2/pages"); assert.throws(() => resolveRequestUrl("https://evil.example/x", "https://site.example/"), /Refusing to send/); assert.throws(() => resolveRequestUrl("/x", "http://site.example/"), /Refusing non-HTTPS/); assert.equal(resolveRequestUrl("/x", "http://127.0.0.1:1234/").href, "http://127.0.0.1:1234/x"); }); test("sends authenticated GET and parses JSON", async () => { let observed; const result = await runRequest( parseArgs(["/wiki/api/v2/pages/1"]), { baseUrl: "https://site.example/", email: "person@example.com" }, "secret", async (url, init) => { observed = { url: url.href, init }; return response({ id: "1" }); }, ); assert.deepEqual(result, { id: "1" }); assert.equal(observed.url, "https://site.example/wiki/api/v2/pages/1"); assert.equal(observed.init.method, "GET"); assert.equal(observed.init.headers.Authorization, `Basic ${Buffer.from("person@example.com:secret").toString("base64")}`); assert.equal(observed.init.redirect, "manual"); }); test("sends JSON body files for explicitly allowed mutations", async (t) => { const dir = mkdtempSync(path.join(tmpdir(), "atlassian-request-")); t.after(() => rmSync(dir, { recursive: true, force: true })); const bodyFile = path.join(dir, "body.json"); writeFileSync(bodyFile, '{"value": 1}\n'); let observed; const options = parseArgs(["POST", "/wiki/api/v2/pages", "--body", bodyFile, "--allow-mutation"]); const result = await runRequest(options, { baseUrl: "https://site.example/", email: "person@example.com" }, "secret", async (_url, init) => { observed = init; return response({ created: true }, 201); }); assert.deepEqual(result, { created: true }); assert.equal(observed.body, '{"value":1}'); assert.equal(observed.headers["Content-Type"], "application/json"); }); test("follows linked pagination and returns complete response pages", async () => { const seen = []; const result = await runRequest( parseArgs(["/wiki/api/v2/pages?limit=1", "--paginate"]), { baseUrl: "https://site.example/", email: "person@example.com" }, "secret", async (url) => { seen.push(url.href); return seen.length === 1 ? response({ results: [{ id: "1" }], _links: { next: "/wiki/api/v2/pages?limit=1&cursor=next" } }) : response({ results: [{ id: "2" }], _links: {} }); }, ); assert.equal(result.length, 2); assert.deepEqual(result.flatMap((page) => page.results), [{ id: "1" }, { id: "2" }]); assert.equal(seen[1], "https://site.example/wiki/api/v2/pages?limit=1&cursor=next"); }); test("recognizes numeric and token pagination", () => { assert.equal(nextPageUrl({ startAt: 0, maxResults: 50, total: 51 }, new URL("https://site.example/rest?startAt=0")).href, "https://site.example/rest?startAt=50"); assert.equal(nextPageUrl({ nextPageToken: "abc", isLast: false }, new URL("https://site.example/rest")).href, "https://site.example/rest?nextPageToken=abc"); assert.equal(nextPageUrl({ startAt: 50, maxResults: 50, total: 51 }, new URL("https://site.example/rest")), undefined); }); test("fails instead of returning partial pagination", async () => { const options = parseArgs(["/wiki/api/v2/pages", "--paginate", "--max-pages", "1"]); await assert.rejects( runRequest(options, { baseUrl: "https://site.example/", email: "person@example.com" }, "secret", async () => response({ _links: { next: "/next" } })), /Pagination exceeded/, ); }); test("reports HTTP failures, rejects redirects, and preserves text responses", async () => { await assert.rejects(requestJson(new URL("https://site.example/x"), { method: "GET" }, async () => response("denied", 403, { "content-type": "text/plain" })), /HTTP 403\ndenied/); await assert.rejects(requestJson(new URL("https://site.example/x"), { method: "GET" }, async () => response("", 302)), /redirect 302 refused/); assert.equal(await requestJson(new URL("https://site.example/x"), { method: "GET" }, async () => response("plain", 200, { "content-type": "text/plain" })), "plain"); });