admin/lib/logoutSession.test.ts
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

83 lines
2.5 KiB
TypeScript

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()
})
})