testcfapishellextensionsipc.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  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 "configfile.h"
  8. #include "account.h"
  9. #include "accountstate.h"
  10. #include "accountmanager.h"
  11. #include "common/vfs.h"
  12. #include "common/shellextensionutils.h"
  13. #include "config.h"
  14. #include "folderman.h"
  15. #include "libsync/vfs/cfapi/shellext/configvfscfapishellext.h"
  16. #include "ocssharejob.h"
  17. #include "shellextensionsserver.h"
  18. #include "syncengine.h"
  19. #include "syncenginetestutils.h"
  20. #include "testhelper.h"
  21. #include "vfs/cfapi/shellext/customstateprovideripc.h"
  22. #include "vfs/cfapi/shellext/thumbnailprovideripc.h"
  23. #include <QTemporaryDir>
  24. #include <QtTest>
  25. #include <QImage>
  26. #include <QPainter>
  27. namespace {
  28. static constexpr auto roootFolderName = "A";
  29. static constexpr auto imagesFolderName = "photos";
  30. static constexpr auto filesFolderName = "files";
  31. static constexpr auto shellExtensionServerOverrideIntervalMs = 1000LL * 2LL;
  32. }
  33. using namespace OCC;
  34. class TestCfApiShellExtensionsIPC : public QObject
  35. {
  36. Q_OBJECT
  37. FolderMan _fm;
  38. FakeFolder fakeFolder{FileInfo()};
  39. QScopedPointer<FakeQNAM> fakeQnam;
  40. OCC::AccountPtr account;
  41. OCC::AccountState* accountState = nullptr;
  42. QScopedPointer<ShellExtensionsServer> _shellExtensionsServer;
  43. const QStringList dummmyImageNames = {
  44. { QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imageJpg.jpg")) },
  45. { QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imagePng.png")) },
  46. { QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imagePng.bmp")) }
  47. };
  48. QMap<QString, QByteArray> dummyImages;
  49. QString currentImage;
  50. struct FileStates
  51. {
  52. bool _isShared = false;
  53. bool _isLocked = false;
  54. };
  55. const QMap<QString, FileStates> dummyFileStates = {
  56. { QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_locked_file.txt")), { false, true } },
  57. { QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_shared_file.txt")), { true, false } },
  58. { QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_shared_and_locked_file.txt")), { true, true }},
  59. { QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_non_shared_and_non_locked_file.txt")), { false, false }}
  60. };
  61. public:
  62. static bool replyWithNoShares;
  63. signals:
  64. void propfindRequested();
  65. private slots:
  66. void initTestCase()
  67. {
  68. QTemporaryDir dir;
  69. ConfigFile::setConfDir(dir.path());
  70. VfsShellExtensions::ThumbnailProviderIpc::overrideServerName = VfsShellExtensions::serverNameForApplicationNameDefault();
  71. VfsShellExtensions::CustomStateProviderIpc::overrideServerName = VfsShellExtensions::serverNameForApplicationNameDefault();
  72. _shellExtensionsServer.reset(new ShellExtensionsServer);
  73. _shellExtensionsServer->setIsSharedInvalidationInterval(shellExtensionServerOverrideIntervalMs);
  74. for (const auto &dummyImageName : dummmyImageNames) {
  75. const auto extension = dummyImageName.split(".").last();
  76. const auto format = dummyImageName.endsWith("PNG", Qt::CaseInsensitive) ? QImage::Format_ARGB32 : QImage::Format_RGB32;
  77. QImage image(QSize(640, 480), format);
  78. QPainter painter(&image);
  79. painter.setBrush(QBrush(Qt::red));
  80. painter.fillRect(QRectF(0, 0, 640, 480), Qt::red);
  81. QByteArray byteArray;
  82. QBuffer buffer(&byteArray);
  83. buffer.open(QIODevice::WriteOnly);
  84. image.save(&buffer, extension.toStdString().c_str());
  85. dummyImages.insert(dummyImageName, byteArray);
  86. }
  87. fakeFolder.remoteModifier().mkdir(roootFolderName);
  88. fakeFolder.remoteModifier().mkdir(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName));
  89. fakeFolder.remoteModifier().mkdir(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName));
  90. for (const auto &fileStateKey : dummyFileStates.keys()) {
  91. fakeFolder.remoteModifier().insert(fileStateKey, 256);
  92. }
  93. fakeQnam.reset(new FakeQNAM({}));
  94. account = OCC::Account::create();
  95. account->setCredentials(new FakeCredentials{fakeQnam.data()});
  96. account->setUrl(QUrl(("http://example.de")));
  97. accountState = new OCC::AccountState(account);
  98. OCC::AccountManager::instance()->addAccount(account);
  99. FolderMan *folderman = FolderMan::instance();
  100. QCOMPARE(folderman, &_fm);
  101. QVERIFY(folderman->addFolder(accountState, folderDefinition(fakeFolder.localPath())));
  102. fakeQnam->setOverride(
  103. [this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) {
  104. Q_UNUSED(device);
  105. QNetworkReply *reply = nullptr;
  106. const auto path = req.url().path();
  107. const auto customOperation = req.attribute(QNetworkRequest::CustomVerbAttribute);
  108. if (customOperation == QStringLiteral("PROPFIND")) {
  109. emit propfindRequested();
  110. reply = new FakePayloadReply(op, req, {}, nullptr);
  111. } else if (path.endsWith(ShellExtensionsServer::getFetchThumbnailPath())) {
  112. const auto urlQuery = QUrlQuery(req.url());
  113. const auto fileId = urlQuery.queryItemValue(QStringLiteral("fileId"));
  114. const auto x = urlQuery.queryItemValue(QStringLiteral("x")).toInt();
  115. const auto y = urlQuery.queryItemValue(QStringLiteral("y")).toInt();
  116. if (fileId.isEmpty() || x <= 0 || y <= 0) {
  117. reply = new FakePayloadReply(op, req, {}, nullptr);
  118. } else {
  119. const auto foundImageIt = dummyImages.find(currentImage);
  120. QByteArray byteArray;
  121. if (foundImageIt != dummyImages.end()) {
  122. byteArray = foundImageIt.value();
  123. }
  124. currentImage.clear();
  125. auto fakePayloadReply = new FakePayloadReply(op, req, byteArray, nullptr);
  126. QMap<QNetworkRequest::KnownHeaders, QByteArray> additionalHeaders = {
  127. {QNetworkRequest::KnownHeaders::ContentTypeHeader, "image/jpeg"}};
  128. fakePayloadReply->_additionalHeaders = additionalHeaders;
  129. reply = fakePayloadReply;
  130. }
  131. } else {
  132. reply = new FakePayloadReply(op, req, {}, nullptr);
  133. }
  134. return reply;
  135. });
  136. };
  137. void testRequestThumbnails()
  138. {
  139. QTemporaryDir dir;
  140. ConfigFile::setConfDir(dir.path());
  141. FolderMan *folderman = FolderMan::instance();
  142. QVERIFY(folderman);
  143. auto folder = FolderMan::instance()->folderForPath(fakeFolder.localPath());
  144. QVERIFY(folder);
  145. folder->setVirtualFilesEnabled(true);
  146. QVERIFY(fakeFolder.syncOnce());
  147. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  148. ItemCompletedSpy completeSpy(fakeFolder);
  149. auto cleanup = [&]() {
  150. completeSpy.clear();
  151. };
  152. cleanup();
  153. // Create a virtual file for remote files
  154. for (const auto &dummyImageName : dummmyImageNames) {
  155. fakeFolder.remoteModifier().insert(dummyImageName, 256);
  156. }
  157. QVERIFY(fakeFolder.syncOnce());
  158. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  159. cleanup();
  160. // just add records from fake folder's journal to real one's to make test work
  161. SyncJournalFileRecord record;
  162. auto realFolder = FolderMan::instance()->folderForPath(fakeFolder.localPath());
  163. QVERIFY(realFolder);
  164. for (const auto &dummyImageName : dummmyImageNames) {
  165. if (fakeFolder.syncJournal().getFileRecord(dummyImageName, &record)) {
  166. QVERIFY(realFolder->journalDb()->setFileRecord(record));
  167. }
  168. }
  169. // #1 Test every fake image fetching. Everything must succeed.
  170. for (const auto &dummyImageName : dummmyImageNames) {
  171. QEventLoop loop;
  172. QByteArray thumbnailReplyData;
  173. currentImage = dummyImageName;
  174. // emulate thumbnail request from a separate thread (just like the real shell extension does)
  175. std::thread t([&] {
  176. VfsShellExtensions::ThumbnailProviderIpc thumbnailProviderIpc;
  177. thumbnailReplyData = thumbnailProviderIpc.fetchThumbnailForFile(
  178. fakeFolder.localPath() + dummyImageName, QSize(256, 256));
  179. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  180. });
  181. loop.exec();
  182. t.detach();
  183. QVERIFY(!thumbnailReplyData.isEmpty());
  184. const auto imageFromData = QImage::fromData(thumbnailReplyData);
  185. QVERIFY(!imageFromData.isNull());
  186. }
  187. // #2 Test wrong image fetching. It must fail.
  188. QEventLoop loop;
  189. QByteArray thumbnailReplyData;
  190. std::thread t1([&] {
  191. VfsShellExtensions::ThumbnailProviderIpc thumbnailProviderIpc;
  192. thumbnailReplyData = thumbnailProviderIpc.fetchThumbnailForFile(
  193. fakeFolder.localPath() + QString("A/photos/wrong.jpg"), QSize(256, 256));
  194. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  195. });
  196. loop.exec();
  197. t1.detach();
  198. QVERIFY(thumbnailReplyData.isEmpty());
  199. // #3 Test one image fetching, but set incorrect size. It must fail.
  200. currentImage = dummyImages.keys().first();
  201. std::thread t2([&] {
  202. VfsShellExtensions::ThumbnailProviderIpc thumbnailProviderIpc;
  203. thumbnailReplyData = thumbnailProviderIpc.fetchThumbnailForFile(fakeFolder.localPath() + currentImage, {});
  204. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  205. });
  206. loop.exec();
  207. t2.detach();
  208. QVERIFY(thumbnailReplyData.isEmpty());
  209. }
  210. void testRequestCustomStates()
  211. {
  212. QTemporaryDir dir;
  213. ConfigFile::setConfDir(dir.path());
  214. FolderMan *folderman = FolderMan::instance();
  215. QVERIFY(folderman);
  216. auto folder = FolderMan::instance()->folderForPath(fakeFolder.localPath());
  217. QVERIFY(folder);
  218. folder->setVirtualFilesEnabled(true);
  219. QVERIFY(fakeFolder.syncOnce());
  220. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  221. // just add records from fake folder's journal to real one's to make test work
  222. SyncJournalFileRecord record;
  223. auto realFolder = FolderMan::instance()->folderForPath(fakeFolder.localPath());
  224. QVERIFY(realFolder);
  225. for (auto it = std::begin(dummyFileStates); it != std::end(dummyFileStates); ++it) {
  226. if (fakeFolder.syncJournal().getFileRecord(it.key(), &record)) {
  227. record._isShared = it.value()._isShared;
  228. if (record._isShared) {
  229. record._remotePerm.setPermission(OCC::RemotePermissions::Permissions::IsShared);
  230. }
  231. record._lockstate._locked = it.value()._isLocked;
  232. if (record._lockstate._locked) {
  233. record._lockstate._lockOwnerId = "admin@example.cloud.com";
  234. record._lockstate._lockOwnerDisplayName = "Admin";
  235. record._lockstate._lockOwnerType = static_cast<int>(SyncFileItem::LockOwnerType::UserLock);
  236. record._lockstate._lockTime = QDateTime::currentMSecsSinceEpoch();
  237. record._lockstate._lockTimeout = 1000 * 60 * 60;
  238. }
  239. QVERIFY(fakeFolder.syncJournal().setFileRecord(record));
  240. QVERIFY(realFolder->journalDb()->setFileRecord(record));
  241. }
  242. }
  243. QSignalSpy propfindRequestedSpy(this, &TestCfApiShellExtensionsIPC::propfindRequested);
  244. // #1 Test every file's states fetching. Everything must succeed.
  245. for (auto it = std::cbegin(dummyFileStates); it != std::cend(dummyFileStates); ++it) {
  246. QEventLoop loop;
  247. QVariantList customStates;
  248. std::thread t([&] {
  249. VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
  250. customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + it.key());
  251. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  252. });
  253. loop.exec();
  254. t.detach();
  255. if (!customStates.isEmpty()) {
  256. const auto lockedIndex = QString(CUSTOM_STATE_ICON_LOCKED_INDEX).toInt() - QString(CUSTOM_STATE_ICON_INDEX_OFFSET).toInt();
  257. const auto sharedIndex = QString(CUSTOM_STATE_ICON_SHARED_INDEX).toInt() - QString(CUSTOM_STATE_ICON_INDEX_OFFSET).toInt();
  258. if (customStates.contains(lockedIndex) && customStates.contains(sharedIndex)) {
  259. QVERIFY(it.value()._isLocked && it.value()._isShared);
  260. }
  261. if (customStates.contains(lockedIndex)) {
  262. QVERIFY(it.value()._isLocked);
  263. }
  264. if (customStates.contains(sharedIndex)) {
  265. QVERIFY(it.value()._isShared);
  266. }
  267. }
  268. QVERIFY(!customStates.isEmpty() || (!it.value()._isLocked && !it.value()._isShared));
  269. }
  270. QVERIFY(propfindRequestedSpy.isEmpty());
  271. // #2 Test wrong file's states fetching. It must fail.
  272. QEventLoop loop;
  273. QVariantList customStates;
  274. std::thread t1([&] {
  275. VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
  276. customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + QStringLiteral("A/files/wrong.jpg"));
  277. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  278. });
  279. loop.exec();
  280. t1.detach();
  281. QVERIFY(customStates.isEmpty());
  282. QVERIFY(propfindRequestedSpy.isEmpty());
  283. // #3 Test wrong file states fetching. It must fail.
  284. customStates.clear();
  285. std::thread t2([&] {
  286. VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
  287. customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + QStringLiteral("A/files/test_non_shared_and_non_locked_file.txt"));
  288. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  289. });
  290. loop.exec();
  291. t2.detach();
  292. QVERIFY(customStates.isEmpty());
  293. QVERIFY(propfindRequestedSpy.isEmpty());
  294. // reset all share states to make sure we'll get new states when fetching
  295. for (auto it = std::begin(dummyFileStates); it != std::end(dummyFileStates); ++it) {
  296. if (fakeFolder.syncJournal().getFileRecord(it.key(), &record)) {
  297. record._remotePerm.unsetPermission(OCC::RemotePermissions::Permissions::IsShared);
  298. record._isShared = false;
  299. QVERIFY(fakeFolder.syncJournal().setFileRecord(record));
  300. QVERIFY(realFolder->journalDb()->setFileRecord(record));
  301. }
  302. }
  303. QVERIFY(fakeFolder.syncOnce());
  304. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  305. //
  306. // wait enough time to make shares' state invalid
  307. QTest::qWait(shellExtensionServerOverrideIntervalMs + 1000);
  308. // #4 Test every file's states fetching. Everything must succeed.
  309. for (auto it = std::cbegin(dummyFileStates); it != std::cend(dummyFileStates); ++it) {
  310. QEventLoop loop;
  311. QVariantList customStates;
  312. std::thread t([&] {
  313. VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
  314. customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + it.key());
  315. QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
  316. });
  317. loop.exec();
  318. t.detach();
  319. }
  320. QVERIFY(propfindRequestedSpy.count() == dummyFileStates.size());
  321. }
  322. void cleanupTestCase()
  323. {
  324. QTemporaryDir dir;
  325. ConfigFile::setConfDir(dir.path());
  326. VfsShellExtensions::ThumbnailProviderIpc::overrideServerName.clear();
  327. if (auto folder = FolderMan::instance()->folderForPath(fakeFolder.localPath())) {
  328. folder->setVirtualFilesEnabled(false);
  329. }
  330. FolderMan::instance()->unloadAndDeleteAllFolders();
  331. if (auto accountToDelete = OCC::AccountManager::instance()->accounts().first()) {
  332. OCC::AccountManager::instance()->deleteAccount(accountToDelete.data());
  333. }
  334. }
  335. };
  336. bool TestCfApiShellExtensionsIPC::replyWithNoShares = false;
  337. QTEST_GUILESS_MAIN(TestCfApiShellExtensionsIPC)
  338. #include "testcfapishellextensionsipc.moc"