import { describe, expect, it } from "vitest"; import { replayFetch } from "./replay.js"; import type { HttpExchangeFixture } from "./types.js"; const fixture: HttpExchangeFixture = { request: { method: "POST", url: "https://api.example.com/v1/chat", headers: { "content-type": "application/json", authorization: "Bearer sk-abc" }, body: '{"model":"gpt-4"}', }, response: { status: 200, statusText: "OK", headers: { "content-type": "application/json", "x-request-id": "req-123" }, body: '{"choices":[{"text":"hello"}]}', }, meta: { model: "gpt-4", capturedAt: 1700000000000 }, }; describe("replayFetch", () => { it("returns a Response with fixture status/statusText/headers", async () => { const { fetch } = replayFetch(fixture); const res = await fetch("https://api.example.com/v1/chat", { method: "POST" }); expect(res.status).toBe(200); expect(res.statusText).toBe("OK"); expect(res.headers.get("content-type")).toBe("application/json"); expect(res.headers.get("x-request-id")).toBe("req-123"); }); it("body reads exactly fixture.response.body", async () => { const { fetch } = replayFetch(fixture); const res = await fetch("https://api.example.com/v1/chat"); expect(await res.text()).toBe('{"choices":[{"text":"hello"}]}'); }); it("chunkBytes splits body into correct number of chunks and reassembles", async () => { const body = "abcdefghij"; // 10 bytes const fx: HttpExchangeFixture = { ...fixture, response: { ...fixture.response, body }, }; const { fetch } = replayFetch(fx, { chunkBytes: 3 }); const res = await fetch("https://x"); const responseBody = res.body; if (!responseBody) throw new Error("body is null"); const reader = responseBody.getReader(); const chunks: Uint8Array[] = []; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); } expect(chunks).toHaveLength(4); // 3+3+3+1 let reassembled = ""; for (const c of chunks) reassembled += new TextDecoder().decode(c); expect(reassembled).toBe("abcdefghij"); }); it("getCapturedRequest returns undefined before first call", () => { const { getCapturedRequest } = replayFetch(fixture); expect(getCapturedRequest()).toBeUndefined(); }); it("getCapturedRequest returns the request after fetch", async () => { const { fetch, getCapturedRequest } = replayFetch(fixture); await fetch("https://api.example.com/v1/chat", { method: "POST", headers: { "content-type": "application/json" }, body: '{"prompt":"hi"}', }); const captured = getCapturedRequest(); expect(captured).toBeDefined(); expect(captured?.method).toBe("POST"); expect(captured?.url).toBe("https://api.example.com/v1/chat"); expect(captured?.headers["content-type"]).toBe("application/json"); expect(captured?.body).toBe('{"prompt":"hi"}'); }); it("getCapturedRequest updates on subsequent calls", async () => { const { fetch, getCapturedRequest } = replayFetch(fixture); await fetch("https://first", { method: "GET" }); expect(getCapturedRequest()?.url).toBe("https://first"); await fetch("https://second", { method: "PUT" }); expect(getCapturedRequest()?.url).toBe("https://second"); expect(getCapturedRequest()?.method).toBe("PUT"); }); it("captures URL and method from Request object input", async () => { const { fetch, getCapturedRequest } = replayFetch(fixture); const req = new Request("https://from-request.com/path", { method: "DELETE" }); await fetch(req); expect(getCapturedRequest()?.url).toBe("https://from-request.com/path"); expect(getCapturedRequest()?.method).toBe("DELETE"); }); it("defaults method to GET when no init", async () => { const { fetch, getCapturedRequest } = replayFetch(fixture); await fetch("https://x"); expect(getCapturedRequest()?.method).toBe("GET"); }); it("handles null request body", async () => { const { fetch, getCapturedRequest } = replayFetch(fixture); await fetch("https://x", { method: "GET" }); expect(getCapturedRequest()?.body).toBeNull(); }); it("handles headers as array of tuples", async () => { const { fetch, getCapturedRequest } = replayFetch(fixture); await fetch("https://x", { headers: [["x-custom", "val"]], }); expect(getCapturedRequest()?.headers["x-custom"]).toBe("val"); }); it("handles headers as Headers object", async () => { const { fetch, getCapturedRequest } = replayFetch(fixture); const h = new Headers({ "x-h": "v" }); await fetch("https://x", { headers: h }); expect(getCapturedRequest()?.headers["x-h"]).toBe("v"); }); it("works with minimal response (no statusText, status 204, empty body)", async () => { const fx: HttpExchangeFixture = { request: { method: "GET", url: "https://x", headers: {}, body: null }, response: { status: 204, headers: {}, body: "" }, }; const { fetch } = replayFetch(fx); const res = await fetch("https://x"); expect(res.status).toBe(204); expect(await res.text()).toBe(""); }); });