Persist collector packages and disclosure state through validated atomic AppData storage with backup recovery and shutdown synchronization. Align download sidebar metrics with the hidden-extracted presentation scope and keep Start available through temporary Real-Debrid cooldowns while preserving hard account blocks.
170 lines
7.0 KiB
TypeScript
170 lines
7.0 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: [] };
|
|
|
|
await electron.api?.getCollectorState();
|
|
await electron.api?.saveCollectorState(state);
|
|
electron.api?.saveCollectorStateSync(state);
|
|
|
|
expect(electron.invoke.mock.calls).toEqual([
|
|
[IPC_CHANNELS.GET_COLLECTOR_STATE],
|
|
[IPC_CHANNELS.SAVE_COLLECTOR_STATE, state]
|
|
]);
|
|
expect(electron.sendSync).toHaveBeenCalledWith(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);
|
|
});
|
|
});
|