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<typeof vi.fn>
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<string, string>
expect(headers['Authorization']).toBeUndefined()
})
it('stored token → fetch called with Authorization: Bearer <token>', 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<string, string>
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<string, string>
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<string, string>
expect(headers['Authorization']).toBeUndefined()
})
it('stored token → fetch called with Authorization: Bearer <token>', 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<string, string>
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<string, string>
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)
})
})