fix(ui): guard Overview Lighthouse context against API error responses (#12284)

This commit is contained in:
Alejandro Bailo
2026-08-04 13:37:47 +02:00
committed by GitHub
parent 90905dcc9f
commit f0d2972969
8 changed files with 153 additions and 7 deletions
@@ -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();
});
});