feat: implement the production HTTP API
This commit is contained in:
+7
-3
@@ -1,9 +1,9 @@
|
||||
CC ?= cc
|
||||
CFLAGS ?= -std=c11 -Wall -Wextra -Werror -I../../include
|
||||
|
||||
all: test_command_queue test_game_domain test_bot_player test_game_lifecycle test_state_presenter
|
||||
all: test_command_queue test_game_domain test_bot_player test_game_lifecycle test_state_presenter test_http_api
|
||||
|
||||
test_command_queue: test_command_queue.c ../../src/command_queue.c ../../src/application.c
|
||||
test_command_queue: test_command_queue.c ../../src/command_queue.c
|
||||
$(CC) $(CFLAGS) $^ -o $@
|
||||
|
||||
run: all
|
||||
@@ -12,6 +12,7 @@ run: all
|
||||
./test_bot_player
|
||||
./test_game_lifecycle
|
||||
./test_state_presenter
|
||||
./test_http_api
|
||||
|
||||
test_game_domain: test_game_domain.c ../../src/fleet_generator.c ../../src/game_engine.c
|
||||
$(CC) $(CFLAGS) $^ -o $@
|
||||
@@ -25,5 +26,8 @@ test_game_lifecycle: test_game_lifecycle.c ../../src/session_manager.c ../../src
|
||||
test_state_presenter: test_state_presenter.c ../../src/state_presenter.c
|
||||
$(CC) $(CFLAGS) $^ -o $@
|
||||
|
||||
test_http_api: test_http_api.c ../../src/http_api.c ../../src/application.c ../../src/command_queue.c ../../src/session_manager.c ../../src/game_lifecycle.c ../../src/fleet_generator.c ../../src/game_engine.c ../../src/state_presenter.c
|
||||
$(CC) $(CFLAGS) $^ -o $@
|
||||
|
||||
clean:
|
||||
rm -f test_command_queue test_game_domain test_bot_player test_game_lifecycle test_state_presenter
|
||||
rm -f test_command_queue test_game_domain test_bot_player test_game_lifecycle test_state_presenter test_http_api
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#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, .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);
|
||||
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\":\"<bad>\",\"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_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");
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user