#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_t new_api(application_t *application, test_random_t *random) { application_init(application, (random_source_t){.next_u32 = next_random, .context = random}); http_api_t api; http_api_init(&api, application); http_api_set_health(&api, &(http_api_health_t){.uptime_ms = 12U, .free_heap_bytes = 200000U, .minimum_free_heap_bytes = 190000U, .largest_free_block_bytes = 180000U, .connected_clients = 2U, .rejected_input = 3U, .reset_reason = 11, .wifi_state = "connected"}); return api; } 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 expect_code(const http_api_response_t *response, uint16_t status, const char *code) { char expected[64]; snprintf(expected, sizeof(expected), "\"code\":\"%s\"", code); assert(response->status == status); assert(strstr(response->body, expected) != NULL); assert(response->body_length < 160U); } static void token_from_response(const http_api_response_t *response, char token[33]) { const char *start = strstr(response->body, "\"token\":\""); assert(start != NULL); start += strlen("\"token\":\""); memcpy(token, start, 32U); token[32] = '\0'; } static void test_public_routes_and_parse_limits(void) { application_t application; test_random_t random = {.value = 1U}; http_api_t api = new_api(&application, &random); http_api_response_t response = call(&api, HTTP_API_ROUTE_INFO, HTTP_API_GET, NULL, NULL); assert(response.status == 200U && response.body_length <= 192U && strstr(response.body, "player1Available") != NULL); response = call(&api, HTTP_API_ROUTE_HEALTH, HTTP_API_GET, NULL, NULL); assert(response.status == 200U && response.body_length <= 320U && strstr(response.body, "connected") != NULL && strstr(response.body, "\"resetReason\":11") != NULL); const http_api_request_t long_target = {.method = HTTP_API_GET, .route = HTTP_API_ROUTE_INFO, .target_too_large = true}; assert(http_api_handle(&api, &long_target, &response)); expect_code(&response, 413U, "PAYLOAD_TOO_LARGE"); const char oversized[194] = {[0 ... 192] = 'a', [193] = '\0'}; response = call(&api, HTTP_API_ROUTE_JOIN, HTTP_API_POST, oversized, NULL); expect_code(&response, 413U, "PAYLOAD_TOO_LARGE"); const http_api_request_t non_json = {.method = HTTP_API_POST, .route = HTTP_API_ROUTE_JOIN, .content_type_json = false, .body = "{}", .body_length = 2U}; assert(http_api_handle(&api, &non_json, &response)); expect_code(&response, 400U, "MALFORMED_JSON"); response = call(&api, HTTP_API_ROUTE_JOIN, HTTP_API_POST, "{\"name\":\"A\",\"requestedRole\":\"invalid\"}", NULL); expect_code(&response, 400U, "INVALID_ROLE"); response = call(&api, HTTP_API_ROUTE_JOIN, HTTP_API_POST, "{\"name\":\"\",\"requestedRole\":\"player1\"}", NULL); expect_code(&response, 400U, "INVALID_NAME"); } static void test_sessions_commands_and_state(void) { application_t application; test_random_t random = {.value = 9U}; 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); assert(response.status == 200U); char player_1_token[33]; token_from_response(&response, player_1_token); response = call(&api, HTTP_API_ROUTE_JOIN, HTTP_API_POST, "{\"name\":\"Other\",\"requestedRole\":\"player1\"}", NULL); expect_code(&response, 409U, "NO_PLAYER_SLOT"); response = call(&api, HTTP_API_ROUTE_RESUME, HTTP_API_POST, "{\"token\":\"00000000000000000000000000000000\"}", NULL); expect_code(&response, 401U, "UNAUTHORIZED"); char resume_body[48]; snprintf(resume_body, sizeof(resume_body), "{\"token\":\"%s\"}", player_1_token); response = call(&api, HTTP_API_ROUTE_RESUME, HTTP_API_POST, resume_body, NULL); assert(response.status == 200U && strstr(response.body, "player1") != NULL); response = call(&api, HTTP_API_ROUTE_JOIN, HTTP_API_POST, "{\"name\":\"Bob\",\"requestedRole\":\"player2\"}", NULL); assert(response.status == 200U); char player_2_token[33]; token_from_response(&response, player_2_token); response = call(&api, HTTP_API_ROUTE_INFO, HTTP_API_GET, NULL, NULL); assert(response.status == 200U && strstr(response.body, "\"player1Name\":\"Alice\"") != NULL && strstr(response.body, "\"player2Name\":\"Bob\"") != NULL); response = call(&api, HTTP_API_ROUTE_JOIN, HTTP_API_POST, "{\"name\":\"Watch\",\"requestedRole\":\"spectator\"}", NULL); assert(response.status == 200U); char spectator_token[33]; token_from_response(&response, spectator_token); for (uint8_t index = 1U; index < kSpectatorCapacity; ++index) { response = call(&api, HTTP_API_ROUTE_JOIN, HTTP_API_POST, "{\"name\":\"Watch\",\"requestedRole\":\"spectator\"}", NULL); assert(response.status == 200U); } response = call(&api, HTTP_API_ROUTE_JOIN, HTTP_API_POST, "{\"name\":\"Extra\",\"requestedRole\":\"spectator\"}", NULL); expect_code(&response, 409U, "NO_SPECTATOR_SLOT"); response = call(&api, HTTP_API_ROUTE_CONFIG, HTTP_API_POST, "{\"token\":\"00000000000000000000000000000000\",\"gameId\":1,\"mode\":\"human\"}", NULL); expect_code(&response, 401U, "UNAUTHORIZED"); char command[128]; snprintf(command, sizeof(command), "{\"token\":\"%s\",\"gameId\":2,\"mode\":\"human\"}", player_1_token); response = call(&api, HTTP_API_ROUTE_CONFIG, HTTP_API_POST, command, NULL); expect_code(&response, 409U, "STALE_GAME"); snprintf(command, sizeof(command), "{\"token\":\"%s\",\"gameId\":1,\"mode\":\"invalid\"}", player_1_token); response = call(&api, HTTP_API_ROUTE_CONFIG, HTTP_API_POST, command, NULL); expect_code(&response, 400U, "INVALID_MODE"); snprintf(command, sizeof(command), "{\"token\":\"%s\",\"gameId\":1,\"mode\":\"human\"}", player_2_token); response = call(&api, HTTP_API_ROUTE_CONFIG, HTTP_API_POST, command, NULL); expect_code(&response, 403U, "FORBIDDEN_ROLE"); snprintf(command, sizeof(command), "{\"token\":\"%s\",\"gameId\":1,\"mode\":\"human\"}", player_1_token); response = call(&api, HTTP_API_ROUTE_CONFIG, HTTP_API_POST, command, NULL); assert(response.status == 200U); snprintf(command, sizeof(command), "{\"token\":\"%s\",\"gameId\":1}", player_1_token); response = call(&api, HTTP_API_ROUTE_START, HTTP_API_POST, command, NULL); assert(response.status == 200U); snprintf(command, sizeof(command), "{\"token\":\"%s\",\"gameId\":1,\"mode\":\"human\"}", player_1_token); response = call(&api, HTTP_API_ROUTE_CONFIG, HTTP_API_POST, command, NULL); expect_code(&response, 409U, "WRONG_PHASE"); response = call(&api, HTTP_API_ROUTE_STATE, HTTP_API_GET, NULL, NULL); assert(response.status == 200U && strstr(response.body, "\"viewer\":\"spectator\"") != NULL); response = call(&api, HTTP_API_ROUTE_STATE, HTTP_API_GET, NULL, player_1_token); assert(response.status == 200U && strstr(response.body, "\"viewer\":\"player1\"") != NULL); response = call(&api, HTTP_API_ROUTE_STATE, HTTP_API_GET, NULL, "bad"); expect_code(&response, 401U, "UNAUTHORIZED"); response = call(&api, HTTP_API_ROUTE_STATISTICS, HTTP_API_GET, NULL, player_1_token); assert(response.status == 200U && strstr(response.body, "\"viewer\":\"player1\"") != NULL && strstr(response.body, "\"match\":[[") != NULL && strstr(response.body, "\"cumulative\":[[") != NULL); response = call(&api, HTTP_API_ROUTE_STATISTICS, HTTP_API_GET, NULL, "bad"); expect_code(&response, 401U, "UNAUTHORIZED"); snprintf(command, sizeof(command), "{\"token\":\"%s\",\"gameId\":1,\"x\":10,\"y\":0}", player_1_token); response = call(&api, HTTP_API_ROUTE_SHOT, HTTP_API_POST, command, NULL); expect_code(&response, 400U, "INVALID_COORDINATE"); snprintf(command, sizeof(command), "{\"token\":\"%s\",\"gameId\":1,\"x\":0,\"y\":0}", spectator_token); response = call(&api, HTTP_API_ROUTE_SHOT, HTTP_API_POST, command, NULL); expect_code(&response, 403U, "FORBIDDEN_ROLE"); application.lifecycle.game.state.current_player = 0U; application.lifecycle.game.state.boards[1].cells[0] = CELL_SHIP; snprintf(command, sizeof(command), "{\"token\":\"%s\",\"gameId\":1,\"x\":0,\"y\":0}", player_2_token); response = call(&api, HTTP_API_ROUTE_SHOT, HTTP_API_POST, command, NULL); expect_code(&response, 409U, "NOT_YOUR_TURN"); snprintf(command, sizeof(command), "{\"token\":\"%s\",\"gameId\":1,\"x\":0,\"y\":0}", player_1_token); response = call(&api, HTTP_API_ROUTE_SHOT, HTTP_API_POST, command, NULL); assert(response.status == 200U); response = call(&api, HTTP_API_ROUTE_SHOT, HTTP_API_POST, command, NULL); expect_code(&response, 409U, "CELL_ALREADY_SHOT"); application.lifecycle.game.state.phase = PHASE_FINISHED; snprintf(command, sizeof(command), "{\"token\":\"%s\",\"gameId\":1}", player_1_token); response = call(&api, HTTP_API_ROUTE_REMATCH, HTTP_API_POST, command, NULL); assert(response.status == 200U && application.lifecycle.game.state.phase == PHASE_REMATCH_WAIT); application.lifecycle.game.state.phase = PHASE_IN_PROGRESS; application.lifecycle.game.state.mode = MODE_HUMAN; application.lifecycle.sessions.entries[1].connected = false; response = call(&api, HTTP_API_ROUTE_ABORT, HTTP_API_POST, command, NULL); assert(response.status == 200U && application.lifecycle.game.state.phase == PHASE_LOBBY); for (uint8_t index = 0; index < kCommandQueueCapacity; ++index) { assert(application_enqueue(&application, &(app_command_t){.type = COMMAND_START})); } snprintf(command, sizeof(command), "{\"token\":\"%s\",\"gameId\":1}", player_1_token); response = call(&api, HTTP_API_ROUTE_REMATCH, HTTP_API_POST, command, NULL); expect_code(&response, 503U, "SERVER_BUSY"); } int main(void) { test_public_routes_and_parse_limits(); test_sessions_commands_and_state(); puts("http api tests passed"); return 0; }