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 e58f6ad855ccc74b596cba27006e8d1db96acbb5 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});