mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-30 07:14:20 +00:00
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
1420 lines
42 KiB
TypeScript
1420 lines
42 KiB
TypeScript
import { render, screen, waitFor, within } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import type { ReactNode } from "react";
|
|
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
|
|
const { toastMock } = vi.hoisted(() => ({
|
|
toastMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/actions/findings/findings-triage", () => ({
|
|
loadFindingTriageDetail: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/components/shadcn/toast/use-toast", () => ({
|
|
useToast: () => ({ toast: toastMock }),
|
|
}));
|
|
|
|
vi.mock("@/components/shadcn/modal", () => ({
|
|
Modal: ({
|
|
children,
|
|
open,
|
|
title,
|
|
description,
|
|
}: {
|
|
children: ReactNode;
|
|
open: boolean;
|
|
title?: string;
|
|
description?: string;
|
|
}) =>
|
|
open ? (
|
|
<div role="dialog" aria-label={title}>
|
|
<h2>{title}</h2>
|
|
{description && <p>{description}</p>}
|
|
{children}
|
|
</div>
|
|
) : null,
|
|
}));
|
|
|
|
vi.mock("next/navigation", () => ({
|
|
useRouter: () => ({
|
|
push: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
// CustomLink pulls the "@/lib" barrel (and next-auth with it) into the unit env.
|
|
vi.mock("@/components/shadcn/custom/custom-link", () => ({
|
|
CustomLink: ({ href, children }: { href: string; children: ReactNode }) => (
|
|
<a href={href}>{children}</a>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/components/shadcn/dropdown", () => ({
|
|
ActionDropdownItem: ({
|
|
label,
|
|
onSelect,
|
|
disabled,
|
|
}: {
|
|
label: ReactNode;
|
|
onSelect?: () => void;
|
|
disabled?: boolean;
|
|
}) => (
|
|
<button disabled={disabled} onClick={onSelect}>
|
|
{label}
|
|
</button>
|
|
),
|
|
}));
|
|
|
|
beforeAll(() => {
|
|
Object.defineProperty(HTMLElement.prototype, "hasPointerCapture", {
|
|
configurable: true,
|
|
value: vi.fn(() => false),
|
|
});
|
|
Object.defineProperty(HTMLElement.prototype, "releasePointerCapture", {
|
|
configurable: true,
|
|
value: vi.fn(),
|
|
});
|
|
Object.defineProperty(HTMLElement.prototype, "scrollIntoView", {
|
|
configurable: true,
|
|
value: vi.fn(),
|
|
});
|
|
});
|
|
|
|
import { useCloudUpgradeStore } from "@/store";
|
|
import { CLOUD_UPGRADE_FEATURE } from "@/types/cloud-upgrade";
|
|
import { FINDING_STATUS } from "@/types/components";
|
|
import {
|
|
FINDING_TRIAGE_DISABLED_REASON,
|
|
FINDING_TRIAGE_STATUS,
|
|
type FindingTriageDetail,
|
|
type FindingTriageSummary,
|
|
} from "@/types/findings-triage";
|
|
|
|
import {
|
|
FindingNoteActionItem,
|
|
FindingTriageStatusBadge,
|
|
FindingTriageStatusCell,
|
|
} from "./finding-triage-cells";
|
|
|
|
function makeTriageSummary(
|
|
overrides?: Partial<FindingTriageSummary>,
|
|
): FindingTriageSummary {
|
|
return {
|
|
findingId: "finding-1",
|
|
findingUid: "prowler-finding-uid-1",
|
|
triageId: "triage-1",
|
|
notesCount: 0,
|
|
status: FINDING_TRIAGE_STATUS.UNDER_REVIEW,
|
|
label: "Under Review",
|
|
hasVisibleNote: false,
|
|
isMuted: false,
|
|
canEdit: true,
|
|
billingHref: "https://prowler.com/pricing",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeTriageDetail(
|
|
overrides?: Partial<FindingTriageDetail>,
|
|
): FindingTriageDetail {
|
|
return {
|
|
...makeTriageSummary({ rawFindingStatus: FINDING_STATUS.MANUAL }),
|
|
noteId: null,
|
|
noteBody: "",
|
|
maxNoteLength: 500,
|
|
rawFindingStatus: FINDING_STATUS.MANUAL,
|
|
manualPassCreatedByName: null,
|
|
manualPassCreatedAt: null,
|
|
manualPassExpiresAt: null,
|
|
manualPassActive: null,
|
|
manualPassEvidence: null,
|
|
manualPassDeactivatedAt: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
afterEach(() => {
|
|
toastMock.mockReset();
|
|
useCloudUpgradeStore.getState().closeCloudUpgrade();
|
|
});
|
|
|
|
it("should open finding triage upgrade from a Cloud-only status cell", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
canEdit: false,
|
|
disabledReason: FINDING_TRIAGE_DISABLED_REASON.CLOUD_ONLY,
|
|
})}
|
|
/>,
|
|
);
|
|
|
|
// When
|
|
await user.click(
|
|
screen.getByRole("button", {
|
|
name: "Change triage status - available in Prowler Cloud",
|
|
}),
|
|
);
|
|
|
|
// Then
|
|
expect(useCloudUpgradeStore.getState().activeFeature).toBe(
|
|
CLOUD_UPGRADE_FEATURE.FINDING_TRIAGE,
|
|
);
|
|
});
|
|
|
|
it("should render the Cloud-only triage action with the shared button", () => {
|
|
// Given / When
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
canEdit: false,
|
|
disabledReason: FINDING_TRIAGE_DISABLED_REASON.CLOUD_ONLY,
|
|
})}
|
|
/>,
|
|
);
|
|
|
|
// Then
|
|
expect(
|
|
screen.getByRole("button", {
|
|
name: "Change triage status - available in Prowler Cloud",
|
|
}),
|
|
).toHaveAttribute("data-slot", "button");
|
|
});
|
|
|
|
describe("finding triage cells", () => {
|
|
it("should open the Note modal from the note action with the current status preselected", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
render(
|
|
<FindingNoteActionItem
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.REMEDIATING,
|
|
label: "Remediating",
|
|
})}
|
|
findingContext={{ title: "S3 bucket allows public reads" }}
|
|
onTriageUpdateAction={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
// When
|
|
const addNoteButton = screen.getByRole("button", {
|
|
name: "Add Triage Note",
|
|
});
|
|
expect(addNoteButton).toHaveTextContent("Add Triage Note");
|
|
await user.click(addNoteButton);
|
|
|
|
// Then
|
|
expect(
|
|
screen.getByRole("dialog", { name: "Add Triage Note" }),
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.getByRole("combobox", { name: "Triage status" }),
|
|
).toHaveTextContent("Remediating");
|
|
});
|
|
|
|
it("should not propagate table status clicks to the row", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
const onRowClick = vi.fn();
|
|
render(
|
|
<div onClick={onRowClick}>
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.OPEN,
|
|
label: "Open",
|
|
})}
|
|
onTriageUpdateAction={vi.fn()}
|
|
/>
|
|
</div>,
|
|
);
|
|
|
|
// When
|
|
await user.click(screen.getByRole("combobox", { name: "Triage status" }));
|
|
|
|
// Then
|
|
expect(onRowClick).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should render status picker with fixed width and colored options", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.UNDER_REVIEW,
|
|
label: "Under Review",
|
|
})}
|
|
onTriageUpdateAction={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
const statusControl = screen.getByRole("combobox", {
|
|
name: "Triage status",
|
|
});
|
|
|
|
// When
|
|
await user.click(statusControl);
|
|
|
|
// Then
|
|
expect(statusControl.parentElement).toHaveClass("w-32");
|
|
expect(statusControl).toHaveAttribute("data-size", "xs");
|
|
expect(within(statusControl).getByText("Under Review")).toHaveClass(
|
|
"text-text-warning-primary",
|
|
);
|
|
expect(
|
|
within(screen.getByRole("option", { name: "Open" })).getByText("Open"),
|
|
).toHaveClass("text-text-error-primary");
|
|
expect(
|
|
within(screen.getByRole("option", { name: "Under Review" })).getByText(
|
|
"Under Review",
|
|
),
|
|
).toHaveClass("text-text-warning-primary");
|
|
expect(
|
|
within(screen.getByRole("option", { name: "Remediating" })).getByText(
|
|
"Remediating",
|
|
),
|
|
).toHaveClass("text-bg-data-info");
|
|
expect(
|
|
within(screen.getByRole("option", { name: "Risk Accepted" })).getByText(
|
|
"Risk Accepted",
|
|
),
|
|
).toHaveClass("text-bg-pass");
|
|
expect(
|
|
within(screen.getByRole("option", { name: "False Positive" })).getByText(
|
|
"False Positive",
|
|
),
|
|
).toHaveClass("text-text-neutral-secondary");
|
|
expect(screen.queryByRole("option", { name: "Resolved" })).toBeNull();
|
|
});
|
|
|
|
it("should show Resolved as a normal enabled option only for an authoritative MANUAL finding", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
rawFindingStatus: FINDING_STATUS.MANUAL,
|
|
})}
|
|
onTriageUpdateAction={vi.fn()}
|
|
onTriageDetailLoadAction={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
// When
|
|
await user.click(screen.getByRole("combobox", { name: "Triage status" }));
|
|
|
|
// Then
|
|
const resolvedOption = screen.getByRole("option", {
|
|
name: "Resolved",
|
|
});
|
|
expect(resolvedOption).not.toHaveAttribute("aria-disabled");
|
|
expect(resolvedOption).not.toHaveAttribute("disabled");
|
|
expect(resolvedOption).not.toHaveAttribute("data-disabled");
|
|
expect(screen.queryByRole("option", { name: "Pass" })).toBeNull();
|
|
});
|
|
|
|
it.each([FINDING_STATUS.FAIL, FINDING_STATUS.PASS] as const)(
|
|
"should not show the extra Resolved workflow for a %s raw finding",
|
|
async (rawFindingStatus) => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({ rawFindingStatus })}
|
|
onTriageUpdateAction={vi.fn()}
|
|
onTriageDetailLoadAction={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
// When
|
|
await user.click(screen.getByRole("combobox", { name: "Triage status" }));
|
|
|
|
// Then
|
|
expect(screen.queryByRole("option", { name: "Resolved" })).toBeNull();
|
|
},
|
|
);
|
|
|
|
it("should explain the gated manual pass action inline", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
rawFindingStatus: FINDING_STATUS.MANUAL,
|
|
})}
|
|
onTriageUpdateAction={vi.fn()}
|
|
onTriageDetailLoadAction={vi.fn()}
|
|
/>,
|
|
);
|
|
await user.click(screen.getByRole("combobox", { name: "Triage status" }));
|
|
// Then
|
|
expect(
|
|
within(screen.getByRole("option", { name: "Resolved" })).getByText(
|
|
"Add a Triage Note explaining why this finding passes.",
|
|
),
|
|
).toBeVisible();
|
|
});
|
|
|
|
it("should open Triage Note with Resolved preselected without a direct update", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
const triage = makeTriageSummary({
|
|
rawFindingStatus: FINDING_STATUS.MANUAL,
|
|
});
|
|
const detail = makeTriageDetail();
|
|
const onTriageUpdateAction = vi.fn();
|
|
const onTriageDetailLoadAction = vi.fn().mockResolvedValue(detail);
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={triage}
|
|
findingContext={{ title: "S3 bucket allows public reads" }}
|
|
onTriageUpdateAction={onTriageUpdateAction}
|
|
onTriageDetailLoadAction={onTriageDetailLoadAction}
|
|
/>,
|
|
);
|
|
await user.click(screen.getByRole("combobox", { name: "Triage status" }));
|
|
|
|
// When
|
|
await user.click(screen.getByRole("option", { name: "Resolved" }));
|
|
|
|
// Then
|
|
expect(onTriageUpdateAction).not.toHaveBeenCalled();
|
|
expect(onTriageDetailLoadAction).toHaveBeenCalledWith(triage);
|
|
const dialog = await screen.findByRole("dialog", {
|
|
name: "Add Triage Note",
|
|
});
|
|
expect(dialog).toBeVisible();
|
|
expect(
|
|
within(dialog).getByRole("combobox", { name: "Triage status" }),
|
|
).toHaveTextContent("Resolved");
|
|
expect(within(dialog).queryByText("Pass")).not.toBeInTheDocument();
|
|
expect(within(dialog).queryByText("Manual")).not.toBeInTheDocument();
|
|
expect(screen.getByLabelText("Manual pass evidence")).toHaveValue("");
|
|
});
|
|
|
|
it("should use fresh detail status instead of stale MANUAL summary preselection", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
const staleTriage = makeTriageSummary({
|
|
rawFindingStatus: FINDING_STATUS.MANUAL,
|
|
});
|
|
const freshDetail = makeTriageDetail({
|
|
status: FINDING_TRIAGE_STATUS.UNDER_REVIEW,
|
|
rawFindingStatus: FINDING_STATUS.FAIL,
|
|
});
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={staleTriage}
|
|
onTriageUpdateAction={vi.fn()}
|
|
onTriageDetailLoadAction={vi.fn().mockResolvedValue(freshDetail)}
|
|
/>,
|
|
);
|
|
await user.click(screen.getByRole("combobox", { name: "Triage status" }));
|
|
|
|
// When
|
|
await user.click(screen.getByRole("option", { name: "Resolved" }));
|
|
|
|
// Then
|
|
const dialog = await screen.findByRole("dialog", {
|
|
name: "Add Triage Note",
|
|
});
|
|
expect(
|
|
within(dialog).getByRole("combobox", { name: "Triage status" }),
|
|
).toHaveTextContent("Under Review");
|
|
expect(within(dialog).queryByLabelText("Manual pass evidence")).toBeNull();
|
|
});
|
|
|
|
it("should optimistically show Resolved after manual pass submission", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
const onTriageUpdateAction = vi.fn();
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
rawFindingStatus: FINDING_STATUS.MANUAL,
|
|
})}
|
|
onTriageUpdateAction={onTriageUpdateAction}
|
|
onTriageDetailLoadAction={vi.fn().mockResolvedValue(makeTriageDetail())}
|
|
/>,
|
|
);
|
|
await user.click(screen.getByRole("combobox", { name: "Triage status" }));
|
|
await user.click(screen.getByRole("option", { name: "Resolved" }));
|
|
await user.type(
|
|
await screen.findByLabelText("Manual pass evidence"),
|
|
"Verified by the control owner.",
|
|
);
|
|
|
|
// When
|
|
await user.click(screen.getByRole("button", { name: "Save" }));
|
|
|
|
// Then
|
|
await waitFor(() => expect(onTriageUpdateAction).toHaveBeenCalledOnce());
|
|
expect(
|
|
screen.getByRole("combobox", { name: "Triage status" }),
|
|
).toHaveTextContent("Resolved");
|
|
expect(screen.getByText("Manually verified")).toBeVisible();
|
|
});
|
|
|
|
it("should clear cancelled manual pass evidence before reopening", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
rawFindingStatus: FINDING_STATUS.MANUAL,
|
|
})}
|
|
onTriageUpdateAction={vi.fn()}
|
|
onTriageDetailLoadAction={vi.fn().mockResolvedValue(makeTriageDetail())}
|
|
/>,
|
|
);
|
|
await user.click(screen.getByRole("combobox", { name: "Triage status" }));
|
|
await user.click(screen.getByRole("option", { name: "Resolved" }));
|
|
await user.type(
|
|
await screen.findByLabelText("Manual pass evidence"),
|
|
"Evidence that must not leak.",
|
|
);
|
|
|
|
// When
|
|
await user.click(screen.getByRole("button", { name: "Cancel" }));
|
|
await user.click(screen.getByRole("combobox", { name: "Triage status" }));
|
|
await user.click(screen.getByRole("option", { name: "Resolved" }));
|
|
|
|
// Then
|
|
expect(await screen.findByLabelText("Manual pass evidence")).toHaveValue(
|
|
"",
|
|
);
|
|
});
|
|
|
|
it("should keep Manual Pass provenance stable while preventing duplicate detail requests", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
let resolveDetail: (detail: FindingTriageDetail) => void = () => {};
|
|
const onTriageDetailLoadAction = vi.fn(
|
|
() =>
|
|
new Promise<FindingTriageDetail>((resolve) => {
|
|
resolveDetail = resolve;
|
|
}),
|
|
);
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.RESOLVED,
|
|
label: "Resolved",
|
|
manualPassProvenance: "Manually verified",
|
|
rawFindingStatus: FINDING_STATUS.MANUAL,
|
|
})}
|
|
onTriageUpdateAction={vi.fn()}
|
|
onTriageDetailLoadAction={onTriageDetailLoadAction}
|
|
/>,
|
|
);
|
|
const provenanceControl = screen.getByRole("button", {
|
|
name: "View Manual Pass details",
|
|
});
|
|
|
|
// When
|
|
await user.click(provenanceControl);
|
|
await user.click(provenanceControl);
|
|
|
|
// Then
|
|
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
|
expect(
|
|
screen.queryByText("Loading current triage details..."),
|
|
).not.toBeInTheDocument();
|
|
expect(provenanceControl).toBeVisible();
|
|
expect(provenanceControl).toBeDisabled();
|
|
expect(onTriageDetailLoadAction).toHaveBeenCalledOnce();
|
|
|
|
resolveDetail(makeTriageDetail());
|
|
expect(
|
|
await screen.findByRole("dialog", { name: "Manual Pass Details" }),
|
|
).toBeVisible();
|
|
});
|
|
|
|
it("should open Triage Note from keyboard activation without a direct update", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
const triage = makeTriageSummary({
|
|
rawFindingStatus: FINDING_STATUS.MANUAL,
|
|
});
|
|
const onTriageUpdateAction = vi.fn();
|
|
const onTriageDetailLoadAction = vi
|
|
.fn()
|
|
.mockResolvedValue(makeTriageDetail());
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={triage}
|
|
onTriageUpdateAction={onTriageUpdateAction}
|
|
onTriageDetailLoadAction={onTriageDetailLoadAction}
|
|
/>,
|
|
);
|
|
await user.click(screen.getByRole("combobox", { name: "Triage status" }));
|
|
await user.keyboard("{End}");
|
|
|
|
// When
|
|
await user.keyboard("{Enter}");
|
|
|
|
// Then
|
|
expect(onTriageUpdateAction).not.toHaveBeenCalled();
|
|
expect(onTriageDetailLoadAction).toHaveBeenCalledWith(triage);
|
|
expect(
|
|
await screen.findByRole("dialog", { name: "Add Triage Note" }),
|
|
).toBeVisible();
|
|
expect(screen.getByLabelText("Manual pass evidence")).toHaveValue("");
|
|
});
|
|
|
|
it("should show Resolved as current triage and manual verification as secondary provenance", () => {
|
|
// Given / When
|
|
const { rerender } = render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.RESOLVED,
|
|
label: "Resolved",
|
|
manualPassProvenance: "Manually verified",
|
|
rawFindingStatus: FINDING_STATUS.MANUAL,
|
|
})}
|
|
onTriageUpdateAction={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
// Then
|
|
expect(
|
|
screen.getByRole("combobox", { name: "Triage status" }),
|
|
).toHaveTextContent("Resolved");
|
|
expect(
|
|
screen.getByRole("combobox", { name: "Triage status" }),
|
|
).not.toHaveTextContent("Pass");
|
|
expect(screen.getByText("Manually verified")).toBeVisible();
|
|
expect(
|
|
screen.getByRole("combobox", { name: "Triage status" }),
|
|
).not.toHaveTextContent("Manual");
|
|
|
|
// When
|
|
rerender(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.RESOLVED,
|
|
label: "Resolved",
|
|
rawFindingStatus: FINDING_STATUS.PASS,
|
|
})}
|
|
onTriageUpdateAction={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
// Then
|
|
expect(
|
|
screen.getByRole("combobox", { name: "Triage status" }),
|
|
).toHaveTextContent("Resolved");
|
|
expect(screen.queryByText("Manually verified")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("should load authoritative Manual Pass evidence from the provenance action", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
const onTriageUpdateAction = vi.fn();
|
|
const onRowClick = vi.fn();
|
|
const triage = makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.RESOLVED,
|
|
label: "Resolved",
|
|
manualPassProvenance: "Manually verified",
|
|
rawFindingStatus: FINDING_STATUS.MANUAL,
|
|
});
|
|
const detail = makeTriageDetail({
|
|
status: FINDING_TRIAGE_STATUS.RESOLVED,
|
|
label: "Resolved",
|
|
manualPassActive: true,
|
|
manualPassEvidence: "The control owner verified the production evidence.",
|
|
manualPassCreatedByName: "Alex Security",
|
|
manualPassCreatedAt: "2026-06-03T10:00:00Z",
|
|
manualPassExpiresAt: "2026-06-17T10:00:00Z",
|
|
});
|
|
const onTriageDetailLoadAction = vi.fn().mockResolvedValue(detail);
|
|
render(
|
|
<div onClick={onRowClick}>
|
|
<FindingTriageStatusCell
|
|
triage={triage}
|
|
findingContext={{ title: "S3 bucket allows public reads" }}
|
|
onTriageUpdateAction={onTriageUpdateAction}
|
|
onTriageDetailLoadAction={onTriageDetailLoadAction}
|
|
/>
|
|
</div>,
|
|
);
|
|
|
|
// When
|
|
await user.click(
|
|
screen.getByRole("button", { name: "View Manual Pass details" }),
|
|
);
|
|
|
|
// Then
|
|
expect(onTriageDetailLoadAction).toHaveBeenCalledWith(triage);
|
|
expect(onRowClick).not.toHaveBeenCalled();
|
|
const dialog = await screen.findByRole("dialog", {
|
|
name: "Manual Pass Details",
|
|
});
|
|
expect(
|
|
within(dialog).getByText(
|
|
"The control owner verified the production evidence.",
|
|
),
|
|
).toBeVisible();
|
|
expect(
|
|
within(dialog).getByText(/Manually verified by Alex Security/i),
|
|
).toBeVisible();
|
|
expect(within(dialog).getByText("Active")).toBeVisible();
|
|
expect(within(dialog).getByText("Jun 03, 2026")).toBeVisible();
|
|
expect(within(dialog).getByText("Jun 17, 2026")).toBeVisible();
|
|
const statusControl = within(dialog).getByRole("combobox", {
|
|
name: "Triage status",
|
|
});
|
|
expect(statusControl).toHaveTextContent("Resolved");
|
|
expect(statusControl).toBeDisabled();
|
|
expect(
|
|
within(dialog).queryByLabelText("Note text"),
|
|
).not.toBeInTheDocument();
|
|
expect(
|
|
within(dialog).queryByLabelText("Manual pass evidence"),
|
|
).not.toBeInTheDocument();
|
|
expect(
|
|
within(dialog).queryByRole("button", { name: "Save" }),
|
|
).not.toBeInTheDocument();
|
|
expect(onTriageUpdateAction).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should preserve Open in inactive Manual Pass details", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
const triage = makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.OPEN,
|
|
label: "Open",
|
|
manualPassProvenance: "Manually verified",
|
|
rawFindingStatus: FINDING_STATUS.MANUAL,
|
|
});
|
|
const onTriageDetailLoadAction = vi.fn().mockResolvedValue(
|
|
makeTriageDetail({
|
|
status: FINDING_TRIAGE_STATUS.OPEN,
|
|
label: "Open",
|
|
manualPassActive: false,
|
|
manualPassEvidence: "Previously verified.",
|
|
manualPassCreatedAt: "2026-06-03T10:00:00Z",
|
|
manualPassExpiresAt: "2026-06-17T10:00:00Z",
|
|
manualPassDeactivatedAt: "2026-06-10T10:00:00Z",
|
|
}),
|
|
);
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={triage}
|
|
onTriageUpdateAction={vi.fn()}
|
|
onTriageDetailLoadAction={onTriageDetailLoadAction}
|
|
/>,
|
|
);
|
|
|
|
// When
|
|
await user.click(
|
|
screen.getByRole("button", { name: "View Manual Pass details" }),
|
|
);
|
|
|
|
// Then
|
|
expect(
|
|
within(
|
|
await screen.findByRole("dialog", { name: "Manual Pass Details" }),
|
|
).getByRole("combobox", { name: "Triage status" }),
|
|
).toHaveTextContent("Open");
|
|
});
|
|
|
|
it("should keep Manual Pass provenance inert without a detail loader", () => {
|
|
// Given / When
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.RESOLVED,
|
|
label: "Resolved",
|
|
manualPassProvenance: "Manually verified",
|
|
rawFindingStatus: FINDING_STATUS.MANUAL,
|
|
})}
|
|
onTriageUpdateAction={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
// Then
|
|
expect(screen.getByText("Manually verified")).toBeVisible();
|
|
expect(
|
|
screen.queryByRole("button", { name: "View Manual Pass details" }),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("should report a failed Manual Pass provenance detail load", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.RESOLVED,
|
|
label: "Resolved",
|
|
manualPassProvenance: "Manually verified",
|
|
rawFindingStatus: FINDING_STATUS.MANUAL,
|
|
})}
|
|
onTriageUpdateAction={vi.fn()}
|
|
onTriageDetailLoadAction={vi
|
|
.fn()
|
|
.mockRejectedValue(new Error("load failed"))}
|
|
/>,
|
|
);
|
|
|
|
// When
|
|
await user.click(
|
|
screen.getByRole("button", { name: "View Manual Pass details" }),
|
|
);
|
|
|
|
// Then
|
|
expect(await screen.findByRole("alert")).toHaveTextContent(
|
|
"Could not load current triage details.",
|
|
);
|
|
expect(
|
|
screen.queryByRole("dialog", { name: "Manual Pass Details" }),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("should handle missing Manual Pass detail without opening an empty modal", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.RESOLVED,
|
|
label: "Resolved",
|
|
manualPassProvenance: "Manually verified",
|
|
rawFindingStatus: FINDING_STATUS.MANUAL,
|
|
})}
|
|
onTriageUpdateAction={vi.fn()}
|
|
onTriageDetailLoadAction={vi.fn().mockResolvedValue(undefined)}
|
|
/>,
|
|
);
|
|
|
|
// When
|
|
await user.click(
|
|
screen.getByRole("button", { name: "View Manual Pass details" }),
|
|
);
|
|
|
|
// Then
|
|
expect(await screen.findByRole("alert")).toHaveTextContent(
|
|
"Could not load current triage details.",
|
|
);
|
|
expect(
|
|
screen.queryByRole("dialog", { name: "Manual Pass Details" }),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("renders a read-only triage status badge with the status color", () => {
|
|
// Given / When
|
|
render(
|
|
<FindingTriageStatusBadge
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.REMEDIATING,
|
|
label: "Remediating",
|
|
})}
|
|
/>,
|
|
);
|
|
|
|
// Then
|
|
expect(screen.getByText("Triage:")).toBeInTheDocument();
|
|
expect(screen.getByText("Remediating")).toHaveClass("text-bg-data-info");
|
|
});
|
|
|
|
it("should disable table status mutation when no update handler is wired", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.OPEN,
|
|
label: "Open",
|
|
canEdit: true,
|
|
})}
|
|
/>,
|
|
);
|
|
|
|
const statusControl = screen.getByRole("combobox", {
|
|
name: "Triage status",
|
|
});
|
|
|
|
// When
|
|
await user.click(statusControl);
|
|
|
|
// Then
|
|
expect(statusControl).toBeDisabled();
|
|
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("should lock the table status picker for resolved findings", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
const onTriageUpdateAction = vi.fn();
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.RESOLVED,
|
|
label: "Resolved",
|
|
})}
|
|
onTriageUpdateAction={onTriageUpdateAction}
|
|
/>,
|
|
);
|
|
|
|
const statusControl = screen.getByRole("combobox", {
|
|
name: "Triage status",
|
|
});
|
|
|
|
// When
|
|
await user.click(statusControl);
|
|
|
|
// Then — automation owns the transition out of Resolved.
|
|
expect(statusControl).toBeDisabled();
|
|
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
expect(
|
|
screen.getAllByText(
|
|
"Triage status is managed automatically once the finding is resolved.",
|
|
).length,
|
|
).toBeGreaterThan(0);
|
|
expect(onTriageUpdateAction).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should keep the note action available for resolved findings", () => {
|
|
// Given
|
|
render(
|
|
<FindingNoteActionItem
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.RESOLVED,
|
|
label: "Resolved",
|
|
hasVisibleNote: false,
|
|
})}
|
|
onTriageUpdateAction={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
// Then — the lock only applies to status transitions, not notes.
|
|
expect(
|
|
screen.getByRole("button", { name: "Add Triage Note" }),
|
|
).toBeEnabled();
|
|
});
|
|
|
|
it("should not open an editable empty-note modal for an existing note without a loader", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
render(
|
|
<FindingNoteActionItem
|
|
triage={makeTriageSummary({ hasVisibleNote: true })}
|
|
findingContext={{ title: "S3 bucket allows public reads" }}
|
|
onTriageUpdateAction={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
const existingNoteButton = screen.getByRole("button", {
|
|
name: "Open note",
|
|
});
|
|
|
|
// When
|
|
await user.click(existingNoteButton);
|
|
|
|
// Then
|
|
expect(existingNoteButton).toBeDisabled();
|
|
expect(
|
|
screen.queryByRole("dialog", { name: "Add Triage Note" }),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("should load an existing note before opening the modal", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
const onTriageNoteLoadAction = vi.fn().mockResolvedValue({
|
|
noteId: "note-1",
|
|
noteBody: "Loaded existing note",
|
|
});
|
|
render(
|
|
<FindingNoteActionItem
|
|
triage={makeTriageSummary({ hasVisibleNote: true, notesCount: 1 })}
|
|
findingContext={{ title: "S3 bucket allows public reads" }}
|
|
onTriageUpdateAction={vi.fn()}
|
|
onTriageNoteLoadAction={onTriageNoteLoadAction}
|
|
/>,
|
|
);
|
|
|
|
// When
|
|
await user.click(screen.getByRole("button", { name: "Open note" }));
|
|
|
|
// Then
|
|
expect(onTriageNoteLoadAction).toHaveBeenCalledWith(
|
|
expect.objectContaining({ triageId: "triage-1", notesCount: 1 }),
|
|
);
|
|
expect(
|
|
await screen.findByRole("dialog", { name: "Add Triage Note" }),
|
|
).toBeVisible();
|
|
expect(screen.getByLabelText("Note text")).toHaveValue(
|
|
"Loaded existing note",
|
|
);
|
|
});
|
|
|
|
it("should open a disabled billing upsell modal for Cloud-only Add Triage Note", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
render(
|
|
<FindingNoteActionItem
|
|
triage={makeTriageSummary({
|
|
canEdit: false,
|
|
hasVisibleNote: false,
|
|
disabledReason: FINDING_TRIAGE_DISABLED_REASON.CLOUD_ONLY,
|
|
})}
|
|
findingContext={{ title: "S3 bucket allows public reads" }}
|
|
/>,
|
|
);
|
|
|
|
// When
|
|
await user.click(screen.getByRole("button", { name: "Add Triage Note" }));
|
|
|
|
// Then
|
|
expect(
|
|
screen.getByRole("dialog", { name: "Add Triage Note" }),
|
|
).toBeVisible();
|
|
expect(screen.getByLabelText("Note text")).toBeDisabled();
|
|
const saveUpgrade = screen.getByRole("button", {
|
|
name: "Save - available in Prowler Cloud",
|
|
});
|
|
expect(saveUpgrade).not.toBeDisabled();
|
|
|
|
await user.click(saveUpgrade);
|
|
|
|
expect(useCloudUpgradeStore.getState().activeFeature).toBe(
|
|
CLOUD_UPGRADE_FEATURE.FINDING_TRIAGE,
|
|
);
|
|
});
|
|
|
|
it("should disable Add Triage Note when no update handler is wired", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
render(
|
|
<FindingNoteActionItem
|
|
triage={makeTriageSummary({ hasVisibleNote: false, canEdit: true })}
|
|
findingContext={{ title: "S3 bucket allows public reads" }}
|
|
/>,
|
|
);
|
|
|
|
const addNoteButton = screen.getByRole("button", {
|
|
name: "Add Triage Note",
|
|
});
|
|
|
|
// When
|
|
await user.click(addNoteButton);
|
|
|
|
// Then
|
|
expect(addNoteButton).toBeDisabled();
|
|
expect(
|
|
screen.queryByRole("dialog", { name: "Add Triage Note" }),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("should show a visible error when an existing note cannot load", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
const onTriageNoteLoadAction = vi
|
|
.fn()
|
|
.mockRejectedValue(new Error("load failed"));
|
|
render(
|
|
<FindingNoteActionItem
|
|
triage={makeTriageSummary({ hasVisibleNote: true, notesCount: 1 })}
|
|
findingContext={{ title: "S3 bucket allows public reads" }}
|
|
onTriageUpdateAction={vi.fn()}
|
|
onTriageNoteLoadAction={onTriageNoteLoadAction}
|
|
/>,
|
|
);
|
|
|
|
// When
|
|
await user.click(screen.getByRole("button", { name: "Open note" }));
|
|
|
|
// Then
|
|
await waitFor(() =>
|
|
expect(toastMock).toHaveBeenCalledWith({
|
|
variant: "destructive",
|
|
title: "Could not load the existing note.",
|
|
description: "Please try again.",
|
|
}),
|
|
);
|
|
expect(
|
|
screen.queryByRole("dialog", { name: "Add Triage Note" }),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("should show a visible error when current triage details cannot load for a new note", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
const onTriageDetailLoadAction = vi
|
|
.fn()
|
|
.mockRejectedValue(new Error("load failed"));
|
|
render(
|
|
<FindingNoteActionItem
|
|
triage={makeTriageSummary()}
|
|
findingContext={{ title: "S3 bucket allows public reads" }}
|
|
onTriageUpdateAction={vi.fn()}
|
|
onTriageDetailLoadAction={onTriageDetailLoadAction}
|
|
/>,
|
|
);
|
|
|
|
// When
|
|
await user.click(screen.getByRole("button", { name: "Add Triage Note" }));
|
|
|
|
// Then
|
|
await waitFor(() =>
|
|
expect(toastMock).toHaveBeenCalledWith({
|
|
variant: "destructive",
|
|
title: "Could not load current triage details.",
|
|
description: "Please try again.",
|
|
}),
|
|
);
|
|
expect(
|
|
screen.queryByRole("dialog", { name: "Add Triage Note" }),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("should visibly announce when current triage details cannot load", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
rawFindingStatus: FINDING_STATUS.MANUAL,
|
|
})}
|
|
onTriageUpdateAction={vi.fn()}
|
|
onTriageDetailLoadAction={vi
|
|
.fn()
|
|
.mockRejectedValue(new Error("load failed"))}
|
|
/>,
|
|
);
|
|
await user.click(screen.getByRole("combobox", { name: "Triage status" }));
|
|
|
|
// When
|
|
await user.click(screen.getByRole("option", { name: "Resolved" }));
|
|
|
|
// Then
|
|
const alert = await screen.findByRole("alert");
|
|
expect(alert).toHaveTextContent("Could not load current triage details.");
|
|
expect(alert).toBeVisible();
|
|
});
|
|
|
|
it("should keep the optimistic table status while stale props are rendered during update", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
let resolveUpdate: () => void = () => {};
|
|
const onTriageUpdateAction = vi.fn(
|
|
() =>
|
|
new Promise<void>((resolve) => {
|
|
resolveUpdate = resolve;
|
|
}),
|
|
);
|
|
const { rerender } = render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.OPEN,
|
|
label: "Open",
|
|
})}
|
|
onTriageUpdateAction={onTriageUpdateAction}
|
|
/>,
|
|
);
|
|
|
|
const statusControl = screen.getByRole("combobox", {
|
|
name: "Triage status",
|
|
});
|
|
|
|
// When: user selects a new status.
|
|
await user.click(statusControl);
|
|
await user.click(screen.getByRole("option", { name: "Under Review" }));
|
|
|
|
// Then: the optimistic status is visible immediately.
|
|
expect(statusControl).toHaveTextContent("Under Review");
|
|
|
|
// When: the parent renders stale data while the request is still pending.
|
|
rerender(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.OPEN,
|
|
label: "Open",
|
|
})}
|
|
onTriageUpdateAction={onTriageUpdateAction}
|
|
/>,
|
|
);
|
|
|
|
// Then: the control must not flicker back to Open.
|
|
expect(
|
|
screen.getByRole("combobox", { name: "Triage status" }),
|
|
).toHaveTextContent("Under Review");
|
|
|
|
// When: backend completes and fresh props arrive.
|
|
resolveUpdate();
|
|
await waitFor(() => expect(onTriageUpdateAction).toHaveBeenCalled());
|
|
rerender(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.UNDER_REVIEW,
|
|
label: "Under Review",
|
|
})}
|
|
onTriageUpdateAction={onTriageUpdateAction}
|
|
/>,
|
|
);
|
|
|
|
// Then
|
|
expect(
|
|
screen.getByRole("combobox", { name: "Triage status" }),
|
|
).toHaveTextContent("Under Review");
|
|
});
|
|
|
|
it("should not resurrect a stale optimistic status after the server later returns the previous status", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
const onTriageUpdateAction = vi.fn().mockResolvedValue(undefined);
|
|
const { rerender } = render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.OPEN,
|
|
label: "Open",
|
|
})}
|
|
onTriageUpdateAction={onTriageUpdateAction}
|
|
/>,
|
|
);
|
|
|
|
// When: user optimistically moves Open -> Under Review and it succeeds.
|
|
await user.click(screen.getByRole("combobox", { name: "Triage status" }));
|
|
await user.click(screen.getByRole("option", { name: "Under Review" }));
|
|
await waitFor(() => expect(onTriageUpdateAction).toHaveBeenCalled());
|
|
|
|
// And: fresh props converge on the optimistic status (server confirmed).
|
|
rerender(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.UNDER_REVIEW,
|
|
label: "Under Review",
|
|
})}
|
|
onTriageUpdateAction={onTriageUpdateAction}
|
|
/>,
|
|
);
|
|
|
|
// When: a later refetch legitimately returns the previous status again.
|
|
rerender(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.OPEN,
|
|
label: "Open",
|
|
})}
|
|
onTriageUpdateAction={onTriageUpdateAction}
|
|
/>,
|
|
);
|
|
|
|
// Then: the real server status wins; the stale optimistic value is gone.
|
|
expect(
|
|
screen.getByRole("combobox", { name: "Triage status" }),
|
|
).toHaveTextContent("Open");
|
|
});
|
|
|
|
it("should refresh the visible table status when triage props change", () => {
|
|
// Given
|
|
const { rerender } = render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.OPEN,
|
|
label: "Open",
|
|
})}
|
|
onTriageUpdateAction={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(
|
|
screen.getByRole("combobox", { name: "Triage status" }),
|
|
).toHaveTextContent("Open");
|
|
|
|
// When
|
|
rerender(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.REMEDIATING,
|
|
label: "Remediating",
|
|
})}
|
|
onTriageUpdateAction={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
// Then
|
|
expect(
|
|
screen.getByRole("combobox", { name: "Triage status" }),
|
|
).toHaveTextContent("Remediating");
|
|
});
|
|
|
|
it("should not submit when table status selection matches the current status", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
const onTriageUpdateAction = vi.fn();
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.OPEN,
|
|
label: "Open",
|
|
})}
|
|
onTriageUpdateAction={onTriageUpdateAction}
|
|
/>,
|
|
);
|
|
|
|
// When
|
|
await user.click(screen.getByRole("combobox", { name: "Triage status" }));
|
|
await user.click(screen.getByRole("option", { name: "Open" }));
|
|
|
|
// Then
|
|
expect(onTriageUpdateAction).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should rollback table status and show an error when update fails", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
const onTriageUpdateAction = vi.fn().mockRejectedValue(new Error("fail"));
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.OPEN,
|
|
label: "Open",
|
|
})}
|
|
onTriageUpdateAction={onTriageUpdateAction}
|
|
/>,
|
|
);
|
|
|
|
const statusControl = screen.getByRole("combobox", {
|
|
name: "Triage status",
|
|
});
|
|
|
|
// When
|
|
await user.click(statusControl);
|
|
await user.click(screen.getByRole("option", { name: "Remediating" }));
|
|
|
|
// Then
|
|
await waitFor(() =>
|
|
expect(toastMock).toHaveBeenCalledWith({
|
|
variant: "destructive",
|
|
title: "Could not update triage status.",
|
|
description: "Please try again.",
|
|
}),
|
|
);
|
|
expect(statusControl).toHaveTextContent("Open");
|
|
});
|
|
|
|
it("should not confirm when moving between Mutelist shortcut statuses", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
const onTriageUpdateAction = vi.fn();
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.RISK_ACCEPTED,
|
|
label: "Risk Accepted",
|
|
})}
|
|
onTriageUpdateAction={onTriageUpdateAction}
|
|
/>,
|
|
);
|
|
|
|
// When
|
|
await user.click(screen.getByRole("combobox", { name: "Triage status" }));
|
|
await user.click(screen.getByRole("option", { name: "False Positive" }));
|
|
|
|
// Then
|
|
expect(screen.queryByRole("dialog", { name: "Mute finding?" })).toBeNull();
|
|
await waitFor(() =>
|
|
expect(onTriageUpdateAction).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
status: FINDING_TRIAGE_STATUS.FALSE_POSITIVE,
|
|
previousStatus: FINDING_TRIAGE_STATUS.RISK_ACCEPTED,
|
|
isMuted: false,
|
|
}),
|
|
),
|
|
);
|
|
});
|
|
|
|
it("should not confirm or mute again when an already muted finding enters a shortcut status", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
const onTriageUpdateAction = vi.fn();
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.OPEN,
|
|
label: "Open",
|
|
isMuted: true,
|
|
})}
|
|
onTriageUpdateAction={onTriageUpdateAction}
|
|
/>,
|
|
);
|
|
|
|
// When
|
|
await user.click(screen.getByRole("combobox", { name: "Triage status" }));
|
|
await user.click(screen.getByRole("option", { name: "Risk Accepted" }));
|
|
|
|
// Then
|
|
expect(screen.queryByRole("dialog", { name: "Mute finding?" })).toBeNull();
|
|
await waitFor(() =>
|
|
expect(onTriageUpdateAction).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
status: FINDING_TRIAGE_STATUS.RISK_ACCEPTED,
|
|
previousStatus: FINDING_TRIAGE_STATUS.OPEN,
|
|
isMuted: true,
|
|
}),
|
|
),
|
|
);
|
|
});
|
|
|
|
it("should confirm before applying Mutelist shortcut statuses from the table", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
let resolveUpdate: () => void = () => {};
|
|
const onTriageUpdateAction = vi.fn(
|
|
() =>
|
|
new Promise<void>((resolve) => {
|
|
resolveUpdate = resolve;
|
|
}),
|
|
);
|
|
render(
|
|
<FindingTriageStatusCell
|
|
triage={makeTriageSummary({
|
|
status: FINDING_TRIAGE_STATUS.OPEN,
|
|
label: "Open",
|
|
})}
|
|
onTriageUpdateAction={onTriageUpdateAction}
|
|
/>,
|
|
);
|
|
|
|
const statusControl = screen.getByRole("combobox", {
|
|
name: "Triage status",
|
|
});
|
|
|
|
// When: user selects a Mutelist shortcut.
|
|
await user.click(statusControl);
|
|
await user.click(screen.getByRole("option", { name: "False Positive" }));
|
|
|
|
// Then: the user is warned before the server action handles muting.
|
|
expect(screen.getByRole("dialog", { name: "Mute finding?" })).toBeVisible();
|
|
expect(
|
|
screen.getByText(
|
|
"Changing triage to False Positive will mute the finding",
|
|
),
|
|
).toBeVisible();
|
|
expect(onTriageUpdateAction).not.toHaveBeenCalled();
|
|
|
|
// When
|
|
await user.click(screen.getByRole("button", { name: "Mute finding" }));
|
|
|
|
// Then
|
|
await waitFor(() =>
|
|
expect(onTriageUpdateAction).toHaveBeenCalledWith({
|
|
findingId: "finding-1",
|
|
findingUid: "prowler-finding-uid-1",
|
|
triageId: "triage-1",
|
|
notesCount: 0,
|
|
status: FINDING_TRIAGE_STATUS.FALSE_POSITIVE,
|
|
previousStatus: FINDING_TRIAGE_STATUS.OPEN,
|
|
isMuted: false,
|
|
}),
|
|
);
|
|
expect(statusControl).toHaveTextContent("False Positive");
|
|
|
|
resolveUpdate();
|
|
});
|
|
});
|