summaryrefslogtreecommitdiffstats
path: root/tests/test_notmuchworker.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-13 20:32:47 +0200
committerDanilo M. <danix@danix.xyz>2026-09-13 20:32:47 +0200
commitc7a2444ed5b9bb30b9f10d3e8dff8a6d6b6bb16a (patch)
treede416968b7dbb90e6ec4a8e1c55743ab71eea533 /tests/test_notmuchworker.cpp
parent533c9243bede053f4ea7e1f20de13e72c2b939b8 (diff)
downloadqtmaildir-c7a2444ed5b9bb30b9f10d3e8dff8a6d6b6bb16a.tar.gz
qtmaildir-c7a2444ed5b9bb30b9f10d3e8dff8a6d6b6bb16a.zip
feat: mark spam moves mail to the account's spam folder
Mark spam was a tag-only action that added the spam tag and removed inbox, so a message marked as spam stayed in the inbox on disk. It now MOVES the file into the account's configured spam folder, exactly mirroring Delete: the account-relative spam key is the destination, the move records moved-from: with the origin, and unread and inbox are stripped in the same confirmed write so one undo returns the folder and the tags together. NotmuchWorker::moveMessages already handled a folder generically and applyTags already overwrote an older moved-from: tag, so the worker needed no change; the tests pin that behaviour for the spam destination. Five existing tests used spam as a worker-free, tag-only stand-in for the old Delete. Since spam is now a move too, they are retargeted to flag, the remaining selection-scoped tag-only action.
Diffstat (limited to 'tests/test_notmuchworker.cpp')
-rw-r--r--tests/test_notmuchworker.cpp105
1 files changed, 105 insertions, 0 deletions
diff --git a/tests/test_notmuchworker.cpp b/tests/test_notmuchworker.cpp
index 2877894..b2810d1 100644
--- a/tests/test_notmuchworker.cpp
+++ b/tests/test_notmuchworker.cpp
@@ -100,6 +100,8 @@ private slots:
void requestFoldersOnUnreadableConfigEmitsError();
void moveMessagesRelocatesTheFile();
+ void moveMessagesToSpamRelocatesTheFileAndTagsIt();
+ void aSecondMoveToSpamKeepsOnlyTheNewestOriginTag();
void moveMessagesReindexesAtTheNewPath();
void moveMessagesKeepsTheMessagesTags();
void moveMessagesReportsOnlyWhatMoved();
@@ -1796,6 +1798,109 @@ void TestNotmuchWorker::moveMessagesRelocatesTheFile()
QVERIFY(!QFile::exists(before));
}
+void TestNotmuchWorker::moveMessagesToSpamRelocatesTheFileAndTagsIt()
+{
+ // Mark spam is Delete's sibling, and its own fixture rather than the
+ // shared one: this needs an UNREAD message so the `unread` removal the
+ // account's spam folder config implies is actually observable. The shared
+ // fixture's movable messages are all read, which would make that assertion
+ // pass against nothing.
+ NotmuchFixture fixture;
+ QVERIFY(fixture.isValid());
+ const QString id = QStringLiteral("spam1@example.org");
+ QVERIFY(fixture.addMessage(QStringLiteral("inbox"), id,
+ QStringLiteral("Suspect"),
+ QStringLiteral("Erin <erin@example.org>"),
+ QStringLiteral("Sun, 7 Jun 2026 10:00:00 +0000"),
+ QStringLiteral("body"), true));
+ QVERIFY2(fixture.index(), qPrintable(fixture.error()));
+
+ QVERIFY2(tagsOf(id, fixture.configPath()).contains(QStringLiteral("unread")),
+ "the fixture message is already read, so the unread removal below "
+ "would prove nothing");
+
+ NotmuchWorker worker(fixture.configPath());
+ QSignalSpy moved(&worker, &NotmuchWorker::messagesMoved);
+ QSignalSpy errors(&worker, &NotmuchWorker::errorOccurred);
+
+ worker.moveMessages({ id }, QStringLiteral("spam"));
+ QVERIFY2(errors.isEmpty(), qPrintable(errors.value(0).value(0).toString()));
+
+ QCOMPARE(moved.size(), 1);
+ QCOMPARE(moved.first().at(0).toStringList(), QStringList{ id });
+ QCOMPARE(moved.first().at(1).toString(), QStringLiteral("spam"));
+
+ const QString expectedDir =
+ fixture.maildirPath() + QStringLiteral("/spam/cur");
+ const QString after = fileOf(id, fixture.configPath());
+ QVERIFY2(!after.isEmpty(),
+ "the message is not in the database after the move");
+ QCOMPARE(QFileInfo(after).absolutePath(), expectedDir);
+ QVERIFY2(QFile::exists(after), qPrintable(after));
+
+ // The tag half travels with the move the way onMessagesMoved() composes
+ // it: `spam` and the origin in, `unread` and `inbox` out.
+ worker.applyTags(TagChange{ { id },
+ { QStringLiteral("spam"),
+ QStringLiteral("moved-from:inbox") },
+ { QStringLiteral("unread"),
+ QStringLiteral("inbox") },
+ QStringLiteral("Mark spam") });
+ QVERIFY2(errors.isEmpty(), qPrintable(errors.value(0).value(0).toString()));
+
+ const QStringList tags = tagsOf(id, fixture.configPath());
+ QVERIFY(tags.contains(QStringLiteral("spam")));
+ QVERIFY(tags.contains(QStringLiteral("moved-from:inbox")));
+ QVERIFY(!tags.contains(QStringLiteral("unread")));
+}
+
+void TestNotmuchWorker::aSecondMoveToSpamKeepsOnlyTheNewestOriginTag()
+{
+ // The overwrite rule, reached through a second move: inbox -> spam -> a
+ // later move that writes `moved-from:Spam` must leave exactly ONE
+ // `moved-from:` tag, the newest. Two would make Restore's first-match scan
+ // pick an origin arbitrarily, and the message would go home by a coin toss.
+ NotmuchFixture fixture;
+ QVERIFY(fixture.isValid());
+ const QString id = QStringLiteral("reorigin@example.org");
+ QVERIFY(fixture.addMessage(QStringLiteral("inbox"), id,
+ QStringLiteral("Travelled"),
+ QStringLiteral("Erin <erin@example.org>"),
+ QStringLiteral("Sun, 7 Jun 2026 10:00:00 +0000"),
+ QStringLiteral("body"), true));
+ QVERIFY2(fixture.index(), qPrintable(fixture.error()));
+
+ NotmuchWorker worker(fixture.configPath());
+ QSignalSpy errors(&worker, &NotmuchWorker::errorOccurred);
+
+ // An earlier move left an origin behind.
+ worker.applyTags(TagChange{ { id },
+ { QStringLiteral("moved-from:inbox") },
+ {},
+ QStringLiteral("Earlier move") });
+ QVERIFY2(errors.isEmpty(), qPrintable(errors.value(0).value(0).toString()));
+
+ // The move under test: a new origin lands while the old one is still there.
+ worker.applyTags(TagChange{ { id },
+ { QStringLiteral("spam"),
+ QStringLiteral("moved-from:Spam") },
+ { QStringLiteral("unread"),
+ QStringLiteral("inbox") },
+ QStringLiteral("Mark spam") });
+ QVERIFY2(errors.isEmpty(), qPrintable(errors.value(0).value(0).toString()));
+
+ const QStringList tags = tagsOf(id, fixture.configPath());
+ QVERIFY(!tags.contains(QStringLiteral("moved-from:inbox")));
+ QVERIFY(tags.contains(QStringLiteral("moved-from:Spam")));
+
+ int origins = 0;
+ for (const QString &tag : tags) {
+ if (tag.startsWith(QStringLiteral("moved-from:")))
+ ++origins;
+ }
+ QCOMPARE(origins, 1);
+}
+
void TestNotmuchWorker::purgeMessagesDoesNotClaimAnIdItCouldNotDelete()
{
// The report drives what the UI tells the user, and the one number they