testsyncengine.cpp 32 KB

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