testcfapishellextensionsipc.cpp 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  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. QTemporaryDir dir;
  152. ConfigFile::setConfDir(dir.path());
  153. VfsShellExtensions::ThumbnailProviderIpc::overrideServerName = VfsShellExtensions::serverNameForApplicationNameDefault();
  154. VfsShellExtensions::CustomStateProviderIpc::overrideServerName = VfsShellExtensions::serverNameForApplicationNameDefault();
  155. _shellExtensionsServer.reset(new ShellExtensionsServer);
  156. _shellExtensionsServer->setIsSharedInvalidationInterval(shellExtensionServerOverrideIntervalMs);
  157. for (const auto &dummyImageName : dummmyImageNames) {
  158. const auto extension = dummyImageName.split(".").last();
  159. const auto format = dummyImageName.endsWith("PNG", Qt::CaseInsensitive) ? QImage::Format_ARGB32 : QImage::Format_RGB32;
  160. QImage image(QSize(640, 480), format);
  161. QPainter painter(&image);
  162. painter.setBrush(QBrush(Qt::red));
  163. painter.fillRect(QRectF(0, 0, 640, 480), Qt::red);
  164. QByteArray byteArray;
  165. QBuffer buffer(&byteArray);
  166. buffer.open(QIODevice::WriteOnly);
  167. image.save(&buffer, extension.toStdString().c_str());
  168. dummyImages.insert(dummyImageName, byteArray);
  169. }
  170. fakeFolder.remoteModifier().mkdir(roootFolderName);
  171. fakeFolder.remoteModifier().mkdir(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName));
  172. fakeFolder.remoteModifier().mkdir(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName));
  173. for (const auto &fileStateKey : dummyFileStates.keys()) {
  174. fakeFolder.remoteModifier().insert(fileStateKey, 256);
  175. }
  176. fakeQnam.reset(new FakeQNAM({}));
  177. account = OCC::Account::create();
  178. account->setCredentials(new FakeCredentials{fakeQnam.data()});
  179. account->setUrl(QUrl(("http://example.de")));
  180. accountState = new OCC::AccountState(account);
  181. OCC::AccountManager::instance()->addAccount(account);
  182. FolderMan *folderman = FolderMan::instance();
  183. QCOMPARE(folderman, &_fm);
  184. QVERIFY(folderman->addFolder(accountState, folderDefinition(fakeFolder.localPath())));
  185. fakeQnam->setOverride(
  186. [this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) {
  187. Q_UNUSED(device);
  188. QNetworkReply *reply = nullptr;
  189. const auto path = req.url().path();
  190. if (path.endsWith(OCC::OcsShareJob::_pathForSharesRequest)) {
  191. const auto jsonReply = TestCfApiShellExtensionsIPC::replyWithNoShares ? fakeNoSharesResponse : fakeSharedFilesResponse;
  192. TestCfApiShellExtensionsIPC::replyWithNoShares = false;
  193. auto fakePayloadReply = new FakePayloadReply(op, req, jsonReply, nullptr);
  194. QMap<QNetworkRequest::KnownHeaders, QByteArray> additionalHeaders = {
  195. {QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"}};
  196. fakePayloadReply->_additionalHeaders = additionalHeaders;
  197. reply = fakePayloadReply;
  198. } else if (path.endsWith(ShellExtensionsServer::getFetchThumbnailPath())) {
  199. const auto urlQuery = QUrlQuery(req.url());
  200. const auto fileId = urlQuery.queryItemValue(QStringLiteral("fileId"));
  201. const auto x = urlQuery.queryItemValue(QStringLiteral("x")).toInt();
  202. const auto y = urlQuery.queryItemValue(QStringLiteral("y")).toInt();
  203. if (fileId.isEmpty() || x <= 0 || y <= 0) {
  204. reply = new FakePayloadReply(op, req, {}, nullptr);
  205. } else {
  206. const auto foundImageIt = dummyImages.find(currentImage);
  207. QByteArray byteArray;
  208. if (foundImageIt != dummyImages.end()) {
  209. byteArray = foundImageIt.value();
  210. }
  211. currentImage.clear();
  212. auto fakePayloadReply = new FakePayloadReply(op, req, byteArray, nullptr);
  213. QMap<QNetworkRequest::KnownHeaders, QByteArray> additionalHeaders = {
  214. {QNetworkRequest::KnownHeaders::ContentTypeHeader, "image/jpeg"}};
  215. fakePayloadReply->_additionalHeaders = additionalHeaders;
  216. reply = fakePayloadReply;
  217. }
  218. } else {
  219. reply = new FakePayloadReply(op, req, {}, nullptr);
  220. }
  221. return reply;
  222. });
  223. };
  224. void testRequestThumbnails()
  225. {
  226. QTemporaryDir dir;
  227. ConfigFile::setConfDir(dir.path());
  228. FolderMan *folderman = FolderMan::instance();
  229. QVERIFY(folderman);
  230. auto folder = FolderMan::instance()->folderForPath(fakeFolder.localPath());
  231. QVERIFY(folder);
  232. folder->setVirtualFilesEnabled(true);
  233. QVERIFY(fakeFolder.syncOnce());
  234. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  235. ItemCompletedSpy completeSpy(fakeFolder);
  236. auto cleanup = [&]() {
  237. completeSpy.clear();
  238. };
  239. cleanup();
  240. // Create a virtual file for remote files
  241. for (const auto &dummyImageName : dummmyImageNames) {
  242. fakeFolder.remoteModifier().insert(dummyImageName, 256);
  243. }
  244. QVERIFY(fakeFolder.syncOnce());
  245. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  246. cleanup();
  247. // just add records from fake folder's journal to real one's to make test work
  248. SyncJournalFileRecord record;
  249. auto realFolder = FolderMan::instance()->folderForPath(fakeFolder.localPath());
  250. QVERIFY(realFolder);
  251. for (const auto &dummyImageName : dummmyImageNames) {
  252. if (fakeFolder.syncJournal().getFileRecord(dummyImageName, &record)) {
  253. QVERIFY(realFolder->journalDb()->setFileRecord(record));
  254. }
  255. }
  256. // #1 Test every fake image fetching. Everything must succeed.
  257. for (const auto &dummyImageName : dummmyImageNames) {
  258. QEventLoop loop;
  259. QByteArray thumbnailReplyData;
  260. currentImage = dummyImageName;
  261. // emulate thumbnail request from a separate thread (just like the real shell extension does)
  262. std::thread t([&] {
  263. VfsShellExtensions::ThumbnailProviderIpc thumbnailProviderIpc;
  264. thumbnailReplyData = thumbnailProviderIpc.fetchThumbnailForFile(
  265. fakeFolder.localPath() + dummyImageName, QSize(256, 256));
  266. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  267. });
  268. loop.exec();
  269. t.detach();
  270. QVERIFY(!thumbnailReplyData.isEmpty());
  271. const auto imageFromData = QImage::fromData(thumbnailReplyData);
  272. QVERIFY(!imageFromData.isNull());
  273. }
  274. // #2 Test wrong image fetching. It must fail.
  275. QEventLoop loop;
  276. QByteArray thumbnailReplyData;
  277. std::thread t1([&] {
  278. VfsShellExtensions::ThumbnailProviderIpc thumbnailProviderIpc;
  279. thumbnailReplyData = thumbnailProviderIpc.fetchThumbnailForFile(
  280. fakeFolder.localPath() + QString("A/photos/wrong.jpg"), QSize(256, 256));
  281. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  282. });
  283. loop.exec();
  284. t1.detach();
  285. QVERIFY(thumbnailReplyData.isEmpty());
  286. // #3 Test one image fetching, but set incorrect size. It must fail.
  287. currentImage = dummyImages.keys().first();
  288. std::thread t2([&] {
  289. VfsShellExtensions::ThumbnailProviderIpc thumbnailProviderIpc;
  290. thumbnailReplyData = thumbnailProviderIpc.fetchThumbnailForFile(fakeFolder.localPath() + currentImage, {});
  291. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  292. });
  293. loop.exec();
  294. t2.detach();
  295. QVERIFY(thumbnailReplyData.isEmpty());
  296. }
  297. void testRequestCustomStates()
  298. {
  299. QTemporaryDir dir;
  300. ConfigFile::setConfDir(dir.path());
  301. FolderMan *folderman = FolderMan::instance();
  302. QVERIFY(folderman);
  303. auto folder = FolderMan::instance()->folderForPath(fakeFolder.localPath());
  304. QVERIFY(folder);
  305. folder->setVirtualFilesEnabled(true);
  306. QVERIFY(fakeFolder.syncOnce());
  307. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  308. // just add records from fake folder's journal to real one's to make test work
  309. SyncJournalFileRecord record;
  310. auto realFolder = FolderMan::instance()->folderForPath(fakeFolder.localPath());
  311. QVERIFY(realFolder);
  312. for (auto it = std::begin(dummyFileStates); it != std::end(dummyFileStates); ++it) {
  313. if (fakeFolder.syncJournal().getFileRecord(it.key(), &record)) {
  314. record._isShared = it.value()._isShared;
  315. if (record._isShared) {
  316. record._remotePerm.setPermission(OCC::RemotePermissions::Permissions::IsShared);
  317. }
  318. record._lockstate._locked = it.value()._isLocked;
  319. if (record._lockstate._locked) {
  320. record._lockstate._lockOwnerId = "admin@example.cloud.com";
  321. record._lockstate._lockOwnerDisplayName = "Admin";
  322. record._lockstate._lockOwnerType = static_cast<int>(SyncFileItem::LockOwnerType::UserLock);
  323. record._lockstate._lockTime = QDateTime::currentMSecsSinceEpoch();
  324. record._lockstate._lockTimeout = 1000 * 60 * 60;
  325. }
  326. QVERIFY(fakeFolder.syncJournal().setFileRecord(record));
  327. QVERIFY(realFolder->journalDb()->setFileRecord(record));
  328. }
  329. }
  330. // #1 Test every file's states fetching. Everything must succeed.
  331. for (auto it = std::cbegin(dummyFileStates); it != std::cend(dummyFileStates); ++it) {
  332. QEventLoop loop;
  333. QVariantList customStates;
  334. std::thread t([&] {
  335. VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
  336. customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + it.key());
  337. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  338. });
  339. loop.exec();
  340. t.detach();
  341. QVERIFY(!customStates.isEmpty() || (!it.value()._isLocked && !it.value()._isShared));
  342. }
  343. // #2 Test wrong file's states fetching. It must fail.
  344. QEventLoop loop;
  345. QVariantList customStates;
  346. std::thread t1([&] {
  347. VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
  348. customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + QStringLiteral("A/files/wrong.jpg"));
  349. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  350. });
  351. loop.exec();
  352. t1.detach();
  353. QVERIFY(customStates.isEmpty());
  354. // #3 Test wrong file states fetching. It must fail.
  355. customStates.clear();
  356. std::thread t2([&] {
  357. VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
  358. customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + QStringLiteral("A/files/test_non_shared_and_non_locked_file.txt"));
  359. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  360. });
  361. loop.exec();
  362. t2.detach();
  363. QVERIFY(customStates.isEmpty());
  364. // reset all share states to make sure we'll get new states when fetching
  365. for (auto it = std::begin(dummyFileStates); it != std::end(dummyFileStates); ++it) {
  366. if (fakeFolder.syncJournal().getFileRecord(it.key(), &record)) {
  367. record._remotePerm.unsetPermission(OCC::RemotePermissions::Permissions::IsShared);
  368. record._isShared = false;
  369. QVERIFY(fakeFolder.syncJournal().setFileRecord(record));
  370. QVERIFY(realFolder->journalDb()->setFileRecord(record));
  371. }
  372. }
  373. QVERIFY(fakeFolder.syncOnce());
  374. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  375. //
  376. // wait enough time to make shares' state invalid
  377. QTest::qWait(shellExtensionServerOverrideIntervalMs + 1000);
  378. // #4 Test every file's states fetching. Everything must succeed.
  379. for (auto it = std::cbegin(dummyFileStates); it != std::cend(dummyFileStates); ++it) {
  380. QEventLoop loop;
  381. QVariantList customStates;
  382. std::thread t([&] {
  383. VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
  384. customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + it.key());
  385. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  386. });
  387. loop.exec();
  388. t.detach();
  389. QVERIFY(!customStates.isEmpty() || (!it.value()._isLocked && !it.value()._isShared));
  390. if (!customStates.isEmpty()) {
  391. const auto lockedIndex = QString(CUSTOM_STATE_ICON_LOCKED_INDEX).toInt() - QString(CUSTOM_STATE_ICON_INDEX_OFFSET).toInt();
  392. const auto sharedIndex = QString(CUSTOM_STATE_ICON_SHARED_INDEX).toInt() - QString(CUSTOM_STATE_ICON_INDEX_OFFSET).toInt();
  393. if (customStates.contains(lockedIndex) && customStates.contains(sharedIndex)) {
  394. QVERIFY(it.value()._isLocked && it.value()._isShared);
  395. }
  396. if (customStates.contains(lockedIndex)) {
  397. QVERIFY(it.value()._isLocked);
  398. }
  399. if (customStates.contains(sharedIndex)) {
  400. QVERIFY(it.value()._isShared);
  401. }
  402. }
  403. }
  404. // #5 Test no shares response for a file
  405. QTest::qWait(shellExtensionServerOverrideIntervalMs + 1000);
  406. TestCfApiShellExtensionsIPC::replyWithNoShares = true;
  407. customStates.clear();
  408. std::thread t3([&] {
  409. VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
  410. customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + QStringLiteral("A/files/test_non_shared_and_non_locked_file.txt"));
  411. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  412. });
  413. loop.exec();
  414. t3.detach();
  415. QVERIFY(customStates.isEmpty());
  416. }
  417. void cleanupTestCase()
  418. {
  419. QTemporaryDir dir;
  420. ConfigFile::setConfDir(dir.path());
  421. VfsShellExtensions::ThumbnailProviderIpc::overrideServerName.clear();
  422. if (auto folder = FolderMan::instance()->folderForPath(fakeFolder.localPath())) {
  423. folder->setVirtualFilesEnabled(false);
  424. }
  425. FolderMan::instance()->unloadAndDeleteAllFolders();
  426. if (auto accountToDelete = OCC::AccountManager::instance()->accounts().first()) {
  427. OCC::AccountManager::instance()->deleteAccount(accountToDelete.data());
  428. }
  429. }
  430. };
  431. bool TestCfApiShellExtensionsIPC::replyWithNoShares = false;
  432. QTEST_GUILESS_MAIN(TestCfApiShellExtensionsIPC)
  433. #include "testcfapishellextensionsipc.moc"