feat(test): add new tests

This commit is contained in:
2026-03-29 18:39:16 +02:00
parent c183477686
commit 4c6729eebc
12 changed files with 450 additions and 73 deletions
+2 -10
View File
@@ -22,6 +22,7 @@ add_global_arguments('-DEVELOPER', language: 'c')
add_global_arguments('-D_POSIX_C_SOURCE=200809L', language: 'c') add_global_arguments('-D_POSIX_C_SOURCE=200809L', language: 'c')
exe = executable('cws', server, include_directories: include_dirs, dependencies: deps) exe = executable('cws', server, include_directories: include_dirs, dependencies: deps)
subdir('test')
# Commands # Commands
@@ -35,13 +36,4 @@ if clangformat.found()
'clang-format', 'clang-format',
], ],
) )
endif 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()])
+1 -1
View File
@@ -21,7 +21,7 @@ static int response_get_headers(hashmap_s *headers, char *out_headers, size_t le
size_t keys_len = 0; size_t keys_len = 0;
char **keys = (char **)hm_get_keys(headers, &keys_len); char **keys = (char **)hm_get_keys(headers, &keys_len);
if (!keys) { if (!keys) {
cws_log_debug("no headers??"); cws_log_debug("%s", "no headers??");
return -1; return -1;
} }
+4 -4
View File
@@ -16,18 +16,18 @@ int main(void) {
cws_log_init(); cws_log_init();
if (signal(SIGINT, cws_signal_handler) == SIG_ERR) { if (signal(SIGINT, cws_signal_handler) == SIG_ERR) {
cws_log_error("signal()"); cws_log_error("%s", "signal()");
return EXIT_FAILURE; return EXIT_FAILURE;
} }
if (signal(SIGTERM, cws_signal_handler) == SIG_ERR) { if (signal(SIGTERM, cws_signal_handler) == SIG_ERR) {
cws_log_error("signal()"); cws_log_error("%s", "signal()");
return EXIT_FAILURE; return EXIT_FAILURE;
} }
cws_config_s *config = cws_config_init(); cws_config_s *config = cws_config_init();
if (!config) { if (!config) {
cws_log_error("Unable to parse config"); cws_log_error("%s", "Unable to parse config");
cws_log_shutdown(); cws_log_shutdown();
return EXIT_FAILURE; 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_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_server_shutdown(&server);
cws_config_free(config); cws_config_free(config);
cws_log_shutdown(); cws_log_shutdown();
+107
View File
@@ -0,0 +1,107 @@
#include "common.h"
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
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;
}
+26
View File
@@ -0,0 +1,26 @@
#ifndef CWS_TEST_COMMON_H
#define CWS_TEST_COMMON_H
#include <curl/curl.h>
#include <stdbool.h>
#include <sys/types.h>
#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
+43
View File
@@ -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()])
-58
View File
@@ -1,58 +0,0 @@
#include <curl/curl.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#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;
}
+67
View File
@@ -0,0 +1,67 @@
#include "common.h"
#include <stdio.h>
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 <server_binary_path>\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;
}
+46
View File
@@ -0,0 +1,46 @@
#include "common.h"
#include <stdio.h>
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 <server_binary_path>\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;
}
+46
View File
@@ -0,0 +1,46 @@
#include "common.h"
#include <stdio.h>
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 <server_binary_path>\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;
}
+46
View File
@@ -0,0 +1,46 @@
#include "common.h"
#include <stdio.h>
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 <server_binary_path>\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;
}
+62
View File
@@ -0,0 +1,62 @@
#include "common.h"
#include <stdio.h>
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 <server_binary_path>\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;
}