import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { IPC_CHANNELS } from "../src/shared/ipc"; import type { ElectronApi } from "../src/shared/preload-api"; const electron = vi.hoisted(() => ({ api: undefined as ElectronApi | undefined, invoke: vi.fn<(...args: unknown[]) => Promise>(async () => undefined), getPathForFile: vi.fn(() => "C:\\Imports\\dropped.dlc"), on: vi.fn(), removeListener: vi.fn() })); vi.mock("electron", () => ({ contextBridge: { exposeInMainWorld: (_name: string, api: ElectronApi) => { electron.api = api; } }, ipcRenderer: { invoke: electron.invoke, on: electron.on, removeListener: electron.removeListener, send: vi.fn() }, webUtils: { getPathForFile: electron.getPathForFile } })); describe("account preload contract", () => { beforeAll(async () => { await import("../src/preload/preload"); }); beforeEach(() => { electron.invoke.mockClear(); }); it("forwards submitted secrets only in write-only account commands", async () => { const secret = "fixture-preload-secret-5zK1"; electron.invoke.mockResolvedValueOnce({ accountId: "mda_fixture", settings: { language: "de" }, accounts: [] }); const result = await electron.api?.createAccount({ action: "create", kind: "megadebrid-api", identity: "preload-account@example.test", secret, dailyLimitBytes: 0 }); expect(electron.invoke).toHaveBeenCalledWith( IPC_CHANNELS.CREATE_ACCOUNT, expect.objectContaining({ secret }) ); expect(JSON.stringify(result)).not.toContain(secret); }); it("exposes separate replace, update-secret and delete channels", async () => { electron.invoke.mockResolvedValue({ accountId: "mda_fixture", settings: { language: "de" }, accounts: [] }); await electron.api?.replaceAccount({ action: "replace", kind: "megadebrid-api", accountId: "mda_fixture", identity: "account@example.test", secret: "", dailyLimitBytes: 0 }); await electron.api?.updateAccountSecret({ action: "update-secret", kind: "megadebrid-api", accountId: "mda_fixture", secret: "fixture-new-secret-8sP4" }); await electron.api?.deleteAccount({ action: "delete", kind: "megadebrid-api", accountId: "mda_fixture" }); expect(electron.invoke.mock.calls.map((call) => call[0])).toEqual([ IPC_CHANNELS.REPLACE_ACCOUNT, IPC_CHANNELS.UPDATE_ACCOUNT_SECRET, IPC_CHANNELS.DELETE_ACCOUNT ]); }); it("forwards the selected bulk account-check scope", async () => { electron.invoke.mockResolvedValue([]); await electron.api?.checkDebridAccounts("active"); await electron.api?.checkDebridAccounts("all"); expect(electron.invoke.mock.calls).toEqual([ [IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, "active"], [IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, "all"] ]); }); it("forwards account-bound existing and create browser logins", async () => { await electron.api?.openRealDebridLogin({ accountId: "rdw_existing" }); await electron.api?.openRealDebridLogin({ accountId: "rdw_reserved", create: true, dailyLimitBytes: 10_000 }); expect(electron.invoke.mock.calls).toEqual([ [IPC_CHANNELS.OPEN_REALDEBRID_LOGIN, { accountId: "rdw_existing" }], [IPC_CHANNELS.OPEN_REALDEBRID_LOGIN, { accountId: "rdw_reserved", create: true, dailyLimitBytes: 10_000 }] ]); }); it("reveals a stored secret only through the explicit account channel", async () => { electron.invoke.mockResolvedValueOnce({ secret: "fixture-revealed-secret-7gH8" }); const result = await (electron.api as ElectronApi & { revealAccountSecret: (request: { kind: "realdebrid-api"; accountId: string }) => Promise<{ secret: string }>; }).revealAccountSecret({ kind: "realdebrid-api", accountId: "svc-realdebrid" }); expect(electron.invoke).toHaveBeenCalledWith( IPC_CHANNELS.REVEAL_ACCOUNT_SECRET, { kind: "realdebrid-api", accountId: "svc-realdebrid" } ); expect(result).toEqual({ secret: "fixture-revealed-secret-7gH8" }); }); it("loads the stored archive password list only through its dedicated channel", async () => { const passwords = "fixture-archive-one\nfixture-archive-two"; electron.invoke.mockResolvedValueOnce({ passwords }); const result = await (electron.api as ElectronApi & { getArchivePasswordList: () => Promise<{ passwords: string }>; }).getArchivePasswordList(); expect(electron.invoke).toHaveBeenCalledWith(IPC_CHANNELS.GET_ARCHIVE_PASSWORD_LIST); expect(result).toEqual({ passwords }); }); it("exposes separate collector preparation and enrichment channels", async () => { const textRequest = { rawText: "https://example.com/file", addedAt: 1234 }; const packages = [{ id: "package", name: "Paket", nameSource: "inferred" as const, addedAt: 1234, links: [] }]; await electron.api?.prepareCollectorText(textRequest); await electron.api?.prepareCollectorContainers(["C:\\Imports\\sample.dlc"], 2345); await electron.api?.enrichCollectorPackages({ requestId: "request-preload", packages }); expect(electron.invoke.mock.calls).toEqual([ [IPC_CHANNELS.PREPARE_COLLECTOR_TEXT, textRequest], [IPC_CHANNELS.PREPARE_COLLECTOR_CONTAINERS, ["C:\\Imports\\sample.dlc"], 2345], [IPC_CHANNELS.ENRICH_COLLECTOR_PACKAGES, { requestId: "request-preload", packages }] ]); }); it("resolves dropped files through Electron webUtils without IPC", () => { const file = { name: "dropped.dlc" } as File; expect(electron.api?.getPathForDroppedFile(file)).toBe("C:\\Imports\\dropped.dlc"); expect(electron.getPathForFile).toHaveBeenCalledWith(file); expect(electron.invoke).not.toHaveBeenCalled(); }); it("subscribes and unsubscribes collector enrichment progress", () => { const callback = vi.fn(); const unsubscribe = electron.api?.onCollectorEnrichmentProgress(callback); const listener = electron.on.mock.calls.find((call) => call[0] === IPC_CHANNELS.COLLECTOR_ENRICHMENT_PROGRESS)?.[1] as ((event: unknown, value: unknown) => void) | undefined; const progress = { requestId: "request-progress", result: { packages: [], invalidCount: 0, duplicateCount: 0 } }; listener?.({}, progress); unsubscribe?.(); expect(callback).toHaveBeenCalledWith(progress); expect(electron.removeListener).toHaveBeenCalledWith(IPC_CHANNELS.COLLECTOR_ENRICHMENT_PROGRESS, listener); }); });