1 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
2
3 This file is part of the Trojita Qt IMAP e-mail client,
4 http://trojita.flaska.net/
5
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License as
8 published by the Free Software Foundation; either version 2 of
9 the License or (at your option) version 3 or any later version
10 accepted by the membership of KDE e.V. (or its successor approved
11 by the membership of KDE e.V.), which shall act as a proxy
12 defined in Section 14 of version 3 of the license.
13
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with this program. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23 #include <QtTest>
24 #include "test_Imap_DisappearingMailboxes.h"
25 #include "Imap/Model/ItemRoles.h"
26 #include "Imap/Model/TaskPresentationModel.h"
27 #include "Streams/FakeSocket.h"
28 #include "Utils/FakeCapabilitiesInjector.h"
29
30 /** @short This test verifies that we don't segfault during offline -> online transition */
testGoingOfflineOnline()31 void ImapModelDisappearingMailboxTest::testGoingOfflineOnline()
32 {
33 helperSyncBNoMessages();
34 LibMailboxSync::setModelNetworkPolicy(model, Imap::Mailbox::NETWORK_OFFLINE);
35 QCoreApplication::processEvents();
36 QCoreApplication::processEvents();
37 QCOMPARE(SOCK->writtenStuff(), t.mk("LOGOUT\r\n"));
38 LibMailboxSync::setModelNetworkPolicy(model, Imap::Mailbox::NETWORK_ONLINE);
39 t.reset();
40
41 // We can't call helperSyncBNoMessages() here, it relies on msgListB's validity,
42 // but that index is not necessary valid because of our kludgy fake listing...
43
44 QCOMPARE( model->rowCount( msgListB ), 0 );
45 model->switchToMailbox( idxB );
46 QCoreApplication::processEvents();
47 QCoreApplication::processEvents();
48 QCOMPARE( SOCK->writtenStuff(), t.mk("SELECT b\r\n") );
49 SOCK->fakeReading( QByteArray("* 0 exists\r\n")
50 + t.last("ok completed\r\n") );
51 QCoreApplication::processEvents();
52 QCoreApplication::processEvents();
53 QCoreApplication::processEvents();
54 QCoreApplication::processEvents();
55 }
56
testGoingOfflineOnlineExamine()57 void ImapModelDisappearingMailboxTest::testGoingOfflineOnlineExamine()
58 {
59 helperTestGoingReallyOfflineOnline(false);
60 }
61
testGoingOfflineOnlineUnselect()62 void ImapModelDisappearingMailboxTest::testGoingOfflineOnlineUnselect()
63 {
64 helperTestGoingReallyOfflineOnline(true);
65 }
66
67 /** @short Simulate what happens when user goes offline with views attached
68
69 This is intended to be very similar to how real application behaves, reacting to events etc.
70
71 This is a test for issue #88 where the ObtainSynchronizedMailboxTask failed to account for the possibility
72 of indexes getting invalidated while the sync is in progress.
73 */
helperTestGoingReallyOfflineOnline(bool withUnselect)74 void ImapModelDisappearingMailboxTest::helperTestGoingReallyOfflineOnline(bool withUnselect)
75 {
76 // At first, open mailbox B
77 helperSyncBNoMessages();
78
79 // Make sure the socket is present
80 QPointer<Streams::Socket> socketPtr(factory->lastSocket());
81 Q_ASSERT(!socketPtr.isNull());
82
83 // Go offline
84 LibMailboxSync::setModelNetworkPolicy(model, Imap::Mailbox::NETWORK_OFFLINE);
85 QCoreApplication::processEvents();
86 QCoreApplication::processEvents();
87
88 QCOMPARE(SOCK->writtenStuff(), t.mk("LOGOUT\r\n"));
89 SOCK->fakeReading(QByteArray("* BYE see ya\r\n")
90 + t.last("ok logged out\r\n"));
91 QCoreApplication::processEvents();
92 QCoreApplication::processEvents();
93 QCoreApplication::processEvents();
94 QCoreApplication::processEvents();
95
96 // It should be gone by now
97 QVERIFY(socketPtr.isNull());
98
99 // So now we're offline and want to reconnect back to see if we break.
100
101 // Try a reconnect
102 taskFactoryUnsafe->fakeListChildMailboxes = false;
103 t.reset();
104 LibMailboxSync::setModelNetworkPolicy(model, Imap::Mailbox::NETWORK_ONLINE);
105 QCoreApplication::processEvents();
106 QCoreApplication::processEvents();
107 QCoreApplication::processEvents();
108
109 // The trick here is that the reconnect resulted in querying a mailbox listing again
110 QCOMPARE(SOCK->writtenStuff(), t.mk("LIST \"\" \"%\"\r\n"));
111 QByteArray listResponse = QByteArray("* LIST (\\HasNoChildren) \".\" \"b\"\r\n"
112 "* LIST (\\HasNoChildren) \".\" \"a\"\r\n")
113 + t.last("OK List done.\r\n");
114
115 if (withUnselect) {
116 // We'll need the UNSELECT later on
117 FakeCapabilitiesInjector injector(model);
118 injector.injectCapability(QStringLiteral("UNSELECT"));
119 }
120
121 // But before we "receive" the LIST responses, GUI could easily request syncing of mailbox B again,
122 // which is what we do here
123 QCOMPARE( model->rowCount( msgListB ), 0 );
124 model->switchToMailbox( idxB );
125 QCoreApplication::processEvents();
126 QCoreApplication::processEvents();
127 QCOMPARE( SOCK->writtenStuff(), t.mk("SELECT b\r\n") );
128 QByteArray selectResponse = QByteArray("* 0 exists\r\n") + t.last("ok completed\r\n");
129
130 // Nice, so we're in the middle of a SELECT. Let's confuse things a bit by finalizing the LIST now :).
131 SOCK->fakeReading(listResponse);
132 QCoreApplication::processEvents();
133 QCoreApplication::processEvents();
134
135 // At this point, the msgListB should be invalidated
136 QVERIFY(!idxB.isValid());
137 QVERIFY(!msgListB.isValid());
138 // ... and therefore the SELECT handler should take care not to rely on it being valid
139 SOCK->fakeReading(selectResponse);
140 QCoreApplication::processEvents();
141 QCoreApplication::processEvents();
142 QCoreApplication::processEvents();
143
144 if (withUnselect) {
145 // It should've noticed that the index is gone, and try to get out of there
146 QCOMPARE(SOCK->writtenStuff(), t.mk("UNSELECT\r\n"));
147 } else {
148 // The actual mailbox contains a timestamp, so let's take a shortcut here
149 QVERIFY(SOCK->writtenStuff().startsWith(t.mk("EXAMINE \"trojita non existing ")));
150 }
151
152 // Make sure it really ignores stuff
153 SOCK->fakeReading(QByteArray("* 666 FETCH (FLAGS ())\r\n")
154 // and make it happy by switching away from that mailbox
155 + t.last("OK gone from mailbox\r\n"));
156 QCoreApplication::processEvents();
157 QCoreApplication::processEvents();
158
159 // Verify the shape of the tree now
160 QCOMPARE(model->rowCount(QModelIndex()), 3);
161 // the first one will be "list of messages"
162 idxA = model->index(1, 0, QModelIndex());
163 idxB = model->index(2, 0, QModelIndex());
164 QVERIFY(idxA.isValid());
165 QVERIFY(idxB.isValid());
166 QCOMPARE( model->data(idxA, Qt::DisplayRole), QVariant(QLatin1String("a")));
167 QCOMPARE( model->data(idxB, Qt::DisplayRole), QVariant(QLatin1String("b")));
168 msgListA = idxA.child(0, 0);
169 msgListB = idxB.child(0, 0);
170 QVERIFY(msgListA.isValid());
171 QVERIFY(msgListB.isValid());
172
173 QCoreApplication::processEvents();
174 QCoreApplication::processEvents();
175
176 if (!withUnselect) {
177 QVERIFY(SOCK->writtenStuff().startsWith(t.mk("EXAMINE \"trojita non existing ")));
178 cServer(t.last("NO no such mailbox\r\n"));
179 }
180
181 QVERIFY(SOCK->writtenStuff().isEmpty());
182 }
183
184 /** @short Simulate traffic into a selected mailbox whose index got invalidated
185
186 This is a test for issue #124 where Trojita's KeepMailboxOpenTask assert()ed on an index getting invalidated
187 while the mailbox was still selected and synced properly.
188 */
testTrafficAfterSyncedMailboxGoesAway()189 void ImapModelDisappearingMailboxTest::testTrafficAfterSyncedMailboxGoesAway()
190 {
191 existsA = 2;
192 uidValidityA = 333;
193 uidMapA << 666 << 686;
194 uidNextA = 1337;
195 helperSyncAWithMessagesEmptyState();
196
197 // disable preload
198 LibMailboxSync::setModelNetworkPolicy(model, Imap::Mailbox::NETWORK_EXPENSIVE);
199
200 // and request some FETCH command
201 QModelIndex messageIdx = msgListA.child(0, 0);
202 Q_ASSERT(messageIdx.isValid());
203 QCOMPARE(messageIdx.data(Imap::Mailbox::RoleMessageSubject), QVariant());
204 cClient(t.mk("UID FETCH 666 (" FETCH_METADATA_ITEMS ")\r\n"));
205 QByteArray fetchResponse = helperCreateTrivialEnvelope(1, 666, QStringLiteral("blah")) + t.last("OK fetched\r\n");
206
207 // Request going to another mailbox, eventually
208 QCOMPARE(model->rowCount(msgListB), 0);
209
210 // Ask for mailbox metadata
211 QCOMPARE(idxB.data(Imap::Mailbox::RoleTotalMessageCount), QVariant());
212 cClient(t.mk("STATUS b (MESSAGES UNSEEN RECENT)\r\n"));
213 QByteArray statusBResp = QByteArray("* STATUS b (MESSAGES 3 UNSEEN 0 RECENT 0)\r\n") + t.last("OK status sent\r\n");
214
215 // We want to control this stuff
216 taskFactoryUnsafe->fakeListChildMailboxes = false;
217
218 model->reloadMailboxList();
219 // And for simplicity, let's enable UNSELECT
220 FakeCapabilitiesInjector injector(model);
221 injector.injectCapability(QStringLiteral("UNSELECT"));
222
223 // The trick here is that the reconnect resulted in querying a mailbox listing again
224 cClient(t.mk("LIST \"\" \"%\"\r\n"));
225 cServer(QByteArray("* LIST (\\HasNoChildren) \".\" \"b\"\r\n"
226 "* LIST (\\HasChildren) \".\" \"a\"\r\n"
227 "* LIST (\\HasNoChildren) \".\" \"c\"\r\n")
228 + t.last("OK List done.\r\n"));
229
230 // We have to refresh the indexes, of course
231 idxA = model->index(1, 0, QModelIndex());
232 idxB = model->index(2, 0, QModelIndex());
233 idxC = model->index(3, 0, QModelIndex());
234 QCOMPARE(model->data(idxA, Qt::DisplayRole), QVariant(QLatin1String("a")));
235 QCOMPARE(model->data(idxB, Qt::DisplayRole), QVariant(QLatin1String("b")));
236 QCOMPARE(model->data(idxC, Qt::DisplayRole), QVariant(QLatin1String("c")));
237 msgListA = model->index(0, 0, idxA);
238 msgListB = model->index(0, 0, idxB);
239 msgListC = model->index(0, 0, idxC);
240
241 // Add some unsolicited untagged data
242 cServer(QByteArray("* 666 FETCH (FLAGS ())\r\n"));
243 cClient(t.mk("UNSELECT\r\n"));
244
245 // ...once again
246 cServer(QByteArray("* 333 FETCH (FLAGS ())\r\n"));
247
248 // At this point, send also a tagged OK for the fetch command; this used to hit an assert
249 cServer(fetchResponse);
250 cServer(t.last("OK unselected\r\n"));
251
252 // Queue a few requests for status of a few mailboxes
253 QCOMPARE(idxA.data(Imap::Mailbox::RoleTotalMessageCount), QVariant());
254
255 // now receive the bits about the (long forgotten) STATUS b
256 cServer(statusBResp);
257 QCOMPARE(idxB.data(Imap::Mailbox::RoleTotalMessageCount), QVariant(3));
258 // because STATUS responses are handled through the Model itself, we get correct data here
259
260 // ...answer the STATUS a
261 cClient(t.mk("STATUS a (MESSAGES UNSEEN RECENT)\r\n"));
262 cServer(t.last("OK status sent\r\n"));
263
264 // And yet another mailbox request
265 QCOMPARE(model->rowCount(msgListC), 0);
266
267 cClient(t.mk("SELECT c\r\n"));
268 cServer(t.last("OK selected\r\n"));
269
270 cEmpty();
271 }
272
273 /** @short Connection going offline shall not be reused for further requests for message structure
274
275 The code in the Imap::Mailbox::Model already checks for connection status before asking for message structure.
276 */
testSlowOfflineMsgStructure()277 void ImapModelDisappearingMailboxTest::testSlowOfflineMsgStructure()
278 {
279 // Initialize the environment
280 existsA = 1;
281 uidValidityA = 1;
282 uidMapA << 1;
283 uidNextA = 2;
284 helperSyncAWithMessagesEmptyState();
285 idxA = model->index(1, 0, QModelIndex());
286 QVERIFY(idxA.isValid());
287 QCOMPARE(model->data(idxA, Qt::DisplayRole), QVariant(QLatin1String("a")));
288 msgListA = idxA.child(0, 0);
289 QVERIFY(msgListA.isValid());
290 QModelIndex msg = msgListA.child(0, 0);
291 QVERIFY(msg.isValid());
292 Streams::FakeSocket *origSocket = SOCK;
293
294 // Switch the connection to an offline mode, but postpone the BYE response
295 LibMailboxSync::setModelNetworkPolicy(model, Imap::Mailbox::NETWORK_OFFLINE);
296 QCoreApplication::processEvents();
297 QCoreApplication::processEvents();
298 QCOMPARE(SOCK->writtenStuff(), t.mk("LOGOUT\r\n"));
299
300 // Ask for the bodystructure of this message
301 QCOMPARE(model->rowCount(msg), 0);
302
303 // Make sure that nothing else happens
304 QCoreApplication::processEvents();
305 QCoreApplication::processEvents();
306 QCoreApplication::processEvents();
307 QVERIFY(SOCK->writtenStuff().isEmpty());
308 QVERIFY(SOCK == origSocket);
309 }
310
311 /** @short Test that requests for updating message flags will fail when offline */
testSlowOfflineFlags()312 void ImapModelDisappearingMailboxTest::testSlowOfflineFlags()
313 {
314 // Initialize the environment
315 existsA = 1;
316 uidValidityA = 1;
317 uidMapA << 1;
318 uidNextA = 2;
319 helperSyncAWithMessagesEmptyState();
320 idxA = model->index(1, 0, QModelIndex());
321 idxB = model->index(2, 0, QModelIndex());
322 QVERIFY(idxA.isValid());
323 QVERIFY(idxB.isValid());
324 QCOMPARE(model->data(idxA, Qt::DisplayRole), QVariant(QLatin1String("a")));
325 QCOMPARE(model->data(idxB, Qt::DisplayRole), QVariant(QLatin1String("b")));
326 msgListA = idxA.child(0, 0);
327 QVERIFY(msgListA.isValid());
328 QModelIndex msg = msgListA.child(0, 0);
329 QVERIFY(msg.isValid());
330 Streams::FakeSocket *origSocket = SOCK;
331
332 // Switch the connection to an offline mode, but postpone the BYE response
333 LibMailboxSync::setModelNetworkPolicy(model, Imap::Mailbox::NETWORK_OFFLINE);
334 QCoreApplication::processEvents();
335 QCoreApplication::processEvents();
336 QCOMPARE(SOCK->writtenStuff(), t.mk("LOGOUT\r\n"));
337
338 // Ask for the bodystructure of this message
339 model->markMessagesDeleted(QModelIndexList() << msg, Imap::Mailbox::FLAG_ADD);
340
341 // Make sure that nothing else happens
342 QCoreApplication::processEvents();
343 QCoreApplication::processEvents();
344 QCoreApplication::processEvents();
345 QVERIFY(SOCK->writtenStuff().isEmpty());
346 QVERIFY(SOCK == origSocket);
347 }
348
349 /** @short Test what happens when we switch to offline after the flag update request, but before the underlying task gets activated */
testSlowOfflineFlags2()350 void ImapModelDisappearingMailboxTest::testSlowOfflineFlags2()
351 {
352 // Initialize the environment
353 existsA = 1;
354 uidValidityA = 1;
355 uidMapA << 1;
356 uidNextA = 2;
357 helperSyncAWithMessagesEmptyState();
358 idxA = model->index(1, 0, QModelIndex());
359 idxB = model->index(2, 0, QModelIndex());
360 QVERIFY(idxA.isValid());
361 QVERIFY(idxB.isValid());
362 QCOMPARE(model->data(idxA, Qt::DisplayRole), QVariant(QLatin1String("a")));
363 QCOMPARE(model->data(idxB, Qt::DisplayRole), QVariant(QLatin1String("b")));
364 msgListA = idxA.child(0, 0);
365 QVERIFY(msgListA.isValid());
366 QModelIndex msg = msgListA.child(0, 0);
367 QVERIFY(msg.isValid());
368 Streams::FakeSocket *origSocket = SOCK;
369
370 // Ask for the bodystructure of this message
371 model->markMessagesDeleted(QModelIndexList() << msg, Imap::Mailbox::FLAG_ADD);
372
373 // Switch the connection to an offline mode, but postpone the BYE response
374 LibMailboxSync::setModelNetworkPolicy(model, Imap::Mailbox::NETWORK_OFFLINE);
375 QCoreApplication::processEvents();
376 QCoreApplication::processEvents();
377 QCOMPARE(SOCK->writtenStuff(), t.mk("LOGOUT\r\n"));
378
379 // Make sure that nothing else happens
380 QCoreApplication::processEvents();
381 QCoreApplication::processEvents();
382 QCoreApplication::processEvents();
383 QVERIFY(SOCK->writtenStuff().isEmpty());
384 QVERIFY(SOCK == origSocket);
385
386 }
387
388 /** @short Test what happens when we switch to offline after the flag update request and the task got activated, but before the tagged response */
testSlowOfflineFlags3()389 void ImapModelDisappearingMailboxTest::testSlowOfflineFlags3()
390 {
391 // Initialize the environment
392 existsA = 1;
393 uidValidityA = 1;
394 uidMapA << 1;
395 uidNextA = 2;
396 helperSyncAWithMessagesEmptyState();
397 idxA = model->index(1, 0, QModelIndex());
398 idxB = model->index(2, 0, QModelIndex());
399 QVERIFY(idxA.isValid());
400 QVERIFY(idxB.isValid());
401 QCOMPARE(model->data(idxA, Qt::DisplayRole), QVariant(QLatin1String("a")));
402 QCOMPARE(model->data(idxB, Qt::DisplayRole), QVariant(QLatin1String("b")));
403 msgListA = idxA.child(0, 0);
404 QVERIFY(msgListA.isValid());
405 QModelIndex msg = msgListA.child(0, 0);
406 QVERIFY(msg.isValid());
407 Streams::FakeSocket *origSocket = SOCK;
408
409 // Ask for the bodystructure of this message
410 model->markMessagesDeleted(QModelIndexList() << msg, Imap::Mailbox::FLAG_ADD);
411
412 // Switch the connection to an offline mode, but postpone the BYE response
413 QCoreApplication::processEvents();
414 LibMailboxSync::setModelNetworkPolicy(model, Imap::Mailbox::NETWORK_OFFLINE);
415 QCoreApplication::processEvents();
416 QCoreApplication::processEvents();
417 QByteArray writtenStuff = t.mk("UID STORE 1 +FLAGS (\\Deleted)\r\n");
418 writtenStuff += t.mk("LOGOUT\r\n");
419 QCOMPARE(SOCK->writtenStuff(), writtenStuff);
420
421 // Make sure that nothing else happens
422 QCoreApplication::processEvents();
423 QCoreApplication::processEvents();
424 QCoreApplication::processEvents();
425 QVERIFY(SOCK->writtenStuff().isEmpty());
426 QVERIFY(SOCK == origSocket);
427 }
428
testMailboxHoping()429 void ImapModelDisappearingMailboxTest::testMailboxHoping()
430 {
431 int mailboxes = model->rowCount(QModelIndex());
432 // The "off-by-one" is intentional, the first item is TreeItemMsgList
433 for (int i = 1; i < mailboxes; ++i) {
434 QModelIndex mailboxIndex = model->index(i, 0, QModelIndex());
435 model->switchToMailbox(mailboxIndex);
436 }
437 //Imap::Mailbox::dumpModelContents(model->taskModel());
438 cClient(t.mk("SELECT a\r\n"));
439 cEmpty();
440 cServer("* 0 EXISTS\r\n* OK [UIDNEXT 0] x\r\n* OK [UIDVALIDITY 1] x\r\n");
441 cEmpty();
442 cServer(t.last("OK selected\r\n"));
443 cClient(t.mk("SELECT b\r\n"));
444 cServer(t.last("OK B selected\r\n"));
445 //Imap::Mailbox::dumpModelContents(model->taskModel());
446 cClient(t.mk("SELECT c\r\n"));
447 cEmpty();
448 }
449
450 // FIXME: write test for the UnSelectTask and its interaction with different scenarios about opened/to-be-opened tasks
451 // Redmine #486
452
453 QTEST_GUILESS_MAIN( ImapModelDisappearingMailboxTest )
454