diff options
Diffstat (limited to 'tests/test_signatures.cpp')
| -rw-r--r-- | tests/test_signatures.cpp | 294 |
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" |
