(frontend) Improve test coverage

Improve the test coverage of the "api" modules.

Signed-off-by: Zorin95670 <moittie.vincent@gmail.com>
This commit is contained in:
Zorin95670
2025-05-07 14:26:30 +02:00
committed by Anthony LC
parent a692fa6f39
commit 29ea6b8ef7
6 changed files with 178 additions and 0 deletions

View File

@@ -16,6 +16,7 @@ and this project adheres to
## Changed
- 📝(frontend) Update documentation
- ✅(frontend) Improve tests coverage
## [3.2.1] - 2025-05-06

View File

@@ -0,0 +1,36 @@
import { APIError, isAPIError } from '@/api';
describe('APIError', () => {
it('should correctly instantiate with required fields', () => {
const error = new APIError('Something went wrong', { status: 500 });
expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(APIError);
expect(error.message).toBe('Something went wrong');
expect(error.status).toBe(500);
expect(error.cause).toBeUndefined();
expect(error.data).toBeUndefined();
});
it('should correctly instantiate with all fields', () => {
const details = { field: 'email' };
const error = new APIError('Validation failed', {
status: 400,
cause: ['Invalid email format'],
data: details,
});
expect(error.name).toBe('APIError');
expect(error.status).toBe(400);
expect(error.cause).toEqual(['Invalid email format']);
expect(error.data).toEqual(details);
});
it('should be detected by isAPIError type guard', () => {
const error = new APIError('Unauthorized', { status: 401 });
const notAnError = { message: 'Fake error' };
expect(isAPIError(error)).toBe(true);
expect(isAPIError(notAnError)).toBe(false);
});
});

View File

@@ -0,0 +1,16 @@
import { baseApiUrl } from '@/api';
describe('config', () => {
it('constructs URL with default version', () => {
expect(baseApiUrl()).toBe('http://test.jest/api/v1.0/');
});
it('constructs URL with custom version', () => {
expect(baseApiUrl('2.0')).toBe('http://test.jest/api/v2.0/');
});
it('uses env origin if available', () => {
process.env.NEXT_PUBLIC_API_ORIGIN = 'https://env.example.com';
expect(baseApiUrl('3.0')).toBe('https://env.example.com/api/v3.0/');
});
});

View File

@@ -36,4 +36,13 @@ describe('fetchAPI', () => {
expect(fetchMock.lastUrl()).toEqual('http://test.jest/api/v2.0/some/url');
});
it('removes Content-Type header when withoutContentType is true', async () => {
fetchMock.mock('http://test.jest/api/v1.0/some/url', 200);
await fetchAPI('some/url', { withoutContentType: true });
const options = fetchMock.lastOptions();
expect(options?.headers).not.toHaveProperty('Content-Type');
});
});

View File

@@ -0,0 +1,59 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderHook, waitFor } from '@testing-library/react';
import { useAPIInfiniteQuery } from '@/api';
interface DummyItem {
id: number;
}
interface DummyResponse {
results: DummyItem[];
next?: string;
}
const createWrapper = () => {
const queryClient = new QueryClient();
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
describe('helpers', () => {
it('fetches and paginates correctly', async () => {
const mockAPI = jest
.fn<Promise<DummyResponse>, [{ page: number; query: string }]>()
.mockResolvedValueOnce({
results: [{ id: 1 }],
next: 'url?page=2',
})
.mockResolvedValueOnce({
results: [{ id: 2 }],
next: undefined,
});
const { result } = renderHook(
() => useAPIInfiniteQuery('test-key', mockAPI, { query: 'test' }),
{ wrapper: createWrapper() },
);
// Wait for first page
await waitFor(() => {
expect(result.current.data?.pages[0].results[0].id).toBe(1);
});
// Fetch next page
await result.current.fetchNextPage();
await waitFor(() => {
expect(result.current.data?.pages.length).toBe(2);
});
await waitFor(() => {
expect(result.current.data?.pages[1].results[0].id).toBe(2);
});
expect(mockAPI).toHaveBeenCalledWith({ query: 'test', page: 1 });
expect(mockAPI).toHaveBeenCalledWith({ query: 'test', page: 2 });
});
});

View File

@@ -0,0 +1,57 @@
import { errorCauses, getCSRFToken } from '@/api';
describe('utils', () => {
describe('errorCauses', () => {
const createMockResponse = (jsonData: any, status = 400): Response => {
return {
status,
json: () => jsonData,
} as unknown as Response;
};
it('parses multiple string causes from error body', async () => {
const mockResponse = createMockResponse(
{
field: ['error message 1', 'error message 2'],
},
400,
);
const result = await errorCauses(mockResponse, { context: 'login' });
expect(result.status).toBe(400);
expect(result.cause).toEqual(['error message 1', 'error message 2']);
expect(result.data).toEqual({ context: 'login' });
});
it('returns undefined causes if no JSON body', async () => {
const mockResponse = createMockResponse(null, 500);
const result = await errorCauses(mockResponse);
expect(result.status).toBe(500);
expect(result.cause).toBeUndefined();
expect(result.data).toBeUndefined();
});
});
describe('getCSRFToken', () => {
it('extracts csrftoken from document.cookie', () => {
Object.defineProperty(document, 'cookie', {
writable: true,
value: 'sessionid=xyz; csrftoken=abc123; theme=dark',
});
expect(getCSRFToken()).toBe('abc123');
});
it('returns undefined if csrftoken is not present', () => {
Object.defineProperty(document, 'cookie', {
writable: true,
value: 'sessionid=xyz; theme=dark',
});
expect(getCSRFToken()).toBeUndefined();
});
});
});