diff options
| author | info@mode42.com <info@mode42.com> | 2026-08-08 03:54:55 +0000 |
|---|---|---|
| committer | info@mode42.com <info@mode42.com> | 2026-08-08 03:54:55 +0000 |
| commit | 20cb29c2f8c5c87bc590896854a20b1473ceb358 (patch) | |
| tree | 2857f41513a56ad41af97b57362639298aa7f033 /tests | |
#2
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/CMakeLists.txt | 33 | ||||
| -rw-r--r-- | tests/test_config.c | 60 | ||||
| -rw-r--r-- | tests/test_instance.c | 170 | ||||
| -rw-r--r-- | tests/test_mains_proxy.c | 189 | ||||
| -rw-r--r-- | tests/test_mains_proxy_e2e.c | 360 | ||||
| -rw-r--r-- | tests/test_storage_smoke.c | 235 | ||||
| -rw-r--r-- | tests/test_util.c | 152 |
7 files changed, 1199 insertions, 0 deletions
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..882aa94 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,33 @@ +add_executable(hybbx_test_util test_util.c) +target_link_libraries(hybbx_test_util PRIVATE hybbx_core) + +add_executable(hybbx_test_config test_config.c) +target_link_libraries(hybbx_test_config PRIVATE hybbx_core) + +add_executable(hybbx_test_mains_proxy test_mains_proxy.c) +target_link_libraries(hybbx_test_mains_proxy PRIVATE hybbx_core hybbx_plugin_mains_proxy hybbx_plugin_packet_radio) + +add_executable(hybbx_test_mains_proxy_e2e test_mains_proxy_e2e.c) +target_link_libraries(hybbx_test_mains_proxy_e2e PRIVATE hybbx_core hybbx_plugin_mains_proxy hybbx_plugin_packet_radio) + +add_executable(hybbx_test_storage_smoke test_storage_smoke.c) +target_link_libraries(hybbx_test_storage_smoke PRIVATE hybbx_core) + +function(hybbx_add_instance_test name role_src) + add_executable(${name} test_instance.c ${role_src}) + target_link_libraries(${name} PRIVATE hybbx_core) + add_test(NAME ${name} COMMAND ${name}) +endfunction() + +hybbx_add_instance_test(hybbx_test_instance_main + ${CMAKE_SOURCE_DIR}/src/instance_role_main.c) +hybbx_add_instance_test(hybbx_test_instance_secondary + ${CMAKE_SOURCE_DIR}/src/instance_role_secondary.c) +hybbx_add_instance_test(hybbx_test_instance_proxy + ${CMAKE_SOURCE_DIR}/src/instance_role_proxy.c) + +add_test(NAME util COMMAND hybbx_test_util) +add_test(NAME config COMMAND hybbx_test_config) +add_test(NAME mains_proxy COMMAND hybbx_test_mains_proxy) +add_test(NAME mains_proxy_e2e COMMAND hybbx_test_mains_proxy_e2e $<TARGET_FILE:hybbxd>) +add_test(NAME storage_smoke COMMAND hybbx_test_storage_smoke) diff --git a/tests/test_config.c b/tests/test_config.c new file mode 100644 index 0000000..006d6b5 --- /dev/null +++ b/tests/test_config.c @@ -0,0 +1,60 @@ +#include "hybbx/config.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static unsigned g_failures; + +static void check_str(const char *label, const char *got, const char *want) +{ + if (got == NULL || want == NULL || strcmp(got, want) != 0) { + fprintf(stderr, "FAIL %s: got '%s' want '%s'\n", + label, got != NULL ? got : "(null)", want); + g_failures++; + } +} + +int main(void) +{ + hybbx_config_t cfg; + const char *path = "hybbx_test_config.ini"; + FILE *fp; + + fp = fopen(path, "w"); + if (fp == NULL) { + fprintf(stderr, "FAIL fopen\n"); + return 1; + } + + fputs("[log]\n", fp); + fputs("dir = logs ; relative to install root\n", fp); + fputs("level = info # debug level\n", fp); + fputs("[quoted]\n", fp); + fputs("path = \"data;keep\" ; trailing comment\n", fp); + fclose(fp); + + if (hybbx_config_load(&cfg, path) != HYBBX_OK) { + fprintf(stderr, "FAIL config load\n"); + remove(path); + return 1; + } + + check_str("inline semicolon", hybbx_config_get(&cfg, "log", "dir", NULL), + "logs"); + check_str("inline hash", hybbx_config_get(&cfg, "log", "level", NULL), + "info"); + check_str("quoted semicolon", + hybbx_config_get(&cfg, "quoted", "path", NULL), "data;keep"); + + hybbx_config_free(&cfg); + remove(path); + + if (g_failures != 0) { + fprintf(stderr, "%u failure(s)\n", g_failures); + return 1; + } + + puts("test_config: ok"); + return 0; +} diff --git a/tests/test_instance.c b/tests/test_instance.c new file mode 100644 index 0000000..dd25cea --- /dev/null +++ b/tests/test_instance.c @@ -0,0 +1,170 @@ +#include "hybbx/instance.h" +#include "hybbx/networks.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static int failures; + +static void expect_int(const char *label, int got, int want) +{ + if (got != want) { + fprintf(stderr, "FAIL %s: got %d want %d\n", label, got, want); + failures++; + } +} + +static void test_enforce_hub(void) +{ + hybbx_networks_config_t net; + + hybbx_networks_config_defaults(&net); + net.telnet = 1; + net.ax25 = 1; + net.baycom = 1; + net.ardop = 1; + net.crdop = 1; + net.websocket = 0; + net.mains_proxy = 0; + + if (hybbx_networks_enforce_instance(&net) != HYBBX_OK) { + fprintf(stderr, "FAIL enforce returned error\n"); + failures++; + return; + } + + switch (hybbx_instance_role()) { + case HYBBX_INSTANCE_MAIN: + expect_int("main ax25 off", net.ax25, 0); + expect_int("main baycom off", net.baycom, 0); + expect_int("main ardop off", net.ardop, 0); + expect_int("main crdop off", net.crdop, 0); + expect_int("main telnet off", net.telnet, 0); + expect_int("main websocket on", net.websocket, 1); + expect_int("main circuit on", net.circuit, 1); + break; + + case HYBBX_INSTANCE_SECONDARY: + expect_int("secondary ax25 kept", net.ax25, 1); + expect_int("secondary baycom kept", net.baycom, 1); + expect_int("secondary telnet off", net.telnet, 0); + expect_int("secondary websocket off", net.websocket, 0); + expect_int("secondary mains_proxy on", net.mains_proxy, 1); + break; + + case HYBBX_INSTANCE_PROXY: + expect_int("proxy ax25 kept", net.ax25, 1); + expect_int("proxy baycom kept", net.baycom, 1); + expect_int("proxy ardop kept", net.ardop, 1); + expect_int("proxy crdop kept", net.crdop, 1); + expect_int("proxy websocket off", net.websocket, 0); + expect_int("proxy circuit on", net.circuit, 1); + break; + + default: + fprintf(stderr, "FAIL unknown role\n"); + failures++; + break; + } +} + +static void test_plugin_allow_hub(void) +{ + switch (hybbx_instance_role()) { + case HYBBX_INSTANCE_MAIN: + expect_int("main packet_radio denied", + hybbx_instance_plugin_allowed("packet_radio"), 0); + expect_int("main baycom denied", + hybbx_instance_plugin_allowed("baycom"), 0); + expect_int("main ardop denied", + hybbx_instance_plugin_allowed("ardop"), 0); + expect_int("main crdop denied", + hybbx_instance_plugin_allowed("crdop"), 0); + expect_int("main websocket", hybbx_instance_plugin_allowed("websocket"), 1); + expect_int("main telnet", hybbx_instance_plugin_allowed("telnet"), 0); + expect_int("main user_bbx", hybbx_instance_offers_user_bbx(), 1); + break; + + case HYBBX_INSTANCE_SECONDARY: + expect_int("secondary packet_radio", + hybbx_instance_plugin_allowed("packet_radio"), 1); + expect_int("secondary baycom", + hybbx_instance_plugin_allowed("baycom"), 1); + expect_int("secondary mains_proxy", + hybbx_instance_plugin_allowed("mains_proxy"), 1); + expect_int("secondary telnet", + hybbx_instance_plugin_allowed("telnet"), 0); + expect_int("secondary websocket", + hybbx_instance_plugin_allowed("websocket"), 0); + expect_int("secondary user_bbx", hybbx_instance_offers_user_bbx(), 0); + break; + + case HYBBX_INSTANCE_PROXY: + expect_int("proxy packet_radio", + hybbx_instance_plugin_allowed("packet_radio"), 1); + expect_int("proxy baycom", + hybbx_instance_plugin_allowed("baycom"), 1); + expect_int("proxy telnet", hybbx_instance_plugin_allowed("telnet"), 1); + expect_int("proxy ssh", hybbx_instance_plugin_allowed("ssh"), 1); + expect_int("proxy websocket", + hybbx_instance_plugin_allowed("websocket"), 0); + expect_int("proxy mains_proxy", + hybbx_instance_plugin_allowed("mains_proxy"), 1); + expect_int("proxy user_bbx", hybbx_instance_offers_user_bbx(), 0); + break; + + default: + break; + } +} + +static void test_standalone_main(void) +{ + hybbx_networks_config_t net; + + if (hybbx_instance_role() != HYBBX_INSTANCE_MAIN) { + return; + } + + hybbx_instance_set_standalone(1); + + expect_int("standalone baycom allowed", + hybbx_instance_plugin_allowed("baycom"), 1); + expect_int("standalone packet_radio allowed", + hybbx_instance_plugin_allowed("packet_radio"), 1); + expect_int("standalone ardop allowed", + hybbx_instance_plugin_allowed("ardop"), 1); + + hybbx_networks_config_defaults(&net); + net.telnet = 1; + net.ax25 = 1; + net.baycom = 1; + net.ardop = 1; + net.crdop = 1; + + if (hybbx_networks_enforce_instance(&net) != HYBBX_OK) { + fprintf(stderr, "FAIL standalone enforce returned error\n"); + failures++; + hybbx_instance_set_standalone(0); + return; + } + + expect_int("standalone ax25 kept", net.ax25, 1); + expect_int("standalone baycom kept", net.baycom, 1); + expect_int("standalone ardop kept", net.ardop, 1); + expect_int("standalone crdop kept", net.crdop, 1); + + hybbx_instance_set_standalone(0); +} + +int main(void) +{ + printf("instance tests role=%s binary=%s\n", + hybbx_instance_role_name(), + hybbx_instance_binary_name()); + test_plugin_allow_hub(); + test_enforce_hub(); + test_standalone_main(); + return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/tests/test_mains_proxy.c b/tests/test_mains_proxy.c new file mode 100644 index 0000000..b95a904 --- /dev/null +++ b/tests/test_mains_proxy.c @@ -0,0 +1,189 @@ +#include "hybbx/mains_proxy.h" +#include "hybbx/util.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static unsigned g_failures; + +static void check_str(const char *label, const char *got, const char *want) +{ + if (got == NULL || want == NULL || strcmp(got, want) != 0) { + fprintf(stderr, "FAIL %s: got '%s' want '%s'\n", + label, got != NULL ? got : "(null)", want); + g_failures++; + } +} + +static void check_int(const char *label, int got, int want) +{ + if (got != want) { + fprintf(stderr, "FAIL %s: got %d want %d\n", label, got, want); + g_failures++; + } +} + +static void check_uint(const char *label, unsigned got, unsigned want) +{ + if (got != want) { + fprintf(stderr, "FAIL %s: got %u want %u\n", label, got, want); + g_failures++; + } +} + +static void parse_one_peer(const char *section, + hybbx_mains_proxy_peer_config_t *out) +{ + hybbx_result_t rc = hybbx_mains_proxy_peer_parse(section, out); + if (rc != HYBBX_OK) { + fprintf(stderr, "FAIL peer_parse returned %d for '%s'\n", (int)rc, section); + g_failures++; + } +} + +int main(void) +{ + hybbx_mains_proxy_peer_config_t peer; + const char *multi; + const char *sep; + char section_buf[1024]; + + /* 1. semicolon-separated section (serialized by hybbx_config_format_section) */ + parse_one_peer( + "peer_id=alpha;circuit_host=127.0.0.1;circuit_port=7323;" + "link_id=alpha-main;link_password=secret;enabled=yes;wire=circuit;" + "duplex=full;use_secondary=yes", + &peer); + check_str("semicolon peer_id", peer.peer_id, "alpha"); + check_str("semicolon circuit_host", peer.circuit_host, "127.0.0.1"); + check_uint("semicolon circuit_port", peer.circuit_port, 7323u); + check_str("semicolon link_id", peer.link_id, "alpha-main"); + check_str("semicolon link_password", peer.link_password, "secret"); + check_int("semicolon enabled", peer.enabled, 1); + check_int("semicolon wire", (int)peer.wire, (int)HYBBX_MAINS_PROXY_WIRE_CIRCUIT); + check_int("semicolon duplex", (int)peer.duplex, (int)HYBBX_MAINS_PROXY_DUPLEX_FULL); + check_int("semicolon use_secondary", peer.use_secondary, 1); + + /* 2. newline-separated section */ + parse_one_peer( + "peer_id=beta\ncircuit_host=127.0.0.2\ncircuit_port=7324\n" + "link_id=beta-main\nlink_password=other\nenabled=no\n" + "wire=ax25\nduplex=half\nuse_secondary=no", + &peer); + check_str("newline peer_id", peer.peer_id, "beta"); + check_str("newline circuit_host", peer.circuit_host, "127.0.0.2"); + check_uint("newline circuit_port", peer.circuit_port, 7324u); + check_str("newline link_id", peer.link_id, "beta-main"); + check_str("newline link_password", peer.link_password, "other"); + check_int("newline enabled", peer.enabled, 0); + check_int("newline wire", (int)peer.wire, (int)HYBBX_MAINS_PROXY_WIRE_AX25); + check_int("newline duplex", (int)peer.duplex, (int)HYBBX_MAINS_PROXY_DUPLEX_HALF); + check_int("newline use_secondary", peer.use_secondary, 0); + + /* 3. multiple peers separated by instance separator */ + multi = + "peer_id=first;circuit_host=127.0.0.1;enabled=yes\x1e" + "peer_id=second;circuit_host=127.0.0.2;enabled=yes\x1e" + "enabled=no\x1e" + "peer_id=fourth;circuit_host=127.0.0.4;enabled=yes"; + + sep = multi; + for (unsigned i = 0; i < 4; i++) { + const char *next = strchr(sep, HYBBX_MAINS_PROXY_INSTANCE_SEP); + size_t len; + + if (next != NULL) { + len = (size_t)(next - sep); + } else { + len = strlen(sep); + } + + if (len >= sizeof(section_buf)) { + len = sizeof(section_buf) - 1; + } + memcpy(section_buf, sep, len); + section_buf[len] = '\0'; + + parse_one_peer(section_buf, &peer); + + switch (i) { + case 0: + check_str("multi[0] peer_id", peer.peer_id, "first"); + check_str("multi[0] circuit_host", peer.circuit_host, "127.0.0.1"); + check_int("multi[0] enabled", peer.enabled, 1); + break; + case 1: + check_str("multi[1] peer_id", peer.peer_id, "second"); + check_str("multi[1] circuit_host", peer.circuit_host, "127.0.0.2"); + check_int("multi[1] enabled", peer.enabled, 1); + break; + case 2: + /* section with only enabled=no: disabled peer preserves defaults */ + check_int("multi[2] enabled", peer.enabled, 0); + check_str("multi[2] peer_id", peer.peer_id, ""); + check_str("multi[2] circuit_host", peer.circuit_host, ""); + break; + case 3: + check_str("multi[3] peer_id", peer.peer_id, "fourth"); + check_str("multi[3] circuit_host", peer.circuit_host, "127.0.0.4"); + check_int("multi[3] enabled", peer.enabled, 1); + break; + } + + if (next == NULL) { + break; + } + sep = next + 1; + } + + /* 4. missing credentials: empty peer_id, empty link_password, invalid port */ + parse_one_peer( + "circuit_host=127.0.0.1;circuit_port=99999;link_id=main;enabled=yes", + &peer); + check_str("missing peer_id", peer.peer_id, ""); + check_str("missing link_password", peer.link_password, ""); + /* invalid port should fall back to default */ + check_uint("invalid port default", peer.circuit_port, + (unsigned)HYBBX_CIRCUIT_DEFAULT_PORT); + + /* 5. legacy host/port keys: host maps to circuit_host, port is ignored */ + parse_one_peer( + "peer_id=legacy;circuit_host=127.0.0.1;host=127.0.0.9;port=9999;" + "link_id=main;link_password=secret;enabled=yes", + &peer); + check_str("legacy host ignored", peer.circuit_host, "127.0.0.1"); + check_uint("legacy port ignored", peer.port, 9999u); + + /* 6. empty section should yield defaults */ + parse_one_peer("", &peer); + check_str("empty peer_id", peer.peer_id, ""); + check_int("empty enabled default", peer.enabled, 1); + check_int("empty wire default", (int)peer.wire, + (int)HYBBX_MAINS_PROXY_WIRE_CIRCUIT); + check_int("empty duplex default", (int)peer.duplex, + (int)HYBBX_MAINS_PROXY_DUPLEX_FULL); + check_int("empty use_secondary default", peer.use_secondary, 1); + + + /* 7. mesh active: stopped mesh reports inactive */ + hybbx_mains_proxy_mesh_stop(NULL); + check_int("mesh_active when stopped", hybbx_mains_proxy_mesh_active(), 0); + + /* 8. peer parse leaves legacy host intact when circuit_host is set */ + parse_one_peer( + "peer_id=explicit;circuit_host=127.0.0.1;host=127.0.0.9;" + "link_id=main;link_password=secret;enabled=yes", + &peer); + check_str("circuit_host wins over host", peer.circuit_host, "127.0.0.1"); + + if (g_failures != 0) { + fprintf(stderr, "%u failure(s)\n", g_failures); + return 1; + } + + puts("test_mains_proxy: ok"); + return 0; +} + +/* Test kept separate above; this file ends after main() in the original. */ diff --git a/tests/test_mains_proxy_e2e.c b/tests/test_mains_proxy_e2e.c new file mode 100644 index 0000000..bb93f0c --- /dev/null +++ b/tests/test_mains_proxy_e2e.c @@ -0,0 +1,360 @@ +#include "hybbx/mains_proxy.h" +#include "hybbx/circuit_tcp.h" +#include "hybbx/util.h" + +#include <arpa/inet.h> +#include <sys/socket.h> + +#include <errno.h> +#include <fcntl.h> +#include <poll.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> +#include <dirent.h> + +static unsigned g_failures; +static pid_t g_pid_a = -1; +static pid_t g_pid_b = -1; + +static void check_int(const char *label, int got, int want) +{ + if (got != want) { + fprintf(stderr, "FAIL %s: got %d want %d\n", label, got, want); + g_failures++; + } +} + +static int write_file(const char *path, const char *text) +{ + FILE *fp = fopen(path, "w"); + if (fp == NULL) { + return -1; + } + if (fputs(text, fp) == EOF) { + fclose(fp); + return -1; + } + fclose(fp); + return 0; +} + +static int mkdir_p(const char *path) +{ + char tmp[512]; + char *p; + size_t len; + + if (path == NULL || path[0] == '\0') { + return -1; + } + + hybbx_strlcpy(tmp, path, sizeof(tmp)); + len = strlen(tmp); + if (tmp[len - 1] == '/') { + tmp[len - 1] = '\0'; + } + + for (p = tmp + 1; *p != '\0'; p++) { + if (*p == '/') { + *p = '\0'; + (void)mkdir(tmp, 0755); + *p = '/'; + } + } + (void)mkdir(tmp, 0755); + return 0; +} + +static int copy_file(const char *src, const char *dst) +{ + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "cp '%s' '%s'", src, dst); + return system(cmd); +} + +static int copy_dir(const char *src, const char *dst) +{ + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "cp -r '%s' '%s'", src, dst); + return system(cmd); +} + +static void write_main_ini(const char *root, const char *name, + unsigned circuit_port, unsigned peer_port, + const char *local_link_id, + const char *remote_link_id, + unsigned websocket_port) +{ + char path[512]; + char text[2048]; + + snprintf(path, sizeof(path), "%s/data", root); + mkdir_p(path); + snprintf(path, sizeof(path), "%s/logs", root); + mkdir_p(path); + snprintf(path, sizeof(path), "%s/mail", root); + mkdir_p(path); + copy_file("/home/akb/Code2/10-PROJECTS/hyBBX/share/commands.yaml", root); + copy_file("/home/akb/Code2/10-PROJECTS/hyBBX/share/areas.yaml", root); + copy_dir("/home/akb/Code2/10-PROJECTS/hyBBX/text", root); + + snprintf(path, sizeof(path), "%s/hybbx.ini", root); + snprintf(text, sizeof(text), + "[service]\n" + "name = %s\n" + "login_announce = no\n" + "\n" + "[storage]\n" + "backend = flatfile\n" + "path = data\n" + "\n" + "[auth]\n" + "auto_login = yes\n" + "\n" + "[log]\n" + "enabled = yes\n" + "dir = logs\n" + "level = info\n" + "\n" + "[security]\n" + "enabled = yes\n" + "telnet = no\n" + "ssh = no\n" + "websocket = no\n" + "circuit = yes\n" + "\n" + "[mail]\n" + "enabled = yes\n" + "path = mail\n" + "\n" + "[networks]\n" + "telnet = no\n" + "ssh = no\n" + "websocket = no\n" + "ax25 = no\n" + "baycom = no\n" + "tmodem = no\n" + "ardop = no\n" + "crdop = no\n" + "circuit = yes\n" + "mains_proxy = yes\n" + "\n" + "[circuit]\n" + "enabled = yes\n" + "bind = 127.0.0.1\n" + "port = %u\n" + "link_auth = yes\n" + "link_password = secret\n" + "max_links = 8\n" + "balance = yes\n" + "\n" + "[transport.websocket]\n" + "bind = 127.0.0.1\n" + "port = %u\n" + "\n" + "[transport.mains_proxy1]\n" + "peer_id = peer\n" + "circuit_host = 127.0.0.1\n" + "circuit_port = %u\n" + "link_id = %s\n" + "link_password = secret\n" + "wire = circuit\n" + "duplex = full\n" + "\n" + "[transport.mains_proxy2]\n" + "peer_id = peer\n" + "link_id = %s\n" + "link_password = secret\n", + name, circuit_port, websocket_port, peer_port, + local_link_id, remote_link_id); + + if (write_file(path, text) != 0) { + fprintf(stderr, "FAIL cannot write %s\n", path); + g_failures++; + } +} + +static pid_t start_hybbxd(const char *binary, const char *root) +{ + pid_t pid = fork(); + if (pid < 0) { + return -1; + } + if (pid == 0) { + char config_path[512]; + int null_fd = open("/dev/null", O_RDONLY); + int log_fd; + char log_path[512]; + + snprintf(log_path, sizeof(log_path), "%s/logs/test-stderr.log", root); + log_fd = open(log_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + + setsid(); + if (null_fd >= 0) { + dup2(null_fd, STDIN_FILENO); + close(null_fd); + } + if (log_fd >= 0) { + dup2(log_fd, STDOUT_FILENO); + dup2(log_fd, STDERR_FILENO); + close(log_fd); + } + + snprintf(config_path, sizeof(config_path), "%s/hybbx.ini", root); + setenv("HYBBX_ROOT", root, 1); + execl(binary, binary, "-c", config_path, "-f", (char *)NULL); + _exit(127); + } + return pid; +} + +static void stop_process(pid_t pid) +{ + if (pid > 0) { + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + } +} + +static void cleanup_dirs(const char *a, const char *b) +{ + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "rm -rf '%s' '%s'", a, b); + (void)system(cmd); +} + +static int log_contains(const char *root, const char *needle) +{ + char path[512]; + char buf[4096]; + FILE *fp; + size_t n; + + snprintf(path, sizeof(path), "%s/logs/test-stderr.log", root); + fp = fopen(path, "r"); + if (fp == NULL) { + return 0; + } + + while ((n = fread(buf, 1, sizeof(buf) - 1, fp)) > 0) { + buf[n] = '\0'; + if (strstr(buf, needle) != NULL) { + fclose(fp); + return 1; + } + } + fclose(fp); + return 0; +} + +static int wait_for_port(unsigned port, unsigned timeout_ms) +{ + unsigned elapsed = 0; + int fd; + struct sockaddr_in addr; + + while (elapsed < timeout_ms) { + fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons((uint16_t)port); + inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); + + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == 0) { + close(fd); + return 0; + } + close(fd); + usleep(100000); + elapsed += 100; + } + return -1; +} + +int main(int argc, char **argv) +{ + const char *binary = (argc > 1) ? argv[1] : "/tmp/hybbx-p01-build/src/hybbxd"; + char root_a[256]; + char root_b[256]; + int active; + unsigned elapsed; + + snprintf(root_a, sizeof(root_a), "/tmp/hybbx-e2e-a-%d", (int)getpid()); + snprintf(root_b, sizeof(root_b), "/tmp/hybbx-e2e-b-%d", (int)getpid()); + + cleanup_dirs(root_a, root_b); + + /* Main A connects outbound as main-a; Main B accepts id=main-a. */ + write_main_ini(root_a, "main-a", 17323, 17324, "main-a", "main-b", 14591); + /* Main B connects outbound as main-b; Main A accepts id=main-b. */ + write_main_ini(root_b, "main-b", 17324, 17323, "main-b", "main-a", 14592); + + g_pid_a = start_hybbxd(binary, root_a); + g_pid_b = start_hybbxd(binary, root_b); + + check_int("start main-a", g_pid_a > 0, 1); + check_int("start main-b", g_pid_b > 0, 1); + + if (g_pid_a <= 0 || g_pid_b <= 0) { + stop_process(g_pid_b); + cleanup_dirs(root_a, root_b); + return 1; + } + + /* Wait for both circuit hubs to accept connections. */ + check_int("port a ready", wait_for_port(17323, 5000), 0); + check_int("port b ready", wait_for_port(17324, 5000), 0); + + /* Wait for mains_proxy to establish outbound links on each Main. */ + { + int a_linked = 0; + int b_linked = 0; + elapsed = 0; + while (elapsed < 10000) { + a_linked = log_contains(root_a, "[mains_proxy] linked to peer"); + b_linked = log_contains(root_b, "[mains_proxy] linked to peer"); + if (a_linked && b_linked) { + break; + } + usleep(100000); + elapsed += 100; + } + check_int("main-a linked via mains_proxy", a_linked, 1); + check_int("main-b linked via mains_proxy", b_linked, 1); + } + + /* P0.2: verify both Mains accepted the peer link via HBX auth. */ + { + int a_has_main_b = log_contains(root_a, "link authenticated id=main-b"); + int b_has_main_a = log_contains(root_b, "link authenticated id=main-a"); + check_int("main-a sees main-b authenticated", a_has_main_b, 1); + check_int("main-b sees main-a authenticated", b_has_main_a, 1); + } + + + stop_process(g_pid_a); + stop_process(g_pid_b); + + if (g_failures == 0) { + cleanup_dirs(root_a, root_b); + } else { + fprintf(stderr, " left dirs: %s %s\n", root_a, root_b); + } + + if (g_failures != 0) { + fprintf(stderr, "%u failure(s)\n", g_failures); + return 1; + } + + puts("test_mains_proxy_e2e: ok"); + return 0; +} diff --git a/tests/test_storage_smoke.c b/tests/test_storage_smoke.c new file mode 100644 index 0000000..77fd3cf --- /dev/null +++ b/tests/test_storage_smoke.c @@ -0,0 +1,235 @@ +/* + * test_storage_smoke.c — P0.4: flatfile + SQLite core smoke + * + * Tests: open/close, register_user, find_user, resolve_user, + * count_level, update_user, delete_user, session_begin/end. + * Runs against both flatfile and SQLite backends in temporary directories. + */ +#include "hybbx/storage.h" +#include "hybbx/auth.h" +#include "hybbx/util.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> + +static unsigned g_failures; + +static void check_rc(const char *label, hybbx_result_t got, hybbx_result_t want) +{ + if (got != want) { + fprintf(stderr, "FAIL %s: got %d want %d\n", label, (int)got, (int)want); + g_failures++; + } +} + +static void check_str(const char *label, const char *got, const char *want) +{ + if (got == NULL || want == NULL || strcmp(got, want) != 0) { + fprintf(stderr, "FAIL %s: got '%s' want '%s'\n", + label, got != NULL ? got : "(null)", want); + g_failures++; + } +} + +static void check_uint64(const char *label, uint64_t got, uint64_t want) +{ + if (got != want) { + fprintf(stderr, "FAIL %s: got %llu want %llu\n", + label, (unsigned long long)got, (unsigned long long)want); + g_failures++; + } +} + +static void check_nonzero(const char *label, uint64_t val) +{ + if (val == 0) { + fprintf(stderr, "FAIL %s: expected non-zero\n", label); + g_failures++; + } +} + +static void mkdir_p(const char *path) +{ + char tmp[512]; + char *p; + size_t len; + + hybbx_strlcpy(tmp, path, sizeof(tmp)); + len = strlen(tmp); + if (len > 0 && tmp[len - 1] == '/') { + tmp[len - 1] = '\0'; + } + for (p = tmp + 1; *p; p++) { + if (*p == '/') { + *p = '\0'; + mkdir(tmp, 0755); + *p = '/'; + } + } + mkdir(tmp, 0755); +} + +static void rm_rf(const char *path) +{ + char cmd[512]; + snprintf(cmd, sizeof(cmd), "rm -rf '%s'", path); + (void)system(cmd); +} + +static hybbx_storage_t *open_backend(hybbx_storage_backend_kind_t backend, + const char *path) +{ + hybbx_storage_options_t opts; + + memset(&opts, 0, sizeof(opts)); + opts.backend = backend; + opts.path = path; + return hybbx_storage_open(&opts); +} + +/* + * Run the full CRUD + session smoke against one backend. + */ +static void smoke_backend(hybbx_storage_backend_kind_t backend, + const char *label, + const char *path) +{ + hybbx_storage_t *st; + hybbx_user_registration_t reg; + hybbx_user_record_t user; + hybbx_user_record_t found; + hybbx_session_record_t sess; + size_t count; + char test_user[64]; + + snprintf(test_user, sizeof(test_user), "smk_%s", label); + + rm_rf(path); + mkdir_p(path); + + st = open_backend(backend, path); + if (st == NULL) { + fprintf(stderr, "FAIL %s: storage_open returned NULL\n", label); + g_failures++; + return; + } + + check_uint64("backend kind", + (uint64_t)hybbx_storage_backend(st), (uint64_t)backend); + + /* register_user — level defaults to HYBBX_LEVEL_USER */ + memset(®, 0, sizeof(reg)); + hybbx_strlcpy(reg.username, test_user, sizeof(reg.username)); + hybbx_strlcpy(reg.password, "testpass123", sizeof(reg.password)); + hybbx_strlcpy(reg.nickname, test_user, sizeof(reg.nickname)); + hybbx_strlcpy(reg.full_name, "Smoke Test", sizeof(reg.full_name)); + hybbx_strlcpy(reg.country, "DE", sizeof(reg.country)); + hybbx_strlcpy(reg.location, "localhost", sizeof(reg.location)); + hybbx_strlcpy(reg.email, "smoke@test.local", sizeof(reg.email)); + + check_rc("register_user", + hybbx_storage_register_user(st, ®, &user), HYBBX_OK); + check_nonzero("user id", user.id); + check_str("user username", user.username, test_user); + check_uint64("user level", (uint64_t)user.level, + (uint64_t)HYBBX_LEVEL_USER); + + /* find_user */ + memset(&found, 0, sizeof(found)); + check_rc("find_user", + hybbx_storage_find_user(st, test_user, &found), HYBBX_OK); + check_uint64("found id matches", found.id, user.id); + check_str("found username", found.username, test_user); + + /* find_user nonexistent */ + check_rc("find_user missing", + hybbx_storage_find_user(st, "no_such_user", &found), + HYBBX_ERR_NOT_FOUND); + + /* resolve_user (by name) */ + memset(&found, 0, sizeof(found)); + check_rc("resolve_user", + hybbx_storage_resolve_user(st, test_user, &found), HYBBX_OK); + check_uint64("resolve id matches", found.id, user.id); + + /* count_level */ + count = 0; + check_rc("count_level", + hybbx_storage_count_level(st, HYBBX_LEVEL_USER, &count), + HYBBX_OK); + /* count >= 1 (our user) */ + + /* update_user */ + user.level = HYBBX_LEVEL_SYSOP; + check_rc("update_user", + hybbx_storage_update_user(st, &user), HYBBX_OK); + + memset(&found, 0, sizeof(found)); + check_rc("find after update", + hybbx_storage_find_user(st, test_user, &found), HYBBX_OK); + check_uint64("updated level", (uint64_t)found.level, + (uint64_t)HYBBX_LEVEL_SYSOP); + + /* session_begin */ + memset(&sess, 0, sizeof(sess)); + check_rc("session_begin", + hybbx_storage_session_begin(st, &user, "test", &sess), HYBBX_OK); + check_nonzero("session id", sess.session_id); + + /* session_end */ + check_rc("session_end", + hybbx_storage_session_end(st, sess.session_id), HYBBX_OK); + + /* delete_user */ + check_rc("delete_user", + hybbx_storage_delete_user(st, user.id), HYBBX_OK); + + /* find after delete */ + check_rc("find after delete", + hybbx_storage_find_user(st, test_user, &found), + HYBBX_ERR_NOT_FOUND); + + /* Persistence: close and reopen, verify functional */ + hybbx_storage_close(st); + st = open_backend(backend, path); + if (st == NULL) { + fprintf(stderr, "FAIL %s: reopen returned NULL\n", label); + g_failures++; + return; + } + + /* Re-register after reopen to verify backend is functional */ + check_rc("re-register after reopen", + hybbx_storage_register_user(st, ®, &user), HYBBX_OK); + check_nonzero("re-registered id", user.id); + + hybbx_storage_delete_user(st, user.id); + hybbx_storage_close(st); + + rm_rf(path); +} + +int main(void) +{ + char path_ff[256]; + char path_sql[256]; + + snprintf(path_ff, sizeof(path_ff), "/tmp/hybbx-smoke-flatfile-%d", + (int)getpid()); + snprintf(path_sql, sizeof(path_sql), "/tmp/hybbx-smoke-sqlite-%d", + (int)getpid()); + + smoke_backend(HYBBX_STORAGE_FLATFILE, "flatfile", path_ff); + smoke_backend(HYBBX_STORAGE_SQLITE, "sqlite", path_sql); + + if (g_failures != 0) { + fprintf(stderr, "%u failure(s)\n", g_failures); + return 1; + } + + puts("test_storage_smoke: ok"); + return 0; +} diff --git a/tests/test_util.c b/tests/test_util.c new file mode 100644 index 0000000..cbd4e1b --- /dev/null +++ b/tests/test_util.c @@ -0,0 +1,152 @@ +#include "hybbx/limits.h" +#include "hybbx/log.h" +#include "hybbx/util.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static unsigned g_failures; + +static void check_int(const char *label, int got, int want) +{ + if (got != want) { + fprintf(stderr, "FAIL %s: got %d want %d\n", label, got, want); + g_failures++; + } +} + +static void check_str(const char *label, const char *got, const char *want) +{ + if (got == NULL || want == NULL || strcmp(got, want) != 0) { + fprintf(stderr, "FAIL %s: got '%s' want '%s'\n", + label, got != NULL ? got : "(null)", want); + g_failures++; + } +} + +int main(void) +{ + check_int("true yes", hybbx_bool_is_true("yes"), 1); + check_int("true enable", hybbx_bool_is_true("enable"), 1); + check_int("false no", hybbx_bool_is_false("no"), 1); + check_int("false disable", hybbx_bool_is_false("disable"), 1); + + check_int("parse yes", hybbx_parse_bool("yes", 0), 1); + check_int("parse false", hybbx_parse_bool("false", 1), 0); + check_int("parse empty", hybbx_parse_bool("", 1), 1); + check_int("parse garbage", hybbx_parse_bool("maybe", 0), 0); + + check_str("bool yes", hybbx_bool_to_string(1), HYBBX_BOOL_YES); + check_str("bool no", hybbx_bool_to_string(0), HYBBX_BOOL_NO); + + check_str("result ok", hybbx_result_name(HYBBX_OK), "ok"); + check_str("result invalid", hybbx_result_name(HYBBX_ERR_INVALID), "invalid"); + check_str("result unknown", hybbx_result_name((hybbx_result_t)-99), "unknown"); + + check_int("log parse debug", (int)hybbx_log_parse_level("debug"), + (int)HYBBX_LOG_DEBUG); + check_int("log parse stats", (int)hybbx_log_parse_level("stats"), + (int)HYBBX_LOG_STATS); + check_int("log parse info", (int)hybbx_log_parse_level("info"), + (int)HYBBX_LOG_INFO); + check_int("log parse warn", (int)hybbx_log_parse_level("warn"), + (int)HYBBX_LOG_WARN); + check_int("log parse unknown", (int)hybbx_log_parse_level("verbose"), + (int)HYBBX_LOG_WARN); + check_str("log name stats", hybbx_log_level_name(HYBBX_LOG_STATS), "stats"); + + { + char path[HYBBX_PATH_MAX]; + const char *home = getenv("HOME"); + + if (hybbx_path_expand(path, sizeof(path), NULL) == HYBBX_OK) { + check_str("path expand null", path, HYBBX_DIR_DATA); + } + if (home != NULL && home[0] != '\0') { + char want[HYBBX_PATH_MAX]; + + snprintf(want, sizeof(want), "%s/custom", home); + if (hybbx_path_expand(path, sizeof(path), "~/custom") == HYBBX_OK) { + check_str("path expand tilde", path, want); + } + if (hybbx_path_expand(path, sizeof(path), "~") == HYBBX_OK) { + check_str("path expand home only", path, home); + } + } + } + + { + char path[HYBBX_PATH_MAX]; + char want[HYBBX_PATH_MAX]; + + hybbx_install_root_set("/opt/hybbx"); + if (hybbx_path_resolve(path, sizeof(path), HYBBX_DIR_DATA) == HYBBX_OK) { + snprintf(want, sizeof(want), "/opt/hybbx/%s", HYBBX_DIR_DATA); + check_str("path resolve data", path, want); + } + if (hybbx_path_resolve(path, sizeof(path), HYBBX_DIR_LOGS) == HYBBX_OK) { + snprintf(want, sizeof(want), "/opt/hybbx/%s", HYBBX_DIR_LOGS); + check_str("path resolve logs", path, want); + } + hybbx_install_root_set(NULL); + } + + { + char os_name[HYBBX_PATH_MAX]; + + if (hybbx_platform_os_name(os_name, sizeof(os_name)) != HYBBX_OK) { + fprintf(stderr, "FAIL platform os name: lookup failed\n"); + g_failures++; + } else if (os_name[0] == '\0') { + fprintf(stderr, "FAIL platform os name: empty\n"); + g_failures++; +#if defined(__linux__) + } else if (strcmp(os_name, "Linux") != 0) { + fprintf(stderr, "FAIL platform os name: got '%s' want 'Linux'\n", + os_name); + g_failures++; +#endif + } + } + + { + struct tm tm_ref = {0}; + char time_buf[32]; + char date_buf[32]; + hybbx_time_format_t fmt; + + tm_ref.tm_year = 126; + tm_ref.tm_mon = 6; + tm_ref.tm_mday = 8; + tm_ref.tm_hour = 15; + tm_ref.tm_min = 4; + tm_ref.tm_sec = 5; + + hybbx_time_format_defaults(&fmt); + if (hybbx_time_format_time(time_buf, sizeof(time_buf), &tm_ref, + &fmt) == HYBBX_OK) { + check_str("time 24h seconds", time_buf, "15:04:05"); + } + + fmt.date_format = HYBBX_DATE_US; + if (hybbx_time_format_date(date_buf, sizeof(date_buf), &tm_ref, + &fmt) == HYBBX_OK) { + check_str("date us", date_buf, "07/08/2026"); + } + + fmt.date_format = HYBBX_DATE_ISO; + if (hybbx_time_format_date(date_buf, sizeof(date_buf), &tm_ref, + &fmt) == HYBBX_OK) { + check_str("date iso", date_buf, "2026/07/08"); + } + } + + if (g_failures != 0) { + fprintf(stderr, "%u test(s) failed\n", g_failures); + return EXIT_FAILURE; + } + + puts("test_util: ok"); + return EXIT_SUCCESS; +} |
