test(raft-adapter-conformance): full config-push reply integrity coverage

Adds the remaining 3 of 4 handle_repl_config_push reject-path tests
(malformed request, missing bytes/sha256, conf_save_config failure via
a deterministic ENOTDIR fixture), the EAGAIN-still-accepts test, and
term-adoption-on-accept (task 2.2's own test only covered the reject
case). Requirement 2.4 (unknown-outcome handling) is already covered
by the pre-existing test_wait_all_with_timeout_slow_threads, cited
rather than duplicated.

1055 -> 1060 tests, 0 failures.
This commit is contained in:
Rongsong Shen 2026-07-29 13:13:25 +00:00
parent 1e7a7312bf
commit 65afa04aaf
2 changed files with 328 additions and 1 deletions

View file

@ -3728,6 +3728,325 @@ static void test_config_push_reply_higher_term_adopts_and_steps_down(void)
printf("test_config_push_reply_higher_term_adopts_and_steps_down: PASS\n");
}
/* raft-adapter-conformance task 2.3 (Req 2.2, 2.3, 2.5, 7.4): task 2.1's own
* test (above) proved only ONE of the four handle_repl_config_push reject
* early-returns actually invokes the send-hook (SHA-256 mismatch). This
* closes the gap for the FIRST reject condition: a malformed request
* (body_which mismatch or a NULL replconfigpushrequestmsg payload). */
static void test_config_push_malformed_request_sends_explicit_reject_reply(void)
{
char tmp[256];
strcpy(tmp, "/tmp/xce_cr23_malformed_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
cluster_t *c = t42_make_cluster(tmp);
TEST_ASSERT_NOT_NULL(c);
t43_attach_myconf(c, tmp);
atomic_store(&c->ha.role, CTRL_STANDBY);
atomic_store(&c->ha.term, 11);
/* Malformed: body_which claims a config-push request, but the payload
* pointer itself is NULL -- the adapter's own dereference guard. */
xce_request_t req;
memset(&req, 0, sizeof(req));
req.body_which = Request_body_replconfigpushrequestmsg;
req.body.replconfigpushrequestmsg = NULL;
g_cpr21_hook_called = 0;
config_push_reply_send_hook = cpr21_reply_capture;
int rc = handle_repl_config_push(c, /*ep*/NULL, &req);
config_push_reply_send_hook = NULL; /* uninstall before any assertion can fail */
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
TEST_ASSERT_TRUE_MESSAGE(g_cpr21_hook_called,
"raft-adapter-conformance 2.3: a malformed config-push request must "
"send an explicit ReplConfigPushReply, not silence it entirely");
TEST_ASSERT_EQUAL_INT_MESSAGE(0, g_cpr21_hook_accepted,
"the reply for a malformed config-push request must carry accepted=0");
TEST_ASSERT_EQUAL_UINT64(11, g_cpr21_hook_term);
t43_free_myconf(c);
t42_teardown(c);
char cmd[512];
snprintf(cmd, sizeof(cmd), "rm -rf %s", tmp);
system(cmd);
printf("test_config_push_malformed_request_sends_explicit_reject_reply: PASS\n");
}
/* raft-adapter-conformance task 2.3 (Req 2.2, 2.3, 2.5, 7.4): the SECOND
* reject condition -- a well-formed envelope whose payload is missing bytes
* and/or a sha256 -- must also send an explicit reject reply. */
static void test_config_push_missing_bytes_sends_explicit_reject_reply(void)
{
char tmp[256];
strcpy(tmp, "/tmp/xce_cr23_missing_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
cluster_t *c = t42_make_cluster(tmp);
TEST_ASSERT_NOT_NULL(c);
t43_attach_myconf(c, tmp);
atomic_store(&c->ha.role, CTRL_STANDBY);
atomic_store(&c->ha.term, 12);
/* Well-formed envelope, but bytes/len/sha256 are entirely absent. */
repl_config_push_req_t rcpr;
memset(&rcpr, 0, sizeof(rcpr));
rcpr.version_tag = 0x4242;
rcpr.raw_bytes = NULL;
rcpr.raw_bytes_len = 0;
rcpr.raw_sha256 = NULL;
xce_request_t req;
memset(&req, 0, sizeof(req));
req.body_which = Request_body_replconfigpushrequestmsg;
req.body.replconfigpushrequestmsg = &rcpr;
g_cpr21_hook_called = 0;
config_push_reply_send_hook = cpr21_reply_capture;
int rc = handle_repl_config_push(c, /*ep*/NULL, &req);
config_push_reply_send_hook = NULL; /* uninstall before any assertion can fail */
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
TEST_ASSERT_TRUE_MESSAGE(g_cpr21_hook_called,
"raft-adapter-conformance 2.3: missing bytes/sha256 must send an "
"explicit ReplConfigPushReply, not silence it entirely");
TEST_ASSERT_EQUAL_INT_MESSAGE(0, g_cpr21_hook_accepted,
"the reply for missing bytes/sha256 must carry accepted=0");
TEST_ASSERT_EQUAL_UINT64(12, g_cpr21_hook_term);
t43_free_myconf(c);
t42_teardown(c);
char cmd[512];
snprintf(cmd, sizeof(cmd), "rm -rf %s", tmp);
system(cmd);
printf("test_config_push_missing_bytes_sends_explicit_reject_reply: PASS\n");
}
/* raft-adapter-conformance task 2.3 (Req 2.2, 2.3, 2.5, 7.4): the FOURTH
* reject condition -- a local conf_save_config() failure -- must also send
* an explicit reject reply. conf_save_config is forced to fail
* deterministically by planting its target "conf" directory as a plain FILE
* instead: mkdir(conf_dir, 0700) then fails with EEXIST (tolerated --
* conf_save_config only bails on a DIFFERENT errno), but the very next step
* -- open()ing a path INSIDE conf_dir -- hits ENOTDIR because conf_dir is
* not actually a directory. This gives a real, deterministic failure without
* relying on filesystem permission semantics (chmod-based EACCES tricks are
* unreliable when the test runs as a privileged/sandboxed uid). */
static void test_config_push_save_failure_sends_explicit_reject_reply(void)
{
char tmp[256];
strcpy(tmp, "/tmp/xce_cr23_savefail_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
cluster_t *c = t42_make_cluster(tmp);
TEST_ASSERT_NOT_NULL(c);
t43_attach_myconf(c, tmp);
atomic_store(&c->ha.role, CTRL_STANDBY);
atomic_store(&c->ha.term, 13);
/* persist_init (t42_make_cluster) already creates tmp/ctrl-test/data/...
* which brings tmp/ctrl-test itself into existence -- tolerate EEXIST. */
char instdir[512];
snprintf(instdir, sizeof(instdir), "%s/ctrl-test", tmp);
TEST_ASSERT_TRUE_MESSAGE(mkdir(instdir, 0700) == 0 || errno == EEXIST,
"must be able to ensure the myconf effective_dir exists for the fixture");
char conf_as_file[560];
snprintf(conf_as_file, sizeof(conf_as_file), "%s/conf", instdir);
int trap_fd = open(conf_as_file, O_WRONLY | O_CREAT | O_TRUNC, 0600);
TEST_ASSERT_TRUE_MESSAGE(trap_fd >= 0, "must be able to plant the conf-as-file trap");
close(trap_fd);
uint8_t payload[16];
memset(payload, 0xCD, sizeof(payload));
uint8_t sha[32];
SHA256(payload, sizeof(payload), sha);
repl_config_push_req_t rcpr;
memset(&rcpr, 0, sizeof(rcpr));
rcpr.version_tag = 0x5353;
rcpr.raw_bytes = payload;
rcpr.raw_bytes_len = sizeof(payload);
rcpr.raw_sha256 = sha;
xce_request_t req;
memset(&req, 0, sizeof(req));
req.body_which = Request_body_replconfigpushrequestmsg;
req.body.replconfigpushrequestmsg = &rcpr;
g_cpr21_hook_called = 0;
config_push_reply_send_hook = cpr21_reply_capture;
int rc = handle_repl_config_push(c, /*ep*/NULL, &req);
config_push_reply_send_hook = NULL; /* uninstall before any assertion can fail */
TEST_ASSERT_TRUE_MESSAGE(rc != 0,
"the conf-as-file trap must make conf_save_config fail (rc != 0)");
TEST_ASSERT_TRUE_MESSAGE(g_cpr21_hook_called,
"raft-adapter-conformance 2.3: a conf_save_config failure must send "
"an explicit ReplConfigPushReply, not silence it entirely");
TEST_ASSERT_EQUAL_INT_MESSAGE(0, g_cpr21_hook_accepted,
"the reply for a conf_save_config failure must carry accepted=0");
TEST_ASSERT_EQUAL_UINT64(13, g_cpr21_hook_term);
t43_free_myconf(c);
t42_teardown(c);
char cmd[512];
snprintf(cmd, sizeof(cmd), "rm -rf %s", tmp);
system(cmd);
printf("test_config_push_save_failure_sends_explicit_reject_reply: PASS\n");
}
/* raft-adapter-conformance task 2.3 (Req 2.2, 2.3, 2.5, 7.5): the
* `-EAGAIN`-from-config_activate_committed activation-defer branch must
* still send accepted=1 -- activation-deferred is NOT a rejection (the
* bytes WERE saved out-of-band successfully; only the separate,
* committed-marker-gated activation step is deferred). Fixture mirrors
* test_push_path_mismatch_does_not_preactivate (config-upgrade-snapshot
* task 4.3 remediation r1) but drives the REAL handle_repl_config_push
* entry point end-to-end instead of calling config_activate_committed
* directly, and observes the reply via the send-hook installed by task
* 2.1. */
static void test_config_push_eagain_activation_deferred_still_sends_accepted(void)
{
char tmp[256];
strcpy(tmp, "/tmp/xce_cr23_eagain_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
cluster_t *c = t42_make_cluster(tmp);
TEST_ASSERT_NOT_NULL(c);
t42_make_leader(c, /*term*/14, /*n_peers*/1);
TEST_ASSERT_EQUAL_INT(0,
commit_engine_init(&c->commit, &c->persist, 1, 8));
repl_set_commit_engine(&c->persist.repl, &c->commit);
t43_attach_myconf(c, tmp);
/* A committed marker references blob B (sha B) -- the config the
* cluster agreed to, whose bytes have NOT yet arrived here. */
size_t blenB = 0;
uint8_t *bytesB = t43_make_config_blob(&blenB);
bytesB[blenB - 1] ^= 0x5A; /* perturb -> distinct sha */
uint8_t shaB[32];
SHA256(bytesB, blenB, shaB);
uint64_t tagB; { uint64_t t; memcpy(&t, shaB, 8); tagB = be64toh(t); }
t43_commit_marker(c, tagB, shaB);
free(bytesB);
/* The push delivers blob A -- a DIFFERENT config than the committed
* marker references. conf_save_config succeeds (the transit sha256
* verification is self-consistent), but config_activate_committed()
* then defers with -EAGAIN because blob A doesn't hash-verify against
* the committed marker. */
size_t blenA = 0;
uint8_t *bytesA = t43_make_config_blob(&blenA);
uint8_t shaA[32];
SHA256(bytesA, blenA, shaA);
TEST_ASSERT_FALSE(memcmp(shaA, shaB, 32) == 0);
repl_config_push_req_t rcpr;
memset(&rcpr, 0, sizeof(rcpr));
{ uint64_t t; memcpy(&t, shaA, 8); rcpr.version_tag = be64toh(t); }
rcpr.raw_bytes = bytesA;
rcpr.raw_bytes_len = blenA;
rcpr.raw_sha256 = shaA;
xce_request_t req;
memset(&req, 0, sizeof(req));
req.body_which = Request_body_replconfigpushrequestmsg;
req.body.replconfigpushrequestmsg = &rcpr;
g_cpr21_hook_called = 0;
config_push_reply_send_hook = cpr21_reply_capture;
int rc = handle_repl_config_push(c, /*ep*/NULL, &req);
config_push_reply_send_hook = NULL; /* uninstall before any assertion can fail */
free(bytesA);
TEST_ASSERT_EQUAL_INT(0, rc);
/* Still fenced -- proves the -EAGAIN branch was genuinely taken, not
* the plain-success branch (which would clear the fence). */
TEST_ASSERT_EQUAL_INT(1, config_serve_fenced(c));
TEST_ASSERT_TRUE_MESSAGE(g_cpr21_hook_called,
"raft-adapter-conformance 2.3: the -EAGAIN activation-defer path "
"must still send an explicit ReplConfigPushReply");
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_cpr21_hook_accepted,
"raft-adapter-conformance 2.5/7.5: activation-deferred (-EAGAIN) is "
"NOT a rejection -- the reply must carry accepted=1");
TEST_ASSERT_EQUAL_UINT64(14, g_cpr21_hook_term);
t43_free_myconf(c);
commit_engine_fini(&c->commit);
t42_teardown(c);
char cmd[512];
snprintf(cmd, sizeof(cmd), "rm -rf %s", tmp);
system(cmd);
printf("test_config_push_eagain_activation_deferred_still_sends_accepted: PASS\n");
}
/* raft-adapter-conformance task 2.3 (Req 7.4, 7.5): task 2.2's own test
* (test_config_push_reply_higher_term_adopts_and_steps_down, above) only
* exercised the term-adoption check on a REJECTED (accepted=0) reply.
* Req 7.4/7.5 require the term check to fire "regardless of accept/reject"
* -- this closes that gap by driving the identical real dispatch path
* (myreq_handler_for_test) with accepted=1 instead. */
static void test_config_push_reply_higher_term_adopts_on_accept(void)
{
char tmp[256];
strcpy(tmp, "/tmp/xce_cr23_termacc_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
cluster_t *c = t42_make_cluster(tmp);
TEST_ASSERT_NOT_NULL(c);
atomic_store(&c->ha.role, CTRL_STANDBY);
atomic_store(&c->ha.term, 5);
ha_init_raft(&c->ha, c, /*n_peers*/3);
c->ha.raft.term = 5;
c->ha.raft.role = RAFT_LEADER;
endpoint_t ep;
memset(&ep, 0, sizeof(ep));
ep.owner = c;
/* Accept (accepted=1) carrying a higher term (9 > 5): the term-adoption
* check must fire on the ACCEPT branch exactly as it does on reject. */
io_packet_t *pkt =
cpr22_build_config_push_reply_pkt(/*accepted*/1,
/*version_tag*/0xBEEF,
/*term*/9);
TEST_ASSERT_NOT_NULL(pkt);
int rc = myreq_handler_for_test(&ep, pkt);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_UINT64_MESSAGE(9, c->ha.raft.term,
"raft-adapter-conformance 7.4: a config-push ACCEPT reply's higher "
"term must also be adopted via raft_on_higher_term_observed");
TEST_ASSERT_NOT_EQUAL_INT_MESSAGE(RAFT_LEADER, c->ha.raft.role,
"raft-adapter-conformance 7.5: leadership held at the lower term "
"must be stepped down once a higher term is observed, even on the "
"accept branch");
/* pkt is already fully freed at this point (myreq_handler_for_test ->
* xce_decode() unconditionally frees it) -- see the ownership note on
* test_config_push_reply_higher_term_adopts_and_steps_down above
* (bug-1482). Do NOT free pkt here. */
t42_teardown(c);
char cmd[512];
snprintf(cmd, sizeof(cmd), "rm -rf %s", tmp);
system(cmd);
printf("test_config_push_reply_higher_term_adopts_on_accept: PASS\n");
}
#endif /* XCE_TESTING */
/* ── raw capnp shims (Task A) ───────────────────── */
@ -7097,6 +7416,14 @@ void test_config_repl(void)
RUN_TEST(test_config_push_sha256_mismatch_sends_explicit_reject_reply);
/* raft-adapter-conformance task 2.2: config-push reply term adoption */
RUN_TEST(test_config_push_reply_higher_term_adopts_and_steps_down);
/* raft-adapter-conformance task 2.3: full config-push reply-integrity
* coverage -- remaining reject paths, the EAGAIN accept path, and the
* term-adoption check on the accept branch. */
RUN_TEST(test_config_push_malformed_request_sends_explicit_reject_reply);
RUN_TEST(test_config_push_missing_bytes_sends_explicit_reject_reply);
RUN_TEST(test_config_push_save_failure_sends_explicit_reject_reply);
RUN_TEST(test_config_push_eagain_activation_deferred_still_sends_accepted);
RUN_TEST(test_config_push_reply_higher_term_adopts_on_accept);
/* Task 2.1: OPCode_walCatchupEntry handler */
RUN_TEST(test_wal_catchup_entry_advances_applied_lsn);
RUN_TEST(test_wal_catchup_entry_error_skipped_no_crash);