mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +00:00
383 lines
10 KiB
TypeScript
383 lines
10 KiB
TypeScript
import { act, renderHook } from "@testing-library/react";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const { useFindingGroupResourcesMock, useResourceDetailDrawerMock } =
|
|
vi.hoisted(() => ({
|
|
useFindingGroupResourcesMock: vi.fn(),
|
|
useResourceDetailDrawerMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/hooks/use-finding-group-resources", () => ({
|
|
useFindingGroupResources: useFindingGroupResourcesMock,
|
|
}));
|
|
|
|
vi.mock("@/lib", () => ({
|
|
applyDefaultMutedFilter: (
|
|
filters: Record<string, string | string[] | undefined>,
|
|
) => ({
|
|
"filter[muted]": "false",
|
|
...filters,
|
|
}),
|
|
}));
|
|
|
|
vi.mock(
|
|
"@/components/findings/table/resource-detail-drawer/use-resource-detail-drawer",
|
|
() => ({
|
|
useResourceDetailDrawer: useResourceDetailDrawerMock,
|
|
}),
|
|
);
|
|
|
|
import {
|
|
type FindingGroupRow,
|
|
type FindingResourceRow,
|
|
FINDINGS_ROW_TYPE,
|
|
} from "@/types";
|
|
import {
|
|
FINDING_TRIAGE_STATUS,
|
|
type FindingTriageUpdateResult,
|
|
} from "@/types/findings-triage";
|
|
|
|
import { useFindingGroupResourceState } from "./use-finding-group-resource-state";
|
|
|
|
const group: FindingGroupRow = {
|
|
id: "group-1",
|
|
rowType: FINDINGS_ROW_TYPE.GROUP,
|
|
checkId: "s3_bucket_public_access",
|
|
checkTitle: "S3 Bucket Public Access",
|
|
severity: "high",
|
|
status: "FAIL",
|
|
resourcesTotal: 3,
|
|
resourcesFail: 2,
|
|
newCount: 1,
|
|
changedCount: 0,
|
|
mutedCount: 0,
|
|
providers: ["aws"],
|
|
updatedAt: "2026-04-22T10:00:00Z",
|
|
};
|
|
|
|
function findingResource(status: string): FindingResourceRow {
|
|
return {
|
|
id: "resource-1",
|
|
rowType: FINDINGS_ROW_TYPE.RESOURCE,
|
|
findingId: "finding-1",
|
|
checkId: "check-1",
|
|
providerType: "aws",
|
|
providerAlias: "production",
|
|
providerUid: "provider-1",
|
|
resourceName: "resource-1",
|
|
resourceType: "Bucket",
|
|
resourceGroup: "default",
|
|
resourceUid: "resource-uid-1",
|
|
service: "s3",
|
|
region: "us-east-1",
|
|
severity: "high",
|
|
status,
|
|
statusExtended: `${status} finding`,
|
|
delta: null,
|
|
isMuted: false,
|
|
firstSeenAt: null,
|
|
lastSeenAt: "2026-04-22T10:00:00Z",
|
|
};
|
|
}
|
|
|
|
describe("useFindingGroupResourceState", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
|
|
useFindingGroupResourcesMock.mockReturnValue({
|
|
sentinelRef: vi.fn(),
|
|
refresh: vi.fn(),
|
|
loadMore: vi.fn(),
|
|
totalCount: 3,
|
|
});
|
|
|
|
useResourceDetailDrawerMock.mockReturnValue({
|
|
isOpen: false,
|
|
isLoading: false,
|
|
isNavigating: false,
|
|
checkMeta: null,
|
|
currentIndex: 0,
|
|
totalResources: 3,
|
|
currentResource: null,
|
|
currentFinding: null,
|
|
otherFindings: [],
|
|
openDrawer: vi.fn(),
|
|
closeDrawer: vi.fn(),
|
|
navigatePrev: vi.fn(),
|
|
navigateNext: vi.fn(),
|
|
refetchCurrent: vi.fn(),
|
|
});
|
|
});
|
|
|
|
it("applies the shared default muted filter when the user has not opted in", () => {
|
|
renderHook(() =>
|
|
useFindingGroupResourceState({
|
|
group,
|
|
filters: {
|
|
"filter[provider_type__in]": "aws",
|
|
},
|
|
hasHistoricalData: false,
|
|
}),
|
|
);
|
|
|
|
expect(useFindingGroupResourcesMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
filters: {
|
|
"filter[provider_type__in]": "aws",
|
|
"filter[muted]": "false",
|
|
},
|
|
}),
|
|
);
|
|
expect(useResourceDetailDrawerMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
includeMutedInOtherFindings: false,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("includes muted findings in the drawer only when filter[muted]=include is active", () => {
|
|
renderHook(() =>
|
|
useFindingGroupResourceState({
|
|
group,
|
|
filters: {
|
|
"filter[muted]": "include",
|
|
},
|
|
hasHistoricalData: false,
|
|
}),
|
|
);
|
|
|
|
expect(useFindingGroupResourcesMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
filters: {
|
|
"filter[muted]": "include",
|
|
},
|
|
}),
|
|
);
|
|
expect(useResourceDetailDrawerMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
includeMutedInOtherFindings: true,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("derives selected resources from the latest loaded data", async () => {
|
|
// Given
|
|
const { result } = renderHook(() =>
|
|
useFindingGroupResourceState({
|
|
group,
|
|
filters: {},
|
|
hasHistoricalData: false,
|
|
}),
|
|
);
|
|
const onSetResources = useFindingGroupResourcesMock.mock.calls[0][0]
|
|
.onSetResources as (
|
|
resources: FindingResourceRow[],
|
|
hasMore: boolean,
|
|
) => void;
|
|
await act(async () => {
|
|
onSetResources([findingResource("FAIL")], false);
|
|
result.current.handleRowSelectionChange({ "finding-1": true });
|
|
});
|
|
|
|
// When: a refresh updates the selected finding without another selection event
|
|
await act(async () => {
|
|
onSetResources([findingResource("MUTED")], false);
|
|
});
|
|
|
|
// Then
|
|
expect(result.current.selectedResources).toEqual([
|
|
expect.objectContaining({
|
|
findingId: "finding-1",
|
|
status: "MUTED",
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("keeps selection bound to finding ids when resources are reordered", async () => {
|
|
// Given
|
|
const firstResource = findingResource("FAIL");
|
|
const secondResource = {
|
|
...findingResource("PASS"),
|
|
id: "resource-2",
|
|
findingId: "finding-2",
|
|
resourceName: "resource-2",
|
|
resourceUid: "resource-uid-2",
|
|
};
|
|
const { result } = renderHook(() =>
|
|
useFindingGroupResourceState({
|
|
group,
|
|
filters: {},
|
|
hasHistoricalData: false,
|
|
}),
|
|
);
|
|
const onSetResources = useFindingGroupResourcesMock.mock.calls[0][0]
|
|
.onSetResources as (
|
|
resources: FindingResourceRow[],
|
|
hasMore: boolean,
|
|
) => void;
|
|
await act(async () => {
|
|
onSetResources([firstResource, secondResource], false);
|
|
result.current.handleRowSelectionChange({ "finding-1": true });
|
|
});
|
|
|
|
// When
|
|
await act(async () => {
|
|
onSetResources(
|
|
[secondResource, { ...firstResource, status: "MUTED" }],
|
|
false,
|
|
);
|
|
});
|
|
|
|
// Then
|
|
expect(result.current.selectedResources).toEqual([
|
|
expect.objectContaining({
|
|
findingId: "finding-1",
|
|
status: "MUTED",
|
|
}),
|
|
]);
|
|
expect(result.current.selectedFindingIds).toEqual(["finding-1"]);
|
|
});
|
|
|
|
it("preserves an existing mute reason for already-muted optimistic shortcut updates", async () => {
|
|
// Given
|
|
const mutedResource: FindingResourceRow = {
|
|
id: "resource-1",
|
|
rowType: FINDINGS_ROW_TYPE.RESOURCE,
|
|
findingId: "finding-1",
|
|
checkId: "check-1",
|
|
providerType: "aws",
|
|
providerAlias: "production",
|
|
providerUid: "provider-1",
|
|
resourceName: "resource-1",
|
|
resourceType: "Bucket",
|
|
resourceGroup: "default",
|
|
resourceUid: "resource-uid-1",
|
|
service: "s3",
|
|
region: "us-east-1",
|
|
severity: "high",
|
|
status: "MUTED",
|
|
statusExtended: "Muted finding",
|
|
delta: null,
|
|
isMuted: true,
|
|
mutedReason: "Existing mute rule",
|
|
firstSeenAt: null,
|
|
lastSeenAt: "2026-04-22T10:00:00Z",
|
|
triage: {
|
|
findingId: "finding-1",
|
|
findingUid: "finding-uid-1",
|
|
triageId: "triage-1",
|
|
notesCount: 0,
|
|
status: FINDING_TRIAGE_STATUS.OPEN,
|
|
label: "Open",
|
|
hasVisibleNote: false,
|
|
isMuted: true,
|
|
canEdit: true,
|
|
billingHref: "https://prowler.com/pricing",
|
|
},
|
|
};
|
|
|
|
const { result } = renderHook(() =>
|
|
useFindingGroupResourceState({
|
|
group,
|
|
filters: {},
|
|
hasHistoricalData: false,
|
|
}),
|
|
);
|
|
const onSetResources = useFindingGroupResourcesMock.mock.calls[0][0]
|
|
.onSetResources as (
|
|
resources: FindingResourceRow[],
|
|
hasMore: boolean,
|
|
) => void;
|
|
|
|
await act(async () => {
|
|
onSetResources([mutedResource], false);
|
|
});
|
|
|
|
// When
|
|
let updateResult: FindingTriageUpdateResult | void = undefined;
|
|
await act(async () => {
|
|
updateResult = await result.current.updateTriageOptimistically(
|
|
{
|
|
findingId: "finding-1",
|
|
findingUid: "finding-uid-1",
|
|
triageId: "triage-1",
|
|
notesCount: 0,
|
|
status: FINDING_TRIAGE_STATUS.RISK_ACCEPTED,
|
|
previousStatus: FINDING_TRIAGE_STATUS.OPEN,
|
|
isMuted: true,
|
|
},
|
|
async () => ({
|
|
manualPassExpiresAt: "2026-10-28T12:00:00Z",
|
|
}),
|
|
);
|
|
});
|
|
|
|
// Then
|
|
expect(result.current.resources[0]).toEqual(
|
|
expect.objectContaining({
|
|
isMuted: true,
|
|
mutedReason: "Existing mute rule",
|
|
}),
|
|
);
|
|
expect(updateResult).toEqual({
|
|
manualPassExpiresAt: "2026-10-28T12:00:00Z",
|
|
});
|
|
});
|
|
|
|
it("shows effective Pass while a grouped manual pass update is pending", async () => {
|
|
// Given
|
|
const manualResource = {
|
|
...findingResource("MANUAL"),
|
|
triage: {
|
|
findingId: "finding-1",
|
|
findingUid: "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",
|
|
},
|
|
};
|
|
const { result } = renderHook(() =>
|
|
useFindingGroupResourceState({
|
|
group,
|
|
filters: {},
|
|
hasHistoricalData: false,
|
|
}),
|
|
);
|
|
const onSetResources = useFindingGroupResourcesMock.mock.calls[0][0]
|
|
.onSetResources as (
|
|
resources: FindingResourceRow[],
|
|
hasMore: boolean,
|
|
) => void;
|
|
await act(async () => onSetResources([manualResource], false));
|
|
let resolveUpdate: () => void = () => {};
|
|
|
|
// When
|
|
let updatePromise: Promise<FindingTriageUpdateResult | void> | undefined;
|
|
act(() => {
|
|
updatePromise = result.current.updateTriageOptimistically(
|
|
{
|
|
findingId: "finding-1",
|
|
findingUid: "finding-uid-1",
|
|
triageId: "triage-1",
|
|
notesCount: 0,
|
|
status: FINDING_TRIAGE_STATUS.RESOLVED,
|
|
previousStatus: FINDING_TRIAGE_STATUS.UNDER_REVIEW,
|
|
manualPassEvidence: "Verified by the control owner.",
|
|
},
|
|
() => new Promise<void>((resolve) => (resolveUpdate = resolve)),
|
|
);
|
|
});
|
|
|
|
// Then
|
|
expect(result.current.resources[0]?.status).toBe("PASS");
|
|
|
|
resolveUpdate();
|
|
await act(async () => updatePromise);
|
|
});
|
|
});
|