nextcloud-desktop/test/sharetestutils.cpp
Claudio Cambra 7ba6969e65
Moved all server mocking code for share model test to independent ShareTestUtils file
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
2022-10-31 18:06:10 +01:00

442 lines
18 KiB
C++

/*
* 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);
}