diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_maildirname.cpp | 96 | ||||
| -rw-r--r-- | tests/test_mainwindow.cpp | 78 |
2 files changed, 174 insertions, 0 deletions
diff --git a/tests/test_maildirname.cpp b/tests/test_maildirname.cpp index dcc8fab..a8e4c01 100644 --- a/tests/test_maildirname.cpp +++ b/tests/test_maildirname.cpp @@ -18,7 +18,10 @@ #include "maildirname.h" +#include <QDir> +#include <QFile> #include <QSet> +#include <QTemporaryDir> #include <QTest> class TestMaildirName : public QObject @@ -31,6 +34,11 @@ private slots: void anEmptyFlagSuffixIsPreserved(); void aNameWithNoSuffixGetsNone(); void theUidInfixIsNotCarriedAcross(); + void resolveRenamedReturnsAPathThatStillExists(); + void resolveRenamedFindsTheFileMbsyncRenamed(); + void resolveRenamedIsEmptyWhenTheFileIsReallyGone(); + void resolveRenamedDoesNotMatchADifferentMessage(); + void resolveRenamedRefusesAnAmbiguousMatch(); }; // Two messages written in the same second must not collide, which a @@ -89,5 +97,93 @@ void TestMaildirName::theUidInfixIsNotCarriedAcross() .arg(name))); } +namespace { + +/// One empty file, so a test can assert on which PATH is chosen rather than on +/// content. resolveRenamed() answers a filesystem question and never opens the +/// file. +bool touch(const QString &path) +{ + QFile file(path); + if (!file.open(QIODevice::WriteOnly)) + return false; + file.close(); + return true; +} + +} // namespace + +void TestMaildirName::resolveRenamedReturnsAPathThatStillExists() +{ + // The ordinary case, and the one that must stay cheap: nothing was + // renamed, so the answer is the question. + QTemporaryDir dir; + QVERIFY(dir.isValid()); + + const QString path = dir.filePath(QStringLiteral("1787647354.M369Q2.host:2,D")); + QVERIFY(touch(path)); + + QCOMPARE(MaildirName::resolveRenamed(path), path); +} + +void TestMaildirName::resolveRenamedFindsTheFileMbsyncRenamed() +{ + // Item 163. mbsync uploads the file and inserts its `,U=<uid>` infix + // before the flag suffix, leaving the unique stem alone. + QTemporaryDir dir; + QVERIFY(dir.isValid()); + + const QString stale = dir.filePath(QStringLiteral("1787647354.M369Q2.host:2,D")); + const QString renamed = + dir.filePath(QStringLiteral("1787647354.M369Q2.host,U=5:2,D")); + QVERIFY(touch(renamed)); + QVERIFY2(!QFile::exists(stale), "the stale path must not exist"); + + QCOMPARE(MaildirName::resolveRenamed(stale), renamed); +} + +void TestMaildirName::resolveRenamedIsEmptyWhenTheFileIsReallyGone() +{ + // The bounded half. A deleted file must NOT be recovered from, or a + // reportable defect becomes a wrong answer. + QTemporaryDir dir; + QVERIFY(dir.isValid()); + + const QString gone = dir.filePath(QStringLiteral("1787647354.M369Q2.host:2,D")); + QVERIFY(!QFile::exists(gone)); + + QVERIFY(MaildirName::resolveRenamed(gone).isEmpty()); +} + +void TestMaildirName::resolveRenamedDoesNotMatchADifferentMessage() +{ + // A neighbouring file in the same folder is not this message. Matching on + // anything looser than the whole stem would return it, and the caller + // would then open, display or MOVE the wrong mail. + QTemporaryDir dir; + QVERIFY(dir.isValid()); + + const QString stale = dir.filePath(QStringLiteral("1787647354.M369Q2.host:2,D")); + QVERIFY(touch(dir.filePath(QStringLiteral("1787647354.M369Q3.host,U=5:2,D")))); + QVERIFY(touch(dir.filePath(QStringLiteral("9999999999.M111Q1.host,U=6:2,D")))); + + QVERIFY(MaildirName::resolveRenamed(stale).isEmpty()); +} + +void TestMaildirName::resolveRenamedRefusesAnAmbiguousMatch() +{ + // Two files sharing one stem cannot happen in a correct Maildir, so this + // is a "the world is not what I assumed" case. Guessing between them could + // move or delete the wrong file, and the caller reports honestly instead. + QTemporaryDir dir; + QVERIFY(dir.isValid()); + + const QString stale = dir.filePath(QStringLiteral("1787647354.M369Q2.host:2,D")); + QVERIFY(touch(dir.filePath(QStringLiteral("1787647354.M369Q2.host,U=5:2,D")))); + QVERIFY(touch(dir.filePath(QStringLiteral("1787647354.M369Q2.host,U=6:2,S")))); + + QVERIFY(MaildirName::resolveRenamed(stale).isEmpty()); +} + QTEST_MAIN(TestMaildirName) #include "test_maildirname.moc" diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp index f935cac..21c3661 100644 --- a/tests/test_mainwindow.cpp +++ b/tests/test_mainwindow.cpp @@ -490,6 +490,7 @@ private slots: void doubleClickingADraftOpensTheComposer(); void aResumedDraftReplacesItsFileRatherThanAddingOne(); void aResumedDraftKeepsItsBlindRecipients(); + void aDraftRenamedByASyncStillReopensAndReplacesItsFile(); void theComposerSplitsItsToolbarByScope(); void ccAndBccHideBehindADisclosure(); void ccAndBccAreRevealedWhenTheyCarryAValue(); @@ -12861,6 +12862,83 @@ void TestMainWindow::aResumedDraftReplacesItsFileRatherThanAddingOne() "now exists twice"); } +void TestMainWindow::aDraftRenamedByASyncStillReopensAndReplacesItsFile() +{ + // Item 163, the composer site, and the one that costs data rather than + // display. mbsync uploads a draft and renames it to add its `,U=<uid>` + // infix; the model's path was captured when the query ran, so the reopen + // is handed a name that no longer exists. + // + // The refusal happens BEFORE any composer exists, so the user composes + // again into a FRESH window whose autosave has no previous path to unlink. + // The old revision survives, each save mints a new Message-ID, and both + // files reach the server. Asserted as the file COUNT, which is the shape + // the fork actually takes. + ComposeFixture fixture; + QVERIFY(fixture.build()); + + OutgoingMessage message; + message.accountKey = QStringLiteral("acct"); + message.to = { QStringLiteral("someone@example.org") }; + message.subject = QStringLiteral("Written before a sync"); + message.markdownBody = QStringLiteral("The first half."); + + const QString folder = fixture.mailRoot() + QStringLiteral("/acct/Drafts"); + const QString path = writeDraftFile(folder, message, + fixture.config().account( + QStringLiteral("acct"))); + QVERIFY(!path.isEmpty()); + + // mbsync's rename: same directory, same unique stem, `,U=<uid>` inserted + // before the flag suffix. Nothing reindexes, so the caller below still + // holds the pre-rename name, which is the whole precondition. + const QFileInfo before(path); + const QString base = before.fileName(); + const int suffix = base.indexOf(QStringLiteral(":2,")); + QVERIFY2(suffix > 0, "the draft fixture has no maildir flag suffix"); + const QString renamed = before.absolutePath() + QLatin1Char('/') + + base.left(suffix) + QStringLiteral(",U=7") + + base.mid(suffix); + QVERIFY2(QFile::rename(path, renamed), "could not stage the sync rename"); + + // The guard that proves this test can fail: without it, a fixture that + // quietly left the original in place would pass against the bug. + QVERIFY2(!QFile::exists(path), "the stale path should no longer exist"); + + const auto draftCount = [&folder]() { + return QDir(folder + QStringLiteral("/cur")) + .entryList(QDir::Files).size(); + }; + QCOMPARE(draftCount(), 1); + + // The STALE path, exactly as openComposerFor() passes MessageRef::filePath. + const ComposeContext context = + ComposeContextBuilder::forDraft(fixture.config(), path); + QVERIFY2(context.kind == ComposeContext::Kind::Draft, + "the reopen was refused, so the user would compose a second draft"); + // Resolved, not the caller's: seeding the stale path would let the reopen + // succeed and the unlink still miss, forking the draft one step later. + QCOMPARE(context.draftPath, renamed); + + ComposeWindow window(context, fixture.config(), fixture.mailRoot()); + auto *body = window.findChild<QPlainTextEdit *>(QStringLiteral("body")); + QVERIFY(body); + body->setPlainText(QStringLiteral("The second half.")); + + auto *timer = window.findChild<QTimer *>(QStringLiteral("autosave")); + QVERIFY2(timer, "the composer has no autosave timer"); + QVERIFY2(timer->isActive(), "editing the body did not arm the autosave"); + timer->setInterval(0); + QTRY_VERIFY_WITH_TIMEOUT(!timer->isActive(), 5000); + + // Still ONE draft: the autosave replaced the renamed file rather than + // leaving it behind beside a new one. + QCOMPARE(draftCount(), 1); + QVERIFY2(!QFile::exists(renamed), + "the renamed draft survived the autosave, so the draft was forked " + "into two files and both would reach the server"); +} + void TestMainWindow::aResumedDraftKeepsItsBlindRecipients() { // MessageBuilder writes Bcc into the draft file deliberately, and says |
