/** * Copyright (c) 2026 Dominic Masters * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #include "dusktest.h" #include "network/socket/client/client.h" #include "network/socket/client/clientroster.h" #include "network/socket/server/serveronline.h" #include "network/socket/payload/packetplayerstate.h" #include "util/memory.h" #include "util/string.h" #include "time/time.h" #include #define TEST_PORT 58211 typedef struct { bool_t connected; bool_t rejected; bool_t disconnected; } clientevents_t; static void onConnected(void *user) { ((clientevents_t *)user)->connected = true; } static void onRejected( const packethandshakerejectreason_t reason, void *user ) { (void)reason; ((clientevents_t *)user)->rejected = true; } static void onDisconnected(errorret_t error, void *user) { errorCatch(error); ((clientevents_t *)user)->disconnected = true; } static void pumpServerAndClients( localclient_t *clients[], const uint32_t clientCount, const int32_t ms ) { for(int32_t i = 0; i < ms; i++) { for(uint32_t c = 0; c < clientCount; c++) { errorCatch(localClientUpdate(clients[c])); } errorCatch(serverOnlineUpdate()); usleep(1000); } } static uint32_t countServerRemotes(void) { uint32_t count = 0; for(uint32_t i = 0; i < SERVER_CLIENT_COUNT_MAX; i++) { if(SERVER_ONLINE.clients[i].type == SERVER_CLIENT_TYPE_REMOTE) count++; } return count; } static int socket_setup(void **state) { clientRosterInit(); return 0; } static int socket_teardown(void **state) { if(SERVER_ONLINE.running) errorCatch(serverOnlineStop()); clientRosterInit(); return 0; } static void test_host_connects_local_client_to_itself(void **state) { assert_true(errorIsOk(serverOnlineHost(TEST_PORT))); client_t *local = clientRosterClaim(CLIENT_TYPE_LOCAL); assert_non_null(local); clientevents_t events; memoryZero(&events, sizeof(events)); const errorret_t ret = localClientConnect( &local->local, "127.0.0.1", TEST_PORT, "host", onConnected, onRejected, onDisconnected, &events ); assert_true(errorIsOk(ret)); localclient_t *clients[] = { &local->local }; pumpServerAndClients(clients, 1, 2000); assert_true(events.connected); assert_false(events.rejected); assert_int_equal(local->local.state, LOCAL_CLIENT_STATE_CONNECTED); assert_int_equal(countServerRemotes(), 1); serverclient_t *remote = NULL; for(uint32_t i = 0; i < SERVER_CLIENT_COUNT_MAX; i++) { if(SERVER_ONLINE.clients[i].type == SERVER_CLIENT_TYPE_REMOTE) { remote = &SERVER_ONLINE.clients[i]; } } assert_non_null(remote); assert_true(uuidEquals(&remote->remote.id, &local->local.id)); assert_string_equal(remote->remote.username, "host"); localClientDisconnect(&local->local); clientRosterRemove(local); pumpServerAndClients(NULL, 0, 200); assert_int_equal(countServerRemotes(), 0); errorCatch(serverOnlineStop()); assert_int_equal(memoryGetAllocatedCount(), 0); } // Regression test: a quiet connection (no state/chat traffic) must not // falsely time itself out. The server has to keep sending the client // something (a PING) on its own idle timer -- otherwise the client only // ever hears from the server once, at handshake, and its own 10s // no-packets-received timeout eventually fires even though the // connection is healthy. static void test_server_keepalive_prevents_client_timeout(void **state) { assert_true(errorIsOk(serverOnlineHost(TEST_PORT))); client_t *local = clientRosterClaim(CLIENT_TYPE_LOCAL); assert_non_null(local); clientevents_t events; memoryZero(&events, sizeof(events)); assert_true(errorIsOk(localClientConnect( &local->local, "127.0.0.1", TEST_PORT, "host", onConnected, onRejected, onDisconnected, &events ))); localclient_t *clients[] = { &local->local }; pumpServerAndClients(clients, 1, 500); assert_true(events.connected); // Simulate several ping intervals of elapsed game time -- well past // SERVER_CLIENT_TIMEOUT_SECONDS in total -- jumping the logical clock // directly (no real 10s wait) but still pumping for real in between // each jump so any keepalive packets actually go out over the real // loopback socket and get processed. for(int32_t i = 0; i < 6; i++) { TIME.time += 3.0f; pumpServerAndClients(clients, 1, 100); } assert_false(events.disconnected); assert_int_equal(local->local.state, LOCAL_CLIENT_STATE_CONNECTED); assert_int_equal(countServerRemotes(), 1); localClientDisconnect(&local->local); clientRosterRemove(local); pumpServerAndClients(NULL, 0, 200); errorCatch(serverOnlineStop()); assert_int_equal(memoryGetAllocatedCount(), 0); } static void test_second_client_joins_and_state_relays(void **state) { assert_true(errorIsOk(serverOnlineHost(TEST_PORT))); client_t *alice = clientRosterClaim(CLIENT_TYPE_LOCAL); client_t *bob = clientRosterClaim(CLIENT_TYPE_LOCAL); assert_non_null(alice); assert_non_null(bob); clientevents_t aliceEvents, bobEvents; memoryZero(&aliceEvents, sizeof(aliceEvents)); memoryZero(&bobEvents, sizeof(bobEvents)); assert_true(errorIsOk(localClientConnect( &alice->local, "127.0.0.1", TEST_PORT, "alice", onConnected, onRejected, onDisconnected, &aliceEvents ))); localclient_t *clients[] = { &alice->local, &bob->local }; pumpServerAndClients(clients, 1, 500); assert_true(aliceEvents.connected); assert_true(errorIsOk(localClientConnect( &bob->local, "127.0.0.1", TEST_PORT, "bob", onConnected, onRejected, onDisconnected, &bobEvents ))); pumpServerAndClients(clients, 2, 500); assert_true(bobEvents.connected); assert_int_equal(countServerRemotes(), 2); // Alice reports a position; the server should track it on her slot and // relay it to bob (who doesn't know alice's id yet, so his handler // claims a fresh roster slot for her -- exactly what a real second // process would do). packetplayerstate_t playerState; memoryZero(&playerState, sizeof(playerState)); playerState.position[0] = 1.0f; playerState.position[1] = 2.0f; playerState.position[2] = 3.0f; assert_true(errorIsOk(localClientSend( &alice->local, PACKET_TYPE_PLAYER_STATE, &playerState, false ))); pumpServerAndClients(clients, 2, 500); serverclient_t *aliceRemote = NULL; for(uint32_t i = 0; i < SERVER_CLIENT_COUNT_MAX; i++) { if( SERVER_ONLINE.clients[i].type == SERVER_CLIENT_TYPE_REMOTE && stringEquals(SERVER_ONLINE.clients[i].remote.username, "alice") ) { aliceRemote = &SERVER_ONLINE.clients[i]; } } assert_non_null(aliceRemote); assert_true(aliceRemote->remote.position[0] == 1.0f); assert_true(aliceRemote->remote.position[1] == 2.0f); assert_true(aliceRemote->remote.position[2] == 3.0f); client_t *bobsViewOfAlice = clientRosterFindById(&alice->local.id); assert_non_null(bobsViewOfAlice); assert_true(bobsViewOfAlice->remote.hasState); assert_true(bobsViewOfAlice->remote.lastPosition[0] == 1.0f); localClientDisconnect(&alice->local); localClientDisconnect(&bob->local); clientRosterRemove(alice); clientRosterRemove(bob); pumpServerAndClients(NULL, 0, 200); errorCatch(serverOnlineStop()); assert_int_equal(memoryGetAllocatedCount(), 0); } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test_setup_teardown( test_host_connects_local_client_to_itself, socket_setup, socket_teardown ), cmocka_unit_test_setup_teardown( test_second_client_joins_and_state_relays, socket_setup, socket_teardown ), cmocka_unit_test_setup_teardown( test_server_keepalive_prevents_client_timeout, socket_setup, socket_teardown ), }; return cmocka_run_group_tests(tests, NULL, NULL); }