From 08ec6ba26ccc7da50bee5742c1fe1eaaf4b4758d Mon Sep 17 00:00:00 2001 From: heidi-dang Date: Mon, 16 Feb 2026 18:26:07 +1100 Subject: [PATCH] Tests: add HttpServer unit coverage - Make parse_request() and format_response() public for testability - Create heidi-dashd-lib static library for shared use by daemon and tests - Add comprehensive HttpServer tests (9 new test cases) - No production behavior changes - security fixes remain intact Test coverage: - Valid GET/POST request parsing - Request body handling - Incomplete/empty request handling - Response formatting (200, 404, custom headers) - Path with query strings - Method case preservation All 29 tests passing (10 config + 2 ipc + 2 metrics + 6 job + 9 http) --- include/heidi-kernel/http.h | 6 ++- src/dashd/CMakeLists.txt | 17 +++++- tests/CMakeLists.txt | 2 + tests/http_test.cpp | 104 ++++++++++++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 tests/http_test.cpp diff --git a/include/heidi-kernel/http.h b/include/heidi-kernel/http.h index b818457..1b32aee 100644 --- a/include/heidi-kernel/http.h +++ b/include/heidi-kernel/http.h @@ -34,11 +34,13 @@ class HttpServer { void register_handler(std::string_view path, RequestHandler handler); void serve_forever(); -private: - void handle_client(int client_fd); + // Exposed for testing HttpRequest parse_request(const std::string& data) const; std::string format_response(const HttpResponse& resp) const; +private: + void handle_client(int client_fd); + std::string address_; uint16_t port_; int server_fd_ = -1; diff --git a/src/dashd/CMakeLists.txt b/src/dashd/CMakeLists.txt index 35be0c3..c883c8d 100644 --- a/src/dashd/CMakeLists.txt +++ b/src/dashd/CMakeLists.txt @@ -1,9 +1,22 @@ +# Library for HTTP server (shared between daemon and tests) +add_library(heidi-dashd-lib STATIC + http.cpp +) + +target_link_libraries(heidi-dashd-lib PRIVATE heidi-kernel-lib pthread) + +target_include_directories(heidi-dashd-lib PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/../../include +) + +target_compile_options(heidi-dashd-lib PRIVATE -Wall -Wextra -Wpedantic) + +# Daemon executable add_executable(heidi-dashd main.cpp - http.cpp ) -target_link_libraries(heidi-dashd PRIVATE heidi-kernel-lib pthread) +target_link_libraries(heidi-dashd PRIVATE heidi-dashd-lib heidi-kernel-lib pthread) target_include_directories(heidi-dashd PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../include diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bb13fce..f27f28d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,6 +15,7 @@ add_executable(unit_tests test_ipc.cpp test_metrics.cpp test_job.cpp + http_test.cpp ../src/config.cpp ) @@ -27,6 +28,7 @@ target_link_libraries(unit_tests heidi-ipc heidi-metrics heidi-kernel-job + heidi-dashd-lib ) include(GoogleTest) diff --git a/tests/http_test.cpp b/tests/http_test.cpp new file mode 100644 index 0000000..aa1add7 --- /dev/null +++ b/tests/http_test.cpp @@ -0,0 +1,104 @@ +#include "heidi-kernel/http.h" +#include + +using namespace heidi; + +// Test fixture that creates an HttpServer instance +class HttpServerTest : public ::testing::Test { +protected: + HttpServer server{"127.0.0.1", 0}; // Port 0 = let OS assign +}; + +TEST_F(HttpServerTest, ParseRequest_ValidGet) { + std::string data = "GET /api/status HTTP/1.1\r\nHost: localhost\r\n\r\n"; + HttpRequest req = server.parse_request(data); + + EXPECT_EQ(std::string(req.method), "GET"); + EXPECT_EQ(std::string(req.path), "/api/status"); + EXPECT_TRUE(req.body.empty()); +} + +TEST_F(HttpServerTest, ParseRequest_ValidPostWithBody) { + std::string data = "POST /submit HTTP/1.1\r\nContent-Length: 5\r\n\r\nhello"; + HttpRequest req = server.parse_request(data); + + EXPECT_EQ(std::string(req.method), "POST"); + EXPECT_EQ(std::string(req.path), "/submit"); + EXPECT_EQ(std::string(req.body), "hello"); +} + +TEST_F(HttpServerTest, ParseRequest_Incomplete) { + std::string data = "GET /"; // Missing CRLFs + HttpRequest req = server.parse_request(data); + + EXPECT_TRUE(req.method.empty()); + EXPECT_TRUE(req.path.empty()); + EXPECT_TRUE(req.body.empty()); +} + +TEST_F(HttpServerTest, ParseRequest_Empty) { + std::string data = ""; + HttpRequest req = server.parse_request(data); + + EXPECT_TRUE(req.method.empty()); + EXPECT_TRUE(req.path.empty()); +} + +TEST_F(HttpServerTest, FormatResponse_Simple) { + HttpResponse resp; + resp.status_code = 200; + resp.status_text = "OK"; + resp.body = "hello"; + resp.headers["Content-Type"] = "text/plain"; + + std::string s = server.format_response(resp); + + EXPECT_NE(s.find("HTTP/1.1 200 OK\r\n"), std::string::npos); + EXPECT_NE(s.find("Content-Type: text/plain\r\n"), std::string::npos); + EXPECT_NE(s.find("Content-Length: 5\r\n"), std::string::npos); + // Check end of headers and body + EXPECT_NE(s.find("\r\n\r\nhello"), std::string::npos); +} + +TEST_F(HttpServerTest, FormatResponse_404) { + HttpResponse resp; + resp.status_code = 404; + resp.status_text = "Not Found"; + + std::string s = server.format_response(resp); + + EXPECT_NE(s.find("HTTP/1.1 404 Not Found\r\n"), std::string::npos); + EXPECT_NE(s.find("Content-Length: 0\r\n"), std::string::npos); +} + +TEST_F(HttpServerTest, FormatResponse_WithHeaders) { + HttpResponse resp; + resp.status_code = 201; + resp.status_text = "Created"; + resp.body = "{\"id\": 123}"; + resp.headers["Content-Type"] = "application/json"; + resp.headers["Location"] = "/items/123"; + + std::string s = server.format_response(resp); + + EXPECT_NE(s.find("HTTP/1.1 201 Created\r\n"), std::string::npos); + EXPECT_NE(s.find("Content-Type: application/json\r\n"), std::string::npos); + EXPECT_NE(s.find("Location: /items/123\r\n"), std::string::npos); + EXPECT_NE(s.find("\r\n\r\n{\"id\": 123}"), std::string::npos); +} + +TEST_F(HttpServerTest, ParseRequest_PathWithQuery) { + std::string data = "GET /api/status?verbose=true HTTP/1.1\r\n\r\n"; + HttpRequest req = server.parse_request(data); + + EXPECT_EQ(std::string(req.method), "GET"); + EXPECT_EQ(std::string(req.path), "/api/status?verbose=true"); +} + +TEST_F(HttpServerTest, ParseRequest_MethodCasePreserved) { + std::string data = "post /data HTTP/1.1\r\n\r\n"; + HttpRequest req = server.parse_request(data); + + EXPECT_EQ(std::string(req.method), "post"); // Lowercase preserved + EXPECT_EQ(std::string(req.path), "/data"); +}