Skip to content
File

Blob: tests/client/api/http.test.ts

typescript113 lines
1import { describe, expect, it, vi } from "vitest";
2import { ADMIN_ACCESS_DISABLED_ERROR_CODE } from "@/shared/contracts";
3import { D1_BOOKMARK_HEADER } from "@/shared/d1";
4import { getStoredBookmark, setStoredBookmark } from "@/client/lib/api/bookmarks";
5import { fetchWithSession, parseError, request } from "@/client/lib/api/http";
6import { fetchMock } from "../../setup/client";
7 
8describe("client api http helpers", () => {
9 it("adds the authorization header when a token is provided", async () => {
10 fetchMock.mockResolvedValue(
11 new Response(JSON.stringify({ ok: true }), {
12 headers: {
13 "content-type": "application/json",
14 },
15 }),
16 );
17 
18 await fetchWithSession("/api/test", {
19 token: "tok_user",
20 });
21 
22 const [, requestInit] = fetchMock.mock.calls[0] ?? [];
23 expect(requestInit).toBeDefined();
24 expect((requestInit?.headers as Headers).get("authorization")).toBe("Bearer tok_user");
25 });
26 
27 it("sends a stored bookmark and applies the response bookmark", async () => {
28 setStoredBookmark("inbox:reader@mail.test", "bookmark-before");
29 fetchMock.mockResolvedValue(
30 new Response(JSON.stringify({ ok: true }), {
31 headers: {
32 "content-type": "application/json",
33 [D1_BOOKMARK_HEADER]: "bookmark-after",
34 },
35 }),
36 );
37 const onBookmark = vi.fn<(bookmark: string | null) => void>();
38 
39 await fetchWithSession("/api/test", {
40 bookmarkScope: "inbox:reader@mail.test",
41 onBookmark,
42 });
43 
44 const [, requestInit] = fetchMock.mock.calls[0] ?? [];
45 expect(requestInit).toBeDefined();
46 expect((requestInit?.headers as Headers).get(D1_BOOKMARK_HEADER)).toBe("bookmark-before");
47 expect(getStoredBookmark("inbox:reader@mail.test")).toBe("bookmark-after");
48 expect(onBookmark).toHaveBeenCalledWith("bookmark-after");
49 });
50 
51 it("parses structured api errors", async () => {
52 const response = new Response(
53 JSON.stringify({
54 error: "Forbidden",
55 code: ADMIN_ACCESS_DISABLED_ERROR_CODE,
56 }),
57 {
58 status: 503,
59 statusText: "Service Unavailable",
60 headers: {
61 "content-type": "application/json",
62 },
63 },
64 );
65 
66 await expect(parseError(response)).resolves.toEqual({
67 code: ADMIN_ACCESS_DISABLED_ERROR_CODE,
68 message: "Forbidden",
69 });
70 });
71 
72 it("falls back to the response status text for non-structured errors", async () => {
73 const response = new Response("broken", {
74 status: 500,
75 statusText: "Server exploded",
76 headers: {
77 "content-type": "text/plain",
78 },
79 });
80 
81 await expect(parseError(response)).resolves.toEqual({
82 message: "500 Server exploded",
83 });
84 });
85 
86 it("requests json payloads and decodes successful responses", async () => {
87 const decoder = {
88 assertDecode: vi.fn((value: unknown) => value as { ok: boolean }),
89 };
90 fetchMock.mockResolvedValue(
91 new Response(JSON.stringify({ ok: true }), {
92 headers: {
93 "content-type": "application/json",
94 },
95 }),
96 );
97 
98 const payload = await request("/api/test", decoder, {
99 method: "POST",
100 body: JSON.stringify({ hello: "world" }),
101 });
102 
103 expect(payload).toEqual({ ok: true });
104 const [, requestInit] = fetchMock.mock.calls[0] ?? [];
105 expect(requestInit).toBeDefined();
106 expect((requestInit?.headers as Headers).get("accept")).toBe("application/json");
107 expect((requestInit?.headers as Headers).get("content-type")).toBe("application/json");
108 expect(requestInit?.method).toBe("POST");
109 expect(requestInit?.body).toBe(JSON.stringify({ hello: "world" }));
110 expect(decoder.assertDecode).toHaveBeenCalledWith({ ok: true });
111 });
112});