Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@/test/test-utils";
import { AccessMappingDetailPanel } from "./AccessMappingDetailPanel";

// =============================================================================
// Mock hooks
// =============================================================================

let mockEC2Instance: Record<string, unknown> | null = null;
let mockEC2Loading = false;
let mockRDSInstance: Record<string, unknown> | null = null;
let mockRDSLoading = false;

vi.mock("@/hooks", () => ({
useEC2Instance: () => ({
data: mockEC2Instance,
isLoading: mockEC2Loading,
}),
useRDSInstance: () => ({
data: mockRDSInstance,
isLoading: mockRDSLoading,
}),
}));

describe("AccessMappingDetailPanel", () => {
const onClose = vi.fn();

beforeEach(() => {
vi.clearAllMocks();
mockEC2Instance = null;
mockEC2Loading = false;
mockRDSInstance = null;
mockRDSLoading = false;
});

it("renders account detail panel for access-account type", () => {
render(
<AccessMappingDetailPanel
selectedNode={{
nodeType: "access-account",
nodeData: {
accountName: "root-account",
accountId: "acc-1",
safeName: "AdminSafe",
address: "10.0.1.5",
platformId: "UnixSSH",
secretType: "password",
username: "root",
},
}}
onClose={onClose}
/>,
);
expect(screen.getByText("Account Details")).toBeInTheDocument();
// "root-account" appears in heading + DetailRow
const accountTexts = screen.getAllByText("root-account");
expect(accountTexts.length).toBeGreaterThanOrEqual(1);
expect(screen.getByText("acc-1")).toBeInTheDocument();
expect(screen.getByText("AdminSafe")).toBeInTheDocument();
expect(screen.getByText("10.0.1.5")).toBeInTheDocument();
expect(screen.getByText("UnixSSH")).toBeInTheDocument();
});

it("renders EC2 loading state for ec2-target type", () => {
mockEC2Loading = true;
const { container } = render(
<AccessMappingDetailPanel
selectedNode={{
nodeType: "access-ec2-target",
nodeData: { instanceId: "i-123" },
}}
onClose={onClose}
/>,
);
expect(
container.querySelector(".animate-spin") ||
screen.getByText("Loading instance details..."),
).toBeTruthy();
});

it("renders EC2 not found when instance is null", () => {
mockEC2Instance = null;
mockEC2Loading = false;
render(
<AccessMappingDetailPanel
selectedNode={{
nodeType: "access-ec2-target",
nodeData: { instanceId: "i-123" },
}}
onClose={onClose}
/>,
);
expect(screen.getByText(/Instance not found/)).toBeInTheDocument();
});

it("renders RDS loading state", () => {
mockRDSLoading = true;
render(
<AccessMappingDetailPanel
selectedNode={{
nodeType: "access-rds-target",
nodeData: { dbIdentifier: "mydb" },
}}
onClose={onClose}
/>,
);
expect(screen.getByText("Loading database details...")).toBeInTheDocument();
});

it("renders RDS not found when instance is null", () => {
mockRDSInstance = null;
mockRDSLoading = false;
render(
<AccessMappingDetailPanel
selectedNode={{
nodeType: "access-rds-target",
nodeData: { dbIdentifier: "mydb" },
}}
onClose={onClose}
/>,
);
expect(screen.getByText(/Database not found/)).toBeInTheDocument();
});

it("returns null for unknown node type", () => {
const { container } = render(
<AccessMappingDetailPanel
selectedNode={{
nodeType: "unknown",
nodeData: {},
}}
onClose={onClose}
/>,
);
expect(container.innerHTML).toBe("");
});

it("renders terraform section for tf-managed account", () => {
render(
<AccessMappingDetailPanel
selectedNode={{
nodeType: "access-account",
nodeData: {
accountName: "tf-account",
tfManaged: true,
tfStateSource: "s3://bucket/state",
tfResourceAddress: "cyberark_account.x",
},
}}
onClose={onClose}
/>,
);
// "Terraform" appears in the section header and TerraformBadge
const tfTexts = screen.getAllByText(/Terraform/i);
expect(tfTexts.length).toBeGreaterThanOrEqual(1);
});
});
125 changes: 125 additions & 0 deletions frontend/src/components/access-mapping/AccessMappingFilterBar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@/test/test-utils";
import { AccessMappingFilterBar } from "./AccessMappingFilterBar";
import type { AccessMappingFilters } from "./AccessMappingFilterBar";

describe("AccessMappingFilterBar", () => {
const defaultFilters: AccessMappingFilters = {
search: "",
accessType: "",
selectedUser: "",
};

const onChange = vi.fn();
const users = ["john", "jane", "bob"];

it("renders search input", () => {
render(
<AccessMappingFilterBar
filters={defaultFilters}
onChange={onChange}
users={users}
isAdmin={true}
/>,
);
expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument();
});

it("renders access type selector", () => {
render(
<AccessMappingFilterBar
filters={defaultFilters}
onChange={onChange}
users={users}
isAdmin={false}
/>,
);
expect(screen.getByText("All Access Types")).toBeInTheDocument();
});

it("renders user selector for admin", () => {
render(
<AccessMappingFilterBar
filters={defaultFilters}
onChange={onChange}
users={users}
isAdmin={true}
/>,
);
expect(screen.getByText("All Users")).toBeInTheDocument();
expect(screen.getByText("john")).toBeInTheDocument();
expect(screen.getByText("jane")).toBeInTheDocument();
expect(screen.getByText("bob")).toBeInTheDocument();
});

it("hides user selector for non-admin", () => {
render(
<AccessMappingFilterBar
filters={defaultFilters}
onChange={onChange}
users={users}
isAdmin={false}
/>,
);
expect(screen.queryByText("All Users")).not.toBeInTheDocument();
});

it("calls onChange on search input", () => {
render(
<AccessMappingFilterBar
filters={defaultFilters}
onChange={onChange}
users={users}
isAdmin={false}
/>,
);
const input = screen.getByPlaceholderText("Search...");
fireEvent.change(input, { target: { value: "test" } });
expect(onChange).toHaveBeenCalledWith({
...defaultFilters,
search: "test",
});
});

it("shows clear button when filters active", () => {
render(
<AccessMappingFilterBar
filters={{ ...defaultFilters, search: "test" }}
onChange={onChange}
users={users}
isAdmin={false}
/>,
);
const clearButton = screen.getByTitle("Clear filters");
expect(clearButton).toBeInTheDocument();
});

it("hides clear button when no filters", () => {
render(
<AccessMappingFilterBar
filters={defaultFilters}
onChange={onChange}
users={users}
isAdmin={false}
/>,
);
expect(screen.queryByTitle("Clear filters")).not.toBeInTheDocument();
});

it("clear button resets all filters", () => {
render(
<AccessMappingFilterBar
filters={{ search: "test", accessType: "standing", selectedUser: "" }}
onChange={onChange}
users={users}
isAdmin={false}
/>,
);
fireEvent.click(screen.getByTitle("Clear filters"));
expect(onChange).toHaveBeenCalledWith({
search: "",
accessType: "",
selectedUser: "",
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, it, expect } from "vitest";
import { render, screen, fireEvent } from "@/test/test-utils";
import { AccessMappingLegend } from "./AccessMappingLegend";

describe("AccessMappingLegend", () => {
const stats = {
total_users: 5,
total_targets: 10,
total_standing_paths: 8,
total_jit_paths: 3,
};

it("renders stats when provided", () => {
render(<AccessMappingLegend stats={stats} />);
expect(screen.getByText("5")).toBeInTheDocument();
expect(screen.getByText("10")).toBeInTheDocument();
expect(screen.getByText("8")).toBeInTheDocument();
expect(screen.getByText("3")).toBeInTheDocument();
});

it("renders stat labels", () => {
render(<AccessMappingLegend stats={stats} />);
expect(screen.getByText("Users")).toBeInTheDocument();
expect(screen.getByText("Targets")).toBeInTheDocument();
expect(screen.getByText("Standing")).toBeInTheDocument();
expect(screen.getByText("JIT")).toBeInTheDocument();
});

it("does not render stats when null", () => {
render(<AccessMappingLegend stats={null} />);
expect(screen.queryByText("Users")).not.toBeInTheDocument();
expect(screen.queryByText("Targets")).not.toBeInTheDocument();
});

it("renders Legend toggle button", () => {
render(<AccessMappingLegend stats={stats} />);
expect(screen.getByText("Legend")).toBeInTheDocument();
});

it("legend content hidden by default", () => {
render(<AccessMappingLegend stats={stats} />);
expect(screen.queryByText("Nodes")).not.toBeInTheDocument();
});

it("expands legend on click", () => {
render(<AccessMappingLegend stats={stats} />);
fireEvent.click(screen.getByText("Legend"));
expect(screen.getByText("Nodes")).toBeInTheDocument();
expect(screen.getByText("Connections")).toBeInTheDocument();
});

it("shows node type labels when expanded", () => {
render(<AccessMappingLegend stats={stats} />);
fireEvent.click(screen.getByText("Legend"));
expect(screen.getByText("User")).toBeInTheDocument();
expect(screen.getByText("Role")).toBeInTheDocument();
expect(screen.getByText("Safe")).toBeInTheDocument();
expect(screen.getByText("Account")).toBeInTheDocument();
expect(screen.getByText("SIA Policy")).toBeInTheDocument();
expect(screen.getByText("EC2 Target")).toBeInTheDocument();
expect(screen.getByText("RDS Target")).toBeInTheDocument();
});

it("shows connection types when expanded", () => {
render(<AccessMappingLegend stats={stats} />);
fireEvent.click(screen.getByText("Legend"));
expect(screen.getByText("Standing Access")).toBeInTheDocument();
expect(screen.getByText("JIT Access")).toBeInTheDocument();
});

it("collapses legend on second click", () => {
render(<AccessMappingLegend stats={stats} />);
fireEvent.click(screen.getByText("Legend"));
expect(screen.getByText("Nodes")).toBeInTheDocument();
fireEvent.click(screen.getByText("Legend"));
expect(screen.queryByText("Nodes")).not.toBeInTheDocument();
});
});
Loading
Loading