/* test_config_repl.c — Unity tests for config replication Phase 1 */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include "config_repl.h" #include "myconf.h" #include "xce/cluster_def.h" /* Per-test directory helpers — not Unity setUp/tearDown (those are in * controller_test.c). Each test that needs a temp dir calls cr_setup() * at the top and cr_teardown() via the label before returning. */ static char g_tmpdir[256]; static char g_conf_dir[300]; /* tmpdir string for load_primary_config tests (no conf/current) */ static char tmpdir[256]; static void cr_setup(void) { snprintf(g_tmpdir, sizeof(g_tmpdir), "/tmp/test_cr_%d_%ld", (int)getpid(), (long)time(NULL)); mkdir(g_tmpdir, 0700); snprintf(g_conf_dir, sizeof(g_conf_dir), "%s/conf", g_tmpdir); mkdir(g_conf_dir, 0700); } static void cr_teardown(void) { char cmd[512]; snprintf(cmd, sizeof(cmd), "rm -rf %s", g_tmpdir); system(cmd); g_tmpdir[0] = '\0'; g_conf_dir[0] = '\0'; } /* ── conf_read_current_filename ─────────────────── */ static void test_conf_read_current_filename_missing(void) { cr_setup(); char out[256] = {0}; int rc = conf_read_current_filename(g_tmpdir, out, sizeof(out)); TEST_ASSERT_EQUAL_INT(-ENOENT, rc); cr_teardown(); printf("test_conf_read_current_filename_missing: PASS\n"); } static void test_conf_read_current_filename_empty_file(void) { cr_setup(); char ptr_path[400]; snprintf(ptr_path, sizeof(ptr_path), "%s/current", g_conf_dir); int fd = open(ptr_path, O_WRONLY | O_CREAT | O_TRUNC, 0600); TEST_ASSERT_NOT_EQUAL(-1, fd); close(fd); char out[256] = {0}; int rc = conf_read_current_filename(g_tmpdir, out, sizeof(out)); TEST_ASSERT_EQUAL_INT(-ENODATA, rc); cr_teardown(); printf("test_conf_read_current_filename_empty_file: PASS\n"); } static void test_conf_read_current_filename_path_separator(void) { cr_setup(); char ptr_path[400]; snprintf(ptr_path, sizeof(ptr_path), "%s/current", g_conf_dir); FILE *f = fopen(ptr_path, "w"); fprintf(f, "cluster.bin/../evil\n"); fclose(f); char out[256] = {0}; int rc = conf_read_current_filename(g_tmpdir, out, sizeof(out)); TEST_ASSERT_EQUAL_INT(-EINVAL, rc); cr_teardown(); printf("test_conf_read_current_filename_path_separator: PASS\n"); } static void test_conf_read_current_filename_bad_prefix(void) { cr_setup(); char ptr_path[400]; snprintf(ptr_path, sizeof(ptr_path), "%s/current", g_conf_dir); FILE *f = fopen(ptr_path, "w"); fprintf(f, "badprefix.aabbccddeeff\n"); fclose(f); char out[256] = {0}; int rc = conf_read_current_filename(g_tmpdir, out, sizeof(out)); TEST_ASSERT_EQUAL_INT(-EINVAL, rc); cr_teardown(); printf("test_conf_read_current_filename_bad_prefix: PASS\n"); } static void test_conf_read_current_filename_wrong_hex_length(void) { cr_setup(); char ptr_path[400]; snprintf(ptr_path, sizeof(ptr_path), "%s/current", g_conf_dir); FILE *f = fopen(ptr_path, "w"); fprintf(f, "cluster.bin.aabb\n"); /* only 4 hex chars, need 12 */ fclose(f); char out[256] = {0}; int rc = conf_read_current_filename(g_tmpdir, out, sizeof(out)); TEST_ASSERT_EQUAL_INT(-EINVAL, rc); cr_teardown(); printf("test_conf_read_current_filename_wrong_hex_length: PASS\n"); } /* ── conf_save_config + conf_read_current_filename round-trip ─ */ static void test_conf_save_creates_pointer_file(void) { cr_setup(); const uint8_t data[] = "fake capnproto config blob"; size_t len = sizeof(data) - 1; uint8_t sha256[32]; SHA256(data, len, sha256); myconf_t myconf; memset(&myconf, 0, sizeof(myconf)); myconf.workdir = g_tmpdir; myconf.name = "ctrl0"; myconf.conf_keep_n = 2; myconf.conffile = NULL; int rc = conf_save_config(&myconf, data, len, sha256); TEST_ASSERT_EQUAL_INT(0, rc); /* conf/current must exist */ char ptr_path[400]; snprintf(ptr_path, sizeof(ptr_path), "%s/current", g_conf_dir); TEST_ASSERT_EQUAL_INT(0, access(ptr_path, F_OK)); free(myconf.conffile); cr_teardown(); printf("test_conf_save_creates_pointer_file: PASS\n"); } static void test_conf_save_filename_has_12_hex_suffix(void) { cr_setup(); const uint8_t data[] = "another fake blob"; size_t len = sizeof(data) - 1; uint8_t sha256[32]; SHA256(data, len, sha256); myconf_t myconf; memset(&myconf, 0, sizeof(myconf)); myconf.workdir = g_tmpdir; myconf.name = "ctrl0"; myconf.conf_keep_n = 2; myconf.conffile = NULL; conf_save_config(&myconf, data, len, sha256); char fname[256] = {0}; int rc = conf_read_current_filename(g_tmpdir, fname, sizeof(fname)); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_INT(0, strncmp(fname, "cluster.bin.", 12)); const char *hex = fname + 12; TEST_ASSERT_EQUAL_INT(12, (int)strlen(hex)); TEST_ASSERT_EQUAL_INT(12, (int)strspn(hex, "0123456789abcdef")); free(myconf.conffile); cr_teardown(); printf("test_conf_save_filename_has_12_hex_suffix: PASS\n"); } /* ── conf_trim_old_versions ─────────────────────── */ static void test_conf_trim_keeps_only_n_versions(void) { cr_setup(); myconf_t myconf; memset(&myconf, 0, sizeof(myconf)); myconf.workdir = g_tmpdir; myconf.name = "ctrl0"; myconf.conf_keep_n = 2; myconf.conffile = NULL; uint8_t sha256[32]; for (int i = 0; i < 3; i++) { char data[32]; snprintf(data, sizeof(data), "config version %d", i); SHA256((uint8_t *)data, strlen(data), sha256); usleep(15000); /* ensure distinct mtime ordering */ conf_save_config(&myconf, (uint8_t *)data, strlen(data), sha256); free(myconf.conffile); myconf.conffile = NULL; } /* Count cluster.bin.* files (exclude .tmp) */ int count = 0; DIR *d = opendir(g_conf_dir); TEST_ASSERT_NOT_NULL(d); struct dirent *e; while ((e = readdir(d)) != NULL) { if (strncmp(e->d_name, "cluster.bin.", 12) == 0 && strstr(e->d_name, ".tmp") == NULL) count++; } closedir(d); TEST_ASSERT_EQUAL_INT(2, count); free(myconf.conffile); cr_teardown(); printf("test_conf_trim_keeps_only_n_versions: PASS\n"); } /* ── conf_get_saved_tag ─────────────────────────── */ static void test_conf_get_saved_tag_no_config(void) { cr_setup(); uint64_t tag = conf_get_saved_tag(g_tmpdir); TEST_ASSERT_EQUAL_UINT64(0, tag); cr_teardown(); printf("test_conf_get_saved_tag_no_config: PASS\n"); } static void test_conf_get_saved_tag_after_save(void) { cr_setup(); const uint8_t data[] = "tag test blob"; size_t len = sizeof(data) - 1; uint8_t sha256[32]; SHA256(data, len, sha256); myconf_t myconf; memset(&myconf, 0, sizeof(myconf)); myconf.workdir = g_tmpdir; myconf.name = "ctrl0"; myconf.conf_keep_n = 2; myconf.conffile = NULL; conf_save_config(&myconf, data, len, sha256); free(myconf.conffile); uint64_t tag = conf_get_saved_tag(g_tmpdir); TEST_ASSERT_NOT_EQUAL(0, tag); /* Tag must equal first 8 bytes of sha256 in host order */ uint64_t expected; memcpy(&expected, sha256, 8); /* be64toh conversion */ #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ expected = __builtin_bswap64(expected); #endif TEST_ASSERT_EQUAL_UINT64(expected, tag); cr_teardown(); printf("test_conf_get_saved_tag_after_save: PASS\n"); } /* ── conf_check_stale ───────────────────────────── */ static void test_conf_check_stale_disabled(void) { cr_setup(); /* timeout 0 = disabled, never stale */ TEST_ASSERT_EQUAL_INT(0, conf_check_stale(g_tmpdir, 0)); cr_teardown(); printf("test_conf_check_stale_disabled: PASS\n"); } static void test_conf_check_stale_fresh(void) { cr_setup(); const uint8_t data[] = "stale test blob"; uint8_t sha256[32]; SHA256(data, sizeof(data) - 1, sha256); myconf_t myconf; memset(&myconf, 0, sizeof(myconf)); myconf.workdir = g_tmpdir; myconf.name = "ctrl0"; myconf.conf_keep_n = 2; myconf.conffile = NULL; conf_save_config(&myconf, data, sizeof(data) - 1, sha256); free(myconf.conffile); /* Just saved — not stale with 24h timeout */ TEST_ASSERT_EQUAL_INT(0, conf_check_stale(g_tmpdir, 86400)); cr_teardown(); printf("test_conf_check_stale_fresh: PASS\n"); } static void test_conf_check_stale_aged(void) { cr_setup(); const uint8_t data[] = "old config blob"; uint8_t sha256[32]; SHA256(data, sizeof(data) - 1, sha256); myconf_t myconf; memset(&myconf, 0, sizeof(myconf)); myconf.workdir = g_tmpdir; myconf.name = "ctrl0"; myconf.conf_keep_n = 2; myconf.conffile = NULL; conf_save_config(&myconf, data, sizeof(data) - 1, sha256); free(myconf.conffile); /* Age the versioned file by 20 seconds via utime */ char fname[256] = {0}; conf_read_current_filename(g_tmpdir, fname, sizeof(fname)); char ver_path[512]; snprintf(ver_path, sizeof(ver_path), "%s/%s", g_conf_dir, fname); struct utimbuf old_time; old_time.actime = time(NULL) - 20; old_time.modtime = time(NULL) - 20; utime(ver_path, &old_time); /* 5-second threshold → stale */ TEST_ASSERT_EQUAL_INT(1, conf_check_stale(g_tmpdir, 5)); /* 30-second threshold → not stale */ TEST_ASSERT_EQUAL_INT(0, conf_check_stale(g_tmpdir, 30)); cr_teardown(); printf("test_conf_check_stale_aged: PASS\n"); } /* ── config_contains_reserved_name ─────────────── */ static void test_config_contains_reserved_name_absent(void) { /* NULL config — no reserved name */ TEST_ASSERT_EQUAL_INT(0, config_contains_reserved_name(NULL)); printf("test_config_contains_reserved_name_absent: PASS\n"); } /* ── sanitise_cn ────────────────────────────────── */ static void test_sanitise_cn_printable(void) { char *out = sanitise_cn("ctrl01.example.com", 64); TEST_ASSERT_EQUAL_STRING("ctrl01.example.com", out); printf("test_sanitise_cn_printable: PASS\n"); } static void test_sanitise_cn_control_chars(void) { char *out = sanitise_cn("evil\ninjection", 64); /* newline replaced with '?' */ TEST_ASSERT_NOT_NULL(strchr(out, '?')); TEST_ASSERT_NULL(strchr(out, '\n')); printf("test_sanitise_cn_control_chars: PASS\n"); } static void test_sanitise_cn_truncated(void) { char *out = sanitise_cn("abcdefghij", 5); TEST_ASSERT_EQUAL_INT(5, (int)strlen(out)); printf("test_sanitise_cn_truncated: PASS\n"); } /* ── load_primary_config (via test-only helper) ── */ #ifdef XCE_TESTING static void test_load_primary_config_missing_returns_error(void) { /* Create a minimal cluster_t with no conf/current file */ cluster_t c; memset(&c, 0, sizeof(c)); myconf_t myconf; memset(&myconf, 0, sizeof(myconf)); /* Use a fresh tmpdir with no conf/current */ snprintf(tmpdir, sizeof(tmpdir), "/tmp/test_lpc_%d_%ld", (int)getpid(), (long)time(NULL)); mkdir(tmpdir, 0700); myconf.workdir = tmpdir; myconf.name = "ctrl0"; myconf.conf_keep_n = 2; myconf.conf_stale_timeout = 86400; myconf.conffile = NULL; c.myconf = &myconf; /* No conf/current exists — must fail (not FATAL in test context) */ int rc = load_primary_config_for_test(&c); TEST_ASSERT_TRUE(rc < 0); /* conffile must still be NULL (no heap allocation on failure) */ TEST_ASSERT_NULL(myconf.conffile); /* Cleanup */ char cmd[512]; snprintf(cmd, sizeof(cmd), "rm -rf %s", tmpdir); system(cmd); printf("test_load_primary_config_missing_returns_error: PASS\n"); } #endif /* ── dispatch ───────────────────────────────────── */ void test_config_repl(void) { RUN_TEST(test_conf_read_current_filename_missing); RUN_TEST(test_conf_read_current_filename_empty_file); RUN_TEST(test_conf_read_current_filename_path_separator); RUN_TEST(test_conf_read_current_filename_bad_prefix); RUN_TEST(test_conf_read_current_filename_wrong_hex_length); RUN_TEST(test_conf_save_creates_pointer_file); RUN_TEST(test_conf_save_filename_has_12_hex_suffix); RUN_TEST(test_conf_trim_keeps_only_n_versions); RUN_TEST(test_conf_get_saved_tag_no_config); RUN_TEST(test_conf_get_saved_tag_after_save); RUN_TEST(test_conf_check_stale_disabled); RUN_TEST(test_conf_check_stale_fresh); RUN_TEST(test_conf_check_stale_aged); RUN_TEST(test_config_contains_reserved_name_absent); RUN_TEST(test_sanitise_cn_printable); RUN_TEST(test_sanitise_cn_control_chars); RUN_TEST(test_sanitise_cn_truncated); #ifdef XCE_TESTING RUN_TEST(test_load_primary_config_missing_returns_error); #endif }