mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
adding tests
This commit is contained in:
parent
2f3dc4860f
commit
ca2ce4a946
3 changed files with 947 additions and 0 deletions
|
|
@ -0,0 +1,242 @@
|
|||
/* @vitest-environment jsdom */
|
||||
import React from "react";
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useAccessGroups, AccessGroupResponse } from "./useAccessGroups";
|
||||
import * as networking from "@/components/networking";
|
||||
|
||||
vi.mock("@/components/networking", () => ({
|
||||
getProxyBaseUrl: vi.fn(() => "http://proxy.example"),
|
||||
getGlobalLitellmHeaderName: vi.fn(() => "Authorization"),
|
||||
deriveErrorMessage: vi.fn((data: unknown) => (data as { detail?: string })?.detail ?? "Unknown error"),
|
||||
handleError: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
|
||||
default: vi.fn(() => ({
|
||||
accessToken: "test-token-123",
|
||||
userRole: "Admin",
|
||||
})),
|
||||
}));
|
||||
|
||||
const createQueryClient = () =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
gcTime: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const queryClient = createQueryClient();
|
||||
return React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
};
|
||||
|
||||
const mockAccessToken = "test-token-123";
|
||||
const mockAccessGroups: AccessGroupResponse[] = [
|
||||
{
|
||||
access_group_id: "ag-1",
|
||||
access_group_name: "Group One",
|
||||
description: "First group",
|
||||
access_model_ids: [],
|
||||
access_mcp_server_ids: [],
|
||||
access_agent_ids: [],
|
||||
assigned_team_ids: [],
|
||||
assigned_key_ids: [],
|
||||
created_at: "2025-01-01T00:00:00Z",
|
||||
created_by: "user-1",
|
||||
updated_at: "2025-01-01T00:00:00Z",
|
||||
updated_by: "user-1",
|
||||
},
|
||||
];
|
||||
|
||||
const fetchMock = vi.fn();
|
||||
|
||||
describe("useAccessGroups", () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(networking.getProxyBaseUrl).mockReturnValue("http://proxy.example");
|
||||
vi.mocked(networking.getGlobalLitellmHeaderName).mockReturnValue("Authorization");
|
||||
|
||||
const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized");
|
||||
vi.mocked(useAuthorizedModule.default).mockReturnValue({
|
||||
accessToken: mockAccessToken,
|
||||
userRole: "Admin",
|
||||
} as any);
|
||||
|
||||
global.fetch = fetchMock;
|
||||
});
|
||||
|
||||
it("should return hook result without errors", () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
expect(result.current).toBeDefined();
|
||||
expect(result.current).toHaveProperty("data");
|
||||
expect(result.current).toHaveProperty("isSuccess");
|
||||
expect(result.current).toHaveProperty("isError");
|
||||
expect(result.current).toHaveProperty("status");
|
||||
});
|
||||
|
||||
it("should return access groups when access token and admin role are present", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockAccessGroups),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"http://proxy.example/v1/access_group",
|
||||
expect.objectContaining({
|
||||
method: "GET",
|
||||
headers: expect.objectContaining({
|
||||
Authorization: `Bearer ${mockAccessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(result.current.data).toEqual(mockAccessGroups);
|
||||
});
|
||||
|
||||
it("should not fetch when access token is null", async () => {
|
||||
const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized");
|
||||
vi.mocked(useAuthorizedModule.default).mockReturnValue({
|
||||
accessToken: null,
|
||||
userRole: "Admin",
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
expect(result.current.isFetching).toBe(false);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.data).toBeUndefined();
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not fetch when access token is empty string", async () => {
|
||||
const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized");
|
||||
vi.mocked(useAuthorizedModule.default).mockReturnValue({
|
||||
accessToken: "",
|
||||
userRole: "Admin",
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
expect(result.current.isFetching).toBe(false);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.data).toBeUndefined();
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not fetch when user role is not an admin role", async () => {
|
||||
const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized");
|
||||
vi.mocked(useAuthorizedModule.default).mockReturnValue({
|
||||
accessToken: mockAccessToken,
|
||||
userRole: "Viewer",
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
expect(result.current.isFetching).toBe(false);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.data).toBeUndefined();
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not fetch when user role is null", async () => {
|
||||
const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized");
|
||||
vi.mocked(useAuthorizedModule.default).mockReturnValue({
|
||||
accessToken: mockAccessToken,
|
||||
userRole: null,
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
expect(result.current.isFetching).toBe(false);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.data).toBeUndefined();
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should fetch when user role is proxy_admin", async () => {
|
||||
const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized");
|
||||
vi.mocked(useAuthorizedModule.default).mockReturnValue({
|
||||
accessToken: mockAccessToken,
|
||||
userRole: "proxy_admin",
|
||||
} as any);
|
||||
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockAccessGroups),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalled();
|
||||
expect(result.current.data).toEqual(mockAccessGroups);
|
||||
});
|
||||
|
||||
it("should expose error state when fetch fails", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ detail: "Forbidden" }),
|
||||
} as Response);
|
||||
vi.mocked(networking.deriveErrorMessage).mockReturnValue("Forbidden");
|
||||
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.error).toBeInstanceOf(Error);
|
||||
expect((result.current.error as Error).message).toBe("Forbidden");
|
||||
expect(result.current.data).toBeUndefined();
|
||||
expect(networking.handleError).toHaveBeenCalledWith("Forbidden");
|
||||
});
|
||||
|
||||
it("should return empty array when API returns empty list", async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
|
||||
it("should propagate network errors", async () => {
|
||||
const networkError = new Error("Network failure");
|
||||
fetchMock.mockRejectedValue(networkError);
|
||||
|
||||
const { result } = renderHook(() => useAccessGroups(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.error).toEqual(networkError);
|
||||
expect(result.current.data).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,384 @@
|
|||
import { useAccessGroupDetails } from "@/app/(dashboard)/hooks/accessGroups/useAccessGroupDetails";
|
||||
import { AccessGroupResponse } from "@/app/(dashboard)/hooks/accessGroups/useAccessGroups";
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import { AccessGroupDetail } from "./AccessGroupsDetailsPage";
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/accessGroups/useAccessGroupDetails");
|
||||
vi.mock("./AccessGroupsModal/AccessGroupEditModal", () => ({
|
||||
AccessGroupEditModal: ({
|
||||
visible,
|
||||
onCancel,
|
||||
}: {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
}) =>
|
||||
visible ? (
|
||||
<div role="dialog" aria-label="Edit Access Group">
|
||||
<button onClick={onCancel}>Close Modal</button>
|
||||
</div>
|
||||
) : null,
|
||||
}));
|
||||
|
||||
const mockUseAccessGroupDetails = vi.mocked(useAccessGroupDetails);
|
||||
|
||||
const baseMockReturnValue = {
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
isFetching: false,
|
||||
isPending: false,
|
||||
isSuccess: true,
|
||||
status: "success" as const,
|
||||
dataUpdatedAt: 0,
|
||||
errorUpdatedAt: 0,
|
||||
failureCount: 0,
|
||||
failureReason: null,
|
||||
errorUpdateCount: 0,
|
||||
isFetched: true,
|
||||
isFetchedAfterMount: true,
|
||||
isRefetching: false,
|
||||
isLoadingError: false,
|
||||
isPaused: false,
|
||||
isPlaceholderData: false,
|
||||
isRefetchError: false,
|
||||
isStale: false,
|
||||
fetchStatus: "idle" as const,
|
||||
refetch: vi.fn(),
|
||||
} as unknown as ReturnType<typeof useAccessGroupDetails>;
|
||||
|
||||
const createMockAccessGroup = (
|
||||
overrides: Partial<AccessGroupResponse> = {}
|
||||
): AccessGroupResponse => ({
|
||||
access_group_id: "ag-1",
|
||||
access_group_name: "Test Group",
|
||||
description: "A test access group",
|
||||
access_model_ids: ["model-1", "model-2"],
|
||||
access_mcp_server_ids: ["mcp-1"],
|
||||
access_agent_ids: ["agent-1"],
|
||||
assigned_team_ids: ["team-1"],
|
||||
assigned_key_ids: ["key-1", "key-2"],
|
||||
created_at: "2025-01-01T00:00:00Z",
|
||||
created_by: null,
|
||||
updated_at: "2025-01-02T00:00:00Z",
|
||||
updated_by: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("AccessGroupDetail", () => {
|
||||
const mockOnBack = vi.fn();
|
||||
const accessGroupId = "ag-1";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUseAccessGroupDetails.mockReturnValue({
|
||||
...baseMockReturnValue,
|
||||
data: createMockAccessGroup(),
|
||||
} as ReturnType<typeof useAccessGroupDetails>);
|
||||
});
|
||||
|
||||
it("should render the component", () => {
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
expect(screen.getByRole("heading", { name: "Test Group" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show access group content when loading", () => {
|
||||
mockUseAccessGroupDetails.mockReturnValue({
|
||||
...baseMockReturnValue,
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
} as ReturnType<typeof useAccessGroupDetails>);
|
||||
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
expect(screen.queryByRole("heading", { name: "Test Group" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show empty state when access group is not found", () => {
|
||||
mockUseAccessGroupDetails.mockReturnValue({
|
||||
...baseMockReturnValue,
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
} as ReturnType<typeof useAccessGroupDetails>);
|
||||
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
expect(screen.getByText("Access group not found")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onBack when back button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
const buttons = screen.getAllByRole("button");
|
||||
const backButton = buttons.find((btn) => !btn.textContent?.includes("Edit"));
|
||||
await user.click(backButton!);
|
||||
|
||||
expect(mockOnBack).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should display access group name and ID", () => {
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
expect(screen.getByRole("heading", { name: "Test Group" })).toBeInTheDocument();
|
||||
expect(screen.getByText(/ID:/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display description in Group Details", () => {
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
expect(screen.getByText("Group Details")).toBeInTheDocument();
|
||||
expect(screen.getByText("A test access group")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display em dash when description is empty", () => {
|
||||
mockUseAccessGroupDetails.mockReturnValue({
|
||||
...baseMockReturnValue,
|
||||
data: createMockAccessGroup({ description: null }),
|
||||
} as ReturnType<typeof useAccessGroupDetails>);
|
||||
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
expect(screen.getByText("—")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should open edit modal when Edit Access Group button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
expect(screen.queryByRole("dialog", { name: "Edit Access Group" })).not.toBeInTheDocument();
|
||||
|
||||
const editButton = screen.getByRole("button", { name: /Edit Access Group/i });
|
||||
await user.click(editButton);
|
||||
|
||||
expect(screen.getByRole("dialog", { name: "Edit Access Group" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should close edit modal when Close Modal is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /Edit Access Group/i }));
|
||||
expect(screen.getByRole("dialog", { name: "Edit Access Group" })).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Close Modal" }));
|
||||
expect(screen.queryByRole("dialog", { name: "Edit Access Group" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display attached keys", () => {
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
expect(screen.getByText("Attached Keys")).toBeInTheDocument();
|
||||
expect(screen.getByText("key-1")).toBeInTheDocument();
|
||||
expect(screen.getByText("key-2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display attached teams", () => {
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
expect(screen.getByText("Attached Teams")).toBeInTheDocument();
|
||||
expect(screen.getByText("team-1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show View All button for keys when more than 5", () => {
|
||||
mockUseAccessGroupDetails.mockReturnValue({
|
||||
...baseMockReturnValue,
|
||||
data: createMockAccessGroup({
|
||||
assigned_key_ids: ["k1", "k2", "k3", "k4", "k5", "k6"],
|
||||
}),
|
||||
} as ReturnType<typeof useAccessGroupDetails>);
|
||||
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "View All (6)" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should toggle between View All and Show Less for keys", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockUseAccessGroupDetails.mockReturnValue({
|
||||
...baseMockReturnValue,
|
||||
data: createMockAccessGroup({
|
||||
assigned_key_ids: ["k1", "k2", "k3", "k4", "k5", "k6"],
|
||||
}),
|
||||
} as ReturnType<typeof useAccessGroupDetails>);
|
||||
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "View All (6)" }));
|
||||
expect(screen.getByRole("button", { name: "Show Less" })).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Show Less" }));
|
||||
expect(screen.getByRole("button", { name: "View All (6)" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show View All button for teams when more than 5", () => {
|
||||
mockUseAccessGroupDetails.mockReturnValue({
|
||||
...baseMockReturnValue,
|
||||
data: createMockAccessGroup({
|
||||
assigned_team_ids: ["t1", "t2", "t3", "t4", "t5", "t6"],
|
||||
}),
|
||||
} as ReturnType<typeof useAccessGroupDetails>);
|
||||
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "View All (6)" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show empty state when no keys attached", () => {
|
||||
mockUseAccessGroupDetails.mockReturnValue({
|
||||
...baseMockReturnValue,
|
||||
data: createMockAccessGroup({ assigned_key_ids: [] }),
|
||||
} as ReturnType<typeof useAccessGroupDetails>);
|
||||
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
expect(screen.getByText("No keys attached")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show empty state when no teams attached", () => {
|
||||
mockUseAccessGroupDetails.mockReturnValue({
|
||||
...baseMockReturnValue,
|
||||
data: createMockAccessGroup({ assigned_team_ids: [] }),
|
||||
} as ReturnType<typeof useAccessGroupDetails>);
|
||||
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
expect(screen.getByText("No teams attached")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display Models tab with model IDs", () => {
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
expect(screen.getByRole("tab", { name: /Models/i })).toBeInTheDocument();
|
||||
expect(screen.getByText("model-1")).toBeInTheDocument();
|
||||
expect(screen.getByText("model-2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display MCP Servers tab with server IDs", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
const mcpTab = screen.getByRole("tab", { name: /MCP Servers/i });
|
||||
expect(mcpTab).toBeInTheDocument();
|
||||
await user.click(mcpTab);
|
||||
expect(screen.getByText("mcp-1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display Agents tab with agent IDs", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
const agentsTab = screen.getByRole("tab", { name: /Agents/i });
|
||||
expect(agentsTab).toBeInTheDocument();
|
||||
await user.click(agentsTab);
|
||||
expect(screen.getByText("agent-1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show empty state in Models tab when no models assigned", () => {
|
||||
mockUseAccessGroupDetails.mockReturnValue({
|
||||
...baseMockReturnValue,
|
||||
data: createMockAccessGroup({ access_model_ids: [] }),
|
||||
} as ReturnType<typeof useAccessGroupDetails>);
|
||||
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
expect(screen.getByText("No models assigned to this group")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show empty state in MCP Servers tab when none assigned", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockUseAccessGroupDetails.mockReturnValue({
|
||||
...baseMockReturnValue,
|
||||
data: createMockAccessGroup({ access_mcp_server_ids: [] }),
|
||||
} as ReturnType<typeof useAccessGroupDetails>);
|
||||
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("tab", { name: /MCP Servers/i }));
|
||||
expect(screen.getByText("No MCP servers assigned to this group")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show empty state in Agents tab when none assigned", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockUseAccessGroupDetails.mockReturnValue({
|
||||
...baseMockReturnValue,
|
||||
data: createMockAccessGroup({ access_agent_ids: [] }),
|
||||
} as ReturnType<typeof useAccessGroupDetails>);
|
||||
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("tab", { name: /Agents/i }));
|
||||
expect(screen.getByText("No agents assigned to this group")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should truncate long key IDs with ellipsis", () => {
|
||||
const longKeyId = "a".repeat(25);
|
||||
mockUseAccessGroupDetails.mockReturnValue({
|
||||
...baseMockReturnValue,
|
||||
data: createMockAccessGroup({ assigned_key_ids: [longKeyId] }),
|
||||
} as ReturnType<typeof useAccessGroupDetails>);
|
||||
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
expect(screen.getByText(/a{10}\.\.\.a{6}/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display created and last updated timestamps", () => {
|
||||
renderWithProviders(
|
||||
<AccessGroupDetail accessGroupId={accessGroupId} onBack={mockOnBack} />
|
||||
);
|
||||
|
||||
expect(screen.getByText("Created")).toBeInTheDocument();
|
||||
expect(screen.getByText("Last Updated")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,321 @@
|
|||
import { renderWithProviders, screen, within } from "@/../tests/test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { AccessGroupsPage } from "./AccessGroupsPage";
|
||||
import type { AccessGroupResponse } from "@/app/(dashboard)/hooks/accessGroups/useAccessGroups";
|
||||
|
||||
const mockAccessGroups: AccessGroupResponse[] = [
|
||||
{
|
||||
access_group_id: "ag-1",
|
||||
access_group_name: "Admin Group",
|
||||
description: "Administrators with full access",
|
||||
access_model_ids: ["m1", "m2"],
|
||||
access_mcp_server_ids: ["s1"],
|
||||
access_agent_ids: ["a1"],
|
||||
assigned_team_ids: [],
|
||||
assigned_key_ids: [],
|
||||
created_at: "2024-01-15T10:00:00Z",
|
||||
created_by: "user-1",
|
||||
updated_at: "2024-01-20T12:00:00Z",
|
||||
updated_by: "user-1",
|
||||
},
|
||||
{
|
||||
access_group_id: "ag-2",
|
||||
access_group_name: "Read Only",
|
||||
description: "Read-only access to models",
|
||||
access_model_ids: ["m1"],
|
||||
access_mcp_server_ids: [],
|
||||
access_agent_ids: [],
|
||||
assigned_team_ids: [],
|
||||
assigned_key_ids: [],
|
||||
created_at: "2024-01-10T09:00:00Z",
|
||||
created_by: null,
|
||||
updated_at: "2024-01-12T11:00:00Z",
|
||||
updated_by: null,
|
||||
},
|
||||
];
|
||||
|
||||
const mockUseAccessGroups = vi.fn();
|
||||
const mockUseDeleteAccessGroup = vi.fn();
|
||||
const mockMutate = vi.fn();
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/accessGroups/useAccessGroups", () => ({
|
||||
useAccessGroups: () => mockUseAccessGroups(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/accessGroups/useDeleteAccessGroup", () => ({
|
||||
useDeleteAccessGroup: () => mockUseDeleteAccessGroup(),
|
||||
}));
|
||||
|
||||
vi.mock("./AccessGroupsDetailsPage", () => ({
|
||||
AccessGroupDetail: ({
|
||||
accessGroupId,
|
||||
onBack,
|
||||
}: {
|
||||
accessGroupId: string;
|
||||
onBack: () => void;
|
||||
}) => (
|
||||
<div data-testid="access-group-detail">
|
||||
<span>Detail for {accessGroupId}</span>
|
||||
<button onClick={onBack}>Back</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("./AccessGroupsModal/AccessGroupCreateModal", () => ({
|
||||
AccessGroupCreateModal: ({
|
||||
visible,
|
||||
onCancel,
|
||||
}: {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
}) =>
|
||||
visible ? (
|
||||
<div data-testid="create-access-group-modal">
|
||||
<button onClick={onCancel}>Cancel</button>
|
||||
</div>
|
||||
) : null,
|
||||
}));
|
||||
|
||||
vi.mock("../common_components/IconActionButton/TableIconActionButtons/TableIconActionButton", () => ({
|
||||
default: ({
|
||||
variant,
|
||||
tooltipText,
|
||||
onClick,
|
||||
}: {
|
||||
variant: string;
|
||||
tooltipText: string;
|
||||
onClick: () => void;
|
||||
}) => (
|
||||
<button
|
||||
data-testid={`action-button-${variant.toLowerCase()}`}
|
||||
aria-label={tooltipText}
|
||||
onClick={onClick}
|
||||
>
|
||||
{variant}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("AccessGroupsPage", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUseAccessGroups.mockReturnValue({
|
||||
data: mockAccessGroups,
|
||||
isLoading: false,
|
||||
});
|
||||
mockUseDeleteAccessGroup.mockReturnValue({
|
||||
mutate: mockMutate,
|
||||
isPending: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
expect(screen.getByRole("heading", { name: "Access Groups" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display page title and subtitle", () => {
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
expect(screen.getByRole("heading", { name: "Access Groups" })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Manage resource permissions for your organization"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display Create Access Group button", () => {
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
expect(
|
||||
screen.getByRole("button", { name: /create access group/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display search input with placeholder", () => {
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
expect(
|
||||
screen.getByPlaceholderText("Search groups by name, ID, or description..."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display access groups in table", () => {
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
expect(screen.getByText("ag-1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Admin Group")).toBeInTheDocument();
|
||||
expect(screen.getByText("ag-2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Read Only")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display resource counts for each group", () => {
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
const table = screen.getByRole("table");
|
||||
expect(table).toHaveTextContent("2");
|
||||
expect(table).toHaveTextContent("1");
|
||||
});
|
||||
|
||||
it("should filter groups by search text matching name", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
const searchInput = screen.getByPlaceholderText(
|
||||
"Search groups by name, ID, or description...",
|
||||
);
|
||||
await user.type(searchInput, "Admin");
|
||||
expect(screen.getByText("Admin Group")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Read Only")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should filter groups by search text matching ID", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
const searchInput = screen.getByPlaceholderText(
|
||||
"Search groups by name, ID, or description...",
|
||||
);
|
||||
await user.type(searchInput, "ag-2");
|
||||
expect(screen.getByText("Read Only")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Admin Group")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should filter groups by search text matching description", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
const searchInput = screen.getByPlaceholderText(
|
||||
"Search groups by name, ID, or description...",
|
||||
);
|
||||
await user.type(searchInput, "read-only");
|
||||
expect(screen.getByText("Read Only")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Admin Group")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should reset to first page when search text changes", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
const searchInput = screen.getByPlaceholderText(
|
||||
"Search groups by name, ID, or description...",
|
||||
);
|
||||
await user.type(searchInput, "Admin");
|
||||
const pagination = screen.getByText(/groups/);
|
||||
expect(pagination).toHaveTextContent("1 groups");
|
||||
});
|
||||
|
||||
it("should open create modal when Create Access Group button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
await user.click(screen.getByRole("button", { name: /create access group/i }));
|
||||
expect(screen.getByTestId("create-access-group-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should close create modal when cancel is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
await user.click(screen.getByRole("button", { name: /create access group/i }));
|
||||
expect(screen.getByTestId("create-access-group-modal")).toBeInTheDocument();
|
||||
await user.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
expect(screen.queryByTestId("create-access-group-modal")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should navigate to detail view when group ID is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
await user.click(screen.getByText("ag-1"));
|
||||
expect(screen.getByTestId("access-group-detail")).toBeInTheDocument();
|
||||
expect(screen.getByText("Detail for ag-1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should return to list view when Back is clicked from detail", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
await user.click(screen.getByText("ag-1"));
|
||||
expect(screen.getByTestId("access-group-detail")).toBeInTheDocument();
|
||||
await user.click(screen.getByRole("button", { name: "Back" }));
|
||||
expect(screen.queryByTestId("access-group-detail")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Admin Group")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should open delete modal when delete action is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
const deleteButtons = screen.getAllByRole("button", {
|
||||
name: "Delete access group",
|
||||
});
|
||||
await user.click(deleteButtons[0]);
|
||||
const dialog = screen.getByRole("dialog", { name: "Delete Access Group" });
|
||||
expect(dialog).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).getByText(
|
||||
"Are you sure you want to delete this access group? This action cannot be undone.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(within(dialog).getByText("Access Group Information")).toBeInTheDocument();
|
||||
expect(within(dialog).getByText("ag-1")).toBeInTheDocument();
|
||||
expect(within(dialog).getByText("Admin Group")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should close delete modal when cancel is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
const deleteButtons = screen.getAllByRole("button", {
|
||||
name: "Delete access group",
|
||||
});
|
||||
await user.click(deleteButtons[0]);
|
||||
const dialog = screen.getByRole("dialog", { name: "Delete Access Group" });
|
||||
await user.click(within(dialog).getByRole("button", { name: "Cancel" }));
|
||||
expect(screen.queryByRole("dialog", { name: "Delete Access Group" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call delete mutation when delete is confirmed", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockMutate.mockImplementation((_id: string, opts?: { onSuccess?: () => void }) => {
|
||||
opts?.onSuccess?.();
|
||||
});
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
const deleteButtons = screen.getAllByRole("button", {
|
||||
name: "Delete access group",
|
||||
});
|
||||
await user.click(deleteButtons[0]);
|
||||
const dialog = screen.getByRole("dialog", { name: "Delete Access Group" });
|
||||
const deleteConfirmButton = within(dialog).getByRole("button", { name: /delete/i });
|
||||
await user.click(deleteConfirmButton);
|
||||
expect(mockMutate).toHaveBeenCalledWith("ag-1", expect.any(Object));
|
||||
});
|
||||
|
||||
it("should display pagination with total count", () => {
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
expect(screen.getByText("2 groups")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show table headers for ID, Name, Resources, and Actions", () => {
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
expect(screen.getByRole("columnheader", { name: /ID/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("columnheader", { name: /Name/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("columnheader", { name: /Resources/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("columnheader", { name: /Actions/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display loading state when data is loading", () => {
|
||||
mockUseAccessGroups.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
});
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
const table = screen.getByRole("table");
|
||||
expect(table).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display empty state when no groups match search", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
const searchInput = screen.getByPlaceholderText(
|
||||
"Search groups by name, ID, or description...",
|
||||
);
|
||||
await user.type(searchInput, "nonexistent-group-xyz");
|
||||
expect(screen.getByRole("table")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display empty data when useAccessGroups returns empty array", () => {
|
||||
mockUseAccessGroups.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
});
|
||||
renderWithProviders(<AccessGroupsPage />);
|
||||
expect(screen.getByRole("table")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue