#include #include #include #include "sync_service.h" typedef struct { uint32_t value; } test_random_t; typedef struct { int fail_client; uint8_t sends[kSessionCapacity]; char payloads[kSessionCapacity][kStateMessageCapacity]; } capture_t; static uint32_t next_random(void *context) { test_random_t *random = context; random->value = random->value * 1664525U + 1013904223U; return random->value; } static void token_text(const uint8_t token[kSessionTokenBytes], char output[33]) { static const char hex[] = "0123456789abcdef"; for (uint8_t index = 0; index < kSessionTokenBytes; ++index) { output[index * 2U] = hex[token[index] >> 4U]; output[index * 2U + 1U] = hex[token[index] & 15U]; } output[32] = '\0'; } static bool capture_send(void *context, int client_id, const char *payload, size_t length) { capture_t *capture = context; if (client_id == capture->fail_client) return false; assert(client_id >= 0 && client_id < kSessionCapacity); assert(length < kStateMessageCapacity); ++capture->sends[client_id]; memcpy(capture->payloads[client_id], payload, length); capture->payloads[client_id][length] = '\0'; return true; } static const char *board_start(const char *json, uint8_t board) { const char *start = strstr(json, "\"boards\":[\""); assert(start != NULL); start += strlen("\"boards\":[\""); return board == 0U ? start : start + kBoardCellCount + 3U; } static void hello(sync_service_t *service, int client_id, const uint8_t token[kSessionTokenBytes], uint32_t version) { char token_value[33]; char frame[96]; char output[kStateMessageCapacity]; size_t output_length = 0U; bool changed = false; bool close = false; token_text(token, token_value); snprintf(frame, sizeof(frame), "{\"type\":\"hello\",\"token\":\"%s\",\"version\":%u}", token_value, version); assert(sync_service_receive(service, client_id, frame, strlen(frame), 10U, output, &output_length, &changed, &close)); assert(!changed && !close && output_length > 0U && strstr(output, "\"type\":\"state\"") != NULL); } static void test_authentication_visibility_and_backpressure(void) { test_random_t random = {.value = 1U}; application_t application; application_init(&application, (random_source_t){.next_u32 = next_random, .context = &random}); uint8_t player_1 = 0; uint8_t player_2 = 0; uint8_t spectator = 0; assert(application_join(&application, ROLE_PLAYER_1, "Alice", &player_1) == LIFECYCLE_RESULT_OK); assert(application_join(&application, ROLE_PLAYER_2, "Bob", &player_2) == LIFECYCLE_RESULT_OK); assert(application_join(&application, ROLE_SPECTATOR, "Watch", &spectator) == LIFECYCLE_RESULT_OK); sync_service_t service; sync_service_init(&service, &application); assert(sync_service_open(&service, 0, 0U)); assert(sync_service_open(&service, 1, 0U)); assert(sync_service_open(&service, 2, 0U)); for (int client_id = 3; client_id < kSessionCapacity; ++client_id) assert(sync_service_open(&service, client_id, 0U)); assert(!sync_service_open(&service, kSessionCapacity, 0U)); hello(&service, 0, application.lifecycle.sessions.entries[player_1].token, 0U); hello(&service, 1, application.lifecycle.sessions.entries[player_2].token, 999U); hello(&service, 2, application.lifecycle.sessions.entries[spectator].token, 0U); application.lifecycle.game.state.phase = PHASE_IN_PROGRESS; application.lifecycle.game.state.boards[0].cells[0] = CELL_SHIP; application.lifecycle.game.state.boards[1].cells[0] = CELL_SHIP; application.lifecycle.game.state.boards[0].cells[1] = CELL_HIT; application.lifecycle.game.state.boards[1].cells[1] = CELL_MISS; capture_t capture = {.fail_client = -1}; sync_service_broadcast(&service, capture_send, &capture); assert(board_start(capture.payloads[0], 0)[0] == '1' && board_start(capture.payloads[0], 1)[0] == '0'); assert(board_start(capture.payloads[1], 0)[0] == '0' && board_start(capture.payloads[1], 1)[0] == '1'); assert(board_start(capture.payloads[2], 0)[0] == '0' && board_start(capture.payloads[2], 1)[0] == '0'); assert(board_start(capture.payloads[2], 0)[1] == '3' && board_start(capture.payloads[2], 1)[1] == '2'); capture.fail_client = 2; const uint8_t previous_sends = capture.sends[2]; sync_service_broadcast(&service, capture_send, &capture); sync_service_broadcast(&service, capture_send, &capture); assert(capture.sends[2] == previous_sends); } static void test_timeout_ping_and_command(void) { test_random_t random = {.value = 9U}; application_t application; application_init(&application, (random_source_t){.next_u32 = next_random, .context = &random}); uint8_t player_1 = 0; assert(application_join(&application, ROLE_PLAYER_1, "Alice", &player_1) == LIFECYCLE_RESULT_OK); sync_service_t service; sync_service_init(&service, &application); assert(sync_service_open(&service, 3, 0U)); int expired[kSessionCapacity] = {0}; size_t expired_count = 0U; sync_service_expire(&service, kWebSocketHelloTimeoutMs + 1U, expired, &expired_count); assert(expired_count == 1U && expired[0] == 3); assert(sync_service_open(&service, 0, 0U)); hello(&service, 0, application.lifecycle.sessions.entries[player_1].token, 0U); char output[kStateMessageCapacity]; size_t output_length = 0U; bool changed = false; bool close = false; assert(sync_service_receive(&service, 0, "{\"type\":\"ping\"}", 15U, 20U, output, &output_length, &changed, &close)); assert(!changed && !close && strcmp(output, "{\"type\":\"pong\"}") == 0); char token[33]; char frame[128]; token_text(application.lifecycle.sessions.entries[player_1].token, token); snprintf(frame, sizeof(frame), "{\"type\":\"config\",\"token\":\"%s\",\"gameId\":1,\"mode\":\"bot\"}", token); assert(sync_service_receive(&service, 0, frame, strlen(frame), 30U, output, &output_length, &changed, &close)); assert(changed && !close && output_length == 0U && application.lifecycle.game.state.mode == MODE_BOT); } static void test_reset_notification_invalidates_authenticated_client(void) { test_random_t random = {.value = 17U}; application_t application; application_init(&application, (random_source_t){.next_u32 = next_random, .context = &random}); uint8_t player = 0; assert(application_join(&application, ROLE_PLAYER_1, "Alice", &player) == LIFECYCLE_RESULT_OK); sync_service_t service; sync_service_init(&service, &application); assert(sync_service_open(&service, 0, 0U)); hello(&service, 0, application.lifecycle.sessions.entries[player].token, 0U); assert(game_lifecycle_reset(&application.lifecycle, player, 1U) == LIFECYCLE_RESULT_OK); capture_t capture = {.fail_client = -1}; sync_service_broadcast(&service, capture_send, &capture); assert(capture.sends[0] == 1U && strstr(capture.payloads[0], "\"type\":\"reset\"") != NULL); assert(strstr(capture.payloads[0], "game_reset") != NULL && !service.connections[0].active); } int main(void) { test_authentication_visibility_and_backpressure(); test_timeout_ping_and_command(); test_reset_notification_invalidates_authenticated_client(); puts("sync service tests passed"); return 0; }