Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { commonTestProviders } from "../../../../common/testing/test-utils";
import type { Mocked } from "vitest";
import { WORKFLOW_EXECUTIONS_API_BASE_URL } from "../workflow-executions/workflow-executions.service";
import { DashboardWorkflowComputingUnit } from "../../../../common/type/workflow-computing-unit";
import JSZip from "jszip";

function computingUnit(type: string, cuid: number): DashboardWorkflowComputingUnit {
return { computingUnit: { cuid, type } } as unknown as DashboardWorkflowComputingUnit;
Expand Down Expand Up @@ -224,6 +225,47 @@ describe("DownloadService", () => {
expect(notificationServiceSpy.error).toHaveBeenCalledWith("Error downloading workflows as ZIP");
});

// ─── createWorkflowsZip / nameWorkflow (real zip assembly) ────────────────
// These drive downloadWorkflowsAsZip through the real (un-mocked) private
// createWorkflowsZip → downloadWorkflow → nameWorkflow chain, so the produced
// blob is a genuine zip we can load back and inspect.

it("assembles a real zip with one JSON entry per workflow", async () => {
workflowPersistServiceSpy.retrieveWorkflow.mockReturnValue(of({ content: { op: "x" } } as any));

const result = await firstValueFrom(
downloadService.downloadWorkflowsAsZip([
{ id: 1, name: "Alpha" },
{ id: 2, name: "Beta" },
])
);

expect(result).toBeInstanceOf(Blob);
expect(workflowPersistServiceSpy.retrieveWorkflow).toHaveBeenCalledWith(1);
expect(workflowPersistServiceSpy.retrieveWorkflow).toHaveBeenCalledWith(2);
expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(result, expect.stringMatching(/^workflowExports-.*\.zip$/));
expect(notificationServiceSpy.success).toHaveBeenCalledWith("Workflows have been downloaded as ZIP");

const loaded = await JSZip.loadAsync(result);
expect(Object.keys(loaded.files).sort()).toEqual(["Alpha.json", "Beta.json"]);
});

it("de-duplicates colliding workflow filenames inside the zip", async () => {
workflowPersistServiceSpy.retrieveWorkflow.mockReturnValue(of({ content: { op: "x" } } as any));

const result = await firstValueFrom(
downloadService.downloadWorkflowsAsZip([
{ id: 1, name: "Dup" },
{ id: 2, name: "Dup" },
{ id: 3, name: "Dup" },
])
);

// nameWorkflow appends -1, -2, ... on each collision so no entry is lost.
const loaded = await JSZip.loadAsync(result);
expect(Object.keys(loaded.files).sort()).toEqual(["Dup-1.json", "Dup-2.json", "Dup.json"]);
});

// ─── downloadOperatorsResult ──────────────────────────────────────────────

it("downloads a single operator file directly when there's exactly one file", async () => {
Expand All @@ -241,14 +283,10 @@ describe("DownloadService", () => {
expect(notificationServiceSpy.success).toHaveBeenCalledWith("Operator result has been downloaded");
});

// The multi-file zip path goes through `new JSZip()` against the
// `import * as JSZip from "jszip"` namespace, which the build flags as
// `Constructing "JSZip" will crash at run-time because it's an import
// namespace object`. Vitest reproduces the failure (`__vite_ssr_import_*
// is not a constructor`). Tracked as a separate cleanup in the codebase;
// the test is here as a placeholder so we re-enable it once the import
// is normalised to a default import.
it.skip("zips multiple operator files into a workflow-named archive", async () => {
// The multi-file zip path goes through `new JSZip()`; now that the source
// imports JSZip as a default import (rather than an import namespace object),
// it constructs correctly under Vitest so this exercises the real archive.
it("zips multiple operator files into a workflow-named archive", async () => {
const a = new Blob(["a"], { type: "text/plain" });
const b = new Blob(["b"], { type: "text/plain" });
const result = await firstValueFrom(
Expand All @@ -264,8 +302,32 @@ describe("DownloadService", () => {
);

expect(result).toBeInstanceOf(Blob);
expect(notificationServiceSpy.info).toHaveBeenCalledWith("Starting to download operator results as ZIP");
expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(expect.any(Blob), "results_7_TwoFile.zip");
expect(notificationServiceSpy.success).toHaveBeenCalledWith("Operator results have been downloaded as ZIP");

// The produced blob is a real zip carrying both operator files.
const loaded = await JSZip.loadAsync(result);
expect(Object.keys(loaded.files).sort()).toEqual(["a.csv", "b.csv"]);
});

it("flattens operator result batches before deciding single vs zip", async () => {
// Two source observables, each emitting one file: flat() merges them into
// two files, which routes through the multi-file zip branch.
const result = await firstValueFrom(
downloadService.downloadOperatorsResult(
[
of([{ filename: "first.csv", blob: new Blob(["1"], { type: "text/plain" }) }]),
of([{ filename: "second.csv", blob: new Blob(["2"], { type: "text/plain" }) }]),
],
{ wid: 3, name: "Flat" } as any
)
);

expect(result).toBeInstanceOf(Blob);
expect(fileSaverServiceSpy.saveAs).toHaveBeenCalledWith(expect.any(Blob), "results_3_Flat.zip");
const loaded = await JSZip.loadAsync(result);
expect(Object.keys(loaded.files).sort()).toEqual(["first.csv", "second.csv"]);
});

it("errors out cleanly when no operator result files are provided", async () => {
Expand Down Expand Up @@ -409,6 +471,30 @@ describe("DownloadService", () => {
expect(document.querySelector('iframe[name="download-iframe"]')).toBeNull();
});

it("falls back to an empty token when none is stored", () => {
// Override the beforeEach stub so getItem returns null, exercising the
// `?? ""` fallback on the auth token.
vi.stubGlobal("localStorage", {
getItem: vi.fn().mockReturnValue(null),
setItem: vi.fn(),
removeItem: vi.fn(),
});

downloadService.exportWorkflowResultToLocal(
"csv",
1,
"WF",
EXPORT_OPERATORS,
0,
0,
"out.csv",
computingUnit("local", 5)
);

const form = document.querySelector('form[target="download-iframe"]') as HTMLFormElement;
expect((form.querySelector('input[name="token"]') as HTMLInputElement).value).toBe("");
});

it("targets the cuid-scoped endpoint for a kubernetes computing unit", () => {
downloadService.exportWorkflowResultToLocal(
"csv",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { FileSaverService } from "../file/file-saver.service";
import { NotificationService } from "../../../../common/service/notification/notification.service";
import { DatasetService } from "../dataset/dataset.service";
import { WorkflowPersistService } from "src/app/common/service/workflow-persist/workflow-persist.service";
import * as JSZip from "jszip";
import JSZip from "jszip";
import { Workflow } from "../../../../common/type/workflow";
import { HttpClient, HttpResponse } from "@angular/common/http";
import { WORKFLOW_EXECUTIONS_API_BASE_URL } from "../workflow-executions/workflow-executions.service";
Expand Down
Loading