import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' import { setActivePinia, createPinia } from 'pinia' import { fetchWithAuth, apiClient } from '../client' import { useAuthStore } from '@/stores/auth' import * as authApi from '@/api/auth' import router from '@/router' describe('api client (fetchWithAuth)', () => { const originalFetch = global.fetch beforeEach(async () => { setActivePinia(createPinia()) localStorage.clear() vi.restoreAllMocks() await router.push('/') }) afterEach(() => { global.fetch = originalFetch localStorage.clear() }) it('adds Authorization header if accessToken is available in store', async () => { const authStore = useAuthStore() authStore.setTokens({ access_token: 'test-token', refresh_token: 'refresh-token', }) const fetchMock = vi.fn().mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ success: true }), } as unknown as Response) global.fetch = fetchMock await fetchWithAuth('/lists') expect(fetchMock).toHaveBeenCalled() const callArgs = fetchMock.mock.calls[0] expect(callArgs).toBeDefined() const headers = callArgs?.[1]?.headers as Headers expect(headers.get('Authorization')).toBe('Bearer test-token') }) it('omits Authorization header when skipAuth is true', async () => { const authStore = useAuthStore() authStore.setTokens({ access_token: 'test-token', refresh_token: 'refresh-token', }) const fetchMock = vi.fn().mockResolvedValueOnce({ ok: true, status: 200, } as unknown as Response) global.fetch = fetchMock await fetchWithAuth('/health', { skipAuth: true }) const callArgs = fetchMock.mock.calls[0] expect(callArgs).toBeDefined() const headers = callArgs?.[1]?.headers as Headers expect(headers.has('Authorization')).toBe(false) }) it('refreshes token and retries request on 401 response', async () => { const authStore = useAuthStore() authStore.setTokens({ access_token: 'expired-token', refresh_token: 'valid-refresh-token', }) const refreshSpy = vi.spyOn(authApi, 'refreshApi').mockResolvedValueOnce({ access_token: 'new-access-token', refresh_token: 'new-refresh-token', }) const firstResponse = { ok: false, status: 401, statusText: 'Unauthorized', } const secondResponse = { ok: true, status: 200, json: async () => ({ data: 'protected data' }), } const fetchMock = vi .fn() .mockResolvedValueOnce(firstResponse as unknown as Response) .mockResolvedValueOnce(secondResponse as unknown as Response) global.fetch = fetchMock const res = await fetchWithAuth('/lists') expect(refreshSpy).toHaveBeenCalledWith({ refresh_token: 'valid-refresh-token' }) expect(fetchMock).toHaveBeenCalledTimes(2) expect(authStore.accessToken).toBe('new-access-token') // Second call should have new access token const secondCall = fetchMock.mock.calls[1] expect(secondCall).toBeDefined() const secondCallHeaders = secondCall?.[1]?.headers as Headers expect(secondCallHeaders.get('Authorization')).toBe('Bearer new-access-token') expect(res).toBe(secondResponse) }) it('redirects to login when a 401 occurs and no refresh token is available', async () => { const authStore = useAuthStore() authStore.setTokens({ access_token: 'expired-token', refresh_token: 'valid-refresh-token', }) authStore.clearTokens() const fetchMock = vi.fn().mockResolvedValueOnce({ ok: false, status: 401, statusText: 'Unauthorized', } as unknown as Response) global.fetch = fetchMock await fetchWithAuth('/lists') expect(fetchMock).toHaveBeenCalledTimes(1) expect(router.currentRoute.value.name).toBe('login') }) it('redirects to login when refreshing the token fails', async () => { const authStore = useAuthStore() authStore.setTokens({ access_token: 'expired-token', refresh_token: 'expired-refresh-token', }) vi.spyOn(authApi, 'refreshApi').mockRejectedValueOnce(new Error('Refresh token expired')) const fetchMock = vi.fn().mockResolvedValueOnce({ ok: false, status: 401, statusText: 'Unauthorized', } as unknown as Response) global.fetch = fetchMock await fetchWithAuth('/lists') expect(fetchMock).toHaveBeenCalledTimes(1) expect(authStore.accessToken).toBeNull() expect(router.currentRoute.value.name).toBe('login') }) it('calls apiClient helper methods correctly', async () => { const authStore = useAuthStore() authStore.setTokens({ access_token: 'my-token', refresh_token: 'my-refresh', }) const mockResponse = { ok: true, status: 200, } const fetchMock = vi.fn().mockResolvedValue(mockResponse as unknown as Response) global.fetch = fetchMock await apiClient.get('/lists') expect(fetchMock).toHaveBeenLastCalledWith( expect.stringContaining('/lists'), expect.objectContaining({ method: 'GET' }), ) await apiClient.post('/lists', { name: 'My List' }) expect(fetchMock).toHaveBeenLastCalledWith( expect.stringContaining('/lists'), expect.objectContaining({ method: 'POST', body: JSON.stringify({ name: 'My List' }), }), ) await apiClient.put('/lists/item', { title: 'Updated' }) expect(fetchMock).toHaveBeenLastCalledWith( expect.stringContaining('/lists/item'), expect.objectContaining({ method: 'PUT', body: JSON.stringify({ title: 'Updated' }), }), ) await apiClient.delete('/lists/user') expect(fetchMock).toHaveBeenLastCalledWith( expect.stringContaining('/lists/user'), expect.objectContaining({ method: 'DELETE' }), ) }) })