diff options
author | Hannah von Reth <hannah.vonreth@owncloud.com> | 2021-07-02 11:21:01 +0300 |
---|---|---|
committer | Hannah von Reth <vonreth@kde.org> | 2021-07-13 11:14:00 +0300 |
commit | 3f5163a4f30cf5978ffd7ee363e0d81065eb29c0 (patch) | |
tree | 43be5adbc8934993f79df4fa931d07450dae7dfe /test/testutils | |
parent | f1ee3e18e858cd2816dbf588e35cb7f1441f8511 (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.cpp | 1100 | ||||
-rw-r--r-- | test/testutils/syncenginetestutils.h | 610 | ||||
-rw-r--r-- | test/testutils/testutils.cpp | 59 | ||||
-rw-r--r-- | test/testutils/testutils.h | 14 |
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 ¤tRemoteState() { 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(); +} +} |