151 lines
7.0 KiB
C
151 lines
7.0 KiB
C
#include <assert.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#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;
|
|
}
|