#include #include #include #include "http_api.h" typedef struct { uint32_t value; } test_random_t; static uint32_t next_random(void *context) { test_random_t *random = context; random->value = random->value * 1664525U + 1013904223U; return random->value; } static http_api_response_t call(http_api_t *api, http_api_route_t route, http_api_method_t method, const char *body, const char *token) { http_api_response_t response; const http_api_request_t request = {.method = method, .route = route, .content_type_json = true, .body = body, .body_length = body == NULL ? 0U : strlen(body), .session_token = token}; assert(http_api_handle(api, &request, &response)); assert(response.body_length == strlen(response.body)); return response; } static void token_from_response(const http_api_response_t *response, char token[33]) { const char *start = strstr(response->body, "\"token\":\""); assert(start != NULL); memcpy(token, start + strlen("\"token\":\""), 32U); token[32] = '\0'; } static const char *board_start(const char *json, uint8_t board) { const char *first = strstr(json, "\"boards\":[\""); assert(first != NULL); first += strlen("\"boards\":[\""); return board == 0U ? first : first + kBoardCellCount + 3U; } static void assert_hidden(const char *json, uint8_t board) { const char *cells = board_start(json, board); for (uint8_t index = 0; index < kBoardCellCount; ++index) assert(cells[index] != '1'); } static void resume(http_api_t *api, const char token[33], const char *role) { char body[48]; snprintf(body, sizeof(body), "{\"token\":\"%s\"}", token); const http_api_response_t response = call(api, HTTP_API_ROUTE_RESUME, HTTP_API_POST, body, NULL); assert(response.status == 200U && strstr(response.body, role) != NULL); } static void command(http_api_t *api, http_api_route_t route, const char token[33], uint32_t game_id) { char body[80]; snprintf(body, sizeof(body), "{\"token\":\"%s\",\"gameId\":%u}", token, (unsigned int)game_id); const http_api_response_t response = call(api, route, HTTP_API_POST, body, NULL); assert(response.status == 200U); } static void shot(http_api_t *api, const char token[33], uint32_t game_id, coordinate_t coordinate) { char body[96]; snprintf(body, sizeof(body), "{\"token\":\"%s\",\"gameId\":%u,\"x\":%u,\"y\":%u}", token, (unsigned int)game_id, coordinate.x, coordinate.y); const http_api_response_t response = call(api, HTTP_API_ROUTE_SHOT, HTTP_API_POST, body, NULL); assert(response.status == 200U); } static coordinate_t first_cell(const board_t *board, cell_t cell) { for (uint8_t index = 0; index < kBoardCellCount; ++index) { if (board->cells[index] == cell) return (coordinate_t){.x = (uint8_t)(index % kBoardWidth), .y = (uint8_t)(index / kBoardWidth)}; } assert(false); return (coordinate_t){0}; } static void test_human_game_journey(void) { test_random_t random = {.value = 57U}; application_t application; application_init(&application, (random_source_t){.next_u32 = next_random, .context = &random}); http_api_t api; http_api_init(&api, &application); char player_tokens[2][33]; http_api_response_t response = call(&api, HTTP_API_ROUTE_JOIN, HTTP_API_POST, "{\"name\":\"Алиса\",\"requestedRole\":\"player1\"}", NULL); assert(response.status == 200U); token_from_response(&response, player_tokens[0]); response = call(&api, HTTP_API_ROUTE_JOIN, HTTP_API_POST, "{\"name\":\"Борис\",\"requestedRole\":\"player2\"}", NULL); assert(response.status == 200U); token_from_response(&response, player_tokens[1]); response = call(&api, HTTP_API_ROUTE_JOIN, HTTP_API_POST, "{\"name\":\"Зритель\",\"requestedRole\":\"spectator\"}", NULL); assert(response.status == 200U); char spectator_token[33]; token_from_response(&response, spectator_token); resume(&api, player_tokens[0], "player1"); resume(&api, player_tokens[1], "player2"); resume(&api, spectator_token, "spectator"); const uint32_t game_id = application.lifecycle.game.state.game_id; char config[96]; snprintf(config, sizeof(config), "{\"token\":\"%s\",\"gameId\":%u,\"mode\":\"human\"}", player_tokens[0], (unsigned int)game_id); response = call(&api, HTTP_API_ROUTE_CONFIG, HTTP_API_POST, config, NULL); assert(response.status == 200U); command(&api, HTTP_API_ROUTE_START, player_tokens[0], game_id); assert(application.lifecycle.game.state.phase == PHASE_IN_PROGRESS); response = call(&api, HTTP_API_ROUTE_STATE, HTTP_API_GET, NULL, player_tokens[0]); assert(response.status == 200U && board_start(response.body, 0)[0] <= '3'); assert_hidden(response.body, 1U); response = call(&api, HTTP_API_ROUTE_STATE, HTTP_API_GET, NULL, player_tokens[1]); assert(response.status == 200U); assert_hidden(response.body, 0U); response = call(&api, HTTP_API_ROUTE_STATE, HTTP_API_GET, NULL, spectator_token); assert(response.status == 200U); assert_hidden(response.body, 0U); assert_hidden(response.body, 1U); uint8_t first_player = application.lifecycle.game.state.current_player; shot(&api, player_tokens[first_player], game_id, first_cell(&application.lifecycle.game.state.boards[first_player ^ 1U], CELL_WATER)); const uint8_t winner = application.lifecycle.game.state.current_player; assert(winner == (first_player ^ 1U)); bool saw_sunk = false; while (application.lifecycle.game.state.phase == PHASE_IN_PROGRESS) { const coordinate_t target = first_cell(&application.lifecycle.game.state.boards[winner ^ 1U], CELL_SHIP); shot(&api, player_tokens[winner], game_id, target); response = call(&api, HTTP_API_ROUTE_STATE, HTTP_API_GET, NULL, spectator_token); saw_sunk = saw_sunk || strchr(board_start(response.body, winner ^ 1U), '4') != NULL; } assert(saw_sunk); assert(application.lifecycle.game.state.winner == winner); assert(application.lifecycle.game.state.statistics[winner].shots == 20U); assert(application.lifecycle.game.state.statistics[winner].hits == 20U); assert(application.lifecycle.game.state.statistics[winner].ships_sunk == 10U); assert(application.lifecycle.game.state.statistics[first_player].misses == 1U); response = call(&api, HTTP_API_ROUTE_STATE, HTTP_API_GET, NULL, spectator_token); assert(response.status == 200U && strstr(response.body, "\"winner\":") != NULL); assert(strstr(response.body, "\"statistics\":[") != NULL); assert(strchr(board_start(response.body, 0U), '1') != NULL || strchr(board_start(response.body, 1U), '1') != NULL); resume(&api, player_tokens[0], "player1"); resume(&api, player_tokens[1], "player2"); command(&api, HTTP_API_ROUTE_REMATCH, player_tokens[0], game_id); assert(application.lifecycle.game.state.phase == PHASE_REMATCH_WAIT); resume(&api, spectator_token, "spectator"); command(&api, HTTP_API_ROUTE_REMATCH, player_tokens[1], game_id); assert(application.lifecycle.game.state.phase == PHASE_IN_PROGRESS && application.lifecycle.game.state.game_id != game_id); assert(game_lifecycle_disconnect(&application.lifecycle, 1U) == LIFECYCLE_RESULT_OK); command(&api, HTTP_API_ROUTE_ABORT, player_tokens[0], application.lifecycle.game.state.game_id); assert(application.lifecycle.game.state.phase == PHASE_LOBBY); } static void test_atomic_two_player_lobby(void) { test_random_t random = {.value = 91U}; application_t application; application_init(&application, (random_source_t){.next_u32 = next_random, .context = &random}); http_api_t api; http_api_init(&api, &application); http_api_response_t response = call(&api, HTTP_API_ROUTE_JOIN, HTTP_API_POST, "{\"name\":\"Алиса\",\"requestedRole\":\"player\"}", NULL); assert(response.status == 200U && strstr(response.body, "\"role\":\"player1\"") != NULL); char player_1[33]; token_from_response(&response, player_1); response = call(&api, HTTP_API_ROUTE_JOIN, HTTP_API_POST, "{\"name\":\"Борис\",\"requestedRole\":\"player\"}", NULL); assert(response.status == 200U && strstr(response.body, "\"role\":\"player2\"") != NULL); char player_2[33]; token_from_response(&response, player_2); const uint32_t game_id = application.lifecycle.game.state.game_id; response = call(&api, HTTP_API_ROUTE_STATE, HTTP_API_GET, NULL, player_1); assert(response.status == 200U && strstr(response.body, "\"viewer\":\"player1\"") != NULL && strstr(response.body, "Алиса") != NULL && strstr(response.body, "Борис") != NULL); response = call(&api, HTTP_API_ROUTE_STATE, HTTP_API_GET, NULL, player_2); assert(response.status == 200U && strstr(response.body, "\"viewer\":\"player2\"") != NULL); char config[96]; snprintf(config, sizeof(config), "{\"token\":\"%s\",\"gameId\":%u,\"mode\":\"human\"}", player_2, (unsigned int)game_id); response = call(&api, HTTP_API_ROUTE_CONFIG, HTTP_API_POST, config, NULL); assert(response.status == 403U); char start[80]; snprintf(start, sizeof(start), "{\"token\":\"%s\",\"gameId\":%u}", player_2, (unsigned int)game_id); response = call(&api, HTTP_API_ROUTE_START, HTTP_API_POST, start, NULL); assert(response.status == 403U); snprintf(config, sizeof(config), "{\"token\":\"%s\",\"gameId\":%u,\"mode\":\"human\"}", player_1, (unsigned int)game_id); response = call(&api, HTTP_API_ROUTE_CONFIG, HTTP_API_POST, config, NULL); assert(response.status == 200U); response = call(&api, HTTP_API_ROUTE_START, HTTP_API_POST, start, NULL); assert(response.status == 403U); snprintf(start, sizeof(start), "{\"token\":\"%s\",\"gameId\":%u}", player_1, (unsigned int)game_id); response = call(&api, HTTP_API_ROUTE_START, HTTP_API_POST, start, NULL); assert(response.status == 200U && application.lifecycle.game.state.phase == PHASE_IN_PROGRESS); response = call(&api, HTTP_API_ROUTE_LEAVE, HTTP_API_POST, (snprintf(start, sizeof(start), "{\"token\":\"%s\",\"gameId\":%u}", player_2, (unsigned int)game_id), start), NULL); assert(response.status == 200U && !application.lifecycle.sessions.entries[1].occupied); response = call(&api, HTTP_API_ROUTE_JOIN, HTTP_API_POST, "{\"name\":\"Вера\",\"requestedRole\":\"player\"}", NULL); assert(response.status == 200U && strstr(response.body, "\"role\":\"player2\"") != NULL); resume(&api, player_1, "player1"); } int main(void) { test_human_game_journey(); test_atomic_two_player_lobby(); puts("human game integration tests passed"); return 0; }