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 )