Files
xiaoxia-saas/apps/web/src/test/hooks/useAuth.test.tsx
T
xiaoxia 0b98d05b81
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 36s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 46s
AI Code Review / AI Code Review (pull_request) Successful in 1m8s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m31s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m35s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m4s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m48s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m49s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m21s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m38s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m2s
CI/CD Pipeline / CI Gate (pull_request) Successful in 11s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 1m7s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m29s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1035h4m11s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 1035h4m12s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1035h4m12s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1035h4m12s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1035h4m12s
CI/CD Pipeline / PR Build Worker Image (pull_request) Failing after 1035h7m35s
CI/CD Pipeline / PR Build API Image (pull_request) Failing after 1035h7m36s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 1035h7m37s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1035h8m9s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1035h8m9s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1035h8m13s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1035h8m15s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1035h8m15s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1035h8m15s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1035h37m28s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 1035h37m29s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 1035h41m25s
fix: prettier format useAuth.test.tsx
2026-08-04 12:19:05 +08:00

201 lines
5.9 KiB
TypeScript

/**
* useAuth hooks 测试
* 测试 useLogin / useRegister / useLogout / useCurrentUser
*/
import { describe, it, expect, vi, beforeEach } from "vitest"
import { renderHook, act } from "@testing-library/react"
import { MemoryRouter } from "react-router-dom"
const mockNavigate = vi.fn()
const mockSetAuth = vi.fn((_user: any, accessToken: string, refreshToken?: string | null) => {
localStorage.setItem("access_token", accessToken)
if (refreshToken) {
localStorage.setItem("refresh_token", refreshToken)
} else {
localStorage.removeItem("refresh_token")
}
})
const mockClearAuth = vi.fn(() => {
localStorage.removeItem("access_token")
localStorage.removeItem("refresh_token")
})
const mockMutateAsync = vi.fn()
const mockQueryClear = vi.fn()
vi.mock("react-router-dom", async () => {
const actual = await vi.importActual<typeof import("react-router-dom")>("react-router-dom")
return {
...actual,
useNavigate: () => mockNavigate,
}
})
vi.mock("@/store/authStore", () => ({
useAuthStore: (selector: any) =>
selector({
user: { id: "1", username: "testuser" },
token: "mock-token",
isAuthenticated: true,
setAuth: mockSetAuth,
clearAuth: mockClearAuth,
}),
}))
vi.mock("@tanstack/react-query", () => ({
useMutation: () => ({
mutate: vi.fn(),
mutateAsync: mockMutateAsync,
isLoading: false,
isError: false,
isSuccess: false,
data: null,
reset: vi.fn(),
}),
useQuery: ({ queryKey, queryFn, enabled }: any) => ({
data: enabled ? { id: "1", username: "testuser" } : undefined,
isLoading: false,
isError: false,
refetch: vi.fn(),
}),
useQueryClient: () => ({
clear: mockQueryClear,
invalidateQueries: vi.fn(),
setQueryData: vi.fn(),
}),
QueryClient: class {},
}))
vi.mock("@/api/auth", () => ({
login: vi.fn(),
register: vi.fn(),
logout: vi.fn(),
getCurrentUser: vi.fn().mockResolvedValue({ id: "1", username: "testuser" }),
}))
import { useLogin, useRegister, useLogout, useCurrentUser } from "@/hooks/useAuth"
import * as authApi from "@/api/auth"
const wrapper = ({ children }: { children: React.ReactNode }) => (
<MemoryRouter>{children}</MemoryRouter>
)
describe("useAuth hooks", () => {
beforeEach(() => {
vi.clearAllMocks()
mockMutateAsync.mockReset()
// 清空 localStorage
localStorage.clear()
})
describe("useLogin", () => {
it("应该返回 mutation 对象", () => {
const { result } = renderHook(() => useLogin(), { wrapper })
expect(result.current).toHaveProperty("mutateAsync")
expect(typeof result.current.mutateAsync).toBe("function")
})
it("登录成功时调用 setAuth 并跳转到登录前页面或首页", async () => {
mockMutateAsync.mockResolvedValue({
access_token: "access-123",
refresh_token: "refresh-456",
})
const { result } = renderHook(() => useLogin(), { wrapper })
await act(async () => {
await result.current.mutateAsync({ username: "test", password: "123" })
})
expect(mockSetAuth).toHaveBeenCalled()
expect(mockNavigate).toHaveBeenCalledWith("/app/dashboard", { replace: true })
})
it("登录成功后跳转到 login_redirect 指定的页面", async () => {
localStorage.setItem("login_redirect", "/app/templates")
mockMutateAsync.mockResolvedValue({
access_token: "access-123",
refresh_token: "refresh-456",
})
const { result } = renderHook(() => useLogin(), { wrapper })
await act(async () => {
await result.current.mutateAsync({ username: "test", password: "123" })
})
expect(mockNavigate).toHaveBeenCalledWith("/app/templates", { replace: true })
expect(localStorage.getItem("login_redirect")).toBeNull()
})
})
describe("useRegister", () => {
it("应该返回 mutation 对象", () => {
const { result } = renderHook(() => useRegister(), { wrapper })
expect(result.current).toHaveProperty("mutateAsync")
})
it("注册成功后跳转到登录页", async () => {
mockMutateAsync.mockResolvedValue({ success: true })
const { result } = renderHook(() => useRegister(), { wrapper })
await act(async () => {
await result.current.mutateAsync({ username: "test", password: "123", email: "a@b.com" })
})
expect(mockNavigate).toHaveBeenCalledWith(
"/login",
expect.objectContaining({ state: expect.any(Object) }),
)
})
})
describe("useLogout", () => {
it("应该返回 mutation 对象", () => {
const { result } = renderHook(() => useLogout(), { wrapper })
expect(result.current).toHaveProperty("mutateAsync")
})
it("登出成功时清除认证状态并跳转", async () => {
mockMutateAsync.mockResolvedValue({ success: true })
const { result } = renderHook(() => useLogout(), { wrapper })
await act(async () => {
await result.current.mutateAsync()
})
expect(mockClearAuth).toHaveBeenCalled()
expect(mockQueryClear).toHaveBeenCalled()
expect(mockNavigate).toHaveBeenCalledWith("/")
})
it("登出失败时仍然清除本地状态", async () => {
mockMutateAsync.mockRejectedValue(new Error("logout failed"))
const { result } = renderHook(() => useLogout(), { wrapper })
await act(async () => {
// 即使失败也不抛异常
try {
await result.current.mutateAsync()
} catch {
// expected
}
})
expect(mockClearAuth).toHaveBeenCalled()
expect(mockQueryClear).toHaveBeenCalled()
expect(mockNavigate).toHaveBeenCalledWith("/")
})
})
describe("useCurrentUser", () => {
it("应该返回 useQuery 结果", () => {
const { result } = renderHook(() => useCurrentUser(), { wrapper })
expect(result.current).toHaveProperty("data")
expect(result.current).toHaveProperty("isLoading")
})
})
})