diff --git a/src/gui/guiutility.h b/src/gui/guiutility.h index b4e87f33d60..fd2e3f1d62e 100644 --- a/src/gui/guiutility.h +++ b/src/gui/guiutility.h @@ -50,7 +50,7 @@ namespace Utility { void startShellIntegration(); - QString socketApiSocketPath(); + OWNCLOUDGUI_EXPORT QString socketApiSocketPath(); OWNCLOUDGUI_EXPORT void markDirectoryAsSyncRoot(const QString &path, const QUuid &accountUuid); std::pair getDirectorySyncRootMarkings(const QString &path); diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index ba73cf2e7fe..e6def8b4892 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -493,6 +493,13 @@ void SocketApi::command_COPY_PRIVATE_LINK(const QString &localFile, SocketListen fetchPrivateLinkUrlHelper(localFile, &SocketApi::copyUrlToClipboard); } +void SocketApi::command_GET_PRIVATE_LINK(const QString &localFile, SocketListener *listener) +{ + fetchPrivateLinkUrlHelper(localFile, [listener](const QUrl &url) { + listener->sendMessage(QStringLiteral("PRIVATE_LINK:") + url.toString()); + }); +} + void SocketApi::command_EMAIL_PRIVATE_LINK(const QString &localFile, SocketListener *) { fetchPrivateLinkUrlHelper(localFile, &SocketApi::emailPrivateLink); diff --git a/src/gui/socketapi/socketapi.h b/src/gui/socketapi/socketapi.h index 55556a75bb8..4a10c55dc36 100644 --- a/src/gui/socketapi/socketapi.h +++ b/src/gui/socketapi/socketapi.h @@ -116,6 +116,7 @@ private Q_SLOTS: // The context menu actions Q_INVOKABLE void command_SHARE(const QString &localFile, SocketListener *listener); Q_INVOKABLE void command_COPY_PRIVATE_LINK(const QString &localFile, SocketListener *listener); + Q_INVOKABLE void command_GET_PRIVATE_LINK(const QString &localFile, SocketListener *listener); Q_INVOKABLE void command_EMAIL_PRIVATE_LINK(const QString &localFile, SocketListener *listener); Q_INVOKABLE void command_OPEN_PRIVATE_LINK(const QString &localFile, SocketListener *listener); Q_INVOKABLE void command_OPEN_PRIVATE_LINK_VERSIONS(const QString &localFile, SocketListener *listener); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d78765e0b84..ace2e069584 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -58,6 +58,7 @@ if( UNIX AND NOT APPLE ) endif(UNIX AND NOT APPLE) owncloud_add_test(FileSystem) +owncloud_add_test(SocketApi) owncloud_add_test(FolderMan) diff --git a/test/testsocketapi.cpp b/test/testsocketapi.cpp new file mode 100644 index 00000000000..09aaf4666cb --- /dev/null +++ b/test/testsocketapi.cpp @@ -0,0 +1,171 @@ +/* + * Copyright (C) by Thomas Müller + * + * 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 +#include +#include + +#include +using namespace std::chrono_literals; + +#include "socketapi/socketapi.h" +#include "socketapi/socketapi_p.h" +#include "folderman.h" +#include "accountmanager.h" + +#include "testutils/syncenginetestutils.h" +#include "testutils/testutils.h" + +using namespace OCC; + +// Minimal 207 multistatus reply for intercepting PROPFIND requests in tests. +class FakeMultistatReply : public FakeReply +{ + Q_OBJECT +public: + QByteArray _body; + FakeMultistatReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req, + const QByteArray &body, QObject *parent) + : FakeReply(parent), _body(body) + { + setRequest(req); + setUrl(req.url()); + setOperation(op); + open(QIODevice::ReadOnly); + QTimer::singleShot(10ms, this, &FakeMultistatReply::respond); + } + void respond() + { + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 207); + setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/xml; charset=utf-8")); + setHeader(QNetworkRequest::ContentLengthHeader, _body.size()); + Q_EMIT metaDataChanged(); + Q_EMIT readyRead(); + checkedFinished(); + } + qint64 readData(char *buf, qint64 max) override + { + max = qMin(max, _body.size()); + memcpy(buf, _body.constData(), static_cast(max)); + _body.remove(0, static_cast(max)); + return max; + } + qint64 bytesAvailable() const override { return _body.size(); } +}; + +class TestSocketApi : public QObject +{ + Q_OBJECT + + QString _registeredFolderPath; + +private Q_SLOTS: + void initTestCase() + { + TestUtils::folderMan(); + } + + void cleanup() + { + if (!_registeredFolderPath.isEmpty()) { + Folder *folder = TestUtils::folderMan()->folderForPath(_registeredFolderPath); + if (folder) { + TestUtils::folderMan()->removeFolderFromGui(folder); + } + _registeredFolderPath.clear(); + } + } + + void testGetPrivateLinkSendsUrlOverSocket() + { + const QTemporaryDir tempDir = TestUtils::createTempDir(); + QVERIFY(tempDir.isValid()); + const QString localPath = QDir::cleanPath(tempDir.path()) + QLatin1Char('/'); + + auto accountState = createDummyAccount(); + Account *account = accountState->account(); + + auto cap = TestUtils::testCapabilities(); + cap[QStringLiteral("files")] = QVariantMap{{QStringLiteral("privateLinks"), true}}; + account->setCapabilities({account->url(), cap}); + + const QString expectedUrl = QStringLiteral("https://example.com/f/abc123"); + auto *fakeAm = dynamic_cast(account->accessManager()); + QVERIFY(fakeAm); + fakeAm->setOverride([expectedUrl](QNetworkAccessManager::Operation op, + const QNetworkRequest &req, + QIODevice *) -> QNetworkReply * { + if (op != QNetworkAccessManager::CustomOperation) + return nullptr; + if (req.attribute(QNetworkRequest::CustomVerbAttribute).toByteArray() != QByteArrayLiteral("PROPFIND")) + return nullptr; + if (req.rawHeader(QByteArrayLiteral("Depth")) != QByteArrayLiteral("0")) + return nullptr; + + const QString path = req.url().path(); + const QByteArray body = + QByteArrayLiteral("" + "" + "") + path.toUtf8() + + QByteArrayLiteral("" + "") + expectedUrl.toUtf8() + + QByteArrayLiteral("" + "HTTP/1.1 200 OK" + ""); + + return new FakeMultistatReply(op, req, body, nullptr); + }); + + auto def = TestUtils::createDummyFolderDefinition(account, localPath); + def.setPaused(true); + Folder *folder = TestUtils::folderMan()->addFolder(accountState.get(), def); + QVERIFY(folder); + _registeredFolderPath = localPath; + + // Use a QBuffer as the socket so SocketListener::sendMessage has somewhere to write. + QBuffer capture; + capture.open(QIODevice::ReadWrite); + SocketListener listener(&capture); + + SocketApi api; + QMetaObject::invokeMethod(&api, "command_GET_PRIVATE_LINK", + Q_ARG(QString, localPath), Q_ARG(OCC::SocketListener *, &listener)); + + const QByteArray expected = + QByteArrayLiteral("PRIVATE_LINK:") + expectedUrl.toUtf8() + '\n'; + QTRY_VERIFY2(capture.data().contains(expected), + qPrintable(QStringLiteral("Expected '%1' in buffer '%2'") + .arg(QString::fromUtf8(expected), QString::fromUtf8(capture.data())))); + } + + void testGetPrivateLinkNoResponseWhenFileNotSynced() + { + QBuffer capture; + capture.open(QIODevice::ReadWrite); + SocketListener listener(&capture); + + SocketApi api; + QMetaObject::invokeMethod(&api, "command_GET_PRIVATE_LINK", + Q_ARG(QString, QStringLiteral("/tmp/not_a_synced_file_testsocketapi.txt")), + Q_ARG(OCC::SocketListener *, &listener)); + + QTest::qWait(500); + QVERIFY2(!capture.data().contains("PRIVATE_LINK:"), + qPrintable(QStringLiteral("Expected no PRIVATE_LINK response for unsynced file, got: ") + + QString::fromUtf8(capture.data()))); + } +}; + +QTEST_GUILESS_MAIN(TestSocketApi) +#include "testsocketapi.moc"