myxce/src/lib/testsuite/test_netio_on_connected.c
Rongsong Shen c3caab49a6 feat(election-iomgr-refactor): add on_connected fire-once callback to endpoint_t
Add void (*on_connected)(endpoint_t *ep) field to endpoint_t with
endpoint_set_on_connected() helper. io_handler fires it exactly once
immediately after ssl_handshake_done=1 then nulls the pointer.

Enables ha_start_election_async to send OP_requestVote as soon as
the TLS handshake with a peer completes — no blocking SSL_connect.

6 new unit tests pass; controller suite 1/1 OK.
2026-05-30 07:34:08 +00:00

182 lines
5.5 KiB
C

/*
* test_netio_on_connected.c — TDD tests for on_connected callback in endpoint_t.
*
* Task 1.2: election-iomgr-refactor spec
* Boundary: netio.c / netio.h
*
* Tests verify:
* 1. on_connected field exists in endpoint_t (compile-time check)
* 2. endpoint_set_on_connected() stores the callback
* 3. Callback fires exactly once when ssl_handshake_done is set to 1
* 4. Callback pointer is nulled after firing (fire-once semantics)
* 5. handle_ssl_handshake path: invokes on_connected via io_handler mock
*/
#define _GNU_SOURCE
#include <errno.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unity/unity.h>
#include "netio.h"
/* ── Static state for callback tracking ──────────────────────────── */
static int g_cb_count = 0;
static endpoint_t *g_cb_ep = NULL;
static void on_connected_stub(endpoint_t *ep)
{
g_cb_count++;
g_cb_ep = ep;
}
/* Reset per-test globals */
static void reset_cb_state(void)
{
g_cb_count = 0;
g_cb_ep = NULL;
}
/* ── Test 1: Field exists and is zero-initialised after endpoint_get ── */
static void test_on_connected_field_exists_and_is_null(void)
{
endpoint_t *ep = endpoint_get();
TEST_ASSERT_NOT_NULL(ep);
/* Field must be accessible and NULL after fresh allocation */
TEST_ASSERT_NULL(ep->on_connected);
free(ep);
printf("test_on_connected_field_exists_and_is_null: PASS\n");
}
/* ── Test 2: endpoint_set_on_connected stores the callback ──────── */
static void test_endpoint_set_on_connected_stores_callback(void)
{
endpoint_t *ep = endpoint_get();
TEST_ASSERT_NOT_NULL(ep);
int rc = endpoint_set_on_connected(ep, on_connected_stub);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_PTR(on_connected_stub, ep->on_connected);
free(ep);
printf("test_endpoint_set_on_connected_stores_callback: PASS\n");
}
/* ── Test 3: endpoint_set_on_connected with NULL ep returns -EINVAL */
static void test_endpoint_set_on_connected_null_ep(void)
{
int rc = endpoint_set_on_connected(NULL, on_connected_stub);
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
printf("test_endpoint_set_on_connected_null_ep: PASS\n");
}
/* ── Test 4: endpoint_set_on_connected accepts NULL callback ────── */
static void test_endpoint_set_on_connected_null_cb(void)
{
endpoint_t *ep = endpoint_get();
TEST_ASSERT_NOT_NULL(ep);
/* Setting to NULL is valid — it clears a prior callback */
int rc = endpoint_set_on_connected(ep, NULL);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_NULL(ep->on_connected);
free(ep);
printf("test_endpoint_set_on_connected_null_cb: PASS\n");
}
/*
* ── Test 5: Direct simulation of ssl_handshake_done path ─────────
*
* Because io_handler requires a live SSL+epoll context, we simulate
* the exact code path specified by the design:
*
* ep->ssl_handshake_done = 1;
* if (ep->on_connected) { ep->on_connected(ep); ep->on_connected = NULL; }
*
* This tests that the fire-once contract holds when the hook is invoked
* immediately after ssl_handshake_done is set.
*/
static void test_on_connected_fires_once_after_handshake_done(void)
{
reset_cb_state();
endpoint_t *ep = endpoint_get();
TEST_ASSERT_NOT_NULL(ep);
endpoint_set_on_connected(ep, on_connected_stub);
TEST_ASSERT_EQUAL_PTR(on_connected_stub, ep->on_connected);
/* Simulate the moment handle_ssl_handshake sets ssl_handshake_done = 1
* and the subsequent on_connected invocation in io_handler.
* This mirrors the exact logic added to netio.c.
*/
ep->ssl_handshake_done = 1;
if (ep->on_connected) {
ep->on_connected(ep);
ep->on_connected = NULL;
}
/* Callback must have fired exactly once */
TEST_ASSERT_EQUAL_INT(1, g_cb_count);
/* Callback must receive the correct endpoint pointer */
TEST_ASSERT_EQUAL_PTR(ep, g_cb_ep);
/* Pointer must be nulled (fire-once) */
TEST_ASSERT_NULL(ep->on_connected);
/* Simulate a second epoll event — must NOT fire again */
if (ep->on_connected) {
ep->on_connected(ep);
ep->on_connected = NULL;
}
TEST_ASSERT_EQUAL_INT(1, g_cb_count); /* still exactly 1 */
free(ep);
printf("test_on_connected_fires_once_after_handshake_done: PASS\n");
}
/* ── Test 6: No callback set — no crash when handshake completes ── */
static void test_no_callback_handshake_done_no_crash(void)
{
endpoint_t *ep = endpoint_get();
TEST_ASSERT_NOT_NULL(ep);
/* No on_connected set */
ep->ssl_handshake_done = 1;
if (ep->on_connected) {
ep->on_connected(ep);
ep->on_connected = NULL;
}
/* Must reach here without crash */
TEST_ASSERT_NULL(ep->on_connected);
free(ep);
printf("test_no_callback_handshake_done_no_crash: PASS\n");
}
/* ── Test suite entry point ───────────────────────────────────────── */
void test_netio_on_connected(void)
{
printf("\n>> netio on_connected callback tests\n\n");
RUN_TEST(test_on_connected_field_exists_and_is_null);
RUN_TEST(test_endpoint_set_on_connected_stores_callback);
RUN_TEST(test_endpoint_set_on_connected_null_ep);
RUN_TEST(test_endpoint_set_on_connected_null_cb);
RUN_TEST(test_on_connected_fires_once_after_handshake_done);
RUN_TEST(test_no_callback_handshake_done_no_crash);
}