aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-29 16:52:59 +0200
committerDanilo M. <danix@danix.xyz>2026-09-29 16:52:59 +0200
commite168af8d7d9a500759565719f16ed188ba8a8bcd (patch)
tree0246e655d2bcc1051d9e70c263f39692dd3d6e55 /tests
parentd2793dd583041941348d5a984764160ddd48102b (diff)
downloadqtmaildir-e168af8d7d9a500759565719f16ed188ba8a8bcd.tar.gz
qtmaildir-e168af8d7d9a500759565719f16ed188ba8a8bcd.zip
feat: parse the launch selectors as a value type
--account, --thread and --message, plus the payload that crosses the socket. A value type with no GUI dependency: it is parsed before QApplication exists and both halves need tests no window has to be built for. QDataStream rather than a line-based payload, because a Message-ID may contain a newline. Every read is status-checked, which is what catches a truncated payload: a short read otherwise leaves the fields default-constructed and a half-written id would be applied as an empty one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'tests')
-rw-r--r--tests/CMakeLists.txt1
-rw-r--r--tests/test_launchselectors.cpp173
2 files changed, 174 insertions, 0 deletions
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index bdea4ed..8cc4d8b 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -92,6 +92,7 @@ add_qtmaildir_test(composecontext)
add_qtmaildir_test(composewindow)
add_qtmaildir_test(formattoolbar)
add_qtmaildir_test(senddialog)
+add_qtmaildir_test(launchselectors)
add_qtmaildir_test(translations)
# Asserts on the tracked .ts rather than the generated .qm: an untranslated
# string is dropped by lrelease, so it is invisible in the .qm and shows up
diff --git a/tests/test_launchselectors.cpp b/tests/test_launchselectors.cpp
new file mode 100644
index 0000000..08b89ad
--- /dev/null
+++ b/tests/test_launchselectors.cpp
@@ -0,0 +1,173 @@
+/*
+ * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <QtTest>
+
+#include "launchselectors.h"
+
+/// The command line and the socket payload, over values. No window, no
+/// QApplication: this is the half of item 200 that can be asserted exactly,
+/// which is why it is its own unit rather than code inside main.cpp.
+class TestLaunchSelectors : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void anEmptyCommandLineSelectsNothing();
+ void eachSelectorIsParsed();
+ void theThreeSelectorsCompose();
+ void anUnknownOptionIsReportedNotFatal();
+ void aPayloadRoundTrips();
+ void anEmptyPayloadRoundTripsToNothing();
+ void aTruncatedPayloadIsRejected();
+ void anOversizedPayloadIsRejected();
+};
+
+void TestLaunchSelectors::anEmptyCommandLineSelectsNothing()
+{
+ QString error;
+ const LaunchSelectors selectors =
+ LaunchSelectors::parse({ QStringLiteral("qtmaildir") }, &error);
+
+ QVERIFY(error.isEmpty());
+ QVERIFY(selectors.isEmpty());
+ QVERIFY(selectors.account.isEmpty());
+ QVERIFY(selectors.threadId.isEmpty());
+ QVERIFY(selectors.messageId.isEmpty());
+}
+
+void TestLaunchSelectors::eachSelectorIsParsed()
+{
+ QString error;
+
+ const LaunchSelectors account = LaunchSelectors::parse(
+ { QStringLiteral("qtmaildir"), QStringLiteral("--account"),
+ QStringLiteral("work") }, &error);
+ QVERIFY(error.isEmpty());
+ QCOMPARE(account.account, QStringLiteral("work"));
+ QVERIFY(!account.isEmpty());
+
+ const LaunchSelectors thread = LaunchSelectors::parse(
+ { QStringLiteral("qtmaildir"), QStringLiteral("--thread"),
+ QStringLiteral("0000000000001a2b") }, &error);
+ QVERIFY(error.isEmpty());
+ QCOMPARE(thread.threadId, QStringLiteral("0000000000001a2b"));
+
+ const LaunchSelectors message = LaunchSelectors::parse(
+ { QStringLiteral("qtmaildir"), QStringLiteral("--message"),
+ QStringLiteral("<abc@example.org>") }, &error);
+ QVERIFY(error.isEmpty());
+ QCOMPARE(message.messageId, QStringLiteral("<abc@example.org>"));
+}
+
+void TestLaunchSelectors::theThreeSelectorsCompose()
+{
+ // They are not exclusive: "open this message, in this account's view" is
+ // one sensible request, and the spec says they compose.
+ QString error;
+ const LaunchSelectors selectors = LaunchSelectors::parse(
+ { QStringLiteral("qtmaildir"),
+ QStringLiteral("--account"), QStringLiteral("work"),
+ QStringLiteral("--thread"), QStringLiteral("00001a2b"),
+ QStringLiteral("--message"), QStringLiteral("<abc@example.org>") },
+ &error);
+
+ QVERIFY(error.isEmpty());
+ QCOMPARE(selectors.account, QStringLiteral("work"));
+ QCOMPARE(selectors.threadId, QStringLiteral("00001a2b"));
+ QCOMPARE(selectors.messageId, QStringLiteral("<abc@example.org>"));
+}
+
+void TestLaunchSelectors::anUnknownOptionIsReportedNotFatal()
+{
+ // Reported so main() can print it, and NOT a crash or a silent ignore.
+ // Today's strcmp loop ignores everything it does not know, which is how a
+ // typo currently produces a normal window and no clue.
+ QString error;
+ const LaunchSelectors selectors = LaunchSelectors::parse(
+ { QStringLiteral("qtmaildir"), QStringLiteral("--nonsense") }, &error);
+
+ QVERIFY(!error.isEmpty());
+ QVERIFY(selectors.isEmpty());
+}
+
+void TestLaunchSelectors::aPayloadRoundTrips()
+{
+ // What crosses the socket. A round trip is the whole contract: the values
+ // that go in are the values that come out, including one with an embedded
+ // newline, which is what defeats a line-based format.
+ LaunchSelectors original;
+ original.account = QStringLiteral("work");
+ original.threadId = QStringLiteral("00001a2b");
+ original.messageId = QStringLiteral("<a\nb@example.org>");
+
+ QString error;
+ const LaunchSelectors parsed =
+ LaunchSelectors::fromPayload(original.toPayload(), &error);
+
+ QVERIFY(error.isEmpty());
+ QCOMPARE(parsed.account, original.account);
+ QCOMPARE(parsed.threadId, original.threadId);
+ QCOMPARE(parsed.messageId, original.messageId);
+}
+
+void TestLaunchSelectors::anEmptyPayloadRoundTripsToNothing()
+{
+ // A bare `qtmaildir` with a running instance still sends a payload: it
+ // means "raise yourself", which is a real request and not an error.
+ QString error;
+ const LaunchSelectors parsed =
+ LaunchSelectors::fromPayload(LaunchSelectors().toPayload(), &error);
+
+ QVERIFY(error.isEmpty());
+ QVERIFY(parsed.isEmpty());
+}
+
+void TestLaunchSelectors::aTruncatedPayloadIsRejected()
+{
+ // The socket hands over whatever it is given. A half-written payload must
+ // be refused rather than half-applied.
+ LaunchSelectors original;
+ original.account = QStringLiteral("work");
+ const QByteArray payload = original.toPayload();
+ QVERIFY(payload.size() > 4);
+
+ QString error;
+ const LaunchSelectors parsed =
+ LaunchSelectors::fromPayload(payload.left(payload.size() - 2), &error);
+
+ QVERIFY(!error.isEmpty());
+ QVERIFY(parsed.isEmpty());
+}
+
+void TestLaunchSelectors::anOversizedPayloadIsRejected()
+{
+ // A cap, because a local socket will hand over as much as the peer sends.
+ // The peer is the user's own process, so this is not a hostile-input
+ // defence; it is what stops a confused writer from being read as a
+ // gigabyte of selector.
+ QString error;
+ const LaunchSelectors parsed = LaunchSelectors::fromPayload(
+ QByteArray(LaunchSelectors::kMaxPayloadBytes + 1, 'x'), &error);
+
+ QVERIFY(!error.isEmpty());
+ QVERIFY(parsed.isEmpty());
+}
+
+QTEST_MAIN(TestLaunchSelectors)
+#include "test_launchselectors.moc"