mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
Revert "fix(ui): stream scan report downloads (#11330)"
This reverts commit 6cffd0d17f.
This commit is contained in:
@@ -1,103 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { GET } from "./route";
|
||||
|
||||
const { getAuthHeadersMock } = vi.hoisted(() => ({
|
||||
getAuthHeadersMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib", () => ({
|
||||
apiBaseUrl: "https://api.example.com/api/v1",
|
||||
getAuthHeaders: getAuthHeadersMock,
|
||||
}));
|
||||
|
||||
describe("GET /api/scans/[scanId]/report", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("streams the upstream report body without buffering it", async () => {
|
||||
const upstreamBody = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new Uint8Array([1, 2, 3]));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(upstreamBody, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/zip",
|
||||
"content-length": "3",
|
||||
},
|
||||
}),
|
||||
);
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
|
||||
|
||||
const response = await GET(new Request("http://localhost/api"), {
|
||||
params: Promise.resolve({ scanId: "scan-123" }),
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"https://api.example.com/api/v1/scans/scan-123/report",
|
||||
expect.objectContaining({
|
||||
headers: { Authorization: "Bearer token" },
|
||||
cache: "no-store",
|
||||
}),
|
||||
);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toBe("application/zip");
|
||||
expect(response.headers.get("content-length")).toBe("3");
|
||||
expect(response.headers.get("content-disposition")).toBe(
|
||||
'attachment; filename="scan-scan-123-report.zip"',
|
||||
);
|
||||
expect(response.body).toBe(upstreamBody);
|
||||
});
|
||||
|
||||
it("checks report readiness without streaming ready report bytes", async () => {
|
||||
const cancelMock = vi.fn();
|
||||
const upstreamBody = new ReadableStream({
|
||||
cancel: cancelMock,
|
||||
start(controller) {
|
||||
controller.enqueue(new Uint8Array([1, 2, 3]));
|
||||
},
|
||||
});
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue(new Response(upstreamBody, { status: 200 })),
|
||||
);
|
||||
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
|
||||
|
||||
const response = await GET(
|
||||
new Request("http://localhost/api?preflight=1"),
|
||||
{
|
||||
params: Promise.resolve({ scanId: "scan-123" }),
|
||||
},
|
||||
);
|
||||
|
||||
expect(response.status).toBe(204);
|
||||
expect(response.body).toBeNull();
|
||||
expect(cancelMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("preserves pending report responses from the API", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
Response.json({ data: { id: "task-1" } }, { status: 202 }),
|
||||
),
|
||||
);
|
||||
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
|
||||
|
||||
const response = await GET(new Request("http://localhost/api"), {
|
||||
params: Promise.resolve({ scanId: "scan-123" }),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(202);
|
||||
await expect(response.json()).resolves.toEqual({ data: { id: "task-1" } });
|
||||
});
|
||||
});
|
||||
@@ -1,98 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const runtime = "nodejs";
|
||||
|
||||
interface ScanReportRouteContext {
|
||||
params: Promise<{
|
||||
scanId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const COPY_RESPONSE_HEADERS = [
|
||||
"content-length",
|
||||
"content-type",
|
||||
"etag",
|
||||
"last-modified",
|
||||
] as const;
|
||||
|
||||
const buildAttachmentFilename = (scanId: string) =>
|
||||
`scan-${scanId.replace(/[^a-zA-Z0-9._-]/g, "-")}-report.zip`;
|
||||
|
||||
const buildDownloadHeaders = (upstreamHeaders: Headers, scanId: string) => {
|
||||
const headers = new Headers({
|
||||
"Cache-Control": "no-store",
|
||||
"Content-Disposition": `attachment; filename="${buildAttachmentFilename(scanId)}"`,
|
||||
});
|
||||
|
||||
COPY_RESPONSE_HEADERS.forEach((headerName) => {
|
||||
const value = upstreamHeaders.get(headerName);
|
||||
if (value) headers.set(headerName, value);
|
||||
});
|
||||
|
||||
if (!headers.has("content-type")) {
|
||||
headers.set("content-type", "application/zip");
|
||||
}
|
||||
|
||||
return headers;
|
||||
};
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: ScanReportRouteContext,
|
||||
) {
|
||||
const { scanId } = await params;
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
const upstreamUrl = `${apiBaseUrl}/scans/${encodeURIComponent(scanId)}/report`;
|
||||
const isPreflight =
|
||||
new URL(request.url).searchParams.get("preflight") === "1";
|
||||
|
||||
const upstreamResponse = await fetch(upstreamUrl, {
|
||||
headers,
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (upstreamResponse.status === 202) {
|
||||
const body = await upstreamResponse.json().catch(() => ({}));
|
||||
return NextResponse.json(body, {
|
||||
status: 202,
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
});
|
||||
}
|
||||
|
||||
if (!upstreamResponse.ok) {
|
||||
const body = await upstreamResponse.text().catch(() => "");
|
||||
return new Response(body, {
|
||||
status: upstreamResponse.status,
|
||||
statusText: upstreamResponse.statusText,
|
||||
headers: {
|
||||
"Cache-Control": "no-store",
|
||||
"Content-Type":
|
||||
upstreamResponse.headers.get("content-type") || "text/plain",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (isPreflight) {
|
||||
await upstreamResponse.body?.cancel();
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
});
|
||||
}
|
||||
|
||||
if (!upstreamResponse.body) {
|
||||
return NextResponse.json(
|
||||
{ error: "Report response did not include a readable body." },
|
||||
{ status: 502, headers: { "Cache-Control": "no-store" } },
|
||||
);
|
||||
}
|
||||
|
||||
return new Response(upstreamResponse.body, {
|
||||
status: upstreamResponse.status,
|
||||
statusText: upstreamResponse.statusText,
|
||||
headers: buildDownloadHeaders(upstreamResponse.headers, scanId),
|
||||
});
|
||||
}
|
||||
@@ -30,9 +30,7 @@ describe("resource detail content", () => {
|
||||
it("renders the external resource link below the resource title row", () => {
|
||||
expect(source).toContain(`</div>
|
||||
<ExternalResourceLink`);
|
||||
expect(source).toMatch(
|
||||
/className="(?:self-start justify-start|justify-start self-start)"/,
|
||||
);
|
||||
expect(source).toContain('className="self-start justify-start"');
|
||||
});
|
||||
|
||||
it("keeps resource date fields together on the third details row", () => {
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { downloadScanZip } from "./helper";
|
||||
|
||||
vi.mock("@/actions/scans", () => ({
|
||||
getComplianceCsv: vi.fn(),
|
||||
getCompliancePdfReport: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/actions/task", () => ({
|
||||
getTask: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/auth.config", () => ({
|
||||
auth: vi.fn(),
|
||||
}));
|
||||
|
||||
const createToast = () => vi.fn();
|
||||
|
||||
const getAnchor = () => {
|
||||
const anchor = document.createElement("a");
|
||||
const clickMock = vi.spyOn(anchor, "click").mockImplementation(() => {});
|
||||
vi.spyOn(document, "createElement").mockReturnValue(anchor);
|
||||
return { anchor, clickMock };
|
||||
};
|
||||
|
||||
describe("downloadScanZip", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
document.body.replaceChildren();
|
||||
});
|
||||
|
||||
it("preflights the report and starts a browser-native download when ready", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue(new Response(null, { status: 204 })),
|
||||
);
|
||||
const { anchor, clickMock } = getAnchor();
|
||||
const toast = createToast();
|
||||
|
||||
await downloadScanZip("scan-123", toast);
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"/api/scans/scan-123/report?preflight=1",
|
||||
{
|
||||
cache: "no-store",
|
||||
},
|
||||
);
|
||||
expect(anchor.href).toContain("/api/scans/scan-123/report");
|
||||
expect(anchor.download).toBe("scan-scan-123-report.zip");
|
||||
expect(clickMock).toHaveBeenCalledTimes(1);
|
||||
expect(toast).toHaveBeenCalledWith({
|
||||
title: "Download Started",
|
||||
description: "Your browser is downloading the scan report.",
|
||||
});
|
||||
});
|
||||
|
||||
it("shows the pending report message without starting a download", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue(new Response("{}", { status: 202 })),
|
||||
);
|
||||
const { clickMock } = getAnchor();
|
||||
const toast = createToast();
|
||||
|
||||
await downloadScanZip("scan-123", toast);
|
||||
|
||||
expect(clickMock).not.toHaveBeenCalled();
|
||||
expect(toast).toHaveBeenCalledWith({
|
||||
title: "The report is still being generated",
|
||||
description: "Please try again in a few minutes.",
|
||||
});
|
||||
});
|
||||
|
||||
it("shows an error without starting a download when preflight fails", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue(new Response("not found", { status: 404 })),
|
||||
);
|
||||
const { clickMock } = getAnchor();
|
||||
const toast = createToast();
|
||||
|
||||
await downloadScanZip("scan-123", toast);
|
||||
|
||||
expect(clickMock).not.toHaveBeenCalled();
|
||||
expect(toast).toHaveBeenCalledWith({
|
||||
variant: "destructive",
|
||||
title: "Download Failed",
|
||||
description: "not found",
|
||||
});
|
||||
});
|
||||
});
|
||||
+32
-37
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
getComplianceCsv,
|
||||
getCompliancePdfReport,
|
||||
getExportsZip,
|
||||
type ScanBinaryResult,
|
||||
} from "@/actions/scans";
|
||||
import { getTask } from "@/actions/task";
|
||||
@@ -105,50 +106,44 @@ export const downloadScanZip = async (
|
||||
scanId: string,
|
||||
toast: ReturnType<typeof useToast>["toast"],
|
||||
) => {
|
||||
const reportUrl = `/api/scans/${encodeURIComponent(scanId)}/report`;
|
||||
const result = await getExportsZip(scanId);
|
||||
|
||||
try {
|
||||
const preflightResponse = await fetch(`${reportUrl}?preflight=1`, {
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (preflightResponse.status === 202) {
|
||||
toast({
|
||||
title: "The report is still being generated",
|
||||
description: "Please try again in a few minutes.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!preflightResponse.ok) {
|
||||
const errorMessage = await preflightResponse.text();
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Download Failed",
|
||||
description: errorMessage || "An unknown error occurred.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (_error) {
|
||||
if (result?.pending) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Download Failed",
|
||||
description: "Unable to start the report download. Please try again.",
|
||||
title: "The report is still being generated",
|
||||
description: "Please try again in a few minutes.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = reportUrl;
|
||||
a.download = `scan-${scanId}-report.zip`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
if (result?.success && result.data) {
|
||||
const binaryString = window.atob(result.data);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Download Started",
|
||||
description: "Your browser is downloading the scan report.",
|
||||
});
|
||||
const blob = new Blob([bytes], { type: "application/zip" });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = result.filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
toast({
|
||||
title: "Download Complete",
|
||||
description: "Your scan report has been downloaded successfully.",
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Download Failed",
|
||||
description: result?.error || "An unknown error occurred.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user