testcfapishellextensionsipc.cpp 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. /*
  2. * This software is in the public domain, furnished "as is", without technical
  3. * support, and with no warranty, express or implied, as to its usefulness for
  4. * any purpose.
  5. *
  6. */
  7. #include <account.h>
  8. #include <accountstate.h>
  9. #include <accountmanager.h>
  10. #include <common/vfs.h>
  11. #include <common/shellextensionutils.h>
  12. #include "config.h"
  13. #include <folderman.h>
  14. #include <libsync/vfs/cfapi/shellext/configvfscfapishellext.h>
  15. #include <ocssharejob.h>
  16. #include <shellextensionsserver.h>
  17. #include <syncengine.h>
  18. #include "syncenginetestutils.h"
  19. #include "testhelper.h"
  20. #include <vfs/cfapi/shellext/customstateprovideripc.h>
  21. #include <vfs/cfapi/shellext/thumbnailprovideripc.h>
  22. #include <QtTest>
  23. #include <QImage>
  24. #include <QPainter>
  25. namespace {
  26. static constexpr auto roootFolderName = "A";
  27. static constexpr auto imagesFolderName = "photos";
  28. static constexpr auto filesFolderName = "files";
  29. static const QByteArray fakeNoSharesResponse = R"({"ocs":{"data":[],"meta":{"message":"OK","status":"ok","statuscode":200}}})";
  30. static const QByteArray fakeSharedFilesResponse = R"({"ocs":{"data":[{
  31. "attributes": null,
  32. "can_delete": true,
  33. "can_edit": true,
  34. "displayname_file_owner": "admin",
  35. "displayname_owner": "admin",
  36. "expiration": null,
  37. "file_parent": 2981,
  38. "file_source": 3538,
  39. "file_target": "/test_shared_file.txt",
  40. "has_preview": true,
  41. "hide_download": 0,
  42. "id": "36",
  43. "item_source": 3538,
  44. "item_type": "file",
  45. "label": null,
  46. "mail_send": 0,
  47. "mimetype": "text/plain",
  48. "note": "",
  49. "parent": null,
  50. "path": "A/files/test_shared_file.txt",
  51. "permissions": 19,
  52. "share_type": 0,
  53. "share_with": "newstandard",
  54. "share_with_displayname": "newstandard",
  55. "share_with_displayname_unique": "newstandard",
  56. "status": {
  57. "clearAt": null,
  58. "icon": null,
  59. "message": null,
  60. "status": "offline"
  61. },
  62. "stime": 1662995777,
  63. "storage": 2,
  64. "storage_id": "home::admin",
  65. "token": null,
  66. "uid_file_owner": "admin",
  67. "uid_owner": "admin"
  68. },
  69. {
  70. "attributes": null,
  71. "can_delete": true,
  72. "can_edit": true,
  73. "displayname_file_owner": "admin",
  74. "displayname_owner": "admin",
  75. "expiration": null,
  76. "file_parent": 2981,
  77. "file_source": 3538,
  78. "file_target": "/test_shared_and_locked_file.txt",
  79. "has_preview": true,
  80. "hide_download": 0,
  81. "id": "36",
  82. "item_source": 3538,
  83. "item_type": "file",
  84. "label": null,
  85. "mail_send": 0,
  86. "mimetype": "text/plain",
  87. "note": "",
  88. "parent": null,
  89. "path": "A/files/test_shared_and_locked_file.txt",
  90. "permissions": 19,
  91. "share_type": 0,
  92. "share_with": "newstandard",
  93. "share_with_displayname": "newstandard",
  94. "share_with_displayname_unique": "newstandard",
  95. "status": {
  96. "clearAt": null,
  97. "icon": null,
  98. "message": null,
  99. "status": "offline"
  100. },
  101. "stime": 1662995777,
  102. "storage": 2,
  103. "storage_id": "home::admin",
  104. "token": null,
  105. "uid_file_owner": "admin",
  106. "uid_owner": "admin"
  107. }
  108. ],
  109. "meta": {
  110. "message": "OK",
  111. "status": "ok",
  112. "statuscode": 200
  113. }
  114. }
  115. })";
  116. static constexpr auto shellExtensionServerOverrideIntervalMs = 1000LL * 2LL;
  117. }
  118. using namespace OCC;
  119. class TestCfApiShellExtensionsIPC : public QObject
  120. {
  121. Q_OBJECT
  122. FolderMan _fm;
  123. FakeFolder fakeFolder{FileInfo()};
  124. QScopedPointer<FakeQNAM> fakeQnam;
  125. OCC::AccountPtr account;
  126. OCC::AccountState* accountState;
  127. QScopedPointer<ShellExtensionsServer> _shellExtensionsServer;
  128. const QStringList dummmyImageNames = {
  129. { QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imageJpg.jpg")) },
  130. { QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imagePng.png")) },
  131. { QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imagePng.bmp")) }
  132. };
  133. QMap<QString, QByteArray> dummyImages;
  134. QString currentImage;
  135. struct FileStates
  136. {
  137. bool _isShared = false;
  138. bool _isLocked = false;
  139. };
  140. const QMap<QString, FileStates> dummyFileStates = {
  141. { QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_locked_file.txt")), { false, true } },
  142. { QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_shared_file.txt")), { true, false } },
  143. { QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_shared_and_locked_file.txt")), { true, true }},
  144. { QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_non_shared_and_non_locked_file.txt")), { false, false }}
  145. };
  146. public:
  147. static bool replyWithNoShares;
  148. private slots:
  149. void initTestCase()
  150. {
  151. VfsShellExtensions::ThumbnailProviderIpc::overrideServerName = VfsShellExtensions::serverNameForApplicationNameDefault();
  152. VfsShellExtensions::CustomStateProviderIpc::overrideServerName = VfsShellExtensions::serverNameForApplicationNameDefault();
  153. _shellExtensionsServer.reset(new ShellExtensionsServer);
  154. _shellExtensionsServer->setIsSharedInvalidationInterval(shellExtensionServerOverrideIntervalMs);
  155. for (const auto &dummyImageName : dummmyImageNames) {
  156. const auto extension = dummyImageName.split(".").last();
  157. const auto format = dummyImageName.endsWith("PNG", Qt::CaseInsensitive) ? QImage::Format_ARGB32 : QImage::Format_RGB32;
  158. QImage image(QSize(640, 480), format);
  159. QPainter painter(&image);
  160. painter.setBrush(QBrush(Qt::red));
  161. painter.fillRect(QRectF(0, 0, 640, 480), Qt::red);
  162. QByteArray byteArray;
  163. QBuffer buffer(&byteArray);
  164. buffer.open(QIODevice::WriteOnly);
  165. image.save(&buffer, extension.toStdString().c_str());
  166. dummyImages.insert(dummyImageName, byteArray);
  167. }
  168. fakeFolder.remoteModifier().mkdir(roootFolderName);
  169. fakeFolder.remoteModifier().mkdir(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName));
  170. fakeFolder.remoteModifier().mkdir(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName));
  171. for (const auto &fileStateKey : dummyFileStates.keys()) {
  172. fakeFolder.remoteModifier().insert(fileStateKey, 256);
  173. }
  174. fakeQnam.reset(new FakeQNAM({}));
  175. account = OCC::Account::create();
  176. account->setCredentials(new FakeCredentials{fakeQnam.data()});
  177. account->setUrl(QUrl(("http://example.de")));
  178. accountState = new OCC::AccountState(account);
  179. OCC::AccountManager::instance()->addAccount(account);
  180. FolderMan *folderman = FolderMan::instance();
  181. QCOMPARE(folderman, &_fm);
  182. QVERIFY(folderman->addFolder(accountState, folderDefinition(fakeFolder.localPath())));
  183. fakeQnam->setOverride(
  184. [this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) {
  185. Q_UNUSED(device);
  186. QNetworkReply *reply = nullptr;
  187. const auto path = req.url().path();
  188. if (path.endsWith(OCC::OcsShareJob::_pathForSharesRequest)) {
  189. const auto jsonReply = TestCfApiShellExtensionsIPC::replyWithNoShares ? fakeNoSharesResponse : fakeSharedFilesResponse;
  190. TestCfApiShellExtensionsIPC::replyWithNoShares = false;
  191. auto fakePayloadReply = new FakePayloadReply(op, req, jsonReply, nullptr);
  192. QMap<QNetworkRequest::KnownHeaders, QByteArray> additionalHeaders = {
  193. {QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"}};
  194. fakePayloadReply->_additionalHeaders = additionalHeaders;
  195. reply = fakePayloadReply;
  196. } else if (path.endsWith(ShellExtensionsServer::getFetchThumbnailPath())) {
  197. const auto urlQuery = QUrlQuery(req.url());
  198. const auto fileId = urlQuery.queryItemValue(QStringLiteral("fileId"));
  199. const auto x = urlQuery.queryItemValue(QStringLiteral("x")).toInt();
  200. const auto y = urlQuery.queryItemValue(QStringLiteral("y")).toInt();
  201. if (fileId.isEmpty() || x <= 0 || y <= 0) {
  202. reply = new FakePayloadReply(op, req, {}, nullptr);
  203. } else {
  204. const auto foundImageIt = dummyImages.find(currentImage);
  205. QByteArray byteArray;
  206. if (foundImageIt != dummyImages.end()) {
  207. byteArray = foundImageIt.value();
  208. }
  209. currentImage.clear();
  210. auto fakePayloadReply = new FakePayloadReply(op, req, byteArray, nullptr);
  211. QMap<QNetworkRequest::KnownHeaders, QByteArray> additionalHeaders = {
  212. {QNetworkRequest::KnownHeaders::ContentTypeHeader, "image/jpeg"}};
  213. fakePayloadReply->_additionalHeaders = additionalHeaders;
  214. reply = fakePayloadReply;
  215. }
  216. } else {
  217. reply = new FakePayloadReply(op, req, {}, nullptr);
  218. }
  219. return reply;
  220. });
  221. };
  222. void testRequestThumbnails()
  223. {
  224. FolderMan *folderman = FolderMan::instance();
  225. QVERIFY(folderman);
  226. auto folder = FolderMan::instance()->folderForPath(fakeFolder.localPath());
  227. QVERIFY(folder);
  228. folder->setVirtualFilesEnabled(true);
  229. QVERIFY(fakeFolder.syncOnce());
  230. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  231. ItemCompletedSpy completeSpy(fakeFolder);
  232. auto cleanup = [&]() {
  233. completeSpy.clear();
  234. };
  235. cleanup();
  236. // Create a virtual file for remote files
  237. for (const auto &dummyImageName : dummmyImageNames) {
  238. fakeFolder.remoteModifier().insert(dummyImageName, 256);
  239. }
  240. QVERIFY(fakeFolder.syncOnce());
  241. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  242. cleanup();
  243. // just add records from fake folder's journal to real one's to make test work
  244. SyncJournalFileRecord record;
  245. auto realFolder = FolderMan::instance()->folderForPath(fakeFolder.localPath());
  246. QVERIFY(realFolder);
  247. for (const auto &dummyImageName : dummmyImageNames) {
  248. if (fakeFolder.syncJournal().getFileRecord(dummyImageName, &record)) {
  249. QVERIFY(realFolder->journalDb()->setFileRecord(record));
  250. }
  251. }
  252. // #1 Test every fake image fetching. Everything must succeed.
  253. for (const auto &dummyImageName : dummmyImageNames) {
  254. QEventLoop loop;
  255. QByteArray thumbnailReplyData;
  256. currentImage = dummyImageName;
  257. // emulate thumbnail request from a separate thread (just like the real shell extension does)
  258. std::thread t([&] {
  259. VfsShellExtensions::ThumbnailProviderIpc thumbnailProviderIpc;
  260. thumbnailReplyData = thumbnailProviderIpc.fetchThumbnailForFile(
  261. fakeFolder.localPath() + dummyImageName, QSize(256, 256));
  262. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  263. });
  264. loop.exec();
  265. t.detach();
  266. QVERIFY(!thumbnailReplyData.isEmpty());
  267. const auto imageFromData = QImage::fromData(thumbnailReplyData);
  268. QVERIFY(!imageFromData.isNull());
  269. }
  270. // #2 Test wrong image fetching. It must fail.
  271. QEventLoop loop;
  272. QByteArray thumbnailReplyData;
  273. std::thread t1([&] {
  274. VfsShellExtensions::ThumbnailProviderIpc thumbnailProviderIpc;
  275. thumbnailReplyData = thumbnailProviderIpc.fetchThumbnailForFile(
  276. fakeFolder.localPath() + QString("A/photos/wrong.jpg"), QSize(256, 256));
  277. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  278. });
  279. loop.exec();
  280. t1.detach();
  281. QVERIFY(thumbnailReplyData.isEmpty());
  282. // #3 Test one image fetching, but set incorrect size. It must fail.
  283. currentImage = dummyImages.keys().first();
  284. std::thread t2([&] {
  285. VfsShellExtensions::ThumbnailProviderIpc thumbnailProviderIpc;
  286. thumbnailReplyData = thumbnailProviderIpc.fetchThumbnailForFile(fakeFolder.localPath() + currentImage, {});
  287. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  288. });
  289. loop.exec();
  290. t2.detach();
  291. QVERIFY(thumbnailReplyData.isEmpty());
  292. }
  293. void testRequestCustomStates()
  294. {
  295. FolderMan *folderman = FolderMan::instance();
  296. QVERIFY(folderman);
  297. auto folder = FolderMan::instance()->folderForPath(fakeFolder.localPath());
  298. QVERIFY(folder);
  299. folder->setVirtualFilesEnabled(true);
  300. QVERIFY(fakeFolder.syncOnce());
  301. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  302. // just add records from fake folder's journal to real one's to make test work
  303. SyncJournalFileRecord record;
  304. auto realFolder = FolderMan::instance()->folderForPath(fakeFolder.localPath());
  305. QVERIFY(realFolder);
  306. for (auto it = std::begin(dummyFileStates); it != std::end(dummyFileStates); ++it) {
  307. if (fakeFolder.syncJournal().getFileRecord(it.key(), &record)) {
  308. record._isShared = it.value()._isShared;
  309. if (record._isShared) {
  310. record._remotePerm.setPermission(OCC::RemotePermissions::Permissions::IsShared);
  311. }
  312. record._lockstate._locked = it.value()._isLocked;
  313. if (record._lockstate._locked) {
  314. record._lockstate._lockOwnerId = "admin@example.cloud.com";
  315. record._lockstate._lockOwnerDisplayName = "Admin";
  316. record._lockstate._lockOwnerType = static_cast<int>(SyncFileItem::LockOwnerType::UserLock);
  317. record._lockstate._lockTime = QDateTime::currentMSecsSinceEpoch();
  318. record._lockstate._lockTimeout = 1000 * 60 * 60;
  319. }
  320. QVERIFY(fakeFolder.syncJournal().setFileRecord(record));
  321. QVERIFY(realFolder->journalDb()->setFileRecord(record));
  322. }
  323. }
  324. // #1 Test every file's states fetching. Everything must succeed.
  325. for (auto it = std::cbegin(dummyFileStates); it != std::cend(dummyFileStates); ++it) {
  326. QEventLoop loop;
  327. QVariantList customStates;
  328. std::thread t([&] {
  329. VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
  330. customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + it.key());
  331. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  332. });
  333. loop.exec();
  334. t.detach();
  335. QVERIFY(!customStates.isEmpty() || (!it.value()._isLocked && !it.value()._isShared));
  336. }
  337. // #2 Test wrong file's states fetching. It must fail.
  338. QEventLoop loop;
  339. QVariantList customStates;
  340. std::thread t1([&] {
  341. VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
  342. customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + QStringLiteral("A/files/wrong.jpg"));
  343. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  344. });
  345. loop.exec();
  346. t1.detach();
  347. QVERIFY(customStates.isEmpty());
  348. // #3 Test wrong file states fetching. It must fail.
  349. customStates.clear();
  350. std::thread t2([&] {
  351. VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
  352. customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + QStringLiteral("A/files/test_non_shared_and_non_locked_file.txt"));
  353. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  354. });
  355. loop.exec();
  356. t2.detach();
  357. QVERIFY(customStates.isEmpty());
  358. // reset all share states to make sure we'll get new states when fetching
  359. for (auto it = std::begin(dummyFileStates); it != std::end(dummyFileStates); ++it) {
  360. if (fakeFolder.syncJournal().getFileRecord(it.key(), &record)) {
  361. record._remotePerm.unsetPermission(OCC::RemotePermissions::Permissions::IsShared);
  362. record._isShared = false;
  363. QVERIFY(fakeFolder.syncJournal().setFileRecord(record));
  364. QVERIFY(realFolder->journalDb()->setFileRecord(record));
  365. }
  366. }
  367. QVERIFY(fakeFolder.syncOnce());
  368. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  369. //
  370. // wait enough time to make shares' state invalid
  371. QTest::qWait(shellExtensionServerOverrideIntervalMs + 1000);
  372. // #4 Test every file's states fetching. Everything must succeed.
  373. for (auto it = std::cbegin(dummyFileStates); it != std::cend(dummyFileStates); ++it) {
  374. QEventLoop loop;
  375. QVariantList customStates;
  376. std::thread t([&] {
  377. VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
  378. customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + it.key());
  379. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  380. });
  381. loop.exec();
  382. t.detach();
  383. QVERIFY(!customStates.isEmpty() || (!it.value()._isLocked && !it.value()._isShared));
  384. if (!customStates.isEmpty()) {
  385. const auto lockedIndex = QString(CUSTOM_STATE_ICON_LOCKED_INDEX).toInt() - QString(CUSTOM_STATE_ICON_INDEX_OFFSET).toInt();
  386. const auto sharedIndex = QString(CUSTOM_STATE_ICON_SHARED_INDEX).toInt() - QString(CUSTOM_STATE_ICON_INDEX_OFFSET).toInt();
  387. if (customStates.contains(lockedIndex) && customStates.contains(sharedIndex)) {
  388. QVERIFY(it.value()._isLocked && it.value()._isShared);
  389. }
  390. if (customStates.contains(lockedIndex)) {
  391. QVERIFY(it.value()._isLocked);
  392. }
  393. if (customStates.contains(sharedIndex)) {
  394. QVERIFY(it.value()._isShared);
  395. }
  396. }
  397. }
  398. // #5 Test no shares response for a file
  399. QTest::qWait(shellExtensionServerOverrideIntervalMs + 1000);
  400. TestCfApiShellExtensionsIPC::replyWithNoShares = true;
  401. customStates.clear();
  402. std::thread t3([&] {
  403. VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
  404. customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + QStringLiteral("A/files/test_non_shared_and_non_locked_file.txt"));
  405. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  406. });
  407. loop.exec();
  408. t3.detach();
  409. QVERIFY(customStates.isEmpty());
  410. }
  411. void cleanupTestCase()
  412. {
  413. VfsShellExtensions::ThumbnailProviderIpc::overrideServerName.clear();
  414. if (auto folder = FolderMan::instance()->folderForPath(fakeFolder.localPath())) {
  415. folder->setVirtualFilesEnabled(false);
  416. }
  417. FolderMan::instance()->unloadAndDeleteAllFolders();
  418. if (auto accountToDelete = OCC::AccountManager::instance()->accounts().first()) {
  419. OCC::AccountManager::instance()->deleteAccount(accountToDelete.data());
  420. }
  421. }
  422. };
  423. bool TestCfApiShellExtensionsIPC::replyWithNoShares = false;
  424. QTEST_GUILESS_MAIN(TestCfApiShellExtensionsIPC)
  425. #include "testcfapishellextensionsipc.moc"