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