animetix-web / frontend /src /utils /__tests__ /apiClient.test.ts
MissawB's picture
Upload folder using huggingface_hub (part 5)
c288a20 verified
Raw
History Blame Contribute Delete
6.23 kB
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { apiClient } from '../apiClient';
import { useToastStore } from '../../store/toastStore';
import { usePersonalizationStore } from '../../store/personalizationStore';
import { auth } from '../firebase';
// firebase.ts runs real Firebase init at module load; replace it with a stub
// `auth` object whose currentUser we control per-test.
vi.mock('../firebase', () => ({
auth: { currentUser: null as { getIdToken: () => Promise<string> } | null },
}));
const addToast = vi.fn();
vi.mock('../../store/toastStore', () => ({
useToastStore: { getState: vi.fn(() => ({ addToast })) },
}));
const updateConfig = vi.fn();
vi.mock('../../store/personalizationStore', () => ({
usePersonalizationStore: { getState: vi.fn(() => ({ updateConfig })) },
}));
type FetchResponse = {
ok: boolean;
status: number;
json: () => Promise<unknown>;
};
const mockFetch = (response: FetchResponse) => {
const fn = vi.fn().mockResolvedValue(response as unknown as Response);
global.fetch = fn as unknown as typeof fetch;
return fn;
};
describe('apiClient', () => {
beforeEach(() => {
vi.clearAllMocks();
(auth as { currentUser: unknown }).currentUser = null;
// Reset cookies between tests.
Object.defineProperty(document, 'cookie', {
writable: true,
configurable: true,
value: '',
});
});
afterEach(() => {
vi.restoreAllMocks();
});
it('returns parsed JSON on a successful response', async () => {
mockFetch({ ok: true, status: 200, json: async () => ({ id: 7 }) });
const data = await apiClient('/api/thing');
expect(data).toEqual({ id: 7 });
});
it('returns null for a 204 No Content response without parsing JSON', async () => {
const json = vi.fn();
mockFetch({ ok: true, status: 204, json });
const data = await apiClient('/api/thing', { method: 'DELETE' });
expect(data).toBeNull();
expect(json).not.toHaveBeenCalled();
});
it('throws an Error with .status set and toasts on a non-ok response', async () => {
mockFetch({
ok: false,
status: 403,
json: async () => ({ message: 'Forbidden' }),
});
await expect(apiClient('/api/secret')).rejects.toMatchObject({
message: 'Forbidden',
status: 403,
});
expect(addToast).toHaveBeenCalledWith('Forbidden', 'error');
});
it('falls back to a generic message when error body has no message', async () => {
mockFetch({ ok: false, status: 500, json: async () => null });
await expect(apiClient('/api/boom')).rejects.toMatchObject({ status: 500 });
expect(addToast).toHaveBeenCalledWith(
'Erreur 500: Impossible de récupérer les données.',
'error',
);
});
it('does not toast on a non-ok response when skipToast is set', async () => {
mockFetch({
ok: false,
status: 404,
json: async () => ({ message: 'Not found' }),
});
await expect(
apiClient('/api/missing', { skipToast: true }),
).rejects.toMatchObject({ status: 404 });
expect(addToast).not.toHaveBeenCalled();
});
it('toasts and rethrows on a network TypeError', async () => {
const netErr = new TypeError('Failed to fetch');
global.fetch = vi.fn().mockRejectedValue(netErr) as unknown as typeof fetch;
await expect(apiClient('/api/thing')).rejects.toBe(netErr);
expect(addToast).toHaveBeenCalledWith(
'Serveur injoignable. Vérifiez votre connexion.',
'error',
);
});
it('does not toast a network TypeError when skipToast is set', async () => {
const netErr = new TypeError('Failed to fetch');
global.fetch = vi.fn().mockRejectedValue(netErr) as unknown as typeof fetch;
await expect(
apiClient('/api/thing', { skipToast: true }),
).rejects.toBe(netErr);
expect(addToast).not.toHaveBeenCalled();
});
it('sends the CSRF token from document.cookie as X-CSRFToken header', async () => {
Object.defineProperty(document, 'cookie', {
writable: true,
configurable: true,
value: 'foo=bar; csrftoken=abc123; baz=qux',
});
const fetchFn = mockFetch({ ok: true, status: 200, json: async () => ({}) });
await apiClient('/api/thing');
const headers = (fetchFn.mock.calls[0][1] as RequestInit)
.headers as Record<string, string>;
expect(headers['X-CSRFToken']).toBe('abc123');
});
it('sets Content-Type application/json by default but not for FormData', async () => {
const fetchFn = mockFetch({ ok: true, status: 200, json: async () => ({}) });
await apiClient('/api/a');
let headers = (fetchFn.mock.calls[0][1] as RequestInit).headers as Record<
string,
string
>;
expect(headers['Content-Type']).toBe('application/json');
await apiClient('/api/b', { isFormData: true });
headers = (fetchFn.mock.calls[1][1] as RequestInit).headers as Record<
string,
string
>;
expect(headers['Content-Type']).toBeUndefined();
});
it('attaches a Firebase bearer token when a user is signed in', async () => {
(auth as { currentUser: unknown }).currentUser = {
getIdToken: vi.fn().mockResolvedValue('fb-token'),
};
const fetchFn = mockFetch({ ok: true, status: 200, json: async () => ({}) });
await apiClient('/api/thing');
const headers = (fetchFn.mock.calls[0][1] as RequestInit)
.headers as Record<string, string>;
expect(headers['Authorization']).toBe('Bearer fb-token');
});
it('applies personalization visual_config from the response meta', async () => {
const visualConfig = { theme: 'dark' };
mockFetch({
ok: true,
status: 200,
json: async () => ({ meta: { visual_config: visualConfig } }),
});
await apiClient('/api/thing');
expect(usePersonalizationStore.getState().updateConfig).toHaveBeenCalledWith(
visualConfig,
);
});
it('does not call updateConfig when no visual_config is present', async () => {
mockFetch({ ok: true, status: 200, json: async () => ({ data: 1 }) });
await apiClient('/api/thing');
expect(useToastStore.getState().addToast).not.toHaveBeenCalled();
expect(updateConfig).not.toHaveBeenCalled();
});
});