aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-29 17:08:56 +0200
committerDanilo M. <danix@danix.xyz>2026-09-29 17:08:56 +0200
commit0402b5b683c7412daace2414f4b903b677b5405c (patch)
tree897846e05de9e784fb2b9d584c82a3e67916c1f8 /tests
parent0cf2c008d46ca2e935987ecfa51a3e712d40420e (diff)
downloadqtmaildir-0402b5b683c7412daace2414f4b903b677b5405c.tar.gz
qtmaildir-0402b5b683c7412daace2414f4b903b677b5405c.zip
feat: resolve a Message-ID to its thread id
For --message (item 200), which knows an id and needs the conversation: opening a message means opening its thread with that message selected, never an id: query showing one card out of a conversation (item 91). The id is quoted before it reaches notmuch, unlike every other id in this class. Those came out of notmuch; this one comes off another program's command line, and notmuch parses garbage happily while matching nothing, so an id carrying query syntax would be read AS syntax with no error anywhere. The quoting is local rather than SearchTerm::quote(): that escapes an embedded quote with a backslash, which notmuch does not honour inside a phrase, so the quote closed the phrase and the injected query matched a real thread (the new test failed against it). notmuch escapes a quote by doubling it. threadIdForTesting() keeps its name and gains a shared helper rather than being promoted: it is documented as not being a slot, and the new entry point has to answer asynchronously. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'tests')
-rw-r--r--tests/test_notmuchworker.cpp59
1 files changed, 59 insertions, 0 deletions
diff --git a/tests/test_notmuchworker.cpp b/tests/test_notmuchworker.cpp
index b2810d1..10f8dbc 100644
--- a/tests/test_notmuchworker.cpp
+++ b/tests/test_notmuchworker.cpp
@@ -70,6 +70,9 @@ private slots:
void loadMessageReturnsOnlyThatMessage();
void loadMessageOnAnUnknownIdReturnsNothing();
+ void resolvingAMessageIdAnswersItsThreadId();
+ void resolvingAnUnknownMessageIdAnswersEmpty();
+ void resolvingAMessageIdQuotesTheId();
void aQueryCarriesEachThreadsFirstMessageId();
void aSentQueryCarriesTheMatchedMessageNotTheThreadsFirst();
void queryCarriesTheFirstMessageSender();
@@ -459,6 +462,62 @@ void TestNotmuchWorker::loadMessageOnAnUnknownIdReturnsNothing()
QCOMPARE(errors.count(), 0);
}
+void TestNotmuchWorker::resolvingAMessageIdAnswersItsThreadId()
+{
+ // What --message needs (item 200): the CLI knows a Message-ID and the
+ // window needs the thread id, because opening the message means opening
+ // its conversation with that message selected.
+ NotmuchWorker worker(m_fixture.configPath());
+ QSignalSpy resolved(&worker, &NotmuchWorker::threadForMessageResolved);
+
+ worker.resolveThreadForMessage(QStringLiteral("a2@example.org"));
+
+ QCOMPARE(resolved.count(), 1);
+ QCOMPARE(resolved.first().at(0).toString(), QStringLiteral("a2@example.org"));
+
+ // a2 is a REPLY, so its thread id is the thread's, not its own. It must
+ // agree with the thread a1 resolves to.
+ const QString threadId = resolved.first().at(1).toString();
+ QVERIFY(!threadId.isEmpty());
+ QCOMPARE(threadId,
+ worker.threadIdForTesting(QStringLiteral("id:a1@example.org")));
+}
+
+void TestNotmuchWorker::resolvingAnUnknownMessageIdAnswersEmpty()
+{
+ // Answers rather than staying silent: the window shows the miss in the
+ // status bar, and a slot that never replies would leave it waiting.
+ NotmuchWorker worker(m_fixture.configPath());
+ QSignalSpy resolved(&worker, &NotmuchWorker::threadForMessageResolved);
+ QSignalSpy errors(&worker, &NotmuchWorker::errorOccurred);
+
+ worker.resolveThreadForMessage(QStringLiteral("nonexistent@example.org"));
+
+ QCOMPARE(resolved.count(), 1);
+ QCOMPARE(resolved.first().at(0).toString(),
+ QStringLiteral("nonexistent@example.org"));
+ QVERIFY(resolved.first().at(1).toString().isEmpty());
+ QCOMPARE(errors.count(), 0);
+}
+
+void TestNotmuchWorker::resolvingAMessageIdQuotesTheId()
+{
+ // The id comes from argv, not from notmuch, and notmuch's parser rejects
+ // almost nothing: an unquoted id carrying query syntax would be PARSED as
+ // syntax. It must stay a miss, with no error.
+ NotmuchWorker worker(m_fixture.configPath());
+ QSignalSpy resolved(&worker, &NotmuchWorker::threadForMessageResolved);
+ QSignalSpy errors(&worker, &NotmuchWorker::errorOccurred);
+
+ worker.resolveThreadForMessage(
+ QStringLiteral("a2@example.org\" or from:alice or \"x"));
+
+ QCOMPARE(resolved.count(), 1);
+ QVERIFY2(resolved.first().at(1).toString().isEmpty(),
+ "an id carrying query syntax resolved to a thread: it was not quoted");
+ QCOMPARE(errors.count(), 0);
+}
+
void TestNotmuchWorker::aQueryCarriesEachThreadsFirstMessageId()
{
// The root card IS the thread's first message, so selecting it must be