/* * 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 "common/filesystembase.h" #include "common/syncjournaldb.h" #include "creds/abstractcredentials.h" #include "folder.h" #include "libsync/vfs/vfs.h" #include "syncengine.h" #include "testutils.h" #include #include #include #include #include #include using namespace OCC::FileSystem::SizeLiterals; /* * TODO: In theory we should use QVERIFY instead of Q_ASSERT for testing, but this * only works when directly called from a QTest :-( */ QString getFilePathFromUrl(const QUrl &url); inline QString generateEtag() { return QString::number(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch(), 16) + QString::number(QRandomGenerator::global()->generate(), 16); } inline QByteArray generateFileId() { return QByteArray::number(QRandomGenerator::global()->generate(), 16); } class PathComponents : public QStringList { public: 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(); } }; /** * @brief The FileModifier class defines the interface for both the local on-disk modifier and the * remote in-memory modifier. */ class FileModifier { public: static constexpr auto DefaultFileSize = 64_B; static constexpr char DefaultContentChar = 'X'; virtual ~FileModifier() { } virtual void remove(const QString &relativePath) = 0; virtual void insert(const QString &relativePath, quint64 size = DefaultFileSize, char contentChar = DefaultContentChar) = 0; virtual void setContents(const QString &relativePath, quint64 newSize, char contentChar = DefaultContentChar) = 0; // prevent implicit cast to quint64 template void setContents(const QString &relativePath, T newSize, char contentChar = DefaultContentChar) = delete; // prevent implicit cast to quint64 template void insert(const QString &relativePath, T size, char contentChar = DefaultContentChar) = delete; virtual void appendByte(const QString &relativePath, char contentChar = DefaultContentChar) = 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 FakeFolder; /** * @brief The DiskFileModifier class is a FileModifier for files on the local disk. * * It uses a helper program (`test_helper`) to do the actual modifications. When running without a * VFS, or when running with suffix-VFS, the changes are done before doing a sync. When running * with a VFS, the OS will do callbacks to the test program. So in this case the modifications are * done while the event loop is running, in order to process those OS callbacks. * * Note: these actions do NOT read content from files on disk: if the file would be dehydrated, * reading will cause a re-hydration, which means network requests, and some tests explicitly check * for the amount of requests. */ class DiskFileModifier : public FileModifier { QDir _rootDir; QStringList _processArguments; public: DiskFileModifier(const QString &rootDirPath) : _rootDir(rootDirPath) { } void remove(const QString &relativePath) override; void insert(const QString &relativePath, quint64 size = DefaultFileSize, char contentChar = DefaultContentChar) override; void setContents(const QString &relativePath, quint64 newSize, char contentChar = DefaultContentChar) override; void appendByte(const QString &relativePath, char contentChar = DefaultContentChar) 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; Q_REQUIRED_RESULT bool applyModifications(); // prevent implicit cast to quint64 template void setContents(const QString &relativePath, T newSize, char contentChar = DefaultContentChar) = delete; // prevent implicit cast to quint64 template void insert(const QString &relativePath, T size, char contentChar = DefaultContentChar) = delete; }; static inline qint64 defaultLastModified() { auto precise = QDateTime::currentDateTimeUtc().addDays(-7); time_t timeInSeconds = OCC::Utility::qDateTimeToTime_t(precise); return timeInSeconds; } /** * @brief The FileInfo class represents the remotely stored (on-server) content. To be able to * modify the content, this class is also the remote \c FileModifier. */ class FileInfo : public FileModifier { /// FIXME: we should make it explicit in the construtor if we're talking about a hydrated or a dehydrated file! /// FIXME: this class is both a remote folder, as the root folder which in turn is the remote FileModifier. /// This is a mess: unifying remote files/folders is ok, but because it's also the remote root (which /// should implement the FileModifier), this means that each single remote file/folder also implements /// this interface. public: static FileInfo A12_B12_C12_S12(); FileInfo() = default; FileInfo(const QString &name) : name { name } { } FileInfo(const QString &name, quint64 size) : name { name } , isDir { false } , fileSize(size) , contentSize { size } { } FileInfo(const QString &name, quint64 size, char contentChar) : name { name } , isDir { false } , fileSize(size) , contentSize { size } , contentChar { contentChar } { } FileInfo(const QString &name, const std::initializer_list &children); FileInfo &addChild(const FileInfo &info); void remove(const QString &relativePath) override; void insert(const QString &relativePath, quint64 size = DefaultFileSize, char contentChar = DefaultContentChar) override; void setContents(const QString &relativePath, quint64 newSize, char contentChar = DefaultContentChar) override; void appendByte(const QString &relativePath, char contentChar = DefaultContentChar) 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; /// Return a pointer to the FileInfo, or a nullptr if it doesn't exist FileInfo *find(PathComponents pathComponents, const bool invalidateEtags = false); FileInfo *find(const QString &pathComponents, const bool invalidateEtags = false) { return find(PathComponents{pathComponents}, invalidateEtags); } FileInfo *createDir(const QString &relativePath); FileInfo *create(const QString &relativePath, quint64 size, char contentChar); bool operator<(const FileInfo &other) const { return name < other.name; } enum CompareWhat { CompareLastModified, IgnoreLastModified, }; bool operator==(const FileInfo &other) const { return equals(other, CompareLastModified); } bool equals(const FileInfo &other, CompareWhat compareWhat) const; bool operator!=(const FileInfo &other) const { return !operator==(other); } QDateTime lastModified() const { return QDateTime::fromSecsSinceEpoch(_lastModifiedInSecondsUTC, QTimeZone::LocalTime); } QDateTime lastModifiedInUtc() const { return QDateTime::fromSecsSinceEpoch(_lastModifiedInSecondsUTC, QTimeZone::UTC); } void setLastModified(const QDateTime &t) { _lastModifiedInSecondsUTC = t.toSecsSinceEpoch(); } qint64 lastModifiedInSecondsUTC() const { return _lastModifiedInSecondsUTC; } void setLastModifiedFromSecondsUTC(qint64 utcSecs) { _lastModifiedInSecondsUTC = utcSecs; } QString path() const; QString absolutePath() const; void fixupParentPathRecursively(); QString name; bool isDir = true; bool isShared = false; OCC::RemotePermissions permissions; // When uset, defaults to everything qint64 _lastModifiedInSecondsUTC = defaultLastModified(); QString etag = generateEtag(); QByteArray fileId = generateFileId(); QByteArray checksums = OCC::CheckSums::toString(OCC::CheckSums::Algorithm::DUMMY_FOR_TESTS).data() + QByteArrayLiteral(":0x1"); QByteArray extraDavProperties; quint64 fileSize = 0; quint64 contentSize = 0; char contentChar = 'W'; bool isDehydratedPlaceholder = false; // Sorted by name to be able to compare trees QMap children; QString parentPath; FileInfo *findInvalidatingEtags(PathComponents pathComponents); friend inline QDebug operator<<(QDebug dbg, const FileInfo &fi) { return dbg.nospace().noquote() << "{ '" << fi.path() << "': " << ", isDir:" << fi.isDir << QStringLiteral(", lastModified: %1 (%2)").arg(QString::number(fi._lastModifiedInSecondsUTC), fi.lastModifiedInUtc().toString()) << ", fileSize:" << fi.fileSize << ", contentSize:" << fi.contentSize << QStringLiteral(", contentChar: 0x%1").arg(QString::number(int(fi.contentChar), 16)) << ", isDehydratedPlaceholder:" << fi.isDehydratedPlaceholder << ", children:" << fi.children << " }"; } // prevent implicit cast to quint64 template void setContents(const QString &relativePath, T newSize, char contentChar = DefaultContentChar) = delete; // prevent implicit cast to quint64 template void insert(const QString &relativePath, T size, char contentChar = DefaultContentChar) = delete; }; class FakeReply : public QNetworkReply { Q_OBJECT public: FakeReply(QObject *parent); virtual ~FakeReply() override; // useful to be public for testing using QNetworkReply::setAttribute; using QNetworkReply::setRawHeader; void checkedFinished(); virtual void abort() override; }; 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(); qint64 bytesAvailable() const override; qint64 readData(char *data, qint64 maxlen) override; }; class FakePutReply : public FakeReply { Q_OBJECT 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(); qint64 readData(char *, qint64) override { return 0; } private: FileInfo *fileInfo; }; class FakeMkcolReply : public FakeReply { Q_OBJECT public: FakeMkcolReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent); Q_INVOKABLE void respond(); qint64 readData(char *, qint64) override { return 0; } private: FileInfo *fileInfo; }; class FakeDeleteReply : public FakeReply { Q_OBJECT public: FakeDeleteReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent); Q_INVOKABLE void respond(); 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(); qint64 readData(char *, qint64) override { return 0; } }; class FakeGetReply : public FakeReply { Q_OBJECT public: enum class State { Ok, Aborted, FileNotFound, }; Q_ENUM(State); const FileInfo *fileInfo; char payload; qint64 size; State state = State::Ok; const std::pair _range; FakeGetReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent); virtual void respond(); void abort() override; virtual qint64 bytesAvailable() const override; virtual qint64 readData(char *data, qint64 maxlen) override; static std::pair parseRange(const QNetworkRequest &request); }; class FakePayloadReply : public FakeReply { Q_OBJECT public: FakePayloadReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &body, QObject *parent); virtual void respond(); 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 Q_SLOTS: void slotSetFinished(); public: 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); qint64 readData(char *, qint64) override { return 0; } }; // A delayed reply template class DelayedReply : public OriginalReply { public: template explicit DelayedReply(const std::chrono::milliseconds delayMS, Args &&...args) : OriginalReply(std::forward(args)...) , _delayMs(delayMS) { } std::chrono::milliseconds _delayMs; void respond() override { QTimer::singleShot(_delayMs, static_cast(this), [this] { // Explicit call to bases's respond(); this->OriginalReply::respond(); }); } }; class FakeAM : public OCC::AccessManager { public: using Override = std::function; private: FileInfo _remoteRootFileInfo; // maps a path to an HTTP error QHash _errorPaths; // monitor requests and optionally provide custom replies Override _override; public: FakeAM(FileInfo initialRoot, QObject *parent); FileInfo ¤tRemoteState() { return _remoteRootFileInfo; } QHash &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 { public: FakeCredentials(OCC::AccessManager *am) : _am { am } { } OCC::AccessManager *createAM() const override { return _am; } bool ready() const override { return true; } void fetchFromKeychain() override { } void restartOauth() override { } void persist() override { } void invalidateToken() override { } void forgetSensitiveData() override { } private: OCC::AccessManager *_am; }; class FakeFolder : public QObject { Q_OBJECT const QTemporaryDir _tempDir = OCC::TestUtils::createTempDir(); DiskFileModifier _localModifier; FakeAM *_fakeAm; OCC::TestUtils::TestUtilsPrivate::AccountStateRaii _accountState = OCC::TestUtils::TestUtilsPrivate::AccountStateRaii{nullptr, &OCC::TestUtils::TestUtilsPrivate::accountStateDeleter}; std::unique_ptr _journalDb; std::unique_ptr _syncEngine; public: FakeFolder(const FileInfo &fileTemplate, OCC::Vfs::Mode vfsMode = OCC::Vfs::Mode::Off, bool filesAreDehydrated = false); ~FakeFolder(); void switchToVfs(QSharedPointer vfs); OCC::AccountPtr account() const { return _accountState->account(); } OCC::SyncEngine &syncEngine() const { return *_syncEngine; } OCC::SyncJournalDb &syncJournal() const { return *_journalDb; } FileModifier &localModifier() { return _localModifier; } FileInfo &remoteModifier() { return _fakeAm->currentRemoteState(); } FileInfo currentLocalState(); FileInfo ¤tRemoteState() { return _fakeAm->currentRemoteState(); } FileInfo dbState() const; struct ErrorList { FakeAM *_qnam; void append(const QString &path, int error = 500) { _qnam->errorPaths().insert(path, error); } void clear() { _qnam->errorPaths().clear(); } }; ErrorList serverErrorPaths() { return { _fakeAm }; } void setServerOverride(const FakeAM::Override &override) { _fakeAm->setOverride(override); } QString localPath() const; void scheduleSync(); void execUntilBeforePropagation(); void execUntilItemCompleted(const QString &relativePath); bool execUntilFinished(); bool syncOnce(); Q_REQUIRED_RESULT bool applyLocalModificationsAndSync() { if (!_localModifier.applyModifications()) { return false; } return syncOnce(); } Q_REQUIRED_RESULT bool applyLocalModificationsWithoutSync() { return _localModifier.applyModifications(); } bool isDehydratedPlaceholder(const QString &filePath); QSharedPointer vfs() const; private: static void toDisk(QDir &dir, const FileInfo &templateFi); 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 { char *toString(const SyncFileStatus &s); } void addFiles(QStringList &dest, const FileInfo &fi); QString toStringNoElide(const FileInfo &fi); inline char *toString(const FileInfo &fi) { return QTest::toString(toStringNoElide(fi)); } void addFilesDbData(QStringList &dest, const FileInfo &fi); char *printDbData(const FileInfo &fi); struct OperationCounter { int nGET = 0; int nPUT = 0; int nMOVE = 0; int nDELETE = 0; OperationCounter() {}; OperationCounter(const OperationCounter &) = delete; OperationCounter(OperationCounter &&) = delete; void operator=(OperationCounter const &) = delete; void operator=(OperationCounter &&) = delete; // for debugging, a list of requests by operator QMap> requests; void reset() { nGET = 0; nPUT = 0; nMOVE = 0; nDELETE = 0; requests.clear(); } QNetworkReply *serverOverride(QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *) { const auto customVerb = req.attribute(QNetworkRequest::CustomVerbAttribute).toByteArray(); requests[customVerb].append(req.url()); if (op == QNetworkAccessManager::GetOperation) { ++nGET; } else if (op == QNetworkAccessManager::PutOperation) { ++nPUT; } else if (op == QNetworkAccessManager::DeleteOperation) { ++nDELETE; } else if (customVerb == QByteArrayLiteral("MOVE")) { ++nMOVE; } return nullptr; } auto functor() { return [this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) { return serverOverride(op, req, device); }; } OperationCounter(FakeFolder &fakeFolder) { fakeFolder.setServerOverride(functor()); } friend inline QDebug operator<<(QDebug dbg, const OperationCounter &oc) { return dbg << "nGET:" << oc.nGET << " nPUT:" << oc.nPUT << " nMOVE:" << oc.nMOVE << " nDELETE:" << oc.nDELETE; } };