/* * This software is in the public domain, furnished "as is", without technical * support, and with no warranty, express or implied, as to its usefulness for * any purpose. * */ #include "syncenginetestutils.h" #include "syncengine.h" #include "propagatorjobs.h" #include "caseclashconflictsolver.h" #include using namespace OCC; namespace { QStringList findCaseClashConflicts(const FileInfo &dir) { QStringList conflicts; for (const auto &item : dir.children) { if (item.name.contains("(case clash from")) { conflicts.append(item.path()); } } return conflicts; } bool expectConflict(FileInfo state, const QString path) { PathComponents pathComponents(path); auto base = state.find(pathComponents.parentDirComponents()); if (!base) return false; for (const auto &item : qAsConst(base->children)) { if (item.name.startsWith(pathComponents.fileName()) && item.name.contains("(case clash from")) { return true; } } return false; } bool itemDidComplete(const ItemCompletedSpy &spy, const QString &path) { if (auto item = spy.findItem(path)) { return item->_instruction != CSYNC_INSTRUCTION_NONE && item->_instruction != CSYNC_INSTRUCTION_UPDATE_METADATA; } return false; } bool itemDidCompleteSuccessfully(const ItemCompletedSpy &spy, const QString &path) { if (auto item = spy.findItem(path)) { return item->_status == SyncFileItem::Success; } return false; } bool itemDidCompleteSuccessfullyWithExpectedRank(const ItemCompletedSpy &spy, const QString &path, int rank) { if (auto item = spy.findItemWithExpectedRank(path, rank)) { return item->_status == SyncFileItem::Success; } return false; } int itemSuccessfullyCompletedGetRank(const ItemCompletedSpy &spy, const QString &path) { auto itItem = std::find_if(spy.begin(), spy.end(), [&path] (auto currentItem) { auto item = currentItem[0].template value(); return item->destination() == path; }); if (itItem != spy.end()) { return itItem - spy.begin(); } return -1; } } class TestSyncEngine : public QObject { Q_OBJECT private slots: void testFileDownload() { FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; ItemCompletedSpy completeSpy(fakeFolder); fakeFolder.remoteModifier().insert("A/a0"); fakeFolder.syncOnce(); QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a0")); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); } void testFileUpload() { FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; ItemCompletedSpy completeSpy(fakeFolder); fakeFolder.localModifier().insert("A/a0"); fakeFolder.syncOnce(); QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a0")); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); } void testDirDownload() { FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; ItemCompletedSpy completeSpy(fakeFolder); fakeFolder.remoteModifier().mkdir("Y"); fakeFolder.remoteModifier().mkdir("Z"); fakeFolder.remoteModifier().insert("Z/d0"); fakeFolder.syncOnce(); QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Y")); QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z")); QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z/d0")); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); } void testDirUpload() { FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; ItemCompletedSpy completeSpy(fakeFolder); fakeFolder.localModifier().mkdir("Y"); fakeFolder.localModifier().mkdir("Z"); fakeFolder.localModifier().insert("Z/d0"); fakeFolder.syncOnce(); QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Y")); QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z")); QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z/d0")); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); } void testDirUploadWithDelayedAlgorithm() { FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"bulkupload", "1.0"} } } }); ItemCompletedSpy completeSpy(fakeFolder); fakeFolder.localModifier().mkdir("Y"); fakeFolder.localModifier().insert("Y/d0"); fakeFolder.localModifier().mkdir("Z"); fakeFolder.localModifier().insert("Z/d0"); fakeFolder.localModifier().insert("A/a0"); fakeFolder.localModifier().insert("B/b0"); fakeFolder.localModifier().insert("r0"); fakeFolder.localModifier().insert("r1"); fakeFolder.syncOnce(); QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "Y", 0)); QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "Z", 1)); QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Y/d0")); QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "Y/d0") > 1); QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z/d0")); QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "Z/d0") > 1); QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a0")); QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "A/a0") > 1); QVERIFY(itemDidCompleteSuccessfully(completeSpy, "B/b0")); QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "B/b0") > 1); QVERIFY(itemDidCompleteSuccessfully(completeSpy, "r0")); QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "r0") > 1); QVERIFY(itemDidCompleteSuccessfully(completeSpy, "r1")); QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "r1") > 1); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); } void testLocalDelete() { FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; ItemCompletedSpy completeSpy(fakeFolder); fakeFolder.remoteModifier().remove("A/a1"); fakeFolder.syncOnce(); QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a1")); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); } void testRemoteDelete() { FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; ItemCompletedSpy completeSpy(fakeFolder); fakeFolder.localModifier().remove("A/a1"); fakeFolder.syncOnce(); QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a1")); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); } void testLocalDeleteWithReuploadForNewLocalFiles() { FakeFolder fakeFolder{FileInfo{}}; // create folders hierarchy with some nested dirs and files fakeFolder.localModifier().mkdir("A"); fakeFolder.localModifier().insert("A/existingfile_A.txt", 100); fakeFolder.localModifier().mkdir("A/B"); fakeFolder.localModifier().insert("A/B/existingfile_B.data", 100); fakeFolder.localModifier().mkdir("A/B/C"); fakeFolder.localModifier().mkdir("A/B/C/c1"); fakeFolder.localModifier().mkdir("A/B/C/c1/c2"); fakeFolder.localModifier().insert("A/B/C/c1/c2/existingfile_C2.md", 100); QVERIFY(fakeFolder.syncOnce()); // make sure everything is uploaded QVERIFY(fakeFolder.currentRemoteState().find("A/B/C/c1/c2")); QVERIFY(fakeFolder.currentRemoteState().find("A/existingfile_A.txt")); QVERIFY(fakeFolder.currentRemoteState().find("A/B/existingfile_B.data")); QVERIFY(fakeFolder.currentRemoteState().find("A/B/C/c1/c2/existingfile_C2.md")); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); // remove a folder "A" on the server fakeFolder.remoteModifier().remove("A"); // put new files and folders into a local folder "A" fakeFolder.localModifier().insert("A/B/C/c1/c2/newfile.txt", 100); fakeFolder.localModifier().insert("A/B/C/c1/c2/Readme.data", 100); fakeFolder.localModifier().mkdir("A/B/C/c1/c2/newfiles"); fakeFolder.localModifier().insert("A/B/C/c1/c2/newfiles/newfile.txt", 100); fakeFolder.localModifier().insert("A/B/C/c1/c2/newfiles/Readme.data", 100); QVERIFY(fakeFolder.syncOnce()); // make sure new files and folders are uploaded (restored) QVERIFY(fakeFolder.currentLocalState().find("A/B/C/c1/c2")); QVERIFY(fakeFolder.currentLocalState().find("A/B/C/c1/c2/Readme.data")); QVERIFY(fakeFolder.currentLocalState().find("A/B/C/c1/c2/newfiles/newfile.txt")); QVERIFY(fakeFolder.currentLocalState().find("A/B/C/c1/c2/newfiles/Readme.data")); // and the old files are removed QVERIFY(!fakeFolder.currentLocalState().find("A/existingfile_A.txt")); QVERIFY(!fakeFolder.currentLocalState().find("A/B/existingfile_B.data")); QVERIFY(!fakeFolder.currentLocalState().find("A/B/C/c1/c2/existingfile_C2.md")); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); } void testEmlLocalChecksum() { FakeFolder fakeFolder{FileInfo{}}; fakeFolder.localModifier().insert("a1.eml", 64, 'A'); fakeFolder.localModifier().insert("a2.eml", 64, 'A'); fakeFolder.localModifier().insert("a3.eml", 64, 'A'); fakeFolder.localModifier().insert("b3.txt", 64, 'A'); // Upload and calculate the checksums // fakeFolder.syncOnce(); fakeFolder.syncOnce(); auto getDbChecksum = [&](QString path) { SyncJournalFileRecord record; [[maybe_unused]] const auto result = fakeFolder.syncJournal().getFileRecord(path, &record); return record._checksumHeader; }; // printf 'A%.0s' {1..64} | sha1sum - QByteArray referenceChecksum("SHA1:30b86e44e6001403827a62c58b08893e77cf121f"); QCOMPARE(getDbChecksum("a1.eml"), referenceChecksum); QCOMPARE(getDbChecksum("a2.eml"), referenceChecksum); QCOMPARE(getDbChecksum("a3.eml"), referenceChecksum); QCOMPARE(getDbChecksum("b3.txt"), referenceChecksum); ItemCompletedSpy completeSpy(fakeFolder); // Touch the file without changing the content, shouldn't upload fakeFolder.localModifier().setContents("a1.eml", 'A'); // Change the content/size fakeFolder.localModifier().setContents("a2.eml", 'B'); fakeFolder.localModifier().appendByte("a3.eml"); fakeFolder.localModifier().appendByte("b3.txt"); fakeFolder.syncOnce(); QCOMPARE(getDbChecksum("a1.eml"), referenceChecksum); QCOMPARE(getDbChecksum("a2.eml"), QByteArray("SHA1:84951fc23a4dafd10020ac349da1f5530fa65949")); QCOMPARE(getDbChecksum("a3.eml"), QByteArray("SHA1:826b7e7a7af8a529ae1c7443c23bf185c0ad440c")); QCOMPARE(getDbChecksum("b3.eml"), getDbChecksum("a3.txt")); QVERIFY(!itemDidComplete(completeSpy, "a1.eml")); QVERIFY(itemDidCompleteSuccessfully(completeSpy, "a2.eml")); QVERIFY(itemDidCompleteSuccessfully(completeSpy, "a3.eml")); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); } void testSelectiveSyncBug() { // issue owncloud/enterprise#1965: files from selective-sync ignored // folders are uploaded anyway is some circumstances. FakeFolder fakeFolder{FileInfo{ QString(), { FileInfo { QStringLiteral("parentFolder"), { FileInfo{ QStringLiteral("subFolderA"), { { QStringLiteral("fileA.txt"), 400 }, { QStringLiteral("fileB.txt"), 400, 'o' }, FileInfo { QStringLiteral("subsubFolder"), { { QStringLiteral("fileC.txt"), 400 }, { QStringLiteral("fileD.txt"), 400, 'o' } }}, FileInfo{ QStringLiteral("anotherFolder"), { FileInfo { QStringLiteral("emptyFolder"), { } }, FileInfo { QStringLiteral("subsubFolder"), { { QStringLiteral("fileE.txt"), 400 }, { QStringLiteral("fileF.txt"), 400, 'o' } }} }} }}, FileInfo{ QStringLiteral("subFolderB"), {} } }} }}}; QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); auto expectedServerState = fakeFolder.currentRemoteState(); // Remove subFolderA with selectiveSync: fakeFolder.syncEngine().journal()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, {"parentFolder/subFolderA/"}); fakeFolder.syncEngine().journal()->schedulePathForRemoteDiscovery(QByteArrayLiteral("parentFolder/subFolderA/")); auto getEtag = [&](const QByteArray &file) { SyncJournalFileRecord rec; [[maybe_unused]] const auto result = fakeFolder.syncJournal().getFileRecord(file, &rec); return rec._etag; }; QVERIFY(getEtag("parentFolder") == "_invalid_"); QVERIFY(getEtag("parentFolder/subFolderA") == "_invalid_"); QVERIFY(getEtag("parentFolder/subFolderA/subsubFolder") != "_invalid_"); // But touch local file before the next sync, such that the local folder // can't be removed fakeFolder.localModifier().setContents("parentFolder/subFolderA/fileB.txt", 'n'); fakeFolder.localModifier().setContents("parentFolder/subFolderA/subsubFolder/fileD.txt", 'n'); fakeFolder.localModifier().setContents("parentFolder/subFolderA/anotherFolder/subsubFolder/fileF.txt", 'n'); // Several follow-up syncs don't change the state at all, // in particular the remote state doesn't change and fileB.txt // isn't uploaded. for (int i = 0; i < 3; ++i) { fakeFolder.syncOnce(); { // Nothing changed on the server QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState); // The local state should still have subFolderA auto local = fakeFolder.currentLocalState(); QVERIFY(local.find("parentFolder/subFolderA")); QVERIFY(!local.find("parentFolder/subFolderA/fileA.txt")); QVERIFY(local.find("parentFolder/subFolderA/fileB.txt")); QVERIFY(!local.find("parentFolder/subFolderA/subsubFolder/fileC.txt")); QVERIFY(local.find("parentFolder/subFolderA/subsubFolder/fileD.txt")); QVERIFY(!local.find("parentFolder/subFolderA/anotherFolder/subsubFolder/fileE.txt")); QVERIFY(local.find("parentFolder/subFolderA/anotherFolder/subsubFolder/fileF.txt")); QVERIFY(!local.find("parentFolder/subFolderA/anotherFolder/emptyFolder")); QVERIFY(local.find("parentFolder/subFolderB")); } } } void abortAfterFailedMkdir() { FakeFolder fakeFolder{FileInfo{}}; QSignalSpy finishedSpy(&fakeFolder.syncEngine(), SIGNAL(finished(bool))); fakeFolder.serverErrorPaths().append("NewFolder"); fakeFolder.localModifier().mkdir("NewFolder"); // This should be aborted and would otherwise fail in FileInfo::create. fakeFolder.localModifier().insert("NewFolder/NewFile"); fakeFolder.syncOnce(); QCOMPARE(finishedSpy.size(), 1); QCOMPARE(finishedSpy.first().first().toBool(), false); } /** Verify that an incompletely propagated directory doesn't have the server's * etag stored in the database yet. */ void testDirEtagAfterIncompleteSync() { FakeFolder fakeFolder{FileInfo{}}; QSignalSpy finishedSpy(&fakeFolder.syncEngine(), SIGNAL(finished(bool))); fakeFolder.serverErrorPaths().append("NewFolder/foo"); fakeFolder.remoteModifier().mkdir("NewFolder"); fakeFolder.remoteModifier().insert("NewFolder/foo"); QVERIFY(!fakeFolder.syncOnce()); SyncJournalFileRecord rec; QVERIFY(fakeFolder.syncJournal().getFileRecord(QByteArrayLiteral("NewFolder"), &rec) && rec.isValid()); QCOMPARE(rec._etag, QByteArrayLiteral("_invalid_")); QVERIFY(!rec._fileId.isEmpty()); } void testDirDownloadWithError() { FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; ItemCompletedSpy completeSpy(fakeFolder); fakeFolder.remoteModifier().mkdir("Y"); fakeFolder.remoteModifier().mkdir("Y/Z"); fakeFolder.remoteModifier().insert("Y/Z/d0"); fakeFolder.remoteModifier().insert("Y/Z/d1"); fakeFolder.remoteModifier().insert("Y/Z/d2"); fakeFolder.remoteModifier().insert("Y/Z/d3"); fakeFolder.remoteModifier().insert("Y/Z/d4"); fakeFolder.remoteModifier().insert("Y/Z/d5"); fakeFolder.remoteModifier().insert("Y/Z/d6"); fakeFolder.remoteModifier().insert("Y/Z/d7"); fakeFolder.remoteModifier().insert("Y/Z/d8"); fakeFolder.remoteModifier().insert("Y/Z/d9"); fakeFolder.serverErrorPaths().append("Y/Z/d2", 503); fakeFolder.serverErrorPaths().append("Y/Z/d3", 503); QVERIFY(!fakeFolder.syncOnce()); QCoreApplication::processEvents(); // should not crash QSet seen; for(const QList &args : completeSpy) { auto item = args[0].value(); qDebug() << item->_file << item->isDirectory() << item->_status; QVERIFY(!seen.contains(item->_file)); // signal only sent once per item seen.insert(item->_file); if (item->_file == "Y/Z/d2") { QVERIFY(item->_status == SyncFileItem::NormalError); } else if (item->_file == "Y/Z/d3") { QVERIFY(item->_status != SyncFileItem::Success); } else if (!item->isDirectory()) { QVERIFY(item->_status == SyncFileItem::Success); } } } void testFakeConflict_data() { QTest::addColumn("sameMtime"); QTest::addColumn("checksums"); QTest::addColumn("expectedGET"); QTest::newRow("Same mtime, but no server checksum -> ignored in reconcile") << true << QByteArray() << 0; QTest::newRow("Same mtime, weak server checksum differ -> downloaded") << true << QByteArray("Adler32:bad") << 1; QTest::newRow("Same mtime, matching weak checksum -> skipped") << true << QByteArray("Adler32:2a2010d") << 0; QTest::newRow("Same mtime, strong server checksum differ -> downloaded") << true << QByteArray("SHA1:bad") << 1; QTest::newRow("Same mtime, matching strong checksum -> skipped") << true << QByteArray("SHA1:56900fb1d337cf7237ff766276b9c1e8ce507427") << 0; QTest::newRow("mtime changed, but no server checksum -> download") << false << QByteArray() << 1; QTest::newRow("mtime changed, weak checksum match -> download anyway") << false << QByteArray("Adler32:2a2010d") << 1; QTest::newRow("mtime changed, strong checksum match -> skip") << false << QByteArray("SHA1:56900fb1d337cf7237ff766276b9c1e8ce507427") << 0; } void testFakeConflict() { QFETCH(bool, sameMtime); QFETCH(QByteArray, checksums); QFETCH(int, expectedGET); FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; int nGET = 0; fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &, QIODevice *) { if (op == QNetworkAccessManager::GetOperation) ++nGET; return nullptr; }); // For directly editing the remote checksum auto &remoteInfo = fakeFolder.remoteModifier(); // Base mtime with no ms content (filesystem is seconds only) auto mtime = QDateTime::currentDateTimeUtc().addDays(-4); mtime.setMSecsSinceEpoch(mtime.toMSecsSinceEpoch() / 1000 * 1000); fakeFolder.localModifier().setContents("A/a1", 'C'); fakeFolder.localModifier().setModTime("A/a1", mtime); fakeFolder.remoteModifier().setContents("A/a1", 'C'); if (!sameMtime) mtime = mtime.addDays(1); fakeFolder.remoteModifier().setModTime("A/a1", mtime); remoteInfo.find("A/a1")->checksums = checksums; QVERIFY(fakeFolder.syncOnce()); QCOMPARE(nGET, expectedGET); // check that mtime in journal and filesystem agree QString a1path = fakeFolder.localPath() + "A/a1"; SyncJournalFileRecord a1record; QVERIFY(fakeFolder.syncJournal().getFileRecord(QByteArray("A/a1"), &a1record)); QCOMPARE(a1record._modtime, (qint64)FileSystem::getModTime(a1path)); // Extra sync reads from db, no difference QVERIFY(fakeFolder.syncOnce()); QCOMPARE(nGET, expectedGET); } /** * Checks whether SyncFileItems have the expected properties before start * of propagation. */ void testSyncFileItemProperties() { auto initialMtime = QDateTime::currentDateTimeUtc().addDays(-7); auto changedMtime = QDateTime::currentDateTimeUtc().addDays(-4); auto changedMtime2 = QDateTime::currentDateTimeUtc().addDays(-3); // Base mtime with no ms content (filesystem is seconds only) initialMtime.setMSecsSinceEpoch(initialMtime.toMSecsSinceEpoch() / 1000 * 1000); changedMtime.setMSecsSinceEpoch(changedMtime.toMSecsSinceEpoch() / 1000 * 1000); changedMtime2.setMSecsSinceEpoch(changedMtime2.toMSecsSinceEpoch() / 1000 * 1000); // Ensure the initial mtimes are as expected auto initialFileInfo = FileInfo::A12_B12_C12_S12(); initialFileInfo.setModTime("A/a1", initialMtime); initialFileInfo.setModTime("B/b1", initialMtime); initialFileInfo.setModTime("C/c1", initialMtime); FakeFolder fakeFolder{ initialFileInfo }; // upload a fakeFolder.localModifier().appendByte("A/a1"); fakeFolder.localModifier().setModTime("A/a1", changedMtime); // download b fakeFolder.remoteModifier().appendByte("B/b1"); fakeFolder.remoteModifier().setModTime("B/b1", changedMtime); // conflict c fakeFolder.localModifier().appendByte("C/c1"); fakeFolder.localModifier().appendByte("C/c1"); fakeFolder.localModifier().setModTime("C/c1", changedMtime); fakeFolder.remoteModifier().appendByte("C/c1"); fakeFolder.remoteModifier().setModTime("C/c1", changedMtime2); connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToPropagate, [&](SyncFileItemVector &items) { SyncFileItemPtr a1, b1, c1; for (auto &item : items) { if (item->_file == "A/a1") a1 = item; if (item->_file == "B/b1") b1 = item; if (item->_file == "C/c1") c1 = item; } // a1: should have local size and modtime QVERIFY(a1); QCOMPARE(a1->_instruction, CSYNC_INSTRUCTION_SYNC); QCOMPARE(a1->_direction, SyncFileItem::Up); QCOMPARE(a1->_size, qint64(5)); QCOMPARE(Utility::qDateTimeFromTime_t(a1->_modtime), changedMtime); QCOMPARE(a1->_previousSize, qint64(4)); QCOMPARE(Utility::qDateTimeFromTime_t(a1->_previousModtime), initialMtime); // b2: should have remote size and modtime QVERIFY(b1); QCOMPARE(b1->_instruction, CSYNC_INSTRUCTION_SYNC); QCOMPARE(b1->_direction, SyncFileItem::Down); QCOMPARE(b1->_size, qint64(17)); QCOMPARE(Utility::qDateTimeFromTime_t(b1->_modtime), changedMtime); QCOMPARE(b1->_previousSize, qint64(16)); QCOMPARE(Utility::qDateTimeFromTime_t(b1->_previousModtime), initialMtime); // c1: conflicts are downloads, so remote size and modtime QVERIFY(c1); QCOMPARE(c1->_instruction, CSYNC_INSTRUCTION_CONFLICT); QCOMPARE(c1->_direction, SyncFileItem::None); QCOMPARE(c1->_size, qint64(25)); QCOMPARE(Utility::qDateTimeFromTime_t(c1->_modtime), changedMtime2); QCOMPARE(c1->_previousSize, qint64(26)); QCOMPARE(Utility::qDateTimeFromTime_t(c1->_previousModtime), changedMtime); }); QVERIFY(fakeFolder.syncOnce()); } /** * Checks whether subsequent large uploads are skipped after a 507 error */ void testInsufficientRemoteStorage() { FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; // Disable parallel uploads SyncOptions syncOptions; syncOptions._parallelNetworkJobs = 0; fakeFolder.syncEngine().setSyncOptions(syncOptions); // Produce an error based on upload size int remoteQuota = 1000; int n507 = 0, nPUT = 0; QObject parent; fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * { Q_UNUSED(outgoingData) if (op == QNetworkAccessManager::PutOperation) { nPUT++; if (request.rawHeader("OC-Total-Length").toInt() > remoteQuota) { n507++; return new FakeErrorReply(op, request, &parent, 507); } } return nullptr; }); fakeFolder.localModifier().insert("A/big", 800); QVERIFY(fakeFolder.syncOnce()); QCOMPARE(nPUT, 1); QCOMPARE(n507, 0); nPUT = 0; fakeFolder.localModifier().insert("A/big1", 500); // ok fakeFolder.localModifier().insert("A/big2", 1200); // 507 (quota guess now 1199) fakeFolder.localModifier().insert("A/big3", 1200); // skipped fakeFolder.localModifier().insert("A/big4", 1500); // skipped fakeFolder.localModifier().insert("A/big5", 1100); // 507 (quota guess now 1099) fakeFolder.localModifier().insert("A/big6", 900); // ok (quota guess now 199) fakeFolder.localModifier().insert("A/big7", 200); // skipped fakeFolder.localModifier().insert("A/big8", 199); // ok (quota guess now 0) fakeFolder.localModifier().insert("B/big8", 1150); // 507 QVERIFY(!fakeFolder.syncOnce()); QCOMPARE(nPUT, 6); QCOMPARE(n507, 3); } // Checks whether downloads with bad checksums are accepted void testChecksumValidation() { FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; QObject parent; QByteArray checksumValue; QByteArray checksumValueRecalculated; QByteArray contentMd5Value; bool isChecksumRecalculateSupported = false; fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * { if (op == QNetworkAccessManager::GetOperation) { auto reply = new FakeGetReply(fakeFolder.remoteModifier(), op, request, &parent); if (!checksumValue.isNull()) reply->setRawHeader(OCC::checkSumHeaderC, checksumValue); if (!contentMd5Value.isNull()) reply->setRawHeader(OCC::contentMd5HeaderC, contentMd5Value); return reply; } else if (op == QNetworkAccessManager::CustomOperation) { if (request.hasRawHeader(OCC::checksumRecalculateOnServerHeaderC)) { if (!isChecksumRecalculateSupported) { return new FakeErrorReply(op, request, &parent, 402); } auto reply = new FakeGetReply(fakeFolder.remoteModifier(), op, request, &parent); reply->setRawHeader(OCC::checkSumHeaderC, checksumValueRecalculated); return reply; } } return nullptr; }); // Basic case fakeFolder.remoteModifier().create("A/a3", 16, 'A'); QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); // Bad OC-Checksum checksumValue = "SHA1:bad"; fakeFolder.remoteModifier().create("A/a4", 16, 'A'); QVERIFY(!fakeFolder.syncOnce()); const QByteArray matchedSha1Checksum(QByteArrayLiteral("SHA1:19b1928d58a2030d08023f3d7054516dbc186f20")); const QByteArray mismatchedSha1Checksum(matchedSha1Checksum.chopped(1)); // Good OC-Checksum checksumValue = matchedSha1Checksum; // printf 'A%.0s' {1..16} | sha1sum - QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); checksumValue = QByteArray(); // Bad Content-MD5 contentMd5Value = "bad"; fakeFolder.remoteModifier().create("A/a5", 16, 'A'); QVERIFY(!fakeFolder.syncOnce()); // Good Content-MD5 contentMd5Value = "d8a73157ce10cd94a91c2079fc9a92c8"; // printf 'A%.0s' {1..16} | md5sum - QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); // Invalid OC-Checksum is ignored checksumValue = "garbage"; // contentMd5Value is still good fakeFolder.remoteModifier().create("A/a6", 16, 'A'); QVERIFY(fakeFolder.syncOnce()); contentMd5Value = "bad"; fakeFolder.remoteModifier().create("A/a7", 16, 'A'); QVERIFY(!fakeFolder.syncOnce()); contentMd5Value.clear(); QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); // OC-Checksum contains Unsupported checksums checksumValue = "Unsupported:XXXX SHA1:invalid Invalid:XxX"; fakeFolder.remoteModifier().create("A/a8", 16, 'A'); QVERIFY(!fakeFolder.syncOnce()); // Since the supported SHA1 checksum is invalid, no download checksumValue = "Unsupported:XXXX SHA1:19b1928d58a2030d08023f3d7054516dbc186f20 Invalid:XxX"; QVERIFY(fakeFolder.syncOnce()); // The supported SHA1 checksum is valid now, so the file are downloaded QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); // Begin Test mismatch recalculation--------------------------------------------------------------------------------- const auto prevServerVersion = fakeFolder.account()->serverVersion(); fakeFolder.account()->setServerVersion(QString("%1.0.0").arg(fakeFolder.account()->checksumRecalculateServerVersionMinSupportedMajor())); // Mismatched OC-Checksum and X-Recalculate-Hash is not supported -> sync must fail isChecksumRecalculateSupported = false; checksumValue = mismatchedSha1Checksum; checksumValueRecalculated = matchedSha1Checksum; fakeFolder.remoteModifier().create("A/a9", 16, 'A'); QVERIFY(!fakeFolder.syncOnce()); // Mismatched OC-Checksum and X-Recalculate-Hash is supported, but, recalculated checksum is again mismatched -> sync must fail isChecksumRecalculateSupported = true; checksumValue = mismatchedSha1Checksum; checksumValueRecalculated = mismatchedSha1Checksum; QVERIFY(!fakeFolder.syncOnce()); // Mismatched OC-Checksum and X-Recalculate-Hash is supported, and, recalculated checksum is a match -> sync must succeed isChecksumRecalculateSupported = true; checksumValue = mismatchedSha1Checksum; checksumValueRecalculated = matchedSha1Checksum; QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); checksumValue = QByteArray(); fakeFolder.account()->setServerVersion(prevServerVersion); // End Test mismatch recalculation----------------------------------------------------------------------------------- } // Tests the behavior of invalid filename detection void testInvalidFilenameRegex() { FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; #ifndef Q_OS_WIN // We can't have local file with these character // For current servers, no characters are forbidden fakeFolder.syncEngine().account()->setServerVersion("10.0.0"); fakeFolder.localModifier().insert("A/\\:?*\"<>|.txt"); QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); // For legacy servers, some characters were forbidden by the client fakeFolder.syncEngine().account()->setServerVersion("8.0.0"); fakeFolder.localModifier().insert("B/\\:?*\"<>|.txt"); QVERIFY(fakeFolder.syncOnce()); QVERIFY(!fakeFolder.currentRemoteState().find("B/\\:?*\"<>|.txt")); #endif // We can override that by setting the capability fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "invalidFilenameRegex", "" } } } }); QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); // Check that new servers also accept the capability fakeFolder.syncEngine().account()->setServerVersion("10.0.0"); fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "invalidFilenameRegex", "my[fgh]ile" } } } }); fakeFolder.localModifier().insert("C/myfile.txt"); QVERIFY(fakeFolder.syncOnce()); QVERIFY(!fakeFolder.currentRemoteState().find("C/myfile.txt")); } void testDiscoveryHiddenFile() { FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); // We can't depend on currentLocalState for hidden files since // it should rightfully skip things like download temporaries auto localFileExists = [&](QString name) { return QFileInfo(fakeFolder.localPath() + name).exists(); }; fakeFolder.syncEngine().setIgnoreHiddenFiles(true); fakeFolder.remoteModifier().insert("A/.hidden"); fakeFolder.localModifier().insert("B/.hidden"); QVERIFY(fakeFolder.syncOnce()); QVERIFY(!localFileExists("A/.hidden")); QVERIFY(!fakeFolder.currentRemoteState().find("B/.hidden")); fakeFolder.syncEngine().setIgnoreHiddenFiles(false); fakeFolder.syncJournal().forceRemoteDiscoveryNextSync(); QVERIFY(fakeFolder.syncOnce()); QVERIFY(localFileExists("A/.hidden")); QVERIFY(fakeFolder.currentRemoteState().find("B/.hidden")); } void testNoLocalEncoding() { auto utf8Locale = QTextCodec::codecForLocale(); if (utf8Locale->mibEnum() != 106) { QSKIP("Test only works for UTF8 locale"); } FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); // Utf8 locale can sync both fakeFolder.remoteModifier().insert("A/tößt"); fakeFolder.remoteModifier().insert("A/t𠜎t"); QVERIFY(fakeFolder.syncOnce()); QVERIFY(fakeFolder.currentLocalState().find("A/tößt")); QVERIFY(fakeFolder.currentLocalState().find("A/t𠜎t")); #if !defined(Q_OS_MAC) && !defined(Q_OS_WIN) // Try again with a locale that can represent ö but not 𠜎 (4-byte utf8). QTextCodec::setCodecForLocale(QTextCodec::codecForName("ISO-8859-15")); QVERIFY(QTextCodec::codecForLocale()->mibEnum() == 111); fakeFolder.remoteModifier().insert("B/tößt"); fakeFolder.remoteModifier().insert("B/t𠜎t"); QVERIFY(fakeFolder.syncOnce()); QVERIFY(fakeFolder.currentLocalState().find("B/tößt")); QVERIFY(!fakeFolder.currentLocalState().find("B/t𠜎t")); QVERIFY(!fakeFolder.currentLocalState().find("B/t?t")); QVERIFY(!fakeFolder.currentLocalState().find("B/t??t")); QVERIFY(!fakeFolder.currentLocalState().find("B/t???t")); QVERIFY(!fakeFolder.currentLocalState().find("B/t????t")); QVERIFY(fakeFolder.syncOnce()); QVERIFY(fakeFolder.currentRemoteState().find("B/tößt")); QVERIFY(fakeFolder.currentRemoteState().find("B/t𠜎t")); // Try again with plain ascii QTextCodec::setCodecForLocale(QTextCodec::codecForName("ASCII")); QVERIFY(QTextCodec::codecForLocale()->mibEnum() == 3); fakeFolder.remoteModifier().insert("C/tößt"); QVERIFY(fakeFolder.syncOnce()); QVERIFY(!fakeFolder.currentLocalState().find("C/tößt")); QVERIFY(!fakeFolder.currentLocalState().find("C/t??t")); QVERIFY(!fakeFolder.currentLocalState().find("C/t????t")); QVERIFY(fakeFolder.syncOnce()); QVERIFY(fakeFolder.currentRemoteState().find("C/tößt")); QTextCodec::setCodecForLocale(utf8Locale); #endif } // Aborting has had bugs when there are parallel upload jobs void testUploadV1Multiabort() { FakeFolder fakeFolder{ FileInfo{} }; SyncOptions options; options._initialChunkSize = 10; options._maxChunkSize = 10; options._minChunkSize = 10; fakeFolder.syncEngine().setSyncOptions(options); QObject parent; int nPUT = 0; fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * { if (op == QNetworkAccessManager::PutOperation) { ++nPUT; return new FakeHangingReply(op, request, &parent); } return nullptr; }); fakeFolder.localModifier().insert("file", 100, 'W'); QTimer::singleShot(100, &fakeFolder.syncEngine(), [&]() { fakeFolder.syncEngine().abort(); }); QVERIFY(!fakeFolder.syncOnce()); QCOMPARE(nPUT, 3); } #ifndef Q_OS_WIN void testPropagatePermissions() { FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; auto perm = QFileDevice::Permission(0x7704); // user/owner: rwx, group: r, other: - QFile::setPermissions(fakeFolder.localPath() + "A/a1", perm); QFile::setPermissions(fakeFolder.localPath() + "A/a2", perm); fakeFolder.syncOnce(); // get the metadata-only change out of the way fakeFolder.remoteModifier().appendByte("A/a1"); fakeFolder.remoteModifier().appendByte("A/a2"); fakeFolder.localModifier().appendByte("A/a2"); fakeFolder.localModifier().appendByte("A/a2"); fakeFolder.syncOnce(); // perms should be preserved QCOMPARE(QFileInfo(fakeFolder.localPath() + "A/a1").permissions(), perm); QCOMPARE(QFileInfo(fakeFolder.localPath() + "A/a2").permissions(), perm); auto conflictName = fakeFolder.syncJournal().conflictRecord(fakeFolder.syncJournal().conflictRecordPaths().first()).path; QVERIFY(conflictName.contains("A/a2")); QCOMPARE(QFileInfo(fakeFolder.localPath() + conflictName).permissions(), perm); } #endif void testEmptyLocalButHasRemote() { FakeFolder fakeFolder{ FileInfo{} }; fakeFolder.remoteModifier().mkdir("foo"); QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); QVERIFY(fakeFolder.currentLocalState().find("foo")); } // Check that server mtime is set on directories on initial propagation void testDirectoryInitialMtime() { FakeFolder fakeFolder{ FileInfo{} }; fakeFolder.remoteModifier().mkdir("foo"); fakeFolder.remoteModifier().insert("foo/bar"); auto datetime = QDateTime::currentDateTime(); datetime.setSecsSinceEpoch(datetime.toSecsSinceEpoch()); // wipe ms fakeFolder.remoteModifier().find("foo")->lastModified = datetime; QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); QCOMPARE(QFileInfo(fakeFolder.localPath() + "foo").lastModified(), datetime); } /** * Checks whether subsequent large uploads are skipped after a 507 error */ void testErrorsWithBulkUpload() { FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"bulkupload", "1.0"} } } }); // Disable parallel uploads SyncOptions syncOptions; syncOptions._parallelNetworkJobs = 0; fakeFolder.syncEngine().setSyncOptions(syncOptions); int nPUT = 0; int nPOST = 0; fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * { auto contentType = request.header(QNetworkRequest::ContentTypeHeader).toString(); if (op == QNetworkAccessManager::PostOperation) { ++nPOST; if (contentType.startsWith(QStringLiteral("multipart/related; boundary="))) { auto jsonReplyObject = fakeFolder.forEachReplyPart(outgoingData, contentType, [] (const QMap &allHeaders) -> QJsonObject { auto reply = QJsonObject{}; const auto fileName = allHeaders[QStringLiteral("X-File-Path")]; if (fileName.endsWith("A/big2") || fileName.endsWith("A/big3") || fileName.endsWith("A/big4") || fileName.endsWith("A/big5") || fileName.endsWith("A/big7") || fileName.endsWith("B/big8")) { reply.insert(QStringLiteral("error"), true); reply.insert(QStringLiteral("etag"), {}); return reply; } else { reply.insert(QStringLiteral("error"), false); reply.insert(QStringLiteral("etag"), {}); } return reply; }); if (jsonReplyObject.size()) { auto jsonReply = QJsonDocument{}; jsonReply.setObject(jsonReplyObject); return new FakeJsonErrorReply{op, request, this, 200, jsonReply}; } return nullptr; } } else if (op == QNetworkAccessManager::PutOperation) { ++nPUT; const auto fileName = getFilePathFromUrl(request.url()); if (fileName.endsWith("A/big2") || fileName.endsWith("A/big3") || fileName.endsWith("A/big4") || fileName.endsWith("A/big5") || fileName.endsWith("A/big7") || fileName.endsWith("B/big8")) { return new FakeErrorReply(op, request, this, 412); } return nullptr; } return nullptr; }); fakeFolder.localModifier().insert("A/big", 1); QVERIFY(fakeFolder.syncOnce()); QCOMPARE(nPUT, 0); QCOMPARE(nPOST, 1); nPUT = 0; nPOST = 0; fakeFolder.localModifier().insert("A/big1", 1); // ok fakeFolder.localModifier().insert("A/big2", 1); // ko fakeFolder.localModifier().insert("A/big3", 1); // ko fakeFolder.localModifier().insert("A/big4", 1); // ko fakeFolder.localModifier().insert("A/big5", 1); // ko fakeFolder.localModifier().insert("A/big6", 1); // ok fakeFolder.localModifier().insert("A/big7", 1); // ko fakeFolder.localModifier().insert("A/big8", 1); // ok fakeFolder.localModifier().insert("B/big8", 1); // ko QVERIFY(!fakeFolder.syncOnce()); QCOMPARE(nPUT, 0); QCOMPARE(nPOST, 1); nPUT = 0; nPOST = 0; QVERIFY(!fakeFolder.syncOnce()); QCOMPARE(nPUT, 6); QCOMPARE(nPOST, 0); } /** * Checks whether subsequent large uploads are skipped after a 507 error */ void testNetworkErrorsWithBulkUpload() { FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"bulkupload", "1.0"} } } }); // Disable parallel uploads SyncOptions syncOptions; syncOptions._parallelNetworkJobs = 0; fakeFolder.syncEngine().setSyncOptions(syncOptions); int nPUT = 0; int nPOST = 0; fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * { auto contentType = request.header(QNetworkRequest::ContentTypeHeader).toString(); if (op == QNetworkAccessManager::PostOperation) { ++nPOST; if (contentType.startsWith(QStringLiteral("multipart/related; boundary="))) { return new FakeErrorReply(op, request, this, 400); } return nullptr; } else if (op == QNetworkAccessManager::PutOperation) { ++nPUT; } return nullptr; }); fakeFolder.localModifier().insert("A/big1", 1); fakeFolder.localModifier().insert("A/big2", 1); fakeFolder.localModifier().insert("A/big3", 1); fakeFolder.localModifier().insert("A/big4", 1); fakeFolder.localModifier().insert("A/big5", 1); fakeFolder.localModifier().insert("A/big6", 1); fakeFolder.localModifier().insert("A/big7", 1); fakeFolder.localModifier().insert("A/big8", 1); fakeFolder.localModifier().insert("B/big8", 1); QVERIFY(!fakeFolder.syncOnce()); QCOMPARE(nPUT, 0); QCOMPARE(nPOST, 1); nPUT = 0; nPOST = 0; QVERIFY(fakeFolder.syncOnce()); QCOMPARE(nPUT, 9); QCOMPARE(nPOST, 0); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); } void testRemoteMoveFailedInsufficientStorageLocalMoveRolledBack() { FakeFolder fakeFolder{FileInfo{}}; // create a big shared folder with some files fakeFolder.remoteModifier().mkdir("big_shared_folder"); fakeFolder.remoteModifier().mkdir("big_shared_folder/shared_files"); fakeFolder.remoteModifier().insert("big_shared_folder/shared_files/big_shared_file_A.data", 1000); fakeFolder.remoteModifier().insert("big_shared_folder/shared_files/big_shared_file_B.data", 1000); // make sure big shared folder is synced QVERIFY(fakeFolder.syncOnce()); QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_A.data")); QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_B.data")); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); // try to move from a big shared folder to your own folder fakeFolder.localModifier().mkdir("own_folder"); fakeFolder.localModifier().rename( "big_shared_folder/shared_files/big_shared_file_A.data", "own_folder/big_shared_file_A.data"); fakeFolder.localModifier().rename( "big_shared_folder/shared_files/big_shared_file_B.data", "own_folder/big_shared_file_B.data"); // emulate server MOVE 507 error QObject parent; fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * { Q_UNUSED(outgoingData) if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("MOVE")) { return new FakeErrorReply(op, request, &parent, 507); } return nullptr; }); // make sure the first sync failes and files get restored to original folder QVERIFY(!fakeFolder.syncOnce()); QVERIFY(fakeFolder.syncOnce()); QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_A.data")); QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_B.data")); QVERIFY(!fakeFolder.currentLocalState().find("own_folder/big_shared_file_A.data")); QVERIFY(!fakeFolder.currentLocalState().find("own_folder/big_shared_file_B.data")); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); } void testRemoteMoveFailedForbiddenLocalMoveRolledBack() { FakeFolder fakeFolder{FileInfo{}}; // create a big shared folder with some files fakeFolder.remoteModifier().mkdir("big_shared_folder"); fakeFolder.remoteModifier().mkdir("big_shared_folder/shared_files"); fakeFolder.remoteModifier().insert("big_shared_folder/shared_files/big_shared_file_A.data", 1000); fakeFolder.remoteModifier().insert("big_shared_folder/shared_files/big_shared_file_B.data", 1000); // make sure big shared folder is synced QVERIFY(fakeFolder.syncOnce()); QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_A.data")); QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_B.data")); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); // try to move from a big shared folder to your own folder fakeFolder.localModifier().mkdir("own_folder"); fakeFolder.localModifier().rename( "big_shared_folder/shared_files/big_shared_file_A.data", "own_folder/big_shared_file_A.data"); fakeFolder.localModifier().rename( "big_shared_folder/shared_files/big_shared_file_B.data", "own_folder/big_shared_file_B.data"); // emulate server MOVE 507 error QObject parent; fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * { Q_UNUSED(outgoingData) auto attributeCustomVerb = request.attribute(QNetworkRequest::CustomVerbAttribute).toString(); if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("MOVE")) { return new FakeErrorReply(op, request, &parent, 403); } return nullptr; }); // make sure the first sync failes and files get restored to original folder QVERIFY(!fakeFolder.syncOnce()); QVERIFY(fakeFolder.syncOnce()); QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_A.data")); QVERIFY(fakeFolder.currentLocalState().find("big_shared_folder/shared_files/big_shared_file_B.data")); QVERIFY(!fakeFolder.currentLocalState().find("own_folder/big_shared_file_A.data")); QVERIFY(!fakeFolder.currentLocalState().find("own_folder/big_shared_file_B.data")); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); } void testFolderWithFilesInError() { FakeFolder fakeFolder{FileInfo{}}; fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * { Q_UNUSED(outgoingData) if (op == QNetworkAccessManager::GetOperation) { const auto fileName = getFilePathFromUrl(request.url()); if (fileName == QStringLiteral("aaa/subfolder/foo")) { return new FakeErrorReply(op, request, &fakeFolder.syncEngine(), 403); } } return nullptr; }); fakeFolder.remoteModifier().mkdir(QStringLiteral("aaa")); fakeFolder.remoteModifier().mkdir(QStringLiteral("aaa/subfolder")); fakeFolder.remoteModifier().insert(QStringLiteral("aaa/subfolder/bar")); QVERIFY(fakeFolder.syncOnce()); fakeFolder.remoteModifier().insert(QStringLiteral("aaa/subfolder/foo")); QVERIFY(!fakeFolder.syncOnce()); QVERIFY(!fakeFolder.syncOnce()); } void testInvalidMtimeRecoveryAtStart() { constexpr auto INVALID_MTIME = 0; constexpr auto CURRENT_MTIME = 1646057277; FakeFolder fakeFolder{FileInfo{}}; QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); const QString fooFileRootFolder("foo"); const QString barFileRootFolder("bar"); const QString fooFileSubFolder("subfolder/foo"); const QString barFileSubFolder("subfolder/bar"); const QString fooFileAaaSubFolder("aaa/subfolder/foo"); const QString barFileAaaSubFolder("aaa/subfolder/bar"); fakeFolder.remoteModifier().insert(fooFileRootFolder); fakeFolder.remoteModifier().insert(barFileRootFolder); fakeFolder.remoteModifier().mkdir(QStringLiteral("subfolder")); fakeFolder.remoteModifier().insert(fooFileSubFolder); fakeFolder.remoteModifier().insert(barFileSubFolder); fakeFolder.remoteModifier().mkdir(QStringLiteral("aaa")); fakeFolder.remoteModifier().mkdir(QStringLiteral("aaa/subfolder")); fakeFolder.remoteModifier().insert(fooFileAaaSubFolder); fakeFolder.remoteModifier().setModTime(fooFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(INVALID_MTIME)); fakeFolder.remoteModifier().insert(barFileAaaSubFolder); fakeFolder.remoteModifier().setModTime(barFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(INVALID_MTIME)); QVERIFY(!fakeFolder.syncOnce()); QVERIFY(!fakeFolder.syncOnce()); fakeFolder.remoteModifier().setModTimeKeepEtag(fooFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(CURRENT_MTIME)); fakeFolder.remoteModifier().setModTimeKeepEtag(barFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(CURRENT_MTIME)); QVERIFY(fakeFolder.syncOnce()); QVERIFY(fakeFolder.syncOnce()); auto expectedState = fakeFolder.currentLocalState(); QCOMPARE(fakeFolder.currentRemoteState(), expectedState); } void testInvalidMtimeRecovery() { constexpr auto INVALID_MTIME = 0; constexpr auto CURRENT_MTIME = 1646057277; FakeFolder fakeFolder{FileInfo{}}; QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); const QString fooFileRootFolder("foo"); const QString barFileRootFolder("bar"); const QString fooFileSubFolder("subfolder/foo"); const QString barFileSubFolder("subfolder/bar"); const QString fooFileAaaSubFolder("aaa/subfolder/foo"); const QString barFileAaaSubFolder("aaa/subfolder/bar"); fakeFolder.remoteModifier().insert(fooFileRootFolder); fakeFolder.remoteModifier().insert(barFileRootFolder); fakeFolder.remoteModifier().mkdir(QStringLiteral("subfolder")); fakeFolder.remoteModifier().insert(fooFileSubFolder); fakeFolder.remoteModifier().insert(barFileSubFolder); fakeFolder.remoteModifier().mkdir(QStringLiteral("aaa")); fakeFolder.remoteModifier().mkdir(QStringLiteral("aaa/subfolder")); fakeFolder.remoteModifier().insert(fooFileAaaSubFolder); fakeFolder.remoteModifier().insert(barFileAaaSubFolder); QVERIFY(fakeFolder.syncOnce()); fakeFolder.remoteModifier().setModTime(fooFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(INVALID_MTIME)); fakeFolder.remoteModifier().setModTime(barFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(INVALID_MTIME)); QVERIFY(!fakeFolder.syncOnce()); QVERIFY(!fakeFolder.syncOnce()); fakeFolder.remoteModifier().setModTimeKeepEtag(fooFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(CURRENT_MTIME)); fakeFolder.remoteModifier().setModTimeKeepEtag(barFileAaaSubFolder, QDateTime::fromSecsSinceEpoch(CURRENT_MTIME)); QVERIFY(fakeFolder.syncOnce()); QVERIFY(fakeFolder.syncOnce()); auto expectedState = fakeFolder.currentLocalState(); QCOMPARE(fakeFolder.currentRemoteState(), expectedState); } void testServerUpdatingMTimeShouldNotCreateConflicts() { constexpr auto testFile = "test.txt"; constexpr auto CURRENT_MTIME = 1646057277; FakeFolder fakeFolder{ FileInfo{} }; fakeFolder.remoteModifier().insert(testFile); fakeFolder.remoteModifier().setModTimeKeepEtag(testFile, QDateTime::fromSecsSinceEpoch(CURRENT_MTIME - 2)); fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem); QVERIFY(fakeFolder.syncOnce()); auto localState = fakeFolder.currentLocalState(); QCOMPARE(localState, fakeFolder.currentRemoteState()); QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState())); const auto fileFirstSync = localState.find(testFile); QVERIFY(fileFirstSync); QCOMPARE(fileFirstSync->lastModified.toSecsSinceEpoch(), CURRENT_MTIME - 2); fakeFolder.remoteModifier().setModTimeKeepEtag(testFile, QDateTime::fromSecsSinceEpoch(CURRENT_MTIME - 1)); fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::FilesystemOnly); QVERIFY(fakeFolder.syncOnce()); localState = fakeFolder.currentLocalState(); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState())); const auto fileSecondSync = localState.find(testFile); QVERIFY(fileSecondSync); QCOMPARE(fileSecondSync->lastModified.toSecsSinceEpoch(), CURRENT_MTIME - 1); fakeFolder.remoteModifier().setModTime(testFile, QDateTime::fromSecsSinceEpoch(CURRENT_MTIME)); fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::FilesystemOnly); QVERIFY(fakeFolder.syncOnce()); localState = fakeFolder.currentLocalState(); QCOMPARE(localState, fakeFolder.currentRemoteState()); QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState())); const auto fileThirdSync = localState.find(testFile); QVERIFY(fileThirdSync); QCOMPARE(fileThirdSync->lastModified.toSecsSinceEpoch(), CURRENT_MTIME); } void testFolderRemovalWithCaseClash() { FakeFolder fakeFolder{ FileInfo{} }; fakeFolder.remoteModifier().mkdir("A"); fakeFolder.remoteModifier().mkdir("toDelete"); fakeFolder.remoteModifier().insert("A/file"); QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); fakeFolder.remoteModifier().insert("A/FILE"); QVERIFY(fakeFolder.syncOnce()); fakeFolder.remoteModifier().mkdir("a"); fakeFolder.remoteModifier().remove("toDelete"); QVERIFY(fakeFolder.syncOnce()); auto folderA = fakeFolder.currentLocalState().find("toDelete"); QCOMPARE(folderA, nullptr); } void testServer_caseClash_createConflict() { constexpr auto testLowerCaseFile = "test"; constexpr auto testUpperCaseFile = "TEST"; #if defined Q_OS_LINUX constexpr auto shouldHaveCaseClashConflict = false; #else constexpr auto shouldHaveCaseClashConflict = true; #endif FakeFolder fakeFolder{ FileInfo{} }; fakeFolder.remoteModifier().insert("otherFile.txt"); fakeFolder.remoteModifier().insert(testLowerCaseFile); fakeFolder.remoteModifier().insert(testUpperCaseFile); fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem); QVERIFY(fakeFolder.syncOnce()); auto conflicts = findCaseClashConflicts(fakeFolder.currentLocalState()); QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0); const auto hasConflict = expectConflict(fakeFolder.currentLocalState(), testLowerCaseFile); QCOMPARE(hasConflict, shouldHaveCaseClashConflict ? true : false); fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem); QVERIFY(fakeFolder.syncOnce()); conflicts = findCaseClashConflicts(fakeFolder.currentLocalState()); QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0); } void testServer_subFolderCaseClash_createConflict() { constexpr auto testLowerCaseFile = "a/b/test"; constexpr auto testUpperCaseFile = "a/b/TEST"; #if defined Q_OS_LINUX constexpr auto shouldHaveCaseClashConflict = false; #else constexpr auto shouldHaveCaseClashConflict = true; #endif FakeFolder fakeFolder{ FileInfo{} }; fakeFolder.remoteModifier().mkdir("a"); fakeFolder.remoteModifier().mkdir("a/b"); fakeFolder.remoteModifier().insert("a/b/otherFile.txt"); fakeFolder.remoteModifier().insert(testLowerCaseFile); fakeFolder.remoteModifier().insert(testUpperCaseFile); fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem); QVERIFY(fakeFolder.syncOnce()); auto conflicts = findCaseClashConflicts(*fakeFolder.currentLocalState().find("a/b")); QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0); const auto hasConflict = expectConflict(fakeFolder.currentLocalState(), testLowerCaseFile); QCOMPARE(hasConflict, shouldHaveCaseClashConflict ? true : false); fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem); QVERIFY(fakeFolder.syncOnce()); conflicts = findCaseClashConflicts(*fakeFolder.currentLocalState().find("a/b")); QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0); } void testServer_caseClash_createConflictOnMove() { constexpr auto testLowerCaseFile = "test"; constexpr auto testUpperCaseFile = "TEST2"; constexpr auto testUpperCaseFileAfterMove = "TEST"; #if defined Q_OS_LINUX constexpr auto shouldHaveCaseClashConflict = false; #else constexpr auto shouldHaveCaseClashConflict = true; #endif FakeFolder fakeFolder{ FileInfo{} }; fakeFolder.remoteModifier().insert("otherFile.txt"); fakeFolder.remoteModifier().insert(testLowerCaseFile); fakeFolder.remoteModifier().insert(testUpperCaseFile); fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem); QVERIFY(fakeFolder.syncOnce()); auto conflicts = findCaseClashConflicts(fakeFolder.currentLocalState()); QCOMPARE(conflicts.size(), 0); const auto hasConflict = expectConflict(fakeFolder.currentLocalState(), testLowerCaseFile); QCOMPARE(hasConflict, false); fakeFolder.remoteModifier().rename(testUpperCaseFile, testUpperCaseFileAfterMove); fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem); QVERIFY(fakeFolder.syncOnce()); conflicts = findCaseClashConflicts(fakeFolder.currentLocalState()); QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0); const auto hasConflictAfterMove = expectConflict(fakeFolder.currentLocalState(), testUpperCaseFileAfterMove); QCOMPARE(hasConflictAfterMove, shouldHaveCaseClashConflict ? true : false); fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem); QVERIFY(fakeFolder.syncOnce()); conflicts = findCaseClashConflicts(fakeFolder.currentLocalState()); QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0); } void testServer_subFolderCaseClash_createConflictOnMove() { constexpr auto testLowerCaseFile = "a/b/test"; constexpr auto testUpperCaseFile = "a/b/TEST2"; constexpr auto testUpperCaseFileAfterMove = "a/b/TEST"; #if defined Q_OS_LINUX constexpr auto shouldHaveCaseClashConflict = false; #else constexpr auto shouldHaveCaseClashConflict = true; #endif FakeFolder fakeFolder{ FileInfo{} }; fakeFolder.remoteModifier().mkdir("a"); fakeFolder.remoteModifier().mkdir("a/b"); fakeFolder.remoteModifier().insert("a/b/otherFile.txt"); fakeFolder.remoteModifier().insert(testLowerCaseFile); fakeFolder.remoteModifier().insert(testUpperCaseFile); fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem); QVERIFY(fakeFolder.syncOnce()); auto conflicts = findCaseClashConflicts(*fakeFolder.currentLocalState().find("a/b")); QCOMPARE(conflicts.size(), 0); const auto hasConflict = expectConflict(fakeFolder.currentLocalState(), testLowerCaseFile); QCOMPARE(hasConflict, false); fakeFolder.remoteModifier().rename(testUpperCaseFile, testUpperCaseFileAfterMove); fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem); QVERIFY(fakeFolder.syncOnce()); conflicts = findCaseClashConflicts(*fakeFolder.currentLocalState().find("a/b")); QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0); const auto hasConflictAfterMove = expectConflict(fakeFolder.currentLocalState(), testUpperCaseFileAfterMove); QCOMPARE(hasConflictAfterMove, shouldHaveCaseClashConflict ? true : false); fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem); QVERIFY(fakeFolder.syncOnce()); conflicts = findCaseClashConflicts(*fakeFolder.currentLocalState().find("a/b")); QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0); } void testServer_caseClash_createConflictAndSolveIt() { constexpr auto testLowerCaseFile = "test"; constexpr auto testUpperCaseFile = "TEST"; #if defined Q_OS_LINUX constexpr auto shouldHaveCaseClashConflict = false; #else constexpr auto shouldHaveCaseClashConflict = true; #endif FakeFolder fakeFolder{ FileInfo{} }; fakeFolder.remoteModifier().insert("otherFile.txt"); fakeFolder.remoteModifier().insert(testLowerCaseFile); fakeFolder.remoteModifier().insert(testUpperCaseFile); fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem); QVERIFY(fakeFolder.syncOnce()); auto conflicts = findCaseClashConflicts(fakeFolder.currentLocalState()); QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0); const auto hasConflict = expectConflict(fakeFolder.currentLocalState(), testLowerCaseFile); QCOMPARE(hasConflict, shouldHaveCaseClashConflict ? true : false); fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem); QVERIFY(fakeFolder.syncOnce()); conflicts = findCaseClashConflicts(fakeFolder.currentLocalState()); QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0); if (shouldHaveCaseClashConflict) { const auto conflictFileName = QString{conflicts.constFirst()}; qDebug() << conflictFileName; CaseClashConflictSolver conflictSolver(fakeFolder.localPath() + testLowerCaseFile, fakeFolder.localPath() + conflictFileName, QStringLiteral("/"), fakeFolder.localPath(), fakeFolder.account(), &fakeFolder.syncJournal()); QSignalSpy conflictSolverDone(&conflictSolver, &CaseClashConflictSolver::done); QSignalSpy conflictSolverFailed(&conflictSolver, &CaseClashConflictSolver::failed); conflictSolver.solveConflict("test2"); QVERIFY(conflictSolverDone.wait()); QVERIFY(fakeFolder.syncOnce()); conflicts = findCaseClashConflicts(fakeFolder.currentLocalState()); QCOMPARE(conflicts.size(), 0); } } void testServer_subFolderCaseClash_createConflictAndSolveIt() { constexpr auto testLowerCaseFile = "a/b/test"; constexpr auto testUpperCaseFile = "a/b/TEST"; #if defined Q_OS_LINUX constexpr auto shouldHaveCaseClashConflict = false; #else constexpr auto shouldHaveCaseClashConflict = true; #endif FakeFolder fakeFolder{ FileInfo{} }; fakeFolder.remoteModifier().mkdir("a"); fakeFolder.remoteModifier().mkdir("a/b"); fakeFolder.remoteModifier().insert("a/b/otherFile.txt"); fakeFolder.remoteModifier().insert(testLowerCaseFile); fakeFolder.remoteModifier().insert(testUpperCaseFile); fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem); QVERIFY(fakeFolder.syncOnce()); auto conflicts = findCaseClashConflicts(*fakeFolder.currentLocalState().find("a/b")); QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0); const auto hasConflict = expectConflict(fakeFolder.currentLocalState(), testLowerCaseFile); QCOMPARE(hasConflict, shouldHaveCaseClashConflict ? true : false); fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem); QVERIFY(fakeFolder.syncOnce()); conflicts = findCaseClashConflicts(*fakeFolder.currentLocalState().find("a/b")); QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0); if (shouldHaveCaseClashConflict) { CaseClashConflictSolver conflictSolver(fakeFolder.localPath() + testLowerCaseFile, fakeFolder.localPath() + conflicts.constFirst(), QStringLiteral("/"), fakeFolder.localPath(), fakeFolder.account(), &fakeFolder.syncJournal()); QSignalSpy conflictSolverDone(&conflictSolver, &CaseClashConflictSolver::done); QSignalSpy conflictSolverFailed(&conflictSolver, &CaseClashConflictSolver::failed); conflictSolver.solveConflict("a/b/test2"); QVERIFY(conflictSolverDone.wait()); QVERIFY(fakeFolder.syncOnce()); conflicts = findCaseClashConflicts(*fakeFolder.currentLocalState().find("a/b")); QCOMPARE(conflicts.size(), 0); } } }; QTEST_GUILESS_MAIN(TestSyncEngine) #include "testsyncengine.moc"