feat: implement safe leave, profile reset, and full game reset semantics
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user