| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640 |
- /*
- * Copyright (C) by Oleksandr Zolotov <alex@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/unifiedsearchresultslistmodel.h"
- #include "account.h"
- #include "accountstate.h"
- #include "syncenginetestutils.h"
- #include <QAbstractItemModelTester>
- #include <QDesktopServices>
- #include <QSignalSpy>
- #include <QTest>
- namespace {
- /**
- * @brief The FakeDesktopServicesUrlHandler
- * overrides QDesktopServices::openUrl
- **/
- class FakeDesktopServicesUrlHandler : public QObject
- {
- Q_OBJECT
- public:
- FakeDesktopServicesUrlHandler(QObject *parent = nullptr)
- : QObject(parent)
- {}
- public:
- signals:
- void resultClicked(const QUrl &url);
- };
- /**
- * @brief The FakeProvider
- * is a simple structure that represents initial list of providers and their properties
- **/
- class FakeProvider
- {
- public:
- QString _id;
- QString _name;
- qint32 _order = std::numeric_limits<qint32>::max();
- quint32 _numItemsToInsert = 5; // how many fake resuls to insert
- };
- // this will be used when initializing fake search results data for each provider
- static const QVector<FakeProvider> fakeProvidersInitInfo = {
- {QStringLiteral("settings_apps"), QStringLiteral("Apps"), -50, 10},
- {QStringLiteral("talk-message"), QStringLiteral("Messages"), -2, 17},
- {QStringLiteral("files"), QStringLiteral("Files"), 5, 3},
- {QStringLiteral("deck"), QStringLiteral("Deck"), 10, 5},
- {QStringLiteral("comments"), QStringLiteral("Comments"), 10, 2},
- {QStringLiteral("mail"), QStringLiteral("Mails"), 10, 15},
- {QStringLiteral("calendar"), QStringLiteral("Events"), 30, 11}
- };
- 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":[]}}
- )";
- /**
- * @brief The FakeSearchResultsStorage
- * emulates the real server storage that contains all the results that UnifiedSearchListmodel will search for
- **/
- class FakeSearchResultsStorage
- {
- class Provider
- {
- public:
- class SearchResult
- {
- public:
- QString _thumbnailUrl;
- QString _title;
- QString _subline;
- QString _resourceUrl;
- QString _icon;
- bool _rounded;
- };
- QString _id;
- QString _name;
- qint32 _order = std::numeric_limits<qint32>::max();
- qint32 _cursor = 0;
- bool _isPaginated = false;
- QVector<SearchResult> _results;
- };
- FakeSearchResultsStorage() = default;
- public:
- static FakeSearchResultsStorage *instance()
- {
- if (!_instance) {
- _instance = new FakeSearchResultsStorage();
- _instance->init();
- }
- return _instance;
- };
- static void destroy()
- {
- if (_instance) {
- delete _instance;
- }
- _instance = nullptr;
- }
- void init()
- {
- if (!_searchResultsData.isEmpty()) {
- return;
- }
- _metaSuccess = {{QStringLiteral("status"), QStringLiteral("ok")}, {QStringLiteral("statuscode"), 200},
- {QStringLiteral("message"), QStringLiteral("OK")}};
- initProvidersResponse();
- initSearchResultsData();
- }
- // initialize the JSON response containing the fake list of providers and their properties
- void initProvidersResponse()
- {
- QList<QVariant> providersList;
- for (const auto &fakeProviderInitInfo : fakeProvidersInitInfo) {
- providersList.push_back(QVariantMap{
- {QStringLiteral("id"), fakeProviderInitInfo._id},
- {QStringLiteral("name"), fakeProviderInitInfo._name},
- {QStringLiteral("order"), fakeProviderInitInfo._order},
- });
- }
- const QVariantMap ocsMap = {
- {QStringLiteral("meta"), _metaSuccess},
- {QStringLiteral("data"), providersList}
- };
- _providersResponse =
- QJsonDocument::fromVariant(QVariantMap{{QStringLiteral("ocs"), ocsMap}}).toJson(QJsonDocument::Compact);
- }
- // init the map of fake search results for each provider
- void initSearchResultsData()
- {
- for (const auto &fakeProvider : fakeProvidersInitInfo) {
- auto &providerData = _searchResultsData[fakeProvider._id];
- providerData._id = fakeProvider._id;
- providerData._name = fakeProvider._name;
- providerData._order = fakeProvider._order;
- if (fakeProvider._numItemsToInsert > pageSize) {
- providerData._isPaginated = true;
- }
- for (quint32 i = 0; i < fakeProvider._numItemsToInsert; ++i) {
- providerData._results.push_back(
- {"http://example.de/avatar/john/64", QString(QStringLiteral("John Doe in ") + fakeProvider._name),
- QString(QStringLiteral("We a discussion about ") + fakeProvider._name
- + QStringLiteral(" already. But, let's have a follow up tomorrow afternoon.")),
- "http://example.de/call/abcde12345#message_12345", QStringLiteral("icon-talk"), true});
- }
- }
- }
- const QList<QVariant> resultsForProvider(const QString &providerId, int cursor)
- {
- QList<QVariant> list;
- const auto results = resultsForProviderAsVector(providerId, cursor);
- if (results.isEmpty()) {
- return list;
- }
- for (const auto &result : results) {
- list.push_back(QVariantMap{
- {"thumbnailUrl", result._thumbnailUrl},
- {"title", result._title},
- {"subline", result._subline},
- {"resourceUrl", result._resourceUrl},
- {"icon", result._icon},
- {"rounded", result._rounded}
- });
- }
- return list;
- }
- const QVector<Provider::SearchResult> resultsForProviderAsVector(const QString &providerId, int cursor)
- {
- QVector<Provider::SearchResult> results;
- const auto provider = _searchResultsData.value(providerId, Provider());
- if (provider._id.isEmpty() || cursor > provider._results.size()) {
- return results;
- }
- const int n = cursor + pageSize > provider._results.size()
- ? 0
- : cursor + pageSize;
- for (int i = cursor; i < n; ++i) {
- results.push_back(provider._results[i]);
- }
- return results;
- }
- const QByteArray queryProvider(const QString &providerId, const QString &searchTerm, int cursor)
- {
- if (!_searchResultsData.contains(providerId)) {
- return fake404Response;
- }
- if (searchTerm == QStringLiteral("[HTTP500]")) {
- return fake500Response;
- }
- if (searchTerm == QStringLiteral("[empty]")) {
- const QVariantMap dataMap = {{QStringLiteral("name"), _searchResultsData[providerId]._name},
- {QStringLiteral("isPaginated"), false}, {QStringLiteral("cursor"), 0},
- {QStringLiteral("entries"), QVariantList{}}};
- const QVariantMap ocsMap = {{QStringLiteral("meta"), _metaSuccess}, {QStringLiteral("data"), dataMap}};
- return QJsonDocument::fromVariant(QVariantMap{{QStringLiteral("ocs"), ocsMap}})
- .toJson(QJsonDocument::Compact);
- }
- const auto provider = _searchResultsData.value(providerId, Provider());
- const auto nextCursor = cursor + pageSize;
- const QVariantMap dataMap = {{QStringLiteral("name"), _searchResultsData[providerId]._name},
- {QStringLiteral("isPaginated"), _searchResultsData[providerId]._isPaginated},
- {QStringLiteral("cursor"), nextCursor},
- {QStringLiteral("entries"), resultsForProvider(providerId, cursor)}};
- const QVariantMap ocsMap = {{QStringLiteral("meta"), _metaSuccess}, {QStringLiteral("data"), dataMap}};
- return QJsonDocument::fromVariant(QVariantMap{{QStringLiteral("ocs"), ocsMap}}).toJson(QJsonDocument::Compact);
- }
- const QByteArray &fakeProvidersResponseJson() const { return _providersResponse; }
- private:
- static FakeSearchResultsStorage *_instance;
- static const int pageSize = 5;
- QMap<QString, Provider> _searchResultsData;
- QByteArray _providersResponse = fake404Response;
- QVariantMap _metaSuccess;
- };
- FakeSearchResultsStorage *FakeSearchResultsStorage::_instance = nullptr;
- }
- class TestUnifiedSearchListmodel : public QObject
- {
- Q_OBJECT
- public:
- TestUnifiedSearchListmodel() = default;
- QScopedPointer<FakeQNAM> fakeQnam;
- OCC::AccountPtr account;
- QScopedPointer<OCC::AccountState> accountState;
- QScopedPointer<OCC::UnifiedSearchResultsListModel> model;
- QScopedPointer<QAbstractItemModelTester> modelTester;
- QScopedPointer<FakeDesktopServicesUrlHandler> fakeDesktopServicesUrlHandler;
- static const 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 cursor = urlQuery.queryItemValue(QStringLiteral("cursor")).toInt();
- const auto searchTerm = urlQuery.queryItemValue(QStringLiteral("term"));
- 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);
- }
- // handle fetch of providers list
- if (path.startsWith(QStringLiteral("/ocs/v2.php/search/providers")) && searchTerm.isEmpty()) {
- reply = new FakePayloadReply(op, req,
- FakeSearchResultsStorage::instance()->fakeProvidersResponseJson(), fakeQnam.data());
- // handle search for provider
- } else if (path.startsWith(QStringLiteral("/ocs/v2.php/search/providers")) && !searchTerm.isEmpty()) {
- const auto pathSplit = path.mid(QString(QStringLiteral("/ocs/v2.php/search/providers")).size())
- .split(QLatin1Char('/'), Qt::SkipEmptyParts);
- if (!pathSplit.isEmpty() && path.contains(pathSplit.first())) {
- reply = new FakePayloadReply(op, req,
- FakeSearchResultsStorage::instance()->queryProvider(pathSplit.first(), searchTerm, cursor),
- searchResultsReplyDelay, fakeQnam.data());
- }
- }
- if (!reply) {
- return qobject_cast<QNetworkReply*>(new FakeErrorReply(op, req, this, 404, QByteArrayLiteral("{error: \"Not found!\"}")));
- }
- return reply;
- });
- model.reset(new OCC::UnifiedSearchResultsListModel(accountState.data()));
- modelTester.reset(new QAbstractItemModelTester(model.data()));
- fakeDesktopServicesUrlHandler.reset(new FakeDesktopServicesUrlHandler);
- }
- void testSetSearchTermStartStopSearch()
- {
- // make sure the model is empty
- model->setSearchTerm(QStringLiteral(""));
- QVERIFY(model->rowCount() == 0);
- // #1 test setSearchTerm actually sets the search term and the signal is emitted
- QSignalSpy searhTermChanged(model.data(), &OCC::UnifiedSearchResultsListModel::searchTermChanged);
- model->setSearchTerm(QStringLiteral("dis"));
- QCOMPARE(searhTermChanged.count(), 1);
- QCOMPARE(model->searchTerm(), QStringLiteral("dis"));
- // #2 test setSearchTerm actually sets the search term and the signal is emitted
- searhTermChanged.clear();
- model->setSearchTerm(model->searchTerm() + QStringLiteral("cuss"));
- QCOMPARE(model->searchTerm(), QStringLiteral("discuss"));
- QCOMPARE(searhTermChanged.count(), 1);
- // #3 test that model has not started search yet
- QVERIFY(!model->isSearchInProgress());
-
- // #4 test that model has started the search after specific delay
- QSignalSpy searchInProgressChanged(model.data(), &OCC::UnifiedSearchResultsListModel::isSearchInProgressChanged);
- // allow search jobs to get created within the model
- QVERIFY(searchInProgressChanged.wait());
- QCOMPARE(searchInProgressChanged.count(), 1);
- QVERIFY(model->isSearchInProgress());
- // #5 test that model has stopped the search after setting empty search term
- model->setSearchTerm(QStringLiteral(""));
- QVERIFY(!model->isSearchInProgress());
- }
- void testSetSearchTermResultsFound()
- {
- // make sure the model is empty
- model->setSearchTerm(QStringLiteral(""));
- QVERIFY(model->rowCount() == 0);
- // test that search term gets set, search gets started and enough results get returned
- model->setSearchTerm(model->searchTerm() + QStringLiteral("discuss"));
- QSignalSpy searchInProgressChanged(
- model.data(), &OCC::UnifiedSearchResultsListModel::isSearchInProgressChanged);
- QVERIFY(searchInProgressChanged.wait());
- // make sure search has started
- QCOMPARE(searchInProgressChanged.count(), 1);
- QVERIFY(model->isSearchInProgress());
- QVERIFY(searchInProgressChanged.wait());
- // make sure search has finished
- QVERIFY(!model->isSearchInProgress());
- QVERIFY(model->rowCount() > 0);
- }
- void testSetSearchTermResultsNotFound()
- {
- // make sure the model is empty
- model->setSearchTerm(QStringLiteral(""));
- QVERIFY(model->rowCount() == 0);
- // test that search term gets set, search gets started and enough results get returned
- model->setSearchTerm(model->searchTerm() + QStringLiteral("[empty]"));
- QSignalSpy searchInProgressChanged(
- model.data(), &OCC::UnifiedSearchResultsListModel::isSearchInProgressChanged);
- QVERIFY(searchInProgressChanged.wait());
- // make sure search has started
- QCOMPARE(searchInProgressChanged.count(), 1);
- QVERIFY(model->isSearchInProgress());
- QVERIFY(searchInProgressChanged.wait());
- // make sure search has finished
- QVERIFY(!model->isSearchInProgress());
- QVERIFY(model->rowCount() == 0);
- }
- void testFetchMoreClicked()
- {
- // make sure the model is empty
- model->setSearchTerm(QStringLiteral(""));
- QVERIFY(model->rowCount() == 0);
- QSignalSpy searchInProgressChanged(
- model.data(), &OCC::UnifiedSearchResultsListModel::isSearchInProgressChanged);
- // test that search term gets set, search gets started and enough results get returned
- model->setSearchTerm(model->searchTerm() + QStringLiteral("whatever"));
- QVERIFY(searchInProgressChanged.wait());
- // make sure search has started
- QVERIFY(model->isSearchInProgress());
- QVERIFY(searchInProgressChanged.wait());
- // make sure search has finished
- QVERIFY(!model->isSearchInProgress());
- const auto numRowsInModelPrev = model->rowCount();
- // test fetch more results
- QSignalSpy currentFetchMoreInProgressProviderIdChanged(
- model.data(), &OCC::UnifiedSearchResultsListModel::currentFetchMoreInProgressProviderIdChanged);
- QSignalSpy rowsInserted(model.data(), &OCC::UnifiedSearchResultsListModel::rowsInserted);
- for (int i = 0; i < model->rowCount(); ++i) {
- const auto type = model->data(model->index(i), OCC::UnifiedSearchResultsListModel::DataRole::TypeRole);
- if (type == OCC::UnifiedSearchResult::Type::FetchMoreTrigger) {
- const auto providerId =
- model->data(model->index(i), OCC::UnifiedSearchResultsListModel::DataRole::ProviderIdRole)
- .toString();
- model->fetchMoreTriggerClicked(providerId);
- break;
- }
- }
- // make sure the currentFetchMoreInProgressProviderId was set back and forth and correct number fows has been inserted
- QCOMPARE(currentFetchMoreInProgressProviderIdChanged.count(), 1);
- const auto providerIdFetchMoreTriggered = model->currentFetchMoreInProgressProviderId();
- QVERIFY(!providerIdFetchMoreTriggered.isEmpty());
- QVERIFY(currentFetchMoreInProgressProviderIdChanged.wait());
- QVERIFY(model->currentFetchMoreInProgressProviderId().isEmpty());
- QCOMPARE(rowsInserted.count(), 1);
- const auto arguments = rowsInserted.takeFirst();
- QVERIFY(arguments.size() > 0);
- const auto first = arguments.at(0).toInt();
- const auto last = arguments.at(1).toInt();
- const int numInsertedExpected = last - first;
- QCOMPARE(model->rowCount() - numRowsInModelPrev, numInsertedExpected);
- // make sure the FetchMoreTrigger gets removed when no more results available
- if (!providerIdFetchMoreTriggered.isEmpty()) {
- currentFetchMoreInProgressProviderIdChanged.clear();
- rowsInserted.clear();
- QSignalSpy rowsRemoved(model.data(), &OCC::UnifiedSearchResultsListModel::rowsRemoved);
- for (int i = 0; i < 10; ++i) {
- model->fetchMoreTriggerClicked(providerIdFetchMoreTriggered);
- QVERIFY(currentFetchMoreInProgressProviderIdChanged.wait());
- if (rowsRemoved.count() > 0) {
- break;
- }
- }
-
- QCOMPARE(rowsRemoved.count(), 1);
- bool isFetchMoreTriggerFound = false;
- for (int i = 0; i < model->rowCount(); ++i) {
- const auto type = model->data(model->index(i), OCC::UnifiedSearchResultsListModel::DataRole::TypeRole);
- const auto providerId = model->data(model->index(i), OCC::UnifiedSearchResultsListModel::DataRole::ProviderIdRole)
- .toString();
- if (type == OCC::UnifiedSearchResult::Type::FetchMoreTrigger
- && providerId == providerIdFetchMoreTriggered) {
- isFetchMoreTriggerFound = true;
- break;
- }
- }
- QVERIFY(!isFetchMoreTriggerFound);
- }
- }
- void testSearchResultlicked()
- {
- // make sure the model is empty
- model->setSearchTerm(QStringLiteral(""));
- QVERIFY(model->rowCount() == 0);
- // test that search term gets set, search gets started and enough results get returned
- model->setSearchTerm(model->searchTerm() + QStringLiteral("discuss"));
- QSignalSpy searchInProgressChanged(
- model.data(), &OCC::UnifiedSearchResultsListModel::isSearchInProgressChanged);
- QVERIFY(searchInProgressChanged.wait());
- // make sure search has started
- QCOMPARE(searchInProgressChanged.count(), 1);
- QVERIFY(model->isSearchInProgress());
- QVERIFY(searchInProgressChanged.wait());
- // make sure search has finished and some results has been received
- QVERIFY(!model->isSearchInProgress());
- QVERIFY(model->rowCount() != 0);
- QDesktopServices::setUrlHandler("http", fakeDesktopServicesUrlHandler.data(), "resultClicked");
- QDesktopServices::setUrlHandler("https", fakeDesktopServicesUrlHandler.data(), "resultClicked");
- QSignalSpy resultClicked(fakeDesktopServicesUrlHandler.data(), &FakeDesktopServicesUrlHandler::resultClicked);
-
- // test click on a result item
- QString urlForClickedResult;
- for (int i = 0; i < model->rowCount(); ++i) {
- const auto type = model->data(model->index(i), OCC::UnifiedSearchResultsListModel::DataRole::TypeRole);
- if (type == OCC::UnifiedSearchResult::Type::Default) {
- const auto providerId =
- model->data(model->index(i), OCC::UnifiedSearchResultsListModel::DataRole::ProviderIdRole)
- .toString();
- urlForClickedResult = model->data(model->index(i), OCC::UnifiedSearchResultsListModel::DataRole::ResourceUrlRole).toString();
- if (!providerId.isEmpty() && !urlForClickedResult.isEmpty()) {
- model->resultClicked(providerId, QUrl(urlForClickedResult));
- break;
- }
- }
- }
- QCOMPARE(resultClicked.count(), 1);
- const auto arguments = resultClicked.takeFirst();
- const auto urlOpenTriggeredViaDesktopServices = arguments.at(0).toString();
- QCOMPARE(urlOpenTriggeredViaDesktopServices, urlForClickedResult);
- }
- void testSetSearchTermResultsError()
- {
- // make sure the model is empty
- model->setSearchTerm(QStringLiteral(""));
- QVERIFY(model->rowCount() == 0);
- QSignalSpy errorStringChanged(model.data(), &OCC::UnifiedSearchResultsListModel::errorStringChanged);
- QSignalSpy searchInProgressChanged(
- model.data(), &OCC::UnifiedSearchResultsListModel::isSearchInProgressChanged);
- model->setSearchTerm(model->searchTerm() + QStringLiteral("[HTTP500]"));
- QVERIFY(searchInProgressChanged.wait());
- // make sure search has started
- QVERIFY(model->isSearchInProgress());
- QVERIFY(searchInProgressChanged.wait());
- // make sure search has finished
- QVERIFY(!model->isSearchInProgress());
- // make sure the model is empty and an error string has been set
- QVERIFY(model->rowCount() == 0);
- QVERIFY(errorStringChanged.count() > 0);
- QVERIFY(!model->errorString().isEmpty());
- }
- void cleanupTestCase()
- {
- FakeSearchResultsStorage::destroy();
- }
- };
- QTEST_MAIN(TestUnifiedSearchListmodel)
- #include "testunifiedsearchlistmodel.moc"
|