mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-09-19 09:40:13 +00:00
Add an API tokens management route to create, list, and revoke PATs, showing the plaintext token once on creation with scope and server-id selection. Mirror the nz-csrf cookie into the X-CSRF-Token header on unsafe fetcher methods (POST/PUT/PATCH/DELETE) for the server-side double-submit check, and self-heal expired sessions via refresh-token without a recursive fetch loop. Gate protected routes behind resolved auth state to avoid pre-auth SWR fetches, and fix the login loading/race so stale probes cannot clobber the session. Add i18n keys for the new screens across all locales. Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
152 lines
4.2 KiB
TypeScript
152 lines
4.2 KiB
TypeScript
import { act, render } from "@testing-library/react"
|
|
import { useEffect } from "react"
|
|
import { afterEach, beforeEach, expect, test, vi } from "vitest"
|
|
|
|
let profileStore: { id: number; role: number } | undefined
|
|
const setProfileSpy = vi.fn((p: any) => {
|
|
profileStore = p
|
|
})
|
|
|
|
vi.mock("./useMainStore", () => ({}))
|
|
vi.mock("@/hooks/useMainStore", () => ({
|
|
useMainStore: (selector: any) =>
|
|
selector({ profile: profileStore, setProfile: setProfileSpy }),
|
|
}))
|
|
|
|
vi.mock("react-i18next", () => ({
|
|
useTranslation: () => ({ t: (k: string) => k }),
|
|
}))
|
|
|
|
vi.mock("sonner", () => ({ toast: () => {} }))
|
|
|
|
const navigate = vi.fn()
|
|
vi.mock("react-router-dom", () => ({
|
|
useNavigate: () => navigate,
|
|
}))
|
|
|
|
// Initial getProfile() stays pending forever so loading can only be cleared
|
|
// by an explicit login/logout, which is exactly what these tests assert.
|
|
let initialProfilePromise: Promise<any>
|
|
let getProfileCall = 0
|
|
const loginRequest = vi.fn(async () => {})
|
|
vi.mock("@/api/user", () => ({
|
|
getProfile: vi.fn(() => {
|
|
getProfileCall++
|
|
if (getProfileCall === 1) return initialProfilePromise
|
|
return Promise.resolve({ id: 42, role: 0 })
|
|
}),
|
|
login: () => loginRequest(),
|
|
}))
|
|
|
|
beforeEach(() => {
|
|
profileStore = undefined
|
|
getProfileCall = 0
|
|
initialProfilePromise = new Promise<any>(() => {})
|
|
setProfileSpy.mockClear()
|
|
navigate.mockClear()
|
|
})
|
|
|
|
afterEach(() => {
|
|
document.body.innerHTML = ""
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
// AuthProvider starts with loading=true and only clears it in the initial
|
|
// mount probe's finally{}. A user can log in while that probe is still in
|
|
// flight (ProtectedRoute renders the login page during loading). If login()
|
|
// does not clear loading itself, ProtectedRoute keeps returning null for
|
|
// /dashboard and the freshly-authenticated user sees a blank screen until the
|
|
// unrelated probe settles.
|
|
test("login() clears loading even while the initial profile probe is still pending", async () => {
|
|
const { AuthProvider, useAuth } = await import("@/hooks/useAuth")
|
|
|
|
const captured: { auth?: ReturnType<typeof useAuth> } = {}
|
|
function Capture() {
|
|
const auth = useAuth()
|
|
useEffect(() => {
|
|
captured.auth = auth
|
|
})
|
|
captured.auth = auth
|
|
return null
|
|
}
|
|
|
|
await act(async () => {
|
|
render(
|
|
<AuthProvider>
|
|
<Capture />
|
|
</AuthProvider>,
|
|
)
|
|
})
|
|
|
|
// Initial probe still pending -> loading must be true.
|
|
expect(captured.auth!.loading).toBe(true)
|
|
|
|
await act(async () => {
|
|
await captured.auth!.login("u", "p")
|
|
})
|
|
|
|
// Login succeeded; loading must be false so ProtectedRoute renders.
|
|
expect(profileStore).toEqual({ id: 42, role: 0 })
|
|
expect(captured.auth!.loading).toBe(false)
|
|
})
|
|
|
|
test("loginOauth2() clears loading even while the initial profile probe is still pending", async () => {
|
|
const { AuthProvider, useAuth } = await import("@/hooks/useAuth")
|
|
|
|
const captured: { auth?: ReturnType<typeof useAuth> } = {}
|
|
function Capture() {
|
|
const auth = useAuth()
|
|
useEffect(() => {
|
|
captured.auth = auth
|
|
})
|
|
captured.auth = auth
|
|
return null
|
|
}
|
|
|
|
await act(async () => {
|
|
render(
|
|
<AuthProvider>
|
|
<Capture />
|
|
</AuthProvider>,
|
|
)
|
|
})
|
|
|
|
expect(captured.auth!.loading).toBe(true)
|
|
|
|
await act(async () => {
|
|
await captured.auth!.loginOauth2()
|
|
})
|
|
|
|
expect(captured.auth!.loading).toBe(false)
|
|
})
|
|
|
|
test("logout() clears loading even while the initial profile probe is still pending", async () => {
|
|
const { AuthProvider, useAuth } = await import("@/hooks/useAuth")
|
|
|
|
const captured: { auth?: ReturnType<typeof useAuth> } = {}
|
|
function Capture() {
|
|
const auth = useAuth()
|
|
useEffect(() => {
|
|
captured.auth = auth
|
|
})
|
|
captured.auth = auth
|
|
return null
|
|
}
|
|
|
|
await act(async () => {
|
|
render(
|
|
<AuthProvider>
|
|
<Capture />
|
|
</AuthProvider>,
|
|
)
|
|
})
|
|
|
|
expect(captured.auth!.loading).toBe(true)
|
|
|
|
await act(async () => {
|
|
captured.auth!.logout()
|
|
})
|
|
|
|
expect(captured.auth!.loading).toBe(false)
|
|
})
|