зеркало из https://github.com/nextcloud/desktop.git
Merge pull request #2814 from nextcloud/files_push_notifications
Add push notifications for file changes
This commit is contained in:
Коммит
8c66b9f89e
|
@ -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() { }
|
||||
|
|
|
@ -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"
|
Загрузка…
Ссылка в новой задаче