Restore and persist the package-based link collector with progressive metadata, bounded high-volume updates, safe hydration, visible selection, and complete localization. Harden download controls, snapshot ordering, history pagination, statistics recovery, settings saves, backup imports, notification persistence, and Windows storage races with regression coverage.
180 lines
7.4 KiB
TypeScript
180 lines
7.4 KiB
TypeScript
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<unknown>>(async () => undefined),
|
|
getPathForFile: vi.fn(() => "C:\\Imports\\dropped.dlc"),
|
|
on: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
sendSync: vi.fn((_: unknown, state: unknown) => state)
|
|
}));
|
|
|
|
vi.mock("electron", () => ({
|
|
contextBridge: {
|
|
exposeInMainWorld: (_name: string, api: ElectronApi) => {
|
|
electron.api = api;
|
|
}
|
|
},
|
|
ipcRenderer: {
|
|
invoke: electron.invoke,
|
|
on: electron.on,
|
|
removeListener: electron.removeListener,
|
|
sendSync: electron.sendSync,
|
|
send: vi.fn()
|
|
},
|
|
webUtils: { getPathForFile: electron.getPathForFile }
|
|
}));
|
|
|
|
describe("account preload contract", () => {
|
|
beforeAll(async () => {
|
|
await import("../src/preload/preload");
|
|
});
|
|
|
|
beforeEach(() => {
|
|
electron.invoke.mockClear();
|
|
electron.sendSync.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("loads and saves the persistent collector state through dedicated channels", async () => {
|
|
const state = { packages: [], collapsedPackageIds: [] };
|
|
electron.sendSync.mockReturnValueOnce(state);
|
|
electron.sendSync.mockReturnValueOnce(true);
|
|
|
|
await electron.api?.getCollectorState();
|
|
const syncState = (electron.api as ElectronApi & {
|
|
getCollectorStateSync: () => typeof state;
|
|
}).getCollectorStateSync();
|
|
await electron.api?.saveCollectorState(state);
|
|
const savedSync = electron.api?.saveCollectorStateSync(state);
|
|
|
|
expect(electron.invoke.mock.calls).toEqual([
|
|
[IPC_CHANNELS.GET_COLLECTOR_STATE],
|
|
[IPC_CHANNELS.SAVE_COLLECTOR_STATE, state]
|
|
]);
|
|
expect(syncState).toEqual(state);
|
|
expect(savedSync).toBe(true);
|
|
expect(electron.sendSync.mock.calls).toEqual([
|
|
[IPC_CHANNELS.GET_COLLECTOR_STATE_SYNC],
|
|
[IPC_CHANNELS.SAVE_COLLECTOR_STATE_SYNC, state]
|
|
]);
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|