diff --git a/src/gui/tray/activitydata.h b/src/gui/tray/activitydata.h index c5517d1161..29df3c878c 100644 --- a/src/gui/tray/activitydata.h +++ b/src/gui/tray/activitydata.h @@ -16,7 +16,7 @@ #define ACTIVITYDATA_H #include "syncfileitem.h" -#include "folder.h" +#include "syncresult.h" #include "account.h" #include diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp index 9da43924d6..f6c28a85d3 100644 --- a/src/gui/tray/activitylistmodel.cpp +++ b/src/gui/tray/activitylistmodel.cpp @@ -438,18 +438,6 @@ void ActivityListModel::startFetchJob() job->start(); } -void ActivityListModel::setFinalList(const ActivityList &finalList) -{ - _finalList = finalList; - - emit allConflictsChanged(); -} - -const ActivityList &ActivityListModel::finalList() const -{ - return _finalList; -} - int ActivityListModel::currentItem() const { return _currentItem; diff --git a/src/gui/tray/activitylistmodel.h b/src/gui/tray/activitylistmodel.h index 72716aa4d4..6335e8413f 100644 --- a/src/gui/tray/activitylistmodel.h +++ b/src/gui/tray/activitylistmodel.h @@ -21,6 +21,10 @@ class QJsonDocument; +namespace ActivityListModelTestUtils { +class TestingALM; +} + namespace OCC { Q_DECLARE_LOGGING_CATEGORY(lcActivity) @@ -89,9 +93,7 @@ public: Q_ENUM(ErrorType) explicit ActivityListModel(QObject *parent = nullptr); - - explicit ActivityListModel(AccountState *accountState, - QObject *parent = nullptr); + explicit ActivityListModel(AccountState *accountState, QObject *parent = nullptr); [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; @@ -99,7 +101,6 @@ public: [[nodiscard]] QHash roleNames() const override; [[nodiscard]] bool canFetchMore(const QModelIndex &) const override; - void fetchMore(const QModelIndex &) override; ActivityList activityList() { return _finalList; } ActivityList errorsList() { return _notificationErrorsLists; } @@ -120,6 +121,8 @@ public: [[nodiscard]] OCC::ActivityList allConflicts() const; public slots: + void fetchMore(const QModelIndex &) override; + void slotRefreshActivity(); void slotRefreshActivityInitial(); void slotRemoveAccount(); @@ -151,21 +154,22 @@ signals: protected: [[nodiscard]] bool currentlyFetching() const; - [[nodiscard]] const ActivityList &finalList() const; // added for unit tests - protected slots: void activitiesReceived(const QJsonDocument &json, int statusCode); void setAndRefreshCurrentlyFetching(bool value); void setDoneFetching(bool value); void setHideOldActivities(bool value); void setDisplayActions(bool value); - void setFinalList(const OCC::ActivityList &finalList); // added for unit tests virtual void startFetchJob(); private slots: void addEntriesToActivityList(const OCC::ActivityList &activityList); void accountStateHasChanged(); + void ingestActivities(const QJsonArray &activities); + void appendMoreActivitiesAvailableEntry(); + void insertOrRemoveDummyFetchingActivity(); + void triggerCaseClashAction(OCC::Activity activity); private: static QVariantList convertLinksToMenuEntries(const Activity &activity); @@ -174,11 +178,6 @@ private: [[nodiscard]] bool canFetchActivities() const; - void ingestActivities(const QJsonArray &activities); - void appendMoreActivitiesAvailableEntry(); - void insertOrRemoveDummyFetchingActivity(); - void triggerCaseClashAction(Activity activity); - void displaySingleConflictDialog(const Activity &activity); void setHasSyncConflicts(bool conflictsFound); @@ -197,8 +196,8 @@ private: bool _displayActions = true; int _currentItem = 0; - int _maxActivities = 100; - int _maxActivitiesDays = 30; + static constexpr int _maxActivities = 100; + static constexpr int _maxActivitiesDays = 30; bool _showMoreActivitiesAvailableEntry = false; QPointer _currentConflictDialog; @@ -217,6 +216,8 @@ private: QElapsedTimer _durationSinceDisconnection; static constexpr quint32 MaxActionButtons = 3; + + friend class ActivityListModelTestUtils::TestingALM; }; } diff --git a/src/gui/tray/sortedactivitylistmodel.cpp b/src/gui/tray/sortedactivitylistmodel.cpp index fcd58d1385..d62ad4985b 100644 --- a/src/gui/tray/sortedactivitylistmodel.cpp +++ b/src/gui/tray/sortedactivitylistmodel.cpp @@ -44,30 +44,47 @@ bool SortedActivityListModel::lessThan(const QModelIndex &sourceLeft, const QMod return false; } + // Let's now check for errors as we want those near the top too + // Sync result errors go first + const auto leftSyncResultStatus = leftActivity._syncResultStatus; + const auto rightSyncResultStatus = rightActivity._syncResultStatus; + + const auto leftIsSyncResultError = leftSyncResultStatus == SyncResult::Error || + leftSyncResultStatus == SyncResult::SetupError || + leftSyncResultStatus == SyncResult::Problem; + + const auto rightIsSyncResultError = rightSyncResultStatus == SyncResult::Error || + rightSyncResultStatus == SyncResult::SetupError || + rightSyncResultStatus == SyncResult::Problem; + + if (leftIsSyncResultError != rightIsSyncResultError) { + return leftIsSyncResultError; + } // If they are both errors then we will order the errors according to enum order later + + // Then sync file item status errors + const auto leftSyncFileItemStatus = leftActivity._syncFileItemStatus; + const auto rightSyncFileItemStatus = rightActivity._syncFileItemStatus; + const bool leftIsErrorFileItemStatus = leftSyncFileItemStatus != SyncFileItem::NoStatus && + leftSyncFileItemStatus != SyncFileItem::Success; + + const bool rightIsErrorFileItemStatus = rightSyncFileItemStatus != SyncFileItem::NoStatus && + rightSyncFileItemStatus != SyncFileItem::Success; + + if (leftIsErrorFileItemStatus != rightIsErrorFileItemStatus) { + return leftIsErrorFileItemStatus; + } + + // Let's go back to more broadly comparing by type if (const auto rightType = rightActivity._type; leftType != rightType) { return leftType < rightType; } - const auto leftSyncFileItemStatus = leftActivity._syncFileItemStatus; - const auto rightSyncFileItemStatus = rightActivity._syncFileItemStatus; - - // Then compare by status - if (leftSyncFileItemStatus != rightSyncFileItemStatus) { - // We want to shove errors towards the top. - return (leftSyncFileItemStatus != SyncFileItem::NoStatus && - leftSyncFileItemStatus != SyncFileItem::Success) || - leftSyncFileItemStatus == SyncFileItem::FatalError || - leftSyncFileItemStatus < rightSyncFileItemStatus; + if (leftSyncResultStatus != rightSyncResultStatus) { + return leftSyncResultStatus < rightSyncResultStatus; } - const auto leftSyncResultStatus = leftActivity._syncResultStatus; - const auto rightSyncResultStatus = rightActivity._syncResultStatus; - - if (leftSyncResultStatus != rightSyncResultStatus) { - // We only ever use SyncResult::Error in activities - return (leftSyncResultStatus != SyncResult::Undefined && - leftSyncResultStatus != SyncResult::Success) || - leftSyncResultStatus == SyncResult::Error; + if (leftSyncFileItemStatus != rightSyncFileItemStatus) { + return leftSyncFileItemStatus < rightSyncFileItemStatus; } // Finally sort by time, latest first diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 2dbb5a9b7f..69ebc08d51 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -12,6 +12,7 @@ add_library(testutils testhelper.cpp sharetestutils.cpp endtoendtestutils.cpp + activitylistmodeltestutils.cpp ) target_link_libraries(testutils PUBLIC Nextcloud::sync Qt5::Test) @@ -63,6 +64,7 @@ nextcloud_add_test(IconUtils) nextcloud_add_test(SetUserStatusDialog) nextcloud_add_test(UnifiedSearchListmodel) nextcloud_add_test(ActivityListModel) +nextcloud_add_test(SortedActivityListModel) nextcloud_add_test(ActivityData) nextcloud_add_test(TalkReply) nextcloud_add_test(LockFile) diff --git a/test/activitylistmodeltestutils.cpp b/test/activitylistmodeltestutils.cpp new file mode 100644 index 0000000000..4700a6b6d3 --- /dev/null +++ b/test/activitylistmodeltestutils.cpp @@ -0,0 +1,511 @@ +/* + * Copyright (C) by Claudio Cambra + * + * 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. + */ + +#include "activitylistmodeltestutils.h" +#include "syncenginetestutils.h" + +#include +#include +#include +#include + +namespace { +static QByteArray fake404Response = R"( +{"ocs":{"meta":{"status":"failure","statuscode":404,"message":"Invalid query, please check the syntax. API specifications are here: http:\/\/www.freedesktop.org\/wiki\/Specifications\/open-collaboration-services.\n"},"data":[]}} +)"; + +static QByteArray fake400Response = R"( +{"ocs":{"meta":{"status":"failure","statuscode":400,"message":"Parameter is incorrect.\n"},"data":[]}} +)"; + +static QByteArray fake500Response = R"( +{"ocs":{"meta":{"status":"failure","statuscode":500,"message":"Internal Server Error.\n"},"data":[]}} +)"; +} + +namespace ActivityListModelTestUtils +{ + +QNetworkReply *almTestQnamOverride(FakeQNAM * const fakeQnam, + const QNetworkAccessManager::Operation op, + const QNetworkRequest &req, + const QString &accountUrl, + QObject * const parent, + const int searchResultsReplyDelay, + QIODevice * const device) +{ + Q_UNUSED(device); + QNetworkReply *reply = nullptr; + + const auto urlQuery = QUrlQuery(req.url()); + const auto format = urlQuery.queryItemValue(QStringLiteral("format")); + const auto since = urlQuery.queryItemValue(QStringLiteral("since")).toInt(); + const auto limit = urlQuery.queryItemValue(QStringLiteral("limit")).toInt(); + const auto path = req.url().path(); + + if (!req.url().toString().startsWith(accountUrl)) { + reply = new FakeErrorReply(op, req, parent, 404, fake404Response); + } + if (format != QStringLiteral("json")) { + reply = new FakeErrorReply(op, req, parent, 400, fake400Response); + } + + if (path.startsWith(QStringLiteral("/ocs/v2.php/apps/activity/api/v2/activity"))) { + reply = new FakePayloadReply(op, req, FakeRemoteActivityStorage::instance()->activityJsonData(since, limit), searchResultsReplyDelay, fakeQnam); + } + + if (!reply) { + return qobject_cast(new FakeErrorReply(op, req, parent, 404, QByteArrayLiteral("{error: \"Not found!\"}"))); + } + + return reply; +} + +// Activity comparison is done by checking type, id, and accName +// We need an activity with these details, at least + +OCC::Activity exampleNotificationActivity(const QString &accountName, const int id) +{ + OCC::Activity testNotificationActivity; + + testNotificationActivity._accName = accountName; + testNotificationActivity._id = id; + testNotificationActivity._type = OCC::Activity::NotificationType; + testNotificationActivity._dateTime = QDateTime::currentDateTime(); + testNotificationActivity._subject = QStringLiteral("Sample notification text"); + + return testNotificationActivity; +} + +OCC::Activity exampleSyncResultErrorActivity(const QString &accountName, const int id) +{ + OCC::Activity testSyncResultErrorActivity; + + testSyncResultErrorActivity._id = id; + testSyncResultErrorActivity._type = OCC::Activity::SyncResultType; + testSyncResultErrorActivity._syncResultStatus = OCC::SyncResult::Error; + testSyncResultErrorActivity._dateTime = QDateTime::currentDateTime(); + testSyncResultErrorActivity._subject = QStringLiteral("Sample failed sync text"); + testSyncResultErrorActivity._message = QStringLiteral("/path/to/thingy"); + testSyncResultErrorActivity._link = QStringLiteral("/path/to/thingy"); + testSyncResultErrorActivity._accName = accountName; + + return testSyncResultErrorActivity; +} + +OCC::Activity exampleSyncFileItemActivity(const QString &accountName, const QUrl &link, const int id) +{ + OCC::Activity testSyncFileItemActivity; + + testSyncFileItemActivity._id = id; + testSyncFileItemActivity._type = OCC::Activity::SyncFileItemType; //client activity + testSyncFileItemActivity._syncFileItemStatus = OCC::SyncFileItem::Success; + testSyncFileItemActivity._dateTime = QDateTime::currentDateTime(); + testSyncFileItemActivity._message = QStringLiteral("Sample file successfully synced text"); + testSyncFileItemActivity._link = link; + testSyncFileItemActivity._accName = accountName; + testSyncFileItemActivity._file = QStringLiteral("xyz.pdf"); + + return testSyncFileItemActivity; +} + +OCC::Activity exampleFileIgnoredActivity(const QString &accountName, const QUrl &link, const int id) +{ + OCC::Activity testFileIgnoredActivity; + + testFileIgnoredActivity._id = id; + testFileIgnoredActivity._type = OCC::Activity::SyncFileItemType; + testFileIgnoredActivity._syncFileItemStatus = OCC::SyncFileItem::FileIgnored; + testFileIgnoredActivity._dateTime = QDateTime::currentDateTime(); + testFileIgnoredActivity._subject = QStringLiteral("Sample ignored file sync text"); + testFileIgnoredActivity._link = link; + testFileIgnoredActivity._accName = accountName; + testFileIgnoredActivity._folder = QStringLiteral("thingy"); + testFileIgnoredActivity._file = QStringLiteral("test.txt"); + + return testFileIgnoredActivity; +} + +FakeRemoteActivityStorage *FakeRemoteActivityStorage::_instance = nullptr; + +FakeRemoteActivityStorage* FakeRemoteActivityStorage::instance() +{ + if (!_instance) { + _instance = new FakeRemoteActivityStorage(); + _instance->init(); + } + + return _instance; +} + +void FakeRemoteActivityStorage::destroy() +{ + if (_instance) { + delete _instance; + } + + _instance = nullptr; +} + +void FakeRemoteActivityStorage::init() +{ + if (!_activityData.isEmpty()) { + return; + } + + _metaSuccess = {{QStringLiteral("status"), QStringLiteral("ok")}, {QStringLiteral("statuscode"), 200}, + {QStringLiteral("message"), QStringLiteral("OK")}}; + + initActivityData(); +} + +void FakeRemoteActivityStorage::initActivityData() +{ + _activityData = {}; + + // Insert activity data + for (quint32 i = 0; i <= _numItemsToInsert; i++) { + QJsonObject activity; + activity.insert(QStringLiteral("object_type"), "files"); + activity.insert(QStringLiteral("activity_id"), _startingId); + activity.insert(QStringLiteral("type"), QStringLiteral("file")); + activity.insert(QStringLiteral("subject"), QStringLiteral("You created %1.txt").arg(i)); + activity.insert(QStringLiteral("message"), QStringLiteral("")); + activity.insert(QStringLiteral("object_name"), QStringLiteral("%1.txt").arg(i)); + activity.insert(QStringLiteral("datetime"), QDateTime::currentDateTime().toString(Qt::ISODate)); + activity.insert(QStringLiteral("icon"), QStringLiteral("http://example.de/apps/files/img/add-color.svg")); + + _activityData.push_back(activity); + + _startingId++; + } + + // Insert notification data + for (quint32 i = 0; i < _numItemsToInsert; i++) { + QJsonObject activity; + activity.insert(QStringLiteral("activity_id"), _startingId); + activity.insert(QStringLiteral("object_type"), "calendar"); + activity.insert(QStringLiteral("type"), QStringLiteral("calendar-event")); + activity.insert( + QStringLiteral("subject"), QStringLiteral("You created event %1 in calendar Events").arg(i)); + activity.insert(QStringLiteral("message"), QStringLiteral("")); + activity.insert(QStringLiteral("object_name"), QStringLiteral("")); + activity.insert(QStringLiteral("datetime"), QDateTime::currentDateTime().toString(Qt::ISODate)); + activity.insert(QStringLiteral("icon"), QStringLiteral("http://example.de/core/img/places/calendar.svg")); + + _activityData.push_back(activity); + + _startingId++; + } + + // Insert notification data + for (quint32 i = 0; i < _numItemsToInsert; i++) { + QJsonObject activity; + activity.insert(QStringLiteral("activity_id"), _startingId); + activity.insert(QStringLiteral("object_type"), "chat"); + activity.insert(QStringLiteral("type"), QStringLiteral("chat")); + activity.insert(QStringLiteral("subject"), QStringLiteral("You have received %1's message").arg(i)); + activity.insert(QStringLiteral("message"), QStringLiteral("")); + activity.insert(QStringLiteral("object_name"), QStringLiteral("")); + activity.insert(QStringLiteral("datetime"), QDateTime::currentDateTime().toString(Qt::ISODate)); + activity.insert(QStringLiteral("icon"), QStringLiteral("http://example.de/core/img/places/talk.svg")); + + QJsonArray actionsArray; + + QJsonObject replyAction; + replyAction.insert(QStringLiteral("label"), QStringLiteral("Reply")); + replyAction.insert(QStringLiteral("link"), QStringLiteral("")); + replyAction.insert(QStringLiteral("type"), QStringLiteral("REPLY")); + replyAction.insert(QStringLiteral("primary"), true); + actionsArray.push_back(replyAction); + + QJsonObject primaryAction; + primaryAction.insert(QStringLiteral("label"), QStringLiteral("View chat")); + primaryAction.insert(QStringLiteral("link"), QStringLiteral("http://cloud.example.de/call/9p4vjdzd")); + primaryAction.insert(QStringLiteral("type"), QStringLiteral("WEB")); + primaryAction.insert(QStringLiteral("primary"), false); + actionsArray.push_back(primaryAction); + + QJsonObject additionalAction; + additionalAction.insert(QStringLiteral("label"), QStringLiteral("Additional 1")); + additionalAction.insert(QStringLiteral("link"), QStringLiteral("http://cloud.example.de/call/9p4vjdzd")); + additionalAction.insert(QStringLiteral("type"), QStringLiteral("POST")); + additionalAction.insert(QStringLiteral("primary"), false); + actionsArray.push_back(additionalAction); + additionalAction.insert(QStringLiteral("label"), QStringLiteral("Additional 2")); + actionsArray.push_back(additionalAction); + + activity.insert(QStringLiteral("actions"), actionsArray); + + _activityData.push_back(activity); + + _startingId++; + } + + // Insert notification data + for (quint32 i = 0; i < _numItemsToInsert; i++) { + QJsonObject activity; + activity.insert(QStringLiteral("activity_id"), _startingId); + activity.insert(QStringLiteral("object_type"), "room"); + activity.insert(QStringLiteral("type"), QStringLiteral("room")); + activity.insert(QStringLiteral("subject"), QStringLiteral("You have been invited into room%1").arg(i)); + activity.insert(QStringLiteral("message"), QStringLiteral("")); + activity.insert(QStringLiteral("object_name"), QStringLiteral("")); + activity.insert(QStringLiteral("datetime"), QDateTime::currentDateTime().toString(Qt::ISODate)); + activity.insert(QStringLiteral("icon"), QStringLiteral("http://example.de/core/img/places/talk.svg")); + + QJsonArray actionsArray; + + QJsonObject replyAction; + replyAction.insert(QStringLiteral("label"), QStringLiteral("Reply")); + replyAction.insert(QStringLiteral("link"), QStringLiteral("")); + replyAction.insert(QStringLiteral("type"), QStringLiteral("REPLY")); + replyAction.insert(QStringLiteral("primary"), true); + actionsArray.push_back(replyAction); + + QJsonObject primaryAction; + primaryAction.insert(QStringLiteral("label"), QStringLiteral("View chat")); + primaryAction.insert(QStringLiteral("link"), QStringLiteral("http://cloud.example.de/call/9p4vjdzd")); + primaryAction.insert(QStringLiteral("type"), QStringLiteral("WEB")); + primaryAction.insert(QStringLiteral("primary"), false); + actionsArray.push_back(primaryAction); + + activity.insert(QStringLiteral("actions"), actionsArray); + + _activityData.push_back(activity); + + _startingId++; + } + + // Insert notification data + for (quint32 i = 0; i < _numItemsToInsert; i++) { + QJsonObject activity; + activity.insert(QStringLiteral("activity_id"), _startingId); + activity.insert(QStringLiteral("object_type"), "call"); + activity.insert(QStringLiteral("type"), QStringLiteral("call")); + activity.insert(QStringLiteral("subject"), QStringLiteral("You have missed a %1's call").arg(i)); + activity.insert(QStringLiteral("message"), QStringLiteral("")); + activity.insert(QStringLiteral("object_name"), QStringLiteral("")); + activity.insert(QStringLiteral("datetime"), QDateTime::currentDateTime().toString(Qt::ISODate)); + activity.insert(QStringLiteral("icon"), QStringLiteral("http://example.de/core/img/places/talk.svg")); + + QJsonArray actionsArray; + + QJsonObject primaryAction; + primaryAction.insert(QStringLiteral("label"), QStringLiteral("Call back")); + primaryAction.insert(QStringLiteral("link"), QStringLiteral("http://cloud.example.de/call/9p4vjdzd")); + primaryAction.insert(QStringLiteral("type"), QStringLiteral("WEB")); + primaryAction.insert(QStringLiteral("primary"), true); + actionsArray.push_back(primaryAction); + + QJsonObject replyAction; + replyAction.insert(QStringLiteral("label"), QStringLiteral("Reply")); + replyAction.insert(QStringLiteral("link"), QStringLiteral("")); + replyAction.insert(QStringLiteral("type"), QStringLiteral("REPLY")); + replyAction.insert(QStringLiteral("primary"), false); + actionsArray.push_back(replyAction); + + activity.insert(QStringLiteral("actions"), actionsArray); + + _activityData.push_back(activity); + + _startingId++; + } + + // Insert notification data + for (quint32 i = 0; i < _numItemsToInsert; i++) { + QJsonObject activity; + activity.insert(QStringLiteral("activity_id"), _startingId); + activity.insert(QStringLiteral("object_type"), "2fa_id"); + activity.insert(QStringLiteral("subject"), QStringLiteral("Login attempt from 127.0.0.1")); + activity.insert(QStringLiteral("message"), QStringLiteral("Please approve or deny the login attempt.")); + activity.insert(QStringLiteral("object_name"), QStringLiteral("")); + activity.insert(QStringLiteral("datetime"), QDateTime::currentDateTime().toString(Qt::ISODate)); + activity.insert(QStringLiteral("icon"), QStringLiteral("http://example.de/core/img/places/password.svg")); + + QJsonArray actionsArray; + + QJsonObject primaryAction; + primaryAction.insert(QStringLiteral("label"), QStringLiteral("Approve")); + primaryAction.insert(QStringLiteral("link"), QStringLiteral("/ocs/v2.php/apps/twofactor_nextcloud_notification/api/v1/attempt/39")); + primaryAction.insert(QStringLiteral("type"), QStringLiteral("POST")); + primaryAction.insert(QStringLiteral("primary"), true); + actionsArray.push_back(primaryAction); + + QJsonObject secondaryAction; + secondaryAction.insert(QStringLiteral("label"), QStringLiteral("Cancel")); + secondaryAction.insert(QStringLiteral("link"), + QString(QStringLiteral("/ocs/v2.php/apps/twofactor_nextcloud_notification/api/v1/attempt/39"))); + secondaryAction.insert(QStringLiteral("type"), QStringLiteral("DELETE")); + secondaryAction.insert(QStringLiteral("primary"), false); + actionsArray.push_back(secondaryAction); + + activity.insert(QStringLiteral("actions"), actionsArray); + + _activityData.push_back(activity); + + _startingId++; + } + + // Insert notification data + for (quint32 i = 0; i < _numItemsToInsert; i++) { + QJsonObject activity; + activity.insert(QStringLiteral("activity_id"), _startingId); + activity.insert(QStringLiteral("object_type"), "create"); + activity.insert(QStringLiteral("subject"), QStringLiteral("Generate backup codes")); + activity.insert(QStringLiteral("message"), + QStringLiteral("You enabled two-factor authentication but did not generate backup codes yet. They are needed to restore access to your " + "account in case you lose your second factor.")); + activity.insert(QStringLiteral("object_name"), QStringLiteral("")); + activity.insert(QStringLiteral("datetime"), QDateTime::currentDateTime().toString(Qt::ISODate)); + activity.insert(QStringLiteral("icon"), QStringLiteral("http://example.de/core/img/places/password.svg")); + + QJsonArray actionsArray; + + QJsonObject secondaryAction; + secondaryAction.insert(QStringLiteral("label"), QStringLiteral("Dismiss")); + secondaryAction.insert(QStringLiteral("link"), QString(QStringLiteral("ocs/v2.php/apps/notifications/api/v2/notifications/19867"))); + secondaryAction.insert(QStringLiteral("type"), QStringLiteral("DELETE")); + secondaryAction.insert(QStringLiteral("primary"), false); + actionsArray.push_back(secondaryAction); + + activity.insert(QStringLiteral("actions"), actionsArray); + + _activityData.push_back(activity); + + _startingId++; + } + + _startingId--; +} + +QByteArray FakeRemoteActivityStorage::activityJsonData(const int sinceId, const int limit) +{ + QJsonArray data; + + const auto itFound = std::find_if( + std::cbegin(_activityData), std::cend(_activityData), [&sinceId](const QJsonValue ¤tActivityValue) { + const auto currentActivityId = + currentActivityValue.toObject().value(QStringLiteral("activity_id")).toInt(); + return currentActivityId == sinceId; + }); + + const int startIndex = itFound != std::cend(_activityData) + ? static_cast(std::distance(std::cbegin(_activityData), itFound)) + : -1; + + if (startIndex > 0) { + for (int dataIndex = startIndex, iteration = 0; dataIndex >= 0 && iteration < limit; + --dataIndex, ++iteration) { + if (_activityData[dataIndex].toObject().value(QStringLiteral("activity_id")).toInt() + > sinceId - limit) { + data.append(_activityData[dataIndex]); + } + } + } + + QJsonObject root; + QJsonObject ocs; + ocs.insert(QStringLiteral("data"), data); + root.insert(QStringLiteral("ocs"), ocs); + + return QJsonDocument(root).toJson(); +} + +QJsonValue FakeRemoteActivityStorage::activityById(const int id) const +{ + const auto itFound = std::find_if( + std::cbegin(_activityData), std::cend(_activityData), [&id](const QJsonValue ¤tActivityValue) { + const auto currentActivityId = + currentActivityValue.toObject().value(QStringLiteral("activity_id")).toInt(); + return currentActivityId == id; + }); + + if (itFound != std::cend(_activityData)) { + return (*itFound); + } + + return {}; +} + +int FakeRemoteActivityStorage::startingIdLast() const +{ + return _startingId; +} + +int FakeRemoteActivityStorage::numItemsToInsert() const { + return _numItemsToInsert; +} + +int FakeRemoteActivityStorage::totalNumActivites() const { + return _activityData.count(); +} + + +void TestingALM::startFetchJobWithNumActivities(const int numActivities) +{ + auto *job = new OCC::JsonApiJob( + accountState()->account(), QLatin1String("ocs/v2.php/apps/activity/api/v2/activity"), this); + QObject::connect(this, &TestingALM::activityJobStatusCode, this, &TestingALM::slotProcessReceivedActivities); + QObject::connect(job, &OCC::JsonApiJob::jsonReceived, this, &TestingALM::activitiesReceived); + + QUrlQuery params; + params.addQueryItem(QLatin1String("since"), QString::number(currentItem())); + params.addQueryItem(QLatin1String("limit"), QString::number(numActivities)); + job->addQueryParams(params); + + setAndRefreshCurrentlyFetching(true); + job->start(); +} + +void TestingALM::startFetchJob() +{ + startFetchJobWithNumActivities(); +} + +void TestingALM::startMaxActivitiesFetchJob() +{ + startFetchJobWithNumActivities(_maxActivities + 1); +} + +void TestingALM::slotProcessReceivedActivities() +{ + auto finalListCopy = _finalList; + for (int i = _numRowsPrev; i < rowCount(); ++i) { + const auto modelIndex = index(i, 0); + auto activity = finalListCopy.at(modelIndex.row()); + if (activity._links.isEmpty()) { + const auto activityJsonObject = FakeRemoteActivityStorage::instance()->activityById(activity._id); + + if (!activityJsonObject.isNull()) { + // because "_links" are normally populated within the notificationhandler.cpp, which we don't run as part of this unit test, we have to fill them here + // TODO: move the logic to populate "_links" to "activitylistmodel.cpp" + const auto actions = activityJsonObject.toObject().value("actions").toArray(); + for (const auto &action : actions) { + activity._links.append(OCC::ActivityLink::createFomJsonObject(action.toObject())); + } + } + } + + finalListCopy[modelIndex.row()] = activity; + } + + _finalList = finalListCopy; + emit allConflictsChanged(); + + setAndRefreshCurrentlyFetching(false); + emit activitiesProcessed(); +} + +} diff --git a/test/activitylistmodeltestutils.h b/test/activitylistmodeltestutils.h new file mode 100644 index 0000000000..55310f197b --- /dev/null +++ b/test/activitylistmodeltestutils.h @@ -0,0 +1,105 @@ +/* + * Copyright (C) by Claudio Cambra + * + * 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. + */ + +#include +#include + +#include "gui/tray/activitylistmodel.h" + +#include "libsync/account.h" +#include "gui/accountstate.h" +#include "gui/accountmanager.h" + +#pragma once + +class FakeQNAM; +class QByteArray; +class QJsonValue; + +namespace ActivityListModelTestUtils +{ + +[[nodiscard]] QNetworkReply *almTestQnamOverride(FakeQNAM * const fakeQnam, + const QNetworkAccessManager::Operation op, + const QNetworkRequest &req, + const QString &accountUrl, + QObject * const parent = nullptr, + const int searchResultsReplyDelay = 0, + QIODevice * const device = nullptr); + +[[nodiscard]] OCC::Activity exampleNotificationActivity(const QString &accountName, const int id = 1); +[[nodiscard]] OCC::Activity exampleSyncResultErrorActivity(const QString &accountName, const int id = 2); +[[nodiscard]] OCC::Activity exampleSyncFileItemActivity(const QString &accountName, const QUrl &link, const int id = 3); +[[nodiscard]] OCC::Activity exampleFileIgnoredActivity(const QString &accountName, const QUrl &link = {}, const int id = 4); + +class FakeRemoteActivityStorage +{ + FakeRemoteActivityStorage() = default; + +public: + static FakeRemoteActivityStorage *instance(); + + [[nodiscard]] QByteArray activityJsonData(const int sinceId, const int limit); + [[nodiscard]] QJsonValue activityById(const int id) const; + + [[nodiscard]] int startingIdLast() const; + [[nodiscard]] int numItemsToInsert() const; + [[nodiscard]] int totalNumActivites() const; + + static void destroy(); + void init(); + void initActivityData(); + +private: + QJsonArray _activityData; + QVariantMap _metaSuccess; + quint32 _numItemsToInsert = 30; + int _startingId = 90000; + + static FakeRemoteActivityStorage *_instance; +}; + +class TestingALM : public OCC::ActivityListModel +{ + Q_OBJECT + +public: + TestingALM() = default; + + [[nodiscard]] int maxActivities() const + { + return _maxActivities; + }; + // Need to include the dummy "show more in activities app" activity + [[nodiscard]] int maxPossibleActivities() const + { + return maxActivities() + 1; + } + +public slots: + void startFetchJob() override; + void startMaxActivitiesFetchJob(); + void slotProcessReceivedActivities(); + +signals: + void activitiesProcessed(); + +private slots: + void startFetchJobWithNumActivities(const int numActivities = 50); + +private: + int _numRowsPrev = 0; +}; + +} diff --git a/test/testactivitylistmodel.cpp b/test/testactivitylistmodel.cpp index a98db6b612..7e2a734e75 100644 --- a/test/testactivitylistmodel.cpp +++ b/test/testactivitylistmodel.cpp @@ -12,11 +12,7 @@ * for more details. */ -#include "gui/tray/activitylistmodel.h" - -#include "account.h" -#include "accountstate.h" -#include "accountmanager.h" +#include "activitylistmodeltestutils.h" #include "syncenginetestutils.h" #include "syncresult.h" @@ -25,395 +21,7 @@ #include #include -namespace { -constexpr auto startingId = 90000; -} - -static QByteArray fake404Response = R"( -{"ocs":{"meta":{"status":"failure","statuscode":404,"message":"Invalid query, please check the syntax. API specifications are here: http:\/\/www.freedesktop.org\/wiki\/Specifications\/open-collaboration-services.\n"},"data":[]}} -)"; - -static QByteArray fake400Response = R"( -{"ocs":{"meta":{"status":"failure","statuscode":400,"message":"Parameter is incorrect.\n"},"data":[]}} -)"; - -static QByteArray fake500Response = R"( -{"ocs":{"meta":{"status":"failure","statuscode":500,"message":"Internal Server Error.\n"},"data":[]}} -)"; - -class FakeRemoteActivityStorage -{ - FakeRemoteActivityStorage() = default; - -public: - static FakeRemoteActivityStorage *instance() - { - if (!_instance) { - _instance = new FakeRemoteActivityStorage(); - _instance->init(); - } - - return _instance; - } - - static void destroy() - { - if (_instance) { - delete _instance; - } - - _instance = nullptr; - } - - void init() - { - if (!_activityData.isEmpty()) { - return; - } - - _metaSuccess = {{QStringLiteral("status"), QStringLiteral("ok")}, {QStringLiteral("statuscode"), 200}, - {QStringLiteral("message"), QStringLiteral("OK")}}; - - initActivityData(); - } - - void initActivityData() - { - // Insert activity data - for (quint32 i = 0; i <= _numItemsToInsert; i++) { - QJsonObject activity; - activity.insert(QStringLiteral("object_type"), "files"); - activity.insert(QStringLiteral("activity_id"), _startingId); - activity.insert(QStringLiteral("type"), QStringLiteral("file")); - activity.insert(QStringLiteral("subject"), QStringLiteral("You created %1.txt").arg(i)); - activity.insert(QStringLiteral("message"), QStringLiteral("")); - activity.insert(QStringLiteral("object_name"), QStringLiteral("%1.txt").arg(i)); - activity.insert(QStringLiteral("datetime"), QDateTime::currentDateTime().toString(Qt::ISODate)); - activity.insert(QStringLiteral("icon"), QStringLiteral("http://example.de/apps/files/img/add-color.svg")); - - _activityData.push_back(activity); - - _startingId++; - } - - // Insert notification data - for (quint32 i = 0; i < _numItemsToInsert; i++) { - QJsonObject activity; - activity.insert(QStringLiteral("activity_id"), _startingId); - activity.insert(QStringLiteral("object_type"), "calendar"); - activity.insert(QStringLiteral("type"), QStringLiteral("calendar-event")); - activity.insert( - QStringLiteral("subject"), QStringLiteral("You created event %1 in calendar Events").arg(i)); - activity.insert(QStringLiteral("message"), QStringLiteral("")); - activity.insert(QStringLiteral("object_name"), QStringLiteral("")); - activity.insert(QStringLiteral("datetime"), QDateTime::currentDateTime().toString(Qt::ISODate)); - activity.insert(QStringLiteral("icon"), QStringLiteral("http://example.de/core/img/places/calendar.svg")); - - _activityData.push_back(activity); - - _startingId++; - } - - // Insert notification data - for (quint32 i = 0; i < _numItemsToInsert; i++) { - QJsonObject activity; - activity.insert(QStringLiteral("activity_id"), _startingId); - activity.insert(QStringLiteral("object_type"), "chat"); - activity.insert(QStringLiteral("type"), QStringLiteral("chat")); - activity.insert(QStringLiteral("subject"), QStringLiteral("You have received %1's message").arg(i)); - activity.insert(QStringLiteral("message"), QStringLiteral("")); - activity.insert(QStringLiteral("object_name"), QStringLiteral("")); - activity.insert(QStringLiteral("datetime"), QDateTime::currentDateTime().toString(Qt::ISODate)); - activity.insert(QStringLiteral("icon"), QStringLiteral("http://example.de/core/img/places/talk.svg")); - - QJsonArray actionsArray; - - QJsonObject replyAction; - replyAction.insert(QStringLiteral("label"), QStringLiteral("Reply")); - replyAction.insert(QStringLiteral("link"), QStringLiteral("")); - replyAction.insert(QStringLiteral("type"), QStringLiteral("REPLY")); - replyAction.insert(QStringLiteral("primary"), true); - actionsArray.push_back(replyAction); - - QJsonObject primaryAction; - primaryAction.insert(QStringLiteral("label"), QStringLiteral("View chat")); - primaryAction.insert(QStringLiteral("link"), QStringLiteral("http://cloud.example.de/call/9p4vjdzd")); - primaryAction.insert(QStringLiteral("type"), QStringLiteral("WEB")); - primaryAction.insert(QStringLiteral("primary"), false); - actionsArray.push_back(primaryAction); - - QJsonObject additionalAction; - additionalAction.insert(QStringLiteral("label"), QStringLiteral("Additional 1")); - additionalAction.insert(QStringLiteral("link"), QStringLiteral("http://cloud.example.de/call/9p4vjdzd")); - additionalAction.insert(QStringLiteral("type"), QStringLiteral("POST")); - additionalAction.insert(QStringLiteral("primary"), false); - actionsArray.push_back(additionalAction); - additionalAction.insert(QStringLiteral("label"), QStringLiteral("Additional 2")); - actionsArray.push_back(additionalAction); - - activity.insert(QStringLiteral("actions"), actionsArray); - - _activityData.push_back(activity); - - _startingId++; - } - - // Insert notification data - for (quint32 i = 0; i < _numItemsToInsert; i++) { - QJsonObject activity; - activity.insert(QStringLiteral("activity_id"), _startingId); - activity.insert(QStringLiteral("object_type"), "room"); - activity.insert(QStringLiteral("type"), QStringLiteral("room")); - activity.insert(QStringLiteral("subject"), QStringLiteral("You have been invited into room%1").arg(i)); - activity.insert(QStringLiteral("message"), QStringLiteral("")); - activity.insert(QStringLiteral("object_name"), QStringLiteral("")); - activity.insert(QStringLiteral("datetime"), QDateTime::currentDateTime().toString(Qt::ISODate)); - activity.insert(QStringLiteral("icon"), QStringLiteral("http://example.de/core/img/places/talk.svg")); - - QJsonArray actionsArray; - - QJsonObject replyAction; - replyAction.insert(QStringLiteral("label"), QStringLiteral("Reply")); - replyAction.insert(QStringLiteral("link"), QStringLiteral("")); - replyAction.insert(QStringLiteral("type"), QStringLiteral("REPLY")); - replyAction.insert(QStringLiteral("primary"), true); - actionsArray.push_back(replyAction); - - QJsonObject primaryAction; - primaryAction.insert(QStringLiteral("label"), QStringLiteral("View chat")); - primaryAction.insert(QStringLiteral("link"), QStringLiteral("http://cloud.example.de/call/9p4vjdzd")); - primaryAction.insert(QStringLiteral("type"), QStringLiteral("WEB")); - primaryAction.insert(QStringLiteral("primary"), false); - actionsArray.push_back(primaryAction); - - activity.insert(QStringLiteral("actions"), actionsArray); - - _activityData.push_back(activity); - - _startingId++; - } - - // Insert notification data - for (quint32 i = 0; i < _numItemsToInsert; i++) { - QJsonObject activity; - activity.insert(QStringLiteral("activity_id"), _startingId); - activity.insert(QStringLiteral("object_type"), "call"); - activity.insert(QStringLiteral("type"), QStringLiteral("call")); - activity.insert(QStringLiteral("subject"), QStringLiteral("You have missed a %1's call").arg(i)); - activity.insert(QStringLiteral("message"), QStringLiteral("")); - activity.insert(QStringLiteral("object_name"), QStringLiteral("")); - activity.insert(QStringLiteral("datetime"), QDateTime::currentDateTime().toString(Qt::ISODate)); - activity.insert(QStringLiteral("icon"), QStringLiteral("http://example.de/core/img/places/talk.svg")); - - QJsonArray actionsArray; - - QJsonObject primaryAction; - primaryAction.insert(QStringLiteral("label"), QStringLiteral("Call back")); - primaryAction.insert(QStringLiteral("link"), QStringLiteral("http://cloud.example.de/call/9p4vjdzd")); - primaryAction.insert(QStringLiteral("type"), QStringLiteral("WEB")); - primaryAction.insert(QStringLiteral("primary"), true); - actionsArray.push_back(primaryAction); - - QJsonObject replyAction; - replyAction.insert(QStringLiteral("label"), QStringLiteral("Reply")); - replyAction.insert(QStringLiteral("link"), QStringLiteral("")); - replyAction.insert(QStringLiteral("type"), QStringLiteral("REPLY")); - replyAction.insert(QStringLiteral("primary"), false); - actionsArray.push_back(replyAction); - - activity.insert(QStringLiteral("actions"), actionsArray); - - _activityData.push_back(activity); - - _startingId++; - } - - // Insert notification data - for (quint32 i = 0; i < _numItemsToInsert; i++) { - QJsonObject activity; - activity.insert(QStringLiteral("activity_id"), _startingId); - activity.insert(QStringLiteral("object_type"), "2fa_id"); - activity.insert(QStringLiteral("subject"), QStringLiteral("Login attempt from 127.0.0.1")); - activity.insert(QStringLiteral("message"), QStringLiteral("Please approve or deny the login attempt.")); - activity.insert(QStringLiteral("object_name"), QStringLiteral("")); - activity.insert(QStringLiteral("datetime"), QDateTime::currentDateTime().toString(Qt::ISODate)); - activity.insert(QStringLiteral("icon"), QStringLiteral("http://example.de/core/img/places/password.svg")); - - QJsonArray actionsArray; - - QJsonObject primaryAction; - primaryAction.insert(QStringLiteral("label"), QStringLiteral("Approve")); - primaryAction.insert(QStringLiteral("link"), QStringLiteral("/ocs/v2.php/apps/twofactor_nextcloud_notification/api/v1/attempt/39")); - primaryAction.insert(QStringLiteral("type"), QStringLiteral("POST")); - primaryAction.insert(QStringLiteral("primary"), true); - actionsArray.push_back(primaryAction); - - QJsonObject secondaryAction; - secondaryAction.insert(QStringLiteral("label"), QStringLiteral("Cancel")); - secondaryAction.insert(QStringLiteral("link"), - QString(QStringLiteral("/ocs/v2.php/apps/twofactor_nextcloud_notification/api/v1/attempt/39"))); - secondaryAction.insert(QStringLiteral("type"), QStringLiteral("DELETE")); - secondaryAction.insert(QStringLiteral("primary"), false); - actionsArray.push_back(secondaryAction); - - activity.insert(QStringLiteral("actions"), actionsArray); - - _activityData.push_back(activity); - - _startingId++; - } - - // Insert notification data - for (quint32 i = 0; i < _numItemsToInsert; i++) { - QJsonObject activity; - activity.insert(QStringLiteral("activity_id"), _startingId); - activity.insert(QStringLiteral("object_type"), "create"); - activity.insert(QStringLiteral("subject"), QStringLiteral("Generate backup codes")); - activity.insert(QStringLiteral("message"), QStringLiteral("You enabled two-factor authentication but did not generate backup codes yet. They are needed to restore access to your account in case you lose your second factor.")); - activity.insert(QStringLiteral("object_name"), QStringLiteral("")); - activity.insert(QStringLiteral("datetime"), QDateTime::currentDateTime().toString(Qt::ISODate)); - activity.insert(QStringLiteral("icon"), QStringLiteral("http://example.de/core/img/places/password.svg")); - - QJsonArray actionsArray; - - QJsonObject secondaryAction; - secondaryAction.insert(QStringLiteral("label"), QStringLiteral("Dismiss")); - secondaryAction.insert(QStringLiteral("link"), - QString(QStringLiteral("ocs/v2.php/apps/notifications/api/v2/notifications/19867"))); - secondaryAction.insert(QStringLiteral("type"), QStringLiteral("DELETE")); - secondaryAction.insert(QStringLiteral("primary"), false); - actionsArray.push_back(secondaryAction); - - activity.insert(QStringLiteral("actions"), actionsArray); - - _activityData.push_back(activity); - - _startingId++; - } - - _startingId--; - } - - const QByteArray activityJsonData(int sinceId, int limit) - { - QJsonArray data; - - const auto itFound = std::find_if( - std::cbegin(_activityData), std::cend(_activityData), [&sinceId](const QJsonValue ¤tActivityValue) { - const auto currentActivityId = - currentActivityValue.toObject().value(QStringLiteral("activity_id")).toInt(); - return currentActivityId == sinceId; - }); - - const int startIndex = itFound != std::cend(_activityData) - ? static_cast(std::distance(std::cbegin(_activityData), itFound)) - : -1; - - if (startIndex > 0) { - for (int dataIndex = startIndex, iteration = 0; dataIndex >= 0 && iteration < limit; - --dataIndex, ++iteration) { - if (_activityData[dataIndex].toObject().value(QStringLiteral("activity_id")).toInt() - > sinceId - limit) { - data.append(_activityData[dataIndex]); - } - } - } - - QJsonObject root; - QJsonObject ocs; - ocs.insert(QStringLiteral("data"), data); - root.insert(QStringLiteral("ocs"), ocs); - - return QJsonDocument(root).toJson(); - } - - QJsonValue activityById(int id) - { - const auto itFound = std::find_if( - std::cbegin(_activityData), std::cend(_activityData), [&id](const QJsonValue ¤tActivityValue) { - const auto currentActivityId = - currentActivityValue.toObject().value(QStringLiteral("activity_id")).toInt(); - return currentActivityId == id; - }); - - if (itFound != std::cend(_activityData)) { - return (*itFound); - } - - return {}; - } - - [[nodiscard]] int startingIdLast() const { return _startingId; } - -private: - static FakeRemoteActivityStorage *_instance; - QJsonArray _activityData; - QVariantMap _metaSuccess; - quint32 _numItemsToInsert = 30; - int _startingId = startingId; -}; - -FakeRemoteActivityStorage *FakeRemoteActivityStorage::_instance = nullptr; - -class TestingALM : public OCC::ActivityListModel -{ - Q_OBJECT - -public: - TestingALM() = default; - - void startFetchJob() override - { - auto *job = new OCC::JsonApiJob( - accountState()->account(), QLatin1String("ocs/v2.php/apps/activity/api/v2/activity"), this); - QObject::connect(this, &TestingALM::activityJobStatusCode, this, &TestingALM::slotProcessReceivedActivities); - QObject::connect(job, &OCC::JsonApiJob::jsonReceived, this, &TestingALM::activitiesReceived); - - QUrlQuery params; - params.addQueryItem(QLatin1String("since"), QString::number(currentItem())); - params.addQueryItem(QLatin1String("limit"), QString::number(50)); - job->addQueryParams(params); - - setAndRefreshCurrentlyFetching(true); - job->start(); - } - -public slots: - void slotProcessReceivedActivities() - { - if (rowCount() > _numRowsPrev) { - auto finalListCopy = finalList(); - for (int i = _numRowsPrev; i < rowCount(); ++i) { - const auto modelIndex = index(i, 0); - auto activity = finalListCopy.at(modelIndex.row()); - if (activity._links.isEmpty()) { - const auto activityJsonObject = FakeRemoteActivityStorage::instance()->activityById(activity._id); - - if (!activityJsonObject.isNull()) { - // because "_links" are normally populated within the notificationhandler.cpp, which we don't run as part of this unit test, we have to fill them here - // TODO: move the logic to populate "_links" to "activitylistmodel.cpp" - auto actions = activityJsonObject.toObject().value("actions").toArray(); - foreach (auto action, actions) { - activity._links.append(OCC::ActivityLink::createFomJsonObject(action.toObject())); - } - - finalListCopy[modelIndex.row()] = activity; - } - } - } - - setFinalList(finalListCopy); - } - _numRowsPrev = rowCount(); - setAndRefreshCurrentlyFetching(false); - emit activitiesProcessed(); - } -signals: - void activitiesProcessed(); - -private: - int _numRowsPrev = 0; -}; +using namespace ActivityListModelTestUtils; class TestActivityListModel : public QObject { @@ -489,70 +97,24 @@ private slots: accountState.reset(new OCC::AccountState(account)); fakeQnam->setOverride([this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) { - Q_UNUSED(device); - QNetworkReply *reply = nullptr; - - const auto urlQuery = QUrlQuery(req.url()); - const auto format = urlQuery.queryItemValue(QStringLiteral("format")); - const auto since = urlQuery.queryItemValue(QStringLiteral("since")).toInt(); - const auto limit = urlQuery.queryItemValue(QStringLiteral("limit")).toInt(); - const auto path = req.url().path(); - - if (!req.url().toString().startsWith(accountState->account()->url().toString())) { - reply = new FakeErrorReply(op, req, this, 404, fake404Response); - } - if (format != QStringLiteral("json")) { - reply = new FakeErrorReply(op, req, this, 400, fake400Response); - } - - if (path.startsWith(QStringLiteral("/ocs/v2.php/apps/activity/api/v2/activity"))) { - reply = new FakePayloadReply(op, req, FakeRemoteActivityStorage::instance()->activityJsonData(since, limit), searchResultsReplyDelay, fakeQnam.data()); - } - - if (!reply) { - return qobject_cast(new FakeErrorReply(op, req, this, 404, QByteArrayLiteral("{error: \"Not found!\"}"))); - } - - return reply; + Q_UNUSED(device) + return almTestQnamOverride(fakeQnam.data(), + op, + req, + accountState->account()->url().toString(), + this, + searchResultsReplyDelay); }); OCC::AccountManager::instance()->addAccount(account); - // Activity comparison is done by checking type, id, and accName - // We need an activity with these details, at least - testNotificationActivity._accName = accountState->account()->displayName(); - testNotificationActivity._id = 1; - testNotificationActivity._type = OCC::Activity::NotificationType; - testNotificationActivity._dateTime = QDateTime::currentDateTime(); - testNotificationActivity._subject = QStringLiteral("Sample notification text"); + const auto accName = accountState->account()->displayName(); + const auto accUrl = accountState->account()->url(); - testSyncResultErrorActivity._id = 2; - testSyncResultErrorActivity._type = OCC::Activity::SyncResultType; - testSyncResultErrorActivity._syncResultStatus = OCC::SyncResult::Error; - testSyncResultErrorActivity._dateTime = QDateTime::currentDateTime(); - testSyncResultErrorActivity._subject = QStringLiteral("Sample failed sync text"); - testSyncResultErrorActivity._message = QStringLiteral("/path/to/thingy"); - testSyncResultErrorActivity._link = QStringLiteral("/path/to/thingy"); - testSyncResultErrorActivity._accName = accountState->account()->displayName(); - - testSyncFileItemActivity._id = 3; - testSyncFileItemActivity._type = OCC::Activity::SyncFileItemType; //client activity - testSyncFileItemActivity._syncFileItemStatus = OCC::SyncFileItem::Success; - testSyncFileItemActivity._dateTime = QDateTime::currentDateTime(); - testSyncFileItemActivity._message = QStringLiteral("Sample file successfully synced text"); - testSyncFileItemActivity._link = accountState->account()->url(); - testSyncFileItemActivity._accName = accountState->account()->displayName(); - testSyncFileItemActivity._file = QStringLiteral("xyz.pdf"); - - testFileIgnoredActivity._id = 4; - testFileIgnoredActivity._type = OCC::Activity::SyncFileItemType; - testFileIgnoredActivity._syncFileItemStatus = OCC::SyncFileItem::FileIgnored; - testFileIgnoredActivity._dateTime = QDateTime::currentDateTime(); - testFileIgnoredActivity._subject = QStringLiteral("Sample ignored file sync text"); - testFileIgnoredActivity._link = accountState->account()->url(); - testFileIgnoredActivity._accName = accountState->account()->displayName(); - testFileIgnoredActivity._folder = QStringLiteral("thingy"); - testFileIgnoredActivity._file = QStringLiteral("test.txt"); + testNotificationActivity = exampleNotificationActivity(accName); + testSyncResultErrorActivity = exampleSyncResultErrorActivity(accName); + testSyncFileItemActivity = exampleSyncFileItemActivity(accName, accUrl); + testFileIgnoredActivity = exampleFileIgnoredActivity(accName, accUrl); }; // Test receiving activity from server diff --git a/test/testsortedactivitylistmodel.cpp b/test/testsortedactivitylistmodel.cpp new file mode 100644 index 0000000000..90b8d74c8b --- /dev/null +++ b/test/testsortedactivitylistmodel.cpp @@ -0,0 +1,205 @@ +/* + * Copyright (C) by Claudio Cambra + * + * 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. + */ + +#include "activitylistmodeltestutils.h" +#include "syncenginetestutils.h" +#include "syncresult.h" +#include "tray/sortedactivitylistmodel.h" + +#include +#include +#include +#include + +using namespace ActivityListModelTestUtils; + +class TestSortedActivityListModel : public QObject +{ + Q_OBJECT + +public: + TestSortedActivityListModel() = default; + ~TestSortedActivityListModel() override + { + OCC::AccountManager::instance()->deleteAccount(accountState.data()); + } + + QScopedPointer fakeQnam; + OCC::AccountPtr account; + QScopedPointer accountState; + + OCC::Activity testNotificationActivity; + OCC::Activity testSyncResultErrorActivity; + OCC::Activity testSyncFileItemActivity; + OCC::Activity testFileIgnoredActivity; + + QSharedPointer testingSortedALM() + { + const auto model = new TestingALM; + model->setAccountState(accountState.data()); + + QSharedPointer sortedModel(new OCC::SortedActivityListModel); + sortedModel->setSourceModel(model); + QAbstractItemModelTester sortedModelTester(sortedModel.data()); + + return sortedModel; + } + + void addActivity(QSharedPointer model, + void(OCC::ActivityListModel::*addingMethod)(const OCC::Activity&), + OCC::Activity &activity) + { + const auto originalRowCount = model->rowCount(); + const auto sourceModel = dynamic_cast(model->sourceModel()); + + (sourceModel->*addingMethod)(activity); + QCOMPARE(model->rowCount(), originalRowCount + 1); + } + + void addActivity(QSharedPointer model, + void (OCC::ActivityListModel::*addingMethod)(const OCC::Activity &, OCC::ActivityListModel::ErrorType), + OCC::Activity &activity, + OCC::ActivityListModel::ErrorType type) + { + const auto originalRowCount = model->rowCount(); + const auto sourceModel = dynamic_cast(model->sourceModel()); + + (sourceModel->*addingMethod)(activity, type); + QCOMPARE(model->rowCount(), originalRowCount + 1); + + const auto index = model->index(0, 0); + QVERIFY(index.isValid()); + } + +private slots: + void initTestCase() + { + fakeQnam.reset(new FakeQNAM({})); + account = OCC::Account::create(); + account->setCredentials(new FakeCredentials{fakeQnam.data()}); + account->setUrl(QUrl(("http://example.de"))); + + accountState.reset(new OCC::AccountState(account)); + + fakeQnam->setOverride([this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) { + Q_UNUSED(device) + return almTestQnamOverride(fakeQnam.data(), + op, + req, + accountState->account()->url().toString(), + this); + }); + + OCC::AccountManager::instance()->addAccount(account); + + const auto accName = accountState->account()->displayName(); + const auto accUrl = accountState->account()->url(); + + testNotificationActivity = exampleNotificationActivity(accName); + testSyncResultErrorActivity = exampleSyncResultErrorActivity(accName); + testSyncFileItemActivity = exampleSyncFileItemActivity(accName, accUrl); + testFileIgnoredActivity = exampleFileIgnoredActivity(accName, accUrl); + }; + + void testMatchingRowCounts() + { + const auto model = testingSortedALM(); + const auto sourceModel = dynamic_cast(model->sourceModel()); + QCOMPARE(sourceModel->rowCount(), 0); + QCOMPARE(model->rowCount(), sourceModel->rowCount()); + + sourceModel->setCurrentItem(FakeRemoteActivityStorage::instance()->startingIdLast()); + sourceModel->startFetchJob(); + QSignalSpy activitiesJob(sourceModel, &TestingALM::activitiesProcessed); + QVERIFY(activitiesJob.wait(3000)); + QCOMPARE(sourceModel->rowCount(), 50); + QCOMPARE(model->rowCount(), sourceModel->rowCount()); + } + + void testUpdate() + { + const auto model = testingSortedALM(); + const auto sourceModel = dynamic_cast(model->sourceModel()); + + sourceModel->setCurrentItem(FakeRemoteActivityStorage::instance()->startingIdLast()); + sourceModel->startFetchJob(); + QSignalSpy activitiesJob(sourceModel, &TestingALM::activitiesProcessed); + QVERIFY(activitiesJob.wait(3000)); + QCOMPARE(sourceModel->rowCount(), 50); + + addActivity(model, &TestingALM::addSyncFileItemToActivityList, testSyncFileItemActivity); + addActivity(model, &TestingALM::addNotificationToActivityList, testNotificationActivity); + addActivity(model, &TestingALM::addErrorToActivityList, testSyncResultErrorActivity, OCC::ActivityListModel::ErrorType::SyncError); + addActivity(model, &TestingALM::addIgnoredFileToList, testFileIgnoredActivity); + } + + void testSort() + { + const auto model = testingSortedALM(); + const auto sourceModel = dynamic_cast(model->sourceModel()); + + sourceModel->setCurrentItem(FakeRemoteActivityStorage::instance()->startingIdLast()); + sourceModel->startMaxActivitiesFetchJob(); + QSignalSpy activitiesJob(sourceModel, &TestingALM::activitiesProcessed); + QVERIFY(activitiesJob.wait(3000)); + QCOMPARE(sourceModel->rowCount(), sourceModel->maxPossibleActivities()); + + auto errorSyncFileItemActivity = exampleSyncFileItemActivity(accountState->account()->displayName(), {}); + errorSyncFileItemActivity._message = QStringLiteral("Something went wrong and eveything exploded!"); + errorSyncFileItemActivity._syncFileItemStatus = OCC::SyncFileItem::FatalError; + + addActivity(model, &TestingALM::addSyncFileItemToActivityList, errorSyncFileItemActivity); + addActivity(model, &TestingALM::addSyncFileItemToActivityList, testSyncFileItemActivity); + addActivity(model, &TestingALM::addNotificationToActivityList, testNotificationActivity); + addActivity(model, &TestingALM::addErrorToActivityList, testSyncResultErrorActivity, OCC::ActivityListModel::ErrorType::SyncError); + addActivity(model, &TestingALM::addIgnoredFileToList, testFileIgnoredActivity); + + const QVector activityDefaultTypeOrder { + OCC::Activity::DummyFetchingActivityType, + OCC::Activity::NotificationType, + OCC::Activity::SyncResultType, + OCC::Activity::SyncFileItemType, + OCC::Activity::ActivityType, + OCC::Activity::DummyMoreActivitiesAvailableType}; + auto currentTypeSection = 1; + auto previousType = activityDefaultTypeOrder[currentTypeSection]; + + for (auto i = 0; i < model->rowCount(); ++i) { + const auto index = model->index(i, 0); + const auto activity = index.data(OCC::ActivityListModel::ActivityRole).value(); + + qDebug() << i << activity._type << activity._subject << activity._message; + if (i == 0) { // Error syncresult activity should be at top + QCOMPARE(activity._type, OCC::Activity::SyncResultType); + QCOMPARE(activity._syncResultStatus, OCC::SyncResult::Error); + } else if (i == 1) { // Error syncfileitem activity should be next up + QCOMPARE(activity._type, OCC::Activity::SyncFileItemType); + QCOMPARE(activity._syncFileItemStatus, OCC::SyncFileItem::FatalError); + } else if (i == 2) { // Ignored file syncfileitem activity should be next up + QCOMPARE(activity._type, OCC::Activity::SyncFileItemType); + QCOMPARE(activity._syncFileItemStatus, OCC::SyncFileItem::FileIgnored); + } else { // Now normal type order + while (i != 3 && activity._type != previousType) { + ++currentTypeSection; + previousType = activityDefaultTypeOrder[currentTypeSection]; + } + + QCOMPARE(activity._type, activityDefaultTypeOrder[currentTypeSection]); + } + } + } +}; + +QTEST_MAIN(TestSortedActivityListModel) +#include "testsortedactivitylistmodel.moc"