From 161e111c2fb4deda086b0cdb5aea7537195c8758 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sat, 22 Aug 2026 03:51:47 +0200 Subject: [PATCH] fix(notifications): validate success mode settings --- src/main/renderer-settings.ts | 3 +++ tests/renderer-settings.test.ts | 8 +++++++ tests/storage.test.ts | 38 ++++++++++++++++++++++----------- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/main/renderer-settings.ts b/src/main/renderer-settings.ts index ac219b5..3760727 100644 --- a/src/main/renderer-settings.ts +++ b/src/main/renderer-settings.ts @@ -82,6 +82,9 @@ export function validateRendererSettingsUpdate(value: unknown, current: AppSetti if (!(key in safe)) { invalid(); } + if (key === "notifyPackageSuccessMode" && entry !== "digest" && entry !== "individual") { + invalid(); + } validateTopLevelType(entry, safe[key]); validateJsonValue(entry); output[key] = entry; diff --git a/tests/renderer-settings.test.ts b/tests/renderer-settings.test.ts index 9e4ed28..9450384 100644 --- a/tests/renderer-settings.test.ts +++ b/tests/renderer-settings.test.ts @@ -22,6 +22,14 @@ describe("renderer settings validation", () => { 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 = { diff --git a/tests/storage.test.ts b/tests/storage.test.ts index 64a67a4..121569b 100644 --- a/tests/storage.test.ts +++ b/tests/storage.test.ts @@ -52,14 +52,17 @@ describe("settings storage", () => { expect(loadSettingsFrom({ notifyOnPackageCompleted: true, notifyPackageSuccessMode: "digest" }).notifyPackageSuccessMode).toBe("digest"); }); - it("normalizes notification defaults and numeric limits", () => { + it("preserves an explicit individual success mode when legacy package notifications are disabled", () => { + expect(loadSettingsFrom({ notifyOnPackageCompleted: false, notifyPackageSuccessMode: "individual" }).notifyPackageSuccessMode).toBe("individual"); + }); + + it("falls back from an invalid persisted success mode according to the legacy package toggle", () => { + expect(loadSettingsFrom({ notifyOnPackageCompleted: true, notifyPackageSuccessMode: "invalid" }).notifyPackageSuccessMode).toBe("individual"); + expect(loadSettingsFrom({ notifyOnPackageCompleted: false, notifyPackageSuccessMode: "invalid" }).notifyPackageSuccessMode).toBe("digest"); + }); + + it("loads notification defaults for new settings", () => { const defaults = loadSettingsFrom({}); - const normalized = normalizeSettings({ - ...defaultSettings(), - notifyRemainingThresholdGb: 0, - notifyStallAfterSeconds: 3_601, - notifyStallCooldownMinutes: 1 - }); expect(defaults).toEqual(expect.objectContaining({ notifyPackageSuccessMode: "digest", @@ -70,11 +73,22 @@ describe("settings storage", () => { notifyStallCooldownMinutes: 10, notifyOnDownloadRecovery: true })); - expect(normalized).toEqual(expect.objectContaining({ - notifyRemainingThresholdGb: 1, - notifyStallAfterSeconds: 3_600, - notifyStallCooldownMinutes: 5 - })); + }); + + it.each([ + ["notifyRemainingThresholdGb", 0, 1], + ["notifyRemainingThresholdGb", 100_001, 100_000], + ["notifyRemainingThresholdGb", "invalid", 50], + ["notifyStallAfterSeconds", 59, 60], + ["notifyStallAfterSeconds", 3_601, 3_600], + ["notifyStallAfterSeconds", "invalid", 90], + ["notifyStallCooldownMinutes", 4, 5], + ["notifyStallCooldownMinutes", 1_441, 1_440], + ["notifyStallCooldownMinutes", "invalid", 10] + ] as const)("normalizes %s from %s to %s", (key, input, expected) => { + const normalized = normalizeSettings({ ...defaultSettings(), [key]: input } as AppSettings); + + expect(normalized[key]).toBe(expected); }); it("enables package disclosure motion by default and preserves an explicit opt-out", () => {