import { afterEach, describe, expect, it, vi } from 'vitest' import { attemptServerLogout, expireRefreshSessionOnThisOrigin, LOGOUT_RETRY_DELAYS_MS } from '@/lib/logoutSession' import { CLEAR_REFRESH_SESSION_HEADER, CLEAR_REFRESH_SESSION_PATH } from '@/lib/refreshSessionCookie' import { LOGOUT } from '@/services/auth' vi.mock('@/services/auth', () => ({ LOGOUT: vi.fn(), })) const logoutMock = vi.mocked(LOGOUT) const failedLogout = { ok: false as const, error: { message: 'unavailable' } as never } const okLogout = { ok: true as const, data: {} } describe('attemptServerLogout', () => { afterEach(() => { logoutMock.mockReset() vi.useRealTimers() }) it('returns true on the first successful Nest logout', async () => { logoutMock.mockResolvedValue(okLogout) await expect(attemptServerLogout()).resolves.toBe(true) expect(logoutMock).toHaveBeenCalledTimes(1) expect(logoutMock).toHaveBeenCalledWith({ errorMode: 'silent' }) }) it('retries failed Nest logout then succeeds', async () => { vi.useFakeTimers() logoutMock.mockResolvedValueOnce(failedLogout).mockResolvedValueOnce(failedLogout).mockResolvedValueOnce(okLogout) const pending = attemptServerLogout() await vi.runAllTimersAsync() await expect(pending).resolves.toBe(true) expect(logoutMock).toHaveBeenCalledTimes(3) }) it('gives up after the limited retry budget', async () => { vi.useFakeTimers() logoutMock.mockResolvedValue(failedLogout) const pending = attemptServerLogout() await vi.runAllTimersAsync() await expect(pending).resolves.toBe(false) expect(logoutMock).toHaveBeenCalledTimes(LOGOUT_RETRY_DELAYS_MS.length) }) }) describe('expireRefreshSessionOnThisOrigin', () => { afterEach(() => { vi.unstubAllGlobals() }) it('POSTs the same-origin clear-session route with the CSRF header', async () => { const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ ok: true }))) vi.stubGlobal('fetch', fetchMock) await expireRefreshSessionOnThisOrigin() expect(fetchMock).toHaveBeenCalledWith( CLEAR_REFRESH_SESSION_PATH, expect.objectContaining({ method: 'POST', credentials: 'same-origin', cache: 'no-store', headers: { [CLEAR_REFRESH_SESSION_HEADER]: '1' }, }) ) }) it('swallows network failures so local logout can continue', async () => { vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new TypeError('Failed to fetch'))) await expect(expireRefreshSessionOnThisOrigin()).resolves.toBeUndefined() }) })