Files
Multi-Debrid-Downloader/tests/app-controller.test.ts
Sucukdeluxe 1b7caba2eb feat: add Deepbrid, daily scheduling, and notification center
Add encrypted Deepbrid API accounts with account validation, provider routing, fallback, usage tracking, safe error handling, and verified 1Fichier downloads. Restore persistent recurring daily starts with local-calendar deduplication and legacy schedule compatibility. Add durable Discord package, run, remaining-volume, stall, and recovery notifications with privacy-safe telemetry and disk-failure recovery.
2026-08-24 07:37:55 +02:00

259 lines
8.7 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { AppController } from "../src/main/app-controller";
import { defaultSettings } from "../src/main/constants";
import { configureCredentialProtector } from "../src/main/credential-protection";
import { prepareDailyStartSettingsPatch } from "../src/main/daily-start-scheduler";
import { createStoragePaths, emptySession } from "../src/main/storage";
import type { AppSettings, SessionState } from "../src/shared/types";
const appState = vi.hoisted(() => ({ userDataDir: "C:\\MDD\\Test" }));
const bootStorage = vi.hoisted(() => ({
settings: null as unknown,
loadResult: null as unknown
}));
vi.mock("electron", () => ({
app: {
getPath: (name: string) => name === "desktop" ? path.join(appState.userDataDir, "Desktop") : appState.userDataDir
},
BrowserWindow: class {},
clipboard: {},
dialog: {},
ipcMain: { handle: vi.fn(), on: vi.fn() },
Menu: { buildFromTemplate: vi.fn(), setApplicationMenu: vi.fn() },
safeStorage: { isEncryptionAvailable: () => false, encryptString: vi.fn(), decryptString: vi.fn() },
shell: {},
Tray: class {}
}));
vi.mock("../src/main/debug-server", async () => {
const actual = await vi.importActual<typeof import("../src/main/debug-server")>("../src/main/debug-server");
return {
...actual,
startDebugServer: vi.fn(),
stopDebugServer: vi.fn()
};
});
vi.mock("../src/main/storage", async () => {
const actual = await vi.importActual<typeof import("../src/main/storage")>("../src/main/storage");
return {
...actual,
loadSettings: (...args: Parameters<typeof actual.loadSettings>) => bootStorage.settings
? bootStorage.settings as AppSettings
: actual.loadSettings(...args),
loadSessionWithStatus: (...args: Parameters<typeof actual.loadSessionWithStatus>) => bootStorage.loadResult
? bootStorage.loadResult as ReturnType<typeof actual.loadSessionWithStatus>
: actual.loadSessionWithStatus(...args)
};
});
const tempDirs: string[] = [];
const liveControllers: AppController[] = [];
function createController(settings: AppSettings): AppController {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "mdd-controller-"));
tempDirs.push(dir);
const controller = Object.create(AppController.prototype) as any;
controller.settings = settings;
controller.storagePaths = createStoragePaths(dir);
controller.manager = { setSettings: vi.fn() };
controller.audit = vi.fn();
controller.overlayLiveUsageCounters = vi.fn();
controller.pruneRealDebridWebFallbacks = vi.fn();
controller.realDebridWebFallbacks = new Map();
return controller as AppController;
}
function queuedSession(): SessionState {
return {
...emptySession(),
packageOrder: ["pkg"],
packages: {
pkg: {
id: "pkg",
name: "Daily queue",
outputDir: "C:\\Downloads",
extractDir: "C:\\Downloads",
status: "queued",
itemIds: ["item"],
cancelled: false,
enabled: true,
createdAt: 1,
updatedAt: 1
}
},
items: {
item: {
id: "item",
packageId: "pkg",
url: "https://example.test/file",
provider: null,
status: "queued",
retries: 0,
speedBps: 0,
downloadedBytes: 0,
totalBytes: null,
progressPercent: 0,
fileName: "file.bin",
targetPath: "C:\\Downloads\\file.bin",
resumable: true,
attempts: 0,
lastError: "",
fullStatus: "",
createdAt: 1,
updatedAt: 1
}
}
};
}
function tomorrowLocalDate(): string {
const now = new Date();
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 12, 0, 0, 0);
return `${tomorrow.getFullYear().toString().padStart(4, "0")}-${(tomorrow.getMonth() + 1).toString().padStart(2, "0")}-${tomorrow.getDate().toString().padStart(2, "0")}`;
}
afterEach(async () => {
for (const controller of liveControllers.splice(0)) {
await controller.shutdown();
}
bootStorage.settings = null;
bootStorage.loadResult = null;
vi.restoreAllMocks();
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
describe("AppController daily start settings", () => {
it("does not clear a legacy one-time schedule for an unprepared daily settings patch", () => {
configureCredentialProtector({
isEncryptionAvailable: () => false,
encryptString: (value) => Buffer.from(value, "utf8"),
decryptString: (value) => Buffer.from(value).toString("utf8")
});
const scheduledStartEpochMs = 1_800_000_000_000;
const controller = createController({
...defaultSettings(),
scheduledStartEpochMs
});
const updated = controller.updateSettings({
dailyStartEnabled: true,
dailyStartMinuteOfDay: 18 * 60 + 45,
dailyStartFirstLocalDate: "2026-08-23"
});
expect(updated.scheduledStartEpochMs).toBe(scheduledStartEpochMs);
});
it("clears a legacy one-time schedule when an explicit daily rule is saved", () => {
configureCredentialProtector({
isEncryptionAvailable: () => false,
encryptString: (value) => Buffer.from(value, "utf8"),
decryptString: (value) => Buffer.from(value).toString("utf8")
});
const controller = createController({
...defaultSettings(),
scheduledStartEpochMs: 1_800_000_000_000
});
const updated = controller.updateSettings(prepareDailyStartSettingsPatch({
dailyStartEnabled: true,
dailyStartMinuteOfDay: 18 * 60 + 45,
dailyStartFirstLocalDate: "2026-08-23"
}, controller.getSettings()));
expect(updated.scheduledStartEpochMs).toBe(0);
expect(updated).toMatchObject({
dailyStartEnabled: true,
dailyStartMinuteOfDay: 18 * 60 + 45,
dailyStartFirstLocalDate: "2026-08-23"
});
});
it("preserves a legacy one-time schedule when an account mutation saves a complete settings state", async () => {
configureCredentialProtector({
isEncryptionAvailable: () => false,
encryptString: (value) => Buffer.from(value, "utf8"),
decryptString: (value) => Buffer.from(value).toString("utf8")
});
const scheduledStartEpochMs = 1_800_000_000_000;
const controller = createController({
...defaultSettings(),
ddownloadLogin: "account@example.test",
ddownloadPassword: "secret",
scheduledStartEpochMs
});
await controller.executeAccountCommand({
action: "delete",
kind: "ddownload-login",
accountId: "svc-ddownload"
});
expect(controller.getSettings().scheduledStartEpochMs).toBe(scheduledStartEpochMs);
});
it("preserves a legacy one-time schedule when a complete unchanged settings payload is prepared", () => {
configureCredentialProtector({
isEncryptionAvailable: () => false,
encryptString: (value) => Buffer.from(value, "utf8"),
decryptString: (value) => Buffer.from(value).toString("utf8")
});
const scheduledStartEpochMs = 1_800_000_000_000;
const controller = createController({
...defaultSettings(),
dailyStartEnabled: false,
dailyStartMinuteOfDay: 18 * 60 + 45,
dailyStartFirstLocalDate: "2026-08-23",
scheduledStartEpochMs
});
const current = controller.getSettings();
const updated = controller.updateSettings(prepareDailyStartSettingsPatch({ ...current }, current));
expect(updated.scheduledStartEpochMs).toBe(scheduledStartEpochMs);
});
});
describe("AppController boot auto-resume", () => {
it.each([
{ wasRunning: false, expectedAutoResume: false },
{ wasRunning: true, expectedAutoResume: true }
])("uses persisted running evidence when a daily start is still in the future", async ({ wasRunning, expectedAutoResume }) => {
configureCredentialProtector({
isEncryptionAvailable: () => false,
encryptString: (value) => Buffer.from(value, "utf8"),
decryptString: (value) => Buffer.from(value).toString("utf8")
});
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mdd-controller-boot-"));
tempDirs.push(root);
appState.userDataDir = root;
bootStorage.settings = {
...defaultSettings(),
token: "token",
autoResumeOnStart: true,
dailyStartEnabled: true,
dailyStartMinuteOfDay: 0,
dailyStartFirstLocalDate: tomorrowLocalDate()
} satisfies AppSettings;
bootStorage.loadResult = {
session: queuedSession(),
status: "ok",
wasRunning
};
const controller = new AppController();
liveControllers.push(controller);
const beginAutoResume = vi.spyOn(controller as any, "beginAutoResume").mockImplementation(() => {});
controller.onState = vi.fn();
expect(beginAutoResume).toHaveBeenCalledTimes(expectedAutoResume ? 1 : 0);
});
});