summaryrefslogtreecommitdiffstats
path: root/tests/test_config.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-15 11:13:28 +0200
committerDanilo M. <danix@danix.xyz>2026-08-15 11:13:28 +0200
commit4757cc5780de1f7a7478cf07948f5659913d31b5 (patch)
tree80a0366d74cb98b5499abeb9209a689d8b834292 /tests/test_config.cpp
parente6e18849bd5aa49dbdd299982d39a92e7360d0aa (diff)
parent8ea004e67427693139ddd55384c7963d856650f0 (diff)
downloadqtmaildir-4757cc5780de1f7a7478cf07948f5659913d31b5.tar.gz
qtmaildir-4757cc5780de1f7a7478cf07948f5659913d31b5.zip
Merge branch 'builtin-filters': four built-in query filters
Items 93, 95 and 90. The query row starts with Unread, Inbox, Flagged and Sent, shipped by the application rather than pinned by the user, and they compose with the account dropdown instead of resetting it. Item 90 is absorbed: the button that cleared the account selection is no longer a saved query at all. Item 95 was found by hand testing this and is not caused by it: no entry in the saved query overflow menu had ever been runnable, because Qt emits no triggered for an action that owns a submenu.
Diffstat (limited to 'tests/test_config.cpp')
-rw-r--r--tests/test_config.cpp209
1 files changed, 203 insertions, 6 deletions
diff --git a/tests/test_config.cpp b/tests/test_config.cpp
index 3b094ba..6c1815c 100644
--- a/tests/test_config.cpp
+++ b/tests/test_config.cpp
@@ -95,6 +95,13 @@ private slots:
void allSentQueryIsEmptyWhenNoAccountHasOne();
void allSentQuerySkipsAccountsWithoutTheKey();
void allSentQueryJoinsEveryConfiguredAccount();
+ void aStoredGeneratedQueryIsUnpinnedNotDropped();
+ void everyBuiltinFilterIsAKnownGenerator();
+ void aFilterAcrossAllAccountsIsTheUnscopedQuery();
+ void aTagFilterScopedToAnAccountCarriesThatAccountsPath();
+ void sentScopedToAnAccountIsThatAccountsSentFolderAlone();
+ void sentScopedToAnAccountWithNoSentFolderMatchesNothing();
+ void aFilterKeepsItsViewMode();
void draftsQueryIsEmptyWithoutTheKey();
void draftsQuerySurvivesABracketedPath();
void allDraftsQuerySkipsAccountsWithoutTheKey();
@@ -1020,6 +1027,136 @@ void TestConfig::allSentQueryJoinsEveryConfiguredAccount()
QCOMPARE(all.count(QStringLiteral(" or ")), 1);
}
+/// Two accounts, one with a sent folder and one without. The second is the
+/// case that matters most: folderQuery() returns empty for an unset folder and
+/// an empty query means "match everything" to notmuch, so a filter that falls
+/// back to it silently shows the whole Maildir.
+static QString writeTwoAccounts(const QTemporaryDir &dir)
+{
+ return writeIni(dir, QStringLiteral(
+ "[account.work]\n"
+ "maildir=work\n"
+ "sent=Sent\n"
+ "\n"
+ "[account.personal]\n"
+ "maildir=personal\n"));
+}
+
+void TestConfig::everyBuiltinFilterIsAKnownGenerator()
+{
+ // The guard for every case below. A filter whose generator is not in the
+ // closed set loads with a reported problem and resolves to an empty query,
+ // which means "match everything": the assertions that follow would then be
+ // measuring a typo rather than the design.
+ Config config;
+ const QList<SavedQuery> filters = config.builtinFilters();
+
+ QCOMPARE(filters.size(), 4);
+
+ QStringList names;
+ for (const SavedQuery &filter : filters) {
+ QVERIFY2(filter.isGenerated(),
+ qPrintable(QStringLiteral("filter '%1' stores a query instead "
+ "of naming a generator")
+ .arg(filter.name)));
+ QVERIFY2(Config::isKnownGenerator(filter.generated),
+ qPrintable(QStringLiteral("filter '%1' names the unknown "
+ "generator '%2'")
+ .arg(filter.name, filter.generated)));
+ names.append(filter.name);
+ }
+
+ // The order is the row's order, left to right, and is fixed rather than
+ // configurable: item 94 removes the mixed row entirely, so a settings
+ // surface for this would be built and deleted inside two items.
+ QCOMPARE(names, (QStringList{ QStringLiteral("Unread"),
+ QStringLiteral("Inbox"),
+ QStringLiteral("Flagged"),
+ QStringLiteral("Sent") }));
+}
+
+void TestConfig::aFilterAcrossAllAccountsIsTheUnscopedQuery()
+{
+ QTemporaryDir dir;
+ Config config;
+ config.load(writeTwoAccounts(dir));
+
+ // An empty account key is "All accounts", which is what the dropdown holds
+ // by default.
+ const SavedQuery unread = config.builtinFilter(QStringLiteral("unread"));
+ QCOMPARE(config.resolvedQuery(unread, QString()),
+ QStringLiteral("tag:unread"));
+}
+
+void TestConfig::aTagFilterScopedToAnAccountCarriesThatAccountsPath()
+{
+ QTemporaryDir dir;
+ Config config;
+ config.load(writeTwoAccounts(dir));
+
+ // A tag filter has no path of its own, so scoping it is exactly what
+ // Account::scopedQuery() does and nothing more is needed.
+ const SavedQuery unread = config.builtinFilter(QStringLiteral("unread"));
+ QCOMPARE(config.resolvedQuery(unread, QStringLiteral("work")),
+ QStringLiteral("path:\"work/**\" and (tag:unread)"));
+}
+
+void TestConfig::sentScopedToAnAccountIsThatAccountsSentFolderAlone()
+{
+ QTemporaryDir dir;
+ Config config;
+ config.load(writeTwoAccounts(dir));
+
+ const SavedQuery sent = config.builtinFilter(QStringLiteral("sent"));
+ const QString scoped = config.resolvedQuery(sent, QStringLiteral("work"));
+
+ // The whole point of a per-account generator. Wrapping the all-accounts
+ // query instead would give
+ // path:"work/**" and (path:"work/Sent/**" or path:"personal/Sent/**")
+ // which returns the RIGHT ROWS, because path: is hierarchical and the
+ // personal half cannot match inside work. It is still wrong to build: it
+ // double-scopes and works by accident of the path syntax rather than by
+ // saying what is meant. A row-count assertion passes against it, which is
+ // why this asserts on the string.
+ QCOMPARE(scoped, QStringLiteral("path:\"work/Sent/**\""));
+ QVERIFY2(!scoped.contains(QStringLiteral("personal")),
+ "another account's sent folder leaked into a scoped Sent filter");
+ QCOMPARE(scoped.count(QStringLiteral("path:")), 1);
+}
+
+void TestConfig::sentScopedToAnAccountWithNoSentFolderMatchesNothing()
+{
+ QTemporaryDir dir;
+ Config config;
+ config.load(writeTwoAccounts(dir));
+
+ // `personal` configures no sent folder, so folderQuery() gives an empty
+ // string. Returned as-is that is "match everything" to notmuch, so Sent
+ // under this account would show the entire Maildir: the worst possible
+ // answer for a button labelled Sent.
+ const SavedQuery sent = config.builtinFilter(QStringLiteral("sent"));
+ const QString scoped =
+ config.resolvedQuery(sent, QStringLiteral("personal"));
+
+ QVERIFY2(!scoped.isEmpty(),
+ "an account with no sent folder resolved to an empty query, "
+ "which notmuch reads as 'match everything'");
+ QCOMPARE(scoped, Config::matchNothingQuery());
+}
+
+void TestConfig::aFilterKeepsItsViewMode()
+{
+ Config config;
+
+ // Sent lists MESSAGES, the other three list threads. Not a detail to
+ // unify: a thread would fold the user's sent message back into the
+ // conversation it belongs to, which is item 63's finding.
+ QVERIFY(config.builtinFilter(QStringLiteral("sent")).flat);
+ QVERIFY(!config.builtinFilter(QStringLiteral("unread")).flat);
+ QVERIFY(!config.builtinFilter(QStringLiteral("inbox")).flat);
+ QVERIFY(!config.builtinFilter(QStringLiteral("flagged")).flat);
+}
+
void TestConfig::draftsQueryIsEmptyWithoutTheKey()
{
// Optional for the same reason `sent` is, and more often absent: an
@@ -1327,6 +1464,55 @@ void TestConfig::jsonWinsOnceItExists()
QCOMPARE(queries.at(0).name, QStringLiteral("FromTheJson"));
}
+void TestConfig::aStoredGeneratedQueryIsUnpinnedNotDropped()
+{
+ // An existing install carries a Sent entry in queries.json: 0.19.0 migrated
+ // the hardcoded button into one. Item 93 ships Sent as a built-in filter,
+ // so that stored entry is now a DUPLICATE and would put two Sent buttons on
+ // the row, one editable and one not.
+ //
+ // Unpinned rather than deleted. This file's whole design is that a reader
+ // preserves what it does not own, and the user's instruction for their own
+ // redundant queries was the same: fold them into the menu, do not drop
+ // them. An unpin is reversible from the UI; a delete is not.
+ QTemporaryDir dir;
+ const QString path = writeIni(dir, QStringLiteral(
+ "[account.work]\n"
+ "maildir=work\n"
+ "sent=Sent\n"));
+ writeQueries(dir, QStringLiteral(R"({
+ "version": 1,
+ "queries": [
+ { "name": "Sent", "generated": "sent", "pinned": true },
+ { "name": "Mine", "query": "tag:todo", "pinned": true }
+ ]
+ })"));
+
+ Config config;
+ config.load(path);
+
+ const QList<SavedQuery> queries = config.savedQueries();
+ QCOMPARE(queries.size(), 2);
+
+ bool sawSent = false;
+ for (const SavedQuery &query : queries) {
+ if (query.generated != QStringLiteral("sent"))
+ continue;
+ sawSent = true;
+ QVERIFY2(!query.pinned,
+ "the stored Sent entry is still a button beside the built-in "
+ "filter of the same name");
+ }
+ QVERIFY2(sawSent, "the stored Sent entry was DROPPED rather than unpinned");
+
+ // The user's own query is untouched: only the entry duplicating a built-in
+ // filter is unpinned.
+ for (const SavedQuery &query : queries) {
+ if (query.name == QStringLiteral("Mine"))
+ QVERIFY2(query.pinned, "an unrelated pinned query was unpinned");
+ }
+}
+
void TestConfig::malformedQueriesFileIsAProblemNotACrash()
{
QTemporaryDir dir;
@@ -1540,13 +1726,24 @@ void TestConfig::migrationAddsSentWhenAnAccountHasOne()
Config config;
config.load(path);
+ // The migration used to invent a generated Sent entry here, so the
+ // hardcoded button could be reordered, renamed or removed like any other
+ // row. Item 93 ships Sent as one of four BUILT-IN filters instead, so
+ // migrating one as well would put two Sent buttons on the row: one the
+ // user's to edit and one not.
+ //
+ // Nothing is lost. The built-in resolves through the same generator, so it
+ // still follows the accounts, and it now composes with the account dropdown
+ // rather than resetting it.
const QList<SavedQuery> queries = config.savedQueries();
- QCOMPARE(queries.size(), 2);
- // Last, where the button already sat: after the saved queries.
- QCOMPARE(queries.at(1).name, QStringLiteral("Sent"));
- QVERIFY(queries.at(1).isGenerated());
- QVERIFY(queries.at(1).pinned);
- QVERIFY(queries.at(1).flat);
+ QCOMPARE(queries.size(), 1);
+ QCOMPARE(queries.at(0).name, QStringLiteral("Inbox"));
+
+ for (const SavedQuery &query : queries) {
+ QVERIFY2(!query.isGenerated(),
+ "the migration invented a generated entry that now duplicates "
+ "a built-in filter");
+ }
}
/// Today the button is hidden entirely when no account configures a sent