Net
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# Copyright (c) 2026 Dominic Masters
|
||||
#
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
include(dusktest)
|
||||
|
||||
dusktest(test_networkhttp.c)
|
||||
@@ -0,0 +1,450 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
Reference in New Issue
Block a user