testsyncengine.cpp 73 KB

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