Files
prowler/ui/proxy.test.ts
César Arroba ea310b1c68 feat(ui): full maintenance mode landing page and gate (cloud-only)
- Add maintenance mode landing page and view component
- Gate maintenance status check and redirect behind isCloud()
- Add changelog fragment
2026-07-08 13:23:08 +02:00

105 lines
3.3 KiB
TypeScript

import type { NextFetchEvent } from "next/server";
import { NextRequest } from "next/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
const {
authHandlerSpy,
authMock,
fetchMaintenanceStatusMock,
maintenanceResponseMock,
} = vi.hoisted(() => ({
authHandlerSpy: vi.fn(),
// Minimal stand-in for next-auth's `auth()` wrapper: it takes the route
// handler and returns a function with the (req, ctx) signature `proxy()`
// calls `authProxy` with. The real handler reads `req.auth`, which a
// plain NextRequest doesn't have, so `user`/`sessionError` resolve to
// `undefined` here — irrelevant to what this suite asserts (the MM gate
// itself), it only needs to observe whether authProxy ran.
authMock: vi.fn(
(handler: (req: unknown) => unknown) => (req: unknown, _ctx: unknown) => {
authHandlerSpy(req);
return handler(req);
},
),
fetchMaintenanceStatusMock: vi.fn(),
maintenanceResponseMock: vi.fn(),
}));
vi.mock("@/auth.config", () => ({
auth: authMock,
}));
vi.mock("@/lib", () => ({
apiBaseUrl: "http://api:8000/api/v1",
}));
vi.mock("@/lib/maintenance", () => ({
fetchMaintenanceStatus: fetchMaintenanceStatusMock,
maintenanceResponse: maintenanceResponseMock,
MAINTENANCE_PATH: "/maintenance",
}));
import proxy from "./proxy";
const makeRequest = (path: string) =>
new NextRequest(new URL(`https://app.prowler.com${path}`));
describe("proxy() Maintenance Mode gate (isCloud gating)", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('runs the maintenance status check when NEXT_PUBLIC_IS_CLOUD_ENV is "true"', async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
fetchMaintenanceStatusMock.mockResolvedValueOnce({
enabled: false,
message: null,
started_at: null,
});
maintenanceResponseMock.mockReturnValueOnce(null);
// When
const response = await proxy(
makeRequest("/scans") as any,
{} as NextFetchEvent,
);
// Then: the gate ran (and, since it returned null here, fell through to
// the auth-wrapped handler, which redirects unauthenticated visitors).
expect(fetchMaintenanceStatusMock).toHaveBeenCalledTimes(1);
expect(maintenanceResponseMock).toHaveBeenCalledTimes(1);
expect(authHandlerSpy).toHaveBeenCalledTimes(1);
expect(response.status).toBe(307);
});
it('is a no-op in self-hosted (NEXT_PUBLIC_IS_CLOUD_ENV is not "true"): the status fetch is never made and the request falls straight through to auth', async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
// When
const response = await proxy(
makeRequest("/scans") as any,
{} as NextFetchEvent,
);
// Then
expect(fetchMaintenanceStatusMock).not.toHaveBeenCalled();
expect(maintenanceResponseMock).not.toHaveBeenCalled();
expect(authHandlerSpy).toHaveBeenCalledTimes(1);
expect(response.status).toBe(307);
});
it("is also a no-op when NEXT_PUBLIC_IS_CLOUD_ENV is unset", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", undefined);
// When
await proxy(makeRequest("/scans") as any, {} as NextFetchEvent);
// Then
expect(fetchMaintenanceStatusMock).not.toHaveBeenCalled();
expect(maintenanceResponseMock).not.toHaveBeenCalled();
});
});