testsyncengine.cpp 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288
  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 <QtTest>
  8. #include "syncenginetestutils.h"
  9. #include <syncengine.h>
  10. #include <propagatorjobs.h>
  11. using namespace OCC;
  12. bool itemDidComplete(const ItemCompletedSpy &spy, const QString &path)
  13. {
  14. if (auto item = spy.findItem(path)) {
  15. return item->_instruction != CSYNC_INSTRUCTION_NONE && item->_instruction != CSYNC_INSTRUCTION_UPDATE_METADATA;
  16. }
  17. return false;
  18. }
  19. bool itemInstruction(const ItemCompletedSpy &spy, const QString &path, const SyncInstructions instr)
  20. {
  21. auto item = spy.findItem(path);
  22. return item->_instruction == instr;
  23. }
  24. bool itemDidCompleteSuccessfully(const ItemCompletedSpy &spy, const QString &path)
  25. {
  26. if (auto item = spy.findItem(path)) {
  27. return item->_status == SyncFileItem::Success;
  28. }
  29. return false;
  30. }
  31. bool itemDidCompleteSuccessfullyWithExpectedRank(const ItemCompletedSpy &spy, const QString &path, int rank)
  32. {
  33. if (auto item = spy.findItemWithExpectedRank(path, rank)) {
  34. return item->_status == SyncFileItem::Success;
  35. }
  36. return false;
  37. }
  38. int itemSuccessfullyCompletedGetRank(const ItemCompletedSpy &spy, const QString &path)
  39. {
  40. auto itItem = std::find_if(spy.begin(), spy.end(), [&path] (auto currentItem) {
  41. auto item = currentItem[0].template value<OCC::SyncFileItemPtr>();
  42. return item->destination() == path;
  43. });
  44. if (itItem != spy.end()) {
  45. return itItem - spy.begin();
  46. }
  47. return -1;
  48. }
  49. class TestSyncEngine : public QObject
  50. {
  51. Q_OBJECT
  52. private slots:
  53. void testFileDownload() {
  54. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  55. ItemCompletedSpy completeSpy(fakeFolder);
  56. fakeFolder.remoteModifier().insert("A/a0");
  57. fakeFolder.syncOnce();
  58. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a0"));
  59. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  60. }
  61. void testFileUpload() {
  62. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  63. ItemCompletedSpy completeSpy(fakeFolder);
  64. fakeFolder.localModifier().insert("A/a0");
  65. fakeFolder.syncOnce();
  66. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a0"));
  67. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  68. }
  69. void testDirDownload() {
  70. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  71. ItemCompletedSpy completeSpy(fakeFolder);
  72. fakeFolder.remoteModifier().mkdir("Y");
  73. fakeFolder.remoteModifier().mkdir("Z");
  74. fakeFolder.remoteModifier().insert("Z/d0");
  75. fakeFolder.syncOnce();
  76. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Y"));
  77. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z"));
  78. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z/d0"));
  79. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  80. }
  81. void testDirUpload() {
  82. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  83. ItemCompletedSpy completeSpy(fakeFolder);
  84. fakeFolder.localModifier().mkdir("Y");
  85. fakeFolder.localModifier().mkdir("Z");
  86. fakeFolder.localModifier().insert("Z/d0");
  87. fakeFolder.syncOnce();
  88. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Y"));
  89. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z"));
  90. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z/d0"));
  91. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  92. }
  93. void testDirUploadWithDelayedAlgorithm() {
  94. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  95. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"bulkupload", "1.0"} } } });
  96. ItemCompletedSpy completeSpy(fakeFolder);
  97. fakeFolder.localModifier().mkdir("Y");
  98. fakeFolder.localModifier().insert("Y/d0");
  99. fakeFolder.localModifier().mkdir("Z");
  100. fakeFolder.localModifier().insert("Z/d0");
  101. fakeFolder.localModifier().insert("A/a0");
  102. fakeFolder.localModifier().insert("B/b0");
  103. fakeFolder.localModifier().insert("r0");
  104. fakeFolder.localModifier().insert("r1");
  105. fakeFolder.syncOnce();
  106. QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "Y", 0));
  107. QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "Z", 1));
  108. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Y/d0"));
  109. QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "Y/d0") > 1);
  110. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z/d0"));
  111. QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "Z/d0") > 1);
  112. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a0"));
  113. QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "A/a0") > 1);
  114. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "B/b0"));
  115. QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "B/b0") > 1);
  116. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "r0"));
  117. QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "r0") > 1);
  118. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "r1"));
  119. QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "r1") > 1);
  120. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  121. }
  122. void testLocalDelete() {
  123. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  124. ItemCompletedSpy completeSpy(fakeFolder);
  125. fakeFolder.remoteModifier().remove("A/a1");
  126. fakeFolder.syncOnce();
  127. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a1"));
  128. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  129. }
  130. void testRemoteDelete() {
  131. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  132. ItemCompletedSpy completeSpy(fakeFolder);
  133. fakeFolder.localModifier().remove("A/a1");
  134. fakeFolder.syncOnce();
  135. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a1"));
  136. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  137. }
  138. void testLocalDeleteWithReuploadForNewLocalFiles()
  139. {
  140. FakeFolder fakeFolder{FileInfo{}};
  141. // create folders hierarchy with some nested dirs and files
  142. fakeFolder.localModifier().mkdir("A");
  143. fakeFolder.localModifier().insert("A/existingfile_A.txt", 100);
  144. fakeFolder.localModifier().mkdir("A/B");
  145. fakeFolder.localModifier().insert("A/B/existingfile_B.data", 100);
  146. fakeFolder.localModifier().mkdir("A/B/C");
  147. fakeFolder.localModifier().mkdir("A/B/C/c1");
  148. fakeFolder.localModifier().mkdir("A/B/C/c1/c2");
  149. fakeFolder.localModifier().insert("A/B/C/c1/c2/existingfile_C2.md", 100);
  150. QVERIFY(fakeFolder.syncOnce());
  151. // make sure everything is uploaded
  152. QVERIFY(fakeFolder.currentRemoteState().find("A/B/C/c1/c2"));
  153. QVERIFY(fakeFolder.currentRemoteState().find("A/existingfile_A.txt"));
  154. QVERIFY(fakeFolder.currentRemoteState().find("A/B/existingfile_B.data"));
  155. QVERIFY(fakeFolder.currentRemoteState().find("A/B/C/c1/c2/existingfile_C2.md"));
  156. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  157. // remove a folder "A" on the server
  158. fakeFolder.remoteModifier().remove("A");
  159. // put new files and folders into a local folder "A"
  160. fakeFolder.localModifier().insert("A/B/C/c1/c2/newfile.txt", 100);
  161. fakeFolder.localModifier().insert("A/B/C/c1/c2/Readme.data", 100);
  162. fakeFolder.localModifier().mkdir("A/B/C/c1/c2/newfiles");
  163. fakeFolder.localModifier().insert("A/B/C/c1/c2/newfiles/newfile.txt", 100);
  164. fakeFolder.localModifier().insert("A/B/C/c1/c2/newfiles/Readme.data", 100);
  165. QVERIFY(fakeFolder.syncOnce());
  166. // make sure new files and folders are uploaded (restored)
  167. QVERIFY(fakeFolder.currentLocalState().find("A/B/C/c1/c2"));
  168. QVERIFY(fakeFolder.currentLocalState().find("A/B/C/c1/c2/Readme.data"));
  169. QVERIFY(fakeFolder.currentLocalState().find("A/B/C/c1/c2/newfiles/newfile.txt"));
  170. QVERIFY(fakeFolder.currentLocalState().find("A/B/C/c1/c2/newfiles/Readme.data"));
  171. // and the old files are removed
  172. QVERIFY(!fakeFolder.currentLocalState().find("A/existingfile_A.txt"));
  173. QVERIFY(!fakeFolder.currentLocalState().find("A/B/existingfile_B.data"));
  174. QVERIFY(!fakeFolder.currentLocalState().find("A/B/C/c1/c2/existingfile_C2.md"));
  175. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  176. }
  177. void testEmlLocalChecksum() {
  178. FakeFolder fakeFolder{FileInfo{}};
  179. fakeFolder.localModifier().insert("a1.eml", 64, 'A');
  180. fakeFolder.localModifier().insert("a2.eml", 64, 'A');
  181. fakeFolder.localModifier().insert("a3.eml", 64, 'A');
  182. fakeFolder.localModifier().insert("b3.txt", 64, 'A');
  183. // Upload and calculate the checksums
  184. // fakeFolder.syncOnce();
  185. fakeFolder.syncOnce();
  186. auto getDbChecksum = [&](QString path) {
  187. SyncJournalFileRecord record;
  188. [[maybe_unused]] const auto result = fakeFolder.syncJournal().getFileRecord(path, &record);
  189. return record._checksumHeader;
  190. };
  191. // printf 'A%.0s' {1..64} | sha1sum -
  192. QByteArray referenceChecksum("SHA1:30b86e44e6001403827a62c58b08893e77cf121f");
  193. QCOMPARE(getDbChecksum("a1.eml"), referenceChecksum);
  194. QCOMPARE(getDbChecksum("a2.eml"), referenceChecksum);
  195. QCOMPARE(getDbChecksum("a3.eml"), referenceChecksum);
  196. QCOMPARE(getDbChecksum("b3.txt"), referenceChecksum);
  197. ItemCompletedSpy completeSpy(fakeFolder);
  198. // Touch the file without changing the content, shouldn't upload
  199. fakeFolder.localModifier().setContents("a1.eml", 'A');
  200. // Change the content/size
  201. fakeFolder.localModifier().setContents("a2.eml", 'B');
  202. fakeFolder.localModifier().appendByte("a3.eml");
  203. fakeFolder.localModifier().appendByte("b3.txt");
  204. fakeFolder.syncOnce();
  205. QCOMPARE(getDbChecksum("a1.eml"), referenceChecksum);
  206. QCOMPARE(getDbChecksum("a2.eml"), QByteArray("SHA1:84951fc23a4dafd10020ac349da1f5530fa65949"));
  207. QCOMPARE(getDbChecksum("a3.eml"), QByteArray("SHA1:826b7e7a7af8a529ae1c7443c23bf185c0ad440c"));
  208. QCOMPARE(getDbChecksum("b3.eml"), getDbChecksum("a3.txt"));
  209. QVERIFY(!itemDidComplete(completeSpy, "a1.eml"));
  210. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "a2.eml"));
  211. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "a3.eml"));
  212. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  213. }
  214. void testSelectiveSyncBug() {
  215. // issue owncloud/enterprise#1965: files from selective-sync ignored
  216. // folders are uploaded anyway is some circumstances.
  217. FakeFolder fakeFolder{FileInfo{ QString(), {
  218. FileInfo { QStringLiteral("parentFolder"), {
  219. FileInfo{ QStringLiteral("subFolderA"), {
  220. { QStringLiteral("fileA.txt"), 400 },
  221. { QStringLiteral("fileB.txt"), 400, 'o' },
  222. FileInfo { QStringLiteral("subsubFolder"), {
  223. { QStringLiteral("fileC.txt"), 400 },
  224. { QStringLiteral("fileD.txt"), 400, 'o' }
  225. }},
  226. FileInfo{ QStringLiteral("anotherFolder"), {
  227. FileInfo { QStringLiteral("emptyFolder"), { } },
  228. FileInfo { QStringLiteral("subsubFolder"), {
  229. { QStringLiteral("fileE.txt"), 400 },
  230. { QStringLiteral("fileF.txt"), 400, 'o' }
  231. }}
  232. }}
  233. }},
  234. FileInfo{ QStringLiteral("subFolderB"), {} }
  235. }}
  236. }}};
  237. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  238. auto expectedServerState = fakeFolder.currentRemoteState();
  239. // Remove subFolderA with selectiveSync:
  240. fakeFolder.syncEngine().journal()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList,
  241. {"parentFolder/subFolderA/"});
  242. fakeFolder.syncEngine().journal()->schedulePathForRemoteDiscovery(QByteArrayLiteral("parentFolder/subFolderA/"));
  243. auto getEtag = [&](const QByteArray &file) {
  244. SyncJournalFileRecord rec;
  245. [[maybe_unused]] const auto result = fakeFolder.syncJournal().getFileRecord(file, &rec);
  246. return rec._etag;
  247. };
  248. QVERIFY(getEtag("parentFolder") == "_invalid_");
  249. QVERIFY(getEtag("parentFolder/subFolderA") == "_invalid_");
  250. QVERIFY(getEtag("parentFolder/subFolderA/subsubFolder") != "_invalid_");
  251. // But touch local file before the next sync, such that the local folder
  252. // can't be removed
  253. fakeFolder.localModifier().setContents("parentFolder/subFolderA/fileB.txt", 'n');
  254. fakeFolder.localModifier().setContents("parentFolder/subFolderA/subsubFolder/fileD.txt", 'n');
  255. fakeFolder.localModifier().setContents("parentFolder/subFolderA/anotherFolder/subsubFolder/fileF.txt", 'n');
  256. // Several follow-up syncs don't change the state at all,
  257. // in particular the remote state doesn't change and fileB.txt
  258. // isn't uploaded.
  259. for (int i = 0; i < 3; ++i) {
  260. fakeFolder.syncOnce();
  261. {
  262. // Nothing changed on the server
  263. QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
  264. // The local state should still have subFolderA
  265. auto local = fakeFolder.currentLocalState();
  266. QVERIFY(local.find("parentFolder/subFolderA"));
  267. QVERIFY(!local.find("parentFolder/subFolderA/fileA.txt"));
  268. QVERIFY(local.find("parentFolder/subFolderA/fileB.txt"));
  269. QVERIFY(!local.find("parentFolder/subFolderA/subsubFolder/fileC.txt"));
  270. QVERIFY(local.find("parentFolder/subFolderA/subsubFolder/fileD.txt"));
  271. QVERIFY(!local.find("parentFolder/subFolderA/anotherFolder/subsubFolder/fileE.txt"));
  272. QVERIFY(local.find("parentFolder/subFolderA/anotherFolder/subsubFolder/fileF.txt"));
  273. QVERIFY(!local.find("parentFolder/subFolderA/anotherFolder/emptyFolder"));
  274. QVERIFY(local.find("parentFolder/subFolderB"));
  275. }
  276. }
  277. }
  278. void abortAfterFailedMkdir() {
  279. FakeFolder fakeFolder{FileInfo{}};
  280. QSignalSpy finishedSpy(&fakeFolder.syncEngine(), SIGNAL(finished(bool)));
  281. fakeFolder.serverErrorPaths().append("NewFolder");
  282. fakeFolder.localModifier().mkdir("NewFolder");
  283. // This should be aborted and would otherwise fail in FileInfo::create.
  284. fakeFolder.localModifier().insert("NewFolder/NewFile");
  285. fakeFolder.syncOnce();
  286. QCOMPARE(finishedSpy.size(), 1);
  287. QCOMPARE(finishedSpy.first().first().toBool(), false);
  288. }
  289. /** Verify that an incompletely propagated directory doesn't have the server's
  290. * etag stored in the database yet. */
  291. void testDirEtagAfterIncompleteSync() {
  292. FakeFolder fakeFolder{FileInfo{}};
  293. QSignalSpy finishedSpy(&fakeFolder.syncEngine(), SIGNAL(finished(bool)));
  294. fakeFolder.serverErrorPaths().append("NewFolder/foo");
  295. fakeFolder.remoteModifier().mkdir("NewFolder");
  296. fakeFolder.remoteModifier().insert("NewFolder/foo");
  297. QVERIFY(!fakeFolder.syncOnce());
  298. SyncJournalFileRecord rec;
  299. QVERIFY(fakeFolder.syncJournal().getFileRecord(QByteArrayLiteral("NewFolder"), &rec) && rec.isValid());
  300. QCOMPARE(rec._etag, QByteArrayLiteral("_invalid_"));
  301. QVERIFY(!rec._fileId.isEmpty());
  302. }
  303. void testDirDownloadWithError() {
  304. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  305. ItemCompletedSpy completeSpy(fakeFolder);
  306. fakeFolder.remoteModifier().mkdir("Y");
  307. fakeFolder.remoteModifier().mkdir("Y/Z");
  308. fakeFolder.remoteModifier().insert("Y/Z/d0");
  309. fakeFolder.remoteModifier().insert("Y/Z/d1");
  310. fakeFolder.remoteModifier().insert("Y/Z/d2");
  311. fakeFolder.remoteModifier().insert("Y/Z/d3");
  312. fakeFolder.remoteModifier().insert("Y/Z/d4");
  313. fakeFolder.remoteModifier().insert("Y/Z/d5");
  314. fakeFolder.remoteModifier().insert("Y/Z/d6");
  315. fakeFolder.remoteModifier().insert("Y/Z/d7");
  316. fakeFolder.remoteModifier().insert("Y/Z/d8");
  317. fakeFolder.remoteModifier().insert("Y/Z/d9");
  318. fakeFolder.serverErrorPaths().append("Y/Z/d2", 503);
  319. fakeFolder.serverErrorPaths().append("Y/Z/d3", 503);
  320. QVERIFY(!fakeFolder.syncOnce());
  321. QCoreApplication::processEvents(); // should not crash
  322. QSet<QString> seen;
  323. for(const QList<QVariant> &args : completeSpy) {
  324. auto item = args[0].value<SyncFileItemPtr>();
  325. qDebug() << item->_file << item->isDirectory() << item->_status;
  326. QVERIFY(!seen.contains(item->_file)); // signal only sent once per item
  327. seen.insert(item->_file);
  328. if (item->_file == "Y/Z/d2") {
  329. QVERIFY(item->_status == SyncFileItem::NormalError);
  330. } else if (item->_file == "Y/Z/d3") {
  331. QVERIFY(item->_status != SyncFileItem::Success);
  332. } else if (!item->isDirectory()) {
  333. QVERIFY(item->_status == SyncFileItem::Success);
  334. }
  335. }
  336. }
  337. void testFakeConflict_data()
  338. {
  339. QTest::addColumn<bool>("sameMtime");
  340. QTest::addColumn<QByteArray>("checksums");
  341. QTest::addColumn<int>("expectedGET");
  342. QTest::newRow("Same mtime, but no server checksum -> ignored in reconcile")
  343. << true << QByteArray()
  344. << 0;
  345. QTest::newRow("Same mtime, weak server checksum differ -> downloaded")
  346. << true << QByteArray("Adler32:bad")
  347. << 1;
  348. QTest::newRow("Same mtime, matching weak checksum -> skipped")
  349. << true << QByteArray("Adler32:2a2010d")
  350. << 0;
  351. QTest::newRow("Same mtime, strong server checksum differ -> downloaded")
  352. << true << QByteArray("SHA1:bad")
  353. << 1;
  354. QTest::newRow("Same mtime, matching strong checksum -> skipped")
  355. << true << QByteArray("SHA1:56900fb1d337cf7237ff766276b9c1e8ce507427")
  356. << 0;
  357. QTest::newRow("mtime changed, but no server checksum -> download")
  358. << false << QByteArray()
  359. << 1;
  360. QTest::newRow("mtime changed, weak checksum match -> download anyway")
  361. << false << QByteArray("Adler32:2a2010d")
  362. << 1;
  363. QTest::newRow("mtime changed, strong checksum match -> skip")
  364. << false << QByteArray("SHA1:56900fb1d337cf7237ff766276b9c1e8ce507427")
  365. << 0;
  366. }
  367. void testFakeConflict()
  368. {
  369. QFETCH(bool, sameMtime);
  370. QFETCH(QByteArray, checksums);
  371. QFETCH(int, expectedGET);
  372. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  373. int nGET = 0;
  374. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &, QIODevice *) {
  375. if (op == QNetworkAccessManager::GetOperation)
  376. ++nGET;
  377. return nullptr;
  378. });
  379. // For directly editing the remote checksum
  380. auto &remoteInfo = fakeFolder.remoteModifier();
  381. // Base mtime with no ms content (filesystem is seconds only)
  382. auto mtime = QDateTime::currentDateTimeUtc().addDays(-4);
  383. mtime.setMSecsSinceEpoch(mtime.toMSecsSinceEpoch() / 1000 * 1000);
  384. fakeFolder.localModifier().setContents("A/a1", 'C');
  385. fakeFolder.localModifier().setModTime("A/a1", mtime);
  386. fakeFolder.remoteModifier().setContents("A/a1", 'C');
  387. if (!sameMtime)
  388. mtime = mtime.addDays(1);
  389. fakeFolder.remoteModifier().setModTime("A/a1", mtime);
  390. remoteInfo.find("A/a1")->checksums = checksums;
  391. QVERIFY(fakeFolder.syncOnce());
  392. QCOMPARE(nGET, expectedGET);
  393. // check that mtime in journal and filesystem agree
  394. QString a1path = fakeFolder.localPath() + "A/a1";
  395. SyncJournalFileRecord a1record;
  396. QVERIFY(fakeFolder.syncJournal().getFileRecord(QByteArray("A/a1"), &a1record));
  397. QCOMPARE(a1record._modtime, (qint64)FileSystem::getModTime(a1path));
  398. // Extra sync reads from db, no difference
  399. QVERIFY(fakeFolder.syncOnce());
  400. QCOMPARE(nGET, expectedGET);
  401. }
  402. /**
  403. * Checks whether SyncFileItems have the expected properties before start
  404. * of propagation.
  405. */
  406. void testSyncFileItemProperties()
  407. {
  408. auto initialMtime = QDateTime::currentDateTimeUtc().addDays(-7);
  409. auto changedMtime = QDateTime::currentDateTimeUtc().addDays(-4);
  410. auto changedMtime2 = QDateTime::currentDateTimeUtc().addDays(-3);
  411. // Base mtime with no ms content (filesystem is seconds only)
  412. initialMtime.setMSecsSinceEpoch(initialMtime.toMSecsSinceEpoch() / 1000 * 1000);
  413. changedMtime.setMSecsSinceEpoch(changedMtime.toMSecsSinceEpoch() / 1000 * 1000);
  414. changedMtime2.setMSecsSinceEpoch(changedMtime2.toMSecsSinceEpoch() / 1000 * 1000);
  415. // Ensure the initial mtimes are as expected
  416. auto initialFileInfo = FileInfo::A12_B12_C12_S12();
  417. initialFileInfo.setModTime("A/a1", initialMtime);
  418. initialFileInfo.setModTime("B/b1", initialMtime);
  419. initialFileInfo.setModTime("C/c1", initialMtime);
  420. FakeFolder fakeFolder{ initialFileInfo };
  421. // upload a
  422. fakeFolder.localModifier().appendByte("A/a1");
  423. fakeFolder.localModifier().setModTime("A/a1", changedMtime);
  424. // download b
  425. fakeFolder.remoteModifier().appendByte("B/b1");
  426. fakeFolder.remoteModifier().setModTime("B/b1", changedMtime);
  427. // conflict c
  428. fakeFolder.localModifier().appendByte("C/c1");
  429. fakeFolder.localModifier().appendByte("C/c1");
  430. fakeFolder.localModifier().setModTime("C/c1", changedMtime);
  431. fakeFolder.remoteModifier().appendByte("C/c1");
  432. fakeFolder.remoteModifier().setModTime("C/c1", changedMtime2);
  433. connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToPropagate, [&](SyncFileItemVector &items) {
  434. SyncFileItemPtr a1, b1, c1;
  435. for (auto &item : items) {
  436. if (item->_file == "A/a1")
  437. a1 = item;
  438. if (item->_file == "B/b1")
  439. b1 = item;
  440. if (item->_file == "C/c1")
  441. c1 = item;
  442. }
  443. // a1: should have local size and modtime
  444. QVERIFY(a1);
  445. QCOMPARE(a1->_instruction, CSYNC_INSTRUCTION_SYNC);
  446. QCOMPARE(a1->_direction, SyncFileItem::Up);
  447. QCOMPARE(a1->_size, qint64(5));
  448. QCOMPARE(Utility::qDateTimeFromTime_t(a1->_modtime), changedMtime);
  449. QCOMPARE(a1->_previousSize, qint64(4));
  450. QCOMPARE(Utility::qDateTimeFromTime_t(a1->_previousModtime), initialMtime);
  451. // b2: should have remote size and modtime
  452. QVERIFY(b1);
  453. QCOMPARE(b1->_instruction, CSYNC_INSTRUCTION_SYNC);
  454. QCOMPARE(b1->_direction, SyncFileItem::Down);
  455. QCOMPARE(b1->_size, qint64(17));
  456. QCOMPARE(Utility::qDateTimeFromTime_t(b1->_modtime), changedMtime);
  457. QCOMPARE(b1->_previousSize, qint64(16));
  458. QCOMPARE(Utility::qDateTimeFromTime_t(b1->_previousModtime), initialMtime);
  459. // c1: conflicts are downloads, so remote size and modtime
  460. QVERIFY(c1);
  461. QCOMPARE(c1->_instruction, CSYNC_INSTRUCTION_CONFLICT);
  462. QCOMPARE(c1->_direction, SyncFileItem::None);
  463. QCOMPARE(c1->_size, qint64(25));
  464. QCOMPARE(Utility::qDateTimeFromTime_t(c1->_modtime), changedMtime2);
  465. QCOMPARE(c1->_previousSize, qint64(26));
  466. QCOMPARE(Utility::qDateTimeFromTime_t(c1->_previousModtime), changedMtime);
  467. });
  468. QVERIFY(fakeFolder.syncOnce());
  469. }
  470. /**
  471. * Checks whether subsequent large uploads are skipped after a 507 error
  472. */
  473. void testInsufficientRemoteStorage()
  474. {
  475. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  476. // Disable parallel uploads
  477. SyncOptions syncOptions;
  478. syncOptions._parallelNetworkJobs = 0;
  479. fakeFolder.syncEngine().setSyncOptions(syncOptions);
  480. // Produce an error based on upload size
  481. int remoteQuota = 1000;
  482. int n507 = 0, nPUT = 0;
  483. QObject parent;
  484. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * {
  485. Q_UNUSED(outgoingData)
  486. if (op == QNetworkAccessManager::PutOperation) {
  487. nPUT++;
  488. if (request.rawHeader("OC-Total-Length").toInt() > remoteQuota) {
  489. n507++;
  490. return new FakeErrorReply(op, request, &parent, 507);
  491. }
  492. }
  493. return nullptr;
  494. });
  495. fakeFolder.localModifier().insert("A/big", 800);
  496. QVERIFY(fakeFolder.syncOnce());
  497. QCOMPARE(nPUT, 1);
  498. QCOMPARE(n507, 0);
  499. nPUT = 0;
  500. fakeFolder.localModifier().insert("A/big1", 500); // ok
  501. fakeFolder.localModifier().insert("A/big2", 1200); // 507 (quota guess now 1199)
  502. fakeFolder.localModifier().insert("A/big3", 1200); // skipped
  503. fakeFolder.localModifier().insert("A/big4", 1500); // skipped
  504. fakeFolder.localModifier().insert("A/big5", 1100); // 507 (quota guess now 1099)
  505. fakeFolder.localModifier().insert("A/big6", 900); // ok (quota guess now 199)
  506. fakeFolder.localModifier().insert("A/big7", 200); // skipped
  507. fakeFolder.localModifier().insert("A/big8", 199); // ok (quota guess now 0)
  508. fakeFolder.localModifier().insert("B/big8", 1150); // 507
  509. QVERIFY(!fakeFolder.syncOnce());
  510. QCOMPARE(nPUT, 6);
  511. QCOMPARE(n507, 3);
  512. }
  513. // Checks whether downloads with bad checksums are accepted
  514. void testChecksumValidation()
  515. {
  516. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  517. QObject parent;
  518. QByteArray checksumValue;
  519. QByteArray checksumValueRecalculated;
  520. QByteArray contentMd5Value;
  521. bool isChecksumRecalculateSupported = false;
  522. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  523. if (op == QNetworkAccessManager::GetOperation) {
  524. auto reply = new FakeGetReply(fakeFolder.remoteModifier(), op, request, &parent);
  525. if (!checksumValue.isNull())
  526. reply->setRawHeader(OCC::checkSumHeaderC, checksumValue);
  527. if (!contentMd5Value.isNull())
  528. reply->setRawHeader(OCC::contentMd5HeaderC, contentMd5Value);
  529. return reply;
  530. } else if (op == QNetworkAccessManager::CustomOperation) {
  531. if (request.hasRawHeader(OCC::checksumRecalculateOnServerHeaderC)) {
  532. if (!isChecksumRecalculateSupported) {
  533. return new FakeErrorReply(op, request, &parent, 402);
  534. }
  535. auto reply = new FakeGetReply(fakeFolder.remoteModifier(), op, request, &parent);
  536. reply->setRawHeader(OCC::checkSumHeaderC, checksumValueRecalculated);
  537. return reply;
  538. }
  539. }
  540. return nullptr;
  541. });
  542. // Basic case
  543. fakeFolder.remoteModifier().create("A/a3", 16, 'A');
  544. QVERIFY(fakeFolder.syncOnce());
  545. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  546. // Bad OC-Checksum
  547. checksumValue = "SHA1:bad";
  548. fakeFolder.remoteModifier().create("A/a4", 16, 'A');
  549. QVERIFY(!fakeFolder.syncOnce());
  550. const QByteArray matchedSha1Checksum(QByteArrayLiteral("SHA1:19b1928d58a2030d08023f3d7054516dbc186f20"));
  551. const QByteArray mismatchedSha1Checksum(matchedSha1Checksum.chopped(1));
  552. // Good OC-Checksum
  553. checksumValue = matchedSha1Checksum; // printf 'A%.0s' {1..16} | sha1sum -
  554. QVERIFY(fakeFolder.syncOnce());
  555. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  556. checksumValue = QByteArray();
  557. // Bad Content-MD5
  558. contentMd5Value = "bad";
  559. fakeFolder.remoteModifier().create("A/a5", 16, 'A');
  560. QVERIFY(!fakeFolder.syncOnce());
  561. // Good Content-MD5
  562. contentMd5Value = "d8a73157ce10cd94a91c2079fc9a92c8"; // printf 'A%.0s' {1..16} | md5sum -
  563. QVERIFY(fakeFolder.syncOnce());
  564. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  565. // Invalid OC-Checksum is ignored
  566. checksumValue = "garbage";
  567. // contentMd5Value is still good
  568. fakeFolder.remoteModifier().create("A/a6", 16, 'A');
  569. QVERIFY(fakeFolder.syncOnce());
  570. contentMd5Value = "bad";
  571. fakeFolder.remoteModifier().create("A/a7", 16, 'A');
  572. QVERIFY(!fakeFolder.syncOnce());
  573. contentMd5Value.clear();
  574. QVERIFY(fakeFolder.syncOnce());
  575. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  576. // OC-Checksum contains Unsupported checksums
  577. checksumValue = "Unsupported:XXXX SHA1:invalid Invalid:XxX";
  578. fakeFolder.remoteModifier().create("A/a8", 16, 'A');
  579. QVERIFY(!fakeFolder.syncOnce()); // Since the supported SHA1 checksum is invalid, no download
  580. checksumValue = "Unsupported:XXXX SHA1:19b1928d58a2030d08023f3d7054516dbc186f20 Invalid:XxX";
  581. QVERIFY(fakeFolder.syncOnce()); // The supported SHA1 checksum is valid now, so the file are downloaded
  582. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  583. // Begin Test mismatch recalculation---------------------------------------------------------------------------------
  584. const auto prevServerVersion = fakeFolder.account()->serverVersion();
  585. fakeFolder.account()->setServerVersion(QString("%1.0.0").arg(fakeFolder.account()->checksumRecalculateServerVersionMinSupportedMajor()));
  586. // Mismatched OC-Checksum and X-Recalculate-Hash is not supported -> sync must fail
  587. isChecksumRecalculateSupported = false;
  588. checksumValue = mismatchedSha1Checksum;
  589. checksumValueRecalculated = matchedSha1Checksum;
  590. fakeFolder.remoteModifier().create("A/a9", 16, 'A');
  591. QVERIFY(!fakeFolder.syncOnce());
  592. // Mismatched OC-Checksum and X-Recalculate-Hash is supported, but, recalculated checksum is again mismatched -> sync must fail
  593. isChecksumRecalculateSupported = true;
  594. checksumValue = mismatchedSha1Checksum;
  595. checksumValueRecalculated = mismatchedSha1Checksum;
  596. QVERIFY(!fakeFolder.syncOnce());
  597. // Mismatched OC-Checksum and X-Recalculate-Hash is supported, and, recalculated checksum is a match -> sync must succeed
  598. isChecksumRecalculateSupported = true;
  599. checksumValue = mismatchedSha1Checksum;
  600. checksumValueRecalculated = matchedSha1Checksum;
  601. QVERIFY(fakeFolder.syncOnce());
  602. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  603. checksumValue = QByteArray();
  604. fakeFolder.account()->setServerVersion(prevServerVersion);
  605. // End Test mismatch recalculation-----------------------------------------------------------------------------------
  606. }
  607. // Tests the behavior of invalid filename detection
  608. void testInvalidFilenameRegex()
  609. {
  610. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  611. #ifndef Q_OS_WIN // We can't have local file with these character
  612. // For current servers, no characters are forbidden
  613. fakeFolder.syncEngine().account()->setServerVersion("10.0.0");
  614. fakeFolder.localModifier().insert("A/\\:?*\"<>|.txt");
  615. QVERIFY(fakeFolder.syncOnce());
  616. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  617. // For legacy servers, some characters were forbidden by the client
  618. fakeFolder.syncEngine().account()->setServerVersion("8.0.0");
  619. fakeFolder.localModifier().insert("B/\\:?*\"<>|.txt");
  620. QVERIFY(fakeFolder.syncOnce());
  621. QVERIFY(!fakeFolder.currentRemoteState().find("B/\\:?*\"<>|.txt"));
  622. #endif
  623. // We can override that by setting the capability
  624. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "invalidFilenameRegex", "" } } } });
  625. QVERIFY(fakeFolder.syncOnce());
  626. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  627. // Check that new servers also accept the capability
  628. fakeFolder.syncEngine().account()->setServerVersion("10.0.0");
  629. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "invalidFilenameRegex", "my[fgh]ile" } } } });
  630. fakeFolder.localModifier().insert("C/myfile.txt");
  631. QVERIFY(fakeFolder.syncOnce());
  632. QVERIFY(!fakeFolder.currentRemoteState().find("C/myfile.txt"));
  633. }
  634. void testDiscoveryHiddenFile()
  635. {
  636. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  637. QVERIFY(fakeFolder.syncOnce());
  638. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  639. // We can't depend on currentLocalState for hidden files since
  640. // it should rightfully skip things like download temporaries
  641. auto localFileExists = [&](QString name) {
  642. return QFileInfo(fakeFolder.localPath() + name).exists();
  643. };
  644. fakeFolder.syncEngine().setIgnoreHiddenFiles(true);
  645. fakeFolder.remoteModifier().insert("A/.hidden");
  646. fakeFolder.localModifier().insert("B/.hidden");
  647. QVERIFY(fakeFolder.syncOnce());
  648. QVERIFY(!localFileExists("A/.hidden"));
  649. QVERIFY(!fakeFolder.currentRemoteState().find("B/.hidden"));
  650. fakeFolder.syncEngine().setIgnoreHiddenFiles(false);
  651. fakeFolder.syncJournal().forceRemoteDiscoveryNextSync();
  652. QVERIFY(fakeFolder.syncOnce());
  653. QVERIFY(localFileExists("A/.hidden"));
  654. QVERIFY(fakeFolder.currentRemoteState().find("B/.hidden"));
  655. }
  656. void testNoLocalEncoding()
  657. {
  658. auto utf8Locale = QTextCodec::codecForLocale();
  659. if (utf8Locale->mibEnum() != 106) {
  660. QSKIP("Test only works for UTF8 locale");
  661. }
  662. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  663. QVERIFY(fakeFolder.syncOnce());
  664. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  665. // Utf8 locale can sync both
  666. fakeFolder.remoteModifier().insert("A/tößt");
  667. fakeFolder.remoteModifier().insert("A/t𠜎t");
  668. QVERIFY(fakeFolder.syncOnce());
  669. QVERIFY(fakeFolder.currentLocalState().find("A/tößt"));
  670. QVERIFY(fakeFolder.currentLocalState().find("A/t𠜎t"));
  671. #if !defined(Q_OS_MAC) && !defined(Q_OS_WIN)
  672. // Try again with a locale that can represent ö but not 𠜎 (4-byte utf8).
  673. QTextCodec::setCodecForLocale(QTextCodec::codecForName("ISO-8859-15"));
  674. QVERIFY(QTextCodec::codecForLocale()->mibEnum() == 111);
  675. fakeFolder.remoteModifier().insert("B/tößt");
  676. fakeFolder.remoteModifier().insert("B/t𠜎t");
  677. QVERIFY(fakeFolder.syncOnce());
  678. QVERIFY(fakeFolder.currentLocalState().find("B/tößt"));
  679. QVERIFY(!fakeFolder.currentLocalState().find("B/t𠜎t"));
  680. QVERIFY(!fakeFolder.currentLocalState().find("B/t?t"));
  681. QVERIFY(!fakeFolder.currentLocalState().find("B/t??t"));
  682. QVERIFY(!fakeFolder.currentLocalState().find("B/t???t"));
  683. QVERIFY(!fakeFolder.currentLocalState().find("B/t????t"));
  684. QVERIFY(fakeFolder.syncOnce());
  685. QVERIFY(fakeFolder.currentRemoteState().find("B/tößt"));
  686. QVERIFY(fakeFolder.currentRemoteState().find("B/t𠜎t"));
  687. // Try again with plain ascii
  688. QTextCodec::setCodecForLocale(QTextCodec::codecForName("ASCII"));
  689. QVERIFY(QTextCodec::codecForLocale()->mibEnum() == 3);
  690. fakeFolder.remoteModifier().insert("C/tößt");
  691. QVERIFY(fakeFolder.syncOnce());
  692. QVERIFY(!fakeFolder.currentLocalState().find("C/tößt"));
  693. QVERIFY(!fakeFolder.currentLocalState().find("C/t??t"));
  694. QVERIFY(!fakeFolder.currentLocalState().find("C/t????t"));
  695. QVERIFY(fakeFolder.syncOnce());
  696. QVERIFY(fakeFolder.currentRemoteState().find("C/tößt"));
  697. QTextCodec::setCodecForLocale(utf8Locale);
  698. #endif
  699. }
  700. // Aborting has had bugs when there are parallel upload jobs
  701. void testUploadV1Multiabort()
  702. {
  703. FakeFolder fakeFolder{ FileInfo{} };
  704. SyncOptions options;
  705. options._initialChunkSize = 10;
  706. options._maxChunkSize = 10;
  707. options._minChunkSize = 10;
  708. fakeFolder.syncEngine().setSyncOptions(options);
  709. QObject parent;
  710. int nPUT = 0;
  711. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  712. if (op == QNetworkAccessManager::PutOperation) {
  713. ++nPUT;
  714. return new FakeHangingReply(op, request, &parent);
  715. }
  716. return nullptr;
  717. });
  718. fakeFolder.localModifier().insert("file", 100, 'W');
  719. QTimer::singleShot(100, &fakeFolder.syncEngine(), [&]() { fakeFolder.syncEngine().abort(); });
  720. QVERIFY(!fakeFolder.syncOnce());
  721. QCOMPARE(nPUT, 3);
  722. }
  723. #ifndef Q_OS_WIN
  724. void testPropagatePermissions()
  725. {
  726. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  727. auto perm = QFileDevice::Permission(0x7704); // user/owner: rwx, group: r, other: -
  728. QFile::setPermissions(fakeFolder.localPath() + "A/a1", perm);
  729. QFile::setPermissions(fakeFolder.localPath() + "A/a2", perm);
  730. fakeFolder.syncOnce(); // get the metadata-only change out of the way
  731. fakeFolder.remoteModifier().appendByte("A/a1");
  732. fakeFolder.remoteModifier().appendByte("A/a2");
  733. fakeFolder.localModifier().appendByte("A/a2");
  734. fakeFolder.localModifier().appendByte("A/a2");
  735. fakeFolder.syncOnce(); // perms should be preserved
  736. QCOMPARE(QFileInfo(fakeFolder.localPath() + "A/a1").permissions(), perm);
  737. QCOMPARE(QFileInfo(fakeFolder.localPath() + "A/a2").permissions(), perm);
  738. auto conflictName = fakeFolder.syncJournal().conflictRecord(fakeFolder.syncJournal().conflictRecordPaths().first()).path;
  739. QVERIFY(conflictName.contains("A/a2"));
  740. QCOMPARE(QFileInfo(fakeFolder.localPath() + conflictName).permissions(), perm);
  741. }
  742. #endif
  743. void testEmptyLocalButHasRemote()
  744. {
  745. FakeFolder fakeFolder{ FileInfo{} };
  746. fakeFolder.remoteModifier().mkdir("foo");
  747. QVERIFY(fakeFolder.syncOnce());
  748. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  749. QVERIFY(fakeFolder.currentLocalState().find("foo"));
  750. }
  751. // Check that server mtime is set on directories on initial propagation
  752. void testDirectoryInitialMtime()
  753. {
  754. FakeFolder fakeFolder{ FileInfo{} };
  755. fakeFolder.remoteModifier().mkdir("foo");
  756. fakeFolder.remoteModifier().insert("foo/bar");
  757. auto datetime = QDateTime::currentDateTime();
  758. datetime.setSecsSinceEpoch(datetime.toSecsSinceEpoch()); // wipe ms
  759. fakeFolder.remoteModifier().find("foo")->lastModified = datetime;
  760. QVERIFY(fakeFolder.syncOnce());
  761. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  762. QCOMPARE(QFileInfo(fakeFolder.localPath() + "foo").lastModified(), datetime);
  763. }
  764. /**
  765. * Checks whether subsequent large uploads are skipped after a 507 error
  766. */
  767. void testErrorsWithBulkUpload()
  768. {
  769. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  770. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"bulkupload", "1.0"} } } });
  771. // Disable parallel uploads
  772. SyncOptions syncOptions;
  773. syncOptions._parallelNetworkJobs = 0;
  774. fakeFolder.syncEngine().setSyncOptions(syncOptions);
  775. int nPUT = 0;
  776. int nPOST = 0;
  777. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * {
  778. auto contentType = request.header(QNetworkRequest::ContentTypeHeader).toString();
  779. if (op == QNetworkAccessManager::PostOperation) {
  780. ++nPOST;
  781. if (contentType.startsWith(QStringLiteral("multipart/related; boundary="))) {
  782. auto jsonReplyObject = fakeFolder.forEachReplyPart(outgoingData, contentType, [] (const QMap<QString, QByteArray> &allHeaders) -> QJsonObject {
  783. auto reply = QJsonObject{};
  784. const auto fileName = allHeaders[QStringLiteral("X-File-Path")];
  785. if (fileName.endsWith("A/big2") ||
  786. fileName.endsWith("A/big3") ||
  787. fileName.endsWith("A/big4") ||
  788. fileName.endsWith("A/big5") ||
  789. fileName.endsWith("A/big7") ||
  790. fileName.endsWith("B/big8")) {
  791. reply.insert(QStringLiteral("error"), true);
  792. reply.insert(QStringLiteral("etag"), {});
  793. return reply;
  794. } else {
  795. reply.insert(QStringLiteral("error"), false);
  796. reply.insert(QStringLiteral("etag"), {});
  797. }
  798. return reply;
  799. });
  800. if (jsonReplyObject.size()) {
  801. auto jsonReply = QJsonDocument{};
  802. jsonReply.setObject(jsonReplyObject);
  803. return new FakeJsonErrorReply{op, request, this, 200, jsonReply};
  804. }
  805. return nullptr;
  806. }
  807. } else if (op == QNetworkAccessManager::PutOperation) {
  808. ++nPUT;
  809. const auto fileName = getFilePathFromUrl(request.url());
  810. if (fileName.endsWith("A/big2") ||
  811. fileName.endsWith("A/big3") ||
  812. fileName.endsWith("A/big4") ||
  813. fileName.endsWith("A/big5") ||
  814. fileName.endsWith("A/big7") ||
  815. fileName.endsWith("B/big8")) {
  816. return new FakeErrorReply(op, request, this, 412);
  817. }
  818. return nullptr;
  819. }
  820. return nullptr;
  821. });
  822. fakeFolder.localModifier().insert("A/big", 1);
  823. QVERIFY(fakeFolder.syncOnce());
  824. QCOMPARE(nPUT, 0);
  825. QCOMPARE(nPOST, 1);
  826. nPUT = 0;
  827. nPOST = 0;
  828. fakeFolder.localModifier().insert("A/big1", 1); // ok
  829. fakeFolder.localModifier().insert("A/big2", 1); // ko
  830. fakeFolder.localModifier().insert("A/big3", 1); // ko
  831. fakeFolder.localModifier().insert("A/big4", 1); // ko
  832. fakeFolder.localModifier().insert("A/big5", 1); // ko
  833. fakeFolder.localModifier().insert("A/big6", 1); // ok
  834. fakeFolder.localModifier().insert("A/big7", 1); // ko
  835. fakeFolder.localModifier().insert("A/big8", 1); // ok
  836. fakeFolder.localModifier().insert("B/big8", 1); // ko
  837. QVERIFY(!fakeFolder.syncOnce());
  838. QCOMPARE(nPUT, 0);
  839. QCOMPARE(nPOST, 1);
  840. nPUT = 0;
  841. nPOST = 0;
  842. QVERIFY(!fakeFolder.syncOnce());
  843. QCOMPARE(nPUT, 6);
  844. QCOMPARE(nPOST, 0);
  845. }
  846. /**
  847. * Checks whether subsequent large uploads are skipped after a 507 error
  848. */
  849. void testNetworkErrorsWithBulkUpload()
  850. {
  851. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  852. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"bulkupload", "1.0"} } } });
  853. // Disable parallel uploads
  854. SyncOptions syncOptions;
  855. syncOptions._parallelNetworkJobs = 0;
  856. fakeFolder.syncEngine().setSyncOptions(syncOptions);
  857. int nPUT = 0;
  858. int nPOST = 0;
  859. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  860. auto contentType = request.header(QNetworkRequest::ContentTypeHeader).toString();
  861. if (op == QNetworkAccessManager::PostOperation) {
  862. ++nPOST;
  863. if (contentType.startsWith(QStringLiteral("multipart/related; boundary="))) {
  864. return new FakeErrorReply(op, request, this, 400);
  865. }
  866. return nullptr;
  867. } else if (op == QNetworkAccessManager::PutOperation) {
  868. ++nPUT;
  869. }
  870. return nullptr;
  871. });
  872. fakeFolder.localModifier().insert("A/big1", 1);
  873. fakeFolder.localModifier().insert("A/big2", 1);
  874. fakeFolder.localModifier().insert("A/big3", 1);
  875. fakeFolder.localModifier().insert("A/big4", 1);
  876. fakeFolder.localModifier().insert("A/big5", 1);
  877. fakeFolder.localModifier().insert("A/big6", 1);
  878. fakeFolder.localModifier().insert("A/big7", 1);
  879. fakeFolder.localModifier().insert("A/big8", 1);
  880. fakeFolder.localModifier().insert("B/big8", 1);
  881. QVERIFY(!fakeFolder.syncOnce());
  882. QCOMPARE(nPUT, 0);
  883. QCOMPARE(nPOST, 1);
  884. nPUT = 0;
  885. nPOST = 0;
  886. QVERIFY(fakeFolder.syncOnce());
  887. QCOMPARE(nPUT, 9);
  888. QCOMPARE(nPOST, 0);
  889. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  890. }
  891. void testRemoteMoveFailedInsufficientStorageLocalMoveRolledBack()
  892. {
  893. FakeFolder fakeFolder{FileInfo{}};
  894. // create a big shared folder with some files
  895. fakeFolder.remoteModifier().mkdir("big_shared_folder");
  896. fakeFolder.remoteModifier().mkdir("big_shared_folder/shared_files");
  897. fakeFolder.remoteModifier().insert("big_shared_folder/shared_files/big_shared_file_A.data", 1000);
  898. fakeFolder.remoteModifier().insert("big_shared_folder/shared_files/big_shared_file_B.data", 1000);
  899. // make sure big shared folder is synced
  900. QVERIFY(fakeFolder.syncOnce());
  901. QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_A.data"));
  902. QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_B.data"));
  903. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  904. // try to move from a big shared folder to your own folder
  905. fakeFolder.localModifier().mkdir("own_folder");
  906. fakeFolder.localModifier().rename(
  907. "big_shared_folder/shared_files/big_shared_file_A.data", "own_folder/big_shared_file_A.data");
  908. fakeFolder.localModifier().rename(
  909. "big_shared_folder/shared_files/big_shared_file_B.data", "own_folder/big_shared_file_B.data");
  910. // emulate server MOVE 507 error
  911. QObject parent;
  912. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request,
  913. QIODevice *outgoingData) -> QNetworkReply * {
  914. Q_UNUSED(outgoingData)
  915. if (op == QNetworkAccessManager::CustomOperation
  916. && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("MOVE")) {
  917. return new FakeErrorReply(op, request, &parent, 507);
  918. }
  919. return nullptr;
  920. });
  921. // make sure the first sync failes and files get restored to original folder
  922. QVERIFY(!fakeFolder.syncOnce());
  923. QVERIFY(fakeFolder.syncOnce());
  924. QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_A.data"));
  925. QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_B.data"));
  926. QVERIFY(!fakeFolder.currentLocalState().find("own_folder/big_shared_file_A.data"));
  927. QVERIFY(!fakeFolder.currentLocalState().find("own_folder/big_shared_file_B.data"));
  928. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  929. }
  930. void testRemoteMoveFailedForbiddenLocalMoveRolledBack()
  931. {
  932. FakeFolder fakeFolder{FileInfo{}};
  933. // create a big shared folder with some files
  934. fakeFolder.remoteModifier().mkdir("big_shared_folder");
  935. fakeFolder.remoteModifier().mkdir("big_shared_folder/shared_files");
  936. fakeFolder.remoteModifier().insert("big_shared_folder/shared_files/big_shared_file_A.data", 1000);
  937. fakeFolder.remoteModifier().insert("big_shared_folder/shared_files/big_shared_file_B.data", 1000);
  938. // make sure big shared folder is synced
  939. QVERIFY(fakeFolder.syncOnce());
  940. QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_A.data"));
  941. QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_B.data"));
  942. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  943. // try to move from a big shared folder to your own folder
  944. fakeFolder.localModifier().mkdir("own_folder");
  945. fakeFolder.localModifier().rename(
  946. "big_shared_folder/shared_files/big_shared_file_A.data", "own_folder/big_shared_file_A.data");
  947. fakeFolder.localModifier().rename(
  948. "big_shared_folder/shared_files/big_shared_file_B.data", "own_folder/big_shared_file_B.data");
  949. // emulate server MOVE 507 error
  950. QObject parent;
  951. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request,
  952. QIODevice *outgoingData) -> QNetworkReply * {
  953. Q_UNUSED(outgoingData)
  954. auto attributeCustomVerb = request.attribute(QNetworkRequest::CustomVerbAttribute).toString();
  955. if (op == QNetworkAccessManager::CustomOperation
  956. && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("MOVE")) {
  957. return new FakeErrorReply(op, request, &parent, 403);
  958. }
  959. return nullptr;
  960. });
  961. // make sure the first sync failes and files get restored to original folder
  962. QVERIFY(!fakeFolder.syncOnce());
  963. QVERIFY(fakeFolder.syncOnce());
  964. QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_A.data"));
  965. QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_B.data"));
  966. QVERIFY(!fakeFolder.currentLocalState().find("own_folder/big_shared_file_A.data"));
  967. QVERIFY(!fakeFolder.currentLocalState().find("own_folder/big_shared_file_B.data"));
  968. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  969. }
  970. void testFolderWithFilesInError()
  971. {
  972. FakeFolder fakeFolder{FileInfo{}};
  973. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * {
  974. Q_UNUSED(outgoingData)
  975. if (op == QNetworkAccessManager::GetOperation) {
  976. const auto fileName = getFilePathFromUrl(request.url());
  977. if (fileName == QStringLiteral("aaa/subfolder/foo")) {
  978. return new FakeErrorReply(op, request, &fakeFolder.syncEngine(), 403);
  979. }
  980. }
  981. return nullptr;
  982. });
  983. fakeFolder.remoteModifier().mkdir(QStringLiteral("aaa"));
  984. fakeFolder.remoteModifier().mkdir(QStringLiteral("aaa/subfolder"));
  985. fakeFolder.remoteModifier().insert(QStringLiteral("aaa/subfolder/bar"));
  986. QVERIFY(fakeFolder.syncOnce());
  987. fakeFolder.remoteModifier().insert(QStringLiteral("aaa/subfolder/foo"));
  988. QVERIFY(!fakeFolder.syncOnce());
  989. QVERIFY(!fakeFolder.syncOnce());
  990. }
  991. void testInvalidMtimeRecoveryAtStart()
  992. {
  993. constexpr auto INVALID_MTIME = 0;
  994. constexpr auto CURRENT_MTIME = 1646057277;
  995. FakeFolder fakeFolder{FileInfo{}};
  996. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  997. const QString fooFileRootFolder("foo");
  998. const QString barFileRootFolder("bar");
  999. const QString fooFileSubFolder("subfolder/foo");
  1000. const QString barFileSubFolder("subfolder/bar");
  1001. const QString fooFileAaaSubFolder("aaa/subfolder/foo");
  1002. const QString barFileAaaSubFolder("aaa/subfolder/bar");
  1003. fakeFolder.remoteModifier().insert(fooFileRootFolder);
  1004. fakeFolder.remoteModifier().insert(barFileRootFolder);
  1005. fakeFolder.remoteModifier().mkdir(QStringLiteral("subfolder"));
  1006. fakeFolder.remoteModifier().insert(fooFileSubFolder);
  1007. fakeFolder.remoteModifier().insert(barFileSubFolder);
  1008. fakeFolder.remoteModifier().mkdir(QStringLiteral("aaa"));
  1009. fakeFolder.remoteModifier().mkdir(QStringLiteral("aaa/subfolder"));
  1010. fakeFolder.remoteModifier().insert(fooFileAaaSubFolder);
  1011. fakeFolder.remoteModifier().setModTime(fooFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(INVALID_MTIME));
  1012. fakeFolder.remoteModifier().insert(barFileAaaSubFolder);
  1013. fakeFolder.remoteModifier().setModTime(barFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(INVALID_MTIME));
  1014. QVERIFY(!fakeFolder.syncOnce());
  1015. QVERIFY(!fakeFolder.syncOnce());
  1016. fakeFolder.remoteModifier().setModTimeKeepEtag(fooFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(CURRENT_MTIME));
  1017. fakeFolder.remoteModifier().setModTimeKeepEtag(barFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(CURRENT_MTIME));
  1018. QVERIFY(fakeFolder.syncOnce());
  1019. QVERIFY(fakeFolder.syncOnce());
  1020. auto expectedState = fakeFolder.currentLocalState();
  1021. QCOMPARE(fakeFolder.currentRemoteState(), expectedState);
  1022. }
  1023. void testInvalidMtimeRecovery()
  1024. {
  1025. constexpr auto INVALID_MTIME = 0;
  1026. constexpr auto CURRENT_MTIME = 1646057277;
  1027. FakeFolder fakeFolder{FileInfo{}};
  1028. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  1029. const QString fooFileRootFolder("foo");
  1030. const QString barFileRootFolder("bar");
  1031. const QString fooFileSubFolder("subfolder/foo");
  1032. const QString barFileSubFolder("subfolder/bar");
  1033. const QString fooFileAaaSubFolder("aaa/subfolder/foo");
  1034. const QString barFileAaaSubFolder("aaa/subfolder/bar");
  1035. fakeFolder.remoteModifier().insert(fooFileRootFolder);
  1036. fakeFolder.remoteModifier().insert(barFileRootFolder);
  1037. fakeFolder.remoteModifier().mkdir(QStringLiteral("subfolder"));
  1038. fakeFolder.remoteModifier().insert(fooFileSubFolder);
  1039. fakeFolder.remoteModifier().insert(barFileSubFolder);
  1040. fakeFolder.remoteModifier().mkdir(QStringLiteral("aaa"));
  1041. fakeFolder.remoteModifier().mkdir(QStringLiteral("aaa/subfolder"));
  1042. fakeFolder.remoteModifier().insert(fooFileAaaSubFolder);
  1043. fakeFolder.remoteModifier().insert(barFileAaaSubFolder);
  1044. QVERIFY(fakeFolder.syncOnce());
  1045. fakeFolder.remoteModifier().setModTime(fooFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(INVALID_MTIME));
  1046. fakeFolder.remoteModifier().setModTime(barFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(INVALID_MTIME));
  1047. QVERIFY(!fakeFolder.syncOnce());
  1048. QVERIFY(!fakeFolder.syncOnce());
  1049. fakeFolder.remoteModifier().setModTimeKeepEtag(fooFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(CURRENT_MTIME));
  1050. fakeFolder.remoteModifier().setModTimeKeepEtag(barFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(CURRENT_MTIME));
  1051. QVERIFY(fakeFolder.syncOnce());
  1052. QVERIFY(fakeFolder.syncOnce());
  1053. auto expectedState = fakeFolder.currentLocalState();
  1054. QCOMPARE(fakeFolder.currentRemoteState(), expectedState);
  1055. }
  1056. void testServerUpdatingMTimeShouldNotCreateConflicts()
  1057. {
  1058. constexpr auto testFile = "test.txt";
  1059. constexpr auto CURRENT_MTIME = 1646057277;
  1060. FakeFolder fakeFolder{ FileInfo{} };
  1061. fakeFolder.remoteModifier().insert(testFile);
  1062. fakeFolder.remoteModifier().setModTimeKeepEtag(testFile, QDateTime::fromSecsSinceEpoch(CURRENT_MTIME - 2));
  1063. fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem);
  1064. QVERIFY(fakeFolder.syncOnce());
  1065. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  1066. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
  1067. const auto fileFirstSync = fakeFolder.currentLocalState().find(testFile);
  1068. QVERIFY(fileFirstSync);
  1069. QCOMPARE(fileFirstSync->lastModified.toSecsSinceEpoch(), CURRENT_MTIME - 2);
  1070. fakeFolder.remoteModifier().setModTimeKeepEtag(testFile, QDateTime::fromSecsSinceEpoch(CURRENT_MTIME - 1));
  1071. fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::FilesystemOnly);
  1072. QVERIFY(fakeFolder.syncOnce());
  1073. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  1074. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
  1075. const auto fileSecondSync = fakeFolder.currentLocalState().find(testFile);
  1076. QVERIFY(fileSecondSync);
  1077. QCOMPARE(fileSecondSync->lastModified.toSecsSinceEpoch(), CURRENT_MTIME - 1);
  1078. fakeFolder.remoteModifier().setModTime(testFile, QDateTime::fromSecsSinceEpoch(CURRENT_MTIME));
  1079. fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::FilesystemOnly);
  1080. QVERIFY(fakeFolder.syncOnce());
  1081. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  1082. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
  1083. const auto fileThirdSync = fakeFolder.currentLocalState().find(testFile);
  1084. QVERIFY(fileThirdSync);
  1085. QCOMPARE(fileThirdSync->lastModified.toSecsSinceEpoch(), CURRENT_MTIME);
  1086. }
  1087. };
  1088. QTEST_GUILESS_MAIN(TestSyncEngine)
  1089. #include "testsyncengine.moc"