import ky from "ky"; import { describe, expect, it, vi } from "vitest"; import { normalizeApiRequestError, REQUEST_UPLIFTS_ROLE_DENIAL_MESSAGE } from "@/api/api-error"; const RESOURCE_URL = "https://example.test/api/resource"; function createClient(fetchImpl: typeof fetch) { return ky.create({ fetch: fetchImpl, retry: 0, timeout: 50, hooks: { beforeError: [({ error }) => normalizeApiRequestError(error)] }, }); } describe("normalizeApiRequestError", () => { it("surfaces Ky's pre-parsed JSON body for HTTP errors via the hook", async () => { const fetchImpl = vi.fn(() => Promise.resolve( new Response(JSON.stringify({ message: "Validation failed" }), { status: 422, headers: { "Content-Type": "application/json" }, }), ), ) as unknown as typeof fetch; await expect(createClient(fetchImpl).get(RESOURCE_URL).json()).rejects.toThrow( "Validation failed", ); }); it("preserves only the allowlisted public denial for 403 responses", async () => { const allowedFetch = vi.fn(() => Promise.resolve( new Response(JSON.stringify({ message: REQUEST_UPLIFTS_ROLE_DENIAL_MESSAGE }), { status: 403, headers: { "Content-Type": "application/json" }, }), ), ) as unknown as typeof fetch; const unrelatedFetch = vi.fn(() => Promise.resolve( new Response(JSON.stringify({ message: "accountId 42 is forbidden" }), { status: 403, headers: { "Content-Type": "application/json" }, }), ), ) as unknown as typeof fetch; await expect(createClient(allowedFetch).get(RESOURCE_URL).json()).rejects.toThrow( REQUEST_UPLIFTS_ROLE_DENIAL_MESSAGE, ); await expect(createClient(unrelatedFetch).get(RESOURCE_URL).json()).rejects.toThrow( "You do not have permission to perform this action.", ); }); it("reports a connection failure for network errors", async () => { const fetchImpl = vi.fn(() => Promise.reject(new TypeError("fetch failed")), ) as unknown as typeof fetch; await expect(createClient(fetchImpl).get(RESOURCE_URL).json()).rejects.toThrow( "No response from server. Please check your connection.", ); }); it("returns a friendly timeout message", async () => { const fetchImpl = vi.fn((_input: RequestInfo | URL, init?: RequestInit) => { const signal = init?.signal; return new Promise((_resolve, reject) => { signal?.addEventListener("abort", () => reject(signal?.reason)); }); }) as unknown as typeof fetch; await expect(createClient(fetchImpl).get(RESOURCE_URL).json()).rejects.toThrow( "The request timed out. Please try again.", ); }); });