✅ Add unit tests for API client utility functions
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { createApiClient } from '@/utilities/api-client';
|
||||
|
||||
describe('api-client', () => {
|
||||
let fetchMock;
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock = vi.fn();
|
||||
global.fetch = fetchMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllTimers();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe('createApiClient', () => {
|
||||
it('should create client with default config', () => {
|
||||
const client = createApiClient();
|
||||
expect(client).toHaveProperty('get');
|
||||
expect(client).toHaveProperty('post');
|
||||
expect(client).toHaveProperty('put');
|
||||
expect(client).toHaveProperty('patch');
|
||||
expect(client).toHaveProperty('delete');
|
||||
});
|
||||
|
||||
it('should merge baseURL with request path', async () => {
|
||||
const client = createApiClient({ baseURL: '/api' });
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
json: async () => ({ success: true })
|
||||
});
|
||||
|
||||
await client.get('/users');
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/users',
|
||||
expect.objectContaining({ method: 'GET' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET requests', () => {
|
||||
it('should make successful GET request', async () => {
|
||||
const client = createApiClient();
|
||||
const mockData = { id: 1, name: 'Test' };
|
||||
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
json: async () => mockData
|
||||
});
|
||||
|
||||
const result = await client.get('/test');
|
||||
|
||||
expect(result).toEqual(mockData);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/test',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
headers: expect.objectContaining({
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle non-JSON responses', async () => {
|
||||
const client = createApiClient();
|
||||
const textResponse = 'Plain text response';
|
||||
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'text/plain' }),
|
||||
text: async () => textResponse
|
||||
});
|
||||
|
||||
const result = await client.get('/text');
|
||||
|
||||
expect(result).toBe(textResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST requests', () => {
|
||||
it('should make POST request with data', async () => {
|
||||
const client = createApiClient();
|
||||
const postData = { name: 'Test' };
|
||||
const mockResponse = { id: 1, ...postData };
|
||||
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
json: async () => mockResponse
|
||||
});
|
||||
|
||||
const result = await client.post('/users', postData);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/users',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify(postData)
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should throw error on HTTP error status', async () => {
|
||||
const client = createApiClient();
|
||||
const errorResponse = { message: 'Not found' };
|
||||
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
json: async () => errorResponse
|
||||
});
|
||||
|
||||
await expect(client.get('/missing')).rejects.toThrow('HTTP 404');
|
||||
});
|
||||
|
||||
it('should not retry on 4xx errors', async () => {
|
||||
const client = createApiClient({ maxRetries: 3 });
|
||||
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 400,
|
||||
statusText: 'Bad Request',
|
||||
headers: new Headers(),
|
||||
json: async () => ({})
|
||||
});
|
||||
|
||||
await expect(client.get('/bad')).rejects.toThrow();
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should retry on 5xx errors', async () => {
|
||||
vi.useFakeTimers();
|
||||
const client = createApiClient({ maxRetries: 2, retryDelay: 100 });
|
||||
|
||||
fetchMock
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
headers: new Headers(),
|
||||
json: async () => ({})
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
headers: new Headers(),
|
||||
json: async () => ({})
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
json: async () => ({ success: true })
|
||||
});
|
||||
|
||||
const promise = client.get('/retry');
|
||||
|
||||
// Advance timers for retries
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
const result = await promise;
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe('request deduplication', () => {
|
||||
it('should deduplicate identical concurrent requests', async () => {
|
||||
const client = createApiClient();
|
||||
const mockData = { data: 'test' };
|
||||
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
json: async () => mockData
|
||||
});
|
||||
|
||||
// Make two identical concurrent requests
|
||||
const [result1, result2] = await Promise.all([
|
||||
client.get('/test'),
|
||||
client.get('/test')
|
||||
]);
|
||||
|
||||
expect(result1).toEqual(mockData);
|
||||
expect(result2).toEqual(mockData);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1); // Only one actual fetch
|
||||
});
|
||||
|
||||
it('should allow disabling deduplication', async () => {
|
||||
const client = createApiClient();
|
||||
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
json: async () => ({ data: 'test' })
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
client.get('/test', { deduplicate: false }),
|
||||
client.get('/test', { deduplicate: false })
|
||||
]);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interceptors', () => {
|
||||
it('should call onRequest interceptor', async () => {
|
||||
const onRequest = vi.fn((url, options) => options);
|
||||
const client = createApiClient({ onRequest });
|
||||
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
json: async () => ({})
|
||||
});
|
||||
|
||||
await client.get('/test');
|
||||
|
||||
expect(onRequest).toHaveBeenCalledWith(
|
||||
'/test',
|
||||
expect.objectContaining({ method: 'GET' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onResponse interceptor', async () => {
|
||||
const onResponse = vi.fn();
|
||||
const client = createApiClient({ onResponse });
|
||||
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
json: async () => ({}),
|
||||
clone: function() { return this; }
|
||||
});
|
||||
|
||||
await client.get('/test');
|
||||
|
||||
expect(onResponse).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call onError interceptor', async () => {
|
||||
vi.useFakeTimers();
|
||||
const onError = vi.fn();
|
||||
const client = createApiClient({ onError, maxRetries: 0 });
|
||||
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: 'Server Error',
|
||||
headers: new Headers(),
|
||||
json: async () => ({})
|
||||
});
|
||||
|
||||
await expect(client.get('/error')).rejects.toThrow();
|
||||
expect(onError).toHaveBeenCalled();
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user