summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorinfo@mode42.com <info@mode42.com>2026-08-08 03:54:55 +0000
committerinfo@mode42.com <info@mode42.com>2026-08-08 03:54:55 +0000
commit20cb29c2f8c5c87bc590896854a20b1473ceb358 (patch)
tree2857f41513a56ad41af97b57362639298aa7f033 /tests
#2
Diffstat (limited to 'tests')
-rw-r--r--tests/CMakeLists.txt33
-rw-r--r--tests/test_config.c60
-rw-r--r--tests/test_instance.c170
-rw-r--r--tests/test_mains_proxy.c189
-rw-r--r--tests/test_mains_proxy_e2e.c360
-rw-r--r--tests/test_storage_smoke.c235
-rw-r--r--tests/test_util.c152
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(&reg, 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, &reg, &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, &reg, &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;
+}
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com