- load_primary_config(): resolves conf/current, reads raw bytes, computes SHA-256 tag, decodes Cap'n Proto via load_cluster_config_from_file(), checks reserved names, sets config_generation, checks staleness - myconf_fini(): frees conffile and cluster_config (calls free_ClusterConfig) - Remove static cf global from myconf.c; get_default_conf() now uses a local struct cluster_configuration* via load_cluster_config() - Add load_primary_config_for_test() under XCE_TESTING in config_repl.c to test path resolution without triggering FATAL - Add xce/cluster_def.h include to config_repl.c and test_config_repl.c so cluster_t is complete when XCE_TESTING is active - TDD: test_load_primary_config_missing_returns_error passes (18/18)
436 lines
13 KiB
C
436 lines
13 KiB
C
/* test_config_repl.c — Unity tests for config replication Phase 1 */
|
|
#define _GNU_SOURCE
|
|
#include <unity/unity.h>
|
|
#include <dirent.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <openssl/sha.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <time.h>
|
|
#include <utime.h>
|
|
#include <unistd.h>
|
|
|
|
#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
|
|
}
|