/** * Copyright (c) 2026 Dominic Masters * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #include "dusktest.h" #include "network/http/networkhttp.h" #include "network/http/networkhttprequest.h" #include "util/memory.h" #include "util/string.h" #include #include #include // ============================================================ // Fake single-connection HTTP server, driven on a background thread_t // ============================================================ typedef struct { int_t listenFd; uint16_t port; thread_t thread; const uint8_t *responseData; size_t responseLength; uint8_t receivedData[8192]; size_t receivedLength; } fakeserver_t; static void fakeServerRun(thread_t *thread) { fakeserver_t *server = (fakeserver_t *)thread->data; struct sockaddr_in clientAddr; socklen_t clientAddrLen = sizeof(clientAddr); const int_t clientFd = accept( server->listenFd, (struct sockaddr *)&clientAddr, &clientAddrLen ); if(clientFd < 0) return; struct timeval timeout; timeout.tv_sec = 0; timeout.tv_usec = 300000; setsockopt(clientFd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); server->receivedLength = 0; for(;;) { if(server->receivedLength >= sizeof(server->receivedData) - 1) break; const ssize_t got = recv( clientFd, server->receivedData + server->receivedLength, sizeof(server->receivedData) - 1 - server->receivedLength, 0 ); if(got <= 0) break; server->receivedLength += (size_t)got; } send(clientFd, server->responseData, server->responseLength, 0); close(clientFd); close(server->listenFd); server->listenFd = -1; } static void fakeServerStart( fakeserver_t *server, const uint8_t *responseData, const size_t responseLength ) { memoryZero(server, sizeof(fakeserver_t)); server->responseData = responseData; server->responseLength = responseLength; server->listenFd = socket(AF_INET, SOCK_STREAM, 0); assert_true(server->listenFd >= 0); struct sockaddr_in addr; memoryZero(&addr, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); addr.sin_port = 0; assert_int_equal( bind(server->listenFd, (struct sockaddr *)&addr, sizeof(addr)), 0 ); socklen_t addrLen = sizeof(addr); assert_int_equal( getsockname(server->listenFd, (struct sockaddr *)&addr, &addrLen), 0 ); server->port = ntohs(addr.sin_port); assert_int_equal(listen(server->listenFd, 1), 0); threadInit(&server->thread, fakeServerRun); server->thread.data = server; threadStart(&server->thread); } static void fakeServerStop(fakeserver_t *server) { threadStop(&server->thread); if(server->listenFd >= 0) close(server->listenFd); } // ============================================================ // networkHttpRequest completion capture // ============================================================ typedef struct { bool_t completed; bool_t errored; uint16_t status; uint8_t body[4096]; size_t bodyLength; } capturedresponse_t; static void captureOnComplete(void *params, void *user) { const networkhttprequest_t *request = (const networkhttprequest_t *)params; capturedresponse_t *captured = (capturedresponse_t *)user; captured->status = request->response.status; captured->bodyLength = request->response.bodyLength; if(request->response.bodyLength > 0) { memoryCopy( captured->body, request->response.body, request->response.bodyLength ); } captured->completed = true; } static void captureOnError(void *params, void *user) { capturedresponse_t *captured = (capturedresponse_t *)user; captured->errored = true; captured->completed = true; } static void waitForCompletion(capturedresponse_t *captured) { for(int32_t i = 0; i < 5000 && !captured->completed; i++) { networkHttpUpdate(); usleep(1000); } assert_true(captured->completed); } // ============================================================ // Per-test setup / teardown // ============================================================ static int http_setup(void **state) { networkHttpInit(); return 0; } static int http_teardown(void **state) { networkHttpDispose(); return 0; } // ============================================================ // Tests // ============================================================ static void test_get_request_headers_and_body(void **state) { const char_t *responseText = "HTTP/1.1 200 OK\r\n" "Content-Type: text/plain\r\n" "Content-Length: 5\r\n" "\r\n" "hello"; fakeserver_t server; fakeServerStart( &server, (const uint8_t *)responseText, strlen(responseText) ); char_t url[128]; stringFormat(url, sizeof(url) - 1, "http://127.0.0.1:%u/foo", server.port); const networkhttpheader_t headers[1] = { { .name = "X-Test", .value = "abc" } }; capturedresponse_t captured; memoryZero(&captured, sizeof(captured)); const errorret_t ret = networkHttpRequest( NETWORK_HTTP_METHOD_GET, url, headers, 1, NULL, 0, NULL, 0, captureOnComplete, captureOnError, &captured ); assert_true(errorIsOk(ret)); waitForCompletion(&captured); fakeServerStop(&server); assert_false(captured.errored); assert_int_equal(captured.status, 200); assert_int_equal(captured.bodyLength, 5); assert_memory_equal(captured.body, "hello", 5); server.receivedData[server.receivedLength] = '\0'; const char_t *received = (const char_t *)server.receivedData; assert_non_null(strstr(received, "GET /foo HTTP/1.1")); assert_non_null(strstr(received, "X-Test: abc")); assert_non_null(strstr(received, "Connection: close")); assert_non_null(strstr(received, "Host: 127.0.0.1")); assert_int_equal(memoryGetAllocatedCount(), 0); } static void test_query_params_are_encoded_and_appended(void **state) { const char_t *responseText = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"; fakeserver_t server; fakeServerStart( &server, (const uint8_t *)responseText, strlen(responseText) ); char_t url[128]; stringFormat( url, sizeof(url) - 1, "http://127.0.0.1:%u/search", server.port ); const networkhttpheader_t query[2] = { { .name = "q", .value = "hello world" }, { .name = "page", .value = "2" } }; capturedresponse_t captured; memoryZero(&captured, sizeof(captured)); const errorret_t ret = networkHttpRequest( NETWORK_HTTP_METHOD_GET, url, NULL, 0, query, 2, NULL, 0, captureOnComplete, captureOnError, &captured ); assert_true(errorIsOk(ret)); waitForCompletion(&captured); fakeServerStop(&server); assert_false(captured.errored); server.receivedData[server.receivedLength] = '\0'; const char_t *received = (const char_t *)server.receivedData; assert_non_null( strstr(received, "GET /search?q=hello%20world&page=2 HTTP/1.1") ); assert_int_equal(memoryGetAllocatedCount(), 0); } static void test_post_sends_body_with_content_length(void **state) { const char_t *responseText = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK"; fakeserver_t server; fakeServerStart( &server, (const uint8_t *)responseText, strlen(responseText) ); char_t url[128]; stringFormat( url, sizeof(url) - 1, "http://127.0.0.1:%u/submit", server.port ); const char_t *body = "{\"a\":1}"; capturedresponse_t captured; memoryZero(&captured, sizeof(captured)); const errorret_t ret = networkHttpRequest( NETWORK_HTTP_METHOD_POST, url, NULL, 0, NULL, 0, (const uint8_t *)body, strlen(body), captureOnComplete, captureOnError, &captured ); assert_true(errorIsOk(ret)); waitForCompletion(&captured); fakeServerStop(&server); assert_false(captured.errored); assert_int_equal(captured.status, 200); assert_memory_equal(captured.body, "OK", 2); server.receivedData[server.receivedLength] = '\0'; const char_t *received = (const char_t *)server.receivedData; assert_non_null(strstr(received, "POST /submit HTTP/1.1")); assert_non_null(strstr(received, "Content-Length: 7")); assert_non_null(strstr(received, "{\"a\":1}")); assert_int_equal(memoryGetAllocatedCount(), 0); } static void test_put_method_is_used(void **state) { const char_t *responseText = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"; fakeserver_t server; fakeServerStart( &server, (const uint8_t *)responseText, strlen(responseText) ); char_t url[128]; stringFormat(url, sizeof(url) - 1, "http://127.0.0.1:%u/item/1", server.port); capturedresponse_t captured; memoryZero(&captured, sizeof(captured)); const errorret_t ret = networkHttpRequest( NETWORK_HTTP_METHOD_PUT, url, NULL, 0, NULL, 0, NULL, 0, captureOnComplete, captureOnError, &captured ); assert_true(errorIsOk(ret)); waitForCompletion(&captured); fakeServerStop(&server); assert_false(captured.errored); server.receivedData[server.receivedLength] = '\0'; assert_non_null( strstr((const char_t *)server.receivedData, "PUT /item/1 HTTP/1.1") ); assert_int_equal(memoryGetAllocatedCount(), 0); } static void test_301_redirect_is_followed_automatically(void **state) { const char_t *finalResponseText = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK"; fakeserver_t target; fakeServerStart( &target, (const uint8_t *)finalResponseText, strlen(finalResponseText) ); char_t targetUrl[128]; stringFormat( targetUrl, sizeof(targetUrl) - 1, "http://127.0.0.1:%u/target", target.port ); char_t redirectResponseText[256]; stringFormat( redirectResponseText, sizeof(redirectResponseText) - 1, "HTTP/1.1 301 Moved Permanently\r\n" "Location: %s\r\n" "Content-Length: 0\r\n" "\r\n", targetUrl ); fakeserver_t initial; fakeServerStart( &initial, (const uint8_t *)redirectResponseText, strlen(redirectResponseText) ); char_t url[128]; stringFormat(url, sizeof(url) - 1, "http://127.0.0.1:%u/old", initial.port); capturedresponse_t captured; memoryZero(&captured, sizeof(captured)); const errorret_t ret = networkHttpRequest( NETWORK_HTTP_METHOD_GET, url, NULL, 0, NULL, 0, NULL, 0, captureOnComplete, captureOnError, &captured ); assert_true(errorIsOk(ret)); waitForCompletion(&captured); fakeServerStop(&initial); fakeServerStop(&target); assert_false(captured.errored); assert_int_equal(captured.status, 200); assert_memory_equal(captured.body, "OK", 2); assert_int_equal(memoryGetAllocatedCount(), 0); } static void test_connection_refused_triggers_onError(void **state) { const int_t fd = socket(AF_INET, SOCK_STREAM, 0); assert_true(fd >= 0); struct sockaddr_in addr; memoryZero(&addr, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); addr.sin_port = 0; assert_int_equal(bind(fd, (struct sockaddr *)&addr, sizeof(addr)), 0); socklen_t addrLen = sizeof(addr); assert_int_equal(getsockname(fd, (struct sockaddr *)&addr, &addrLen), 0); const uint16_t port = ntohs(addr.sin_port); close(fd); // Nothing is listening on this port now. char_t url[64]; stringFormat(url, sizeof(url) - 1, "http://127.0.0.1:%u/", port); capturedresponse_t captured; memoryZero(&captured, sizeof(captured)); const errorret_t ret = networkHttpRequest( NETWORK_HTTP_METHOD_GET, url, NULL, 0, NULL, 0, NULL, 0, captureOnComplete, captureOnError, &captured ); assert_true(errorIsOk(ret)); waitForCompletion(&captured); assert_true(captured.errored); assert_int_equal(memoryGetAllocatedCount(), 0); } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test_setup_teardown( test_get_request_headers_and_body, http_setup, http_teardown ), cmocka_unit_test_setup_teardown( test_query_params_are_encoded_and_appended, http_setup, http_teardown ), cmocka_unit_test_setup_teardown( test_post_sends_body_with_content_length, http_setup, http_teardown ), cmocka_unit_test_setup_teardown( test_put_method_is_used, http_setup, http_teardown ), cmocka_unit_test_setup_teardown( test_301_redirect_is_followed_automatically, http_setup, http_teardown ), cmocka_unit_test_setup_teardown( test_connection_refused_triggers_onError, http_setup, http_teardown ), }; return cmocka_run_group_tests(tests, NULL, NULL); }