diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-29 16:52:59 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-29 16:52:59 +0200 |
| commit | e168af8d7d9a500759565719f16ed188ba8a8bcd (patch) | |
| tree | 0246e655d2bcc1051d9e70c263f39692dd3d6e55 /tests/test_launchselectors.cpp | |
| parent | d2793dd583041941348d5a984764160ddd48102b (diff) | |
| download | qtmaildir-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/test_launchselectors.cpp')
| -rw-r--r-- | tests/test_launchselectors.cpp | 173 |
1 files changed, 173 insertions, 0 deletions
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" |
