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 / load.test.ts
4.0 kB 122 lines
1import { describe, expect, it, vi } from 'vitest'; 2import { ClientResponseError } from './client'; 3import { createRequestCache, httpStatusFor, parallel, toHttpError } from './load'; 4 5const cre = (status: number, error: string, message?: string): ClientResponseError => 6 new ClientResponseError({ status, data: { error, message } }); 7 8describe('httpStatusFor', () => { 9 it('passes through an in-band XRPC status', () => { 10 expect(httpStatusFor(cre(404, 'RecordNotFound'))).toBe(404); 11 }); 12 13 it('prefers an in-band status over the error-name mapping', () => { 14 // status 503 is in [400,599] so it wins even though the name maps to 404. 15 expect(httpStatusFor(cre(503, 'RecordNotFound'))).toBe(503); 16 }); 17 18 it('maps the error name when the status is out of the 400-599 band', () => { 19 expect(httpStatusFor(cre(200, 'RecordNotFound'))).toBe(404); 20 expect(httpStatusFor(cre(200, 'InvalidRequest'))).toBe(400); 21 expect(httpStatusFor(cre(200, 'UpstreamFailed'))).toBe(502); 22 expect(httpStatusFor(cre(200, 'UpstreamGone'))).toBe(502); 23 expect(httpStatusFor(cre(200, 'InvalidRecord'))).toBe(502); 24 expect(httpStatusFor(cre(200, 'Overloaded'))).toBe(503); 25 expect(httpStatusFor(cre(200, 'SomethingElse'))).toBe(500); 26 }); 27 28 it('maps any non-XRPC cause to 500', () => { 29 expect(httpStatusFor(new Error('boom'))).toBe(500); 30 expect(httpStatusFor('a string')).toBe(500); 31 expect(httpStatusFor(undefined)).toBe(500); 32 expect(httpStatusFor({ status: 404 })).toBe(500); 33 }); 34}); 35 36describe('toHttpError', () => { 37 it('throws a SvelteKit error with the mapped status and the XRPC description', () => { 38 let thrown: unknown; 39 try { 40 toHttpError(cre(404, 'RecordNotFound', 'no such repo')); 41 } catch (e) { 42 thrown = e; 43 } 44 expect(thrown).toMatchObject({ status: 404, body: { message: 'no such repo' } }); 45 }); 46 47 it('falls back to the error name when the description is absent', () => { 48 let thrown: unknown; 49 try { 50 toHttpError(cre(200, 'Overloaded')); 51 } catch (e) { 52 thrown = e; 53 } 54 expect(thrown).toMatchObject({ status: 503, body: { message: 'Overloaded' } }); 55 }); 56 57 it('uses the fallback message + 500 for a non-XRPC cause', () => { 58 let thrown: unknown; 59 try { 60 toHttpError(new Error('boom'), 'could not load'); 61 } catch (e) { 62 thrown = e; 63 } 64 expect(thrown).toMatchObject({ status: 500, body: { message: 'could not load' } }); 65 }); 66}); 67 68describe('parallel', () => { 69 it('preserves key/value pairing regardless of settle order', async () => { 70 // deferred across a few microtasks (no real timer) so it settles after `fast`. 71 const slow = Promise.resolve() 72 .then(() => Promise.resolve()) 73 .then(() => 'slow'); 74 const result = await parallel({ fast: Promise.resolve('fast'), slow }); 75 expect(result).toEqual({ fast: 'fast', slow: 'slow' }); 76 }); 77 78 it('resolves to an empty object for no tasks', async () => { 79 await expect(parallel({})).resolves.toEqual({}); 80 }); 81 82 it('rejects if any input promise rejects', async () => { 83 await expect( 84 parallel({ a: Promise.resolve(1), b: Promise.reject(new Error('boom')) }) 85 ).rejects.toThrow('boom'); 86 }); 87}); 88 89describe('createRequestCache', () => { 90 it('invokes the loader once per key and shares the value', async () => { 91 const cache = createRequestCache(); 92 const load = vi.fn(async () => 42); 93 94 const first = await cache.run('k', load); 95 const second = await cache.run('k', load); 96 97 expect(first).toBe(42); 98 expect(second).toBe(42); 99 expect(load).toHaveBeenCalledTimes(1); 100 }); 101 102 it('shares one in-flight promise for concurrent same-key calls', () => { 103 const cache = createRequestCache(); 104 const load = vi.fn(async () => 'v'); 105 106 const a = cache.run('k', load); 107 const b = cache.run('k', load); 108 109 expect(a).toBe(b); 110 expect(load).toHaveBeenCalledTimes(1); 111 }); 112 113 it('runs distinct keys independently', async () => { 114 const cache = createRequestCache(); 115 const load = vi.fn(async (key: string) => key.toUpperCase()); 116 117 await cache.run('a', () => load('a')); 118 await cache.run('b', () => load('b')); 119 120 expect(load).toHaveBeenCalledTimes(2); 121 }); 122});