nextcloud-desktop/test/testsyncengine.cpp

1986 lines
88 KiB
C++
Raw Normal View History

/*
* 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 <QtTest>
#include <QTextCodec>
#include "syncenginetestutils.h"
#include "caseclashconflictsolver.h"
#include "configfile.h"
#include "propagatorjobs.h"
#include "syncengine.h"
#include <QFile>
#include <QtTest>
#include <filesystem>
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<OCC::SyncFileItemPtr>();
return item->destination() == path;
});
if (itItem != spy.end()) {
return itItem - spy.begin();
}
return -1;
}
}
class TestSyncEngine : public QObject
{
Q_OBJECT
private slots:
void initTestCase()
{
Logger::instance()->setLogFlush(true);
Logger::instance()->setLogDebug(true);
QStandardPaths::setTestModeEnabled(true);
}
void init()
{
QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));
}
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(), &SyncEngine::finished);
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(), &SyncEngine::finished);
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<QString> seen;
for(const QList<QVariant> &args : completeSpy) {
auto item = args[0].value<SyncFileItemPtr>();
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);
}
}
}
2017-11-16 14:35:40 +03:00
void testFakeConflict_data()
{
QTest::addColumn<bool>("sameMtime");
QTest::addColumn<QByteArray>("checksums");
QTest::addColumn<int>("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()
{
2017-11-16 14:35:40 +03:00
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
2021-01-19 16:28:04 +03:00
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');
2017-11-16 14:35:40 +03:00
if (!sameMtime)
mtime = mtime.addDays(1);
fakeFolder.remoteModifier().setModTime("A/a1", mtime);
2017-11-16 14:35:40 +03:00
remoteInfo.find("A/a1")->checksums = checksums;
QVERIFY(fakeFolder.syncOnce());
2017-11-16 14:35:40 +03:00
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());
2017-11-16 14:35:40 +03:00
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() };
2017-12-15 16:34:36 +03:00
#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"));
2017-12-15 16:34:36 +03:00
#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 {
// 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"));
}
catch (const std::filesystem::filesystem_error &e)
{
qCritical() << e.what() << e.path1().c_str() << e.path2().c_str() << e.code().message().c_str();
}
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.setMaxChunkSize(10);
options.setMinChunkSize(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);
}
// A local file should not be modified after upload to server if nothing has changed.
void testLocalFileInitialMtime()
{
constexpr auto fooFolder = "foo/";
constexpr auto barFile = "foo/bar";
FakeFolder fakeFolder{FileInfo{}};
fakeFolder.localModifier().mkdir(fooFolder);
fakeFolder.localModifier().insert(barFile);
const auto localDiskFileModifier = dynamic_cast<DiskFileModifier &>(fakeFolder.localModifier());
const auto localFile = localDiskFileModifier.find(barFile);
const auto localFileInfo = QFileInfo(localFile);
const auto expectedMtime = localFileInfo.metadataChangeTime();
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
const auto currentMtime = localFileInfo.metadataChangeTime();
QCOMPARE(currentMtime, expectedMtime);
}
/**
* 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<QString, QByteArray> &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 fails 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 fails 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);
}
}
void testServer_caseClash_createConflict_thenRemoveOneRemoteFile()
{
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);
// remove (UPPERCASE) file
fakeFolder.remoteModifier().remove(testUpperCaseFile);
QVERIFY(fakeFolder.syncOnce());
// make sure we got no conflicts now (conflicted copy gets removed)
conflicts = findCaseClashConflicts(fakeFolder.currentLocalState());
QCOMPARE(conflicts.size(), 0);
// insert (UPPERCASE) file back
fakeFolder.remoteModifier().insert(testUpperCaseFile);
QVERIFY(fakeFolder.syncOnce());
// we must get conflicts
conflicts = findCaseClashConflicts(fakeFolder.currentLocalState());
QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0);
// now remove (lowercase) file
fakeFolder.remoteModifier().remove(testLowerCaseFile);
QVERIFY(fakeFolder.syncOnce());
// make sure we got no conflicts now (conflicted copy gets removed)
conflicts = findCaseClashConflicts(fakeFolder.currentLocalState());
QCOMPARE(conflicts.size(), 0);
// remove the other file
fakeFolder.remoteModifier().remove(testUpperCaseFile);
QVERIFY(fakeFolder.syncOnce());
}
void testServer_caseClash_createDiverseConflictsInsideOneFolderAndSolveThem()
{
FakeFolder fakeFolder{FileInfo{}};
const QStringList conflictsFolderPathComponents = {"Documents", "DiverseConflicts"};
QString diverseConflictsFolderPath;
for (const auto &conflictsFolderPathComponent : conflictsFolderPathComponents) {
if (diverseConflictsFolderPath.isEmpty()) {
diverseConflictsFolderPath += conflictsFolderPathComponent;
} else {
diverseConflictsFolderPath += "/" + conflictsFolderPathComponent;
}
fakeFolder.remoteModifier().mkdir(diverseConflictsFolderPath);
}
constexpr auto testLowerCaseFile = "testfile";
constexpr auto testUpperCaseFile = "TESTFILE";
constexpr auto testLowerCaseFolder = "testfolder";
constexpr auto testUpperCaseFolder = "TESTFOLDER";
constexpr auto testInvalidCharFolder = "Really?";
fakeFolder.remoteModifier().insert(diverseConflictsFolderPath + "/" + testLowerCaseFile);
fakeFolder.remoteModifier().insert(diverseConflictsFolderPath + "/" + testUpperCaseFile);
fakeFolder.remoteModifier().mkdir(diverseConflictsFolderPath + "/" + testLowerCaseFolder);
fakeFolder.remoteModifier().mkdir(diverseConflictsFolderPath + "/" + testUpperCaseFolder);
fakeFolder.remoteModifier().mkdir(diverseConflictsFolderPath + "/" + testInvalidCharFolder);
#if defined Q_OS_LINUX
constexpr auto shouldHaveCaseClashConflict = false;
#else
constexpr auto shouldHaveCaseClashConflict = true;
#endif
if (shouldHaveCaseClashConflict) {
ItemCompletedSpy completeSpy(fakeFolder);
fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem);
QVERIFY(fakeFolder.syncOnce());
// verify the parent of a folder where caseclash and invalidchar conflicts were found, has corresponding flags set (conflict info must get
// propagated to the very top)
const auto diverseConflictsFolderParent = completeSpy.findItem(conflictsFolderPathComponents.first());
QVERIFY(diverseConflictsFolderParent);
QVERIFY(diverseConflictsFolderParent->_isAnyCaseClashChild);
QVERIFY(diverseConflictsFolderParent->_isAnyInvalidCharChild);
completeSpy.clear();
auto diverseConflictsFolderInfo = fakeFolder.currentLocalState().findRecursive(diverseConflictsFolderPath);
QVERIFY(!diverseConflictsFolderInfo.name.isEmpty());
auto conflictsFile = findCaseClashConflicts(diverseConflictsFolderInfo);
QCOMPARE(conflictsFile.size(), shouldHaveCaseClashConflict ? 1 : 0);
const auto hasFileCaseClashConflict = expectConflict(diverseConflictsFolderInfo, testLowerCaseFile);
QCOMPARE(hasFileCaseClashConflict, shouldHaveCaseClashConflict ? true : false);
fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem);
QVERIFY(fakeFolder.syncOnce());
diverseConflictsFolderInfo = fakeFolder.currentLocalState().findRecursive(diverseConflictsFolderPath);
QVERIFY(!diverseConflictsFolderInfo.name.isEmpty());
conflictsFile = findCaseClashConflicts(diverseConflictsFolderInfo);
QCOMPARE(conflictsFile.size(), shouldHaveCaseClashConflict ? 1 : 0);
const auto conflictFileName = QString{conflictsFile.constFirst()};
qDebug() << conflictFileName;
CaseClashConflictSolver conflictSolver(fakeFolder.localPath() + diverseConflictsFolderPath + "/" + testLowerCaseFile,
fakeFolder.localPath() + conflictFileName,
QStringLiteral("/"),
fakeFolder.localPath(),
fakeFolder.account(),
&fakeFolder.syncJournal());
QSignalSpy conflictSolverDone(&conflictSolver, &CaseClashConflictSolver::done);
conflictSolver.solveConflict("testfile2");
QVERIFY(conflictSolverDone.wait());
QVERIFY(fakeFolder.syncOnce());
diverseConflictsFolderInfo = fakeFolder.currentLocalState().findRecursive(diverseConflictsFolderPath);
QVERIFY(!diverseConflictsFolderInfo.name.isEmpty());
conflictsFile = findCaseClashConflicts(diverseConflictsFolderInfo);
QCOMPARE(conflictsFile.size(), 0);
fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem);
QVERIFY(fakeFolder.syncOnce());
// After solving file conflict, verify that we did not lose conflict detection of caseclash and invalidchar for folders
for (auto it = completeSpy.begin(); it != completeSpy.end(); ++it) {
auto item = (*it).first().value<OCC::SyncFileItemPtr>();
item = nullptr;
}
auto invalidFilenameConflictFolderItem = completeSpy.findItem(diverseConflictsFolderPath + "/" + testInvalidCharFolder);
QVERIFY(invalidFilenameConflictFolderItem);
QVERIFY(invalidFilenameConflictFolderItem->_status == SyncFileItem::FileNameInvalid);
auto caseClashConflictFolderItemLower = completeSpy.findItem(diverseConflictsFolderPath + "/" + testLowerCaseFolder);
auto caseClashConflictFolderItemUpper = completeSpy.findItem(diverseConflictsFolderPath + "/" + testUpperCaseFolder);
completeSpy.clear();
// we always create UPPERCASE folder in current syncengine logic for now and then create a conflict for a lowercase folder, but this may change, so
// keep this check more future proof
const auto upperOrLowerCaseFolderCaseClashFound =
(caseClashConflictFolderItemLower && caseClashConflictFolderItemLower->_status == SyncFileItem::FileNameClash)
|| (caseClashConflictFolderItemUpper && caseClashConflictFolderItemUpper->_status == SyncFileItem::FileNameClash);
QVERIFY(upperOrLowerCaseFolderCaseClashFound);
// solve case clash folders conflict
CaseClashConflictSolver conflictSolverCaseClashForFolder(fakeFolder.localPath() + diverseConflictsFolderPath + "/" + testLowerCaseFolder,
fakeFolder.localPath() + diverseConflictsFolderPath + "/" + testUpperCaseFolder,
QStringLiteral("/"),
fakeFolder.localPath(),
fakeFolder.account(),
&fakeFolder.syncJournal());
QSignalSpy conflictSolverCaseClashForFolderDone(&conflictSolverCaseClashForFolder, &CaseClashConflictSolver::done);
conflictSolverCaseClashForFolder.solveConflict("testfolder1");
QVERIFY(conflictSolverCaseClashForFolderDone.wait());
QVERIFY(fakeFolder.syncOnce());
// verify no case clash conflicts folder items are found
caseClashConflictFolderItemLower = completeSpy.findItem(diverseConflictsFolderPath + "/" + testLowerCaseFolder);
caseClashConflictFolderItemUpper = completeSpy.findItem(diverseConflictsFolderPath + "/" + testUpperCaseFolder);
QVERIFY((!caseClashConflictFolderItemLower || caseClashConflictFolderItemLower->_file.isEmpty())
&& (!caseClashConflictFolderItemUpper || caseClashConflictFolderItemUpper->_file.isEmpty()));
// veriy invalid filename conflict folder item is still present
invalidFilenameConflictFolderItem = completeSpy.findItem(diverseConflictsFolderPath + "/" + testInvalidCharFolder);
completeSpy.clear();
QVERIFY(invalidFilenameConflictFolderItem);
QVERIFY(invalidFilenameConflictFolderItem->_status == SyncFileItem::FileNameInvalid);
}
}
void testExistingFolderBecameBig()
{
constexpr auto testFolder = "folder";
constexpr auto testSmallFile = "folder/small_file.txt";
constexpr auto testLargeFile = "folder/large_file.txt";
QTemporaryDir dir;
ConfigFile::setConfDir(dir.path()); // we don't want to pollute the user's config file
auto config = ConfigFile();
config.setNotifyExistingFoldersOverLimit(true);
FakeFolder fakeFolder{FileInfo{}};
QSignalSpy spy(&fakeFolder.syncEngine(), &SyncEngine::existingFolderNowBig);
auto syncOptions = fakeFolder.syncEngine().syncOptions();
syncOptions._newBigFolderSizeLimit = 128; // 128 bytes
fakeFolder.syncEngine().setSyncOptions(syncOptions);
fakeFolder.remoteModifier().mkdir(testFolder);
fakeFolder.remoteModifier().insert(testSmallFile, 64);
fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem);
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(spy.count(), 0);
fakeFolder.remoteModifier().insert(testLargeFile, 256);
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(spy.count(), 1);
}
void testFileDownloadWithUnicodeCharacterInName() {
FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
ItemCompletedSpy completeSpy(fakeFolder);
fakeFolder.remoteModifier().insert("A/abcdęfg.txt");
fakeFolder.syncOnce();
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/abcdęfg.txt"));
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
}
void testRemoteTypeChangeExistingLocalMustGetRemoved()
{
FakeFolder fakeFolder{FileInfo{}};
// test file change to directory on remote
fakeFolder.remoteModifier().mkdir("a");
fakeFolder.remoteModifier().insert("a/TESTFILE");
QVERIFY(fakeFolder.syncOnce());
fakeFolder.remoteModifier().remove("a/TESTFILE");
fakeFolder.remoteModifier().mkdir("a/TESTFILE");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
// test directory change to file on remote
fakeFolder.remoteModifier().mkdir("a/TESTDIR");
QVERIFY(fakeFolder.syncOnce());
fakeFolder.remoteModifier().remove("a/TESTDIR");
fakeFolder.remoteModifier().insert("a/TESTDIR");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
}
void testRemoveAllFilesWithNextcloudCmd()
{
FakeFolder fakeFolder{FileInfo{}};
auto nextcloudCmdSyncOptions = fakeFolder.syncEngine().syncOptions();
nextcloudCmdSyncOptions.setIsCmd(true);
fakeFolder.syncEngine().setSyncOptions(nextcloudCmdSyncOptions);
ConfigFile().setPromptDeleteFiles(true);
QSignalSpy displayDialogSignal(&fakeFolder.syncEngine(), &SyncEngine::aboutToRemoveAllFiles);
fakeFolder.remoteModifier().mkdir("folder");
fakeFolder.remoteModifier().insert("folder/file1");
fakeFolder.remoteModifier().insert("folder/file2");
fakeFolder.remoteModifier().insert("folder/file3");
fakeFolder.remoteModifier().mkdir("folder2");
fakeFolder.remoteModifier().insert("file1");
fakeFolder.remoteModifier().insert("file2");
fakeFolder.remoteModifier().insert("file3");
QVERIFY(fakeFolder.syncOnce());
fakeFolder.remoteModifier().remove("folder");
fakeFolder.remoteModifier().remove("folder2");
fakeFolder.remoteModifier().remove("file1");
fakeFolder.remoteModifier().remove("file2");
fakeFolder.remoteModifier().remove("file3");
QVERIFY(fakeFolder.syncOnce());
// the signal to display the dialog should not be emitted
QCOMPARE(displayDialogSignal.count(), 0);
QCOMPARE(fakeFolder.remoteModifier().find("folder"), nullptr);
QCOMPARE(fakeFolder.remoteModifier().find("folder2"), nullptr);
QCOMPARE(fakeFolder.remoteModifier().find("file1"), nullptr);
}
void testRemoveAllFilesWithoutNextcloudCmd()
{
FakeFolder fakeFolder{FileInfo{}};
auto nextcloudCmdSyncOptions = fakeFolder.syncEngine().syncOptions();
nextcloudCmdSyncOptions.setIsCmd(false);
fakeFolder.syncEngine().setSyncOptions(nextcloudCmdSyncOptions);
ConfigFile().setPromptDeleteFiles(true);
QSignalSpy displayDialogSignal(&fakeFolder.syncEngine(), &SyncEngine::aboutToRemoveAllFiles);
fakeFolder.remoteModifier().mkdir("folder");
fakeFolder.remoteModifier().insert("folder/file1");
fakeFolder.remoteModifier().insert("folder/file2");
fakeFolder.remoteModifier().insert("folder/file3");
fakeFolder.remoteModifier().mkdir("folder2");
fakeFolder.remoteModifier().insert("file1");
fakeFolder.remoteModifier().insert("file2");
fakeFolder.remoteModifier().insert("file3");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
fakeFolder.remoteModifier().remove("folder");
fakeFolder.remoteModifier().remove("folder2");
fakeFolder.remoteModifier().remove("file1");
fakeFolder.remoteModifier().remove("file2");
fakeFolder.remoteModifier().remove("file3");
QVERIFY(fakeFolder.syncOnce());
// the signal to show the dialog should be emitted
QCOMPARE(displayDialogSignal.count(), 1);
QCOMPARE(fakeFolder.remoteModifier().find("folder"), nullptr);
QCOMPARE(fakeFolder.remoteModifier().find("folder2"), nullptr);
QCOMPARE(fakeFolder.remoteModifier().find("file1"), nullptr);
}
};
QTEST_GUILESS_MAIN(TestSyncEngine)
#include "testsyncengine.moc"