206 lines
5.9 KiB
TypeScript
206 lines
5.9 KiB
TypeScript
import { beforeEach, describe, it, expect, vi, afterEach } from 'vitest'
|
|
|
|
type ResponseHandler = (value: unknown) => unknown
|
|
type ErrorHandler = (error: ResponseError) => Promise<never>
|
|
|
|
type ResponseError = {
|
|
response?: {
|
|
status?: number
|
|
data?: Record<string, unknown>
|
|
}
|
|
config?: {
|
|
url?: string
|
|
}
|
|
message?: string
|
|
}
|
|
|
|
// Use vi.hoisted() to declare variables accessible in hoisted mocks
|
|
const capturedHandlers = vi.hoisted(() => ({
|
|
onFulfilled: undefined as ResponseHandler | undefined,
|
|
onRejected: undefined as ErrorHandler | undefined,
|
|
}))
|
|
|
|
vi.mock('axios', () => {
|
|
const mockClient = {
|
|
defaults: {
|
|
headers: {
|
|
common: {} as Record<string, string>,
|
|
},
|
|
},
|
|
interceptors: {
|
|
response: {
|
|
use: vi.fn((onFulfilled?: ResponseHandler, onRejected?: ErrorHandler) => {
|
|
capturedHandlers.onFulfilled = onFulfilled
|
|
capturedHandlers.onRejected = onRejected
|
|
return vi.fn()
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
|
|
return {
|
|
default: {
|
|
create: vi.fn(() => mockClient),
|
|
},
|
|
}
|
|
})
|
|
|
|
// Must import AFTER mock definition
|
|
import { setAuthErrorHandler, setAuthToken } from '../client'
|
|
import axios from 'axios'
|
|
|
|
// Get mock client instance for header assertions
|
|
const getMockClient = () => {
|
|
const mockAxios = vi.mocked(axios)
|
|
return mockAxios.create()
|
|
}
|
|
|
|
describe('api client', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('sets and clears the Authorization header', () => {
|
|
const mockClientInstance = getMockClient()
|
|
|
|
setAuthToken('test-token')
|
|
expect(mockClientInstance.defaults.headers.common.Authorization).toBe('Bearer test-token')
|
|
|
|
setAuthToken(null)
|
|
expect(mockClientInstance.defaults.headers.common.Authorization).toBeUndefined()
|
|
})
|
|
|
|
it('extracts error message from response payload', async () => {
|
|
const error: ResponseError = {
|
|
response: { data: { error: 'Bad request' } },
|
|
config: { url: '/test' },
|
|
message: 'Original',
|
|
}
|
|
|
|
const handler = capturedHandlers.onRejected
|
|
expect(handler).toBeDefined()
|
|
|
|
const resultPromise = handler ? handler(error) : Promise.reject(new Error('handler missing'))
|
|
|
|
await expect(resultPromise).rejects.toBe(error)
|
|
expect(error.message).toBe('Bad request')
|
|
})
|
|
|
|
it('keeps original message when response payload is not an object', async () => {
|
|
const error: ResponseError = {
|
|
response: { data: 'plain text error' as unknown as Record<string, unknown> },
|
|
config: { url: '/test' },
|
|
message: 'Original',
|
|
}
|
|
|
|
const handler = capturedHandlers.onRejected
|
|
expect(handler).toBeDefined()
|
|
|
|
const resultPromise = handler ? handler(error) : Promise.reject(new Error('handler missing'))
|
|
|
|
await expect(resultPromise).rejects.toBe(error)
|
|
expect(error.message).toBe('Original')
|
|
})
|
|
|
|
it('uses error field over message field when both exist', async () => {
|
|
const error: ResponseError = {
|
|
response: { data: { error: 'Preferred error', message: 'Secondary message' } },
|
|
config: { url: '/test' },
|
|
message: 'Original',
|
|
}
|
|
|
|
const handler = capturedHandlers.onRejected
|
|
expect(handler).toBeDefined()
|
|
|
|
const resultPromise = handler ? handler(error) : Promise.reject(new Error('handler missing'))
|
|
|
|
await expect(resultPromise).rejects.toBe(error)
|
|
expect(error.message).toBe('Preferred error')
|
|
})
|
|
|
|
it('invokes auth error handler on 401 outside auth endpoints', async () => {
|
|
const onAuthError = vi.fn()
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
|
|
setAuthErrorHandler(onAuthError)
|
|
|
|
const error: ResponseError = {
|
|
response: { status: 401, data: { message: 'Unauthorized' } },
|
|
config: { url: '/proxy-hosts' },
|
|
message: 'Original',
|
|
}
|
|
|
|
const handler = capturedHandlers.onRejected
|
|
expect(handler).toBeDefined()
|
|
|
|
const resultPromise = handler ? handler(error) : Promise.reject(new Error('handler missing'))
|
|
|
|
await expect(resultPromise).rejects.toBe(error)
|
|
expect(onAuthError).toHaveBeenCalledTimes(1)
|
|
expect(error.message).toBe('Unauthorized')
|
|
|
|
warnSpy.mockRestore()
|
|
})
|
|
|
|
it('skips auth error handler for auth endpoints', async () => {
|
|
const onAuthError = vi.fn()
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
|
|
setAuthErrorHandler(onAuthError)
|
|
|
|
const error: ResponseError = {
|
|
response: { status: 401, data: { message: 'Unauthorized' } },
|
|
config: { url: '/auth/login' },
|
|
message: 'Original',
|
|
}
|
|
|
|
const handler = capturedHandlers.onRejected
|
|
expect(handler).toBeDefined()
|
|
|
|
// Call handler with auth endpoint error to verify it skips the auth error handler
|
|
const resultPromise = handler ? handler(error) : Promise.reject(new Error('handler missing'))
|
|
|
|
await expect(resultPromise).rejects.toBe(error)
|
|
expect(onAuthError).not.toHaveBeenCalled()
|
|
|
|
warnSpy.mockRestore()
|
|
})
|
|
|
|
it('does not invoke auth error handler when status is not 401', async () => {
|
|
const onAuthError = vi.fn()
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
|
|
setAuthErrorHandler(onAuthError)
|
|
|
|
const error: ResponseError = {
|
|
response: { status: 403, data: { message: 'Forbidden' } },
|
|
config: { url: '/proxy-hosts' },
|
|
message: 'Original',
|
|
}
|
|
|
|
const handler = capturedHandlers.onRejected
|
|
expect(handler).toBeDefined()
|
|
|
|
const resultPromise = handler ? handler(error) : Promise.reject(new Error('handler missing'))
|
|
|
|
await expect(resultPromise).rejects.toBe(error)
|
|
expect(onAuthError).not.toHaveBeenCalled()
|
|
expect(warnSpy).not.toHaveBeenCalled()
|
|
|
|
warnSpy.mockRestore()
|
|
})
|
|
|
|
it('passes through successful responses via fulfilled interceptor', () => {
|
|
const responsePayload = { data: { ok: true } }
|
|
const fulfilled = capturedHandlers.onFulfilled
|
|
|
|
expect(fulfilled).toBeDefined()
|
|
const result = fulfilled ? fulfilled(responsePayload) : undefined
|
|
expect(result).toBe(responsePayload)
|
|
})
|
|
})
|