#include #include #include #include #include "xce/cluster.h" /* ── Test seam: mock connect function ───────────────────────────── */ static int s_connect_call_count = 0; static int s_connect_fail_n = 0; /* fail first N attempts, then succeed */ static char s_last_host[256] = ""; static int s_last_port = 0; static void* mock_connect(const char* host, int port, const char* ca_cert) { (void)ca_cert; strncpy(s_last_host, host, sizeof(s_last_host) - 1); s_last_port = port; s_connect_call_count++; if (s_connect_call_count <= s_connect_fail_n) return NULL; return (void*)0xDEADBEEF; /* non-NULL = success */ } static xce_cluster_config_t make_valid_config(void) { xce_cluster_config_t cfg = {0}; strncpy(cfg.candidates[0].hostname, "10.0.1.1", XCE_MAXHOSTNAME_LEN - 1); cfg.candidates[0].port = 7000; strncpy(cfg.candidates[1].hostname, "10.0.1.2", XCE_MAXHOSTNAME_LEN - 1); cfg.candidates[1].port = 7001; cfg.candidates_count = 2; strncpy(cfg.ca_cert_path, "/etc/xce/ca.pem", sizeof(cfg.ca_cert_path) - 1); strncpy(cfg.identity, "worker-01", sizeof(cfg.identity) - 1); return cfg; } /* ── Test cases ─────────────────────────────────────────────────── */ /* Test 1: validate rejects empty config */ static void test_validate_rejects_zero_candidates(void) { xce_cluster_config_t cfg = {0}; cfg.candidates_count = 0; TEST_ASSERT_NOT_EQUAL(0, xce_cluster_config_validate(&cfg)); printf("test_validate_rejects_zero_candidates: PASS\n"); } /* Test 2: validate rejects too many candidates */ static void test_validate_rejects_over_max_candidates(void) { xce_cluster_config_t cfg = {0}; cfg.candidates_count = MAX_HA_CANDIDATES + 1; TEST_ASSERT_NOT_EQUAL(0, xce_cluster_config_validate(&cfg)); printf("test_validate_rejects_over_max_candidates: PASS\n"); } /* Test 3: validate rejects missing identity */ static void test_validate_rejects_empty_identity(void) { xce_cluster_config_t cfg = make_valid_config(); cfg.identity[0] = '\0'; TEST_ASSERT_NOT_EQUAL(0, xce_cluster_config_validate(&cfg)); printf("test_validate_rejects_empty_identity: PASS\n"); } /* Test 4: validate accepts valid config */ static void test_validate_accepts_valid_config(void) { xce_cluster_config_t cfg = make_valid_config(); TEST_ASSERT_EQUAL_INT(0, xce_cluster_config_validate(&cfg)); printf("test_validate_accepts_valid_config: PASS\n"); } /* Test 5: connect succeeds on first candidate */ static void test_connect_succeeds_first_candidate(void) { xce_cluster_config_t cfg = make_valid_config(); s_connect_call_count = 0; s_connect_fail_n = 0; xce_cluster_set_connect_fn(mock_connect); void* ep = xce_connect_cluster(&cfg); TEST_ASSERT_NOT_NULL(ep); TEST_ASSERT_EQUAL_INT(1, s_connect_call_count); TEST_ASSERT_EQUAL_STRING("10.0.1.1", s_last_host); TEST_ASSERT_EQUAL_INT(7000, s_last_port); xce_cluster_set_connect_fn(NULL); printf("test_connect_succeeds_first_candidate: PASS\n"); } /* Test 6: connect falls back to second candidate when first fails */ static void test_connect_fallback_to_second_candidate(void) { xce_cluster_config_t cfg = make_valid_config(); s_connect_call_count = 0; s_connect_fail_n = 1; /* first attempt fails */ xce_cluster_set_connect_fn(mock_connect); void* ep = xce_connect_cluster(&cfg); TEST_ASSERT_NOT_NULL(ep); TEST_ASSERT_EQUAL_INT(2, s_connect_call_count); TEST_ASSERT_EQUAL_STRING("10.0.1.2", s_last_host); xce_cluster_set_connect_fn(NULL); printf("test_connect_fallback_to_second_candidate: PASS\n"); } /* Test 7: connect returns NULL when all candidates fail */ static void test_connect_returns_null_all_fail(void) { xce_cluster_config_t cfg = make_valid_config(); s_connect_call_count = 0; s_connect_fail_n = 999; /* all fail */ xce_cluster_set_connect_fn(mock_connect); void* ep = xce_connect_cluster(&cfg); TEST_ASSERT_NULL(ep); xce_cluster_set_connect_fn(NULL); printf("test_connect_returns_null_all_fail: PASS\n"); } /* Test 8: reconnect state machine — IDLE → CONNECTING → CONNECTED */ static void test_reconnect_step_reaches_connected(void) { xce_cluster_config_t cfg = make_valid_config(); xce_reconnect_ctx_t rctx; s_connect_call_count = 0; s_connect_fail_n = 0; xce_cluster_set_connect_fn(mock_connect); xce_reconnect_init(&rctx, &cfg); TEST_ASSERT_EQUAL_INT(XCE_RECONNECT_IDLE, (int)rctx.state); int steps = 0; while (rctx.state != XCE_RECONNECT_CONNECTED && rctx.state != XCE_RECONNECT_FAILED && steps < 100) { xce_reconnect_step(&rctx); steps++; } TEST_ASSERT_EQUAL_INT(XCE_RECONNECT_CONNECTED, (int)rctx.state); TEST_ASSERT_NOT_NULL(rctx.endpoint); xce_reconnect_fini(&rctx); xce_cluster_set_connect_fn(NULL); printf("test_reconnect_step_reaches_connected: PASS\n"); } /* Test 9: reconnect state machine backs off on failure */ static void test_reconnect_step_backs_off_on_failure(void) { xce_cluster_config_t cfg = make_valid_config(); cfg.candidates_count = 1; /* only one candidate so it hits backoff */ xce_reconnect_ctx_t rctx; s_connect_call_count = 0; s_connect_fail_n = 999; /* always fail */ xce_cluster_set_connect_fn(mock_connect); xce_reconnect_init(&rctx, &cfg); xce_reconnect_step(&rctx); /* IDLE → CONNECTING (attempt connect) */ xce_reconnect_step(&rctx); /* CONNECTING → BACKOFF (all candidates exhausted) */ /* When mock is set, sleep is skipped — state must be BACKOFF */ TEST_ASSERT_EQUAL_INT(XCE_RECONNECT_BACKOFF, (int)rctx.state); TEST_ASSERT_TRUE(rctx.backoff_ms > 0); xce_reconnect_fini(&rctx); xce_cluster_set_connect_fn(NULL); printf("test_reconnect_step_backs_off_on_failure: PASS\n"); } /* ── Test group entry point ─────────────────────────────────────── */ void test_cluster(void) { RUN_TEST(test_validate_rejects_zero_candidates); RUN_TEST(test_validate_rejects_over_max_candidates); RUN_TEST(test_validate_rejects_empty_identity); RUN_TEST(test_validate_accepts_valid_config); RUN_TEST(test_connect_succeeds_first_candidate); RUN_TEST(test_connect_fallback_to_second_candidate); RUN_TEST(test_connect_returns_null_all_fail); RUN_TEST(test_reconnect_step_reaches_connected); RUN_TEST(test_reconnect_step_backs_off_on_failure); }