admin/config/axios.functional.test.ts
alisaza 3f072976fe test(axios): refactor axios functional tests for improved clarity
Refactored the axios functional tests by removing unnecessary asynchronous imports and simplifying the setup in the `beforeEach` hook. This change enhances the readability and maintainability of the test code, ensuring it aligns with the project's coding standards.
2026-09-13 13:31:19 +03:30

101 lines
3.2 KiB
TypeScript

import axios, { AxiosError, type InternalAxiosRequestConfig } from 'axios'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import axiosInstance, { getOrRefreshAccessToken, resetAxiosAuthModuleState } from '@/config/axios'
const mocks = vi.hoisted(() => ({
expireRefreshSession: vi.fn().mockResolvedValue(undefined),
}))
vi.mock('@/lib/expireRefreshSession', () => ({
expireRefreshSessionOnThisOrigin: () => mocks.expireRefreshSession(),
}))
const writeStoredToken = (accessToken: string): void => {
window.localStorage.setItem(
'user',
JSON.stringify({
accessToken,
userId: 'admin-1',
role: 'admin',
sessionId: accessToken === 'access-a' ? 'session-a' : 'session-b',
AccessTokenExpireTime: Date.now() + 10 * 60_000,
})
)
}
describe('admin access-token refresh races', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.restoreAllMocks()
window.localStorage.clear()
resetAxiosAuthModuleState()
})
it('replays a late 401 with the newer stored token without rotating refresh again', async () => {
writeStoredToken('access-a')
const refreshRequest = vi.spyOn(axios, 'post')
const authorizationHeaders: string[] = []
let requestCount = 0
const adapter = async (config: InternalAxiosRequestConfig) => {
requestCount += 1
authorizationHeaders.push(String(config.headers.get('Authorization')))
if (requestCount === 1) {
// Another request/tab finished refresh while this request was in flight.
writeStoredToken('access-b')
throw new AxiosError('Unauthorized', 'ERR_BAD_REQUEST', config, undefined, {
config,
data: { message: 'Unauthorized' },
headers: {},
status: 401,
statusText: 'Unauthorized',
})
}
return {
config,
data: { ok: true },
headers: {},
status: 200,
statusText: 'OK',
}
}
await expect(axiosInstance.get('/race', { adapter })).resolves.toMatchObject({ data: { ok: true } })
expect(authorizationHeaders).toEqual(['Bearer access-a', 'Bearer access-b'])
expect(refreshRequest).not.toHaveBeenCalled()
expect(mocks.expireRefreshSession).not.toHaveBeenCalled()
})
it('reuses a token published by another tab while waiting for the refresh lock', async () => {
writeStoredToken('access-a')
const locksDescriptor = Object.getOwnPropertyDescriptor(navigator, 'locks')
const requestLock = vi.fn(async (_name: string, callback: () => Promise<string | null>) => {
// The other lock owner completes rotation before this callback starts.
writeStoredToken('access-b')
return callback()
})
Object.defineProperty(navigator, 'locks', {
configurable: true,
value: { request: requestLock },
})
try {
const refreshRequest = vi.spyOn(axios, 'post')
await expect(getOrRefreshAccessToken()).resolves.toBe('access-b')
expect(requestLock).toHaveBeenCalledOnce()
expect(refreshRequest).not.toHaveBeenCalled()
} finally {
if (locksDescriptor) {
Object.defineProperty(navigator, 'locks', locksDescriptor)
} else {
Reflect.deleteProperty(navigator, 'locks')
}
}
})
})