import { describe, it, expect, vi, afterEach } from "vitest"; import { NextRequest } from "next/server"; import { errorResponse, resolveOrigin } from "./giellie-http"; const err = (status: unknown, message = "m", extra: Record = {}) => Object.assign(new Error(message), { status }, extra); afterEach(() => { vi.unstubAllEnvs(); vi.restoreAllMocks(); }); describe("errorResponse", () => { it("keeps valid 4xx/5xx statuses", async () => { expect(errorResponse(err(404, "nope")).status).toBe(404); expect(errorResponse(err(401, "Not authenticated.")).status).toBe(401); const res = errorResponse(err(409, "busy")); expect(await res.json()).toEqual({ error: "busy" }); }); it.each([0, NaN, undefined, 99, 302, 600, 404.5, "500"])("falls back to 500 for status %s without throwing", (s) => { vi.spyOn(console, "error").mockImplementation(() => {}); expect(errorResponse(err(s)).status).toBe(500); }); it("turns upstream PocketBase auth/network failures into 502", () => { vi.spyOn(console, "error").mockImplementation(() => {}); for (const s of [0, 401, 403]) { const pb = err(s, "pb down", { name: "ClientResponseError" }); expect(errorResponse(pb).status).toBe(502); } const viaResponse = err(401, "x", { response: { message: "bad token" } }); expect(errorResponse(viaResponse).status).toBe(502); }); it("does not remap PocketBase errors with other statuses", () => { expect(errorResponse(err(404, "x", { name: "ClientResponseError" })).status).toBe(404); }); it("logs >=500 as a plain string and never leaks env secrets", () => { vi.stubEnv("AGENT_API_KEY", "sekret-agent-key-123"); const spy = vi.spyOn(console, "error").mockImplementation(() => {}); errorResponse(err(500, "failed with sekret-agent-key-123")); expect(spy).toHaveBeenCalledTimes(1); expect(spy.mock.calls[0][0]).toBe("[giellie]"); expect(typeof spy.mock.calls[0][1]).toBe("string"); expect(JSON.stringify(spy.mock.calls)).not.toContain("sekret-agent-key-123"); }); it("does not log client errors", () => { const spy = vi.spyOn(console, "error").mockImplementation(() => {}); errorResponse(err(400, "bad")); expect(spy).not.toHaveBeenCalled(); }); }); describe("resolveOrigin", () => { const req = () => new NextRequest("https://req.test/api/v1/giellie/x"); it("prefers AGENT_API_BASE_URL", () => { vi.stubEnv("AGENT_API_BASE_URL", "https://agent.test/some/path"); vi.stubEnv("NEXT_PUBLIC_BASE_URL", "https://public.test"); expect(resolveOrigin(req())).toBe("https://agent.test"); }); it("uses NEXT_PUBLIC_BASE_URL when the agent one is unset", () => { vi.stubEnv("AGENT_API_BASE_URL", ""); vi.stubEnv("NEXT_PUBLIC_BASE_URL", "https://public.test/"); expect(resolveOrigin(req())).toBe("https://public.test"); }); it("falls back to the request origin when unset", () => { vi.stubEnv("AGENT_API_BASE_URL", ""); vi.stubEnv("NEXT_PUBLIC_BASE_URL", ""); expect(resolveOrigin(req())).toBe("https://req.test"); }); it("ignores a scheme-less env value like localhost:4001 and uses the request origin", () => { vi.stubEnv("AGENT_API_BASE_URL", "localhost:4001"); vi.stubEnv("NEXT_PUBLIC_BASE_URL", ""); expect(resolveOrigin(req())).toBe("https://req.test"); }); it("skips a non-http scheme and tries the next candidate", () => { vi.stubEnv("AGENT_API_BASE_URL", "ftp://agent.test"); vi.stubEnv("NEXT_PUBLIC_BASE_URL", "http://public.test:3000/x"); expect(resolveOrigin(req())).toBe("http://public.test:3000"); }); it("falls back when the env value is malformed", () => { vi.stubEnv("AGENT_API_BASE_URL", "not a url"); vi.stubEnv("NEXT_PUBLIC_BASE_URL", ""); expect(resolveOrigin(req())).toBe("https://req.test"); }); });