File
Blob: firmware/platform/tests/http/http_test.c
| 1 | #include <assert.h> |
| 2 | #include <stdint.h> |
| 3 | #include <stdio.h> |
| 4 | #include <stdlib.h> |
| 5 | #include <string.h> |
| 6 | #include "esp_http_client.h" |
| 7 | #include "esp_heap_caps.h" |
| 8 | #include "radio_bridge.h" |
| 9 | |
| 10 | #define RESPONSE_LIMIT 24576 |
| 11 | |
| 12 | struct fake_http_client { |
| 13 | const esp_http_client_config_t *config; |
| 14 | const char *body; |
| 15 | int body_length; |
| 16 | const char *content_type; |
| 17 | }; |
| 18 | |
| 19 | typedef struct { |
| 20 | const uint8_t *bytes; |
| 21 | size_t length; |
| 22 | } chunk; |
| 23 | |
| 24 | static struct { |
| 25 | chunk chunks[4]; |
| 26 | size_t count; |
| 27 | int status; |
| 28 | int perform_result; |
| 29 | int set_url_result; |
| 30 | int set_body_result; |
| 31 | int release_body_result; |
| 32 | int performs; |
| 33 | int releases; |
| 34 | int null_clears; |
| 35 | int form_defaults; |
| 36 | int closes; |
| 37 | int cleanups; |
| 38 | bool online; |
| 39 | void *response_allocation; |
| 40 | esp_http_client_handle_t client; |
| 41 | } fake; |
| 42 | |
| 43 | static const uint8_t request[] = "{}"; |
| 44 | |
| 45 | void *heap_caps_malloc(size_t length, uint32_t capabilities) |
| 46 | { |
| 47 | assert(length == RESPONSE_LIMIT); |
| 48 | assert(capabilities == (MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); |
| 49 | fake.response_allocation = malloc(length); |
| 50 | return fake.response_allocation; |
| 51 | } |
| 52 | |
| 53 | esp_err_t esp_crt_bundle_attach(void *context) |
| 54 | { |
| 55 | (void)context; |
| 56 | return ESP_OK; |
| 57 | } |
| 58 | |
| 59 | int32_t radio_network_ready(void) |
| 60 | { |
| 61 | return fake.online; |
| 62 | } |
| 63 | |
| 64 | esp_http_client_handle_t esp_http_client_init(const esp_http_client_config_t *config) |
| 65 | { |
| 66 | assert(config->disable_auto_redirect); |
| 67 | assert(config->crt_bundle_attach); |
| 68 | esp_http_client_handle_t client = calloc(1, sizeof(*client)); |
| 69 | assert(client); |
| 70 | client->config = config; |
| 71 | fake.client = client; |
| 72 | return client; |
| 73 | } |
| 74 | |
| 75 | esp_err_t esp_http_client_set_method(esp_http_client_handle_t client, int method) |
| 76 | { |
| 77 | assert(client && method == HTTP_METHOD_POST); |
| 78 | return ESP_OK; |
| 79 | } |
| 80 | |
| 81 | esp_err_t esp_http_client_set_header(esp_http_client_handle_t client, const char *name, |
| 82 | const char *value) |
| 83 | { |
| 84 | assert(client && name); |
| 85 | if (strcmp(name, "Content-Type") == 0) |
| 86 | client->content_type = value; |
| 87 | else |
| 88 | assert(value); |
| 89 | return ESP_OK; |
| 90 | } |
| 91 | |
| 92 | esp_err_t esp_http_client_set_url(esp_http_client_handle_t client, const char *url) |
| 93 | { |
| 94 | assert(client && strstr(url, "/api/device/")); |
| 95 | return fake.set_url_result; |
| 96 | } |
| 97 | |
| 98 | esp_err_t esp_http_client_set_post_field(esp_http_client_handle_t client, const char *body, |
| 99 | int length) |
| 100 | { |
| 101 | /* Match ESP-IDF: the pointer is stored before header work can fail. NULL |
| 102 | * also deletes Content-Type; a later body installs the form default. */ |
| 103 | client->body = body; |
| 104 | client->body_length = length; |
| 105 | if (!body) { |
| 106 | assert(length == 0); |
| 107 | client->content_type = NULL; |
| 108 | fake.null_clears++; |
| 109 | return ESP_OK; |
| 110 | } |
| 111 | if (!client->content_type) { |
| 112 | client->content_type = "application/x-www-form-urlencoded"; |
| 113 | fake.form_defaults++; |
| 114 | } |
| 115 | if (length == 0) { |
| 116 | fake.releases++; |
| 117 | return fake.release_body_result; |
| 118 | } |
| 119 | if (fake.set_body_result) |
| 120 | return fake.set_body_result; |
| 121 | return ESP_OK; |
| 122 | } |
| 123 | |
| 124 | esp_err_t esp_http_client_perform(esp_http_client_handle_t client) |
| 125 | { |
| 126 | assert(client->body == (const char *)request); |
| 127 | assert(client->body_length == sizeof(request) - 1); |
| 128 | assert(strcmp(client->content_type, "application/json") == 0); |
| 129 | fake.performs++; |
| 130 | for (size_t i = 0; i < fake.count; i++) { |
| 131 | esp_http_client_event_t event = {.user_data = client->config->user_data, |
| 132 | .event_id = HTTP_EVENT_ON_DATA, |
| 133 | .data_len = (int)fake.chunks[i].length, |
| 134 | .data = (void *)fake.chunks[i].bytes}; |
| 135 | assert(client->config->event_handler(&event) == ESP_OK); |
| 136 | } |
| 137 | return fake.perform_result; |
| 138 | } |
| 139 | |
| 140 | int esp_http_client_get_status_code(esp_http_client_handle_t client) |
| 141 | { |
| 142 | assert(client); |
| 143 | return fake.status; |
| 144 | } |
| 145 | |
| 146 | esp_err_t esp_http_client_close(esp_http_client_handle_t client) |
| 147 | { |
| 148 | assert(client->body); |
| 149 | assert(client->body != (const char *)request); |
| 150 | assert(client->body_length == 0); |
| 151 | assert(strcmp(client->content_type, "application/json") == 0); |
| 152 | fake.closes++; |
| 153 | return ESP_OK; |
| 154 | } |
| 155 | |
| 156 | esp_err_t esp_http_client_cleanup(esp_http_client_handle_t client) |
| 157 | { |
| 158 | assert(client->body); |
| 159 | assert(client->body != (const char *)request); |
| 160 | assert(client->body_length == 0); |
| 161 | assert(strcmp(client->content_type, "application/json") == 0); |
| 162 | /* The retained configuration remains valid until cleanup completes. */ |
| 163 | assert(client->config->user_data); |
| 164 | fake.cleanups++; |
| 165 | free(client); |
| 166 | return ESP_OK; |
| 167 | } |
| 168 | |
| 169 | static void expect_failure(radio_http *http, const char *path, size_t request_length) |
| 170 | { |
| 171 | uint8_t *large_request = NULL; |
| 172 | if (request_length > sizeof(request) - 1) { |
| 173 | large_request = calloc(request_length, 1); |
| 174 | assert(large_request); |
| 175 | } |
| 176 | const uint8_t *response = (const uint8_t *)(uintptr_t)1; |
| 177 | size_t used = SIZE_MAX; |
| 178 | assert(radio_http_post(http, path, large_request ? large_request : request, request_length, |
| 179 | &response, &used) == -1); |
| 180 | assert(response == NULL && used == 0); |
| 181 | assert(fake.client->body); |
| 182 | assert(fake.client->body != (const char *)request); |
| 183 | assert(fake.client->body_length == 0); |
| 184 | assert(strcmp(fake.client->content_type, "application/json") == 0); |
| 185 | free(large_request); |
| 186 | } |
| 187 | |
| 188 | static void expect_response(radio_http *http, int status, const uint8_t *bytes, size_t length) |
| 189 | { |
| 190 | const uint8_t *response = NULL; |
| 191 | size_t used = SIZE_MAX; |
| 192 | int releases_before = fake.releases; |
| 193 | assert(radio_http_post(http, "heartbeat", request, sizeof(request) - 1, &response, &used) == |
| 194 | status); |
| 195 | assert(fake.releases == releases_before + 1); |
| 196 | assert(fake.client->body); |
| 197 | assert(fake.client->body != (const char *)request); |
| 198 | assert(fake.client->body_length == 0); |
| 199 | assert(strcmp(fake.client->content_type, "application/json") == 0); |
| 200 | /* A successful response borrows the original allocation, without a copy. */ |
| 201 | assert(response == fake.response_allocation && used == length); |
| 202 | if (length) |
| 203 | assert(memcmp(response, bytes, length) == 0); |
| 204 | } |
| 205 | |
| 206 | int main(void) |
| 207 | { |
| 208 | fake.online = true; |
| 209 | fake.status = 200; |
| 210 | radio_http *http = radio_http_open(); |
| 211 | assert(http); |
| 212 | |
| 213 | static const uint8_t first[] = "{\"ok\":true}"; |
| 214 | fake.chunks[0] = (chunk){first, 3}; |
| 215 | fake.chunks[1] = (chunk){first + 3, sizeof(first) - 4}; |
| 216 | fake.count = 2; |
| 217 | expect_response(http, 200, first, sizeof(first) - 1); |
| 218 | |
| 219 | /* A shorter next response must not expose the previous response's tail. */ |
| 220 | fake.chunks[0] = (chunk){request, sizeof(request) - 1}; |
| 221 | fake.count = 1; |
| 222 | expect_response(http, 200, request, sizeof(request) - 1); |
| 223 | fake.count = 0; |
| 224 | fake.status = 204; |
| 225 | expect_response(http, 204, NULL, 0); |
| 226 | |
| 227 | /* Error-status bodies are still complete HTTP responses for retry policy. */ |
| 228 | fake.status = 503; |
| 229 | fake.count = 1; |
| 230 | expect_response(http, 503, request, sizeof(request) - 1); |
| 231 | fake.status = 200; |
| 232 | |
| 233 | uint8_t *large = malloc(RESPONSE_LIMIT + 1); |
| 234 | assert(large); |
| 235 | memset(large, 'x', RESPONSE_LIMIT + 1); |
| 236 | fake.chunks[0] = (chunk){large, RESPONSE_LIMIT}; |
| 237 | expect_response(http, 200, large, RESPONSE_LIMIT); |
| 238 | |
| 239 | /* Overflow latches across chunks, including later chunks that would fit. */ |
| 240 | fake.chunks[0] = (chunk){large, RESPONSE_LIMIT - 1}; |
| 241 | fake.chunks[1] = (chunk){large, 2}; |
| 242 | fake.chunks[2] = (chunk){large, 1}; |
| 243 | fake.count = 3; |
| 244 | int closed_before = fake.closes; |
| 245 | expect_failure(http, "heartbeat", sizeof(request) - 1); |
| 246 | assert(fake.closes == closed_before + 1); |
| 247 | fake.chunks[0] = (chunk){large, RESPONSE_LIMIT + 1}; |
| 248 | fake.count = 1; |
| 249 | expect_failure(http, "heartbeat", sizeof(request) - 1); |
| 250 | |
| 251 | /* A partial transport failure never lends its partial response. */ |
| 252 | fake.chunks[0] = (chunk){first, sizeof(first) - 1}; |
| 253 | fake.perform_result = -1; |
| 254 | expect_failure(http, "heartbeat", sizeof(request) - 1); |
| 255 | fake.perform_result = 0; |
| 256 | expect_response(http, 200, first, sizeof(first) - 1); |
| 257 | |
| 258 | fake.set_url_result = -1; |
| 259 | expect_failure(http, "heartbeat", sizeof(request) - 1); |
| 260 | fake.set_url_result = 0; |
| 261 | fake.set_body_result = -1; |
| 262 | expect_failure(http, "heartbeat", sizeof(request) - 1); |
| 263 | fake.set_body_result = 0; |
| 264 | |
| 265 | /* A failed release still replaces the borrowed Rust pointer before return. */ |
| 266 | fake.release_body_result = -1; |
| 267 | expect_failure(http, "heartbeat", sizeof(request) - 1); |
| 268 | fake.release_body_result = 0; |
| 269 | expect_response(http, 200, first, sizeof(first) - 1); |
| 270 | |
| 271 | int performed_before = fake.performs; |
| 272 | fake.online = false; |
| 273 | expect_failure(http, "heartbeat", sizeof(request) - 1); |
| 274 | fake.online = true; |
| 275 | expect_failure(http, "heartbeat", 20001); |
| 276 | char long_path[300]; |
| 277 | memset(long_path, 'a', sizeof(long_path) - 1); |
| 278 | long_path[sizeof(long_path) - 1] = '\0'; |
| 279 | expect_failure(http, long_path, sizeof(request) - 1); |
| 280 | assert(fake.performs == performed_before); |
| 281 | expect_response(http, 200, first, sizeof(first) - 1); |
| 282 | assert(fake.null_clears == 0); |
| 283 | assert(fake.form_defaults == 0); |
| 284 | |
| 285 | radio_http_free(http); |
| 286 | assert(fake.cleanups == 1); |
| 287 | radio_http_free(NULL); |
| 288 | free(large); |
| 289 | puts("HTTP response borrowing, bounds, retry state and cleanup passed"); |
| 290 | return 0; |
| 291 | } |