WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
at atb-59-theme-editor 123 lines 3.6 kB view raw
1import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 2 3const mockFetch = vi.fn(); 4 5describe("fetchApi", () => { 6 beforeEach(() => { 7 vi.stubGlobal("fetch", mockFetch); 8 vi.stubEnv("APPVIEW_URL", "http://localhost:3000"); 9 vi.resetModules(); 10 }); 11 12 afterEach(() => { 13 vi.unstubAllGlobals(); 14 vi.unstubAllEnvs(); 15 mockFetch.mockReset(); 16 }); 17 18 async function loadFetchApi() { 19 const mod = await import("../api.js"); 20 return mod.fetchApi; 21 } 22 23 it("calls the correct URL", async () => { 24 mockFetch.mockResolvedValueOnce({ 25 ok: true, 26 json: () => Promise.resolve({ data: "test" }), 27 }); 28 29 const fetchApi = await loadFetchApi(); 30 await fetchApi("/categories"); 31 32 expect(mockFetch).toHaveBeenCalledOnce(); 33 const calledUrl = mockFetch.mock.calls[0][0]; 34 expect(calledUrl).toBe("http://localhost:3000/api/categories"); 35 }); 36 37 it("returns parsed JSON on success", async () => { 38 const expected = { categories: [{ id: 1, name: "General" }] }; 39 mockFetch.mockResolvedValueOnce({ 40 ok: true, 41 json: () => Promise.resolve(expected), 42 }); 43 44 const fetchApi = await loadFetchApi(); 45 const result = await fetchApi("/categories"); 46 expect(result).toEqual(expected); 47 }); 48 49 it("throws on non-ok response", async () => { 50 mockFetch.mockResolvedValueOnce({ 51 ok: false, 52 status: 500, 53 statusText: "Internal Server Error", 54 }); 55 56 const fetchApi = await loadFetchApi(); 57 await expect(fetchApi("/fail")).rejects.toThrow( 58 "AppView API error: 500 Internal Server Error" 59 ); 60 }); 61 62 it("throws on 404 response", async () => { 63 mockFetch.mockResolvedValueOnce({ 64 ok: false, 65 status: 404, 66 statusText: "Not Found", 67 }); 68 69 const fetchApi = await loadFetchApi(); 70 await expect(fetchApi("/missing")).rejects.toThrow( 71 "AppView API error: 404 Not Found" 72 ); 73 }); 74 75 it("forwards cookieHeader as Cookie header when provided", async () => { 76 mockFetch.mockResolvedValueOnce({ 77 ok: true, 78 json: () => Promise.resolve({}), 79 }); 80 81 const fetchApi = await loadFetchApi(); 82 await fetchApi("/boards", { cookieHeader: "atbb_session=mytoken" }); 83 84 const [, init] = mockFetch.mock.calls[0] as [string, RequestInit]; 85 expect((init.headers as Record<string, string>)["Cookie"]).toBe( 86 "atbb_session=mytoken" 87 ); 88 }); 89 90 it("does not set Cookie header when cookieHeader is not provided", async () => { 91 mockFetch.mockResolvedValueOnce({ 92 ok: true, 93 json: () => Promise.resolve({}), 94 }); 95 96 const fetchApi = await loadFetchApi(); 97 await fetchApi("/boards"); 98 99 const [, init] = mockFetch.mock.calls[0] as [string, RequestInit]; 100 expect((init.headers as Record<string, string>)["Cookie"]).toBeUndefined(); 101 }); 102 103 it("throws a network error with descriptive message when AppView is unreachable", async () => { 104 mockFetch.mockRejectedValueOnce(new Error("fetch failed: ECONNREFUSED")); 105 106 const fetchApi = await loadFetchApi(); 107 await expect(fetchApi("/boards")).rejects.toThrow( 108 "AppView network error: fetch failed: ECONNREFUSED" 109 ); 110 }); 111 112 it("throws a response error when AppView returns malformed JSON", async () => { 113 mockFetch.mockResolvedValueOnce({ 114 ok: true, 115 json: () => Promise.reject(new SyntaxError("Unexpected token '<'")), 116 }); 117 118 const fetchApi = await loadFetchApi(); 119 await expect(fetchApi("/boards")).rejects.toThrow( 120 "AppView response error: invalid JSON from /boards" 121 ); 122 }); 123});