Moved all server mocking code for share model test to independent ShareTestUtils...
authorClaudio Cambra <claudio.cambra@nextcloud.com>
Tue, 18 Oct 2022 15:15:10 +0000 (17:15 +0200)
committerClaudio Cambra <claudio.cambra@nextcloud.com>
Mon, 31 Oct 2022 17:06:10 +0000 (18:06 +0100)
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
src/gui/folderman.h
test/CMakeLists.txt
test/sharetestutils.cpp [new file with mode: 0644]
test/sharetestutils.h [new file with mode: 0644]
test/testhelper.cpp
test/testhelper.h
test/testsharemodel.cpp

index 8cc499cd8fc3a0cb079bb095fe20c97a8002bc2f..6409cc8b462f4b4599b5393ff1df48baf5e5976f 100644 (file)
@@ -28,6 +28,7 @@
 class TestFolderMan;
 class TestCfApiShellExtensionsIPC;
 class TestShareModel;
+class ShareTestHelper;
 
 namespace OCC {
 
@@ -380,7 +381,7 @@ private:
     friend class OCC::Application;
     friend class ::TestFolderMan;
     friend class ::TestCfApiShellExtensionsIPC;
-    friend class ::TestShareModel;
+    friend class ::ShareTestHelper;
 };
 
 } // namespace OCC
index 820984631df9dd460a77192a4cdc58b407203b80..d3cd1f93a5fcf91714ffc7109756c191cc17193a 100644 (file)
@@ -10,6 +10,7 @@ add_library(testutils
   pushnotificationstestutils.cpp
   themeutils.cpp
   testhelper.cpp
+  sharetestutils.cpp
 )
 
 target_link_libraries(testutils PUBLIC Nextcloud::sync Qt5::Test)
diff --git a/test/sharetestutils.cpp b/test/sharetestutils.cpp
new file mode 100644 (file)
index 0000000..7f481a6
--- /dev/null
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "sharetestutils.h"
+
+#include "testhelper.h"
+
+using namespace OCC;
+
+FakeShareDefinition::FakeShareDefinition(ShareTestHelper *helper,
+                                         const Share::ShareType type,
+                                         const QString &shareWith,
+                                         const QString &displayString,
+                                         const QString &password,
+                                         const QString &note,
+                                         const QString &expiration)
+{
+    ++helper->latestShareId;
+    const auto idString = QString::number(helper->latestShareId);
+
+
+    fileDefinition = helper->fakeFileDefinition;
+    shareId = idString;
+    shareCanDelete = true;
+    shareCanEdit = true;
+    shareUidOwner = helper->account->davUser();;
+    shareDisplayNameOwner = helper->account->davDisplayName();
+    sharePassword = password;
+    sharePermissions = static_cast<int>(SharePermissions(SharePermissionRead |
+                                                         SharePermissionUpdate |
+                                                         SharePermissionCreate |
+                                                         SharePermissionDelete |
+                                                         SharePermissionShare));
+    shareNote = note;
+    shareHideDownload = 0;
+    shareExpiration = expiration;
+    shareSendPasswordByTalk = false;
+    shareType = type;
+
+    const auto token = QString(QStringLiteral("GQ4aLrZEdJJkopW-") + idString);
+    // Weird, but it's what the server does
+    const auto finalShareWith = type == Share::TypeLink ? password : shareWith;
+    const auto shareWithDisplayName = type == Share::TypeLink ? QStringLiteral("(Shared Link)") : displayString;
+    const auto linkLabel = type == Share::TypeLink ? displayString : QString();
+    const auto linkName = linkShareLabel;
+    const auto linkUrl = type == Share::TypeLink ? QString(helper->account->davUrl().toString() + QStringLiteral("/s/") + token) : QString();
+
+    shareShareWith = finalShareWith;
+    shareShareWithDisplayName = shareWithDisplayName;
+    shareToken = token;
+    linkShareName = linkName;
+    linkShareLabel = linkLabel;
+    linkShareUrl = linkUrl;
+}
+
+QJsonObject FakeShareDefinition::toShareJsonObject() const
+{
+    QJsonObject newShareJson;
+    newShareJson.insert("uid_file_owner", fileDefinition.fileOwnerUid);
+    newShareJson.insert("displayname_file_owner", fileDefinition.fileOwnerDisplayName);
+    newShareJson.insert("file_target", fileDefinition.fileTarget);
+    newShareJson.insert("has_preview", fileDefinition.fileHasPreview);
+    newShareJson.insert("file_parent", fileDefinition.fileFileParent);
+    newShareJson.insert("file_source", fileDefinition.fileSource);
+    newShareJson.insert("item_source", fileDefinition.fileItemSource);
+    newShareJson.insert("item_type", fileDefinition.fileItemType);
+    newShareJson.insert("mail_send", fileDefinition.fileMailSend);
+    newShareJson.insert("mimetype", fileDefinition.fileMimeType);
+    newShareJson.insert("parent", fileDefinition.fileParent);
+    newShareJson.insert("path", fileDefinition.filePath);
+    newShareJson.insert("storage", fileDefinition.fileStorage);
+    newShareJson.insert("storage_id", fileDefinition.fileStorageId);
+    newShareJson.insert("id", shareId);
+    newShareJson.insert("can_delete", shareCanDelete);
+    newShareJson.insert("can_edit", shareCanEdit);
+    newShareJson.insert("uid_owner", shareUidOwner);
+    newShareJson.insert("displayname_owner", shareDisplayNameOwner);
+    newShareJson.insert("password", sharePassword);
+    newShareJson.insert("permissions", sharePermissions);
+    newShareJson.insert("note", shareNote);
+    newShareJson.insert("hide_download", shareHideDownload);
+    newShareJson.insert("expiration", shareExpiration);
+    newShareJson.insert("send_password_by_talk", shareSendPasswordByTalk);
+    newShareJson.insert("share_type", shareType);
+    newShareJson.insert("share_with", shareShareWith);
+    newShareJson.insert("share_with_displayname", shareShareWithDisplayName);
+    newShareJson.insert("token", shareToken);
+    newShareJson.insert("name", linkShareName);
+    newShareJson.insert("label", linkShareLabel);
+    newShareJson.insert("url", linkShareUrl);
+
+    return newShareJson;
+}
+
+QByteArray FakeShareDefinition::toRequestReply() const
+{
+    const auto shareJson = toShareJsonObject();
+    return jsonValueToOccReply(shareJson);
+}
+
+// Below is ShareTestHelper
+ShareTestHelper::ShareTestHelper(QObject *parent)
+    : QObject(parent)
+{
+}
+
+ShareTestHelper::~ShareTestHelper()
+{
+    const auto folder = FolderMan::instance()->folder(fakeFolder.localPath());
+    if (folder) {
+        FolderMan::instance()->removeFolder(folder);
+    }
+    AccountManager::instance()->deleteAccount(accountState.data());
+}
+
+void ShareTestHelper::setup()
+{
+    _fakeQnam.reset(new FakeQNAM({}));
+    _fakeQnam->setOverride([this](const QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) {
+        return qnamOverride(op, req, device);
+    });
+
+    account = Account::create();
+    account->setCredentials(new FakeCredentials{_fakeQnam.data()});
+    account->setUrl(QUrl(("owncloud://somehost/owncloud")));
+    account->setCapabilities(_fakeCapabilities);
+    accountState = new AccountState(account);
+    AccountManager::instance()->addAccount(account);
+
+    QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+    fakeFolder.localModifier().insert(testFileName);
+
+    const auto folderMan = FolderMan::instance();
+    QCOMPARE(folderMan, &fm);
+    QVERIFY(folderMan->addFolder(accountState.data(), folderDefinition(fakeFolder.localPath())));
+    const auto folder = FolderMan::instance()->folder(fakeFolder.localPath());
+    QVERIFY(folder);
+    QVERIFY(fakeFolder.syncOnce());
+    QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+    const auto fakeFileInfo = fakeFolder.remoteModifier().find(testFileName);
+    QVERIFY(fakeFileInfo);
+    fakeFileInfo->permissions.setPermission(RemotePermissions::CanReshare);
+    QVERIFY(fakeFolder.syncOnce());
+    QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+    QVERIFY(fakeFileInfo->permissions.CanReshare);
+
+    _fakeCapabilities = QVariantMap {
+        {QStringLiteral("files_sharing"), QVariantMap {
+            {QStringLiteral("api_enabled"), true},
+            {QStringLiteral("default_permissions"), 19},
+            {QStringLiteral("public"), QVariantMap {
+                {QStringLiteral("enabled"), true},
+                {QStringLiteral("expire_date"), QVariantMap {
+                    {QStringLiteral("days"), 30},
+                    {QStringLiteral("enforced"), false},
+                }},
+                {QStringLiteral("expire_date_internal"), QVariantMap {
+                     {QStringLiteral("days"), 30},
+                     {QStringLiteral("enforced"), false},
+                }},
+                {QStringLiteral("expire_date_remote"), QVariantMap {
+                     {QStringLiteral("days"), 30},
+                     {QStringLiteral("enforced"), false},
+                }},
+                {QStringLiteral("password"), QVariantMap {
+                    {QStringLiteral("enforced"), false},
+                }},
+            }},
+            {QStringLiteral("sharebymail"), QVariantMap {
+                {QStringLiteral("enabled"), true},
+                {QStringLiteral("password"), QVariantMap {
+                    {QStringLiteral("enforced"), false},
+                }},
+            }},
+        }},
+    };
+
+   // Generate test data
+   // Properties that apply to the file generally
+    const auto fileOwnerUid = account->davUser();
+    const auto fileOwnerDisplayName = account->davDisplayName();
+    const auto fileTarget = QString(QStringLiteral("/") + fakeFileInfo->name);
+    const auto fileHasPreview = true;
+    const auto fileFileParent = QString(fakeFolder.remoteModifier().fileId);
+    const auto fileSource = QString(fakeFileInfo->fileId);
+    const auto fileItemSource = fileSource;
+    const auto fileItemType = QStringLiteral("file");
+    const auto fileMailSend = 0;
+    const auto fileMimeType = QStringLiteral("text/markdown");
+    const auto fileParent = QString();
+    const auto filePath = fakeFileInfo->path();
+    const auto fileStorage = 3;
+    const auto fileStorageId = QString(QStringLiteral("home::") + account->davUser());
+
+    fakeFileDefinition = FakeFileReplyDefinition {
+        fileOwnerUid,
+        fileOwnerDisplayName,
+        fileTarget,
+        fileHasPreview,
+        fileFileParent,
+        fileSource,
+        fileItemSource,
+        fileItemType,
+        fileMailSend,
+        fileMimeType,
+        fileParent,
+        filePath,
+        fileStorage,
+        fileStorageId,
+    };
+
+    emit setupSucceeded();
+}
+
+QNetworkReply *ShareTestHelper::qnamOverride(QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device)
+{
+    QNetworkReply *reply = nullptr;
+
+    const auto reqUrl = req.url();
+    const auto reqRawPath = reqUrl.path();
+    const auto reqPath = reqRawPath.startsWith("/owncloud/") ? reqRawPath.mid(10) : reqRawPath;
+    qDebug() << req.url() << reqPath << op;
+
+    // Properly formatted PROPFIND URL goes something like:
+    // https://cloud.nextcloud.com/remote.php/dav/files/claudio/Readme.md
+    if(reqPath.endsWith(testFileName) && req.attribute(QNetworkRequest::CustomVerbAttribute) == "PROPFIND") {
+
+        reply = new FakePropfindReply(fakeFolder.remoteModifier(), op, req, this);
+
+    } else if (req.url().toString().startsWith(accountState->account()->url().toString()) &&
+               reqPath.startsWith(QStringLiteral("ocs/v2.php/apps/files_sharing/api/v1/shares"))) {
+
+        if (op == QNetworkAccessManager::PostOperation) {
+            reply = handleSharePostOperation(op, req, device);
+
+        } else if(req.attribute(QNetworkRequest::CustomVerbAttribute) == "DELETE") {
+            reply = handleShareDeleteOperation(op, req, reqPath);
+
+        } else if(op == QNetworkAccessManager::PutOperation) {
+            reply = handleSharePutOperation(op, req, reqPath, device);
+
+        } else if(req.attribute(QNetworkRequest::CustomVerbAttribute) == "GET") {
+            reply = handleShareGetOperation(op, req, reqPath);
+        }
+    } else {
+        reply = new FakeErrorReply(op, req, this, 404, _fake404Response);
+    }
+
+    return reply;
+}
+
+QNetworkReply *ShareTestHelper::handleSharePostOperation(QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device)
+{
+    QNetworkReply *reply = nullptr;
+
+    // POST https://somehost/owncloud/ocs/v2.php/apps/files_sharing/api/v1/shares?format=json
+    // Header: { Ocs-APIREQUEST: true, Content-Type: application/x-www-form-urlencoded, X-Request-ID: 1527752d-e147-4da7-89b8-fb06315a5fad, }
+    // Data: [path=file.md&shareType=3]"
+    const QUrlQuery urlQuery(req.url());
+    const auto formatParam = urlQuery.queryItemValue(QStringLiteral("format"));
+
+    if (formatParam == QStringLiteral("json")) {
+        device->open(QIODevice::ReadOnly);
+        const auto requestBody = device->readAll();
+        device->close();
+
+        const auto requestData = requestBody.split('&');
+        // We don't care about path since we know the file we are testing with
+        auto requestShareType = -10; // Just in case
+        QString requestShareWith;
+        QString requestName;
+        QString requestPassword;
+
+        for(const auto &data : requestData) {
+            const auto requestDataUrl = QUrl::fromPercentEncoding(data);
+            const QString requestDataUrlString(requestDataUrl);
+
+            if (data.contains("shareType=")) {
+                const auto shareTypeString = requestDataUrlString.mid(10);
+                requestShareType = Share::ShareType(shareTypeString.toInt());
+            } else if (data.contains("shareWith=")) {
+                requestShareWith = data.mid(10);
+            } else if (data.contains("name=")) {
+                requestName = data.mid(5);
+            } else if (data.contains("password=")) {
+                requestPassword = data.mid(9);
+            }
+        }
+
+        if (requestPassword.isEmpty() &&
+            ((requestShareType == Share::TypeEmail && account->capabilities().shareEmailPasswordEnforced()) ||
+             (requestShareType == Share::TypeLink && account->capabilities().sharePublicLinkEnforcePassword()))) {
+
+            reply = new FakePayloadReply(op, req, _fake403Response, searchResultsReplyDelay, _fakeQnam.data());
+
+        } else if (requestShareType >= 0) {
+            const auto shareType = Share::ShareType(requestShareType);
+            reply = new FakePayloadReply(op, req, createNewShare(shareType, requestShareWith, requestPassword), searchResultsReplyDelay, _fakeQnam.data());
+        }
+    }
+
+    return reply;
+}
+
+QNetworkReply *ShareTestHelper::handleSharePutOperation(const QNetworkAccessManager::Operation op, const QNetworkRequest &req, const QString &reqPath, QIODevice *device)
+{
+    QNetworkReply *reply = nullptr;
+
+    const auto splitUrlPath = reqPath.split('/');
+    const auto shareId = splitUrlPath.last();
+
+    const QUrlQuery urlQuery(req.url());
+    const auto formatParam = urlQuery.queryItemValue(QStringLiteral("format"));
+
+    if (formatParam == QStringLiteral("json")) {
+        device->open(QIODevice::ReadOnly);
+        const auto requestBody = device->readAll();
+        device->close();
+
+        const auto requestData = requestBody.split('&');
+
+        const auto existingShareIterator = std::find_if(_sharesReplyData.cbegin(), _sharesReplyData.cend(), [&shareId](const QJsonValue &value) {
+            return value.toObject().value("id").toString() == shareId;
+        });
+
+        if (existingShareIterator == _sharesReplyData.cend()) {
+            reply = new FakeErrorReply(op, req, this, 404, _fake404Response);
+        } else {
+            const auto existingShareValue = *existingShareIterator;
+            auto shareObject = existingShareValue.toObject();
+
+            for (const auto &requestDataItem : requestData) {
+                const auto requestSplit = requestDataItem.split('=');
+                auto requestKey = requestSplit.first();
+                auto requestValue = requestSplit.last();
+
+                     // We send expireDate without time but the server returns with time at 00:00:00
+                if (requestKey == "expireDate") {
+                    requestKey = "expiration";
+                    requestValue.append(" 00:00:00");
+                }
+
+                shareObject.insert(QString(requestKey), QString(requestValue));
+            }
+
+            _sharesReplyData.replace(existingShareIterator - _sharesReplyData.cbegin(), shareObject);
+            reply = new FakePayloadReply(op, req, jsonValueToOccReply(shareObject), searchResultsReplyDelay, _fakeQnam.data());
+        }
+    }
+
+    return reply;
+}
+
+
+QNetworkReply *ShareTestHelper::handleShareDeleteOperation(const QNetworkAccessManager::Operation op, const QNetworkRequest &req, const QString &reqPath)
+{
+    QNetworkReply *reply = nullptr;
+
+    const auto splitUrlPath = reqPath.split('/');
+    const auto shareId = splitUrlPath.last();
+
+    const auto existingShareIterator = std::find_if(_sharesReplyData.cbegin(), _sharesReplyData.cend(), [&shareId](const QJsonValue &value) {
+        return value.toObject().value("id").toString() == shareId;
+    });
+
+    if (existingShareIterator == _sharesReplyData.cend()) {
+        reply = new FakeErrorReply(op, req, this, 404, _fake404Response);
+    } else {
+        _sharesReplyData.removeAt(existingShareIterator - _sharesReplyData.cbegin());
+        reply = new FakePayloadReply(op, req, _fake200JsonResponse, searchResultsReplyDelay, _fakeQnam.data());
+    }
+
+    return reply;
+}
+
+QNetworkReply *ShareTestHelper::handleShareGetOperation(const QNetworkAccessManager::Operation op, const QNetworkRequest &req, const QString &reqPath)
+{
+    QNetworkReply *reply = nullptr;
+
+    // Properly formatted request to fetch shares goes something like:
+    // GET https://somehost/owncloud/ocs/v2.php/apps/files_sharing/api/v1/shares?path=file.md&reshares=true&format=json
+    // Header: { Ocs-APIREQUEST: true, Content-Type: application/x-www-form-urlencoded, X-Request-ID: 8ba8960d-ca0d-45ba-abf4-03ab95ba6064, }
+    // Data: []
+    const auto urlQuery = QUrlQuery(req.url());
+    const auto pathParam = urlQuery.queryItemValue(QStringLiteral("path"));
+    const auto resharesParam = urlQuery.queryItemValue(QStringLiteral("reshares"));
+    const auto formatParam = urlQuery.queryItemValue(QStringLiteral("format"));
+
+    if (formatParam != QStringLiteral("json") || (!pathParam.isEmpty() && !pathParam.endsWith(QString(testFileName)))) {
+        reply = new FakeErrorReply(op, req, this, 400, _fake400Response);
+    } else if (reqPath.contains(QStringLiteral("ocs/v2.php/apps/files_sharing/api/v1/shares"))) {
+        reply = new FakePayloadReply(op, req, jsonValueToOccReply(_sharesReplyData), searchResultsReplyDelay, _fakeQnam.data());
+    }
+
+    return reply;
+}
+
+const QByteArray ShareTestHelper::createNewShare(const Share::ShareType shareType, const QString &shareWith, const QString &password)
+{
+    const auto displayString = shareType == Share::TypeLink ? QString() : shareWith;
+    const FakeShareDefinition newShareDefinition(this,
+                                                 shareType,
+                                                 shareWith,
+                                                 displayString,
+                                                 password);
+
+    _sharesReplyData.append(newShareDefinition.toShareJsonObject());
+    return newShareDefinition.toRequestReply();
+}
+
+int ShareTestHelper::shareCount() const
+{
+    return _sharesReplyData.count();
+}
+
+void ShareTestHelper::appendShareReplyData(const FakeShareDefinition &definition)
+{
+    _sharesReplyData.append(definition.toShareJsonObject());
+}
+
+void ShareTestHelper::resetTestShares()
+{
+    _sharesReplyData = QJsonArray();
+}
+
+void ShareTestHelper::resetTestData()
+{
+    resetTestShares();
+    account->setCapabilities(_fakeCapabilities);
+}
diff --git a/test/sharetestutils.h b/test/sharetestutils.h
new file mode 100644 (file)
index 0000000..c539547
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QJsonArray>
+#include <QJsonObject>
+#include <QJsonDocument>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+
+#include "gui/accountmanager.h"
+#include "gui/folderman.h"
+#include "gui/sharemanager.h"
+
+#include "syncenginetestutils.h"
+
+using namespace OCC;
+
+struct FakeFileReplyDefinition
+{
+    QString fileOwnerUid;
+    QString fileOwnerDisplayName;
+    QString fileTarget;
+    bool fileHasPreview;
+    QString fileFileParent;
+    QString fileSource;
+    QString fileItemSource;
+    QString fileItemType;
+    int fileMailSend;
+    QString fileMimeType;
+    QString fileParent;
+    QString filePath;
+    int fileStorage;
+    QString fileStorageId;
+};
+
+struct FakeShareDefinition
+{
+    FakeShareDefinition() = default;
+    FakeShareDefinition(ShareTestHelper *helper,
+                        const Share::ShareType type,
+                        const QString &shareWith,
+                        const QString &displayString,
+                        const QString &password = QString(),
+                        const QString &note = QString(),
+                        const QString &expiration = QString());
+
+    FakeFileReplyDefinition fileDefinition;
+    QString shareId;
+    bool shareCanDelete;
+    bool shareCanEdit;
+    QString shareUidOwner;
+    QString shareDisplayNameOwner;
+    QString sharePassword;
+    int sharePermissions;
+    QString shareNote;
+    int shareHideDownload;
+    QString shareExpiration;
+    bool shareSendPasswordByTalk;
+    int shareType;
+    QString shareShareWith;
+    QString shareShareWithDisplayName;
+    QString shareToken;
+    QString linkShareName;
+    QString linkShareLabel;
+    QString linkShareUrl;
+
+    [[nodiscard]] QJsonObject toShareJsonObject() const;
+    [[nodiscard]] QByteArray toRequestReply() const;
+};
+
+class ShareTestHelper : public QObject
+{
+    Q_OBJECT
+
+public:
+    ShareTestHelper(QObject *parent = nullptr);
+    ~ShareTestHelper() override;
+
+    FolderMan fm;
+    FakeFolder fakeFolder{FileInfo{}};
+    FakeFileReplyDefinition fakeFileDefinition;
+
+    AccountPtr account;
+    AccountStatePtr accountState;
+
+    int latestShareId = 0;
+
+    static constexpr auto testFileName = "file.md";
+    static constexpr auto searchResultsReplyDelay = 100;
+    static constexpr auto expectedDtFormat = "yyyy-MM-dd 00:00:00";
+
+    const QByteArray createNewShare(const Share::ShareType shareType, const QString &shareWith, const QString &password);
+    [[nodiscard]] int shareCount() const;
+
+signals:
+    void setupSucceeded();
+
+public slots:
+    void setup();
+    void appendShareReplyData(const FakeShareDefinition &definition);
+    void resetTestShares();
+    void resetTestData();
+
+private slots:
+    [[nodiscard]] QNetworkReply *qnamOverride(const QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device);
+    [[nodiscard]] QNetworkReply *handleSharePostOperation(const QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device);
+    [[nodiscard]] QNetworkReply *handleSharePutOperation(const QNetworkAccessManager::Operation op, const QNetworkRequest &req, const QString &reqPath, QIODevice *device);
+    [[nodiscard]] QNetworkReply *handleShareDeleteOperation(const QNetworkAccessManager::Operation op, const QNetworkRequest &req, const QString &reqPath);
+    [[nodiscard]] QNetworkReply *handleShareGetOperation(const QNetworkAccessManager::Operation op, const QNetworkRequest &req, const QString &reqPath);
+
+private:
+    QScopedPointer<FakeQNAM> _fakeQnam;
+
+    QByteArray _fake404Response = R"({"ocs":{"meta":{"status":"failure","statuscode":404,"message":"Invalid query, please check the syntax. API specifications are here: http:\/\/www.freedesktop.org\/wiki\/Specifications\/open-collaboration-services.\n"},"data":[]}})";
+    QByteArray _fake403Response = R"({"ocs":{"meta":{"status":"failure","statuscode":403,"message":"Operation not allowed."},"data":[]}})";
+    QByteArray _fake400Response = R"({"ocs":{"meta":{"status":"failure","statuscode":400,"message":"Parameter is incorrect.\n"},"data":[]}})";
+    QByteArray _fake200JsonResponse = R"({"ocs":{"data":[],"meta":{"message":"OK","status":"ok","statuscode":200}}})";
+
+    QJsonArray _sharesReplyData;
+    QVariantMap _fakeCapabilities;
+    QSet<int> _liveShareIds;
+};
index 8ba0f151b2e07b5e2d2be32d7958e79b1dac90c3..2a540965f40d3ef50fc79175cc7bf9801999c6b6 100644 (file)
@@ -1,4 +1,6 @@
 #include "testhelper.h"
+#include <QJsonObject>
+#include <QJsonDocument>
 
 OCC::FolderDefinition folderDefinition(const QString &path)
 {
@@ -8,3 +10,19 @@ OCC::FolderDefinition folderDefinition(const QString &path)
     d.alias = path;
     return d;
 }
+
+
+const QByteArray jsonValueToOccReply(const QJsonValue &jsonValue)
+{
+    QJsonObject root;
+    QJsonObject ocs;
+    QJsonObject meta;
+
+    meta.insert("statuscode", 200);
+
+    ocs.insert(QStringLiteral("data"), jsonValue);
+    ocs.insert(QStringLiteral("meta"), meta);
+    root.insert(QStringLiteral("ocs"), ocs);
+
+    return QJsonDocument(root).toJson();
+}
index 40379577cb3a325418586d7c8612007aed761563..2ddb859e599d4fe9f0faf4a57725f1ce2ac65d74 100644 (file)
@@ -18,4 +18,6 @@ public:
 
 OCC::FolderDefinition folderDefinition(const QString &path);
 
+const QByteArray jsonValueToOccReply(const QJsonValue &jsonValue);
+
 #endif // TESTHELPER_H
index 04c5fcd05e8239f27ba0fd3870d8542788572c5c..9a3b10853048f063856327b13996dbbc95b2ef79 100644 (file)
 #include "gui/filedetails/sharemodel.h"
 
 #include <QTest>
+#include <QAbstractItemModelTester>
 #include <QSignalSpy>
 #include <QFileInfo>
 #include <QFlags>
+#include <QDateTime>
+#include <QTimeZone>
 
-#include "accountmanager.h"
-#include "folderman.h"
-#include "syncenginetestutils.h"
-#include "testhelper.h"
+#include "sharetestutils.h"
 #include "libsync/theme.h"
 
 using namespace OCC;
 
-static QByteArray fake404Response = R"(
-{"ocs":{"meta":{"status":"failure","statuscode":404,"message":"Invalid query, please check the syntax. API specifications are here: http:\/\/www.freedesktop.org\/wiki\/Specifications\/open-collaboration-services.\n"},"data":[]}}
-)";
-
-static QByteArray fake403Response = R"(
-{"ocs":{"meta":{"status":"failure","statuscode":403,"message":"Operation not allowed."},"data":[]}}
-)";
-
-static QByteArray fake400Response = R"(
-{"ocs":{"meta":{"status":"failure","statuscode":400,"message":"Parameter is incorrect.\n"},"data":[]}}
-)";
-
-static QByteArray fake200JsonResponse = R"(
-{"ocs":{"data":[],"meta":{"message":"OK","status":"ok","statuscode":200}}}
-)";
-
-constexpr auto testFileName = "file.md";
-constexpr auto searchResultsReplyDelay = 100;
-constexpr auto expectedDtFormat = "yyyy-MM-dd 00:00:00";
-
 class TestShareModel : public QObject
 {
     Q_OBJECT
 
-public:
-    TestShareModel() = default;
-    ~TestShareModel() override
-    {
-        const auto folder = FolderMan::instance()->folder(_fakeFolder.localPath());
-        if (folder) {
-            FolderMan::instance()->removeFolder(folder);
-        }
-        AccountManager::instance()->deleteAccount(_accountState.data());
-    }
-
-    struct FakeFileReplyDefinition
-    {
-        QString fileOwnerUid;
-        QString fileOwnerDisplayName;
-        QString fileTarget;
-        bool fileHasPreview;
-        QString fileFileParent;
-        QString fileSource;
-        QString fileItemSource;
-        QString fileItemType;
-        int fileMailSend;
-        QString fileMimeType;
-        QString fileParent;
-        QString filePath;
-        int fileStorage;
-        QString fileStorageId;
-    };
-
-    struct FakeShareDefinition
-    {
-        FakeFileReplyDefinition fileDefinition;
-        QString shareId;
-        bool shareCanDelete;
-        bool shareCanEdit;
-        QString shareUidOwner;
-        QString shareDisplayNameOwner;
-        QString sharePassword;
-        int sharePermissions;
-        QString shareNote;
-        int shareHideDownload;
-        QString shareExpiration;
-        bool shareSendPasswordByTalk;
-        int shareType;
-        QString shareShareWith;
-        QString shareShareWithDisplayName;
-        QString shareToken;
-        QString linkShareName;
-        QString linkShareLabel;
-        QString linkShareUrl;
-    };
-
-    const QByteArray fakeSharesResponse() const
-    {
-        QJsonObject root;
-        QJsonObject ocs;
-        QJsonObject meta;
-
-        meta.insert("statuscode", 200);
-
-        ocs.insert(QStringLiteral("data"), _sharesReplyData);
-        ocs.insert(QStringLiteral("meta"), meta);
-        root.insert(QStringLiteral("ocs"), ocs);
-
-        return QJsonDocument(root).toJson();
-    }
-
-    QJsonObject shareDefinitionToJson(const FakeShareDefinition &definition)
-    {
-        QJsonObject newShareJson;
-        newShareJson.insert("uid_file_owner", definition.fileDefinition.fileOwnerUid);
-        newShareJson.insert("displayname_file_owner", definition.fileDefinition.fileOwnerDisplayName);
-        newShareJson.insert("file_target", definition.fileDefinition.fileTarget);
-        newShareJson.insert("has_preview", definition.fileDefinition.fileHasPreview);
-        newShareJson.insert("file_parent", definition.fileDefinition.fileFileParent);
-        newShareJson.insert("file_source", definition.fileDefinition.fileSource);
-        newShareJson.insert("item_source", definition.fileDefinition.fileItemSource);
-        newShareJson.insert("item_type", definition.fileDefinition.fileItemType);
-        newShareJson.insert("mail_send", definition.fileDefinition.fileMailSend);
-        newShareJson.insert("mimetype", definition.fileDefinition.fileMimeType);
-        newShareJson.insert("parent", definition.fileDefinition.fileParent);
-        newShareJson.insert("path", definition.fileDefinition.filePath);
-        newShareJson.insert("storage", definition.fileDefinition.fileStorage);
-        newShareJson.insert("storage_id", definition.fileDefinition.fileStorageId);
-        newShareJson.insert("id", definition.shareId);
-        newShareJson.insert("can_delete", definition.shareCanDelete);
-        newShareJson.insert("can_edit", definition.shareCanEdit);
-        newShareJson.insert("uid_owner", definition.shareUidOwner);
-        newShareJson.insert("displayname_owner", definition.shareDisplayNameOwner);
-        newShareJson.insert("password", definition.sharePassword);
-        newShareJson.insert("permissions", definition.sharePermissions);
-        newShareJson.insert("note", definition.shareNote);
-        newShareJson.insert("hide_download", definition.shareHideDownload);
-        newShareJson.insert("expiration", definition.shareExpiration);
-        newShareJson.insert("send_password_by_talk", definition.shareSendPasswordByTalk);
-        newShareJson.insert("share_type", definition.shareType);
-        newShareJson.insert("share_with", definition.shareShareWith); // Doesn't seem to make sense but is server behaviour
-        newShareJson.insert("share_with_displayname", definition.shareShareWithDisplayName);
-        newShareJson.insert("token", definition.shareToken);
-        newShareJson.insert("name", definition.linkShareName);
-        newShareJson.insert("label", definition.linkShareLabel);
-        newShareJson.insert("url", definition.linkShareUrl);
-
-        return newShareJson;
-    }
-
-    void appendShareReplyData(const FakeShareDefinition &definition)
-    {
-        const auto shareJson = shareDefinitionToJson(definition);
-        _sharesReplyData.append(shareJson);
-    }
-
-    const QByteArray createNewShare(const Share::ShareType shareType, const QString &shareWith)
-    {
-        ++_latestShareId;
-        const auto newShareId = QString::number(_latestShareId);
-        const auto newShareCanDelete = true;
-        const auto newShareCanEdit = true;
-        const auto newShareUidOwner = _account->davUser();
-        const auto newShareDisplayNameOwner = _account->davDisplayName();
-        const auto newSharePassword = QString();
-        const auto newSharePermissions = static_cast<int>(SharePermissions(SharePermissionRead |
-                                                                           SharePermissionUpdate |
-                                                                           SharePermissionCreate |
-                                                                           SharePermissionDelete |
-                                                                           SharePermissionShare));
-        const auto newShareNote = QString();
-        const auto newShareHideDownload = 0;
-        const auto newShareExpiration = QString();
-        const auto newShareSendPasswordByTalk = false;
-        const auto newShareType = shareType;
-        const auto newShareShareWith = shareType == Share::TypeLink ? newSharePassword : shareWith;
-        const auto newShareShareWithDisplayName = shareType == Share::TypeLink ? QStringLiteral("(Shared Link)") : shareWith;
-        const auto newShareToken = QString::number(qHash(newShareId + _fakeFileDefinition.filePath));
-        const auto newLinkShareName = QString();
-        const auto newLinkShareLabel = QString();
-        const auto newLinkShareUrl = shareType == Share::TypeLink ? QString(_account->davUrl().toString() + QStringLiteral("/s/") + newShareToken) : QString();
-
-        const FakeShareDefinition newShareDefinition {
-            _fakeFileDefinition,
-            newShareId,
-            newShareCanDelete,
-            newShareCanEdit,
-            newShareUidOwner,
-            newShareDisplayNameOwner,
-            newSharePassword,
-            newSharePermissions,
-            newShareNote,
-            newShareHideDownload,
-            newShareExpiration,
-            newShareSendPasswordByTalk,
-            newShareType,
-            newShareShareWith,
-            newShareShareWithDisplayName,
-            newShareToken,
-            newLinkShareName,
-            newLinkShareLabel,
-            newLinkShareUrl,
-        };
-
-        const auto shareJson = shareDefinitionToJson(newShareDefinition);
-        _sharesReplyData.append(shareJson);
-        return shareWrappedAsReply(shareJson);
-    }
-
-    QByteArray shareWrappedAsReply(const QJsonObject &shareObject)
-    {
-        QJsonObject root;
-        QJsonObject ocs;
-        QJsonObject meta;
-
-        meta.insert("statuscode", 200);
-
-        ocs.insert(QStringLiteral("data"), shareObject);
-        ocs.insert(QStringLiteral("meta"), meta);
-        root.insert(QStringLiteral("ocs"), ocs);
-
-        return QJsonDocument(root).toJson();
-    }
-
-    void resetTestData()
-    {
-        _sharesReplyData = QJsonArray();
-        _account->setCapabilities(_fakeCapabilities);
-    }
-
 private:
-    FolderMan _fm;
-    FakeFolder _fakeFolder{FileInfo{}};
+    ShareTestHelper helper;
 
-    AccountPtr _account;
-    AccountStatePtr _accountState;
-    QScopedPointer<FakeQNAM> _fakeQnam;
-    FakeFileReplyDefinition _fakeFileDefinition;
     FakeShareDefinition _testLinkShareDefinition;
     FakeShareDefinition _testEmailShareDefinition;
     FakeShareDefinition _testUserShareDefinition;
     FakeShareDefinition _testRemoteShareDefinition;
-    QJsonArray _sharesReplyData;
-    QVariantMap _fakeCapabilities;
-    QSet<int> _liveShareIds;
-    int _latestShareId = 0;
 
 private slots:
     void initTestCase()
     {
-        _fakeQnam.reset(new FakeQNAM({}));
-        _fakeQnam->setOverride([this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) {
-            QNetworkReply *reply = nullptr;
-
-            const auto reqUrl = req.url();
-            const auto reqRawPath = reqUrl.path();
-            const auto reqPath = reqRawPath.startsWith("/owncloud/") ? reqRawPath.mid(10) : reqRawPath;
-            qDebug() << req.url() << reqPath << op;
-
-            // Properly formatted PROPFIND URL goes something like:
-            // https://cloud.nextcloud.com/remote.php/dav/files/claudio/Readme.md
-            if(reqPath.endsWith(testFileName) && req.attribute(QNetworkRequest::CustomVerbAttribute) == "PROPFIND") {
-
-                reply = new FakePropfindReply(_fakeFolder.remoteModifier(), op, req, this);
-
-            } else if (req.url().toString().startsWith(_accountState->account()->url().toString()) &&
-                       reqPath.startsWith(QStringLiteral("ocs/v2.php/apps/files_sharing/api/v1/shares")) &&
-                       op == QNetworkAccessManager::PostOperation) {
-
-                // POST https://somehost/owncloud/ocs/v2.php/apps/files_sharing/api/v1/shares?format=json
-                // Header: { Ocs-APIREQUEST: true, Content-Type: application/x-www-form-urlencoded, X-Request-ID: 1527752d-e147-4da7-89b8-fb06315a5fad, }
-                // Data: [path=file.md&shareType=3]"
-                const QUrlQuery urlQuery(req.url());
-                const auto formatParam = urlQuery.queryItemValue(QStringLiteral("format"));
-
-                if (formatParam == QStringLiteral("json")) {
-                    device->open(QIODevice::ReadOnly);
-                    const auto requestBody = device->readAll();
-                    device->close();
-
-                    const auto requestData = requestBody.split('&');
-                    // We don't care about path since we know the file we are testing with
-                    auto requestShareType = -10; // Just in case
-                    QString requestShareWith;
-                    QString requestName;
-                    QString requestPassword;
-
-                    for(const auto &data : requestData) {
-                        const auto requestDataUrl = QUrl::fromPercentEncoding(data);
-                        const QString requestDataUrlString(requestDataUrl);
-
-                        if (data.contains("shareType=")) {
-                            const auto shareTypeString = requestDataUrlString.mid(10);
-                            requestShareType = Share::ShareType(shareTypeString.toInt());
-                        } else if (data.contains("shareWith=")) {
-                            requestShareWith = data.mid(10);
-                        } else if (data.contains("name=")) {
-                            requestName = data.mid(5);
-                        } else if (data.contains("password=")) {
-                            requestPassword = data.mid(9);
-                        }
-                    }
-
-                    if (requestPassword.isEmpty() &&
-                            ((requestShareType == Share::TypeEmail && _account->capabilities().shareEmailPasswordEnforced()) ||
-                            (requestShareType == Share::TypeLink && _account->capabilities().sharePublicLinkEnforcePassword()))) {
-
-                        reply = new FakePayloadReply(op, req, fake403Response, searchResultsReplyDelay, _fakeQnam.data());
-
-                    } else if (requestShareType >= 0) {
-                        const auto shareType = Share::ShareType(requestShareType);
-                        reply = new FakePayloadReply(op, req, createNewShare(shareType, requestShareWith), searchResultsReplyDelay, _fakeQnam.data());
-                    }
-                }
-
-            } else if(req.url().toString().startsWith(_accountState->account()->url().toString()) &&
-                      reqPath.startsWith(QStringLiteral("ocs/v2.php/apps/files_sharing/api/v1/shares")) &&
-                      req.attribute(QNetworkRequest::CustomVerbAttribute) == "DELETE") {
-
-                const auto splitUrlPath = reqPath.split('/');
-                const auto shareId = splitUrlPath.last();
-
-                const auto existingShareIterator = std::find_if(_sharesReplyData.cbegin(), _sharesReplyData.cend(), [&shareId](const QJsonValue &value) {
-                    return value.toObject().value("id").toString() == shareId;
-                });
-
-                if (existingShareIterator == _sharesReplyData.cend()) {
-                    reply = new FakeErrorReply(op, req, this, 404, fake404Response);
-                } else {
-                    _sharesReplyData.removeAt(existingShareIterator - _sharesReplyData.cbegin());
-                    reply = new FakePayloadReply(op, req, fake200JsonResponse, searchResultsReplyDelay, _fakeQnam.data());
-                }
-
-            } else if(req.url().toString().startsWith(_accountState->account()->url().toString()) &&
-                      reqPath.startsWith(QStringLiteral("ocs/v2.php/apps/files_sharing/api/v1/shares")) &&
-                      op == QNetworkAccessManager::PutOperation) {
-
-                const auto splitUrlPath = reqPath.split('/');
-                const auto shareId = splitUrlPath.last();
-
-                const QUrlQuery urlQuery(req.url());
-                const auto formatParam = urlQuery.queryItemValue(QStringLiteral("format"));
-
-                if (formatParam == QStringLiteral("json")) {
-                    device->open(QIODevice::ReadOnly);
-                    const auto requestBody = device->readAll();
-                    device->close();
-
-                    const auto requestData = requestBody.split('&');
-
-                    const auto existingShareIterator = std::find_if(_sharesReplyData.cbegin(), _sharesReplyData.cend(), [&shareId](const QJsonValue &value) {
-                        return value.toObject().value("id").toString() == shareId;
-                    });
-
-                    if (existingShareIterator == _sharesReplyData.cend()) {
-                        reply = new FakeErrorReply(op, req, this, 404, fake404Response);
-                    } else {
-                        const auto existingShareValue = *existingShareIterator;
-                        auto shareObject = existingShareValue.toObject();
-
-                        for (const auto &requestDataItem : requestData) {
-                            const auto requestSplit = requestDataItem.split('=');
-                            auto requestKey = requestSplit.first();
-                            auto requestValue = requestSplit.last();
-
-                            // We send expireDate without time but the server returns with time at 00:00:00
-                            if (requestKey == "expireDate") {
-                                requestKey = "expiration";
-                                requestValue.append(" 00:00:00");
-                            }
-
-                            shareObject.insert(QString(requestKey), QString(requestValue));
-                        }
-
-                        _sharesReplyData.replace(existingShareIterator - _sharesReplyData.cbegin(), shareObject);
-                        reply = new FakePayloadReply(op, req, shareWrappedAsReply(shareObject), searchResultsReplyDelay, _fakeQnam.data());
-                    }
-                }
-
-            } else if(req.url().toString().startsWith(_accountState->account()->url().toString()) &&
-                      reqPath.startsWith(QStringLiteral("ocs/v2.php/apps/files_sharing/api/v1/shares")) &&
-                      req.attribute(QNetworkRequest::CustomVerbAttribute) == "GET") {
-
-                // Properly formatted request to fetch shares goes something like:
-                // GET https://somehost/owncloud/ocs/v2.php/apps/files_sharing/api/v1/shares?path=file.md&reshares=true&format=json
-                // Header: { Ocs-APIREQUEST: true, Content-Type: application/x-www-form-urlencoded, X-Request-ID: 8ba8960d-ca0d-45ba-abf4-03ab95ba6064, }
-                // Data: []
-                const auto urlQuery = QUrlQuery(req.url());
-                const auto pathParam = urlQuery.queryItemValue(QStringLiteral("path"));
-                const auto resharesParam = urlQuery.queryItemValue(QStringLiteral("reshares"));
-                const auto formatParam = urlQuery.queryItemValue(QStringLiteral("format"));
-
-                if (formatParam != QStringLiteral("json") || (!pathParam.isEmpty() && pathParam != QString(testFileName))) {
-                    reply = new FakeErrorReply(op, req, this, 400, fake400Response);
-                } else if (reqPath.contains(QStringLiteral("ocs/v2.php/apps/files_sharing/api/v1/shares"))) {
-                    reply = new FakePayloadReply(op, req, fakeSharesResponse(), searchResultsReplyDelay, _fakeQnam.data());
-                }
-
-            } else if (!req.url().toString().startsWith(_accountState->account()->url().toString())) {
-                reply = new FakeErrorReply(op, req, this, 404, fake404Response);
-            } else if (!reply) {
-                return qobject_cast<QNetworkReply*>(new FakeErrorReply(op, req, this, 404, QByteArrayLiteral("{error: \"Not found!\"}")));
-            }
-
-            return reply;
-        });
-
-        _fakeCapabilities = QVariantMap {
-            {QStringLiteral("files_sharing"), QVariantMap {
-                {QStringLiteral("api_enabled"), true},
-                {QStringLiteral("default_permissions"), 19},
-                {QStringLiteral("public"), QVariantMap {
-                    {QStringLiteral("enabled"), true},
-                    {QStringLiteral("expire_date"), QVariantMap {
-                        {QStringLiteral("days"), 30},
-                        {QStringLiteral("enforced"), false},
-                    }},
-                    {QStringLiteral("expire_date_internal"), QVariantMap {
-                         {QStringLiteral("days"), 30},
-                         {QStringLiteral("enforced"), false},
-                    }},
-                    {QStringLiteral("expire_date_remote"), QVariantMap {
-                         {QStringLiteral("days"), 30},
-                         {QStringLiteral("enforced"), false},
-                    }},
-                    {QStringLiteral("password"), QVariantMap {
-                        {QStringLiteral("enforced"), false},
-                    }},
-                }},
-                {QStringLiteral("sharebymail"), QVariantMap {
-                    {QStringLiteral("enabled"), true},
-                    {QStringLiteral("password"), QVariantMap {
-                        {QStringLiteral("enforced"), false},
-                    }},
-                }},
-            }},
-        };
-
-        _account = Account::create();
-        _account->setCredentials(new FakeCredentials{_fakeQnam.data()});
-        _account->setUrl(QUrl(("owncloud://somehost/owncloud")));
-        _account->setCapabilities(_fakeCapabilities);
-        _accountState = new AccountState(_account);
-        AccountManager::instance()->addAccount(_account);
-
-        QCOMPARE(_fakeFolder.currentLocalState(), _fakeFolder.currentRemoteState());
-        _fakeFolder.localModifier().insert(testFileName);
-
-        const auto folderMan = FolderMan::instance();
-        QCOMPARE(folderMan, &_fm);
-        QVERIFY(folderMan->addFolder(_accountState.data(), folderDefinition(_fakeFolder.localPath())));
-        const auto folder = FolderMan::instance()->folder(_fakeFolder.localPath());
-        QVERIFY(folder);
-        QVERIFY(_fakeFolder.syncOnce());
-        QCOMPARE(_fakeFolder.currentLocalState(), _fakeFolder.currentRemoteState());
-        ItemCompletedSpy completeSpy(_fakeFolder);
-
-        const auto fakeFileInfo = _fakeFolder.remoteModifier().find(testFileName);
-        QVERIFY(fakeFileInfo);
-        fakeFileInfo->permissions.setPermission(RemotePermissions::CanReshare);
-        QVERIFY(_fakeFolder.syncOnce());
-        QCOMPARE(_fakeFolder.currentLocalState(), _fakeFolder.currentRemoteState());
-        QVERIFY(fakeFileInfo->permissions.CanReshare);
-
-        // Generate test data
-        // Properties that apply to the file generally
-        const auto fileOwnerUid = _account->davUser();
-        const auto fileOwnerDisplayName = _account->davDisplayName();
-        const auto fileTarget = QString(QStringLiteral("/") + fakeFileInfo->name);
-        const auto fileHasPreview = true;
-        const auto fileFileParent = QString(_fakeFolder.remoteModifier().fileId);
-        const auto fileSource = QString(fakeFileInfo->fileId);
-        const auto fileItemSource = fileSource;
-        const auto fileItemType = QStringLiteral("file");
-        const auto fileMailSend = 0;
-        const auto fileMimeType = QStringLiteral("text/markdown");
-        const auto fileParent = QString();
-        const auto filePath = fakeFileInfo->path();
-        const auto fileStorage = 3;
-        const auto fileStorageId = QString(QStringLiteral("home::") + _account->davUser());
-
-        _fakeFileDefinition = FakeFileReplyDefinition {
-            fileOwnerUid,
-            fileOwnerDisplayName,
-            fileTarget,
-            fileHasPreview,
-            fileFileParent,
-            fileSource,
-            fileItemSource,
-            fileItemType,
-            fileMailSend,
-            fileMimeType,
-            fileParent,
-            filePath,
-            fileStorage,
-            fileStorageId,
-        };
+        QSignalSpy helperSetupSucceeded(&helper, &ShareTestHelper::setupSucceeded);
+        helper.setup();
+        QCOMPARE(helperSetupSucceeded.count(), 1);
 
         const auto testSharePassword = "3|$argon2id$v=19$m=65536,"
                                        "t=4,"
                                        "p=1$M2FoLnliWkhIZkwzWjFBQg$BPraP+JUqP1sV89rkymXpCGxHBlCct6bZ39xUGaYQ5w";
-        const auto testShareToken = "GQ4aLrZEdJJkopW";
-        const auto testShareCanDelete = true;
-        const auto testShareCanEdit = true;
-        const auto testShareUidOwner = _account->davUser();
-        const auto testShareDisplayNameOwner = _account->davDisplayName();
-        const auto testSharePermissions = static_cast<int>(SharePermissions(SharePermissionRead |
-                                                                            SharePermissionUpdate |
-                                                                            SharePermissionCreate |
-                                                                            SharePermissionDelete |
-                                                                            SharePermissionShare));
         const auto testShareNote = QStringLiteral("This is a note!");
-        const auto testShareHideDownload = 0;
-        const auto testShareExpiration = QDate::currentDate().addDays(1).toString(expectedDtFormat);
-        const auto testShareSendPasswordByTalk = false;
-
-        ++_latestShareId;
-        const auto linkShareShareWith = testSharePassword; // Weird, but it's what the server does
-        const auto linkShareShareWithDisplayName = QStringLiteral("(Shared Link)");
-        const auto linkShareUrl = QString(_account->davUrl().toString() + QStringLiteral("/s/") + testShareToken);
-
-        _testLinkShareDefinition = FakeShareDefinition {
-            _fakeFileDefinition,
-            QString::number(_latestShareId),
-            testShareCanDelete,
-            testShareCanEdit,
-            testShareUidOwner,
-            testShareDisplayNameOwner,
-            testSharePassword,
-            testSharePermissions,
-            testShareNote,
-            testShareHideDownload,
-            testShareExpiration,
-            testShareSendPasswordByTalk,
-            Share::TypeLink,
-            linkShareShareWith,
-            linkShareShareWithDisplayName,
-            testShareToken,
-            QStringLiteral("Link share name"),
-            QStringLiteral("Link share label"),
-            linkShareUrl,
-        };
+        const auto testShareExpiration = QDate::currentDate().addDays(1).toString(helper.expectedDtFormat);
+
+        const auto linkShareLabel = QStringLiteral("Link share label");
+        _testLinkShareDefinition = FakeShareDefinition(&helper,
+                                                      Share::TypeLink,
+                                                      {},
+                                                      linkShareLabel,
+                                                      testSharePassword,
+                                                      testShareNote,
+                                                      testShareExpiration);
 
-        ++_latestShareId;
         const auto emailShareShareWith = QStringLiteral("test-email@nextcloud.com");
         const auto emailShareShareWithDisplayName = QStringLiteral("Test email");
+        _testEmailShareDefinition = FakeShareDefinition(&helper,
+                                                        Share::TypeEmail,
+                                                        emailShareShareWith,
+                                                        emailShareShareWithDisplayName,
+                                                        testSharePassword,
+                                                        testShareNote,
+                                                        testShareExpiration);
 
-        _testEmailShareDefinition = FakeShareDefinition {
-            _fakeFileDefinition,
-            QString::number(_latestShareId),
-            testShareCanDelete,
-            testShareCanEdit,
-            testShareUidOwner,
-            testShareDisplayNameOwner,
-            testSharePassword,
-            testSharePermissions,
-            testShareNote,
-            testShareHideDownload,
-            testShareExpiration,
-            testShareSendPasswordByTalk,
-            Share::TypeEmail,
-            emailShareShareWith,
-            emailShareShareWithDisplayName,
-            testShareToken,
-            {},
-            {},
-            {},
-        };
 
-        ++_latestShareId;
         const auto userShareShareWith = QStringLiteral("user");
         const auto userShareShareWithDisplayName("A Nextcloud user");
+        _testUserShareDefinition = FakeShareDefinition(&helper,
+                                                       Share::TypeUser,
+                                                       userShareShareWith,
+                                                       userShareShareWithDisplayName);
+
 
-        _testUserShareDefinition = FakeShareDefinition {
-            _fakeFileDefinition,
-            QString::number(_latestShareId),
-            testShareCanDelete,
-            testShareCanEdit,
-            testShareUidOwner,
-            testShareDisplayNameOwner,
-            testSharePassword,
-            testSharePermissions,
-            testShareNote,
-            testShareHideDownload,
-            testShareExpiration,
-            testShareSendPasswordByTalk,
-            Share::TypeUser,
-            userShareShareWith,
-            userShareShareWithDisplayName,
-            testShareToken,
-            {},
-            {},
-            {},
-        };
 
-        ++_latestShareId;
         const auto remoteShareShareWith = QStringLiteral("remote_share");
         const auto remoteShareShareWithDisplayName("A remote share");
-
-        _testRemoteShareDefinition = FakeShareDefinition {
-           _fakeFileDefinition,
-           QString::number(_latestShareId),
-           testShareCanDelete,
-           testShareCanEdit,
-           testShareUidOwner,
-           testShareDisplayNameOwner,
-           testSharePassword,
-           testSharePermissions,
-           testShareNote,
-           testShareHideDownload,
-           testShareExpiration,
-           testShareSendPasswordByTalk,
-           Share::TypeRemote,
-           remoteShareShareWith,
-           remoteShareShareWithDisplayName,
-           testShareToken,
-           {},
-           {},
-           {},
-       };
+        _testRemoteShareDefinition = FakeShareDefinition(&helper,
+                                                         Share::TypeRemote,
+                                                         remoteShareShareWith,
+                                                         remoteShareShareWithDisplayName);
 
         qRegisterMetaType<ShareePtr>("ShareePtr");
     }
 
     void testSetAccountAndPath()
     {
-        resetTestData();
+        helper.resetTestData();
         // Test with a link share
-        appendShareReplyData(_testLinkShareDefinition);
+        helper.appendShareReplyData(_testLinkShareDefinition);
+        QCOMPARE(helper.shareCount(), 1);
 
         ShareModel model;
         QAbstractItemModelTester modelTester(&model);
@@ -648,16 +109,16 @@ private slots:
         QSignalSpy sharingEnabledChanged(&model, &ShareModel::sharingEnabledChanged);
         QSignalSpy publicLinkSharesEnabledChanged(&model, &ShareModel::publicLinkSharesEnabledChanged);
 
-        model.setAccountState(_accountState.data());
+        model.setAccountState(helper.accountState.data());
         QCOMPARE(accountStateChanged.count(), 1);
 
         // Check all the account-related properties of the model
-        QCOMPARE(model.accountConnected(), _accountState->isConnected());
-        QCOMPARE(model.sharingEnabled(), _account->capabilities().shareAPI());
-        QCOMPARE(model.publicLinkSharesEnabled() && Theme::instance()->linkSharing(), _account->capabilities().sharePublicLink());
+        QCOMPARE(model.accountConnected(), helper.accountState->isConnected());
+        QCOMPARE(model.sharingEnabled(), helper.account->capabilities().shareAPI());
+        QCOMPARE(model.publicLinkSharesEnabled() && Theme::instance()->linkSharing(), helper.account->capabilities().sharePublicLink());
         QCOMPARE(Theme::instance()->userGroupSharing(), model.userGroupSharingEnabled());
 
-        const QString localPath(_fakeFolder.localPath() + testFileName);
+        const QString localPath(helper.fakeFolder.localPath() + helper.testFileName);
         model.setLocalPath(localPath);
         QCOMPARE(localPathChanged.count(), 1);
         QCOMPARE(model.localPath(), localPath);
@@ -665,11 +126,12 @@ private slots:
 
     void testSuccessfulFetchShares()
     {
-        resetTestData();
+        helper.resetTestData();
         // Test with a link share and a user/group email share "from the server"
-        appendShareReplyData(_testLinkShareDefinition);
-        appendShareReplyData(_testEmailShareDefinition);
-        appendShareReplyData(_testUserShareDefinition);
+        helper.appendShareReplyData(_testLinkShareDefinition);
+        helper.appendShareReplyData(_testEmailShareDefinition);
+        helper.appendShareReplyData(_testUserShareDefinition);
+        QCOMPARE(helper.shareCount(), 3);
 
         ShareModel model;
         QAbstractItemModelTester modelTester(&model);
@@ -677,18 +139,18 @@ private slots:
 
         QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
 
-        model.setAccountState(_accountState.data());
-        model.setLocalPath(_fakeFolder.localPath() + testFileName);
+        model.setAccountState(helper.accountState.data());
+        model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
 
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(model.rowCount(), helper.shareCount());
     }
 
     void testFetchSharesFailedError()
     {
-        resetTestData();
+        helper.resetTestData();
         // Test with a link share "from the server"
-        appendShareReplyData(_testLinkShareDefinition);
+        helper.appendShareReplyData(_testLinkShareDefinition);
 
         ShareModel model;
         QAbstractItemModelTester modelTester(&model);
@@ -697,8 +159,8 @@ private slots:
         QSignalSpy serverError(&model, &ShareModel::serverError);
 
         // Test fetching the shares of a file that does not exist
-        model.setAccountState(_accountState.data());
-        model.setLocalPath(_fakeFolder.localPath() + "wrong-filename-oops.md");
+        model.setAccountState(helper.accountState.data());
+        model.setLocalPath(helper.fakeFolder.localPath() + "wrong-filename-oops.md");
         QVERIFY(serverError.wait(3000));
         QCOMPARE(model.hasInitialShareFetchCompleted(), true);
         QCOMPARE(model.rowCount(), 0); // Make sure no placeholder
@@ -706,10 +168,11 @@ private slots:
 
     void testCorrectFetchOngoingSignalling()
     {
-        resetTestData();
+        helper.resetTestData();
 
         // Test with a link share "from the server"
-        appendShareReplyData(_testLinkShareDefinition);
+        helper.appendShareReplyData(_testLinkShareDefinition);
+        QCOMPARE(helper.shareCount(), 1);
 
         ShareModel model;
         QAbstractItemModelTester modelTester(&model);
@@ -720,11 +183,11 @@ private slots:
         // Make sure we are correctly signalling the loading state of the fetch
         // Model resets twice when we set account and local path, resetting all model state.
 
-        model.setAccountState(_accountState.data());
+        model.setAccountState(helper.accountState.data());
         QCOMPARE(fetchOngoingChanged.count(), 1);
         QCOMPARE(model.fetchOngoing(), false);
 
-        model.setLocalPath(_fakeFolder.localPath() + testFileName);
+        model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
         // If we can grab shares it then indicates fetch ongoing...
         QCOMPARE(fetchOngoingChanged.count(), 3);
         QCOMPARE(model.fetchOngoing(), true);
@@ -736,10 +199,11 @@ private slots:
 
     void testCorrectInitialFetchCompleteSignalling()
     {
-        resetTestData();
+        helper.resetTestData();
 
         // Test with a link share "from the server"
-        appendShareReplyData(_testLinkShareDefinition);
+        helper.appendShareReplyData(_testLinkShareDefinition);
+        QCOMPARE(helper.shareCount(), 1);
 
         ShareModel model;
         QAbstractItemModelTester modelTester(&model);
@@ -752,12 +216,12 @@ private slots:
         // Make sure we are correctly signalling the loading state of the fetch
         // Model resets twice when we set account and local path, resetting all model state.
 
-        model.setAccountState(_accountState.data());
+        model.setAccountState(helper.accountState.data());
         QCOMPARE(accountStateChanged.count(), 1);
         QCOMPARE(hasInitialShareFetchCompletedChanged.count(), 1);
         QCOMPARE(model.hasInitialShareFetchCompleted(), false);
 
-        model.setLocalPath(_fakeFolder.localPath() + testFileName);
+        model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
         QCOMPARE(localPathChanged.count(), 1);
         QCOMPARE(hasInitialShareFetchCompletedChanged.count(), 2);
         QCOMPARE(model.hasInitialShareFetchCompleted(), false);
@@ -771,9 +235,10 @@ private slots:
     // Link shares and user group shares have slightly different behaviour in model.data()
     void testModelLinkShareData()
     {
-        resetTestData();
+        helper.resetTestData();
         // Test with a link share "from the server"
-        appendShareReplyData(_testLinkShareDefinition);
+        helper.appendShareReplyData(_testLinkShareDefinition);
+        QCOMPARE(helper.shareCount(), 1);
 
         ShareModel model;
         QAbstractItemModelTester modelTester(&model);
@@ -781,11 +246,11 @@ private slots:
 
         QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
 
-        model.setAccountState(_accountState.data());
-        model.setLocalPath(_fakeFolder.localPath() + testFileName);
+        model.setAccountState(helper.accountState.data());
+        model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
 
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
         const auto shareIndex = model.index(model.rowCount() - 1, 0, {});
         QVERIFY(!shareIndex.data(Qt::DisplayRole).toString().isEmpty());
@@ -801,7 +266,7 @@ private slots:
         QCOMPARE(shareIndex.data(ShareModel::PasswordRole).toString(), QString());
         QCOMPARE(shareIndex.data(ShareModel::EditingAllowedRole).toBool(), SharePermissions(_testLinkShareDefinition.sharePermissions).testFlag(SharePermissionUpdate));
 
-        const auto expectedLinkShareExpireDate = QDate::fromString(_testLinkShareDefinition.shareExpiration, expectedDtFormat);
+        const auto expectedLinkShareExpireDate = QDate::fromString(_testLinkShareDefinition.shareExpiration, helper.expectedDtFormat);
         QCOMPARE(shareIndex.data(ShareModel::ExpireDateEnabledRole).toBool(), expectedLinkShareExpireDate.isValid());
         QCOMPARE(shareIndex.data(ShareModel::ExpireDateRole).toLongLong(), expectedLinkShareExpireDate.startOfDay(Qt::UTC).toMSecsSinceEpoch());
 
@@ -811,9 +276,10 @@ private slots:
 
     void testModelEmailShareData()
     {
-        resetTestData();
+        helper.resetTestData();
         // Test with a user/group email share "from the server"
-        appendShareReplyData(_testEmailShareDefinition);
+        helper.appendShareReplyData(_testEmailShareDefinition);
+        QCOMPARE(helper.shareCount(), 1);
 
         ShareModel model;
         QAbstractItemModelTester modelTester(&model);
@@ -821,8 +287,8 @@ private slots:
 
         QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
 
-        model.setAccountState(_accountState.data());
-        model.setLocalPath(_fakeFolder.localPath() + testFileName);
+        model.setAccountState(helper.accountState.data());
+        model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
 
         QVERIFY(sharesChanged.wait(5000));
         QCOMPARE(model.rowCount(), 2); // Remember about placeholder link share
@@ -838,7 +304,7 @@ private slots:
         QCOMPARE(shareIndex.data(ShareModel::PasswordRole).toString(), QString());
         QCOMPARE(shareIndex.data(ShareModel::EditingAllowedRole).toBool(), SharePermissions(_testEmailShareDefinition.sharePermissions).testFlag(SharePermissionUpdate));
 
-        const auto expectedShareExpireDate = QDate::fromString(_testEmailShareDefinition.shareExpiration, expectedDtFormat);
+        const auto expectedShareExpireDate = QDate::fromString(_testEmailShareDefinition.shareExpiration, helper.expectedDtFormat);
         QCOMPARE(shareIndex.data(ShareModel::ExpireDateEnabledRole).toBool(), expectedShareExpireDate.isValid());
         QCOMPARE(shareIndex.data(ShareModel::ExpireDateRole).toLongLong(), expectedShareExpireDate.startOfDay(Qt::UTC).toMSecsSinceEpoch());
 
@@ -848,9 +314,10 @@ private slots:
 
     void testModelUserShareData()
     {
-        resetTestData();
+        helper.resetTestData();
         // Test with a user/group user share "from the server"
-        appendShareReplyData(_testUserShareDefinition);
+        helper.appendShareReplyData(_testUserShareDefinition);
+        QCOMPARE(helper.shareCount(), 1);
 
         ShareModel model;
         QAbstractItemModelTester modelTester(&model);
@@ -858,8 +325,8 @@ private slots:
 
         QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
 
-        model.setAccountState(_accountState.data());
-        model.setLocalPath(_fakeFolder.localPath() + testFileName);
+        model.setAccountState(helper.accountState.data());
+        model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
 
         QVERIFY(sharesChanged.wait(5000));
         QCOMPARE(model.rowCount(), 2); // Remember about placeholder link share
@@ -875,7 +342,7 @@ private slots:
         QCOMPARE(shareIndex.data(ShareModel::PasswordRole).toString(), QString());
         QCOMPARE(shareIndex.data(ShareModel::EditingAllowedRole).toBool(), SharePermissions(_testUserShareDefinition.sharePermissions).testFlag(SharePermissionUpdate));
 
-        const auto expectedShareExpireDate = QDate::fromString(_testUserShareDefinition.shareExpiration, expectedDtFormat);
+        const auto expectedShareExpireDate = QDate::fromString(_testUserShareDefinition.shareExpiration, helper.expectedDtFormat);
         QCOMPARE(shareIndex.data(ShareModel::ExpireDateEnabledRole).toBool(), expectedShareExpireDate.isValid());
         QCOMPARE(shareIndex.data(ShareModel::ExpireDateRole).toLongLong(), expectedShareExpireDate.startOfDay(Qt::UTC).toMSecsSinceEpoch());
 
@@ -885,17 +352,18 @@ private slots:
         // Check correct user avatar
         const auto avatarUrl = shareIndex.data(ShareModel::AvatarUrlRole).toString();
         const auto relativeAvatarPath = QString("remote.php/dav/avatars/%1/%2.png").arg(_testUserShareDefinition.shareShareWith, QString::number(64));
-        const auto expectedAvatarPath = Utility::concatUrlPath(_account->url(), relativeAvatarPath).toString();
+        const auto expectedAvatarPath = Utility::concatUrlPath(helper.account->url(), relativeAvatarPath).toString();
         const QString expectedUrl(QStringLiteral("image://tray-image-provider/") + expectedAvatarPath);
         QCOMPARE(avatarUrl, expectedUrl);
     }
 
     void testSuccessfulCreateShares()
     {
-        resetTestData();
+        helper.resetTestData();
 
         // Test with an existing link share
-        appendShareReplyData(_testLinkShareDefinition);
+        helper.appendShareReplyData(_testLinkShareDefinition);
+        QCOMPARE(helper.shareCount(), 1);
 
         ShareModel model;
         QAbstractItemModelTester modelTester(&model);
@@ -903,18 +371,18 @@ private slots:
 
         QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
 
-        model.setAccountState(_accountState.data());
-        model.setLocalPath(_fakeFolder.localPath() + testFileName);
+        model.setAccountState(helper.accountState.data());
+        model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
 
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(_sharesReplyData.count(), 1); // Check our test is working!
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
         // Test if it gets added
         model.createNewLinkShare();
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(_sharesReplyData.count(), 2); // Check our test is working!
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(helper.shareCount(), 2); // Check our test is working!
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
         // Test if it's the type we wanted
         const auto newLinkShareIndex = model.index(model.rowCount() - 1, 0, {});
@@ -924,8 +392,8 @@ private slots:
         const ShareePtr sharee(new Sharee("testsharee@nextcloud.com", "Test sharee", Sharee::Type::Email));
         model.createNewUserGroupShare(sharee);
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(_sharesReplyData.count(), 3); // Check our test is working!
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(helper.shareCount(), 3); // Check our test is working!
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
         // Test if it's the type we wanted
         const auto newUserGroupShareIndex = model.index(model.rowCount() - 1, 0, {});
@@ -935,20 +403,20 @@ private slots:
         const auto password = QStringLiteral("a pretty bad password but good thing it doesn't matter!");
         model.createNewLinkShareWithPassword(password);
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(_sharesReplyData.count(), 4); // Check our test is working!
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(helper.shareCount(), 4); // Check our test is working!
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
         model.createNewUserGroupShareWithPassword(sharee, password);
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(_sharesReplyData.count(), 5); // Check our test is working!
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(helper.shareCount(), 5); // Check our test is working!
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
-        resetTestData();
+        helper.resetTestData();
     }
 
     void testEnforcePasswordShares()
     {
-        resetTestData();
+        helper.resetTestData();
 
         // Enforce passwords for shares in capabilities
         const QVariantMap enforcePasswordsCapabilities {
@@ -982,12 +450,13 @@ private slots:
             }},
         };
 
-        _account->setCapabilities(enforcePasswordsCapabilities);
-        QVERIFY(_account->capabilities().sharePublicLinkEnforcePassword());
-        QVERIFY(_account->capabilities().shareEmailPasswordEnforced());
+        helper.account->setCapabilities(enforcePasswordsCapabilities);
+        QVERIFY(helper.account->capabilities().sharePublicLinkEnforcePassword());
+        QVERIFY(helper.account->capabilities().shareEmailPasswordEnforced());
 
         // Test with a link share "from the server"
-        appendShareReplyData(_testLinkShareDefinition);
+        helper.appendShareReplyData(_testLinkShareDefinition);
+        QCOMPARE(helper.shareCount(), 1);
 
         ShareModel model;
         QAbstractItemModelTester modelTester(&model);
@@ -995,11 +464,11 @@ private slots:
 
         QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
 
-        model.setAccountState(_accountState.data());
-        model.setLocalPath(_fakeFolder.localPath() + testFileName);
+        model.setAccountState(helper.accountState.data());
+        model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
 
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
         // Confirm that the model requests a password
         QSignalSpy requestPasswordForLinkShare(&model, &ShareModel::requestPasswordForLinkShare);
@@ -1014,11 +483,12 @@ private slots:
         // Test that the model data is correctly reporting that passwords are enforced
         const auto shareIndex = model.index(model.rowCount() - 1, 0, {});
         QCOMPARE(shareIndex.data(ShareModel::PasswordEnforcedRole).toBool(), true);
+        QCOMPARE(shareIndex.data(ShareModel::PasswordProtectEnabledRole).toBool(), true);
     }
 
     void testEnforceExpireDate()
     {
-        resetTestData();
+        helper.resetTestData();
 
         const auto internalExpireDays = 45;
         const auto publicExpireDays = 30;
@@ -1056,15 +526,16 @@ private slots:
             }},
         };
 
-        _account->setCapabilities(enforcePasswordsCapabilities);
-        QVERIFY(_account->capabilities().sharePublicLinkEnforceExpireDate());
-        QVERIFY(_account->capabilities().shareInternalEnforceExpireDate());
-        QVERIFY(_account->capabilities().shareRemoteEnforceExpireDate());
+        helper.account->setCapabilities(enforcePasswordsCapabilities);
+        QVERIFY(helper.account->capabilities().sharePublicLinkEnforceExpireDate());
+        QVERIFY(helper.account->capabilities().shareInternalEnforceExpireDate());
+        QVERIFY(helper.account->capabilities().shareRemoteEnforceExpireDate());
 
         // Test with shares "from the server"
-        appendShareReplyData(_testLinkShareDefinition);
-        appendShareReplyData(_testEmailShareDefinition);
-        appendShareReplyData(_testRemoteShareDefinition);
+        helper.appendShareReplyData(_testLinkShareDefinition);
+        helper.appendShareReplyData(_testEmailShareDefinition);
+        helper.appendShareReplyData(_testRemoteShareDefinition);
+        QCOMPARE(helper.shareCount(), 3);
 
         ShareModel model;
         QAbstractItemModelTester modelTester(&model);
@@ -1072,11 +543,11 @@ private slots:
 
         QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
 
-        model.setAccountState(_accountState.data());
-        model.setLocalPath(_fakeFolder.localPath() + testFileName);
+        model.setAccountState(helper.accountState.data());
+        model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
 
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
         // Test that the model data is correctly reporting that expire dates are enforced for all share types
         for(auto i = 0; i < model.rowCount(); ++i) {
@@ -1108,10 +579,11 @@ private slots:
 
     void testSuccessfulDeleteShares()
     {
-        resetTestData();
+        helper.resetTestData();
 
         // Test with an existing link share
-        appendShareReplyData(_testLinkShareDefinition);
+        helper.appendShareReplyData(_testLinkShareDefinition);
+        QCOMPARE(helper.shareCount(), 1);
 
         ShareModel model;
         QAbstractItemModelTester modelTester(&model);
@@ -1119,33 +591,33 @@ private slots:
 
         QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
 
-        model.setAccountState(_accountState.data());
-        model.setLocalPath(_fakeFolder.localPath() + testFileName);
+        model.setAccountState(helper.accountState.data());
+        model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
 
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(_sharesReplyData.count(), 1); // Check our test is working!
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
         // Create share
         model.createNewLinkShare();
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(_sharesReplyData.count(), 2); // Check our test is working!
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(helper.shareCount(), 2); // Check our test is working!
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
         // Test if it gets deleted properly
         const auto latestLinkShare = model.index(model.rowCount() - 1, 0, {}).data(ShareModel::ShareRole).value<SharePtr>();
         QSignalSpy shareDeleted(latestLinkShare.data(), &LinkShare::shareDeleted);
         model.deleteShare(latestLinkShare);
         QVERIFY(shareDeleted.wait(5000));
-        QCOMPARE(_sharesReplyData.count(), 1); // Check our test is working!
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
-        resetTestData();
+        helper.resetTestData();
     }
 
     void testPlaceholderLinkShare()
     {
-        resetTestData();
+        helper.resetTestData();
 
         // Start with no shares; should show the placeholder link share
         ShareModel model;
@@ -1154,8 +626,8 @@ private slots:
 
         QSignalSpy hasInitialShareFetchCompletedChanged(&model, &ShareModel::hasInitialShareFetchCompletedChanged);
 
-        model.setAccountState(_accountState.data());
-        model.setLocalPath(_fakeFolder.localPath() + testFileName);
+        model.setAccountState(helper.accountState.data());
+        model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
         QVERIFY(hasInitialShareFetchCompletedChanged.wait(5000));
         QVERIFY(model.hasInitialShareFetchCompleted());
         QCOMPARE(model.rowCount(), 1); // There should be a placeholder now
@@ -1168,8 +640,8 @@ private slots:
         const ShareePtr sharee(new Sharee("testsharee@nextcloud.com", "Test sharee", Sharee::Type::Email));
         model.createNewUserGroupShare(sharee);
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(_sharesReplyData.count(), 1); // Check our test is working!
-        QCOMPARE(model.rowCount(), _sharesReplyData.count() + 1);
+        QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+        QCOMPARE(model.rowCount(), helper.shareCount() + 1);
 
         QVERIFY(placeholderLinkShareIndex.isValid());
         QCOMPARE(placeholderLinkShareIndex.data(ShareModel::ShareTypeRole).toInt(), Share::TypePlaceholderLink);
@@ -1177,8 +649,8 @@ private slots:
         // Now try adding a link share, which should remove the placeholder
         model.createNewLinkShare();
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(_sharesReplyData.count(), 2); // Check our test is working!
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(helper.shareCount(), 2); // Check our test is working!
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
         QVERIFY(!placeholderLinkShareIndex.isValid());
 
@@ -1187,21 +659,22 @@ private slots:
         QSignalSpy shareDeleted(latestLinkShare.data(), &LinkShare::shareDeleted);
         model.deleteShare(latestLinkShare);
         QVERIFY(shareDeleted.wait(5000));
-        QCOMPARE(_sharesReplyData.count(), 1); // Check our test is working!
-        QCOMPARE(model.rowCount(), _sharesReplyData.count() + 1);
+        QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+        QCOMPARE(model.rowCount(), helper.shareCount() + 1);
 
         const auto newPlaceholderLinkShareIndex = model.index(model.rowCount() - 1, 0, {});
         QCOMPARE(newPlaceholderLinkShareIndex.data(ShareModel::ShareTypeRole).toInt(), Share::TypePlaceholderLink);
 
-        resetTestData();
+        helper.resetTestData();
     }
 
     void testSuccessfulToggleAllowEditing()
     {
-        resetTestData();
+        helper.resetTestData();
 
         // Test with an existing link share
-        appendShareReplyData(_testLinkShareDefinition);
+        helper.appendShareReplyData(_testLinkShareDefinition);
+        QCOMPARE(helper.shareCount(), 1);
 
         ShareModel model;
         QAbstractItemModelTester modelTester(&model);
@@ -1209,12 +682,12 @@ private slots:
 
         QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
 
-        model.setAccountState(_accountState.data());
-        model.setLocalPath(_fakeFolder.localPath() + testFileName);
+        model.setAccountState(helper.accountState.data());
+        model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
 
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(_sharesReplyData.count(), 1); // Check our test is working!
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
         const auto shareIndex = model.index(model.rowCount() - 1, 0, {});
         QCOMPARE(shareIndex.data(ShareModel::EditingAllowedRole).toBool(), SharePermissions(_testLinkShareDefinition.sharePermissions).testFlag(SharePermissionUpdate));
@@ -1229,11 +702,12 @@ private slots:
 
     void testSuccessfulPasswordSet()
     {
-        resetTestData();
+        helper.resetTestData();
 
         // Test with an existing link share.
         // This one has a pre-existing password
-        appendShareReplyData(_testLinkShareDefinition);
+        helper.appendShareReplyData(_testLinkShareDefinition);
+        QCOMPARE(helper.shareCount(), 1);
 
         ShareModel model;
         QAbstractItemModelTester modelTester(&model);
@@ -1241,12 +715,12 @@ private slots:
 
         QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
 
-        model.setAccountState(_accountState.data());
-        model.setLocalPath(_fakeFolder.localPath() + testFileName);
+        model.setAccountState(helper.accountState.data());
+        model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
 
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(_sharesReplyData.count(), 1); // Check our test is working!
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
         const auto shareIndex = model.index(model.rowCount() - 1, 0, {});
         QCOMPARE(shareIndex.data(ShareModel::PasswordProtectEnabledRole).toBool(), true);
@@ -1269,11 +743,12 @@ private slots:
 
     void testSuccessfulExpireDateSet()
     {
-        resetTestData();
+        helper.resetTestData();
 
         // Test with an existing link share.
         // This one has a pre-existing expire date
-        appendShareReplyData(_testLinkShareDefinition);
+        helper.appendShareReplyData(_testLinkShareDefinition);
+        QCOMPARE(helper.shareCount(), 1);
 
         ShareModel model;
         QAbstractItemModelTester modelTester(&model);
@@ -1281,12 +756,12 @@ private slots:
 
         QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
 
-        model.setAccountState(_accountState.data());
-        model.setLocalPath(_fakeFolder.localPath() + testFileName);
+        model.setAccountState(helper.accountState.data());
+        model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
 
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(_sharesReplyData.count(), 1); // Check our test is working!
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
         // Check what we know
         const auto shareIndex = model.index(model.rowCount() - 1, 0, {});
@@ -1318,11 +793,12 @@ private slots:
 
     void testSuccessfulNoteSet()
     {
-        resetTestData();
+        helper.resetTestData();
 
         // Test with an existing link share.
         // This one has a pre-existing password
-        appendShareReplyData(_testLinkShareDefinition);
+        helper.appendShareReplyData(_testLinkShareDefinition);
+        QCOMPARE(helper.shareCount(), 1);
 
         ShareModel model;
         QAbstractItemModelTester modelTester(&model);
@@ -1330,12 +806,12 @@ private slots:
 
         QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
 
-        model.setAccountState(_accountState.data());
-        model.setLocalPath(_fakeFolder.localPath() + testFileName);
+        model.setAccountState(helper.accountState.data());
+        model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
 
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(_sharesReplyData.count(), 1); // Check our test is working!
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
         const auto shareIndex = model.index(model.rowCount() - 1, 0, {});
         QCOMPARE(shareIndex.data(ShareModel::NoteEnabledRole).toBool(), true);
@@ -1359,10 +835,11 @@ private slots:
 
     void testSuccessfulLinkShareLabelSet()
     {
-        resetTestData();
+        helper.resetTestData();
 
         // Test with an existing link share.
-        appendShareReplyData(_testLinkShareDefinition);
+        helper.appendShareReplyData(_testLinkShareDefinition);
+        QCOMPARE(helper.shareCount(), 1);
 
         ShareModel model;
         QAbstractItemModelTester modelTester(&model);
@@ -1370,12 +847,12 @@ private slots:
 
         QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
 
-        model.setAccountState(_accountState.data());
-        model.setLocalPath(_fakeFolder.localPath() + testFileName);
+        model.setAccountState(helper.accountState.data());
+        model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
 
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(_sharesReplyData.count(), 1); // Check our test is working!
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
         const auto shareIndex = model.index(model.rowCount() - 1, 0, {});
         QCOMPARE(shareIndex.data(ShareModel::LinkShareLabelRole).toBool(), true);
@@ -1391,11 +868,12 @@ private slots:
 
     void testSharees()
     {
-        resetTestData();
+        helper.resetTestData();
 
-        appendShareReplyData(_testLinkShareDefinition);
-        appendShareReplyData(_testEmailShareDefinition);
-        appendShareReplyData(_testUserShareDefinition);
+        helper.appendShareReplyData(_testLinkShareDefinition);
+        helper.appendShareReplyData(_testEmailShareDefinition);
+        helper.appendShareReplyData(_testUserShareDefinition);
+        QCOMPARE(helper.shareCount(), 3);
 
         ShareModel model;
         QAbstractItemModelTester modelTester(&model);
@@ -1403,11 +881,11 @@ private slots:
 
         QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
 
-        model.setAccountState(_accountState.data());
-        model.setLocalPath(_fakeFolder.localPath() + testFileName);
+        model.setAccountState(helper.accountState.data());
+        model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
 
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
         QCOMPARE(model.sharees().count(), 2); // Link shares don't have sharees
 
@@ -1415,8 +893,8 @@ private slots:
         const ShareePtr sharee(new Sharee("testsharee@nextcloud.com", "Test sharee", Sharee::Type::Email));
         model.createNewUserGroupShare(sharee);
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(_sharesReplyData.count(), 4); // Check our test is working!
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(helper.shareCount(), 4); // Check our test is working!
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
         const auto sharees = model.sharees();
         QCOMPARE(sharees.count(), 3); // Link shares don't have sharees
@@ -1428,7 +906,7 @@ private slots:
         const auto sharePtr = shareIndex.data(ShareModel::ShareRole).value<SharePtr>();
         model.deleteShare(sharePtr);
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
         // Now check the sharee is gone
         QCOMPARE(model.sharees().count(), 2);
@@ -1436,13 +914,14 @@ private slots:
 
     void testSharePropertySetError()
     {
-        resetTestData();
+        helper.resetTestData();
 
         // Serve a broken share definition from the server to force an error
         auto brokenLinkShareDefinition = _testLinkShareDefinition;
         brokenLinkShareDefinition.shareId = QString();
 
-        appendShareReplyData(brokenLinkShareDefinition);
+        helper.appendShareReplyData(brokenLinkShareDefinition);
+        QCOMPARE(helper.shareCount(), 1);
 
         ShareModel model;
         QAbstractItemModelTester modelTester(&model);
@@ -1450,16 +929,17 @@ private slots:
 
         QSignalSpy sharesChanged(&model, &ShareModel::sharesChanged);
 
-        model.setAccountState(_accountState.data());
-        model.setLocalPath(_fakeFolder.localPath() + testFileName);
+        model.setAccountState(helper.accountState.data());
+        model.setLocalPath(helper.fakeFolder.localPath() + helper.testFileName);
 
         QVERIFY(sharesChanged.wait(5000));
-        QCOMPARE(_sharesReplyData.count(), 1); // Check our test is working!
-        QCOMPARE(model.rowCount(), _sharesReplyData.count());
+        QCOMPARE(helper.shareCount(), 1); // Check our test is working!
+        QCOMPARE(model.rowCount(), helper.shareCount());
 
         // Reset the fake server to pretend like nothing is wrong there
-        _sharesReplyData = QJsonArray();
-        appendShareReplyData(_testLinkShareDefinition);
+        helper.resetTestShares();
+        helper.appendShareReplyData(_testLinkShareDefinition);
+        QCOMPARE(helper.shareCount(), 1);
 
         // Now try changing a property of the share
         const auto shareIndex = model.index(model.rowCount() - 1, 0, {});