aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_signatures.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-25 09:13:21 +0200
committerDanilo M. <danix@danix.xyz>2026-08-25 09:13:21 +0200
commit3e196bbaf6a5f3ebf6597e0ada1f559d24627ca6 (patch)
tree5d76949bf34ab581826f0f699d60e0fc45b3e175 /tests/test_signatures.cpp
parentf8d136432466479c892841bc73bd85e58674da86 (diff)
parent0a26961f9a7ae6ab98051e182b92e64165758cf1 (diff)
downloadqtmaildir-3e196bbaf6a5f3ebf6597e0ada1f559d24627ca6.tar.gz
qtmaildir-3e196bbaf6a5f3ebf6597e0ada1f559d24627ca6.zip
Merge branch 'signatures'
Signatures (item 152): one markdown file per signature under ~/.config/qtmaildir/signatures/, spliced into the composer buffer and chosen from a switch on the editor bar. [compose] signature seeds a new message, [account.<key>] signature overrides per account, and [compose] signature_position picks end or above_quote. Also carries three fixes found by hand-testing it: a saved draft is indexed so it appears without a sync (item 158), the Drafts filter lists messages rather than threads so a draft reply can be opened (item 159), and a resumed draft no longer re-seeds its signature on a From: change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UUQS6n3cmsFrsjCNmwNtf8
Diffstat (limited to 'tests/test_signatures.cpp')
-rw-r--r--tests/test_signatures.cpp294
1 files changed, 294 insertions, 0 deletions
diff --git a/tests/test_signatures.cpp b/tests/test_signatures.cpp
new file mode 100644
index 0000000..47b404b
--- /dev/null
+++ b/tests/test_signatures.cpp
@@ -0,0 +1,294 @@
+/*
+ * 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 <QTemporaryDir>
+
+#include "signatures.h"
+
+class TestSignatures : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void namesAreTheFileStemsSorted();
+ void namesIgnoreFilesThatAreNotMarkdown();
+ void aMissingDirectoryHasNoNames();
+ void textIsTheFileContent();
+ void textOfAnUnknownNameIsEmpty();
+ void insertingAtTheEndAppendsAfterADelimiter();
+ void insertingAboveTheQuotePutsItBeforeTheFirstQuotedLine();
+ void insertingAboveTheQuoteWithNoQuoteIsTheSameAsEnd();
+ void insertingNothingLeavesTheBufferAlone();
+ void switchingReplacesAKnownSignature();
+ void switchingReplacesAKnownSignatureAboveAQuote();
+ void selectingNoneRemovesAKnownSignature();
+ void aBlockMatchingNoKnownSignatureIsNotRemoved();
+ void aDelimiterInsideTheQuoteIsNotTheSignature();
+ void aSignatureReadBackFromDiskIsReplaced();
+
+private:
+ /// Writes \p files as name -> content into a fresh temporary directory.
+ static void write(const QTemporaryDir &dir,
+ const QList<QPair<QString, QString>> &files);
+};
+
+void TestSignatures::write(const QTemporaryDir &dir,
+ const QList<QPair<QString, QString>> &files)
+{
+ for (const auto &entry : files) {
+ QFile file(dir.path() + QStringLiteral("/") + entry.first);
+ QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Text));
+ file.write(entry.second.toUtf8());
+ file.close();
+ }
+}
+
+void TestSignatures::namesAreTheFileStemsSorted()
+{
+ QTemporaryDir dir;
+ QVERIFY(dir.isValid());
+ write(dir, { { QStringLiteral("work.md"), QStringLiteral("Work") },
+ { QStringLiteral("brief.md"), QStringLiteral("Brief") } });
+
+ QCOMPARE(Signatures::names(dir.path()),
+ QStringList({ QStringLiteral("brief"), QStringLiteral("work") }));
+}
+
+void TestSignatures::namesIgnoreFilesThatAreNotMarkdown()
+{
+ QTemporaryDir dir;
+ QVERIFY(dir.isValid());
+ write(dir, { { QStringLiteral("work.md"), QStringLiteral("Work") },
+ { QStringLiteral("notes.txt"), QStringLiteral("Not one") },
+ { QStringLiteral("README"), QStringLiteral("Nor this") } });
+
+ QCOMPARE(Signatures::names(dir.path()),
+ QStringList({ QStringLiteral("work") }));
+}
+
+void TestSignatures::aMissingDirectoryHasNoNames()
+{
+ QTemporaryDir dir;
+ QVERIFY(dir.isValid());
+ const QString missing = dir.path() + QStringLiteral("/nothing-here");
+
+ QVERIFY(Signatures::names(missing).isEmpty());
+}
+
+void TestSignatures::textIsTheFileContent()
+{
+ QTemporaryDir dir;
+ QVERIFY(dir.isValid());
+ write(dir, { { QStringLiteral("work.md"),
+ QStringLiteral("Jane Doe\n**qtmaildir**\n") } });
+
+ QCOMPARE(Signatures::text(dir.path(), QStringLiteral("work")),
+ QStringLiteral("Jane Doe\n**qtmaildir**\n"));
+}
+
+void TestSignatures::textOfAnUnknownNameIsEmpty()
+{
+ QTemporaryDir dir;
+ QVERIFY(dir.isValid());
+
+ QVERIFY(Signatures::text(dir.path(), QStringLiteral("absent")).isEmpty());
+}
+
+void TestSignatures::insertingAtTheEndAppendsAfterADelimiter()
+{
+ const QString buffer = QStringLiteral("Hello.\n");
+
+ const QString result = Signatures::replace(
+ buffer, QStringLiteral("Jane Doe"), {}, Signatures::Position::End);
+
+ QCOMPARE(result, QStringLiteral("Hello.\n\n-- \nJane Doe"));
+}
+
+void TestSignatures::insertingAboveTheQuotePutsItBeforeTheFirstQuotedLine()
+{
+ const QString buffer = QStringLiteral(
+ "My reply.\n"
+ "\n"
+ "On Mon, someone wrote:\n"
+ "> the original\n"
+ "> second line\n");
+
+ const QString result = Signatures::replace(
+ buffer, QStringLiteral("Jane Doe"), {},
+ Signatures::Position::AboveQuote);
+
+ // Before the QUOTED lines, and the attribution stays with the quote it
+ // introduces: it is the line the quote hangs from, not part of the reply.
+ QCOMPARE(result, QStringLiteral(
+ "My reply.\n"
+ "\n"
+ "-- \n"
+ "Jane Doe\n"
+ "\n"
+ "On Mon, someone wrote:\n"
+ "> the original\n"
+ "> second line\n"));
+}
+
+void TestSignatures::insertingAboveTheQuoteWithNoQuoteIsTheSameAsEnd()
+{
+ const QString buffer = QStringLiteral("A new message.\n");
+
+ const QString above = Signatures::replace(
+ buffer, QStringLiteral("Jane Doe"), {},
+ Signatures::Position::AboveQuote);
+ const QString end = Signatures::replace(
+ buffer, QStringLiteral("Jane Doe"), {}, Signatures::Position::End);
+
+ QCOMPARE(above, end);
+}
+
+void TestSignatures::insertingNothingLeavesTheBufferAlone()
+{
+ const QString buffer = QStringLiteral("Hello.\n");
+
+ QCOMPARE(Signatures::replace(buffer, QString(), {},
+ Signatures::Position::End),
+ buffer);
+}
+
+void TestSignatures::switchingReplacesAKnownSignature()
+{
+ const QStringList known = { QStringLiteral("Jane Doe"),
+ QStringLiteral("Jane Doe\nqtmaildir") };
+ const QString buffer = QStringLiteral("Hello.\n\n-- \nJane Doe");
+
+ const QString result = Signatures::replace(
+ buffer, QStringLiteral("Jane Doe\nqtmaildir"), known,
+ Signatures::Position::End);
+
+ QCOMPARE(result,
+ QStringLiteral("Hello.\n\n-- \nJane Doe\nqtmaildir"));
+}
+
+void TestSignatures::switchingReplacesAKnownSignatureAboveAQuote()
+{
+ const QStringList known = { QStringLiteral("Jane Doe"),
+ QStringLiteral("Brief") };
+ const QString buffer = QStringLiteral(
+ "My reply.\n"
+ "\n"
+ "-- \n"
+ "Jane Doe\n"
+ "\n"
+ "On Mon, someone wrote:\n"
+ "> the original\n");
+
+ const QString result = Signatures::replace(
+ buffer, QStringLiteral("Brief"), known,
+ Signatures::Position::AboveQuote);
+
+ QCOMPARE(result, QStringLiteral(
+ "My reply.\n"
+ "\n"
+ "-- \n"
+ "Brief\n"
+ "\n"
+ "On Mon, someone wrote:\n"
+ "> the original\n"));
+}
+
+void TestSignatures::selectingNoneRemovesAKnownSignature()
+{
+ const QStringList known = { QStringLiteral("Jane Doe") };
+ const QString buffer = QStringLiteral("Hello.\n\n-- \nJane Doe");
+
+ const QString result = Signatures::replace(
+ buffer, QString(), known, Signatures::Position::End);
+
+ QCOMPARE(result, QStringLiteral("Hello.\n"));
+}
+
+void TestSignatures::aBlockMatchingNoKnownSignatureIsNotRemoved()
+{
+ // THE test for the data-loss guard, and it must not be dropped. A "-- "
+ // reaches a buffer without the user ever choosing a signature, pasted in
+ // with quoted text from another client. Replacing from there would delete
+ // everything after it silently.
+ const QStringList known = { QStringLiteral("Jane Doe") };
+ const QString buffer = QStringLiteral(
+ "Hello.\n"
+ "\n"
+ "-- \n"
+ "text the user pasted and wants to keep");
+
+ const QString result = Signatures::replace(
+ buffer, QStringLiteral("Jane Doe"), known, Signatures::Position::End);
+
+ // The user's text survives, and the signature is ADDED. A wrong guess
+ // produces a visible duplicate, never a deletion.
+ QVERIFY(result.contains(
+ QStringLiteral("text the user pasted and wants to keep")));
+ QVERIFY(result.endsWith(QStringLiteral("-- \nJane Doe")));
+}
+
+void TestSignatures::aDelimiterInsideTheQuoteIsNotTheSignature()
+{
+ // The quoted original carries the sender's own signature, quoted. A tail
+ // rule would find it, and under End it would append after it; the block
+ // must not be treated as this message's signature whichever way it goes.
+ //
+ // This test DOCUMENTS the case rather than pinning it, and that is worth
+ // knowing before trying to strengthen it. Two mutations were measured
+ // against it and both stayed green: trimming the delimiter comparison so
+ // that "> -- " matches, and making the quoted text one of the known
+ // signatures so the match guard could not be what refuses the removal.
+ // Neither changes the output, because blockEnd() stops the block at the
+ // quote, so the quoted signature survives whether or not the delimiter
+ // inside it is recognised. The behaviour is correct under both, and no
+ // assertion on the result can separate them.
+ const QStringList known = { QStringLiteral("Jane Doe") };
+ const QString buffer = QStringLiteral(
+ "My reply.\n"
+ "\n"
+ "On Mon, someone wrote:\n"
+ "> the original\n"
+ "> -- \n"
+ "> Their Name\n");
+
+ const QString result = Signatures::replace(
+ buffer, QStringLiteral("Jane Doe"), known, Signatures::Position::End);
+
+ QVERIFY(result.contains(QStringLiteral("> -- \n> Their Name")));
+ QVERIFY(result.endsWith(QStringLiteral("-- \nJane Doe")));
+}
+
+void TestSignatures::aSignatureReadBackFromDiskIsReplaced()
+{
+ // known here is what knownSignatures() produces: text() verbatim, carrying
+ // the trailing newline every editor writes into a file. The block scan
+ // treats a trailing blank line as separation rather than text, so a naive
+ // match compares "Jane Doe" against "Jane Doe\n" and silently fails, and
+ // switching then APPENDS a second signature instead of replacing the first.
+ const QStringList known = { QStringLiteral("Jane Doe\n") };
+ const QString buffer = QStringLiteral("Hello.\n\n-- \nJane Doe\n");
+
+ const QString result = Signatures::replace(
+ buffer, QStringLiteral("Brief"), known, Signatures::Position::End);
+
+ QCOMPARE(result, QStringLiteral("Hello.\n\n-- \nBrief"));
+}
+
+QTEST_MAIN(TestSignatures)
+#include "test_signatures.moc"