From b131e24a10c791c98fe59459443dafb44038e85d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Wed, 6 May 2026 10:05:57 +0200 Subject: [PATCH 1/5] feat: add GET_PRIVATE_LINK socket command for Wayland clipboard support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sends PRIVATE_LINK: back over the socket so that file manager extensions running inside processes with a valid Wayland surface can write to the clipboard themselves, working around the compositor serial requirement that prevents the background daemon from doing so. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --- src/gui/socketapi/socketapi.cpp | 7 +++++++ src/gui/socketapi/socketapi.h | 1 + 2 files changed, 8 insertions(+) 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); From 413bd0296d34fe2df921f2265587a55f64cd2644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Wed, 6 May 2026 10:06:03 +0200 Subject: [PATCH 2/5] test: add unit tests for GET_PRIVATE_LINK socket command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --- test/CMakeLists.txt | 1 + test/testsocketapi.cpp | 257 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 test/testsocketapi.cpp 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..59350972403 --- /dev/null +++ b/test/testsocketapi.cpp @@ -0,0 +1,257 @@ +/* + * 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 +#include + +#include +using namespace std::chrono_literals; + +#include "socketapi/socketapi.h" +#include "folderman.h" +#include "accountmanager.h" + +#include "testutils/syncenginetestutils.h" +#include "testutils/testutils.h" + +using namespace OCC; + +// A minimal fake reply that emits a Depth:0 PROPFIND multistatus response +// with status 207 and the correct content-type, suitable for private link lookups. +class FakeMultistatReply : public FakeReply +{ + Q_OBJECT +public: + QByteArray _body; + + FakeMultistatReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, + const QByteArray &body, QObject *parent) + : FakeReply(parent), _body(body) + { + setRequest(request); + setUrl(request.url()); + setOperation(op); + open(QIODevice::ReadOnly); + QTimer::singleShot(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()); + std::copy(_body.cbegin(), _body.cbegin() + max, buf); + _body.remove(0, static_cast(max)); + return max; + } + + qint64 bytesAvailable() const override { return _body.size(); } +}; + +class TestSocketApi : public QObject +{ + Q_OBJECT + + // Path of the folder registered during a test (reset by cleanup()). + QString _registeredFolderPath; + + // Compute the socket path the same way guiutility_unix.cpp does. + // Utility::socketApiSocketPath() provides the same logic, but it lives in the + // GUI library and is not exposed as a public method of SocketApi, so the + // computation is duplicated here rather than introducing an extra dependency. + static QString socketApiPath() + { + return QStringLiteral("%1/ownCloud/socket") + .arg(QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation)); + } + + // Connect to the SocketApi's server and wait for the connection to be accepted + // (slotNewConnection fired). Returns the connected socket or nullptr on failure. + static QLocalSocket *connectAndWaitForListener() + { + const QString path = socketApiPath(); + auto *sock = new QLocalSocket; + sock->connectToServer(path); + if (!sock->waitForConnected(3000)) { + qWarning() << "Failed to connect to socket" << path + << "error:" << sock->errorString(); + delete sock; + return nullptr; + } + // Give the server side a chance to run slotNewConnection and + // insert the listener into _listeners. + QCoreApplication::processEvents(); + return sock; + } + +private Q_SLOTS: + void initTestCase() + { + // Ensure FolderMan singleton exists before any test + TestUtils::folderMan(); + } + + void cleanup() + { + // Remove any folder registered during the test to avoid polluting later tests. + if (!_registeredFolderPath.isEmpty()) { + Folder *folder = TestUtils::folderMan()->folderForPath(_registeredFolderPath); + if (folder) { + TestUtils::folderMan()->removeFolderFromGui(folder); + } + _registeredFolderPath.clear(); + } + } + + void testGetPrivateLinkSendsUrlOverSocket() + { + // ----- Set up a paused sync folder so no sync fires ----- + + // Create a real directory on disk as the local sync root + const QTemporaryDir tempDir = TestUtils::createTempDir(); + QVERIFY(tempDir.isValid()); + // Ensure trailing slash (FolderDefinition normalises it, but be explicit) + const QString localPath = QDir::cleanPath(tempDir.path()) + QLatin1Char('/'); + + // createDummyAccount creates an account with a FakeAM (empty FileInfo) + auto accountState = createDummyAccount(); + Account *account = accountState->account(); + + // Enable private links capability + auto cap = TestUtils::testCapabilities(); + cap[QStringLiteral("files")] = QVariantMap{{QStringLiteral("privateLinks"), true}}; + account->setCapabilities({account->url(), cap}); + + // Set a server override: intercept the Depth:0 PROPFIND that fetchPrivateLinkUrl issues + // and reply with a 207 multistatus containing the private link URL. + 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); + }); + + // Register the directory with FolderMan (paused so no sync fires) + auto def = TestUtils::createDummyFolderDefinition(account, localPath); + def.setPaused(true); + Folder *folder = TestUtils::folderMan()->addFolder(accountState.get(), def); + QVERIFY(folder); + // Track the path so cleanup() can remove it after the test. + _registeredFolderPath = localPath; + + // ----- Start SocketApi server ----- + SocketApi api; + api.startShellIntegration(); + + // Connect a client socket (triggers slotNewConnection → listener registered) + std::unique_ptr clientSocket(connectAndWaitForListener()); + QVERIFY(clientSocket); + + // Drain any broadcast messages sent on connect (e.g. REGISTER_PATH) + QCoreApplication::processEvents(); + clientSocket->readAll(); + + // ----- Send the GET_PRIVATE_LINK command ----- + // Use the sync-folder root as the localFile argument. + // isSyncFolder() is true for the root path, so the journal record + // check is bypassed. + const QString command = + QStringLiteral("GET_PRIVATE_LINK:") + localPath + QLatin1Char('\n'); + clientSocket->write(command.toUtf8()); + QVERIFY(clientSocket->flush()); + + // ----- Wait for the PRIVATE_LINK response ----- + QSignalSpy readySpy(clientSocket.get(), &QLocalSocket::readyRead); + QVERIFY(readySpy.wait(5000)); + + // Collect all data (may arrive in chunks) + QByteArray received; + for (int retries = 5; retries > 0; --retries) { + received += clientSocket->readAll(); + if (received.contains("PRIVATE_LINK:")) + break; + if (!readySpy.wait(1000)) + break; + } + + const QByteArray expectedResponse = + QByteArrayLiteral("PRIVATE_LINK:") + expectedUrl.toUtf8() + '\n'; + QVERIFY2(received.contains(expectedResponse), + qPrintable(QStringLiteral("Expected '%1' in received data '%2'") + .arg(QString::fromUtf8(expectedResponse), QString::fromUtf8(received)))); + } + + void testGetPrivateLinkNoResponseWhenFileNotSynced() + { + // The path /tmp/not_a_synced_file_testsocketapi.txt is not under any registered + // sync root → command_GET_PRIVATE_LINK should return early with no reply. + + SocketApi api; + api.startShellIntegration(); + + std::unique_ptr clientSocket(connectAndWaitForListener()); + QVERIFY(clientSocket); + + // Drain broadcast messages + QCoreApplication::processEvents(); + clientSocket->readAll(); + + const QString command = + QStringLiteral("GET_PRIVATE_LINK:/tmp/not_a_synced_file_testsocketapi.txt\n"); + clientSocket->write(command.toUtf8()); + QVERIFY(clientSocket->flush()); + + // Wait briefly — there should be no PRIVATE_LINK message. + // Using QTest::qWait (unconditional) rather than QSignalSpy::wait so the + // assertion is always evaluated and cannot pass vacuously when no data arrives. + QTest::qWait(500); + const QByteArray received = clientSocket->readAll(); + QVERIFY2(!received.contains("PRIVATE_LINK:"), + qPrintable(QStringLiteral("Expected no PRIVATE_LINK response for unsynced file, got: ") + received)); + } +}; + +QTEST_GUILESS_MAIN(TestSocketApi) +#include "testsocketapi.moc" From 8a1b4991c3947e8b6fd42ae00cd0c5b2fa839515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Wed, 6 May 2026 11:28:09 +0200 Subject: [PATCH 3/5] fix: use Utility::socketApiSocketPath() in SocketApi test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test duplicated the Unix socket path logic, which broke on macOS where socketApiSocketPath() returns an XPC-style name instead of a filesystem path. Use the canonical helper so the test connects to the same socket the server listens on across all platforms. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --- test/testsocketapi.cpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/test/testsocketapi.cpp b/test/testsocketapi.cpp index 59350972403..bd2eb1c40b9 100644 --- a/test/testsocketapi.cpp +++ b/test/testsocketapi.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include #include @@ -24,6 +23,7 @@ using namespace std::chrono_literals; #include "socketapi/socketapi.h" #include "folderman.h" #include "accountmanager.h" +#include "guiutility.h" #include "testutils/syncenginetestutils.h" #include "testutils/testutils.h" @@ -77,14 +77,9 @@ class TestSocketApi : public QObject // Path of the folder registered during a test (reset by cleanup()). QString _registeredFolderPath; - // Compute the socket path the same way guiutility_unix.cpp does. - // Utility::socketApiSocketPath() provides the same logic, but it lives in the - // GUI library and is not exposed as a public method of SocketApi, so the - // computation is duplicated here rather than introducing an extra dependency. static QString socketApiPath() { - return QStringLiteral("%1/ownCloud/socket") - .arg(QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation)); + return Utility::socketApiSocketPath(); } // Connect to the SocketApi's server and wait for the connection to be accepted From 22c1d463cbac22b32fdaa252db8593ff4bcfc74a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Wed, 6 May 2026 16:50:31 +0200 Subject: [PATCH 4/5] fix: export socketApiSocketPath() from owncloudGui shared library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without OWNCLOUDGUI_EXPORT the symbol had hidden visibility and was not exported, causing an undefined symbol linker error when SocketApiTest tried to call it on macOS. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --- src/gui/guiutility.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From a6e1f4a769c39d6f21d0fac1c3d6b8acd71c8939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Wed, 6 May 2026 23:33:50 +0200 Subject: [PATCH 5/5] refactor: rewrite SocketApi test to avoid socket transport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous test used QLocalSocket to connect to the SocketApi server, which doesn't work on macOS where the server uses an XPC-based transport instead of a local socket file. Replace with direct invocation of command_GET_PRIVATE_LINK via QMetaObject::invokeMethod, capturing output in a QBuffer backed SocketListener. This is a pure unit test of the command handler with no dependency on the platform-specific socket transport. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --- test/testsocketapi.cpp | 139 +++++++++-------------------------------- 1 file changed, 29 insertions(+), 110 deletions(-) diff --git a/test/testsocketapi.cpp b/test/testsocketapi.cpp index bd2eb1c40b9..09aaf4666cb 100644 --- a/test/testsocketapi.cpp +++ b/test/testsocketapi.cpp @@ -13,42 +13,38 @@ */ #include -#include -#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 "guiutility.h" #include "testutils/syncenginetestutils.h" #include "testutils/testutils.h" using namespace OCC; -// A minimal fake reply that emits a Depth:0 PROPFIND multistatus response -// with status 207 and the correct content-type, suitable for private link lookups. +// 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 &request, + FakeMultistatReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req, const QByteArray &body, QObject *parent) : FakeReply(parent), _body(body) { - setRequest(request); - setUrl(request.url()); + setRequest(req); + setUrl(req.url()); setOperation(op); open(QIODevice::ReadOnly); QTimer::singleShot(10ms, this, &FakeMultistatReply::respond); } - void respond() { setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 207); @@ -58,15 +54,13 @@ class FakeMultistatReply : public FakeReply Q_EMIT readyRead(); checkedFinished(); } - qint64 readData(char *buf, qint64 max) override { max = qMin(max, _body.size()); - std::copy(_body.cbegin(), _body.cbegin() + max, buf); + memcpy(buf, _body.constData(), static_cast(max)); _body.remove(0, static_cast(max)); return max; } - qint64 bytesAvailable() const override { return _body.size(); } }; @@ -74,43 +68,16 @@ class TestSocketApi : public QObject { Q_OBJECT - // Path of the folder registered during a test (reset by cleanup()). QString _registeredFolderPath; - static QString socketApiPath() - { - return Utility::socketApiSocketPath(); - } - - // Connect to the SocketApi's server and wait for the connection to be accepted - // (slotNewConnection fired). Returns the connected socket or nullptr on failure. - static QLocalSocket *connectAndWaitForListener() - { - const QString path = socketApiPath(); - auto *sock = new QLocalSocket; - sock->connectToServer(path); - if (!sock->waitForConnected(3000)) { - qWarning() << "Failed to connect to socket" << path - << "error:" << sock->errorString(); - delete sock; - return nullptr; - } - // Give the server side a chance to run slotNewConnection and - // insert the listener into _listeners. - QCoreApplication::processEvents(); - return sock; - } - private Q_SLOTS: void initTestCase() { - // Ensure FolderMan singleton exists before any test TestUtils::folderMan(); } void cleanup() { - // Remove any folder registered during the test to avoid polluting later tests. if (!_registeredFolderPath.isEmpty()) { Folder *folder = TestUtils::folderMan()->folderForPath(_registeredFolderPath); if (folder) { @@ -122,25 +89,17 @@ private Q_SLOTS: void testGetPrivateLinkSendsUrlOverSocket() { - // ----- Set up a paused sync folder so no sync fires ----- - - // Create a real directory on disk as the local sync root const QTemporaryDir tempDir = TestUtils::createTempDir(); QVERIFY(tempDir.isValid()); - // Ensure trailing slash (FolderDefinition normalises it, but be explicit) const QString localPath = QDir::cleanPath(tempDir.path()) + QLatin1Char('/'); - // createDummyAccount creates an account with a FakeAM (empty FileInfo) auto accountState = createDummyAccount(); Account *account = accountState->account(); - // Enable private links capability auto cap = TestUtils::testCapabilities(); cap[QStringLiteral("files")] = QVariantMap{{QStringLiteral("privateLinks"), true}}; account->setCapabilities({account->url(), cap}); - // Set a server override: intercept the Depth:0 PROPFIND that fetchPrivateLinkUrl issues - // and reply with a 207 multistatus containing the private link URL. const QString expectedUrl = QStringLiteral("https://example.com/f/abc123"); auto *fakeAm = dynamic_cast(account->accessManager()); QVERIFY(fakeAm); @@ -149,8 +108,7 @@ private Q_SLOTS: QIODevice *) -> QNetworkReply * { if (op != QNetworkAccessManager::CustomOperation) return nullptr; - if (req.attribute(QNetworkRequest::CustomVerbAttribute).toByteArray() - != QByteArrayLiteral("PROPFIND")) + if (req.attribute(QNetworkRequest::CustomVerbAttribute).toByteArray() != QByteArrayLiteral("PROPFIND")) return nullptr; if (req.rawHeader(QByteArrayLiteral("Depth")) != QByteArrayLiteral("0")) return nullptr; @@ -165,86 +123,47 @@ private Q_SLOTS: + QByteArrayLiteral("" "HTTP/1.1 200 OK" ""); + return new FakeMultistatReply(op, req, body, nullptr); }); - // Register the directory with FolderMan (paused so no sync fires) auto def = TestUtils::createDummyFolderDefinition(account, localPath); def.setPaused(true); Folder *folder = TestUtils::folderMan()->addFolder(accountState.get(), def); QVERIFY(folder); - // Track the path so cleanup() can remove it after the test. _registeredFolderPath = localPath; - // ----- Start SocketApi server ----- + // Use a QBuffer as the socket so SocketListener::sendMessage has somewhere to write. + QBuffer capture; + capture.open(QIODevice::ReadWrite); + SocketListener listener(&capture); + SocketApi api; - api.startShellIntegration(); - - // Connect a client socket (triggers slotNewConnection → listener registered) - std::unique_ptr clientSocket(connectAndWaitForListener()); - QVERIFY(clientSocket); - - // Drain any broadcast messages sent on connect (e.g. REGISTER_PATH) - QCoreApplication::processEvents(); - clientSocket->readAll(); - - // ----- Send the GET_PRIVATE_LINK command ----- - // Use the sync-folder root as the localFile argument. - // isSyncFolder() is true for the root path, so the journal record - // check is bypassed. - const QString command = - QStringLiteral("GET_PRIVATE_LINK:") + localPath + QLatin1Char('\n'); - clientSocket->write(command.toUtf8()); - QVERIFY(clientSocket->flush()); - - // ----- Wait for the PRIVATE_LINK response ----- - QSignalSpy readySpy(clientSocket.get(), &QLocalSocket::readyRead); - QVERIFY(readySpy.wait(5000)); - - // Collect all data (may arrive in chunks) - QByteArray received; - for (int retries = 5; retries > 0; --retries) { - received += clientSocket->readAll(); - if (received.contains("PRIVATE_LINK:")) - break; - if (!readySpy.wait(1000)) - break; - } + QMetaObject::invokeMethod(&api, "command_GET_PRIVATE_LINK", + Q_ARG(QString, localPath), Q_ARG(OCC::SocketListener *, &listener)); - const QByteArray expectedResponse = + const QByteArray expected = QByteArrayLiteral("PRIVATE_LINK:") + expectedUrl.toUtf8() + '\n'; - QVERIFY2(received.contains(expectedResponse), - qPrintable(QStringLiteral("Expected '%1' in received data '%2'") - .arg(QString::fromUtf8(expectedResponse), QString::fromUtf8(received)))); + QTRY_VERIFY2(capture.data().contains(expected), + qPrintable(QStringLiteral("Expected '%1' in buffer '%2'") + .arg(QString::fromUtf8(expected), QString::fromUtf8(capture.data())))); } void testGetPrivateLinkNoResponseWhenFileNotSynced() { - // The path /tmp/not_a_synced_file_testsocketapi.txt is not under any registered - // sync root → command_GET_PRIVATE_LINK should return early with no reply. + QBuffer capture; + capture.open(QIODevice::ReadWrite); + SocketListener listener(&capture); SocketApi api; - api.startShellIntegration(); - - std::unique_ptr clientSocket(connectAndWaitForListener()); - QVERIFY(clientSocket); - - // Drain broadcast messages - QCoreApplication::processEvents(); - clientSocket->readAll(); - - const QString command = - QStringLiteral("GET_PRIVATE_LINK:/tmp/not_a_synced_file_testsocketapi.txt\n"); - clientSocket->write(command.toUtf8()); - QVERIFY(clientSocket->flush()); + QMetaObject::invokeMethod(&api, "command_GET_PRIVATE_LINK", + Q_ARG(QString, QStringLiteral("/tmp/not_a_synced_file_testsocketapi.txt")), + Q_ARG(OCC::SocketListener *, &listener)); - // Wait briefly — there should be no PRIVATE_LINK message. - // Using QTest::qWait (unconditional) rather than QSignalSpy::wait so the - // assertion is always evaluated and cannot pass vacuously when no data arrives. QTest::qWait(500); - const QByteArray received = clientSocket->readAll(); - QVERIFY2(!received.contains("PRIVATE_LINK:"), - qPrintable(QStringLiteral("Expected no PRIVATE_LINK response for unsynced file, got: ") + received)); + QVERIFY2(!capture.data().contains("PRIVATE_LINK:"), + qPrintable(QStringLiteral("Expected no PRIVATE_LINK response for unsynced file, got: ") + + QString::fromUtf8(capture.data()))); } };