From 3f5163a4f30cf5978ffd7ee363e0d81065eb29c0 Mon Sep 17 00:00:00 2001 From: Hannah von Reth Date: Fri, 2 Jul 2021 10:21:01 +0200 Subject: Delay the deletion of Folder objects This removes the need to check for the existance of the pointers Fixes: #8690 --- src/gui/folder.h | 4 - src/gui/folderman.cpp | 46 +- src/gui/folderman.h | 18 +- src/gui/folderwatcher_win.cpp | 6 +- src/gui/folderwizard.cpp | 5 +- src/gui/folderwizard.h | 4 +- src/gui/issueswidget.cpp | 12 +- src/gui/models/protocolitemmodel.cpp | 17 +- src/gui/protocolitem.cpp | 8 +- src/gui/protocolitem.h | 8 +- src/gui/protocolwidget.cpp | 36 +- src/gui/protocolwidget.ui | 2 +- test/CMakeLists.txt | 2 +- test/benchmarks/benchlargesync.cpp | 2 +- test/modeltests/testactivitymodel.cpp | 19 +- test/modeltests/testprotocolmodel.cpp | 34 +- test/syncenginetestutils.cpp | 1099 ------------------------------- test/syncenginetestutils.h | 574 ----------------- test/testallfilesdeleted.cpp | 2 +- test/testblacklist.cpp | 2 +- test/testchunkingng.cpp | 2 +- test/testcredentialmanager.cpp | 2 +- test/testdatabaseerror.cpp | 2 +- test/testdownload.cpp | 2 +- test/testfolderman.cpp | 59 +- test/testjobqueue.cpp | 2 +- test/testlocaldiscovery.cpp | 2 +- test/testlockedfiles.cpp | 2 +- test/testoauth.cpp | 2 +- test/testpermissions.cpp | 2 +- test/testremotediscovery.cpp | 2 +- test/testselectivesync.cpp | 2 +- test/testsyncconflict.cpp | 2 +- test/testsyncdelete.cpp | 2 +- test/testsyncengine.cpp | 2 +- test/testsyncfilestatustracker.cpp | 2 +- test/testsyncmove.cpp | 2 +- test/testsyncvirtualfiles.cpp | 2 +- test/testuploadreset.cpp | 2 +- test/testutils/syncenginetestutils.cpp | 1100 ++++++++++++++++++++++++++++++++ test/testutils/syncenginetestutils.h | 610 ++++++++++++++++++ test/testutils/testutils.cpp | 59 ++ test/testutils/testutils.h | 14 + 43 files changed, 1922 insertions(+), 1856 deletions(-) delete mode 100644 test/syncenginetestutils.cpp delete mode 100644 test/syncenginetestutils.h create mode 100644 test/testutils/syncenginetestutils.cpp create mode 100644 test/testutils/syncenginetestutils.h create mode 100644 test/testutils/testutils.cpp create mode 100644 test/testutils/testutils.h diff --git a/src/gui/folder.h b/src/gui/folder.h index 5c34b90e5..7b3103985 100644 --- a/src/gui/folder.h +++ b/src/gui/folder.h @@ -124,10 +124,6 @@ public: Folder(const FolderDefinition &definition, AccountState *accountState, std::unique_ptr vfs, QObject *parent = nullptr); ~Folder() override; - - typedef QMap Map; - typedef QMapIterator MapIterator; - /** * The account the folder is configured on. */ diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp index 58e6bdff5..92f9659f7 100644 --- a/src/gui/folderman.cpp +++ b/src/gui/folderman.cpp @@ -95,21 +95,14 @@ FolderMan::~FolderMan() _instance = nullptr; } -const OCC::Folder::Map &FolderMan::map() const +const QMap &FolderMan::map() const { return _folderMap; } -QList FolderMan::list() const -{ - return _folderMap.values(); -} - void FolderMan::unloadFolder(Folder *f) { - if (!f) { - return; - } + Q_ASSERT(f); _socketApi->slotUnregisterPath(f->alias()); @@ -129,28 +122,19 @@ void FolderMan::unloadFolder(Folder *f) &f->syncEngine().syncFileStatusTracker(), &SyncFileStatusTracker::slotPathTouched); } -int FolderMan::unloadAndDeleteAllFolders() +void FolderMan::unloadAndDeleteAllFolders() { - int cnt = 0; - // clear the list of existing folders. - Folder::MapIterator i(_folderMap); - while (i.hasNext()) { - i.next(); - Folder *f = i.value(); - unloadFolder(f); - delete f; - cnt++; + const auto folders = std::move(_folderMap); + for (auto *folder : folders) { + _socketApi->slotUnregisterPath(folder->alias()); + folder->deleteLater(); } - OC_ASSERT(_folderMap.isEmpty()); - _lastSyncFolder = nullptr; _currentSyncFolder = nullptr; _scheduledFolders.clear(); emit folderListChanged(_folderMap); emit scheduleQueueChanged(); - - return cnt; } void FolderMan::registerFolderWithSocketApi(Folder *folder) @@ -704,7 +688,7 @@ void FolderMan::setSyncEnabled(bool enabled) } _syncEnabled = enabled; // force a redraw in case the network connect status changed - emit(folderSyncStateChange(nullptr)); + Q_EMIT folderSyncStateChange(nullptr); } void FolderMan::startScheduledSyncSoon() @@ -825,16 +809,14 @@ void FolderMan::slotEtagPollTimerTimeout() void FolderMan::slotRemoveFoldersForAccount(AccountState *accountState) { - QVarLengthArray foldersToRemove; - Folder::MapIterator i(_folderMap); - while (i.hasNext()) { - i.next(); - Folder *folder = i.value(); + QList foldersToRemove; + // reserve a magic number + foldersToRemove.reserve(16); + for (auto *folder : qAsConst(_folderMap)) { if (folder->accountState() == accountState) { foldersToRemove.append(folder); } } - for (const auto &f : foldersToRemove) { removeFolder(f); } @@ -1128,13 +1110,13 @@ void FolderMan::removeFolder(Folder *f) // Let the folder delete itself when done. connect(f, &Folder::syncFinished, f, &QObject::deleteLater); } else { - delete f; + f->deleteLater(); } #ifdef Q_OS_WIN _navigationPaneHelper.scheduleUpdateCloudStorageRegistry(); #endif - + Q_EMIT folderRemoved(f); emit folderListChanged(_folderMap); } diff --git a/src/gui/folderman.h b/src/gui/folderman.h index 0a639c314..c137378c8 100644 --- a/src/gui/folderman.h +++ b/src/gui/folderman.h @@ -25,9 +25,11 @@ #include "navigationpanehelper.h" #include "syncfileitem.h" -class TestFolderMan; - namespace OCC { +namespace TestUtils { + // prototype for test friend + FolderMan *folderMan(); +} class Application; class SyncResult; @@ -99,8 +101,7 @@ public: */ static void backwardMigrationSettingsKeys(QStringList *deleteKeys, QStringList *ignoreKeys); - const Folder::Map &map() const; - QList list() const; + const QMap &map() const; /** Adds a folder for an account, ensures the journal is gone and saves it in the settings. */ @@ -216,7 +217,7 @@ public: bool isAnySyncRunning() const; /** Removes all folders */ - int unloadAndDeleteAllFolders(); + void unloadAndDeleteAllFolders(); /** * If enabled is set to false, no new folders will start to sync. @@ -252,7 +253,8 @@ signals: /** * Emitted whenever the list of configured folders changes. */ - void folderListChanged(const Folder::Map &); + void folderListChanged(const QMap &); + void folderRemoved(Folder *folder); public slots: @@ -341,7 +343,7 @@ private: void setupFoldersHelper(QSettings &settings, AccountStatePtr account, const QStringList &ignoreKeys, bool backwardsCompatible, bool foldersWithPlaceholders); QSet _disabledFolders; - Folder::Map _folderMap; + QMap _folderMap; QString _folderConfigPath; Folder *_currentSyncFolder; QPointer _lastSyncFolder; @@ -377,7 +379,7 @@ private: static FolderMan *_instance; explicit FolderMan(QObject *parent = nullptr); friend class OCC::Application; - friend class ::TestFolderMan; + friend OCC::FolderMan *OCC::TestUtils::folderMan(); }; } // namespace OCC diff --git a/src/gui/folderwatcher_win.cpp b/src/gui/folderwatcher_win.cpp index fd20725bd..4541c895a 100644 --- a/src/gui/folderwatcher_win.cpp +++ b/src/gui/folderwatcher_win.cpp @@ -216,10 +216,8 @@ FolderWatcherPrivate::FolderWatcherPrivate(FolderWatcher *p, const QString &path : _parent(p) { _thread = new WatcherThread(path); - connect(_thread, SIGNAL(changed(const QString &)), - _parent, SLOT(changeDetected(const QString &))); - connect(_thread, SIGNAL(lostChanges()), - _parent, SIGNAL(lostChanges())); + connect(_thread, &WatcherThread::changed, _parent, qOverload(&FolderWatcher::changeDetected)); + connect(_thread, &WatcherThread::lostChanges, _parent, &FolderWatcher::lostChanges); connect(_thread, &WatcherThread::ready, this, [this]() { _ready = 1; }); _thread->start(); diff --git a/src/gui/folderwizard.cpp b/src/gui/folderwizard.cpp index e481333d8..35c44c69b 100644 --- a/src/gui/folderwizard.cpp +++ b/src/gui/folderwizard.cpp @@ -424,10 +424,7 @@ bool FolderWizardRemotePath::isComplete() const } wizard()->setProperty("targetPath", dir); - Folder::Map map = FolderMan::instance()->map(); - Folder::Map::const_iterator i = map.constBegin(); - for (i = map.constBegin(); i != map.constEnd(); i++) { - Folder *f = static_cast(i.value()); + for (auto *f : qAsConst(FolderMan::instance()->map())) { if (f->accountState()->account() != _account) { continue; } diff --git a/src/gui/folderwizard.h b/src/gui/folderwizard.h index 251f4afd2..f0b4e42e8 100644 --- a/src/gui/folderwizard.h +++ b/src/gui/folderwizard.h @@ -58,14 +58,12 @@ public: bool isComplete() const override; void initializePage() override; void cleanupPage() override; - - void setFolderMap(const Folder::Map &fm) { _folderMap = fm; } protected slots: void slotChooseLocalFolder(); private: Ui_FolderWizardSourcePage _ui; - Folder::Map _folderMap; + QMap _folderMap; AccountPtr _account; }; diff --git a/src/gui/issueswidget.cpp b/src/gui/issueswidget.cpp index 0b801115f..99d622d19 100644 --- a/src/gui/issueswidget.cpp +++ b/src/gui/issueswidget.cpp @@ -84,14 +84,12 @@ IssuesWidget::IssuesWidget(QWidget *parent) header->setExpandingColumn(static_cast(ProtocolItemModel::ProtocolItemRole::Action)); header->setSortIndicator(static_cast(ProtocolItemModel::ProtocolItemRole::Time), Qt::DescendingOrder); - connect(_ui->_tableView, &QTreeView::customContextMenuRequested, this, &IssuesWidget::slotItemContextMenu); _ui->_tableView->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); connect(header, &QHeaderView::customContextMenuRequested, header, [header, this] { ProtocolWidget::showHeaderContextMenu(header, _sortModel); }); - _ui->_tooManyIssuesWarning->hide(); connect(_model, &ProtocolItemModel::rowsInserted, this, [this] { _ui->_tooManyIssuesWarning->setVisible(_model->isModelFull()); @@ -104,6 +102,12 @@ IssuesWidget::IssuesWidget(QWidget *parent) _ui->_conflictHelp->setText( tr("There were conflicts. Check the documentation on how to resolve them.") .arg(Theme::instance()->conflictHelpUrl())); + + connect(FolderMan::instance(), &FolderMan::folderRemoved, this, [this](Folder *f) { + _model->remove_if([f](const ProtocolItem &item) { + return item.folder() == f; + }); + }); } IssuesWidget::~IssuesWidget() @@ -122,7 +126,7 @@ void IssuesWidget::slotProgressInfo(const QString &folder, const ProgressInfo &p const auto &engine = f->syncEngine(); const auto style = engine.lastLocalDiscoveryStyle(); _model->remove_if([&](const ProtocolItem &item) { - if (item.folderName() != folder) { + if (item.folder()->path() != folder) { return false; } if (style == LocalDiscoveryStyle::FilesystemOnly) { @@ -148,7 +152,7 @@ void IssuesWidget::slotProgressInfo(const QString &folder, const ProgressInfo &p // Inform other components about them. QStringList conflicts; for (const auto &data : _model->rawData()) { - if (data.folderName() == folder + if (data.folder()->path() == folder && data.status() == SyncFileItem::Conflict) { conflicts.append(data.path()); } diff --git a/src/gui/models/protocolitemmodel.cpp b/src/gui/models/protocolitemmodel.cpp index 0ecc7cdd1..76f5c557a 100644 --- a/src/gui/models/protocolitemmodel.cpp +++ b/src/gui/models/protocolitemmodel.cpp @@ -22,15 +22,6 @@ #include -namespace { -auto getFolder(const OCC::ProtocolItem &item) -{ - auto f = OCC::FolderMan::instance()->folder(item.folderName()); - OC_ASSERT(f); - return f; -} -} - using namespace OCC; ProtocolItemModel::ProtocolItemModel(QObject *parent, bool issueMode) @@ -69,7 +60,7 @@ QVariant ProtocolItemModel::data(const QModelIndex &index, int role) const case ProtocolItemRole::Time: return item.timestamp(); case ProtocolItemRole::Folder: - return getFolder(item)->shortGuiLocalPath(); + return item.folder()->shortGuiLocalPath(); case ProtocolItemRole::Action: return item.message(); case ProtocolItemRole::Size: @@ -77,7 +68,7 @@ QVariant ProtocolItemModel::data(const QModelIndex &index, int role) const case ProtocolItemRole::File: return Utility::fileNameForGuiUse(item.path()); case ProtocolItemRole::Account: - return getFolder(item)->accountState()->account()->displayName(); + return item.folder()->accountState()->account()->displayName(); case ProtocolItemRole::ColumnCount: Q_UNREACHABLE(); break; @@ -105,7 +96,7 @@ QVariant ProtocolItemModel::data(const QModelIndex &index, int role) const case ProtocolItemRole::Time: return item.timestamp(); case ProtocolItemRole::Folder: - return item.folderName(); + return item.folder()->path(); case ProtocolItemRole::Action: return item.message(); case ProtocolItemRole::Size: @@ -113,7 +104,7 @@ QVariant ProtocolItemModel::data(const QModelIndex &index, int role) const case ProtocolItemRole::File: return item.path(); case ProtocolItemRole::Account: - return getFolder(item)->accountState()->account()->displayName(); + return item.folder()->accountState()->account()->displayName(); case ProtocolItemRole::ColumnCount: Q_UNREACHABLE(); break; diff --git a/src/gui/protocolitem.cpp b/src/gui/protocolitem.cpp index 769236005..cc7ed5c40 100644 --- a/src/gui/protocolitem.cpp +++ b/src/gui/protocolitem.cpp @@ -13,6 +13,7 @@ */ #include "protocolitem.h" +#include "folderman.h" #include "progressdispatcher.h" #include @@ -20,11 +21,12 @@ #include #include + using namespace OCC; ProtocolItem::ProtocolItem(const QString &folder, const SyncFileItemPtr &item) : _path(item->destination()) - , _folderName(folder) + , _folder(FolderMan::instance()->folder(folder)) , _size(item->_size) , _status(item->_status) , _direction(item->_direction) @@ -46,9 +48,9 @@ QString ProtocolItem::path() const return _path; } -QString ProtocolItem::folderName() const +Folder *ProtocolItem::folder() const { - return _folderName; + return _folder; } QDateTime ProtocolItem::timestamp() const diff --git a/src/gui/protocolitem.h b/src/gui/protocolitem.h index ab2ee44e0..8f064a1aa 100644 --- a/src/gui/protocolitem.h +++ b/src/gui/protocolitem.h @@ -13,6 +13,8 @@ */ #pragma once +#include "folder.h" + #include "csync/csync.h" #include "libsync/syncfileitem.h" @@ -29,7 +31,7 @@ public: QString path() const; - QString folderName() const; + Folder *folder() const; QDateTime timestamp() const; @@ -45,7 +47,7 @@ public: private: QString _path; - QString _folderName; + Folder *_folder; QDateTime _timestamp; qint64 _size; SyncFileItem::Status _status BITFIELD(4); @@ -53,6 +55,8 @@ private: QString _message; bool _sizeIsRelevant; + + friend class TestProtocolModel; }; } diff --git a/src/gui/protocolwidget.cpp b/src/gui/protocolwidget.cpp index c8f674938..c39c44386 100644 --- a/src/gui/protocolwidget.cpp +++ b/src/gui/protocolwidget.cpp @@ -64,8 +64,11 @@ ProtocolWidget::ProtocolWidget(QWidget *parent) showHeaderContextMenu(header, _sortModel); }); - - _ui->_headerLabel->setText(tr("Local sync protocol")); + connect(FolderMan::instance(), &FolderMan::folderRemoved, this, [this](Folder *f) { + _model->remove_if([f](const ProtocolItem &item) { + return item.folder() == f; + }); + }); } ProtocolWidget::~ProtocolWidget() @@ -92,22 +95,19 @@ void ProtocolWidget::showContextMenu(QWidget *parent, ProtocolItemModel *model, if (items.size() == 1) { const auto &data = model->protocolItem(items.first()); - auto folder = FolderMan::instance()->folder(data.folderName()); - if (folder) { - { - const QString localPath = folder->path() + data.path(); - if (QFileInfo::exists(localPath)) { - // keep in sync with ActivityWidget::slotItemContextMenu - menu->addAction(tr("Show in file browser"), parent, [localPath] { - if (QFileInfo::exists(localPath)) { - showInFileManager(localPath); - } - }); - } + { + const QString localPath = data.folder()->path() + data.path(); + if (QFileInfo::exists(localPath)) { + // keep in sync with ActivityWidget::slotItemContextMenu + menu->addAction(tr("Show in file browser"), parent, [localPath] { + if (QFileInfo::exists(localPath)) { + showInFileManager(localPath); + } + }); } // "Open in Browser" action { - fetchPrivateLinkUrl(folder->accountState()->account(), folder->remotePathTrailingSlash() + data.path(), parent, [parent, menu = QPointer(menu)](const QString &url) { + fetchPrivateLinkUrl(data.folder()->accountState()->account(), data.folder()->remotePathTrailingSlash() + data.path(), parent, [parent, menu = QPointer(menu)](const QString &url) { // as fetchPrivateLinkUrl is async we need to check the menu still exists if (menu) { menu->addAction(tr("Show in web browser"), parent, [url, parent] { @@ -123,9 +123,9 @@ void ProtocolWidget::showContextMenu(QWidget *parent, ProtocolItemModel *model, case SyncFileItem::SoftError: Q_FALLTHROUGH(); case SyncFileItem::BlacklistedError: - menu->addAction(tr("Retry sync"), parent, [folder, &data] { - folder->journalDb()->wipeErrorBlacklistEntry(data.path()); - FolderMan::instance()->scheduleFolderNext(folder); + menu->addAction(tr("Retry sync"), parent, [&data] { + data.folder()->journalDb()->wipeErrorBlacklistEntry(data.path()); + FolderMan::instance()->scheduleFolderNext(data.folder()); }); default: break; diff --git a/src/gui/protocolwidget.ui b/src/gui/protocolwidget.ui index 7941d7418..cdd97d3d7 100644 --- a/src/gui/protocolwidget.ui +++ b/src/gui/protocolwidget.ui @@ -17,7 +17,7 @@ - TextLabel + Local sync protocol Qt::PlainText diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index eb2c669bf..b27278825 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,6 +1,6 @@ include(owncloud_add_test.cmake) -add_library(syncenginetestutils STATIC syncenginetestutils.cpp) +add_library(syncenginetestutils STATIC testutils/syncenginetestutils.cpp testutils/testutils.cpp) target_link_libraries(syncenginetestutils PUBLIC owncloudCore Qt5::Test) owncloud_add_test(OwncloudPropagator) diff --git a/test/benchmarks/benchlargesync.cpp b/test/benchmarks/benchlargesync.cpp index 9e78b9d5c..05b7c1b9d 100644 --- a/test/benchmarks/benchlargesync.cpp +++ b/test/benchmarks/benchlargesync.cpp @@ -5,7 +5,7 @@ * */ -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include using namespace OCC; diff --git a/test/modeltests/testactivitymodel.cpp b/test/modeltests/testactivitymodel.cpp index ef7899257..fa9b7d2d6 100644 --- a/test/modeltests/testactivitymodel.cpp +++ b/test/modeltests/testactivitymodel.cpp @@ -9,6 +9,8 @@ #include "gui/models/activitylistmodel.h" #include "gui/accountmanager.h" +#include "testutils/testutils.h" + #include #include @@ -25,19 +27,8 @@ private Q_SLOTS: new QAbstractItemModelTester(model, this); - auto manager = AccountManager::instance(); - - auto createAcc = [&] { - // don't use the account manager to create the account, it would try to use widgets - auto acc = Account::create(); - acc->setUrl(QUrl(QStringLiteral("http://admin:admin@localhost/owncloud"))); - acc->setDavDisplayName(QStringLiteral("fakename") + acc->uuid().toString()); - acc->setServerVersion(QStringLiteral("10.0.0")); - manager->addAccount(acc); - return acc; - }; - auto acc1 = createAcc(); - auto acc2 = createAcc(); + auto acc1 = TestUtils::createDummyAccount(); + auto acc2 = TestUtils::createDummyAccount(); model->setActivityList({ Activity { Activity::ActivityType, 1, acc1, "test", "test", "foo.cpp", QUrl::fromUserInput("https://owncloud.com"), QDateTime::currentDateTime() }, @@ -49,7 +40,7 @@ private Q_SLOTS: Activity { Activity::ActivityType, 2, acc1, "test", "test", "foo.cpp", QUrl::fromUserInput("https://owncloud.com"), QDateTime::currentDateTime() }, Activity { Activity::ActivityType, 4, acc2, "test", "test", "foo.cpp", QUrl::fromUserInput("https://owncloud.com"), QDateTime::currentDateTime() }, }); - model->slotRemoveAccount(manager->accounts().first().data()); + model->slotRemoveAccount(AccountManager::instance()->accounts().first().data()); } }; } diff --git a/test/modeltests/testprotocolmodel.cpp b/test/modeltests/testprotocolmodel.cpp index 8132f9f63..e2c194c2f 100644 --- a/test/modeltests/testprotocolmodel.cpp +++ b/test/modeltests/testprotocolmodel.cpp @@ -7,9 +7,15 @@ */ #include "gui/models/protocolitemmodel.h" +#include "gui/accountmanager.h" +#include "gui/accountstate.h" +#include "gui/folderman.h" + +#include "testutils/testutils.h" #include #include +#include namespace OCC { @@ -24,6 +30,20 @@ private Q_SLOTS: new QAbstractItemModelTester(model, this); + QTemporaryDir dir; + + auto account = TestUtils::createDummyAccount(); + + AccountStatePtr newAccountState(new AccountState(account)); + const QDir d(dir.path()); + QVERIFY(d.mkdir("foo")); + QVERIFY(d.mkdir("bar")); + const QString foo = dir.path() + QStringLiteral("/foo"); + const QString bar = dir.path() + QStringLiteral("/bar"); + QVERIFY(TestUtils::folderMan()->addFolder(newAccountState.data(), TestUtils::createDummyFolderDefinition(foo))); + QVERIFY(TestUtils::folderMan()->addFolder(newAccountState.data(), TestUtils::createDummyFolderDefinition(bar))); + + // populate with dummy data // -1 to test the ring buffer window roll over const auto size = model->rawData().capacity() - 1; @@ -31,13 +51,15 @@ private Q_SLOTS: std::vector tmp; tmp.reserve(size); for (size_t i = 0; i < size; ++i) { - tmp.emplace_back(QStringLiteral("foo") + QString::number(i), item); + item->_file = QString::number(i); + tmp.emplace_back(foo, item); } model->reset(std::move(tmp)); // test some inserts for (int i = 0; i < 5; ++i) { - model->addProtocolItem(ProtocolItem { QStringLiteral("bar") + QString::number(i), item }); + item->_file = QString::number(i); + model->addProtocolItem(ProtocolItem { bar, item }); } const auto oldSize = model->rowCount(); @@ -45,8 +67,8 @@ private Q_SLOTS: // pick one from the middle const auto toBeRemoved = { model->protocolItem(model->index(0, 0)), - model->protocolItem(model->index(model->rawData().capacity() / 2, 0)), - model->protocolItem(model->index(model->rawData().capacity() / 3, 0)) + model->protocolItem(model->index(static_cast(model->rawData().capacity()) / 2, 0)), + model->protocolItem(model->index(static_cast(model->rawData().capacity()) / 3, 0)) }; std::vector copy; @@ -58,7 +80,7 @@ private Q_SLOTS: int matches = 0; const auto filter = [&toBeRemoved, &matches](const ProtocolItem &pi) { for (const auto &tb : toBeRemoved) { - if (pi.folderName() == tb.folderName()) { + if (pi.folder() == tb.folder() && pi.path() == tb.path()) { matches++; return true; } @@ -74,7 +96,7 @@ private Q_SLOTS: // ensure we kept the original order for (int i = 0; i < model->rowCount(); ++i) { - QCOMPARE(model->protocolItem(model->index(i, 0)).folderName(), copy[i].folderName()); + QCOMPARE(model->protocolItem(model->index(i, 0)).folder(), copy[i].folder()); } } }; diff --git a/test/syncenginetestutils.cpp b/test/syncenginetestutils.cpp deleted file mode 100644 index a4b1a1c37..000000000 --- a/test/syncenginetestutils.cpp +++ /dev/null @@ -1,1099 +0,0 @@ -/* - * This software is in the public domain, furnished "as is", without technical - * support, and with no warranty, express or implied, as to its usefulness for - * any purpose. - * - */ - -#include "syncenginetestutils.h" -#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 &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(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=(?\\d+)-(?\\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(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(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 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 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 &args : spy) { - auto item = args[0].value(); - 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 &args : *this) { - auto item = args[0].value(); - 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/syncenginetestutils.h b/test/syncenginetestutils.h deleted file mode 100644 index 72ef5d91a..000000000 --- a/test/syncenginetestutils.h +++ /dev/null @@ -1,574 +0,0 @@ -/* - * 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 "syncengine.h" -#include "common/syncjournaldb.h" -#include "common/syncjournalfilerecord.h" -#include "common/vfs.h" -#include "csync_exclude.h" -#include - -#include -#include -#include -#include -#include -#include - -/* - * 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 &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 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::setError; - using QNetworkReply::setAttribute; - -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 DelayedReply : public OriginalReply -{ -public: - template - explicit DelayedReply(quint64 delayMS, Args &&... args) - : OriginalReply(std::forward(args)...) - , _delayMs(delayMS) - { - } - quint64 _delayMs; - - void respond() override - { - QTimer::singleShot(_delayMs, static_cast(this), [this] { - // Explicit call to bases's respond(); - this->OriginalReply::respond(); - }); - } -}; - -class FakeQNAM : public QNetworkAccessManager -{ -public: - using Override = std::function; - -private: - FileInfo _remoteRootFileInfo; - FileInfo _uploadFileInfo; - // maps a path to an HTTP error - QHash _errorPaths; - // monitor requests and optionally provide custom replies - Override _override; - -public: - FakeQNAM(FileInfo initialRoot); - FileInfo ¤tRemoteState() { return _remoteRootFileInfo; } - FileInfo &uploadState() { return _uploadFileInfo; } - - 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 -{ - 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 _journalDb; - std::unique_ptr _syncEngine; - -public: - FakeFolder(const FileInfo &fileTemplate); - - void switchToVfs(QSharedPointer 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/testallfilesdeleted.cpp b/test/testallfilesdeleted.cpp index e7b89cfec..e3ad69995 100644 --- a/test/testallfilesdeleted.cpp +++ b/test/testallfilesdeleted.cpp @@ -6,7 +6,7 @@ */ #include -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include using namespace OCC; diff --git a/test/testblacklist.cpp b/test/testblacklist.cpp index d71326560..3a4fe93be 100644 --- a/test/testblacklist.cpp +++ b/test/testblacklist.cpp @@ -6,7 +6,7 @@ */ #include -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include using namespace OCC; diff --git a/test/testchunkingng.cpp b/test/testchunkingng.cpp index 24e248d09..401a21c13 100644 --- a/test/testchunkingng.cpp +++ b/test/testchunkingng.cpp @@ -6,7 +6,7 @@ */ #include -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include using namespace OCC; diff --git a/test/testcredentialmanager.cpp b/test/testcredentialmanager.cpp index f0ac646b2..9afea5ef0 100644 --- a/test/testcredentialmanager.cpp +++ b/test/testcredentialmanager.cpp @@ -7,7 +7,7 @@ #include "account.h" #include "libsync/creds/credentialmanager.h" -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include diff --git a/test/testdatabaseerror.cpp b/test/testdatabaseerror.cpp index 4c2ad5de2..bb19c6477 100644 --- a/test/testdatabaseerror.cpp +++ b/test/testdatabaseerror.cpp @@ -6,7 +6,7 @@ */ #include -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include using namespace OCC; diff --git a/test/testdownload.cpp b/test/testdownload.cpp index ec451e07b..cd0635ad6 100644 --- a/test/testdownload.cpp +++ b/test/testdownload.cpp @@ -6,7 +6,7 @@ */ #include -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include #include diff --git a/test/testfolderman.cpp b/test/testfolderman.cpp index 6cbc0e03c..4e9437b09 100644 --- a/test/testfolderman.cpp +++ b/test/testfolderman.cpp @@ -14,36 +14,15 @@ #include "account.h" #include "accountstate.h" #include "configfile.h" -#include "creds/httpcredentials.h" -using namespace OCC; - -class HttpCredentialsTest : public HttpCredentials { -public: - HttpCredentialsTest(const QString& user, const QString& password) - : HttpCredentials(DetermineAuthTypeJob::AuthType::Basic, user, password) - {} - - void askFromUser() override { - - } -}; +#include "testutils/testutils.h" -static FolderDefinition folderDefinition(const QString &path) { - FolderDefinition d; - d.localPath = path; - d.targetPath = path; - d.alias = path; - return d; -} +using namespace OCC; class TestFolderMan: public QObject { Q_OBJECT - - FolderMan _fm; - private slots: void testCheckPathValidityForNewFolder() { @@ -64,17 +43,12 @@ private slots: } QString dirPath = dir2.canonicalPath(); - AccountPtr account = Account::create(); - QUrl url("http://example.de"); - HttpCredentialsTest *cred = new HttpCredentialsTest("testuser", "secret"); - account->setCredentials(cred); - account->setUrl( url ); - + AccountPtr account = TestUtils::createDummyAccount(); AccountStatePtr newAccountState(new AccountState(account)); - FolderMan *folderman = FolderMan::instance(); - QCOMPARE(folderman, &_fm); - QVERIFY(folderman->addFolder(newAccountState.data(), folderDefinition(dirPath + "/sub/ownCloud1"))); - QVERIFY(folderman->addFolder(newAccountState.data(), folderDefinition(dirPath + "/ownCloud2"))); + FolderMan *folderman = TestUtils::folderMan(); + QCOMPARE(folderman, FolderMan::instance()); + QVERIFY(folderman->addFolder(newAccountState.data(), TestUtils::createDummyFolderDefinition(dirPath + "/sub/ownCloud1"))); + QVERIFY(folderman->addFolder(newAccountState.data(), TestUtils::createDummyFolderDefinition(dirPath + "/ownCloud2"))); // those should be allowed @@ -91,7 +65,7 @@ private slots: QVERIFY(!folderman->checkPathValidityForNewFolder(dirPath + "/sub/file.txt").isNull()); // There are folders configured in those folders, url needs to be taken into account: -> ERROR - QUrl url2(url); + QUrl url2(account->url()); const QString user = account->credentials()->user(); url2.setUserName(user); @@ -174,7 +148,6 @@ private slots: // SETUP QTemporaryDir dir; - ConfigFile::setConfDir(dir.path()); // we don't want to pollute the user's config file QVERIFY(dir.isValid()); QDir dir2(dir.path()); QVERIFY(dir2.mkpath("sub/ownCloud1/folder/f")); @@ -185,18 +158,14 @@ private slots: QVERIFY(dir2.mkpath("free2/sub")); QString dirPath = dir2.canonicalPath(); - AccountPtr account = Account::create(); - QUrl url("http://example.de"); - HttpCredentialsTest *cred = new HttpCredentialsTest("testuser", "secret"); - account->setCredentials(cred); - account->setUrl( url ); - url.setUserName(cred->user()); + AccountPtr account = TestUtils::createDummyAccount(); + QUrl url(account->url()); + url.setUserName(account->credentials()->user()); AccountStatePtr newAccountState(new AccountState(account)); - FolderMan *folderman = FolderMan::instance(); - QCOMPARE(folderman, &_fm); - QVERIFY(folderman->addFolder(newAccountState.data(), folderDefinition(dirPath + "/sub/ownCloud/"))); - QVERIFY(folderman->addFolder(newAccountState.data(), folderDefinition(dirPath + "/ownCloud2/"))); + FolderMan *folderman = TestUtils::folderMan(); + QVERIFY(folderman->addFolder(newAccountState.data(), TestUtils::createDummyFolderDefinition(dirPath + "/sub/ownCloud/"))); + QVERIFY(folderman->addFolder(newAccountState.data(), TestUtils::createDummyFolderDefinition(dirPath + "/ownCloud2/"))); // TEST diff --git a/test/testjobqueue.cpp b/test/testjobqueue.cpp index aeb2063cb..7fca54082 100644 --- a/test/testjobqueue.cpp +++ b/test/testjobqueue.cpp @@ -9,7 +9,7 @@ #include "abstractnetworkjob.h" #include "account.h" -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include diff --git a/test/testlocaldiscovery.cpp b/test/testlocaldiscovery.cpp index 18e1c1b9d..643a3c96a 100644 --- a/test/testlocaldiscovery.cpp +++ b/test/testlocaldiscovery.cpp @@ -6,7 +6,7 @@ */ #include -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include #include diff --git a/test/testlockedfiles.cpp b/test/testlockedfiles.cpp index 2ffedb8e3..a96dbea64 100644 --- a/test/testlockedfiles.cpp +++ b/test/testlockedfiles.cpp @@ -6,7 +6,7 @@ */ #include -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include "lockwatcher.h" #include #include diff --git a/test/testoauth.cpp b/test/testoauth.cpp index 42de3a3cf..a8b0dbaaf 100644 --- a/test/testoauth.cpp +++ b/test/testoauth.cpp @@ -9,7 +9,7 @@ #include #include "libsync/creds/oauth.h" -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include "theme.h" #include "common/asserts.h" diff --git a/test/testpermissions.cpp b/test/testpermissions.cpp index 6836c331f..012c91a71 100644 --- a/test/testpermissions.cpp +++ b/test/testpermissions.cpp @@ -6,7 +6,7 @@ */ #include -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include #include "common/ownsql.h" diff --git a/test/testremotediscovery.cpp b/test/testremotediscovery.cpp index a93c16039..b6b696bb3 100644 --- a/test/testremotediscovery.cpp +++ b/test/testremotediscovery.cpp @@ -6,7 +6,7 @@ */ #include -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include #include diff --git a/test/testselectivesync.cpp b/test/testselectivesync.cpp index c91bd8b76..67f44d3fc 100644 --- a/test/testselectivesync.cpp +++ b/test/testselectivesync.cpp @@ -6,7 +6,7 @@ */ #include -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include using namespace OCC; diff --git a/test/testsyncconflict.cpp b/test/testsyncconflict.cpp index 1a1becdc9..dde1eb751 100644 --- a/test/testsyncconflict.cpp +++ b/test/testsyncconflict.cpp @@ -6,7 +6,7 @@ */ #include -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include using namespace OCC; diff --git a/test/testsyncdelete.cpp b/test/testsyncdelete.cpp index c2633ba4b..780b5e684 100644 --- a/test/testsyncdelete.cpp +++ b/test/testsyncdelete.cpp @@ -6,7 +6,7 @@ */ #include -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include using namespace OCC; diff --git a/test/testsyncengine.cpp b/test/testsyncengine.cpp index 1ee5bdb84..89fe33420 100644 --- a/test/testsyncengine.cpp +++ b/test/testsyncengine.cpp @@ -6,7 +6,7 @@ */ #include -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include using namespace OCC; diff --git a/test/testsyncfilestatustracker.cpp b/test/testsyncfilestatustracker.cpp index f1b27447c..823910514 100644 --- a/test/testsyncfilestatustracker.cpp +++ b/test/testsyncfilestatustracker.cpp @@ -6,7 +6,7 @@ */ #include -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include "csync_exclude.h" using namespace OCC; diff --git a/test/testsyncmove.cpp b/test/testsyncmove.cpp index 8e09ba693..047e5149b 100644 --- a/test/testsyncmove.cpp +++ b/test/testsyncmove.cpp @@ -6,7 +6,7 @@ */ #include -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include using namespace OCC; diff --git a/test/testsyncvirtualfiles.cpp b/test/testsyncvirtualfiles.cpp index f46888518..ffcf1c1fa 100644 --- a/test/testsyncvirtualfiles.cpp +++ b/test/testsyncvirtualfiles.cpp @@ -6,7 +6,7 @@ */ #include -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include "common/vfs.h" #include "config.h" #include diff --git a/test/testuploadreset.cpp b/test/testuploadreset.cpp index cc7d4c5ae..d954e7553 100644 --- a/test/testuploadreset.cpp +++ b/test/testuploadreset.cpp @@ -6,7 +6,7 @@ */ #include -#include "syncenginetestutils.h" +#include "testutils/syncenginetestutils.h" #include #include 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 &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(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=(?\\d+)-(?\\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(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(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 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 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 &args : spy) { + auto item = args[0].value(); + 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 &args : *this) { + auto item = args[0].value(); + 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 + +#include +#include +#include +#include +#include +#include + +/* + * 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 &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 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 DelayedReply : public OriginalReply +{ +public: + template + explicit DelayedReply(quint64 delayMS, Args &&...args) + : OriginalReply(std::forward(args)...) + , _delayMs(delayMS) + { + } + quint64 _delayMs; + + void respond() override + { + QTimer::singleShot(_delayMs, static_cast(this), [this] { + // Explicit call to bases's respond(); + this->OriginalReply::respond(); + }); + } +}; + +class FakeQNAM : public QNetworkAccessManager +{ +public: + using Override = std::function; + +private: + FileInfo _remoteRootFileInfo; + FileInfo _uploadFileInfo; + // maps a path to an HTTP error + QHash _errorPaths; + // monitor requests and optionally provide custom replies + Override _override; + +public: + FakeQNAM(FileInfo initialRoot); + FileInfo ¤tRemoteState() { return _remoteRootFileInfo; } + FileInfo &uploadState() { return _uploadFileInfo; } + + 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 +{ + 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 _journalDb; + std::unique_ptr _syncEngine; + +public: + FakeFolder(const FileInfo &fileTemplate); + + void switchToVfs(QSharedPointer 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 + +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(); +} +} -- cgit v1.2.3