feat(ui): forward provider filters in compliance server actions

- getCompliancesOverview and getComplianceRequirements forward provider
  filters and drop scan_id when present (backend XOR)
- getComplianceRequirements now accepts an optional scanId

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Pablo F.G
2026-06-22 10:11:25 +02:00
parent 7fd281cf02
commit eab9eebacf
2 changed files with 119 additions and 4 deletions
+100
View File
@@ -0,0 +1,100 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const { fetchMock, getAuthHeadersMock, handleApiResponseMock } = vi.hoisted(
() => ({
fetchMock: vi.fn(),
getAuthHeadersMock: vi.fn(),
handleApiResponseMock: vi.fn(),
}),
);
vi.mock("@/lib", () => ({
apiBaseUrl: "https://api.example.com/api/v1",
getAuthHeaders: getAuthHeadersMock,
}));
vi.mock("@/lib/server-actions-helper", () => ({
handleApiResponse: handleApiResponseMock,
}));
import {
getComplianceOverviewMetadataInfo,
getComplianceRequirements,
getCompliancesOverview,
} from "./compliances";
const calledUrl = () => new URL(fetchMock.mock.calls[0][0] as string);
beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal("fetch", fetchMock);
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
fetchMock.mockResolvedValue(new Response(null, { status: 200 }));
handleApiResponseMock.mockResolvedValue({ data: [] });
});
describe("getCompliancesOverview", () => {
it("sends scan_id and region in scan mode", async () => {
await getCompliancesOverview({ scanId: "scan-1", region: "eu-west-1" });
const url = calledUrl();
expect(url.searchParams.get("filter[scan_id]")).toBe("scan-1");
expect(url.searchParams.get("filter[region__in]")).toBe("eu-west-1");
});
it("forwards provider filters and omits scan_id in aggregated mode", async () => {
await getCompliancesOverview({
scanId: "scan-1",
filters: { "filter[provider_type__in]": "aws,gcp" },
});
const url = calledUrl();
expect(url.searchParams.get("filter[provider_type__in]")).toBe("aws,gcp");
// XOR: provider filters present -> never send scan_id (avoids backend 400)
expect(url.searchParams.get("filter[scan_id]")).toBeNull();
});
});
describe("getComplianceOverviewMetadataInfo", () => {
it("forwards provider filters", async () => {
await getComplianceOverviewMetadataInfo({
filters: { "filter[provider_groups__in]": "g1,g2" },
});
expect(calledUrl().searchParams.get("filter[provider_groups__in]")).toBe(
"g1,g2",
);
});
});
describe("getComplianceRequirements", () => {
it("appends compliance_id and scan_id in scan mode", async () => {
await getComplianceRequirements({
complianceId: "cis_2.0_aws",
scanId: "scan-1",
});
const url = calledUrl();
expect(url.searchParams.get("filter[compliance_id]")).toBe("cis_2.0_aws");
expect(url.searchParams.get("filter[scan_id]")).toBe("scan-1");
});
it("forwards provider filters and omits scan_id in aggregated mode", async () => {
await getComplianceRequirements({
complianceId: "cis_2.0_aws",
scanId: "scan-1",
filters: { "filter[provider_id__in]": "p1,p2" },
});
const url = calledUrl();
expect(url.searchParams.get("filter[compliance_id]")).toBe("cis_2.0_aws");
expect(url.searchParams.get("filter[provider_id__in]")).toBe("p1,p2");
expect(url.searchParams.get("filter[scan_id]")).toBeNull();
});
it("omits scan_id when no scan is provided", async () => {
await getComplianceRequirements({ complianceId: "cis_2.0_aws" });
expect(calledUrl().searchParams.get("filter[scan_id]")).toBeNull();
});
});
+19 -4
View File
@@ -1,6 +1,7 @@
"use server";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { hasComplianceProviderFilters } from "@/lib/compliance/compliance-provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
export const getCompliancesOverview = async ({
@@ -27,7 +28,10 @@ export const getCompliancesOverview = async ({
Object.entries(filters).forEach(([key, value]) => setParam(key, value));
setParam("filter[scan_id]", scanId);
// XOR: the backend rejects filter[scan_id] together with provider filters.
if (!hasComplianceProviderFilters(filters)) {
setParam("filter[scan_id]", scanId);
}
setParam("filter[region__in]", region);
try {
const response = await fetch(url.toString(), {
@@ -111,22 +115,33 @@ export const getComplianceRequirements = async ({
complianceId,
scanId,
region,
filters = {},
}: {
complianceId: string;
scanId: string;
scanId?: string;
region?: string | string[];
filters?: Record<string, string | string[] | undefined>;
}) => {
const headers = await getAuthHeaders({ contentType: false });
try {
const url = new URL(`${apiBaseUrl}/compliance-overviews/requirements`);
url.searchParams.append("filter[compliance_id]", complianceId);
url.searchParams.append("filter[scan_id]", scanId);
// Forward provider-scope filters (aggregated mode); XOR with scan_id.
Object.entries(filters).forEach(([key, value]) => {
if (value === undefined) return;
const serialized = Array.isArray(value) ? value.join(",") : value;
if (serialized.trim() !== "") url.searchParams.append(key, serialized);
});
if (scanId && !hasComplianceProviderFilters(filters)) {
url.searchParams.append("filter[scan_id]", scanId);
}
if (region) {
const regionValue = Array.isArray(region) ? region.join(",") : region;
url.searchParams.append("filter[region__in]", regionValue);
//remove page param
}
url.searchParams.delete("page");