/* test_ctrlstatus.c — Unity tests for the controller-status (ctrlstatus) * libxce client reply path (controller-status task 3.1). * * Proves the reply path that xcecli_ctrlstatus relies on: * 1. A true ENCODE→DECODE round-trip of xce_ctrlstatus_t through the * generated Cap'n Proto codec (encode_CtrlStatus/write_CtrlStatus → * bytes → read_CtrlStatus/decode_CtrlStatus): every status field, * including the peers list and the configChecksum hex Text, survives * the wire (Req 1.1 — the client decodes every field a handler emits). * 2. xcecli_ctrlstatus(NULL, ...) returns a typed negative rather than a * partial/empty success (Req 7.3 — no reachable controller ⇒ typed * failure). * * A full live round-trip (client → running controller → handler reply) * needs a controller and is the E2E task; here we prove the encode/decode * round-trip + the no-context typed failure only. */ #include #include #include #include #include "client.h" #include "config.h" /* XCE_MAXHOSTNAME_LEN */ #include "request.h" #include "xce/types.h" /* generated Cap'n Proto codec for CtrlStatus / CtrlPeer */ #include "controller.capnp.h" /* ── encode→decode round-trip of xce_ctrlstatus_t ───────────────────────── */ static void test_ctrlstatus_roundtrip_all_fields(void) { /* Arrange: a fully-populated status with two configured peers */ xce_ctrlpeer_t p0 = { .name = "ctrl-a", .host = "10.0.0.1", .port = 9991 }; xce_ctrlpeer_t p1 = { .name = "ctrl-b", .host = "10.0.0.2", .port = 9992 }; xce_ctrlpeer_t* peerlist[2] = { &p0, &p1 }; xce_ctrlstatus_t in; memset(&in, 0, sizeof(in)); in.clusterName = "lab-cluster"; in.primaryName = "ctrl-a"; in.responderName = "ctrl-b"; in.responderRole = 3; /* STANDBY */ in.term = 42; in.configVersion = 0x1122334455667788ULL; in.configChecksum = "1122334455667788aabbccddeeff0011" "2233445566778899aabbccddeeff0011"; in.committedConfigVersion = 0x1122334455667788ULL; in.committedLsn = 5000; in.appliedLsn = 4999; in.fenced = 1; in.peers.count = 2; in.peers.list = peerlist; in.peerCount = 2; /* Act: encode into a capnp segment, serialize to bytes, then decode * a fresh struct back out — mirrors xce_encode/xce_decode. */ struct capn c; struct capn_segment* cs; struct CtrlStatus cap_in; CtrlStatus_ptr sp; capn_init_malloc(&c); cs = capn_root(&c).seg; memset(&cap_in, 0, sizeof(cap_in)); encode_CtrlStatus(cs, &cap_in, &in); sp = new_CtrlStatus(cs); write_CtrlStatus(&cap_in, sp); capn_setp(capn_root(&c), 0, sp.p); size_t sz = capn_size(&c); uint8_t* buf = (uint8_t*)malloc(sz); TEST_ASSERT_NOT_NULL(buf); TEST_ASSERT_EQUAL_size_t(sz, capn_write_mem(&c, buf, sz, 0)); capn_free(&c); /* decode side */ struct capn dc; struct CtrlStatus cap_out; CtrlStatus_ptr dp; xce_ctrlstatus_t out; capn_init_mem(&dc, buf, sz, 0); dp.p = capn_getp(capn_root(&dc), 0, 1); read_CtrlStatus(&cap_out, dp); memset(&out, 0, sizeof(out)); decode_CtrlStatus(&out, &cap_out); /* Assert: every scalar/text field survived */ TEST_ASSERT_NOT_NULL(out.clusterName); TEST_ASSERT_EQUAL_STRING("lab-cluster", out.clusterName); TEST_ASSERT_NOT_NULL(out.primaryName); TEST_ASSERT_EQUAL_STRING("ctrl-a", out.primaryName); TEST_ASSERT_NOT_NULL(out.responderName); TEST_ASSERT_EQUAL_STRING("ctrl-b", out.responderName); TEST_ASSERT_EQUAL_UINT8(3, out.responderRole); TEST_ASSERT_EQUAL_UINT64(42, out.term); TEST_ASSERT_EQUAL_UINT64(0x1122334455667788ULL, out.configVersion); TEST_ASSERT_NOT_NULL(out.configChecksum); TEST_ASSERT_EQUAL_STRING(in.configChecksum, out.configChecksum); TEST_ASSERT_EQUAL_UINT64(0x1122334455667788ULL, out.committedConfigVersion); TEST_ASSERT_EQUAL_UINT64(5000, out.committedLsn); TEST_ASSERT_EQUAL_UINT64(4999, out.appliedLsn); TEST_ASSERT_EQUAL_UINT32(1, out.fenced); TEST_ASSERT_EQUAL_UINT32(2, out.peerCount); /* Assert: the peers list survived with both entries */ TEST_ASSERT_EQUAL_INT(2, out.peers.count); TEST_ASSERT_NOT_NULL(out.peers.list); TEST_ASSERT_NOT_NULL(out.peers.list[0]); TEST_ASSERT_EQUAL_STRING("ctrl-a", out.peers.list[0]->name); TEST_ASSERT_EQUAL_STRING("10.0.0.1", out.peers.list[0]->host); TEST_ASSERT_EQUAL_UINT32(9991, out.peers.list[0]->port); TEST_ASSERT_NOT_NULL(out.peers.list[1]); TEST_ASSERT_EQUAL_STRING("ctrl-b", out.peers.list[1]->name); TEST_ASSERT_EQUAL_STRING("10.0.0.2", out.peers.list[1]->host); TEST_ASSERT_EQUAL_UINT32(9992, out.peers.list[1]->port); free_CtrlStatus(&out); capn_free(&dc); free(buf); printf("test_ctrlstatus_roundtrip_all_fields: PASS\n"); } /* An "unknown" primary (empty primaryName) must round-trip as empty/NULL, * never garbage — the client renders it as "unknown" (Req 1.3, 2.2). */ static void test_ctrlstatus_roundtrip_unknown_primary(void) { xce_ctrlstatus_t in; memset(&in, 0, sizeof(in)); in.clusterName = "lab-cluster"; in.primaryName = ""; /* unknown */ in.responderName = "ctrl-a"; in.responderRole = 1; /* ELECTING */ in.peers.count = 0; in.peers.list = NULL; in.peerCount = 0; struct capn c; struct capn_segment* cs; struct CtrlStatus cap_in; CtrlStatus_ptr sp; capn_init_malloc(&c); cs = capn_root(&c).seg; memset(&cap_in, 0, sizeof(cap_in)); encode_CtrlStatus(cs, &cap_in, &in); sp = new_CtrlStatus(cs); write_CtrlStatus(&cap_in, sp); capn_setp(capn_root(&c), 0, sp.p); size_t sz = capn_size(&c); uint8_t* buf = (uint8_t*)malloc(sz); TEST_ASSERT_NOT_NULL(buf); capn_write_mem(&c, buf, sz, 0); capn_free(&c); struct capn dc; struct CtrlStatus cap_out; CtrlStatus_ptr dp; xce_ctrlstatus_t out; capn_init_mem(&dc, buf, sz, 0); dp.p = capn_getp(capn_root(&dc), 0, 1); read_CtrlStatus(&cap_out, dp); memset(&out, 0, sizeof(out)); decode_CtrlStatus(&out, &cap_out); TEST_ASSERT_EQUAL_STRING("ctrl-a", out.responderName); TEST_ASSERT_EQUAL_UINT8(1, out.responderRole); /* empty primaryName decodes to NULL or "" — not a stale/guessed value */ if (out.primaryName != NULL) { TEST_ASSERT_EQUAL_STRING("", out.primaryName); } TEST_ASSERT_EQUAL_INT(0, out.peers.count); TEST_ASSERT_EQUAL_UINT32(0, out.peerCount); free_CtrlStatus(&out); capn_free(&dc); free(buf); printf("test_ctrlstatus_roundtrip_unknown_primary: PASS\n"); } /* ── no reachable controller ⇒ typed failure (Req 7.3) ──────────────────── */ static void test_xcecli_ctrlstatus_null_ctx_typed_failure(void) { xce_ctrlstatus_t* status = (xce_ctrlstatus_t*)0xdeadbeef; int rc = xcecli_ctrlstatus(NULL, &status); /* typed negative, not a partial/empty success */ TEST_ASSERT_EQUAL_INT(-EINVAL, rc); printf("test_xcecli_ctrlstatus_null_ctx_typed_failure: PASS\n"); } /* ── authoritative (primary-only) read: task 3.2 ─────────────────────────── */ /* The authoritative call must reject a NULL/unconnected ctx with the same * typed negative as the any-node read — it must NEVER hand back a hint reply * as the authoritative result (Req 9.2). The `status` out-param is left * untouched on failure (it still holds the poisoned sentinel below). */ static void test_xcecli_ctrlstatus_primary_null_ctx_typed_failure(void) { xce_ctrlstatus_t* status = (xce_ctrlstatus_t*)0xdeadbeef; int rc = xcecli_ctrlstatus_primary(NULL, &status); TEST_ASSERT_EQUAL_INT(-EINVAL, rc); /* never returned a hint reply as the authoritative result */ TEST_ASSERT_EQUAL_PTR((xce_ctrlstatus_t*)0xdeadbeef, status); printf("test_xcecli_ctrlstatus_primary_null_ctx_typed_failure: PASS\n"); } /* NULL status out-param is also a typed failure (boundary validation). */ static void test_xcecli_ctrlstatus_primary_null_out_typed_failure(void) { int rc = xcecli_ctrlstatus_primary(NULL, NULL); TEST_ASSERT_EQUAL_INT(-EINVAL, rc); printf("test_xcecli_ctrlstatus_primary_null_out_typed_failure: PASS\n"); } /* The pure authoritative-decision helper: a reply is authoritative iff the * responder's role is PRIMARY (ctrl_role_t PRIMARY == 0). A standby (1), * init (2), or electing (3) responder is a non-authoritative HINT — the call * must keep following the redirect (Req 9.1, 2.3, 3.2). */ static void test_ctrlstatus_is_authoritative_decision(void) { xce_ctrlstatus_t reply; memset(&reply, 0, sizeof(reply)); reply.responderRole = 0; /* PRIMARY */ TEST_ASSERT_TRUE(xcecli_ctrlstatus_is_authoritative(&reply)); reply.responderRole = 1; /* STANDBY */ TEST_ASSERT_FALSE(xcecli_ctrlstatus_is_authoritative(&reply)); reply.responderRole = 2; /* INIT */ TEST_ASSERT_FALSE(xcecli_ctrlstatus_is_authoritative(&reply)); reply.responderRole = 3; /* ELECTING */ TEST_ASSERT_FALSE(xcecli_ctrlstatus_is_authoritative(&reply)); /* NULL reply is never authoritative (defensive). */ TEST_ASSERT_FALSE(xcecli_ctrlstatus_is_authoritative(NULL)); printf("test_ctrlstatus_is_authoritative_decision: PASS\n"); } /* The pure next-target helper: given a standby's hint reply, map the reported * primaryName to its host:port via the reply's peers list so the next query * can target the primary directly (the redirect step of Req 9.1). */ static void test_ctrlstatus_primary_addr_from_peers(void) { xce_ctrlpeer_t p0 = { .name = "ctrl-a", .host = "10.0.0.1", .port = 9991 }; xce_ctrlpeer_t p1 = { .name = "ctrl-b", .host = "10.0.0.2", .port = 9992 }; xce_ctrlpeer_t* peerlist[2] = { &p0, &p1 }; xce_ctrlstatus_t hint; memset(&hint, 0, sizeof(hint)); hint.responderName = "ctrl-a"; hint.responderRole = 1; /* STANDBY */ hint.primaryName = "ctrl-b"; /* the standby knows ctrl-b is primary */ hint.peers.count = 2; hint.peers.list = peerlist; hint.peerCount = 2; char host[XCE_MAXHOSTNAME_LEN]; uint16_t port = 0; int rc = xcecli_ctrlstatus_primary_addr(&hint, host, sizeof(host), &port); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_STRING("10.0.0.2", host); TEST_ASSERT_EQUAL_UINT16(9992, port); printf("test_ctrlstatus_primary_addr_from_peers: PASS\n"); } /* When the standby reports no known primary (empty primaryName) or the named * primary is not in the peers list, the address lookup fails (-ENOENT) so the * caller falls back to plain master rotation rather than targeting a bogus * address (Req 2.3 unknown-primary, redirect best-effort). */ static void test_ctrlstatus_primary_addr_unknown_or_missing(void) { xce_ctrlpeer_t p0 = { .name = "ctrl-a", .host = "10.0.0.1", .port = 9991 }; xce_ctrlpeer_t* peerlist[1] = { &p0 }; xce_ctrlstatus_t hint; memset(&hint, 0, sizeof(hint)); hint.responderName = "ctrl-a"; hint.responderRole = 3; /* ELECTING */ hint.peers.count = 1; hint.peers.list = peerlist; hint.peerCount = 1; char host[XCE_MAXHOSTNAME_LEN]; uint16_t port = 7; /* unknown primary (empty) */ hint.primaryName = ""; TEST_ASSERT_EQUAL_INT(-ENOENT, xcecli_ctrlstatus_primary_addr( &hint, host, sizeof(host), &port)); /* NULL primary */ hint.primaryName = NULL; TEST_ASSERT_EQUAL_INT(-ENOENT, xcecli_ctrlstatus_primary_addr( &hint, host, sizeof(host), &port)); /* named primary not present in peers */ hint.primaryName = "ctrl-z"; TEST_ASSERT_EQUAL_INT(-ENOENT, xcecli_ctrlstatus_primary_addr( &hint, host, sizeof(host), &port)); /* NULL/zero args are rejected */ hint.primaryName = "ctrl-a"; TEST_ASSERT_EQUAL_INT(-EINVAL, xcecli_ctrlstatus_primary_addr( NULL, host, sizeof(host), &port)); TEST_ASSERT_EQUAL_INT(-EINVAL, xcecli_ctrlstatus_primary_addr( &hint, NULL, sizeof(host), &port)); TEST_ASSERT_EQUAL_INT( -EINVAL, xcecli_ctrlstatus_primary_addr(&hint, host, 0, &port)); TEST_ASSERT_EQUAL_INT( -EINVAL, xcecli_ctrlstatus_primary_addr(&hint, host, sizeof(host), NULL)); printf("test_ctrlstatus_primary_addr_unknown_or_missing: PASS\n"); } /* ── Suite entry point ──────────────────────────────────────────────────── */ void test_ctrlstatus(void) { RUN_TEST(test_ctrlstatus_roundtrip_all_fields); RUN_TEST(test_ctrlstatus_roundtrip_unknown_primary); RUN_TEST(test_xcecli_ctrlstatus_null_ctx_typed_failure); RUN_TEST(test_xcecli_ctrlstatus_primary_null_ctx_typed_failure); RUN_TEST(test_xcecli_ctrlstatus_primary_null_out_typed_failure); RUN_TEST(test_ctrlstatus_is_authoritative_decision); RUN_TEST(test_ctrlstatus_primary_addr_from_peers); RUN_TEST(test_ctrlstatus_primary_addr_unknown_or_missing); }