deskflow/src/unittests/client/ServerProxyTests.cpp

331 lines
7.5 KiB
C++

/*
* 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;
}
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)