diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroups.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroups.test.ts
new file mode 100644
index 00000000000..587064353ac
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroups.test.ts
@@ -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();
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsDetailsPage.test.tsx b/ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsDetailsPage.test.tsx
new file mode 100644
index 00000000000..db9d25d886f
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsDetailsPage.test.tsx
@@ -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 ? (
+
+
+
+ ) : 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;
+
+const createMockAccessGroup = (
+ overrides: Partial = {}
+): 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);
+ });
+
+ it("should render the component", () => {
+ renderWithProviders(
+
+ );
+ 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);
+
+ renderWithProviders(
+
+ );
+
+ 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);
+
+ renderWithProviders(
+
+ );
+
+ 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(
+
+ );
+
+ 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(
+
+ );
+
+ expect(screen.getByRole("heading", { name: "Test Group" })).toBeInTheDocument();
+ expect(screen.getByText(/ID:/)).toBeInTheDocument();
+ });
+
+ it("should display description in Group Details", () => {
+ renderWithProviders(
+
+ );
+
+ 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);
+
+ renderWithProviders(
+
+ );
+
+ expect(screen.getByText("—")).toBeInTheDocument();
+ });
+
+ it("should open edit modal when Edit Access Group button is clicked", async () => {
+ const user = userEvent.setup();
+ renderWithProviders(
+
+ );
+
+ 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(
+
+ );
+
+ 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(
+
+ );
+
+ expect(screen.getByText("Attached Keys")).toBeInTheDocument();
+ expect(screen.getByText("key-1")).toBeInTheDocument();
+ expect(screen.getByText("key-2")).toBeInTheDocument();
+ });
+
+ it("should display attached teams", () => {
+ renderWithProviders(
+
+ );
+
+ 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);
+
+ renderWithProviders(
+
+ );
+
+ 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);
+
+ renderWithProviders(
+
+ );
+
+ 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);
+
+ renderWithProviders(
+
+ );
+
+ 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);
+
+ renderWithProviders(
+
+ );
+
+ 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);
+
+ renderWithProviders(
+
+ );
+
+ expect(screen.getByText("No teams attached")).toBeInTheDocument();
+ });
+
+ it("should display Models tab with model IDs", () => {
+ renderWithProviders(
+
+ );
+
+ 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(
+
+ );
+
+ 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(
+
+ );
+
+ 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);
+
+ renderWithProviders(
+
+ );
+
+ 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);
+
+ renderWithProviders(
+
+ );
+
+ 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);
+
+ renderWithProviders(
+
+ );
+
+ 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);
+
+ renderWithProviders(
+
+ );
+
+ expect(screen.getByText(/a{10}\.\.\.a{6}/)).toBeInTheDocument();
+ });
+
+ it("should display created and last updated timestamps", () => {
+ renderWithProviders(
+
+ );
+
+ expect(screen.getByText("Created")).toBeInTheDocument();
+ expect(screen.getByText("Last Updated")).toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsPage.test.tsx b/ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsPage.test.tsx
new file mode 100644
index 00000000000..dd38cd61d92
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsPage.test.tsx
@@ -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;
+ }) => (
+
+ Detail for {accessGroupId}
+
+
+ ),
+}));
+
+vi.mock("./AccessGroupsModal/AccessGroupCreateModal", () => ({
+ AccessGroupCreateModal: ({
+ visible,
+ onCancel,
+ }: {
+ visible: boolean;
+ onCancel: () => void;
+ }) =>
+ visible ? (
+
+
+
+ ) : null,
+}));
+
+vi.mock("../common_components/IconActionButton/TableIconActionButtons/TableIconActionButton", () => ({
+ default: ({
+ variant,
+ tooltipText,
+ onClick,
+ }: {
+ variant: string;
+ tooltipText: string;
+ onClick: () => void;
+ }) => (
+
+ ),
+}));
+
+describe("AccessGroupsPage", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockUseAccessGroups.mockReturnValue({
+ data: mockAccessGroups,
+ isLoading: false,
+ });
+ mockUseDeleteAccessGroup.mockReturnValue({
+ mutate: mockMutate,
+ isPending: false,
+ });
+ });
+
+ it("should render", () => {
+ renderWithProviders();
+ expect(screen.getByRole("heading", { name: "Access Groups" })).toBeInTheDocument();
+ });
+
+ it("should display page title and subtitle", () => {
+ renderWithProviders();
+ 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();
+ expect(
+ screen.getByRole("button", { name: /create access group/i }),
+ ).toBeInTheDocument();
+ });
+
+ it("should display search input with placeholder", () => {
+ renderWithProviders();
+ expect(
+ screen.getByPlaceholderText("Search groups by name, ID, or description..."),
+ ).toBeInTheDocument();
+ });
+
+ it("should display access groups in table", () => {
+ renderWithProviders();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ expect(screen.getByText("2 groups")).toBeInTheDocument();
+ });
+
+ it("should show table headers for ID, Name, Resources, and Actions", () => {
+ renderWithProviders();
+ 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();
+ const table = screen.getByRole("table");
+ expect(table).toBeInTheDocument();
+ });
+
+ it("should display empty state when no groups match search", async () => {
+ const user = userEvent.setup();
+ renderWithProviders();
+ 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();
+ expect(screen.getByRole("table")).toBeInTheDocument();
+ });
+});