nextcloud-desktop/test/testcfapishellextensionsipc.cpp
2022-10-04 18:32:51 +02:00

487 lines
21 KiB
C++

/*
* This software is in the public domain, furnished "as is", without technical
* support, and with no warranty, express or implied, as to its usefulness for
* any purpose.
*
*/
#include <account.h>
#include <accountstate.h>
#include <accountmanager.h>
#include <common/vfs.h>
#include <common/shellextensionutils.h>
#include "config.h"
#include <folderman.h>
#include <libsync/vfs/cfapi/shellext/configvfscfapishellext.h>
#include <ocssharejob.h>
#include <shellextensionsserver.h>
#include <syncengine.h>
#include "syncenginetestutils.h"
#include "testhelper.h"
#include <vfs/cfapi/shellext/customstateprovideripc.h>
#include <vfs/cfapi/shellext/thumbnailprovideripc.h>
#include <QtTest>
#include <QImage>
#include <QPainter>
namespace {
static constexpr auto roootFolderName = "A";
static constexpr auto imagesFolderName = "photos";
static constexpr auto filesFolderName = "files";
static const QByteArray fakeNoSharesResponse = R"({"ocs":{"data":[],"meta":{"message":"OK","status":"ok","statuscode":200}}})";
static const QByteArray fakeSharedFilesResponse = R"({"ocs":{"data":[{
"attributes": null,
"can_delete": true,
"can_edit": true,
"displayname_file_owner": "admin",
"displayname_owner": "admin",
"expiration": null,
"file_parent": 2981,
"file_source": 3538,
"file_target": "/test_shared_file.txt",
"has_preview": true,
"hide_download": 0,
"id": "36",
"item_source": 3538,
"item_type": "file",
"label": null,
"mail_send": 0,
"mimetype": "text/plain",
"note": "",
"parent": null,
"path": "A/files/test_shared_file.txt",
"permissions": 19,
"share_type": 0,
"share_with": "newstandard",
"share_with_displayname": "newstandard",
"share_with_displayname_unique": "newstandard",
"status": {
"clearAt": null,
"icon": null,
"message": null,
"status": "offline"
},
"stime": 1662995777,
"storage": 2,
"storage_id": "home::admin",
"token": null,
"uid_file_owner": "admin",
"uid_owner": "admin"
},
{
"attributes": null,
"can_delete": true,
"can_edit": true,
"displayname_file_owner": "admin",
"displayname_owner": "admin",
"expiration": null,
"file_parent": 2981,
"file_source": 3538,
"file_target": "/test_shared_and_locked_file.txt",
"has_preview": true,
"hide_download": 0,
"id": "36",
"item_source": 3538,
"item_type": "file",
"label": null,
"mail_send": 0,
"mimetype": "text/plain",
"note": "",
"parent": null,
"path": "A/files/test_shared_and_locked_file.txt",
"permissions": 19,
"share_type": 0,
"share_with": "newstandard",
"share_with_displayname": "newstandard",
"share_with_displayname_unique": "newstandard",
"status": {
"clearAt": null,
"icon": null,
"message": null,
"status": "offline"
},
"stime": 1662995777,
"storage": 2,
"storage_id": "home::admin",
"token": null,
"uid_file_owner": "admin",
"uid_owner": "admin"
}
],
"meta": {
"message": "OK",
"status": "ok",
"statuscode": 200
}
}
})";
static constexpr auto shellExtensionServerOverrideIntervalMs = 1000LL * 2LL;
}
using namespace OCC;
class TestCfApiShellExtensionsIPC : public QObject
{
Q_OBJECT
FolderMan _fm;
FakeFolder fakeFolder{FileInfo()};
QScopedPointer<FakeQNAM> fakeQnam;
OCC::AccountPtr account;
OCC::AccountState* accountState;
QScopedPointer<ShellExtensionsServer> _shellExtensionsServer;
const QStringList dummmyImageNames = {
{ QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imageJpg.jpg")) },
{ QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imagePng.png")) },
{ QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imagePng.bmp")) }
};
QMap<QString, QByteArray> dummyImages;
QString currentImage;
struct FileStates
{
bool _isShared = false;
bool _isLocked = false;
};
const QMap<QString, FileStates> dummyFileStates = {
{ QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_locked_file.txt")), { false, true } },
{ QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_shared_file.txt")), { true, false } },
{ QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_shared_and_locked_file.txt")), { true, true }},
{ QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_non_shared_and_non_locked_file.txt")), { false, false }}
};
public:
static bool replyWithNoShares;
private slots:
void initTestCase()
{
VfsShellExtensions::ThumbnailProviderIpc::overrideServerName = VfsShellExtensions::serverNameForApplicationNameDefault();
VfsShellExtensions::CustomStateProviderIpc::overrideServerName = VfsShellExtensions::serverNameForApplicationNameDefault();
_shellExtensionsServer.reset(new ShellExtensionsServer);
_shellExtensionsServer->setIsSharedInvalidationInterval(shellExtensionServerOverrideIntervalMs);
for (const auto &dummyImageName : dummmyImageNames) {
const auto extension = dummyImageName.split(".").last();
const auto format = dummyImageName.endsWith("PNG", Qt::CaseInsensitive) ? QImage::Format_ARGB32 : QImage::Format_RGB32;
QImage image(QSize(640, 480), format);
QPainter painter(&image);
painter.setBrush(QBrush(Qt::red));
painter.fillRect(QRectF(0, 0, 640, 480), Qt::red);
QByteArray byteArray;
QBuffer buffer(&byteArray);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, extension.toStdString().c_str());
dummyImages.insert(dummyImageName, byteArray);
}
fakeFolder.remoteModifier().mkdir(roootFolderName);
fakeFolder.remoteModifier().mkdir(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName));
fakeFolder.remoteModifier().mkdir(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName));
for (const auto &fileStateKey : dummyFileStates.keys()) {
fakeFolder.remoteModifier().insert(fileStateKey, 256);
}
fakeQnam.reset(new FakeQNAM({}));
account = OCC::Account::create();
account->setCredentials(new FakeCredentials{fakeQnam.data()});
account->setUrl(QUrl(("http://example.de")));
accountState = new OCC::AccountState(account);
OCC::AccountManager::instance()->addAccount(account);
FolderMan *folderman = FolderMan::instance();
QCOMPARE(folderman, &_fm);
QVERIFY(folderman->addFolder(accountState, folderDefinition(fakeFolder.localPath())));
fakeQnam->setOverride(
[this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) {
Q_UNUSED(device);
QNetworkReply *reply = nullptr;
const auto path = req.url().path();
if (path.endsWith(OCC::OcsShareJob::_pathForSharesRequest)) {
const auto jsonReply = TestCfApiShellExtensionsIPC::replyWithNoShares ? fakeNoSharesResponse : fakeSharedFilesResponse;
TestCfApiShellExtensionsIPC::replyWithNoShares = false;
auto fakePayloadReply = new FakePayloadReply(op, req, jsonReply, nullptr);
QMap<QNetworkRequest::KnownHeaders, QByteArray> additionalHeaders = {
{QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"}};
fakePayloadReply->_additionalHeaders = additionalHeaders;
reply = fakePayloadReply;
} else if (path.endsWith(ShellExtensionsServer::getFetchThumbnailPath())) {
const auto urlQuery = QUrlQuery(req.url());
const auto fileId = urlQuery.queryItemValue(QStringLiteral("fileId"));
const auto x = urlQuery.queryItemValue(QStringLiteral("x")).toInt();
const auto y = urlQuery.queryItemValue(QStringLiteral("y")).toInt();
if (fileId.isEmpty() || x <= 0 || y <= 0) {
reply = new FakePayloadReply(op, req, {}, nullptr);
} else {
const auto foundImageIt = dummyImages.find(currentImage);
QByteArray byteArray;
if (foundImageIt != dummyImages.end()) {
byteArray = foundImageIt.value();
}
currentImage.clear();
auto fakePayloadReply = new FakePayloadReply(op, req, byteArray, nullptr);
QMap<QNetworkRequest::KnownHeaders, QByteArray> additionalHeaders = {
{QNetworkRequest::KnownHeaders::ContentTypeHeader, "image/jpeg"}};
fakePayloadReply->_additionalHeaders = additionalHeaders;
reply = fakePayloadReply;
}
} else {
reply = new FakePayloadReply(op, req, {}, nullptr);
}
return reply;
});
};
void testRequestThumbnails()
{
FolderMan *folderman = FolderMan::instance();
QVERIFY(folderman);
auto folder = FolderMan::instance()->folderForPath(fakeFolder.localPath());
QVERIFY(folder);
folder->setVirtualFilesEnabled(true);
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
ItemCompletedSpy completeSpy(fakeFolder);
auto cleanup = [&]() {
completeSpy.clear();
};
cleanup();
// Create a virtual file for remote files
for (const auto &dummyImageName : dummmyImageNames) {
fakeFolder.remoteModifier().insert(dummyImageName, 256);
}
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
cleanup();
// just add records from fake folder's journal to real one's to make test work
SyncJournalFileRecord record;
auto realFolder = FolderMan::instance()->folderForPath(fakeFolder.localPath());
QVERIFY(realFolder);
for (const auto &dummyImageName : dummmyImageNames) {
if (fakeFolder.syncJournal().getFileRecord(dummyImageName, &record)) {
QVERIFY(realFolder->journalDb()->setFileRecord(record));
}
}
// #1 Test every fake image fetching. Everything must succeed.
for (const auto &dummyImageName : dummmyImageNames) {
QEventLoop loop;
QByteArray thumbnailReplyData;
currentImage = dummyImageName;
// emulate thumbnail request from a separate thread (just like the real shell extension does)
std::thread t([&] {
VfsShellExtensions::ThumbnailProviderIpc thumbnailProviderIpc;
thumbnailReplyData = thumbnailProviderIpc.fetchThumbnailForFile(
fakeFolder.localPath() + dummyImageName, QSize(256, 256));
QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
});
loop.exec();
t.detach();
QVERIFY(!thumbnailReplyData.isEmpty());
const auto imageFromData = QImage::fromData(thumbnailReplyData);
QVERIFY(!imageFromData.isNull());
}
// #2 Test wrong image fetching. It must fail.
QEventLoop loop;
QByteArray thumbnailReplyData;
std::thread t1([&] {
VfsShellExtensions::ThumbnailProviderIpc thumbnailProviderIpc;
thumbnailReplyData = thumbnailProviderIpc.fetchThumbnailForFile(
fakeFolder.localPath() + QString("A/photos/wrong.jpg"), QSize(256, 256));
QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
});
loop.exec();
t1.detach();
QVERIFY(thumbnailReplyData.isEmpty());
// #3 Test one image fetching, but set incorrect size. It must fail.
currentImage = dummyImages.keys().first();
std::thread t2([&] {
VfsShellExtensions::ThumbnailProviderIpc thumbnailProviderIpc;
thumbnailReplyData = thumbnailProviderIpc.fetchThumbnailForFile(fakeFolder.localPath() + currentImage, {});
QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
});
loop.exec();
t2.detach();
QVERIFY(thumbnailReplyData.isEmpty());
}
void testRequestCustomStates()
{
FolderMan *folderman = FolderMan::instance();
QVERIFY(folderman);
auto folder = FolderMan::instance()->folderForPath(fakeFolder.localPath());
QVERIFY(folder);
folder->setVirtualFilesEnabled(true);
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
// just add records from fake folder's journal to real one's to make test work
SyncJournalFileRecord record;
auto realFolder = FolderMan::instance()->folderForPath(fakeFolder.localPath());
QVERIFY(realFolder);
for (auto it = std::begin(dummyFileStates); it != std::end(dummyFileStates); ++it) {
if (fakeFolder.syncJournal().getFileRecord(it.key(), &record)) {
record._isShared = it.value()._isShared;
if (record._isShared) {
record._remotePerm.setPermission(OCC::RemotePermissions::Permissions::IsShared);
}
record._lockstate._locked = it.value()._isLocked;
if (record._lockstate._locked) {
record._lockstate._lockOwnerId = "admin@example.cloud.com";
record._lockstate._lockOwnerDisplayName = "Admin";
record._lockstate._lockOwnerType = static_cast<int>(SyncFileItem::LockOwnerType::UserLock);
record._lockstate._lockTime = QDateTime::currentMSecsSinceEpoch();
record._lockstate._lockTimeout = 1000 * 60 * 60;
}
QVERIFY(fakeFolder.syncJournal().setFileRecord(record));
QVERIFY(realFolder->journalDb()->setFileRecord(record));
}
}
// #1 Test every file's states fetching. Everything must succeed.
for (auto it = std::cbegin(dummyFileStates); it != std::cend(dummyFileStates); ++it) {
QEventLoop loop;
QVariantList customStates;
std::thread t([&] {
VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + it.key());
QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
});
loop.exec();
t.detach();
QVERIFY(!customStates.isEmpty() || (!it.value()._isLocked && !it.value()._isShared));
}
// #2 Test wrong file's states fetching. It must fail.
QEventLoop loop;
QVariantList customStates;
std::thread t1([&] {
VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + QStringLiteral("A/files/wrong.jpg"));
QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
});
loop.exec();
t1.detach();
QVERIFY(customStates.isEmpty());
// #3 Test wrong file states fetching. It must fail.
customStates.clear();
std::thread t2([&] {
VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + QStringLiteral("A/files/test_non_shared_and_non_locked_file.txt"));
QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
});
loop.exec();
t2.detach();
QVERIFY(customStates.isEmpty());
// reset all share states to make sure we'll get new states when fetching
for (auto it = std::begin(dummyFileStates); it != std::end(dummyFileStates); ++it) {
if (fakeFolder.syncJournal().getFileRecord(it.key(), &record)) {
record._remotePerm.unsetPermission(OCC::RemotePermissions::Permissions::IsShared);
record._isShared = false;
QVERIFY(fakeFolder.syncJournal().setFileRecord(record));
QVERIFY(realFolder->journalDb()->setFileRecord(record));
}
}
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
//
// wait enough time to make shares' state invalid
QTest::qWait(shellExtensionServerOverrideIntervalMs + 1000);
// #4 Test every file's states fetching. Everything must succeed.
for (auto it = std::cbegin(dummyFileStates); it != std::cend(dummyFileStates); ++it) {
QEventLoop loop;
QVariantList customStates;
std::thread t([&] {
VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + it.key());
QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
});
loop.exec();
t.detach();
QVERIFY(!customStates.isEmpty() || (!it.value()._isLocked && !it.value()._isShared));
if (!customStates.isEmpty()) {
const auto lockedIndex = QString(CUSTOM_STATE_ICON_LOCKED_INDEX).toInt() - QString(CUSTOM_STATE_ICON_INDEX_OFFSET).toInt();
const auto sharedIndex = QString(CUSTOM_STATE_ICON_SHARED_INDEX).toInt() - QString(CUSTOM_STATE_ICON_INDEX_OFFSET).toInt();
if (customStates.contains(lockedIndex) && customStates.contains(sharedIndex)) {
QVERIFY(it.value()._isLocked && it.value()._isShared);
}
if (customStates.contains(lockedIndex)) {
QVERIFY(it.value()._isLocked);
}
if (customStates.contains(sharedIndex)) {
QVERIFY(it.value()._isShared);
}
}
}
// #5 Test no shares response for a file
QTest::qWait(shellExtensionServerOverrideIntervalMs + 1000);
TestCfApiShellExtensionsIPC::replyWithNoShares = true;
customStates.clear();
std::thread t3([&] {
VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + QStringLiteral("A/files/test_non_shared_and_non_locked_file.txt"));
QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
});
loop.exec();
t3.detach();
QVERIFY(customStates.isEmpty());
}
void cleanupTestCase()
{
VfsShellExtensions::ThumbnailProviderIpc::overrideServerName.clear();
if (auto folder = FolderMan::instance()->folderForPath(fakeFolder.localPath())) {
folder->setVirtualFilesEnabled(false);
}
FolderMan::instance()->unloadAndDeleteAllFolders();
if (auto accountToDelete = OCC::AccountManager::instance()->accounts().first()) {
OCC::AccountManager::instance()->deleteAccount(accountToDelete.data());
}
}
};
bool TestCfApiShellExtensionsIPC::replyWithNoShares = false;
QTEST_GUILESS_MAIN(TestCfApiShellExtensionsIPC)
#include "testcfapishellextensionsipc.moc"