import { describe, it, expect, vi, beforeEach } from "vitest"; import { renderHook, act, waitFor } from "@testing-library/react"; import type { User, Workspace, MemberWithUser, Agent } from "@multica/types"; // Mock next/navigation const mockPush = vi.fn(); vi.mock("next/navigation", () => ({ useRouter: () => ({ push: mockPush }), })); // Must use vi.hoisted so the mock object is defined before vi.mock factory runs const mockApi = vi.hoisted(() => ({ setToken: vi.fn(), setWorkspaceId: vi.fn(), login: vi.fn(), getMe: vi.fn(), listWorkspaces: vi.fn(), listMembers: vi.fn(), listAgents: vi.fn(), })); vi.mock("./api", () => ({ api: mockApi, })); import { AuthProvider, useAuth } from "./auth-context"; const mockUser: User = { id: "user-1", name: "Test User", email: "test@multica.ai", avatar_url: null, created_at: "2026-01-01T00:00:00Z", updated_at: "2026-01-01T00:00:00Z", }; const mockWorkspace: Workspace = { id: "ws-1", name: "Test WS", slug: "test", description: null, settings: {}, created_at: "2026-01-01T00:00:00Z", updated_at: "2026-01-01T00:00:00Z", }; const mockMembers: MemberWithUser[] = [ { id: "member-1", workspace_id: "ws-1", user_id: "user-1", role: "owner", created_at: "2026-01-01T00:00:00Z", name: "Test User", email: "test@multica.ai", avatar_url: null, }, { id: "member-2", workspace_id: "ws-1", user_id: "user-2", role: "member", created_at: "2026-01-01T00:00:00Z", name: "Other User", email: "other@multica.ai", avatar_url: null, }, ]; const mockAgents: Agent[] = [ { id: "agent-1", workspace_id: "ws-1", name: "Claude", status: "idle", runtime_mode: "cloud", visibility: "workspace", max_concurrent_tasks: 3, description: null, system_prompt: null, config: {}, created_at: "2026-01-01T00:00:00Z", updated_at: "2026-01-01T00:00:00Z", }, ]; function wrapper({ children }: { children: React.ReactNode }) { return {children}; } describe("AuthContext", () => { beforeEach(() => { vi.clearAllMocks(); // Clear localStorage manually since jsdom may not have .clear() localStorage.removeItem("multica_token"); localStorage.removeItem("multica_workspace_id"); }); it("starts with null user when no token stored", async () => { const { result } = renderHook(() => useAuth(), { wrapper }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.user).toBeNull(); expect(result.current.workspace).toBeNull(); }); it("login stores token and navigates to /issues", async () => { mockApi.login.mockResolvedValueOnce({ token: "test-jwt", user: mockUser }); mockApi.listWorkspaces.mockResolvedValueOnce([mockWorkspace]); mockApi.listMembers.mockResolvedValueOnce(mockMembers); mockApi.listAgents.mockResolvedValueOnce(mockAgents); const { result } = renderHook(() => useAuth(), { wrapper }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); await act(async () => { await result.current.login("test@multica.ai", "Test User"); }); expect(mockApi.login).toHaveBeenCalledWith("test@multica.ai", "Test User"); expect(mockApi.setToken).toHaveBeenCalledWith("test-jwt"); expect(localStorage.getItem("multica_token")).toBe("test-jwt"); expect(result.current.user).toEqual(mockUser); expect(result.current.workspace).toEqual(mockWorkspace); expect(result.current.members).toEqual(mockMembers); expect(result.current.agents).toEqual(mockAgents); expect(mockPush).toHaveBeenCalledWith("/issues"); }); it("logout clears state and navigates to /login", async () => { mockApi.login.mockResolvedValueOnce({ token: "test-jwt", user: mockUser }); mockApi.listWorkspaces.mockResolvedValueOnce([mockWorkspace]); mockApi.listMembers.mockResolvedValueOnce(mockMembers); mockApi.listAgents.mockResolvedValueOnce(mockAgents); const { result } = renderHook(() => useAuth(), { wrapper }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); await act(async () => { await result.current.login("test@multica.ai"); }); act(() => { result.current.logout(); }); expect(localStorage.getItem("multica_token")).toBeNull(); expect(localStorage.getItem("multica_workspace_id")).toBeNull(); expect(result.current.user).toBeNull(); expect(result.current.workspace).toBeNull(); expect(result.current.members).toEqual([]); expect(result.current.agents).toEqual([]); expect(mockPush).toHaveBeenCalledWith("/login"); }); it("getMemberName returns correct name for known user", async () => { mockApi.login.mockResolvedValueOnce({ token: "test-jwt", user: mockUser }); mockApi.listWorkspaces.mockResolvedValueOnce([mockWorkspace]); mockApi.listMembers.mockResolvedValueOnce(mockMembers); mockApi.listAgents.mockResolvedValueOnce(mockAgents); const { result } = renderHook(() => useAuth(), { wrapper }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); await act(async () => { await result.current.login("test@multica.ai"); }); expect(result.current.getMemberName("user-1")).toBe("Test User"); expect(result.current.getMemberName("user-2")).toBe("Other User"); expect(result.current.getMemberName("unknown")).toBe("Unknown"); }); it("getAgentName returns correct name for known agent", async () => { mockApi.login.mockResolvedValueOnce({ token: "test-jwt", user: mockUser }); mockApi.listWorkspaces.mockResolvedValueOnce([mockWorkspace]); mockApi.listMembers.mockResolvedValueOnce(mockMembers); mockApi.listAgents.mockResolvedValueOnce(mockAgents); const { result } = renderHook(() => useAuth(), { wrapper }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); await act(async () => { await result.current.login("test@multica.ai"); }); expect(result.current.getAgentName("agent-1")).toBe("Claude"); expect(result.current.getAgentName("unknown")).toBe("Unknown Agent"); }); it("getActorName dispatches to member or agent", async () => { mockApi.login.mockResolvedValueOnce({ token: "test-jwt", user: mockUser }); mockApi.listWorkspaces.mockResolvedValueOnce([mockWorkspace]); mockApi.listMembers.mockResolvedValueOnce(mockMembers); mockApi.listAgents.mockResolvedValueOnce(mockAgents); const { result } = renderHook(() => useAuth(), { wrapper }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); await act(async () => { await result.current.login("test@multica.ai"); }); expect(result.current.getActorName("member", "user-1")).toBe("Test User"); expect(result.current.getActorName("agent", "agent-1")).toBe("Claude"); expect(result.current.getActorName("system", "xxx")).toBe("System"); }); it("getActorInitials returns uppercase initials", async () => { mockApi.login.mockResolvedValueOnce({ token: "test-jwt", user: mockUser }); mockApi.listWorkspaces.mockResolvedValueOnce([mockWorkspace]); mockApi.listMembers.mockResolvedValueOnce(mockMembers); mockApi.listAgents.mockResolvedValueOnce(mockAgents); const { result } = renderHook(() => useAuth(), { wrapper }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); await act(async () => { await result.current.login("test@multica.ai"); }); expect(result.current.getActorInitials("member", "user-1")).toBe("TU"); expect(result.current.getActorInitials("agent", "agent-1")).toBe("C"); }); it("initializes from localStorage token on mount", async () => { localStorage.setItem("multica_token", "stored-token"); localStorage.setItem("multica_workspace_id", "ws-1"); mockApi.getMe.mockResolvedValueOnce(mockUser); mockApi.listWorkspaces.mockResolvedValueOnce([mockWorkspace]); mockApi.listMembers.mockResolvedValueOnce(mockMembers); mockApi.listAgents.mockResolvedValueOnce(mockAgents); const { result } = renderHook(() => useAuth(), { wrapper }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(mockApi.setToken).toHaveBeenCalledWith("stored-token"); expect(result.current.user).toEqual(mockUser); expect(result.current.workspace).toEqual(mockWorkspace); }); it("clears token when stored token is invalid", async () => { localStorage.setItem("multica_token", "invalid-token"); mockApi.getMe.mockRejectedValueOnce(new Error("Unauthorized")); const { result } = renderHook(() => useAuth(), { wrapper }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.user).toBeNull(); expect(localStorage.getItem("multica_token")).toBeNull(); }); });