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.
101 lines
3.2 KiB
TypeScript
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')
|
|
}
|
|
}
|
|
})
|
|
})
|