testsyncengine.cpp 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782
  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. using namespace OCC;
  11. bool itemDidComplete(const ItemCompletedSpy &spy, const QString &path)
  12. {
  13. if (auto item = spy.findItem(path)) {
  14. return item->_instruction != CSYNC_INSTRUCTION_NONE && item->_instruction != CSYNC_INSTRUCTION_UPDATE_METADATA;
  15. }
  16. return false;
  17. }
  18. bool itemInstruction(const ItemCompletedSpy &spy, const QString &path, const SyncInstructions instr)
  19. {
  20. auto item = spy.findItem(path);
  21. return item->_instruction == instr;
  22. }
  23. bool itemDidCompleteSuccessfully(const ItemCompletedSpy &spy, const QString &path)
  24. {
  25. if (auto item = spy.findItem(path)) {
  26. return item->_status == SyncFileItem::Success;
  27. }
  28. return false;
  29. }
  30. bool itemDidCompleteSuccessfullyWithExpectedRank(const ItemCompletedSpy &spy, const QString &path, int rank)
  31. {
  32. if (auto item = spy.findItemWithExpectedRank(path, rank)) {
  33. return item->_status == SyncFileItem::Success;
  34. }
  35. return false;
  36. }
  37. class TestSyncEngine : public QObject
  38. {
  39. Q_OBJECT
  40. private slots:
  41. void testFileDownload() {
  42. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  43. ItemCompletedSpy completeSpy(fakeFolder);
  44. fakeFolder.remoteModifier().insert("A/a0");
  45. fakeFolder.syncOnce();
  46. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a0"));
  47. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  48. }
  49. void testFileUpload() {
  50. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  51. ItemCompletedSpy completeSpy(fakeFolder);
  52. fakeFolder.localModifier().insert("A/a0");
  53. fakeFolder.syncOnce();
  54. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a0"));
  55. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  56. }
  57. void testDirDownload() {
  58. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  59. ItemCompletedSpy completeSpy(fakeFolder);
  60. fakeFolder.remoteModifier().mkdir("Y");
  61. fakeFolder.remoteModifier().mkdir("Z");
  62. fakeFolder.remoteModifier().insert("Z/d0");
  63. fakeFolder.syncOnce();
  64. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Y"));
  65. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z"));
  66. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z/d0"));
  67. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  68. }
  69. void testDirUpload() {
  70. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  71. ItemCompletedSpy completeSpy(fakeFolder);
  72. fakeFolder.localModifier().mkdir("Y");
  73. fakeFolder.localModifier().mkdir("Z");
  74. fakeFolder.localModifier().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 testDirUploadWithDelayedAlgorithm() {
  82. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  83. ItemCompletedSpy completeSpy(fakeFolder);
  84. fakeFolder.localModifier().mkdir("Y");
  85. fakeFolder.localModifier().insert("Y/d0");
  86. fakeFolder.localModifier().mkdir("Z");
  87. fakeFolder.localModifier().insert("Z/d0");
  88. fakeFolder.localModifier().insert("A/a0");
  89. fakeFolder.localModifier().insert("B/b0");
  90. fakeFolder.localModifier().insert("r0");
  91. fakeFolder.localModifier().insert("r1");
  92. fakeFolder.syncOnce();
  93. QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "Y", 0));
  94. QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "Z", 1));
  95. QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "Y/d0", 2));
  96. QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "Z/d0", 3));
  97. QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "A/a0", 4));
  98. QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "B/b0", 5));
  99. QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "r0", 6));
  100. QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "r1", 7));
  101. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  102. }
  103. void testLocalDelete() {
  104. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  105. ItemCompletedSpy completeSpy(fakeFolder);
  106. fakeFolder.remoteModifier().remove("A/a1");
  107. fakeFolder.syncOnce();
  108. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a1"));
  109. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  110. }
  111. void testRemoteDelete() {
  112. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  113. ItemCompletedSpy completeSpy(fakeFolder);
  114. fakeFolder.localModifier().remove("A/a1");
  115. fakeFolder.syncOnce();
  116. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a1"));
  117. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  118. }
  119. void testEmlLocalChecksum() {
  120. FakeFolder fakeFolder{FileInfo{}};
  121. fakeFolder.localModifier().insert("a1.eml", 64, 'A');
  122. fakeFolder.localModifier().insert("a2.eml", 64, 'A');
  123. fakeFolder.localModifier().insert("a3.eml", 64, 'A');
  124. fakeFolder.localModifier().insert("b3.txt", 64, 'A');
  125. // Upload and calculate the checksums
  126. // fakeFolder.syncOnce();
  127. fakeFolder.syncOnce();
  128. auto getDbChecksum = [&](QString path) {
  129. SyncJournalFileRecord record;
  130. fakeFolder.syncJournal().getFileRecord(path, &record);
  131. return record._checksumHeader;
  132. };
  133. // printf 'A%.0s' {1..64} | sha1sum -
  134. QByteArray referenceChecksum("SHA1:30b86e44e6001403827a62c58b08893e77cf121f");
  135. QCOMPARE(getDbChecksum("a1.eml"), referenceChecksum);
  136. QCOMPARE(getDbChecksum("a2.eml"), referenceChecksum);
  137. QCOMPARE(getDbChecksum("a3.eml"), referenceChecksum);
  138. QCOMPARE(getDbChecksum("b3.txt"), referenceChecksum);
  139. ItemCompletedSpy completeSpy(fakeFolder);
  140. // Touch the file without changing the content, shouldn't upload
  141. fakeFolder.localModifier().setContents("a1.eml", 'A');
  142. // Change the content/size
  143. fakeFolder.localModifier().setContents("a2.eml", 'B');
  144. fakeFolder.localModifier().appendByte("a3.eml");
  145. fakeFolder.localModifier().appendByte("b3.txt");
  146. fakeFolder.syncOnce();
  147. QCOMPARE(getDbChecksum("a1.eml"), referenceChecksum);
  148. QCOMPARE(getDbChecksum("a2.eml"), QByteArray("SHA1:84951fc23a4dafd10020ac349da1f5530fa65949"));
  149. QCOMPARE(getDbChecksum("a3.eml"), QByteArray("SHA1:826b7e7a7af8a529ae1c7443c23bf185c0ad440c"));
  150. QCOMPARE(getDbChecksum("b3.eml"), getDbChecksum("a3.txt"));
  151. QVERIFY(!itemDidComplete(completeSpy, "a1.eml"));
  152. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "a2.eml"));
  153. QVERIFY(itemDidCompleteSuccessfully(completeSpy, "a3.eml"));
  154. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  155. }
  156. void testSelectiveSyncBug() {
  157. // issue owncloud/enterprise#1965: files from selective-sync ignored
  158. // folders are uploaded anyway is some circumstances.
  159. FakeFolder fakeFolder{FileInfo{ QString(), {
  160. FileInfo { QStringLiteral("parentFolder"), {
  161. FileInfo{ QStringLiteral("subFolderA"), {
  162. { QStringLiteral("fileA.txt"), 400 },
  163. { QStringLiteral("fileB.txt"), 400, 'o' },
  164. FileInfo { QStringLiteral("subsubFolder"), {
  165. { QStringLiteral("fileC.txt"), 400 },
  166. { QStringLiteral("fileD.txt"), 400, 'o' }
  167. }},
  168. FileInfo{ QStringLiteral("anotherFolder"), {
  169. FileInfo { QStringLiteral("emptyFolder"), { } },
  170. FileInfo { QStringLiteral("subsubFolder"), {
  171. { QStringLiteral("fileE.txt"), 400 },
  172. { QStringLiteral("fileF.txt"), 400, 'o' }
  173. }}
  174. }}
  175. }},
  176. FileInfo{ QStringLiteral("subFolderB"), {} }
  177. }}
  178. }}};
  179. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  180. auto expectedServerState = fakeFolder.currentRemoteState();
  181. // Remove subFolderA with selectiveSync:
  182. fakeFolder.syncEngine().journal()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList,
  183. {"parentFolder/subFolderA/"});
  184. fakeFolder.syncEngine().journal()->schedulePathForRemoteDiscovery(QByteArrayLiteral("parentFolder/subFolderA/"));
  185. auto getEtag = [&](const QByteArray &file) {
  186. SyncJournalFileRecord rec;
  187. fakeFolder.syncJournal().getFileRecord(file, &rec);
  188. return rec._etag;
  189. };
  190. QVERIFY(getEtag("parentFolder") == "_invalid_");
  191. QVERIFY(getEtag("parentFolder/subFolderA") == "_invalid_");
  192. QVERIFY(getEtag("parentFolder/subFolderA/subsubFolder") != "_invalid_");
  193. // But touch local file before the next sync, such that the local folder
  194. // can't be removed
  195. fakeFolder.localModifier().setContents("parentFolder/subFolderA/fileB.txt", 'n');
  196. fakeFolder.localModifier().setContents("parentFolder/subFolderA/subsubFolder/fileD.txt", 'n');
  197. fakeFolder.localModifier().setContents("parentFolder/subFolderA/anotherFolder/subsubFolder/fileF.txt", 'n');
  198. // Several follow-up syncs don't change the state at all,
  199. // in particular the remote state doesn't change and fileB.txt
  200. // isn't uploaded.
  201. for (int i = 0; i < 3; ++i) {
  202. fakeFolder.syncOnce();
  203. {
  204. // Nothing changed on the server
  205. QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
  206. // The local state should still have subFolderA
  207. auto local = fakeFolder.currentLocalState();
  208. QVERIFY(local.find("parentFolder/subFolderA"));
  209. QVERIFY(!local.find("parentFolder/subFolderA/fileA.txt"));
  210. QVERIFY(local.find("parentFolder/subFolderA/fileB.txt"));
  211. QVERIFY(!local.find("parentFolder/subFolderA/subsubFolder/fileC.txt"));
  212. QVERIFY(local.find("parentFolder/subFolderA/subsubFolder/fileD.txt"));
  213. QVERIFY(!local.find("parentFolder/subFolderA/anotherFolder/subsubFolder/fileE.txt"));
  214. QVERIFY(local.find("parentFolder/subFolderA/anotherFolder/subsubFolder/fileF.txt"));
  215. QVERIFY(!local.find("parentFolder/subFolderA/anotherFolder/emptyFolder"));
  216. QVERIFY(local.find("parentFolder/subFolderB"));
  217. }
  218. }
  219. }
  220. void abortAfterFailedMkdir() {
  221. FakeFolder fakeFolder{FileInfo{}};
  222. QSignalSpy finishedSpy(&fakeFolder.syncEngine(), SIGNAL(finished(bool)));
  223. fakeFolder.serverErrorPaths().append("NewFolder");
  224. fakeFolder.localModifier().mkdir("NewFolder");
  225. // This should be aborted and would otherwise fail in FileInfo::create.
  226. fakeFolder.localModifier().insert("NewFolder/NewFile");
  227. fakeFolder.syncOnce();
  228. QCOMPARE(finishedSpy.size(), 1);
  229. QCOMPARE(finishedSpy.first().first().toBool(), false);
  230. }
  231. /** Verify that an incompletely propagated directory doesn't have the server's
  232. * etag stored in the database yet. */
  233. void testDirEtagAfterIncompleteSync() {
  234. FakeFolder fakeFolder{FileInfo{}};
  235. QSignalSpy finishedSpy(&fakeFolder.syncEngine(), SIGNAL(finished(bool)));
  236. fakeFolder.serverErrorPaths().append("NewFolder/foo");
  237. fakeFolder.remoteModifier().mkdir("NewFolder");
  238. fakeFolder.remoteModifier().insert("NewFolder/foo");
  239. QVERIFY(!fakeFolder.syncOnce());
  240. SyncJournalFileRecord rec;
  241. fakeFolder.syncJournal().getFileRecord(QByteArrayLiteral("NewFolder"), &rec);
  242. QVERIFY(rec.isValid());
  243. QCOMPARE(rec._etag, QByteArrayLiteral("_invalid_"));
  244. QVERIFY(!rec._fileId.isEmpty());
  245. }
  246. void testDirDownloadWithError() {
  247. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  248. ItemCompletedSpy completeSpy(fakeFolder);
  249. fakeFolder.remoteModifier().mkdir("Y");
  250. fakeFolder.remoteModifier().mkdir("Y/Z");
  251. fakeFolder.remoteModifier().insert("Y/Z/d0");
  252. fakeFolder.remoteModifier().insert("Y/Z/d1");
  253. fakeFolder.remoteModifier().insert("Y/Z/d2");
  254. fakeFolder.remoteModifier().insert("Y/Z/d3");
  255. fakeFolder.remoteModifier().insert("Y/Z/d4");
  256. fakeFolder.remoteModifier().insert("Y/Z/d5");
  257. fakeFolder.remoteModifier().insert("Y/Z/d6");
  258. fakeFolder.remoteModifier().insert("Y/Z/d7");
  259. fakeFolder.remoteModifier().insert("Y/Z/d8");
  260. fakeFolder.remoteModifier().insert("Y/Z/d9");
  261. fakeFolder.serverErrorPaths().append("Y/Z/d2", 503);
  262. fakeFolder.serverErrorPaths().append("Y/Z/d3", 503);
  263. QVERIFY(!fakeFolder.syncOnce());
  264. QCoreApplication::processEvents(); // should not crash
  265. QSet<QString> seen;
  266. for(const QList<QVariant> &args : completeSpy) {
  267. auto item = args[0].value<SyncFileItemPtr>();
  268. qDebug() << item->_file << item->isDirectory() << item->_status;
  269. QVERIFY(!seen.contains(item->_file)); // signal only sent once per item
  270. seen.insert(item->_file);
  271. if (item->_file == "Y/Z/d2") {
  272. QVERIFY(item->_status == SyncFileItem::NormalError);
  273. } else if (item->_file == "Y/Z/d3") {
  274. QVERIFY(item->_status != SyncFileItem::Success);
  275. } else if (!item->isDirectory()) {
  276. QVERIFY(item->_status == SyncFileItem::Success);
  277. }
  278. }
  279. }
  280. void testFakeConflict_data()
  281. {
  282. QTest::addColumn<bool>("sameMtime");
  283. QTest::addColumn<QByteArray>("checksums");
  284. QTest::addColumn<int>("expectedGET");
  285. QTest::newRow("Same mtime, but no server checksum -> ignored in reconcile")
  286. << true << QByteArray()
  287. << 0;
  288. QTest::newRow("Same mtime, weak server checksum differ -> downloaded")
  289. << true << QByteArray("Adler32:bad")
  290. << 1;
  291. QTest::newRow("Same mtime, matching weak checksum -> skipped")
  292. << true << QByteArray("Adler32:2a2010d")
  293. << 0;
  294. QTest::newRow("Same mtime, strong server checksum differ -> downloaded")
  295. << true << QByteArray("SHA1:bad")
  296. << 1;
  297. QTest::newRow("Same mtime, matching strong checksum -> skipped")
  298. << true << QByteArray("SHA1:56900fb1d337cf7237ff766276b9c1e8ce507427")
  299. << 0;
  300. QTest::newRow("mtime changed, but no server checksum -> download")
  301. << false << QByteArray()
  302. << 1;
  303. QTest::newRow("mtime changed, weak checksum match -> download anyway")
  304. << false << QByteArray("Adler32:2a2010d")
  305. << 1;
  306. QTest::newRow("mtime changed, strong checksum match -> skip")
  307. << false << QByteArray("SHA1:56900fb1d337cf7237ff766276b9c1e8ce507427")
  308. << 0;
  309. }
  310. void testFakeConflict()
  311. {
  312. QFETCH(bool, sameMtime);
  313. QFETCH(QByteArray, checksums);
  314. QFETCH(int, expectedGET);
  315. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  316. int nGET = 0;
  317. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &, QIODevice *) {
  318. if (op == QNetworkAccessManager::GetOperation)
  319. ++nGET;
  320. return nullptr;
  321. });
  322. // For directly editing the remote checksum
  323. auto &remoteInfo = fakeFolder.remoteModifier();
  324. // Base mtime with no ms content (filesystem is seconds only)
  325. auto mtime = QDateTime::currentDateTimeUtc().addDays(-4);
  326. mtime.setMSecsSinceEpoch(mtime.toMSecsSinceEpoch() / 1000 * 1000);
  327. fakeFolder.localModifier().setContents("A/a1", 'C');
  328. fakeFolder.localModifier().setModTime("A/a1", mtime);
  329. fakeFolder.remoteModifier().setContents("A/a1", 'C');
  330. if (!sameMtime)
  331. mtime = mtime.addDays(1);
  332. fakeFolder.remoteModifier().setModTime("A/a1", mtime);
  333. remoteInfo.find("A/a1")->checksums = checksums;
  334. QVERIFY(fakeFolder.syncOnce());
  335. QCOMPARE(nGET, expectedGET);
  336. // check that mtime in journal and filesystem agree
  337. QString a1path = fakeFolder.localPath() + "A/a1";
  338. SyncJournalFileRecord a1record;
  339. fakeFolder.syncJournal().getFileRecord(QByteArray("A/a1"), &a1record);
  340. QCOMPARE(a1record._modtime, (qint64)FileSystem::getModTime(a1path));
  341. // Extra sync reads from db, no difference
  342. QVERIFY(fakeFolder.syncOnce());
  343. QCOMPARE(nGET, expectedGET);
  344. }
  345. /**
  346. * Checks whether SyncFileItems have the expected properties before start
  347. * of propagation.
  348. */
  349. void testSyncFileItemProperties()
  350. {
  351. auto initialMtime = QDateTime::currentDateTimeUtc().addDays(-7);
  352. auto changedMtime = QDateTime::currentDateTimeUtc().addDays(-4);
  353. auto changedMtime2 = QDateTime::currentDateTimeUtc().addDays(-3);
  354. // Base mtime with no ms content (filesystem is seconds only)
  355. initialMtime.setMSecsSinceEpoch(initialMtime.toMSecsSinceEpoch() / 1000 * 1000);
  356. changedMtime.setMSecsSinceEpoch(changedMtime.toMSecsSinceEpoch() / 1000 * 1000);
  357. changedMtime2.setMSecsSinceEpoch(changedMtime2.toMSecsSinceEpoch() / 1000 * 1000);
  358. // Ensure the initial mtimes are as expected
  359. auto initialFileInfo = FileInfo::A12_B12_C12_S12();
  360. initialFileInfo.setModTime("A/a1", initialMtime);
  361. initialFileInfo.setModTime("B/b1", initialMtime);
  362. initialFileInfo.setModTime("C/c1", initialMtime);
  363. FakeFolder fakeFolder{ initialFileInfo };
  364. // upload a
  365. fakeFolder.localModifier().appendByte("A/a1");
  366. fakeFolder.localModifier().setModTime("A/a1", changedMtime);
  367. // download b
  368. fakeFolder.remoteModifier().appendByte("B/b1");
  369. fakeFolder.remoteModifier().setModTime("B/b1", changedMtime);
  370. // conflict c
  371. fakeFolder.localModifier().appendByte("C/c1");
  372. fakeFolder.localModifier().appendByte("C/c1");
  373. fakeFolder.localModifier().setModTime("C/c1", changedMtime);
  374. fakeFolder.remoteModifier().appendByte("C/c1");
  375. fakeFolder.remoteModifier().setModTime("C/c1", changedMtime2);
  376. connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToPropagate, [&](SyncFileItemVector &items) {
  377. SyncFileItemPtr a1, b1, c1;
  378. for (auto &item : items) {
  379. if (item->_file == "A/a1")
  380. a1 = item;
  381. if (item->_file == "B/b1")
  382. b1 = item;
  383. if (item->_file == "C/c1")
  384. c1 = item;
  385. }
  386. // a1: should have local size and modtime
  387. QVERIFY(a1);
  388. QCOMPARE(a1->_instruction, CSYNC_INSTRUCTION_SYNC);
  389. QCOMPARE(a1->_direction, SyncFileItem::Up);
  390. QCOMPARE(a1->_size, qint64(5));
  391. QCOMPARE(Utility::qDateTimeFromTime_t(a1->_modtime), changedMtime);
  392. QCOMPARE(a1->_previousSize, qint64(4));
  393. QCOMPARE(Utility::qDateTimeFromTime_t(a1->_previousModtime), initialMtime);
  394. // b2: should have remote size and modtime
  395. QVERIFY(b1);
  396. QCOMPARE(b1->_instruction, CSYNC_INSTRUCTION_SYNC);
  397. QCOMPARE(b1->_direction, SyncFileItem::Down);
  398. QCOMPARE(b1->_size, qint64(17));
  399. QCOMPARE(Utility::qDateTimeFromTime_t(b1->_modtime), changedMtime);
  400. QCOMPARE(b1->_previousSize, qint64(16));
  401. QCOMPARE(Utility::qDateTimeFromTime_t(b1->_previousModtime), initialMtime);
  402. // c1: conflicts are downloads, so remote size and modtime
  403. QVERIFY(c1);
  404. QCOMPARE(c1->_instruction, CSYNC_INSTRUCTION_CONFLICT);
  405. QCOMPARE(c1->_direction, SyncFileItem::None);
  406. QCOMPARE(c1->_size, qint64(25));
  407. QCOMPARE(Utility::qDateTimeFromTime_t(c1->_modtime), changedMtime2);
  408. QCOMPARE(c1->_previousSize, qint64(26));
  409. QCOMPARE(Utility::qDateTimeFromTime_t(c1->_previousModtime), changedMtime);
  410. });
  411. QVERIFY(fakeFolder.syncOnce());
  412. }
  413. /**
  414. * Checks whether subsequent large uploads are skipped after a 507 error
  415. */
  416. void testInsufficientRemoteStorage()
  417. {
  418. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  419. // Disable parallel uploads
  420. SyncOptions syncOptions;
  421. syncOptions._parallelNetworkJobs = 0;
  422. fakeFolder.syncEngine().setSyncOptions(syncOptions);
  423. // Produce an error based on upload size
  424. int remoteQuota = 1000;
  425. int n507 = 0, nPUT = 0;
  426. QObject parent;
  427. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  428. if (op == QNetworkAccessManager::PutOperation) {
  429. nPUT++;
  430. if (request.rawHeader("OC-Total-Length").toInt() > remoteQuota) {
  431. n507++;
  432. return new FakeErrorReply(op, request, &parent, 507);
  433. }
  434. }
  435. return nullptr;
  436. });
  437. fakeFolder.localModifier().insert("A/big", 800);
  438. QVERIFY(fakeFolder.syncOnce());
  439. QCOMPARE(nPUT, 1);
  440. QCOMPARE(n507, 0);
  441. nPUT = 0;
  442. fakeFolder.localModifier().insert("A/big1", 500); // ok
  443. fakeFolder.localModifier().insert("A/big2", 1200); // 507 (quota guess now 1199)
  444. fakeFolder.localModifier().insert("A/big3", 1200); // skipped
  445. fakeFolder.localModifier().insert("A/big4", 1500); // skipped
  446. fakeFolder.localModifier().insert("A/big5", 1100); // 507 (quota guess now 1099)
  447. fakeFolder.localModifier().insert("A/big6", 900); // ok (quota guess now 199)
  448. fakeFolder.localModifier().insert("A/big7", 200); // skipped
  449. fakeFolder.localModifier().insert("A/big8", 199); // ok (quota guess now 0)
  450. fakeFolder.localModifier().insert("B/big8", 1150); // 507
  451. QVERIFY(!fakeFolder.syncOnce());
  452. QCOMPARE(nPUT, 6);
  453. QCOMPARE(n507, 3);
  454. }
  455. // Checks whether downloads with bad checksums are accepted
  456. void testChecksumValidation()
  457. {
  458. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  459. QObject parent;
  460. QByteArray checksumValue;
  461. QByteArray contentMd5Value;
  462. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  463. if (op == QNetworkAccessManager::GetOperation) {
  464. auto reply = new FakeGetReply(fakeFolder.remoteModifier(), op, request, &parent);
  465. if (!checksumValue.isNull())
  466. reply->setRawHeader("OC-Checksum", checksumValue);
  467. if (!contentMd5Value.isNull())
  468. reply->setRawHeader("Content-MD5", contentMd5Value);
  469. return reply;
  470. }
  471. return nullptr;
  472. });
  473. // Basic case
  474. fakeFolder.remoteModifier().create("A/a3", 16, 'A');
  475. QVERIFY(fakeFolder.syncOnce());
  476. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  477. // Bad OC-Checksum
  478. checksumValue = "SHA1:bad";
  479. fakeFolder.remoteModifier().create("A/a4", 16, 'A');
  480. QVERIFY(!fakeFolder.syncOnce());
  481. // Good OC-Checksum
  482. checksumValue = "SHA1:19b1928d58a2030d08023f3d7054516dbc186f20"; // printf 'A%.0s' {1..16} | sha1sum -
  483. QVERIFY(fakeFolder.syncOnce());
  484. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  485. checksumValue = QByteArray();
  486. // Bad Content-MD5
  487. contentMd5Value = "bad";
  488. fakeFolder.remoteModifier().create("A/a5", 16, 'A');
  489. QVERIFY(!fakeFolder.syncOnce());
  490. // Good Content-MD5
  491. contentMd5Value = "d8a73157ce10cd94a91c2079fc9a92c8"; // printf 'A%.0s' {1..16} | md5sum -
  492. QVERIFY(fakeFolder.syncOnce());
  493. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  494. // Invalid OC-Checksum is ignored
  495. checksumValue = "garbage";
  496. // contentMd5Value is still good
  497. fakeFolder.remoteModifier().create("A/a6", 16, 'A');
  498. QVERIFY(fakeFolder.syncOnce());
  499. contentMd5Value = "bad";
  500. fakeFolder.remoteModifier().create("A/a7", 16, 'A');
  501. QVERIFY(!fakeFolder.syncOnce());
  502. contentMd5Value.clear();
  503. QVERIFY(fakeFolder.syncOnce());
  504. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  505. // OC-Checksum contains Unsupported checksums
  506. checksumValue = "Unsupported:XXXX SHA1:invalid Invalid:XxX";
  507. fakeFolder.remoteModifier().create("A/a8", 16, 'A');
  508. QVERIFY(!fakeFolder.syncOnce()); // Since the supported SHA1 checksum is invalid, no download
  509. checksumValue = "Unsupported:XXXX SHA1:19b1928d58a2030d08023f3d7054516dbc186f20 Invalid:XxX";
  510. QVERIFY(fakeFolder.syncOnce()); // The supported SHA1 checksum is valid now, so the file are downloaded
  511. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  512. }
  513. // Tests the behavior of invalid filename detection
  514. void testInvalidFilenameRegex()
  515. {
  516. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  517. #ifndef Q_OS_WIN // We can't have local file with these character
  518. // For current servers, no characters are forbidden
  519. fakeFolder.syncEngine().account()->setServerVersion("10.0.0");
  520. fakeFolder.localModifier().insert("A/\\:?*\"<>|.txt");
  521. QVERIFY(fakeFolder.syncOnce());
  522. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  523. // For legacy servers, some characters were forbidden by the client
  524. fakeFolder.syncEngine().account()->setServerVersion("8.0.0");
  525. fakeFolder.localModifier().insert("B/\\:?*\"<>|.txt");
  526. QVERIFY(fakeFolder.syncOnce());
  527. QVERIFY(!fakeFolder.currentRemoteState().find("B/\\:?*\"<>|.txt"));
  528. #endif
  529. // We can override that by setting the capability
  530. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "invalidFilenameRegex", "" } } } });
  531. QVERIFY(fakeFolder.syncOnce());
  532. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  533. // Check that new servers also accept the capability
  534. fakeFolder.syncEngine().account()->setServerVersion("10.0.0");
  535. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "invalidFilenameRegex", "my[fgh]ile" } } } });
  536. fakeFolder.localModifier().insert("C/myfile.txt");
  537. QVERIFY(fakeFolder.syncOnce());
  538. QVERIFY(!fakeFolder.currentRemoteState().find("C/myfile.txt"));
  539. }
  540. void testDiscoveryHiddenFile()
  541. {
  542. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  543. QVERIFY(fakeFolder.syncOnce());
  544. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  545. // We can't depend on currentLocalState for hidden files since
  546. // it should rightfully skip things like download temporaries
  547. auto localFileExists = [&](QString name) {
  548. return QFileInfo(fakeFolder.localPath() + name).exists();
  549. };
  550. fakeFolder.syncEngine().setIgnoreHiddenFiles(true);
  551. fakeFolder.remoteModifier().insert("A/.hidden");
  552. fakeFolder.localModifier().insert("B/.hidden");
  553. QVERIFY(fakeFolder.syncOnce());
  554. QVERIFY(!localFileExists("A/.hidden"));
  555. QVERIFY(!fakeFolder.currentRemoteState().find("B/.hidden"));
  556. fakeFolder.syncEngine().setIgnoreHiddenFiles(false);
  557. fakeFolder.syncJournal().forceRemoteDiscoveryNextSync();
  558. QVERIFY(fakeFolder.syncOnce());
  559. QVERIFY(localFileExists("A/.hidden"));
  560. QVERIFY(fakeFolder.currentRemoteState().find("B/.hidden"));
  561. }
  562. void testNoLocalEncoding()
  563. {
  564. auto utf8Locale = QTextCodec::codecForLocale();
  565. if (utf8Locale->mibEnum() != 106) {
  566. QSKIP("Test only works for UTF8 locale");
  567. }
  568. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  569. QVERIFY(fakeFolder.syncOnce());
  570. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  571. // Utf8 locale can sync both
  572. fakeFolder.remoteModifier().insert("A/tößt");
  573. fakeFolder.remoteModifier().insert("A/t𠜎t");
  574. QVERIFY(fakeFolder.syncOnce());
  575. QVERIFY(fakeFolder.currentLocalState().find("A/tößt"));
  576. QVERIFY(fakeFolder.currentLocalState().find("A/t𠜎t"));
  577. #if !defined(Q_OS_MAC) && !defined(Q_OS_WIN)
  578. // Try again with a locale that can represent ö but not 𠜎 (4-byte utf8).
  579. QTextCodec::setCodecForLocale(QTextCodec::codecForName("ISO-8859-15"));
  580. QVERIFY(QTextCodec::codecForLocale()->mibEnum() == 111);
  581. fakeFolder.remoteModifier().insert("B/tößt");
  582. fakeFolder.remoteModifier().insert("B/t𠜎t");
  583. QVERIFY(fakeFolder.syncOnce());
  584. QVERIFY(fakeFolder.currentLocalState().find("B/tößt"));
  585. QVERIFY(!fakeFolder.currentLocalState().find("B/t𠜎t"));
  586. QVERIFY(!fakeFolder.currentLocalState().find("B/t?t"));
  587. QVERIFY(!fakeFolder.currentLocalState().find("B/t??t"));
  588. QVERIFY(!fakeFolder.currentLocalState().find("B/t???t"));
  589. QVERIFY(!fakeFolder.currentLocalState().find("B/t????t"));
  590. QVERIFY(fakeFolder.syncOnce());
  591. QVERIFY(fakeFolder.currentRemoteState().find("B/tößt"));
  592. QVERIFY(fakeFolder.currentRemoteState().find("B/t𠜎t"));
  593. // Try again with plain ascii
  594. QTextCodec::setCodecForLocale(QTextCodec::codecForName("ASCII"));
  595. QVERIFY(QTextCodec::codecForLocale()->mibEnum() == 3);
  596. fakeFolder.remoteModifier().insert("C/tößt");
  597. QVERIFY(fakeFolder.syncOnce());
  598. QVERIFY(!fakeFolder.currentLocalState().find("C/tößt"));
  599. QVERIFY(!fakeFolder.currentLocalState().find("C/t??t"));
  600. QVERIFY(!fakeFolder.currentLocalState().find("C/t????t"));
  601. QVERIFY(fakeFolder.syncOnce());
  602. QVERIFY(fakeFolder.currentRemoteState().find("C/tößt"));
  603. QTextCodec::setCodecForLocale(utf8Locale);
  604. #endif
  605. }
  606. // Aborting has had bugs when there are parallel upload jobs
  607. void testUploadV1Multiabort()
  608. {
  609. FakeFolder fakeFolder{ FileInfo{} };
  610. SyncOptions options;
  611. options._initialChunkSize = 10;
  612. options._maxChunkSize = 10;
  613. options._minChunkSize = 10;
  614. fakeFolder.syncEngine().setSyncOptions(options);
  615. QObject parent;
  616. int nPUT = 0;
  617. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  618. if (op == QNetworkAccessManager::PutOperation) {
  619. ++nPUT;
  620. return new FakeHangingReply(op, request, &parent);
  621. }
  622. return nullptr;
  623. });
  624. fakeFolder.localModifier().insert("file", 100, 'W');
  625. QTimer::singleShot(100, &fakeFolder.syncEngine(), [&]() { fakeFolder.syncEngine().abort(); });
  626. QVERIFY(!fakeFolder.syncOnce());
  627. QCOMPARE(nPUT, 3);
  628. }
  629. #ifndef Q_OS_WIN
  630. void testPropagatePermissions()
  631. {
  632. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  633. auto perm = QFileDevice::Permission(0x7704); // user/owner: rwx, group: r, other: -
  634. QFile::setPermissions(fakeFolder.localPath() + "A/a1", perm);
  635. QFile::setPermissions(fakeFolder.localPath() + "A/a2", perm);
  636. fakeFolder.syncOnce(); // get the metadata-only change out of the way
  637. fakeFolder.remoteModifier().appendByte("A/a1");
  638. fakeFolder.remoteModifier().appendByte("A/a2");
  639. fakeFolder.localModifier().appendByte("A/a2");
  640. fakeFolder.localModifier().appendByte("A/a2");
  641. fakeFolder.syncOnce(); // perms should be preserved
  642. QCOMPARE(QFileInfo(fakeFolder.localPath() + "A/a1").permissions(), perm);
  643. QCOMPARE(QFileInfo(fakeFolder.localPath() + "A/a2").permissions(), perm);
  644. auto conflictName = fakeFolder.syncJournal().conflictRecord(fakeFolder.syncJournal().conflictRecordPaths().first()).path;
  645. QVERIFY(conflictName.contains("A/a2"));
  646. QCOMPARE(QFileInfo(fakeFolder.localPath() + conflictName).permissions(), perm);
  647. }
  648. #endif
  649. void testEmptyLocalButHasRemote()
  650. {
  651. FakeFolder fakeFolder{ FileInfo{} };
  652. fakeFolder.remoteModifier().mkdir("foo");
  653. QVERIFY(fakeFolder.syncOnce());
  654. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  655. QVERIFY(fakeFolder.currentLocalState().find("foo"));
  656. }
  657. // Check that server mtime is set on directories on initial propagation
  658. void testDirectoryInitialMtime()
  659. {
  660. FakeFolder fakeFolder{ FileInfo{} };
  661. fakeFolder.remoteModifier().mkdir("foo");
  662. fakeFolder.remoteModifier().insert("foo/bar");
  663. auto datetime = QDateTime::currentDateTime();
  664. datetime.setSecsSinceEpoch(datetime.toSecsSinceEpoch()); // wipe ms
  665. fakeFolder.remoteModifier().find("foo")->lastModified = datetime;
  666. QVERIFY(fakeFolder.syncOnce());
  667. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  668. QCOMPARE(QFileInfo(fakeFolder.localPath() + "foo").lastModified(), datetime);
  669. }
  670. };
  671. QTEST_GUILESS_MAIN(TestSyncEngine)
  672. #include "testsyncengine.moc"