mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
587187419f
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
915 lines
25 KiB
TypeScript
915 lines
25 KiB
TypeScript
import { act, renderHook } from "@testing-library/react";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Hoist mocks before imports that chain to next-auth
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const {
|
|
getFindingByIdMock,
|
|
getLatestFindingsByResourceUidMock,
|
|
adaptFindingsByResourceResponseMock,
|
|
} = vi.hoisted(() => ({
|
|
getFindingByIdMock: vi.fn(),
|
|
getLatestFindingsByResourceUidMock: vi.fn(),
|
|
adaptFindingsByResourceResponseMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/actions/findings", () => ({
|
|
getFindingById: getFindingByIdMock,
|
|
getLatestFindingsByResourceUid: getLatestFindingsByResourceUidMock,
|
|
adaptFindingsByResourceResponse: adaptFindingsByResourceResponseMock,
|
|
}));
|
|
|
|
vi.mock("next/navigation", () => ({
|
|
redirect: vi.fn(),
|
|
}));
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Import after mocks
|
|
// ---------------------------------------------------------------------------
|
|
|
|
import type { ResourceDrawerFinding } from "@/actions/findings";
|
|
import type { FindingResourceRow } from "@/types";
|
|
import {
|
|
FINDING_TRIAGE_STATUS,
|
|
type FindingTriageSummary,
|
|
} from "@/types/findings-triage";
|
|
|
|
import { useResourceDetailDrawer } from "./use-resource-detail-drawer";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function makeResource(
|
|
overrides?: Partial<FindingResourceRow>,
|
|
): FindingResourceRow {
|
|
return {
|
|
id: "row-1",
|
|
rowType: "resource" as const,
|
|
findingId: "finding-1",
|
|
checkId: "s3_check",
|
|
providerType: "aws",
|
|
providerAlias: "prod",
|
|
providerUid: "123",
|
|
resourceName: "my-bucket",
|
|
resourceGroup: "default",
|
|
resourceUid: "arn:aws:s3:::my-bucket",
|
|
service: "s3",
|
|
region: "us-east-1",
|
|
severity: "critical",
|
|
status: "FAIL",
|
|
isMuted: false,
|
|
mutedReason: undefined,
|
|
firstSeenAt: null,
|
|
lastSeenAt: null,
|
|
...overrides,
|
|
} as FindingResourceRow;
|
|
}
|
|
|
|
function makeDrawerFinding(
|
|
overrides?: Partial<ResourceDrawerFinding>,
|
|
): ResourceDrawerFinding {
|
|
return {
|
|
id: "finding-1",
|
|
uid: "uid-1",
|
|
checkId: "s3_check",
|
|
checkTitle: "S3 Check",
|
|
status: "FAIL",
|
|
severity: "high",
|
|
delta: null,
|
|
isMuted: false,
|
|
mutedReason: null,
|
|
firstSeenAt: null,
|
|
updatedAt: null,
|
|
resourceId: "resource-1",
|
|
resourceUid: "arn:aws:s3:::my-bucket",
|
|
resourceName: "my-bucket",
|
|
resourceService: "s3",
|
|
resourceRegion: "us-east-1",
|
|
resourceType: "bucket",
|
|
resourceGroup: "default",
|
|
resourceDetails: null,
|
|
resourceMetadata: null,
|
|
providerType: "aws",
|
|
providerAlias: "prod",
|
|
providerUid: "123",
|
|
risk: "high",
|
|
description: "desc",
|
|
statusExtended: "status",
|
|
complianceFrameworks: [],
|
|
categories: [],
|
|
remediation: {
|
|
recommendation: { text: "", url: "" },
|
|
code: { cli: "", other: "", nativeiac: "", terraform: "" },
|
|
},
|
|
additionalUrls: [],
|
|
scan: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeTriageSummary(
|
|
overrides?: Partial<FindingTriageSummary>,
|
|
): FindingTriageSummary {
|
|
return {
|
|
findingId: "finding-1",
|
|
findingUid: "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,
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Fix 2: AbortController cleanup on unmount
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("useResourceDetailDrawer — unmount cleanup", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.restoreAllMocks();
|
|
getLatestFindingsByResourceUidMock.mockResolvedValue({ data: [] });
|
|
});
|
|
|
|
it("should abort the in-flight fetch controller when the hook unmounts", async () => {
|
|
// Given — spy on AbortController.prototype.abort to detect abort calls
|
|
const abortSpy = vi.spyOn(AbortController.prototype, "abort");
|
|
|
|
// never-resolving fetch to simulate in-flight request
|
|
getFindingByIdMock.mockImplementation(() => new Promise(() => {}));
|
|
adaptFindingsByResourceResponseMock.mockReturnValue([]);
|
|
|
|
const resources = [makeResource()];
|
|
|
|
const { result, unmount } = renderHook(() =>
|
|
useResourceDetailDrawer({
|
|
resources,
|
|
}),
|
|
);
|
|
|
|
// When — trigger a fetch by opening the drawer
|
|
act(() => {
|
|
result.current.openDrawer(0);
|
|
});
|
|
|
|
// Verify a fetch was started
|
|
expect(getFindingByIdMock).toHaveBeenCalledTimes(1);
|
|
|
|
// Reset spy count to detect only the unmount abort
|
|
abortSpy.mockClear();
|
|
|
|
// Then — unmount while fetch is in flight
|
|
unmount();
|
|
|
|
// The abort should have been called on unmount
|
|
expect(abortSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("should not abort when no fetch has been started yet", () => {
|
|
// Given — spy on abort
|
|
const abortSpy = vi.spyOn(AbortController.prototype, "abort");
|
|
|
|
const resources = [makeResource()];
|
|
|
|
// When — render without opening drawer (no fetch started)
|
|
const { unmount } = renderHook(() =>
|
|
useResourceDetailDrawer({
|
|
resources,
|
|
}),
|
|
);
|
|
|
|
// Then — unmount without any fetch
|
|
unmount();
|
|
|
|
// abort should NOT have been called (fetchControllerRef.current is null)
|
|
expect(abortSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("useResourceDetailDrawer — other findings filtering", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
getLatestFindingsByResourceUidMock.mockResolvedValue({ data: [] });
|
|
});
|
|
|
|
it("should load other findings from the current resource uid and exclude only the current finding (status is filtered server-side)", async () => {
|
|
const resources = [makeResource()];
|
|
|
|
// Given — the API call applies filter[status]=FAIL server-side, so the
|
|
// mock returns only FAIL rows. The hook's only client-side job is to
|
|
// drop the row already shown above the table.
|
|
getFindingByIdMock.mockResolvedValue({ data: ["detail"] });
|
|
getLatestFindingsByResourceUidMock.mockResolvedValue({
|
|
data: ["resource"],
|
|
});
|
|
adaptFindingsByResourceResponseMock.mockImplementation(
|
|
(response: { data: string[] }) => {
|
|
if (response.data[0] === "detail") {
|
|
return [
|
|
makeDrawerFinding({
|
|
id: "finding-1",
|
|
checkId: "s3_check",
|
|
checkTitle: "Current",
|
|
status: "FAIL",
|
|
severity: "informational",
|
|
}),
|
|
];
|
|
}
|
|
|
|
return [
|
|
makeDrawerFinding({
|
|
id: "finding-3",
|
|
checkTitle: "First other finding",
|
|
status: "FAIL",
|
|
severity: "high",
|
|
}),
|
|
makeDrawerFinding({
|
|
id: "finding-1",
|
|
checkTitle: "Current finding duplicate from resource fetch",
|
|
status: "FAIL",
|
|
severity: "critical",
|
|
}),
|
|
makeDrawerFinding({
|
|
id: "finding-5",
|
|
checkTitle: "Second other finding",
|
|
status: "FAIL",
|
|
severity: "medium",
|
|
}),
|
|
];
|
|
},
|
|
);
|
|
|
|
const { result } = renderHook(() =>
|
|
useResourceDetailDrawer({
|
|
resources,
|
|
}),
|
|
);
|
|
|
|
await act(async () => {
|
|
result.current.openDrawer(0);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
// Then
|
|
expect(getFindingByIdMock).toHaveBeenCalledWith(
|
|
"finding-1",
|
|
"resources,scan.provider",
|
|
{ source: "resource-detail-drawer" },
|
|
);
|
|
expect(getLatestFindingsByResourceUidMock).toHaveBeenCalledWith({
|
|
resourceUid: "arn:aws:s3:::my-bucket",
|
|
pageSize: 50,
|
|
includeMuted: false,
|
|
});
|
|
expect(result.current.currentFinding?.id).toBe("finding-1");
|
|
expect(result.current.otherFindings.map((finding) => finding.id)).toEqual([
|
|
"finding-3",
|
|
"finding-5",
|
|
]);
|
|
});
|
|
|
|
it("should skip loading other findings for synthetic IaC resources and keep the current detail on findingId", async () => {
|
|
const resources = [
|
|
makeResource({
|
|
findingId: "synthetic-finding",
|
|
resourceUid: "synthetic://iac-resource",
|
|
}),
|
|
];
|
|
|
|
// Given
|
|
getFindingByIdMock.mockResolvedValue({ data: ["detail"] });
|
|
adaptFindingsByResourceResponseMock.mockReturnValue([
|
|
makeDrawerFinding({
|
|
id: "synthetic-finding",
|
|
checkId: "s3_check",
|
|
status: "MANUAL",
|
|
severity: "informational",
|
|
}),
|
|
]);
|
|
|
|
const { result } = renderHook(() =>
|
|
useResourceDetailDrawer({
|
|
resources,
|
|
canLoadOtherFindings: false,
|
|
}),
|
|
);
|
|
|
|
await act(async () => {
|
|
// When
|
|
result.current.openDrawer(0);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
// Then
|
|
expect(getFindingByIdMock).toHaveBeenCalledWith(
|
|
"synthetic-finding",
|
|
"resources,scan.provider",
|
|
{ source: "resource-detail-drawer" },
|
|
);
|
|
expect(getLatestFindingsByResourceUidMock).not.toHaveBeenCalled();
|
|
expect(result.current.currentFinding?.id).toBe("synthetic-finding");
|
|
expect(result.current.otherFindings).toEqual([]);
|
|
});
|
|
|
|
it("should request muted findings only when explicitly enabled", async () => {
|
|
const resources = [makeResource()];
|
|
|
|
getLatestFindingsByResourceUidMock.mockResolvedValue({ data: [] });
|
|
adaptFindingsByResourceResponseMock.mockReturnValue([makeDrawerFinding()]);
|
|
|
|
const { result } = renderHook(() =>
|
|
useResourceDetailDrawer({
|
|
resources,
|
|
includeMutedInOtherFindings: true,
|
|
}),
|
|
);
|
|
|
|
await act(async () => {
|
|
result.current.openDrawer(0);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(getLatestFindingsByResourceUidMock).toHaveBeenCalledWith({
|
|
resourceUid: "arn:aws:s3:::my-bucket",
|
|
pageSize: 50,
|
|
includeMuted: true,
|
|
});
|
|
});
|
|
|
|
it("should keep isNavigating true for a cached resource long enough to render skeletons", async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const resources = [
|
|
makeResource({
|
|
id: "row-1",
|
|
findingId: "finding-1",
|
|
resourceUid: "arn:aws:s3:::first-bucket",
|
|
resourceName: "first-bucket",
|
|
}),
|
|
makeResource({
|
|
id: "row-2",
|
|
findingId: "finding-2",
|
|
resourceUid: "arn:aws:s3:::second-bucket",
|
|
resourceName: "second-bucket",
|
|
}),
|
|
];
|
|
|
|
getFindingByIdMock.mockImplementation(async (findingId: string) => ({
|
|
data: [findingId],
|
|
}));
|
|
adaptFindingsByResourceResponseMock.mockImplementation(
|
|
(response: { data: string[] }) => [
|
|
makeDrawerFinding({
|
|
id: response.data[0],
|
|
resourceUid:
|
|
response.data[0] === "finding-1"
|
|
? "arn:aws:s3:::first-bucket"
|
|
: "arn:aws:s3:::second-bucket",
|
|
resourceName:
|
|
response.data[0] === "finding-1" ? "first-bucket" : "second-bucket",
|
|
}),
|
|
],
|
|
);
|
|
|
|
const { result } = renderHook(() =>
|
|
useResourceDetailDrawer({
|
|
resources,
|
|
}),
|
|
);
|
|
|
|
await act(async () => {
|
|
result.current.openDrawer(0);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
await act(async () => {
|
|
result.current.navigateNext();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(result.current.currentIndex).toBe(1);
|
|
expect(result.current.currentFinding?.id).toBe("finding-2");
|
|
|
|
act(() => {
|
|
result.current.navigatePrev();
|
|
});
|
|
|
|
expect(result.current.currentIndex).toBe(0);
|
|
expect(result.current.isNavigating).toBe(true);
|
|
|
|
await act(async () => {
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
vi.runAllTimers();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(result.current.isNavigating).toBe(false);
|
|
expect(result.current.currentFinding?.id).toBe("finding-1");
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("should keep isNavigating true for a fast uncached navigation long enough to avoid flicker", async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-04-08T15:00:00.000Z"));
|
|
|
|
const resources = [
|
|
makeResource({
|
|
id: "row-1",
|
|
findingId: "finding-1",
|
|
resourceUid: "arn:aws:s3:::first-bucket",
|
|
resourceName: "first-bucket",
|
|
}),
|
|
makeResource({
|
|
id: "row-2",
|
|
findingId: "finding-2",
|
|
resourceUid: "arn:aws:s3:::second-bucket",
|
|
resourceName: "second-bucket",
|
|
}),
|
|
];
|
|
|
|
getFindingByIdMock.mockImplementation(async (findingId: string) => ({
|
|
data: [findingId],
|
|
}));
|
|
adaptFindingsByResourceResponseMock.mockImplementation(
|
|
(response: { data: string[] }) => [
|
|
makeDrawerFinding({
|
|
id: response.data[0],
|
|
resourceUid:
|
|
response.data[0] === "finding-1"
|
|
? "arn:aws:s3:::first-bucket"
|
|
: "arn:aws:s3:::second-bucket",
|
|
resourceName:
|
|
response.data[0] === "finding-1" ? "first-bucket" : "second-bucket",
|
|
}),
|
|
],
|
|
);
|
|
|
|
const { result } = renderHook(() =>
|
|
useResourceDetailDrawer({
|
|
resources,
|
|
}),
|
|
);
|
|
|
|
await act(async () => {
|
|
result.current.openDrawer(0);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
act(() => {
|
|
result.current.navigateNext();
|
|
});
|
|
|
|
expect(result.current.currentIndex).toBe(1);
|
|
expect(result.current.isNavigating).toBe(true);
|
|
|
|
await act(async () => {
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(result.current.currentFinding?.id).toBe("finding-2");
|
|
expect(result.current.isNavigating).toBe(true);
|
|
|
|
await act(async () => {
|
|
vi.advanceTimersByTime(119);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(result.current.isNavigating).toBe(true);
|
|
|
|
await act(async () => {
|
|
vi.advanceTimersByTime(1);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
await act(async () => {
|
|
vi.runOnlyPendingTimers();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(result.current.isNavigating).toBe(false);
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("should update checkMeta when navigating to a resource with a different check", async () => {
|
|
// Given
|
|
const resources = [
|
|
makeResource({
|
|
id: "row-1",
|
|
findingId: "finding-1",
|
|
checkId: "s3_check",
|
|
}),
|
|
makeResource({
|
|
id: "row-2",
|
|
findingId: "finding-2",
|
|
checkId: "ec2_check",
|
|
resourceUid: "arn:aws:ec2:::instance/i-123",
|
|
resourceName: "instance-1",
|
|
service: "ec2",
|
|
}),
|
|
];
|
|
|
|
getFindingByIdMock.mockImplementation(async (findingId: string) => ({
|
|
data: [findingId],
|
|
}));
|
|
getLatestFindingsByResourceUidMock.mockResolvedValue({ data: [] });
|
|
adaptFindingsByResourceResponseMock.mockImplementation(
|
|
(response: { data: string[] }) => [
|
|
response.data[0] === "finding-1"
|
|
? makeDrawerFinding({
|
|
id: "finding-1",
|
|
checkId: "s3_check",
|
|
checkTitle: "S3 Check",
|
|
description: "s3 description",
|
|
})
|
|
: makeDrawerFinding({
|
|
id: "finding-2",
|
|
checkId: "ec2_check",
|
|
checkTitle: "EC2 Check",
|
|
description: "ec2 description",
|
|
}),
|
|
],
|
|
);
|
|
|
|
const { result } = renderHook(() =>
|
|
useResourceDetailDrawer({
|
|
resources,
|
|
}),
|
|
);
|
|
|
|
// When
|
|
await act(async () => {
|
|
result.current.openDrawer(0);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(result.current.checkMeta?.checkTitle).toBe("S3 Check");
|
|
|
|
await act(async () => {
|
|
result.current.navigateNext();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
// Then
|
|
expect(result.current.checkMeta?.checkTitle).toBe("EC2 Check");
|
|
expect(result.current.checkMeta?.description).toBe("ec2 description");
|
|
});
|
|
|
|
it("should keep the previous check metadata cached while reopening until the new finding arrives", async () => {
|
|
// Given
|
|
const resources = [
|
|
makeResource({
|
|
id: "row-1",
|
|
findingId: "finding-1",
|
|
checkId: "s3_check",
|
|
}),
|
|
makeResource({
|
|
id: "row-2",
|
|
findingId: "finding-2",
|
|
checkId: "ec2_check",
|
|
resourceUid: "arn:aws:ec2:::instance/i-123",
|
|
resourceName: "instance-1",
|
|
service: "ec2",
|
|
}),
|
|
];
|
|
|
|
let resolveSecondFinding: ((value: { data: string[] }) => void) | null =
|
|
null;
|
|
|
|
getFindingByIdMock.mockImplementation((findingId: string) => {
|
|
if (findingId === "finding-2") {
|
|
return new Promise((resolve) => {
|
|
resolveSecondFinding = resolve;
|
|
});
|
|
}
|
|
|
|
return Promise.resolve({ data: [findingId] });
|
|
});
|
|
getLatestFindingsByResourceUidMock.mockResolvedValue({ data: [] });
|
|
adaptFindingsByResourceResponseMock.mockImplementation(
|
|
(response: { data: string[] }) => [
|
|
response.data[0] === "finding-1"
|
|
? makeDrawerFinding({
|
|
id: "finding-1",
|
|
checkId: "s3_check",
|
|
checkTitle: "S3 Check",
|
|
description: "s3 description",
|
|
})
|
|
: makeDrawerFinding({
|
|
id: "finding-2",
|
|
checkId: "ec2_check",
|
|
checkTitle: "EC2 Check",
|
|
description: "ec2 description",
|
|
}),
|
|
],
|
|
);
|
|
|
|
const { result } = renderHook(() =>
|
|
useResourceDetailDrawer({
|
|
resources,
|
|
}),
|
|
);
|
|
|
|
await act(async () => {
|
|
result.current.openDrawer(0);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(result.current.checkMeta?.checkTitle).toBe("S3 Check");
|
|
|
|
// When
|
|
act(() => {
|
|
result.current.closeDrawer();
|
|
result.current.openDrawer(1);
|
|
});
|
|
|
|
// Then
|
|
expect(result.current.isOpen).toBe(true);
|
|
expect(result.current.currentIndex).toBe(1);
|
|
expect(result.current.currentFinding).toBeNull();
|
|
expect(result.current.checkMeta?.checkTitle).toBe("S3 Check");
|
|
|
|
await act(async () => {
|
|
resolveSecondFinding?.({ data: ["finding-2"] });
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(result.current.checkMeta?.checkTitle).toBe("EC2 Check");
|
|
expect(result.current.checkMeta?.description).toBe("ec2 description");
|
|
});
|
|
|
|
it("should clear the previous resource findings when navigation to the next resource fails", async () => {
|
|
// Given
|
|
const resources = [
|
|
makeResource({
|
|
id: "row-1",
|
|
findingId: "finding-1",
|
|
resourceUid: "arn:aws:s3:::first-bucket",
|
|
resourceName: "first-bucket",
|
|
}),
|
|
makeResource({
|
|
id: "row-2",
|
|
findingId: "finding-2",
|
|
resourceUid: "arn:aws:s3:::second-bucket",
|
|
resourceName: "second-bucket",
|
|
}),
|
|
];
|
|
|
|
getFindingByIdMock.mockImplementation(async (findingId: string) => {
|
|
if (findingId === "finding-2") {
|
|
throw new Error("Fetch failed");
|
|
}
|
|
|
|
return { data: [findingId] };
|
|
});
|
|
|
|
adaptFindingsByResourceResponseMock.mockImplementation(
|
|
(response: { data: string[] }) => [
|
|
makeDrawerFinding({
|
|
id: response.data[0],
|
|
resourceUid:
|
|
response.data[0] === "finding-1"
|
|
? "arn:aws:s3:::first-bucket"
|
|
: "arn:aws:s3:::second-bucket",
|
|
resourceName:
|
|
response.data[0] === "finding-1" ? "first-bucket" : "second-bucket",
|
|
}),
|
|
],
|
|
);
|
|
|
|
const { result } = renderHook(() =>
|
|
useResourceDetailDrawer({
|
|
resources,
|
|
}),
|
|
);
|
|
|
|
await act(async () => {
|
|
result.current.openDrawer(0);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(result.current.currentFinding?.resourceUid).toBe(
|
|
"arn:aws:s3:::first-bucket",
|
|
);
|
|
expect(result.current.checkMeta?.checkTitle).toBe("S3 Check");
|
|
|
|
// When
|
|
await act(async () => {
|
|
result.current.navigateNext();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
// Then
|
|
expect(result.current.currentIndex).toBe(1);
|
|
expect(result.current.currentFinding).toBeNull();
|
|
expect(result.current.otherFindings).toEqual([]);
|
|
expect(result.current.checkMeta).toBeNull();
|
|
});
|
|
|
|
it("should clear other findings immediately while the next resource is loading", async () => {
|
|
// Given
|
|
const resources = [
|
|
makeResource({
|
|
id: "row-1",
|
|
findingId: "finding-1",
|
|
resourceUid: "arn:aws:s3:::first-bucket",
|
|
resourceName: "first-bucket",
|
|
}),
|
|
makeResource({
|
|
id: "row-2",
|
|
findingId: "finding-2",
|
|
resourceUid: "arn:aws:s3:::second-bucket",
|
|
resourceName: "second-bucket",
|
|
}),
|
|
];
|
|
|
|
let resolveSecondFinding: ((value: { data: string[] }) => void) | null =
|
|
null;
|
|
let resolveSecondResource: ((value: { data: string[] }) => void) | null =
|
|
null;
|
|
|
|
getFindingByIdMock.mockImplementation((findingId: string) => {
|
|
if (findingId === "finding-2") {
|
|
return new Promise((resolve) => {
|
|
resolveSecondFinding = resolve;
|
|
});
|
|
}
|
|
|
|
return Promise.resolve({ data: [findingId] });
|
|
});
|
|
|
|
getLatestFindingsByResourceUidMock.mockImplementation(
|
|
({ resourceUid }: { resourceUid: string }) => {
|
|
if (resourceUid === "arn:aws:s3:::second-bucket") {
|
|
return new Promise((resolve) => {
|
|
resolveSecondResource = resolve;
|
|
});
|
|
}
|
|
|
|
return Promise.resolve({ data: ["resource-1"] });
|
|
},
|
|
);
|
|
|
|
adaptFindingsByResourceResponseMock.mockImplementation(
|
|
(response: { data: string[] }) => {
|
|
if (response.data[0] === "finding-1") {
|
|
return [makeDrawerFinding({ id: "finding-1" })];
|
|
}
|
|
|
|
if (response.data[0] === "finding-2") {
|
|
return [
|
|
makeDrawerFinding({
|
|
id: "finding-2",
|
|
resourceUid: "arn:aws:s3:::second-bucket",
|
|
resourceName: "second-bucket",
|
|
}),
|
|
];
|
|
}
|
|
|
|
if (response.data[0] === "resource-1") {
|
|
return [
|
|
makeDrawerFinding({
|
|
id: "finding-3",
|
|
checkTitle: "First bucket other finding",
|
|
resourceUid: "arn:aws:s3:::first-bucket",
|
|
}),
|
|
];
|
|
}
|
|
|
|
return [
|
|
makeDrawerFinding({
|
|
id: "finding-4",
|
|
checkTitle: "Second bucket other finding",
|
|
resourceUid: "arn:aws:s3:::second-bucket",
|
|
}),
|
|
];
|
|
},
|
|
);
|
|
|
|
const { result } = renderHook(() =>
|
|
useResourceDetailDrawer({
|
|
resources,
|
|
}),
|
|
);
|
|
|
|
await act(async () => {
|
|
result.current.openDrawer(0);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(result.current.otherFindings.map((finding) => finding.id)).toEqual([
|
|
"finding-3",
|
|
]);
|
|
|
|
// When
|
|
act(() => {
|
|
result.current.navigateNext();
|
|
});
|
|
|
|
// Then
|
|
expect(result.current.currentIndex).toBe(1);
|
|
expect(result.current.currentFinding).toBeNull();
|
|
expect(result.current.otherFindings).toEqual([]);
|
|
|
|
await act(async () => {
|
|
resolveSecondFinding?.({ data: ["finding-2"] });
|
|
resolveSecondResource?.({ data: ["resource-2"] });
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(result.current.otherFindings.map((finding) => finding.id)).toEqual([
|
|
"finding-4",
|
|
]);
|
|
});
|
|
|
|
it("should patch current and other finding triage locally without refetching", async () => {
|
|
const resources = [makeResource()];
|
|
|
|
// Given
|
|
getFindingByIdMock.mockResolvedValue({ data: ["detail"] });
|
|
getLatestFindingsByResourceUidMock.mockResolvedValue({
|
|
data: ["resource"],
|
|
});
|
|
adaptFindingsByResourceResponseMock.mockImplementation(
|
|
(response: { data: string[] }) =>
|
|
response.data[0] === "detail"
|
|
? [
|
|
makeDrawerFinding({
|
|
id: "finding-1",
|
|
triage: makeTriageSummary(),
|
|
}),
|
|
]
|
|
: [
|
|
makeDrawerFinding({
|
|
id: "finding-2",
|
|
uid: "uid-2",
|
|
triage: makeTriageSummary({
|
|
findingId: "finding-2",
|
|
findingUid: "uid-2",
|
|
status: FINDING_TRIAGE_STATUS.OPEN,
|
|
label: "Open",
|
|
}),
|
|
}),
|
|
],
|
|
);
|
|
|
|
const { result } = renderHook(() =>
|
|
useResourceDetailDrawer({
|
|
resources,
|
|
}),
|
|
);
|
|
|
|
await act(async () => {
|
|
result.current.openDrawer(0);
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const findingFetchCount = getFindingByIdMock.mock.calls.length;
|
|
const resourceFetchCount =
|
|
getLatestFindingsByResourceUidMock.mock.calls.length;
|
|
|
|
// When
|
|
act(() => {
|
|
result.current.patchTriageUpdate({
|
|
findingId: "finding-2",
|
|
findingUid: "uid-2",
|
|
triageId: "triage-2",
|
|
notesCount: 0,
|
|
status: FINDING_TRIAGE_STATUS.REMEDIATING,
|
|
previousStatus: FINDING_TRIAGE_STATUS.OPEN,
|
|
isMuted: false,
|
|
note: "Investigating",
|
|
});
|
|
});
|
|
|
|
// Then
|
|
expect(result.current.otherFindings[0]?.triage).toEqual(
|
|
expect.objectContaining({
|
|
status: FINDING_TRIAGE_STATUS.REMEDIATING,
|
|
label: "Remediating",
|
|
hasVisibleNote: true,
|
|
notesCount: 1,
|
|
}),
|
|
);
|
|
expect(result.current.currentFinding?.triage?.status).toBe(
|
|
FINDING_TRIAGE_STATUS.UNDER_REVIEW,
|
|
);
|
|
expect(getFindingByIdMock).toHaveBeenCalledTimes(findingFetchCount);
|
|
expect(getLatestFindingsByResourceUidMock).toHaveBeenCalledTimes(
|
|
resourceFetchCount,
|
|
);
|
|
});
|
|
});
|