Speedup test build by compile the fake server just once
authorHannah von Reth <hannah.vonreth@owncloud.com>
Thu, 10 Dec 2020 15:50:59 +0000 (16:50 +0100)
committerKevin Ottens <kevin.ottens@nextcloud.com>
Tue, 15 Dec 2020 09:59:27 +0000 (10:59 +0100)
test/CMakeLists.txt
test/nextcloud_add_test.cmake
test/syncenginetestutils.cpp [new file with mode: 0644]
test/syncenginetestutils.h
test/testasyncop.cpp
test/testblacklist.cpp
test/testchunkingng.cpp

index dc4100e97c03c20c62e97ae8540cb4241257727c..8932d2b80ecd33186bd52431cd8198fa4beec6c8 100644 (file)
@@ -14,6 +14,10 @@ include_directories(${CMAKE_SOURCE_DIR}/src
 
 include(nextcloud_add_test.cmake)
 
+set(CMAKE_AUTOMOC TRUE)
+add_library(syncenginetestutils STATIC syncenginetestutils.cpp)
+target_link_libraries(syncenginetestutils PUBLIC ${APPLICATION_EXECUTABLE}sync Qt5::Test)
+
 nextcloud_add_test(NextcloudPropagator "")
 
 IF(BUILD_UPDATER)
@@ -45,31 +49,31 @@ nextcloud_add_test(ClientSideEncryption "")
 nextcloud_add_test(ExcludedFiles "")
 
 nextcloud_add_test(Utility "")
-nextcloud_add_test(SyncEngine "syncenginetestutils.h")
-nextcloud_add_test(SyncVirtualFiles "syncenginetestutils.h")
-nextcloud_add_test(SyncMove "syncenginetestutils.h")
-nextcloud_add_test(SyncDelete "syncenginetestutils.h")
-nextcloud_add_test(SyncConflict "syncenginetestutils.h")
-nextcloud_add_test(SyncFileStatusTracker "syncenginetestutils.h")
-nextcloud_add_test(Download "syncenginetestutils.h")
-nextcloud_add_test(ChunkingNg "syncenginetestutils.h")
-nextcloud_add_test(AsyncOp "syncenginetestutils.h")
-nextcloud_add_test(UploadReset "syncenginetestutils.h")
-nextcloud_add_test(AllFilesDeleted "syncenginetestutils.h")
-nextcloud_add_test(Blacklist "syncenginetestutils.h")
-nextcloud_add_test(LocalDiscovery "syncenginetestutils.h")
-nextcloud_add_test(RemoteDiscovery "syncenginetestutils.h")
-nextcloud_add_test(Permissions "syncenginetestutils.h")
-nextcloud_add_test(SelectiveSync "syncenginetestutils.h")
-nextcloud_add_test(DatabaseError "syncenginetestutils.h")
-nextcloud_add_test(LockedFiles "syncenginetestutils.h;../src/gui/lockwatcher.cpp")
+nextcloud_add_test(SyncEngine "")
+nextcloud_add_test(SyncVirtualFiles "")
+nextcloud_add_test(SyncMove "")
+nextcloud_add_test(SyncDelete "")
+nextcloud_add_test(SyncConflict "")
+nextcloud_add_test(SyncFileStatusTracker "")
+nextcloud_add_test(Download "")
+nextcloud_add_test(ChunkingNg "")
+nextcloud_add_test(AsyncOp "")
+nextcloud_add_test(UploadReset "")
+nextcloud_add_test(AllFilesDeleted "")
+nextcloud_add_test(Blacklist "")
+nextcloud_add_test(LocalDiscovery "")
+nextcloud_add_test(RemoteDiscovery "")
+nextcloud_add_test(Permissions "")
+nextcloud_add_test(SelectiveSync "")
+nextcloud_add_test(DatabaseError "")
+nextcloud_add_test(LockedFiles "../src/gui/lockwatcher.cpp")
 nextcloud_add_test(FolderWatcher "${FolderWatcher_SRC}")
 
 if( UNIX AND NOT APPLE )
     nextcloud_add_test(InotifyWatcher "${FolderWatcher_SRC}")
 endif(UNIX AND NOT APPLE)
 
-nextcloud_add_benchmark(LargeSync "syncenginetestutils.h")
+nextcloud_add_benchmark(LargeSync "")
 
 SET(FolderMan_SRC ../src/gui/folderman.cpp)
 list(APPEND FolderMan_SRC ../src/gui/folder.cpp )
@@ -106,7 +110,7 @@ list(APPEND RemoteWipe_SRC ${RemoteWipe_SRC})
 list(APPEND RemoteWipe_SRC stubremotewipe.cpp )
 nextcloud_add_test(RemoteWipe "${RemoteWipe_SRC}")
 
-nextcloud_add_test(OAuth "syncenginetestutils.h;../src/gui/creds/oauth.cpp")
+nextcloud_add_test(OAuth "../src/gui/creds/oauth.cpp")
 
 configure_file(test_journal.db "${PROJECT_BINARY_DIR}/bin/test_journal.db" COPYONLY)
 
index 1cfeec83210aea8399754ca7932d15eebe4baa78..d43e4e37d95e826dad05ccb6fdeca4c3bc06a66a 100644 (file)
@@ -9,7 +9,7 @@ macro(nextcloud_add_test test_class additional_cpp)
     set_target_properties(${OWNCLOUD_TEST_CLASS}Test PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${BIN_OUTPUT_DIRECTORY})
 
     target_link_libraries(${OWNCLOUD_TEST_CLASS}Test
-        ${APPLICATION_EXECUTABLE}sync
+        ${APPLICATION_EXECUTABLE}sync syncenginetestutils
         Qt5::Core Qt5::Test Qt5::Xml Qt5::Network Qt5::Qml Qt5::Quick
     )
 
@@ -37,7 +37,7 @@ macro(nextcloud_add_benchmark test_class additional_cpp)
     set_target_properties(${OWNCLOUD_TEST_CLASS}Bench PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${BIN_OUTPUT_DIRECTORY})
 
     target_link_libraries(${OWNCLOUD_TEST_CLASS}Bench
-        ${APPLICATION_EXECUTABLE}sync
+        ${APPLICATION_EXECUTABLE}sync syncenginetestutils
         Qt5::Core Qt5::Test Qt5::Xml Qt5::Network
     )
 
diff --git a/test/syncenginetestutils.cpp b/test/syncenginetestutils.cpp
new file mode 100644 (file)
index 0000000..36e81e3
--- /dev/null
@@ -0,0 +1,1009 @@
+/*
+ *    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"
+
+
+PathComponents::PathComponents(const char *path)
+    : PathComponents { QString::fromUtf8(path) }
+{
+}
+
+PathComponents::PathComponents(const QString &path)
+    : QStringList { path.split(QLatin1Char('/'), QString::SkipEmptyParts) }
+{
+}
+
+PathComponents::PathComponents(const QStringList &pathComponents)
+    : QStringList { pathComponents }
+{
+}
+
+PathComponents PathComponents::parentDirComponents() const
+{
+    return PathComponents { mid(0, size() - 1) };
+}
+
+PathComponents PathComponents::subComponents() const &
+{
+    return PathComponents { mid(1) };
+}
+
+void DiskFileModifier::remove(const QString &relativePath)
+{
+    QFileInfo fi { _rootDir.filePath(relativePath) };
+    if (fi.isFile())
+        QVERIFY(_rootDir.remove(relativePath));
+    else
+        QVERIFY(QDir { fi.filePath() }.removeRecursively());
+}
+
+void DiskFileModifier::insert(const QString &relativePath, qint64 size, char contentChar)
+{
+    QFile file { _rootDir.filePath(relativePath) };
+    QVERIFY(!file.exists());
+    file.open(QFile::WriteOnly);
+    QByteArray buf(1024, contentChar);
+    for (int x = 0; x < size / buf.size(); ++x) {
+        file.write(buf);
+    }
+    file.write(buf.data(), size % buf.size());
+    file.close();
+    // Set the mtime 30 seconds in the past, for some tests that need to make sure that the mtime differs.
+    OCC::FileSystem::setModTime(file.fileName(), OCC::Utility::qDateTimeToTime_t(QDateTime::currentDateTimeUtc().addSecs(-30)));
+    QCOMPARE(file.size(), size);
+}
+
+void DiskFileModifier::setContents(const QString &relativePath, char contentChar)
+{
+    QFile file { _rootDir.filePath(relativePath) };
+    QVERIFY(file.exists());
+    qint64 size = file.size();
+    file.open(QFile::WriteOnly);
+    file.write(QByteArray {}.fill(contentChar, size));
+}
+
+void DiskFileModifier::appendByte(const QString &relativePath)
+{
+    QFile file { _rootDir.filePath(relativePath) };
+    QVERIFY(file.exists());
+    file.open(QFile::ReadWrite);
+    QByteArray contents = file.read(1);
+    file.seek(file.size());
+    file.write(contents);
+}
+
+void DiskFileModifier::mkdir(const QString &relativePath)
+{
+    _rootDir.mkpath(relativePath);
+}
+
+void DiskFileModifier::rename(const QString &from, const QString &to)
+{
+    QVERIFY(_rootDir.exists(from));
+    QVERIFY(_rootDir.rename(from, to));
+}
+
+void DiskFileModifier::setModTime(const QString &relativePath, const QDateTime &modTime)
+{
+    OCC::FileSystem::setModTime(_rootDir.filePath(relativePath), OCC::Utility::qDateTimeToTime_t(modTime));
+}
+
+FileInfo FileInfo::A12_B12_C12_S12()
+{
+    FileInfo fi { QString {}, {
+                                  { QStringLiteral("A"), { { QStringLiteral("a1"), 4 }, { QStringLiteral("a2"), 4 } } },
+                                  { QStringLiteral("B"), { { QStringLiteral("b1"), 16 }, { QStringLiteral("b2"), 16 } } },
+                                  { QStringLiteral("C"), { { QStringLiteral("c1"), 24 }, { QStringLiteral("c2"), 24 } } },
+                              } };
+    FileInfo sharedFolder { QStringLiteral("S"), { { QStringLiteral("s1"), 32 }, { QStringLiteral("s2"), 32 } } };
+    sharedFolder.isShared = true;
+    sharedFolder.children[QStringLiteral("s1")].isShared = true;
+    sharedFolder.children[QStringLiteral("s2")].isShared = true;
+    fi.children.insert(sharedFolder.name, std::move(sharedFolder));
+    return fi;
+}
+
+FileInfo::FileInfo(const QString &name, const std::initializer_list<FileInfo> &children)
+    : name { name }
+{
+    for (const auto &source : children)
+        addChild(source);
+}
+
+void FileInfo::addChild(const FileInfo &info)
+{
+    auto &dest = this->children[info.name] = info;
+    dest.parentPath = path();
+    dest.fixupParentPathRecursively();
+}
+
+void FileInfo::remove(const QString &relativePath)
+{
+    const PathComponents pathComponents { relativePath };
+    FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
+    Q_ASSERT(parent);
+    parent->children.erase(std::find_if(parent->children.begin(), parent->children.end(),
+        [&pathComponents](const FileInfo &fi) { return fi.name == pathComponents.fileName(); }));
+}
+
+void FileInfo::insert(const QString &relativePath, qint64 size, char contentChar)
+{
+    create(relativePath, size, contentChar);
+}
+
+void FileInfo::setContents(const QString &relativePath, char contentChar)
+{
+    FileInfo *file = findInvalidatingEtags(relativePath);
+    Q_ASSERT(file);
+    file->contentChar = contentChar;
+}
+
+void FileInfo::appendByte(const QString &relativePath)
+{
+    FileInfo *file = findInvalidatingEtags(relativePath);
+    Q_ASSERT(file);
+    file->size += 1;
+}
+
+void FileInfo::mkdir(const QString &relativePath)
+{
+    createDir(relativePath);
+}
+
+void FileInfo::rename(const QString &oldPath, const QString &newPath)
+{
+    const PathComponents newPathComponents { newPath };
+    FileInfo *dir = findInvalidatingEtags(newPathComponents.parentDirComponents());
+    Q_ASSERT(dir);
+    Q_ASSERT(dir->isDir);
+    const PathComponents pathComponents { oldPath };
+    FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
+    Q_ASSERT(parent);
+    FileInfo fi = parent->children.take(pathComponents.fileName());
+    fi.parentPath = dir->path();
+    fi.name = newPathComponents.fileName();
+    fi.fixupParentPathRecursively();
+    dir->children.insert(newPathComponents.fileName(), std::move(fi));
+}
+
+void FileInfo::setModTime(const QString &relativePath, const QDateTime &modTime)
+{
+    FileInfo *file = findInvalidatingEtags(relativePath);
+    Q_ASSERT(file);
+    file->lastModified = modTime;
+}
+
+FileInfo *FileInfo::find(PathComponents pathComponents, const bool invalidateEtags)
+{
+    if (pathComponents.isEmpty()) {
+        if (invalidateEtags) {
+            etag = generateEtag();
+        }
+        return this;
+    }
+    QString childName = pathComponents.pathRoot();
+    auto it = children.find(childName);
+    if (it != children.end()) {
+        auto file = it->find(std::move(pathComponents).subComponents(), invalidateEtags);
+        if (file && invalidateEtags) {
+            // Update parents on the way back
+            etag = generateEtag();
+        }
+        return file;
+    }
+    return nullptr;
+}
+
+FileInfo *FileInfo::createDir(const QString &relativePath)
+{
+    const PathComponents pathComponents { relativePath };
+    FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
+    Q_ASSERT(parent);
+    FileInfo &child = parent->children[pathComponents.fileName()] = FileInfo { pathComponents.fileName() };
+    child.parentPath = parent->path();
+    child.etag = generateEtag();
+    return &child;
+}
+
+FileInfo *FileInfo::create(const QString &relativePath, qint64 size, char contentChar)
+{
+    const PathComponents pathComponents { relativePath };
+    FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
+    Q_ASSERT(parent);
+    FileInfo &child = parent->children[pathComponents.fileName()] = FileInfo { pathComponents.fileName(), size };
+    child.parentPath = parent->path();
+    child.contentChar = contentChar;
+    child.etag = generateEtag();
+    return &child;
+}
+
+bool FileInfo::operator==(const FileInfo &other) const
+{
+    // Consider files to be equal between local<->remote as a user would.
+    return name == other.name
+        && isDir == other.isDir
+        && size == other.size
+        && contentChar == other.contentChar
+        && children == other.children;
+}
+
+QString FileInfo::path() const
+{
+    return (parentPath.isEmpty() ? QString() : (parentPath + QLatin1Char('/'))) + name;
+}
+
+void FileInfo::fixupParentPathRecursively()
+{
+    auto p = path();
+    for (auto it = children.begin(); it != children.end(); ++it) {
+        Q_ASSERT(it.key() == it->name);
+        it->parentPath = p;
+        it->fixupParentPathRecursively();
+    }
+}
+
+FileInfo *FileInfo::findInvalidatingEtags(PathComponents pathComponents)
+{
+    return find(std::move(pathComponents), true);
+}
+
+FakePropfindReply::FakePropfindReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
+    : QNetworkReply { parent }
+{
+    setRequest(request);
+    setUrl(request.url());
+    setOperation(op);
+    open(QIODevice::ReadOnly);
+
+    QString fileName = getFilePathFromUrl(request.url());
+    Q_ASSERT(!fileName.isNull()); // for root, it should be empty
+    const FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
+    if (!fileInfo) {
+        QMetaObject::invokeMethod(this, "respond404", Qt::QueuedConnection);
+        return;
+    }
+    QString prefix = request.url().path().left(request.url().path().size() - fileName.size());
+
+    // Don't care about the request and just return a full propfind
+    const QString davUri { QStringLiteral("DAV:") };
+    const QString ocUri { QStringLiteral("http://owncloud.org/ns") };
+    QBuffer buffer { &payload };
+    buffer.open(QIODevice::WriteOnly);
+    QXmlStreamWriter xml(&buffer);
+    xml.writeNamespace(davUri, QStringLiteral("d"));
+    xml.writeNamespace(ocUri, QStringLiteral("oc"));
+    xml.writeStartDocument();
+    xml.writeStartElement(davUri, QStringLiteral("multistatus"));
+    auto writeFileResponse = [&](const FileInfo &fileInfo) {
+        xml.writeStartElement(davUri, QStringLiteral("response"));
+
+        xml.writeTextElement(davUri, QStringLiteral("href"), prefix + QString::fromUtf8(QUrl::toPercentEncoding(fileInfo.path(), "/")));
+        xml.writeStartElement(davUri, QStringLiteral("propstat"));
+        xml.writeStartElement(davUri, QStringLiteral("prop"));
+
+        if (fileInfo.isDir) {
+            xml.writeStartElement(davUri, QStringLiteral("resourcetype"));
+            xml.writeEmptyElement(davUri, QStringLiteral("collection"));
+            xml.writeEndElement(); // resourcetype
+        } else
+            xml.writeEmptyElement(davUri, QStringLiteral("resourcetype"));
+
+        auto gmtDate = fileInfo.lastModified.toUTC();
+        auto stringDate = QLocale::c().toString(gmtDate, QStringLiteral("ddd, dd MMM yyyy HH:mm:ss 'GMT'"));
+        xml.writeTextElement(davUri, QStringLiteral("getlastmodified"), stringDate);
+        xml.writeTextElement(davUri, QStringLiteral("getcontentlength"), QString::number(fileInfo.size));
+        xml.writeTextElement(davUri, QStringLiteral("getetag"), QStringLiteral("\"%1\"").arg(QString::fromLatin1(fileInfo.etag)));
+        xml.writeTextElement(ocUri, QStringLiteral("permissions"), !fileInfo.permissions.isNull() ? QString(fileInfo.permissions.toString()) : fileInfo.isShared ? QStringLiteral("SRDNVCKW") : QStringLiteral("RDNVCKW"));
+        xml.writeTextElement(ocUri, QStringLiteral("id"), QString::fromUtf8(fileInfo.fileId));
+        xml.writeTextElement(ocUri, QStringLiteral("checksums"), QString::fromUtf8(fileInfo.checksums));
+        buffer.write(fileInfo.extraDavProperties);
+        xml.writeEndElement(); // prop
+        xml.writeTextElement(davUri, QStringLiteral("status"), QStringLiteral("HTTP/1.1 200 OK"));
+        xml.writeEndElement(); // propstat
+        xml.writeEndElement(); // response
+    };
+
+    writeFileResponse(*fileInfo);
+    foreach (const FileInfo &childFileInfo, fileInfo->children)
+        writeFileResponse(childFileInfo);
+    xml.writeEndElement(); // multistatus
+    xml.writeEndDocument();
+
+    QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
+}
+
+void FakePropfindReply::respond()
+{
+    setHeader(QNetworkRequest::ContentLengthHeader, payload.size());
+    setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/xml; charset=utf-8"));
+    setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 207);
+    setFinished(true);
+    emit metaDataChanged();
+    if (bytesAvailable())
+        emit readyRead();
+    emit finished();
+}
+
+void FakePropfindReply::respond404()
+{
+    setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 404);
+    setError(InternalServerError, QStringLiteral("Not Found"));
+    emit metaDataChanged();
+    emit finished();
+}
+
+qint64 FakePropfindReply::bytesAvailable() const
+{
+    return payload.size() + QIODevice::bytesAvailable();
+}
+
+qint64 FakePropfindReply::readData(char *data, qint64 maxlen)
+{
+    qint64 len = std::min(qint64 { payload.size() }, maxlen);
+    std::copy(payload.cbegin(), payload.cbegin() + len, data);
+    payload.remove(0, static_cast<int>(len));
+    return len;
+}
+
+FakePutReply::FakePutReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &putPayload, QObject *parent)
+    : QNetworkReply { parent }
+{
+    setRequest(request);
+    setUrl(request.url());
+    setOperation(op);
+    open(QIODevice::ReadOnly);
+    fileInfo = perform(remoteRootFileInfo, request, putPayload);
+    QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
+}
+
+FileInfo *FakePutReply::perform(FileInfo &remoteRootFileInfo, const QNetworkRequest &request, const QByteArray &putPayload)
+{
+    QString fileName = getFilePathFromUrl(request.url());
+    Q_ASSERT(!fileName.isEmpty());
+    FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
+    if (fileInfo) {
+        fileInfo->size = putPayload.size();
+        fileInfo->contentChar = putPayload.at(0);
+    } else {
+        // Assume that the file is filled with the same character
+        fileInfo = remoteRootFileInfo.create(fileName, putPayload.size(), putPayload.at(0));
+    }
+    fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong());
+    remoteRootFileInfo.find(fileName, /*invalidate_etags=*/true);
+    return fileInfo;
+}
+
+void FakePutReply::respond()
+{
+    emit uploadProgress(fileInfo->size, fileInfo->size);
+    setRawHeader("OC-ETag", fileInfo->etag);
+    setRawHeader("ETag", fileInfo->etag);
+    setRawHeader("OC-FileID", fileInfo->fileId);
+    setRawHeader("X-OC-MTime", "accepted"); // Prevents Q_ASSERT(!_runningNow) since we'll call PropagateItemJob::done twice in that case.
+    setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
+    emit metaDataChanged();
+    emit finished();
+}
+
+void FakePutReply::abort()
+{
+    setError(OperationCanceledError, QStringLiteral("abort"));
+    emit finished();
+}
+
+FakeMkcolReply::FakeMkcolReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
+    : QNetworkReply { parent }
+{
+    setRequest(request);
+    setUrl(request.url());
+    setOperation(op);
+    open(QIODevice::ReadOnly);
+
+    QString fileName = getFilePathFromUrl(request.url());
+    Q_ASSERT(!fileName.isEmpty());
+    fileInfo = remoteRootFileInfo.createDir(fileName);
+
+    if (!fileInfo) {
+        abort();
+        return;
+    }
+    QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
+}
+
+void FakeMkcolReply::respond()
+{
+    setRawHeader("OC-FileId", fileInfo->fileId);
+    setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
+    emit metaDataChanged();
+    emit finished();
+}
+
+FakeDeleteReply::FakeDeleteReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
+    : QNetworkReply { parent }
+{
+    setRequest(request);
+    setUrl(request.url());
+    setOperation(op);
+    open(QIODevice::ReadOnly);
+
+    QString fileName = getFilePathFromUrl(request.url());
+    Q_ASSERT(!fileName.isEmpty());
+    remoteRootFileInfo.remove(fileName);
+    QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
+}
+
+void FakeDeleteReply::respond()
+{
+    setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 204);
+    emit metaDataChanged();
+    emit finished();
+}
+
+FakeMoveReply::FakeMoveReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
+    : QNetworkReply { parent }
+{
+    setRequest(request);
+    setUrl(request.url());
+    setOperation(op);
+    open(QIODevice::ReadOnly);
+
+    QString fileName = getFilePathFromUrl(request.url());
+    Q_ASSERT(!fileName.isEmpty());
+    QString dest = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination")));
+    Q_ASSERT(!dest.isEmpty());
+    remoteRootFileInfo.rename(fileName, dest);
+    QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
+}
+
+void FakeMoveReply::respond()
+{
+    setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
+    emit metaDataChanged();
+    emit finished();
+}
+
+FakeGetReply::FakeGetReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
+    : QNetworkReply { parent }
+{
+    setRequest(request);
+    setUrl(request.url());
+    setOperation(op);
+    open(QIODevice::ReadOnly);
+
+    QString fileName = getFilePathFromUrl(request.url());
+    Q_ASSERT(!fileName.isEmpty());
+    fileInfo = remoteRootFileInfo.find(fileName);
+    if (!fileInfo)
+        qWarning() << "Could not find file" << fileName << "on the remote";
+    QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
+}
+
+void FakeGetReply::respond()
+{
+    if (aborted) {
+        setError(OperationCanceledError, QStringLiteral("Operation Canceled"));
+        emit metaDataChanged();
+        emit finished();
+        return;
+    }
+    payload = fileInfo->contentChar;
+    size = fileInfo->size;
+    setHeader(QNetworkRequest::ContentLengthHeader, size);
+    setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
+    setRawHeader("OC-ETag", fileInfo->etag);
+    setRawHeader("ETag", fileInfo->etag);
+    setRawHeader("OC-FileId", fileInfo->fileId);
+    emit metaDataChanged();
+    if (bytesAvailable())
+        emit readyRead();
+    emit finished();
+}
+
+void FakeGetReply::abort()
+{
+    setError(OperationCanceledError, QStringLiteral("Operation Canceled"));
+    aborted = true;
+}
+
+qint64 FakeGetReply::bytesAvailable() const
+{
+    if (aborted)
+        return 0;
+    return size + QIODevice::bytesAvailable();
+}
+
+qint64 FakeGetReply::readData(char *data, qint64 maxlen)
+{
+    qint64 len = std::min(qint64 { size }, maxlen);
+    std::fill_n(data, len, payload);
+    size -= len;
+    return len;
+}
+
+FakeGetWithDataReply::FakeGetWithDataReply(FileInfo &remoteRootFileInfo, const QByteArray &data, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
+    : QNetworkReply { parent }
+{
+    setRequest(request);
+    setUrl(request.url());
+    setOperation(op);
+    open(QIODevice::ReadOnly);
+
+    Q_ASSERT(!data.isEmpty());
+    payload = data;
+    QString fileName = getFilePathFromUrl(request.url());
+    Q_ASSERT(!fileName.isEmpty());
+    fileInfo = remoteRootFileInfo.find(fileName);
+    QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
+
+    if (request.hasRawHeader("Range")) {
+        const QString range = QString::fromUtf8(request.rawHeader("Range"));
+        const QRegularExpression bytesPattern(QStringLiteral("bytes=(?<start>\\d+)-(?<end>\\d+)"));
+        const QRegularExpressionMatch match = bytesPattern.match(range);
+        if (match.hasMatch()) {
+            const int start = match.captured(QStringLiteral("start")).toInt();
+            const int end = match.captured(QStringLiteral("end")).toInt();
+            payload = payload.mid(start, end - start + 1);
+        }
+    }
+}
+
+void FakeGetWithDataReply::respond()
+{
+    if (aborted) {
+        setError(OperationCanceledError, QStringLiteral("Operation Canceled"));
+        emit metaDataChanged();
+        emit finished();
+        return;
+    }
+    setHeader(QNetworkRequest::ContentLengthHeader, payload.size());
+    setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
+    setRawHeader("OC-ETag", fileInfo->etag);
+    setRawHeader("ETag", fileInfo->etag);
+    setRawHeader("OC-FileId", fileInfo->fileId);
+    emit metaDataChanged();
+    if (bytesAvailable())
+        emit readyRead();
+    emit finished();
+}
+
+void FakeGetWithDataReply::abort()
+{
+    setError(OperationCanceledError, QStringLiteral("Operation Canceled"));
+    aborted = true;
+}
+
+qint64 FakeGetWithDataReply::bytesAvailable() const
+{
+    if (aborted)
+        return 0;
+    return payload.size() - offset + QIODevice::bytesAvailable();
+}
+
+qint64 FakeGetWithDataReply::readData(char *data, qint64 maxlen)
+{
+    qint64 len = std::min(payload.size() - offset, quint64(maxlen));
+    std::memcpy(data, payload.constData() + offset, len);
+    offset += len;
+    return len;
+}
+
+FakeChunkMoveReply::FakeChunkMoveReply(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
+    : QNetworkReply { parent }
+{
+    setRequest(request);
+    setUrl(request.url());
+    setOperation(op);
+    open(QIODevice::ReadOnly);
+    fileInfo = perform(uploadsFileInfo, remoteRootFileInfo, request);
+    if (!fileInfo) {
+        QTimer::singleShot(0, this, &FakeChunkMoveReply::respondPreconditionFailed);
+    } else {
+        QTimer::singleShot(0, this, &FakeChunkMoveReply::respond);
+    }
+}
+
+FileInfo *FakeChunkMoveReply::perform(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo, const QNetworkRequest &request)
+{
+    QString source = getFilePathFromUrl(request.url());
+    Q_ASSERT(!source.isEmpty());
+    Q_ASSERT(source.endsWith(QLatin1String("/.file")));
+    source = source.left(source.length() - qstrlen("/.file"));
+
+    auto sourceFolder = uploadsFileInfo.find(source);
+    Q_ASSERT(sourceFolder);
+    Q_ASSERT(sourceFolder->isDir);
+    int count = 0;
+    qlonglong size = 0;
+    char payload = '\0';
+
+    QString fileName = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination")));
+    Q_ASSERT(!fileName.isEmpty());
+
+    // Compute the size and content from the chunks if possible
+    for (auto chunkName : sourceFolder->children.keys()) {
+        auto &x = sourceFolder->children[chunkName];
+        Q_ASSERT(!x.isDir);
+        Q_ASSERT(x.size > 0); // There should not be empty chunks
+        size += x.size;
+        Q_ASSERT(!payload || payload == x.contentChar);
+        payload = x.contentChar;
+        ++count;
+    }
+    Q_ASSERT(sourceFolder->children.count() == count); // There should not be holes or extra files
+
+    // NOTE: This does not actually assemble the file data from the chunks!
+    FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
+    if (fileInfo) {
+        // The client should put this header
+        Q_ASSERT(request.hasRawHeader("If"));
+
+        // And it should condition on the destination file
+        auto start = QByteArray("<" + request.rawHeader("Destination") + ">");
+        Q_ASSERT(request.rawHeader("If").startsWith(start));
+
+        if (request.rawHeader("If") != start + " ([\"" + fileInfo->etag + "\"])") {
+            return nullptr;
+        }
+        fileInfo->size = size;
+        fileInfo->contentChar = payload;
+    } else {
+        Q_ASSERT(!request.hasRawHeader("If"));
+        // Assume that the file is filled with the same character
+        fileInfo = remoteRootFileInfo.create(fileName, size, payload);
+    }
+    fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong());
+    remoteRootFileInfo.find(fileName, /*invalidate_etags=*/true);
+
+    return fileInfo;
+}
+
+void FakeChunkMoveReply::respond()
+{
+    setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
+    setRawHeader("OC-ETag", fileInfo->etag);
+    setRawHeader("ETag", fileInfo->etag);
+    setRawHeader("OC-FileId", fileInfo->fileId);
+    emit metaDataChanged();
+    emit finished();
+}
+
+void FakeChunkMoveReply::respondPreconditionFailed()
+{
+    setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 412);
+    setError(InternalServerError, QStringLiteral("Precondition Failed"));
+    emit metaDataChanged();
+    emit finished();
+}
+
+void FakeChunkMoveReply::abort()
+{
+    setError(OperationCanceledError, QStringLiteral("abort"));
+    emit finished();
+}
+
+FakePayloadReply::FakePayloadReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &body, QObject *parent)
+    : QNetworkReply { parent }
+    , _body(body)
+{
+    setRequest(request);
+    setUrl(request.url());
+    setOperation(op);
+    open(QIODevice::ReadOnly);
+    QTimer::singleShot(10, this, &FakePayloadReply::respond);
+}
+
+void FakePayloadReply::respond()
+{
+    setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
+    setHeader(QNetworkRequest::ContentLengthHeader, _body.size());
+    emit metaDataChanged();
+    emit readyRead();
+    setFinished(true);
+    emit finished();
+}
+
+qint64 FakePayloadReply::readData(char *buf, qint64 max)
+{
+    max = qMin<qint64>(max, _body.size());
+    memcpy(buf, _body.constData(), max);
+    _body = _body.mid(max);
+    return max;
+}
+
+qint64 FakePayloadReply::bytesAvailable() const
+{
+    return _body.size();
+}
+
+FakeErrorReply::FakeErrorReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent, int httpErrorCode, const QByteArray &body)
+    : QNetworkReply { parent }
+    , _body(body)
+{
+    setRequest(request);
+    setUrl(request.url());
+    setOperation(op);
+    open(QIODevice::ReadOnly);
+    setAttribute(QNetworkRequest::HttpStatusCodeAttribute, httpErrorCode);
+    setError(InternalServerError, QStringLiteral("Internal Server Fake Error"));
+    QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
+}
+
+void FakeErrorReply::respond()
+{
+    emit metaDataChanged();
+    emit readyRead();
+    // finishing can come strictly after readyRead was called
+    QTimer::singleShot(5, this, &FakeErrorReply::slotSetFinished);
+}
+
+void FakeErrorReply::slotSetFinished()
+{
+    setFinished(true);
+    emit finished();
+}
+
+qint64 FakeErrorReply::readData(char *buf, qint64 max)
+{
+    max = qMin<qint64>(max, _body.size());
+    memcpy(buf, _body.constData(), max);
+    _body = _body.mid(max);
+    return max;
+}
+
+qint64 FakeErrorReply::bytesAvailable() const
+{
+    return _body.size();
+}
+
+void FakeHangingReply::abort()
+{
+    // Follow more or less the implementation of QNetworkReplyImpl::abort
+    close();
+    setError(OperationCanceledError, tr("Operation canceled"));
+    emit error(OperationCanceledError);
+    setFinished(true);
+    emit finished();
+}
+
+FakeQNAM::FakeQNAM(FileInfo initialRoot)
+    : _remoteRootFileInfo { std::move(initialRoot) }
+{
+    setCookieJar(new OCC::CookieJar);
+}
+
+QNetworkReply *FakeQNAM::createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData)
+{
+    if (_override) {
+        if (auto reply = _override(op, request, outgoingData))
+            return reply;
+    }
+    const QString fileName = getFilePathFromUrl(request.url());
+    Q_ASSERT(!fileName.isNull());
+    if (_errorPaths.contains(fileName))
+        return new FakeErrorReply { op, request, this, _errorPaths[fileName] };
+
+    bool isUpload = request.url().path().startsWith(sUploadUrl.path());
+    FileInfo &info = isUpload ? _uploadFileInfo : _remoteRootFileInfo;
+
+    auto verb = request.attribute(QNetworkRequest::CustomVerbAttribute);
+    if (verb == QLatin1String("PROPFIND"))
+        // Ignore outgoingData always returning somethign good enough, works for now.
+        return new FakePropfindReply { info, op, request, this };
+    else if (verb == QLatin1String("GET") || op == QNetworkAccessManager::GetOperation)
+        return new FakeGetReply { info, op, request, this };
+    else if (verb == QLatin1String("PUT") || op == QNetworkAccessManager::PutOperation)
+        return new FakePutReply { info, op, request, outgoingData->readAll(), this };
+    else if (verb == QLatin1String("MKCOL"))
+        return new FakeMkcolReply { info, op, request, this };
+    else if (verb == QLatin1String("DELETE") || op == QNetworkAccessManager::DeleteOperation)
+        return new FakeDeleteReply { info, op, request, this };
+    else if (verb == QLatin1String("MOVE") && !isUpload)
+        return new FakeMoveReply { info, op, request, this };
+    else if (verb == QLatin1String("MOVE") && isUpload)
+        return new FakeChunkMoveReply { info, _remoteRootFileInfo, op, request, this };
+    else {
+        qDebug() << verb << outgoingData;
+        Q_UNREACHABLE();
+    }
+}
+
+FakeFolder::FakeFolder(const FileInfo &fileTemplate)
+    : _localModifier(_tempDir.path())
+{
+    // Needs to be done once
+    OCC::SyncEngine::minimumFileAgeForUpload = std::chrono::milliseconds(0);
+    OCC::Logger::instance()->setLogFile(QStringLiteral("-"));
+
+    QDir rootDir { _tempDir.path() };
+    qDebug() << "FakeFolder operating on" << rootDir;
+    toDisk(rootDir, fileTemplate);
+
+    _fakeQnam = new FakeQNAM(fileTemplate);
+    _account = OCC::Account::create();
+    _account->setUrl(QUrl(QStringLiteral("http://admin:admin@localhost/owncloud")));
+    _account->setCredentials(new FakeCredentials { _fakeQnam });
+    _account->setDavDisplayName(QStringLiteral("fakename"));
+    _account->setServerVersion(QStringLiteral("10.0.0"));
+
+    _journalDb.reset(new OCC::SyncJournalDb(localPath() + QStringLiteral(".sync_test.db")));
+    _syncEngine.reset(new OCC::SyncEngine(_account, localPath(), QString(), _journalDb.get()));
+    // Ignore temporary files from the download. (This is in the default exclude list, but we don't load it)
+    _syncEngine->excludedFiles().addManualExclude(QStringLiteral("]*.~*"));
+
+    // handle aboutToRemoveAllFiles with a timeout in case our test does not handle it
+    QObject::connect(_syncEngine.get(), &OCC::SyncEngine::aboutToRemoveAllFiles, _syncEngine.get(), [this](OCC::SyncFileItem::Direction, std::function<void(bool)> callback) {
+        QTimer::singleShot(1 * 1000, _syncEngine.get(), [callback] {
+            callback(false);
+        });
+    });
+
+    // Ensure we have a valid VfsOff instance "running"
+    switchToVfs(_syncEngine->syncOptions()._vfs);
+
+    // A new folder will update the local file state database on first sync.
+    // To have a state matching what users will encounter, we have to a sync
+    // using an identical local/remote file tree first.
+    ENFORCE(syncOnce());
+}
+
+void FakeFolder::switchToVfs(QSharedPointer<OCC::Vfs> vfs)
+{
+    auto opts = _syncEngine->syncOptions();
+
+    opts._vfs->stop();
+    QObject::disconnect(_syncEngine.get(), nullptr, opts._vfs.data(), nullptr);
+
+    opts._vfs = vfs;
+    _syncEngine->setSyncOptions(opts);
+
+    OCC::VfsSetupParams vfsParams;
+    vfsParams.filesystemPath = localPath();
+    vfsParams.remotePath = QLatin1Char('/');
+    vfsParams.account = _account;
+    vfsParams.journal = _journalDb.get();
+    vfsParams.providerName = QStringLiteral("OC-TEST");
+    vfsParams.providerVersion = QStringLiteral("0.1");
+    QObject::connect(_syncEngine.get(), &QObject::destroyed, vfs.data(), [vfs]() {
+        vfs->stop();
+        vfs->unregisterFolder();
+    });
+
+    vfs->start(vfsParams);
+}
+
+FileInfo FakeFolder::currentLocalState()
+{
+    QDir rootDir { _tempDir.path() };
+    FileInfo rootTemplate;
+    fromDisk(rootDir, rootTemplate);
+    rootTemplate.fixupParentPathRecursively();
+    return rootTemplate;
+}
+
+QString FakeFolder::localPath() const
+{
+    // SyncEngine wants a trailing slash
+    if (_tempDir.path().endsWith(QLatin1Char('/')))
+        return _tempDir.path();
+    return _tempDir.path() + QLatin1Char('/');
+}
+
+void FakeFolder::scheduleSync()
+{
+    // Have to be done async, else, an error before exec() does not terminate the event loop.
+    QMetaObject::invokeMethod(_syncEngine.get(), "startSync", Qt::QueuedConnection);
+}
+
+void FakeFolder::execUntilBeforePropagation()
+{
+    QSignalSpy spy(_syncEngine.get(), SIGNAL(aboutToPropagate(SyncFileItemVector &)));
+    QVERIFY(spy.wait());
+}
+
+void FakeFolder::execUntilItemCompleted(const QString &relativePath)
+{
+    QSignalSpy spy(_syncEngine.get(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
+    QElapsedTimer t;
+    t.start();
+    while (t.elapsed() < 5000) {
+        spy.clear();
+        QVERIFY(spy.wait());
+        for (const QList<QVariant> &args : spy) {
+            auto item = args[0].value<OCC::SyncFileItemPtr>();
+            if (item->destination() == relativePath)
+                return;
+        }
+    }
+    QVERIFY(false);
+}
+
+void FakeFolder::toDisk(QDir &dir, const FileInfo &templateFi)
+{
+    foreach (const FileInfo &child, templateFi.children) {
+        if (child.isDir) {
+            QDir subDir(dir);
+            dir.mkdir(child.name);
+            subDir.cd(child.name);
+            toDisk(subDir, child);
+        } else {
+            QFile file { dir.filePath(child.name) };
+            file.open(QFile::WriteOnly);
+            file.write(QByteArray {}.fill(child.contentChar, child.size));
+            file.close();
+            OCC::FileSystem::setModTime(file.fileName(), OCC::Utility::qDateTimeToTime_t(child.lastModified));
+        }
+    }
+}
+
+void FakeFolder::fromDisk(QDir &dir, FileInfo &templateFi)
+{
+    foreach (const QFileInfo &diskChild, dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) {
+        if (diskChild.isDir()) {
+            QDir subDir = dir;
+            subDir.cd(diskChild.fileName());
+            FileInfo &subFi = templateFi.children[diskChild.fileName()] = FileInfo { diskChild.fileName() };
+            fromDisk(subDir, subFi);
+        } else {
+            QFile f { diskChild.filePath() };
+            f.open(QFile::ReadOnly);
+            auto content = f.read(1);
+            if (content.size() == 0) {
+                qWarning() << "Empty file at:" << diskChild.filePath();
+                continue;
+            }
+            char contentChar = content.at(0);
+            templateFi.children.insert(diskChild.fileName(), FileInfo { diskChild.fileName(), diskChild.size(), contentChar });
+        }
+    }
+}
+
+FileInfo &findOrCreateDirs(FileInfo &base, PathComponents components)
+{
+    if (components.isEmpty())
+        return base;
+    auto childName = components.pathRoot();
+    auto it = base.children.find(childName);
+    if (it != base.children.end()) {
+        return findOrCreateDirs(*it, components.subComponents());
+    }
+    auto &newDir = base.children[childName] = FileInfo { childName };
+    newDir.parentPath = base.path();
+    return findOrCreateDirs(newDir, components.subComponents());
+}
+
+FileInfo FakeFolder::dbState() const
+{
+    FileInfo result;
+    _journalDb->getFilesBelowPath("", [&](const OCC::SyncJournalFileRecord &record) {
+        auto components = PathComponents(QString::fromUtf8(record._path));
+        auto &parentDir = findOrCreateDirs(result, components.parentDirComponents());
+        auto name = components.fileName();
+        auto &item = parentDir.children[name];
+        item.name = name;
+        item.parentPath = parentDir.path();
+        item.size = record._fileSize;
+        item.isDir = record._type == ItemTypeDirectory;
+        item.permissions = record._remotePerm;
+        item.etag = record._etag;
+        item.lastModified = OCC::Utility::qDateTimeFromTime_t(record._modtime);
+        item.fileId = record._fileId;
+        item.checksums = record._checksumHeader;
+        // item.contentChar can't be set from the db
+    });
+    return result;
+}
+
+OCC::SyncFileItemPtr ItemCompletedSpy::findItem(const QString &path) const
+{
+    for (const QList<QVariant> &args : *this) {
+        auto item = args[0].value<OCC::SyncFileItemPtr>();
+        if (item->destination() == path)
+            return item;
+    }
+    return OCC::SyncFileItemPtr::create();
+}
index b19f2d0f51d30c4daccc079e0d4df96cfaa50d69..52c330c6260e7596fd2593fb6e1758d7231bda7c 100644 (file)
@@ -20,6 +20,8 @@
 #include <QNetworkReply>
 #include <QMap>
 #include <QtTest>
+
+#include <cstring>
 #include <memory>
 
 #include <cookiejar.h>
@@ -47,8 +49,8 @@ inline QString getFilePathFromUrl(const QUrl &url) {
 }
 
 
-inline QString generateEtag() {
-    return QString::number(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch(), 16) + QByteArray::number(qrand(), 16);
+inline QByteArray generateEtag() {
+    return QByteArray::number(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch(), 16) + QByteArray::number(qrand(), 16);
 }
 inline QByteArray generateFileId() {
     return QByteArray::number(qrand(), 16);
@@ -56,14 +58,12 @@ inline QByteArray generateFileId() {
 
 class PathComponents : public QStringList {
 public:
-    PathComponents(const char *path) : PathComponents{QString::fromUtf8(path)} {}
-    PathComponents(const QString &path) : QStringList{path.split('/', QString::SkipEmptyParts)} { }
-    PathComponents(const QStringList &pathComponents) : QStringList{pathComponents} { }
+    PathComponents(const char *path);
+    PathComponents(const QString &path);
+    PathComponents(const QStringList &pathComponents);
 
-    PathComponents parentDirComponents() const {
-        return PathComponents{mid(0, size() - 1)};
-    }
-    PathComponents subComponents() const& { return PathComponents{mid(1)}; }
+    PathComponents parentDirComponents() const;
+    PathComponents subComponents() const &;
     PathComponents subComponents() && { removeFirst(); return std::move(*this); }
     QString pathRoot() const { return first(); }
     QString fileName() const { return last(); }
@@ -87,225 +87,69 @@ class DiskFileModifier : public FileModifier
     QDir _rootDir;
 public:
     DiskFileModifier(const QString &rootDirPath) : _rootDir(rootDirPath) { }
-    void remove(const QString &relativePath) override {
-        QFileInfo fi{_rootDir.filePath(relativePath)};
-        if (fi.isFile())
-            QVERIFY(_rootDir.remove(relativePath));
-        else
-            QVERIFY(QDir{fi.filePath()}.removeRecursively());
-    }
-    void insert(const QString &relativePath, qint64 size = 64, char contentChar = 'W') override {
-        QFile file{_rootDir.filePath(relativePath)};
-        QVERIFY(!file.exists());
-        file.open(QFile::WriteOnly);
-        QByteArray buf(1024, contentChar);
-        for (int x = 0; x < size/buf.size(); ++x) {
-            file.write(buf);
-        }
-        file.write(buf.data(), size % buf.size());
-        file.close();
-        // Set the mtime 30 seconds in the past, for some tests that need to make sure that the mtime differs.
-        OCC::FileSystem::setModTime(file.fileName(), OCC::Utility::qDateTimeToTime_t(QDateTime::currentDateTimeUtc().addSecs(-30)));
-        QCOMPARE(file.size(), size);
-    }
-    void setContents(const QString &relativePath, char contentChar) override {
-        QFile file{_rootDir.filePath(relativePath)};
-        QVERIFY(file.exists());
-        qint64 size = file.size();
-        file.open(QFile::WriteOnly);
-        file.write(QByteArray{}.fill(contentChar, size));
-    }
-    void appendByte(const QString &relativePath) override {
-        QFile file{_rootDir.filePath(relativePath)};
-        QVERIFY(file.exists());
-        file.open(QFile::ReadWrite);
-        QByteArray contents = file.read(1);
-        file.seek(file.size());
-        file.write(contents);
-    }
-    void mkdir(const QString &relativePath) override {
-        _rootDir.mkpath(relativePath);
-    }
-    void rename(const QString &from, const QString &to) override {
-        QVERIFY(_rootDir.exists(from));
-        QVERIFY(_rootDir.rename(from, to));
-    }
-    void setModTime(const QString &relativePath, const QDateTime &modTime) override {
-        OCC::FileSystem::setModTime(_rootDir.filePath(relativePath), OCC::Utility::qDateTimeToTime_t(modTime));
-    }
+    void remove(const QString &relativePath) override;
+    void insert(const QString &relativePath, qint64 size = 64, char contentChar = 'W') override;
+    void setContents(const QString &relativePath, char contentChar) override;
+    void appendByte(const QString &relativePath) override;
+
+    void mkdir(const QString &relativePath) override;
+    void rename(const QString &from, const QString &to) override;
+    void setModTime(const QString &relativePath, const QDateTime &modTime) override;
 };
 
 class FileInfo : public FileModifier
 {
 public:
-    static FileInfo A12_B12_C12_S12() {
-        FileInfo fi{QString{}, {
-            {QStringLiteral("A"), {
-                {QStringLiteral("a1"), 4},
-                {QStringLiteral("a2"), 4}
-            }},
-            {QStringLiteral("B"), {
-                {QStringLiteral("b1"), 16},
-                {QStringLiteral("b2"), 16}
-            }},
-            {QStringLiteral("C"), {
-                {QStringLiteral("c1"), 24},
-                {QStringLiteral("c2"), 24}
-            }},
-        }};
-        FileInfo sharedFolder{QStringLiteral("S"), {
-            {QStringLiteral("s1"), 32},
-            {QStringLiteral("s2"), 32}
-        }};
-        sharedFolder.isShared = true;
-        sharedFolder.children[QStringLiteral("s1")].isShared = true;
-        sharedFolder.children[QStringLiteral("s2")].isShared = true;
-        fi.children.insert(sharedFolder.name, std::move(sharedFolder));
-        return fi;
-    }
+    static FileInfo A12_B12_C12_S12();
 
     FileInfo() = default;
     FileInfo(const QString &name) : name{name} { }
     FileInfo(const QString &name, qint64 size) : name{name}, isDir{false}, size{size} { }
     FileInfo(const QString &name, qint64 size, char contentChar) : name{name}, isDir{false}, size{size}, contentChar{contentChar} { }
-    FileInfo(const QString &name, const std::initializer_list<FileInfo> &children) : name{name} {
-        for (const auto &source : children)
-            addChild(source);
-    }
+    FileInfo(const QString &name, const std::initializer_list<FileInfo> &children);
 
-    void addChild(const FileInfo &info)
-    {
-        auto &dest = this->children[info.name] = info;
-        dest.parentPath = path();
-        dest.fixupParentPathRecursively();
-    }
+    void addChild(const FileInfo &info);
 
-    void remove(const QString &relativePath) override {
-        const PathComponents pathComponents{relativePath};
-        FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
-        Q_ASSERT(parent);
-        parent->children.erase(std::find_if(parent->children.begin(), parent->children.end(),
-                [&pathComponents](const FileInfo &fi){ return fi.name == pathComponents.fileName(); }));
-    }
+    void remove(const QString &relativePath) override;
 
-    void insert(const QString &relativePath, qint64 size = 64, char contentChar = 'W') override {
-        create(relativePath, size, contentChar);
-    }
+    void insert(const QString &relativePath, qint64 size = 64, char contentChar = 'W') override;
 
-    void setContents(const QString &relativePath, char contentChar) override {
-        FileInfo *file = findInvalidatingEtags(relativePath);
-        Q_ASSERT(file);
-        file->contentChar = contentChar;
-    }
+    void setContents(const QString &relativePath, char contentChar) override;
 
-    void appendByte(const QString &relativePath) override {
-        FileInfo *file = findInvalidatingEtags(relativePath);
-        Q_ASSERT(file);
-        file->size += 1;
-    }
+    void appendByte(const QString &relativePath) override;
 
-    void mkdir(const QString &relativePath) override {
-        createDir(relativePath);
-    }
+    void mkdir(const QString &relativePath) override;
 
-    void rename(const QString &oldPath, const QString &newPath) override {
-        const PathComponents newPathComponents{newPath};
-        FileInfo *dir = findInvalidatingEtags(newPathComponents.parentDirComponents());
-        Q_ASSERT(dir);
-        Q_ASSERT(dir->isDir);
-        const PathComponents pathComponents{oldPath};
-        FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
-        Q_ASSERT(parent);
-        FileInfo fi = parent->children.take(pathComponents.fileName());
-        fi.parentPath = dir->path();
-        fi.name = newPathComponents.fileName();
-        fi.fixupParentPathRecursively();
-        dir->children.insert(newPathComponents.fileName(), std::move(fi));
-    }
+    void rename(const QString &oldPath, const QString &newPath) override;
 
-    void setModTime(const QString &relativePath, const QDateTime &modTime) override {
-        FileInfo *file = findInvalidatingEtags(relativePath);
-        Q_ASSERT(file);
-        file->lastModified = modTime;
-    }
+    void setModTime(const QString &relativePath, const QDateTime &modTime) override;
 
-    FileInfo *find(PathComponents pathComponents, const bool invalidateEtags = false) {
-        if (pathComponents.isEmpty()) {
-            if (invalidateEtags) {
-                etag = generateEtag();
-            }
-            return this;
-        }
-        QString childName = pathComponents.pathRoot();
-        auto it = children.find(childName);
-        if (it != children.end()) {
-            auto file = it->find(std::move(pathComponents).subComponents(), invalidateEtags);
-            if (file && invalidateEtags) {
-                // Update parents on the way back
-                etag = generateEtag();
-            }
-            return file;
-        }
-        return nullptr;
-    }
+    FileInfo *find(PathComponents pathComponents, const bool invalidateEtags = false);
 
-    FileInfo *createDir(const QString &relativePath) {
-        const PathComponents pathComponents{relativePath};
-        FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
-        Q_ASSERT(parent);
-        FileInfo &child = parent->children[pathComponents.fileName()] = FileInfo{pathComponents.fileName()};
-        child.parentPath = parent->path();
-        child.etag = generateEtag();
-        return &child;
-    }
+    FileInfo *createDir(const QString &relativePath);
 
-    FileInfo *create(const QString &relativePath, qint64 size, char contentChar) {
-        const PathComponents pathComponents{relativePath};
-        FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
-        Q_ASSERT(parent);
-        FileInfo &child = parent->children[pathComponents.fileName()] = FileInfo{pathComponents.fileName(), size};
-        child.parentPath = parent->path();
-        child.contentChar = contentChar;
-        child.etag = generateEtag();
-        return &child;
-    }
+    FileInfo *create(const QString &relativePath, qint64 size, char contentChar);
 
     bool operator<(const FileInfo &other) const {
         return name < other.name;
     }
 
-    bool operator==(const FileInfo &other) const {
-        // Consider files to be equal between local<->remote as a user would.
-        return name == other.name
-            && isDir == other.isDir
-            && size == other.size
-            && contentChar == other.contentChar
-            && children == other.children;
-    }
+    bool operator==(const FileInfo &other) const;
 
     bool operator!=(const FileInfo &other) const {
         return !operator==(other);
     }
 
-    QString path() const {
-        return (parentPath.isEmpty() ? QString() : (parentPath + '/')) + name;
-    }
+    QString path() const;
 
-    void fixupParentPathRecursively() {
-        auto p = path();
-        for (auto it = children.begin(); it != children.end(); ++it) {
-            Q_ASSERT(it.key() == it->name);
-            it->parentPath = p;
-            it->fixupParentPathRecursively();
-        }
-    }
+    void fixupParentPathRecursively();
 
     QString name;
     bool isDir = true;
     bool isShared = false;
     OCC::RemotePermissions permissions; // When uset, defaults to everything
     QDateTime lastModified = QDateTime::currentDateTimeUtc().addDays(-7);
-    QString etag = generateEtag();
+    QByteArray etag = generateEtag();
     QByteArray fileId = generateFileId();
     QByteArray checksums;
     QByteArray extraDavProperties;
@@ -316,9 +160,7 @@ public:
     QMap<QString, FileInfo> children;
     QString parentPath;
 
-    FileInfo *findInvalidatingEtags(PathComponents pathComponents) {
-        return find(std::move(pathComponents), true);
-    }
+    FileInfo *findInvalidatingEtags(PathComponents pathComponents);
 
     friend inline QDebug operator<<(QDebug dbg, const FileInfo& fi) {
         return dbg << "{ " << fi.path() << ": " << fi.children;
@@ -331,99 +173,16 @@ class FakePropfindReply : public QNetworkReply
 public:
     QByteArray payload;
 
-    FakePropfindReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
-    : QNetworkReply{parent} {
-        setRequest(request);
-        setUrl(request.url());
-        setOperation(op);
-        open(QIODevice::ReadOnly);
-
-        QString fileName = getFilePathFromUrl(request.url());
-        Q_ASSERT(!fileName.isNull()); // for root, it should be empty
-        const FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
-        if (!fileInfo) {
-            QMetaObject::invokeMethod(this, "respond404", Qt::QueuedConnection);
-            return;
-        }
-        QString prefix = request.url().path().left(request.url().path().size() - fileName.size());
-
-        // Don't care about the request and just return a full propfind
-        const QString davUri{QStringLiteral("DAV:")};
-        const QString ocUri{QStringLiteral("http://owncloud.org/ns")};
-        QBuffer buffer{&payload};
-        buffer.open(QIODevice::WriteOnly);
-        QXmlStreamWriter xml( &buffer );
-        xml.writeNamespace(davUri, "d");
-        xml.writeNamespace(ocUri, "oc");
-        xml.writeStartDocument();
-        xml.writeStartElement(davUri, QStringLiteral("multistatus"));
-        auto writeFileResponse = [&](const FileInfo &fileInfo) {
-            xml.writeStartElement(davUri, QStringLiteral("response"));
-
-            xml.writeTextElement(davUri, QStringLiteral("href"), prefix + QUrl::toPercentEncoding(fileInfo.path(), "/"));
-            xml.writeStartElement(davUri, QStringLiteral("propstat"));
-            xml.writeStartElement(davUri, QStringLiteral("prop"));
-
-            if (fileInfo.isDir) {
-                xml.writeStartElement(davUri, QStringLiteral("resourcetype"));
-                xml.writeEmptyElement(davUri, QStringLiteral("collection"));
-                xml.writeEndElement(); // resourcetype
-            } else
-                xml.writeEmptyElement(davUri, QStringLiteral("resourcetype"));
-
-            auto gmtDate = fileInfo.lastModified.toUTC();
-            auto stringDate = QLocale::c().toString(gmtDate, "ddd, dd MMM yyyy HH:mm:ss 'GMT'");
-            xml.writeTextElement(davUri, QStringLiteral("getlastmodified"), stringDate);
-            xml.writeTextElement(davUri, QStringLiteral("getcontentlength"), QString::number(fileInfo.size));
-            xml.writeTextElement(davUri, QStringLiteral("getetag"), QStringLiteral("\"%1\"").arg(fileInfo.etag));
-            xml.writeTextElement(ocUri, QStringLiteral("permissions"), !fileInfo.permissions.isNull()
-                ? QString(fileInfo.permissions.toString())
-                : fileInfo.isShared ? QStringLiteral("SRDNVCKW") : QStringLiteral("RDNVCKW"));
-            xml.writeTextElement(ocUri, QStringLiteral("id"), fileInfo.fileId);
-            xml.writeTextElement(ocUri, QStringLiteral("checksums"), fileInfo.checksums);
-            buffer.write(fileInfo.extraDavProperties);
-            xml.writeEndElement(); // prop
-            xml.writeTextElement(davUri, QStringLiteral("status"), "HTTP/1.1 200 OK");
-            xml.writeEndElement(); // propstat
-            xml.writeEndElement(); // response
-        };
-
-        writeFileResponse(*fileInfo);
-        foreach(const FileInfo &childFileInfo, fileInfo->children)
-           writeFileResponse(childFileInfo);
-        xml.writeEndElement(); // multistatus
-        xml.writeEndDocument();
-
-        QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
-    }
+    FakePropfindReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent);
 
-    Q_INVOKABLE void respond() {
-        setHeader(QNetworkRequest::ContentLengthHeader, payload.size());
-        setHeader(QNetworkRequest::ContentTypeHeader, "application/xml; charset=utf-8");
-        setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 207);
-        setFinished(true);
-        emit metaDataChanged();
-        if (bytesAvailable())
-            emit readyRead();
-        emit finished();
-    }
+    Q_INVOKABLE void respond();
 
-    Q_INVOKABLE void respond404() {
-        setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 404);
-        setError(InternalServerError, "Not Found");
-        emit metaDataChanged();
-        emit finished();
-    }
+    Q_INVOKABLE void respond404();
 
     void abort() override { }
 
-    qint64 bytesAvailable() const override { return payload.size() + QIODevice::bytesAvailable(); }
-    qint64 readData(char *data, qint64 maxlen) override {
-        qint64 len = std::min(qint64{payload.size()}, maxlen);
-        strncpy(data, payload.constData(), len);
-        payload.remove(0, len);
-        return len;
-    }
+    qint64 bytesAvailable() const override;
+    qint64 readData(char *data, qint64 maxlen) override;
 };
 
 class FakePutReply : public QNetworkReply
@@ -431,50 +190,13 @@ class FakePutReply : public QNetworkReply
     Q_OBJECT
     FileInfo *fileInfo;
 public:
-    FakePutReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &putPayload, QObject *parent)
-    : QNetworkReply{parent} {
-        setRequest(request);
-        setUrl(request.url());
-        setOperation(op);
-        open(QIODevice::ReadOnly);
-        fileInfo = perform(remoteRootFileInfo, request, putPayload);
-        QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
-    }
+    FakePutReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &putPayload, QObject *parent);
 
-    static FileInfo *perform(FileInfo &remoteRootFileInfo, const QNetworkRequest &request, const QByteArray &putPayload)
-    {
-        QString fileName = getFilePathFromUrl(request.url());
-        Q_ASSERT(!fileName.isEmpty());
-        FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
-        if (fileInfo) {
-            fileInfo->size = putPayload.size();
-            fileInfo->contentChar = putPayload.at(0);
-        } else {
-            // Assume that the file is filled with the same character
-            fileInfo = remoteRootFileInfo.create(fileName, putPayload.size(), putPayload.at(0));
-        }
-        fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong());
-        remoteRootFileInfo.find(fileName, /*invalidateEtags=*/true);
-        return fileInfo;
-    }
+    static FileInfo *perform(FileInfo &remoteRootFileInfo, const QNetworkRequest &request, const QByteArray &putPayload);
 
-    Q_INVOKABLE virtual void respond()
-    {
-        emit uploadProgress(fileInfo->size, fileInfo->size);
-        setRawHeader("OC-ETag", fileInfo->etag.toLatin1());
-        setRawHeader("ETag", fileInfo->etag.toLatin1());
-        setRawHeader("OC-FileID", fileInfo->fileId);
-        setRawHeader("X-OC-MTime", "accepted"); // Prevents Q_ASSERT(!_runningNow) since we'll call PropagateItemJob::done twice in that case.
-        setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
-        emit metaDataChanged();
-        emit finished();
-    }
+    Q_INVOKABLE virtual void respond();
 
-    void abort() override
-    {
-        setError(OperationCanceledError, "abort");
-        emit finished();
-    }
+    void abort() override;
     qint64 readData(char *, qint64) override { return 0; }
 };
 
@@ -483,30 +205,9 @@ class FakeMkcolReply : public QNetworkReply
     Q_OBJECT
     FileInfo *fileInfo;
 public:
-    FakeMkcolReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
-    : QNetworkReply{parent} {
-        setRequest(request);
-        setUrl(request.url());
-        setOperation(op);
-        open(QIODevice::ReadOnly);
+    FakeMkcolReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent);
 
-        QString fileName = getFilePathFromUrl(request.url());
-        Q_ASSERT(!fileName.isEmpty());
-        fileInfo = remoteRootFileInfo.createDir(fileName);
-
-        if (!fileInfo) {
-            abort();
-            return;
-        }
-        QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
-    }
-
-    Q_INVOKABLE void respond() {
-        setRawHeader("OC-FileId", fileInfo->fileId);
-        setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
-        emit metaDataChanged();
-        emit finished();
-    }
+    Q_INVOKABLE void respond();
 
     void abort() override { }
     qint64 readData(char *, qint64) override { return 0; }
@@ -516,24 +217,9 @@ class FakeDeleteReply : public QNetworkReply
 {
     Q_OBJECT
 public:
-    FakeDeleteReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
-    : QNetworkReply{parent} {
-        setRequest(request);
-        setUrl(request.url());
-        setOperation(op);
-        open(QIODevice::ReadOnly);
-
-        QString fileName = getFilePathFromUrl(request.url());
-        Q_ASSERT(!fileName.isEmpty());
-        remoteRootFileInfo.remove(fileName);
-        QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
-    }
+    FakeDeleteReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent);
 
-    Q_INVOKABLE void respond() {
-        setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 204);
-        emit metaDataChanged();
-        emit finished();
-    }
+    Q_INVOKABLE void respond();
 
     void abort() override { }
     qint64 readData(char *, qint64) override { return 0; }
@@ -543,26 +229,9 @@ class FakeMoveReply : public QNetworkReply
 {
     Q_OBJECT
 public:
-    FakeMoveReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
-    : QNetworkReply{parent} {
-        setRequest(request);
-        setUrl(request.url());
-        setOperation(op);
-        open(QIODevice::ReadOnly);
+    FakeMoveReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent);
 
-        QString fileName = getFilePathFromUrl(request.url());
-        Q_ASSERT(!fileName.isEmpty());
-        QString dest = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination")));
-        Q_ASSERT(!dest.isEmpty());
-        remoteRootFileInfo.rename(fileName, dest);
-        QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
-    }
-
-    Q_INVOKABLE void respond() {
-        setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
-        emit metaDataChanged();
-        emit finished();
-    }
+    Q_INVOKABLE void respond();
 
     void abort() override { }
     qint64 readData(char *, qint64) override { return 0; }
@@ -577,61 +246,40 @@ public:
     int size;
     bool aborted = false;
 
-    FakeGetReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
-    : QNetworkReply{parent} {
-        setRequest(request);
-        setUrl(request.url());
-        setOperation(op);
-        open(QIODevice::ReadOnly);
+    FakeGetReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent);
 
-        QString fileName = getFilePathFromUrl(request.url());
-        Q_ASSERT(!fileName.isEmpty());
-        fileInfo = remoteRootFileInfo.find(fileName);
-        if (!fileInfo)
-            qWarning() << "Could not find file" << fileName << "on the remote";
-        QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
-    }
+    Q_INVOKABLE void respond();
 
-    Q_INVOKABLE void respond() {
-        if (aborted) {
-            setError(OperationCanceledError, "Operation Canceled");
-            emit metaDataChanged();
-            emit finished();
-            return;
-        }
-        payload = fileInfo->contentChar;
-        size = fileInfo->size;
-        setHeader(QNetworkRequest::ContentLengthHeader, size);
-        setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
-        setRawHeader("OC-ETag", fileInfo->etag.toLatin1());
-        setRawHeader("ETag", fileInfo->etag.toLatin1());
-        setRawHeader("OC-FileId", fileInfo->fileId);
-        emit metaDataChanged();
-        if (bytesAvailable())
-            emit readyRead();
-        emit finished();
-    }
+    void abort() override;
+    qint64 bytesAvailable() const override;
 
-    void abort() override {
-        aborted = true;
-    }
-    qint64 bytesAvailable() const override {
-        if (aborted)
-            return 0;
-        return size + QIODevice::bytesAvailable();
-    }
-
-    qint64 readData(char *data, qint64 maxlen) override {
-        qint64 len = std::min(qint64{size}, maxlen);
-        std::fill_n(data, len, payload);
-        size -= len;
-        return len;
-    }
+    qint64 readData(char *data, qint64 maxlen) override;
 
     // useful to be public for testing
     using QNetworkReply::setRawHeader;
 };
 
+class FakeGetWithDataReply : public QNetworkReply
+{
+    Q_OBJECT
+public:
+    const FileInfo *fileInfo;
+    QByteArray payload;
+    quint64 offset = 0;
+    bool aborted = false;
+
+    FakeGetWithDataReply(FileInfo &remoteRootFileInfo, const QByteArray &data, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent);
+
+    Q_INVOKABLE void respond();
+
+    void abort() override;
+    qint64 bytesAvailable() const override;
+
+    qint64 readData(char *data, qint64 maxlen) override;
+
+    // useful to be public for testing
+    using QNetworkReply::setRawHeader;
+};
 
 class FakeChunkMoveReply : public QNetworkReply
 {
@@ -640,101 +288,15 @@ class FakeChunkMoveReply : public QNetworkReply
 public:
     FakeChunkMoveReply(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo,
         QNetworkAccessManager::Operation op, const QNetworkRequest &request,
-        QObject *parent)
-        : QNetworkReply{ parent }
-    {
-        setRequest(request);
-        setUrl(request.url());
-        setOperation(op);
-        open(QIODevice::ReadOnly);
-        fileInfo = perform(uploadsFileInfo, remoteRootFileInfo, request);
-        if (!fileInfo) {
-            QTimer::singleShot(0, this, &FakeChunkMoveReply::respondPreconditionFailed);
-        } else {
-            QTimer::singleShot(0, this, &FakeChunkMoveReply::respond);
-        }
-    }
+        QObject *parent);
 
-    static FileInfo *perform(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo, const QNetworkRequest &request)
-    {
-        QString source = getFilePathFromUrl(request.url());
-        Q_ASSERT(!source.isEmpty());
-        Q_ASSERT(source.endsWith("/.file"));
-        source = source.left(source.length() - static_cast<int>(qstrlen("/.file")));
-        auto sourceFolder = uploadsFileInfo.find(source);
-        Q_ASSERT(sourceFolder);
-        Q_ASSERT(sourceFolder->isDir);
-        qint64 count = 0;
-        qint64 size = 0;
-        char payload = '\0';
-
-        do {
-            QString chunkName = QString::number(count).rightJustified(16, '0');
-            if (!sourceFolder->children.contains(chunkName))
-                break;
-            auto &x = sourceFolder->children[chunkName];
-            Q_ASSERT(!x.isDir);
-            Q_ASSERT(x.size > 0); // There should not be empty chunks
-            size += x.size;
-            Q_ASSERT(!payload || payload == x.contentChar);
-            payload = x.contentChar;
-            ++count;
-        } while(true);
-
-        Q_ASSERT(count > 1); // There should be at least two chunks, otherwise why would we use chunking?
-        Q_ASSERT(sourceFolder->children.count() == count); // There should not be holes or extra files
-
-        QString fileName = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination")));
-        Q_ASSERT(!fileName.isEmpty());
-
-        FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
-        if (fileInfo) {
-            // The client should put this header
-            Q_ASSERT(request.hasRawHeader("If"));
-
-            // And it should condition on the destination file
-            auto start = QByteArray("<" + request.rawHeader("Destination") + ">");
-            Q_ASSERT(request.rawHeader("If").startsWith(start));
-
-            if (request.rawHeader("If") != start + " ([\"" + fileInfo->etag.toLatin1() + "\"])") {
-                return nullptr;
-            }
-            fileInfo->size = size;
-            fileInfo->contentChar = payload;
-        } else {
-            Q_ASSERT(!request.hasRawHeader("If"));
-            // Assume that the file is filled with the same character
-            fileInfo = remoteRootFileInfo.create(fileName, size, payload);
-        }
+    static FileInfo *perform(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo, const QNetworkRequest &request);
 
-        fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong());
-        remoteRootFileInfo.find(fileName, /*invalidateEtags=*/true);
+    Q_INVOKABLE virtual void respond();
 
-        return fileInfo;
-    }
+    Q_INVOKABLE void respondPreconditionFailed();
 
-    Q_INVOKABLE virtual void respond()
-    {
-        setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
-        setRawHeader("OC-ETag", fileInfo->etag.toLatin1());
-        setRawHeader("ETag", fileInfo->etag.toLatin1());
-        setRawHeader("OC-FileId", fileInfo->fileId);
-        emit metaDataChanged();
-        emit finished();
-    }
-
-    Q_INVOKABLE void respondPreconditionFailed() {
-        setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 412);
-        setError(InternalServerError, "Precondition Failed");
-        emit metaDataChanged();
-        emit finished();
-    }
-
-    void abort() override
-    {
-        setError(OperationCanceledError, "abort");
-        emit finished();
-    }
+    void abort() override;
 
     qint64 readData(char *, qint64) override { return 0; }
 };
@@ -745,39 +307,13 @@ class FakePayloadReply : public QNetworkReply
     Q_OBJECT
 public:
     FakePayloadReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request,
-        const QByteArray &body, QObject *parent)
-        : QNetworkReply{ parent }
-        , _body(body)
-    {
-        setRequest(request);
-        setUrl(request.url());
-        setOperation(op);
-        open(QIODevice::ReadOnly);
-        QTimer::singleShot(10, this, &FakePayloadReply::respond);
-    }
+        const QByteArray &body, QObject *parent);
 
-    void respond()
-    {
-        setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
-        setHeader(QNetworkRequest::ContentLengthHeader, _body.size());
-        emit metaDataChanged();
-        emit readyRead();
-        setFinished(true);
-        emit finished();
-    }
+    void respond();
 
     void abort() override {}
-    qint64 readData(char *buf, qint64 max) override
-    {
-        max = qMin<qint64>(max, _body.size());
-        memcpy(buf, _body.constData(), max);
-        _body = _body.mid(max);
-        return max;
-    }
-    qint64 bytesAvailable() const override
-    {
-        return _body.size();
-    }
+    qint64 readData(char *buf, qint64 max) override;
+    qint64 bytesAvailable() const override;
     QByteArray _body;
 };
 
@@ -787,45 +323,21 @@ class FakeErrorReply : public QNetworkReply
     Q_OBJECT
 public:
     FakeErrorReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request,
-                   QObject *parent, int httpErrorCode, const QByteArray &body = QByteArray())
-    : QNetworkReply{parent}, _body(body) {
-        setRequest(request);
-        setUrl(request.url());
-        setOperation(op);
-        open(QIODevice::ReadOnly);
-        setAttribute(QNetworkRequest::HttpStatusCodeAttribute, httpErrorCode);
-        setError(InternalServerError, "Internal Server Fake Error");
-        QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
-    }
+        QObject *parent, int httpErrorCode, const QByteArray &body = QByteArray());
 
-    Q_INVOKABLE virtual void respond() {
-        emit metaDataChanged();
-        emit readyRead();
-        // finishing can come strictly after readyRead was called
-        QTimer::singleShot(5, this, &FakeErrorReply::slotSetFinished);
-    }
+    Q_INVOKABLE virtual void respond();
 
     // make public to give tests easy interface
     using QNetworkReply::setError;
     using QNetworkReply::setAttribute;
 
 public slots:
-    void slotSetFinished() {
-        setFinished(true);
-        emit finished();
-    }
+    void slotSetFinished();
 
 public:
     void abort() override { }
-    qint64 readData(char *buf, qint64 max) override {
-        max = qMin<qint64>(max, _body.size());
-        memcpy(buf, _body.constData(), max);
-        _body = _body.mid(max);
-        return max;
-    }
-    qint64 bytesAvailable() const override {
-        return _body.size();
-    }
+    qint64 readData(char *buf, qint64 max) override;
+    qint64 bytesAvailable() const override;
 
     QByteArray _body;
 };
@@ -844,14 +356,7 @@ public:
         open(QIODevice::ReadOnly);
     }
 
-    void abort() override {
-        // Follow more or less the implementation of QNetworkReplyImpl::abort
-        close();
-        setError(OperationCanceledError, tr("Operation canceled"));
-        emit error(OperationCanceledError);
-        setFinished(true);
-        emit finished();
-    }
+    void abort() override;
     qint64 readData(char *, qint64) override { return 0; }
 };
 
@@ -891,11 +396,7 @@ private:
     Override _override;
 
 public:
-    FakeQNAM(FileInfo initialRoot)
-        : _remoteRootFileInfo{std::move(initialRoot)}
-    {
-        setCookieJar(new OCC::CookieJar);
-    }
+    FakeQNAM(FileInfo initialRoot);
     FileInfo &currentRemoteState() { return _remoteRootFileInfo; }
     FileInfo &uploadState() { return _uploadFileInfo; }
 
@@ -905,40 +406,7 @@ public:
 
 protected:
     QNetworkReply *createRequest(Operation op, const QNetworkRequest &request,
-                                         QIODevice *outgoingData = nullptr) {
-        if (_override) {
-            if (auto reply = _override(op, request, outgoingData))
-                return reply;
-        }
-        const QString fileName = getFilePathFromUrl(request.url());
-        Q_ASSERT(!fileName.isNull());
-        if (_errorPaths.contains(fileName))
-            return new FakeErrorReply{op, request, this, _errorPaths[fileName]};
-
-        bool isUpload = request.url().path().startsWith(sUploadUrl.path());
-        FileInfo &info = isUpload ? _uploadFileInfo : _remoteRootFileInfo;
-
-        auto verb = request.attribute(QNetworkRequest::CustomVerbAttribute);
-        if (verb == "PROPFIND")
-            // Ignore outgoingData always returning somethign good enough, works for now.
-            return new FakePropfindReply{info, op, request, this};
-        else if (verb == QLatin1String("GET") || op == QNetworkAccessManager::GetOperation)
-            return new FakeGetReply{info, op, request, this};
-        else if (verb == QLatin1String("PUT") || op == QNetworkAccessManager::PutOperation)
-            return new FakePutReply{info, op, request, outgoingData->readAll(), this};
-        else if (verb == QLatin1String("MKCOL"))
-            return new FakeMkcolReply{info, op, request, this};
-        else if (verb == QLatin1String("DELETE") || op == QNetworkAccessManager::DeleteOperation)
-            return new FakeDeleteReply{info, op, request, this};
-        else if (verb == QLatin1String("MOVE") && !isUpload)
-            return new FakeMoveReply{info, op, request, this};
-        else if (verb == QLatin1String("MOVE") && isUpload)
-            return new FakeChunkMoveReply{ info, _remoteRootFileInfo, op, request, this };
-        else {
-            qDebug() << verb << outgoingData;
-            Q_UNREACHABLE();
-        }
-    }
+        QIODevice *outgoingData = nullptr) override;
 };
 
 class FakeCredentials : public OCC::AbstractCredentials
@@ -969,69 +437,9 @@ class FakeFolder
     std::unique_ptr<OCC::SyncEngine> _syncEngine;
 
 public:
-    FakeFolder(const FileInfo &fileTemplate)
-        : _localModifier(_tempDir.path())
-    {
-        // Needs to be done once
-        OCC::SyncEngine::minimumFileAgeForUpload = std::chrono::milliseconds(0);
-        OCC::Logger::instance()->setLogFile("-");
-
-        QDir rootDir{_tempDir.path()};
-        qDebug() << "FakeFolder operating on" << rootDir;
-        toDisk(rootDir, fileTemplate);
-
-        _fakeQnam = new FakeQNAM(fileTemplate);
-        _account = OCC::Account::create();
-        _account->setUrl(QUrl(QStringLiteral("http://admin:admin@localhost/owncloud")));
-        _account->setCredentials(new FakeCredentials{_fakeQnam});
-        _account->setDavDisplayName("fakename");
-        _account->setServerVersion("10.0.0");
-
-        _journalDb = std::make_unique<OCC::SyncJournalDb>(localPath() + ".sync_test.db");
-        _syncEngine = std::make_unique<OCC::SyncEngine>(_account, localPath(), "", _journalDb.get());
-        // Ignore temporary files from the download. (This is in the default exclude list, but we don't load it)
-        _syncEngine->excludedFiles().addManualExclude("]*.~*");
-
-        // handle aboutToRemoveAllFiles with a timeout in case our test does not handle it
-        QObject::connect(_syncEngine.get(), &OCC::SyncEngine::aboutToRemoveAllFiles, _syncEngine.get(), [this](OCC::SyncFileItem::Direction, std::function<void(bool)> callback){
-            QTimer::singleShot(1 * 1000, _syncEngine.get(), [callback]{
-                callback(false);
-            });
-        });
-
-        // Ensure we have a valid VfsOff instance "running"
-        switchToVfs(_syncEngine->syncOptions()._vfs);
+    FakeFolder(const FileInfo &fileTemplate);
 
-        // A new folder will update the local file state database on first sync.
-        // To have a state matching what users will encounter, we have to a sync
-        // using an identical local/remote file tree first.
-        ENFORCE(syncOnce());
-    }
-
-    void switchToVfs(QSharedPointer<OCC::Vfs> vfs)
-    {
-        auto opts = _syncEngine->syncOptions();
-
-        opts._vfs->stop();
-        QObject::disconnect(_syncEngine.get(), 0, opts._vfs.data(), 0);
-
-        opts._vfs = vfs;
-        _syncEngine->setSyncOptions(opts);
-
-        OCC::VfsSetupParams vfsParams;
-        vfsParams.filesystemPath = localPath();
-        vfsParams.remotePath = "/";
-        vfsParams.account = _account;
-        vfsParams.journal = _journalDb.get();
-        vfsParams.providerName = "OC-TEST";
-        vfsParams.providerVersion = "0.1";
-        QObject::connect(_syncEngine.get(), &QObject::destroyed, vfs.data(), [vfs]() {
-            vfs->stop();
-            vfs->unregisterFolder();
-        });
-
-        vfs->start(vfsParams);
-    }
+    void switchToVfs(QSharedPointer<OCC::Vfs> vfs);
 
     OCC::AccountPtr account() const { return _account; }
     OCC::SyncEngine &syncEngine() const { return *_syncEngine; }
@@ -1039,13 +447,7 @@ public:
 
     FileModifier &localModifier() { return _localModifier; }
     FileInfo &remoteModifier() { return _fakeQnam->currentRemoteState(); }
-    FileInfo currentLocalState() {
-        QDir rootDir{_tempDir.path()};
-        FileInfo rootTemplate;
-        fromDisk(rootDir, rootTemplate);
-        rootTemplate.fixupParentPathRecursively();
-        return rootTemplate;
-    }
+    FileInfo currentLocalState();
 
     FileInfo currentRemoteState() { return _fakeQnam->currentRemoteState(); }
     FileInfo &uploadState() { return _fakeQnam->uploadState(); }
@@ -1060,38 +462,13 @@ public:
     ErrorList serverErrorPaths() { return {_fakeQnam}; }
     void setServerOverride(const FakeQNAM::Override &override) { _fakeQnam->setOverride(override); }
 
-    QString localPath() const {
-        // SyncEngine wants a trailing slash
-        if (_tempDir.path().endsWith('/'))
-            return _tempDir.path();
-        return _tempDir.path() + '/';
-    }
+    QString localPath() const;
 
-    void scheduleSync() {
-        // Have to be done async, else, an error before exec() does not terminate the event loop.
-        QMetaObject::invokeMethod(_syncEngine.get(), "startSync", Qt::QueuedConnection);
-    }
+    void scheduleSync();
 
-    void execUntilBeforePropagation() {
-        QSignalSpy spy(_syncEngine.get(), SIGNAL(aboutToPropagate(SyncFileItemVector&)));
-        QVERIFY(spy.wait());
-    }
+    void execUntilBeforePropagation();
 
-    void execUntilItemCompleted(const QString &relativePath) {
-        QSignalSpy spy(_syncEngine.get(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
-        QElapsedTimer t;
-        t.start();
-        while (t.elapsed() < 5000) {
-            spy.clear();
-            QVERIFY(spy.wait());
-            for(const QList<QVariant> &args : spy) {
-                auto item = args[0].value<OCC::SyncFileItemPtr>();
-                if (item->destination() == relativePath)
-                    return;
-            }
-        }
-        QVERIFY(false);
-    }
+    void execUntilItemCompleted(const QString &relativePath);
 
     bool execUntilFinished() {
         QSignalSpy spy(_syncEngine.get(), SIGNAL(finished(bool)));
@@ -1106,80 +483,13 @@ public:
     }
 
 private:
-    static void toDisk(QDir &dir, const FileInfo &templateFi) {
-        foreach (const FileInfo &child, templateFi.children) {
-            if (child.isDir) {
-                QDir subDir(dir);
-                dir.mkdir(child.name);
-                subDir.cd(child.name);
-                toDisk(subDir, child);
-            } else {
-                QFile file{dir.filePath(child.name)};
-                file.open(QFile::WriteOnly);
-                file.write(QByteArray{}.fill(child.contentChar, child.size));
-                file.close();
-                OCC::FileSystem::setModTime(file.fileName(), OCC::Utility::qDateTimeToTime_t(child.lastModified));
-            }
-        }
-    }
+    static void toDisk(QDir &dir, const FileInfo &templateFi);
 
-    static void fromDisk(QDir &dir, FileInfo &templateFi) {
-        foreach (const QFileInfo &diskChild, dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) {
-            if (diskChild.isDir()) {
-                QDir subDir = dir;
-                subDir.cd(diskChild.fileName());
-                FileInfo &subFi = templateFi.children[diskChild.fileName()] = FileInfo{diskChild.fileName()};
-                fromDisk(subDir, subFi);
-            } else {
-                QFile f{diskChild.filePath()};
-                f.open(QFile::ReadOnly);
-                auto content = f.read(1);
-                if (content.size() == 0) {
-                    qWarning() << "Empty file at:" << diskChild.filePath();
-                    continue;
-                }
-                char contentChar = content.at(0);
-                templateFi.children.insert(diskChild.fileName(), FileInfo{diskChild.fileName(), diskChild.size(), contentChar});
-            }
-        }
-    }
+    static void fromDisk(QDir &dir, FileInfo &templateFi);
 };
 
-static FileInfo &findOrCreateDirs(FileInfo &base, PathComponents components)
-{
-    if (components.isEmpty())
-        return base;
-    auto childName = components.pathRoot();
-    auto it = base.children.find(childName);
-    if (it != base.children.end()) {
-        return findOrCreateDirs(*it, components.subComponents());
-    }
-    auto &newDir = base.children[childName] = FileInfo{childName};
-    newDir.parentPath = base.path();
-    return findOrCreateDirs(newDir, components.subComponents());
-}
+static FileInfo &findOrCreateDirs(FileInfo &base, PathComponents components);
 
-inline FileInfo FakeFolder::dbState() const
-{
-    FileInfo result;
-    _journalDb->getFilesBelowPath("", [&](const OCC::SyncJournalFileRecord &record) {
-        auto components = PathComponents(QString::fromUtf8(record._path));
-        auto &parentDir = findOrCreateDirs(result, components.parentDirComponents());
-        auto name = components.fileName();
-        auto &item = parentDir.children[name];
-        item.name = name;
-        item.parentPath = parentDir.path();
-        item.size = record._fileSize;
-        item.isDir = record._type == ItemTypeDirectory;
-        item.permissions = record._remotePerm;
-        item.etag = record._etag;
-        item.lastModified = OCC::Utility::qDateTimeFromTime_t(record._modtime);
-        item.fileId = record._fileId;
-        item.checksums = record._checksumHeader;
-        // item.contentChar can't be set from the db
-    });
-    return result;
-}
 
 /* Return the FileInfo for a conflict file for the specified relative filename */
 inline const FileInfo *findConflict(FileInfo &dir, const QString &filename)
@@ -1202,15 +512,7 @@ struct ItemCompletedSpy : QSignalSpy {
         : QSignalSpy(&folder.syncEngine(), &OCC::SyncEngine::itemCompleted)
     {}
 
-    OCC::SyncFileItemPtr findItem(const QString &path) const
-    {
-        for (const QList<QVariant> &args : *this) {
-            auto item = args[0].value<OCC::SyncFileItemPtr>();
-            if (item->destination() == path)
-                return item;
-        }
-        return OCC::SyncFileItemPtr::create();
-    }
+    OCC::SyncFileItemPtr findItem(const QString &path) const;
 };
 
 // QTest::toString overloads
index 1f90e4b31502bc872b4c304b77c497e5095e6aed..d8de8bce1e5d64e91a22ebb8c900f4aec583414a 100644 (file)
@@ -113,7 +113,7 @@ private slots:
         auto successCallback = [](TestCase *tc, const QNetworkRequest &request) {
             tc->pollRequest = [](TestCase *, const QNetworkRequest &) -> QNetworkReply * { std::abort(); }; // shall no longer be called
             FileInfo *info = tc->perform();
-            QByteArray body = "{ \"status\":\"finished\", \"ETag\":\"\\\"" + info->etag.toUtf8() + "\\\"\", \"fileId\":\"" + info->fileId + "\"}\n";
+            QByteArray body = "{ \"status\":\"finished\", \"ETag\":\"\\\"" + info->etag + "\\\"\", \"fileId\":\"" + info->fileId + "\"}\n";
             return new FakePayloadReply(QNetworkAccessManager::GetOperation, request, body, nullptr);
         };
         // Callback that never finishes
index 0b399b7022c4723e108d3fc3f8c96f5628c120ea..dd54e6df434458cbd70fb65d3aed56f428ec06d4 100644 (file)
@@ -171,7 +171,7 @@ private slots:
             QCOMPARE(counter, 4);
 
             if (remote)
-                QCOMPARE(journalRecord(fakeFolder, "A")._etag, fakeFolder.currentRemoteState().find("A")->etag.toUtf8());
+                QCOMPARE(journalRecord(fakeFolder, "A")._etag, fakeFolder.currentRemoteState().find("A")->etag);
         }
         cleanup();
 
index 8a49bb53859124f94b6b63689d321f3598297d5d..602f2da5a54f1c705d39dac62cfe11cab2cde651 100644 (file)
@@ -275,7 +275,7 @@ private slots:
             QCOMPARE(items[0]->_file, QLatin1String("A"));
             SyncJournalFileRecord record;
             QVERIFY(fakeFolder.syncJournal().getFileRecord(QByteArray("A/a0"), &record));
-            QCOMPARE(record._etag, fakeFolder.remoteModifier().find("A/a0")->etag.toUtf8());
+            QCOMPARE(record._etag, fakeFolder.remoteModifier().find("A/a0")->etag);
         };
         auto connection = connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToPropagate, checkEtagUpdated);
         QVERIFY(fakeFolder.syncOnce());