chore: add .gitignore entries for build artifacts and test files, restructure Makefile with proper build system
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
#include "test_framework.h"
|
||||
#include "../src/types.h"
|
||||
#include "../src/buffer.h"
|
||||
|
||||
void test_buffer_init(void) {
|
||||
TEST_SUITE_BEGIN("Buffer Initialization");
|
||||
|
||||
buffer_t buf;
|
||||
int result = buffer_init(&buf, 1024);
|
||||
|
||||
TEST_ASSERT_EQ(0, result, "Buffer init returns 0");
|
||||
TEST_ASSERT(buf.data != NULL, "Buffer data is not NULL");
|
||||
TEST_ASSERT_EQ(1024, buf.capacity, "Buffer capacity is 1024");
|
||||
TEST_ASSERT_EQ(0, buf.head, "Buffer head is 0");
|
||||
TEST_ASSERT_EQ(0, buf.tail, "Buffer tail is 0");
|
||||
|
||||
buffer_free(&buf);
|
||||
TEST_ASSERT(buf.data == NULL, "Buffer data is NULL after free");
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_buffer_read_write(void) {
|
||||
TEST_SUITE_BEGIN("Buffer Read/Write Operations");
|
||||
|
||||
buffer_t buf;
|
||||
buffer_init(&buf, 1024);
|
||||
|
||||
TEST_ASSERT_EQ(1024, buffer_available_write(&buf), "Initial write capacity is 1024");
|
||||
TEST_ASSERT_EQ(0, buffer_available_read(&buf), "Initial read capacity is 0");
|
||||
|
||||
const char *test_data = "Hello, World!";
|
||||
size_t len = strlen(test_data);
|
||||
memcpy(buf.data + buf.tail, test_data, len);
|
||||
buf.tail += len;
|
||||
|
||||
TEST_ASSERT_EQ(len, buffer_available_read(&buf), "Read capacity equals written data");
|
||||
TEST_ASSERT_EQ(1024 - len, buffer_available_write(&buf), "Write capacity reduced");
|
||||
|
||||
buffer_free(&buf);
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_buffer_consume(void) {
|
||||
TEST_SUITE_BEGIN("Buffer Consume Operations");
|
||||
|
||||
buffer_t buf;
|
||||
buffer_init(&buf, 1024);
|
||||
|
||||
const char *test_data = "0123456789";
|
||||
size_t len = strlen(test_data);
|
||||
memcpy(buf.data + buf.tail, test_data, len);
|
||||
buf.tail += len;
|
||||
|
||||
buffer_consume(&buf, 5);
|
||||
TEST_ASSERT_EQ(5, buffer_available_read(&buf), "5 bytes remaining after consume");
|
||||
TEST_ASSERT(memcmp(buf.data + buf.head, "56789", 5) == 0, "Correct data after consume");
|
||||
|
||||
buffer_consume(&buf, 5);
|
||||
TEST_ASSERT_EQ(0, buffer_available_read(&buf), "Buffer empty after full consume");
|
||||
TEST_ASSERT_EQ(0, buf.head, "Head reset to 0");
|
||||
TEST_ASSERT_EQ(0, buf.tail, "Tail reset to 0");
|
||||
|
||||
buffer_free(&buf);
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_buffer_compact(void) {
|
||||
TEST_SUITE_BEGIN("Buffer Compact Operations");
|
||||
|
||||
buffer_t buf;
|
||||
buffer_init(&buf, 1024);
|
||||
|
||||
const char *test_data = "ABCDEFGHIJ";
|
||||
size_t len = strlen(test_data);
|
||||
memcpy(buf.data + buf.tail, test_data, len);
|
||||
buf.tail += len;
|
||||
|
||||
buffer_consume(&buf, 5);
|
||||
TEST_ASSERT_EQ(5, buf.head, "Head moved to 5");
|
||||
|
||||
buffer_compact(&buf);
|
||||
TEST_ASSERT_EQ(0, buf.head, "Head is 0 after compact");
|
||||
TEST_ASSERT_EQ(5, buf.tail, "Tail is 5 after compact");
|
||||
TEST_ASSERT(memcmp(buf.data, "FGHIJ", 5) == 0, "Data moved to beginning");
|
||||
|
||||
buffer_free(&buf);
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_buffer_ensure_capacity(void) {
|
||||
TEST_SUITE_BEGIN("Buffer Ensure Capacity");
|
||||
|
||||
buffer_t buf;
|
||||
buffer_init(&buf, 64);
|
||||
|
||||
int result = buffer_ensure_capacity(&buf, 128);
|
||||
TEST_ASSERT_EQ(0, result, "Capacity increase successful");
|
||||
TEST_ASSERT(buf.capacity >= 128, "Capacity at least 128");
|
||||
|
||||
result = buffer_ensure_capacity(&buf, 512);
|
||||
TEST_ASSERT_EQ(0, result, "Second capacity increase successful");
|
||||
TEST_ASSERT(buf.capacity >= 512, "Capacity at least 512");
|
||||
|
||||
result = buffer_ensure_capacity(&buf, 64);
|
||||
TEST_ASSERT_EQ(0, result, "No change when capacity already sufficient");
|
||||
|
||||
buffer_free(&buf);
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_buffer_multiple_operations(void) {
|
||||
TEST_SUITE_BEGIN("Buffer Multiple Operations");
|
||||
|
||||
buffer_t buf;
|
||||
buffer_init(&buf, 128);
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
char data[10];
|
||||
snprintf(data, sizeof(data), "MSG%d", i);
|
||||
size_t len = strlen(data);
|
||||
|
||||
if (buffer_available_write(&buf) < len) {
|
||||
buffer_compact(&buf);
|
||||
}
|
||||
|
||||
memcpy(buf.data + buf.tail, data, len);
|
||||
buf.tail += len;
|
||||
}
|
||||
|
||||
TEST_ASSERT(buffer_available_read(&buf) > 0, "Buffer has data after multiple writes");
|
||||
|
||||
size_t total_read = 0;
|
||||
while (buffer_available_read(&buf) > 0) {
|
||||
size_t to_consume = buffer_available_read(&buf) > 5 ? 5 : buffer_available_read(&buf);
|
||||
buffer_consume(&buf, to_consume);
|
||||
total_read += to_consume;
|
||||
}
|
||||
|
||||
TEST_ASSERT(total_read > 0, "All data was consumed");
|
||||
TEST_ASSERT_EQ(0, buffer_available_read(&buf), "Buffer is empty");
|
||||
|
||||
buffer_free(&buf);
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void run_buffer_tests(void) {
|
||||
test_buffer_init();
|
||||
test_buffer_read_write();
|
||||
test_buffer_consume();
|
||||
test_buffer_compact();
|
||||
test_buffer_ensure_capacity();
|
||||
test_buffer_multiple_operations();
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
#include "test_framework.h"
|
||||
#include "../src/types.h"
|
||||
#include "../src/config.h"
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static const char *TEST_CONFIG_FILE = "/tmp/test_proxy_config.json";
|
||||
|
||||
static void create_test_config(const char *content) {
|
||||
FILE *f = fopen(TEST_CONFIG_FILE, "w");
|
||||
if (f) {
|
||||
fprintf(f, "%s", content);
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
static void cleanup_test_config(void) {
|
||||
unlink(TEST_CONFIG_FILE);
|
||||
}
|
||||
|
||||
void test_config_load_valid(void) {
|
||||
TEST_SUITE_BEGIN("Config Load Valid Configuration");
|
||||
|
||||
const char *valid_config =
|
||||
"{\n"
|
||||
" \"port\": 9090,\n"
|
||||
" \"reverse_proxy\": [\n"
|
||||
" {\n"
|
||||
" \"hostname\": \"test.example.com\",\n"
|
||||
" \"upstream_host\": \"127.0.0.1\",\n"
|
||||
" \"upstream_port\": 3000,\n"
|
||||
" \"use_ssl\": false,\n"
|
||||
" \"rewrite_host\": true\n"
|
||||
" },\n"
|
||||
" {\n"
|
||||
" \"hostname\": \"api.example.com\",\n"
|
||||
" \"upstream_host\": \"192.168.1.100\",\n"
|
||||
" \"upstream_port\": 443,\n"
|
||||
" \"use_ssl\": true,\n"
|
||||
" \"rewrite_host\": false\n"
|
||||
" }\n"
|
||||
" ]\n"
|
||||
"}\n";
|
||||
|
||||
create_test_config(valid_config);
|
||||
|
||||
int result = config_load(TEST_CONFIG_FILE);
|
||||
TEST_ASSERT_EQ(1, result, "Config loaded successfully");
|
||||
TEST_ASSERT_EQ(9090, config.port, "Port is 9090");
|
||||
TEST_ASSERT_EQ(2, config.route_count, "Two routes configured");
|
||||
|
||||
route_config_t *route1 = config_find_route("test.example.com");
|
||||
TEST_ASSERT(route1 != NULL, "Route for test.example.com found");
|
||||
if (route1) {
|
||||
TEST_ASSERT_STR_EQ("127.0.0.1", route1->upstream_host, "First route upstream host");
|
||||
TEST_ASSERT_EQ(3000, route1->upstream_port, "First route upstream port");
|
||||
TEST_ASSERT_EQ(0, route1->use_ssl, "First route SSL disabled");
|
||||
TEST_ASSERT_EQ(1, route1->rewrite_host, "First route host rewrite enabled");
|
||||
}
|
||||
|
||||
route_config_t *route2 = config_find_route("api.example.com");
|
||||
TEST_ASSERT(route2 != NULL, "Route for api.example.com found");
|
||||
if (route2) {
|
||||
TEST_ASSERT_STR_EQ("192.168.1.100", route2->upstream_host, "Second route upstream host");
|
||||
TEST_ASSERT_EQ(443, route2->upstream_port, "Second route upstream port");
|
||||
TEST_ASSERT_EQ(1, route2->use_ssl, "Second route SSL enabled");
|
||||
TEST_ASSERT_EQ(0, route2->rewrite_host, "Second route host rewrite disabled");
|
||||
}
|
||||
|
||||
config_free();
|
||||
cleanup_test_config();
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_config_find_route_case_insensitive(void) {
|
||||
TEST_SUITE_BEGIN("Config Find Route Case Insensitive");
|
||||
|
||||
const char *config_content =
|
||||
"{\n"
|
||||
" \"port\": 8080,\n"
|
||||
" \"reverse_proxy\": [\n"
|
||||
" {\n"
|
||||
" \"hostname\": \"Test.Example.COM\",\n"
|
||||
" \"upstream_host\": \"localhost\",\n"
|
||||
" \"upstream_port\": 3000,\n"
|
||||
" \"use_ssl\": false,\n"
|
||||
" \"rewrite_host\": false\n"
|
||||
" }\n"
|
||||
" ]\n"
|
||||
"}\n";
|
||||
|
||||
create_test_config(config_content);
|
||||
config_load(TEST_CONFIG_FILE);
|
||||
|
||||
route_config_t *route1 = config_find_route("test.example.com");
|
||||
TEST_ASSERT(route1 != NULL, "Lowercase hostname matches");
|
||||
|
||||
route_config_t *route2 = config_find_route("TEST.EXAMPLE.COM");
|
||||
TEST_ASSERT(route2 != NULL, "Uppercase hostname matches");
|
||||
|
||||
route_config_t *route3 = config_find_route("TeSt.ExAmPlE.cOm");
|
||||
TEST_ASSERT(route3 != NULL, "Mixed case hostname matches");
|
||||
|
||||
config_free();
|
||||
cleanup_test_config();
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_config_find_route_nonexistent(void) {
|
||||
TEST_SUITE_BEGIN("Config Find Nonexistent Route");
|
||||
|
||||
const char *config_content =
|
||||
"{\n"
|
||||
" \"port\": 8080,\n"
|
||||
" \"reverse_proxy\": [\n"
|
||||
" {\n"
|
||||
" \"hostname\": \"existing.com\",\n"
|
||||
" \"upstream_host\": \"localhost\",\n"
|
||||
" \"upstream_port\": 3000,\n"
|
||||
" \"use_ssl\": false,\n"
|
||||
" \"rewrite_host\": false\n"
|
||||
" }\n"
|
||||
" ]\n"
|
||||
"}\n";
|
||||
|
||||
create_test_config(config_content);
|
||||
config_load(TEST_CONFIG_FILE);
|
||||
|
||||
route_config_t *route = config_find_route("nonexistent.com");
|
||||
TEST_ASSERT(route == NULL, "Nonexistent route returns NULL");
|
||||
|
||||
route = config_find_route(NULL);
|
||||
TEST_ASSERT(route == NULL, "NULL hostname returns NULL");
|
||||
|
||||
config_free();
|
||||
cleanup_test_config();
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_config_default_port(void) {
|
||||
TEST_SUITE_BEGIN("Config Default Port");
|
||||
|
||||
const char *config_content =
|
||||
"{\n"
|
||||
" \"reverse_proxy\": [\n"
|
||||
" {\n"
|
||||
" \"hostname\": \"test.com\",\n"
|
||||
" \"upstream_host\": \"localhost\",\n"
|
||||
" \"upstream_port\": 3000,\n"
|
||||
" \"use_ssl\": false,\n"
|
||||
" \"rewrite_host\": false\n"
|
||||
" }\n"
|
||||
" ]\n"
|
||||
"}\n";
|
||||
|
||||
create_test_config(config_content);
|
||||
config_load(TEST_CONFIG_FILE);
|
||||
|
||||
TEST_ASSERT_EQ(8080, config.port, "Default port is 8080 when not specified");
|
||||
|
||||
config_free();
|
||||
cleanup_test_config();
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_config_invalid_json(void) {
|
||||
TEST_SUITE_BEGIN("Config Invalid JSON");
|
||||
|
||||
const char *invalid_config = "{ invalid json }";
|
||||
|
||||
create_test_config(invalid_config);
|
||||
|
||||
int result = config_load(TEST_CONFIG_FILE);
|
||||
TEST_ASSERT_EQ(0, result, "Invalid JSON returns 0");
|
||||
|
||||
cleanup_test_config();
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_config_missing_file(void) {
|
||||
TEST_SUITE_BEGIN("Config Missing File");
|
||||
|
||||
unlink("/tmp/nonexistent_config.json");
|
||||
int result = config_load("/tmp/nonexistent_config.json");
|
||||
TEST_ASSERT_EQ(0, result, "Missing file returns 0");
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_config_ssl_rewrite_host_options(void) {
|
||||
TEST_SUITE_BEGIN("Config SSL and Rewrite Host Options");
|
||||
|
||||
const char *config_content =
|
||||
"{\n"
|
||||
" \"port\": 8080,\n"
|
||||
" \"reverse_proxy\": [\n"
|
||||
" {\n"
|
||||
" \"hostname\": \"https-rewrite.com\",\n"
|
||||
" \"upstream_host\": \"secure.example.com\",\n"
|
||||
" \"upstream_port\": 443,\n"
|
||||
" \"use_ssl\": true,\n"
|
||||
" \"rewrite_host\": true\n"
|
||||
" },\n"
|
||||
" {\n"
|
||||
" \"hostname\": \"http-norewrite.com\",\n"
|
||||
" \"upstream_host\": \"plain.example.com\",\n"
|
||||
" \"upstream_port\": 80,\n"
|
||||
" \"use_ssl\": false,\n"
|
||||
" \"rewrite_host\": false\n"
|
||||
" }\n"
|
||||
" ]\n"
|
||||
"}\n";
|
||||
|
||||
create_test_config(config_content);
|
||||
config_load(TEST_CONFIG_FILE);
|
||||
|
||||
route_config_t *ssl_route = config_find_route("https-rewrite.com");
|
||||
TEST_ASSERT(ssl_route != NULL, "SSL route found");
|
||||
if (ssl_route) {
|
||||
TEST_ASSERT_EQ(1, ssl_route->use_ssl, "SSL enabled for https route");
|
||||
TEST_ASSERT_EQ(1, ssl_route->rewrite_host, "Host rewrite enabled for https route");
|
||||
}
|
||||
|
||||
route_config_t *plain_route = config_find_route("http-norewrite.com");
|
||||
TEST_ASSERT(plain_route != NULL, "Plain route found");
|
||||
if (plain_route) {
|
||||
TEST_ASSERT_EQ(0, plain_route->use_ssl, "SSL disabled for http route");
|
||||
TEST_ASSERT_EQ(0, plain_route->rewrite_host, "Host rewrite disabled for http route");
|
||||
}
|
||||
|
||||
config_free();
|
||||
cleanup_test_config();
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void run_config_tests(void) {
|
||||
test_config_load_valid();
|
||||
test_config_find_route_case_insensitive();
|
||||
test_config_find_route_nonexistent();
|
||||
test_config_default_port();
|
||||
test_config_invalid_json();
|
||||
test_config_missing_file();
|
||||
test_config_ssl_rewrite_host_options();
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
#ifndef TEST_FRAMEWORK_H
|
||||
#define TEST_FRAMEWORK_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
extern int tests_run;
|
||||
extern int tests_passed;
|
||||
extern int tests_failed;
|
||||
|
||||
#define TEST_ASSERT(condition, message) do { \
|
||||
tests_run++; \
|
||||
if (condition) { \
|
||||
tests_passed++; \
|
||||
printf(" [PASS] %s\n", message); \
|
||||
} else { \
|
||||
tests_failed++; \
|
||||
printf(" [FAIL] %s (line %d)\n", message, __LINE__); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
#define TEST_ASSERT_EQ(expected, actual, message) do { \
|
||||
tests_run++; \
|
||||
if ((expected) == (actual)) { \
|
||||
tests_passed++; \
|
||||
printf(" [PASS] %s\n", message); \
|
||||
} else { \
|
||||
tests_failed++; \
|
||||
printf(" [FAIL] %s (expected %d, got %d, line %d)\n", message, (int)(expected), (int)(actual), __LINE__); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
#define TEST_ASSERT_STR_EQ(expected, actual, message) do { \
|
||||
tests_run++; \
|
||||
if (strcmp(expected, actual) == 0) { \
|
||||
tests_passed++; \
|
||||
printf(" [PASS] %s\n", message); \
|
||||
} else { \
|
||||
tests_failed++; \
|
||||
printf(" [FAIL] %s (expected '%s', got '%s', line %d)\n", message, expected, actual, __LINE__); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
#define TEST_SUITE_BEGIN(name) do { \
|
||||
printf("\n=== Test Suite: %s ===\n", name); \
|
||||
} while(0)
|
||||
|
||||
#define TEST_SUITE_END() do { \
|
||||
printf("\n"); \
|
||||
} while(0)
|
||||
|
||||
void test_summary(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,196 @@
|
||||
#include "test_framework.h"
|
||||
#include "../src/types.h"
|
||||
#include "../src/http.h"
|
||||
|
||||
void test_http_parse_get_request(void) {
|
||||
TEST_SUITE_BEGIN("HTTP GET Request Parsing");
|
||||
|
||||
http_request_t req;
|
||||
const char *request = "GET /path/to/resource HTTP/1.1\r\nHost: example.com\r\nConnection: keep-alive\r\n\r\n";
|
||||
int result = http_parse_request(request, strlen(request), &req);
|
||||
|
||||
TEST_ASSERT_EQ(1, result, "Parse GET request");
|
||||
TEST_ASSERT_STR_EQ("GET", req.method, "Method is GET");
|
||||
TEST_ASSERT_STR_EQ("/path/to/resource", req.uri, "URI parsed correctly");
|
||||
TEST_ASSERT_STR_EQ("HTTP/1.1", req.version, "Version is HTTP/1.1");
|
||||
TEST_ASSERT_STR_EQ("example.com", req.host, "Host header parsed");
|
||||
TEST_ASSERT_EQ(1, req.keep_alive, "Keep-alive is enabled");
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_http_parse_post_request(void) {
|
||||
TEST_SUITE_BEGIN("HTTP POST Request Parsing");
|
||||
|
||||
http_request_t req;
|
||||
const char *request = "POST /api/data HTTP/1.1\r\nHost: api.example.com\r\nContent-Length: 100\r\nConnection: close\r\n\r\n";
|
||||
int result = http_parse_request(request, strlen(request), &req);
|
||||
|
||||
TEST_ASSERT_EQ(1, result, "Parse POST request");
|
||||
TEST_ASSERT_STR_EQ("POST", req.method, "Method is POST");
|
||||
TEST_ASSERT_STR_EQ("/api/data", req.uri, "URI parsed correctly");
|
||||
TEST_ASSERT_EQ(100, req.content_length, "Content-Length parsed");
|
||||
TEST_ASSERT_EQ(0, req.keep_alive, "Keep-alive is disabled");
|
||||
TEST_ASSERT_EQ(1, req.connection_close, "Connection close flag set");
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_http_parse_webdav_methods(void) {
|
||||
TEST_SUITE_BEGIN("HTTP WebDAV Methods Parsing");
|
||||
|
||||
http_request_t req;
|
||||
|
||||
const char *propfind = "PROPFIND /folder HTTP/1.1\r\nHost: webdav.example.com\r\n\r\n";
|
||||
int result = http_parse_request(propfind, strlen(propfind), &req);
|
||||
TEST_ASSERT_EQ(1, result, "Parse PROPFIND request");
|
||||
TEST_ASSERT_STR_EQ("PROPFIND", req.method, "Method is PROPFIND");
|
||||
|
||||
const char *mkcol = "MKCOL /newfolder HTTP/1.1\r\nHost: webdav.example.com\r\n\r\n";
|
||||
result = http_parse_request(mkcol, strlen(mkcol), &req);
|
||||
TEST_ASSERT_EQ(1, result, "Parse MKCOL request");
|
||||
TEST_ASSERT_STR_EQ("MKCOL", req.method, "Method is MKCOL");
|
||||
|
||||
const char *move = "MOVE /source HTTP/1.1\r\nHost: webdav.example.com\r\n\r\n";
|
||||
result = http_parse_request(move, strlen(move), &req);
|
||||
TEST_ASSERT_EQ(1, result, "Parse MOVE request");
|
||||
TEST_ASSERT_STR_EQ("MOVE", req.method, "Method is MOVE");
|
||||
|
||||
const char *copy = "COPY /source HTTP/1.1\r\nHost: webdav.example.com\r\n\r\n";
|
||||
result = http_parse_request(copy, strlen(copy), &req);
|
||||
TEST_ASSERT_EQ(1, result, "Parse COPY request");
|
||||
TEST_ASSERT_STR_EQ("COPY", req.method, "Method is COPY");
|
||||
|
||||
const char *lock = "LOCK /resource HTTP/1.1\r\nHost: webdav.example.com\r\n\r\n";
|
||||
result = http_parse_request(lock, strlen(lock), &req);
|
||||
TEST_ASSERT_EQ(1, result, "Parse LOCK request");
|
||||
TEST_ASSERT_STR_EQ("LOCK", req.method, "Method is LOCK");
|
||||
|
||||
const char *unlock = "UNLOCK /resource HTTP/1.1\r\nHost: webdav.example.com\r\n\r\n";
|
||||
result = http_parse_request(unlock, strlen(unlock), &req);
|
||||
TEST_ASSERT_EQ(1, result, "Parse UNLOCK request");
|
||||
TEST_ASSERT_STR_EQ("UNLOCK", req.method, "Method is UNLOCK");
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_http_parse_custom_methods(void) {
|
||||
TEST_SUITE_BEGIN("HTTP Custom Methods Parsing");
|
||||
|
||||
http_request_t req;
|
||||
|
||||
const char *custom1 = "MYMETHOD /path HTTP/1.1\r\nHost: example.com\r\n\r\n";
|
||||
int result = http_parse_request(custom1, strlen(custom1), &req);
|
||||
TEST_ASSERT_EQ(1, result, "Parse custom MYMETHOD request");
|
||||
TEST_ASSERT_STR_EQ("MYMETHOD", req.method, "Method is MYMETHOD");
|
||||
|
||||
const char *custom2 = "FOOBAR /path HTTP/1.1\r\nHost: example.com\r\n\r\n";
|
||||
result = http_parse_request(custom2, strlen(custom2), &req);
|
||||
TEST_ASSERT_EQ(1, result, "Parse custom FOOBAR request");
|
||||
TEST_ASSERT_STR_EQ("FOOBAR", req.method, "Method is FOOBAR");
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_http_parse_websocket_upgrade(void) {
|
||||
TEST_SUITE_BEGIN("HTTP WebSocket Upgrade Parsing");
|
||||
|
||||
http_request_t req;
|
||||
const char *request = "GET /ws HTTP/1.1\r\n"
|
||||
"Host: example.com\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: upgrade\r\n"
|
||||
"\r\n";
|
||||
int result = http_parse_request(request, strlen(request), &req);
|
||||
|
||||
TEST_ASSERT_EQ(1, result, "Parse WebSocket upgrade request");
|
||||
TEST_ASSERT_STR_EQ("GET", req.method, "Method is GET");
|
||||
TEST_ASSERT_EQ(1, req.is_websocket, "WebSocket flag is set");
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_http_parse_chunked_encoding(void) {
|
||||
TEST_SUITE_BEGIN("HTTP Chunked Encoding Parsing");
|
||||
|
||||
http_request_t req;
|
||||
const char *request = "POST /api/upload HTTP/1.1\r\n"
|
||||
"Host: example.com\r\n"
|
||||
"Transfer-Encoding: chunked\r\n"
|
||||
"\r\n";
|
||||
int result = http_parse_request(request, strlen(request), &req);
|
||||
|
||||
TEST_ASSERT_EQ(1, result, "Parse chunked request");
|
||||
TEST_ASSERT_EQ(1, req.is_chunked, "Chunked flag is set");
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_http_parse_http10(void) {
|
||||
TEST_SUITE_BEGIN("HTTP/1.0 Request Parsing");
|
||||
|
||||
http_request_t req;
|
||||
const char *request = "GET /path HTTP/1.0\r\nHost: example.com\r\n\r\n";
|
||||
int result = http_parse_request(request, strlen(request), &req);
|
||||
|
||||
TEST_ASSERT_EQ(1, result, "Parse HTTP/1.0 request");
|
||||
TEST_ASSERT_STR_EQ("HTTP/1.0", req.version, "Version is HTTP/1.0");
|
||||
TEST_ASSERT_EQ(0, req.keep_alive, "Keep-alive is disabled by default for HTTP/1.0");
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_http_parse_host_with_port(void) {
|
||||
TEST_SUITE_BEGIN("HTTP Host with Port Parsing");
|
||||
|
||||
http_request_t req;
|
||||
const char *request = "GET / HTTP/1.1\r\nHost: example.com:8080\r\n\r\n";
|
||||
int result = http_parse_request(request, strlen(request), &req);
|
||||
|
||||
TEST_ASSERT_EQ(1, result, "Parse request with host:port");
|
||||
TEST_ASSERT_STR_EQ("example.com", req.host, "Port stripped from host");
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_http_is_request_start(void) {
|
||||
TEST_SUITE_BEGIN("HTTP Request Start Detection");
|
||||
|
||||
TEST_ASSERT_EQ(1, http_is_request_start("GET / HTTP/1.1", 14), "GET is valid request start");
|
||||
TEST_ASSERT_EQ(1, http_is_request_start("POST /api HTTP/1.1", 18), "POST is valid request start");
|
||||
TEST_ASSERT_EQ(1, http_is_request_start("PROPFIND / HTTP/1.1", 19), "PROPFIND is valid request start");
|
||||
TEST_ASSERT_EQ(1, http_is_request_start("CUSTOMMETHOD / HTTP/1.1", 23), "Custom method is valid request start");
|
||||
TEST_ASSERT_EQ(0, http_is_request_start("123 invalid", 11), "Numbers at start are invalid");
|
||||
TEST_ASSERT_EQ(0, http_is_request_start("abc", 3), "Too short is invalid");
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_http_malformed_requests(void) {
|
||||
TEST_SUITE_BEGIN("HTTP Malformed Request Handling");
|
||||
|
||||
http_request_t req;
|
||||
|
||||
const char *no_space = "GETHTTP/1.1\r\nHost: example.com\r\n\r\n";
|
||||
int result = http_parse_request(no_space, strlen(no_space), &req);
|
||||
TEST_ASSERT_EQ(0, result, "Reject request without space after method");
|
||||
|
||||
const char *no_version = "GET /path\r\nHost: example.com\r\n\r\n";
|
||||
result = http_parse_request(no_version, strlen(no_version), &req);
|
||||
TEST_ASSERT_EQ(0, result, "Reject request without version");
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void run_http_tests(void) {
|
||||
test_http_parse_get_request();
|
||||
test_http_parse_post_request();
|
||||
test_http_parse_webdav_methods();
|
||||
test_http_parse_custom_methods();
|
||||
test_http_parse_websocket_upgrade();
|
||||
test_http_parse_chunked_encoding();
|
||||
test_http_parse_http10();
|
||||
test_http_parse_host_with_port();
|
||||
test_http_is_request_start();
|
||||
test_http_malformed_requests();
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "test_framework.h"
|
||||
#include <stdio.h>
|
||||
|
||||
int tests_run = 0;
|
||||
int tests_passed = 0;
|
||||
int tests_failed = 0;
|
||||
|
||||
void test_summary(void) {
|
||||
printf("\n=========================================\n");
|
||||
printf("Test Results: %d/%d passed\n", tests_passed, tests_run);
|
||||
if (tests_failed > 0) {
|
||||
printf("FAILED: %d tests failed\n", tests_failed);
|
||||
} else {
|
||||
printf("SUCCESS: All tests passed\n");
|
||||
}
|
||||
printf("=========================================\n");
|
||||
}
|
||||
|
||||
extern void run_http_tests(void);
|
||||
extern void run_buffer_tests(void);
|
||||
extern void run_config_tests(void);
|
||||
extern void run_routing_tests(void);
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
printf("\n");
|
||||
printf("=========================================\n");
|
||||
printf(" RProxy Enterprise Unit Tests\n");
|
||||
printf("=========================================\n");
|
||||
|
||||
run_buffer_tests();
|
||||
run_http_tests();
|
||||
run_config_tests();
|
||||
run_routing_tests();
|
||||
|
||||
test_summary();
|
||||
|
||||
return tests_failed > 0 ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
#include "test_framework.h"
|
||||
#include "../src/types.h"
|
||||
#include "../src/http.h"
|
||||
#include "../src/config.h"
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static const char *TEST_CONFIG_FILE = "/tmp/test_routing_config.json";
|
||||
|
||||
static void create_routing_config(void) {
|
||||
FILE *f = fopen(TEST_CONFIG_FILE, "w");
|
||||
if (f) {
|
||||
fprintf(f,
|
||||
"{\n"
|
||||
" \"port\": 8080,\n"
|
||||
" \"reverse_proxy\": [\n"
|
||||
" {\n"
|
||||
" \"hostname\": \"api.local\",\n"
|
||||
" \"upstream_host\": \"backend-api\",\n"
|
||||
" \"upstream_port\": 3000,\n"
|
||||
" \"use_ssl\": false,\n"
|
||||
" \"rewrite_host\": true\n"
|
||||
" },\n"
|
||||
" {\n"
|
||||
" \"hostname\": \"secure.local\",\n"
|
||||
" \"upstream_host\": \"backend-secure\",\n"
|
||||
" \"upstream_port\": 443,\n"
|
||||
" \"use_ssl\": true,\n"
|
||||
" \"rewrite_host\": true\n"
|
||||
" },\n"
|
||||
" {\n"
|
||||
" \"hostname\": \"passthrough.local\",\n"
|
||||
" \"upstream_host\": \"backend-pass\",\n"
|
||||
" \"upstream_port\": 8000,\n"
|
||||
" \"use_ssl\": false,\n"
|
||||
" \"rewrite_host\": false\n"
|
||||
" }\n"
|
||||
" ]\n"
|
||||
"}\n");
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
static void cleanup_routing_config(void) {
|
||||
unlink(TEST_CONFIG_FILE);
|
||||
}
|
||||
|
||||
void test_routing_host_rewrite(void) {
|
||||
TEST_SUITE_BEGIN("Routing Host Rewrite Logic");
|
||||
|
||||
create_routing_config();
|
||||
config_load(TEST_CONFIG_FILE);
|
||||
|
||||
route_config_t *route = config_find_route("api.local");
|
||||
TEST_ASSERT(route != NULL, "Route for api.local found");
|
||||
if (route) {
|
||||
TEST_ASSERT_EQ(1, route->rewrite_host, "Host rewrite enabled for api.local");
|
||||
TEST_ASSERT_STR_EQ("backend-api", route->upstream_host, "Upstream host is backend-api");
|
||||
TEST_ASSERT_EQ(3000, route->upstream_port, "Upstream port is 3000");
|
||||
}
|
||||
|
||||
route = config_find_route("passthrough.local");
|
||||
TEST_ASSERT(route != NULL, "Route for passthrough.local found");
|
||||
if (route) {
|
||||
TEST_ASSERT_EQ(0, route->rewrite_host, "Host rewrite disabled for passthrough.local");
|
||||
}
|
||||
|
||||
config_free();
|
||||
cleanup_routing_config();
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_routing_ssl_upstream(void) {
|
||||
TEST_SUITE_BEGIN("Routing SSL Upstream");
|
||||
|
||||
create_routing_config();
|
||||
config_load(TEST_CONFIG_FILE);
|
||||
|
||||
route_config_t *route = config_find_route("secure.local");
|
||||
TEST_ASSERT(route != NULL, "Route for secure.local found");
|
||||
if (route) {
|
||||
TEST_ASSERT_EQ(1, route->use_ssl, "SSL enabled for secure.local");
|
||||
TEST_ASSERT_EQ(443, route->upstream_port, "SSL port is 443");
|
||||
}
|
||||
|
||||
route = config_find_route("api.local");
|
||||
TEST_ASSERT(route != NULL, "Route for api.local found");
|
||||
if (route) {
|
||||
TEST_ASSERT_EQ(0, route->use_ssl, "SSL disabled for api.local");
|
||||
}
|
||||
|
||||
config_free();
|
||||
cleanup_routing_config();
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_routing_dashboard_detection(void) {
|
||||
TEST_SUITE_BEGIN("Routing Dashboard Detection");
|
||||
|
||||
http_request_t req;
|
||||
|
||||
const char *dashboard_req = "GET /rproxy/dashboard HTTP/1.1\r\nHost: anyhost.com\r\n\r\n";
|
||||
http_parse_request(dashboard_req, strlen(dashboard_req), &req);
|
||||
int is_dashboard = (strncmp(req.uri, "/rproxy/dashboard", 17) == 0);
|
||||
TEST_ASSERT_EQ(1, is_dashboard, "Dashboard URI detected");
|
||||
|
||||
const char *api_req = "GET /rproxy/api/stats HTTP/1.1\r\nHost: anyhost.com\r\n\r\n";
|
||||
http_parse_request(api_req, strlen(api_req), &req);
|
||||
int is_api = (strncmp(req.uri, "/rproxy/api/stats", 17) == 0);
|
||||
TEST_ASSERT_EQ(1, is_api, "API stats URI detected");
|
||||
|
||||
const char *regular_req = "GET /some/other/path HTTP/1.1\r\nHost: api.local\r\n\r\n";
|
||||
http_parse_request(regular_req, strlen(regular_req), &req);
|
||||
int is_regular = (strncmp(req.uri, "/rproxy/", 8) != 0);
|
||||
TEST_ASSERT_EQ(1, is_regular, "Regular path not matched as internal");
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_routing_keep_alive_handling(void) {
|
||||
TEST_SUITE_BEGIN("Routing Keep-Alive Handling");
|
||||
|
||||
http_request_t req;
|
||||
|
||||
const char *ka_req = "GET /api HTTP/1.1\r\nHost: api.local\r\nConnection: keep-alive\r\n\r\n";
|
||||
http_parse_request(ka_req, strlen(ka_req), &req);
|
||||
TEST_ASSERT_EQ(1, req.keep_alive, "Keep-alive flag set when Connection: keep-alive");
|
||||
|
||||
const char *close_req = "GET /api HTTP/1.1\r\nHost: api.local\r\nConnection: close\r\n\r\n";
|
||||
http_parse_request(close_req, strlen(close_req), &req);
|
||||
TEST_ASSERT_EQ(0, req.keep_alive, "Keep-alive flag not set when Connection: close");
|
||||
|
||||
const char *default_req = "GET /api HTTP/1.1\r\nHost: api.local\r\n\r\n";
|
||||
http_parse_request(default_req, strlen(default_req), &req);
|
||||
TEST_ASSERT_EQ(1, req.keep_alive, "Keep-alive default for HTTP/1.1");
|
||||
|
||||
const char *http10_req = "GET /api HTTP/1.0\r\nHost: api.local\r\n\r\n";
|
||||
http_parse_request(http10_req, strlen(http10_req), &req);
|
||||
TEST_ASSERT_EQ(0, req.keep_alive, "Keep-alive default off for HTTP/1.0");
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_routing_all_methods_accepted(void) {
|
||||
TEST_SUITE_BEGIN("Routing All HTTP Methods Accepted");
|
||||
|
||||
http_request_t req;
|
||||
int result;
|
||||
|
||||
const char *methods[] = {
|
||||
"GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH", "TRACE", "CONNECT",
|
||||
"PROPFIND", "PROPPATCH", "MKCOL", "MOVE", "COPY", "LOCK", "UNLOCK",
|
||||
"SEARCH", "REPORT", "MKACTIVITY", "CHECKOUT", "MERGE",
|
||||
"NOTIFY", "SUBSCRIBE", "UNSUBSCRIBE",
|
||||
"CUSTOMMETHOD", "FOOBAR", "MYPROTO"
|
||||
};
|
||||
|
||||
int num_methods = sizeof(methods) / sizeof(methods[0]);
|
||||
|
||||
for (int i = 0; i < num_methods; i++) {
|
||||
char request[256];
|
||||
snprintf(request, sizeof(request), "%s /path HTTP/1.1\r\nHost: test.com\r\n\r\n", methods[i]);
|
||||
result = http_parse_request(request, strlen(request), &req);
|
||||
|
||||
char msg[128];
|
||||
snprintf(msg, sizeof(msg), "Method %s accepted", methods[i]);
|
||||
TEST_ASSERT_EQ(1, result, msg);
|
||||
TEST_ASSERT_STR_EQ(methods[i], req.method, msg);
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_routing_pipelined_requests(void) {
|
||||
TEST_SUITE_BEGIN("Routing Pipelined Request Detection");
|
||||
|
||||
http_request_t req1, req2;
|
||||
|
||||
const char *first_req = "GET /first HTTP/1.1\r\nHost: api.local\r\nConnection: keep-alive\r\n\r\n";
|
||||
int result1 = http_parse_request(first_req, strlen(first_req), &req1);
|
||||
TEST_ASSERT_EQ(1, result1, "First request parsed");
|
||||
TEST_ASSERT_STR_EQ("/first", req1.uri, "First request URI");
|
||||
TEST_ASSERT_EQ(1, req1.keep_alive, "First request keep-alive");
|
||||
|
||||
const char *second_req = "GET /second HTTP/1.1\r\nHost: api.local\r\n\r\n";
|
||||
int result2 = http_parse_request(second_req, strlen(second_req), &req2);
|
||||
TEST_ASSERT_EQ(1, result2, "Second request parsed");
|
||||
TEST_ASSERT_STR_EQ("/second", req2.uri, "Second request URI");
|
||||
|
||||
int is_new_request = http_is_request_start(second_req, strlen(second_req));
|
||||
TEST_ASSERT_EQ(1, is_new_request, "New request detected for pipelining");
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_routing_websocket_upgrade(void) {
|
||||
TEST_SUITE_BEGIN("Routing WebSocket Upgrade");
|
||||
|
||||
http_request_t req;
|
||||
|
||||
const char *ws_req =
|
||||
"GET /ws/chat HTTP/1.1\r\n"
|
||||
"Host: api.local\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
|
||||
"Sec-WebSocket-Version: 13\r\n"
|
||||
"\r\n";
|
||||
|
||||
int result = http_parse_request(ws_req, strlen(ws_req), &req);
|
||||
TEST_ASSERT_EQ(1, result, "WebSocket upgrade request parsed");
|
||||
TEST_ASSERT_EQ(1, req.is_websocket, "WebSocket flag set");
|
||||
TEST_ASSERT_STR_EQ("/ws/chat", req.uri, "WebSocket URI parsed");
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void test_routing_chunked_transfer(void) {
|
||||
TEST_SUITE_BEGIN("Routing Chunked Transfer Encoding");
|
||||
|
||||
http_request_t req;
|
||||
|
||||
const char *chunked_req =
|
||||
"POST /upload HTTP/1.1\r\n"
|
||||
"Host: api.local\r\n"
|
||||
"Transfer-Encoding: chunked\r\n"
|
||||
"\r\n";
|
||||
|
||||
int result = http_parse_request(chunked_req, strlen(chunked_req), &req);
|
||||
TEST_ASSERT_EQ(1, result, "Chunked request parsed");
|
||||
TEST_ASSERT_EQ(1, req.is_chunked, "Chunked flag set");
|
||||
|
||||
const char *non_chunked_req =
|
||||
"POST /upload HTTP/1.1\r\n"
|
||||
"Host: api.local\r\n"
|
||||
"Content-Length: 100\r\n"
|
||||
"\r\n";
|
||||
|
||||
result = http_parse_request(non_chunked_req, strlen(non_chunked_req), &req);
|
||||
TEST_ASSERT_EQ(1, result, "Non-chunked request parsed");
|
||||
TEST_ASSERT_EQ(0, req.is_chunked, "Chunked flag not set");
|
||||
TEST_ASSERT_EQ(100, req.content_length, "Content-Length parsed");
|
||||
|
||||
TEST_SUITE_END();
|
||||
}
|
||||
|
||||
void run_routing_tests(void) {
|
||||
test_routing_host_rewrite();
|
||||
test_routing_ssl_upstream();
|
||||
test_routing_dashboard_detection();
|
||||
test_routing_keep_alive_handling();
|
||||
test_routing_all_methods_accepted();
|
||||
test_routing_pipelined_requests();
|
||||
test_routing_websocket_upgrade();
|
||||
test_routing_chunked_transfer();
|
||||
}
|
||||
Reference in New Issue
Block a user