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
201 lines
5.9 KiB
TypeScript
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")
|
|
})
|
|
})
|
|
})
|