testsyncengine.cpp 55 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250
  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. 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. 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. fakeFolder.syncJournal().getFileRecord(QByteArrayLiteral("NewFolder"), &rec);
  300. QVERIFY(rec.isValid());
  301. QCOMPARE(rec._etag, QByteArrayLiteral("_invalid_"));
  302. QVERIFY(!rec._fileId.isEmpty());
  303. }
  304. void testDirDownloadWithError() {
  305. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  306. ItemCompletedSpy completeSpy(fakeFolder);
  307. fakeFolder.remoteModifier().mkdir("Y");
  308. fakeFolder.remoteModifier().mkdir("Y/Z");
  309. fakeFolder.remoteModifier().insert("Y/Z/d0");
  310. fakeFolder.remoteModifier().insert("Y/Z/d1");
  311. fakeFolder.remoteModifier().insert("Y/Z/d2");
  312. fakeFolder.remoteModifier().insert("Y/Z/d3");
  313. fakeFolder.remoteModifier().insert("Y/Z/d4");
  314. fakeFolder.remoteModifier().insert("Y/Z/d5");
  315. fakeFolder.remoteModifier().insert("Y/Z/d6");
  316. fakeFolder.remoteModifier().insert("Y/Z/d7");
  317. fakeFolder.remoteModifier().insert("Y/Z/d8");
  318. fakeFolder.remoteModifier().insert("Y/Z/d9");
  319. fakeFolder.serverErrorPaths().append("Y/Z/d2", 503);
  320. fakeFolder.serverErrorPaths().append("Y/Z/d3", 503);
  321. QVERIFY(!fakeFolder.syncOnce());
  322. QCoreApplication::processEvents(); // should not crash
  323. QSet<QString> seen;
  324. for(const QList<QVariant> &args : completeSpy) {
  325. auto item = args[0].value<SyncFileItemPtr>();
  326. qDebug() << item->_file << item->isDirectory() << item->_status;
  327. QVERIFY(!seen.contains(item->_file)); // signal only sent once per item
  328. seen.insert(item->_file);
  329. if (item->_file == "Y/Z/d2") {
  330. QVERIFY(item->_status == SyncFileItem::NormalError);
  331. } else if (item->_file == "Y/Z/d3") {
  332. QVERIFY(item->_status != SyncFileItem::Success);
  333. } else if (!item->isDirectory()) {
  334. QVERIFY(item->_status == SyncFileItem::Success);
  335. }
  336. }
  337. }
  338. void testFakeConflict_data()
  339. {
  340. QTest::addColumn<bool>("sameMtime");
  341. QTest::addColumn<QByteArray>("checksums");
  342. QTest::addColumn<int>("expectedGET");
  343. QTest::newRow("Same mtime, but no server checksum -> ignored in reconcile")
  344. << true << QByteArray()
  345. << 0;
  346. QTest::newRow("Same mtime, weak server checksum differ -> downloaded")
  347. << true << QByteArray("Adler32:bad")
  348. << 1;
  349. QTest::newRow("Same mtime, matching weak checksum -> skipped")
  350. << true << QByteArray("Adler32:2a2010d")
  351. << 0;
  352. QTest::newRow("Same mtime, strong server checksum differ -> downloaded")
  353. << true << QByteArray("SHA1:bad")
  354. << 1;
  355. QTest::newRow("Same mtime, matching strong checksum -> skipped")
  356. << true << QByteArray("SHA1:56900fb1d337cf7237ff766276b9c1e8ce507427")
  357. << 0;
  358. QTest::newRow("mtime changed, but no server checksum -> download")
  359. << false << QByteArray()
  360. << 1;
  361. QTest::newRow("mtime changed, weak checksum match -> download anyway")
  362. << false << QByteArray("Adler32:2a2010d")
  363. << 1;
  364. QTest::newRow("mtime changed, strong checksum match -> skip")
  365. << false << QByteArray("SHA1:56900fb1d337cf7237ff766276b9c1e8ce507427")
  366. << 0;
  367. }
  368. void testFakeConflict()
  369. {
  370. QFETCH(bool, sameMtime);
  371. QFETCH(QByteArray, checksums);
  372. QFETCH(int, expectedGET);
  373. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  374. int nGET = 0;
  375. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &, QIODevice *) {
  376. if (op == QNetworkAccessManager::GetOperation)
  377. ++nGET;
  378. return nullptr;
  379. });
  380. // For directly editing the remote checksum
  381. auto &remoteInfo = fakeFolder.remoteModifier();
  382. // Base mtime with no ms content (filesystem is seconds only)
  383. auto mtime = QDateTime::currentDateTimeUtc().addDays(-4);
  384. mtime.setMSecsSinceEpoch(mtime.toMSecsSinceEpoch() / 1000 * 1000);
  385. fakeFolder.localModifier().setContents("A/a1", 'C');
  386. fakeFolder.localModifier().setModTime("A/a1", mtime);
  387. fakeFolder.remoteModifier().setContents("A/a1", 'C');
  388. if (!sameMtime)
  389. mtime = mtime.addDays(1);
  390. fakeFolder.remoteModifier().setModTime("A/a1", mtime);
  391. remoteInfo.find("A/a1")->checksums = checksums;
  392. QVERIFY(fakeFolder.syncOnce());
  393. QCOMPARE(nGET, expectedGET);
  394. // check that mtime in journal and filesystem agree
  395. QString a1path = fakeFolder.localPath() + "A/a1";
  396. SyncJournalFileRecord a1record;
  397. fakeFolder.syncJournal().getFileRecord(QByteArray("A/a1"), &a1record);
  398. QCOMPARE(a1record._modtime, (qint64)FileSystem::getModTime(a1path));
  399. // Extra sync reads from db, no difference
  400. QVERIFY(fakeFolder.syncOnce());
  401. QCOMPARE(nGET, expectedGET);
  402. }
  403. /**
  404. * Checks whether SyncFileItems have the expected properties before start
  405. * of propagation.
  406. */
  407. void testSyncFileItemProperties()
  408. {
  409. auto initialMtime = QDateTime::currentDateTimeUtc().addDays(-7);
  410. auto changedMtime = QDateTime::currentDateTimeUtc().addDays(-4);
  411. auto changedMtime2 = QDateTime::currentDateTimeUtc().addDays(-3);
  412. // Base mtime with no ms content (filesystem is seconds only)
  413. initialMtime.setMSecsSinceEpoch(initialMtime.toMSecsSinceEpoch() / 1000 * 1000);
  414. changedMtime.setMSecsSinceEpoch(changedMtime.toMSecsSinceEpoch() / 1000 * 1000);
  415. changedMtime2.setMSecsSinceEpoch(changedMtime2.toMSecsSinceEpoch() / 1000 * 1000);
  416. // Ensure the initial mtimes are as expected
  417. auto initialFileInfo = FileInfo::A12_B12_C12_S12();
  418. initialFileInfo.setModTime("A/a1", initialMtime);
  419. initialFileInfo.setModTime("B/b1", initialMtime);
  420. initialFileInfo.setModTime("C/c1", initialMtime);
  421. FakeFolder fakeFolder{ initialFileInfo };
  422. // upload a
  423. fakeFolder.localModifier().appendByte("A/a1");
  424. fakeFolder.localModifier().setModTime("A/a1", changedMtime);
  425. // download b
  426. fakeFolder.remoteModifier().appendByte("B/b1");
  427. fakeFolder.remoteModifier().setModTime("B/b1", changedMtime);
  428. // conflict c
  429. fakeFolder.localModifier().appendByte("C/c1");
  430. fakeFolder.localModifier().appendByte("C/c1");
  431. fakeFolder.localModifier().setModTime("C/c1", changedMtime);
  432. fakeFolder.remoteModifier().appendByte("C/c1");
  433. fakeFolder.remoteModifier().setModTime("C/c1", changedMtime2);
  434. connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToPropagate, [&](SyncFileItemVector &items) {
  435. SyncFileItemPtr a1, b1, c1;
  436. for (auto &item : items) {
  437. if (item->_file == "A/a1")
  438. a1 = item;
  439. if (item->_file == "B/b1")
  440. b1 = item;
  441. if (item->_file == "C/c1")
  442. c1 = item;
  443. }
  444. // a1: should have local size and modtime
  445. QVERIFY(a1);
  446. QCOMPARE(a1->_instruction, CSYNC_INSTRUCTION_SYNC);
  447. QCOMPARE(a1->_direction, SyncFileItem::Up);
  448. QCOMPARE(a1->_size, qint64(5));
  449. QCOMPARE(Utility::qDateTimeFromTime_t(a1->_modtime), changedMtime);
  450. QCOMPARE(a1->_previousSize, qint64(4));
  451. QCOMPARE(Utility::qDateTimeFromTime_t(a1->_previousModtime), initialMtime);
  452. // b2: should have remote size and modtime
  453. QVERIFY(b1);
  454. QCOMPARE(b1->_instruction, CSYNC_INSTRUCTION_SYNC);
  455. QCOMPARE(b1->_direction, SyncFileItem::Down);
  456. QCOMPARE(b1->_size, qint64(17));
  457. QCOMPARE(Utility::qDateTimeFromTime_t(b1->_modtime), changedMtime);
  458. QCOMPARE(b1->_previousSize, qint64(16));
  459. QCOMPARE(Utility::qDateTimeFromTime_t(b1->_previousModtime), initialMtime);
  460. // c1: conflicts are downloads, so remote size and modtime
  461. QVERIFY(c1);
  462. QCOMPARE(c1->_instruction, CSYNC_INSTRUCTION_CONFLICT);
  463. QCOMPARE(c1->_direction, SyncFileItem::None);
  464. QCOMPARE(c1->_size, qint64(25));
  465. QCOMPARE(Utility::qDateTimeFromTime_t(c1->_modtime), changedMtime2);
  466. QCOMPARE(c1->_previousSize, qint64(26));
  467. QCOMPARE(Utility::qDateTimeFromTime_t(c1->_previousModtime), changedMtime);
  468. });
  469. QVERIFY(fakeFolder.syncOnce());
  470. }
  471. /**
  472. * Checks whether subsequent large uploads are skipped after a 507 error
  473. */
  474. void testInsufficientRemoteStorage()
  475. {
  476. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  477. // Disable parallel uploads
  478. SyncOptions syncOptions;
  479. syncOptions._parallelNetworkJobs = 0;
  480. fakeFolder.syncEngine().setSyncOptions(syncOptions);
  481. // Produce an error based on upload size
  482. int remoteQuota = 1000;
  483. int n507 = 0, nPUT = 0;
  484. QObject parent;
  485. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * {
  486. Q_UNUSED(outgoingData)
  487. if (op == QNetworkAccessManager::PutOperation) {
  488. nPUT++;
  489. if (request.rawHeader("OC-Total-Length").toInt() > remoteQuota) {
  490. n507++;
  491. return new FakeErrorReply(op, request, &parent, 507);
  492. }
  493. }
  494. return nullptr;
  495. });
  496. fakeFolder.localModifier().insert("A/big", 800);
  497. QVERIFY(fakeFolder.syncOnce());
  498. QCOMPARE(nPUT, 1);
  499. QCOMPARE(n507, 0);
  500. nPUT = 0;
  501. fakeFolder.localModifier().insert("A/big1", 500); // ok
  502. fakeFolder.localModifier().insert("A/big2", 1200); // 507 (quota guess now 1199)
  503. fakeFolder.localModifier().insert("A/big3", 1200); // skipped
  504. fakeFolder.localModifier().insert("A/big4", 1500); // skipped
  505. fakeFolder.localModifier().insert("A/big5", 1100); // 507 (quota guess now 1099)
  506. fakeFolder.localModifier().insert("A/big6", 900); // ok (quota guess now 199)
  507. fakeFolder.localModifier().insert("A/big7", 200); // skipped
  508. fakeFolder.localModifier().insert("A/big8", 199); // ok (quota guess now 0)
  509. fakeFolder.localModifier().insert("B/big8", 1150); // 507
  510. QVERIFY(!fakeFolder.syncOnce());
  511. QCOMPARE(nPUT, 6);
  512. QCOMPARE(n507, 3);
  513. }
  514. // Checks whether downloads with bad checksums are accepted
  515. void testChecksumValidation()
  516. {
  517. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  518. QObject parent;
  519. QByteArray checksumValue;
  520. QByteArray checksumValueRecalculated;
  521. QByteArray contentMd5Value;
  522. bool isChecksumRecalculateSupported = false;
  523. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  524. if (op == QNetworkAccessManager::GetOperation) {
  525. auto reply = new FakeGetReply(fakeFolder.remoteModifier(), op, request, &parent);
  526. if (!checksumValue.isNull())
  527. reply->setRawHeader(OCC::checkSumHeaderC, checksumValue);
  528. if (!contentMd5Value.isNull())
  529. reply->setRawHeader(OCC::contentMd5HeaderC, contentMd5Value);
  530. return reply;
  531. } else if (op == QNetworkAccessManager::CustomOperation) {
  532. if (request.hasRawHeader(OCC::checksumRecalculateOnServerHeaderC)) {
  533. if (!isChecksumRecalculateSupported) {
  534. return new FakeErrorReply(op, request, &parent, 402);
  535. }
  536. auto reply = new FakeGetReply(fakeFolder.remoteModifier(), op, request, &parent);
  537. reply->setRawHeader(OCC::checkSumHeaderC, checksumValueRecalculated);
  538. return reply;
  539. }
  540. }
  541. return nullptr;
  542. });
  543. // Basic case
  544. fakeFolder.remoteModifier().create("A/a3", 16, 'A');
  545. QVERIFY(fakeFolder.syncOnce());
  546. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  547. // Bad OC-Checksum
  548. checksumValue = "SHA1:bad";
  549. fakeFolder.remoteModifier().create("A/a4", 16, 'A');
  550. QVERIFY(!fakeFolder.syncOnce());
  551. const QByteArray matchedSha1Checksum(QByteArrayLiteral("SHA1:19b1928d58a2030d08023f3d7054516dbc186f20"));
  552. const QByteArray mismatchedSha1Checksum(matchedSha1Checksum.chopped(1));
  553. // Good OC-Checksum
  554. checksumValue = matchedSha1Checksum; // printf 'A%.0s' {1..16} | sha1sum -
  555. QVERIFY(fakeFolder.syncOnce());
  556. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  557. checksumValue = QByteArray();
  558. // Bad Content-MD5
  559. contentMd5Value = "bad";
  560. fakeFolder.remoteModifier().create("A/a5", 16, 'A');
  561. QVERIFY(!fakeFolder.syncOnce());
  562. // Good Content-MD5
  563. contentMd5Value = "d8a73157ce10cd94a91c2079fc9a92c8"; // printf 'A%.0s' {1..16} | md5sum -
  564. QVERIFY(fakeFolder.syncOnce());
  565. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  566. // Invalid OC-Checksum is ignored
  567. checksumValue = "garbage";
  568. // contentMd5Value is still good
  569. fakeFolder.remoteModifier().create("A/a6", 16, 'A');
  570. QVERIFY(fakeFolder.syncOnce());
  571. contentMd5Value = "bad";
  572. fakeFolder.remoteModifier().create("A/a7", 16, 'A');
  573. QVERIFY(!fakeFolder.syncOnce());
  574. contentMd5Value.clear();
  575. QVERIFY(fakeFolder.syncOnce());
  576. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  577. // OC-Checksum contains Unsupported checksums
  578. checksumValue = "Unsupported:XXXX SHA1:invalid Invalid:XxX";
  579. fakeFolder.remoteModifier().create("A/a8", 16, 'A');
  580. QVERIFY(!fakeFolder.syncOnce()); // Since the supported SHA1 checksum is invalid, no download
  581. checksumValue = "Unsupported:XXXX SHA1:19b1928d58a2030d08023f3d7054516dbc186f20 Invalid:XxX";
  582. QVERIFY(fakeFolder.syncOnce()); // The supported SHA1 checksum is valid now, so the file are downloaded
  583. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  584. // Begin Test mismatch recalculation---------------------------------------------------------------------------------
  585. const auto prevServerVersion = fakeFolder.account()->serverVersion();
  586. fakeFolder.account()->setServerVersion(QString("%1.0.0").arg(fakeFolder.account()->checksumRecalculateServerVersionMinSupportedMajor()));
  587. // Mismatched OC-Checksum and X-Recalculate-Hash is not supported -> sync must fail
  588. isChecksumRecalculateSupported = false;
  589. checksumValue = mismatchedSha1Checksum;
  590. checksumValueRecalculated = matchedSha1Checksum;
  591. fakeFolder.remoteModifier().create("A/a9", 16, 'A');
  592. QVERIFY(!fakeFolder.syncOnce());
  593. // Mismatched OC-Checksum and X-Recalculate-Hash is supported, but, recalculated checksum is again mismatched -> sync must fail
  594. isChecksumRecalculateSupported = true;
  595. checksumValue = mismatchedSha1Checksum;
  596. checksumValueRecalculated = mismatchedSha1Checksum;
  597. QVERIFY(!fakeFolder.syncOnce());
  598. // Mismatched OC-Checksum and X-Recalculate-Hash is supported, and, recalculated checksum is a match -> sync must succeed
  599. isChecksumRecalculateSupported = true;
  600. checksumValue = mismatchedSha1Checksum;
  601. checksumValueRecalculated = matchedSha1Checksum;
  602. QVERIFY(fakeFolder.syncOnce());
  603. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  604. checksumValue = QByteArray();
  605. fakeFolder.account()->setServerVersion(prevServerVersion);
  606. // End Test mismatch recalculation-----------------------------------------------------------------------------------
  607. }
  608. // Tests the behavior of invalid filename detection
  609. void testInvalidFilenameRegex()
  610. {
  611. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  612. #ifndef Q_OS_WIN // We can't have local file with these character
  613. // For current servers, no characters are forbidden
  614. fakeFolder.syncEngine().account()->setServerVersion("10.0.0");
  615. fakeFolder.localModifier().insert("A/\\:?*\"<>|.txt");
  616. QVERIFY(fakeFolder.syncOnce());
  617. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  618. // For legacy servers, some characters were forbidden by the client
  619. fakeFolder.syncEngine().account()->setServerVersion("8.0.0");
  620. fakeFolder.localModifier().insert("B/\\:?*\"<>|.txt");
  621. QVERIFY(fakeFolder.syncOnce());
  622. QVERIFY(!fakeFolder.currentRemoteState().find("B/\\:?*\"<>|.txt"));
  623. #endif
  624. // We can override that by setting the capability
  625. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "invalidFilenameRegex", "" } } } });
  626. QVERIFY(fakeFolder.syncOnce());
  627. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  628. // Check that new servers also accept the capability
  629. fakeFolder.syncEngine().account()->setServerVersion("10.0.0");
  630. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "invalidFilenameRegex", "my[fgh]ile" } } } });
  631. fakeFolder.localModifier().insert("C/myfile.txt");
  632. QVERIFY(fakeFolder.syncOnce());
  633. QVERIFY(!fakeFolder.currentRemoteState().find("C/myfile.txt"));
  634. }
  635. void testDiscoveryHiddenFile()
  636. {
  637. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  638. QVERIFY(fakeFolder.syncOnce());
  639. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  640. // We can't depend on currentLocalState for hidden files since
  641. // it should rightfully skip things like download temporaries
  642. auto localFileExists = [&](QString name) {
  643. return QFileInfo(fakeFolder.localPath() + name).exists();
  644. };
  645. fakeFolder.syncEngine().setIgnoreHiddenFiles(true);
  646. fakeFolder.remoteModifier().insert("A/.hidden");
  647. fakeFolder.localModifier().insert("B/.hidden");
  648. QVERIFY(fakeFolder.syncOnce());
  649. QVERIFY(!localFileExists("A/.hidden"));
  650. QVERIFY(!fakeFolder.currentRemoteState().find("B/.hidden"));
  651. fakeFolder.syncEngine().setIgnoreHiddenFiles(false);
  652. fakeFolder.syncJournal().forceRemoteDiscoveryNextSync();
  653. QVERIFY(fakeFolder.syncOnce());
  654. QVERIFY(localFileExists("A/.hidden"));
  655. QVERIFY(fakeFolder.currentRemoteState().find("B/.hidden"));
  656. }
  657. void testNoLocalEncoding()
  658. {
  659. auto utf8Locale = QTextCodec::codecForLocale();
  660. if (utf8Locale->mibEnum() != 106) {
  661. QSKIP("Test only works for UTF8 locale");
  662. }
  663. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  664. QVERIFY(fakeFolder.syncOnce());
  665. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  666. // Utf8 locale can sync both
  667. fakeFolder.remoteModifier().insert("A/tößt");
  668. fakeFolder.remoteModifier().insert("A/t𠜎t");
  669. QVERIFY(fakeFolder.syncOnce());
  670. QVERIFY(fakeFolder.currentLocalState().find("A/tößt"));
  671. QVERIFY(fakeFolder.currentLocalState().find("A/t𠜎t"));
  672. #if !defined(Q_OS_MAC) && !defined(Q_OS_WIN)
  673. // Try again with a locale that can represent ö but not 𠜎 (4-byte utf8).
  674. QTextCodec::setCodecForLocale(QTextCodec::codecForName("ISO-8859-15"));
  675. QVERIFY(QTextCodec::codecForLocale()->mibEnum() == 111);
  676. fakeFolder.remoteModifier().insert("B/tößt");
  677. fakeFolder.remoteModifier().insert("B/t𠜎t");
  678. QVERIFY(fakeFolder.syncOnce());
  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.currentLocalState().find("B/t????t"));
  685. QVERIFY(fakeFolder.syncOnce());
  686. QVERIFY(fakeFolder.currentRemoteState().find("B/tößt"));
  687. QVERIFY(fakeFolder.currentRemoteState().find("B/t𠜎t"));
  688. // Try again with plain ascii
  689. QTextCodec::setCodecForLocale(QTextCodec::codecForName("ASCII"));
  690. QVERIFY(QTextCodec::codecForLocale()->mibEnum() == 3);
  691. fakeFolder.remoteModifier().insert("C/tößt");
  692. QVERIFY(fakeFolder.syncOnce());
  693. QVERIFY(!fakeFolder.currentLocalState().find("C/tößt"));
  694. QVERIFY(!fakeFolder.currentLocalState().find("C/t??t"));
  695. QVERIFY(!fakeFolder.currentLocalState().find("C/t????t"));
  696. QVERIFY(fakeFolder.syncOnce());
  697. QVERIFY(fakeFolder.currentRemoteState().find("C/tößt"));
  698. QTextCodec::setCodecForLocale(utf8Locale);
  699. #endif
  700. }
  701. // Aborting has had bugs when there are parallel upload jobs
  702. void testUploadV1Multiabort()
  703. {
  704. FakeFolder fakeFolder{ FileInfo{} };
  705. SyncOptions options;
  706. options._initialChunkSize = 10;
  707. options._maxChunkSize = 10;
  708. options._minChunkSize = 10;
  709. fakeFolder.syncEngine().setSyncOptions(options);
  710. QObject parent;
  711. int nPUT = 0;
  712. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  713. if (op == QNetworkAccessManager::PutOperation) {
  714. ++nPUT;
  715. return new FakeHangingReply(op, request, &parent);
  716. }
  717. return nullptr;
  718. });
  719. fakeFolder.localModifier().insert("file", 100, 'W');
  720. QTimer::singleShot(100, &fakeFolder.syncEngine(), [&]() { fakeFolder.syncEngine().abort(); });
  721. QVERIFY(!fakeFolder.syncOnce());
  722. QCOMPARE(nPUT, 3);
  723. }
  724. #ifndef Q_OS_WIN
  725. void testPropagatePermissions()
  726. {
  727. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  728. auto perm = QFileDevice::Permission(0x7704); // user/owner: rwx, group: r, other: -
  729. QFile::setPermissions(fakeFolder.localPath() + "A/a1", perm);
  730. QFile::setPermissions(fakeFolder.localPath() + "A/a2", perm);
  731. fakeFolder.syncOnce(); // get the metadata-only change out of the way
  732. fakeFolder.remoteModifier().appendByte("A/a1");
  733. fakeFolder.remoteModifier().appendByte("A/a2");
  734. fakeFolder.localModifier().appendByte("A/a2");
  735. fakeFolder.localModifier().appendByte("A/a2");
  736. fakeFolder.syncOnce(); // perms should be preserved
  737. QCOMPARE(QFileInfo(fakeFolder.localPath() + "A/a1").permissions(), perm);
  738. QCOMPARE(QFileInfo(fakeFolder.localPath() + "A/a2").permissions(), perm);
  739. auto conflictName = fakeFolder.syncJournal().conflictRecord(fakeFolder.syncJournal().conflictRecordPaths().first()).path;
  740. QVERIFY(conflictName.contains("A/a2"));
  741. QCOMPARE(QFileInfo(fakeFolder.localPath() + conflictName).permissions(), perm);
  742. }
  743. #endif
  744. void testEmptyLocalButHasRemote()
  745. {
  746. FakeFolder fakeFolder{ FileInfo{} };
  747. fakeFolder.remoteModifier().mkdir("foo");
  748. QVERIFY(fakeFolder.syncOnce());
  749. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  750. QVERIFY(fakeFolder.currentLocalState().find("foo"));
  751. }
  752. // Check that server mtime is set on directories on initial propagation
  753. void testDirectoryInitialMtime()
  754. {
  755. FakeFolder fakeFolder{ FileInfo{} };
  756. fakeFolder.remoteModifier().mkdir("foo");
  757. fakeFolder.remoteModifier().insert("foo/bar");
  758. auto datetime = QDateTime::currentDateTime();
  759. datetime.setSecsSinceEpoch(datetime.toSecsSinceEpoch()); // wipe ms
  760. fakeFolder.remoteModifier().find("foo")->lastModified = datetime;
  761. QVERIFY(fakeFolder.syncOnce());
  762. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  763. QCOMPARE(QFileInfo(fakeFolder.localPath() + "foo").lastModified(), datetime);
  764. }
  765. /**
  766. * Checks whether subsequent large uploads are skipped after a 507 error
  767. */
  768. void testErrorsWithBulkUpload()
  769. {
  770. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  771. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"bulkupload", "1.0"} } } });
  772. // Disable parallel uploads
  773. SyncOptions syncOptions;
  774. syncOptions._parallelNetworkJobs = 0;
  775. fakeFolder.syncEngine().setSyncOptions(syncOptions);
  776. int nPUT = 0;
  777. int nPOST = 0;
  778. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * {
  779. auto contentType = request.header(QNetworkRequest::ContentTypeHeader).toString();
  780. if (op == QNetworkAccessManager::PostOperation) {
  781. ++nPOST;
  782. if (contentType.startsWith(QStringLiteral("multipart/related; boundary="))) {
  783. auto jsonReplyObject = fakeFolder.forEachReplyPart(outgoingData, contentType, [] (const QMap<QString, QByteArray> &allHeaders) -> QJsonObject {
  784. auto reply = QJsonObject{};
  785. const auto fileName = allHeaders[QStringLiteral("X-File-Path")];
  786. if (fileName.endsWith("A/big2") ||
  787. fileName.endsWith("A/big3") ||
  788. fileName.endsWith("A/big4") ||
  789. fileName.endsWith("A/big5") ||
  790. fileName.endsWith("A/big7") ||
  791. fileName.endsWith("B/big8")) {
  792. reply.insert(QStringLiteral("error"), true);
  793. reply.insert(QStringLiteral("etag"), {});
  794. return reply;
  795. } else {
  796. reply.insert(QStringLiteral("error"), false);
  797. reply.insert(QStringLiteral("etag"), {});
  798. }
  799. return reply;
  800. });
  801. if (jsonReplyObject.size()) {
  802. auto jsonReply = QJsonDocument{};
  803. jsonReply.setObject(jsonReplyObject);
  804. return new FakeJsonErrorReply{op, request, this, 200, jsonReply};
  805. }
  806. return nullptr;
  807. }
  808. } else if (op == QNetworkAccessManager::PutOperation) {
  809. ++nPUT;
  810. const auto fileName = getFilePathFromUrl(request.url());
  811. if (fileName.endsWith("A/big2") ||
  812. fileName.endsWith("A/big3") ||
  813. fileName.endsWith("A/big4") ||
  814. fileName.endsWith("A/big5") ||
  815. fileName.endsWith("A/big7") ||
  816. fileName.endsWith("B/big8")) {
  817. return new FakeErrorReply(op, request, this, 412);
  818. }
  819. return nullptr;
  820. }
  821. return nullptr;
  822. });
  823. fakeFolder.localModifier().insert("A/big", 1);
  824. QVERIFY(fakeFolder.syncOnce());
  825. QCOMPARE(nPUT, 0);
  826. QCOMPARE(nPOST, 1);
  827. nPUT = 0;
  828. nPOST = 0;
  829. fakeFolder.localModifier().insert("A/big1", 1); // ok
  830. fakeFolder.localModifier().insert("A/big2", 1); // ko
  831. fakeFolder.localModifier().insert("A/big3", 1); // ko
  832. fakeFolder.localModifier().insert("A/big4", 1); // ko
  833. fakeFolder.localModifier().insert("A/big5", 1); // ko
  834. fakeFolder.localModifier().insert("A/big6", 1); // ok
  835. fakeFolder.localModifier().insert("A/big7", 1); // ko
  836. fakeFolder.localModifier().insert("A/big8", 1); // ok
  837. fakeFolder.localModifier().insert("B/big8", 1); // ko
  838. QVERIFY(!fakeFolder.syncOnce());
  839. QCOMPARE(nPUT, 0);
  840. QCOMPARE(nPOST, 1);
  841. nPUT = 0;
  842. nPOST = 0;
  843. QVERIFY(!fakeFolder.syncOnce());
  844. QCOMPARE(nPUT, 6);
  845. QCOMPARE(nPOST, 0);
  846. }
  847. /**
  848. * Checks whether subsequent large uploads are skipped after a 507 error
  849. */
  850. void testNetworkErrorsWithBulkUpload()
  851. {
  852. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  853. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"bulkupload", "1.0"} } } });
  854. // Disable parallel uploads
  855. SyncOptions syncOptions;
  856. syncOptions._parallelNetworkJobs = 0;
  857. fakeFolder.syncEngine().setSyncOptions(syncOptions);
  858. int nPUT = 0;
  859. int nPOST = 0;
  860. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  861. auto contentType = request.header(QNetworkRequest::ContentTypeHeader).toString();
  862. if (op == QNetworkAccessManager::PostOperation) {
  863. ++nPOST;
  864. if (contentType.startsWith(QStringLiteral("multipart/related; boundary="))) {
  865. return new FakeErrorReply(op, request, this, 400);
  866. }
  867. return nullptr;
  868. } else if (op == QNetworkAccessManager::PutOperation) {
  869. ++nPUT;
  870. }
  871. return nullptr;
  872. });
  873. fakeFolder.localModifier().insert("A/big1", 1);
  874. fakeFolder.localModifier().insert("A/big2", 1);
  875. fakeFolder.localModifier().insert("A/big3", 1);
  876. fakeFolder.localModifier().insert("A/big4", 1);
  877. fakeFolder.localModifier().insert("A/big5", 1);
  878. fakeFolder.localModifier().insert("A/big6", 1);
  879. fakeFolder.localModifier().insert("A/big7", 1);
  880. fakeFolder.localModifier().insert("A/big8", 1);
  881. fakeFolder.localModifier().insert("B/big8", 1);
  882. QVERIFY(!fakeFolder.syncOnce());
  883. QCOMPARE(nPUT, 0);
  884. QCOMPARE(nPOST, 1);
  885. nPUT = 0;
  886. nPOST = 0;
  887. QVERIFY(fakeFolder.syncOnce());
  888. QCOMPARE(nPUT, 9);
  889. QCOMPARE(nPOST, 0);
  890. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  891. }
  892. void testRemoteMoveFailedInsufficientStorageLocalMoveRolledBack()
  893. {
  894. FakeFolder fakeFolder{FileInfo{}};
  895. // create a big shared folder with some files
  896. fakeFolder.remoteModifier().mkdir("big_shared_folder");
  897. fakeFolder.remoteModifier().mkdir("big_shared_folder/shared_files");
  898. fakeFolder.remoteModifier().insert("big_shared_folder/shared_files/big_shared_file_A.data", 1000);
  899. fakeFolder.remoteModifier().insert("big_shared_folder/shared_files/big_shared_file_B.data", 1000);
  900. // make sure big shared folder is synced
  901. QVERIFY(fakeFolder.syncOnce());
  902. QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_A.data"));
  903. QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_B.data"));
  904. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  905. // try to move from a big shared folder to your own folder
  906. fakeFolder.localModifier().mkdir("own_folder");
  907. fakeFolder.localModifier().rename(
  908. "big_shared_folder/shared_files/big_shared_file_A.data", "own_folder/big_shared_file_A.data");
  909. fakeFolder.localModifier().rename(
  910. "big_shared_folder/shared_files/big_shared_file_B.data", "own_folder/big_shared_file_B.data");
  911. // emulate server MOVE 507 error
  912. QObject parent;
  913. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request,
  914. QIODevice *outgoingData) -> QNetworkReply * {
  915. Q_UNUSED(outgoingData)
  916. if (op == QNetworkAccessManager::CustomOperation
  917. && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("MOVE")) {
  918. return new FakeErrorReply(op, request, &parent, 507);
  919. }
  920. return nullptr;
  921. });
  922. // make sure the first sync failes and files get restored to original folder
  923. QVERIFY(!fakeFolder.syncOnce());
  924. QVERIFY(fakeFolder.syncOnce());
  925. QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_A.data"));
  926. QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_B.data"));
  927. QVERIFY(!fakeFolder.currentLocalState().find("own_folder/big_shared_file_A.data"));
  928. QVERIFY(!fakeFolder.currentLocalState().find("own_folder/big_shared_file_B.data"));
  929. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  930. }
  931. void testRemoteMoveFailedForbiddenLocalMoveRolledBack()
  932. {
  933. FakeFolder fakeFolder{FileInfo{}};
  934. // create a big shared folder with some files
  935. fakeFolder.remoteModifier().mkdir("big_shared_folder");
  936. fakeFolder.remoteModifier().mkdir("big_shared_folder/shared_files");
  937. fakeFolder.remoteModifier().insert("big_shared_folder/shared_files/big_shared_file_A.data", 1000);
  938. fakeFolder.remoteModifier().insert("big_shared_folder/shared_files/big_shared_file_B.data", 1000);
  939. // make sure big shared folder is synced
  940. QVERIFY(fakeFolder.syncOnce());
  941. QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_A.data"));
  942. QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_B.data"));
  943. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  944. // try to move from a big shared folder to your own folder
  945. fakeFolder.localModifier().mkdir("own_folder");
  946. fakeFolder.localModifier().rename(
  947. "big_shared_folder/shared_files/big_shared_file_A.data", "own_folder/big_shared_file_A.data");
  948. fakeFolder.localModifier().rename(
  949. "big_shared_folder/shared_files/big_shared_file_B.data", "own_folder/big_shared_file_B.data");
  950. // emulate server MOVE 507 error
  951. QObject parent;
  952. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request,
  953. QIODevice *outgoingData) -> QNetworkReply * {
  954. Q_UNUSED(outgoingData)
  955. auto attributeCustomVerb = request.attribute(QNetworkRequest::CustomVerbAttribute).toString();
  956. if (op == QNetworkAccessManager::CustomOperation
  957. && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("MOVE")) {
  958. return new FakeErrorReply(op, request, &parent, 403);
  959. }
  960. return nullptr;
  961. });
  962. // make sure the first sync failes and files get restored to original folder
  963. QVERIFY(!fakeFolder.syncOnce());
  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. QVERIFY(!fakeFolder.currentLocalState().find("own_folder/big_shared_file_A.data"));
  968. QVERIFY(!fakeFolder.currentLocalState().find("own_folder/big_shared_file_B.data"));
  969. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  970. }
  971. void testFolderWithFilesInError()
  972. {
  973. FakeFolder fakeFolder{FileInfo{}};
  974. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * {
  975. Q_UNUSED(outgoingData)
  976. if (op == QNetworkAccessManager::GetOperation) {
  977. const auto fileName = getFilePathFromUrl(request.url());
  978. if (fileName == QStringLiteral("aaa/subfolder/foo")) {
  979. return new FakeErrorReply(op, request, &fakeFolder.syncEngine(), 403);
  980. }
  981. }
  982. return nullptr;
  983. });
  984. fakeFolder.remoteModifier().mkdir(QStringLiteral("aaa"));
  985. fakeFolder.remoteModifier().mkdir(QStringLiteral("aaa/subfolder"));
  986. fakeFolder.remoteModifier().insert(QStringLiteral("aaa/subfolder/bar"));
  987. QVERIFY(fakeFolder.syncOnce());
  988. fakeFolder.remoteModifier().insert(QStringLiteral("aaa/subfolder/foo"));
  989. QVERIFY(!fakeFolder.syncOnce());
  990. QVERIFY(!fakeFolder.syncOnce());
  991. }
  992. void testInvalidMtimeRecoveryAtStart()
  993. {
  994. constexpr auto INVALID_MTIME = 0;
  995. constexpr auto CURRENT_MTIME = 1646057277;
  996. FakeFolder fakeFolder{FileInfo{}};
  997. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  998. const QString fooFileRootFolder("foo");
  999. const QString barFileRootFolder("bar");
  1000. const QString fooFileSubFolder("subfolder/foo");
  1001. const QString barFileSubFolder("subfolder/bar");
  1002. const QString fooFileAaaSubFolder("aaa/subfolder/foo");
  1003. const QString barFileAaaSubFolder("aaa/subfolder/bar");
  1004. fakeFolder.remoteModifier().insert(fooFileRootFolder);
  1005. fakeFolder.remoteModifier().insert(barFileRootFolder);
  1006. fakeFolder.remoteModifier().mkdir(QStringLiteral("subfolder"));
  1007. fakeFolder.remoteModifier().insert(fooFileSubFolder);
  1008. fakeFolder.remoteModifier().insert(barFileSubFolder);
  1009. fakeFolder.remoteModifier().mkdir(QStringLiteral("aaa"));
  1010. fakeFolder.remoteModifier().mkdir(QStringLiteral("aaa/subfolder"));
  1011. fakeFolder.remoteModifier().insert(fooFileAaaSubFolder);
  1012. fakeFolder.remoteModifier().setModTime(fooFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(INVALID_MTIME));
  1013. fakeFolder.remoteModifier().insert(barFileAaaSubFolder);
  1014. fakeFolder.remoteModifier().setModTime(barFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(INVALID_MTIME));
  1015. QVERIFY(!fakeFolder.syncOnce());
  1016. QVERIFY(!fakeFolder.syncOnce());
  1017. fakeFolder.remoteModifier().setModTimeKeepEtag(fooFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(CURRENT_MTIME));
  1018. fakeFolder.remoteModifier().setModTimeKeepEtag(barFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(CURRENT_MTIME));
  1019. QVERIFY(fakeFolder.syncOnce());
  1020. QVERIFY(fakeFolder.syncOnce());
  1021. auto expectedState = fakeFolder.currentLocalState();
  1022. QCOMPARE(fakeFolder.currentRemoteState(), expectedState);
  1023. }
  1024. void testInvalidMtimeRecovery()
  1025. {
  1026. constexpr auto INVALID_MTIME = 0;
  1027. constexpr auto CURRENT_MTIME = 1646057277;
  1028. FakeFolder fakeFolder{FileInfo{}};
  1029. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  1030. const QString fooFileRootFolder("foo");
  1031. const QString barFileRootFolder("bar");
  1032. const QString fooFileSubFolder("subfolder/foo");
  1033. const QString barFileSubFolder("subfolder/bar");
  1034. const QString fooFileAaaSubFolder("aaa/subfolder/foo");
  1035. const QString barFileAaaSubFolder("aaa/subfolder/bar");
  1036. fakeFolder.remoteModifier().insert(fooFileRootFolder);
  1037. fakeFolder.remoteModifier().insert(barFileRootFolder);
  1038. fakeFolder.remoteModifier().mkdir(QStringLiteral("subfolder"));
  1039. fakeFolder.remoteModifier().insert(fooFileSubFolder);
  1040. fakeFolder.remoteModifier().insert(barFileSubFolder);
  1041. fakeFolder.remoteModifier().mkdir(QStringLiteral("aaa"));
  1042. fakeFolder.remoteModifier().mkdir(QStringLiteral("aaa/subfolder"));
  1043. fakeFolder.remoteModifier().insert(fooFileAaaSubFolder);
  1044. fakeFolder.remoteModifier().insert(barFileAaaSubFolder);
  1045. QVERIFY(fakeFolder.syncOnce());
  1046. fakeFolder.remoteModifier().setModTime(fooFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(INVALID_MTIME));
  1047. fakeFolder.remoteModifier().setModTime(barFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(INVALID_MTIME));
  1048. QVERIFY(!fakeFolder.syncOnce());
  1049. QVERIFY(!fakeFolder.syncOnce());
  1050. fakeFolder.remoteModifier().setModTimeKeepEtag(fooFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(CURRENT_MTIME));
  1051. fakeFolder.remoteModifier().setModTimeKeepEtag(barFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(CURRENT_MTIME));
  1052. QVERIFY(fakeFolder.syncOnce());
  1053. QVERIFY(fakeFolder.syncOnce());
  1054. auto expectedState = fakeFolder.currentLocalState();
  1055. QCOMPARE(fakeFolder.currentRemoteState(), expectedState);
  1056. }
  1057. };
  1058. QTEST_GUILESS_MAIN(TestSyncEngine)
  1059. #include "testsyncengine.moc"