test(http_server): add mock TCP client for black-box WS testing

This commit is contained in:
Ashish Sharma
2026-08-19 11:42:59 +08:00
parent f90cd7dd78
commit 78b878dbab
4 changed files with 390 additions and 2 deletions
@@ -1,3 +1,3 @@
idf_component_register(SRC_DIRS "."
PRIV_INCLUDE_DIRS "." "../../src" "../../src/port/esp32"
idf_component_register(SRC_DIRS "." "../mock_client"
PRIV_INCLUDE_DIRS "." "../../src" "../../src/port/esp32" "../mock_client"
PRIV_REQUIRES esp_http_server test_utils unity esp_timer)
@@ -6,6 +6,7 @@
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <esp_system.h>
#include <esp_http_server.h>
#include <esp_heap_caps.h>
@@ -20,6 +21,7 @@
#include "unity.h"
#include "test_utils.h"
#include "mock_http_server_client.h"
int pre_start_mem, post_stop_mem, post_stop_min_mem;
bool basic_sanity = true;
@@ -31,6 +33,30 @@ esp_err_t null_func(httpd_req_t *req)
return ESP_OK;
}
#ifdef CONFIG_HTTPD_WS_SUPPORT
static httpd_handle_t start_test_ws_server(uint16_t server_port, uint16_t ctrl_port)
{
httpd_handle_t hd = NULL;
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = server_port;
config.ctrl_port = ctrl_port;
httpd_uri_t ws_uri = {
.uri = "/ws",
.method = HTTP_GET,
.handler = null_func,
.user_ctx = NULL,
.is_websocket = true,
.handle_ws_control_frames = false,
.supported_subprotocol = NULL,
};
TEST_ASSERT_EQUAL(ESP_OK, httpd_start(&hd, &config));
TEST_ASSERT_EQUAL(ESP_OK, httpd_register_uri_handler(hd, &ws_uri));
return hd;
}
#endif /* CONFIG_HTTPD_WS_SUPPORT */
httpd_uri_t handler_limit_uri (char* path)
{
httpd_uri_t uri = {
@@ -0,0 +1,244 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "unity.h"
#include "mock_http_server_client.h"
static const char *TAG = "mock_srv_client";
/* -----------------------------------------------------------------------
* Internal helpers
* --------------------------------------------------------------------- */
/**
* Parse the HTTP status code from the beginning of a raw response buffer.
* Expects "HTTP/1.x NNN ..." format. Returns -1 on any parse failure.
*/
static int parse_status_code(const char *data, size_t len)
{
/* Minimum: "HTTP/1.1 200 " = 13 chars */
if (len < 13) {
return -1;
}
/* Find the first space (end of "HTTP/1.x") */
const char *p = memchr(data, ' ', len < 16 ? len : 16);
if (!p) {
return -1;
}
p++; /* point at status code digits */
if ((size_t)(p - data) + 3 > len) {
return -1;
}
int code = 0;
for (int i = 0; i < 3; i++) {
if (p[i] < '0' || p[i] > '9') {
return -1;
}
code = code * 10 + (p[i] - '0');
}
/* Sanity: valid HTTP status codes are 100–599 */
if (code < 100 || code > 599) {
return -1;
}
return code;
}
/* -----------------------------------------------------------------------
* Public API
* --------------------------------------------------------------------- */
mock_server_response_t *mock_server_send_request(uint16_t port,
const mock_server_request_t *req)
{
mock_server_response_t *resp = calloc(1, sizeof(*resp));
if (!resp) {
ESP_LOGE(TAG, "calloc() failed");
return NULL;
}
resp->status_code = -1;
/* ---- Create socket and connect ---- */
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
ESP_LOGE(TAG, "socket() failed: errno=%d", errno);
free(resp);
return NULL;
}
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(port),
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
};
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
ESP_LOGE(TAG, "connect() to 127.0.0.1:%u failed: errno=%d", port, errno);
close(fd);
free(resp);
return NULL;
}
ESP_LOGD(TAG, "connected to 127.0.0.1:%u", port);
/* ---- Send the scripted request ---- */
size_t total_len = req->len ? req->len : strlen(req->data);
size_t sent = 0;
/* How many bytes to send in each write() call */
int max_per_write = (req->max_bytes_per_write > 0)
? req->max_bytes_per_write
: (int)total_len;
/* After how many sent bytes to stop writing.
* 0 or negative = send everything (zero is the zero-init default for
* unset struct fields, so it must mean "no early close"). */
int close_after = req->close_after_bytes;
while (sent < total_len) {
/* Honour early-close limit */
if (close_after > 0 && (int)sent >= close_after) {
break;
}
int chunk = max_per_write;
/* Don't exceed the early-close byte count */
if (close_after > 0 && (int)(sent + chunk) > close_after) {
chunk = close_after - (int)sent;
}
/* Don't exceed total length */
if ((size_t)chunk > total_len - sent) {
chunk = (int)(total_len - sent);
}
int n = send(fd, req->data + sent, (size_t)chunk, 0);
if (n <= 0) {
ESP_LOGE(TAG, "send() failed after %zu bytes: errno=%d", sent, errno);
close(fd);
free(resp);
return NULL;
}
sent += (size_t)n;
ESP_LOGD(TAG, "sent %d bytes (%zu/%zu total)", n, sent, total_len);
if (req->write_delay_ms > 0 && sent < total_len) {
vTaskDelay(pdMS_TO_TICKS(req->write_delay_ms));
}
}
/* Signal end-of-request to the server if we stopped sending early */
if (close_after > 0) {
shutdown(fd, SHUT_WR);
ESP_LOGD(TAG, "shutdown(SHUT_WR) after %zu bytes", sent);
}
/* ---- Read the response ---- */
int timeout_ms = (req->recv_timeout_ms > 0) ? req->recv_timeout_ms : 2000;
/* Record absolute deadline so that EINTR retries don't reset the clock. */
struct timeval deadline;
gettimeofday(&deadline, NULL);
deadline.tv_sec += timeout_ms / 1000;
deadline.tv_usec += (timeout_ms % 1000) * 1000;
if (deadline.tv_usec >= 1000000) {
deadline.tv_sec++;
deadline.tv_usec -= 1000000;
}
size_t received = 0;
while (received < sizeof(resp->data) - 1) {
/* Calculate remaining time until deadline. */
struct timeval now, remaining;
gettimeofday(&now, NULL);
remaining.tv_sec = deadline.tv_sec - now.tv_sec;
remaining.tv_usec = deadline.tv_usec - now.tv_usec;
if (remaining.tv_usec < 0) {
remaining.tv_sec--;
remaining.tv_usec += 1000000;
}
if (remaining.tv_sec < 0) {
ESP_LOGD(TAG, "recv deadline elapsed after %zu bytes", received);
break;
}
/* Wait for data (or deadline) — handles EINTR safely. */
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(fd, &read_fds);
int ready = select(fd + 1, &read_fds, NULL, NULL, &remaining);
if (ready < 0) {
if (errno == EINTR) {
continue; /* signal interrupted select — recalculate remaining time */
}
ESP_LOGE(TAG, "select() error: errno=%d", errno);
break;
}
if (ready == 0) {
ESP_LOGD(TAG, "recv deadline elapsed after %zu bytes", received);
break;
}
int n = recv(fd, resp->data + received, sizeof(resp->data) - 1 - received, 0);
if (n < 0) {
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) {
continue;
}
ESP_LOGE(TAG, "recv() error after %zu bytes: errno=%d", received, errno);
break;
}
if (n == 0) {
resp->server_closed = true;
ESP_LOGD(TAG, "server closed connection after %zu bytes", received);
break;
}
received += (size_t)n;
}
resp->len = received;
resp->data[received] = '\0'; /* NUL-terminate for easy string inspection */
if (received > 0) {
resp->status_code = parse_status_code(resp->data, received);
ESP_LOGD(TAG, "response: status=%d, len=%zu", resp->status_code, received);
} else {
ESP_LOGW(TAG, "no response received from server");
}
close(fd);
return resp;
}
void mock_server_response_free(mock_server_response_t *resp)
{
free(resp);
}
void mock_server_assert_status(const mock_server_response_t *resp, int expected_status)
{
if (resp->len == 0) {
TEST_FAIL_MESSAGE("mock_server: no response received from server");
return;
}
if (resp->status_code == -1) {
/* Print the raw response to help diagnose the issue */
ESP_LOGE(TAG, "unparsable response (%zu bytes): %.80s", resp->len, resp->data);
TEST_FAIL_MESSAGE("mock_server: server response is not a valid HTTP status line");
return;
}
TEST_ASSERT_EQUAL_MESSAGE(expected_status, resp->status_code,
"HTTP status code mismatch");
}
@@ -0,0 +1,118 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @file mock_http_server_client.h
* @brief Scripted TCP client for black-box testing of esp_http_server.
*
* Allows tests to send arbitrary (including malformed or fragmented) byte
* sequences to a running httpd instance over loopback and capture the raw
* response. The server under test is started/stopped by the test itself using
* the normal httpd_start() / httpd_stop() APIs; this module only provides the
* "client" side.
*
* Typical usage:
* @code
* // 1. Start the server
* httpd_config_t cfg = HTTPD_DEFAULT_CONFIG();
* cfg.server_port = 8099;
* httpd_handle_t server;
* httpd_start(&server, &cfg);
* httpd_register_uri_handler(server, &my_handler);
*
* // 2. Send a scripted request and capture the response
* mock_server_request_t req = {
* .data = "GET /path HTTP/1.1\r\nHost: localhost\r\n\r\n",
* };
* mock_server_response_t resp = {0};
* mock_server_send_request(8099, &req, &resp);
* TEST_ASSERT_EQUAL(200, resp.status_code);
*
* // 3. Stop the server
* httpd_stop(server);
* @endcode
*/
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/** Maximum number of response bytes the mock client will buffer. */
#define MOCK_SERVER_CLIENT_RESP_BUF_SIZE 4096
/**
* @brief Describes a single scripted request the mock client sends.
*
* Only @p data is required. All other fields have safe defaults when zero.
*/
typedef struct {
const char *data; /**< Raw bytes to send (must not be NULL) */
size_t len; /**< Length of @p data; 0 → strlen(data) */
/** Maximum bytes per write() call. -1 (or 0) = send all at once.
* Set to a small value to simulate a slow/trickled sender. */
int max_bytes_per_write;
/** Delay in milliseconds between fragmented writes. 0 = no delay. */
int write_delay_ms;
/** Close the TCP write-side after sending this many bytes.
* 0 (default) or negative = send everything, keep write-side open.
* Positive N = close after N bytes (useful for partial-request / abrupt-close tests). */
int close_after_bytes;
/** How long (ms) to wait for a response before giving up. 0 → 2000 ms. */
int recv_timeout_ms;
} mock_server_request_t;
/**
* @brief Result captured from the server after sending a scripted request.
*/
typedef struct {
char data[MOCK_SERVER_CLIENT_RESP_BUF_SIZE]; /**< Raw response bytes */
size_t len; /**< Bytes actually received */
int status_code; /**< HTTP status code parsed from the status line,
* or -1 if the response is not a valid HTTP response. */
bool server_closed; /**< True if the server closed the connection (recv returned 0). */
} mock_server_response_t;
/**
* @brief Connect to a running httpd on 127.0.0.1:@p port, send the scripted
* request and capture the response.
*
* The response struct is heap-allocated; the caller must free it with
* mock_server_response_free() when done. The TCP connection is always
* closed before returning.
*
* @param[in] port TCP port the httpd server is listening on.
* @param[in] req Scripted request to send.
*
* @return Heap-allocated response, or NULL if a TCP-level error occurred.
*/
mock_server_response_t *mock_server_send_request(uint16_t port,
const mock_server_request_t *req);
/**
* @brief Free a response returned by mock_server_send_request().
*/
void mock_server_response_free(mock_server_response_t *resp);
/**
* @brief Assert that @p resp contains a valid HTTP response with the expected
* status code. Calls TEST_FAIL() (Unity) if the assertion does not hold.
*/
void mock_server_assert_status(const mock_server_response_t *resp, int expected_status);
#ifdef __cplusplus
}
#endif