mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-24 12:24:17 +00:00
Co-authored-by: Pablo F.G <pablo.fernandez@prowler.com>
257 lines
7.9 KiB
TypeScript
257 lines
7.9 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
SCHEDULE_FREQUENCY,
|
|
type ScheduleUpdatePayload,
|
|
} from "@/types/schedules";
|
|
|
|
const {
|
|
fetchMock,
|
|
getAuthHeadersMock,
|
|
handleApiErrorMock,
|
|
handleApiResponseMock,
|
|
revalidatePathMock,
|
|
} = vi.hoisted(() => ({
|
|
fetchMock: vi.fn(),
|
|
getAuthHeadersMock: vi.fn(),
|
|
handleApiErrorMock: vi.fn(),
|
|
handleApiResponseMock: vi.fn(),
|
|
revalidatePathMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("next/cache", () => ({
|
|
revalidatePath: revalidatePathMock,
|
|
}));
|
|
|
|
vi.mock("@/lib", () => ({
|
|
apiBaseUrl: "https://api.example.com/api/v1",
|
|
getAuthHeaders: getAuthHeadersMock,
|
|
}));
|
|
|
|
vi.mock("@/lib/server-actions-helper", () => ({
|
|
handleApiError: handleApiErrorMock,
|
|
handleApiResponse: handleApiResponseMock,
|
|
}));
|
|
|
|
import {
|
|
getSchedule,
|
|
getSchedulesPage,
|
|
removeSchedule,
|
|
updateSchedule,
|
|
updateSchedulesBulk,
|
|
} from "./schedules";
|
|
|
|
const PROVIDER_ID = "1795f636-37e6-42f6-b158-d4faaa64e0fc";
|
|
const SECOND_PROVIDER_ID = "9b7fae7d-5e72-49fe-8b0b-5bff5930db1a";
|
|
|
|
const payload = {
|
|
scan_enabled: true,
|
|
scan_frequency: SCHEDULE_FREQUENCY.DAILY,
|
|
scan_hour: 12,
|
|
scan_timezone: "Europe/Madrid",
|
|
scan_interval_hours: null,
|
|
scan_day_of_week: null,
|
|
scan_day_of_month: null,
|
|
};
|
|
|
|
describe("schedule write actions revalidate only on success", () => {
|
|
beforeEach(() => {
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
|
|
fetchMock.mockResolvedValue(new Response(null, { status: 204 }));
|
|
handleApiErrorMock.mockReturnValue({ error: "Failed" });
|
|
});
|
|
|
|
it("revalidates /scans and /providers after a successful update", async () => {
|
|
handleApiResponseMock.mockResolvedValue({ success: true });
|
|
|
|
await updateSchedule(PROVIDER_ID, payload);
|
|
|
|
expect(revalidatePathMock).toHaveBeenCalledWith("/scans");
|
|
expect(revalidatePathMock).toHaveBeenCalledWith("/providers");
|
|
});
|
|
|
|
it("posts the JSON:API bulk schedule payload", async () => {
|
|
handleApiResponseMock.mockResolvedValue({
|
|
data: { updated: [PROVIDER_ID, SECOND_PROVIDER_ID], failed: [] },
|
|
});
|
|
|
|
await updateSchedulesBulk([PROVIDER_ID, SECOND_PROVIDER_ID], payload);
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
"https://api.example.com/api/v1/schedules/bulk",
|
|
expect.objectContaining({
|
|
method: "POST",
|
|
headers: { Authorization: "Bearer token" },
|
|
body: JSON.stringify({
|
|
data: {
|
|
type: "schedules-bulk",
|
|
attributes: {
|
|
schedule: payload,
|
|
provider_ids: [PROVIDER_ID, SECOND_PROVIDER_ID],
|
|
},
|
|
},
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("rejects invalid bulk provider ids without issuing a request", async () => {
|
|
expect(
|
|
await updateSchedulesBulk([PROVIDER_ID, "../users/me"], payload),
|
|
).toEqual({
|
|
error: "Invalid provider ids.",
|
|
});
|
|
expect(await updateSchedulesBulk([], payload)).toEqual({
|
|
error: "Invalid provider ids.",
|
|
});
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects invalid bulk schedule payloads without issuing a request", async () => {
|
|
const invalidPayload = {
|
|
...payload,
|
|
scan_hour: "12",
|
|
} as unknown as ScheduleUpdatePayload;
|
|
|
|
expect(
|
|
await updateSchedulesBulk(
|
|
[PROVIDER_ID, SECOND_PROVIDER_ID],
|
|
invalidPayload,
|
|
),
|
|
).toEqual({
|
|
error: "Invalid schedule payload.",
|
|
});
|
|
expect(getAuthHeadersMock).not.toHaveBeenCalled();
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("normalizes bulk auth header errors", async () => {
|
|
const authError = new Error("Session expired");
|
|
getAuthHeadersMock.mockRejectedValue(authError);
|
|
|
|
expect(
|
|
await updateSchedulesBulk([PROVIDER_ID, SECOND_PROVIDER_ID], payload),
|
|
).toEqual({ error: "Failed" });
|
|
expect(handleApiErrorMock).toHaveBeenCalledWith(authError);
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("revalidates /scans and /providers after a partial bulk success", async () => {
|
|
handleApiResponseMock.mockResolvedValue({
|
|
data: {
|
|
updated: [PROVIDER_ID],
|
|
failed: [{ id: SECOND_PROVIDER_ID, error: "Denied" }],
|
|
},
|
|
});
|
|
|
|
const result = await updateSchedulesBulk(
|
|
[PROVIDER_ID, SECOND_PROVIDER_ID],
|
|
payload,
|
|
);
|
|
|
|
expect(result.data?.updated).toEqual([PROVIDER_ID]);
|
|
expect(result.data?.failed).toHaveLength(1);
|
|
expect(revalidatePathMock).toHaveBeenCalledWith("/scans");
|
|
expect(revalidatePathMock).toHaveBeenCalledWith("/providers");
|
|
});
|
|
|
|
it("does not revalidate when the bulk update returns an error result", async () => {
|
|
handleApiResponseMock.mockResolvedValue({ error: "Bulk rejected" });
|
|
|
|
await updateSchedulesBulk([PROVIDER_ID, SECOND_PROVIDER_ID], payload);
|
|
|
|
expect(revalidatePathMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not revalidate when the update returns an error result", async () => {
|
|
handleApiResponseMock.mockResolvedValue({ error: "Schedule rejected" });
|
|
|
|
await updateSchedule(PROVIDER_ID, payload);
|
|
|
|
expect(revalidatePathMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("revalidates /scans and /providers after a successful delete", async () => {
|
|
handleApiResponseMock.mockResolvedValue({ success: true });
|
|
|
|
await removeSchedule(PROVIDER_ID);
|
|
|
|
expect(revalidatePathMock).toHaveBeenCalledWith("/scans");
|
|
expect(revalidatePathMock).toHaveBeenCalledWith("/providers");
|
|
});
|
|
|
|
it("does not revalidate when the delete returns an error result", async () => {
|
|
handleApiResponseMock.mockResolvedValue({ error: "Not allowed" });
|
|
|
|
await removeSchedule(PROVIDER_ID);
|
|
|
|
expect(revalidatePathMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects non-UUID provider ids without issuing a request", async () => {
|
|
const malicious = "../users/me";
|
|
|
|
expect(await getSchedule(malicious)).toEqual({
|
|
error: "Invalid provider id.",
|
|
});
|
|
expect(await updateSchedule(malicious, payload)).toEqual({
|
|
error: "Invalid provider id.",
|
|
});
|
|
expect(await removeSchedule(malicious)).toEqual({
|
|
error: "Invalid provider id.",
|
|
});
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("getSchedulesPage delegates pagination to the endpoint", () => {
|
|
beforeEach(() => {
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
|
|
fetchMock.mockResolvedValue(new Response(null, { status: 200 }));
|
|
handleApiErrorMock.mockReturnValue({ error: "Failed" });
|
|
});
|
|
|
|
it("requests only configured schedules with native pagination and include", async () => {
|
|
handleApiResponseMock.mockResolvedValue({
|
|
data: [],
|
|
included: [],
|
|
meta: { pagination: { page: 2, pages: 4, count: 35 } },
|
|
});
|
|
|
|
const result = await getSchedulesPage({
|
|
page: 2,
|
|
pageSize: 10,
|
|
sort: "next_scan_at",
|
|
filters: { "filter[provider_type__in]": "aws,azure" },
|
|
});
|
|
|
|
const calledUrl = new URL(fetchMock.mock.calls[0][0] as string);
|
|
expect(calledUrl.pathname).toBe("/api/v1/schedules");
|
|
expect(calledUrl.searchParams.get("filter[configured]")).toBe("true");
|
|
expect(calledUrl.searchParams.get("include")).toBe("provider");
|
|
expect(calledUrl.searchParams.get("page[number]")).toBe("2");
|
|
expect(calledUrl.searchParams.get("page[size]")).toBe("10");
|
|
expect(calledUrl.searchParams.get("sort")).toBe("next_scan_at");
|
|
expect(calledUrl.searchParams.get("filter[provider_type__in]")).toBe(
|
|
"aws,azure",
|
|
);
|
|
// meta is propagated verbatim so the table paginates natively.
|
|
expect(result?.meta?.pagination?.count).toBe(35);
|
|
});
|
|
|
|
it("normalizes array filter values into a CSV param", async () => {
|
|
handleApiResponseMock.mockResolvedValue({ data: [], meta: {} });
|
|
|
|
await getSchedulesPage({
|
|
filters: { "filter[provider__in]": ["id-1", "id-2"] },
|
|
});
|
|
|
|
const calledUrl = new URL(fetchMock.mock.calls[0][0] as string);
|
|
expect(calledUrl.searchParams.get("filter[provider__in]")).toBe(
|
|
"id-1,id-2",
|
|
);
|
|
});
|
|
});
|