testshareemodel.cpp 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. /*
  2. * Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
  3. *
  4. * This program is free software; you can redistribute it and/or modify
  5. * it under the terms of the GNU General Public License as published by
  6. * the Free Software Foundation; either version 2 of the License, or
  7. * (at your option) any later version.
  8. *
  9. * This program is distributed in the hope that it will be useful, but
  10. * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  11. * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
  12. * for more details.
  13. */
  14. #include "gui/filedetails/shareemodel.h"
  15. #include <QTest>
  16. #include <QSignalSpy>
  17. #include "accountmanager.h"
  18. #include "syncenginetestutils.h"
  19. #include "testhelper.h"
  20. using namespace OCC;
  21. static QByteArray fake400Response = R"(
  22. {"ocs":{"meta":{"status":"failure","statuscode":400,"message":"Parameter is incorrect.\n"},"data":[]}}
  23. )";
  24. constexpr auto searchResultsReplyDelay = 100;
  25. class TestShareeModel : public QObject
  26. {
  27. Q_OBJECT
  28. int _numLookupSearchParamSet = 0;
  29. public:
  30. ~TestShareeModel() override
  31. {
  32. AccountManager::instance()->deleteAccount(_accountState.data());
  33. };
  34. struct FakeShareeDefinition
  35. {
  36. QString label;
  37. QString shareWith;
  38. Sharee::Type type;
  39. QString shareWithAdditionalInfo;
  40. };
  41. void appendShareeToReply(const FakeShareeDefinition &definition)
  42. {
  43. QJsonObject newShareeJson;
  44. newShareeJson.insert("label", definition.label);
  45. QJsonObject newShareeValueJson;
  46. newShareeValueJson.insert("shareWith", definition.shareWith);
  47. newShareeValueJson.insert("shareType", definition.type);
  48. newShareeValueJson.insert("shareWithAdditionalInfo", definition.shareWithAdditionalInfo);
  49. newShareeJson.insert("value", newShareeValueJson);
  50. QString category;
  51. switch(definition.type) {
  52. case Sharee::Invalid:
  53. category = QStringLiteral("invalid");
  54. break;
  55. case Sharee::Circle:
  56. category = QStringLiteral("circles");
  57. break;
  58. case Sharee::Email:
  59. category = QStringLiteral("emails");
  60. break;
  61. case Sharee::Federated:
  62. category = QStringLiteral("remotes");
  63. break;
  64. case Sharee::Group:
  65. category = QStringLiteral("groups");
  66. break;
  67. case Sharee::Room:
  68. category = QStringLiteral("rooms");
  69. break;
  70. case Sharee::User:
  71. category = QStringLiteral("users");
  72. break;
  73. case Sharee::LookupServerSearch:
  74. category = QStringLiteral("placeholder_lookupserversearch");
  75. break;
  76. case Sharee::LookupServerSearchResults:
  77. category = QStringLiteral("placeholder_lookupserversearchresults");
  78. break;
  79. }
  80. auto shareesInCategory = _shareesMap.value(category).toJsonArray();
  81. shareesInCategory.append(newShareeJson);
  82. _shareesMap.insert(category, shareesInCategory);
  83. }
  84. void standardReplyPopulate()
  85. {
  86. appendShareeToReply(_michaelUserDefinition);
  87. appendShareeToReply(_liamUserDefinition);
  88. appendShareeToReply(_iqbalUserDefinition);
  89. appendShareeToReply(_universityGroupDefinition);
  90. appendShareeToReply(_testEmailDefinition);
  91. }
  92. QVariantMap filteredSharees(const QString &searchString)
  93. {
  94. if (searchString.isEmpty()) {
  95. return _shareesMap;
  96. }
  97. QVariantMap returnSharees;
  98. QJsonArray exactMatches;
  99. for (auto it = _shareesMap.constKeyValueBegin(); it != _shareesMap.constKeyValueEnd(); ++it) {
  100. const auto shareesCategory = it->first;
  101. const auto shareesArray = it->second.toJsonArray();
  102. QJsonArray filteredShareesArray;
  103. std::copy_if(shareesArray.cbegin(), shareesArray.cend(), std::back_inserter(filteredShareesArray), [&searchString](const QJsonValue &shareeValue) {
  104. const auto shareeObject = shareeValue.toObject().value("value").toObject();
  105. const auto shareeShareWith = shareeObject.value("shareWith").toString();
  106. return shareeShareWith.contains(searchString, Qt::CaseInsensitive);
  107. });
  108. std::copy_if(filteredShareesArray.cbegin(), filteredShareesArray.cend(), std::back_inserter(exactMatches), [&searchString](const QJsonValue &shareeValue) {
  109. const auto shareeObject = shareeValue.toObject().value("value").toObject();
  110. const auto shareeShareWith = shareeObject.value("shareWith").toString();
  111. return shareeShareWith == searchString;
  112. });
  113. returnSharees.insert(shareesCategory, filteredShareesArray);
  114. }
  115. returnSharees.insert(QStringLiteral("exact"), exactMatches);
  116. return returnSharees;
  117. }
  118. QByteArray testShareesReply(const QString &searchString)
  119. {
  120. QJsonObject root;
  121. QJsonObject ocs;
  122. QJsonObject meta;
  123. meta.insert("statuscode", 200);
  124. const auto resultSharees = filteredSharees(searchString);
  125. const auto shareesJsonObject = QJsonObject::fromVariantMap(resultSharees);
  126. ocs.insert(QStringLiteral("data"), shareesJsonObject);
  127. ocs.insert(QStringLiteral("meta"), meta);
  128. root.insert(QStringLiteral("ocs"), ocs);
  129. return QJsonDocument(root).toJson();
  130. }
  131. int shareesCount(const QString &searchString)
  132. {
  133. const auto sharees = filteredSharees(searchString);
  134. auto count = 0;
  135. const auto shareesCategories = sharees.values();
  136. for (const auto &shareesArrayValue : shareesCategories) {
  137. const auto shareesArray = shareesArrayValue.toJsonArray();
  138. count += shareesArray.count();
  139. }
  140. return count;
  141. }
  142. void resetTestData()
  143. {
  144. _alwaysReturnErrors = false;
  145. _shareesMap.clear();
  146. }
  147. private:
  148. AccountPtr _account;
  149. AccountStatePtr _accountState;
  150. QScopedPointer<FakeQNAM> _fakeQnam;
  151. QVariantMap _shareesMap;
  152. // Some fake sharees of different categories
  153. // ALL OF THEM CONTAIN AN 'I' !! Important for testing
  154. FakeShareeDefinition _michaelUserDefinition {
  155. QStringLiteral("Michael"),
  156. QStringLiteral("michael"),
  157. Sharee::User,
  158. {},
  159. };
  160. FakeShareeDefinition _liamUserDefinition {
  161. QStringLiteral("Liam"),
  162. QStringLiteral("liam"),
  163. Sharee::User,
  164. {},
  165. };
  166. FakeShareeDefinition _iqbalUserDefinition {
  167. QStringLiteral("Iqbal"),
  168. QStringLiteral("iqbal"),
  169. Sharee::User,
  170. {},
  171. };
  172. FakeShareeDefinition _universityGroupDefinition {
  173. QStringLiteral("University"),
  174. QStringLiteral("university"),
  175. Sharee::Group,
  176. {},
  177. };
  178. FakeShareeDefinition _testEmailDefinition {
  179. QStringLiteral("test.email@nextcloud.com"),
  180. QStringLiteral("test.email@nextcloud.com"),
  181. Sharee::Email,
  182. {},
  183. };
  184. bool _alwaysReturnErrors = false;
  185. private slots:
  186. void initTestCase()
  187. {
  188. _fakeQnam.reset(new FakeQNAM({}));
  189. _fakeQnam->setOverride([this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) {
  190. Q_UNUSED(device);
  191. QNetworkReply *reply = nullptr;
  192. if (_alwaysReturnErrors) {
  193. reply = new FakeErrorReply(op, req, this, 400, fake400Response);
  194. return reply;
  195. }
  196. const auto reqUrl = req.url();
  197. const auto reqRawPath = reqUrl.path();
  198. const auto reqPath = reqRawPath.startsWith("/owncloud/") ? reqRawPath.mid(10) : reqRawPath;
  199. qDebug() << req.url() << reqPath << op;
  200. if(req.url().toString().startsWith(_accountState->account()->url().toString()) &&
  201. reqPath == QStringLiteral("ocs/v2.php/apps/files_sharing/api/v1/sharees") &&
  202. req.attribute(QNetworkRequest::CustomVerbAttribute) == "GET") {
  203. const auto urlQuery = QUrlQuery(req.url());
  204. const auto searchParam = urlQuery.queryItemValue(QStringLiteral("search"));
  205. const auto itemTypeParam = urlQuery.queryItemValue(QStringLiteral("itemType"));
  206. const auto pageParam = urlQuery.queryItemValue(QStringLiteral("page"));
  207. const auto perPageParam = urlQuery.queryItemValue(QStringLiteral("perPage"));
  208. const auto lookupParam = urlQuery.queryItemValue(QStringLiteral("lookup"));
  209. const auto formatParam = urlQuery.queryItemValue(QStringLiteral("format"));
  210. if (!lookupParam.isEmpty() && lookupParam == QStringLiteral("true")) {
  211. ++_numLookupSearchParamSet;
  212. }
  213. if (formatParam != QStringLiteral("json")) {
  214. reply = new FakeErrorReply(op, req, this, 400, fake400Response);
  215. } else {
  216. reply = new FakePayloadReply(op, req, testShareesReply(searchParam), searchResultsReplyDelay, _fakeQnam.data());
  217. }
  218. }
  219. return reply;
  220. });
  221. _account = Account::create();
  222. _account->setCredentials(new FakeCredentials{_fakeQnam.data()});
  223. _account->setUrl(QUrl(("owncloud://somehost/owncloud")));
  224. _accountState = new AccountState(_account);
  225. AccountManager::instance()->addAccount(_account);
  226. // Let's verify our test is working -- all sharees have an I in their "shareWith"
  227. standardReplyPopulate();
  228. const auto searchString = QStringLiteral("i");
  229. QCOMPARE(shareesCount(searchString), 5);
  230. const auto emailSearchString = QStringLiteral("email");
  231. QCOMPARE(shareesCount(emailSearchString), 1);
  232. }
  233. void testSetAccountAndPath()
  234. {
  235. resetTestData();
  236. ShareeModel model;
  237. QAbstractItemModelTester modelTester(&model);
  238. QCOMPARE(model.rowCount(), 0);
  239. QSignalSpy accountStateChanged(&model, &ShareeModel::accountStateChanged);
  240. QSignalSpy shareItemIsFolderChanged(&model, &ShareeModel::shareItemIsFolderChanged);
  241. QSignalSpy searchStringChanged(&model, &ShareeModel::searchStringChanged);
  242. QSignalSpy lookupModeChanged(&model, &ShareeModel::lookupModeChanged);
  243. QSignalSpy shareeBlocklistChanged(&model, &ShareeModel::shareeBlocklistChanged);
  244. model.setAccountState(_accountState.data());
  245. QCOMPARE(accountStateChanged.count(), 1);
  246. QCOMPARE(model.accountState(), _accountState.data());
  247. const auto shareItemIsFolder = !model.shareItemIsFolder();
  248. model.setShareItemIsFolder(shareItemIsFolder);
  249. QCOMPARE(shareItemIsFolderChanged.count(), 1);
  250. QCOMPARE(model.shareItemIsFolder(), shareItemIsFolder);
  251. const auto searchString = QStringLiteral("search string");
  252. model.setSearchString(searchString);
  253. QCOMPARE(searchStringChanged.count(), 1);
  254. QCOMPARE(model.searchString(), searchString);
  255. const auto lookupMode = ShareeModel::LookupMode::GlobalSearch;
  256. model.setLookupMode(lookupMode);
  257. QCOMPARE(lookupModeChanged.count(), 1);
  258. QCOMPARE(model.lookupMode(), lookupMode);
  259. const ShareePtr sharee(new Sharee(_testEmailDefinition.shareWith, _testEmailDefinition.label, _testEmailDefinition.type));
  260. const QVariantList shareeBlocklist {QVariant::fromValue(sharee)};
  261. model.setShareeBlocklist(shareeBlocklist);
  262. QCOMPARE(shareeBlocklistChanged.count(), 1);
  263. QCOMPARE(model.shareeBlocklist(), shareeBlocklist);
  264. }
  265. void testShareesFetch()
  266. {
  267. resetTestData();
  268. standardReplyPopulate();
  269. ShareeModel model;
  270. QAbstractItemModelTester modelTester(&model);
  271. QCOMPARE(model.rowCount(), 0);
  272. model.setAccountState(_accountState.data());
  273. QSignalSpy shareesReady(&model, &ShareeModel::shareesReady);
  274. const auto searchString = QStringLiteral("i");
  275. model.setSearchString(searchString);
  276. QVERIFY(shareesReady.wait(3000));
  277. QCOMPARE(model.rowCount(), shareesCount(searchString) + 1);
  278. QVERIFY(model.rowCount() > 0);
  279. auto lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
  280. QVERIFY(lastElementType == Sharee::Type::LookupServerSearch);
  281. const auto emailSearchString = QStringLiteral("email");
  282. model.setSearchString(emailSearchString);
  283. QVERIFY(shareesReady.wait(3000));
  284. QCOMPARE(model.rowCount(), shareesCount(emailSearchString) + 1);
  285. QVERIFY(model.rowCount() > 0);
  286. lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
  287. QVERIFY(lastElementType == Sharee::Type::LookupServerSearch);
  288. }
  289. void testShareesFetchGlobally()
  290. {
  291. resetTestData();
  292. standardReplyPopulate();
  293. ShareeModel model;
  294. QAbstractItemModelTester modelTester(&model);
  295. QCOMPARE(model.rowCount(), 0);
  296. model.setAccountState(_accountState.data());
  297. QSignalSpy shareesReady(&model, &ShareeModel::shareesReady);
  298. const auto emailSearchString = QStringLiteral("email");
  299. model.setSearchString(emailSearchString);
  300. QVERIFY(shareesReady.wait(3000));
  301. QCOMPARE(model.rowCount(), shareesCount(emailSearchString) + 1);
  302. QVERIFY(model.rowCount() > 0);
  303. auto lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
  304. QVERIFY(lastElementType == Sharee::Type::LookupServerSearch);
  305. QCOMPARE(_numLookupSearchParamSet, 0);
  306. QSignalSpy lookupModeChanged(&model, &ShareeModel::lookupModeChanged);
  307. model.searchGlobally();
  308. QVERIFY(shareesReady.wait(3000));
  309. QCOMPARE(lookupModeChanged.count(), 2);
  310. QVERIFY(model.lookupMode() == ShareeModel::LookupMode::LocalSearch);
  311. QCOMPARE(model.rowCount(), shareesCount(emailSearchString) + 1);
  312. QVERIFY(model.rowCount() > 0);
  313. lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
  314. QVERIFY(lastElementType == Sharee::Type::LookupServerSearchResults);
  315. QCOMPARE(_numLookupSearchParamSet, 1);
  316. }
  317. void testFetchSignalling()
  318. {
  319. resetTestData();
  320. standardReplyPopulate();
  321. ShareeModel model;
  322. QAbstractItemModelTester modelTester(&model);
  323. QCOMPARE(model.rowCount(), 0);
  324. model.setAccountState(_accountState.data());
  325. QSignalSpy fetchOngoingChanged(&model, &ShareeModel::fetchOngoingChanged);
  326. const auto searchString = QStringLiteral("i");
  327. model.setSearchString(searchString);
  328. QVERIFY(fetchOngoingChanged.wait(1000));
  329. QCOMPARE(model.fetchOngoing(), true);
  330. QVERIFY(fetchOngoingChanged.wait(3000));
  331. QCOMPARE(model.fetchOngoing(), false);
  332. }
  333. void testData()
  334. {
  335. resetTestData();
  336. appendShareeToReply(_testEmailDefinition);
  337. ShareeModel model;
  338. QAbstractItemModelTester modelTester(&model);
  339. QCOMPARE(model.rowCount(), 0);
  340. model.setAccountState(_accountState.data());
  341. const auto searchString = QStringLiteral("i");
  342. model.setSearchString(searchString);
  343. QSignalSpy shareesReady(&model, &ShareeModel::shareesReady);
  344. QVERIFY(shareesReady.wait(3000));
  345. QCOMPARE(model.rowCount(), shareesCount(searchString) + 1);
  346. auto lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
  347. QVERIFY(lastElementType == Sharee::Type::LookupServerSearch);
  348. const auto shareeIndex = model.index(0, 0, {});
  349. const ShareePtr expectedSharee(new Sharee(_testEmailDefinition.shareWith, _testEmailDefinition.label, _testEmailDefinition.type));
  350. const auto sharee = shareeIndex.data(ShareeModel::ShareeRole).value<ShareePtr>();
  351. QCOMPARE(sharee->format(), expectedSharee->format());
  352. QCOMPARE(sharee->shareWith(), expectedSharee->shareWith());
  353. QCOMPARE(sharee->displayName(), expectedSharee->displayName());
  354. QCOMPARE(sharee->type(), expectedSharee->type());
  355. const auto expectedShareeDisplay = QString(_testEmailDefinition.label + QStringLiteral(" (email)"));
  356. const auto shareeDisplay = shareeIndex.data(Qt::DisplayRole).toString();
  357. QCOMPARE(shareeDisplay, expectedShareeDisplay);
  358. const auto expectedAutoCompleterStringMatch = QString(_testEmailDefinition.label +
  359. QStringLiteral(" (") +
  360. _testEmailDefinition.shareWith +
  361. QStringLiteral(")"));
  362. const auto autoCompleterStringMatch = shareeIndex.data(ShareeModel::AutoCompleterStringMatchRole).toString();
  363. QCOMPARE(autoCompleterStringMatch, expectedAutoCompleterStringMatch);
  364. }
  365. void testBlocklist()
  366. {
  367. resetTestData();
  368. standardReplyPopulate();
  369. ShareeModel model;
  370. QAbstractItemModelTester modelTester(&model);
  371. QCOMPARE(model.rowCount(), 0);
  372. model.setAccountState(_accountState.data());
  373. const ShareePtr sharee(new Sharee(_testEmailDefinition.shareWith, _testEmailDefinition.label, _testEmailDefinition.type));
  374. const QVariantList shareeBlocklist {QVariant::fromValue(sharee)};
  375. model.setShareeBlocklist(shareeBlocklist);
  376. QSignalSpy shareesReady(&model, &ShareeModel::shareesReady);
  377. const auto searchString = QStringLiteral("i");
  378. model.setSearchString(searchString);
  379. QVERIFY(shareesReady.wait(3000));
  380. QCOMPARE(model.rowCount(), shareesCount(searchString) - 1 + 1);
  381. auto lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
  382. QVERIFY(lastElementType == Sharee::Type::LookupServerSearch);
  383. const ShareePtr shareeTwo(new Sharee(_michaelUserDefinition.shareWith, _michaelUserDefinition.label, _michaelUserDefinition.type));
  384. const QVariantList largerShareeBlocklist {QVariant::fromValue(sharee), QVariant::fromValue(shareeTwo)};
  385. model.setShareeBlocklist(largerShareeBlocklist);
  386. QCOMPARE(model.rowCount(), shareesCount(searchString) - 2 + 1);
  387. lastElementType = model.data(model.index(model.rowCount() - 1), ShareeModel::Roles::TypeRole).toInt();
  388. QVERIFY(lastElementType == Sharee::Type::LookupServerSearch);
  389. }
  390. void testServerError()
  391. {
  392. resetTestData();
  393. _alwaysReturnErrors = true;
  394. ShareeModel model;
  395. QAbstractItemModelTester modelTester(&model);
  396. QCOMPARE(model.rowCount(), 0);
  397. model.setAccountState(_accountState.data());
  398. QSignalSpy displayErrorMessage(&model, &ShareeModel::displayErrorMessage);
  399. QSignalSpy fetchOngoingChanged(&model, &ShareeModel::fetchOngoingChanged);
  400. model.setSearchString(QStringLiteral("i"));
  401. QVERIFY(displayErrorMessage.wait(3000));
  402. QCOMPARE(fetchOngoingChanged.count(), 2);
  403. QCOMPARE(model.fetchOngoing(), false);
  404. }
  405. };
  406. QTEST_MAIN(TestShareeModel)
  407. #include "testshareemodel.moc"