- Replace all mock data with real API calls across pages (issues, agents, inbox, settings) - Add AuthProvider context with JWT login/logout, member/agent name resolution - Implement login page with email-based auth flow - Add settings page with workspace editing and member list - Wire up real-time WebSocket for live issue updates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
281 lines
8.8 KiB
TypeScript
281 lines
8.8 KiB
TypeScript
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 <AuthProvider>{children}</AuthProvider>;
|
|
}
|
|
|
|
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();
|
|
});
|
|
});
|