mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
587187419f
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
210 lines
6.0 KiB
TypeScript
210 lines
6.0 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const { fetchMock, getAuthHeadersMock, handleApiResponseMock } = vi.hoisted(
|
|
() => ({
|
|
fetchMock: vi.fn(),
|
|
getAuthHeadersMock: vi.fn(),
|
|
handleApiResponseMock: vi.fn(),
|
|
}),
|
|
);
|
|
|
|
// Pull every constant transitively required by the modules under test
|
|
// (resources.ts → findings action → finding-groups action) so the `@/lib`
|
|
// mock is a complete surface. Going via the barrel would drag in next-auth.
|
|
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,
|
|
FINDINGS_FILTERED_SORT,
|
|
RESOURCE_DRAWER_OTHER_FINDINGS_SORT,
|
|
} from "@/lib/findings-sort";
|
|
|
|
vi.mock("@/lib", () => ({
|
|
apiBaseUrl: "https://api.example.com/api/v1",
|
|
getAuthHeaders: getAuthHeadersMock,
|
|
GENERIC_SERVER_ERROR_MESSAGE:
|
|
"Server is temporarily unavailable. Please try again in a few minutes.",
|
|
sanitizeErrorMessage: (message: string, fallback: string) =>
|
|
/<html\b|<\/?body\b|<\/?h1\b/i.test(message) ? fallback : message.trim(),
|
|
composeSort,
|
|
FG_FAIL_FIRST,
|
|
FG_RECENT_LAST_SEEN,
|
|
FG_SEVERITY_HIGH_FIRST,
|
|
FINDING_GROUP_RESOURCES_DEFAULT_SORT,
|
|
FINDINGS_FILTERED_SORT,
|
|
RESOURCE_DRAWER_OTHER_FINDINGS_SORT,
|
|
includesMutedFindings,
|
|
splitCsvFilterValues,
|
|
}));
|
|
|
|
vi.mock("@/lib/server-actions-helper", () => ({
|
|
handleApiResponse: handleApiResponseMock,
|
|
}));
|
|
|
|
vi.mock("@/actions/findings", () => ({
|
|
getLatestFindings: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/actions/organizations/organizations", () => ({
|
|
listOrganizationsSafe: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/lib/provider-filters", () => ({
|
|
appendSanitizedProviderTypeFilters: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("next/navigation", () => ({
|
|
redirect: vi.fn(),
|
|
}));
|
|
|
|
import { getResourceEvents } from "./resources";
|
|
|
|
describe("getResourceEvents", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
|
|
});
|
|
|
|
it("calls the correct API endpoint with default parameters", async () => {
|
|
// Given
|
|
const mockResponse = new Response("", { status: 200 });
|
|
fetchMock.mockResolvedValue(mockResponse);
|
|
handleApiResponseMock.mockResolvedValue({ data: [] });
|
|
|
|
// When
|
|
await getResourceEvents("resource-123");
|
|
|
|
// Then
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
const calledUrl = new URL(fetchMock.mock.calls[0][0]);
|
|
expect(calledUrl.pathname).toBe("/api/v1/resources/resource-123/events");
|
|
expect(calledUrl.searchParams.get("include_read_events")).toBe("false");
|
|
expect(calledUrl.searchParams.get("lookback_days")).toBe("90");
|
|
expect(calledUrl.searchParams.get("page[size]")).toBe("50");
|
|
});
|
|
|
|
it("passes custom parameters to the API", async () => {
|
|
// Given
|
|
const mockResponse = new Response("", { status: 200 });
|
|
fetchMock.mockResolvedValue(mockResponse);
|
|
handleApiResponseMock.mockResolvedValue({ data: [] });
|
|
|
|
// When
|
|
await getResourceEvents("resource-456", {
|
|
includeReadEvents: true,
|
|
lookbackDays: 30,
|
|
pageSize: 25,
|
|
});
|
|
|
|
// Then
|
|
const calledUrl = new URL(fetchMock.mock.calls[0][0]);
|
|
expect(calledUrl.searchParams.get("include_read_events")).toBe("true");
|
|
expect(calledUrl.searchParams.get("lookback_days")).toBe("30");
|
|
expect(calledUrl.searchParams.get("page[size]")).toBe("25");
|
|
});
|
|
|
|
it("returns error object for non-ok responses without calling handleApiResponse", async () => {
|
|
// Given
|
|
const errorBody = JSON.stringify({
|
|
errors: [
|
|
{
|
|
detail:
|
|
"Provider credentials are invalid or expired. Please reconnect the provider.",
|
|
},
|
|
],
|
|
});
|
|
const mockResponse = new Response(errorBody, {
|
|
status: 502,
|
|
statusText: "Bad Gateway",
|
|
});
|
|
fetchMock.mockResolvedValue(mockResponse);
|
|
|
|
// When
|
|
const result = await getResourceEvents("resource-123");
|
|
|
|
// Then
|
|
expect(result).toEqual({
|
|
error:
|
|
"Provider credentials are invalid or expired. Please reconnect the provider.",
|
|
status: 502,
|
|
});
|
|
expect(handleApiResponseMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("returns error with statusText when response body is not JSON", async () => {
|
|
// Given
|
|
const mockResponse = new Response("Service Unavailable", {
|
|
status: 503,
|
|
statusText: "Service Unavailable",
|
|
});
|
|
fetchMock.mockResolvedValue(mockResponse);
|
|
|
|
// When
|
|
const result = await getResourceEvents("resource-123");
|
|
|
|
// Then
|
|
expect(result).toEqual({
|
|
error: "Service Unavailable",
|
|
status: 503,
|
|
});
|
|
});
|
|
|
|
it("returns a generic error when a gateway returns HTML", async () => {
|
|
// Given
|
|
const mockResponse = new Response(
|
|
"<html><head><title>502 Bad Gateway</title></head><body><h1>502 Bad Gateway</h1></body></html>",
|
|
{
|
|
status: 502,
|
|
statusText: "Bad Gateway",
|
|
headers: { "content-type": "text/html" },
|
|
},
|
|
);
|
|
fetchMock.mockResolvedValue(mockResponse);
|
|
|
|
// When
|
|
const result = await getResourceEvents("resource-123");
|
|
|
|
// Then
|
|
expect(result).toEqual({
|
|
error:
|
|
"Server is temporarily unavailable. Please try again in a few minutes.",
|
|
status: 502,
|
|
});
|
|
});
|
|
|
|
it("returns generic error when fetch throws", async () => {
|
|
// Given
|
|
fetchMock.mockRejectedValue(new Error("Network failure"));
|
|
|
|
// When
|
|
const result = await getResourceEvents("resource-123");
|
|
|
|
// Then
|
|
expect(result).toEqual({ error: "An unexpected error occurred." });
|
|
});
|
|
|
|
it.each([
|
|
"../../../etc/passwd",
|
|
"resource/../../secret",
|
|
"id with spaces",
|
|
"id;rm -rf /",
|
|
"<script>alert(1)</script>",
|
|
"resource%00id",
|
|
"",
|
|
])("rejects malicious or invalid resourceId: %s", async (maliciousId) => {
|
|
// When
|
|
const result = await getResourceEvents(maliciousId);
|
|
|
|
// Then
|
|
expect(result).toEqual({ error: "Invalid resource ID format." });
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
});
|
|
});
|