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