From eedf22d95d7b461be8808cddecbf16fb7878e6ca Mon Sep 17 00:00:00 2001 From: alireza787b Date: Tue, 28 Jul 2026 06:07:23 +0000 Subject: [PATCH 1/3] test(UnitTest): handle early local HTTP requests --- .../Fixtures/LocalHttpTestServer.cc | 55 ++++++++++++------- test/UnitTestFramework/Tests/CMakeLists.txt | 3 + .../Tests/LocalHttpTestServerTest.cc | 28 ++++++++++ .../Tests/LocalHttpTestServerTest.h | 11 ++++ 4 files changed, 76 insertions(+), 21 deletions(-) create mode 100644 test/UnitTestFramework/Tests/LocalHttpTestServerTest.cc create mode 100644 test/UnitTestFramework/Tests/LocalHttpTestServerTest.h diff --git a/test/UnitTestFramework/Fixtures/LocalHttpTestServer.cc b/test/UnitTestFramework/Fixtures/LocalHttpTestServer.cc index a3ffc7e8edfb..6ae653861695 100644 --- a/test/UnitTestFramework/Fixtures/LocalHttpTestServer.cc +++ b/test/UnitTestFramework/Fixtures/LocalHttpTestServer.cc @@ -45,24 +45,33 @@ namespace { QByteArray httpReasonPhrase(int statusCode) { switch (statusCode) { - case 200: return QByteArrayLiteral("OK"); - case 204: return QByteArrayLiteral("No Content"); - case 206: return QByteArrayLiteral("Partial Content"); - case 304: return QByteArrayLiteral("Not Modified"); - case 400: return QByteArrayLiteral("Bad Request"); - case 404: return QByteArrayLiteral("Not Found"); - case 500: return QByteArrayLiteral("Internal Server Error"); - default: return QByteArrayLiteral("Status"); + case 200: + return QByteArrayLiteral("OK"); + case 204: + return QByteArrayLiteral("No Content"); + case 206: + return QByteArrayLiteral("Partial Content"); + case 304: + return QByteArrayLiteral("Not Modified"); + case 400: + return QByteArrayLiteral("Bad Request"); + case 404: + return QByteArrayLiteral("Not Found"); + case 500: + return QByteArrayLiteral("Internal Server Error"); + default: + return QByteArrayLiteral("Status"); } } -} // namespace +} // namespace void LocalHttpTestServer::installHttpResponder(const QByteArray& body, int statusCode, const QByteArray& contentType, int cacheMaxAge) { - QByteArray header = QStringLiteral("HTTP/1.1 %1 %2\r\n" - "Content-Type: %3\r\n" - "Connection: close\r\n") + QByteArray header = QStringLiteral( + "HTTP/1.1 %1 %2\r\n" + "Content-Type: %3\r\n" + "Connection: close\r\n") .arg(statusCode) .arg(QString::fromLatin1(httpReasonPhrase(statusCode))) .arg(QString::fromLatin1(contentType)) @@ -80,16 +89,20 @@ void LocalHttpTestServer::installRawResponder(const QByteArray& rawResponse) (void) QObject::connect(&_server, &QTcpServer::newConnection, &_server, [this, rawResponse]() { while (_server.hasPendingConnections()) { QTcpSocket* const socket = _server.nextPendingConnection(); - (void) QObject::connect( - socket, &QTcpSocket::readyRead, socket, - [socket, rawResponse]() { - socket->readAll(); - socket->write(rawResponse); - socket->flush(); - socket->disconnectFromHost(); - }, - Qt::SingleShotConnection); (void) QObject::connect(socket, &QTcpSocket::disconnected, socket, &QObject::deleteLater); + + const auto sendResponse = [socket, rawResponse]() { + socket->readAll(); + socket->write(rawResponse); + socket->flush(); + socket->disconnectFromHost(); + }; + + if (socket->bytesAvailable() > 0) { + sendResponse(); + } else { + (void) QObject::connect(socket, &QTcpSocket::readyRead, socket, sendResponse, Qt::SingleShotConnection); + } } }); } diff --git a/test/UnitTestFramework/Tests/CMakeLists.txt b/test/UnitTestFramework/Tests/CMakeLists.txt index ce042e408f0a..73ab42187638 100644 --- a/test/UnitTestFramework/Tests/CMakeLists.txt +++ b/test/UnitTestFramework/Tests/CMakeLists.txt @@ -2,6 +2,8 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/MultiSignalSpyTest.cc ${CMAKE_CURRENT_SOURCE_DIR}/MultiSignalSpyTest.h + ${CMAKE_CURRENT_SOURCE_DIR}/LocalHttpTestServerTest.cc + ${CMAKE_CURRENT_SOURCE_DIR}/LocalHttpTestServerTest.h ${CMAKE_CURRENT_SOURCE_DIR}/SignalEmitter.h ${CMAKE_CURRENT_SOURCE_DIR}/TestBaseClassesTest.cc ${CMAKE_CURRENT_SOURCE_DIR}/TestBaseClassesTest.h @@ -16,6 +18,7 @@ target_sources(${CMAKE_PROJECT_NAME} target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) add_qgc_test(MultiSignalSpyTest LABELS Unit) +add_qgc_test(LocalHttpTestServerTest LABELS Unit) add_qgc_test(TestBaseClassesTest LABELS Unit) add_qgc_test(TestFixturesTest LABELS Unit) add_qgc_test(UnitTestAsyncHelpersTest LABELS Unit Utilities) diff --git a/test/UnitTestFramework/Tests/LocalHttpTestServerTest.cc b/test/UnitTestFramework/Tests/LocalHttpTestServerTest.cc new file mode 100644 index 000000000000..73b2d38b6a10 --- /dev/null +++ b/test/UnitTestFramework/Tests/LocalHttpTestServerTest.cc @@ -0,0 +1,28 @@ +#include "LocalHttpTestServerTest.h" + +#include +#include + +#include "Fixtures/LocalHttpTestServer.h" + +void LocalHttpTestServerTest::_testEarlyRequest() +{ + TestFixtures::LocalHttpTestServer server; + QVERIFY(server.listen()); + server.installHttpResponder(QByteArrayLiteral("ready")); + + QTcpSocket client; + client.connectToHost(QHostAddress::LocalHost, server.port()); + QVERIFY(client.waitForConnected(TestTimeout::mediumMs())); + + const QByteArray request = QByteArrayLiteral("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"); + QCOMPARE(client.write(request), request.size()); + QVERIFY(client.waitForBytesWritten(TestTimeout::mediumMs())); + + QTRY_COMPARE_WITH_TIMEOUT(client.state(), QAbstractSocket::UnconnectedState, TestTimeout::mediumMs()); + const QByteArray response = client.readAll(); + QVERIFY(response.startsWith(QByteArrayLiteral("HTTP/1.1 200 OK\r\n"))); + QVERIFY(response.endsWith(QByteArrayLiteral("\r\n\r\nready"))); +} + +UT_REGISTER_TEST(LocalHttpTestServerTest, TestLabel::Unit) diff --git a/test/UnitTestFramework/Tests/LocalHttpTestServerTest.h b/test/UnitTestFramework/Tests/LocalHttpTestServerTest.h new file mode 100644 index 000000000000..30342d8206b2 --- /dev/null +++ b/test/UnitTestFramework/Tests/LocalHttpTestServerTest.h @@ -0,0 +1,11 @@ +#pragma once + +#include "UnitTest.h" + +class LocalHttpTestServerTest : public UnitTest +{ + Q_OBJECT + +private slots: + void _testEarlyRequest(); +}; From 7a6775381f127e9e0ca0d47396038df3f2cc23b3 Mon Sep 17 00:00:00 2001 From: alireza787b Date: Tue, 28 Jul 2026 12:12:10 +0000 Subject: [PATCH 2/3] test(UnitTest): wait for complete HTTP requests --- .../Fixtures/LocalHttpTestServer.cc | 35 +++++++++++++++---- .../Tests/LocalHttpTestServerTest.cc | 13 +++++-- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/test/UnitTestFramework/Fixtures/LocalHttpTestServer.cc b/test/UnitTestFramework/Fixtures/LocalHttpTestServer.cc index 6ae653861695..c6b21d056069 100644 --- a/test/UnitTestFramework/Fixtures/LocalHttpTestServer.cc +++ b/test/UnitTestFramework/Fixtures/LocalHttpTestServer.cc @@ -1,6 +1,7 @@ #include "LocalHttpTestServer.h" #include +#include #include #include @@ -8,6 +9,16 @@ namespace TestFixtures { +namespace { +constexpr qsizetype MAX_REQUEST_HEADER_SIZE = 64 * 1024; + +struct RawResponderState +{ + QByteArray request; + bool responseSent = false; +}; +} // namespace + LocalHttpTestServer::~LocalHttpTestServer() { close(); @@ -91,18 +102,28 @@ void LocalHttpTestServer::installRawResponder(const QByteArray& rawResponse) QTcpSocket* const socket = _server.nextPendingConnection(); (void) QObject::connect(socket, &QTcpSocket::disconnected, socket, &QObject::deleteLater); - const auto sendResponse = [socket, rawResponse]() { - socket->readAll(); + const auto state = QSharedPointer::create(); + const auto sendResponseWhenRequestComplete = [socket, rawResponse, state]() { + if (state->responseSent) { + return; + } + + state->request.append(socket->readAll()); + if (!state->request.contains(QByteArrayLiteral("\r\n\r\n"))) { + if (state->request.size() > MAX_REQUEST_HEADER_SIZE) { + socket->disconnectFromHost(); + } + return; + } + + state->responseSent = true; socket->write(rawResponse); socket->flush(); socket->disconnectFromHost(); }; - if (socket->bytesAvailable() > 0) { - sendResponse(); - } else { - (void) QObject::connect(socket, &QTcpSocket::readyRead, socket, sendResponse, Qt::SingleShotConnection); - } + (void) QObject::connect(socket, &QTcpSocket::readyRead, socket, sendResponseWhenRequestComplete); + sendResponseWhenRequestComplete(); } }); } diff --git a/test/UnitTestFramework/Tests/LocalHttpTestServerTest.cc b/test/UnitTestFramework/Tests/LocalHttpTestServerTest.cc index 73b2d38b6a10..43175c87b69e 100644 --- a/test/UnitTestFramework/Tests/LocalHttpTestServerTest.cc +++ b/test/UnitTestFramework/Tests/LocalHttpTestServerTest.cc @@ -2,6 +2,7 @@ #include #include +#include #include "Fixtures/LocalHttpTestServer.h" @@ -15,8 +16,16 @@ void LocalHttpTestServerTest::_testEarlyRequest() client.connectToHost(QHostAddress::LocalHost, server.port()); QVERIFY(client.waitForConnected(TestTimeout::mediumMs())); - const QByteArray request = QByteArrayLiteral("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"); - QCOMPARE(client.write(request), request.size()); + QSignalSpy readyReadSpy(&client, &QTcpSocket::readyRead); + const QByteArray firstRequestFragment = + QByteArrayLiteral("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n"); + QCOMPARE(client.write(firstRequestFragment), firstRequestFragment.size()); + QVERIFY(client.waitForBytesWritten(TestTimeout::mediumMs())); + QVERIFY(!readyReadSpy.wait(TestTimeout::shortMs())); + QCOMPARE(client.state(), QAbstractSocket::ConnectedState); + + const QByteArray finalRequestFragment = QByteArrayLiteral("\r\n"); + QCOMPARE(client.write(finalRequestFragment), finalRequestFragment.size()); QVERIFY(client.waitForBytesWritten(TestTimeout::mediumMs())); QTRY_COMPARE_WITH_TIMEOUT(client.state(), QAbstractSocket::UnconnectedState, TestTimeout::mediumMs()); From 67caa46a0cdcb6f3cf4a9c232956dd9a074bdc45 Mon Sep 17 00:00:00 2001 From: alireza787b Date: Fri, 31 Jul 2026 01:21:26 +0000 Subject: [PATCH 3/3] test(UnitTest): address local HTTP fixture review --- .../Fixtures/LocalHttpTestServer.cc | 48 ++++++++----------- test/UnitTestFramework/Tests/CMakeLists.txt | 6 +-- .../Tests/LocalHttpTestServerTest.cc | 13 +++-- .../Tests/LocalHttpTestServerTest.h | 2 +- 4 files changed, 31 insertions(+), 38 deletions(-) diff --git a/test/UnitTestFramework/Fixtures/LocalHttpTestServer.cc b/test/UnitTestFramework/Fixtures/LocalHttpTestServer.cc index c6b21d056069..133950815a3a 100644 --- a/test/UnitTestFramework/Fixtures/LocalHttpTestServer.cc +++ b/test/UnitTestFramework/Fixtures/LocalHttpTestServer.cc @@ -17,7 +17,21 @@ struct RawResponderState QByteArray request; bool responseSent = false; }; -} // namespace + +QByteArray httpReasonPhrase(int statusCode) +{ + switch (statusCode) { + case 200: return QByteArrayLiteral("OK"); + case 204: return QByteArrayLiteral("No Content"); + case 206: return QByteArrayLiteral("Partial Content"); + case 304: return QByteArrayLiteral("Not Modified"); + case 400: return QByteArrayLiteral("Bad Request"); + case 404: return QByteArrayLiteral("Not Found"); + case 500: return QByteArrayLiteral("Internal Server Error"); + default: return QByteArrayLiteral("Status"); + } +} +} // namespace LocalHttpTestServer::~LocalHttpTestServer() { @@ -52,37 +66,12 @@ QString LocalHttpTestServer::url(const QString& path) const return QStringLiteral("http://%1:%2%3").arg(host).arg(port()).arg(path); } -namespace { -QByteArray httpReasonPhrase(int statusCode) -{ - switch (statusCode) { - case 200: - return QByteArrayLiteral("OK"); - case 204: - return QByteArrayLiteral("No Content"); - case 206: - return QByteArrayLiteral("Partial Content"); - case 304: - return QByteArrayLiteral("Not Modified"); - case 400: - return QByteArrayLiteral("Bad Request"); - case 404: - return QByteArrayLiteral("Not Found"); - case 500: - return QByteArrayLiteral("Internal Server Error"); - default: - return QByteArrayLiteral("Status"); - } -} -} // namespace - void LocalHttpTestServer::installHttpResponder(const QByteArray& body, int statusCode, const QByteArray& contentType, int cacheMaxAge) { - QByteArray header = QStringLiteral( - "HTTP/1.1 %1 %2\r\n" - "Content-Type: %3\r\n" - "Connection: close\r\n") + QByteArray header = QStringLiteral("HTTP/1.1 %1 %2\r\n" + "Content-Type: %3\r\n" + "Connection: close\r\n") .arg(statusCode) .arg(QString::fromLatin1(httpReasonPhrase(statusCode))) .arg(QString::fromLatin1(contentType)) @@ -111,6 +100,7 @@ void LocalHttpTestServer::installRawResponder(const QByteArray& rawResponse) state->request.append(socket->readAll()); if (!state->request.contains(QByteArrayLiteral("\r\n\r\n"))) { if (state->request.size() > MAX_REQUEST_HEADER_SIZE) { + state->responseSent = true; socket->disconnectFromHost(); } return; diff --git a/test/UnitTestFramework/Tests/CMakeLists.txt b/test/UnitTestFramework/Tests/CMakeLists.txt index 73ab42187638..d30982487aa7 100644 --- a/test/UnitTestFramework/Tests/CMakeLists.txt +++ b/test/UnitTestFramework/Tests/CMakeLists.txt @@ -1,9 +1,9 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/MultiSignalSpyTest.cc - ${CMAKE_CURRENT_SOURCE_DIR}/MultiSignalSpyTest.h ${CMAKE_CURRENT_SOURCE_DIR}/LocalHttpTestServerTest.cc ${CMAKE_CURRENT_SOURCE_DIR}/LocalHttpTestServerTest.h + ${CMAKE_CURRENT_SOURCE_DIR}/MultiSignalSpyTest.cc + ${CMAKE_CURRENT_SOURCE_DIR}/MultiSignalSpyTest.h ${CMAKE_CURRENT_SOURCE_DIR}/SignalEmitter.h ${CMAKE_CURRENT_SOURCE_DIR}/TestBaseClassesTest.cc ${CMAKE_CURRENT_SOURCE_DIR}/TestBaseClassesTest.h @@ -17,8 +17,8 @@ target_sources(${CMAKE_PROJECT_NAME} target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) -add_qgc_test(MultiSignalSpyTest LABELS Unit) add_qgc_test(LocalHttpTestServerTest LABELS Unit) +add_qgc_test(MultiSignalSpyTest LABELS Unit) add_qgc_test(TestBaseClassesTest LABELS Unit) add_qgc_test(TestFixturesTest LABELS Unit) add_qgc_test(UnitTestAsyncHelpersTest LABELS Unit Utilities) diff --git a/test/UnitTestFramework/Tests/LocalHttpTestServerTest.cc b/test/UnitTestFramework/Tests/LocalHttpTestServerTest.cc index 43175c87b69e..c0f233b6f665 100644 --- a/test/UnitTestFramework/Tests/LocalHttpTestServerTest.cc +++ b/test/UnitTestFramework/Tests/LocalHttpTestServerTest.cc @@ -1,19 +1,22 @@ #include "LocalHttpTestServerTest.h" -#include +#include "Fixtures/LocalHttpTestServer.h" + +#include #include #include -#include "Fixtures/LocalHttpTestServer.h" - -void LocalHttpTestServerTest::_testEarlyRequest() +void LocalHttpTestServerTest::_testFragmentedRequestHeader() { TestFixtures::LocalHttpTestServer server; QVERIFY(server.listen()); server.installHttpResponder(QByteArrayLiteral("ready")); + const QUrl serverUrl(server.url()); + QVERIFY(serverUrl.isValid()); + QTcpSocket client; - client.connectToHost(QHostAddress::LocalHost, server.port()); + client.connectToHost(serverUrl.host(), static_cast(serverUrl.port())); QVERIFY(client.waitForConnected(TestTimeout::mediumMs())); QSignalSpy readyReadSpy(&client, &QTcpSocket::readyRead); diff --git a/test/UnitTestFramework/Tests/LocalHttpTestServerTest.h b/test/UnitTestFramework/Tests/LocalHttpTestServerTest.h index 30342d8206b2..2c077ab1e533 100644 --- a/test/UnitTestFramework/Tests/LocalHttpTestServerTest.h +++ b/test/UnitTestFramework/Tests/LocalHttpTestServerTest.h @@ -7,5 +7,5 @@ class LocalHttpTestServerTest : public UnitTest Q_OBJECT private slots: - void _testEarlyRequest(); + void _testFragmentedRequestHeader(); };