import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { apiFetch, apiFetchVoid, AuthError, APIError } from './client' import { tokenStore } from '../lib/auth' // ── Helpers ────────────────────────────────────────────────────────────────── function makeOkResponse(body: unknown = {}, status = 200): Response { return { ok: true, status, headers: { get: () => null }, json: () => Promise.resolve(body), } as unknown as Response } function makeErrorResponse(status: number, contentType?: string, body?: unknown): Response { return { ok: false, status, headers: { get: (h: string) => h.toLowerCase() === 'content-type' ? (contentType ?? null) : null, }, json: () => Promise.resolve(body), } as unknown as Response } // ── Setup / teardown ───────────────────────────────────────────────────────── let fetchSpy: ReturnType beforeEach(() => { fetchSpy = vi.fn() vi.stubGlobal('fetch', fetchSpy) tokenStore.set(null) }) afterEach(() => { tokenStore.set(null) vi.unstubAllGlobals() vi.restoreAllMocks() }) // ── apiFetch: Authorization header behavior ────────────────────────────────── describe('apiFetch Authorization header', () => { it('no stored token → fetch called without Authorization header', async () => { fetchSpy.mockResolvedValue(makeOkResponse({ result: 1 })) await apiFetch('/api/v1/test') const [, init] = fetchSpy.mock.calls[0] as [string, RequestInit] const headers = init.headers as Record expect(headers['Authorization']).toBeUndefined() }) it('stored token → fetch called with Authorization: Bearer ', async () => { fetchSpy.mockResolvedValue(makeOkResponse({ result: 1 })) tokenStore.set('abc') await apiFetch('/api/v1/test') const [, init] = fetchSpy.mock.calls[0] as [string, RequestInit] const headers = init.headers as Record expect(headers['Authorization']).toBe('Bearer abc') }) it('caller-supplied Authorization header wins over stored token', async () => { fetchSpy.mockResolvedValue(makeOkResponse({ result: 1 })) tokenStore.set('stored-token') await apiFetch('/api/v1/test', { headers: { Authorization: 'Bearer caller-token' }, }) const [, init] = fetchSpy.mock.calls[0] as [string, RequestInit] const headers = init.headers as Record expect(headers['Authorization']).toBe('Bearer caller-token') }) it('401 → AuthError thrown (regression)', async () => { fetchSpy.mockResolvedValue(makeErrorResponse(401)) await expect(apiFetch('/api/v1/test')).rejects.toThrow(AuthError) }) it('500 → APIError thrown (regression)', async () => { fetchSpy.mockResolvedValue(makeErrorResponse(500)) await expect(apiFetch('/api/v1/test')).rejects.toThrow(APIError) }) }) // ── apiFetchVoid: Authorization header behavior ────────────────────────────── describe('apiFetchVoid Authorization header', () => { it('no stored token → fetch called without Authorization header', async () => { fetchSpy.mockResolvedValue(makeOkResponse(undefined, 204)) await apiFetchVoid('/api/v1/test') const [, init] = fetchSpy.mock.calls[0] as [string, RequestInit] const headers = init.headers as Record expect(headers['Authorization']).toBeUndefined() }) it('stored token → fetch called with Authorization: Bearer ', async () => { fetchSpy.mockResolvedValue(makeOkResponse(undefined, 204)) tokenStore.set('xyz') await apiFetchVoid('/api/v1/test') const [, init] = fetchSpy.mock.calls[0] as [string, RequestInit] const headers = init.headers as Record expect(headers['Authorization']).toBe('Bearer xyz') }) it('caller-supplied Authorization header wins over stored token', async () => { fetchSpy.mockResolvedValue(makeOkResponse(undefined, 204)) tokenStore.set('stored-token') await apiFetchVoid('/api/v1/test', { headers: { Authorization: 'Bearer caller-token' }, }) const [, init] = fetchSpy.mock.calls[0] as [string, RequestInit] const headers = init.headers as Record expect(headers['Authorization']).toBe('Bearer caller-token') }) it('401 → AuthError thrown (regression)', async () => { fetchSpy.mockResolvedValue(makeErrorResponse(401)) await expect(apiFetchVoid('/api/v1/test')).rejects.toThrow(AuthError) }) })