MeshiTrack/packages/web/tests/services/api-client.test.ts

167 lines
4.7 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { apiClient as ApiClientType } from '../../src/services/api-client';
// Create a fresh ApiClient for each test by re-importing
let apiClient: typeof ApiClientType;
beforeEach(async () => {
vi.restoreAllMocks();
// Reset module to get a fresh singleton
vi.resetModules();
const mod = await import('../../src/services/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();
});
});
});