This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / web / src / lib / api / client.test.ts
4.4 kB 109 lines
1import { describe, expect, it, vi, type Mock } from "vitest"; 2import { ClientResponseError, createBobbinClient, type BobbinContext } from "./client"; 3import { jsonGet, rawGet } from "./_request"; 4 5const jsonResponse = (body: unknown, init: ResponseInit = {}): Response => 6 new Response(JSON.stringify(body), { 7 status: 200, 8 headers: { "content-type": "application/json" }, 9 ...init 10 }); 11 12const makeCtx = ( 13 fetchMock: typeof globalThis.fetch, 14 serviceUrl = "https://bobbin.test" 15): BobbinContext => createBobbinClient({ serviceUrl, fetch: fetchMock }); 16 17const fetchedUrl = (mock: Mock<typeof globalThis.fetch>, n = 0): URL => 18 new URL(String(mock.mock.calls[n][0])); 19 20describe("createBobbinClient", () => { 21 it("normalizes trailing slashes off the service url", () => { 22 const ctx = makeCtx(vi.fn(), "https://bobbin.test///"); 23 expect(ctx.serviceUrl).toBe("https://bobbin.test"); 24 }); 25}); 26 27describe("jsonGet URL construction", () => { 28 it("appends an array param as repeated keys", async () => { 29 const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue(jsonResponse({ ok: 1 })); 30 await jsonGet(makeCtx(fetchMock), "sh.tangled.repo.getRepos", { repos: ["a", "b"] }); 31 const url = fetchedUrl(fetchMock); 32 expect(url.searchParams.getAll("repos")).toEqual(["a", "b"]); 33 expect(url.search).toBe("?repos=a&repos=b"); 34 }); 35 36 it("omits undefined params entirely", async () => { 37 const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue(jsonResponse({ ok: 1 })); 38 await jsonGet(makeCtx(fetchMock), "sh.tangled.x", { keep: "yes", drop: undefined }); 39 const url = fetchedUrl(fetchMock); 40 expect(url.searchParams.has("drop")).toBe(false); 41 expect(url.searchParams.get("keep")).toBe("yes"); 42 }); 43 44 it("resolves the query against the normalized origin", async () => { 45 const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue(jsonResponse({ ok: 1 })); 46 await jsonGet(makeCtx(fetchMock, "https://bobbin.test/"), "sh.tangled.x", { a: "1" }); 47 expect(fetchedUrl(fetchMock).href).toBe("https://bobbin.test/xrpc/sh.tangled.x?a=1"); 48 }); 49}); 50 51describe("jsonGet request headers & signal", () => { 52 it("sends accept: application/json and merges caller headers + signal", async () => { 53 const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue(jsonResponse({ ok: 1 })); 54 const controller = new AbortController(); 55 await jsonGet(makeCtx(fetchMock), "sh.tangled.x", undefined, { 56 headers: { "x-custom": "1" }, 57 signal: controller.signal 58 }); 59 const init = fetchMock.mock.calls[0][1] as RequestInit; 60 expect(init.headers).toMatchObject({ accept: "application/json", "x-custom": "1" }); 61 expect(init.signal).toBe(controller.signal); 62 }); 63}); 64 65describe("jsonGet responses", () => { 66 it("throws ClientResponseError carrying status + error/description for a JSON error body", async () => { 67 const fetchMock = vi 68 .fn<typeof globalThis.fetch>() 69 .mockResolvedValue( 70 jsonResponse({ error: "RecordNotFound", message: "no such repo" }, { status: 404 }) 71 ); 72 const err = await jsonGet(makeCtx(fetchMock), "sh.tangled.x").catch((e: unknown) => e); 73 expect(err).toBeInstanceOf(ClientResponseError); 74 const cre = err as ClientResponseError; 75 expect(cre.status).toBe(404); 76 expect(cre.error).toBe("RecordNotFound"); 77 expect(cre.description).toBe("no such repo"); 78 }); 79 80 it("falls back to the status line for a non-JSON error body (does not hang)", async () => { 81 const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( 82 new Response("<html>502 Bad Gateway</html>", { 83 status: 502, 84 statusText: "Bad Gateway" 85 }) 86 ); 87 const err = await jsonGet(makeCtx(fetchMock), "sh.tangled.x").catch((e: unknown) => e); 88 expect(err).toBeInstanceOf(ClientResponseError); 89 const cre = err as ClientResponseError; 90 expect(cre.status).toBe(502); 91 expect(cre.error).toBe("XRPCError"); 92 expect(cre.description).toBe("Bad Gateway"); 93 }); 94}); 95 96describe("rawGet", () => { 97 it("throws ClientResponseError on a non-2xx status", async () => { 98 const fetchMock = vi 99 .fn<typeof globalThis.fetch>() 100 .mockResolvedValue( 101 jsonResponse({ error: "UpstreamFailed", message: "knot down" }, { status: 502 }) 102 ); 103 const err = await rawGet(makeCtx(fetchMock), "sh.tangled.repo.archive").catch( 104 (e: unknown) => e 105 ); 106 expect(err).toBeInstanceOf(ClientResponseError); 107 expect((err as ClientResponseError).status).toBe(502); 108 }); 109});