import axios, { AxiosError, type InternalAxiosRequestConfig } from 'axios' import { beforeEach, describe, expect, it, vi } from 'vitest' 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(async () => { vi.clearAllMocks() vi.restoreAllMocks() window.localStorage.clear() const { resetAxiosAuthModuleState } = await import('@/config/axios') resetAxiosAuthModuleState() }) it('replays a late 401 with the newer stored token without rotating refresh again', async () => { writeStoredToken('access-a') const { default: axiosInstance } = await import('@/config/axios') 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) => { // 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 { getOrRefreshAccessToken } = await import('@/config/axios') 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') } } }) })