84 lines
2.9 KiB
TypeScript
84 lines
2.9 KiB
TypeScript
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();
|
|
});
|
|
});
|