From 90cf14b0b23218f0ca01a280998e855a00b47567 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Thu, 20 Aug 2026 18:38:13 +0200 Subject: fix(config): reject garbage numerics instead of silently reading zero, item 123 toInt() and toLongLong() return 0 on failure rather than the default, so a typo in autosave_interval_ms produced a zero-interval timer. That timer is restarted on every keystroke, so it would fire on the next event-loop pass and turn a 30 second debounce into a Maildir write per keystroke, each one uploaded by mbsync: exactly the behaviour the debounce exists to prevent. This file already had the right shape in five places, a checked parse that reports the bad value and keeps the default. The [compose] keys were the only numerics skipping it. The interval is also clamped, since nothing assigns a meaning to a zero or negative autosave. quote_position now warns on an unrecognised value, matching sync_on_exit, language and date_format; the only silent fallbacks in this file are for absent keys rather than malformed ones. And a missing `sent` folder is a notice rather than a problem, because the spec blesses that configuration and a modal on every launch for a permanently correct setup is how users learn to dismiss dialogs unread. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015muoUo2GdxmBDSp5vjYcbE --- tests/test_config.cpp | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) (limited to 'tests/test_config.cpp') diff --git a/tests/test_config.cpp b/tests/test_config.cpp index 2df4c40..a902425 100644 --- a/tests/test_config.cpp +++ b/tests/test_config.cpp @@ -126,6 +126,11 @@ private slots: void aZeroSendDelayIsHonouredRatherThanTreatedAsUnset(); void aDefaultAccountThatCannotSendIsWarnedAbout(); void anInstallationWhereNoAccountCanSendIsNotWarnedAbout(); + void garbageAutosaveIntervalIsRejectedNotZero(); + void garbageSendDelayIsRejectedNotZero(); + void garbageAttachmentWarnBytesIsRejectedNotZero(); + void zeroOrNegativeAutosaveIntervalIsClamped(); + void unrecognisedQuotePositionWarnsAndFallsBackToAbove(); }; static QString writeIni(const QTemporaryDir &dir, const QString &body) @@ -2418,5 +2423,91 @@ void TestConfig::anInstallationWhereNoAccountCanSendIsNotWarnedAbout() } } +void TestConfig::garbageAutosaveIntervalIsRejectedNotZero() +{ + // toInt() alone returns 0 on a parse failure, not the default, and 0 + // reaches a QTimer restarted on every keystroke: a typo here would have + // turned the debounce into a write per keystroke, uploaded by mbsync. + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral( + "[compose]\n" + "autosave_interval_ms=oops\n"))); + + QCOMPARE(config.compose().autosaveIntervalMs, 30000); + QVERIFY2(!config.problems().isEmpty(), + "a garbage autosave_interval_ms was accepted silently"); +} + +void TestConfig::garbageSendDelayIsRejectedNotZero() +{ + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral( + "[compose]\n" + "send_delay_ms=soon\n"))); + + QCOMPARE(config.compose().sendDelayMs, 5000); + QVERIFY2(!config.problems().isEmpty(), + "a garbage send_delay_ms was accepted silently"); +} + +void TestConfig::garbageAttachmentWarnBytesIsRejectedNotZero() +{ + // Verified against the actual defect: attachment_warn_bytes=banana gave 0 + // via a bare toLongLong(), which would have warned about every attachment + // no matter how small. + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral( + "[compose]\n" + "attachment_warn_bytes=banana\n"))); + + QCOMPARE(config.compose().attachmentWarnBytes, qint64(26214400)); + QVERIFY2(!config.problems().isEmpty(), + "a garbage attachment_warn_bytes was accepted silently"); +} + +void TestConfig::zeroOrNegativeAutosaveIntervalIsClamped() +{ + // Independent of the parse fix: a value that parses fine but is zero or + // negative must still not reach setInterval(), since nothing assigns a + // meaning to one, unlike mark_read_delay_ms's documented negative-means-off. + QTemporaryDir dir; + Config zero; + zero.load(writeIni(dir, QStringLiteral( + "[compose]\n" + "autosave_interval_ms=0\n"))); + QVERIFY2(zero.compose().autosaveIntervalMs >= 1000, + qPrintable(QStringLiteral("zero autosave interval was not clamped: %1") + .arg(zero.compose().autosaveIntervalMs))); + + QTemporaryDir dir2; + Config negative; + negative.load(writeIni(dir2, QStringLiteral( + "[compose]\n" + "autosave_interval_ms=-500\n"))); + QVERIFY2(negative.compose().autosaveIntervalMs >= 1000, + qPrintable(QStringLiteral("negative autosave interval was not clamped: %1") + .arg(negative.compose().autosaveIntervalMs))); +} + +void TestConfig::unrecognisedQuotePositionWarnsAndFallsBackToAbove() +{ + // Matches the precedent set by sync_on_exit, language and date_format: + // the only silent fallbacks in this file are for ABSENT keys, never for + // malformed ones. + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral( + "[compose]\n" + "quote_position=abov\n"))); + + QVERIFY2(config.compose().quotePosition == ComposeSettings::QuotePosition::Above, + "an unrecognised quote_position must still fall back to Above"); + QVERIFY2(!config.problems().isEmpty(), + "an unrecognised quote_position was accepted silently"); +} + QTEST_MAIN(TestConfig) #include "test_config.moc" -- cgit v1.2.3