feat: implement safe leave, profile reset, and full game reset semantics

This commit is contained in:
2026-08-30 23:18:07 +03:00
parent c0c3c73c7a
commit a68023e684
16 changed files with 281 additions and 9 deletions
+25
View File
@@ -104,10 +104,35 @@ static void test_bot_lifecycle_and_stale_actions(void) {
assert(memcmp(&before, &lifecycle, sizeof(lifecycle)) == 0);
}
static void test_leave_and_full_reset(void) {
test_random_t random = {.value = 23U};
game_lifecycle_t lifecycle = new_lifecycle(&random);
uint8_t player_1 = 0;
uint8_t player_2 = 0;
uint8_t spectator = 0;
join_players(&lifecycle, &player_1, &player_2);
assert(game_lifecycle_join(&lifecycle, ROLE_SPECTATOR, "Watch", &spectator) == LIFECYCLE_RESULT_OK);
const uint32_t lobby_game_id = lifecycle.game.state.game_id;
assert(game_lifecycle_leave(&lifecycle, spectator) == LIFECYCLE_RESULT_OK);
assert(!lifecycle.sessions.entries[spectator].occupied && lifecycle.game.state.game_id == lobby_game_id);
assert(game_lifecycle_start(&lifecycle, player_1, lobby_game_id) == LIFECYCLE_RESULT_OK);
assert(game_lifecycle_leave(&lifecycle, player_2) == LIFECYCLE_RESULT_OK);
assert(!lifecycle.sessions.entries[player_2].occupied && lifecycle.sessions.entries[player_1].occupied);
assert(lifecycle.game.state.phase == PHASE_LOBBY && lifecycle.game.state.game_id != lobby_game_id);
const uint32_t reset_game_id = lifecycle.game.state.game_id;
lifecycle.cumulative[0].games = 3U;
assert(game_lifecycle_reset(&lifecycle, player_1, reset_game_id) == LIFECYCLE_RESULT_OK);
assert(lifecycle.game.state.phase == PHASE_LOBBY && lifecycle.game.state.game_id != reset_game_id);
assert(!lifecycle.sessions.entries[0].occupied && !lifecycle.sessions.entries[1].occupied);
assert(lifecycle.cumulative[0].games == 0U && lifecycle.recovery_reason == RECOVERY_REASON_GAME_RESET);
assert(game_lifecycle_reset(&lifecycle, player_1, reset_game_id) == LIFECYCLE_RESULT_FORBIDDEN_ROLE);
}
int main(void) {
test_capacity_sanitize_and_resume();
test_human_lifecycle_and_guards();
test_bot_lifecycle_and_stale_actions();
test_leave_and_full_reset();
puts("game lifecycle tests passed");
return 0;
}
+27
View File
@@ -186,9 +186,36 @@ static void test_sessions_commands_and_state(void) {
expect_code(&response, 503U, "SERVER_BUSY");
}
static void test_recovery_routes(void) {
application_t application;
test_random_t random = {.value = 31U};
http_api_t api = new_api(&application, &random);
http_api_response_t response = call(&api, HTTP_API_ROUTE_JOIN, HTTP_API_POST,
"{\"name\":\"Alice\",\"requestedRole\":\"player1\"}", NULL);
char player_token[33]; token_from_response(&response, player_token);
char command[96];
snprintf(command, sizeof(command), "{\"token\":\"%s\",\"gameId\":1}", player_token);
response = call(&api, HTTP_API_ROUTE_LEAVE, HTTP_API_POST, command, NULL);
assert(response.status == 200U && strstr(response.body, "session_left") != NULL);
response = call(&api, HTTP_API_ROUTE_LEAVE, HTTP_API_POST, command, NULL);
assert(response.status == 200U && strstr(response.body, "session_left") != NULL);
response = call(&api, HTTP_API_ROUTE_JOIN, HTTP_API_POST,
"{\"name\":\"Alice\",\"requestedRole\":\"player1\"}", NULL);
token_from_response(&response, player_token);
snprintf(command, sizeof(command), "{\"token\":\"%s\",\"gameId\":1}", player_token);
response = call(&api, HTTP_API_ROUTE_RESET, HTTP_API_POST, command, NULL);
assert(response.status == 200U && strstr(response.body, "game_reset") != NULL);
response = call(&api, HTTP_API_ROUTE_STATE, HTTP_API_GET, NULL, player_token);
assert(response.status == 401U && strstr(response.body, "SESSION_INVALIDATED") != NULL && strstr(response.body, "game_reset") != NULL);
response = call(&api, HTTP_API_ROUTE_RESET, HTTP_API_POST,
"{\"token\":\"00000000000000000000000000000000\",\"gameId\":1}", NULL);
assert(response.status == 200U && strstr(response.body, "game_reset") != NULL);
}
int main(void) {
test_public_routes_and_parse_limits();
test_sessions_commands_and_state();
test_recovery_routes();
puts("http api tests passed");
return 0;
}
+18
View File
@@ -124,9 +124,27 @@ static void test_timeout_ping_and_command(void) {
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;
}