Merge pull request #2814 from nextcloud/files_push_notifications

Add push notifications for file changes
This commit is contained in:
Kevin Ottens 2021-01-25 18:17:39 +01:00 коммит произвёл GitHub
Родитель fd604ddc9e 78f00acaa2
Коммит 8c66b9f89e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
19 изменённых файлов: 1076 добавлений и 23 удалений

Просмотреть файл

@ -24,6 +24,7 @@
#include "filesystem.h"
#include "lockwatcher.h"
#include "common/asserts.h"
#include <pushnotifications.h>
#include <syncengine.h>
#ifdef Q_OS_MAC
@ -77,6 +78,8 @@ FolderMan::FolderMan(QObject *parent)
connect(_lockWatcher.data(), &LockWatcher::fileUnlocked,
this, &FolderMan::slotWatchedFileUnlocked);
connect(this, &FolderMan::folderListChanged, this, &FolderMan::slotSetupPushNotifications);
}
FolderMan *FolderMan::instance()
@ -823,34 +826,82 @@ void FolderMan::slotStartScheduledFolderSync()
}
}
bool FolderMan::pushNotificationsFilesReady(Account *account)
{
const auto pushNotifications = account->pushNotifications();
const auto pushFilesAvailable = account->capabilities().availablePushNotifications() & PushNotificationType::Files;
return pushFilesAvailable && pushNotifications && pushNotifications->isReady();
}
void FolderMan::slotEtagPollTimerTimeout()
{
ConfigFile cfg;
auto polltime = cfg.remotePollInterval();
qCInfo(lcFolderMan) << "Etag poll timer timeout";
for (Folder *f : qAsConst(_folderMap)) {
if (!f) {
continue;
}
if (f->isSyncRunning()) {
continue;
}
if (_scheduledFolders.contains(f)) {
continue;
}
if (_disabledFolders.contains(f)) {
continue;
}
if (f->etagJob() || f->isBusy() || !f->canSync()) {
continue;
}
if (f->msecSinceLastSync() < polltime) {
continue;
}
QMetaObject::invokeMethod(f, "slotRunEtagJob", Qt::QueuedConnection);
const auto folderMapValues = _folderMap.values();
qCInfo(lcFolderMan) << "Folders to sync:" << folderMapValues.size();
QList<Folder *> foldersToRun;
// Some folders need not to be checked because they use the push notifications
std::copy_if(folderMapValues.begin(), folderMapValues.end(), std::back_inserter(foldersToRun), [this](Folder *folder) -> bool {
const auto account = folder->accountState()->account();
const auto capabilities = account->capabilities();
const auto pushNotifications = account->pushNotifications();
return !pushNotificationsFilesReady(account.data());
});
qCInfo(lcFolderMan) << "Number of folders that don't use push notifications:" << foldersToRun.size();
runEtagJobsIfPossible(foldersToRun);
}
void FolderMan::runEtagJobsIfPossible(const QList<Folder *> &folderMap)
{
for (auto folder : folderMap) {
runEtagJobIfPossible(folder);
}
}
void FolderMan::runEtagJobIfPossible(Folder *folder)
{
const ConfigFile cfg;
const auto polltime = cfg.remotePollInterval();
qCInfo(lcFolderMan) << "Run etag job on folder" << folder;
if (!folder) {
return;
}
if (folder->isSyncRunning()) {
qCInfo(lcFolderMan) << "Can not run etag job: Sync is running";
return;
}
if (_scheduledFolders.contains(folder)) {
qCInfo(lcFolderMan) << "Can not run etag job: Folder is alreday scheduled";
return;
}
if (_disabledFolders.contains(folder)) {
qCInfo(lcFolderMan) << "Can not run etag job: Folder is disabled";
return;
}
if (folder->etagJob() || folder->isBusy() || !folder->canSync()) {
qCInfo(lcFolderMan) << "Can not run etag job: Folder is busy";
return;
}
// When not using push notifications, make sure polltime is reached
if (!pushNotificationsFilesReady(folder->accountState()->account().data())) {
if (folder->msecSinceLastSync() < polltime) {
qCInfo(lcFolderMan) << "Can not run etag job: Polltime not reached";
return;
}
}
QMetaObject::invokeMethod(folder, "slotRunEtagJob", Qt::QueuedConnection);
}
void FolderMan::slotRemoveFoldersForAccount(AccountState *accountState)
{
QVarLengthArray<Folder *, 16> foldersToRemove;
@ -1631,4 +1682,42 @@ void FolderMan::restartApplication()
}
}
void FolderMan::slotSetupPushNotifications(const Folder::Map &folderMap)
{
for (auto folder : folderMap) {
const auto account = folder->accountState()->account();
// See if the account already provides the PushNotifications object and if yes connect to it.
// If we can't connect at this point, the signals will be connected in slotPushNotificationsReady()
// after the PushNotification object emitted the ready signal
slotConnectToPushNotifications(account.data());
connect(account.data(), &Account::pushNotificationsReady, this, &FolderMan::slotConnectToPushNotifications, Qt::UniqueConnection);
}
}
void FolderMan::slotProcessFilesPushNotification(Account *account)
{
qCInfo(lcFolderMan) << "Got files push notification for account" << account;
for (auto folder : _folderMap) {
// Just run on the folders that belong to this account
if (folder->accountState()->account() != account) {
continue;
}
qCInfo(lcFolderMan) << "Schedule folder" << folder << "for sync";
scheduleFolder(folder);
}
}
void FolderMan::slotConnectToPushNotifications(Account *account)
{
const auto pushNotifications = account->pushNotifications();
if (pushNotificationsFilesReady(account)) {
qCInfo(lcFolderMan) << "Push notifications ready";
connect(pushNotifications, &PushNotifications::filesChanged, this, &FolderMan::slotProcessFilesPushNotification, Qt::UniqueConnection);
}
}
} // namespace OCC

Просмотреть файл

@ -288,6 +288,10 @@ private slots:
*/
void slotScheduleFolderByTime();
void slotSetupPushNotifications(const Folder::Map &);
void slotProcessFilesPushNotification(Account *account);
void slotConnectToPushNotifications(Account *account);
private:
/** Adds a new folder, does not add it to the account settings and
* does not set an account on the new folder.
@ -313,6 +317,11 @@ private:
void setupFoldersHelper(QSettings &settings, AccountStatePtr account, const QStringList &ignoreKeys, bool backwardsCompatible, bool foldersWithPlaceholders);
void runEtagJobsIfPossible(const QList<Folder *> &folderMap);
void runEtagJobIfPossible(Folder *folder);
bool pushNotificationsFilesReady(Account *account);
QSet<Folder *> _disabledFolders;
Folder::Map _folderMap;
QString _folderConfigPath;

Просмотреть файл

@ -19,6 +19,7 @@ ENDIF(${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD|NetBSD|OpenBSD")
set(libsync_SRCS
account.cpp
pushnotifications.cpp
wordlist.cpp
bandwidthmanager.cpp
capabilities.cpp
@ -116,6 +117,7 @@ IF (NOT APPLE)
)
ENDIF(NOT APPLE)
find_package(Qt5 REQUIRED COMPONENTS WebSockets)
add_library(${synclib_NAME} SHARED ${libsync_SRCS})
target_link_libraries(${synclib_NAME}
"${csync_NAME}"
@ -123,6 +125,7 @@ target_link_libraries(${synclib_NAME}
OpenSSL::SSL
${OS_SPECIFIC_LINK_LIBRARIES}
Qt5::Core Qt5::Network
Qt5::WebSockets
)
if (NOT TOKEN_AUTH_ONLY)

Просмотреть файл

@ -20,6 +20,7 @@
#include "creds/abstractcredentials.h"
#include "capabilities.h"
#include "theme.h"
#include "pushnotifications.h"
#include "common/asserts.h"
#include "clientsideencryption.h"
@ -56,6 +57,7 @@ Account::Account(QObject *parent)
, _davPath(Theme::instance()->webDavPath())
{
qRegisterMetaType<AccountPtr>("AccountPtr");
qRegisterMetaType<Account *>("Account*");
}
AccountPtr Account::create()
@ -201,6 +203,32 @@ void Account::setCredentials(AbstractCredentials *cred)
this, &Account::slotCredentialsFetched);
connect(_credentials.data(), &AbstractCredentials::asked,
this, &Account::slotCredentialsAsked);
trySetupPushNotifications();
}
void Account::trySetupPushNotifications()
{
if (_capabilities.availablePushNotifications() != PushNotificationType::None) {
qCInfo(lcAccount) << "Try to setup push notifications";
if (!_pushNotifications) {
_pushNotifications = new PushNotifications(this, this);
connect(_pushNotifications, &PushNotifications::ready, this, [this]() { emit pushNotificationsReady(this); });
const auto deletePushNotifications = [this]() {
qCInfo(lcAccount) << "Delete push notifications object because authentication failed or connection lost";
_pushNotifications->deleteLater();
_pushNotifications = nullptr;
};
connect(_pushNotifications, &PushNotifications::connectionLost, this, deletePushNotifications);
connect(_pushNotifications, &PushNotifications::authenticationFailed, this, deletePushNotifications);
}
// If push notifications already running it is no problem to call setup again
_pushNotifications->setup();
}
}
QUrl Account::davUrl() const
@ -478,6 +506,8 @@ const Capabilities &Account::capabilities() const
void Account::setCapabilities(const QVariantMap &caps)
{
_capabilities = Capabilities(caps);
trySetupPushNotifications();
}
QString Account::serverVersion() const
@ -661,4 +691,9 @@ void Account::slotDirectEditingRecieved(const QJsonDocument &json)
}
}
PushNotifications *Account::pushNotifications() const
{
return _pushNotifications;
}
} // namespace OCC

Просмотреть файл

@ -54,6 +54,7 @@ class Account;
using AccountPtr = QSharedPointer<Account>;
class AccessManager;
class SimpleNetworkJob;
class PushNotifications;
/**
* @brief Reimplement this to handle SSL errors from libsync
@ -250,6 +251,8 @@ public:
// Check for the directEditing capability
void fetchDirectEditors(const QUrl &directEditingURL, const QString &directEditingETag);
PushNotifications *pushNotifications() const;
public slots:
/// Used when forgetting credentials
void clearQNAMCache();
@ -279,6 +282,8 @@ signals:
/// Used in RemoteWipe
void appPasswordRetrieved(QString);
void pushNotificationsReady(Account *account);
protected Q_SLOTS:
void slotCredentialsFetched();
void slotCredentialsAsked();
@ -287,6 +292,7 @@ protected Q_SLOTS:
private:
Account(QObject *parent = nullptr);
void setSharedThis(AccountPtr sharedThis);
void trySetupPushNotifications();
QWeakPointer<Account> _sharedThis;
QString _id;
@ -331,6 +337,8 @@ private:
// Direct Editing
QString _lastDirectEditingETag;
PushNotifications *_pushNotifications = nullptr;
/* IMPORTANT - remove later - FIXME MS@2019-12-07 -->
* TODO: For "Log out" & "Remove account": Remove client CA certs and KEY!
*
@ -350,5 +358,6 @@ private:
}
Q_DECLARE_METATYPE(OCC::AccountPtr)
Q_DECLARE_METATYPE(OCC::Account *)
#endif //SERVERCONNECTION_H

Просмотреть файл

@ -16,6 +16,7 @@
#include <QVariantMap>
#include <QLoggingCategory>
#include <QUrl>
#include <QDebug>
@ -176,6 +177,28 @@ bool Capabilities::chunkingNg() const
return _capabilities["dav"].toMap()["chunking"].toByteArray() >= "1.0";
}
PushNotificationTypes Capabilities::availablePushNotifications() const
{
if (!_capabilities.contains("notify_push")) {
return PushNotificationType::None;
}
const auto types = _capabilities["notify_push"].toMap()["type"].toStringList();
PushNotificationTypes pushNotificationTypes;
if (types.contains("files")) {
pushNotificationTypes.setFlag(PushNotificationType::Files);
}
return pushNotificationTypes;
}
QUrl Capabilities::pushNotificationsWebSocketUrl() const
{
const auto websocket = _capabilities["notify_push"].toMap()["endpoints"].toMap()["websocket"].toString();
return QUrl(websocket);
}
bool Capabilities::chunkingParallelUploadDisabled() const
{
return _capabilities["dav"].toMap()["chunkingParallelUploadDisabled"].toBool();

Просмотреть файл

@ -26,6 +26,13 @@ namespace OCC {
class DirectEditor;
enum PushNotificationType {
None = 0,
Files = 1
};
Q_DECLARE_FLAGS(PushNotificationTypes, PushNotificationType)
Q_DECLARE_OPERATORS_FOR_FLAGS(PushNotificationTypes)
/**
* @brief The Capabilities class represents the capabilities of an ownCloud
* server
@ -48,6 +55,12 @@ public:
bool shareResharing() const;
bool chunkingNg() const;
/// Returns which kind of push notfications are available
PushNotificationTypes availablePushNotifications() const;
/// Websocket url for files push notifications if available
QUrl pushNotificationsWebSocketUrl() const;
/// disable parallel upload in chunking
bool chunkingParallelUploadDisabled() const;

Просмотреть файл

@ -45,6 +45,7 @@ public:
virtual QString authType() const = 0;
virtual QString user() const = 0;
virtual QString password() const = 0;
virtual QNetworkAccessManager *createQNAM() const = 0;
/** Whether there are credentials that can be used for a connection attempt. */

Просмотреть файл

@ -27,6 +27,12 @@ QString DummyCredentials::user() const
return _user;
}
QString DummyCredentials::password() const
{
Q_UNREACHABLE();
return QString();
}
QNetworkAccessManager *DummyCredentials::createQNAM() const
{
return new AccessManager;

Просмотреть файл

@ -28,6 +28,7 @@ public:
QString _password;
QString authType() const override;
QString user() const override;
QString password() const override;
QNetworkAccessManager *createQNAM() const override;
bool ready() const override;
bool stillValid(QNetworkReply *reply) override;

Просмотреть файл

@ -91,7 +91,7 @@ public:
void persist() override;
QString user() const override;
// the password or token
QString password() const;
QString password() const override;
void invalidateToken() override;
void forgetSensitiveData() override;
QString fetchUser();

Просмотреть файл

@ -0,0 +1,189 @@
#include "pushnotifications.h"
#include "creds/abstractcredentials.h"
#include "account.h"
namespace {
static constexpr int MAX_ALLOWED_FAILED_AUTHENTICATION_ATTEMPTS = 3;
}
namespace OCC {
Q_LOGGING_CATEGORY(lcPushNotifications, "nextcloud.sync.pushnotifications", QtInfoMsg)
PushNotifications::PushNotifications(Account *account, QObject *parent)
: QObject(parent)
, _account(account)
{
}
PushNotifications::~PushNotifications()
{
closeWebSocket();
}
void PushNotifications::setup()
{
_isReady = false;
_failedAuthenticationAttemptsCount = 0;
reconnectToWebSocket();
}
void PushNotifications::reconnectToWebSocket()
{
closeWebSocket();
openWebSocket();
}
void PushNotifications::closeWebSocket()
{
if (_webSocket) {
qCInfo(lcPushNotifications) << "Close websocket";
_webSocket->close();
}
}
void PushNotifications::onWebSocketConnected()
{
qCInfo(lcPushNotifications) << "Connected to websocket";
connect(_webSocket, &QWebSocket::textMessageReceived, this, &PushNotifications::onWebSocketTextMessageReceived, Qt::UniqueConnection);
authenticateOnWebSocket();
}
void PushNotifications::authenticateOnWebSocket()
{
const auto credentials = _account->credentials();
const auto username = credentials->user();
const auto password = credentials->password();
// Authenticate
_webSocket->sendTextMessage(username);
_webSocket->sendTextMessage(password);
}
void PushNotifications::onWebSocketDisconnected()
{
qCInfo(lcPushNotifications) << "Disconnected from websocket";
}
void PushNotifications::onWebSocketTextMessageReceived(const QString &message)
{
qCInfo(lcPushNotifications) << "Received push notification:" << message;
if (message == "notify_file") {
handleNotifyFile();
} else if (message == "notify_activity" || message == "notify_notification") {
handleNotification();
} else if (message == "authenticated") {
handleAuthenticated();
} else if (message == "err: Invalid credentials") {
handleInvalidCredentials();
}
}
void PushNotifications::onWebSocketError(QAbstractSocket::SocketError error)
{
// This error gets thrown in testSetup_maxConnectionAttemptsReached_deletePushNotifications after
// the second connection attempt. I have no idea why this happens. Maybe the socket gets not closed correctly?
// I think it's fine to ignore this error.
if (error == QAbstractSocket::UnfinishedSocketOperationError) {
return;
}
qCWarning(lcPushNotifications) << "Websocket error" << error;
_isReady = false;
emit connectionLost();
}
bool PushNotifications::tryReconnectToWebSocket()
{
++_failedAuthenticationAttemptsCount;
if (_failedAuthenticationAttemptsCount >= MAX_ALLOWED_FAILED_AUTHENTICATION_ATTEMPTS) {
qCInfo(lcPushNotifications) << "Max authentication attempts reached";
return false;
}
if (!_reconnectTimer) {
_reconnectTimer = new QTimer(this);
}
_reconnectTimer->setInterval(_reconnectTimerInterval);
_reconnectTimer->setSingleShot(true);
connect(_reconnectTimer, &QTimer::timeout, [this]() {
reconnectToWebSocket();
});
_reconnectTimer->start();
return true;
}
void PushNotifications::onWebSocketSslErrors(const QList<QSslError> &errors)
{
qCWarning(lcPushNotifications) << "Received websocket ssl errors:" << errors;
_isReady = false;
emit authenticationFailed();
}
void PushNotifications::openWebSocket()
{
// Open websocket
const auto capabilities = _account->capabilities();
const auto webSocketUrl = capabilities.pushNotificationsWebSocketUrl();
if (!_webSocket) {
qCInfo(lcPushNotifications) << "Create websocket";
_webSocket = new QWebSocket(QString(), QWebSocketProtocol::VersionLatest, this);
}
if (_webSocket) {
connect(_webSocket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error), this, &PushNotifications::onWebSocketError, Qt::UniqueConnection);
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);
qCInfo(lcPushNotifications) << "Open connection to websocket on:" << webSocketUrl;
_webSocket->open(webSocketUrl);
}
}
void PushNotifications::setReconnectTimerInterval(uint32_t interval)
{
_reconnectTimerInterval = interval;
}
bool PushNotifications::isReady() const
{
return _isReady;
}
void PushNotifications::handleAuthenticated()
{
qCInfo(lcPushNotifications) << "Authenticated successful on websocket";
_failedAuthenticationAttemptsCount = 0;
_isReady = true;
emit ready();
}
void PushNotifications::handleNotifyFile()
{
qCInfo(lcPushNotifications) << "Files push notification arrived";
emit filesChanged(_account);
}
void PushNotifications::handleInvalidCredentials()
{
qCInfo(lcPushNotifications) << "Invalid credentials submitted to websocket";
if (!tryReconnectToWebSocket()) {
_isReady = false;
emit authenticationFailed();
}
}
void PushNotifications::handleNotification()
{
qCInfo(lcPushNotifications) << "Notification or activity push notification arrived";
emit notification(_account);
}
}

Просмотреть файл

@ -0,0 +1,113 @@
/*
* Copyright (C) by Felix Weilbach <felix.weilbach@nextcloud.com>
*
* 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.
*/
#pragma once
#include <QWebSocket>
#include <QTimer>
#include "capabilities.h"
namespace OCC {
class Account;
class AbstractCredentials;
class OWNCLOUDSYNC_EXPORT PushNotifications : public QObject
{
Q_OBJECT
public:
explicit PushNotifications(Account *account, QObject *parent = nullptr);
~PushNotifications();
/**
* Setup push notifications
*
* This method needs to be called before push notifications can be used.
*/
void setup();
/**
* Set the interval for reconnection attempts
*/
void setReconnectTimerInterval(uint32_t interval);
/**
* Indicates if push notifications ready to use
*
* Ready to use means connected and authenticated.
*/
bool isReady() const;
signals:
/**
* Will be emitted after a successful connection and authentication
*/
void ready();
/**
* Will be emitted if files on the server changed
*/
void filesChanged(Account *account);
/**
* Will be emitted if there is a new notification or activity on the server
*/
void notification(Account *account);
/**
* Will be emitted if push notifications are unable to authenticate
*
* It's save to call #PushNotifications::setup() after this signal has been emitted.
*/
void authenticationFailed();
/**
* Will be emitted if push notifications are unable to connect or the connection timed out
*
* It's save to call #PushNotifications::setup() after this signal has been emitted.
*/
void connectionLost();
private slots:
void onWebSocketConnected();
void onWebSocketDisconnected();
void onWebSocketTextMessageReceived(const QString &message);
void onWebSocketError(QAbstractSocket::SocketError error);
void onWebSocketSslErrors(const QList<QSslError> &errors);
private:
void openWebSocket();
void reconnectToWebSocket();
void closeWebSocket();
void authenticateOnWebSocket();
bool tryReconnectToWebSocket();
void initReconnectTimer();
void handleAuthenticated();
void handleNotifyFile();
void handleInvalidCredentials();
void handleNotification();
Account *_account = nullptr;
QWebSocket *_webSocket = nullptr;
uint8_t _failedAuthenticationAttemptsCount = 0;
QTimer *_reconnectTimer = nullptr;
uint32_t _reconnectTimerInterval = 20 * 1000;
bool _isReady = false;
};
}

Просмотреть файл

@ -69,6 +69,8 @@ nextcloud_add_test(SelectiveSync "")
nextcloud_add_test(DatabaseError "")
nextcloud_add_test(LockedFiles "../src/gui/lockwatcher.cpp")
nextcloud_add_test(FolderWatcher "${FolderWatcher_SRC}")
nextcloud_add_test(Capabilities "")
nextcloud_add_test(PushNotifications "pushnotificationstestutils.cpp")
if( UNIX AND NOT APPLE )
nextcloud_add_test(InotifyWatcher "${FolderWatcher_SRC}")

Просмотреть файл

@ -0,0 +1,135 @@
#include <QLoggingCategory>
#include <QSignalSpy>
#include <QTest>
#include "pushnotificationstestutils.h"
Q_LOGGING_CATEGORY(lcFakeWebSocketServer, "nextcloud.test.fakewebserver", QtInfoMsg)
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;
}
Q_UNREACHABLE();
}
FakeWebSocketServer::~FakeWebSocketServer()
{
close();
}
void FakeWebSocketServer::close()
{
if (_webSocketServer->isListening()) {
qCInfo(lcFakeWebSocketServer) << "Close fake websocket server";
_webSocketServer->close();
qDeleteAll(_clients.begin(), _clients.end());
}
}
void FakeWebSocketServer::processTextMessageInternal(const QString &message)
{
auto client = qobject_cast<QWebSocket *>(sender());
emit processTextMessage(client, message);
}
void FakeWebSocketServer::onNewConnection()
{
qCInfo(lcFakeWebSocketServer) << "New connection on fake websocket server";
auto socket = _webSocketServer->nextPendingConnection();
connect(socket, &QWebSocket::textMessageReceived, this, &FakeWebSocketServer::processTextMessageInternal);
connect(socket, &QWebSocket::disconnected, this, &FakeWebSocketServer::socketDisconnected);
_clients << socket;
}
void FakeWebSocketServer::socketDisconnected()
{
qCInfo(lcFakeWebSocketServer) << "Socket disconnected";
auto client = qobject_cast<QWebSocket *>(sender());
if (client) {
_clients.removeAll(client);
client->deleteLater();
}
}
OCC::AccountPtr FakeWebSocketServer::createAccount()
{
auto account = OCC::Account::create();
QStringList typeList;
typeList.append("files");
QString websocketUrl("ws://localhost:12345");
QVariantMap endpointsMap;
endpointsMap["websocket"] = websocketUrl;
QVariantMap notifyPushMap;
notifyPushMap["type"] = typeList;
notifyPushMap["endpoints"] = endpointsMap;
QVariantMap capabilitiesMap;
capabilitiesMap["notify_push"] = notifyPushMap;
account->setCapabilities(capabilitiesMap);
return account;
}
CredentialsStub::CredentialsStub(const QString &user, const QString &password)
: _user(user)
, _password(password)
{
}
QString CredentialsStub::authType() const
{
return "";
}
QString CredentialsStub::user() const
{
return _user;
}
QString CredentialsStub::password() const
{
return _password;
}
QNetworkAccessManager *CredentialsStub::createQNAM() const
{
return nullptr;
}
bool CredentialsStub::ready() const
{
return false;
}
void CredentialsStub::fetchFromKeychain() { }
void CredentialsStub::askFromUser() { }
bool CredentialsStub::stillValid(QNetworkReply * /*reply*/)
{
return false;
}
void CredentialsStub::persist() { }
void CredentialsStub::invalidateToken() { }
void CredentialsStub::forgetSensitiveData() { }

Просмотреть файл

@ -0,0 +1,73 @@
/*
* Copyright (C) by Felix Weilbach <felix.weilbach@nextcloud.com>
*
* 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.
*/
#pragma once
#include <functional>
#include <QWebSocketServer>
#include <QWebSocket>
#include "creds/abstractcredentials.h"
#include "account.h"
class FakeWebSocketServer : public QObject
{
Q_OBJECT
public:
explicit FakeWebSocketServer(quint16 port = 12345, QObject *parent = nullptr);
~FakeWebSocketServer();
void close();
static OCC::AccountPtr createAccount();
signals:
void closed();
void processTextMessage(QWebSocket *sender, const QString &message);
private slots:
void processTextMessageInternal(const QString &message);
void onNewConnection();
void socketDisconnected();
private:
QWebSocketServer *_webSocketServer;
QList<QWebSocket *> _clients;
};
class CredentialsStub : public OCC::AbstractCredentials
{
Q_OBJECT
public:
CredentialsStub(const QString &user, const QString &password);
virtual QString authType() const;
virtual QString user() const;
virtual QString password() const;
virtual QNetworkAccessManager *createQNAM() const;
virtual bool ready() const;
virtual void fetchFromKeychain();
virtual void askFromUser();
virtual bool stillValid(QNetworkReply *reply);
virtual void persist();
virtual void invalidateToken();
virtual void forgetSensitiveData();
private:
QString _user;
QString _password;
};

Просмотреть файл

@ -413,6 +413,7 @@ public:
FakeCredentials(QNetworkAccessManager *qnam) : _qnam{qnam} { }
virtual QString authType() const { return "test"; }
virtual QString user() const { return "admin"; }
virtual QString password() const { return "password"; }
virtual QNetworkAccessManager *createQNAM() const { return _qnam; }
virtual bool ready() const { return true; }
virtual void fetchFromKeychain() { }

72
test/testcapabilities.cpp Normal file
Просмотреть файл

@ -0,0 +1,72 @@
#include <QTest>
#include "capabilities.h"
class TestCapabilities : public QObject
{
Q_OBJECT
private slots:
void testPushNotificationsAvailable_pushNotificationsForFilesAvailable_returnTrue()
{
QStringList typeList;
typeList.append("files");
QVariantMap notifyPushMap;
notifyPushMap["type"] = typeList;
QVariantMap capabilitiesMap;
capabilitiesMap["notify_push"] = notifyPushMap;
const auto &capabilities = OCC::Capabilities(capabilitiesMap);
const auto filesPushNotificationsAvailable = capabilities.availablePushNotifications().testFlag(OCC::PushNotificationType::Files);
QCOMPARE(filesPushNotificationsAvailable, true);
}
void testPushNotificationsAvailable_pushNotificationsForFilesNotAvailable_returnFalse()
{
QStringList typeList;
typeList.append("nofiles");
QVariantMap notifyPushMap;
notifyPushMap["type"] = typeList;
QVariantMap capabilitiesMap;
capabilitiesMap["notify_push"] = notifyPushMap;
const auto &capabilities = OCC::Capabilities(capabilitiesMap);
const auto filesPushNotificationsAvailable = capabilities.availablePushNotifications().testFlag(OCC::PushNotificationType::Files);
QCOMPARE(filesPushNotificationsAvailable, false);
}
void testPushNotificationsAvailable_pushNotificationsNotAvailable_returnFalse()
{
const auto &capabilities = OCC::Capabilities(QVariantMap());
const auto filesPushNotificationsAvailable = capabilities.availablePushNotifications().testFlag(OCC::PushNotificationType::Files);
QCOMPARE(filesPushNotificationsAvailable, false);
}
void testPushNotificationsWebSocketUrl_urlAvailable_returnUrl()
{
QString websocketUrl("testurl");
QVariantMap endpointsMap;
endpointsMap["websocket"] = websocketUrl;
QVariantMap notifyPushMap;
notifyPushMap["endpoints"] = endpointsMap;
QVariantMap capabilitiesMap;
capabilitiesMap["notify_push"] = notifyPushMap;
const auto &capabilities = OCC::Capabilities(capabilitiesMap);
QCOMPARE(capabilities.pushNotificationsWebSocketUrl(), websocketUrl);
}
};
QTEST_GUILESS_MAIN(TestCapabilities)
#include "testcapabilities.moc"

Просмотреть файл

@ -0,0 +1,279 @@
#include <QTest>
#include <QVector>
#include <QWebSocketServer>
#include <QSignalSpy>
#include "pushnotifications.h"
#include "pushnotificationstestutils.h"
class TestPushNotifications : public QObject
{
Q_OBJECT
private slots:
void testSetup_correctCredentials_authenticateAndEmitReady()
{
FakeWebSocketServer fakeServer;
QSignalSpy processTextMessageSpy(&fakeServer, &FakeWebSocketServer::processTextMessage);
QVERIFY(processTextMessageSpy.isValid());
const QString user = "user";
const QString password = "password";
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);
}
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);
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());
}
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);
QSignalSpy notificationSpy(account->pushNotifications(), &OCC::PushNotifications::notification);
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 *>();
socket->sendTextMessage("notify_activity");
// 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());
}
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);
QSignalSpy notificationSpy(account->pushNotifications(), &OCC::PushNotifications::notification);
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 *>();
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());
}
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();
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);
}
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 *>();
socket->abort();
QVERIFY(connectionLostSpy.wait());
// Account handled connectionLost signal and deleted PushNotifications
QCOMPARE(account->pushNotifications(), nullptr);
}
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");
// 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();
// 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 *>();
QVERIFY(pushNotificationsWebSocketChildren.size() == 1);
emit pushNotificationsWebSocketChildren[0]->sslErrors(QList<QSslError>());
// Account handled connectionLost signal and deleted PushNotifications
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());
}
};
QTEST_GUILESS_MAIN(TestPushNotifications)
#include "testpushnotifications.moc"