451 lines
12 KiB
C
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);
|
|
}
|