File size: 2,650 Bytes
5e77d76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
 * Step-1 hotfix regression tests for the Settings API fetch guard.
 *
 * The reported "Unexpected token '<', "<!doctype "... is not valid JSON" error
 * came from parsing the SPA's index.html as JSON when a Settings API route was
 * not forwarded to the backend. fetchApiJson must turn that into an actionable
 * message and never throw a raw JSON.parse error.
 */
import { afterEach, describe, expect, it, vi } from "vitest";
import { fetchApiJson } from "./apiFetch";

function mockFetch(res: { ok?: boolean; status?: number; contentType?: string; body?: unknown }) {
  const headers = new Headers();
  if (res.contentType) headers.set("content-type", res.contentType);
  const ok = res.ok ?? true;
  const status = res.status ?? (ok ? 200 : 500);
  vi.stubGlobal(
    "fetch",
    vi.fn(async () => ({
      ok,
      status,
      headers,
      json: async () => {
        if (res.body === undefined) throw new SyntaxError("Unexpected token '<'");
        return res.body;
      },
    })),
  );
}

afterEach(() => {
  vi.unstubAllGlobals();
  vi.restoreAllMocks();
});

describe("fetchApiJson", () => {
  it("returns parsed JSON on a normal application/json response", async () => {
    mockFetch({ ok: true, status: 200, contentType: "application/json", body: { ok: true, providers: {} } });
    await expect(fetchApiJson("https://homepilot.ruslanmv.com/providers")).resolves.toEqual({
      ok: true,
      providers: {},
    });
  });

  it("throws an actionable deployment message when HTML is returned (SPA fallback)", async () => {
    mockFetch({ ok: true, status: 200, contentType: "text/html; charset=utf-8" });
    await expect(fetchApiJson("https://homepilot.ruslanmv.com/providers")).rejects.toThrow(
      /providers API is unavailable on this deployment/i,
    );
  });

  it("never surfaces a raw JSON parse error for an HTML body", async () => {
    mockFetch({ ok: true, status: 200, contentType: "text/html" });
    await expect(fetchApiJson("https://homepilot.ruslanmv.com/models")).rejects.not.toThrow(
      /Unexpected token/i,
    );
  });

  it("surfaces the JSON error message on a non-OK JSON response", async () => {
    mockFetch({ ok: false, status: 502, contentType: "application/json", body: { message: "cloud down" } });
    await expect(fetchApiJson("https://homepilot.ruslanmv.com/providers")).rejects.toThrow("cloud down");
  });

  it("includes the HTTP status when a non-JSON error has no body message", async () => {
    mockFetch({ ok: false, status: 404, contentType: "text/plain" });
    await expect(fetchApiJson("https://homepilot.ruslanmv.com/providers")).rejects.toThrow(/HTTP 404/);
  });
});