Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/owncloud/client.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHannah von Reth <hannah.vonreth@owncloud.com>2021-07-02 11:21:01 +0300
committerHannah von Reth <vonreth@kde.org>2021-07-13 11:14:00 +0300
commit3f5163a4f30cf5978ffd7ee363e0d81065eb29c0 (patch)
tree43be5adbc8934993f79df4fa931d07450dae7dfe /test/testutils
parentf1ee3e18e858cd2816dbf588e35cb7f1441f8511 (diff)
Delay the deletion of Folder objects
This removes the need to check for the existance of the pointers Fixes: #8690
Diffstat (limited to 'test/testutils')
-rw-r--r--test/testutils/syncenginetestutils.cpp1100
-rw-r--r--test/testutils/syncenginetestutils.h610
-rw-r--r--test/testutils/testutils.cpp59
-rw-r--r--test/testutils/testutils.h14
4 files changed, 1783 insertions, 0 deletions
diff --git a/test/testutils/syncenginetestutils.cpp b/test/testutils/syncenginetestutils.cpp
new file mode 100644
index 000000000..c3422b131
--- /dev/null
+++ b/test/testutils/syncenginetestutils.cpp
@@ -0,0 +1,1100 @@
+/*
+ * 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 "testutils/syncenginetestutils.h"
+#include "httplogger.h"
+#include "accessmanager.h"
+#include "libsync/configfile.h"
+
+
+namespace {
+void setupLogger()
+{
+ static QTemporaryDir dir;
+ OCC::ConfigFile::setConfDir(dir.path()); // we don't want to pollute the user's config file
+
+ OCC::Logger::instance()->setLogFile(QStringLiteral("-"));
+ OCC::Logger::instance()->addLogRule({ QStringLiteral("sync.httplogger=true") });
+}
+Q_COREAPP_STARTUP_FUNCTION(setupLogger);
+}
+
+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, char contentChar)
+{
+ QFile file { _rootDir.filePath(relativePath) };
+ QVERIFY(file.exists());
+ file.open(QFile::ReadWrite);
+ QByteArray contents;
+ if (contentChar)
+ contents += contentChar;
+ else
+ contents = file.read(1);
+ file.seek(file.size());
+ file.write(contents);
+}
+
+void DiskFileModifier::modifyByte(const QString &relativePath, quint64 offset, char contentChar)
+{
+ QFile file { _rootDir.filePath(relativePath) };
+ QVERIFY(file.exists());
+ file.open(QFile::ReadWrite);
+ file.seek(offset);
+ file.write(&contentChar, 1);
+ file.close();
+}
+
+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, char contentChar)
+{
+ Q_UNUSED(contentChar);
+ FileInfo *file = findInvalidatingEtags(relativePath);
+ Q_ASSERT(file);
+ file->size += 1;
+}
+
+void FileInfo::modifyByte(const QString &relativePath, quint64 offset, char contentChar)
+{
+ Q_UNUSED(offset);
+ Q_UNUSED(contentChar);
+ FileInfo *file = findInvalidatingEtags(relativePath);
+ Q_ASSERT(file);
+ Q_ASSERT(!"unimplemented");
+}
+
+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;
+}
+
+QString FileInfo::absolutePath() const
+{
+ if (parentPath.endsWith(QLatin1Char('/'))) {
+ return parentPath + name;
+ } else {
+ return 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)
+ : FakeReply { 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;
+ }
+ const 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"));
+ const auto href = OCC::Utility::concatUrlPath(prefix, QString::fromUtf8(QUrl::toPercentEncoding(fileInfo.absolutePath(), "/"))).path();
+ xml.writeTextElement(davUri, QStringLiteral("href"), href);
+ 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);
+
+ const int depth = request.rawHeader(QByteArrayLiteral("Depth")).toInt();
+ if (depth > 0) {
+ for (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)
+ : FakeReply { 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)
+ : FakeReply { 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)
+ : FakeReply { 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)
+ : FakeReply { 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)
+ : FakeReply { 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) {
+ qDebug() << "meh;";
+ }
+ Q_ASSERT_X(fileInfo, Q_FUNC_INFO, "Could not find file on the remote");
+ QMetaObject::invokeMethod(this, &FakeGetReply::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)
+ : FakeReply { 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)
+ : FakeReply { 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;
+ qlonglong prev = 0;
+ char payload = '\0';
+
+ QString fileName = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination")));
+ Q_ASSERT(!fileName.isEmpty());
+
+ const auto &sourceFolderChildren = sourceFolder->children;
+ // Compute the size and content from the chunks if possible
+ for (auto it = sourceFolderChildren.cbegin(); it != sourceFolderChildren.cend(); ++it) {
+ const auto &chunkNameLongLong = it.key().toLongLong();
+ const auto &x = it.value();
+ if (chunkNameLongLong != prev)
+ break;
+ 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;
+ prev = chunkNameLongLong + x.size;
+ }
+ Q_ASSERT(sourceFolderChildren.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)
+ : FakeReply { 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)
+ : FakeReply { 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, &FakeErrorReply::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();
+}
+
+FakeHangingReply::FakeHangingReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
+ : FakeReply(parent)
+{
+ setRequest(request);
+ setUrl(request.url());
+ setOperation(op);
+ open(QIODevice::ReadOnly);
+}
+
+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)
+{
+ QNetworkReply *reply = nullptr;
+ auto newRequest = request;
+ newRequest.setRawHeader("X-Request-ID", OCC::AccessManager::generateRequestId());
+ if (_override) {
+ if (auto _reply = _override(op, newRequest, outgoingData)) {
+ reply = _reply;
+ }
+ }
+ if (!reply) {
+ const QString fileName = getFilePathFromUrl(newRequest.url());
+ Q_ASSERT(!fileName.isNull());
+ if (_errorPaths.contains(fileName)) {
+ reply = new FakeErrorReply { op, newRequest, this, _errorPaths[fileName] };
+ }
+ }
+ if (!reply) {
+ const bool isUpload = newRequest.url().path().startsWith(sUploadUrl.path());
+ FileInfo &info = isUpload ? _uploadFileInfo : _remoteRootFileInfo;
+
+ auto verb = newRequest.attribute(QNetworkRequest::CustomVerbAttribute);
+ if (verb == QLatin1String("PROPFIND"))
+ // Ignore outgoingData always returning somethign good enough, works for now.
+ reply = new FakePropfindReply { info, op, newRequest, this };
+ else if (verb == QLatin1String("GET") || op == QNetworkAccessManager::GetOperation)
+ reply = new FakeGetReply { info, op, newRequest, this };
+ else if (verb == QLatin1String("PUT") || op == QNetworkAccessManager::PutOperation)
+ reply = new FakePutReply { info, op, newRequest, outgoingData->readAll(), this };
+ else if (verb == QLatin1String("MKCOL"))
+ reply = new FakeMkcolReply { info, op, newRequest, this };
+ else if (verb == QLatin1String("DELETE") || op == QNetworkAccessManager::DeleteOperation)
+ reply = new FakeDeleteReply { info, op, newRequest, this };
+ else if (verb == QLatin1String("MOVE") && !isUpload)
+ reply = new FakeMoveReply { info, op, newRequest, this };
+ else if (verb == QLatin1String("MOVE") && isUpload)
+ reply = new FakeChunkMoveReply { info, _remoteRootFileInfo, op, newRequest, this };
+ else {
+ qDebug() << verb << outgoingData;
+ Q_UNREACHABLE();
+ }
+ }
+ OCC::HttpLogger::logRequest(reply, op, outgoingData);
+ return reply;
+}
+
+FakeFolder::FakeFolder(const FileInfo &fileTemplate)
+ : _localModifier(_tempDir.path())
+{
+ // Needs to be done once
+ OCC::SyncEngine::minimumFileAgeForUpload = std::chrono::milliseconds(0);
+
+ 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.
+ OC_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)
+{
+ for (const auto &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)
+{
+ const auto infoList = dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot);
+ for (const auto &diskChild : infoList) {
+ 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();
+}
+
+FakeReply::FakeReply(QObject *parent)
+ : QNetworkReply(parent)
+{
+ setRawHeader(QByteArrayLiteral("Date"), QDateTime::currentDateTimeUtc().toString(Qt::RFC2822Date).toUtf8());
+}
+
+FakeReply::~FakeReply()
+{
+}
diff --git a/test/testutils/syncenginetestutils.h b/test/testutils/syncenginetestutils.h
new file mode 100644
index 000000000..98c0759ec
--- /dev/null
+++ b/test/testutils/syncenginetestutils.h
@@ -0,0 +1,610 @@
+/*
+ * 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.
+ *
+ */
+#pragma once
+
+#include "account.h"
+#include "creds/abstractcredentials.h"
+#include "logger.h"
+#include "filesystem.h"
+#include "folder.h"
+#include "syncengine.h"
+#include "common/syncjournaldb.h"
+#include "common/syncjournalfilerecord.h"
+#include "common/vfs.h"
+#include "csync_exclude.h"
+#include <cstring>
+
+#include <QDir>
+#include <QNetworkReply>
+#include <QMap>
+#include <QtTest>
+#include <cookiejar.h>
+#include <QTimer>
+
+/*
+ * TODO: In theory we should use QVERIFY instead of Q_ASSERT for testing, but this
+ * only works when directly called from a QTest :-(
+ */
+
+
+static const QUrl sRootUrl = QUrl::fromEncoded("owncloud://somehost/owncloud/remote.php/webdav/");
+static const QUrl sRootUrl2 = QUrl::fromEncoded("owncloud://somehost/owncloud/remote.php/dav/files/admin/");
+static const QUrl sUploadUrl = QUrl::fromEncoded("owncloud://somehost/owncloud/remote.php/dav/uploads/admin/");
+
+inline QString getFilePathFromUrl(const QUrl &url)
+{
+ QString path = url.path();
+ if (path.startsWith(sRootUrl.path()))
+ return path.mid(sRootUrl.path().length());
+ if (path.startsWith(sRootUrl2.path()))
+ return path.mid(sRootUrl2.path().length());
+ if (path.startsWith(sUploadUrl.path()))
+ return path.mid(sUploadUrl.path().length());
+ return {};
+}
+
+
+inline QByteArray generateEtag()
+{
+ return QByteArray::number(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch(), 16) + QByteArray::number(qrand(), 16);
+}
+inline QByteArray generateFileId()
+{
+ return QByteArray::number(qrand(), 16);
+}
+
+class PathComponents : public QStringList
+{
+public:
+ PathComponents(const char *path);
+ PathComponents(const QString &path);
+ PathComponents(const QStringList &pathComponents);
+
+ PathComponents parentDirComponents() const;
+ PathComponents subComponents() const &;
+ PathComponents subComponents() &&
+ {
+ removeFirst();
+ return std::move(*this);
+ }
+ QString pathRoot() const { return first(); }
+ QString fileName() const { return last(); }
+};
+
+class FileModifier
+{
+public:
+ virtual ~FileModifier() { }
+ virtual void remove(const QString &relativePath) = 0;
+ virtual void insert(const QString &relativePath, qint64 size = 64, char contentChar = 'W') = 0;
+ virtual void setContents(const QString &relativePath, char contentChar) = 0;
+ virtual void appendByte(const QString &relativePath, char contentChar = 0) = 0;
+ virtual void modifyByte(const QString &relativePath, quint64 offset, char contentChar) = 0;
+ virtual void mkdir(const QString &relativePath) = 0;
+ virtual void rename(const QString &relativePath, const QString &relativeDestinationDirectory) = 0;
+ virtual void setModTime(const QString &relativePath, const QDateTime &modTime) = 0;
+};
+
+class DiskFileModifier : public FileModifier
+{
+ QDir _rootDir;
+
+public:
+ DiskFileModifier(const QString &rootDirPath)
+ : _rootDir(rootDirPath)
+ {
+ }
+ 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, char contentChar) override;
+ void modifyByte(const QString &relativePath, quint64 offset, char contentChar) 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() = 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);
+
+ void addChild(const FileInfo &info);
+
+ 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, char contentChar = 0) override;
+
+ void modifyByte(const QString &relativePath, quint64 offset, char contentChar) override;
+
+ void mkdir(const QString &relativePath) override;
+
+ void rename(const QString &oldPath, const QString &newPath) override;
+
+ void setModTime(const QString &relativePath, const QDateTime &modTime) override;
+
+ FileInfo *find(PathComponents pathComponents, const bool invalidateEtags = false);
+
+ FileInfo *createDir(const QString &relativePath);
+
+ 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;
+
+ bool operator!=(const FileInfo &other) const
+ {
+ return !operator==(other);
+ }
+
+ QString path() const;
+ QString absolutePath() const;
+
+ void fixupParentPathRecursively();
+
+ QString name;
+ bool isDir = true;
+ bool isShared = false;
+ OCC::RemotePermissions permissions; // When uset, defaults to everything
+ QDateTime lastModified = QDateTime::currentDateTimeUtc().addDays(-7);
+ QByteArray etag = generateEtag();
+ QByteArray fileId = generateFileId();
+ QByteArray checksums;
+ QByteArray extraDavProperties;
+ qint64 size = 0;
+ char contentChar = 'W';
+
+ // Sorted by name to be able to compare trees
+ QMap<QString, FileInfo> children;
+ QString parentPath;
+
+ FileInfo *findInvalidatingEtags(PathComponents pathComponents);
+
+ friend inline QDebug operator<<(QDebug dbg, const FileInfo &fi)
+ {
+ return dbg << "{ " << fi.path() << ": " << fi.children;
+ }
+};
+
+class FakeReply : public QNetworkReply
+{
+ Q_OBJECT
+public:
+ FakeReply(QObject *parent);
+ virtual ~FakeReply();
+
+ // useful to be public for testing
+ using QNetworkReply::setRawHeader;
+};
+
+class FakePropfindReply : public FakeReply
+{
+ Q_OBJECT
+public:
+ QByteArray payload;
+
+ FakePropfindReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent);
+
+ Q_INVOKABLE void respond();
+
+ Q_INVOKABLE void respond404();
+
+ void abort() override { }
+
+ qint64 bytesAvailable() const override;
+ qint64 readData(char *data, qint64 maxlen) override;
+};
+
+class FakePutReply : public FakeReply
+{
+ Q_OBJECT
+ FileInfo *fileInfo;
+
+public:
+ 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);
+
+ Q_INVOKABLE virtual void respond();
+
+ void abort() override;
+ qint64 readData(char *, qint64) override { return 0; }
+};
+
+class FakeMkcolReply : public FakeReply
+{
+ Q_OBJECT
+ FileInfo *fileInfo;
+
+public:
+ FakeMkcolReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent);
+
+ Q_INVOKABLE void respond();
+
+ void abort() override { }
+ qint64 readData(char *, qint64) override { return 0; }
+};
+
+class FakeDeleteReply : public FakeReply
+{
+ Q_OBJECT
+public:
+ FakeDeleteReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent);
+
+ Q_INVOKABLE void respond();
+
+ void abort() override { }
+ qint64 readData(char *, qint64) override { return 0; }
+};
+
+class FakeMoveReply : public FakeReply
+{
+ Q_OBJECT
+public:
+ FakeMoveReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent);
+
+ Q_INVOKABLE void respond();
+
+ void abort() override { }
+ qint64 readData(char *, qint64) override { return 0; }
+};
+
+class FakeGetReply : public FakeReply
+{
+ Q_OBJECT
+public:
+ const FileInfo *fileInfo;
+ char payload;
+ int size;
+ bool aborted = false;
+
+ FakeGetReply(FileInfo &remoteRootFileInfo, 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;
+};
+
+class FakeGetWithDataReply : public FakeReply
+{
+ 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;
+};
+
+class FakeChunkMoveReply : public FakeReply
+{
+ Q_OBJECT
+ FileInfo *fileInfo;
+
+public:
+ FakeChunkMoveReply(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo,
+ QNetworkAccessManager::Operation op, const QNetworkRequest &request,
+ QObject *parent);
+
+ static FileInfo *perform(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo, const QNetworkRequest &request);
+
+ Q_INVOKABLE virtual void respond();
+
+ Q_INVOKABLE void respondPreconditionFailed();
+
+ void abort() override;
+
+ qint64 readData(char *, qint64) override { return 0; }
+};
+
+class FakePayloadReply : public FakeReply
+{
+ Q_OBJECT
+public:
+ FakePayloadReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request,
+ const QByteArray &body, QObject *parent);
+
+ void respond();
+
+ void abort() override { }
+ qint64 readData(char *buf, qint64 max) override;
+ qint64 bytesAvailable() const override;
+ QByteArray _body;
+};
+
+
+class FakeErrorReply : public FakeReply
+{
+ Q_OBJECT
+public:
+ FakeErrorReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request,
+ QObject *parent, int httpErrorCode, const QByteArray &body = QByteArray());
+
+ Q_INVOKABLE virtual void respond();
+
+ // make public to give tests easy interface
+ using QNetworkReply::setAttribute;
+ using QNetworkReply::setError;
+
+public slots:
+ void slotSetFinished();
+
+public:
+ void abort() override { }
+ qint64 readData(char *buf, qint64 max) override;
+ qint64 bytesAvailable() const override;
+
+ QByteArray _body;
+};
+
+// A reply that never responds
+class FakeHangingReply : public FakeReply
+{
+ Q_OBJECT
+public:
+ FakeHangingReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent);
+
+ void abort() override;
+ qint64 readData(char *, qint64) override { return 0; }
+};
+
+// A delayed reply
+template <class OriginalReply>
+class DelayedReply : public OriginalReply
+{
+public:
+ template <typename... Args>
+ explicit DelayedReply(quint64 delayMS, Args &&...args)
+ : OriginalReply(std::forward<Args>(args)...)
+ , _delayMs(delayMS)
+ {
+ }
+ quint64 _delayMs;
+
+ void respond() override
+ {
+ QTimer::singleShot(_delayMs, static_cast<OriginalReply *>(this), [this] {
+ // Explicit call to bases's respond();
+ this->OriginalReply::respond();
+ });
+ }
+};
+
+class FakeQNAM : public QNetworkAccessManager
+{
+public:
+ using Override = std::function<QNetworkReply *(Operation, const QNetworkRequest &, QIODevice *)>;
+
+private:
+ FileInfo _remoteRootFileInfo;
+ FileInfo _uploadFileInfo;
+ // maps a path to an HTTP error
+ QHash<QString, int> _errorPaths;
+ // monitor requests and optionally provide custom replies
+ Override _override;
+
+public:
+ FakeQNAM(FileInfo initialRoot);
+ FileInfo &currentRemoteState() { return _remoteRootFileInfo; }
+ FileInfo &uploadState() { return _uploadFileInfo; }
+
+ QHash<QString, int> &errorPaths() { return _errorPaths; }
+
+ void setOverride(const Override &override) { _override = override; }
+
+protected:
+ QNetworkReply *createRequest(Operation op, const QNetworkRequest &request,
+ QIODevice *outgoingData = nullptr) override;
+};
+
+class FakeCredentials : public OCC::AbstractCredentials
+{
+ QNetworkAccessManager *_qnam;
+
+public:
+ FakeCredentials(QNetworkAccessManager *qnam)
+ : _qnam { qnam }
+ {
+ }
+ QString authType() const override { return QStringLiteral("test"); }
+ QString user() const override { return QStringLiteral("admin"); }
+ QNetworkAccessManager *createQNAM() const override { return _qnam; }
+ bool ready() const override { return true; }
+ void fetchFromKeychain() override { }
+ void askFromUser() override { }
+ bool stillValid(QNetworkReply *) override { return true; }
+ void persist() override { }
+ void invalidateToken() override { }
+ void forgetSensitiveData() override { }
+};
+
+class FakeFolder
+{
+ QTemporaryDir _tempDir;
+ DiskFileModifier _localModifier;
+ // FIXME: Clarify ownership, double delete
+ FakeQNAM *_fakeQnam;
+ OCC::AccountPtr _account;
+ std::unique_ptr<OCC::SyncJournalDb> _journalDb;
+ std::unique_ptr<OCC::SyncEngine> _syncEngine;
+
+public:
+ FakeFolder(const FileInfo &fileTemplate);
+
+ void switchToVfs(QSharedPointer<OCC::Vfs> vfs);
+
+ OCC::AccountPtr account() const { return _account; }
+ OCC::SyncEngine &syncEngine() const { return *_syncEngine; }
+ OCC::SyncJournalDb &syncJournal() const { return *_journalDb; }
+
+ FileModifier &localModifier() { return _localModifier; }
+ FileInfo &remoteModifier() { return _fakeQnam->currentRemoteState(); }
+ FileInfo currentLocalState();
+
+ FileInfo currentRemoteState() { return _fakeQnam->currentRemoteState(); }
+ FileInfo &uploadState() { return _fakeQnam->uploadState(); }
+ FileInfo dbState() const;
+
+ struct ErrorList
+ {
+ FakeQNAM *_qnam;
+ void append(const QString &path, int error = 500)
+ {
+ _qnam->errorPaths().insert(path, error);
+ }
+ void clear() { _qnam->errorPaths().clear(); }
+ };
+ ErrorList serverErrorPaths() { return { _fakeQnam }; }
+ void setServerOverride(const FakeQNAM::Override &override) { _fakeQnam->setOverride(override); }
+
+ QString localPath() const;
+
+ void scheduleSync();
+
+ void execUntilBeforePropagation();
+
+ void execUntilItemCompleted(const QString &relativePath);
+
+ bool execUntilFinished()
+ {
+ QSignalSpy spy(_syncEngine.get(), SIGNAL(finished(bool)));
+ bool ok = spy.wait(3600000);
+ Q_ASSERT(ok && "Sync timed out");
+ return spy[0][0].toBool();
+ }
+
+ bool syncOnce()
+ {
+ scheduleSync();
+ return execUntilFinished();
+ }
+
+private:
+ static void toDisk(QDir &dir, const FileInfo &templateFi);
+
+ static void fromDisk(QDir &dir, FileInfo &templateFi);
+};
+
+
+/* Return the FileInfo for a conflict file for the specified relative filename */
+inline const FileInfo *findConflict(FileInfo &dir, const QString &filename)
+{
+ QFileInfo info(filename);
+ const FileInfo *parentDir = dir.find(info.path());
+ if (!parentDir)
+ return nullptr;
+ QString start = info.baseName() + QStringLiteral(" (conflicted copy");
+ for (const auto &item : parentDir->children) {
+ if (item.name.startsWith(start)) {
+ return &item;
+ }
+ }
+ return nullptr;
+}
+
+struct ItemCompletedSpy : QSignalSpy
+{
+ explicit ItemCompletedSpy(FakeFolder &folder)
+ : QSignalSpy(&folder.syncEngine(), &OCC::SyncEngine::itemCompleted)
+ {
+ }
+
+ OCC::SyncFileItemPtr findItem(const QString &path) const;
+};
+
+// QTest::toString overloads
+namespace OCC {
+inline char *toString(const SyncFileStatus &s)
+{
+ return QTest::toString(QStringLiteral("SyncFileStatus(%1)").arg(s.toSocketAPIString()));
+}
+}
+
+inline void addFiles(QStringList &dest, const FileInfo &fi)
+{
+ if (fi.isDir) {
+ dest += QStringLiteral("%1 - dir").arg(fi.path());
+ for (const auto &fi : fi.children)
+ addFiles(dest, fi);
+ } else {
+ dest += QStringLiteral("%1 - %2 %3-bytes").arg(fi.path()).arg(fi.size).arg(fi.contentChar);
+ }
+}
+
+inline QString toStringNoElide(const FileInfo &fi)
+{
+ QStringList files;
+ for (const auto &fi : fi.children)
+ addFiles(files, fi);
+ files.sort();
+ return QStringLiteral("FileInfo with %1 files(\n\t%2\n)").arg(files.size()).arg(files.join(QStringLiteral("\n\t")));
+}
+
+inline char *toString(const FileInfo &fi)
+{
+ return QTest::toString(toStringNoElide(fi));
+}
+
+inline void addFilesDbData(QStringList &dest, const FileInfo &fi)
+{
+ // could include etag, permissions etc, but would need extra work
+ if (fi.isDir) {
+ dest += QStringLiteral("%1 - %2 %3 %4").arg(fi.name, fi.isDir ? QStringLiteral("dir") : QStringLiteral("file"), QString::number(fi.lastModified.toSecsSinceEpoch()), QString::fromUtf8(fi.fileId));
+ for (const auto &fi : fi.children)
+ addFilesDbData(dest, fi);
+ } else {
+ dest += QStringLiteral("%1 - %2 %3 %4 %5").arg(fi.name, fi.isDir ? QStringLiteral("dir") : QStringLiteral("file"), QString::number(fi.size), QString::number(fi.lastModified.toSecsSinceEpoch()), QString::fromUtf8(fi.fileId));
+ }
+}
+
+inline char *printDbData(const FileInfo &fi)
+{
+ QStringList files;
+ for (const auto &fi : fi.children)
+ addFilesDbData(files, fi);
+ return QTest::toString(QStringLiteral("FileInfo with %1 files(%2)").arg(files.size()).arg(files.join(QStringLiteral(", "))));
+}
diff --git a/test/testutils/testutils.cpp b/test/testutils/testutils.cpp
new file mode 100644
index 000000000..281323073
--- /dev/null
+++ b/test/testutils/testutils.cpp
@@ -0,0 +1,59 @@
+#include "testutils.h"
+
+#include "creds/httpcredentials.h"
+#include "gui/accountmanager.h"
+
+#include <QCoreApplication>
+
+namespace {
+class HttpCredentialsTest : public OCC::HttpCredentials
+{
+public:
+ HttpCredentialsTest(const QString &user, const QString &password)
+ : HttpCredentials(OCC::DetermineAuthTypeJob::AuthType::Basic, user, password)
+ {
+ }
+
+ void askFromUser() override
+ {
+ }
+};
+}
+
+namespace OCC {
+
+namespace TestUtils {
+ AccountPtr createDummyAccount()
+ {
+ // don't use the account manager to create the account, it would try to use widgets
+ auto acc = Account::create();
+ HttpCredentialsTest *cred = new HttpCredentialsTest("testuser", "secret");
+ acc->setCredentials(cred);
+ acc->setUrl(QUrl(QStringLiteral("http://localhost/owncloud")));
+ acc->setDavDisplayName(QStringLiteral("fakename") + acc->uuid().toString());
+ acc->setServerVersion(QStringLiteral("10.0.0"));
+ OCC::AccountManager::instance()->addAccount(acc);
+ return acc;
+ }
+
+ FolderDefinition createDummyFolderDefinition(const QString &path)
+ {
+ OCC::FolderDefinition d;
+ d.localPath = path;
+ d.targetPath = path;
+ d.alias = path;
+ return d;
+ }
+
+ FolderMan *folderMan()
+ {
+ static FolderMan *man = [] {
+ auto man = new FolderMan;
+ QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, man, &FolderMan::deleteLater);
+ return man;
+ }();
+ return man;
+ }
+
+}
+}
diff --git a/test/testutils/testutils.h b/test/testutils/testutils.h
new file mode 100644
index 000000000..c9e8c64e5
--- /dev/null
+++ b/test/testutils/testutils.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "account.h"
+#include "folder.h"
+#include "folderman.h"
+
+namespace OCC {
+
+namespace TestUtils {
+ FolderMan *folderMan();
+ FolderDefinition createDummyFolderDefinition(const QString &path);
+ AccountPtr createDummyAccount();
+}
+}