test(ha-config-bootstrap): add failing startup validation tests (task 2.1)
RED-phase tests for 4 startup validation functions — all fail at link time because the functions under test do not exist yet: - validate_peer_count: even N>1 returns -EINVAL, odd N returns 0 - validate_cert_files: missing cert/key returns -ENOENT with full path in err_buf output param (asserted via strstr) - validate_vcpus: vcpus<=0 returns -EINVAL, positive returns 0 - validate_startup_config: vcpus key in config returns 0 (not fatal) and writes deprecation text into warn_buf (asserted via strstr)
This commit is contained in:
parent
560d8fa1c8
commit
bfa41eb135
4 changed files with 248 additions and 1 deletions
|
|
@ -31,7 +31,7 @@ Dependency order: startup validation (D) unblocks vtaskmgr before election (A);
|
||||||
- _Boundary: Controller FSM (ha.h)_
|
- _Boundary: Controller FSM (ha.h)_
|
||||||
|
|
||||||
- [ ] 2. Change D: Startup validation and ASAN fix
|
- [ ] 2. Change D: Startup validation and ASAN fix
|
||||||
- [ ] 2.1 Write failing tests for startup validation
|
- [x] 2.1 Write failing tests for startup validation
|
||||||
- In `src/controller/testsuite/test_ha_startup.c`: test even-N peer count returns `-EINVAL`; test cert missing returns `-ENOENT` with full path in message; test `--vcpus 0` returns `-EINVAL`; test `vcpus` key in config file logs WARN but does not fail
|
- In `src/controller/testsuite/test_ha_startup.c`: test even-N peer count returns `-EINVAL`; test cert missing returns `-ENOENT` with full path in message; test `--vcpus 0` returns `-EINVAL`; test `vcpus` key in config file logs WARN but does not fail
|
||||||
- `make test` fails (functions under test do not exist yet)
|
- `make test` fails (functions under test do not exist yet)
|
||||||
- _Requirements: 4.1, 4.2, 4.3, 4.4_
|
- _Requirements: 4.1, 4.2, 4.3, 4.4_
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ controller_unit_test_src = [
|
||||||
'testsuite/test_ctrl_metrics.c',
|
'testsuite/test_ctrl_metrics.c',
|
||||||
'testsuite/test_config_repl.c',
|
'testsuite/test_config_repl.c',
|
||||||
'testsuite/test_worker_join.c',
|
'testsuite/test_worker_join.c',
|
||||||
|
'testsuite/test_ha_startup.c',
|
||||||
'worker.c',
|
'worker.c',
|
||||||
'persist.c',
|
'persist.c',
|
||||||
'ha.c',
|
'ha.c',
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ extern void test_p3_manual_failover(void);
|
||||||
extern void test_ctrl_metrics(void);
|
extern void test_ctrl_metrics(void);
|
||||||
extern void test_config_repl(void);
|
extern void test_config_repl(void);
|
||||||
extern void test_worker_join(void);
|
extern void test_worker_join(void);
|
||||||
|
extern void test_ha_startup(void);
|
||||||
|
|
||||||
void setUp(void) { return; }
|
void setUp(void) { return; }
|
||||||
|
|
||||||
|
|
@ -137,6 +138,7 @@ int main(int argc, char* argv[])
|
||||||
TEST(test_ctrl_metrics);
|
TEST(test_ctrl_metrics);
|
||||||
TEST(test_config_repl);
|
TEST(test_config_repl);
|
||||||
TEST(test_worker_join);
|
TEST(test_worker_join);
|
||||||
|
TEST(test_ha_startup);
|
||||||
|
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
244
src/controller/testsuite/test_ha_startup.c
Normal file
244
src/controller/testsuite/test_ha_startup.c
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
/* test_ha_startup.c — Unity RED-phase tests for startup validation
|
||||||
|
*
|
||||||
|
* Tests verify: even-N peer count rejection, cert pre-flight -ENOENT with path
|
||||||
|
* in error message, --vcpus 0 rejection, and vcpus key in config logs WARN
|
||||||
|
* (via caller-supplied warn_buf) without failing.
|
||||||
|
*
|
||||||
|
* Requirements: 4.1, 4.2, 4.3, 4.4
|
||||||
|
* Functions under test: validate_peer_count(), validate_cert_files(),
|
||||||
|
* validate_vcpus(), validate_startup_config()
|
||||||
|
* These functions do NOT exist yet — this file establishes the contract
|
||||||
|
* for tasks 2.2 and 2.3.
|
||||||
|
*/
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <unity/unity.h>
|
||||||
|
|
||||||
|
/* Functions under test — declared here; implementations come in tasks 2.2/2.3 */
|
||||||
|
int validate_peer_count(int n_peers);
|
||||||
|
int validate_cert_files(const char *cert_path, const char *key_path,
|
||||||
|
char *err_buf, size_t err_len);
|
||||||
|
int validate_vcpus(int vcpus);
|
||||||
|
int validate_startup_config(const char *config_path,
|
||||||
|
char *warn_buf, size_t warn_len);
|
||||||
|
|
||||||
|
/* ── helpers ──────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
static char g_tmpdir[256];
|
||||||
|
|
||||||
|
static void startup_setup(void)
|
||||||
|
{
|
||||||
|
snprintf(g_tmpdir, sizeof(g_tmpdir), "/tmp/test_ha_startup_%d_%ld",
|
||||||
|
(int)getpid(), (long)time(NULL));
|
||||||
|
mkdir(g_tmpdir, 0700);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void startup_teardown(void)
|
||||||
|
{
|
||||||
|
char cmd[512];
|
||||||
|
snprintf(cmd, sizeof(cmd), "rm -rf %s", g_tmpdir);
|
||||||
|
system(cmd);
|
||||||
|
g_tmpdir[0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* write a tiny file inside g_tmpdir and return its path in out_path */
|
||||||
|
static void write_tmp_file(const char *name, const char *content,
|
||||||
|
char *out_path, size_t out_len)
|
||||||
|
{
|
||||||
|
snprintf(out_path, out_len, "%s/%s", g_tmpdir, name);
|
||||||
|
FILE *f = fopen(out_path, "w");
|
||||||
|
if (f) {
|
||||||
|
fputs(content, f);
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Req 4.1: even-N peer count rejection ─────────────────────────────── */
|
||||||
|
|
||||||
|
static void test_peer_count_2_is_invalid(void)
|
||||||
|
{
|
||||||
|
/* 2 peers is even (> 1) — must reject with -EINVAL.
|
||||||
|
* Raft quorum: a 2-node cluster cannot form a majority partition safely
|
||||||
|
* (1-of-2 means any single failure loses quorum), so n_peers=2 is invalid. */
|
||||||
|
TEST_ASSERT_EQUAL_INT(-EINVAL, validate_peer_count(2));
|
||||||
|
printf("test_peer_count_2_is_invalid: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_peer_count_4_is_invalid(void)
|
||||||
|
{
|
||||||
|
/* 4 peers is even — must reject with -EINVAL */
|
||||||
|
TEST_ASSERT_EQUAL_INT(-EINVAL, validate_peer_count(4));
|
||||||
|
printf("test_peer_count_4_is_invalid: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_peer_count_1_is_valid(void)
|
||||||
|
{
|
||||||
|
/* 1 peer (single-node mode) must be accepted */
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, validate_peer_count(1));
|
||||||
|
printf("test_peer_count_1_is_valid: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_peer_count_3_is_valid(void)
|
||||||
|
{
|
||||||
|
/* 3 peers is odd — majority quorum possible; must be accepted */
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, validate_peer_count(3));
|
||||||
|
printf("test_peer_count_3_is_valid: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Req 4.2: cert pre-flight — missing cert returns -ENOENT with path ── */
|
||||||
|
|
||||||
|
static void test_cert_missing_returns_enoent_with_path(void)
|
||||||
|
{
|
||||||
|
startup_setup();
|
||||||
|
|
||||||
|
const char *cert_path = "/tmp/test_ha_startup_nonexistent_cert.pem";
|
||||||
|
const char *key_path = "/tmp/test_ha_startup_nonexistent_key.pem";
|
||||||
|
|
||||||
|
char err_buf[256] = {0};
|
||||||
|
|
||||||
|
/* Neither file exists — must return -ENOENT and include cert_path in msg */
|
||||||
|
int rc = validate_cert_files(cert_path, key_path, err_buf, sizeof(err_buf));
|
||||||
|
TEST_ASSERT_EQUAL_INT(-ENOENT, rc);
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(err_buf, cert_path));
|
||||||
|
|
||||||
|
startup_teardown();
|
||||||
|
printf("test_cert_missing_returns_enoent_with_path: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_cert_present_key_missing_returns_enoent_with_path(void)
|
||||||
|
{
|
||||||
|
startup_setup();
|
||||||
|
|
||||||
|
/* Write a real cert file but leave key absent */
|
||||||
|
char cert_path[512];
|
||||||
|
write_tmp_file("ctrl.pem", "-----BEGIN CERTIFICATE-----\n", cert_path,
|
||||||
|
sizeof(cert_path));
|
||||||
|
|
||||||
|
const char *key_path = "/tmp/test_ha_startup_absent_key.pem";
|
||||||
|
|
||||||
|
char err_buf[256] = {0};
|
||||||
|
|
||||||
|
int rc = validate_cert_files(cert_path, key_path, err_buf, sizeof(err_buf));
|
||||||
|
TEST_ASSERT_EQUAL_INT(-ENOENT, rc);
|
||||||
|
/* Key is missing — error message must contain the key path */
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(err_buf, key_path));
|
||||||
|
|
||||||
|
startup_teardown();
|
||||||
|
printf("test_cert_present_key_missing_returns_enoent_with_path: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_cert_and_key_present_returns_zero(void)
|
||||||
|
{
|
||||||
|
startup_setup();
|
||||||
|
|
||||||
|
char cert_path[512], key_path[512];
|
||||||
|
write_tmp_file("ctrl.pem", "-----BEGIN CERTIFICATE-----\n", cert_path,
|
||||||
|
sizeof(cert_path));
|
||||||
|
write_tmp_file("ctrl.key", "-----BEGIN PRIVATE KEY-----\n", key_path,
|
||||||
|
sizeof(key_path));
|
||||||
|
|
||||||
|
char err_buf[256] = {0};
|
||||||
|
|
||||||
|
int rc = validate_cert_files(cert_path, key_path, err_buf, sizeof(err_buf));
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||||
|
|
||||||
|
startup_teardown();
|
||||||
|
printf("test_cert_and_key_present_returns_zero: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Req 4.3: --vcpus validation ─────────────────────────────────────── */
|
||||||
|
|
||||||
|
static void test_vcpus_zero_is_invalid(void)
|
||||||
|
{
|
||||||
|
/* --vcpus 0 must be rejected with -EINVAL */
|
||||||
|
TEST_ASSERT_EQUAL_INT(-EINVAL, validate_vcpus(0));
|
||||||
|
printf("test_vcpus_zero_is_invalid: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_vcpus_negative_is_invalid(void)
|
||||||
|
{
|
||||||
|
TEST_ASSERT_EQUAL_INT(-EINVAL, validate_vcpus(-1));
|
||||||
|
printf("test_vcpus_negative_is_invalid: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_vcpus_positive_is_valid(void)
|
||||||
|
{
|
||||||
|
/* --vcpus 4 is a normal value — must return 0 */
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, validate_vcpus(4));
|
||||||
|
printf("test_vcpus_positive_is_valid: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Req 4.4: vcpus key in config file — WARN only, no failure ────────── */
|
||||||
|
|
||||||
|
static void test_vcpus_key_in_config_warns_but_does_not_fail(void)
|
||||||
|
{
|
||||||
|
/* Create a temp config file with vcpus key using mkstemp */
|
||||||
|
char tmppath[] = "/tmp/test_config_XXXXXX";
|
||||||
|
int fd = mkstemp(tmppath);
|
||||||
|
const char *content = "vcpus = 4\nqueue_depth = 100\n";
|
||||||
|
write(fd, content, strlen(content));
|
||||||
|
close(fd);
|
||||||
|
|
||||||
|
char warn_buf[256] = {0};
|
||||||
|
int result = validate_startup_config(tmppath, warn_buf, sizeof(warn_buf));
|
||||||
|
|
||||||
|
/* vcpus key found — must NOT fail (return 0) */
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, result);
|
||||||
|
/* warn_buf must contain a deprecation message mentioning "vcpus" */
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(warn_buf, "vcpus"));
|
||||||
|
|
||||||
|
unlink(tmppath);
|
||||||
|
printf("test_vcpus_key_in_config_warns_but_does_not_fail: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_config_without_vcpus_key_has_no_warning(void)
|
||||||
|
{
|
||||||
|
char tmppath[] = "/tmp/test_config2_XXXXXX";
|
||||||
|
int fd = mkstemp(tmppath);
|
||||||
|
const char *content = "heartbeat_interval_ms = 3000\nqueue_depth = 100\n";
|
||||||
|
write(fd, content, strlen(content));
|
||||||
|
close(fd);
|
||||||
|
|
||||||
|
char warn_buf[256] = {0};
|
||||||
|
int result = validate_startup_config(tmppath, warn_buf, sizeof(warn_buf));
|
||||||
|
|
||||||
|
/* no vcpus key — must succeed with no warning */
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, result);
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, warn_buf[0]); /* warn_buf empty — no warning */
|
||||||
|
|
||||||
|
unlink(tmppath);
|
||||||
|
printf("test_config_without_vcpus_key_has_no_warning: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Test suite entry point ───────────────────────────────────────────── */
|
||||||
|
|
||||||
|
void test_ha_startup(void)
|
||||||
|
{
|
||||||
|
/* Req 4.1 — even-N peer count */
|
||||||
|
RUN_TEST(test_peer_count_2_is_invalid);
|
||||||
|
RUN_TEST(test_peer_count_4_is_invalid);
|
||||||
|
RUN_TEST(test_peer_count_1_is_valid);
|
||||||
|
RUN_TEST(test_peer_count_3_is_valid);
|
||||||
|
|
||||||
|
/* Req 4.2 — cert pre-flight with full path in error message */
|
||||||
|
RUN_TEST(test_cert_missing_returns_enoent_with_path);
|
||||||
|
RUN_TEST(test_cert_present_key_missing_returns_enoent_with_path);
|
||||||
|
RUN_TEST(test_cert_and_key_present_returns_zero);
|
||||||
|
|
||||||
|
/* Req 4.3 — vcpus validation */
|
||||||
|
RUN_TEST(test_vcpus_zero_is_invalid);
|
||||||
|
RUN_TEST(test_vcpus_negative_is_invalid);
|
||||||
|
RUN_TEST(test_vcpus_positive_is_valid);
|
||||||
|
|
||||||
|
/* Req 4.4 — vcpus key WARN via warn_buf; no failure */
|
||||||
|
RUN_TEST(test_vcpus_key_in_config_warns_but_does_not_fail);
|
||||||
|
RUN_TEST(test_config_without_vcpus_key_has_no_warning);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue