mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-19 09:30:21 +00:00
fix(ui): guard Overview Lighthouse context against API error responses (#12284)
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { adaptComplianceWatchlistResponse } from "./compliance-watchlist.adapter";
|
||||
import type { ComplianceWatchlistResponse } from "./compliance-watchlist.types";
|
||||
|
||||
describe("adaptComplianceWatchlistResponse", () => {
|
||||
it("returns no items on a 4xx error shape", () => {
|
||||
// handleApiResponse resolves truthy {error, status} objects for 4xx.
|
||||
const errorResponse = {
|
||||
error: "Invalid filter",
|
||||
status: 400,
|
||||
} as unknown as ComplianceWatchlistResponse;
|
||||
|
||||
expect(adaptComplianceWatchlistResponse(errorResponse)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns no items on an empty-body success shape", () => {
|
||||
// handleApiResponse resolves {success, status} for 204 and empty bodies.
|
||||
const emptyResponse = {
|
||||
success: true,
|
||||
status: 204,
|
||||
} as unknown as ComplianceWatchlistResponse;
|
||||
|
||||
expect(adaptComplianceWatchlistResponse(emptyResponse)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns no items when the fetch failed with undefined", () => {
|
||||
expect(adaptComplianceWatchlistResponse(undefined)).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { getFindingsByStatus } from "@/actions/overview";
|
||||
|
||||
import { CheckFindingsSSR } from "./check-findings.ssr";
|
||||
|
||||
vi.mock("@/actions/overview", () => ({
|
||||
@@ -33,4 +35,34 @@ describe("CheckFindingsSSR", () => {
|
||||
expect(context).toHaveTextContent('"newPassed":12');
|
||||
expect(context).toHaveTextContent('"newFailed":7');
|
||||
});
|
||||
|
||||
it("renders the error state and publishes no context on a 4xx response", async () => {
|
||||
// handleApiResponse resolves truthy {error, status} objects for 4xx.
|
||||
vi.mocked(getFindingsByStatus).mockResolvedValueOnce({
|
||||
error: "Invalid filter",
|
||||
status: 400,
|
||||
});
|
||||
|
||||
render(await CheckFindingsSSR({ searchParams: {} }));
|
||||
|
||||
expect(
|
||||
screen.getByText("Failed to load findings data"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("status-context")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the error state and publishes no context on an empty body", async () => {
|
||||
// handleApiResponse resolves {success, status} for 204 and empty bodies.
|
||||
vi.mocked(getFindingsByStatus).mockResolvedValueOnce({
|
||||
success: true,
|
||||
status: 204,
|
||||
});
|
||||
|
||||
render(await CheckFindingsSSR({ searchParams: {} }));
|
||||
|
||||
expect(
|
||||
screen.getByText("Failed to load findings data"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("status-context")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,9 @@ export const CheckFindingsSSR = async ({ searchParams }: SSRComponentProps) => {
|
||||
|
||||
const findingsByStatus = await getFindingsByStatus({ filters });
|
||||
|
||||
if (!findingsByStatus) {
|
||||
// handleApiResponse resolves truthy on 4xx ({error, status}) and empty
|
||||
// bodies ({success, status}), so only a payload with attributes is data.
|
||||
if (!findingsByStatus?.data?.attributes) {
|
||||
return (
|
||||
<div className="flex h-[400px] w-full max-w-md items-center justify-center rounded-xl border border-zinc-900 bg-stone-950">
|
||||
<p className="text-zinc-400">Failed to load findings data</p>
|
||||
@@ -19,9 +21,12 @@ export const CheckFindingsSSR = async ({ searchParams }: SSRComponentProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
const attributes = findingsByStatus?.data?.attributes || {};
|
||||
|
||||
const { fail = 0, pass = 0, fail_new = 0, pass_new = 0 } = attributes;
|
||||
const {
|
||||
fail = 0,
|
||||
pass = 0,
|
||||
fail_new = 0,
|
||||
pass_new = 0,
|
||||
} = findingsByStatus.data.attributes;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { getFindingsBySeverity } from "@/actions/overview";
|
||||
|
||||
import { RiskSeverityChartSSR } from "./risk-severity-chart.ssr";
|
||||
|
||||
vi.mock("@/actions/overview", () => ({
|
||||
@@ -38,4 +40,34 @@ describe("RiskSeverityChartSSR", () => {
|
||||
'"severityCounts":{"critical":4,"high":18,"medium":40,"low":15,"informational":3}',
|
||||
);
|
||||
});
|
||||
|
||||
it("renders the error state and publishes no context on a 4xx response", async () => {
|
||||
// handleApiResponse resolves truthy {error, status} objects for 4xx.
|
||||
vi.mocked(getFindingsBySeverity).mockResolvedValueOnce({
|
||||
error: "Invalid filter",
|
||||
status: 400,
|
||||
} as unknown as Awaited<ReturnType<typeof getFindingsBySeverity>>);
|
||||
|
||||
render(await RiskSeverityChartSSR({ searchParams: {} }));
|
||||
|
||||
expect(
|
||||
screen.getByText("Failed to load severity data"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("severity-context")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the error state and publishes no context on an empty body", async () => {
|
||||
// handleApiResponse resolves {success, status} for 204 and empty bodies.
|
||||
vi.mocked(getFindingsBySeverity).mockResolvedValueOnce({
|
||||
success: true,
|
||||
status: 204,
|
||||
} as unknown as Awaited<ReturnType<typeof getFindingsBySeverity>>);
|
||||
|
||||
render(await RiskSeverityChartSSR({ searchParams: {} }));
|
||||
|
||||
expect(
|
||||
screen.getByText("Failed to load severity data"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("severity-context")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,9 @@ export const RiskSeverityChartSSR = async ({
|
||||
|
||||
const findingsBySeverity = await getFindingsBySeverity({ filters });
|
||||
|
||||
if (!findingsBySeverity) {
|
||||
// handleApiResponse resolves truthy on 4xx ({error, status}) and empty
|
||||
// bodies ({success, status}), so only a payload with attributes is data.
|
||||
if (!findingsBySeverity?.data?.attributes) {
|
||||
return (
|
||||
<div className="flex h-[400px] w-full items-center justify-center rounded-xl border border-zinc-900 bg-stone-950">
|
||||
<p className="text-zinc-400">Failed to load severity data</p>
|
||||
@@ -30,7 +32,7 @@ export const RiskSeverityChartSSR = async ({
|
||||
medium = 0,
|
||||
low = 0,
|
||||
informational = 0,
|
||||
} = findingsBySeverity?.data?.attributes || {};
|
||||
} = findingsBySeverity.data.attributes;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -104,4 +104,36 @@ describe("ThreatScoreSSR", () => {
|
||||
'"totals":{"passed":120,"failed":40,"total":160}',
|
||||
);
|
||||
});
|
||||
|
||||
it("renders the empty state and publishes no context on a 4xx response", async () => {
|
||||
// handleApiResponse resolves truthy {error, status} objects for 4xx.
|
||||
vi.mocked(getThreatScore).mockResolvedValueOnce({
|
||||
error: "Invalid filter",
|
||||
status: 400,
|
||||
} as unknown as Awaited<ReturnType<typeof getThreatScore>>);
|
||||
|
||||
render(await ThreatScoreSSR({ searchParams: {} }));
|
||||
|
||||
expect(screen.queryByTestId("overview-context")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("publishes a zero critical count when the field is absent", async () => {
|
||||
vi.mocked(getThreatScore).mockResolvedValueOnce({
|
||||
data: [
|
||||
{
|
||||
attributes: {
|
||||
overall_score: "70",
|
||||
score_delta: null,
|
||||
section_scores: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as Awaited<ReturnType<typeof getThreatScore>>);
|
||||
|
||||
render(await ThreatScoreSSR({ searchParams: {} }));
|
||||
|
||||
expect(screen.getByTestId("overview-context")).toHaveTextContent(
|
||||
'"criticalRequirementsCount":0',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,7 +43,8 @@ export const ThreatScoreSSR = async ({ searchParams }: SSRComponentProps) => {
|
||||
framework: "Prowler ThreatScore",
|
||||
score,
|
||||
scoreDelta: scoreDelta ?? undefined,
|
||||
criticalRequirementsCount: attributes.critical_requirements.length,
|
||||
criticalRequirementsCount:
|
||||
attributes.critical_requirements?.length ?? 0,
|
||||
worstSection: worstSectionEntry?.[0],
|
||||
worstSectionScore: worstSectionEntry?.[1],
|
||||
passed: attributes.passed_requirements,
|
||||
|
||||
@@ -58,4 +58,16 @@ describe("ServiceWatchlistSSR", () => {
|
||||
|
||||
expect(screen.queryByTestId("service-context")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("publishes no service context on a 4xx response", async () => {
|
||||
// handleApiResponse resolves truthy {error, status} objects for 4xx.
|
||||
vi.mocked(getServicesOverview).mockResolvedValueOnce({
|
||||
error: "Invalid filter",
|
||||
status: 400,
|
||||
} as unknown as Awaited<ReturnType<typeof getServicesOverview>>);
|
||||
|
||||
render(await ServiceWatchlistSSR({ searchParams: {} }));
|
||||
|
||||
expect(screen.queryByTestId("service-context")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user