// SPDX-License-Identifier: GPL-3.0-or-later /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2013 - 2024, nymea GmbH * Copyright (C) 2024 - 2025, chargebyte austria GmbH * * This file is part of nymea-remoteproxy. * * nymea-remoteproxy 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 3 of the License, or * (at your option) any later version. * * nymea-remoteproxy 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. * * You should have received a copy of the GNU General Public License * along with nymea-remoteproxy. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "basetest.h" #include "engine.h" #include "loggingcategories.h" #include "../common/slipdataprocessor.h" #include #include #include #include #include #include #include BaseTest::BaseTest(QObject *parent) : QObject(parent) { resetDebugCategories(); qRegisterMetaType("TransportClient*"); } void BaseTest::loadConfiguration(const QString &fileName) { qDebug() << "Load test configurations" << fileName; m_configuration->loadConfiguration(fileName); //restartEngine(); } void BaseTest::resetDebugCategories() { m_currentDebugCategories = m_defaultDebugCategories; QLoggingCategory::setFilterRules(m_currentDebugCategories); } void BaseTest::addDebugCategory(const QString &debugCategory) { m_currentDebugCategories += debugCategory + "\n"; //qDebug() << m_currentDebugCategories; QLoggingCategory::setFilterRules(m_currentDebugCategories); } void BaseTest::cleanUpEngine() { qDebug() << "Clean up engine"; if (Engine::exists()) { Engine::instance()->stop(); Engine::instance()->destroy(); QVERIFY(!Engine::exists()); } if (m_configuration) { m_configuration = nullptr; } } void BaseTest::restartEngine() { cleanUpEngine(); startEngine(); } void BaseTest::startEngine() { if (!m_configuration) m_configuration = new ProxyConfiguration(Engine::instance()); loadConfiguration(":/test-configuration.conf"); QSignalSpy runningSpy(Engine::instance(), &Engine::runningChanged); Engine::instance()->start(m_configuration); runningSpy.wait(); QVERIFY(runningSpy.count() == 1); QVERIFY(Engine::instance()->running()); } void BaseTest::startServer() { restartEngine(); QVERIFY(Engine::instance()->running()); QVERIFY(Engine::instance()->webSocketServerTunnelProxy()->running()); QVERIFY(Engine::instance()->tcpSocketServerTunnelProxy()->running()); QVERIFY(Engine::instance()->unixSocketServerTunnelProxy()->running()); QVERIFY(Engine::instance()->tunnelProxyServer()->running()); QVERIFY(Engine::instance()->monitorServer()->running()); } void BaseTest::stopServer() { if (!Engine::instance()->running()) return; Engine::instance()->stop(); QVERIFY(!Engine::instance()->running()); cleanUpEngine(); } QVariant BaseTest::invokeWebSocketTunnelProxyApiCall(const QString &method, const QVariantMap params) { QVariantMap request; request.insert("id", m_commandCounter); request.insert("method", method); request.insert("params", params); QJsonDocument jsonDoc = QJsonDocument::fromVariant(request); QWebSocket *socket = new QWebSocket("tunnelproxy-testclient", QWebSocketProtocol::Version13); connect(socket, &QWebSocket::sslErrors, this, &BaseTest::sslErrors); QSignalSpy spyConnection(socket, SIGNAL(connected())); socket->open(Engine::instance()->webSocketServerTunnelProxy()->serverUrl()); spyConnection.wait(); if (spyConnection.count() == 0) { qWarning() << "Failed to connect websocket on tunnel proxy"; return QVariant(); } QSignalSpy dataSpy(socket, SIGNAL(textMessageReceived(QString))); socket->sendTextMessage(QString(jsonDoc.toJson(QJsonDocument::Compact))); dataSpy.wait(); socket->close(); socket->deleteLater(); for (int i = 0; i < dataSpy.count(); i++) { // Make sure the response ends with '}\n' if (!dataSpy.at(i).last().toByteArray().endsWith("}\n")) { qWarning() << "JSON data does not end with \"}\n\""; return QVariant(); } // Make sure the response it a valid JSON string QJsonParseError error; jsonDoc = QJsonDocument::fromJson(dataSpy.at(i).last().toByteArray(), &error); if (error.error != QJsonParseError::NoError) { qWarning() << "JSON parser error" << error.errorString(); return QVariant(); } QVariantMap response = jsonDoc.toVariant().toMap(); // skip notifications if (response.contains("notification")) continue; if (response.value("id").toInt() == m_commandCounter) { m_commandCounter++; return jsonDoc.toVariant(); } } m_commandCounter++; return QVariant(); } QVariant BaseTest::injectWebSocketTunnelProxyData(const QByteArray &data) { QWebSocket *socket = new QWebSocket("tunnelproxy-testclient", QWebSocketProtocol::Version13); connect(socket, &QWebSocket::sslErrors, this, &BaseTest::sslErrors); QSignalSpy spyConnection(socket, SIGNAL(connected())); socket->open(Engine::instance()->webSocketServerTunnelProxy()->serverUrl()); spyConnection.wait(); if (spyConnection.count() == 0) { return QVariant(); } QSignalSpy spy(socket, SIGNAL(textMessageReceived(QString))); socket->sendTextMessage(QString(data)); spy.wait(); socket->close(); socket->deleteLater(); for (int i = 0; i < spy.count(); i++) { // Make sure the response ends with '}\n' if (!spy.at(i).last().toByteArray().endsWith("}\n")) { qWarning() << "JSON data does not end with \"}\n\""; return QVariant(); } // Make sure the response it a valid JSON string QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(spy.at(i).last().toByteArray(), &error); if (error.error != QJsonParseError::NoError) { qWarning() << "JSON parser error" << error.errorString(); return QVariant(); } return jsonDoc.toVariant(); } m_commandCounter++; return QVariant(); } QVariant BaseTest::invokeTcpSocketTunnelProxyApiCall(const QString &method, const QVariantMap params) { QVariantMap request; request.insert("id", m_commandCounter); request.insert("method", method); request.insert("params", params); QJsonDocument jsonDoc = QJsonDocument::fromVariant(request); QSslSocket *socket = new QSslSocket(this); typedef void (QSslSocket:: *sslErrorsSignal)(const QList &); QObject::connect(socket, static_cast(&QSslSocket::sslErrors), this, &BaseTest::sslSocketSslErrors); QSignalSpy spyConnection(socket, &QSslSocket::connected); socket->connectToHostEncrypted(Engine::instance()->tcpSocketServerTunnelProxy()->serverUrl().host(), static_cast(Engine::instance()->tcpSocketServerTunnelProxy()->serverUrl().port())); spyConnection.wait(); if (spyConnection.count() == 0) { return QVariant(); } QSignalSpy dataSpy(socket, &QSslSocket::readyRead); socket->write(jsonDoc.toJson(QJsonDocument::Compact) + "\n"); dataSpy.wait(); if (dataSpy.count() != 1) { qWarning() << "No data received"; return QVariant(); } QByteArray data = socket->readAll(); socket->close(); socket->deleteLater(); // Make sure the response ends with '}\n' if (!data.endsWith("}\n")) { qWarning() << "JSON data does not end with \"}\n\""; return QVariant(); } // Make sure the response it a valid JSON string QJsonParseError error; jsonDoc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { qWarning() << "JSON parser error" << error.errorString(); return QVariant(); } QVariantMap response = jsonDoc.toVariant().toMap(); if (response.value("id").toInt() == m_commandCounter) { m_commandCounter++; return jsonDoc.toVariant(); } m_commandCounter++; return QVariant(); } QVariant BaseTest::injectTcpSocketTunnelProxyData(const QByteArray &data) { QSslSocket *socket = new QSslSocket(this); typedef void (QSslSocket:: *sslErrorsSignal)(const QList &); QObject::connect(socket, static_cast(&QSslSocket::sslErrors), this, &BaseTest::sslSocketSslErrors); QSignalSpy spyConnection(socket, &QSslSocket::connected); socket->connectToHostEncrypted(Engine::instance()->tcpSocketServerTunnelProxy()->serverUrl().host(), static_cast(Engine::instance()->tcpSocketServerTunnelProxy()->serverUrl().port())); spyConnection.wait(); if (spyConnection.count() == 0) { return QVariant(); } QSignalSpy dataSpy(socket, &QSslSocket::readyRead); socket->write(data + "\n"); dataSpy.wait(); if (dataSpy.count() != 1) { qWarning() << "No data received"; return QVariant(); } QByteArray socketData = socket->readAll(); socket->close(); socket->deleteLater(); // Make sure the response ends with '}\n' if (!socketData.endsWith("}\n")) { qWarning() << "JSON data does not end with \"}\n\""; return QVariant(); } // Make sure the response it a valid JSON string QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(socketData, &error); if (error.error != QJsonParseError::NoError) { qWarning() << "JSON parser error" << error.errorString(); return QVariant(); } m_commandCounter++; return jsonDoc.toVariant(); } QPair BaseTest::invokeTcpSocketTunnelProxyApiCallPersistant(const QString &method, const QVariantMap params, bool slipEnabled, QSslSocket *existingSocket) { QSslSocket *socket = nullptr; if (!existingSocket) { socket = new QSslSocket(this); typedef void (QSslSocket:: *sslErrorsSignal)(const QList &); QObject::connect(socket, static_cast(&QSslSocket::sslErrors), this, &BaseTest::sslSocketSslErrors); QSignalSpy spyConnection(socket, &QSslSocket::connected); socket->connectToHostEncrypted(Engine::instance()->tcpSocketServerTunnelProxy()->serverUrl().host(), static_cast(Engine::instance()->tcpSocketServerTunnelProxy()->serverUrl().port())); spyConnection.wait(); if (spyConnection.count() == 0) { return QPair(QVariant(), socket); } } else { socket = existingSocket; } QVariantMap request; request.insert("id", m_commandCounter); request.insert("method", method); request.insert("params", params); QJsonDocument jsonDoc = QJsonDocument::fromVariant(request); QByteArray payload = jsonDoc.toJson(QJsonDocument::Compact) + "\n"; QSignalSpy dataSpy(socket, &QSslSocket::readyRead); if (slipEnabled) { SlipDataProcessor::Frame frame; frame.socketAddress = 0x0000; frame.data = payload; socket->write(SlipDataProcessor::serializeData(SlipDataProcessor::buildFrame(frame))); } else { socket->write(payload); } dataSpy.wait(); if (dataSpy.count() < 1) { qWarning() << "No data received"; return QPair(QVariant(), socket); } QByteArray data; if (slipEnabled) { QByteArray rawData = socket->readAll(); QByteArray messageData; // Parse SLIP frame for (int i = 0; i < rawData.length(); i++) { quint8 byte = static_cast(rawData.at(i)); if (byte == SlipDataProcessor::ProtocolByteEnd) { // If there is no data...continue since it might be a starting END byte if (messageData.isEmpty()) continue; break; } else { messageData.append(rawData.at(i)); } } QByteArray deserializedData = SlipDataProcessor::deserializeData(messageData); SlipDataProcessor::Frame frame = SlipDataProcessor::parseFrame(deserializedData); if (frame.data.isEmpty()) { qWarning() << "Could not deserialize SLIP data"; return QPair(QVariant(), socket); } data = frame.data; } else { data = socket->readAll(); // Make sure the response ends with '}\n' if (!data.endsWith("}\n")) { qWarning() << "JSON data does not end with \"}\n\""; return QPair(QVariant(), socket); } } // Make sure the response it a valid JSON string QJsonParseError error; jsonDoc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { qWarning() << "JSON parser error" << error.errorString() << qUtf8Printable(data); return QPair(QVariant(), socket); } QVariantMap response = jsonDoc.toVariant().toMap(); if (response.value("id").toInt() == m_commandCounter) { m_commandCounter++; return QPair(jsonDoc.toVariant(), socket); } m_commandCounter++; return QPair(QVariant(), socket); } QPair BaseTest::invokeWebSocketTunnelProxyApiCallPersistant(const QString &method, const QVariantMap params, bool slipEnabled, QWebSocket *existingSocket) { QWebSocket *socket = nullptr; if (!existingSocket) { socket = new QWebSocket("tunnelproxy-testclient", QWebSocketProtocol::Version13); connect(socket, &QWebSocket::sslErrors, this, &BaseTest::sslErrors); QSignalSpy spyConnection(socket, SIGNAL(connected())); socket->open(Engine::instance()->webSocketServerTunnelProxy()->serverUrl()); spyConnection.wait(); if (spyConnection.count() == 0) { qWarning() << "Failed to connect websocket on tunnel proxy"; return QPair(QVariant(), socket); } } else { socket = existingSocket; } QVariantMap request; request.insert("id", m_commandCounter); request.insert("method", method); request.insert("params", params); QJsonDocument jsonDoc = QJsonDocument::fromVariant(request); QByteArray payload = jsonDoc.toJson(QJsonDocument::Compact) + "\n"; if (socket->state() != QAbstractSocket::ConnectedState) { qWarning() << "Socket not connected"; return QPair(QVariant(), socket); } QSignalSpy dataSpy(socket, SIGNAL(textMessageReceived(QString))); if (slipEnabled) { SlipDataProcessor::Frame frame; frame.socketAddress = 0x0000; frame.data = payload; socket->sendTextMessage(QString::fromUtf8(SlipDataProcessor::serializeData(SlipDataProcessor::buildFrame(frame)))); } else { socket->sendTextMessage(QString(payload)); } dataSpy.wait(); for (int i = 0; i < dataSpy.count(); i++) { QByteArray data; if (slipEnabled) { QByteArray rawData = dataSpy.at(i).last().toByteArray(); QByteArray messageData; // Parse SLIP frame for (int i = 0; i < rawData.length(); i++) { quint8 byte = static_cast(rawData.at(i)); if (byte == SlipDataProcessor::ProtocolByteEnd) { // If there is no data...continue since it might be a starting END byte if (messageData.isEmpty()) continue; break; messageData.clear(); } else { messageData.append(rawData.at(i)); } } QByteArray deserializedData = SlipDataProcessor::deserializeData(messageData); SlipDataProcessor::Frame frame = SlipDataProcessor::parseFrame(deserializedData); if (frame.data.isEmpty()) { qWarning() << "Could not deserialize SLIP data"; return QPair(QVariant(), socket); } data = frame.data; } else { data = dataSpy.at(i).last().toByteArray(); // Make sure the response ends with '}\n' if (!data.endsWith("}\n")) { qWarning() << "JSON data does not end with \"}\n\""; return QPair(QVariant(), socket); } } // Make sure the response it a valid JSON string QJsonParseError error; jsonDoc = QJsonDocument::fromJson(dataSpy.at(i).last().toByteArray(), &error); if (error.error != QJsonParseError::NoError) { qWarning() << "JSON parser error" << error.errorString(); return QPair(QVariant(), socket); } QVariantMap response = jsonDoc.toVariant().toMap(); // skip notifications if (response.contains("notification")) continue; if (response.value("id").toInt() == m_commandCounter) { m_commandCounter++; return QPair(jsonDoc.toVariant(), socket); } } qWarning() << "No websocket data received..."; m_commandCounter++; return QPair(QVariant(), socket); } void BaseTest::initTestCase() { qCDebug(dcApplication()) << "Init test case done."; } void BaseTest::cleanupTestCase() { qCDebug(dcApplication()) << "Clean up test case."; cleanUpEngine(); }