Files
Multi-Debrid-Downloader/tests/renderer-settings.test.ts
Sucukdeluxe 2ea494cd5c Prepare v2.0.74 sidepanel reliability release
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.
2026-08-26 20:35:06 +02:00

139 lines
6.2 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { defaultSettings } from "../src/main/constants";
import { createRendererSettings } from "../src/main/renderer-state";
import { validateRendererSettingsUpdate } from "../src/main/renderer-settings";
describe("renderer settings validation", () => {
it("projects and validates the semantic theme preference independently from the resolved palette", () => {
const current = { ...defaultSettings(), theme: "dark" as const, themePreference: "system" as const };
const projected = createRendererSettings(current);
expect(projected).toEqual(expect.objectContaining({ theme: "dark", themePreference: "system" }));
expect(validateRendererSettingsUpdate({ theme: "light", themePreference: "system" }, current)).toEqual({
theme: "light",
themePreference: "system"
});
expect(() => validateRendererSettingsUpdate({ themePreference: "automatic" }, current)).toThrow("Settings-Payload ist ungültig");
});
it("accepts every editable notification control while keeping the webhook write-only", () => {
const current = { ...defaultSettings(), notifyUrl: "https://notify.example.test/private-hook" };
const update = {
notifyPackageSuccessMode: "individual",
notifyOnRemainingBelow: true,
notifyRemainingThresholdGb: 25,
notifyOnDownloadStall: true,
notifyStallAfterSeconds: 120,
notifyStallCooldownMinutes: 15,
notifyOnDownloadRecovery: false
};
const projected = createRendererSettings(current);
expect(projected).not.toHaveProperty("notifyUrl");
expect(projected.notifyUrlConfigured).toBe(true);
expect(validateRendererSettingsUpdate(update, current)).toEqual(update);
});
it("rejects unsupported package success modes at the IPC boundary", () => {
const current = defaultSettings();
expect(validateRendererSettingsUpdate({ notifyPackageSuccessMode: "digest" }, current)).toEqual({ notifyPackageSuccessMode: "digest" });
expect(validateRendererSettingsUpdate({ notifyPackageSuccessMode: "individual" }, current)).toEqual({ notifyPackageSuccessMode: "individual" });
expect(() => validateRendererSettingsUpdate({ notifyPackageSuccessMode: "batched" }, current)).toThrow("Settings-Payload ist ungültig");
});
it("accepts a complete settings save when an account status has no optional email", () => {
const current = defaultSettings();
current.debridAccountStatuses = {
"svc-realdebrid": {
accountId: "svc-realdebrid",
provider: "realdebrid",
label: "Real-Debrid",
maskedLogin: "user",
valid: true,
isPremium: true,
premiumUntilMs: null,
username: "web-user",
message: "Premium aktiv",
checkedAt: 1_700_000_000_000
}
};
const rendererSettings = createRendererSettings(current);
expect(rendererSettings.debridAccountStatuses["svc-realdebrid"]).toHaveProperty("email", undefined);
expect(rendererSettings.debridAccountStatuses["svc-realdebrid"]).toHaveProperty("username", "web-user");
const validated = validateRendererSettingsUpdate(rendererSettings, current);
expect(validated.debridAccountStatuses).toEqual(rendererSettings.debridAccountStatuses);
expect(validated.animatePackageDisclosure).toBe(true);
expect(validated).not.toHaveProperty("configuredProviders");
expect(validated).not.toHaveProperty("archivePasswordListConfigured");
expect(validated).not.toHaveProperty("notifyUrlConfigured");
});
it("ignores omitted optional top-level values but still rejects unknown concrete settings", () => {
const current = defaultSettings();
expect(validateRendererSettingsUpdate({ columnOrderVersion: undefined }, current)).toEqual({});
expect(() => validateRendererSettingsUpdate({ obsoleteSetting: true }, current)).toThrow("Settings-Payload ist ungültig");
});
it("keeps archive passwords out of general renderer settings", () => {
const password = "fixture-renderer-hidden-archive-password";
const projected = createRendererSettings({ ...defaultSettings(), archivePasswordList: password });
expect(projected.archivePasswordListConfigured).toBe(true);
expect(JSON.stringify(projected)).not.toContain(password);
});
it("projects daily start state and accepts only editable calendar controls", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 7, 22, 9, 0));
try {
const current = {
...defaultSettings(),
dailyStartEnabled: true,
dailyStartMinuteOfDay: 18 * 60 + 45,
dailyStartFirstLocalDate: "2026-08-23",
dailyStartLastHandledLocalDate: "2026-08-22",
dailyStartPendingLocalDate: "",
dailyStartLastOutcome: "started" as const
};
const projected = createRendererSettings(current);
expect(projected).toMatchObject({
dailyStartEnabled: true,
dailyStartMinuteOfDay: 18 * 60 + 45,
dailyStartFirstLocalDate: "2026-08-23",
dailyStartLastHandledLocalDate: "2026-08-22",
dailyStartPendingLocalDate: "",
dailyStartLastOutcome: "started"
});
expect(projected.nextDailyStartEpochMs).toBe(new Date(2026, 7, 23, 18, 45, 0, 0).getTime());
expect(validateRendererSettingsUpdate({
dailyStartEnabled: true,
dailyStartMinuteOfDay: 7 * 60 + 30,
dailyStartFirstLocalDate: "2026-08-24",
dailyStartLastHandledLocalDate: "2026-08-23",
dailyStartPendingLocalDate: "2026-08-24",
dailyStartLastOutcome: "missed",
nextDailyStartEpochMs: 123
}, current)).toEqual({
dailyStartEnabled: true,
dailyStartMinuteOfDay: 7 * 60 + 30,
dailyStartFirstLocalDate: "2026-08-24"
});
} finally {
vi.useRealTimers();
}
});
it("rejects invalid editable daily calendar values at the renderer boundary", () => {
const current = defaultSettings();
expect(() => validateRendererSettingsUpdate({ dailyStartMinuteOfDay: 1_440 }, current)).toThrow("Settings-Payload ist ungültig");
expect(() => validateRendererSettingsUpdate({ dailyStartMinuteOfDay: 12.5 }, current)).toThrow("Settings-Payload ist ungültig");
expect(() => validateRendererSettingsUpdate({ dailyStartFirstLocalDate: "2026-02-29" }, current)).toThrow("Settings-Payload ist ungültig");
});
});