File indexing completed on 2024-06-16 05:01:54

0001 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
0002 
0003    This file is part of the Trojita Qt IMAP e-mail client,
0004    http://trojita.flaska.net/
0005 
0006    This program is free software; you can redistribute it and/or
0007    modify it under the terms of the GNU General Public License as
0008    published by the Free Software Foundation; either version 2 of
0009    the License or (at your option) version 3 or any later version
0010    accepted by the membership of KDE e.V. (or its successor approved
0011    by the membership of KDE e.V.), which shall act as a proxy
0012    defined in Section 14 of version 3 of the license.
0013 
0014    This program is distributed in the hope that it will be useful,
0015    but WITHOUT ANY WARRANTY; without even the implied warranty of
0016    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0017    GNU General Public License for more details.
0018 
0019    You should have received a copy of the GNU General Public License
0020    along with this program.  If not, see <http://www.gnu.org/licenses/>.
0021 */
0022 
0023 #include <QtTest>
0024 #include "test_Imap_DisappearingMailboxes.h"
0025 #include "Imap/Model/ItemRoles.h"
0026 #include "Imap/Model/TaskPresentationModel.h"
0027 #include "Streams/FakeSocket.h"
0028 #include "Utils/FakeCapabilitiesInjector.h"
0029 
0030 /** @short This test verifies that we don't segfault during offline -> online transition */
0031 void ImapModelDisappearingMailboxTest::testGoingOfflineOnline()
0032 {
0033     helperSyncBNoMessages();
0034     LibMailboxSync::setModelNetworkPolicy(model, Imap::Mailbox::NETWORK_OFFLINE);
0035     QCoreApplication::processEvents();
0036     QCoreApplication::processEvents();
0037     QCOMPARE(SOCK->writtenStuff(), t.mk("LOGOUT\r\n"));
0038     LibMailboxSync::setModelNetworkPolicy(model, Imap::Mailbox::NETWORK_ONLINE);
0039     t.reset();
0040 
0041     // We can't call helperSyncBNoMessages() here, it relies on msgListB's validity,
0042     // but that index is not necessary valid because of our kludgy fake listing...
0043 
0044     QCOMPARE( model->rowCount( msgListB ), 0 );
0045     model->switchToMailbox( idxB );
0046     QCoreApplication::processEvents();
0047     QCoreApplication::processEvents();
0048     QCOMPARE( SOCK->writtenStuff(), t.mk("SELECT b\r\n") );
0049     SOCK->fakeReading( QByteArray("* 0 exists\r\n")
0050                                   + t.last("ok completed\r\n") );
0051     QCoreApplication::processEvents();
0052     QCoreApplication::processEvents();
0053     QCoreApplication::processEvents();
0054     QCoreApplication::processEvents();
0055 }
0056 
0057 void ImapModelDisappearingMailboxTest::testGoingOfflineOnlineExamine()
0058 {
0059     helperTestGoingReallyOfflineOnline(false);
0060 }
0061 
0062 void ImapModelDisappearingMailboxTest::testGoingOfflineOnlineUnselect()
0063 {
0064     helperTestGoingReallyOfflineOnline(true);
0065 }
0066 
0067 /** @short Simulate what happens when user goes offline with views attached
0068 
0069 This is intended to be very similar to how real application behaves, reacting to events etc.
0070 
0071 This is a test for issue #88 where the ObtainSynchronizedMailboxTask failed to account for the possibility
0072 of indexes getting invalidated while the sync is in progress.
0073 */
0074 void ImapModelDisappearingMailboxTest::helperTestGoingReallyOfflineOnline(bool withUnselect)
0075 {
0076     // At first, open mailbox B
0077     helperSyncBNoMessages();
0078 
0079     // Make sure the socket is present
0080     QPointer<Streams::Socket> socketPtr(factory->lastSocket());
0081     Q_ASSERT(!socketPtr.isNull());
0082 
0083     // Go offline
0084     LibMailboxSync::setModelNetworkPolicy(model, Imap::Mailbox::NETWORK_OFFLINE);
0085     QCoreApplication::processEvents();
0086     QCoreApplication::processEvents();
0087 
0088     QCOMPARE(SOCK->writtenStuff(), t.mk("LOGOUT\r\n"));
0089     SOCK->fakeReading(QByteArray("* BYE see ya\r\n")
0090                       + t.last("ok logged out\r\n"));
0091     QCoreApplication::processEvents();
0092     QCoreApplication::processEvents();
0093     QCoreApplication::processEvents();
0094     QCoreApplication::processEvents();
0095 
0096     // It should be gone by now
0097     QVERIFY(socketPtr.isNull());
0098 
0099     // So now we're offline and want to reconnect back to see if we break.
0100 
0101     // Try a reconnect
0102     taskFactoryUnsafe->fakeListChildMailboxes = false;
0103     t.reset();
0104     LibMailboxSync::setModelNetworkPolicy(model, Imap::Mailbox::NETWORK_ONLINE);
0105     QCoreApplication::processEvents();
0106     QCoreApplication::processEvents();
0107     QCoreApplication::processEvents();
0108 
0109     // The trick here is that the reconnect resulted in querying a mailbox listing again
0110     QCOMPARE(SOCK->writtenStuff(), t.mk("LIST \"\" \"%\"\r\n"));
0111     QByteArray listResponse = QByteArray("* LIST (\\HasNoChildren) \".\" \"b\"\r\n"
0112                                          "* LIST (\\HasNoChildren) \".\" \"a\"\r\n")
0113                               + t.last("OK List done.\r\n");
0114 
0115     if (withUnselect) {
0116         // We'll need the UNSELECT later on
0117         FakeCapabilitiesInjector injector(model);
0118         injector.injectCapability(QStringLiteral("UNSELECT"));
0119     }
0120 
0121     // But before we "receive" the LIST responses, GUI could easily request syncing of mailbox B again,
0122     // which is what we do here
0123     QCOMPARE( model->rowCount( msgListB ), 0 );
0124     model->switchToMailbox( idxB );
0125     QCoreApplication::processEvents();
0126     QCoreApplication::processEvents();
0127     QCOMPARE( SOCK->writtenStuff(), t.mk("SELECT b\r\n") );
0128     QByteArray selectResponse =  QByteArray("* 0 exists\r\n") + t.last("ok completed\r\n");
0129 
0130     // Nice, so we're in the middle of a SELECT. Let's confuse things a bit by finalizing the LIST now :).
0131     SOCK->fakeReading(listResponse);
0132     QCoreApplication::processEvents();
0133     QCoreApplication::processEvents();
0134 
0135     // At this point, the msgListB should be invalidated
0136     QVERIFY(!idxB.isValid());
0137     QVERIFY(!msgListB.isValid());
0138     // ... and therefore the SELECT handler should take care not to rely on it being valid
0139     SOCK->fakeReading(selectResponse);
0140     QCoreApplication::processEvents();
0141     QCoreApplication::processEvents();
0142     QCoreApplication::processEvents();
0143 
0144     if (withUnselect) {
0145         // It should've noticed that the index is gone, and try to get out of there
0146         QCOMPARE(SOCK->writtenStuff(), t.mk("UNSELECT\r\n"));
0147     } else {
0148         // The actual mailbox contains a timestamp, so let's take a shortcut here
0149         QVERIFY(SOCK->writtenStuff().startsWith(t.mk("EXAMINE \"trojita non existing ")));
0150     }
0151 
0152     // Make sure it really ignores stuff
0153     SOCK->fakeReading(QByteArray("* 666 FETCH (FLAGS ())\r\n")
0154                       // and make it happy by switching away from that mailbox
0155                       + t.last("OK gone from mailbox\r\n"));
0156     QCoreApplication::processEvents();
0157     QCoreApplication::processEvents();
0158 
0159     // Verify the shape of the tree now
0160     QCOMPARE(model->rowCount(QModelIndex()), 3);
0161     // the first one will be "list of messages"
0162     idxA = model->index(1, 0, QModelIndex());
0163     idxB = model->index(2, 0, QModelIndex());
0164     QVERIFY(idxA.isValid());
0165     QVERIFY(idxB.isValid());
0166     QCOMPARE( model->data(idxA, Qt::DisplayRole), QVariant(QLatin1String("a")));
0167     QCOMPARE( model->data(idxB, Qt::DisplayRole), QVariant(QLatin1String("b")));
0168     msgListA = idxA.model()->index(0, 0, idxA);
0169     msgListB = idxB.model()->index(0, 0, idxB);
0170     QVERIFY(msgListA.isValid());
0171     QVERIFY(msgListB.isValid());
0172 
0173     QCoreApplication::processEvents();
0174     QCoreApplication::processEvents();
0175 
0176     if (!withUnselect) {
0177         QVERIFY(SOCK->writtenStuff().startsWith(t.mk("EXAMINE \"trojita non existing ")));
0178         cServer(t.last("NO no such mailbox\r\n"));
0179     }
0180 
0181     QVERIFY(SOCK->writtenStuff().isEmpty());
0182 }
0183 
0184 /** @short Simulate traffic into a selected mailbox whose index got invalidated
0185 
0186 This is a test for issue #124 where Trojita's KeepMailboxOpenTask assert()ed on an index getting invalidated
0187 while the mailbox was still selected and synced properly.
0188 */
0189 void ImapModelDisappearingMailboxTest::testTrafficAfterSyncedMailboxGoesAway()
0190 {
0191     existsA = 2;
0192     uidValidityA = 333;
0193     uidMapA << 666 << 686;
0194     uidNextA = 1337;
0195     helperSyncAWithMessagesEmptyState();
0196 
0197     // disable preload
0198     LibMailboxSync::setModelNetworkPolicy(model, Imap::Mailbox::NETWORK_EXPENSIVE);
0199 
0200     // and request some FETCH command
0201     QModelIndex messageIdx = msgListA.model()->index(0, 0, msgListA);
0202     Q_ASSERT(messageIdx.isValid());
0203     QCOMPARE(messageIdx.data(Imap::Mailbox::RoleMessageSubject), QVariant());
0204     cClient(t.mk("UID FETCH 666 (" FETCH_METADATA_ITEMS ")\r\n"));
0205     QByteArray fetchResponse = helperCreateTrivialEnvelope(1, 666, QStringLiteral("blah")) + t.last("OK fetched\r\n");
0206 
0207     // Request going to another mailbox, eventually
0208     QCOMPARE(model->rowCount(msgListB), 0);
0209 
0210     // Ask for mailbox metadata
0211     QCOMPARE(idxB.data(Imap::Mailbox::RoleTotalMessageCount), QVariant());
0212     cClient(t.mk("STATUS b (MESSAGES UNSEEN RECENT)\r\n"));
0213     QByteArray statusBResp = QByteArray("* STATUS b (MESSAGES 3 UNSEEN 0 RECENT 0)\r\n") + t.last("OK status sent\r\n");
0214 
0215     // We want to control this stuff
0216     taskFactoryUnsafe->fakeListChildMailboxes = false;
0217 
0218     model->reloadMailboxList();
0219     // And for simplicity, let's enable UNSELECT
0220     FakeCapabilitiesInjector injector(model);
0221     injector.injectCapability(QStringLiteral("UNSELECT"));
0222 
0223     // The trick here is that the reconnect resulted in querying a mailbox listing again
0224     cClient(t.mk("LIST \"\" \"%\"\r\n"));
0225     cServer(QByteArray("* LIST (\\HasNoChildren) \".\" \"b\"\r\n"
0226                        "* LIST (\\HasChildren) \".\" \"a\"\r\n"
0227                        "* LIST (\\HasNoChildren) \".\" \"c\"\r\n")
0228             + t.last("OK List done.\r\n"));
0229 
0230     // We have to refresh the indexes, of course
0231     idxA = model->index(1, 0, QModelIndex());
0232     idxB = model->index(2, 0, QModelIndex());
0233     idxC = model->index(3, 0, QModelIndex());
0234     QCOMPARE(model->data(idxA, Qt::DisplayRole), QVariant(QLatin1String("a")));
0235     QCOMPARE(model->data(idxB, Qt::DisplayRole), QVariant(QLatin1String("b")));
0236     QCOMPARE(model->data(idxC, Qt::DisplayRole), QVariant(QLatin1String("c")));
0237     msgListA = model->index(0, 0, idxA);
0238     msgListB = model->index(0, 0, idxB);
0239     msgListC = model->index(0, 0, idxC);
0240 
0241     // Add some unsolicited untagged data
0242     cServer(QByteArray("* 666 FETCH (FLAGS ())\r\n"));
0243     cClient(t.mk("UNSELECT\r\n"));
0244 
0245     // ...once again
0246     cServer(QByteArray("* 333 FETCH (FLAGS ())\r\n"));
0247 
0248     // At this point, send also a tagged OK for the fetch command; this used to hit an assert
0249     cServer(fetchResponse);
0250     cServer(t.last("OK unselected\r\n"));
0251 
0252     // Queue a few requests for status of a few mailboxes
0253     QCOMPARE(idxA.data(Imap::Mailbox::RoleTotalMessageCount), QVariant());
0254 
0255     // now receive the bits about the (long forgotten) STATUS b
0256     cServer(statusBResp);
0257     QCOMPARE(idxB.data(Imap::Mailbox::RoleTotalMessageCount), QVariant(3));
0258     // because STATUS responses are handled through the Model itself, we get correct data here
0259 
0260     // ...answer the STATUS a
0261     cClient(t.mk("STATUS a (MESSAGES UNSEEN RECENT)\r\n"));
0262     cServer(t.last("OK status sent\r\n"));
0263 
0264     // And yet another mailbox request
0265     QCOMPARE(model->rowCount(msgListC), 0);
0266 
0267     cClient(t.mk("SELECT c\r\n"));
0268     cServer(t.last("OK selected\r\n"));
0269 
0270     cEmpty();
0271 }
0272 
0273 /** @short Connection going offline shall not be reused for further requests for message structure
0274 
0275 The code in the Imap::Mailbox::Model already checks for connection status before asking for message structure.
0276 */
0277 void ImapModelDisappearingMailboxTest::testSlowOfflineMsgStructure()
0278 {
0279     // Initialize the environment
0280     existsA = 1;
0281     uidValidityA = 1;
0282     uidMapA << 1;
0283     uidNextA = 2;
0284     helperSyncAWithMessagesEmptyState();
0285     idxA = model->index(1, 0, QModelIndex());
0286     QVERIFY(idxA.isValid());
0287     QCOMPARE(model->data(idxA, Qt::DisplayRole), QVariant(QLatin1String("a")));
0288     msgListA = idxA.model()->index(0, 0, idxA);
0289     QVERIFY(msgListA.isValid());
0290     QModelIndex msg = msgListA.model()->index(0, 0, msgListA);
0291     QVERIFY(msg.isValid());
0292     Streams::FakeSocket *origSocket = SOCK;
0293 
0294     // Switch the connection to an offline mode, but postpone the BYE response
0295     LibMailboxSync::setModelNetworkPolicy(model, Imap::Mailbox::NETWORK_OFFLINE);
0296     QCoreApplication::processEvents();
0297     QCoreApplication::processEvents();
0298     QCOMPARE(SOCK->writtenStuff(), t.mk("LOGOUT\r\n"));
0299 
0300     // Ask for the bodystructure of this message
0301     QCOMPARE(model->rowCount(msg), 0);
0302 
0303     // Make sure that nothing else happens
0304     QCoreApplication::processEvents();
0305     QCoreApplication::processEvents();
0306     QCoreApplication::processEvents();
0307     QVERIFY(SOCK->writtenStuff().isEmpty());
0308     QVERIFY(SOCK == origSocket);
0309 }
0310 
0311 /** @short Test that requests for updating message flags will fail when offline */
0312 void ImapModelDisappearingMailboxTest::testSlowOfflineFlags()
0313 {
0314     // Initialize the environment
0315     existsA = 1;
0316     uidValidityA = 1;
0317     uidMapA << 1;
0318     uidNextA = 2;
0319     helperSyncAWithMessagesEmptyState();
0320     idxA = model->index(1, 0, QModelIndex());
0321     idxB = model->index(2, 0, QModelIndex());
0322     QVERIFY(idxA.isValid());
0323     QVERIFY(idxB.isValid());
0324     QCOMPARE(model->data(idxA, Qt::DisplayRole), QVariant(QLatin1String("a")));
0325     QCOMPARE(model->data(idxB, Qt::DisplayRole), QVariant(QLatin1String("b")));
0326     msgListA = idxA.model()->index(0, 0, idxA);
0327     QVERIFY(msgListA.isValid());
0328     QModelIndex msg = msgListA.model()->index(0, 0, msgListA);
0329     QVERIFY(msg.isValid());
0330     Streams::FakeSocket *origSocket = SOCK;
0331 
0332     // Switch the connection to an offline mode, but postpone the BYE response
0333     LibMailboxSync::setModelNetworkPolicy(model, Imap::Mailbox::NETWORK_OFFLINE);
0334     QCoreApplication::processEvents();
0335     QCoreApplication::processEvents();
0336     QCOMPARE(SOCK->writtenStuff(), t.mk("LOGOUT\r\n"));
0337 
0338     // Ask for the bodystructure of this message
0339     model->markMessagesDeleted(QModelIndexList() << msg, Imap::Mailbox::FLAG_ADD);
0340 
0341     // Make sure that nothing else happens
0342     QCoreApplication::processEvents();
0343     QCoreApplication::processEvents();
0344     QCoreApplication::processEvents();
0345     QVERIFY(SOCK->writtenStuff().isEmpty());
0346     QVERIFY(SOCK == origSocket);
0347 }
0348 
0349 /** @short Test what happens when we switch to offline after the flag update request, but before the underlying task gets activated */
0350 void ImapModelDisappearingMailboxTest::testSlowOfflineFlags2()
0351 {
0352     // Initialize the environment
0353     existsA = 1;
0354     uidValidityA = 1;
0355     uidMapA << 1;
0356     uidNextA = 2;
0357     helperSyncAWithMessagesEmptyState();
0358     idxA = model->index(1, 0, QModelIndex());
0359     idxB = model->index(2, 0, QModelIndex());
0360     QVERIFY(idxA.isValid());
0361     QVERIFY(idxB.isValid());
0362     QCOMPARE(model->data(idxA, Qt::DisplayRole), QVariant(QLatin1String("a")));
0363     QCOMPARE(model->data(idxB, Qt::DisplayRole), QVariant(QLatin1String("b")));
0364     msgListA = idxA.model()->index(0, 0, idxA);
0365     QVERIFY(msgListA.isValid());
0366     QModelIndex msg = msgListA.model()->index(0, 0, msgListA);
0367     QVERIFY(msg.isValid());
0368     Streams::FakeSocket *origSocket = SOCK;
0369 
0370     // Ask for the bodystructure of this message
0371     model->markMessagesDeleted(QModelIndexList() << msg, Imap::Mailbox::FLAG_ADD);
0372 
0373     // Switch the connection to an offline mode, but postpone the BYE response
0374     LibMailboxSync::setModelNetworkPolicy(model, Imap::Mailbox::NETWORK_OFFLINE);
0375     QCoreApplication::processEvents();
0376     QCoreApplication::processEvents();
0377     QCOMPARE(SOCK->writtenStuff(), t.mk("LOGOUT\r\n"));
0378 
0379     // Make sure that nothing else happens
0380     QCoreApplication::processEvents();
0381     QCoreApplication::processEvents();
0382     QCoreApplication::processEvents();
0383     QVERIFY(SOCK->writtenStuff().isEmpty());
0384     QVERIFY(SOCK == origSocket);
0385 
0386 }
0387 
0388 /** @short Test what happens when we switch to offline after the flag update request and the task got activated, but before the tagged response */
0389 void ImapModelDisappearingMailboxTest::testSlowOfflineFlags3()
0390 {
0391     // Initialize the environment
0392     existsA = 1;
0393     uidValidityA = 1;
0394     uidMapA << 1;
0395     uidNextA = 2;
0396     helperSyncAWithMessagesEmptyState();
0397     idxA = model->index(1, 0, QModelIndex());
0398     idxB = model->index(2, 0, QModelIndex());
0399     QVERIFY(idxA.isValid());
0400     QVERIFY(idxB.isValid());
0401     QCOMPARE(model->data(idxA, Qt::DisplayRole), QVariant(QLatin1String("a")));
0402     QCOMPARE(model->data(idxB, Qt::DisplayRole), QVariant(QLatin1String("b")));
0403     msgListA = idxA.model()->index(0, 0, idxA);
0404     QVERIFY(msgListA.isValid());
0405     QModelIndex msg = msgListA.model()->index(0, 0, msgListA);
0406     QVERIFY(msg.isValid());
0407     Streams::FakeSocket *origSocket = SOCK;
0408 
0409     // Ask for the bodystructure of this message
0410     model->markMessagesDeleted(QModelIndexList() << msg, Imap::Mailbox::FLAG_ADD);
0411 
0412     // Switch the connection to an offline mode, but postpone the BYE response
0413     QCoreApplication::processEvents();
0414     LibMailboxSync::setModelNetworkPolicy(model, Imap::Mailbox::NETWORK_OFFLINE);
0415     QCoreApplication::processEvents();
0416     QCoreApplication::processEvents();
0417     QByteArray writtenStuff = t.mk("UID STORE 1 +FLAGS (\\Deleted)\r\n");
0418     writtenStuff += t.mk("LOGOUT\r\n");
0419     QCOMPARE(SOCK->writtenStuff(), writtenStuff);
0420 
0421     // Make sure that nothing else happens
0422     QCoreApplication::processEvents();
0423     QCoreApplication::processEvents();
0424     QCoreApplication::processEvents();
0425     QVERIFY(SOCK->writtenStuff().isEmpty());
0426     QVERIFY(SOCK == origSocket);
0427 }
0428 
0429 void ImapModelDisappearingMailboxTest::testMailboxHoping()
0430 {
0431     int mailboxes = model->rowCount(QModelIndex());
0432     // The "off-by-one" is intentional, the first item is TreeItemMsgList
0433     for (int i = 1; i < mailboxes; ++i) {
0434         QModelIndex mailboxIndex = model->index(i, 0, QModelIndex());
0435         model->switchToMailbox(mailboxIndex);
0436     }
0437     //Imap::Mailbox::dumpModelContents(model->taskModel());
0438     cClient(t.mk("SELECT a\r\n"));
0439     cEmpty();
0440     cServer("* 0 EXISTS\r\n* OK [UIDNEXT 0] x\r\n* OK [UIDVALIDITY 1] x\r\n");
0441     cEmpty();
0442     cServer(t.last("OK selected\r\n"));
0443     cClient(t.mk("SELECT b\r\n"));
0444     cServer(t.last("OK B selected\r\n"));
0445     //Imap::Mailbox::dumpModelContents(model->taskModel());
0446     cClient(t.mk("SELECT c\r\n"));
0447     cEmpty();
0448 }
0449 
0450 // FIXME: write test for the UnSelectTask and its interaction with different scenarios about opened/to-be-opened tasks
0451 // Redmine #486
0452 
0453 QTEST_GUILESS_MAIN( ImapModelDisappearingMailboxTest )