Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
83 lines
2.5 KiB
TypeScript
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()
|
|
})
|
|
})
|