sharetestutils.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  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 "sharetestutils.h"
  15. #include "testhelper.h"
  16. using namespace OCC;
  17. FakeShareDefinition::FakeShareDefinition(ShareTestHelper *helper,
  18. const Share::ShareType type,
  19. const QString &shareWith,
  20. const QString &displayString,
  21. const QString &password,
  22. const QString &note,
  23. const QString &expiration)
  24. {
  25. ++helper->latestShareId;
  26. const auto idString = QString::number(helper->latestShareId);
  27. fileDefinition = helper->fakeFileDefinition;
  28. shareId = idString;
  29. shareCanDelete = true;
  30. shareCanEdit = true;
  31. shareUidOwner = helper->account->davUser();;
  32. shareDisplayNameOwner = helper->account->davDisplayName();
  33. sharePassword = password;
  34. sharePermissions = static_cast<int>(SharePermissions(SharePermissionRead |
  35. SharePermissionUpdate |
  36. SharePermissionCreate |
  37. SharePermissionDelete |
  38. SharePermissionShare));
  39. shareNote = note;
  40. shareHideDownload = 0;
  41. shareExpiration = expiration;
  42. shareSendPasswordByTalk = false;
  43. shareType = type;
  44. const auto token = QString(QStringLiteral("GQ4aLrZEdJJkopW-") + idString);
  45. // Weird, but it's what the server does
  46. const auto finalShareWith = type == Share::TypeLink ? password : shareWith;
  47. const auto shareWithDisplayName = type == Share::TypeLink ? QStringLiteral("(Shared Link)") : displayString;
  48. const auto linkLabel = type == Share::TypeLink ? displayString : QString();
  49. const auto linkName = linkShareLabel;
  50. const auto linkUrl = type == Share::TypeLink ? QString(helper->account->davUrl().toString() + QStringLiteral("/s/") + token) : QString();
  51. shareShareWith = finalShareWith;
  52. shareShareWithDisplayName = shareWithDisplayName;
  53. shareToken = token;
  54. linkShareName = linkName;
  55. linkShareLabel = linkLabel;
  56. linkShareUrl = linkUrl;
  57. }
  58. QJsonObject FakeShareDefinition::toShareJsonObject() const
  59. {
  60. QJsonObject newShareJson;
  61. newShareJson.insert("uid_file_owner", fileDefinition.fileOwnerUid);
  62. newShareJson.insert("displayname_file_owner", fileDefinition.fileOwnerDisplayName);
  63. newShareJson.insert("file_target", fileDefinition.fileTarget);
  64. newShareJson.insert("has_preview", fileDefinition.fileHasPreview);
  65. newShareJson.insert("file_parent", fileDefinition.fileFileParent);
  66. newShareJson.insert("file_source", fileDefinition.fileSource);
  67. newShareJson.insert("item_source", fileDefinition.fileItemSource);
  68. newShareJson.insert("item_type", fileDefinition.fileItemType);
  69. newShareJson.insert("mail_send", fileDefinition.fileMailSend);
  70. newShareJson.insert("mimetype", fileDefinition.fileMimeType);
  71. newShareJson.insert("parent", fileDefinition.fileParent);
  72. newShareJson.insert("path", fileDefinition.filePath);
  73. newShareJson.insert("storage", fileDefinition.fileStorage);
  74. newShareJson.insert("storage_id", fileDefinition.fileStorageId);
  75. newShareJson.insert("id", shareId);
  76. newShareJson.insert("can_delete", shareCanDelete);
  77. newShareJson.insert("can_edit", shareCanEdit);
  78. newShareJson.insert("uid_owner", shareUidOwner);
  79. newShareJson.insert("displayname_owner", shareDisplayNameOwner);
  80. newShareJson.insert("password", sharePassword);
  81. newShareJson.insert("permissions", sharePermissions);
  82. newShareJson.insert("note", shareNote);
  83. newShareJson.insert("hide_download", shareHideDownload);
  84. newShareJson.insert("expiration", shareExpiration);
  85. newShareJson.insert("send_password_by_talk", shareSendPasswordByTalk);
  86. newShareJson.insert("share_type", shareType);
  87. newShareJson.insert("share_with", shareShareWith);
  88. newShareJson.insert("share_with_displayname", shareShareWithDisplayName);
  89. newShareJson.insert("token", shareToken);
  90. newShareJson.insert("name", linkShareName);
  91. newShareJson.insert("label", linkShareLabel);
  92. newShareJson.insert("url", linkShareUrl);
  93. return newShareJson;
  94. }
  95. QByteArray FakeShareDefinition::toRequestReply() const
  96. {
  97. const auto shareJson = toShareJsonObject();
  98. return jsonValueToOccReply(shareJson);
  99. }
  100. // Below is ShareTestHelper
  101. ShareTestHelper::ShareTestHelper(QObject *parent)
  102. : QObject(parent)
  103. {
  104. }
  105. ShareTestHelper::~ShareTestHelper()
  106. {
  107. const auto folder = FolderMan::instance()->folder(fakeFolder.localPath());
  108. if (folder) {
  109. FolderMan::instance()->removeFolder(folder);
  110. }
  111. AccountManager::instance()->deleteAccount(accountState.data());
  112. }
  113. void ShareTestHelper::setup()
  114. {
  115. _fakeQnam.reset(new FakeQNAM({}));
  116. _fakeQnam->setOverride([this](const QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) {
  117. return qnamOverride(op, req, device);
  118. });
  119. account = Account::create();
  120. account->setCredentials(new FakeCredentials{_fakeQnam.data()});
  121. account->setUrl(QUrl(("owncloud://somehost/owncloud")));
  122. account->setCapabilities(_fakeCapabilities);
  123. accountState = new AccountState(account);
  124. AccountManager::instance()->addAccount(account);
  125. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  126. fakeFolder.localModifier().insert(testFileName);
  127. const auto folderMan = FolderMan::instance();
  128. QCOMPARE(folderMan, &fm);
  129. auto folderDef = folderDefinition(fakeFolder.localPath());
  130. folderDef.targetPath = QString();
  131. QVERIFY(folderMan->addFolder(accountState.data(), folderDef));
  132. const auto folder = FolderMan::instance()->folder(fakeFolder.localPath());
  133. QVERIFY(folder);
  134. QVERIFY(fakeFolder.syncOnce());
  135. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  136. const auto fakeFileInfo = fakeFolder.remoteModifier().find(testFileName);
  137. QVERIFY(fakeFileInfo);
  138. fakeFileInfo->permissions.setPermission(RemotePermissions::CanReshare);
  139. QVERIFY(fakeFolder.syncOnce());
  140. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  141. QVERIFY(fakeFileInfo->permissions.CanReshare);
  142. _fakeCapabilities = QVariantMap {
  143. {QStringLiteral("files_sharing"), QVariantMap {
  144. {QStringLiteral("api_enabled"), true},
  145. {QStringLiteral("default_permissions"), 19},
  146. {QStringLiteral("public"), QVariantMap {
  147. {QStringLiteral("enabled"), true},
  148. {QStringLiteral("expire_date"), QVariantMap {
  149. {QStringLiteral("days"), 30},
  150. {QStringLiteral("enforced"), false},
  151. }},
  152. {QStringLiteral("expire_date_internal"), QVariantMap {
  153. {QStringLiteral("days"), 30},
  154. {QStringLiteral("enforced"), false},
  155. }},
  156. {QStringLiteral("expire_date_remote"), QVariantMap {
  157. {QStringLiteral("days"), 30},
  158. {QStringLiteral("enforced"), false},
  159. }},
  160. {QStringLiteral("password"), QVariantMap {
  161. {QStringLiteral("enforced"), false},
  162. }},
  163. }},
  164. {QStringLiteral("sharebymail"), QVariantMap {
  165. {QStringLiteral("enabled"), true},
  166. {QStringLiteral("password"), QVariantMap {
  167. {QStringLiteral("enforced"), false},
  168. }},
  169. }},
  170. }},
  171. };
  172. // Generate test data
  173. // Properties that apply to the file generally
  174. const auto fileOwnerUid = account->davUser();
  175. const auto fileOwnerDisplayName = account->davDisplayName();
  176. const auto fileTarget = QString(QStringLiteral("/") + fakeFileInfo->name);
  177. const auto fileHasPreview = true;
  178. const auto fileFileParent = QString(fakeFolder.remoteModifier().fileId);
  179. const auto fileSource = QString(fakeFileInfo->fileId);
  180. const auto fileItemSource = fileSource;
  181. const auto fileItemType = QStringLiteral("file");
  182. const auto fileMailSend = 0;
  183. const auto fileMimeType = QStringLiteral("text/markdown");
  184. const auto fileParent = QString();
  185. const auto filePath = fakeFileInfo->path();
  186. const auto fileStorage = 3;
  187. const auto fileStorageId = QString(QStringLiteral("home::") + account->davUser());
  188. fakeFileDefinition = FakeFileReplyDefinition {
  189. fileOwnerUid,
  190. fileOwnerDisplayName,
  191. fileTarget,
  192. fileHasPreview,
  193. fileFileParent,
  194. fileSource,
  195. fileItemSource,
  196. fileItemType,
  197. fileMailSend,
  198. fileMimeType,
  199. fileParent,
  200. filePath,
  201. fileStorage,
  202. fileStorageId,
  203. };
  204. emit setupSucceeded();
  205. }
  206. QNetworkReply *ShareTestHelper::qnamOverride(QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device)
  207. {
  208. QNetworkReply *reply = nullptr;
  209. const auto reqUrl = req.url();
  210. const auto reqRawPath = reqUrl.path();
  211. const auto reqPath = reqRawPath.startsWith("/owncloud/") ? reqRawPath.mid(10) : reqRawPath;
  212. qDebug() << req.url() << reqPath << op;
  213. // Properly formatted PROPFIND URL goes something like:
  214. // https://cloud.nextcloud.com/remote.php/dav/files/claudio/Readme.md
  215. if(reqPath.endsWith(testFileName) && req.attribute(QNetworkRequest::CustomVerbAttribute) == "PROPFIND") {
  216. reply = new FakePropfindReply(fakeFolder.remoteModifier(), op, req, this);
  217. } else if (req.url().toString().startsWith(accountState->account()->url().toString()) &&
  218. reqPath.startsWith(QStringLiteral("ocs/v2.php/apps/files_sharing/api/v1/shares"))) {
  219. if (op == QNetworkAccessManager::PostOperation) {
  220. reply = handleSharePostOperation(op, req, device);
  221. } else if(req.attribute(QNetworkRequest::CustomVerbAttribute) == "DELETE") {
  222. reply = handleShareDeleteOperation(op, req, reqPath);
  223. } else if(op == QNetworkAccessManager::PutOperation) {
  224. reply = handleSharePutOperation(op, req, reqPath, device);
  225. } else if(req.attribute(QNetworkRequest::CustomVerbAttribute) == "GET") {
  226. reply = handleShareGetOperation(op, req, reqPath);
  227. }
  228. } else {
  229. reply = new FakeErrorReply(op, req, this, 404, _fake404Response);
  230. }
  231. return reply;
  232. }
  233. QNetworkReply *ShareTestHelper::handleSharePostOperation(QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device)
  234. {
  235. QNetworkReply *reply = nullptr;
  236. // POST https://somehost/owncloud/ocs/v2.php/apps/files_sharing/api/v1/shares?format=json
  237. // Header: { Ocs-APIREQUEST: true, Content-Type: application/x-www-form-urlencoded, X-Request-ID: 1527752d-e147-4da7-89b8-fb06315a5fad, }
  238. // Data: [path=file.md&shareType=3]"
  239. const QUrlQuery urlQuery(req.url());
  240. const auto formatParam = urlQuery.queryItemValue(QStringLiteral("format"));
  241. if (formatParam == QStringLiteral("json")) {
  242. device->open(QIODevice::ReadOnly);
  243. const auto requestBody = device->readAll();
  244. device->close();
  245. const auto requestData = requestBody.split('&');
  246. // We don't care about path since we know the file we are testing with
  247. auto requestShareType = -10; // Just in case
  248. QString requestShareWith;
  249. QString requestName;
  250. QString requestPassword;
  251. for(const auto &data : requestData) {
  252. const auto requestDataUrl = QUrl::fromPercentEncoding(data);
  253. const QString requestDataUrlString(requestDataUrl);
  254. if (data.contains("shareType=")) {
  255. const auto shareTypeString = requestDataUrlString.mid(10);
  256. requestShareType = Share::ShareType(shareTypeString.toInt());
  257. } else if (data.contains("shareWith=")) {
  258. requestShareWith = data.mid(10);
  259. } else if (data.contains("name=")) {
  260. requestName = data.mid(5);
  261. } else if (data.contains("password=")) {
  262. requestPassword = data.mid(9);
  263. }
  264. }
  265. if (requestPassword.isEmpty() &&
  266. ((requestShareType == Share::TypeEmail && account->capabilities().shareEmailPasswordEnforced()) ||
  267. (requestShareType == Share::TypeLink && account->capabilities().sharePublicLinkEnforcePassword()))) {
  268. reply = new FakePayloadReply(op, req, _fake403Response, searchResultsReplyDelay, _fakeQnam.data());
  269. } else if (requestShareType >= 0) {
  270. const auto shareType = Share::ShareType(requestShareType);
  271. reply = new FakePayloadReply(op, req, createNewShare(shareType, requestShareWith, requestPassword), searchResultsReplyDelay, _fakeQnam.data());
  272. }
  273. }
  274. return reply;
  275. }
  276. QNetworkReply *ShareTestHelper::handleSharePutOperation(const QNetworkAccessManager::Operation op, const QNetworkRequest &req, const QString &reqPath, QIODevice *device)
  277. {
  278. QNetworkReply *reply = nullptr;
  279. const auto splitUrlPath = reqPath.split('/');
  280. const auto shareId = splitUrlPath.last();
  281. const QUrlQuery urlQuery(req.url());
  282. const auto formatParam = urlQuery.queryItemValue(QStringLiteral("format"));
  283. if (formatParam == QStringLiteral("json")) {
  284. device->open(QIODevice::ReadOnly);
  285. const auto requestBody = device->readAll();
  286. device->close();
  287. const auto requestData = requestBody.split('&');
  288. const auto existingShareIterator = std::find_if(_sharesReplyData.cbegin(), _sharesReplyData.cend(), [&shareId](const QJsonValue &value) {
  289. return value.toObject().value("id").toString() == shareId;
  290. });
  291. if (existingShareIterator == _sharesReplyData.cend()) {
  292. reply = new FakeErrorReply(op, req, this, 404, _fake404Response);
  293. } else {
  294. const auto existingShareValue = *existingShareIterator;
  295. auto shareObject = existingShareValue.toObject();
  296. for (const auto &requestDataItem : requestData) {
  297. const auto requestSplit = requestDataItem.split('=');
  298. auto requestKey = requestSplit.first();
  299. auto requestValue = requestSplit.last();
  300. // We send expireDate without time but the server returns with time at 00:00:00
  301. if (requestKey == "expireDate") {
  302. requestKey = "expiration";
  303. requestValue.append(" 00:00:00");
  304. }
  305. shareObject.insert(QString(requestKey), QString(requestValue));
  306. }
  307. _sharesReplyData.replace(existingShareIterator - _sharesReplyData.cbegin(), shareObject);
  308. reply = new FakePayloadReply(op, req, jsonValueToOccReply(shareObject), searchResultsReplyDelay, _fakeQnam.data());
  309. }
  310. }
  311. return reply;
  312. }
  313. QNetworkReply *ShareTestHelper::handleShareDeleteOperation(const QNetworkAccessManager::Operation op, const QNetworkRequest &req, const QString &reqPath)
  314. {
  315. QNetworkReply *reply = nullptr;
  316. const auto splitUrlPath = reqPath.split('/');
  317. const auto shareId = splitUrlPath.last();
  318. const auto existingShareIterator = std::find_if(_sharesReplyData.cbegin(), _sharesReplyData.cend(), [&shareId](const QJsonValue &value) {
  319. return value.toObject().value("id").toString() == shareId;
  320. });
  321. if (existingShareIterator == _sharesReplyData.cend()) {
  322. reply = new FakeErrorReply(op, req, this, 404, _fake404Response);
  323. } else {
  324. _sharesReplyData.removeAt(existingShareIterator - _sharesReplyData.cbegin());
  325. reply = new FakePayloadReply(op, req, _fake200JsonResponse, searchResultsReplyDelay, _fakeQnam.data());
  326. }
  327. return reply;
  328. }
  329. QNetworkReply *ShareTestHelper::handleShareGetOperation(const QNetworkAccessManager::Operation op, const QNetworkRequest &req, const QString &reqPath)
  330. {
  331. QNetworkReply *reply = nullptr;
  332. // Properly formatted request to fetch shares goes something like:
  333. // GET https://somehost/owncloud/ocs/v2.php/apps/files_sharing/api/v1/shares?path=file.md&reshares=true&format=json
  334. // Header: { Ocs-APIREQUEST: true, Content-Type: application/x-www-form-urlencoded, X-Request-ID: 8ba8960d-ca0d-45ba-abf4-03ab95ba6064, }
  335. // Data: []
  336. const auto urlQuery = QUrlQuery(req.url());
  337. const auto pathParam = urlQuery.queryItemValue(QStringLiteral("path"));
  338. const auto resharesParam = urlQuery.queryItemValue(QStringLiteral("reshares"));
  339. const auto formatParam = urlQuery.queryItemValue(QStringLiteral("format"));
  340. if (formatParam != QStringLiteral("json") || (!pathParam.isEmpty() && !pathParam.endsWith(QString(testFileName)))) {
  341. reply = new FakeErrorReply(op, req, this, 400, _fake400Response);
  342. } else if (reqPath.contains(QStringLiteral("ocs/v2.php/apps/files_sharing/api/v1/shares"))) {
  343. reply = new FakePayloadReply(op, req, jsonValueToOccReply(_sharesReplyData), searchResultsReplyDelay, _fakeQnam.data());
  344. }
  345. return reply;
  346. }
  347. const QByteArray ShareTestHelper::createNewShare(const Share::ShareType shareType, const QString &shareWith, const QString &password)
  348. {
  349. const auto displayString = shareType == Share::TypeLink ? QString() : shareWith;
  350. const FakeShareDefinition newShareDefinition(this,
  351. shareType,
  352. shareWith,
  353. displayString,
  354. password);
  355. _sharesReplyData.append(newShareDefinition.toShareJsonObject());
  356. return newShareDefinition.toRequestReply();
  357. }
  358. int ShareTestHelper::shareCount() const
  359. {
  360. return _sharesReplyData.count();
  361. }
  362. void ShareTestHelper::appendShareReplyData(const FakeShareDefinition &definition)
  363. {
  364. _sharesReplyData.append(definition.toShareJsonObject());
  365. }
  366. void ShareTestHelper::resetTestShares()
  367. {
  368. _sharesReplyData = QJsonArray();
  369. }
  370. void ShareTestHelper::resetTestData()
  371. {
  372. resetTestShares();
  373. account->setCapabilities(_fakeCapabilities);
  374. }