From 29ea6b8ef75cbadeaf8673d25207596dc1cb36d4 Mon Sep 17 00:00:00 2001 From: Zorin95670 Date: Wed, 7 May 2025 14:26:30 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=85(frontend)=20Improve=20test=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improve the test coverage of the "api" modules. Signed-off-by: Zorin95670 --- CHANGELOG.md | 1 + .../src/api/__tests__/APIError.test.ts | 36 +++++++++++ .../impress/src/api/__tests__/config.test.ts | 16 +++++ .../src/api/__tests__/fetchApi.test.tsx | 9 +++ .../src/api/__tests__/helpers.test.tsx | 59 +++++++++++++++++++ .../impress/src/api/__tests__/utils.test.ts | 57 ++++++++++++++++++ 6 files changed, 178 insertions(+) create mode 100644 src/frontend/apps/impress/src/api/__tests__/APIError.test.ts create mode 100644 src/frontend/apps/impress/src/api/__tests__/config.test.ts create mode 100644 src/frontend/apps/impress/src/api/__tests__/helpers.test.tsx create mode 100644 src/frontend/apps/impress/src/api/__tests__/utils.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a389c0c5..4dfc5baf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to ## Changed - 📝(frontend) Update documentation +- ✅(frontend) Improve tests coverage ## [3.2.1] - 2025-05-06 diff --git a/src/frontend/apps/impress/src/api/__tests__/APIError.test.ts b/src/frontend/apps/impress/src/api/__tests__/APIError.test.ts new file mode 100644 index 00000000..395a5460 --- /dev/null +++ b/src/frontend/apps/impress/src/api/__tests__/APIError.test.ts @@ -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); + }); +}); diff --git a/src/frontend/apps/impress/src/api/__tests__/config.test.ts b/src/frontend/apps/impress/src/api/__tests__/config.test.ts new file mode 100644 index 00000000..cb9bf268 --- /dev/null +++ b/src/frontend/apps/impress/src/api/__tests__/config.test.ts @@ -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/'); + }); +}); diff --git a/src/frontend/apps/impress/src/api/__tests__/fetchApi.test.tsx b/src/frontend/apps/impress/src/api/__tests__/fetchApi.test.tsx index 05dc0b13..9b9d6a9a 100644 --- a/src/frontend/apps/impress/src/api/__tests__/fetchApi.test.tsx +++ b/src/frontend/apps/impress/src/api/__tests__/fetchApi.test.tsx @@ -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'); + }); }); diff --git a/src/frontend/apps/impress/src/api/__tests__/helpers.test.tsx b/src/frontend/apps/impress/src/api/__tests__/helpers.test.tsx new file mode 100644 index 00000000..e4706367 --- /dev/null +++ b/src/frontend/apps/impress/src/api/__tests__/helpers.test.tsx @@ -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 }) => ( + {children} + ); +}; + +describe('helpers', () => { + it('fetches and paginates correctly', async () => { + const mockAPI = jest + .fn, [{ 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 }); + }); +}); diff --git a/src/frontend/apps/impress/src/api/__tests__/utils.test.ts b/src/frontend/apps/impress/src/api/__tests__/utils.test.ts new file mode 100644 index 00000000..86433188 --- /dev/null +++ b/src/frontend/apps/impress/src/api/__tests__/utils.test.ts @@ -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(); + }); + }); +});