/* * 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 #include "syncenginetestutils.h" #include #include using namespace OCC; static void changeAllFileId(FileInfo &info) { info.fileId = generateFileId(); if (!info.isDir) return; info.etag = generateEtag(); for (auto &child : info.children) { changeAllFileId(child); } } /* * This test ensure that the SyncEngine::aboutToRemoveAllFiles is correctly called and that when * we the user choose to remove all files SyncJournalDb::clearFileTable makes works as expected */ class TestAllFilesDeleted : public QObject { Q_OBJECT private slots: void initTestCase() { OCC::Logger::instance()->setLogFlush(true); OCC::Logger::instance()->setLogDebug(true); QStandardPaths::setTestModeEnabled(true); } void testAllFilesDeletedKeep_data() { QTest::addColumn("deleteOnRemote"); QTest::newRow("local") << false; QTest::newRow("remote") << true; } /* * In this test, all files are deleted in the client, or the server, and we simulate * that the users press "keep" */ void testAllFilesDeletedKeep() { QFETCH(bool, deleteOnRemote); FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; ConfigFile config; config.setPromptDeleteFiles(true); //Just set a blacklist so we can check it is still there. This directory does not exists but // that does not matter for our purposes. QStringList selectiveSyncBlackList = { "Q/" }; fakeFolder.syncEngine().journal()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, selectiveSyncBlackList); auto initialState = fakeFolder.currentLocalState(); int aboutToRemoveAllFilesCalled = 0; QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToRemoveAllFiles, [&](SyncFileItem::Direction dir, std::function callback) { QCOMPARE(aboutToRemoveAllFilesCalled, 0); aboutToRemoveAllFilesCalled++; QCOMPARE(dir, deleteOnRemote ? SyncFileItem::Down : SyncFileItem::Up); callback(true); fakeFolder.syncEngine().journal()->clearFileTable(); // That's what Folder is doing }); auto &modifier = deleteOnRemote ? fakeFolder.remoteModifier() : fakeFolder.localModifier(); const auto childrenKeys = fakeFolder.currentRemoteState().children.keys(); for (const auto &key : childrenKeys) { modifier.remove(key); } QVERIFY(!fakeFolder.syncOnce()); // Should fail because we cancel the sync QCOMPARE(aboutToRemoveAllFilesCalled, 1); // Next sync should recover all files QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fakeFolder.currentLocalState(), initialState); QCOMPARE(fakeFolder.currentRemoteState(), initialState); // The selective sync blacklist should be not have been deleted. bool ok = true; QCOMPARE(fakeFolder.syncEngine().journal()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok), selectiveSyncBlackList); } void testAllFilesDeletedDelete_data() { testAllFilesDeletedKeep_data(); } /* * This test is like the previous one but we simulate that the user presses "delete" */ void testAllFilesDeletedDelete() { QFETCH(bool, deleteOnRemote); FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; int aboutToRemoveAllFilesCalled = 0; QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToRemoveAllFiles, [&](SyncFileItem::Direction dir, std::function callback) { QCOMPARE(aboutToRemoveAllFilesCalled, 0); aboutToRemoveAllFilesCalled++; QCOMPARE(dir, deleteOnRemote ? SyncFileItem::Down : SyncFileItem::Up); callback(false); }); auto &modifier = deleteOnRemote ? fakeFolder.remoteModifier() : fakeFolder.localModifier(); const auto childrenKeys = fakeFolder.currentRemoteState().children.keys(); for (const auto &key : childrenKeys) { modifier.remove(key); } QVERIFY(fakeFolder.syncOnce()); // Should succeed, and all files must then be deleted QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); QCOMPARE(fakeFolder.currentLocalState().children.count(), 0); // Try another sync to be sure. QVERIFY(fakeFolder.syncOnce()); // Should succeed (doing nothing) QCOMPARE(aboutToRemoveAllFilesCalled, 1); // should not have been called. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); QCOMPARE(fakeFolder.currentLocalState().children.count(), 0); } void testNotDeleteMetaDataChange() { /** * This test make sure that we don't popup a file deleted message if all the metadata have * been updated (for example when the server is upgraded or something) **/ FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; // We never remove all files. QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToRemoveAllFiles, [&] { QVERIFY(false); }); QVERIFY(fakeFolder.syncOnce()); const auto childrenKeys = fakeFolder.currentRemoteState().children.keys(); for (const auto &s : childrenKeys) { fakeFolder.syncJournal().avoidRenamesOnNextSync(s); // clears all the fileid and inodes. } fakeFolder.localModifier().remove("A/a1"); auto expectedState = fakeFolder.currentLocalState(); QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fakeFolder.currentLocalState(), expectedState); QCOMPARE(fakeFolder.currentRemoteState(), expectedState); fakeFolder.remoteModifier().remove("B/b1"); changeAllFileId(fakeFolder.remoteModifier()); expectedState = fakeFolder.currentRemoteState(); QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fakeFolder.currentLocalState(), expectedState); QCOMPARE(fakeFolder.currentRemoteState(), expectedState); } void testResetServer() { FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; int aboutToRemoveAllFilesCalled = 0; QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToRemoveAllFiles, [&](SyncFileItem::Direction dir, std::function callback) { QCOMPARE(aboutToRemoveAllFilesCalled, 0); aboutToRemoveAllFilesCalled++; QCOMPARE(dir, SyncFileItem::Down); callback(false); }); // Some small changes fakeFolder.localModifier().mkdir("Q"); fakeFolder.localModifier().insert("Q/q1"); fakeFolder.localModifier().appendByte("B/b1"); QVERIFY(fakeFolder.syncOnce()); QCOMPARE(aboutToRemoveAllFilesCalled, 0); // Do some change locally fakeFolder.localModifier().appendByte("A/a1"); // reset the server. fakeFolder.remoteModifier() = FileInfo::A12_B12_C12_S12(); // Now, aboutToRemoveAllFiles with down as a direction QVERIFY(fakeFolder.syncOnce()); QCOMPARE(aboutToRemoveAllFilesCalled, 1); } void testDataFingetPrint_data() { QTest::addColumn("hasInitialFingerPrint"); QTest::newRow("initial finger print") << true; QTest::newRow("no initial finger print") << false; } void testDataFingetPrint() { QFETCH(bool, hasInitialFingerPrint); FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; fakeFolder.remoteModifier().setContents("C/c1", 'N'); fakeFolder.remoteModifier().setModTime("C/c1", QDateTime::currentDateTimeUtc().addDays(-2)); fakeFolder.remoteModifier().remove("C/c2"); if (hasInitialFingerPrint) { fakeFolder.remoteModifier().extraDavProperties = "initial_finger_print"; } else { //Server support finger print, but none is set. fakeFolder.remoteModifier().extraDavProperties = ""; } int fingerprintRequests = 0; fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation, const QNetworkRequest &request, QIODevice *stream) -> QNetworkReply * { auto verb = request.attribute(QNetworkRequest::CustomVerbAttribute).toString(); if (verb == "PROPFIND") { auto data = stream->readAll(); if (data.contains("data-fingerprint")) { if (request.url().path().endsWith("dav/files/admin/")) { ++fingerprintRequests; } else { fingerprintRequests = -10000; // fingerprint queried on incorrect path } } } return nullptr; }); QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fingerprintRequests, 1); // First sync, we did not change the finger print, so the file should be downloaded as normal QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); QCOMPARE(fakeFolder.currentRemoteState().find("C/c1")->contentChar, 'N'); QVERIFY(!fakeFolder.currentRemoteState().find("C/c2")); /* Simulate a backup restoration */ // A/a1 is an old file fakeFolder.remoteModifier().setContents("A/a1", 'O'); fakeFolder.remoteModifier().setModTime("A/a1", QDateTime::currentDateTimeUtc().addDays(-2)); // B/b1 did not exist at the time of the backup fakeFolder.remoteModifier().remove("B/b1"); // B/b2 was uploaded by another user in the mean time. fakeFolder.remoteModifier().setContents("B/b2", 'N'); fakeFolder.remoteModifier().setModTime("B/b2", QDateTime::currentDateTimeUtc().addDays(2)); // C/c3 was removed since we made the backup fakeFolder.remoteModifier().insert("C/c3_removed"); // C/c4 was moved to A/a2 since we made the backup fakeFolder.remoteModifier().rename("A/a2", "C/old_a2_location"); // The admin sets the data-fingerprint property fakeFolder.remoteModifier().extraDavProperties = "new_finger_print"; QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fingerprintRequests, 2); auto currentState = fakeFolder.currentLocalState(); // Although the local file is kept as a conflict, the server file is downloaded QCOMPARE(currentState.find("A/a1")->contentChar, 'O'); auto conflict = findConflict(currentState, "A/a1"); QVERIFY(conflict); QCOMPARE(conflict->contentChar, 'W'); fakeFolder.localModifier().remove(conflict->path()); // b1 was restored (re-uploaded) QVERIFY(currentState.find("B/b1")); // b2 has the new content (was not restored), since its mode time goes forward in time QCOMPARE(currentState.find("B/b2")->contentChar, 'N'); conflict = findConflict(currentState, "B/b2"); QVERIFY(conflict); // Just to be sure, we kept the old file in a conflict QCOMPARE(conflict->contentChar, 'W'); fakeFolder.localModifier().remove(conflict->path()); // We actually do not remove files that technically should have been removed (we don't want data-loss) QVERIFY(currentState.find("C/c3_removed")); QVERIFY(currentState.find("C/old_a2_location")); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); } void testSingleFileRenamed() { FakeFolder fakeFolder{FileInfo{}}; int aboutToRemoveAllFilesCalled = 0; QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToRemoveAllFiles, [&](SyncFileItem::Direction , std::function ) { aboutToRemoveAllFilesCalled++; QFAIL("should not be called"); }); // add a single file fakeFolder.localModifier().insert("hello.txt"); QVERIFY(fakeFolder.syncOnce()); QCOMPARE(aboutToRemoveAllFilesCalled, 0); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); // rename it fakeFolder.localModifier().rename("hello.txt", "goodbye.txt"); QVERIFY(fakeFolder.syncOnce()); QCOMPARE(aboutToRemoveAllFilesCalled, 0); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); } void testSelectiveSyncNoPopup() { // Unselecting all folder should not cause the popup to be shown FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; int aboutToRemoveAllFilesCalled = 0; QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToRemoveAllFiles, [&](SyncFileItem::Direction , std::function) { aboutToRemoveAllFilesCalled++; QFAIL("should not be called"); }); QVERIFY(fakeFolder.syncOnce()); QCOMPARE(aboutToRemoveAllFilesCalled, 0); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); fakeFolder.syncEngine().journal()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, QStringList() << "A/" << "B/" << "C/" << "S/"); QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fakeFolder.currentLocalState(), FileInfo{}); // all files should be one locally QCOMPARE(fakeFolder.currentRemoteState(), FileInfo::A12_B12_C12_S12()); // Server not changed QCOMPARE(aboutToRemoveAllFilesCalled, 0); // But we did not show the popup } }; QTEST_GUILESS_MAIN(TestAllFilesDeleted) #include "testallfilesdeleted.moc"