fix: defer client disconnect cleanup
This commit is contained in:
parent
6934dbe62b
commit
6592dab84e
11 changed files with 483 additions and 16 deletions
|
|
@ -41,6 +41,10 @@ enum class EventTypes : uint32_t
|
|||
*/
|
||||
ClientDisconnected,
|
||||
|
||||
/** Internal client event used to defer disconnect cleanup until the current callback has returned.
|
||||
*/
|
||||
ClientDisconnectRequested,
|
||||
|
||||
/// A stream sends this event when \c read() will return with data.
|
||||
StreamInputReady,
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,19 @@
|
|||
// Client
|
||||
//
|
||||
|
||||
Client::DisconnectRequest::DisconnectRequest(Kind kind, const char *message)
|
||||
: m_kind(kind),
|
||||
m_message(message != nullptr ? message : "")
|
||||
{
|
||||
}
|
||||
|
||||
Client::DisconnectRequest::DisconnectRequest(deskflow::core::ConnectionRefusal reason, const char *message)
|
||||
: m_kind(Kind::Refuse),
|
||||
m_refusalReason(reason),
|
||||
m_message(message != nullptr ? message : "")
|
||||
{
|
||||
}
|
||||
|
||||
Client::Client(
|
||||
IEventQueue *events, const std::string &name, const NetworkAddress &address, ISocketFactory *socketFactory,
|
||||
deskflow::Screen *screen
|
||||
|
|
@ -432,6 +445,9 @@ void Client::setupConnection()
|
|||
{
|
||||
assert(m_stream != nullptr);
|
||||
|
||||
m_events->addHandler(EventTypes::ClientDisconnectRequested, m_stream->getEventTarget(), [this](const auto &e) {
|
||||
handleDisconnectRequested(e);
|
||||
});
|
||||
m_events->addHandler(EventTypes::SocketDisconnected, m_stream->getEventTarget(), [this](const auto &) {
|
||||
handleDisconnected();
|
||||
});
|
||||
|
|
@ -483,6 +499,7 @@ void Client::cleanupConnecting()
|
|||
{
|
||||
if (m_stream != nullptr) {
|
||||
m_events->removeHandler(EventTypes::DataSocketConnected, m_stream->getEventTarget());
|
||||
m_events->removeHandler(EventTypes::DataSocketSecureConnected, m_stream->getEventTarget());
|
||||
m_events->removeHandler(EventTypes::DataSocketConnectionFailed, m_stream->getEventTarget());
|
||||
}
|
||||
}
|
||||
|
|
@ -496,6 +513,7 @@ void Client::cleanupConnection()
|
|||
m_events->removeHandler(StreamInputShutdown, m_stream->getEventTarget());
|
||||
m_events->removeHandler(StreamOutputShutdown, m_stream->getEventTarget());
|
||||
m_events->removeHandler(SocketDisconnected, m_stream->getEventTarget());
|
||||
m_events->removeHandler(ClientDisconnectRequested, m_stream->getEventTarget());
|
||||
cleanupStream();
|
||||
}
|
||||
}
|
||||
|
|
@ -583,6 +601,21 @@ void Client::handleDisconnected()
|
|||
sendEvent(EventTypes::ClientDisconnected);
|
||||
}
|
||||
|
||||
void Client::handleDisconnectRequested(const Event &event)
|
||||
{
|
||||
const auto *request = static_cast<const DisconnectRequest *>(event.getDataObject());
|
||||
if (request == nullptr) {
|
||||
disconnect(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
if (request->kind() == DisconnectRequest::Kind::Refuse) {
|
||||
refuseConnection(request->refusalReason(), request->message());
|
||||
} else {
|
||||
disconnect(request->message());
|
||||
}
|
||||
}
|
||||
|
||||
void Client::handleShapeChanged()
|
||||
{
|
||||
LOG_DEBUG("resolution changed");
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@
|
|||
|
||||
#include "deskflow/IClient.h"
|
||||
|
||||
#include "base/Event.h"
|
||||
#include "base/EventTypes.h"
|
||||
#include "common/Enums.h"
|
||||
#include "deskflow/IClipboard.h"
|
||||
#include "net/NetworkAddress.h"
|
||||
|
||||
#include <climits>
|
||||
#include <string>
|
||||
|
||||
class Event;
|
||||
class EventQueueTimer;
|
||||
|
|
@ -39,6 +41,39 @@ This class implements the top-level client algorithms for deskflow.
|
|||
class Client : public IClient
|
||||
{
|
||||
public:
|
||||
class DisconnectRequest : public EventData
|
||||
{
|
||||
public:
|
||||
enum class Kind
|
||||
{
|
||||
Disconnect,
|
||||
Refuse
|
||||
};
|
||||
|
||||
DisconnectRequest(Kind kind, const char *message);
|
||||
DisconnectRequest(deskflow::core::ConnectionRefusal reason, const char *message);
|
||||
|
||||
Kind kind() const
|
||||
{
|
||||
return m_kind;
|
||||
}
|
||||
|
||||
deskflow::core::ConnectionRefusal refusalReason() const
|
||||
{
|
||||
return m_refusalReason;
|
||||
}
|
||||
|
||||
const char *message() const
|
||||
{
|
||||
return m_message.empty() ? nullptr : m_message.c_str();
|
||||
}
|
||||
|
||||
private:
|
||||
Kind m_kind = Kind::Disconnect;
|
||||
deskflow::core::ConnectionRefusal m_refusalReason = deskflow::core::ConnectionRefusal::ProtocolError;
|
||||
std::string m_message;
|
||||
};
|
||||
|
||||
class FailInfo
|
||||
{
|
||||
public:
|
||||
|
|
@ -175,6 +210,7 @@ private:
|
|||
void handleConnectTimeout();
|
||||
void handleOutputError();
|
||||
void handleDisconnected();
|
||||
void handleDisconnectRequested(const Event &event);
|
||||
void handleShapeChanged();
|
||||
void handleClipboardGrabbed(const Event &event);
|
||||
void handleHello();
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ void ServerProxy::handleData()
|
|||
// verify we got an entire code
|
||||
if (n != 4) {
|
||||
LOG_ERR("incomplete message from server: %d bytes", n);
|
||||
m_client->disconnect("incomplete message from server");
|
||||
requestDisconnect("incomplete message from server");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -112,7 +112,7 @@ void ServerProxy::handleData()
|
|||
} catch (const BadClientException &e) {
|
||||
LOG_ERR("protocol error from server: %s", e.what());
|
||||
ProtocolUtil::writef(m_stream, kMsgEBad);
|
||||
m_client->disconnect("invalid message from server");
|
||||
requestDisconnect("invalid message from server");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -167,7 +167,7 @@ ServerProxy::ConnectionResult ServerProxy::parseHandshakeMessage(const uint8_t *
|
|||
else if (memcmp(code, kMsgCClose, 4) == 0) {
|
||||
// server wants us to hangup
|
||||
LOG_VERBOSE("recv close");
|
||||
m_client->disconnect(nullptr);
|
||||
requestDisconnect(nullptr);
|
||||
return Disconnect;
|
||||
}
|
||||
|
||||
|
|
@ -176,25 +176,25 @@ ServerProxy::ConnectionResult ServerProxy::parseHandshakeMessage(const uint8_t *
|
|||
int32_t minor;
|
||||
ProtocolUtil::readf(m_stream, kMsgEIncompatible + 4, &major, &minor);
|
||||
LOG_ERR("server has incompatible version %d.%d", major, minor);
|
||||
m_client->refuseConnection(IncompatibleVersion, "server has incompatible version");
|
||||
requestRefuseConnection(IncompatibleVersion, "server has incompatible version");
|
||||
return Disconnect;
|
||||
}
|
||||
|
||||
else if (memcmp(code, kMsgEBusy, 4) == 0) {
|
||||
LOG_ERR("server already has a connected client with name \"%s\"", m_client->getName().c_str());
|
||||
m_client->refuseConnection(AlreadyConnected, "server already has a connected client with our name");
|
||||
requestRefuseConnection(AlreadyConnected, "server already has a connected client with our name");
|
||||
return Disconnect;
|
||||
}
|
||||
|
||||
else if (memcmp(code, kMsgEUnknown, 4) == 0) {
|
||||
LOG_ERR("server refused client with name \"%s\"", m_client->getName().c_str());
|
||||
m_client->refuseConnection(UnknownClient, "server refused client with our name");
|
||||
requestRefuseConnection(UnknownClient, "server refused client with our name");
|
||||
return Disconnect;
|
||||
}
|
||||
|
||||
else if (memcmp(code, kMsgEBad, 4) == 0) {
|
||||
LOG_ERR("server disconnected due to a protocol error");
|
||||
m_client->refuseConnection(ProtocolError, "server reported a protocol error");
|
||||
requestRefuseConnection(ProtocolError, "server reported a protocol error");
|
||||
return Disconnect;
|
||||
} else if (memcmp(code, kMsgDLanguageSynchronisation, 4) == 0) {
|
||||
setServerLanguages();
|
||||
|
|
@ -312,11 +312,11 @@ ServerProxy::ConnectionResult ServerProxy::parseMessage(const uint8_t *code)
|
|||
else if (memcmp(code, kMsgCClose, 4) == 0) {
|
||||
// server wants us to hangup
|
||||
LOG_VERBOSE("recv close");
|
||||
m_client->disconnect(nullptr);
|
||||
requestDisconnect(nullptr);
|
||||
return Disconnect;
|
||||
} else if (memcmp(code, kMsgEBad, 4) == 0) {
|
||||
LOG_ERR("server disconnected due to a protocol error");
|
||||
m_client->disconnect("server reported a protocol error");
|
||||
requestDisconnect("server reported a protocol error");
|
||||
return Disconnect;
|
||||
} else {
|
||||
return Unknown;
|
||||
|
|
@ -337,7 +337,22 @@ ServerProxy::ConnectionResult ServerProxy::parseMessage(const uint8_t *code)
|
|||
void ServerProxy::handleKeepAliveAlarm()
|
||||
{
|
||||
LOG_INFO("server is dead");
|
||||
m_client->disconnect("server is not responding");
|
||||
requestDisconnect("server is not responding");
|
||||
}
|
||||
|
||||
void ServerProxy::requestDisconnect(const char *message)
|
||||
{
|
||||
m_events->addEvent(Event(
|
||||
EventTypes::ClientDisconnectRequested, m_stream->getEventTarget(),
|
||||
new Client::DisconnectRequest(Client::DisconnectRequest::Kind::Disconnect, message)
|
||||
));
|
||||
}
|
||||
|
||||
void ServerProxy::requestRefuseConnection(deskflow::core::ConnectionRefusal reason, const char *message)
|
||||
{
|
||||
m_events->addEvent(Event(
|
||||
EventTypes::ClientDisconnectRequested, m_stream->getEventTarget(), new Client::DisconnectRequest(reason, message)
|
||||
));
|
||||
}
|
||||
|
||||
void ServerProxy::onInfoChanged()
|
||||
|
|
@ -550,7 +565,7 @@ void ServerProxy::setClipboard()
|
|||
|
||||
LOG_INFO("clipboard was updated");
|
||||
} else if (r == TransferState::Error) {
|
||||
m_client->disconnect("invalid clipboard data from server");
|
||||
requestDisconnect("invalid clipboard data from server");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include "common/Enums.h"
|
||||
#include "deskflow/ClipboardChunk.h"
|
||||
#include "deskflow/ClipboardTypes.h"
|
||||
#include "deskflow/KeyTypes.h"
|
||||
|
|
@ -77,6 +78,8 @@ private:
|
|||
// event handlers
|
||||
void handleData();
|
||||
void handleKeepAliveAlarm();
|
||||
void requestDisconnect(const char *message);
|
||||
void requestRefuseConnection(deskflow::core::ConnectionRefusal reason, const char *message);
|
||||
|
||||
// message handlers
|
||||
void enter();
|
||||
|
|
|
|||
|
|
@ -262,10 +262,11 @@ void ClientApp::closeClient(Client *client)
|
|||
return;
|
||||
}
|
||||
using enum EventTypes;
|
||||
getEvents()->removeHandler(ClientConnected, client);
|
||||
getEvents()->removeHandler(ClientConnectionFailed, client);
|
||||
getEvents()->removeHandler(ClientConnectionRefused, client);
|
||||
getEvents()->removeHandler(ClientDisconnected, client);
|
||||
auto *target = client->getEventTarget();
|
||||
getEvents()->removeHandler(ClientConnected, target);
|
||||
getEvents()->removeHandler(ClientConnectionFailed, target);
|
||||
getEvents()->removeHandler(ClientConnectionRefused, target);
|
||||
getEvents()->removeHandler(ClientDisconnected, target);
|
||||
delete client;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ enable_testing()
|
|||
find_package(Qt6 ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS Test)
|
||||
|
||||
add_subdirectory(base)
|
||||
add_subdirectory(client)
|
||||
add_subdirectory(common)
|
||||
add_subdirectory(deskflow)
|
||||
add_subdirectory(gui)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ create_test(
|
|||
create_test(
|
||||
NAME EventQueueTests
|
||||
DEPENDS base
|
||||
LIBS arch mt
|
||||
LIBS arch mt ${extra_libs}
|
||||
SOURCE EventQueueTests.cpp
|
||||
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/base"
|
||||
)
|
||||
|
|
|
|||
14
src/unittests/client/CMakeLists.txt
Normal file
14
src/unittests/client/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# SPDX-FileCopyrightText: (C) 2026 Deskflow Developers
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
if(WIN32)
|
||||
set(extra_libs version)
|
||||
endif()
|
||||
|
||||
create_test(
|
||||
NAME ServerProxyTests
|
||||
DEPENDS client
|
||||
LIBS arch base io mt net platform server app ${extra_libs}
|
||||
SOURCE ServerProxyTests.cpp
|
||||
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/client"
|
||||
)
|
||||
335
src/unittests/client/ServerProxyTests.cpp
Normal file
335
src/unittests/client/ServerProxyTests.cpp
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
/*
|
||||
* Deskflow -- mouse and keyboard sharing utility
|
||||
* SPDX-FileCopyrightText: (C) 2026 Deskflow Developers
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
|
||||
*/
|
||||
|
||||
#include "ServerProxyTests.h"
|
||||
|
||||
#include "base/Event.h"
|
||||
#include "base/IEventQueue.h"
|
||||
#include "client/Client.h"
|
||||
#include "client/ServerProxy.h"
|
||||
#include "deskflow/AppUtil.h"
|
||||
#include "deskflow/ProtocolTypes.h"
|
||||
#include "io/IStream.h"
|
||||
|
||||
#include <QTest>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
class TestAppUtil : public AppUtil
|
||||
{
|
||||
public:
|
||||
int run() override
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
void startNode() override
|
||||
{
|
||||
}
|
||||
|
||||
std::vector<std::string> getKeyboardLayoutList() override
|
||||
{
|
||||
return {"en"};
|
||||
}
|
||||
|
||||
std::string getCurrentLanguageCode() override
|
||||
{
|
||||
return "en";
|
||||
}
|
||||
};
|
||||
|
||||
class FakeStream : public deskflow::IStream
|
||||
{
|
||||
public:
|
||||
void push(const std::string &bytes)
|
||||
{
|
||||
m_chunks.push_back(bytes);
|
||||
}
|
||||
|
||||
void close() override
|
||||
{
|
||||
m_chunks.clear();
|
||||
m_inputShutdown = true;
|
||||
}
|
||||
|
||||
uint32_t read(void *buffer, uint32_t size) override
|
||||
{
|
||||
if (m_inputShutdown || m_chunks.empty() || size == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto &front = m_chunks.front();
|
||||
const size_t bytesToRead = std::min(static_cast<size_t>(size), front.size());
|
||||
if (buffer != nullptr) {
|
||||
std::memcpy(buffer, front.data(), bytesToRead);
|
||||
}
|
||||
|
||||
front.erase(0, bytesToRead);
|
||||
if (front.empty()) {
|
||||
m_chunks.pop_front();
|
||||
}
|
||||
return static_cast<uint32_t>(bytesToRead);
|
||||
}
|
||||
|
||||
void write(const void *, uint32_t) override
|
||||
{
|
||||
}
|
||||
|
||||
void flush() override
|
||||
{
|
||||
}
|
||||
|
||||
void shutdownInput() override
|
||||
{
|
||||
close();
|
||||
}
|
||||
|
||||
void shutdownOutput() override
|
||||
{
|
||||
}
|
||||
|
||||
void *getEventTarget() const override
|
||||
{
|
||||
return const_cast<FakeStream *>(this);
|
||||
}
|
||||
|
||||
bool isReady() const override
|
||||
{
|
||||
return !m_inputShutdown && !m_chunks.empty();
|
||||
}
|
||||
|
||||
uint32_t getSize() const override
|
||||
{
|
||||
size_t total = 0;
|
||||
for (const auto &chunk : m_chunks) {
|
||||
total += chunk.size();
|
||||
}
|
||||
return static_cast<uint32_t>(std::min<size_t>(total, UINT32_MAX));
|
||||
}
|
||||
|
||||
private:
|
||||
std::deque<std::string> m_chunks;
|
||||
bool m_inputShutdown = false;
|
||||
};
|
||||
|
||||
class RecordingEventQueue : public IEventQueue
|
||||
{
|
||||
public:
|
||||
~RecordingEventQueue() override
|
||||
{
|
||||
for (const auto &event : m_addedEvents) {
|
||||
Event::deleteData(event);
|
||||
}
|
||||
}
|
||||
|
||||
int loop() override
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
void adoptBuffer(IEventQueueBuffer *) override
|
||||
{
|
||||
}
|
||||
|
||||
bool getEvent(Event &, double = -1.0) override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool dispatchEvent(const Event &event) override
|
||||
{
|
||||
const auto handler = m_handlers.find(HandlerKey{event.getType(), event.getTarget()});
|
||||
if (handler == m_handlers.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto callback = handler->second;
|
||||
callback(event);
|
||||
return true;
|
||||
}
|
||||
|
||||
void addEvent(Event &&event) override
|
||||
{
|
||||
m_addedEvents.emplace_back(std::move(event));
|
||||
}
|
||||
|
||||
EventQueueTimer *newTimer(double, void *) override
|
||||
{
|
||||
return timer();
|
||||
}
|
||||
|
||||
EventQueueTimer *newOneShotTimer(double, void *) override
|
||||
{
|
||||
return timer();
|
||||
}
|
||||
|
||||
void deleteTimer(EventQueueTimer *) override
|
||||
{
|
||||
}
|
||||
|
||||
void addHandler(EventTypes type, void *target, const EventHandler &handler) override
|
||||
{
|
||||
m_handlers[HandlerKey{type, target}] = handler;
|
||||
}
|
||||
|
||||
void removeHandler(EventTypes type, void *target) override
|
||||
{
|
||||
m_handlers.erase(HandlerKey{type, target});
|
||||
}
|
||||
|
||||
void removeHandlers(void *target) override
|
||||
{
|
||||
for (auto it = m_handlers.begin(); it != m_handlers.end();) {
|
||||
if (it->first.target == target) {
|
||||
it = m_handlers.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void waitForReady() const override
|
||||
{
|
||||
}
|
||||
|
||||
void *getSystemTarget() override
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
EventQueueTimer *timer()
|
||||
{
|
||||
return reinterpret_cast<EventQueueTimer *>(&m_timerStorage);
|
||||
}
|
||||
|
||||
const std::vector<Event> &addedEvents() const
|
||||
{
|
||||
return m_addedEvents;
|
||||
}
|
||||
|
||||
private:
|
||||
struct HandlerKey
|
||||
{
|
||||
EventTypes type;
|
||||
void *target;
|
||||
|
||||
bool operator<(const HandlerKey &other) const
|
||||
{
|
||||
if (type != other.type) {
|
||||
return static_cast<uint32_t>(type) < static_cast<uint32_t>(other.type);
|
||||
}
|
||||
return std::less<void *>{}(target, other.target);
|
||||
}
|
||||
};
|
||||
|
||||
int m_timerStorage = 0;
|
||||
std::map<HandlerKey, EventHandler> m_handlers;
|
||||
std::vector<Event> m_addedEvents;
|
||||
};
|
||||
|
||||
class TestServerProxy : public ServerProxy
|
||||
{
|
||||
public:
|
||||
using ServerProxy::ServerProxy;
|
||||
|
||||
bool parseHandshakeMessageReturnsDisconnect(const uint8_t *code)
|
||||
{
|
||||
return parseHandshakeMessage(code) == ConnectionResult::Disconnect;
|
||||
}
|
||||
};
|
||||
|
||||
Client *undereferenceableClient()
|
||||
{
|
||||
// These paths must queue cleanup without calling through to Client.
|
||||
return reinterpret_cast<Client *>(0x1);
|
||||
}
|
||||
|
||||
TestAppUtil &testAppUtil()
|
||||
{
|
||||
static TestAppUtil util;
|
||||
return util;
|
||||
}
|
||||
|
||||
const Client::DisconnectRequest *disconnectRequest(const RecordingEventQueue &events)
|
||||
{
|
||||
if (events.addedEvents().size() != 1) {
|
||||
return nullptr;
|
||||
}
|
||||
return static_cast<const Client::DisconnectRequest *>(events.addedEvents().front().getDataObject());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ServerProxyTests::initTestCase()
|
||||
{
|
||||
(void)testAppUtil();
|
||||
m_log.setFilter(LogLevel::Level::Debug);
|
||||
}
|
||||
|
||||
void ServerProxyTests::handleKeepAliveAlarm_timeout_queuesDisconnectRequest()
|
||||
{
|
||||
RecordingEventQueue events;
|
||||
FakeStream stream;
|
||||
ServerProxy proxy(undereferenceableClient(), &stream, &events);
|
||||
|
||||
QVERIFY(events.dispatchEvent(Event(EventTypes::Timer, events.timer())));
|
||||
|
||||
QCOMPARE(events.addedEvents().size(), static_cast<size_t>(1));
|
||||
const auto &event = events.addedEvents().front();
|
||||
QVERIFY(event.getType() == EventTypes::ClientDisconnectRequested);
|
||||
QCOMPARE(event.getTarget(), stream.getEventTarget());
|
||||
|
||||
const auto *request = disconnectRequest(events);
|
||||
QVERIFY(request != nullptr);
|
||||
QVERIFY(request->kind() == Client::DisconnectRequest::Kind::Disconnect);
|
||||
QCOMPARE(QString::fromUtf8(request->message()), QStringLiteral("server is not responding"));
|
||||
}
|
||||
|
||||
void ServerProxyTests::handleData_incompleteMessage_queuesDisconnectRequest()
|
||||
{
|
||||
RecordingEventQueue events;
|
||||
FakeStream stream;
|
||||
stream.push("DF");
|
||||
ServerProxy proxy(undereferenceableClient(), &stream, &events);
|
||||
|
||||
QVERIFY(events.dispatchEvent(Event(EventTypes::StreamInputReady, stream.getEventTarget())));
|
||||
|
||||
QCOMPARE(events.addedEvents().size(), static_cast<size_t>(1));
|
||||
const auto &event = events.addedEvents().front();
|
||||
QVERIFY(event.getType() == EventTypes::ClientDisconnectRequested);
|
||||
QCOMPARE(event.getTarget(), stream.getEventTarget());
|
||||
|
||||
const auto *request = disconnectRequest(events);
|
||||
QVERIFY(request != nullptr);
|
||||
QVERIFY(request->kind() == Client::DisconnectRequest::Kind::Disconnect);
|
||||
QCOMPARE(QString::fromUtf8(request->message()), QStringLiteral("incomplete message from server"));
|
||||
}
|
||||
|
||||
void ServerProxyTests::parseHandshakeMessage_protocolError_queuesRefusalRequest()
|
||||
{
|
||||
RecordingEventQueue events;
|
||||
FakeStream stream;
|
||||
TestServerProxy proxy(undereferenceableClient(), &stream, &events);
|
||||
|
||||
QVERIFY(proxy.parseHandshakeMessageReturnsDisconnect(reinterpret_cast<const uint8_t *>(kMsgEBad)));
|
||||
QCOMPARE(events.addedEvents().size(), static_cast<size_t>(1));
|
||||
const auto *request = disconnectRequest(events);
|
||||
QVERIFY(request != nullptr);
|
||||
QVERIFY(request->kind() == Client::DisconnectRequest::Kind::Refuse);
|
||||
QVERIFY(request->refusalReason() == deskflow::core::ConnectionRefusal::ProtocolError);
|
||||
QCOMPARE(QString::fromUtf8(request->message()), QStringLiteral("server reported a protocol error"));
|
||||
}
|
||||
|
||||
QTEST_MAIN(ServerProxyTests)
|
||||
25
src/unittests/client/ServerProxyTests.h
Normal file
25
src/unittests/client/ServerProxyTests.h
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Deskflow -- mouse and keyboard sharing utility
|
||||
* SPDX-FileCopyrightText: (C) 2026 Deskflow Developers
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "base/Log.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class ServerProxyTests : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
void handleKeepAliveAlarm_timeout_queuesDisconnectRequest();
|
||||
void handleData_incompleteMessage_queuesDisconnectRequest();
|
||||
void parseHandshakeMessage_protocolError_queuesRefusalRequest();
|
||||
|
||||
private:
|
||||
Log m_log;
|
||||
};
|
||||
Loading…
Reference in a new issue