import test from "node:test"; import assert from "node:assert/strict"; import {ApiError, get, post, stream, multipart, download} from "../public/js/api.js"; test("multipart and downloads share HTTP checks, cancellation and body deadlines", async t => { const form = new FormData(); form.append("image", new Blob(["image"]), "image.png"); t.mock.method(globalThis, "fetch", async (_url, options) => { assert.equal(options.body, form); assert.equal(options.headers["Content-Type"], undefined); return Response.json({id: "uploaded"}); }); assert.deepEqual(await multipart("/uploads", form), {id: "uploaded"}); t.mock.method(globalThis, "fetch", async () => Response.json({error: "Please log in", code: "LOGIN_REQUIRED"}, {status: 403})); const failed = await download("/export"); assert.equal(failed.code, "LOGIN_REQUIRED"); assert.equal(failed.blob, undefined); t.mock.method(globalThis, "fetch", async () => new Response("download content")); assert.equal(await (await download("/export")).blob.text(), "download content"); t.mock.method(globalThis, "fetch", async () => new Response(new ReadableStream())); assert.equal((await download("/stalled", {timeoutMs: 10})).code, "TIMEOUT"); t.mock.method(globalThis, "fetch", async () => new Promise(() => {})); const controller = new AbortController(); const pending = multipart("/upload", form, {signal: controller.signal}); controller.abort(); assert.equal((await pending).code, "ABORTED"); assert.equal((await multipart("/upload", form, {timeoutMs: 10})).code, "TIMEOUT"); }); test("JSON requests retain HTTP metadata, handle empty success, and distinguish bad bodies from network errors", async t => { let response; t.mock.method(globalThis, "fetch", async () => response); for (const status of [204, 205, 200]) { response = new Response(null, {status}); assert.deepEqual(await get("/empty"), {}); } response = new Response("Proxy unavailable", {status: 503, statusText: "Service Unavailable", headers: {"X-Request-ID": "proxy-123"}}); assert.deepEqual(await get("/proxy"), {error: "HTTP 503 Service Unavailable", status: 503, code: "HTTP_503", requestId: "proxy-123"}); response = Response.json({error: "Already exists", code: "USERNAME_TAKEN", status: 400, requestId: "body-123", retryAfter: 5}, {status: 409}); assert.deepEqual(await post("/register", {}), {error: "Already exists", code: "USERNAME_TAKEN", status: 409, requestId: "body-123", retryAfter: 5}); response = Response.json({error: {message: "Forbidden", code: "DENIED"}}, {status: 403}); assert.equal((await get("/private")).code, "DENIED"); response = new Response("not json", {headers: {"X-Request-ID": "invalid-body"}}); assert.deepEqual(await get("/invalid"), {error: "Invalid JSON response.", status: 200, code: "INVALID_RESPONSE", requestId: "invalid-body"}); t.mock.method(globalThis, "fetch", async () => { throw new TypeError("Failed to fetch"); }); assert.equal((await get("/offline")).code, "NETWORK_ERROR"); }); test("JSON requests support caller abort and deadlines across headers and body reads", async t => { let signal; t.mock.method(globalThis, "fetch", async (_url, options) => { signal = options.signal; return new Promise(() => {}); }); const controller = new AbortController(); const cancelled = get("/cancel", {signal: controller.signal}); controller.abort(); assert.equal((await cancelled).code, "ABORTED"); assert.equal((await get("/timeout", {timeoutMs: 10})).code, "TIMEOUT"); assert.equal(signal.aborted, true); t.mock.method(globalThis, "fetch", async () => new Response(new ReadableStream(), {headers: {"X-Request-ID": "slow-body"}})); const bodyTimeout = await get("/slow-body", {timeoutMs: 10}); assert.equal(bodyTimeout.code, "TIMEOUT"); assert.equal(bodyTimeout.status, 200); assert.equal(bodyTimeout.requestId, "slow-body"); }); test("SSE errors reject by default and deliver complete metadata to an explicit handler", async t => { const payload = {error: "Model unavailable", status: 503, code: "MODEL_UNAVAILABLE", requestId: "model-123"}; t.mock.method(globalThis, "fetch", async () => new Response(`event: error\ndata: ${JSON.stringify(payload)}\n\n`)); await assert.rejects(stream("/stream", {}), error => error instanceof ApiError && error.status === 503 && error.code === payload.code && error.requestId === payload.requestId); let handled; await stream("/stream", {}, {onError: error => { handled = error; }}); assert.deepEqual(handled, payload); t.mock.method(globalThis, "fetch", async () => new Response("upstream unavailable", {status: 503, headers: {"X-Request-ID": "proxy-123"}})); await assert.rejects(stream("/stream", {}), error => error.status === 503 && error.code === "HTTP_503" && error.requestId === "proxy-123"); }); test("SSE supports fragmented UTF-8, multiline data, replacements, cancellation and stalled reads", async t => { const received = []; const wire = 'event: chunk\r\ndata: {"chunk":"Hello 🌳"}\r\n\r\nevent: replace\ndata: {"target":"reply",\ndata: "text":"Corrected"}\n\nevent: done\ndata: {"reply":"Corrected"}\n\n'; t.mock.method(globalThis, "fetch", async () => new Response(new ReadableStream({start(controller) { for (const byte of new TextEncoder().encode(wire)) controller.enqueue(Uint8Array.of(byte)); controller.close(); }}))); await stream("/stream", {}, {onChunk: data => received.push(data.chunk), onReplace: data => received.push(data.text), onDone: data => received.push(data.reply)}); assert.deepEqual(received, ["Hello 🌳", "Corrected", "Corrected"]); let cancelled = 0; t.mock.method(globalThis, "fetch", async () => new Response(new ReadableStream({cancel() { cancelled++; }}), {headers: {"X-Request-ID": "stream-123"}})); await assert.rejects(stream("/stream", {}, {}, {timeoutMs: 10}), error => error.code === "TIMEOUT" && error.requestId === "stream-123"); assert.equal(cancelled, 1); const controller = new AbortController(); const pending = stream("/stream", {}, {}, controller.signal); controller.abort(); await assert.rejects(pending, error => error.code === "ABORTED"); });