mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 20:42:02 +00:00
fix(ui): address compliance review feedback
This commit is contained in:
@@ -28,7 +28,12 @@ vi.mock("@sentry/nextjs", () => ({
|
||||
captureException: captureExceptionMock,
|
||||
}));
|
||||
|
||||
import { getCrossAccountComplianceOverview } from "./cross-account";
|
||||
import {
|
||||
generateCrossAccountPdf,
|
||||
getCrossAccountComplianceOverview,
|
||||
getCrossAccountPdfBinary,
|
||||
getLatestCrossAccountPdf,
|
||||
} from "./cross-account";
|
||||
|
||||
const jsonResponse = (body: unknown, status = 200) =>
|
||||
new Response(JSON.stringify(body), {
|
||||
@@ -42,6 +47,15 @@ const lastFetchUrl = () => {
|
||||
return new URL(String(call[0]));
|
||||
};
|
||||
|
||||
const fetchCallAt = (index: number) => {
|
||||
const call = fetchMock.mock.calls[index];
|
||||
if (!call) throw new Error(`fetch call ${index} was not found`);
|
||||
return {
|
||||
init: call[1] as RequestInit,
|
||||
url: new URL(String(call[0])),
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
@@ -81,6 +95,73 @@ describe("cross-account compliance actions", () => {
|
||||
expect(url.searchParams.get("filter[provider_groups__in]")).toBe("group-1");
|
||||
});
|
||||
|
||||
it("routes PDF operations through the cross-account endpoints", async () => {
|
||||
fetchMock
|
||||
.mockResolvedValueOnce(
|
||||
jsonResponse({ data: { type: "tasks", id: "task-1" } }, 202),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
new Response(Buffer.from("pdf-bytes"), {
|
||||
headers: {
|
||||
"Content-Disposition": 'attachment; filename="report.pdf"',
|
||||
"Content-Type": "application/pdf",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
jsonResponse({
|
||||
data: {
|
||||
id: "task-2",
|
||||
attributes: { result: { filename: "latest.pdf" } },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await generateCrossAccountPdf({
|
||||
complianceId: "cis_2.0_aws",
|
||||
providerType: "aws",
|
||||
filters: { scanIds: ["scan-1"] },
|
||||
reportName: "report.pdf",
|
||||
});
|
||||
await getCrossAccountPdfBinary("task-1");
|
||||
await getLatestCrossAccountPdf({
|
||||
complianceId: "cis_2.0_aws",
|
||||
providerType: "aws",
|
||||
filters: { providerIds: "provider-1" },
|
||||
});
|
||||
|
||||
const generation = fetchCallAt(0);
|
||||
expect(generation.init.method).toBe("POST");
|
||||
expect(generation.url.pathname).toBe(
|
||||
"/api/v1/cross-account-compliance-overviews/pdf",
|
||||
);
|
||||
expect(generation.url.searchParams.get("filter[compliance_id]")).toBe(
|
||||
"cis_2.0_aws",
|
||||
);
|
||||
expect(generation.url.searchParams.get("filter[provider_type]")).toBe(
|
||||
"aws",
|
||||
);
|
||||
expect(generation.url.searchParams.get("filter[scan__in]")).toBe("scan-1");
|
||||
expect(generation.url.searchParams.get("report_name")).toBe("report.pdf");
|
||||
|
||||
const binary = fetchCallAt(1);
|
||||
expect(binary.url.pathname).toBe(
|
||||
"/api/v1/cross-account-compliance-overviews/pdf/task-1",
|
||||
);
|
||||
|
||||
const latest = fetchCallAt(2);
|
||||
expect(latest.url.pathname).toBe(
|
||||
"/api/v1/cross-account-compliance-overviews/pdf/latest",
|
||||
);
|
||||
expect(latest.url.searchParams.get("filter[provider_id__in]")).toBe(
|
||||
"provider-1",
|
||||
);
|
||||
|
||||
expect([generation, binary, latest].every(({ init }) => init.signal)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("aborts a stalled request and reports the network failure", async () => {
|
||||
vi.useFakeTimers();
|
||||
let requestSignal: AbortSignal | undefined;
|
||||
|
||||
@@ -77,7 +77,7 @@ export const AggregatedComplianceDetail = ({
|
||||
<ClientAccordionWrapper
|
||||
items={accordionItems}
|
||||
defaultExpandedKeys={initialExpandedKeys}
|
||||
scrollToKey={initialExpandedKeys[0]}
|
||||
scrollToKey={initialExpandedKeys.at(-1)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -40,6 +40,7 @@ describe("ProviderCoverageCard", () => {
|
||||
expect(screen.getByTestId("coverage-row-aws")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("coverage-row-gcp")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("No completed scan")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("8/10 · 1 manual")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows an empty state when no provider has a scan", () => {
|
||||
|
||||
@@ -44,7 +44,7 @@ describe("RequirementAccountChips", () => {
|
||||
render(<RequirementAccountChips accounts={accounts} accountMeta={meta} />);
|
||||
|
||||
const summary = screen.getByRole("button", {
|
||||
name: "Show status breakdown for 13 providers",
|
||||
name: "Show status breakdown for 13 accounts",
|
||||
});
|
||||
expect(summary).toHaveTextContent("Fail×2");
|
||||
expect(summary).toHaveTextContent("Manual×1");
|
||||
|
||||
@@ -40,6 +40,7 @@ export const RequirementAccountChips = ({
|
||||
if (entries.length > MAX_INLINE_ACCOUNT_CHIPS) {
|
||||
return (
|
||||
<RequirementStatusSummary
|
||||
entityLabel="accounts"
|
||||
entries={entries.map((account) => ({
|
||||
key: account.id,
|
||||
label: accountDisplayLabel(account),
|
||||
|
||||
@@ -39,8 +39,10 @@ const SCROLLABLE_BREAKDOWN_MIN_ROWS = 13;
|
||||
*/
|
||||
export const RequirementStatusSummary = ({
|
||||
entries,
|
||||
entityLabel = "providers",
|
||||
}: {
|
||||
entries: RequirementStatusEntry[];
|
||||
entityLabel?: "accounts" | "providers";
|
||||
}) => {
|
||||
const counts = STATUS_ORDER.map((status) => ({
|
||||
status,
|
||||
@@ -74,7 +76,7 @@ export const RequirementStatusSummary = ({
|
||||
type="button"
|
||||
variant="bare"
|
||||
size="link-xs"
|
||||
aria-label={`Show status breakdown for ${entries.length} providers`}
|
||||
aria-label={`Show status breakdown for ${entries.length} ${entityLabel}`}
|
||||
data-testid="requirement-status-summary"
|
||||
>
|
||||
{counts.map(({ status, count }) => (
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { AccordionItemProps } from "@/components/shadcn/accordion/Accordion";
|
||||
import type { Framework } from "@/types/compliance";
|
||||
|
||||
import { getAggregatedInitialExpandedKeys } from "../aggregated-compliance-detail";
|
||||
|
||||
describe("getAggregatedInitialExpandedKeys", () => {
|
||||
it("returns the ancestor path for a nested section deep link", () => {
|
||||
const data = [
|
||||
{ name: "Operational" },
|
||||
{ name: "Organizational" },
|
||||
] as Framework[];
|
||||
const accordionItems: AccordionItemProps[] = [
|
||||
{
|
||||
key: "Operational",
|
||||
title: null,
|
||||
content: null,
|
||||
items: [
|
||||
{
|
||||
key: "Operational-Access control",
|
||||
title: null,
|
||||
content: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
expect(
|
||||
getAggregatedInitialExpandedKeys(data, accordionItems, "Access control"),
|
||||
).toEqual(["Operational", "Operational-Access control"]);
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,21 @@ export const getAggregatedInitialExpandedKeys = (
|
||||
const candidates = new Set(
|
||||
data.map((framework) => `${framework.name}-${targetSection}`),
|
||||
);
|
||||
const match = accordionItems.find((item) => candidates.has(item.key));
|
||||
return match ? [match.key] : [];
|
||||
|
||||
const findExpandedPath = (
|
||||
items: AccordionItemProps[],
|
||||
ancestors: string[] = [],
|
||||
): string[] | undefined => {
|
||||
for (const item of items) {
|
||||
const path = [...ancestors, item.key];
|
||||
if (candidates.has(item.key)) return path;
|
||||
|
||||
const nestedMatch = findExpandedPath(item.items ?? [], path);
|
||||
if (nestedMatch) return nestedMatch;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return findExpandedPath(accordionItems) ?? [];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user