chore: add .gitignore entries for build artifacts and test files, restructure Makefile with proper build system

This commit is contained in:
2025-11-29 00:49:14 +00:00
parent 2a492dd934
commit 3643909bd5
26 changed files with 3705 additions and 6 deletions
+159
View File
@@ -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();
}
+250
View File
@@ -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();
}
+55
View File
@@ -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
+196
View File
@@ -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();
}
+41
View File
@@ -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;
}
+258
View File
@@ -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();
}