From 4c6729eebc3488a953d0d0e0e38df0599dd68331 Mon Sep 17 00:00:00 2001 From: Francesco Date: Sun, 29 Mar 2026 18:39:16 +0200 Subject: [PATCH] feat(test): add new tests --- meson.build | 12 +---- src/http/response.c | 2 +- src/main.c | 8 +-- test/common.c | 107 +++++++++++++++++++++++++++++++++++++ test/common.h | 26 +++++++++ test/meson.build | 43 +++++++++++++++ test/server.c | 58 -------------------- test/test_content_length.c | 67 +++++++++++++++++++++++ test/test_get_index.c | 46 ++++++++++++++++ test/test_get_missing.c | 46 ++++++++++++++++ test/test_post.c | 46 ++++++++++++++++ test/test_traversal.c | 62 +++++++++++++++++++++ 12 files changed, 450 insertions(+), 73 deletions(-) create mode 100644 test/common.c create mode 100644 test/common.h create mode 100644 test/meson.build delete mode 100644 test/server.c create mode 100644 test/test_content_length.c create mode 100644 test/test_get_index.c create mode 100644 test/test_get_missing.c create mode 100644 test/test_post.c create mode 100644 test/test_traversal.c diff --git a/meson.build b/meson.build index 32884ba..8bc4068 100644 --- a/meson.build +++ b/meson.build @@ -22,6 +22,7 @@ add_global_arguments('-DEVELOPER', language: 'c') add_global_arguments('-D_POSIX_C_SOURCE=200809L', language: 'c') exe = executable('cws', server, include_directories: include_dirs, dependencies: deps) +subdir('test') # Commands @@ -35,13 +36,4 @@ if clangformat.found() 'clang-format', ], ) -endif - -# Test - -test_src = files('test/server.c') -test_curl_dep = dependency('libcurl', required: false) -test_deps = [test_curl_dep] -test_exec = executable('test_http', test_src, dependencies: test_deps) - -test('index get', test_exec, args: [exe.full_path()]) +endif \ No newline at end of file diff --git a/src/http/response.c b/src/http/response.c index c158224..31d87e3 100644 --- a/src/http/response.c +++ b/src/http/response.c @@ -21,7 +21,7 @@ static int response_get_headers(hashmap_s *headers, char *out_headers, size_t le size_t keys_len = 0; char **keys = (char **)hm_get_keys(headers, &keys_len); if (!keys) { - cws_log_debug("no headers??"); + cws_log_debug("%s", "no headers??"); return -1; } diff --git a/src/main.c b/src/main.c index 2cae78f..dedd32c 100644 --- a/src/main.c +++ b/src/main.c @@ -16,18 +16,18 @@ int main(void) { cws_log_init(); if (signal(SIGINT, cws_signal_handler) == SIG_ERR) { - cws_log_error("signal()"); + cws_log_error("%s", "signal()"); return EXIT_FAILURE; } if (signal(SIGTERM, cws_signal_handler) == SIG_ERR) { - cws_log_error("signal()"); + cws_log_error("%s", "signal()"); return EXIT_FAILURE; } cws_config_s *config = cws_config_init(); if (!config) { - cws_log_error("Unable to parse config"); + cws_log_error("%s", "Unable to parse config"); cws_log_shutdown(); return EXIT_FAILURE; } @@ -51,7 +51,7 @@ int main(void) { cws_log_error("Unable to start web server: %s", cws_error_str(ret)); } - cws_log_info("Shutting down cws"); + cws_log_info("%s", "Shutting down cws"); cws_server_shutdown(&server); cws_config_free(config); cws_log_shutdown(); diff --git a/test/common.c b/test/common.c new file mode 100644 index 0000000..f3740dc --- /dev/null +++ b/test/common.c @@ -0,0 +1,107 @@ +#include "common.h" +#include +#include +#include +#include +#include +#include + +bool start_server(const char *server_path, pid_t *pid_out) { + pid_t pid = fork(); + if (pid == 0) { + execl(server_path, "cws", NULL); + fprintf(stderr, "execl failed: %s\n", strerror(errno)); + _exit(1); + } + + if (pid < 0) { + fprintf(stderr, "fork failed: %s\n", strerror(errno)); + return false; + } + + *pid_out = pid; + fprintf(stdout, "[INFO] server pid: %d\n", pid); + return true; +} + +void stop_server(pid_t pid) { + if (pid <= 0) { + return; + } + + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + fprintf(stdout, "[INFO] server stopped (pid=%d)\n", pid); +} + +bool perform_request(const char *url, const char *method, http_result_s *out) { + CURL *curl = curl_easy_init(); + if (!curl) { + fprintf(stderr, "[ERROR] curl_easy_init failed\n"); + return false; + } + + memset(out, 0, sizeof(*out)); + out->content_length = -1; + out->error[0] = '\0'; + + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method); + curl_easy_setopt(curl, CURLOPT_NOBODY, 0L); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 3L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 1L); + curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, out->error); + + out->curl_code = curl_easy_perform(curl); + if (out->curl_code == CURLE_OK) { + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &out->code); + curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &out->content_length); + + char *ct = NULL; + curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct); + if (ct) { + strncpy(out->content_type, ct, sizeof(out->content_type) - 1); + out->content_type[sizeof(out->content_type) - 1] = '\0'; + } + } + + curl_easy_cleanup(curl); + return out->curl_code == CURLE_OK; +} + +bool wait_until_ready(void) { + char url[256]; + snprintf(url, sizeof(url), "%s/", BASE_URL); + + for (int i = 0; i < MAX_RETRIES; i++) { + http_result_s result; + if (perform_request(url, "GET", &result)) { + fprintf(stdout, "[INFO] server ready after %d attempt(s), code=%ld\n", i + 1, result.code); + return true; + } + + fprintf(stdout, "[INFO] server not ready (%d/%d): %s\n", i + 1, MAX_RETRIES, + result.error[0] ? result.error : "unknown error"); + sleep(RETRY_SLEEP_SEC); + } + + return false; +} + +int assert_status(const char *name, const char *url, const char *method, long expected) { + http_result_s result; + if (!perform_request(url, method, &result)) { + fprintf(stderr, "[FAIL] %s: request failed (%s)\n", name, + result.error[0] ? result.error : "unknown curl error"); + return 1; + } + + fprintf(stdout, "[TEST] %s -> %s %s => %ld (expected %ld)\n", name, method, url, result.code, expected); + + if (result.code != expected) { + fprintf(stderr, "[FAIL] %s: expected HTTP %ld, got %ld\n", name, expected, result.code); + return 1; + } + + return 0; +} diff --git a/test/common.h b/test/common.h new file mode 100644 index 0000000..a97b58f --- /dev/null +++ b/test/common.h @@ -0,0 +1,26 @@ +#ifndef CWS_TEST_COMMON_H +#define CWS_TEST_COMMON_H + +#include +#include +#include + +#define BASE_URL "http://localhost:3030" +#define MAX_RETRIES 15 +#define RETRY_SLEEP_SEC 1 + +typedef struct http_result { + long code; + long content_length; + char content_type[256]; + char error[CURL_ERROR_SIZE]; + CURLcode curl_code; +} http_result_s; + +bool start_server(const char *server_path, pid_t *pid_out); +void stop_server(pid_t pid); +bool wait_until_ready(void); +bool perform_request(const char *url, const char *method, http_result_s *out); +int assert_status(const char *name, const char *url, const char *method, long expected); + +#endif diff --git a/test/meson.build b/test/meson.build new file mode 100644 index 0000000..466c87b --- /dev/null +++ b/test/meson.build @@ -0,0 +1,43 @@ +test_common_src = files('common.c') +test_curl_dep = dependency('libcurl', required: false) +test_deps = [test_curl_dep] + +test_get_index = executable( + 'test_get_index', + test_common_src, + files('test_get_index.c'), + dependencies: test_deps, +) +test('index get', test_get_index, args: [exe.full_path()]) + +test_get_missing = executable( + 'test_get_missing', + test_common_src, + files('test_get_missing.c'), + dependencies: test_deps, +) +test('missing 404', test_get_missing, args: [exe.full_path()]) + +test_post = executable( + 'test_post', + test_common_src, + files('test_post.c'), + dependencies: test_deps, +) +test('post not implemented', test_post, args: [exe.full_path()]) + +test_traversal = executable( + 'test_traversal', + test_common_src, + files('test_traversal.c'), + dependencies: test_deps, +) +test('traversal blocked', test_traversal, args: [exe.full_path()]) + +test_content_length = executable( + 'test_content_length', + test_common_src, + files('test_content_length.c'), + dependencies: test_deps, +) +test('content length present', test_content_length, args: [exe.full_path()]) \ No newline at end of file diff --git a/test/server.c b/test/server.c deleted file mode 100644 index 6f72ac1..0000000 --- a/test/server.c +++ /dev/null @@ -1,58 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#define URL "http://localhost:3030" - -int main(int argc, char **argv) { - if (argc < 2) { - return 1; - } - - char *server_path = argv[1]; - fprintf(stdout, "server path: %s\n", server_path); - - /* Run the server as child proc */ - pid_t pid = fork(); - if (pid == 0) { - execl(server_path, "cws", NULL); - fprintf(stderr, "execl failed: %s\n", strerror(errno)); - exit(1); - } - - if (pid == -1) { - fprintf(stderr, "fork failed: %s\n", strerror(errno)); - exit(1); - } - - fprintf(stdout, "pid: %d\n", pid); - - CURL *curl = curl_easy_init(); - curl_easy_setopt(curl, CURLOPT_URL, URL); - curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 1L); - - long http_code = 0; - int max_retries = 10; - for (int i = 0; i < max_retries; i++) { - CURLcode res = curl_easy_perform(curl); - if (res == CURLE_OK) { - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); - break; - } - fprintf(stdout, "Server not ready, retry %d/%d...\n", i + 1, max_retries); - sleep(1); - } - - curl_easy_cleanup(curl); - - fprintf(stdout, "http_code: %ld\n", http_code); - - /* Kill the server */ - kill(pid, SIGTERM); - waitpid(pid, NULL, 0); - - return http_code == 200 ? 0 : 1; -} diff --git a/test/test_content_length.c b/test/test_content_length.c new file mode 100644 index 0000000..087f249 --- /dev/null +++ b/test/test_content_length.c @@ -0,0 +1,67 @@ +#include "common.h" +#include + +static int test_content_length_present_on_index(void) { + char url[256]; + snprintf(url, sizeof(url), "%s/", BASE_URL); + + http_result_s result; + if (!perform_request(url, "GET", &result)) { + fprintf(stderr, "[FAIL] content-length on index: request failed (%s)\n", + result.error[0] ? result.error : "unknown curl error"); + return 1; + } + + fprintf(stdout, "[TEST] content-length on index -> code=%ld, content-length=%ld\n", result.code, + result.content_length); + + if (result.code != 200) { + fprintf(stderr, "[FAIL] content-length on index: expected 200, got %ld\n", result.code); + return 1; + } + + if (result.content_length < 0) { + fprintf(stderr, "[FAIL] content-length on index: missing/invalid content length\n"); + return 1; + } + + return 0; +} + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + curl_global_init(CURL_GLOBAL_DEFAULT); + + const char *server_path = argv[1]; + fprintf(stdout, "[INFO] server path: %s\n", server_path); + + pid_t server_pid = -1; + if (!start_server(server_path, &server_pid)) { + curl_global_cleanup(); + return 1; + } + + if (!wait_until_ready()) { + fprintf(stderr, "[FAIL] server did not become ready in time\n"); + stop_server(server_pid); + curl_global_cleanup(); + return 1; + } + + int failures = test_content_length_present_on_index(); + + stop_server(server_pid); + curl_global_cleanup(); + + if (failures == 0) { + fprintf(stdout, "[PASS] test_content_length passed\n"); + return 0; + } + + fprintf(stderr, "[FAIL] test_content_length failed\n"); + return 1; +} diff --git a/test/test_get_index.c b/test/test_get_index.c new file mode 100644 index 0000000..dadd420 --- /dev/null +++ b/test/test_get_index.c @@ -0,0 +1,46 @@ +#include "common.h" +#include + +static int test_get_index_ok(void) { + char url[256]; + snprintf(url, sizeof(url), "%s/", BASE_URL); + return assert_status("GET / returns 200", url, "GET", 200); +} + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + curl_global_init(CURL_GLOBAL_DEFAULT); + + const char *server_path = argv[1]; + fprintf(stdout, "[INFO] server path: %s\n", server_path); + + pid_t server_pid = -1; + if (!start_server(server_path, &server_pid)) { + curl_global_cleanup(); + return 1; + } + + if (!wait_until_ready()) { + fprintf(stderr, "[FAIL] server did not become ready in time\n"); + stop_server(server_pid); + curl_global_cleanup(); + return 1; + } + + int failures = test_get_index_ok(); + + stop_server(server_pid); + curl_global_cleanup(); + + if (failures == 0) { + fprintf(stdout, "[PASS] test_get_index passed\n"); + return 0; + } + + fprintf(stderr, "[FAIL] test_get_index failed\n"); + return 1; +} diff --git a/test/test_get_missing.c b/test/test_get_missing.c new file mode 100644 index 0000000..ce93a94 --- /dev/null +++ b/test/test_get_missing.c @@ -0,0 +1,46 @@ +#include "common.h" +#include + +static int test_get_missing_404(void) { + char url[256]; + snprintf(url, sizeof(url), "%s/does-not-exist-xyz", BASE_URL); + return assert_status("GET /missing returns 404", url, "GET", 404); +} + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + curl_global_init(CURL_GLOBAL_DEFAULT); + + const char *server_path = argv[1]; + fprintf(stdout, "[INFO] server path: %s\n", server_path); + + pid_t server_pid = -1; + if (!start_server(server_path, &server_pid)) { + curl_global_cleanup(); + return 1; + } + + if (!wait_until_ready()) { + fprintf(stderr, "[FAIL] server did not become ready in time\n"); + stop_server(server_pid); + curl_global_cleanup(); + return 1; + } + + int failures = test_get_missing_404(); + + stop_server(server_pid); + curl_global_cleanup(); + + if (failures == 0) { + fprintf(stdout, "[PASS] test_get_missing passed\n"); + return 0; + } + + fprintf(stderr, "[FAIL] test_get_missing failed\n"); + return 1; +} diff --git a/test/test_post.c b/test/test_post.c new file mode 100644 index 0000000..714d760 --- /dev/null +++ b/test/test_post.c @@ -0,0 +1,46 @@ +#include "common.h" +#include + +static int test_post_not_implemented(void) { + char url[256]; + snprintf(url, sizeof(url), "%s/", BASE_URL); + return assert_status("POST / returns 501", url, "POST", 501); +} + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + curl_global_init(CURL_GLOBAL_DEFAULT); + + const char *server_path = argv[1]; + fprintf(stdout, "[INFO] server path: %s\n", server_path); + + pid_t server_pid = -1; + if (!start_server(server_path, &server_pid)) { + curl_global_cleanup(); + return 1; + } + + if (!wait_until_ready()) { + fprintf(stderr, "[FAIL] server did not become ready in time\n"); + stop_server(server_pid); + curl_global_cleanup(); + return 1; + } + + int failures = test_post_not_implemented(); + + stop_server(server_pid); + curl_global_cleanup(); + + if (failures == 0) { + fprintf(stdout, "[PASS] test_post passed\n"); + return 0; + } + + fprintf(stderr, "[FAIL] test_post failed\n"); + return 1; +} diff --git a/test/test_traversal.c b/test/test_traversal.c new file mode 100644 index 0000000..454e73f --- /dev/null +++ b/test/test_traversal.c @@ -0,0 +1,62 @@ +#include "common.h" +#include + +static int test_traversal_blocked(void) { + char url[256]; + snprintf(url, sizeof(url), "%s/../config.toml", BASE_URL); + + http_result_s result; + if (!perform_request(url, "GET", &result)) { + fprintf(stderr, "[FAIL] traversal blocked: request failed (%s)\n", + result.error[0] ? result.error : "unknown curl error"); + return 1; + } + + fprintf(stdout, "[TEST] traversal blocked -> GET %s => %ld (expected 404 or 500)\n", url, result.code); + + /* Accept either 404 (ideal) or 500 (still blocked but internal error path). */ + if (result.code != 404 && result.code != 500) { + fprintf(stderr, "[FAIL] traversal blocked: expected 404/500, got %ld\n", result.code); + return 1; + } + + return 0; +} + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + curl_global_init(CURL_GLOBAL_DEFAULT); + + const char *server_path = argv[1]; + fprintf(stdout, "[INFO] server path: %s\n", server_path); + + pid_t server_pid = -1; + if (!start_server(server_path, &server_pid)) { + curl_global_cleanup(); + return 1; + } + + if (!wait_until_ready()) { + fprintf(stderr, "[FAIL] server did not become ready in time\n"); + stop_server(server_pid); + curl_global_cleanup(); + return 1; + } + + int failures = test_traversal_blocked(); + + stop_server(server_pid); + curl_global_cleanup(); + + if (failures == 0) { + fprintf(stdout, "[PASS] test_traversal passed\n"); + return 0; + } + + fprintf(stderr, "[FAIL] test_traversal failed\n"); + return 1; +}