Apply account and provider changes to active conversions without restarting, isolate API and Web state, and abort the exact fallback attempt when settings change. Bound resume recovery, make disk reservations abortable, preserve cleanup totals and history, stabilize compact UI state, and canonicalize RapidGator host aliases. Expand bounded support diagnostics while redacting account identities, local paths, package names, and file names from current and rotated logs. Add regression coverage for rotation, live settings, HTTP 416 recovery, disk waits, cleanup, context menus, history failures, and support bundle privacy.
119 lines
4.2 KiB
TypeScript
119 lines
4.2 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { defaultSettings } from "../src/main/constants";
|
|
import { AppController } from "../src/main/app-controller";
|
|
import { createStoragePaths, loadHistory, loadSettings, saveHistory, saveSettings } from "../src/main/storage";
|
|
|
|
const electronState = vi.hoisted(() => ({ userDataDir: "" }));
|
|
|
|
vi.mock("electron", () => ({
|
|
app: {
|
|
getPath: () => electronState.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 {}
|
|
}));
|
|
|
|
const tempDirs: string[] = [];
|
|
|
|
function createHistoryEntry(outputDir: string) {
|
|
return {
|
|
id: "history-locked",
|
|
name: "locked",
|
|
totalBytes: 1,
|
|
downloadedBytes: 1,
|
|
fileCount: 1,
|
|
provider: "realdebrid" as const,
|
|
completedAt: 100,
|
|
durationSeconds: 1,
|
|
status: "completed" as const,
|
|
outputDir,
|
|
urls: []
|
|
};
|
|
}
|
|
|
|
function failHistoryDeletion(historyFile: string) {
|
|
const originalUnlink = fs.unlinkSync;
|
|
return vi.spyOn(fs, "unlinkSync").mockImplementation((target) => {
|
|
if (target === historyFile) {
|
|
const error = new Error("EPERM: history file is locked") as NodeJS.ErrnoException;
|
|
error.code = "EPERM";
|
|
throw error;
|
|
}
|
|
return originalUnlink(target);
|
|
});
|
|
}
|
|
|
|
beforeEach(() => {
|
|
electronState.userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "mdd-controller-history-"));
|
|
tempDirs.push(electronState.userDataDir);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
for (const dir of tempDirs.splice(0)) {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe("AppController history retention", () => {
|
|
it("remains startable when session-history cleanup returns EPERM", () => {
|
|
const paths = createStoragePaths(path.join(electronState.userDataDir, "runtime"));
|
|
saveSettings(paths, { ...defaultSettings(), historyRetentionMode: "session" });
|
|
saveHistory(paths, [createHistoryEntry(path.join(electronState.userDataDir, "out"))]);
|
|
const unlinkSpy = failHistoryDeletion(paths.historyFile);
|
|
|
|
let controller!: AppController;
|
|
expect(() => {
|
|
controller = new AppController();
|
|
}).not.toThrow();
|
|
expect(loadHistory(paths)).toHaveLength(1);
|
|
|
|
unlinkSpy.mockRestore();
|
|
controller.shutdown();
|
|
});
|
|
|
|
it("rolls back a retention update when history deletion fails", () => {
|
|
const paths = createStoragePaths(path.join(electronState.userDataDir, "runtime"));
|
|
saveSettings(paths, { ...defaultSettings(), historyRetentionMode: "permanent" });
|
|
saveHistory(paths, [createHistoryEntry(path.join(electronState.userDataDir, "out"))]);
|
|
const controller = new AppController();
|
|
const unlinkSpy = failHistoryDeletion(paths.historyFile);
|
|
|
|
const result = controller.updateSettings({ historyRetentionMode: "session" });
|
|
|
|
expect(result.historyRetentionMode).toBe("permanent");
|
|
expect(controller.getSettings().historyRetentionMode).toBe("permanent");
|
|
expect(loadSettings(paths).historyRetentionMode).toBe("permanent");
|
|
expect(loadHistory(paths)).toHaveLength(1);
|
|
|
|
unlinkSpy.mockRestore();
|
|
controller.shutdown();
|
|
});
|
|
|
|
it("keeps manual history deletion failures visible without auditing false success", () => {
|
|
const paths = createStoragePaths(path.join(electronState.userDataDir, "runtime"));
|
|
saveSettings(paths, { ...defaultSettings(), historyRetentionMode: "permanent" });
|
|
saveHistory(paths, [createHistoryEntry(path.join(electronState.userDataDir, "out"))]);
|
|
const controller = new AppController();
|
|
const audit = vi.fn();
|
|
(controller as unknown as { audit: typeof audit }).audit = audit;
|
|
const unlinkSpy = failHistoryDeletion(paths.historyFile);
|
|
|
|
expect(() => controller.clearHistory()).toThrow(/EPERM/);
|
|
expect(loadHistory(paths)).toHaveLength(1);
|
|
expect(audit.mock.calls.map((call) => call[1])).toEqual(["Verlauf konnte nicht geleert werden"]);
|
|
|
|
unlinkSpy.mockRestore();
|
|
controller.shutdown();
|
|
});
|
|
});
|