Files
dusk/test/network/http/test_networkhttp.c
T
2026-07-16 19:08:32 -05:00

451 lines
12 KiB
C

/**
* 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 <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
// ============================================================
// 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);
}