Просмотр исходного кода

Merge pull request #3031 from nextcloud/feature/push-notifications-ping-server

Push notifications: Ping server
Felix Weilbach 4 лет назад
Родитель
Сommit
9c4a9958f7

+ 3 - 0
src/gui/accountstate.cpp

@@ -328,6 +328,9 @@ void AccountState::slotConnectionValidatorResult(ConnectionValidator::Status sta
 
             // Get the Apps available on the server.
             fetchNavigationApps();
+
+            // Setup push notifications after a successful connection
+            account()->trySetupPushNotifications();
         }
         break;
     case ConnectionValidator::Undefined:

+ 4 - 0
src/libsync/account.cpp

@@ -215,6 +215,10 @@ void Account::trySetupPushNotifications()
 
             const auto deletePushNotifications = [this]() {
                 qCInfo(lcAccount) << "Delete push notifications object because authentication failed or connection lost";
+                if (!_pushNotifications) {
+                    return;
+                }
+                Q_ASSERT(!_pushNotifications->isReady());
                 _pushNotifications->deleteLater();
                 _pushNotifications = nullptr;
                 emit pushNotificationsDisabled(this);

+ 1 - 1
src/libsync/account.h

@@ -251,6 +251,7 @@ public:
     // Check for the directEditing capability
     void fetchDirectEditors(const QUrl &directEditingURL, const QString &directEditingETag);
 
+    void trySetupPushNotifications();
     PushNotifications *pushNotifications() const;
 
 public slots:
@@ -293,7 +294,6 @@ protected Q_SLOTS:
 private:
     Account(QObject *parent = nullptr);
     void setSharedThis(AccountPtr sharedThis);
-    void trySetupPushNotifications();
 
     QWeakPointer<Account> _sharedThis;
     QString _id;

+ 101 - 11
src/libsync/pushnotifications.cpp

@@ -4,6 +4,7 @@
 
 namespace {
 static constexpr int MAX_ALLOWED_FAILED_AUTHENTICATION_ATTEMPTS = 3;
+static constexpr int PING_INTERVAL = 30 * 1000;
 }
 
 namespace OCC {
@@ -14,6 +15,13 @@ PushNotifications::PushNotifications(Account *account, QObject *parent)
     : QObject(parent)
     , _account(account)
 {
+    connect(&_pingTimer, &QTimer::timeout, this, &PushNotifications::pingWebSocketServer);
+    _pingTimer.setSingleShot(true);
+    _pingTimer.setInterval(PING_INTERVAL);
+
+    connect(&_pingTimedOutTimer, &QTimer::timeout, this, &PushNotifications::onPingTimedOut);
+    _pingTimedOutTimer.setSingleShot(true);
+    _pingTimedOutTimer.setInterval(PING_INTERVAL);
 }
 
 PushNotifications::~PushNotifications()
@@ -23,7 +31,7 @@ PushNotifications::~PushNotifications()
 
 void PushNotifications::setup()
 {
-    _isReady = false;
+    qCInfo(lcPushNotifications) << "Setup push notifications";
     _failedAuthenticationAttemptsCount = 0;
     reconnectToWebSocket();
 }
@@ -36,15 +44,25 @@ void PushNotifications::reconnectToWebSocket()
 
 void PushNotifications::closeWebSocket()
 {
+    qCInfo(lcPushNotifications) << "Close websocket" << _webSocket << "for account" << _account->url();
+
+    _pingTimer.stop();
+    _pingTimedOutTimer.stop();
+    _isReady = false;
+
+    // Maybe there run some reconnection attempts
+    if (_reconnectTimer) {
+        _reconnectTimer->stop();
+    }
+
     if (_webSocket) {
-        qCInfo(lcPushNotifications) << "Close websocket";
         _webSocket->close();
     }
 }
 
 void PushNotifications::onWebSocketConnected()
 {
-    qCInfo(lcPushNotifications) << "Connected to websocket";
+    qCInfo(lcPushNotifications) << "Connected to websocket" << _webSocket << "for account" << _account->url();
 
     connect(_webSocket, &QWebSocket::textMessageReceived, this, &PushNotifications::onWebSocketTextMessageReceived, Qt::UniqueConnection);
 
@@ -64,7 +82,7 @@ void PushNotifications::authenticateOnWebSocket()
 
 void PushNotifications::onWebSocketDisconnected()
 {
-    qCInfo(lcPushNotifications) << "Disconnected from websocket";
+    qCInfo(lcPushNotifications) << "Disconnected from websocket" << _webSocket << "for account" << _account->url();
 }
 
 void PushNotifications::onWebSocketTextMessageReceived(const QString &message)
@@ -93,8 +111,7 @@ void PushNotifications::onWebSocketError(QAbstractSocket::SocketError error)
         return;
     }
 
-    qCWarning(lcPushNotifications) << "Websocket error" << error;
-
+    qCWarning(lcPushNotifications) << "Websocket error on" << _webSocket << "with account" << _account->url() << error;
     _isReady = false;
     emit connectionLost();
 }
@@ -123,7 +140,7 @@ bool PushNotifications::tryReconnectToWebSocket()
 
 void PushNotifications::onWebSocketSslErrors(const QList<QSslError> &errors)
 {
-    qCWarning(lcPushNotifications) << "Received websocket ssl errors:" << errors;
+    qCWarning(lcPushNotifications) << "Websocket ssl errors on" << _webSocket << "with account" << _account->url() << errors;
     _isReady = false;
     emit authenticationFailed();
 }
@@ -135,8 +152,8 @@ void PushNotifications::openWebSocket()
     const auto webSocketUrl = capabilities.pushNotificationsWebSocketUrl();
 
     if (!_webSocket) {
-        qCInfo(lcPushNotifications) << "Create websocket";
         _webSocket = new QWebSocket(QString(), QWebSocketProtocol::VersionLatest, this);
+        qCInfo(lcPushNotifications) << "Created websocket" << _webSocket << "for account" << _account->url();
     }
 
     if (_webSocket) {
@@ -144,6 +161,7 @@ void PushNotifications::openWebSocket()
         connect(_webSocket, &QWebSocket::sslErrors, this, &PushNotifications::onWebSocketSslErrors, Qt::UniqueConnection);
         connect(_webSocket, &QWebSocket::connected, this, &PushNotifications::onWebSocketConnected, Qt::UniqueConnection);
         connect(_webSocket, &QWebSocket::disconnected, this, &PushNotifications::onWebSocketDisconnected, Qt::UniqueConnection);
+        connect(_webSocket, &QWebSocket::pong, this, &PushNotifications::onWebSocketPongReceived, Qt::UniqueConnection);
 
         qCInfo(lcPushNotifications) << "Open connection to websocket on:" << webSocketUrl;
         _webSocket->open(webSocketUrl);
@@ -165,20 +183,28 @@ void PushNotifications::handleAuthenticated()
     qCInfo(lcPushNotifications) << "Authenticated successful on websocket";
     _failedAuthenticationAttemptsCount = 0;
     _isReady = true;
+    startPingTimer();
     emit ready();
+
+    // We maybe reconnected to websocket while being offline for a
+    // while. To not miss any notifications that may have happend,
+    // emit all the signals once.
+    emitFilesChanged();
+    emitNotificationsChanged();
+    emitActivitiesChanged();
 }
 
 void PushNotifications::handleNotifyFile()
 {
     qCInfo(lcPushNotifications) << "Files push notification arrived";
-    emit filesChanged(_account);
+    emitFilesChanged();
 }
 
 void PushNotifications::handleInvalidCredentials()
 {
     qCInfo(lcPushNotifications) << "Invalid credentials submitted to websocket";
     if (!tryReconnectToWebSocket()) {
-        _isReady = false;
+        closeWebSocket();
         emit authenticationFailed();
     }
 }
@@ -186,12 +212,76 @@ void PushNotifications::handleInvalidCredentials()
 void PushNotifications::handleNotifyNotification()
 {
     qCInfo(lcPushNotifications) << "Push notification arrived";
-    emit notificationsChanged(_account);
+    emitNotificationsChanged();
 }
 
 void PushNotifications::handleNotifyActivity()
 {
     qCInfo(lcPushNotifications) << "Push activity arrived";
+    emitActivitiesChanged();
+}
+
+void PushNotifications::onWebSocketPongReceived(quint64 /*elapsedTime*/, const QByteArray & /*payload*/)
+{
+    qCDebug(lcPushNotifications) << "Pong received in time";
+    // We are fine with every kind of pong and don't care about the
+    // payload. As long as we receive pongs the server is still alive.
+    _pongReceivedFromWebSocketServer = true;
+    startPingTimer();
+}
+
+void PushNotifications::startPingTimer()
+{
+    _pingTimedOutTimer.stop();
+    _pingTimer.start();
+}
+
+void PushNotifications::startPingTimedOutTimer()
+{
+    _pingTimedOutTimer.start();
+}
+
+void PushNotifications::pingWebSocketServer()
+{
+    Q_ASSERT(_webSocket);
+    qCDebug(lcPushNotifications, "Ping websocket server");
+
+    _pongReceivedFromWebSocketServer = false;
+
+    _webSocket->ping({});
+    startPingTimedOutTimer();
+}
+
+void PushNotifications::onPingTimedOut()
+{
+    if (_pongReceivedFromWebSocketServer) {
+        qCDebug(lcPushNotifications) << "Websocket respond with a pong in time.";
+        return;
+    }
+
+    qCInfo(lcPushNotifications) << "Websocket did not respond with a pong in time. Try to reconnect.";
+    // Try again to connect
+    setup();
+}
+
+void PushNotifications::setPingInterval(int timeoutInterval)
+{
+    _pingTimer.setInterval(timeoutInterval);
+    _pingTimedOutTimer.setInterval(timeoutInterval);
+}
+
+void PushNotifications::emitFilesChanged()
+{
+    emit filesChanged(_account);
+}
+
+void PushNotifications::emitNotificationsChanged()
+{
+    emit notificationsChanged(_account);
+}
+
+void PushNotifications::emitActivitiesChanged()
+{
     emit activitiesChanged(_account);
 }
 }

+ 24 - 1
src/libsync/pushnotifications.h

@@ -42,6 +42,8 @@ public:
 
     /**
      * Set the interval for reconnection attempts
+     *
+     * @param interval Interval in milliseconds.
      */
     void setReconnectTimerInterval(uint32_t interval);
 
@@ -52,6 +54,15 @@ public:
      */
     bool isReady() const;
 
+    /**
+     * Set the interval in which the websocket will ping the server if it is still alive.
+     *
+     * If the websocket does not respond in timeoutInterval, the connection will be terminated.
+     *
+     * @param interval Interval in milliseconds.
+     */
+    void setPingInterval(int interval);
+
 signals:
     /**
      * Will be emitted after a successful connection and authentication
@@ -93,6 +104,8 @@ private slots:
     void onWebSocketTextMessageReceived(const QString &message);
     void onWebSocketError(QAbstractSocket::SocketError error);
     void onWebSocketSslErrors(const QList<QSslError> &errors);
+    void onWebSocketPongReceived(quint64 elapsedTime, const QByteArray &payload);
+    void onPingTimedOut();
 
 private:
     void openWebSocket();
@@ -101,6 +114,9 @@ private:
     void authenticateOnWebSocket();
     bool tryReconnectToWebSocket();
     void initReconnectTimer();
+    void pingWebSocketServer();
+    void startPingTimer();
+    void startPingTimedOutTimer();
 
     void handleAuthenticated();
     void handleNotifyFile();
@@ -108,12 +124,19 @@ private:
     void handleNotifyNotification();
     void handleNotifyActivity();
 
+    void emitFilesChanged();
+    void emitNotificationsChanged();
+    void emitActivitiesChanged();
+
     Account *_account = nullptr;
     QWebSocket *_webSocket = nullptr;
     uint8_t _failedAuthenticationAttemptsCount = 0;
     QTimer *_reconnectTimer = nullptr;
     uint32_t _reconnectTimerInterval = 20 * 1000;
     bool _isReady = false;
-};
 
+    QTimer _pingTimer;
+    QTimer _pingTimedOutTimer;
+    bool _pongReceivedFromWebSocketServer = false;
+};
 }

+ 79 - 7
test/pushnotificationstestutils.cpp

@@ -1,8 +1,10 @@
 #include <QLoggingCategory>
 #include <QSignalSpy>
 #include <QTest>
+#include <functional>
 
 #include "pushnotificationstestutils.h"
+#include "pushnotifications.h"
 
 Q_LOGGING_CATEGORY(lcFakeWebSocketServer, "nextcloud.test.fakewebserver", QtInfoMsg)
 
@@ -10,13 +12,13 @@ FakeWebSocketServer::FakeWebSocketServer(quint16 port, QObject *parent)
     : QObject(parent)
     , _webSocketServer(new QWebSocketServer(QStringLiteral("Fake Server"), QWebSocketServer::NonSecureMode, this))
 {
-    if (_webSocketServer->listen(QHostAddress::Any, port)) {
-        connect(_webSocketServer, &QWebSocketServer::newConnection, this, &FakeWebSocketServer::onNewConnection);
-        connect(_webSocketServer, &QWebSocketServer::closed, this, &FakeWebSocketServer::closed);
-        qCInfo(lcFakeWebSocketServer) << "Open fake websocket server on port:" << port;
-        return;
+    if (!_webSocketServer->listen(QHostAddress::Any, port)) {
+        Q_UNREACHABLE();
     }
-    Q_UNREACHABLE();
+    connect(_webSocketServer, &QWebSocketServer::newConnection, this, &FakeWebSocketServer::onNewConnection);
+    connect(_webSocketServer, &QWebSocketServer::closed, this, &FakeWebSocketServer::closed);
+    qCInfo(lcFakeWebSocketServer) << "Open fake websocket server on port:" << port;
+    _processTextMessageSpy = std::make_unique<QSignalSpy>(this, &FakeWebSocketServer::processTextMessage);
 }
 
 FakeWebSocketServer::~FakeWebSocketServer()
@@ -24,6 +26,46 @@ FakeWebSocketServer::~FakeWebSocketServer()
     close();
 }
 
+QWebSocket *FakeWebSocketServer::authenticateAccount(const OCC::AccountPtr account, std::function<void(OCC::PushNotifications *pushNotifications)> beforeAuthentication, std::function<void(void)> afterAuthentication)
+{
+    const auto pushNotifications = account->pushNotifications();
+    Q_ASSERT(pushNotifications);
+    QSignalSpy readySpy(pushNotifications, &OCC::PushNotifications::ready);
+
+    beforeAuthentication(pushNotifications);
+
+    // Wait for authentication
+    if (!waitForTextMessages()) {
+        return nullptr;
+    }
+
+    // Right authentication data should be sent
+    if (textMessagesCount() != 2) {
+        return nullptr;
+    }
+
+    const auto socket = socketForTextMessage(0);
+    const auto userSent = textMessage(0);
+    const auto passwordSent = textMessage(1);
+
+    if (userSent != account->credentials()->user() || passwordSent != account->credentials()->password()) {
+        return nullptr;
+    }
+
+    // Sent authenticated
+    socket->sendTextMessage("authenticated");
+
+    // Wait for ready signal
+    readySpy.wait();
+    if (readySpy.count() != 1 || !account->pushNotifications()->isReady()) {
+        return nullptr;
+    }
+
+    afterAuthentication();
+
+    return socket;
+}
+
 void FakeWebSocketServer::close()
 {
     if (_webSocketServer->isListening()) {
@@ -64,7 +106,34 @@ void FakeWebSocketServer::socketDisconnected()
     }
 }
 
-OCC::AccountPtr FakeWebSocketServer::createAccount()
+bool FakeWebSocketServer::waitForTextMessages() const
+{
+    return _processTextMessageSpy->wait();
+}
+
+uint32_t FakeWebSocketServer::textMessagesCount() const
+{
+    return _processTextMessageSpy->count();
+}
+
+QString FakeWebSocketServer::textMessage(uint32_t messageNumber) const
+{
+    Q_ASSERT(messageNumber < _processTextMessageSpy->count());
+    return _processTextMessageSpy->at(messageNumber).at(1).toString();
+}
+
+QWebSocket *FakeWebSocketServer::socketForTextMessage(uint32_t messageNumber) const
+{
+    Q_ASSERT(messageNumber < _processTextMessageSpy->count());
+    return _processTextMessageSpy->at(messageNumber).at(0).value<QWebSocket *>();
+}
+
+void FakeWebSocketServer::clearTextMessages()
+{
+    _processTextMessageSpy->clear();
+}
+
+OCC::AccountPtr FakeWebSocketServer::createAccount(const QString &username, const QString &password)
 {
     auto account = OCC::Account::create();
 
@@ -87,6 +156,9 @@ OCC::AccountPtr FakeWebSocketServer::createAccount()
 
     account->setCapabilities(capabilitiesMap);
 
+    auto credentials = new CredentialsStub(username, password);
+    account->setCredentials(credentials);
+
     return account;
 }
 

+ 17 - 1
test/pushnotificationstestutils.h

@@ -18,6 +18,7 @@
 
 #include <QWebSocketServer>
 #include <QWebSocket>
+#include <QSignalSpy>
 
 #include "creds/abstractcredentials.h"
 #include "account.h"
@@ -30,9 +31,22 @@ public:
 
     ~FakeWebSocketServer();
 
+    QWebSocket *authenticateAccount(
+        const OCC::AccountPtr account, std::function<void(OCC::PushNotifications *pushNotifications)> beforeAuthentication = [](OCC::PushNotifications *) {}, std::function<void(void)> afterAuthentication = [] {});
+
     void close();
 
-    static OCC::AccountPtr createAccount();
+    bool waitForTextMessages() const;
+
+    uint32_t textMessagesCount() const;
+
+    QString textMessage(uint32_t messageNumber) const;
+
+    QWebSocket *socketForTextMessage(uint32_t messageNumber) const;
+
+    void clearTextMessages();
+
+    static OCC::AccountPtr createAccount(const QString &username = "user", const QString &password = "password");
 
 signals:
     void closed();
@@ -46,6 +60,8 @@ private slots:
 private:
     QWebSocketServer *_webSocketServer;
     QList<QWebSocket *> _clients;
+
+    std::unique_ptr<QSignalSpy> _processTextMessageSpy;
 };
 
 class CredentialsStub : public OCC::AbstractCredentials

+ 95 - 192
test/testpushnotifications.cpp

@@ -3,9 +3,24 @@
 #include <QWebSocketServer>
 #include <QSignalSpy>
 
+#include "accountfwd.h"
 #include "pushnotifications.h"
 #include "pushnotificationstestutils.h"
 
+bool verifyCalledOnceWithAccount(QSignalSpy &spy, OCC::AccountPtr account)
+{
+    if (spy.count() != 1) {
+        return false;
+    }
+
+    auto accountFromSpy = spy.at(0).at(0).value<OCC::Account *>();
+    if (accountFromSpy != account.data()) {
+        return false;
+    }
+
+    return true;
+}
+
 class TestPushNotifications : public QObject
 {
     Q_OBJECT
@@ -14,170 +29,107 @@ private slots:
     void testSetup_correctCredentials_authenticateAndEmitReady()
     {
         FakeWebSocketServer fakeServer;
-        QSignalSpy processTextMessageSpy(&fakeServer, &FakeWebSocketServer::processTextMessage);
-        QVERIFY(processTextMessageSpy.isValid());
-        const QString user = "user";
-        const QString password = "password";
+        std::unique_ptr<QSignalSpy> filesChangedSpy;
+        std::unique_ptr<QSignalSpy> notificationsChangedSpy;
+        std::unique_ptr<QSignalSpy> activitiesChangedSpy;
         auto account = FakeWebSocketServer::createAccount();
-        auto credentials = new CredentialsStub(user, password);
-        account->setCredentials(credentials);
-        QSignalSpy readySpy(account->pushNotifications(), &OCC::PushNotifications::ready);
-        QVERIFY(readySpy.isValid());
-
-        // Wait for authentication
-        QVERIFY(processTextMessageSpy.wait());
 
-        // Right authentication data should be sent
-        QCOMPARE(processTextMessageSpy.count(), 2);
-
-        const auto socket = processTextMessageSpy.at(0).at(0).value<QWebSocket *>();
-        const auto userSent = processTextMessageSpy.at(0).at(1).toString();
-        const auto passwordSent = processTextMessageSpy.at(1).at(1).toString();
-
-        QCOMPARE(userSent, user);
-        QCOMPARE(passwordSent, password);
-
-        // Sent authenticated
-        socket->sendTextMessage("authenticated");
-
-        // Wait for ready signal
-        readySpy.wait();
-        QCOMPARE(readySpy.count(), 1);
-        QCOMPARE(account->pushNotifications()->isReady(), true);
+        QVERIFY(fakeServer.authenticateAccount(
+            account, [&](OCC::PushNotifications *pushNotifications) {
+                filesChangedSpy.reset(new QSignalSpy(pushNotifications, &OCC::PushNotifications::filesChanged));
+                notificationsChangedSpy.reset(new QSignalSpy(pushNotifications, &OCC::PushNotifications::notificationsChanged));
+                activitiesChangedSpy.reset(new QSignalSpy(pushNotifications, &OCC::PushNotifications::activitiesChanged));
+            },
+            [&] {
+                QVERIFY(verifyCalledOnceWithAccount(*filesChangedSpy, account));
+                QVERIFY(verifyCalledOnceWithAccount(*notificationsChangedSpy, account));
+                QVERIFY(verifyCalledOnceWithAccount(*activitiesChangedSpy, account));
+            }));
     }
 
     void testOnWebSocketTextMessageReceived_notifyFileMessage_emitFilesChanged()
     {
-        const QString user = "user";
-        const QString password = "password";
         FakeWebSocketServer fakeServer;
-        QSignalSpy processTextMessageSpy(&fakeServer, &FakeWebSocketServer::processTextMessage);
-        QVERIFY(processTextMessageSpy.isValid());
-
         auto account = FakeWebSocketServer::createAccount();
-        auto credentials = new CredentialsStub(user, password);
-        account->setCredentials(credentials);
+        const auto socket = fakeServer.authenticateAccount(account);
+        QVERIFY(socket);
         QSignalSpy filesChangedSpy(account->pushNotifications(), &OCC::PushNotifications::filesChanged);
-        QVERIFY(filesChangedSpy.isValid());
 
-        // Wait for authentication and then send notify_file push notification
-        QVERIFY(processTextMessageSpy.wait());
-        QCOMPARE(processTextMessageSpy.count(), 2);
-        const auto socket = processTextMessageSpy.at(0).at(0).value<QWebSocket *>();
         socket->sendTextMessage("notify_file");
 
         // filesChanged signal should be emitted
         QVERIFY(filesChangedSpy.wait());
-        QCOMPARE(filesChangedSpy.count(), 1);
-        auto accountFilesChanged = filesChangedSpy.at(0).at(0).value<OCC::Account *>();
-        QCOMPARE(accountFilesChanged, account.data());
+        QVERIFY(verifyCalledOnceWithAccount(filesChangedSpy, account));
     }
 
     void testOnWebSocketTextMessageReceived_notifyActivityMessage_emitNotification()
     {
-        const QString user = "user";
-        const QString password = "password";
         FakeWebSocketServer fakeServer;
-        QSignalSpy processTextMessageSpy(&fakeServer, &FakeWebSocketServer::processTextMessage);
-        QVERIFY(processTextMessageSpy.isValid());
-
         auto account = FakeWebSocketServer::createAccount();
-        auto credentials = new CredentialsStub(user, password);
-        account->setCredentials(credentials);
+        const auto socket = fakeServer.authenticateAccount(account);
+        QVERIFY(socket);
         QSignalSpy activitySpy(account->pushNotifications(), &OCC::PushNotifications::activitiesChanged);
         QVERIFY(activitySpy.isValid());
 
-        // Wait for authentication and then send notify_file push notification
-        QVERIFY(processTextMessageSpy.wait());
-        QCOMPARE(processTextMessageSpy.count(), 2);
-        const auto socket = processTextMessageSpy.at(0).at(0).value<QWebSocket *>();
+        // Send notify_file push notification
         socket->sendTextMessage("notify_activity");
 
         // notification signal should be emitted
         QVERIFY(activitySpy.wait());
-        QCOMPARE(activitySpy.count(), 1);
-        auto accountFilesChanged = activitySpy.at(0).at(0).value<OCC::Account *>();
-        QCOMPARE(accountFilesChanged, account.data());
+        QVERIFY(verifyCalledOnceWithAccount(activitySpy, account));
     }
 
     void testOnWebSocketTextMessageReceived_notifyNotificationMessage_emitNotification()
     {
-        const QString user = "user";
-        const QString password = "password";
         FakeWebSocketServer fakeServer;
-        QSignalSpy processTextMessageSpy(&fakeServer, &FakeWebSocketServer::processTextMessage);
-        QVERIFY(processTextMessageSpy.isValid());
-
         auto account = FakeWebSocketServer::createAccount();
-        auto credentials = new CredentialsStub(user, password);
-        account->setCredentials(credentials);
+        const auto socket = fakeServer.authenticateAccount(account);
+        QVERIFY(socket);
         QSignalSpy notificationSpy(account->pushNotifications(), &OCC::PushNotifications::notificationsChanged);
         QVERIFY(notificationSpy.isValid());
 
-        // Wait for authentication and then send notify_file push notification
-        QVERIFY(processTextMessageSpy.wait());
-        QCOMPARE(processTextMessageSpy.count(), 2);
-        const auto socket = processTextMessageSpy.at(0).at(0).value<QWebSocket *>();
+        // Send notify_file push notification
         socket->sendTextMessage("notify_notification");
 
         // notification signal should be emitted
         QVERIFY(notificationSpy.wait());
-        QCOMPARE(notificationSpy.count(), 1);
-        auto accountFilesChanged = notificationSpy.at(0).at(0).value<OCC::Account *>();
-        QCOMPARE(accountFilesChanged, account.data());
+        QVERIFY(verifyCalledOnceWithAccount(notificationSpy, account));
     }
 
     void testOnWebSocketTextMessageReceived_invalidCredentialsMessage_reconnectWebSocket()
     {
-        const QString user = "user";
-        const QString password = "password";
         FakeWebSocketServer fakeServer;
-        QSignalSpy processTextMessageSpy(&fakeServer, &FakeWebSocketServer::processTextMessage);
-        QVERIFY(processTextMessageSpy.isValid());
-
         auto account = FakeWebSocketServer::createAccount();
-        auto credentials = new CredentialsStub(user, password);
-        account->setCredentials(credentials);
         // Need to set reconnect timer interval to zero for tests
         account->pushNotifications()->setReconnectTimerInterval(0);
 
         // Wait for authentication attempt and then sent invalid credentials
-        QVERIFY(processTextMessageSpy.wait());
-        QCOMPARE(processTextMessageSpy.count(), 2);
-        const auto socket = processTextMessageSpy.at(0).at(0).value<QWebSocket *>();
-        const auto firstPasswordSent = processTextMessageSpy.at(1).at(1).toString();
-        QCOMPARE(firstPasswordSent, password);
-        processTextMessageSpy.clear();
+        QVERIFY(fakeServer.waitForTextMessages());
+        QCOMPARE(fakeServer.textMessagesCount(), 2);
+        const auto socket = fakeServer.socketForTextMessage(0);
+        const auto firstPasswordSent = fakeServer.textMessage(1);
+        QCOMPARE(firstPasswordSent, account->credentials()->password());
+        fakeServer.clearTextMessages();
         socket->sendTextMessage("err: Invalid credentials");
 
         // Wait for a new authentication attempt
-        QVERIFY(processTextMessageSpy.wait());
-        QCOMPARE(processTextMessageSpy.count(), 2);
-        const auto secondPasswordSent = processTextMessageSpy.at(1).at(1).toString();
-        QCOMPARE(secondPasswordSent, password);
+        QVERIFY(fakeServer.waitForTextMessages());
+        QCOMPARE(fakeServer.textMessagesCount(), 2);
+        const auto secondPasswordSent = fakeServer.textMessage(1);
+        QCOMPARE(secondPasswordSent, account->credentials()->password());
     }
 
     void testOnWebSocketError_connectionLost_emitConnectionLost()
     {
-        const QString user = "user";
-        const QString password = "password";
         FakeWebSocketServer fakeServer;
-        QSignalSpy processTextMessageSpy(&fakeServer, &FakeWebSocketServer::processTextMessage);
-        QVERIFY(processTextMessageSpy.isValid());
-
         auto account = FakeWebSocketServer::createAccount();
-        auto credentials = new CredentialsStub(user, password);
-        account->setCredentials(credentials);
-        // Need to set reconnect timer interval to zero for tests
-        account->pushNotifications()->setReconnectTimerInterval(0);
-
         QSignalSpy connectionLostSpy(account->pushNotifications(), &OCC::PushNotifications::connectionLost);
         QVERIFY(connectionLostSpy.isValid());
 
         // Wait for authentication and then sent a network error
-        processTextMessageSpy.wait();
-        QCOMPARE(processTextMessageSpy.count(), 2);
-        auto socket = processTextMessageSpy.at(0).at(0).value<QWebSocket *>();
+        QVERIFY(fakeServer.waitForTextMessages());
+        QCOMPARE(fakeServer.textMessagesCount(), 2);
+        auto socket = fakeServer.socketForTextMessage(0);
         socket->abort();
 
         QVERIFY(connectionLostSpy.wait());
@@ -187,57 +139,34 @@ private slots:
 
     void testSetup_maxConnectionAttemptsReached_deletePushNotifications()
     {
-        const QString user = "user";
-        const QString password = "password";
         FakeWebSocketServer fakeServer;
-        QSignalSpy processTextMessageSpy(&fakeServer, &FakeWebSocketServer::processTextMessage);
-        QVERIFY(processTextMessageSpy.isValid());
-
         auto account = FakeWebSocketServer::createAccount();
-        auto credentials = new CredentialsStub(user, password);
-        account->setCredentials(credentials);
         account->pushNotifications()->setReconnectTimerInterval(0);
         QSignalSpy authenticationFailedSpy(account->pushNotifications(), &OCC::PushNotifications::authenticationFailed);
         QVERIFY(authenticationFailedSpy.isValid());
 
         // Let three authentication attempts fail
-        QVERIFY(processTextMessageSpy.wait());
-        QCOMPARE(processTextMessageSpy.count(), 2);
-        auto socket = processTextMessageSpy.at(0).at(0).value<QWebSocket *>();
-        socket->sendTextMessage("err: Invalid credentials");
-
-        QVERIFY(processTextMessageSpy.wait());
-        QCOMPARE(processTextMessageSpy.count(), 4);
-        socket = processTextMessageSpy.at(2).at(0).value<QWebSocket *>();
-        socket->sendTextMessage("err: Invalid credentials");
-
-        QVERIFY(processTextMessageSpy.wait());
-        QCOMPARE(processTextMessageSpy.count(), 6);
-        socket = processTextMessageSpy.at(4).at(0).value<QWebSocket *>();
-        socket->sendTextMessage("err: Invalid credentials");
+        for (uint8_t i = 0; i < 3; ++i) {
+            QVERIFY(fakeServer.waitForTextMessages());
+            QCOMPARE(fakeServer.textMessagesCount(), 2);
+            auto socket = fakeServer.socketForTextMessage(0);
+            fakeServer.clearTextMessages();
+            socket->sendTextMessage("err: Invalid credentials");
+        }
 
         // Now the authenticationFailed Signal should be emitted
         QVERIFY(authenticationFailedSpy.wait());
         QCOMPARE(authenticationFailedSpy.count(), 1);
-
         // Account deleted the push notifications
         QCOMPARE(account->pushNotifications(), nullptr);
     }
 
     void testOnWebSocketSslError_sslError_deletePushNotifications()
     {
-        const QString user = "user";
-        const QString password = "password";
         FakeWebSocketServer fakeServer;
-        QSignalSpy processTextMessageSpy(&fakeServer, &FakeWebSocketServer::processTextMessage);
-        QVERIFY(processTextMessageSpy.isValid());
-
         auto account = FakeWebSocketServer::createAccount();
-        auto credentials = new CredentialsStub(user, password);
-        account->setCredentials(credentials);
-
-        processTextMessageSpy.wait();
 
+        QVERIFY(fakeServer.waitForTextMessages());
         // FIXME: This a little bit ugly but I had no better idea how to trigger a error on the websocket client.
         // The websocket that is retrived through the server is not connected to the ssl error signal.
         auto pushNotificationsWebSocketChildren = account->pushNotifications()->findChildren<QWebSocket *>();
@@ -248,45 +177,14 @@ private slots:
         QCOMPARE(account->pushNotifications(), nullptr);
     }
 
-    void testAccountSetCredentials_correctCredentials_emitPushNotificationsReady()
-    {
-        FakeWebSocketServer fakeServer;
-        auto account = FakeWebSocketServer::createAccount();
-        QSignalSpy processTextMessageSpy(&fakeServer, &FakeWebSocketServer::processTextMessage);
-        QVERIFY(processTextMessageSpy.isValid());
-        const QString user = "user";
-        const QString password = "password";
-        auto credentials = new CredentialsStub(user, password);
-        account->setCredentials(credentials);
-
-        QSignalSpy pushNotificationsReady(account.data(), &OCC::Account::pushNotificationsReady);
-        QVERIFY(pushNotificationsReady.isValid());
-
-        // Wait for authentication
-        QVERIFY(processTextMessageSpy.wait());
-        auto socket = processTextMessageSpy.at(0).at(0).value<QWebSocket *>();
-        // Don't care about which message was sent
-        socket->sendTextMessage("authenticated");
-
-        // Wait for push notifactions ready signal
-        QVERIFY(pushNotificationsReady.wait());
-        auto accountSent = pushNotificationsReady.at(0).at(0).value<OCC::Account *>();
-        QCOMPARE(accountSent, account.data());
-    }
-
     void testAccount_web_socket_connectionLost_emitNotificationsDisabled()
     {
         FakeWebSocketServer fakeServer;
         auto account = FakeWebSocketServer::createAccount();
-        QSignalSpy processTextMessageSpy(&fakeServer, &FakeWebSocketServer::processTextMessage);
-        QVERIFY(processTextMessageSpy.isValid());
-        const QString user = "user";
-        const QString password = "password";
-        auto credentials = new CredentialsStub(user, password);
-        account->setCredentials(credentials);
-
         // Need to set reconnect timer interval to zero for tests
         account->pushNotifications()->setReconnectTimerInterval(0);
+        const auto socket = fakeServer.authenticateAccount(account);
+        QVERIFY(socket);
 
         QSignalSpy connectionLostSpy(account->pushNotifications(), &OCC::PushNotifications::connectionLost);
         QVERIFY(connectionLostSpy.isValid());
@@ -295,9 +193,6 @@ private slots:
         QVERIFY(pushNotificationsDisabledSpy.isValid());
 
         // Wait for authentication and then sent a network error
-        processTextMessageSpy.wait();
-        QCOMPARE(processTextMessageSpy.count(), 2);
-        auto socket = processTextMessageSpy.at(0).at(0).value<QWebSocket *>();
         socket->abort();
 
         QVERIFY(pushNotificationsDisabledSpy.wait());
@@ -313,45 +208,53 @@ private slots:
     {
         FakeWebSocketServer fakeServer;
         auto account = FakeWebSocketServer::createAccount();
-        QSignalSpy processTextMessageSpy(&fakeServer, &FakeWebSocketServer::processTextMessage);
-        QVERIFY(processTextMessageSpy.isValid());
-        const QString user = "user";
-        const QString password = "password";
-        auto credentials = new CredentialsStub(user, password);
-        account->setCredentials(credentials);
-
         account->pushNotifications()->setReconnectTimerInterval(0);
         QSignalSpy authenticationFailedSpy(account->pushNotifications(), &OCC::PushNotifications::authenticationFailed);
         QVERIFY(authenticationFailedSpy.isValid());
-
         QSignalSpy pushNotificationsDisabledSpy(account.data(), &OCC::Account::pushNotificationsDisabled);
         QVERIFY(pushNotificationsDisabledSpy.isValid());
 
         // Let three authentication attempts fail
-        QVERIFY(processTextMessageSpy.wait());
-        QCOMPARE(processTextMessageSpy.count(), 2);
-        auto socket = processTextMessageSpy.at(0).at(0).value<QWebSocket *>();
-        socket->sendTextMessage("err: Invalid credentials");
-
-        QVERIFY(processTextMessageSpy.wait());
-        QCOMPARE(processTextMessageSpy.count(), 4);
-        socket = processTextMessageSpy.at(2).at(0).value<QWebSocket *>();
-        socket->sendTextMessage("err: Invalid credentials");
-
-        QVERIFY(processTextMessageSpy.wait());
-        QCOMPARE(processTextMessageSpy.count(), 6);
-        socket = processTextMessageSpy.at(4).at(0).value<QWebSocket *>();
-        socket->sendTextMessage("err: Invalid credentials");
+        for (uint8_t i = 0; i < 3; ++i) {
+            QVERIFY(fakeServer.waitForTextMessages());
+            QCOMPARE(fakeServer.textMessagesCount(), 2);
+            auto socket = fakeServer.socketForTextMessage(0);
+            fakeServer.clearTextMessages();
+            socket->sendTextMessage("err: Invalid credentials");
+        }
 
         // Now the authenticationFailed and pushNotificationsDisabled Signals should be emitted
         QVERIFY(pushNotificationsDisabledSpy.wait());
         QCOMPARE(pushNotificationsDisabledSpy.count(), 1);
-
         QCOMPARE(authenticationFailedSpy.count(), 1);
-
         auto accountSent = pushNotificationsDisabledSpy.at(0).at(0).value<OCC::Account *>();
         QCOMPARE(accountSent, account.data());
     }
+
+    void testPingTimeout_pingTimedOut_reconnect()
+    {
+        FakeWebSocketServer fakeServer;
+        std::unique_ptr<QSignalSpy> filesChangedSpy;
+        std::unique_ptr<QSignalSpy> notificationsChangedSpy;
+        std::unique_ptr<QSignalSpy> activitiesChangedSpy;
+        auto account = FakeWebSocketServer::createAccount();
+        QVERIFY(fakeServer.authenticateAccount(account));
+
+        // Set the ping timeout interval to zero and check if the server attemps to authenticate again
+        fakeServer.clearTextMessages();
+        account->pushNotifications()->setPingInterval(0);
+        QVERIFY(fakeServer.authenticateAccount(
+            account, [&](OCC::PushNotifications *pushNotifications) {
+                filesChangedSpy.reset(new QSignalSpy(pushNotifications, &OCC::PushNotifications::filesChanged));
+                notificationsChangedSpy.reset(new QSignalSpy(pushNotifications, &OCC::PushNotifications::notificationsChanged));
+                activitiesChangedSpy.reset(new QSignalSpy(pushNotifications, &OCC::PushNotifications::activitiesChanged));
+            },
+            [&] {
+                QVERIFY(verifyCalledOnceWithAccount(*filesChangedSpy, account));
+                QVERIFY(verifyCalledOnceWithAccount(*notificationsChangedSpy, account));
+                QVERIFY(verifyCalledOnceWithAccount(*activitiesChangedSpy, account));
+            }));
+    }
 };
 
 QTEST_GUILESS_MAIN(TestPushNotifications)