mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
94ee24071a
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
586 lines
20 KiB
TypeScript
586 lines
20 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Hoisted mocks (must be declared before any imports that need them)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const { fetchMock, getAuthHeadersMock, handleApiResponseMock } = vi.hoisted(
|
|
() => ({
|
|
fetchMock: vi.fn(),
|
|
getAuthHeadersMock: vi.fn(),
|
|
handleApiResponseMock: vi.fn(),
|
|
}),
|
|
);
|
|
|
|
// Real helpers/constants pulled from submodules that don't import server-only
|
|
// code, so the mock factory stays free of top-level variable hoisting issues
|
|
// and the vitest runtime doesn't choke on next-auth's `next/server` import.
|
|
import {
|
|
includesMutedFindings,
|
|
splitCsvFilterValues,
|
|
} from "@/lib/findings-filters";
|
|
import {
|
|
composeSort,
|
|
FG_FAIL_FIRST,
|
|
FG_RECENT_LAST_SEEN,
|
|
FG_SEVERITY_HIGH_FIRST,
|
|
FINDING_GROUP_RESOURCES_DEFAULT_SORT,
|
|
} from "@/lib/findings-sort";
|
|
|
|
vi.mock("@/lib", () => ({
|
|
apiBaseUrl: "https://api.example.com/api/v1",
|
|
getAuthHeaders: getAuthHeadersMock,
|
|
composeSort,
|
|
FG_FAIL_FIRST,
|
|
FG_RECENT_LAST_SEEN,
|
|
FG_SEVERITY_HIGH_FIRST,
|
|
FINDING_GROUP_RESOURCES_DEFAULT_SORT,
|
|
includesMutedFindings,
|
|
splitCsvFilterValues,
|
|
}));
|
|
|
|
vi.mock("@/lib/provider-filters", () => ({
|
|
// Simulate real appendSanitizedProviderFilters: appends all non-undefined filters to the URL.
|
|
appendSanitizedProviderFilters: vi.fn(
|
|
(url: URL, filters: Record<string, string | string[] | undefined>) => {
|
|
Object.entries(filters).forEach(([key, value]) => {
|
|
if (value !== undefined) {
|
|
url.searchParams.append(key, String(value));
|
|
}
|
|
});
|
|
},
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/lib/server-actions-helper", () => ({
|
|
handleApiResponse: handleApiResponseMock,
|
|
}));
|
|
|
|
vi.mock("next/navigation", () => ({
|
|
redirect: vi.fn(),
|
|
}));
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Imports (after vi.mock declarations)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
import {
|
|
getFindingGroupResources,
|
|
getFindingGroups,
|
|
getLatestFindingGroupResources,
|
|
getLatestFindingGroups,
|
|
} from "./finding-groups";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("getFindingGroups — default sort for muted and non-muted rows", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
|
|
handleApiResponseMock.mockResolvedValue({ data: [] });
|
|
fetchMock.mockResolvedValue(new Response("", { status: 200 }));
|
|
});
|
|
|
|
it("should prefer non-muted fail counters when include muted is not active", async () => {
|
|
// When
|
|
await getFindingGroups();
|
|
|
|
// Then
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
expect(url.searchParams.get("sort")).toBe(
|
|
"-status,-severity,-new_fail_count,-changed_fail_count,-fail_count,-last_seen_at",
|
|
);
|
|
});
|
|
|
|
it("should include muted counters when filter[muted]=include is active", async () => {
|
|
// When
|
|
await getFindingGroups({
|
|
filters: { "filter[muted]": "include" },
|
|
});
|
|
|
|
// Then
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
expect(url.searchParams.get("sort")).toBe(
|
|
"-status,-severity,-new_fail_count,-changed_fail_count,-new_fail_muted_count,-changed_fail_muted_count,-fail_count,-fail_muted_count,-last_seen_at",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("getLatestFindingGroups — default sort for muted and non-muted rows", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
|
|
handleApiResponseMock.mockResolvedValue({ data: [] });
|
|
fetchMock.mockResolvedValue(new Response("", { status: 200 }));
|
|
});
|
|
|
|
it("should prefer non-muted fail counters when include muted is not active", async () => {
|
|
// When
|
|
await getLatestFindingGroups();
|
|
|
|
// Then
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
expect(url.searchParams.get("sort")).toBe(
|
|
"-status,-severity,-new_fail_count,-changed_fail_count,-fail_count,-last_seen_at",
|
|
);
|
|
});
|
|
|
|
it("should include muted counters when filter[muted]=include is active", async () => {
|
|
// When
|
|
await getLatestFindingGroups({
|
|
filters: { "filter[muted]": "include" },
|
|
});
|
|
|
|
// Then
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
expect(url.searchParams.get("sort")).toBe(
|
|
"-status,-severity,-new_fail_count,-changed_fail_count,-new_fail_muted_count,-changed_fail_muted_count,-fail_count,-fail_muted_count,-last_seen_at",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("getFindingGroupResources — SSRF path traversal protection", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
|
|
handleApiResponseMock.mockResolvedValue({ data: [] });
|
|
fetchMock.mockResolvedValue(new Response("", { status: 200 }));
|
|
});
|
|
|
|
it("should encode a normal checkId without alteration", async () => {
|
|
// Given
|
|
const checkId = "s3_bucket_public_access";
|
|
|
|
// When
|
|
await getFindingGroupResources({ checkId });
|
|
|
|
// Then — URL path must contain encoded checkId, not raw
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
expect(calledUrl).toContain(
|
|
`/api/v1/finding-groups/${encodeURIComponent(checkId)}/resources`,
|
|
);
|
|
});
|
|
|
|
it("should encode a checkId containing a forward slash (path traversal attempt)", async () => {
|
|
// Given — checkId with embedded slash: attacker attempts path traversal
|
|
const maliciousCheckId = "../../admin/secret";
|
|
|
|
// When
|
|
await getFindingGroupResources({ checkId: maliciousCheckId });
|
|
|
|
// Then — the URL must NOT contain a raw slash from the checkId
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
// The path should NOT end in /resources with traversal segments between
|
|
expect(url.pathname).not.toContain("/admin/secret/resources");
|
|
// The encoded checkId must appear in the path
|
|
expect(url.pathname).toContain(
|
|
`/finding-groups/${encodeURIComponent(maliciousCheckId)}/resources`,
|
|
);
|
|
});
|
|
|
|
it("should encode a checkId containing %2F (URL-encoded slash traversal attempt)", async () => {
|
|
// Given — checkId with %2F: double-encoding traversal attempt
|
|
const maliciousCheckId = "foo%2Fbar";
|
|
|
|
// When
|
|
await getFindingGroupResources({ checkId: maliciousCheckId });
|
|
|
|
// Then
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
expect(url.pathname).toContain(
|
|
`/finding-groups/${encodeURIComponent(maliciousCheckId)}/resources`,
|
|
);
|
|
expect(url.pathname).not.toContain("/foo/bar/resources");
|
|
});
|
|
|
|
it("should encode a checkId containing special chars like ? and #", async () => {
|
|
// Given
|
|
const maliciousCheckId = "check?admin=true#fragment";
|
|
|
|
// When
|
|
await getFindingGroupResources({ checkId: maliciousCheckId });
|
|
|
|
// Then
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
expect(calledUrl).not.toContain("?admin=true");
|
|
expect(calledUrl.split("?")[0]).toContain(
|
|
`/finding-groups/${encodeURIComponent(maliciousCheckId)}/resources`,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("getLatestFindingGroupResources — SSRF path traversal protection", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
|
|
handleApiResponseMock.mockResolvedValue({ data: [] });
|
|
fetchMock.mockResolvedValue(new Response("", { status: 200 }));
|
|
});
|
|
|
|
it("should encode a normal checkId without alteration", async () => {
|
|
// Given
|
|
const checkId = "iam_user_mfa_enabled";
|
|
|
|
// When
|
|
await getLatestFindingGroupResources({ checkId });
|
|
|
|
// Then
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
expect(calledUrl).toContain(
|
|
`/api/v1/finding-groups/latest/${encodeURIComponent(checkId)}/resources`,
|
|
);
|
|
});
|
|
|
|
it("should encode a checkId containing a forward slash in the latest endpoint", async () => {
|
|
// Given
|
|
const maliciousCheckId = "../other-endpoint";
|
|
|
|
// When
|
|
await getLatestFindingGroupResources({ checkId: maliciousCheckId });
|
|
|
|
// Then
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
expect(url.pathname).not.toContain("/other-endpoint/resources");
|
|
expect(url.pathname).toContain(
|
|
`/finding-groups/latest/${encodeURIComponent(maliciousCheckId)}/resources`,
|
|
);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Resources list keeps FAIL-first sort but no longer forces FAIL-only filtering
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("getFindingGroupResources — Blocker 1: FAIL-first sort", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
|
|
handleApiResponseMock.mockResolvedValue({ data: [] });
|
|
fetchMock.mockResolvedValue(new Response("", { status: 200 }));
|
|
});
|
|
|
|
it("should include the composite sort so FAIL resources appear first, then severity", async () => {
|
|
// Given
|
|
const checkId = "s3_bucket_public_access";
|
|
|
|
// When
|
|
await getFindingGroupResources({ checkId });
|
|
|
|
// Then — the URL must contain the composite sort
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
expect(url.searchParams.get("sort")).toBe(
|
|
"-status,-severity,-delta,-last_seen_at",
|
|
);
|
|
});
|
|
|
|
it("should not force filter[status]=FAIL so PASS resources can also be shown", async () => {
|
|
// Given
|
|
const checkId = "s3_bucket_public_access";
|
|
|
|
// When
|
|
await getFindingGroupResources({ checkId });
|
|
|
|
// Then — the URL should not add a hardcoded status filter
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
expect(url.searchParams.get("filter[status]")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("getLatestFindingGroupResources — Blocker 1: FAIL-first sort", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
|
|
handleApiResponseMock.mockResolvedValue({ data: [] });
|
|
fetchMock.mockResolvedValue(new Response("", { status: 200 }));
|
|
});
|
|
|
|
it("should include the composite sort so FAIL resources appear first, then severity", async () => {
|
|
// Given
|
|
const checkId = "iam_user_mfa_enabled";
|
|
|
|
// When
|
|
await getLatestFindingGroupResources({ checkId });
|
|
|
|
// Then
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
expect(url.searchParams.get("sort")).toBe(
|
|
"-status,-severity,-delta,-last_seen_at",
|
|
);
|
|
});
|
|
|
|
it("should not force filter[status]=FAIL so PASS resources can also be shown", async () => {
|
|
// Given
|
|
const checkId = "iam_user_mfa_enabled";
|
|
|
|
// When
|
|
await getLatestFindingGroupResources({ checkId });
|
|
|
|
// Then
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
expect(url.searchParams.get("filter[status]")).toBeNull();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Triangulation: sort + filter coexist with pagination and caller filters
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("getFindingGroupResources — triangulation: params coexist", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
|
|
handleApiResponseMock.mockResolvedValue({ data: [] });
|
|
fetchMock.mockResolvedValue(new Response("", { status: 200 }));
|
|
});
|
|
|
|
it("should send the composite sort alongside pagination params without forcing filter[status]", async () => {
|
|
// Given
|
|
const checkId = "s3_bucket_versioning";
|
|
|
|
// When
|
|
await getFindingGroupResources({ checkId, page: 2, pageSize: 50 });
|
|
|
|
// Then — all four params present together
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
expect(url.searchParams.get("page[number]")).toBe("2");
|
|
expect(url.searchParams.get("page[size]")).toBe("50");
|
|
expect(url.searchParams.get("sort")).toBe(
|
|
"-status,-severity,-delta,-last_seen_at",
|
|
);
|
|
expect(url.searchParams.get("filter[status]")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("getLatestFindingGroupResources — triangulation: params coexist", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
|
|
handleApiResponseMock.mockResolvedValue({ data: [] });
|
|
fetchMock.mockResolvedValue(new Response("", { status: 200 }));
|
|
});
|
|
|
|
it("should send the composite sort alongside pagination params without forcing filter[status]", async () => {
|
|
// Given
|
|
const checkId = "iam_root_mfa_enabled";
|
|
|
|
// When
|
|
await getLatestFindingGroupResources({ checkId, page: 3, pageSize: 20 });
|
|
|
|
// Then
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
expect(url.searchParams.get("page[number]")).toBe("3");
|
|
expect(url.searchParams.get("page[size]")).toBe("20");
|
|
expect(url.searchParams.get("sort")).toBe(
|
|
"-status,-severity,-delta,-last_seen_at",
|
|
);
|
|
expect(url.searchParams.get("filter[status]")).toBeNull();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Caller filters should propagate unchanged to the drill-down resources endpoint
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("getFindingGroupResources — caller filters are preserved", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
|
|
handleApiResponseMock.mockResolvedValue({ data: [] });
|
|
fetchMock.mockResolvedValue(new Response("", { status: 200 }));
|
|
});
|
|
|
|
it("should preserve caller filter[status] when explicitly provided", async () => {
|
|
// Given
|
|
const checkId = "s3_bucket_public_access";
|
|
const filters = { "filter[status]": "PASS" };
|
|
|
|
// When
|
|
await getFindingGroupResources({ checkId, filters });
|
|
|
|
// Then
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
const allStatusValues = url.searchParams.getAll("filter[status]");
|
|
expect(allStatusValues).toHaveLength(1);
|
|
expect(allStatusValues[0]).toBe("PASS");
|
|
});
|
|
|
|
it("should translate a single group status__in filter into filter[status] for resources", async () => {
|
|
// Given
|
|
const checkId = "s3_bucket_public_access";
|
|
const filters = {
|
|
"filter[status__in]": "PASS",
|
|
"filter[severity__in]": "medium",
|
|
"filter[provider_type__in]": "aws",
|
|
};
|
|
|
|
// When
|
|
await getFindingGroupResources({ checkId, filters });
|
|
|
|
// Then
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
expect(url.searchParams.get("filter[status]")).toBe("PASS");
|
|
expect(url.searchParams.get("filter[status__in]")).toBeNull();
|
|
expect(url.searchParams.get("filter[severity__in]")).toBe("medium");
|
|
expect(url.searchParams.get("filter[provider_type__in]")).toBe("aws");
|
|
});
|
|
|
|
it("should keep the composite sort when the resource search filter is applied", async () => {
|
|
// Given
|
|
const checkId = "s3_bucket_public_access";
|
|
const filters = {
|
|
"filter[name__icontains]": "bucket-prod",
|
|
"filter[severity__in]": "high",
|
|
};
|
|
|
|
// When
|
|
await getFindingGroupResources({ checkId, filters });
|
|
|
|
// Then
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
expect(url.searchParams.get("sort")).toBe(
|
|
"-status,-severity,-delta,-last_seen_at",
|
|
);
|
|
expect(url.searchParams.get("filter[name__icontains]")).toBe("bucket-prod");
|
|
expect(url.searchParams.get("filter[severity__in]")).toBe("high");
|
|
});
|
|
|
|
it("should strip scan filters that the group resources endpoint does not accept", async () => {
|
|
// Given
|
|
const checkId = "s3_bucket_public_access";
|
|
const filters = {
|
|
"filter[scan__in]": "scan-1",
|
|
"filter[scan_id]": "scan-1",
|
|
"filter[scan_id__in]": "scan-1,scan-2",
|
|
"filter[region__in]": "eu-west-1",
|
|
};
|
|
|
|
// When
|
|
await getFindingGroupResources({ checkId, filters });
|
|
|
|
// Then
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
expect(url.searchParams.get("filter[scan__in]")).toBeNull();
|
|
expect(url.searchParams.get("filter[scan_id]")).toBeNull();
|
|
expect(url.searchParams.get("filter[scan_id__in]")).toBeNull();
|
|
expect(url.searchParams.get("filter[region__in]")).toBe("eu-west-1");
|
|
});
|
|
});
|
|
|
|
describe("getLatestFindingGroupResources — caller filters are preserved", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
|
|
handleApiResponseMock.mockResolvedValue({ data: [] });
|
|
fetchMock.mockResolvedValue(new Response("", { status: 200 }));
|
|
});
|
|
|
|
it("should preserve caller filter[status] when explicitly provided", async () => {
|
|
// Given
|
|
const checkId = "iam_user_mfa_enabled";
|
|
const filters = { "filter[status]": "PASS" };
|
|
|
|
// When
|
|
await getLatestFindingGroupResources({ checkId, filters });
|
|
|
|
// Then
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
const allStatusValues = url.searchParams.getAll("filter[status]");
|
|
expect(allStatusValues).toHaveLength(1);
|
|
expect(allStatusValues[0]).toBe("PASS");
|
|
});
|
|
|
|
it("should translate a single group status__in filter into filter[status] for latest resources", async () => {
|
|
// Given
|
|
const checkId = "iam_user_mfa_enabled";
|
|
const filters = {
|
|
"filter[status__in]": "PASS",
|
|
"filter[severity__in]": "low",
|
|
"filter[provider_type__in]": "aws",
|
|
};
|
|
|
|
// When
|
|
await getLatestFindingGroupResources({ checkId, filters });
|
|
|
|
// Then
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
expect(url.searchParams.get("filter[status]")).toBe("PASS");
|
|
expect(url.searchParams.get("filter[status__in]")).toBeNull();
|
|
expect(url.searchParams.get("filter[severity__in]")).toBe("low");
|
|
expect(url.searchParams.get("filter[provider_type__in]")).toBe("aws");
|
|
});
|
|
|
|
it("should keep the composite sort when the resource search filter is applied", async () => {
|
|
// Given
|
|
const checkId = "iam_user_mfa_enabled";
|
|
const filters = {
|
|
"filter[name__icontains]": "instance-prod",
|
|
"filter[status__in]": "PASS,FAIL",
|
|
};
|
|
|
|
// When
|
|
await getLatestFindingGroupResources({ checkId, filters });
|
|
|
|
// Then
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
expect(url.searchParams.get("sort")).toBe(
|
|
"-status,-severity,-delta,-last_seen_at",
|
|
);
|
|
expect(url.searchParams.get("filter[name__icontains]")).toBe(
|
|
"instance-prod",
|
|
);
|
|
expect(url.searchParams.get("filter[status__in]")).toBe("PASS,FAIL");
|
|
});
|
|
|
|
it("should strip scan filters before calling latest group resources", async () => {
|
|
// Given
|
|
const checkId = "iam_user_mfa_enabled";
|
|
const filters = {
|
|
"filter[scan__in]": "scan-1",
|
|
"filter[scan_id]": "scan-1",
|
|
"filter[region__in]": "us-east-1",
|
|
};
|
|
|
|
// When
|
|
await getLatestFindingGroupResources({ checkId, filters });
|
|
|
|
// Then
|
|
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
const url = new URL(calledUrl);
|
|
expect(url.searchParams.get("filter[scan__in]")).toBeNull();
|
|
expect(url.searchParams.get("filter[scan_id]")).toBeNull();
|
|
expect(url.searchParams.get("filter[region__in]")).toBe("us-east-1");
|
|
});
|
|
});
|