Files
Alan Buscaglia 587187419f feat(ui): add findings triage (#11704)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2026-07-01 17:55:33 +02:00

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();
});
});