aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_maildirname.cpp96
-rw-r--r--tests/test_mainwindow.cpp78
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