test(esp_http_client): add mock transport and basic characterization tests

Mock TCP transport plus 13 basic cases that pin current client behavior.
Fixture Content-Length values corrected; all mock-based cases compile only
with CONFIG_ESP_HTTP_CLIENT_ENABLE_CUSTOM_TRANSPORT.
This commit is contained in:
Ashish Sharma
2026-08-27 12:46:14 +08:00
parent ece9103d83
commit c57e06b6a9
7 changed files with 1792 additions and 1 deletions

View File

@@ -1,5 +1,9 @@
idf_component_register(SRC_DIRS "."
set(mock_transport_dir ${CMAKE_CURRENT_SOURCE_DIR}/../mock_transport)
idf_component_register(SRC_DIRS "." ${mock_transport_dir}
PRIV_INCLUDE_DIRS "."
"../../lib/include"
PRIV_REQUIRES esp_http_client tcp_transport test_utils unity
INCLUDE_DIRS ${mock_transport_dir}
REQUIRES tcp_transport
WHOLE_ARCHIVE)

View File

@@ -14,6 +14,11 @@
#include "test_utils.h"
#include "sdkconfig.h"
#include "esp_log.h"
#include "test_http_client_mock_transport.h"
#include "esp_transport.h"
#define HOST "httpbin.org"
#define USERNAME "user"
#define PASSWORD "challenge"
@@ -439,6 +444,368 @@ TEST_CASE("esp_http_client_request_send fails when an oversized header is mid-li
#endif // CONFIG_ESP_HTTP_CLIENT_STRICT_HEADER_BUFFER
#if CONFIG_ESP_HTTP_CLIENT_ENABLE_CUSTOM_TRANSPORT
/* ============================================
* Error Recovery Tests with Mock Transport
*
* Every case below injects a mock transport through
* esp_http_client_config_t::transport. Without custom transport support the
* clients would fall back to a real transport aimed at a host that does not
* exist, so the whole section compiles out.
* ============================================ */
/**
* @brief Canned HTTP response for successful requests
* Note: Content-Length must match the actual body length exactly
*/
static const char *mock_http_response_ok =
"HTTP/1.1 200 OK\r\n"
"Content-Type: application/json\r\n"
"Content-Length: 15\r\n" // Actual body is 15 bytes: {"status":"ok"}
"\r\n"
"{\"status\":\"ok\"}";
esp_err_t _http_event_handler(esp_http_client_event_t *evt)
{
switch (evt->event_id) {
case HTTP_EVENT_ON_CONNECTED:
ESP_LOGI("test", "Connected");
break;
case HTTP_EVENT_DISCONNECTED:
ESP_LOGI("test", "Disconnected");
break;
case HTTP_EVENT_HEADERS_SENT:
ESP_LOGI("test", "Headers sent");
break;
case HTTP_EVENT_ON_HEADER:
ESP_LOGI("test", "Header received");
break;
case HTTP_EVENT_ON_DATA:
ESP_LOGI("test", "Data received");
break;
case HTTP_EVENT_ON_FINISH:
ESP_LOGI("test", "Request finished");
break;
case HTTP_EVENT_ERROR:
ESP_LOGI("test", "Error occurred");
break;
default:
break;
}
return ESP_OK;
}
/**
* Test: Client reuse after read timeout
*
* Scenario: First request times out while waiting for response,
* second request should succeed with same client
*
* Expected: Client properly recovers and second request works
*/
TEST_CASE("HTTP client can be reused after read timeout", "[esp_http_client][error_recovery]")
{
// Note: Event loop initialization is optional for these tests
// The ESP_ERR_INVALID_STATE errors are expected if not initialized
// They don't affect the core functionality being tested
// ========== REQUEST 1: Timeout mode ==========
ESP_LOGI("test", "Request 1: Simulating read timeout");
mock_http_transport_config_t mock_config = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG();
mock_config.mode = MOCK_TRANSPORT_MODE_READ_TIMEOUT;
esp_transport_handle_t mock_transport = mock_http_transport_create(&mock_config);
TEST_ASSERT_NOT_NULL(mock_transport);
// Create client with custom transport (disable event posting to avoid errors)
esp_http_client_config_t config = {
.url = "http://mock-server.local/test",
.timeout_ms = 1000,
.is_async = false,
.event_handler = _http_event_handler,
.transport = mock_transport,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
TEST_ASSERT_NOT_NULL(client);
// This should timeout
esp_err_t err = esp_http_client_perform(client);
TEST_ASSERT_NOT_EQUAL(ESP_OK, err);
ESP_LOGI("test", "Request 1 failed as expected: %s", esp_err_to_name(err));
// Verify transport was called
mock_http_transport_stats_t stats = {0};
mock_http_transport_get_stats(mock_transport, &stats);
TEST_ASSERT_GREATER_THAN(0, stats.connect_calls);
// ========== REQUEST 2: Normal mode with same client ==========
ESP_LOGI("test", "Request 2: Normal operation with reused client");
// Reconfigure mock for success
mock_config.mode = MOCK_TRANSPORT_MODE_NORMAL;
mock_config.response_data = mock_http_response_ok;
mock_config.response_len = strlen(mock_http_response_ok);
mock_http_transport_set_config(mock_transport, &mock_config);
mock_http_transport_reset_stats(mock_transport);
// This should succeed
err = esp_http_client_perform(client);
if (err != ESP_OK) {
ESP_LOGE("test", "Request 2 failed: %s (0x%x)", esp_err_to_name(err), err);
}
TEST_ASSERT_EQUAL(ESP_OK, err);
int status_code = esp_http_client_get_status_code(client);
if (status_code != 200) {
ESP_LOGE("test", "Unexpected status code: %d", status_code);
}
TEST_ASSERT_EQUAL(200, status_code);
ESP_LOGI("test", "Request 2 succeeded - client recovered!");
// Verify the second request actually happened
mock_http_transport_get_stats(mock_transport, &stats);
/* Master does not close the connection after a fetch-header failure, so the
* reused client never reconnects; it keeps reading on the same connection. */
// characterization: master behavior, see refactor spec
TEST_ASSERT_EQUAL(0, stats.connect_calls);
TEST_ASSERT_GREATER_THAN(0, stats.read_calls);
// Cleanup
esp_http_client_cleanup(client);
mock_http_transport_destroy(mock_transport);
}
/**
* Test: Client reuse after write failure
*
* Scenario: First POST request fails during body write,
* second POST request should succeed with same client
*
* Expected: Client properly recovers and second request works
*/
TEST_CASE("HTTP client can be reused after write failure", "[esp_http_client][error_recovery]")
{
// ========== REQUEST 1: Write failure during headers ==========
ESP_LOGI("test", "Request 1: Simulating write failure during headers");
mock_http_transport_config_t mock_config = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG();
mock_config.mode = MOCK_TRANSPORT_MODE_WRITE_FAIL;
// Fail after 100 bytes: this causes failure while writing HTTP headers
// (before POST body starts)
mock_config.bytes_before_error = 100;
esp_transport_handle_t mock_transport = mock_http_transport_create(&mock_config);
TEST_ASSERT_NOT_NULL(mock_transport);
// Create client with custom transport
esp_http_client_config_t config = {
.url = "http://mock-server.local/post",
.method = HTTP_METHOD_POST,
.transport = mock_transport,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
TEST_ASSERT_NOT_NULL(client);
// Set POST data
const char *post_data = "{\"test\":\"data\",\"large\":\""
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"\"}";
esp_http_client_set_post_field(client, post_data, strlen(post_data));
// This should fail during write
esp_err_t err = esp_http_client_perform(client);
TEST_ASSERT_NOT_EQUAL(ESP_OK, err);
ESP_LOGI("test", "Request 1 failed as expected: %s", esp_err_to_name(err));
// ========== REQUEST 2: Write failure DURING POST body ==========
ESP_LOGI("test", "Request 2: Simulating write failure during POST body");
mock_config.mode = MOCK_TRANSPORT_MODE_WRITE_FAIL;
// Fail after 120 bytes: allows headers (~100 bytes) to be written,
// but fails during POST body write (which starts around byte 100-110)
mock_config.bytes_before_error = 170;
mock_http_transport_set_config(mock_transport, &mock_config);
mock_http_transport_reset_stats(mock_transport);
esp_http_client_set_post_field(client, post_data, strlen(post_data));
// This should fail during POST body write
err = esp_http_client_perform(client);
TEST_ASSERT_NOT_EQUAL(ESP_OK, err);
ESP_LOGI("test", "Request 2 failed as expected: %s", esp_err_to_name(err));
// ========== REQUEST 3: Normal mode with same client ==========
ESP_LOGI("test", "Request 3: Normal operation with reused client");
// Reconfigure mock for success
mock_config.mode = MOCK_TRANSPORT_MODE_NORMAL;
mock_config.response_data = mock_http_response_ok;
mock_config.response_len = strlen(mock_http_response_ok);
mock_config.bytes_before_error = -1; // No error injection
mock_http_transport_set_config(mock_transport, &mock_config);
mock_http_transport_reset_stats(mock_transport);
// Set smaller POST data
const char *post_data2 = "{\"retry\":\"success\"}";
esp_http_client_set_post_field(client, post_data2, strlen(post_data2));
/* Master leaves stale POST-body write state behind after the failed write,
* so the reused client fails immediately without touching the transport. */
err = esp_http_client_perform(client);
// characterization: master behavior, see refactor spec
TEST_ASSERT_EQUAL(ESP_FAIL, err);
TEST_ASSERT_EQUAL(0, esp_http_client_get_status_code(client));
ESP_LOGI("test", "Request 3 did not recover: %s", esp_err_to_name(err));
// Cleanup
esp_http_client_cleanup(client);
mock_http_transport_destroy(mock_transport);
}
/**
* Test: Client reuse after incomplete data
*
* Scenario: First request gets incomplete response (connection closed mid-read),
* second request should succeed with same client
*
* Expected: Client properly recovers and second request works
*/
TEST_CASE("HTTP client can be reused after incomplete data", "[esp_http_client][error_recovery]")
{
// ========== REQUEST 1: Incomplete response ==========
ESP_LOGI("test", "Request 1: Simulating incomplete response");
mock_http_transport_config_t mock_config = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG();
mock_config.mode = MOCK_TRANSPORT_MODE_INCOMPLETE_READ;
mock_config.response_data = mock_http_response_ok;
mock_config.response_len = strlen(mock_http_response_ok);
mock_config.bytes_before_error = 50; // Close connection after 50 bytes (mid-response)
esp_transport_handle_t mock_transport = mock_http_transport_create(&mock_config);
TEST_ASSERT_NOT_NULL(mock_transport);
// Create client with custom transport
esp_http_client_config_t config = {
.url = "http://mock-server.local/incomplete",
.transport = mock_transport,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
TEST_ASSERT_NOT_NULL(client);
// This should fail due to incomplete data
esp_err_t err = esp_http_client_perform(client);
TEST_ASSERT_NOT_EQUAL(ESP_OK, err);
ESP_LOGI("test", "Request 1 failed as expected: %s", esp_err_to_name(err));
// ========== REQUEST 2: Normal mode with same client ==========
ESP_LOGI("test", "Request 2: Normal operation with reused client");
// Reconfigure mock for success (complete response)
mock_config.mode = MOCK_TRANSPORT_MODE_NORMAL;
mock_config.bytes_before_error = -1; // No error injection
mock_http_transport_set_config(mock_transport, &mock_config);
mock_http_transport_reset_stats(mock_transport);
/* Master does not reset the parser/connection state after the aborted read,
* so the reused client fails header fetching without touching the transport. */
err = esp_http_client_perform(client);
// characterization: master behavior, see refactor spec
TEST_ASSERT_EQUAL(ESP_ERR_HTTP_FETCH_HEADER, err);
TEST_ASSERT_EQUAL(-1, esp_http_client_get_status_code(client));
ESP_LOGI("test", "Request 2 did not recover: %s", esp_err_to_name(err));
// Cleanup
esp_http_client_cleanup(client);
mock_http_transport_destroy(mock_transport);
}
/**
* Test: Multiple requests with alternating success/failure
*
* Scenario: Multiple requests with errors interspersed with successful requests
*
* Expected: Client can be reused multiple times after various error conditions
*/
TEST_CASE("HTTP client survives multiple error/success cycles", "[esp_http_client][error_recovery]")
{
mock_http_transport_config_t mock_config = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG();
esp_transport_handle_t mock_transport = mock_http_transport_create(&mock_config);
TEST_ASSERT_NOT_NULL(mock_transport);
// Create client with custom transport
esp_http_client_config_t config = {
.url = "http://mock-server.local/cycle",
.event_handler = _http_event_handler,
.transport = mock_transport,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
TEST_ASSERT_NOT_NULL(client);
// Define test sequence: success, timeout, success, incomplete, success
// Note: We use INCOMPLETE_READ instead of WRITE_FAIL because write_fail
// requires POST data and bytes_before_error configuration
mock_transport_mode_t sequence[] = {
MOCK_TRANSPORT_MODE_NORMAL,
MOCK_TRANSPORT_MODE_READ_TIMEOUT,
MOCK_TRANSPORT_MODE_NORMAL,
MOCK_TRANSPORT_MODE_INCOMPLETE_READ,
MOCK_TRANSPORT_MODE_NORMAL,
};
/* Master recovers from a read timeout (cycle 1 -> 2) but not from an aborted
* read (cycle 3), so the final cycle fails instead of succeeding. */
// characterization: master behavior, see refactor spec
bool expected_success[] = {true, false, true, false, false};
for (int i = 0; i < sizeof(sequence) / sizeof(sequence[0]); i++) {
ESP_LOGI("test", "Cycle %d: mode=%d, expect %s",
i, sequence[i], expected_success[i] ? "SUCCESS" : "FAILURE");
// Configure mock
mock_config.mode = sequence[i];
if (sequence[i] == MOCK_TRANSPORT_MODE_NORMAL) {
mock_config.response_data = mock_http_response_ok;
mock_config.response_len = strlen(mock_http_response_ok);
mock_config.bytes_before_error = -1; // No error injection
} else if (sequence[i] == MOCK_TRANSPORT_MODE_INCOMPLETE_READ) {
mock_config.response_data = mock_http_response_ok;
mock_config.response_len = strlen(mock_http_response_ok);
mock_config.bytes_before_error = 50; // Close after 50 bytes
}
mock_http_transport_set_config(mock_transport, &mock_config);
mock_http_transport_reset_stats(mock_transport);
// Perform request
esp_err_t err = esp_http_client_perform(client);
// Verify expectation
if (expected_success[i]) {
TEST_ASSERT_EQUAL(ESP_OK, err);
TEST_ASSERT_EQUAL(200, esp_http_client_get_status_code(client));
} else {
TEST_ASSERT_NOT_EQUAL(ESP_OK, err);
}
}
ESP_LOGI("test", "Client survived %d error/success cycles!",
sizeof(sequence) / sizeof(sequence[0]));
// Cleanup
esp_http_client_cleanup(client);
mock_http_transport_destroy(mock_transport);
}
#endif // CONFIG_ESP_HTTP_CLIENT_ENABLE_CUSTOM_TRANSPORT
void app_main(void)
{
unity_run_menu();

View File

@@ -0,0 +1,719 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @file test_http_client_basic.c
* @brief P0 Critical Tests: Basic HTTP Client Functionality
*
* This file contains P0 (critical) tests covering:
* - HTTP methods (GET, POST, PUT, DELETE, HEAD)
* - HTTP status codes (2xx success, 4xx client errors, 5xx server errors)
* - Basic request/response handling
* - Both positive (success) and negative (error) scenarios
*/
#include <stdlib.h>
#include <string.h>
#include "esp_http_client.h"
#include "esp_log.h"
#include "unity.h"
#include "sdkconfig.h"
#include "test_http_client_mock_transport.h"
/*
* Every case in this file drives the client through a mock transport injected
* via esp_http_client_config_t::transport. Without custom transport support
* the clients would fall back to a real transport aimed at test-server.local,
* which does not exist, so the whole file compiles out.
*/
#if CONFIG_ESP_HTTP_CLIENT_ENABLE_CUSTOM_TRANSPORT
static const char *TAG = "test_basic";
/* ============================================
* Test Response Templates
* ============================================ */
// 200 OK - Successful GET response
static const char *response_200_ok =
"HTTP/1.1 200 OK\r\n"
"Content-Type: application/json\r\n"
"Content-Length: 32\r\n"
"\r\n"
"{\"message\":\"success\",\"code\":200}";
// 201 Created - Successful POST response
static const char *response_201_created =
"HTTP/1.1 201 Created\r\n"
"Content-Type: application/json\r\n"
"Location: /resource/12345\r\n"
"Content-Length: 31\r\n"
"\r\n"
"{\"id\":12345,\"status\":\"created\"}";
// 204 No Content - Successful DELETE response (no body)
static const char *response_204_no_content =
"HTTP/1.1 204 No Content\r\n"
"Content-Length: 0\r\n"
"\r\n";
// 400 Bad Request - Client error
static const char *response_400_bad_request =
"HTTP/1.1 400 Bad Request\r\n"
"Content-Type: application/json\r\n"
"Content-Length: 34\r\n"
"\r\n"
"{\"error\":\"Invalid request format\"}";
// 401 Unauthorized - Authentication required
static const char *response_401_unauthorized =
"HTTP/1.1 401 Unauthorized\r\n"
"WWW-Authenticate: Basic realm=\"Test\"\r\n"
"Content-Type: application/json\r\n"
"Content-Length: 35\r\n"
"\r\n"
"{\"error\":\"Authentication required\"}";
// 404 Not Found - Resource not found
static const char *response_404_not_found =
"HTTP/1.1 404 Not Found\r\n"
"Content-Type: application/json\r\n"
"Content-Length: 30\r\n"
"\r\n"
"{\"error\":\"Resource not found\"}";
// 500 Internal Server Error
static const char *response_500_server_error =
"HTTP/1.1 500 Internal Server Error\r\n"
"Content-Type: application/json\r\n"
"Content-Length: 33\r\n"
"\r\n"
"{\"error\":\"Internal server error\"}";
// 503 Service Unavailable
static const char *response_503_unavailable =
"HTTP/1.1 503 Service Unavailable\r\n"
"Retry-After: 60\r\n"
"Content-Type: application/json\r\n"
"Content-Length: 36\r\n"
"\r\n"
"{\"error\":\"Service temporarily down\"}";
/* ============================================
* Helper Functions
* ============================================ */
static esp_err_t basic_event_handler(esp_http_client_event_t *evt)
{
switch (evt->event_id) {
case HTTP_EVENT_ERROR:
ESP_LOGD(TAG, "HTTP_EVENT_ERROR");
break;
case HTTP_EVENT_ON_CONNECTED:
ESP_LOGD(TAG, "HTTP_EVENT_ON_CONNECTED");
break;
case HTTP_EVENT_HEADERS_SENT:
ESP_LOGD(TAG, "HTTP_EVENT_HEADERS_SENT");
break;
case HTTP_EVENT_ON_HEADER:
ESP_LOGD(TAG, "HTTP_EVENT_ON_HEADER: %s: %s", evt->header_key, evt->header_value);
break;
case HTTP_EVENT_ON_DATA:
ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA: %d bytes", evt->data_len);
break;
case HTTP_EVENT_ON_FINISH:
ESP_LOGD(TAG, "HTTP_EVENT_ON_FINISH");
break;
case HTTP_EVENT_DISCONNECTED:
ESP_LOGD(TAG, "HTTP_EVENT_DISCONNECTED");
break;
default:
break;
}
return ESP_OK;
}
/* ============================================
* P0 Test: HTTP Methods - Positive Cases
* ============================================ */
/**
* Test: GET request succeeds with 200 OK
*
* Positive scenario: Normal GET request returns success
*/
TEST_CASE("GET request succeeds with 200 OK", "[esp_http_client][basic][p0][positive]")
{
ESP_LOGI(TAG, "Testing GET request - positive case");
// Setup mock transport with 200 OK response
mock_http_transport_config_t mock_config = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG();
mock_config.mode = MOCK_TRANSPORT_MODE_NORMAL;
mock_config.response_data = response_200_ok;
mock_config.response_len = strlen(response_200_ok);
esp_transport_handle_t mock_transport = mock_http_transport_create(&mock_config);
TEST_ASSERT_NOT_NULL(mock_transport);
// Create HTTP client
esp_http_client_config_t config = {
.url = "http://test-server.local/api/data",
.method = HTTP_METHOD_GET,
.event_handler = basic_event_handler,
.transport = mock_transport,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
TEST_ASSERT_NOT_NULL(client);
// Perform request
esp_err_t err = esp_http_client_perform(client);
// Verify success
TEST_ASSERT_EQUAL(ESP_OK, err);
TEST_ASSERT_EQUAL(200, esp_http_client_get_status_code(client));
TEST_ASSERT_EQUAL(32, esp_http_client_get_content_length(client));
// Verify transport was used
mock_http_transport_stats_t stats;
mock_http_transport_get_stats(mock_transport, &stats);
TEST_ASSERT_EQUAL(1, stats.connect_calls);
TEST_ASSERT_GREATER_THAN(0, stats.write_calls); // Sent request
TEST_ASSERT_GREATER_THAN(0, stats.read_calls); // Received response
ESP_LOGI(TAG, "OK: GET request successful");
esp_http_client_cleanup(client);
mock_http_transport_destroy(mock_transport);
}
/**
* Test: POST request succeeds with 201 Created
*
* Positive scenario: POST with body returns success
*/
TEST_CASE("POST request succeeds with 201 Created", "[esp_http_client][basic][p0][positive]")
{
ESP_LOGI(TAG, "Testing POST request - positive case");
// Setup mock transport with 201 Created response
mock_http_transport_config_t mock_config = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG();
mock_config.mode = MOCK_TRANSPORT_MODE_NORMAL;
mock_config.response_data = response_201_created;
mock_config.response_len = strlen(response_201_created);
esp_transport_handle_t mock_transport = mock_http_transport_create(&mock_config);
TEST_ASSERT_NOT_NULL(mock_transport);
// Create HTTP client
esp_http_client_config_t config = {
.url = "http://test-server.local/api/resource",
.method = HTTP_METHOD_POST,
.event_handler = basic_event_handler,
.transport = mock_transport,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
TEST_ASSERT_NOT_NULL(client);
// Set POST data
const char *post_data = "{\"name\":\"test\",\"value\":123}";
esp_http_client_set_post_field(client, post_data, strlen(post_data));
// Perform request
esp_err_t err = esp_http_client_perform(client);
// Verify success
TEST_ASSERT_EQUAL(ESP_OK, err);
TEST_ASSERT_EQUAL(201, esp_http_client_get_status_code(client));
// Verify POST data was sent
mock_http_transport_stats_t stats;
mock_http_transport_get_stats(mock_transport, &stats);
TEST_ASSERT_GREATER_THAN(strlen(post_data), stats.total_bytes_written);
ESP_LOGI(TAG, "OK: POST request successful");
esp_http_client_cleanup(client);
mock_http_transport_destroy(mock_transport);
}
/**
* Test: PUT request succeeds with 200 OK
*
* Positive scenario: PUT request updates resource
*/
TEST_CASE("PUT request succeeds with 200 OK", "[esp_http_client][basic][p0][positive]")
{
ESP_LOGI(TAG, "Testing PUT request - positive case");
mock_http_transport_config_t mock_config = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG();
mock_config.mode = MOCK_TRANSPORT_MODE_NORMAL;
mock_config.response_data = response_200_ok;
mock_config.response_len = strlen(response_200_ok);
esp_transport_handle_t mock_transport = mock_http_transport_create(&mock_config);
TEST_ASSERT_NOT_NULL(mock_transport);
esp_http_client_config_t config = {
.url = "http://test-server.local/api/resource/123",
.method = HTTP_METHOD_PUT,
.event_handler = basic_event_handler,
.transport = mock_transport,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
TEST_ASSERT_NOT_NULL(client);
const char *put_data = "{\"status\":\"updated\"}";
esp_http_client_set_post_field(client, put_data, strlen(put_data));
esp_err_t err = esp_http_client_perform(client);
TEST_ASSERT_EQUAL(ESP_OK, err);
TEST_ASSERT_EQUAL(200, esp_http_client_get_status_code(client));
ESP_LOGI(TAG, "OK: PUT request successful");
esp_http_client_cleanup(client);
mock_http_transport_destroy(mock_transport);
}
/**
* Test: DELETE request succeeds with 204 No Content
*
* Positive scenario: DELETE removes resource, no body returned
*/
TEST_CASE("DELETE request succeeds with 204 No Content", "[esp_http_client][basic][p0][positive]")
{
ESP_LOGI(TAG, "Testing DELETE request - positive case");
mock_http_transport_config_t mock_config = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG();
mock_config.mode = MOCK_TRANSPORT_MODE_NORMAL;
mock_config.response_data = response_204_no_content;
mock_config.response_len = strlen(response_204_no_content);
esp_transport_handle_t mock_transport = mock_http_transport_create(&mock_config);
TEST_ASSERT_NOT_NULL(mock_transport);
esp_http_client_config_t config = {
.url = "http://test-server.local/api/resource/123",
.method = HTTP_METHOD_DELETE,
.event_handler = basic_event_handler,
.transport = mock_transport,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
TEST_ASSERT_NOT_NULL(client);
esp_err_t err = esp_http_client_perform(client);
TEST_ASSERT_EQUAL(ESP_OK, err);
TEST_ASSERT_EQUAL(204, esp_http_client_get_status_code(client));
TEST_ASSERT_EQUAL(0, esp_http_client_get_content_length(client)); // No content
ESP_LOGI(TAG, "OK: DELETE request successful");
esp_http_client_cleanup(client);
mock_http_transport_destroy(mock_transport);
}
/**
* Test: HEAD request succeeds with headers only
*
* Positive scenario: HEAD request returns headers but no body
*/
TEST_CASE("HEAD request succeeds with headers only", "[esp_http_client][basic][p0][positive]")
{
ESP_LOGI(TAG, "Testing HEAD request - positive case");
// HEAD response has headers but no body
const char *response_head =
"HTTP/1.1 200 OK\r\n"
"Content-Type: application/json\r\n"
"Content-Length: 1024\r\n" // Says content length but sends no body
"\r\n";
mock_http_transport_config_t mock_config = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG();
mock_config.mode = MOCK_TRANSPORT_MODE_NORMAL;
mock_config.response_data = response_head;
mock_config.response_len = strlen(response_head);
esp_transport_handle_t mock_transport = mock_http_transport_create(&mock_config);
TEST_ASSERT_NOT_NULL(mock_transport);
esp_http_client_config_t config = {
.url = "http://test-server.local/api/resource",
.method = HTTP_METHOD_HEAD,
.event_handler = basic_event_handler,
.transport = mock_transport,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
TEST_ASSERT_NOT_NULL(client);
esp_err_t err = esp_http_client_perform(client);
TEST_ASSERT_EQUAL(ESP_OK, err);
TEST_ASSERT_EQUAL(200, esp_http_client_get_status_code(client));
// Note: Content-Length header says 1024, but no body should be received for HEAD
ESP_LOGI(TAG, "OK: HEAD request successful");
esp_http_client_cleanup(client);
mock_http_transport_destroy(mock_transport);
}
/* ============================================
* P0 Test: HTTP Status Codes - 4xx Client Errors
* ============================================ */
/**
* Test: Client handles 400 Bad Request gracefully
*
* Negative scenario: Server rejects malformed request
* Expected: Client reports error but doesn't crash, can be reused
*/
TEST_CASE("Client handles 400 Bad Request error", "[esp_http_client][basic][p0][negative]")
{
ESP_LOGI(TAG, "Testing 400 Bad Request - negative case");
mock_http_transport_config_t mock_config = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG();
mock_config.mode = MOCK_TRANSPORT_MODE_NORMAL;
mock_config.response_data = response_400_bad_request;
mock_config.response_len = strlen(response_400_bad_request);
esp_transport_handle_t mock_transport = mock_http_transport_create(&mock_config);
TEST_ASSERT_NOT_NULL(mock_transport);
esp_http_client_config_t config = {
.url = "http://test-server.local/api/bad",
.event_handler = basic_event_handler,
.transport = mock_transport,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
TEST_ASSERT_NOT_NULL(client);
// Request completes but with error status
esp_err_t err = esp_http_client_perform(client);
TEST_ASSERT_EQUAL(ESP_OK, err); // Transport succeeds, but status is 400
TEST_ASSERT_EQUAL(400, esp_http_client_get_status_code(client));
// Verify client can be reused after 4xx error
mock_config.response_data = response_200_ok;
mock_config.response_len = strlen(response_200_ok);
mock_http_transport_set_config(mock_transport, &mock_config);
err = esp_http_client_perform(client);
TEST_ASSERT_EQUAL(ESP_OK, err);
TEST_ASSERT_EQUAL(200, esp_http_client_get_status_code(client));
ESP_LOGI(TAG, "OK: 400 error handled gracefully, client reusable");
esp_http_client_cleanup(client);
mock_http_transport_destroy(mock_transport);
}
/**
* Test: Client handles 401 Unauthorized
*
* Negative scenario: Authentication required but not provided
*/
TEST_CASE("Client handles 401 Unauthorized error", "[esp_http_client][basic][p0][negative]")
{
ESP_LOGI(TAG, "Testing 401 Unauthorized - negative case");
mock_http_transport_config_t mock_config = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG();
mock_config.mode = MOCK_TRANSPORT_MODE_NORMAL;
mock_config.response_data = response_401_unauthorized;
mock_config.response_len = strlen(response_401_unauthorized);
esp_transport_handle_t mock_transport = mock_http_transport_create(&mock_config);
TEST_ASSERT_NOT_NULL(mock_transport);
esp_http_client_config_t config = {
.url = "http://test-server.local/api/protected",
.event_handler = basic_event_handler,
.transport = mock_transport,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
TEST_ASSERT_NOT_NULL(client);
esp_err_t err = esp_http_client_perform(client);
TEST_ASSERT_EQUAL(ESP_ERR_NOT_SUPPORTED, err);
TEST_ASSERT_EQUAL(401, esp_http_client_get_status_code(client));
// Verify WWW-Authenticate header could be read
// (In real scenarios, this would trigger authentication retry)
ESP_LOGI(TAG, "OK: 401 error handled, authentication required");
esp_http_client_cleanup(client);
mock_http_transport_destroy(mock_transport);
}
/**
* Test: Client handles 404 Not Found
*
* Negative scenario: Requested resource doesn't exist
*/
TEST_CASE("Client handles 404 Not Found error", "[esp_http_client][basic][p0][negative]")
{
ESP_LOGI(TAG, "Testing 404 Not Found - negative case");
mock_http_transport_config_t mock_config = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG();
mock_config.mode = MOCK_TRANSPORT_MODE_NORMAL;
mock_config.response_data = response_404_not_found;
mock_config.response_len = strlen(response_404_not_found);
esp_transport_handle_t mock_transport = mock_http_transport_create(&mock_config);
TEST_ASSERT_NOT_NULL(mock_transport);
esp_http_client_config_t config = {
.url = "http://test-server.local/api/nonexistent",
.event_handler = basic_event_handler,
.transport = mock_transport,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
TEST_ASSERT_NOT_NULL(client);
esp_err_t err = esp_http_client_perform(client);
TEST_ASSERT_EQUAL(ESP_OK, err);
TEST_ASSERT_EQUAL(404, esp_http_client_get_status_code(client));
ESP_LOGI(TAG, "OK: 404 error handled gracefully");
esp_http_client_cleanup(client);
mock_http_transport_destroy(mock_transport);
}
/* ============================================
* P0 Test: HTTP Status Codes - 5xx Server Errors
* ============================================ */
/**
* Test: Client handles 500 Internal Server Error
*
* Negative scenario: Server encounters internal error
* Expected: Client reports error, remains usable for retry
*/
TEST_CASE("Client handles 500 Internal Server Error", "[esp_http_client][basic][p0][negative]")
{
ESP_LOGI(TAG, "Testing 500 Server Error - negative case");
mock_http_transport_config_t mock_config = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG();
mock_config.mode = MOCK_TRANSPORT_MODE_NORMAL;
mock_config.response_data = response_500_server_error;
mock_config.response_len = strlen(response_500_server_error);
esp_transport_handle_t mock_transport = mock_http_transport_create(&mock_config);
TEST_ASSERT_NOT_NULL(mock_transport);
esp_http_client_config_t config = {
.url = "http://test-server.local/api/failing",
.event_handler = basic_event_handler,
.transport = mock_transport,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
TEST_ASSERT_NOT_NULL(client);
// First request gets 500 error
esp_err_t err = esp_http_client_perform(client);
TEST_ASSERT_EQUAL(ESP_OK, err);
TEST_ASSERT_EQUAL(500, esp_http_client_get_status_code(client));
ESP_LOGI(TAG, "First request got 500 error");
// Simulate retry after server recovers
mock_config.response_data = response_200_ok;
mock_config.response_len = strlen(response_200_ok);
mock_http_transport_set_config(mock_transport, &mock_config);
err = esp_http_client_perform(client);
TEST_ASSERT_EQUAL(ESP_OK, err);
TEST_ASSERT_EQUAL(200, esp_http_client_get_status_code(client));
ESP_LOGI(TAG, "OK: 500 error handled, retry successful");
esp_http_client_cleanup(client);
mock_http_transport_destroy(mock_transport);
}
/**
* Test: Client handles 503 Service Unavailable
*
* Negative scenario: Service temporarily down
*/
TEST_CASE("Client handles 503 Service Unavailable", "[esp_http_client][basic][p0][negative]")
{
ESP_LOGI(TAG, "Testing 503 Service Unavailable - negative case");
mock_http_transport_config_t mock_config = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG();
mock_config.mode = MOCK_TRANSPORT_MODE_NORMAL;
mock_config.response_data = response_503_unavailable;
mock_config.response_len = strlen(response_503_unavailable);
esp_transport_handle_t mock_transport = mock_http_transport_create(&mock_config);
TEST_ASSERT_NOT_NULL(mock_transport);
esp_http_client_config_t config = {
.url = "http://test-server.local/api/overloaded",
.event_handler = basic_event_handler,
.transport = mock_transport,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
TEST_ASSERT_NOT_NULL(client);
esp_err_t err = esp_http_client_perform(client);
TEST_ASSERT_EQUAL(ESP_OK, err);
TEST_ASSERT_EQUAL(503, esp_http_client_get_status_code(client));
// Verify Retry-After header could be read
// (In real scenarios, client would wait before retrying)
ESP_LOGI(TAG, "OK: 503 error handled, service unavailable");
esp_http_client_cleanup(client);
mock_http_transport_destroy(mock_transport);
}
/* ============================================
* P0 Test: Connection Failures - Negative Cases
* ============================================ */
/**
* Test: Client handles connection failure gracefully
*
* Negative scenario: Cannot connect to server
* Expected: Error reported, client remains in valid state
*/
TEST_CASE("Client handles connection failure", "[esp_http_client][basic][p0][negative]")
{
ESP_LOGI(TAG, "Testing connection failure - negative case");
mock_http_transport_config_t mock_config = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG();
mock_config.mode = MOCK_TRANSPORT_MODE_CONNECT_FAIL;
esp_transport_handle_t mock_transport = mock_http_transport_create(&mock_config);
TEST_ASSERT_NOT_NULL(mock_transport);
esp_http_client_config_t config = {
.url = "http://unreachable-server.local/api/test",
.event_handler = basic_event_handler,
.transport = mock_transport,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
TEST_ASSERT_NOT_NULL(client);
// Connection should fail
esp_err_t err = esp_http_client_perform(client);
TEST_ASSERT_NOT_EQUAL(ESP_OK, err);
ESP_LOGI(TAG, "Connection failed as expected: %s", esp_err_to_name(err));
// Verify client can retry after connection failure
mock_config.mode = MOCK_TRANSPORT_MODE_NORMAL;
mock_config.response_data = response_200_ok;
mock_config.response_len = strlen(response_200_ok);
mock_http_transport_set_config(mock_transport, &mock_config);
err = esp_http_client_perform(client);
TEST_ASSERT_EQUAL(ESP_OK, err);
TEST_ASSERT_EQUAL(200, esp_http_client_get_status_code(client));
ESP_LOGI(TAG, "OK: Connection failure handled, retry successful");
esp_http_client_cleanup(client);
mock_http_transport_destroy(mock_transport);
}
/**
* Test: POST with empty body succeeds
*
* Edge case: POST request with no data
*/
TEST_CASE("POST with empty body succeeds", "[esp_http_client][basic][p0][positive]")
{
ESP_LOGI(TAG, "Testing POST with empty body - edge case");
mock_http_transport_config_t mock_config = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG();
mock_config.mode = MOCK_TRANSPORT_MODE_NORMAL;
mock_config.response_data = response_201_created;
mock_config.response_len = strlen(response_201_created);
esp_transport_handle_t mock_transport = mock_http_transport_create(&mock_config);
TEST_ASSERT_NOT_NULL(mock_transport);
esp_http_client_config_t config = {
.url = "http://test-server.local/api/resource",
.method = HTTP_METHOD_POST,
.event_handler = basic_event_handler,
.transport = mock_transport,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
TEST_ASSERT_NOT_NULL(client);
// POST with empty body (Content-Length: 0)
esp_http_client_set_post_field(client, "", 0);
esp_err_t err = esp_http_client_perform(client);
TEST_ASSERT_EQUAL(ESP_OK, err);
TEST_ASSERT_EQUAL(201, esp_http_client_get_status_code(client));
ESP_LOGI(TAG, "OK: POST with empty body successful");
esp_http_client_cleanup(client);
mock_http_transport_destroy(mock_transport);
}
/**
* Test: Response with empty body (Content-Length: 0)
*
* Edge case: Server returns headers but no body
*/
TEST_CASE("Client handles response with empty body", "[esp_http_client][basic][p0][positive]")
{
ESP_LOGI(TAG, "Testing response with empty body - edge case");
const char *response_empty_body =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: 0\r\n"
"\r\n";
mock_http_transport_config_t mock_config = MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG();
mock_config.mode = MOCK_TRANSPORT_MODE_NORMAL;
mock_config.response_data = response_empty_body;
mock_config.response_len = strlen(response_empty_body);
esp_transport_handle_t mock_transport = mock_http_transport_create(&mock_config);
TEST_ASSERT_NOT_NULL(mock_transport);
esp_http_client_config_t config = {
.url = "http://test-server.local/api/empty",
.event_handler = basic_event_handler,
.transport = mock_transport,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
TEST_ASSERT_NOT_NULL(client);
esp_err_t err = esp_http_client_perform(client);
TEST_ASSERT_EQUAL(ESP_OK, err);
TEST_ASSERT_EQUAL(200, esp_http_client_get_status_code(client));
TEST_ASSERT_EQUAL(0, esp_http_client_get_content_length(client));
ESP_LOGI(TAG, "OK: Empty response body handled correctly");
esp_http_client_cleanup(client);
mock_http_transport_destroy(mock_transport);
}
#endif // CONFIG_ESP_HTTP_CLIENT_ENABLE_CUSTOM_TRANSPORT

View File

@@ -0,0 +1,529 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "test_http_client_mock_transport.h"
#include "esp_log.h"
#include <string.h>
#include <errno.h>
#include <sys/time.h>
static const char *TAG = "mock_transport";
/**
* @brief Internal context for mock transport
*/
typedef struct {
mock_http_transport_config_t config; /*!< Current configuration */
mock_http_transport_stats_t stats; /*!< Call statistics */
bool is_connected; /*!< Connection state */
size_t read_offset; /*!< Current position in response data */
size_t bytes_processed; /*!< Bytes processed (for error injection) */
char *response_buffer; /*!< Internal copy of response data */
} mock_http_transport_ctx_t;
// Forward declarations of transport function implementations
static int mock_connect(esp_transport_handle_t t, const char *host, int port, int timeout_ms);
static int mock_read(esp_transport_handle_t t, char *buffer, int len, int timeout_ms);
static int mock_write(esp_transport_handle_t t, const char *buffer, int len, int timeout_ms);
static int mock_close(esp_transport_handle_t t);
static int mock_poll_read(esp_transport_handle_t t, int timeout_ms);
static int mock_poll_write(esp_transport_handle_t t, int timeout_ms);
static int mock_destroy(esp_transport_handle_t t);
/**
* @brief Simulate delay (for timeouts and connection delays)
*/
static void simulate_delay_ms(int delay_ms)
{
if (delay_ms <= 0) {
return;
}
struct timeval tv;
tv.tv_sec = delay_ms / 1000;
tv.tv_usec = (delay_ms % 1000) * 1000;
select(0, NULL, NULL, NULL, &tv);
}
/**
* @brief Check if error should be injected based on bytes processed
*/
static bool should_inject_error(mock_http_transport_ctx_t *ctx, size_t bytes_about_to_process)
{
if (ctx->config.bytes_before_error < 0) {
return false; // No error injection configured
}
return (ctx->bytes_processed + bytes_about_to_process) > (size_t)ctx->config.bytes_before_error;
}
/**
* @brief Mock connect implementation
*/
static int mock_connect(esp_transport_handle_t t, const char *host, int port, int timeout_ms)
{
mock_http_transport_ctx_t *ctx = esp_transport_get_context_data(t);
if (!ctx) {
ESP_LOGE(TAG, "Invalid transport context");
errno = EINVAL;
return -1;
}
if (ctx->config.track_calls) {
ctx->stats.connect_calls++;
}
ESP_LOGD(TAG, "Mock connect to %s:%d (mode=%d)", host, port, ctx->config.mode);
// Simulate connection delay
if (ctx->config.connect_delay_ms > 0) {
simulate_delay_ms(ctx->config.connect_delay_ms);
}
// Handle connect failure mode
if (ctx->config.mode == MOCK_TRANSPORT_MODE_CONNECT_FAIL) {
ESP_LOGD(TAG, "Mock connect failed (simulated)");
errno = ECONNREFUSED;
return -1;
}
// Success
ctx->is_connected = true;
ctx->read_offset = 0;
ctx->bytes_processed = 0;
ESP_LOGI(TAG, "Mock connect succeeded");
return 0;
}
/**
* @brief Mock read implementation
*/
static int mock_read(esp_transport_handle_t t, char *buffer, int len, int timeout_ms)
{
mock_http_transport_ctx_t *ctx = esp_transport_get_context_data(t);
if (!ctx || !buffer || len <= 0) {
errno = EINVAL;
return -1;
}
if (ctx->config.track_calls) {
ctx->stats.read_calls++;
}
if (!ctx->is_connected) {
ESP_LOGD(TAG, "Mock read: not connected");
errno = ENOTCONN;
return -1;
}
ESP_LOGD(TAG, "Mock read: requested %d bytes (mode=%d)", len, ctx->config.mode);
// Handle read timeout mode
if (ctx->config.mode == MOCK_TRANSPORT_MODE_READ_TIMEOUT) {
ESP_LOGD(TAG, "Mock read: timeout (simulated)");
errno = ETIMEDOUT;
return -1;
}
// If no response data configured, return 0 (connection closed)
if (!ctx->response_buffer || ctx->config.response_len == 0) {
ESP_LOGD(TAG, "Mock read: no data available (EOF)");
return 0;
}
// Calculate available data
size_t remaining = ctx->config.response_len - ctx->read_offset;
if (remaining == 0) {
ESP_LOGD(TAG, "Mock read: all data consumed (EOF)");
return 0; // All data consumed
}
// Determine how much to read
size_t to_read = (len < remaining) ? len : remaining;
// Handle incomplete read mode (close connection mid-stream)
if (ctx->config.mode == MOCK_TRANSPORT_MODE_INCOMPLETE_READ) {
if (should_inject_error(ctx, to_read)) {
// Read partial data then close connection
size_t partial = ctx->config.bytes_before_error - ctx->bytes_processed;
if (partial > 0 && partial < to_read) {
memcpy(buffer, ctx->response_buffer + ctx->read_offset, partial);
ctx->read_offset += partial;
ctx->bytes_processed += partial;
ctx->stats.total_bytes_read += partial;
ESP_LOGD(TAG, "Mock read: incomplete data %zu bytes, then EOF", partial);
return partial;
}
// Connection closed
ctx->is_connected = false;
ESP_LOGD(TAG, "Mock read: connection closed (incomplete data)");
return 0;
}
}
// Normal read
memcpy(buffer, ctx->response_buffer + ctx->read_offset, to_read);
ctx->read_offset += to_read;
ctx->bytes_processed += to_read;
if (ctx->config.track_calls) {
ctx->stats.total_bytes_read += to_read;
}
ESP_LOGI(TAG, "Mock read: returned %zu bytes (offset now=%zu, remaining=%zu)",
to_read, ctx->read_offset, ctx->config.response_len - ctx->read_offset);
return to_read;
}
/**
* @brief Mock write implementation
*/
static int mock_write(esp_transport_handle_t t, const char *buffer, int len, int timeout_ms)
{
mock_http_transport_ctx_t *ctx = esp_transport_get_context_data(t);
if (!ctx || !buffer || len <= 0) {
errno = EINVAL;
return -1;
}
if (ctx->config.track_calls) {
ctx->stats.write_calls++;
}
if (!ctx->is_connected) {
ESP_LOGD(TAG, "Mock write: not connected");
errno = ENOTCONN;
return -1;
}
ESP_LOGI(TAG, "Mock write: %d bytes (mode=%d, bytes_processed=%zu)",
len, ctx->config.mode, ctx->bytes_processed);
// Handle write failure mode
if (ctx->config.mode == MOCK_TRANSPORT_MODE_WRITE_FAIL) {
if (should_inject_error(ctx, len)) {
ESP_LOGI(TAG, "Mock write: FAILED (simulated) - connection broken");
// Write failure (EPIPE) indicates broken connection
// This simulates real-world behavior where write errors break the connection
ctx->is_connected = false;
errno = EPIPE;
return -1;
}
}
// Handle partial write mode
if (ctx->config.mode == MOCK_TRANSPORT_MODE_WRITE_PARTIAL) {
if (should_inject_error(ctx, len)) {
// Write only partial data
int partial = ctx->config.bytes_before_error - ctx->bytes_processed;
if (partial > 0 && partial < len) {
ctx->bytes_processed += partial;
ctx->stats.total_bytes_written += partial;
ESP_LOGI(TAG, "Mock write: partial write %d bytes (out of %d)", partial, len);
return partial;
}
}
}
// Normal write (just track it, don't actually store)
ctx->bytes_processed += len;
if (ctx->config.track_calls) {
ctx->stats.total_bytes_written += len;
}
ESP_LOGI(TAG, "Mock write: completed %d bytes (total_processed=%zu)",
len, ctx->bytes_processed);
return len;
}
/**
* @brief Mock close implementation
*/
static int mock_close(esp_transport_handle_t t)
{
mock_http_transport_ctx_t *ctx = esp_transport_get_context_data(t);
if (!ctx) {
return -1;
}
if (ctx->config.track_calls) {
ctx->stats.close_calls++;
}
ESP_LOGI(TAG, "Mock close (was_connected=%d)", ctx->is_connected);
ctx->is_connected = false;
ctx->read_offset = 0;
ctx->bytes_processed = 0;
return 0;
}
/**
* @brief Mock poll_read implementation
*/
static int mock_poll_read(esp_transport_handle_t t, int timeout_ms)
{
mock_http_transport_ctx_t *ctx = esp_transport_get_context_data(t);
if (!ctx) {
errno = EINVAL;
return -1;
}
if (ctx->config.track_calls) {
ctx->stats.poll_read_calls++;
}
if (!ctx->is_connected) {
errno = ENOTCONN;
return -1;
}
// In normal mode, indicate data is available if we have response data
if (ctx->response_buffer && ctx->read_offset < ctx->config.response_len) {
return 1; // Data available
}
// Handle timeout mode
if (ctx->config.mode == MOCK_TRANSPORT_MODE_READ_TIMEOUT) {
simulate_delay_ms(timeout_ms);
errno = ETIMEDOUT;
return 0; // Timeout
}
return 0; // No data or timeout
}
/**
* @brief Mock poll_write implementation
*/
static int mock_poll_write(esp_transport_handle_t t, int timeout_ms)
{
mock_http_transport_ctx_t *ctx = esp_transport_get_context_data(t);
if (!ctx) {
errno = EINVAL;
return -1;
}
if (ctx->config.track_calls) {
ctx->stats.poll_write_calls++;
}
if (!ctx->is_connected) {
errno = ENOTCONN;
return -1;
}
// Usually can write (unless in write failure mode)
if (ctx->config.mode == MOCK_TRANSPORT_MODE_WRITE_FAIL) {
return 0; // Can't write
}
return 1; // Can write
}
/**
* @brief Mock destroy implementation
*/
static int mock_destroy(esp_transport_handle_t t)
{
mock_http_transport_ctx_t *ctx = esp_transport_get_context_data(t);
if (ctx) {
if (ctx->response_buffer) {
free(ctx->response_buffer);
}
free(ctx);
}
return 0;
}
/* ============================================
* Public API Implementation
* ============================================ */
esp_transport_handle_t mock_http_transport_create(const mock_http_transport_config_t *config)
{
// Create transport handle
esp_transport_handle_t transport = esp_transport_init();
if (!transport) {
ESP_LOGE(TAG, "Failed to create transport handle");
return NULL;
}
// Allocate context
mock_http_transport_ctx_t *ctx = calloc(1, sizeof(mock_http_transport_ctx_t));
if (!ctx) {
ESP_LOGE(TAG, "Failed to allocate mock transport context");
esp_transport_destroy(transport);
return NULL;
}
// Initialize with config or defaults
if (config) {
memcpy(&ctx->config, config, sizeof(mock_http_transport_config_t));
// Copy response data if provided
if (config->response_data) {
size_t len = config->response_len > 0 ? config->response_len : strlen(config->response_data);
ctx->response_buffer = malloc(len);
if (ctx->response_buffer) {
memcpy(ctx->response_buffer, config->response_data, len);
ctx->config.response_len = len;
} else {
ESP_LOGE(TAG, "Failed to allocate response buffer");
free(ctx);
esp_transport_destroy(transport);
return NULL;
}
}
} else {
// Use defaults
ctx->config.mode = MOCK_TRANSPORT_MODE_NORMAL;
ctx->config.bytes_before_error = -1;
ctx->config.track_calls = true;
}
// Set context
esp_transport_set_context_data(transport, ctx);
// Set transport functions
esp_transport_set_func(transport,
mock_connect,
mock_read,
mock_write,
mock_close,
mock_poll_read,
mock_poll_write,
mock_destroy);
ESP_LOGI(TAG, "Mock HTTP transport created (mode=%d)", ctx->config.mode);
return transport;
}
esp_err_t mock_http_transport_destroy(esp_transport_handle_t transport)
{
if (!transport) {
return ESP_ERR_INVALID_ARG;
}
return esp_transport_destroy(transport);
}
esp_err_t mock_http_transport_set_config(esp_transport_handle_t transport,
const mock_http_transport_config_t *config)
{
if (!transport || !config) {
return ESP_ERR_INVALID_ARG;
}
mock_http_transport_ctx_t *ctx = esp_transport_get_context_data(transport);
if (!ctx) {
return ESP_ERR_INVALID_ARG;
}
// Update config
memcpy(&ctx->config, config, sizeof(mock_http_transport_config_t));
// Update response data if provided
if (config->response_data) {
if (ctx->response_buffer) {
free(ctx->response_buffer);
ctx->response_buffer = NULL;
}
size_t len = config->response_len > 0 ? config->response_len : strlen(config->response_data);
ctx->response_buffer = malloc(len);
if (ctx->response_buffer) {
memcpy(ctx->response_buffer, config->response_data, len);
ctx->config.response_len = len;
} else {
ESP_LOGE(TAG, "Failed to allocate response buffer");
return ESP_ERR_NO_MEM;
}
}
// Reset state for new config (but don't change connection state)
// The connection state should be managed through connect/close calls
ctx->read_offset = 0;
ctx->bytes_processed = 0;
ESP_LOGD(TAG, "Mock transport config updated (mode=%d)", ctx->config.mode);
return ESP_OK;
}
esp_err_t mock_http_transport_get_stats(esp_transport_handle_t transport,
mock_http_transport_stats_t *stats)
{
if (!transport || !stats) {
return ESP_ERR_INVALID_ARG;
}
mock_http_transport_ctx_t *ctx = esp_transport_get_context_data(transport);
if (!ctx) {
return ESP_ERR_INVALID_ARG;
}
memcpy(stats, &ctx->stats, sizeof(mock_http_transport_stats_t));
return ESP_OK;
}
esp_err_t mock_http_transport_reset_stats(esp_transport_handle_t transport)
{
if (!transport) {
return ESP_ERR_INVALID_ARG;
}
mock_http_transport_ctx_t *ctx = esp_transport_get_context_data(transport);
if (!ctx) {
return ESP_ERR_INVALID_ARG;
}
memset(&ctx->stats, 0, sizeof(mock_http_transport_stats_t));
ESP_LOGD(TAG, "Mock transport stats reset");
return ESP_OK;
}
esp_err_t mock_http_transport_set_response(esp_transport_handle_t transport,
const char *response_data,
size_t response_len)
{
if (!transport) {
return ESP_ERR_INVALID_ARG;
}
mock_http_transport_ctx_t *ctx = esp_transport_get_context_data(transport);
if (!ctx) {
return ESP_ERR_INVALID_ARG;
}
// Free old buffer
if (ctx->response_buffer) {
free(ctx->response_buffer);
ctx->response_buffer = NULL;
ctx->config.response_len = 0;
}
// Set new response if provided
if (response_data) {
size_t len = response_len > 0 ? response_len : strlen(response_data);
ctx->response_buffer = malloc(len);
if (!ctx->response_buffer) {
ESP_LOGE(TAG, "Failed to allocate response buffer");
return ESP_ERR_NO_MEM;
}
memcpy(ctx->response_buffer, response_data, len);
ctx->config.response_len = len;
}
// Reset read position
ctx->read_offset = 0;
ctx->bytes_processed = 0;
ESP_LOGD(TAG, "Mock transport response updated (%zu bytes)", ctx->config.response_len);
return ESP_OK;
}

View File

@@ -0,0 +1,158 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include "esp_transport.h"
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Mock transport operation modes for testing error conditions
*/
typedef enum {
MOCK_TRANSPORT_MODE_NORMAL, /*!< Normal operation - successful read/write */
MOCK_TRANSPORT_MODE_CONNECT_FAIL, /*!< Connection fails */
MOCK_TRANSPORT_MODE_READ_TIMEOUT, /*!< Read operation times out */
MOCK_TRANSPORT_MODE_WRITE_FAIL, /*!< Write operation fails */
MOCK_TRANSPORT_MODE_INCOMPLETE_READ, /*!< Connection closes mid-read (incomplete data) */
MOCK_TRANSPORT_MODE_WRITE_PARTIAL, /*!< Write only partial data (simulates buffer full) */
} mock_transport_mode_t;
/**
* @brief Configuration for mock HTTP transport
*/
typedef struct {
mock_transport_mode_t mode; /*!< Operation mode for error injection */
const char *response_data; /*!< Canned HTTP response data to return on read */
size_t response_len; /*!< Length of response data (0 = use strlen) */
int bytes_before_error; /*!< Number of bytes to process before injecting error (-1 = no limit) */
int connect_delay_ms; /*!< Delay before connect succeeds (0 = immediate) */
bool track_calls; /*!< Enable call tracking for verification */
} mock_http_transport_config_t;
/**
* @brief Default configuration initializer
*/
#define MOCK_HTTP_TRANSPORT_DEFAULT_CONFIG() { \
.mode = MOCK_TRANSPORT_MODE_NORMAL, \
.response_data = NULL, \
.response_len = 0, \
.bytes_before_error = -1, \
.connect_delay_ms = 0, \
.track_calls = true, \
}
/**
* @brief Statistics tracked by mock transport
*/
typedef struct {
int connect_calls; /*!< Number of connect() calls */
int read_calls; /*!< Number of read() calls */
int write_calls; /*!< Number of write() calls */
int close_calls; /*!< Number of close() calls */
int poll_read_calls; /*!< Number of poll_read() calls */
int poll_write_calls; /*!< Number of poll_write() calls */
size_t total_bytes_written; /*!< Total bytes written */
size_t total_bytes_read; /*!< Total bytes read */
} mock_http_transport_stats_t;
/**
* @brief Create a mock HTTP transport handle
*
* Creates a transport handle that can be used with esp_http_client for testing.
* The transport simulates network behavior according to the configuration.
*
* @param[in] config Configuration for mock behavior (can be NULL for defaults)
*
* @return
* - Mock transport handle on success
* - NULL on error (memory allocation failure)
*
* @note The returned handle must be destroyed with mock_http_transport_destroy()
*/
esp_transport_handle_t mock_http_transport_create(const mock_http_transport_config_t *config);
/**
* @brief Destroy mock HTTP transport
*
* Frees all resources associated with the mock transport.
*
* @param[in] transport Mock transport handle
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_ARG if transport is NULL
*/
esp_err_t mock_http_transport_destroy(esp_transport_handle_t transport);
/**
* @brief Update mock transport configuration at runtime
*
* Allows changing the mock behavior between requests without recreating the transport.
* This is useful for testing client reuse after errors.
*
* @param[in] transport Mock transport handle
* @param[in] config New configuration
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_ARG if transport is NULL or not a mock transport
*/
esp_err_t mock_http_transport_set_config(esp_transport_handle_t transport,
const mock_http_transport_config_t *config);
/**
* @brief Get statistics from mock transport
*
* Retrieves call counts and byte counters for verification in tests.
*
* @param[in] transport Mock transport handle
* @param[out] stats Statistics structure to fill
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_ARG if transport or stats is NULL
*/
esp_err_t mock_http_transport_get_stats(esp_transport_handle_t transport,
mock_http_transport_stats_t *stats);
/**
* @brief Reset statistics counters
*
* Clears all call counters and byte counters. Useful between test iterations.
*
* @param[in] transport Mock transport handle
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_ARG if transport is NULL
*/
esp_err_t mock_http_transport_reset_stats(esp_transport_handle_t transport);
/**
* @brief Set canned response data
*
* Convenience function to update just the response data without changing other config.
*
* @param[in] transport Mock transport handle
* @param[in] response_data HTTP response to return on read (can be NULL to clear)
* @param[in] response_len Length of response (0 = use strlen)
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_ARG if transport is NULL
*/
esp_err_t mock_http_transport_set_response(esp_transport_handle_t transport,
const char *response_data,
size_t response_len);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.qemu
@idf_parametrize('target', ['esp32c3'], indirect=['target'])
def test_http_client_mock(dut: Dut) -> None:
dut.run_all_single_board_cases(group='basic', timeout=120)

View File

@@ -7,3 +7,6 @@ CONFIG_COMPILER_STACK_CHECK_MODE_STRONG=y
CONFIG_COMPILER_STACK_CHECK=y
CONFIG_ESP_TASK_WDT_EN=n
# Enable custom transport for mock testing
CONFIG_ESP_HTTP_CLIENT_ENABLE_CUSTOM_TRANSPORT=y