[Test] Add unit tests for 5 untested policy components
Adds Vitest + RTL test files for policy_table, policy_templates, guardrail_selection_modal, impact_popover, and add_attachment_form. 53 tests total covering rendering, user interactions, API calls, and conditional UI behavior. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3a2cba43dc
commit
a0fb994ef0
@ -0,0 +1,110 @@
|
||||
import React from "react";
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as networking from "../networking";
|
||||
import AddAttachmentForm from "./add_attachment_form";
|
||||
import { Policy } from "./types";
|
||||
|
||||
vi.mock("../networking");
|
||||
|
||||
vi.mock("./impact_preview_alert", () => ({
|
||||
default: ({ impactResult }: { impactResult: any }) =>
|
||||
React.createElement("div", { "data-testid": "impact-preview" }, `${impactResult.affected_keys_count} keys`),
|
||||
}));
|
||||
|
||||
const makePolicy = (overrides: Partial<Policy> = {}): Policy => ({
|
||||
policy_id: "policy-id-1",
|
||||
policy_name: "test-policy",
|
||||
inherit: null,
|
||||
description: null,
|
||||
guardrails_add: [],
|
||||
guardrails_remove: [],
|
||||
condition: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
visible: true,
|
||||
onClose: vi.fn(),
|
||||
onSuccess: vi.fn(),
|
||||
accessToken: "test-token",
|
||||
policies: [makePolicy({ policy_name: "policy-alpha" }), makePolicy({ policy_name: "policy-beta", policy_id: "id-2" })],
|
||||
createAttachment: vi.fn(),
|
||||
};
|
||||
|
||||
describe("AddAttachmentForm", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(networking.teamListCall).mockResolvedValue([]);
|
||||
vi.mocked(networking.keyListCall).mockResolvedValue({ keys: [] });
|
||||
vi.mocked(networking.modelAvailableCall).mockResolvedValue({ data: [] });
|
||||
});
|
||||
|
||||
it("should render the modal title when visible", async () => {
|
||||
renderWithProviders(<AddAttachmentForm {...defaultProps} />);
|
||||
expect(await screen.findByText("Create Policy Attachment")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not render modal content when visible is false", () => {
|
||||
renderWithProviders(<AddAttachmentForm {...defaultProps} visible={false} />);
|
||||
expect(screen.queryByText("Create Policy Attachment")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should fetch teams, keys, and models on mount when visible and accessToken are provided", async () => {
|
||||
renderWithProviders(<AddAttachmentForm {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(networking.teamListCall).toHaveBeenCalled();
|
||||
expect(networking.keyListCall).toHaveBeenCalled();
|
||||
expect(networking.modelAvailableCall).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not fetch teams, keys, or models when accessToken is null", () => {
|
||||
renderWithProviders(<AddAttachmentForm {...defaultProps} accessToken={null} />);
|
||||
expect(networking.teamListCall).not.toHaveBeenCalled();
|
||||
expect(networking.keyListCall).not.toHaveBeenCalled();
|
||||
expect(networking.modelAvailableCall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call onClose when the Cancel button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<AddAttachmentForm {...defaultProps} />);
|
||||
await user.click(await screen.findByRole("button", { name: /cancel/i }));
|
||||
expect(defaultProps.onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not show scope-specific fields when scope is global (default)", async () => {
|
||||
renderWithProviders(<AddAttachmentForm {...defaultProps} />);
|
||||
await screen.findByText("Create Policy Attachment");
|
||||
expect(screen.queryByText("Teams")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Keys")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Models")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show Teams, Keys, Models, and Tags fields when scope is switched to specific", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<AddAttachmentForm {...defaultProps} />);
|
||||
await screen.findByText("Create Policy Attachment");
|
||||
await user.click(screen.getByRole("radio", { name: /specific/i }));
|
||||
expect(screen.getByText("Teams")).toBeInTheDocument();
|
||||
expect(screen.getByText("Keys")).toBeInTheDocument();
|
||||
expect(screen.getByText("Models")).toBeInTheDocument();
|
||||
expect(screen.getByText("Tags")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the 'Estimate Impact' button only when scope is specific", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<AddAttachmentForm {...defaultProps} />);
|
||||
await screen.findByText("Create Policy Attachment");
|
||||
expect(screen.queryByRole("button", { name: /estimate impact/i })).not.toBeInTheDocument();
|
||||
await user.click(screen.getByRole("radio", { name: /specific/i }));
|
||||
expect(screen.getByRole("button", { name: /estimate impact/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render a 'Create Attachment' submit button", async () => {
|
||||
renderWithProviders(<AddAttachmentForm {...defaultProps} />);
|
||||
expect(await screen.findByRole("button", { name: /create attachment/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,127 @@
|
||||
import React from "react";
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import GuardrailSelectionModal from "./guardrail_selection_modal";
|
||||
|
||||
const makeGuardrailDef = (name: string, description = "A guardrail description") => ({
|
||||
guardrail_name: name,
|
||||
guardrail_info: { description },
|
||||
litellm_params: { guardrail: "presidio", mode: "pre_call" },
|
||||
});
|
||||
|
||||
const makeTemplate = (guardrailDefs: any[] = [], overrides: any = {}) => ({
|
||||
title: "Test Template",
|
||||
guardrailDefinitions: guardrailDefs,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
visible: true,
|
||||
template: makeTemplate([makeGuardrailDef("guardrail-new-1"), makeGuardrailDef("guardrail-new-2")]),
|
||||
existingGuardrails: new Set<string>(),
|
||||
onConfirm: vi.fn(),
|
||||
onCancel: vi.fn(),
|
||||
};
|
||||
|
||||
describe("GuardrailSelectionModal", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render guardrail names from the template", async () => {
|
||||
renderWithProviders(<GuardrailSelectionModal {...defaultProps} />);
|
||||
expect(await screen.findByText("guardrail-new-1")).toBeInTheDocument();
|
||||
expect(screen.getByText("guardrail-new-2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should pre-select only new guardrails when the modal opens", async () => {
|
||||
renderWithProviders(<GuardrailSelectionModal {...defaultProps} />);
|
||||
await screen.findByText("guardrail-new-1");
|
||||
const checkboxes = screen.getAllByRole("checkbox");
|
||||
checkboxes.forEach((cb) => expect(cb).toBeChecked());
|
||||
});
|
||||
|
||||
it("should not show a checkbox for guardrails that already exist", async () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
template: makeTemplate([makeGuardrailDef("existing-g"), makeGuardrailDef("new-g")]),
|
||||
existingGuardrails: new Set(["existing-g"]),
|
||||
};
|
||||
renderWithProviders(<GuardrailSelectionModal {...props} />);
|
||||
await screen.findByText("existing-g");
|
||||
expect(screen.getAllByRole("checkbox")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should show an 'Already exists' tag for guardrails that exist in the system", async () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
template: makeTemplate([makeGuardrailDef("existing-g")]),
|
||||
existingGuardrails: new Set(["existing-g"]),
|
||||
};
|
||||
renderWithProviders(<GuardrailSelectionModal {...props} />);
|
||||
expect(await screen.findByText("Already exists")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show 'Create N Guardrails & Use Template' on the confirm button when N guardrails are selected", async () => {
|
||||
renderWithProviders(<GuardrailSelectionModal {...defaultProps} />);
|
||||
expect(await screen.findByRole("button", { name: /create 2 guardrails & use template/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show 'Use Template' on the confirm button when no new guardrails are selected", async () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
template: makeTemplate([makeGuardrailDef("existing-g")]),
|
||||
existingGuardrails: new Set(["existing-g"]),
|
||||
};
|
||||
renderWithProviders(<GuardrailSelectionModal {...props} />);
|
||||
expect(await screen.findByRole("button", { name: /^use template$/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onConfirm with the definitions of selected guardrails when confirmed", async () => {
|
||||
const user = userEvent.setup();
|
||||
const def = makeGuardrailDef("my-guardrail");
|
||||
const props = { ...defaultProps, template: makeTemplate([def]) };
|
||||
renderWithProviders(<GuardrailSelectionModal {...props} />);
|
||||
await user.click(await screen.findByRole("button", { name: /create 1 guardrail/i }));
|
||||
expect(defaultProps.onConfirm).toHaveBeenCalledWith([def]);
|
||||
});
|
||||
|
||||
it("should deselect all guardrails when 'Deselect All' is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<GuardrailSelectionModal {...defaultProps} />);
|
||||
await screen.findByText("guardrail-new-1");
|
||||
await user.click(screen.getByRole("button", { name: /deselect all/i }));
|
||||
screen.getAllByRole("checkbox").forEach((cb) => expect(cb).not.toBeChecked());
|
||||
});
|
||||
|
||||
it("should re-select all new guardrails when 'Select All New' is clicked after deselecting", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<GuardrailSelectionModal {...defaultProps} />);
|
||||
await screen.findByText("guardrail-new-1");
|
||||
await user.click(screen.getByRole("button", { name: /deselect all/i }));
|
||||
await user.click(screen.getByRole("button", { name: /select all new/i }));
|
||||
screen.getAllByRole("checkbox").forEach((cb) => expect(cb).toBeChecked());
|
||||
});
|
||||
|
||||
it("should show 'No guardrails defined' when the template has no guardrail definitions", async () => {
|
||||
const props = { ...defaultProps, template: makeTemplate([]) };
|
||||
renderWithProviders(<GuardrailSelectionModal {...props} />);
|
||||
expect(await screen.findByText(/no guardrails defined for this template/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show a progress badge when progressInfo is provided", async () => {
|
||||
const props = { ...defaultProps, progressInfo: { current: 2, total: 5 } };
|
||||
renderWithProviders(<GuardrailSelectionModal {...props} />);
|
||||
expect(await screen.findByText(/template 2 of 5/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onCancel when the Cancel button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<GuardrailSelectionModal {...defaultProps} />);
|
||||
await screen.findByText("guardrail-new-1");
|
||||
await user.click(screen.getByRole("button", { name: /^cancel$/i }));
|
||||
expect(defaultProps.onCancel).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,184 @@
|
||||
import React from "react";
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as networking from "../networking";
|
||||
import ImpactPopover from "./impact_popover";
|
||||
import { PolicyAttachment } from "./types";
|
||||
|
||||
vi.mock("../networking");
|
||||
|
||||
vi.mock("@heroicons/react/outline", () => ({
|
||||
EyeIcon: function EyeIcon() { return null; },
|
||||
}));
|
||||
|
||||
vi.mock("@tremor/react", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@tremor/react")>();
|
||||
return {
|
||||
...actual,
|
||||
Icon: ({ icon: IconComp, onClick, className }: any) =>
|
||||
React.createElement("button", { type: "button", onClick, className }, IconComp?.displayName ?? IconComp?.name ?? "icon"),
|
||||
};
|
||||
});
|
||||
|
||||
// Expose the Popover's onOpenChange so tests can trigger it programmatically.
|
||||
vi.mock("antd", async (importOriginal) => {
|
||||
const actual = await importOriginal<any>();
|
||||
return {
|
||||
...actual,
|
||||
Popover: ({ children, onOpenChange, content }: any) =>
|
||||
React.createElement(
|
||||
"div",
|
||||
null,
|
||||
React.createElement("div", { "data-testid": "popover-content" }, content),
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
role: "button",
|
||||
"aria-label": "open-popover",
|
||||
onClick: () => onOpenChange?.(true),
|
||||
},
|
||||
children
|
||||
)
|
||||
),
|
||||
Tooltip: ({ children }: any) => React.createElement(React.Fragment, null, children),
|
||||
Spin: () => React.createElement("span", null, "Loading..."),
|
||||
Tag: ({ children }: any) => React.createElement("span", null, children),
|
||||
};
|
||||
});
|
||||
|
||||
const makeAttachment = (overrides: Partial<PolicyAttachment> = {}): PolicyAttachment => ({
|
||||
attachment_id: "att-001",
|
||||
policy_name: "my-policy",
|
||||
scope: null,
|
||||
teams: [],
|
||||
keys: [],
|
||||
models: [],
|
||||
tags: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("ImpactPopover", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
renderWithProviders(<ImpactPopover attachment={makeAttachment()} accessToken="tok" />);
|
||||
expect(screen.getByRole("button", { name: /open-popover/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show 'Click to load' as the initial popover content", () => {
|
||||
renderWithProviders(<ImpactPopover attachment={makeAttachment()} accessToken="tok" />);
|
||||
expect(screen.getByText(/click to load/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call estimateAttachmentImpactCall when the popover is opened", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(networking.estimateAttachmentImpactCall).mockResolvedValue({
|
||||
affected_keys_count: 0,
|
||||
affected_teams_count: 0,
|
||||
sample_keys: [],
|
||||
sample_teams: [],
|
||||
});
|
||||
const attachment = makeAttachment({ policy_name: "rate-limit", teams: ["team-a"] });
|
||||
renderWithProviders(<ImpactPopover attachment={attachment} accessToken="my-token" />);
|
||||
await user.click(screen.getByRole("button", { name: /open-popover/i }));
|
||||
await waitFor(() => {
|
||||
expect(networking.estimateAttachmentImpactCall).toHaveBeenCalledWith("my-token", {
|
||||
policy_name: "rate-limit",
|
||||
scope: null,
|
||||
teams: ["team-a"],
|
||||
keys: [],
|
||||
models: [],
|
||||
tags: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should not call the API when accessToken is null", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<ImpactPopover attachment={makeAttachment()} accessToken={null} />);
|
||||
await user.click(screen.getByRole("button", { name: /open-popover/i }));
|
||||
expect(networking.estimateAttachmentImpactCall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show a loading indicator while the impact is being fetched", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(networking.estimateAttachmentImpactCall).mockReturnValue(new Promise(() => {}));
|
||||
renderWithProviders(<ImpactPopover attachment={makeAttachment()} accessToken="tok" />);
|
||||
await user.click(screen.getByRole("button", { name: /open-popover/i }));
|
||||
// Multiple "Loading..." nodes exist (Spin + adjacent text) — assert at least one is present
|
||||
expect(screen.queryAllByText(/loading/i).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should show a global scope warning when affected_keys_count is -1", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(networking.estimateAttachmentImpactCall).mockResolvedValue({
|
||||
affected_keys_count: -1,
|
||||
affected_teams_count: -1,
|
||||
sample_keys: [],
|
||||
sample_teams: [],
|
||||
});
|
||||
renderWithProviders(<ImpactPopover attachment={makeAttachment()} accessToken="tok" />);
|
||||
await user.click(screen.getByRole("button", { name: /open-popover/i }));
|
||||
expect(await screen.findByText(/global scope.*affects all keys and teams/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show key and team counts when impact data is loaded for a specific scope", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(networking.estimateAttachmentImpactCall).mockResolvedValue({
|
||||
affected_keys_count: 5,
|
||||
affected_teams_count: 2,
|
||||
sample_keys: ["sk-abc"],
|
||||
sample_teams: ["team-x"],
|
||||
});
|
||||
renderWithProviders(<ImpactPopover attachment={makeAttachment()} accessToken="tok" />);
|
||||
await user.click(screen.getByRole("button", { name: /open-popover/i }));
|
||||
expect(await screen.findByText(/5/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/2/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render sample key tags when returned from the API", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(networking.estimateAttachmentImpactCall).mockResolvedValue({
|
||||
affected_keys_count: 2,
|
||||
affected_teams_count: 0,
|
||||
sample_keys: ["sk-key-one", "sk-key-two"],
|
||||
sample_teams: [],
|
||||
});
|
||||
renderWithProviders(<ImpactPopover attachment={makeAttachment()} accessToken="tok" />);
|
||||
await user.click(screen.getByRole("button", { name: /open-popover/i }));
|
||||
expect(await screen.findByText("sk-key-one")).toBeInTheDocument();
|
||||
expect(screen.getByText("sk-key-two")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show 'No keys or teams currently affected' when both counts are 0", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(networking.estimateAttachmentImpactCall).mockResolvedValue({
|
||||
affected_keys_count: 0,
|
||||
affected_teams_count: 0,
|
||||
sample_keys: [],
|
||||
sample_teams: [],
|
||||
});
|
||||
renderWithProviders(<ImpactPopover attachment={makeAttachment()} accessToken="tok" />);
|
||||
await user.click(screen.getByRole("button", { name: /open-popover/i }));
|
||||
expect(await screen.findByText(/no keys or teams currently affected/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not call the API a second time when the popover is already loaded", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(networking.estimateAttachmentImpactCall).mockResolvedValue({
|
||||
affected_keys_count: 1,
|
||||
affected_teams_count: 0,
|
||||
sample_keys: ["sk-abc"],
|
||||
sample_teams: [],
|
||||
});
|
||||
renderWithProviders(<ImpactPopover attachment={makeAttachment()} accessToken="tok" />);
|
||||
await user.click(screen.getByRole("button", { name: /open-popover/i }));
|
||||
await screen.findByText("sk-abc");
|
||||
await user.click(screen.getByRole("button", { name: /open-popover/i }));
|
||||
expect(networking.estimateAttachmentImpactCall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,150 @@
|
||||
import React from "react";
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import PolicyTable from "./policy_table";
|
||||
import { Policy } from "./types";
|
||||
|
||||
vi.mock("@heroicons/react/outline", () => ({
|
||||
TrashIcon: function TrashIcon() { return null; },
|
||||
PencilIcon: function PencilIcon() { return null; },
|
||||
SwitchVerticalIcon: function SwitchVerticalIcon() { return null; },
|
||||
ChevronUpIcon: function ChevronUpIcon() { return null; },
|
||||
ChevronDownIcon: function ChevronDownIcon() { return null; },
|
||||
}));
|
||||
|
||||
vi.mock("@tremor/react", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@tremor/react")>();
|
||||
return {
|
||||
...actual,
|
||||
Button: React.forwardRef<HTMLButtonElement, any>(({ children, ...props }, ref) =>
|
||||
React.createElement("button", { ...props, ref }, children)
|
||||
),
|
||||
Icon: ({ icon: IconComp, onClick, className }: any) =>
|
||||
React.createElement("button", { type: "button", onClick, className }, IconComp?.displayName ?? IconComp?.name ?? "icon"),
|
||||
Tooltip: ({ children }: { children?: React.ReactNode }) =>
|
||||
React.createElement(React.Fragment, null, children),
|
||||
Badge: ({ children }: { children?: React.ReactNode }) =>
|
||||
React.createElement("span", null, children),
|
||||
};
|
||||
});
|
||||
|
||||
const makePolicy = (overrides: Partial<Policy> = {}): Policy => ({
|
||||
policy_id: "policy-id-1",
|
||||
policy_name: "test-policy",
|
||||
inherit: null,
|
||||
description: null,
|
||||
guardrails_add: [],
|
||||
guardrails_remove: [],
|
||||
condition: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
policies: [],
|
||||
isLoading: false,
|
||||
onDeleteClick: vi.fn(),
|
||||
onEditClick: vi.fn(),
|
||||
onViewClick: vi.fn(),
|
||||
isAdmin: true,
|
||||
};
|
||||
|
||||
describe("PolicyTable", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render column headers", () => {
|
||||
renderWithProviders(<PolicyTable {...defaultProps} />);
|
||||
expect(screen.getByText("Name")).toBeInTheDocument();
|
||||
expect(screen.getByText("Description")).toBeInTheDocument();
|
||||
expect(screen.getByText("Actions")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show a loading message when isLoading is true", () => {
|
||||
renderWithProviders(<PolicyTable {...defaultProps} isLoading />);
|
||||
expect(screen.getByText(/loading/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show 'No policies found' when there are no policies", () => {
|
||||
renderWithProviders(<PolicyTable {...defaultProps} />);
|
||||
expect(screen.getByText(/no policies found/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render a button with the policy name for each grouped policy", () => {
|
||||
const policies = [
|
||||
makePolicy({ policy_name: "alpha-policy", policy_id: "id-1" }),
|
||||
makePolicy({ policy_name: "beta-policy", policy_id: "id-2" }),
|
||||
];
|
||||
renderWithProviders(<PolicyTable {...defaultProps} policies={policies} />);
|
||||
expect(screen.getByRole("button", { name: "alpha-policy" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "beta-policy" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onViewClick with the policy_id when the policy name button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const policy = makePolicy({ policy_name: "my-policy", policy_id: "view-id-1" });
|
||||
renderWithProviders(<PolicyTable {...defaultProps} policies={[policy]} />);
|
||||
await user.click(screen.getByRole("button", { name: "my-policy" }));
|
||||
expect(defaultProps.onViewClick).toHaveBeenCalledWith("view-id-1");
|
||||
});
|
||||
|
||||
it("should call onDeleteClick with policy_id and policy_name when the delete icon is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const policy = makePolicy({ policy_name: "del-policy", policy_id: "del-id-1" });
|
||||
renderWithProviders(<PolicyTable {...defaultProps} policies={[policy]} />);
|
||||
await user.click(screen.getByRole("button", { name: /TrashIcon/i }));
|
||||
expect(defaultProps.onDeleteClick).toHaveBeenCalledWith("del-id-1", "del-policy");
|
||||
});
|
||||
|
||||
it("should call onEditClick with the policy when the edit icon is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const policy = makePolicy({ policy_name: "edit-policy", policy_id: "edit-id-1" });
|
||||
renderWithProviders(<PolicyTable {...defaultProps} policies={[policy]} />);
|
||||
await user.click(screen.getByRole("button", { name: /PencilIcon/i }));
|
||||
expect(defaultProps.onEditClick).toHaveBeenCalledWith(policy);
|
||||
});
|
||||
|
||||
it("should not show admin action icons for non-admins", () => {
|
||||
const policy = makePolicy();
|
||||
renderWithProviders(<PolicyTable {...defaultProps} policies={[policy]} isAdmin={false} />);
|
||||
expect(screen.queryByRole("button", { name: /TrashIcon/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /PencilIcon/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show a version badge when multiple versions of the same policy name exist", () => {
|
||||
const policies = [
|
||||
makePolicy({ policy_name: "versioned", policy_id: "v1", version_status: "published", version_number: 1 }),
|
||||
makePolicy({ policy_name: "versioned", policy_id: "v2", version_status: "production", version_number: 2 }),
|
||||
];
|
||||
renderWithProviders(<PolicyTable {...defaultProps} policies={policies} />);
|
||||
expect(screen.getByText(/2 version/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should group policies with the same name into a single row", () => {
|
||||
const policies = [
|
||||
makePolicy({ policy_name: "shared", policy_id: "s1", version_status: "published" }),
|
||||
makePolicy({ policy_name: "shared", policy_id: "s2", version_status: "production" }),
|
||||
];
|
||||
renderWithProviders(<PolicyTable {...defaultProps} policies={policies} />);
|
||||
expect(screen.getAllByRole("button", { name: "shared" })).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should show an overflow tag when more than 2 guardrails_add exist", () => {
|
||||
const policy = makePolicy({ guardrails_add: ["g1", "g2", "g3", "g4"] });
|
||||
renderWithProviders(<PolicyTable {...defaultProps} policies={[policy]} />);
|
||||
expect(screen.getByText("+2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should prefer the production version as the primary policy when grouping", async () => {
|
||||
const user = userEvent.setup();
|
||||
const policies = [
|
||||
makePolicy({ policy_name: "grouped", policy_id: "published-id", version_status: "published" }),
|
||||
makePolicy({ policy_name: "grouped", policy_id: "prod-id", version_status: "production" }),
|
||||
];
|
||||
renderWithProviders(<PolicyTable {...defaultProps} policies={policies} />);
|
||||
await user.click(screen.getByRole("button", { name: "grouped" }));
|
||||
expect(defaultProps.onViewClick).toHaveBeenCalledWith("prod-id");
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,144 @@
|
||||
import React from "react";
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as networking from "../networking";
|
||||
import PolicyTemplates from "./policy_templates";
|
||||
|
||||
vi.mock("../networking");
|
||||
|
||||
vi.mock("@heroicons/react/outline", () => ({
|
||||
ShieldCheckIcon: function ShieldCheckIcon() { return null; },
|
||||
ShieldExclamationIcon: function ShieldExclamationIcon() { return null; },
|
||||
BeakerIcon: function BeakerIcon() { return null; },
|
||||
CurrencyDollarIcon: function CurrencyDollarIcon() { return null; },
|
||||
CheckCircleIcon: function CheckCircleIcon() { return null; },
|
||||
}));
|
||||
|
||||
const makeTemplate = (overrides: any = {}) => ({
|
||||
id: "tpl-1",
|
||||
title: "Test Template",
|
||||
description: "A test template",
|
||||
icon: "ShieldCheckIcon",
|
||||
iconColor: "text-green-500",
|
||||
iconBg: "bg-green-50",
|
||||
guardrails: ["guardrail-a"],
|
||||
tags: [],
|
||||
complexity: "Low" as const,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
onUseTemplate: vi.fn(),
|
||||
onOpenAiSuggestion: vi.fn(),
|
||||
accessToken: "test-token",
|
||||
};
|
||||
|
||||
describe("PolicyTemplates", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render the section header after loading", async () => {
|
||||
vi.mocked(networking.getPolicyTemplates).mockResolvedValue([]);
|
||||
renderWithProviders(<PolicyTemplates {...defaultProps} />);
|
||||
expect(await screen.findByText("Policy Templates")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show the template grid while fetching", () => {
|
||||
vi.mocked(networking.getPolicyTemplates).mockReturnValue(new Promise(() => {}));
|
||||
renderWithProviders(<PolicyTemplates {...defaultProps} />);
|
||||
expect(screen.queryByText("Policy Templates")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /use ai to find templates/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render a card for each fetched template", async () => {
|
||||
const templates = [
|
||||
makeTemplate({ title: "Template Alpha" }),
|
||||
makeTemplate({ id: "tpl-2", title: "Template Beta" }),
|
||||
];
|
||||
vi.mocked(networking.getPolicyTemplates).mockResolvedValue(templates);
|
||||
renderWithProviders(<PolicyTemplates {...defaultProps} />);
|
||||
expect(await screen.findByText("Template Alpha")).toBeInTheDocument();
|
||||
expect(screen.getByText("Template Beta")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onTemplatesLoaded with the fetched templates after loading", async () => {
|
||||
const templates = [makeTemplate()];
|
||||
vi.mocked(networking.getPolicyTemplates).mockResolvedValue(templates);
|
||||
const onTemplatesLoaded = vi.fn();
|
||||
renderWithProviders(<PolicyTemplates {...defaultProps} onTemplatesLoaded={onTemplatesLoaded} />);
|
||||
await waitFor(() => {
|
||||
expect(onTemplatesLoaded).toHaveBeenCalledWith(templates);
|
||||
});
|
||||
});
|
||||
|
||||
it("should call onOpenAiSuggestion when the AI suggestion button is clicked", async () => {
|
||||
vi.mocked(networking.getPolicyTemplates).mockResolvedValue([]);
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<PolicyTemplates {...defaultProps} />);
|
||||
await screen.findByText("Policy Templates");
|
||||
await user.click(screen.getByRole("button", { name: /use ai to find templates/i }));
|
||||
expect(defaultProps.onOpenAiSuggestion).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should render tag filter checkboxes for unique tags across all templates", async () => {
|
||||
const templates = [
|
||||
makeTemplate({ tags: ["compliance"] }),
|
||||
makeTemplate({ id: "tpl-2", tags: ["compliance", "security"] }),
|
||||
];
|
||||
vi.mocked(networking.getPolicyTemplates).mockResolvedValue(templates);
|
||||
renderWithProviders(<PolicyTemplates {...defaultProps} />);
|
||||
expect(await screen.findByRole("checkbox", { name: /compliance/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("checkbox", { name: /security/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should filter to only matching templates when a tag is selected", async () => {
|
||||
const templates = [
|
||||
makeTemplate({ id: "tpl-1", title: "Compliance Template", tags: ["compliance"] }),
|
||||
makeTemplate({ id: "tpl-2", title: "Security Template", tags: ["security"] }),
|
||||
];
|
||||
vi.mocked(networking.getPolicyTemplates).mockResolvedValue(templates);
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<PolicyTemplates {...defaultProps} />);
|
||||
await screen.findByText("Compliance Template");
|
||||
await user.click(screen.getByRole("checkbox", { name: /compliance/i }));
|
||||
expect(screen.getByText("Compliance Template")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Security Template")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show 'No templates match' when selected tags exclude all templates", async () => {
|
||||
const templates = [
|
||||
makeTemplate({ id: "tpl-1", title: "Alpha Template", tags: ["alpha"] }),
|
||||
makeTemplate({ id: "tpl-2", title: "Beta Template", tags: ["beta"] }),
|
||||
];
|
||||
vi.mocked(networking.getPolicyTemplates).mockResolvedValue(templates);
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<PolicyTemplates {...defaultProps} />);
|
||||
await screen.findByText("Alpha Template");
|
||||
await user.click(screen.getByRole("checkbox", { name: /alpha/i }));
|
||||
await user.click(screen.getByRole("checkbox", { name: /beta/i }));
|
||||
expect(screen.getByText(/no templates match the selected filters/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should restore all templates when 'Clear all' is clicked", async () => {
|
||||
const templates = [
|
||||
makeTemplate({ id: "tpl-1", title: "Alpha Template", tags: ["alpha"] }),
|
||||
makeTemplate({ id: "tpl-2", title: "Beta Template", tags: ["beta"] }),
|
||||
];
|
||||
vi.mocked(networking.getPolicyTemplates).mockResolvedValue(templates);
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<PolicyTemplates {...defaultProps} />);
|
||||
await screen.findByText("Alpha Template");
|
||||
await user.click(screen.getByRole("checkbox", { name: /alpha/i }));
|
||||
expect(screen.queryByText("Beta Template")).not.toBeInTheDocument();
|
||||
await user.click(screen.getByRole("button", { name: /clear all/i }));
|
||||
expect(screen.getByText("Beta Template")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not fetch templates when accessToken is null", () => {
|
||||
renderWithProviders(<PolicyTemplates {...defaultProps} accessToken={null} />);
|
||||
expect(networking.getPolicyTemplates).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user