166 lines
4.6 KiB
TypeScript
166 lines
4.6 KiB
TypeScript
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
|
|
|
||
|
|
// Create a fresh ApiClient for each test by re-importing
|
||
|
|
let apiClient: typeof import('../api-client').apiClient;
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
vi.restoreAllMocks();
|
||
|
|
// Reset module to get a fresh singleton
|
||
|
|
vi.resetModules();
|
||
|
|
const mod = await import('../api-client');
|
||
|
|
apiClient = mod.apiClient;
|
||
|
|
});
|
||
|
|
|
||
|
|
function mockFetch(body: unknown, status = 200) {
|
||
|
|
return vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
||
|
|
ok: status >= 200 && status < 300,
|
||
|
|
status,
|
||
|
|
statusText: status === 200 ? 'OK' : 'Error',
|
||
|
|
json: () => Promise.resolve(body),
|
||
|
|
} as Response);
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('ApiClient', () => {
|
||
|
|
describe('token management', () => {
|
||
|
|
it('hasToken returns false initially', () => {
|
||
|
|
expect(apiClient.hasToken).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('hasToken returns true after setting token', () => {
|
||
|
|
apiClient.accessToken = 'test-token';
|
||
|
|
expect(apiClient.hasToken).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('includes Authorization header when token is set', async () => {
|
||
|
|
const spy = mockFetch({ data: [] });
|
||
|
|
apiClient.accessToken = 'my-jwt';
|
||
|
|
|
||
|
|
await apiClient.get('/test');
|
||
|
|
|
||
|
|
const headers = spy.mock.calls[0][1]?.headers as Record<string, string>;
|
||
|
|
expect(headers['Authorization']).toBe('Bearer my-jwt');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('omits Authorization header when no token', async () => {
|
||
|
|
const spy = mockFetch({ data: [] });
|
||
|
|
|
||
|
|
await apiClient.get('/test');
|
||
|
|
|
||
|
|
const headers = spy.mock.calls[0][1]?.headers as Record<string, string>;
|
||
|
|
expect(headers['Authorization']).toBeUndefined();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('get', () => {
|
||
|
|
it('makes GET request to correct URL', async () => {
|
||
|
|
const spy = mockFetch({ id: '1' });
|
||
|
|
|
||
|
|
await apiClient.get('/households/hh1/stores');
|
||
|
|
|
||
|
|
expect(spy).toHaveBeenCalledWith(
|
||
|
|
expect.stringContaining('/households/hh1/stores'),
|
||
|
|
expect.objectContaining({ headers: expect.any(Object) }),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns parsed JSON body', async () => {
|
||
|
|
mockFetch({ id: '1', name: 'CVS' });
|
||
|
|
|
||
|
|
const result = await apiClient.get('/test');
|
||
|
|
|
||
|
|
expect(result).toEqual({ id: '1', name: 'CVS' });
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('post', () => {
|
||
|
|
it('makes POST request with JSON body', async () => {
|
||
|
|
const spy = mockFetch({ id: '1' });
|
||
|
|
const body = { name: 'Walgreens' };
|
||
|
|
|
||
|
|
await apiClient.post('/stores', body);
|
||
|
|
|
||
|
|
expect(spy).toHaveBeenCalledWith(
|
||
|
|
expect.stringContaining('/stores'),
|
||
|
|
expect.objectContaining({
|
||
|
|
method: 'POST',
|
||
|
|
body: JSON.stringify(body),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles post without body', async () => {
|
||
|
|
const spy = mockFetch({ id: '1' });
|
||
|
|
|
||
|
|
await apiClient.post('/test');
|
||
|
|
|
||
|
|
expect(spy).toHaveBeenCalledWith(
|
||
|
|
expect.anything(),
|
||
|
|
expect.objectContaining({ body: undefined }),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('patch', () => {
|
||
|
|
it('makes PATCH request with JSON body', async () => {
|
||
|
|
const spy = mockFetch({ id: '1' });
|
||
|
|
const body = { name: 'Updated' };
|
||
|
|
|
||
|
|
await apiClient.patch('/stores/1', body);
|
||
|
|
|
||
|
|
expect(spy).toHaveBeenCalledWith(
|
||
|
|
expect.stringContaining('/stores/1'),
|
||
|
|
expect.objectContaining({
|
||
|
|
method: 'PATCH',
|
||
|
|
body: JSON.stringify(body),
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('delete', () => {
|
||
|
|
it('makes DELETE request', async () => {
|
||
|
|
const spy = mockFetch(undefined, 204);
|
||
|
|
|
||
|
|
await apiClient.delete('/stores/1');
|
||
|
|
|
||
|
|
expect(spy).toHaveBeenCalledWith(
|
||
|
|
expect.stringContaining('/stores/1'),
|
||
|
|
expect.objectContaining({ method: 'DELETE' }),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('error handling', () => {
|
||
|
|
it('throws with message from error response body', async () => {
|
||
|
|
mockFetch({ message: 'Store not found' }, 404);
|
||
|
|
|
||
|
|
await expect(apiClient.get('/test')).rejects.toThrow('Store not found');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('throws with status code when body has no message', async () => {
|
||
|
|
mockFetch({}, 500);
|
||
|
|
|
||
|
|
await expect(apiClient.get('/test')).rejects.toThrow('Request failed: 500');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('throws with status text when JSON parsing fails', async () => {
|
||
|
|
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
||
|
|
ok: false,
|
||
|
|
status: 502,
|
||
|
|
statusText: 'Bad Gateway',
|
||
|
|
json: () => Promise.reject(new Error('parse error')),
|
||
|
|
} as Response);
|
||
|
|
|
||
|
|
await expect(apiClient.get('/test')).rejects.toThrow('Request failed: 502 Bad Gateway');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns undefined for 204 No Content', async () => {
|
||
|
|
mockFetch(null, 204);
|
||
|
|
|
||
|
|
const result = await apiClient.get('/test');
|
||
|
|
|
||
|
|
expect(result).toBeUndefined();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|