aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-31 11:42:24 +0100
committerDanilo M. <danix@danix.xyz>2026-10-31 11:42:24 +0100
commita8d84d57b7f3b43fdc0460eb252da45c76ab2baa (patch)
tree9268fce7cce66f624af1d256f391a209d38859b3 /tests
parent8804a75ae561a7e504fdce8aea58d3ddc8670b71 (diff)
downloadqtmaildir-a8d84d57b7f3b43fdc0460eb252da45c76ab2baa.tar.gz
qtmaildir-a8d84d57b7f3b43fdc0460eb252da45c76ab2baa.zip
feat: an optional allow_archive field on a tagging rule
Both readers learn it together, as the rule format requires. No version bump: the field is optional, and each writer emits it only on a rule that removes inbox, so a file without one saves unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'tests')
-rw-r--r--tests/test_tagrules.cpp73
1 files changed, 73 insertions, 0 deletions
diff --git a/tests/test_tagrules.cpp b/tests/test_tagrules.cpp
index c831864..860d433 100644
--- a/tests/test_tagrules.cpp
+++ b/tests/test_tagrules.cpp
@@ -35,6 +35,9 @@ class TestTagRules : public QObject
Q_OBJECT
private slots:
+ void allowArchiveRoundTripsAndIsOmittedWhenFalse();
+ void allowArchiveIsDroppedFromARuleThatKeepsInbox();
+ void hookWouldSkipMirrorsThePostNewRefusal();
void aRuleLoadsWithEveryField();
void absentFieldsTakeTheirDefaults();
void aMalformedRuleIsDroppedWithAWarning();
@@ -1505,5 +1508,75 @@ void TestTagRules::previewClearsTheAccountScope()
"is wrapped twice and matches nothing");
}
+void TestTagRules::allowArchiveRoundTripsAndIsOmittedWhenFalse()
+{
+ // Item 213. Optional, so a file with no archiving rule must save
+ // unchanged: no "allow_archive": false appears from nowhere.
+ const QString path = writeRules(R"({
+ "version": 1,
+ "rules": [
+ {"id": "promo", "add": ["promo"], "remove": ["inbox"],
+ "query": "from:a@example.org", "allow_archive": true},
+ {"id": "plain", "add": ["x"], "query": "from:b@example.org"}
+ ]
+ })");
+
+ TagRules rules;
+ rules.load(path);
+ QCOMPARE(rules.rules().size(), 2);
+ QVERIFY(rules.rules().at(0).allowArchive);
+ QVERIFY(!rules.rules().at(1).allowArchive);
+ // Known now, so it is not ALSO carried as an unknown field.
+ QVERIFY(!rules.rules().at(0).unknown.contains(QStringLiteral("allow_archive")));
+
+ QVERIFY(rules.save(path));
+ QFile file(path);
+ QVERIFY(file.open(QIODevice::ReadOnly));
+ const QJsonArray saved = QJsonDocument::fromJson(file.readAll())
+ .object().value(QStringLiteral("rules")).toArray();
+ QCOMPARE(saved.at(0).toObject().value(QStringLiteral("allow_archive")),
+ QJsonValue(true));
+ QVERIFY(!saved.at(1).toObject().contains(QStringLiteral("allow_archive")));
+}
+
+void TestTagRules::allowArchiveIsDroppedFromARuleThatKeepsInbox()
+{
+ // A flag that does nothing is cleared by the WRITER, never rejected by a
+ // reader: the hook must not drop a whole rule over a harmless flag.
+ TagRule rule;
+ rule.id = QStringLiteral("x");
+ rule.query = QStringLiteral("from:a@example.org");
+ rule.add = { QStringLiteral("x") };
+ rule.allowArchive = true;
+
+ TagRules rules;
+ rules.setRules({ rule });
+ QVERIFY(TagRules::validate(rules.rules()).isEmpty());
+ const QString path = m_dir.filePath(QStringLiteral("cleared.json"));
+ QVERIFY(rules.save(path));
+
+ TagRules reloaded;
+ reloaded.load(path);
+ QVERIFY(!reloaded.rules().at(0).allowArchive);
+}
+
+void TestTagRules::hookWouldSkipMirrorsThePostNewRefusal()
+{
+ // Mirrors PROTECTED_REMOVALS in assets/hooks/post-new, which the Python
+ // suite tests against the hook itself.
+ TagRule rule;
+ rule.remove = { QStringLiteral("inbox") };
+ QVERIFY(TagRules::hookWouldSkip(rule));
+ rule.allowArchive = true;
+ QVERIFY(!TagRules::hookWouldSkip(rule));
+
+ rule.remove = { QStringLiteral("unread") };
+ QVERIFY(TagRules::hookWouldSkip(rule)); // never opted out
+
+ rule.remove = { QStringLiteral("promo") };
+ rule.allowArchive = false;
+ QVERIFY(!TagRules::hookWouldSkip(rule));
+}
+
QTEST_MAIN(TestTagRules)
#include "test_tagrules.moc"