Skip to content

Commit 413bd02

Browse files
DeepDiver1975claude
andcommitted
test: add unit tests for GET_PRIVATE_LINK socket command
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
1 parent b131e24 commit 413bd02

2 files changed

Lines changed: 258 additions & 0 deletions

File tree

test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ if( UNIX AND NOT APPLE )
5858
endif(UNIX AND NOT APPLE)
5959

6060
owncloud_add_test(FileSystem)
61+
owncloud_add_test(SocketApi)
6162

6263
owncloud_add_test(FolderMan)
6364

test/testsocketapi.cpp

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
/*
2+
* Copyright (C) by Thomas Müller
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but
10+
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11+
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* for more details.
13+
*/
14+
15+
#include <QtTest>
16+
#include <QLocalSocket>
17+
#include <QSignalSpy>
18+
#include <QStandardPaths>
19+
#include <QTemporaryDir>
20+
21+
#include <chrono>
22+
using namespace std::chrono_literals;
23+
24+
#include "socketapi/socketapi.h"
25+
#include "folderman.h"
26+
#include "accountmanager.h"
27+
28+
#include "testutils/syncenginetestutils.h"
29+
#include "testutils/testutils.h"
30+
31+
using namespace OCC;
32+
33+
// A minimal fake reply that emits a Depth:0 PROPFIND multistatus response
34+
// with status 207 and the correct content-type, suitable for private link lookups.
35+
class FakeMultistatReply : public FakeReply
36+
{
37+
Q_OBJECT
38+
public:
39+
QByteArray _body;
40+
41+
FakeMultistatReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request,
42+
const QByteArray &body, QObject *parent)
43+
: FakeReply(parent), _body(body)
44+
{
45+
setRequest(request);
46+
setUrl(request.url());
47+
setOperation(op);
48+
open(QIODevice::ReadOnly);
49+
QTimer::singleShot(10ms, this, &FakeMultistatReply::respond);
50+
}
51+
52+
void respond()
53+
{
54+
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 207);
55+
setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/xml; charset=utf-8"));
56+
setHeader(QNetworkRequest::ContentLengthHeader, _body.size());
57+
Q_EMIT metaDataChanged();
58+
Q_EMIT readyRead();
59+
checkedFinished();
60+
}
61+
62+
qint64 readData(char *buf, qint64 max) override
63+
{
64+
max = qMin<qint64>(max, _body.size());
65+
std::copy(_body.cbegin(), _body.cbegin() + max, buf);
66+
_body.remove(0, static_cast<int>(max));
67+
return max;
68+
}
69+
70+
qint64 bytesAvailable() const override { return _body.size(); }
71+
};
72+
73+
class TestSocketApi : public QObject
74+
{
75+
Q_OBJECT
76+
77+
// Path of the folder registered during a test (reset by cleanup()).
78+
QString _registeredFolderPath;
79+
80+
// Compute the socket path the same way guiutility_unix.cpp does.
81+
// Utility::socketApiSocketPath() provides the same logic, but it lives in the
82+
// GUI library and is not exposed as a public method of SocketApi, so the
83+
// computation is duplicated here rather than introducing an extra dependency.
84+
static QString socketApiPath()
85+
{
86+
return QStringLiteral("%1/ownCloud/socket")
87+
.arg(QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation));
88+
}
89+
90+
// Connect to the SocketApi's server and wait for the connection to be accepted
91+
// (slotNewConnection fired). Returns the connected socket or nullptr on failure.
92+
static QLocalSocket *connectAndWaitForListener()
93+
{
94+
const QString path = socketApiPath();
95+
auto *sock = new QLocalSocket;
96+
sock->connectToServer(path);
97+
if (!sock->waitForConnected(3000)) {
98+
qWarning() << "Failed to connect to socket" << path
99+
<< "error:" << sock->errorString();
100+
delete sock;
101+
return nullptr;
102+
}
103+
// Give the server side a chance to run slotNewConnection and
104+
// insert the listener into _listeners.
105+
QCoreApplication::processEvents();
106+
return sock;
107+
}
108+
109+
private Q_SLOTS:
110+
void initTestCase()
111+
{
112+
// Ensure FolderMan singleton exists before any test
113+
TestUtils::folderMan();
114+
}
115+
116+
void cleanup()
117+
{
118+
// Remove any folder registered during the test to avoid polluting later tests.
119+
if (!_registeredFolderPath.isEmpty()) {
120+
Folder *folder = TestUtils::folderMan()->folderForPath(_registeredFolderPath);
121+
if (folder) {
122+
TestUtils::folderMan()->removeFolderFromGui(folder);
123+
}
124+
_registeredFolderPath.clear();
125+
}
126+
}
127+
128+
void testGetPrivateLinkSendsUrlOverSocket()
129+
{
130+
// ----- Set up a paused sync folder so no sync fires -----
131+
132+
// Create a real directory on disk as the local sync root
133+
const QTemporaryDir tempDir = TestUtils::createTempDir();
134+
QVERIFY(tempDir.isValid());
135+
// Ensure trailing slash (FolderDefinition normalises it, but be explicit)
136+
const QString localPath = QDir::cleanPath(tempDir.path()) + QLatin1Char('/');
137+
138+
// createDummyAccount creates an account with a FakeAM (empty FileInfo)
139+
auto accountState = createDummyAccount();
140+
Account *account = accountState->account();
141+
142+
// Enable private links capability
143+
auto cap = TestUtils::testCapabilities();
144+
cap[QStringLiteral("files")] = QVariantMap{{QStringLiteral("privateLinks"), true}};
145+
account->setCapabilities({account->url(), cap});
146+
147+
// Set a server override: intercept the Depth:0 PROPFIND that fetchPrivateLinkUrl issues
148+
// and reply with a 207 multistatus containing the private link URL.
149+
const QString expectedUrl = QStringLiteral("https://example.com/f/abc123");
150+
auto *fakeAm = dynamic_cast<FakeAM *>(account->accessManager());
151+
QVERIFY(fakeAm);
152+
fakeAm->setOverride([expectedUrl](QNetworkAccessManager::Operation op,
153+
const QNetworkRequest &req,
154+
QIODevice *) -> QNetworkReply * {
155+
if (op != QNetworkAccessManager::CustomOperation)
156+
return nullptr;
157+
if (req.attribute(QNetworkRequest::CustomVerbAttribute).toByteArray()
158+
!= QByteArrayLiteral("PROPFIND"))
159+
return nullptr;
160+
if (req.rawHeader(QByteArrayLiteral("Depth")) != QByteArrayLiteral("0"))
161+
return nullptr;
162+
163+
const QString path = req.url().path();
164+
const QByteArray body =
165+
QByteArrayLiteral("<?xml version=\"1.0\"?>"
166+
"<d:multistatus xmlns:d=\"DAV:\" xmlns:oc=\"http://owncloud.org/ns\">"
167+
"<d:response><d:href>") + path.toUtf8()
168+
+ QByteArrayLiteral("</d:href><d:propstat><d:prop>"
169+
"<oc:privatelink>") + expectedUrl.toUtf8()
170+
+ QByteArrayLiteral("</oc:privatelink>"
171+
"</d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat>"
172+
"</d:response></d:multistatus>");
173+
return new FakeMultistatReply(op, req, body, nullptr);
174+
});
175+
176+
// Register the directory with FolderMan (paused so no sync fires)
177+
auto def = TestUtils::createDummyFolderDefinition(account, localPath);
178+
def.setPaused(true);
179+
Folder *folder = TestUtils::folderMan()->addFolder(accountState.get(), def);
180+
QVERIFY(folder);
181+
// Track the path so cleanup() can remove it after the test.
182+
_registeredFolderPath = localPath;
183+
184+
// ----- Start SocketApi server -----
185+
SocketApi api;
186+
api.startShellIntegration();
187+
188+
// Connect a client socket (triggers slotNewConnection → listener registered)
189+
std::unique_ptr<QLocalSocket> clientSocket(connectAndWaitForListener());
190+
QVERIFY(clientSocket);
191+
192+
// Drain any broadcast messages sent on connect (e.g. REGISTER_PATH)
193+
QCoreApplication::processEvents();
194+
clientSocket->readAll();
195+
196+
// ----- Send the GET_PRIVATE_LINK command -----
197+
// Use the sync-folder root as the localFile argument.
198+
// isSyncFolder() is true for the root path, so the journal record
199+
// check is bypassed.
200+
const QString command =
201+
QStringLiteral("GET_PRIVATE_LINK:") + localPath + QLatin1Char('\n');
202+
clientSocket->write(command.toUtf8());
203+
QVERIFY(clientSocket->flush());
204+
205+
// ----- Wait for the PRIVATE_LINK response -----
206+
QSignalSpy readySpy(clientSocket.get(), &QLocalSocket::readyRead);
207+
QVERIFY(readySpy.wait(5000));
208+
209+
// Collect all data (may arrive in chunks)
210+
QByteArray received;
211+
for (int retries = 5; retries > 0; --retries) {
212+
received += clientSocket->readAll();
213+
if (received.contains("PRIVATE_LINK:"))
214+
break;
215+
if (!readySpy.wait(1000))
216+
break;
217+
}
218+
219+
const QByteArray expectedResponse =
220+
QByteArrayLiteral("PRIVATE_LINK:") + expectedUrl.toUtf8() + '\n';
221+
QVERIFY2(received.contains(expectedResponse),
222+
qPrintable(QStringLiteral("Expected '%1' in received data '%2'")
223+
.arg(QString::fromUtf8(expectedResponse), QString::fromUtf8(received))));
224+
}
225+
226+
void testGetPrivateLinkNoResponseWhenFileNotSynced()
227+
{
228+
// The path /tmp/not_a_synced_file_testsocketapi.txt is not under any registered
229+
// sync root → command_GET_PRIVATE_LINK should return early with no reply.
230+
231+
SocketApi api;
232+
api.startShellIntegration();
233+
234+
std::unique_ptr<QLocalSocket> clientSocket(connectAndWaitForListener());
235+
QVERIFY(clientSocket);
236+
237+
// Drain broadcast messages
238+
QCoreApplication::processEvents();
239+
clientSocket->readAll();
240+
241+
const QString command =
242+
QStringLiteral("GET_PRIVATE_LINK:/tmp/not_a_synced_file_testsocketapi.txt\n");
243+
clientSocket->write(command.toUtf8());
244+
QVERIFY(clientSocket->flush());
245+
246+
// Wait briefly — there should be no PRIVATE_LINK message.
247+
// Using QTest::qWait (unconditional) rather than QSignalSpy::wait so the
248+
// assertion is always evaluated and cannot pass vacuously when no data arrives.
249+
QTest::qWait(500);
250+
const QByteArray received = clientSocket->readAll();
251+
QVERIFY2(!received.contains("PRIVATE_LINK:"),
252+
qPrintable(QStringLiteral("Expected no PRIVATE_LINK response for unsynced file, got: ") + received));
253+
}
254+
};
255+
256+
QTEST_GUILESS_MAIN(TestSocketApi)
257+
#include "testsocketapi.moc"

0 commit comments

Comments
 (0)