Add testing for ActivityListModel
authorClaudio Cambra <claudio.cambra@gmail.com>
Thu, 23 Dec 2021 10:57:08 +0000 (11:57 +0100)
committerClaudio Cambra (Rebase PR Action) <claudio.cambra@gmail.com>
Tue, 1 Feb 2022 13:57:08 +0000 (13:57 +0000)
Signed-off-by: Claudio Cambra <claudio.cambra@gmail.com>
src/gui/tray/activitylistmodel.cpp
src/gui/tray/activitylistmodel.h
test/CMakeLists.txt
test/testactivitylistmodel.cpp [new file with mode: 0644]

index eacf36fd8bc6d3b31c8ee995e7963e48bb5ceb73..8120612a541646ef58810b15406aba7913bcfb2d 100644 (file)
@@ -269,8 +269,12 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
     return QVariant();
 }
 
-int ActivityListModel::rowCount(const QModelIndex &) const
+int ActivityListModel::rowCount(const QModelIndex &parent) const
 {
+    if(parent.isValid()) {
+        return 0;
+    }
+
     return _finalList.count();
 }
 
@@ -342,7 +346,6 @@ void ActivityListModel::activitiesReceived(const QJsonDocument &json, int status
         a._icon = json.value(QStringLiteral("icon")).toString();
 
         auto richSubjectData = json.value(QStringLiteral("subject_rich")).toArray();
-        Q_ASSERT(richSubjectData.size() > 1);
 
         if(richSubjectData.size() > 1) {
             a._subjectRich = richSubjectData[0].toString();
@@ -621,14 +624,8 @@ void ActivityListModel::combineActivityLists()
     }
 
     beginResetModel();
-    _finalList.clear();
+    _finalList = resultList;
     endResetModel();
-
-    if (resultList.count() > 0) {
-        beginInsertRows(QModelIndex(), 0, resultList.count() - 1);
-        _finalList = resultList;
-        endInsertRows();
-    }
 }
 
 bool ActivityListModel::canFetchActivities() const
@@ -638,11 +635,8 @@ bool ActivityListModel::canFetchActivities() const
 
 void ActivityListModel::fetchMore(const QModelIndex &)
 {
-    if (canFetchActivities()) {
+    if (canFetchActivities() && !_currentlyFetching) {
         startFetchJob();
-    } else {
-        _doneFetching = true;
-        combineActivityLists();
     }
 }
 
@@ -672,4 +666,6 @@ void ActivityListModel::slotRemoveAccount()
     _totalActivitiesFetched = 0;
     _showMoreActivitiesAvailableEntry = false;
 }
+
 }
+
index 07385e76818130895c0bf21dfa84c3ea5ea3db74..66de2e4986df565f5068fb1d6d78d5e5ddd75342 100644 (file)
@@ -44,7 +44,6 @@ class ActivityListModel : public QAbstractListModel
 public:
     enum DataRole {
         ActionIconRole = Qt::UserRole + 1,
-        UserIconRole,
         AccountRole,
         ObjectTypeRole,
         ActionsLinksRole,
@@ -59,7 +58,6 @@ public:
         LinkRole,
         PointInTimeRole,
         AccountConnectedRole,
-        SyncFileStatusRole,
         DisplayActions,
         ShareableRole,
     };
@@ -90,6 +88,7 @@ public:
     Q_INVOKABLE void triggerAction(int activityIndex, int actionIndex);
 
     AccountState *accountState() const;
+    void setAccountState(AccountState *state);
 
 public slots:
     void slotRefreshActivity();
@@ -103,7 +102,6 @@ protected:
     void activitiesReceived(const QJsonDocument &json, int statusCode);
     QHash<int, QByteArray> roleNames() const override;
 
-    void setAccountState(AccountState *state);
     void setCurrentlyFetching(bool value);
     bool currentlyFetching() const;
     void setDoneFetching(bool value);
index 987fb8c9f4f56c398bee02945e8d505a9f07a7e3..011ad55805ac79fe7a4ad378bb5297d80ea59e48 100644 (file)
@@ -61,6 +61,7 @@ nextcloud_add_test(IconUtils)
 nextcloud_add_test(NotificationCache)
 nextcloud_add_test(SetUserStatusDialog)
 nextcloud_add_test(UnifiedSearchListmodel)
+nextcloud_add_test(ActivityListModel)
 
 if( UNIX AND NOT APPLE )
     nextcloud_add_test(InotifyWatcher)
diff --git a/test/testactivitylistmodel.cpp b/test/testactivitylistmodel.cpp
new file mode 100644 (file)
index 0000000..8faf21d
--- /dev/null
@@ -0,0 +1,413 @@
+/*
+ * Copyright (C) by Claudio Cambra <claudio.cambra@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.
+ */
+
+#include "gui/tray/activitylistmodel.h"
+
+#include "account.h"
+#include "accountstate.h"
+#include "accountmanager.h"
+#include "syncenginetestutils.h"
+#include "syncresult.h"
+
+#include <QAbstractItemModelTester>
+#include <QDesktopServices>
+#include <QSignalSpy>
+#include <QTest>
+
+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 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(job, &OCC::JsonApiJob::jsonReceived,
+            this, &TestingALM::activitiesReceived);
+
+        QUrlQuery params;
+        params.addQueryItem(QLatin1String("since"), QString::number(startingId));
+        params.addQueryItem(QLatin1String("limit"), QString::number(50));
+        job->addQueryParams(params);
+
+        job->start();
+    };
+};
+
+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++) {
+            _startingId++;
+
+            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);
+        }
+
+        // Insert notification data
+        for (quint32 i = 0; i < _numItemsToInsert; i++) {
+            _startingId++;
+            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);
+        }
+    }
+
+    const QByteArray activityJsonData(int sinceId, int limit)
+    {
+        QJsonArray data;
+
+        for(int dataIndex = _activityData.size() - 1, iteration = 0;
+            dataIndex > 0 && iteration < limit;
+            --dataIndex, ++iteration) {
+
+            if(_activityData[dataIndex].toObject().value(QStringLiteral("activity_id")).toInt() > sinceId) {
+                data.append(_activityData[dataIndex]);
+            }
+        }
+
+        QJsonObject root;
+        QJsonObject ocs;
+        ocs.insert(QStringLiteral("data"), data);
+        root.insert(QStringLiteral("ocs"), ocs);
+
+        return QJsonDocument(root).toJson();
+    }
+
+private:
+    static FakeRemoteActivityStorage *_instance;
+    QJsonArray _activityData;
+    QVariantMap _metaSuccess;
+    quint32 _numItemsToInsert = 30;
+    int _startingId = startingId;
+};
+
+FakeRemoteActivityStorage *FakeRemoteActivityStorage::_instance = nullptr;
+
+class TestActivityListModel : public QObject
+{
+    Q_OBJECT
+
+public:
+    TestActivityListModel() = default;
+    ~TestActivityListModel() override {
+        OCC::AccountManager::instance()->deleteAccount(accountState.data());
+    }
+
+    QScopedPointer<FakeQNAM> fakeQnam;
+    OCC::AccountPtr account;
+    QScopedPointer<OCC::AccountState> accountState;
+
+    OCC::Activity testNotificationActivity;
+
+    static constexpr int searchResultsReplyDelay = 100;
+
+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);
+            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<QNetworkReply*>(new FakeErrorReply(op, req, this, 404, QByteArrayLiteral("{error: \"Not found!\"}")));
+            }
+
+            return reply;
+        });
+
+        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();
+    };
+
+    // Test receiving activity from server
+    void testFetchingRemoteActivity() {
+        TestingALM model;
+        model.setAccountState(accountState.data());
+        QAbstractItemModelTester modelTester(&model);
+
+        QCOMPARE(model.rowCount(), 0);
+
+        model.startFetchJob();
+        QSignalSpy activitiesJob(&model, &TestingALM::activityJobStatusCode);
+        QVERIFY(activitiesJob.wait(3000));
+        QCOMPARE(model.rowCount(), 50);
+    };
+
+    // Test receiving activity from local user action
+    void testLocalSyncFileAction() {
+        TestingALM model;
+        model.setAccountState(accountState.data());
+        QAbstractItemModelTester modelTester(&model);
+
+        QCOMPARE(model.rowCount(), 0);
+
+        OCC::Activity activity;
+
+        model.addSyncFileItemToActivityList(activity);
+        QCOMPARE(model.rowCount(), 1);
+
+        const auto index = model.index(0, 0);
+        QVERIFY(index.isValid());
+    };
+
+    void testAddNotification() {
+        TestingALM model;
+        model.setAccountState(accountState.data());
+        QAbstractItemModelTester modelTester(&model);
+
+        QCOMPARE(model.rowCount(), 0);
+
+        model.addNotificationToActivityList(testNotificationActivity);
+        QCOMPARE(model.rowCount(), 1);
+
+        const auto index = model.index(0, 0);
+        QVERIFY(index.isValid());
+    };
+
+    void testAddError() {
+        TestingALM model;
+        model.setAccountState(accountState.data());
+        QAbstractItemModelTester modelTester(&model);
+
+        QCOMPARE(model.rowCount(), 0);
+
+        OCC::Activity activity;
+
+        model.addErrorToActivityList(activity);
+        QCOMPARE(model.rowCount(), 1);
+
+        const auto index = model.index(0, 0);
+        QVERIFY(index.isValid());
+    };
+
+    void testAddIgnoredFile() {
+        TestingALM model;
+        model.setAccountState(accountState.data());
+        QAbstractItemModelTester modelTester(&model);
+
+        QCOMPARE(model.rowCount(), 0);
+
+        OCC::Activity activity;
+        activity._folder = QStringLiteral("thingy");
+        activity._file = QStringLiteral("test.txt");
+
+        model.addIgnoredFileToList(activity);
+        // We need to add another activity to the model for the combineActivityLists method to be called
+        model.addNotificationToActivityList(testNotificationActivity);
+        QCOMPARE(model.rowCount(), 2);
+
+        const auto index = model.index(0, 0);
+        QVERIFY(index.isValid());
+    };
+
+    // Test removing activity from list
+    void testRemoveActivityWithRow() {
+        TestingALM model;
+        model.setAccountState(accountState.data());
+        QAbstractItemModelTester modelTester(&model);
+
+        QCOMPARE(model.rowCount(), 0);
+
+        model.addNotificationToActivityList(testNotificationActivity);
+        QCOMPARE(model.rowCount(), 1);
+
+        model.removeActivityFromActivityList(0);
+        QCOMPARE(model.rowCount(), 0);
+    }
+
+    void testRemoveActivityWithActivity() {
+        TestingALM model;
+        model.setAccountState(accountState.data());
+        QAbstractItemModelTester modelTester(&model);
+
+        QCOMPARE(model.rowCount(), 0);
+
+        model.addNotificationToActivityList(testNotificationActivity);
+        QCOMPARE(model.rowCount(), 1);
+
+        model.removeActivityFromActivityList(testNotificationActivity);
+        QCOMPARE(model.rowCount(), 0);
+    }
+
+    // Test getting the data from the model
+    void testData() {
+        TestingALM model;
+        model.setAccountState(accountState.data());
+        QAbstractItemModelTester modelTester(&model);
+
+        QCOMPARE(model.rowCount(), 0);
+
+        model.startFetchJob();
+        QSignalSpy activitiesJob(&model, &TestingALM::activityJobStatusCode);
+        QVERIFY(activitiesJob.wait(3000));
+        QCOMPARE(model.rowCount(), 50);
+
+        model.addNotificationToActivityList(testNotificationActivity);
+        QCOMPARE(model.rowCount(), 51);
+
+        OCC::Activity syncResultActivity;
+        syncResultActivity._id = 2;
+        syncResultActivity._type = OCC::Activity::SyncResultType;
+        syncResultActivity._status = OCC::SyncResult::Error;
+        syncResultActivity._dateTime = QDateTime::currentDateTime();
+        syncResultActivity._subject = QStringLiteral("Sample failed sync text");
+        syncResultActivity._message = QStringLiteral("/path/to/thingy");
+        syncResultActivity._link = QStringLiteral("/path/to/thingy");
+        syncResultActivity._accName = accountState->account()->displayName();
+        model.addSyncFileItemToActivityList(syncResultActivity);
+        QCOMPARE(model.rowCount(), 52);
+
+        OCC::Activity syncFileItemActivity;
+        syncFileItemActivity._id = 3;
+        syncFileItemActivity._type = OCC::Activity::SyncFileItemType; //client activity
+        syncFileItemActivity._status = OCC::SyncFileItem::Success;
+        syncFileItemActivity._dateTime = QDateTime::currentDateTime();
+        syncFileItemActivity._message = QStringLiteral("You created xyz.pdf");
+        syncFileItemActivity._link = accountState->account()->url();
+        syncFileItemActivity._accName = accountState->account()->displayName();
+        syncFileItemActivity._file = QStringLiteral("xyz.pdf");
+        syncFileItemActivity._fileAction = "";
+        model.addSyncFileItemToActivityList(syncFileItemActivity);
+        QCOMPARE(model.rowCount(), 53);
+
+        // Test all rows for things in common
+        for (int i = 0; i < model.rowCount(); i++) {
+            const auto index = model.index(i, 0);
+
+            QVERIFY(index.data(OCC::ActivityListModel::ObjectTypeRole).canConvert<int>());
+            const auto type = index.data(OCC::ActivityListModel::ObjectTypeRole).toInt();
+            QVERIFY(type >= OCC::Activity::ActivityType);
+
+            QVERIFY(!index.data(OCC::ActivityListModel::ObjectTypeRole).toInt());
+            QVERIFY(!index.data(OCC::ActivityListModel::AccountRole).toString().isEmpty());
+            QVERIFY(!index.data(OCC::ActivityListModel::ActionTextColorRole).toString().isEmpty());
+            QVERIFY(!index.data(OCC::ActivityListModel::ActionIconRole).toString().isEmpty());
+            QVERIFY(!index.data(OCC::ActivityListModel::PointInTimeRole).toString().isEmpty());
+
+            QVERIFY(index.data(OCC::ActivityListModel::ActionsLinksRole).canConvert<QList<QVariant>>());
+            QVERIFY(index.data(OCC::ActivityListModel::ActionTextRole).canConvert<QString>());
+            QVERIFY(index.data(OCC::ActivityListModel::MessageRole).canConvert<QString>());
+            QVERIFY(index.data(OCC::ActivityListModel::LinkRole).canConvert<QUrl>());
+            QVERIFY(index.data(OCC::ActivityListModel::AccountConnectedRole).canConvert<bool>());
+            QVERIFY(index.data(OCC::ActivityListModel::DisplayActions).canConvert<bool>());
+
+            // Unfortunately, trying to check anything relating to filepaths causes a crash
+            // when the folder manager is invoked by the model to look for the relevant file
+        }
+    };
+
+};
+
+QTEST_MAIN(TestActivityListModel)
+#include "testactivitylistmodel.moc"