From: Claudio Cambra Date: Thu, 23 Dec 2021 10:57:08 +0000 (+0100) Subject: Add testing for ActivityListModel X-Git-Tag: archive/raspbian/3.16.7-1_deb13u1+rpi1~1^2~12^2~17^2~170^2 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=ce5494b4ecf3ad094401d4796fdc75dfab7b1b12;p=nextcloud-desktop.git Add testing for ActivityListModel Signed-off-by: Claudio Cambra --- diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp index eacf36fd8..8120612a5 100644 --- a/src/gui/tray/activitylistmodel.cpp +++ b/src/gui/tray/activitylistmodel.cpp @@ -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; } + } + diff --git a/src/gui/tray/activitylistmodel.h b/src/gui/tray/activitylistmodel.h index 07385e768..66de2e498 100644 --- a/src/gui/tray/activitylistmodel.h +++ b/src/gui/tray/activitylistmodel.h @@ -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 roleNames() const override; - void setAccountState(AccountState *state); void setCurrentlyFetching(bool value); bool currentlyFetching() const; void setDoneFetching(bool value); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 987fb8c9f..011ad5580 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -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 index 000000000..8faf21d64 --- /dev/null +++ b/test/testactivitylistmodel.cpp @@ -0,0 +1,413 @@ +/* + * 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 "gui/tray/activitylistmodel.h" + +#include "account.h" +#include "accountstate.h" +#include "accountmanager.h" +#include "syncenginetestutils.h" +#include "syncresult.h" + +#include +#include +#include +#include + +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; + OCC::AccountPtr account; + QScopedPointer 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(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()); + 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>()); + QVERIFY(index.data(OCC::ActivityListModel::ActionTextRole).canConvert()); + QVERIFY(index.data(OCC::ActivityListModel::MessageRole).canConvert()); + QVERIFY(index.data(OCC::ActivityListModel::LinkRole).canConvert()); + QVERIFY(index.data(OCC::ActivityListModel::AccountConnectedRole).canConvert()); + QVERIFY(index.data(OCC::ActivityListModel::DisplayActions).canConvert()); + + // 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"