Initial
This commit is contained in:
commit
b43955e0d9
162 changed files with 15425 additions and 0 deletions
84
tests/unit/client.test.ts
Normal file
84
tests/unit/client.test.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { api, ApiError, setUnauthorizedHandler } from '$api';
|
||||
|
||||
function mockFetch(status: number, body: unknown) {
|
||||
return vi.fn(
|
||||
async () =>
|
||||
new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { 'content-type': 'application/json' }
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
document.cookie = 'csrftoken=abc';
|
||||
});
|
||||
|
||||
describe('api client', () => {
|
||||
it('prepends the base URL and returns parsed JSON', async () => {
|
||||
vi.stubGlobal('fetch', mockFetch(200, { username: 'demo' }));
|
||||
const out = await api.get<{ username: string }>('/whoami/');
|
||||
expect(out).toEqual({ username: 'demo' });
|
||||
const url = (fetch as unknown as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(String(url)).toContain('/api/whoami/');
|
||||
});
|
||||
|
||||
it('throws ApiError carrying the backend detail on 4xx', async () => {
|
||||
vi.stubGlobal('fetch', mockFetch(400, { detail: 'Invalid credentials.' }));
|
||||
await expect(api.post('/login/', {})).rejects.toMatchObject({
|
||||
name: 'ApiError',
|
||||
status: 400,
|
||||
detail: 'Invalid credentials.'
|
||||
});
|
||||
});
|
||||
|
||||
// The backend does not speak one error shape: DMR's response validator emits
|
||||
// `detail` as a list of {msg}, Pydantic bodies arrive as a bare list, and a
|
||||
// few endpoints use DRF-style field maps. Stringifying any of those yields
|
||||
// "[object Object]" in the UI, so each shape needs flattening.
|
||||
it('flattens a list-of-objects detail from DMR response validation', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
mockFetch(422, { detail: [{ msg: 'Returned status code 400 is not specified', type: 'value_error' }] })
|
||||
);
|
||||
await expect(api.post('/register/', {})).rejects.toMatchObject({
|
||||
detail: 'Returned status code 400 is not specified'
|
||||
});
|
||||
});
|
||||
|
||||
it('flattens a bare list body of Pydantic errors', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
mockFetch(400, [
|
||||
{ msg: 'Username must be at least 3 characters', loc: ['username'] },
|
||||
{ msg: 'Invalid email format', loc: ['email'] }
|
||||
])
|
||||
);
|
||||
await expect(api.post('/register/', {})).rejects.toMatchObject({
|
||||
detail: 'Username must be at least 3 characters; Invalid email format'
|
||||
});
|
||||
});
|
||||
|
||||
it('flattens a DRF-style field error map', async () => {
|
||||
vi.stubGlobal('fetch', mockFetch(400, { non_field_errors: ['Point already saved.'] }));
|
||||
await expect(api.post('/points/', {})).rejects.toMatchObject({
|
||||
detail: 'Point already saved.'
|
||||
});
|
||||
});
|
||||
|
||||
it('never surfaces [object Object] for an unrecognised shape', async () => {
|
||||
vi.stubGlobal('fetch', mockFetch(400, { detail: { nested: { deep: true } } }));
|
||||
await expect(api.post('/register/', {})).rejects.toMatchObject({
|
||||
detail: 'Request failed (400)'
|
||||
});
|
||||
});
|
||||
|
||||
it('invokes the unauthorized handler on 401', async () => {
|
||||
const handler = vi.fn();
|
||||
setUnauthorizedHandler(handler);
|
||||
vi.stubGlobal('fetch', mockFetch(401, { detail: 'Unauthorized' }));
|
||||
await expect(api.get('/whoami/')).rejects.toBeInstanceOf(ApiError);
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue