diff --git a/resources/imap/autotests/testsessionpool.cpp b/resources/imap/autotests/testsessionpool.cpp index 423597728..8afeca6b6 100644 --- a/resources/imap/autotests/testsessionpool.cpp +++ b/resources/imap/autotests/testsessionpool.cpp @@ -1,779 +1,834 @@ /* Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company Author: Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or ( at your option ) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "imaptestbase.h" #include #include class TestSessionPool : public ImapTestBase { Q_OBJECT private Q_SLOTS: void shouldPrepareFirstSessionOnConnect_data() { QTest::addColumn("account"); QTest::addColumn("requester"); QTest::addColumn< QList >("scenario"); QTest::addColumn("password"); QTest::addColumn("errorCode"); QTest::addColumn("capabilities"); ImapAccount *account = nullptr; DummyPasswordRequester *requester = nullptr; QList scenario; QString password; QStringList capabilities; account = createDefaultAccount(); requester = createDefaultRequester(); scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 NAMESPACE UIDPLUS IDLE" << "S: A000002 OK Completed" << "C: A000003 NAMESPACE" << "S: * NAMESPACE ( (\"INBOX/\" \"/\") ) ( (\"user/\" \"/\") ) ( (\"\" \"/\") )" << "S: A000003 OK Completed"; password = QStringLiteral("foobar"); int errorCode = SessionPool::NoError; capabilities.clear(); capabilities << QStringLiteral("IMAP4") << QStringLiteral("IMAP4REV1") << QStringLiteral("NAMESPACE") << QStringLiteral("UIDPLUS") << QStringLiteral("IDLE"); QTest::newRow("normal case") << account << requester << scenario << password << errorCode << capabilities; account = createDefaultAccount(); requester = createDefaultRequester(); scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "S: A000002 OK Completed"; password = QStringLiteral("foobar"); errorCode = SessionPool::NoError; capabilities.clear(); capabilities << QStringLiteral("IMAP4") << QStringLiteral("IMAP4REV1") << QStringLiteral("UIDPLUS") << QStringLiteral("IDLE"); QTest::newRow("no NAMESPACE support") << account << requester << scenario << password << errorCode << capabilities; account = createDefaultAccount(); requester = createDefaultRequester(); scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IDLE" << "S: A000002 OK Completed" << "C: A000003 LOGOUT"; password = QStringLiteral("foobar"); errorCode = SessionPool::IncompatibleServerError; capabilities.clear(); QTest::newRow("incompatible server") << account << requester << scenario << password << errorCode << capabilities; QList requests; QList results; account = createDefaultAccount(); requester = createDefaultRequester(); requests.clear(); results.clear(); requests << DummyPasswordRequester::StandardRequest << DummyPasswordRequester::WrongPasswordRequest; results << DummyPasswordRequester::PasswordRetrieved << DummyPasswordRequester::UserRejected; requester->setScenario(requests, results); scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 NO Login failed" << "C: A000002 LOGOUT"; password = QStringLiteral("foobar"); errorCode = SessionPool::LoginFailError; capabilities.clear(); QTest::newRow("login fail, user reject password entry") << account << requester << scenario << password << errorCode << capabilities; account = createDefaultAccount(); requester = createDefaultRequester(); requests.clear(); results.clear(); requests << DummyPasswordRequester::StandardRequest << DummyPasswordRequester::WrongPasswordRequest; results << DummyPasswordRequester::PasswordRetrieved << DummyPasswordRequester::PasswordRetrieved; requester->setScenario(requests, results); scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 NO Login failed" << "C: A000002 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000002 OK Login succeeded" << "C: A000003 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "S: A000003 OK Completed"; password = QStringLiteral("foobar"); errorCode = SessionPool::NoError; capabilities.clear(); capabilities << QStringLiteral("IMAP4") << QStringLiteral("IMAP4REV1") << QStringLiteral("UIDPLUS") << QStringLiteral("IDLE"); QTest::newRow("login fail, user provide new password") << account << requester << scenario << password << errorCode << capabilities; account = createDefaultAccount(); requester = createDefaultRequester(); requests.clear(); results.clear(); requests << DummyPasswordRequester::StandardRequest << DummyPasswordRequester::WrongPasswordRequest; results << DummyPasswordRequester::PasswordRetrieved << DummyPasswordRequester::EmptyPasswordEntered; requester->setScenario(requests, results); scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 NO Login failed" << "C: A000002 LOGOUT"; password = QStringLiteral("foobar"); errorCode = SessionPool::LoginFailError; capabilities.clear(); QTest::newRow("login fail, user provided empty password") << account << requester << scenario << password << errorCode << capabilities; account = createDefaultAccount(); requester = createDefaultRequester(); requests.clear(); results.clear(); requests << DummyPasswordRequester::StandardRequest << DummyPasswordRequester::WrongPasswordRequest; results << DummyPasswordRequester::PasswordRetrieved << DummyPasswordRequester::ReconnectNeeded; requester->setScenario(requests, results); scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 NO Login failed" << "C: A000002 LOGOUT"; password = QStringLiteral("foobar"); errorCode = SessionPool::ReconnectNeededError; capabilities.clear(); QTest::newRow("login fail, user change the settings") << account << requester << scenario << password << errorCode << capabilities; } void shouldPrepareFirstSessionOnConnect() { QFETCH(ImapAccount *, account); QFETCH(DummyPasswordRequester *, requester); QFETCH(QList, scenario); QFETCH(QString, password); QFETCH(int, errorCode); QFETCH(QStringList, capabilities); QSignalSpy requesterSpy(requester, SIGNAL(done(int,QString))); FakeServer server; server.setScenario(scenario); server.startAndWait(); SessionPool pool(2); QVERIFY(!pool.isConnected()); QSignalSpy poolSpy(&pool, SIGNAL(connectDone(int,QString))); pool.setPasswordRequester(requester); QVERIFY(pool.connect(account)); QTest::qWait(200); QVERIFY(requesterSpy.count() > 0); if (requesterSpy.count() == 1) { QCOMPARE(requesterSpy.at(0).at(0).toInt(), 0); QCOMPARE(requesterSpy.at(0).at(1).toString(), password); } QCOMPARE(poolSpy.count(), 1); QCOMPARE(poolSpy.at(0).at(0).toInt(), errorCode); if (errorCode == SessionPool::NoError) { QVERIFY(pool.isConnected()); } else { QVERIFY(!pool.isConnected()); } QCOMPARE(pool.serverCapabilities(), capabilities); QVERIFY(pool.serverNamespaces().isEmpty()); QVERIFY(server.isAllScenarioDone()); server.quit(); } void shouldManageSeveralSessions() { FakeServer server; server.addScenario(QList() << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 NAMESPACE UIDPLUS IDLE" << "S: A000002 OK Completed" << "C: A000003 NAMESPACE" << "S: * NAMESPACE ( (\"INBOX/\" \"/\") ) ( (\"user/\" \"/\") ) ( (\"\" \"/\") )" << "S: A000003 OK Completed" ); server.addScenario(QList() << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" ); server.startAndWait(); ImapAccount *account = createDefaultAccount(); DummyPasswordRequester *requester = createDefaultRequester(); QSignalSpy requesterSpy(requester, SIGNAL(done(int,QString))); SessionPool pool(2); pool.setPasswordRequester(requester); QSignalSpy connectSpy(&pool, SIGNAL(connectDone(int,QString))); QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session *,int,QString))); // Before connect we can't get any session qint64 requestId = pool.requestSession(); QCOMPARE(requestId, qint64(-1)); // Initial connect should trigger only a password request and a connect QVERIFY(pool.connect(account)); QTest::qWait(100); QCOMPARE(requesterSpy.count(), 1); QCOMPARE(connectSpy.count(), 1); QCOMPARE(sessionSpy.count(), 0); // Requesting a first session shouldn't create a new one, // only sessionRequestDone is emitted right away requestId = pool.requestSession(); QCOMPARE(requestId, qint64(1)); QTest::qWait(100); QCOMPARE(requesterSpy.count(), 1); QCOMPARE(connectSpy.count(), 1); QCOMPARE(sessionSpy.count(), 1); QCOMPARE(sessionSpy.at(0).at(0).toLongLong(), requestId); QVERIFY(sessionSpy.at(0).at(1).value() != nullptr); QCOMPARE(sessionSpy.at(0).at(2).toInt(), 0); QCOMPARE(sessionSpy.at(0).at(3).toString(), QString()); // Requesting an extra session should create a new one // So for instance password will be requested requestId = pool.requestSession(); QCOMPARE(requestId, qint64(2)); QTest::qWait(100); QCOMPARE(requesterSpy.count(), 2); QCOMPARE(connectSpy.count(), 1); QCOMPARE(sessionSpy.count(), 2); QCOMPARE(sessionSpy.at(1).at(0).toLongLong(), requestId); QVERIFY(sessionSpy.at(1).at(1).value() != nullptr); // Should be different sessions... QVERIFY(sessionSpy.at(0).at(1).value() != sessionSpy.at(1).at(1).value()); QCOMPARE(sessionSpy.at(1).at(2).toInt(), 0); QCOMPARE(sessionSpy.at(1).at(3).toString(), QString()); // Requesting yet another session should fail as we reached the // maximum pool size, and they're all reserved requestId = pool.requestSession(); QCOMPARE(requestId, qint64(3)); QTest::qWait(100); QCOMPARE(requesterSpy.count(), 2); QCOMPARE(connectSpy.count(), 1); QCOMPARE(sessionSpy.count(), 3); QCOMPARE(sessionSpy.at(2).at(0).toLongLong(), requestId); QVERIFY(sessionSpy.at(2).at(1).value() == nullptr); QCOMPARE(sessionSpy.at(2).at(2).toInt(), (int)SessionPool::NoAvailableSessionError); QVERIFY(!sessionSpy.at(2).at(3).toString().isEmpty()); // OTOH, if we release one now, and then request another one // it should succeed without even creating a new session KIMAP::Session *session = sessionSpy.at(0).at(1).value(); pool.releaseSession(session); requestId = pool.requestSession(); QCOMPARE(requestId, qint64(4)); QTest::qWait(100); QCOMPARE(requesterSpy.count(), 2); QCOMPARE(connectSpy.count(), 1); QCOMPARE(sessionSpy.count(), 4); QCOMPARE(sessionSpy.at(3).at(0).toLongLong(), requestId); // Only one session was available, so that should be the one we get gack QVERIFY(sessionSpy.at(3).at(1).value() == session); QCOMPARE(sessionSpy.at(3).at(2).toInt(), 0); QCOMPARE(sessionSpy.at(3).at(3).toString(), QString()); QVERIFY(server.isAllScenarioDone()); server.quit(); } void shouldNotifyConnectionLost() { FakeServer server; server.addScenario(QList() << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "S: A000002 OK Completed" << "C: A000003 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "X" ); server.startAndWait(); ImapAccount *account = createDefaultAccount(); DummyPasswordRequester *requester = createDefaultRequester(); SessionPool pool(1); pool.setPasswordRequester(requester); QSignalSpy connectSpy(&pool, SIGNAL(connectDone(int,QString))); QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session *,int,QString))); QSignalSpy lostSpy(&pool, SIGNAL(connectionLost(KIMAP::Session *))); // Initial connect should trigger only a password request and a connect QVERIFY(pool.connect(account)); QTest::qWait(100); QCOMPARE(connectSpy.count(), 1); QCOMPARE(sessionSpy.count(), 0); qint64 requestId = pool.requestSession(); QTest::qWait(100); QCOMPARE(sessionSpy.count(), 1); QCOMPARE(sessionSpy.at(0).at(0).toLongLong(), requestId); KIMAP::Session *s = sessionSpy.at(0).at(1).value(); KIMAP::CapabilitiesJob *job = new KIMAP::CapabilitiesJob(s); job->start(); QTest::qWait(100); QCOMPARE(lostSpy.count(), 1); // FIXME extracting the pointer value form QVariant crashes // QCOMPARE(lostSpy.at(0).at(0).value(), s); QVERIFY(server.isAllScenarioDone()); server.quit(); } void shouldNotifyOnDisconnect_data() { QTest::addColumn< QList >("scenario"); QTest::addColumn("termination"); QList scenario; scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "S: A000002 OK Completed" << "C: A000003 LOGOUT"; QTest::newRow("logout session") << scenario << (int)SessionPool::LogoutSession; scenario.clear(); scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "S: A000002 OK Completed"; QTest::newRow("close session") << scenario << (int)SessionPool::CloseSession; } void shouldNotifyOnDisconnect() { QFETCH(QList, scenario); QFETCH(int, termination); FakeServer server; server.addScenario(scenario); server.startAndWait(); ImapAccount *account = createDefaultAccount(); DummyPasswordRequester *requester = createDefaultRequester(); SessionPool pool(1); pool.setPasswordRequester(requester); QSignalSpy disconnectSpy(&pool, SIGNAL(disconnectDone())); // Initial connect should trigger only a password request and a connect QVERIFY(pool.connect(account)); QTest::qWait(100); QCOMPARE(disconnectSpy.count(), 0); pool.disconnect((SessionPool::SessionTermination)termination); QTest::qWait(100); QCOMPARE(disconnectSpy.count(), 1); QVERIFY(server.isAllScenarioDone()); server.quit(); } void shouldCleanupOnClosingDuringLogin_data() { QTest::addColumn< QList >("scenario"); { QList scenario; scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""; QTest::newRow("during login") << scenario; } { QList scenario; scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY"; QTest::newRow("during capability") << scenario; } { QList scenario; scenario << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 NAMESPACE UIDPLUS IDLE" << "S: A000002 OK Completed" << "C: A000003 NAMESPACE"; QTest::newRow("during namespace") << scenario; } } void shouldCleanupOnClosingDuringLogin() { QFETCH(QList, scenario); FakeServer server; server.addScenario(scenario); server.startAndWait(); ImapAccount *account = createDefaultAccount(); DummyPasswordRequester *requester = createDefaultRequester(); SessionPool pool(1); pool.setPasswordRequester(requester); QSignalSpy connectSpy(&pool, SIGNAL(connectDone(int,QString))); QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session *,int,QString))); QSignalSpy lostSpy(&pool, SIGNAL(connectionLost(KIMAP::Session *))); // Initial connect should trigger only a password request and a connect QVERIFY(pool.connect(account)); QTest::qWait(100); QCOMPARE(connectSpy.count(), 0); // Login not done yet QWeakPointer session = qFindChild(&pool); QVERIFY(session.data()); QCOMPARE(sessionSpy.count(), 0); pool.disconnect(SessionPool::CloseSession); QTest::qWait(100); QCOMPARE(connectSpy.count(), 1); // We're informed that connect failed QCOMPARE(connectSpy.at(0).at(0).toInt(), int(SessionPool::CancelledError)); QCOMPARE(lostSpy.count(), 0); // We're not supposed to know the session pointer, so no connectionLost emitted // Make the session->deleteLater work, it can't happen in qWait (nested event loop) QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); QVERIFY(session.isNull()); QVERIFY(server.isAllScenarioDone()); server.quit(); } void shouldHonorCancelRequest() { FakeServer server; server.addScenario(QList() << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "S: A000002 OK Completed" << "C: A000003 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "X" ); server.startAndWait(); ImapAccount *account = createDefaultAccount(); DummyPasswordRequester *requester = createDefaultRequester(); SessionPool pool(1); pool.setPasswordRequester(requester); QSignalSpy connectSpy(&pool, SIGNAL(connectDone(int,QString))); QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session *,int,QString))); QSignalSpy lostSpy(&pool, SIGNAL(connectionLost(KIMAP::Session *))); // Initial connect should trigger only a password request and a connect QVERIFY(pool.connect(account)); QTest::qWait(100); QCOMPARE(connectSpy.count(), 1); QCOMPARE(sessionSpy.count(), 0); qint64 requestId = pool.requestSession(); // Cancel the request pool.cancelSessionRequest(requestId); // The request should not be processed anymore QTest::qWait(100); QCOMPARE(sessionSpy.count(), 0); } void shouldBeDisconnectedOnAllSessionLost() { FakeServer server; server.addScenario(QList() << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 IDLE" << "S: A000002 OK Completed" << "C: A000003 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "X" ); server.addScenario(QList() << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "X" ); server.startAndWait(); ImapAccount *account = createDefaultAccount(); DummyPasswordRequester *requester = createDefaultRequester(); SessionPool pool(2); pool.setPasswordRequester(requester); QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session *,int,QString))); // Initial connect should trigger only a password request and a connect QVERIFY(pool.connect(account)); QTest::qWait(100); // We should be connected now QVERIFY(pool.isConnected()); // Ask for a session pool.requestSession(); QTest::qWait(100); QCOMPARE(sessionSpy.count(), 1); QVERIFY(sessionSpy.at(0).at(1).value() != nullptr); // Still connected obviously QVERIFY(pool.isConnected()); // Ask for a second session pool.requestSession(); QTest::qWait(100); QCOMPARE(sessionSpy.count(), 2); QVERIFY(sessionSpy.at(1).at(1).value() != nullptr); // Still connected of course QVERIFY(pool.isConnected()); KIMAP::Session *session1 = sessionSpy.at(0).at(1).value(); KIMAP::Session *session2 = sessionSpy.at(1).at(1).value(); // Prepare for session disconnects QSignalSpy lostSpy(&pool, SIGNAL(connectionLost(KIMAP::Session *))); // Make the first session drop KIMAP::CapabilitiesJob *job = new KIMAP::CapabilitiesJob(session1); job->start(); QTest::qWait(100); QCOMPARE(lostSpy.count(), 1); // FIXME extracting the pointer value form QVariant crashes // QCOMPARE(lostSpy.at(0).at(0).value(), session1); // We're still connected (one session being alive) QVERIFY(pool.isConnected()); // Make the second session drop job = new KIMAP::CapabilitiesJob(session2); job->start(); QTest::qWait(100); QCOMPARE(lostSpy.count(), 2); // FIXME extracting the pointer value form QVariant crashes // QCOMPARE(lostSpy.at(1).at(0).value(), session2); // We're not connected anymore! All sessions dropped! QVERIFY(!pool.isConnected()); QVERIFY(server.isAllScenarioDone()); server.quit(); } void shouldHandleDisconnectDuringPasswordRequest() { ImapAccount *account = createDefaultAccount(); // This requester will delay the second reply by a second DummyPasswordRequester *requester = createDefaultRequester(); requester->setDelays(QList() << 20 << 1000); QSignalSpy requesterSpy(requester, SIGNAL(done(int,QString))); FakeServer server; server.addScenario(QList() << FakeServer::greeting() << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" << "S: A000001 OK User Logged in" << "C: A000002 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 IDLE" << "S: A000002 OK Completed" << "C: A000003 CAPABILITY" << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE" << "X" ); server.startAndWait(); // The connect should work nicely SessionPool pool(2); QVERIFY(!pool.isConnected()); QSignalSpy poolSpy(&pool, SIGNAL(connectDone(int,QString))); QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session *,int,QString))); QSignalSpy lostSpy(&pool, SIGNAL(connectionLost(KIMAP::Session *))); pool.setPasswordRequester(requester); QVERIFY(pool.connect(account)); QTest::qWait(100); QCOMPARE(requesterSpy.count(), 1); QCOMPARE(poolSpy.count(), 1); QCOMPARE(poolSpy.at(0).at(0).toInt(), (int)SessionPool::NoError); QVERIFY(pool.isConnected()); // Ask for the session we just created pool.requestSession(); QTest::qWait(100); QCOMPARE(sessionSpy.count(), 1); QVERIFY(sessionSpy.at(0).at(1).value() != nullptr); KIMAP::Session *session = sessionSpy.at(0).at(1).value(); // Ask for the second session, the password requested will never reply // and we'll get a disconnect in parallel (by triggering the capability // job on the first session // Done this way to simulate a disconnect during a password request // Ask for the extra session, and make sure the call is placed by waiting // just a bit (but not too long so that the requester didn't reply yet, // we set the reply timeout to 1000 earlier in this test) pool.requestSession(); QTest::qWait(100); QCOMPARE(requesterSpy.count(), 1); // Requester didn't reply yet QCOMPARE(sessionSpy.count(), 1); // Make the first (and only) session drop while we wait for the requester // to reply KIMAP::CapabilitiesJob *job = new KIMAP::CapabilitiesJob(session); job->start(); QTest::qWait(100); QCOMPARE(lostSpy.count(), 1); // FIXME extracting the pointer value form QVariant crashes // QCOMPARE(lostSpy.at(0).at(0).value(), session); // The requester didn't reply yet QCOMPARE(requesterSpy.count(), 1); QCOMPARE(sessionSpy.count(), 1); // Now wait the remaining time to get the session creation to fail QTest::qWait(1000); QCOMPARE(requesterSpy.count(), 2); QCOMPARE(sessionSpy.count(), 2); QCOMPARE(sessionSpy.at(1).at(2).toInt(), (int)SessionPool::LoginFailError); QVERIFY(server.isAllScenarioDone()); server.quit(); } + void shouldHandleDisconnectionDuringSecondLogin() + { + FakeServer server; + server.addScenario(QList() + << FakeServer::greeting() + << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" + << "S: A000001 OK User Logged in" + << "C: A000002 CAPABILITY" + << "S: * CAPABILITY IMAP4 IMAP4rev1 IDLE" + << "S: A000002 OK Completed" + ); + + server.addScenario(QList() + << FakeServer::greeting() + << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"" + << "X" + ); + + server.startAndWait(); + + ImapAccount *account = createDefaultAccount(); + DummyPasswordRequester *requester = createDefaultRequester(); + + SessionPool pool(2); + pool.setPasswordRequester(requester); + + QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session *,int,QString))); + QVERIFY(pool.connect(account)); + + // We should be connected now + QTRY_VERIFY(pool.isConnected()); + + // Ask for a session + pool.requestSession(); + QTRY_COMPARE(sessionSpy.count(), 1); + QVERIFY(sessionSpy.at(0).at(1).value() != nullptr); + + // Prepare for session disconnects + QSignalSpy lostSpy(&pool, SIGNAL(connectionLost(KIMAP::Session *))); + + // Ask for a second session, where we'll lose the connection during the Login job. + pool.requestSession(); + QTest::qWait(100); + QCOMPARE(sessionSpy.count(), 2); + QVERIFY(sessionSpy.at(1).at(1).value() == nullptr); + QCOMPARE(lostSpy.count(), 1); + + // The pool itself is still connected + QVERIFY(pool.isConnected()); + + QVERIFY(server.isAllScenarioDone()); + + server.quit(); + } + void shouldNotifyFailureToConnect() { // This tests what happens when we can't connect to the server, e.g. due to being offline. // In this test we just use 0.0.0.0 as an invalid server IP, instead. ImapAccount *account = createDefaultAccount(); account->setServer(QStringLiteral("0.0.0.0")); // so that the connexion fails DummyPasswordRequester *requester = createDefaultRequester(); QList requests; QList results; // I don't want to see "WrongPasswordRequest". A password popup is annoying when we're offline or the server is down. requests << DummyPasswordRequester::StandardRequest; results << DummyPasswordRequester::PasswordRetrieved; requester->setScenario(requests, results); QSignalSpy requesterSpy(requester, SIGNAL(done(int,QString))); SessionPool pool(2); QSignalSpy connectDoneSpy(&pool, SIGNAL(connectDone(int,QString))); QSignalSpy lostSpy(&pool, SIGNAL(connectionLost(KIMAP::Session *))); QVERIFY(!pool.isConnected()); pool.setPasswordRequester(requester); pool.connect(account); QVERIFY(!pool.isConnected()); QTRY_COMPARE(requesterSpy.count(), requests.count()); QTRY_COMPARE(connectDoneSpy.count(), 1); QCOMPARE(connectDoneSpy.at(0).at(0).toInt(), (int)SessionPool::CouldNotConnectError); QCOMPARE(lostSpy.count(), 0); // don't want this, it makes the resource reconnect immediately (and fail, and reconnect, and so on...) } }; QTEST_GUILESS_MAIN(TestSessionPool) #include "testsessionpool.moc" diff --git a/resources/imap/sessionpool.cpp b/resources/imap/sessionpool.cpp index 165dec123..b188b84e0 100644 --- a/resources/imap/sessionpool.cpp +++ b/resources/imap/sessionpool.cpp @@ -1,603 +1,607 @@ /* Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company Author: Kevin Ottens This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "sessionpool.h" #include #include #include "imapresource_debug.h" #include #include #include #include #include #include "imapaccount.h" #include "passwordrequesterinterface.h" qint64 SessionPool::m_requestCounter = 0; SessionPool::SessionPool(int maxPoolSize, QObject *parent) : QObject(parent) , m_maxPoolSize(maxPoolSize) , m_account(nullptr) , m_passwordRequester(nullptr) , m_initialConnectDone(false) , m_pendingInitialSession(nullptr) { } SessionPool::~SessionPool() { disconnect(CloseSession); } PasswordRequesterInterface *SessionPool::passwordRequester() const { return m_passwordRequester; } void SessionPool::setPasswordRequester(PasswordRequesterInterface *requester) { delete m_passwordRequester; m_passwordRequester = requester; m_passwordRequester->setParent(this); QObject::connect(m_passwordRequester, &PasswordRequesterInterface::done, this, &SessionPool::onPasswordRequestDone); } void SessionPool::cancelPasswordRequests() { m_passwordRequester->cancelPasswordRequests(); } KIMAP::SessionUiProxy::Ptr SessionPool::sessionUiProxy() const { return m_sessionUiProxy; } void SessionPool::setSessionUiProxy(KIMAP::SessionUiProxy::Ptr proxy) { m_sessionUiProxy = proxy; } bool SessionPool::isConnected() const { return m_initialConnectDone; } bool SessionPool::connect(ImapAccount *account) { if (m_account) { return false; } m_account = account; if (m_account->authenticationMode() == KIMAP::LoginJob::GSSAPI) { // for GSSAPI we don't have to ask for username/password, because it uses session wide tickets QMetaObject::invokeMethod(this, "onPasswordRequestDone", Qt::QueuedConnection, Q_ARG(int, PasswordRequesterInterface::PasswordRetrieved), Q_ARG(QString, QString())); } else { m_passwordRequester->requestPassword(); } return true; } void SessionPool::disconnect(SessionTermination termination) { if (!m_account) { return; } foreach (KIMAP::Session *s, m_unusedPool + m_reservedPool + m_connectingPool) { killSession(s, termination); } m_unusedPool.clear(); m_reservedPool.clear(); m_connectingPool.clear(); m_pendingInitialSession = nullptr; m_passwordRequester->cancelPasswordRequests(); delete m_account; m_account = nullptr; m_namespaces.clear(); m_capabilities.clear(); m_initialConnectDone = false; Q_EMIT disconnectDone(); } qint64 SessionPool::requestSession() { if (!m_initialConnectDone) { return -1; } qint64 requestNumber = ++m_requestCounter; // The queue was empty, so trigger the processing if (m_pendingRequests.isEmpty()) { QTimer::singleShot(0, this, &SessionPool::processPendingRequests); } m_pendingRequests << requestNumber; return requestNumber; } void SessionPool::cancelSessionRequest(qint64 id) { Q_ASSERT(id > 0); m_pendingRequests.removeAll(id); } void SessionPool::releaseSession(KIMAP::Session *session) { const int removeSession = m_reservedPool.removeAll(session); if (removeSession > 0) { m_unusedPool << session; } } ImapAccount *SessionPool::account() const { return m_account; } QStringList SessionPool::serverCapabilities() const { return m_capabilities; } QList SessionPool::serverNamespaces() const { return m_namespaces; } QList SessionPool::serverNamespaces(Namespace ns) const { switch (ns) { case Personal: return m_personalNamespaces; case User: return m_userNamespaces; case Shared: return m_sharedNamespaces; default: break; } Q_ASSERT(false); return QList(); } void SessionPool::killSession(KIMAP::Session *session, SessionTermination termination) { Q_ASSERT(session); if (!m_unusedPool.contains(session) && !m_reservedPool.contains(session) && !m_connectingPool.contains(session)) { qCWarning(IMAPRESOURCE_LOG) << "Unmanaged session" << session; Q_ASSERT(false); return; } QObject::disconnect(session, &KIMAP::Session::stateChanged, this, &SessionPool::onSessionStateChanged); m_unusedPool.removeAll(session); m_reservedPool.removeAll(session); m_connectingPool.removeAll(session); if (session->state() != KIMAP::Session::Disconnected && termination == LogoutSession) { KIMAP::LogoutJob *logout = new KIMAP::LogoutJob(session); QObject::connect(logout, &KJob::result, session, &QObject::deleteLater); logout->start(); } else { session->close(); session->deleteLater(); } } void SessionPool::declareSessionReady(KIMAP::Session *session) { //This can happen if we happen to disconnect while capabilities and namespace are being retrieved, //resulting in us keeping a dangling pointer to a deleted session if (!m_connectingPool.contains(session)) { qCWarning(IMAPRESOURCE_LOG) << "Tried to declare a removed session ready"; return; } m_pendingInitialSession = nullptr; if (!m_initialConnectDone) { m_initialConnectDone = true; Q_EMIT connectDone(); // If the slot connected to connectDone() decided to disconnect the SessionPool // then we must end here, because we expect the pools to be empty now! if (!m_initialConnectDone) { return; } } m_connectingPool.removeAll(session); if (m_pendingRequests.isEmpty()) { m_unusedPool << session; } else { m_reservedPool << session; Q_EMIT sessionRequestDone(m_pendingRequests.takeFirst(), session); if (!m_pendingRequests.isEmpty()) { QTimer::singleShot(0, this, &SessionPool::processPendingRequests); } } } void SessionPool::cancelSessionCreation(KIMAP::Session *session, int errorCode, const QString &errorMessage) { m_pendingInitialSession = nullptr; QString msg; if (m_account) { msg = i18n("Could not connect to the IMAP-server %1.\n%2", m_account->server(), errorMessage); } else { // Can happen when we lose all ready connections while trying to establish // a new connection, for example. msg = i18n("Could not connect to the IMAP server.\n%1", errorMessage); } if (!m_initialConnectDone) { disconnect(); // kills all sessions, including \a session } else { if (session) { killSession(session, LogoutSession); } if (!m_pendingRequests.isEmpty()) { Q_EMIT sessionRequestDone(m_pendingRequests.takeFirst(), nullptr, errorCode, errorMessage); if (!m_pendingRequests.isEmpty()) { QTimer::singleShot(0, this, &SessionPool::processPendingRequests); } } } // Always emit this at the end. This can call SessionPool::disconnect via ImapResource. Q_EMIT connectDone(errorCode, msg); } void SessionPool::processPendingRequests() { if (!m_unusedPool.isEmpty()) { // We have a session ready to give out KIMAP::Session *session = m_unusedPool.takeFirst(); m_reservedPool << session; if (!m_pendingRequests.isEmpty()) { Q_EMIT sessionRequestDone(m_pendingRequests.takeFirst(), session); if (!m_pendingRequests.isEmpty()) { QTimer::singleShot(0, this, &SessionPool::processPendingRequests); } } } else if (m_unusedPool.size() + m_reservedPool.size() < m_maxPoolSize) { // We didn't reach the max pool size yet so create a new one m_passwordRequester->requestPassword(); } else { // No session available, and max pool size reached if (!m_pendingRequests.isEmpty()) { Q_EMIT sessionRequestDone( m_pendingRequests.takeFirst(), nullptr, NoAvailableSessionError, i18n("Could not create another extra connection to the IMAP-server %1.", m_account->server())); if (!m_pendingRequests.isEmpty()) { QTimer::singleShot(0, this, &SessionPool::processPendingRequests); } } } } void SessionPool::onPasswordRequestDone(int resultType, const QString &password) { QString errorMessage; if (!m_account) { // it looks like the connection was lost while we were waiting // for the password, we should fail all the pending requests and stop there for (int request : qAsConst(m_pendingRequests)) { Q_EMIT sessionRequestDone(request, nullptr, LoginFailError, i18n("Disconnected from server during login.")); } return; } switch (resultType) { case PasswordRequesterInterface::PasswordRetrieved: // All is fine break; case PasswordRequesterInterface::ReconnectNeeded: cancelSessionCreation(m_pendingInitialSession, ReconnectNeededError, errorMessage); return; case PasswordRequesterInterface::UserRejected: errorMessage = i18n("Could not read the password: user rejected wallet access"); if (m_pendingInitialSession) { cancelSessionCreation(m_pendingInitialSession, LoginFailError, errorMessage); } else { Q_EMIT connectDone(PasswordRequestError, errorMessage); } return; case PasswordRequesterInterface::EmptyPasswordEntered: errorMessage = i18n("Empty password"); if (m_pendingInitialSession) { cancelSessionCreation(m_pendingInitialSession, LoginFailError, errorMessage); } else { Q_EMIT connectDone(PasswordRequestError, errorMessage); } return; } if (m_account->encryptionMode() != KIMAP::LoginJob::Unencrypted && !QSslSocket::supportsSsl()) { qCWarning(IMAPRESOURCE_LOG) << "Crypto not supported!"; Q_EMIT connectDone(EncryptionError, i18n("You requested TLS/SSL to connect to %1, but your " "system does not seem to be set up for that.", m_account->server())); disconnect(); return; } KIMAP::Session *session = nullptr; if (m_pendingInitialSession) { session = m_pendingInitialSession; } else { session = new KIMAP::Session(m_account->server(), m_account->port(), this); QObject::connect(session, &QObject::destroyed, this, &SessionPool::onSessionDestroyed); session->setUiProxy(m_sessionUiProxy); session->setTimeout(m_account->timeout()); m_connectingPool << session; } QObject::connect(session, &KIMAP::Session::stateChanged, this, &SessionPool::onSessionStateChanged); KIMAP::LoginJob *loginJob = new KIMAP::LoginJob(session); loginJob->setUserName(m_account->userName()); loginJob->setPassword(password); loginJob->setEncryptionMode(m_account->encryptionMode()); loginJob->setAuthenticationMode(m_account->authenticationMode()); QObject::connect(loginJob, &KJob::result, this, &SessionPool::onLoginDone); loginJob->start(); } void SessionPool::onLoginDone(KJob *job) { KIMAP::LoginJob *login = static_cast(job); // Can happen if we disconnected meanwhile if (!m_connectingPool.contains(login->session())) { Q_EMIT connectDone(CancelledError, i18n("Disconnected from server during login.")); return; } if (job->error() == 0) { if (m_initialConnectDone) { declareSessionReady(login->session()); } else { // On initial connection we ask for capabilities KIMAP::CapabilitiesJob *capJob = new KIMAP::CapabilitiesJob(login->session()); QObject::connect(capJob, &KIMAP::CapabilitiesJob::result, this, &SessionPool::onCapabilitiesTestDone); capJob->start(); } } else { if (job->error() == KIMAP::LoginJob::ERR_COULD_NOT_CONNECT) { if (m_account) { cancelSessionCreation(login->session(), CouldNotConnectError, i18n("Could not connect to the IMAP-server %1.\n%2", m_account->server(), job->errorString())); } else { // Can happen when we lose all ready connections while trying to login. cancelSessionCreation(login->session(), CouldNotConnectError, i18n("Could not connect to the IMAP-server.\n%1", job->errorString())); } } else { // Connection worked, but login failed -> ask for a different password or ssl settings. m_pendingInitialSession = login->session(); m_passwordRequester->requestPassword(PasswordRequesterInterface::WrongPasswordRequest, job->errorString()); } } } void SessionPool::onCapabilitiesTestDone(KJob *job) { KIMAP::CapabilitiesJob *capJob = qobject_cast(job); // Can happen if we disconnected meanwhile if (!m_connectingPool.contains(capJob->session())) { Q_EMIT connectDone(CancelledError, i18n("Disconnected from server during login.")); return; } if (job->error()) { if (m_account) { cancelSessionCreation(capJob->session(), CapabilitiesTestError, i18n("Could not test the capabilities supported by the " "IMAP server %1.\n%2", m_account->server(), job->errorString())); } else { // Can happen when we lose all ready connections while trying to check capabilities. cancelSessionCreation(capJob->session(), CapabilitiesTestError, i18n("Could not test the capabilities supported by the " "IMAP server.\n%1", job->errorString())); } return; } m_capabilities = capJob->capabilities(); QStringList missing; const QStringList expected = {QStringLiteral("IMAP4REV1")}; for (const QString &capability : expected) { if (!m_capabilities.contains(capability)) { missing << capability; } } if (!missing.isEmpty()) { cancelSessionCreation(capJob->session(), IncompatibleServerError, i18n("Cannot use the IMAP server %1, " "some mandatory capabilities are missing: %2. " "Please ask your sysadmin to upgrade the server.", m_account->server(), missing.join(QStringLiteral(", ")))); return; } // If the extension is supported, grab the namespaces from the server if (m_capabilities.contains(QLatin1String("NAMESPACE"))) { KIMAP::NamespaceJob *nsJob = new KIMAP::NamespaceJob(capJob->session()); QObject::connect(nsJob, &KIMAP::NamespaceJob::result, this, &SessionPool::onNamespacesTestDone); nsJob->start(); return; } else if (m_capabilities.contains(QLatin1String("ID"))) { KIMAP::IdJob *idJob = new KIMAP::IdJob(capJob->session()); idJob->setField("name", m_clientId); QObject::connect(idJob, &KIMAP::IdJob::result, this, &SessionPool::onIdDone); idJob->start(); return; } else { declareSessionReady(capJob->session()); } } void SessionPool::setClientId(const QByteArray &clientId) { m_clientId = clientId; } void SessionPool::onNamespacesTestDone(KJob *job) { KIMAP::NamespaceJob *nsJob = qobject_cast(job); // Can happen if we disconnect meanwhile if (!m_connectingPool.contains(nsJob->session())) { Q_EMIT connectDone(CancelledError, i18n("Disconnected from server during login.")); return; } m_personalNamespaces = nsJob->personalNamespaces(); m_userNamespaces = nsJob->userNamespaces(); m_sharedNamespaces = nsJob->sharedNamespaces(); if (nsJob->containsEmptyNamespace()) { // When we got the empty namespace here, we assume that the other // ones can be freely ignored and that the server will give us all // the mailboxes if we list from the empty namespace itself... m_namespaces.clear(); } else { // ... otherwise we assume that we have to list explicitly each // namespace m_namespaces = nsJob->personalNamespaces() +nsJob->userNamespaces() +nsJob->sharedNamespaces(); } if (m_capabilities.contains(QLatin1String("ID"))) { KIMAP::IdJob *idJob = new KIMAP::IdJob(nsJob->session()); idJob->setField("name", m_clientId); QObject::connect(idJob, &KIMAP::IdJob::result, this, &SessionPool::onIdDone); idJob->start(); return; } else { declareSessionReady(nsJob->session()); } } void SessionPool::onIdDone(KJob *job) { KIMAP::IdJob *idJob = qobject_cast(job); // Can happen if we disconnected meanwhile if (!m_connectingPool.contains(idJob->session())) { Q_EMIT connectDone(CancelledError, i18n("Disconnected during login.")); return; } declareSessionReady(idJob->session()); } void SessionPool::onSessionStateChanged(KIMAP::Session::State newState, KIMAP::Session::State oldState) { if (newState == KIMAP::Session::Disconnected && oldState != KIMAP::Session::Disconnected) { onConnectionLost(); } } void SessionPool::onConnectionLost() { KIMAP::Session *session = static_cast(sender()); m_unusedPool.removeAll(session); m_reservedPool.removeAll(session); m_connectingPool.removeAll(session); if (m_unusedPool.isEmpty() && m_reservedPool.isEmpty()) { m_passwordRequester->cancelPasswordRequests(); delete m_account; m_account = nullptr; m_namespaces.clear(); m_capabilities.clear(); m_initialConnectDone = false; } Q_EMIT connectionLost(session); + if (!m_pendingRequests.isEmpty()) { + cancelSessionCreation(nullptr, CouldNotConnectError, QString()); + } + session->deleteLater(); if (session == m_pendingInitialSession) { m_pendingInitialSession = nullptr; } } void SessionPool::onSessionDestroyed(QObject *object) { //Safety net for bugs that cause dangling session pointers KIMAP::Session *session = static_cast(object); bool sessionInPool = false; if (m_unusedPool.contains(session)) { qCWarning(IMAPRESOURCE_LOG) << "Session" << object << "destroyed while still in unused pool!"; m_unusedPool.removeAll(session); sessionInPool = true; } if (m_reservedPool.contains(session)) { qCWarning(IMAPRESOURCE_LOG) << "Session" << object << "destroyed while still in reserved pool!"; m_reservedPool.removeAll(session); sessionInPool = true; } if (m_connectingPool.contains(session)) { qCWarning(IMAPRESOURCE_LOG) << "Session" << object << "destroyed while still in connecting pool!"; m_connectingPool.removeAll(session); sessionInPool = true; } Q_ASSERT(!sessionInPool); }