feat: harden errors, recovery, and resource usage

This commit is contained in:
2026-08-29 23:40:37 +03:00
parent c8e0c9168b
commit d67fd327c9
10 changed files with 525 additions and 32 deletions
+7 -2
View File
@@ -1,7 +1,7 @@
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 test_http_api test_sync_service test_human_game_integration test_bot_game_integration
all: test_command_queue test_game_domain test_bot_player test_game_lifecycle test_state_presenter test_http_api test_sync_service test_human_game_integration test_bot_game_integration test_robustness
test_command_queue: test_command_queue.c ../../src/command_queue.c
$(CC) $(CFLAGS) $^ -o $@
@@ -16,6 +16,7 @@ run: all
./test_sync_service
./test_human_game_integration
./test_bot_game_integration
./test_robustness
test_game_domain: test_game_domain.c ../../src/fleet_generator.c ../../src/game_engine.c
$(CC) $(CFLAGS) $^ -o $@
@@ -41,5 +42,9 @@ test_human_game_integration: test_human_game_integration.c ../../src/http_api.c
test_bot_game_integration: test_bot_game_integration.c ../../src/session_manager.c ../../src/game_lifecycle.c ../../src/bot_player.c ../../src/fleet_generator.c ../../src/game_engine.c
$(CC) $(CFLAGS) $^ -o $@
test_robustness: test_robustness.c ../../src/http_api.c ../../src/sync_service.c ../../src/application.c ../../src/command_queue.c ../../src/session_manager.c ../../src/game_lifecycle.c ../../src/bot_player.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 test_http_api test_sync_service test_human_game_integration test_bot_game_integration
rm -f test_command_queue test_game_domain test_bot_player test_game_lifecycle test_state_presenter test_http_api test_sync_service test_human_game_integration test_bot_game_integration test_robustness
+3 -2
View File
@@ -18,7 +18,7 @@ static http_api_t new_api(application_t *application, test_random_t *random) {
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"});
.connected_clients = 2U, .rejected_input = 3U, .reset_reason = 11, .wifi_state = "connected"});
return api;
}
@@ -55,7 +55,8 @@ static void test_public_routes_and_parse_limits(void) {
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);
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");
+89
View File
@@ -0,0 +1,89 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "http_api.h"
#include "sync_service.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 application_t new_application(test_random_t *random) {
application_t application;
application_init(&application, (random_source_t){.next_u32 = next_random, .context = random});
return application;
}
static void test_http_malformed_inputs_do_not_mutate(void) {
test_random_t random = {.value = 7U};
application_t application = new_application(&random);
http_api_t api;
http_api_init(&api, &application);
const char *const corpus[] = {"", "{", "[]", "{\"token\":", "{\"token\":null}",
"{\"name\":\"A\"}", "{\"token\":\"00000000000000000000000000000000\",\"gameId\":-1}",
"{\"type\":\"shot\"}", "{\"name\":\"A\",\"requestedRole\":\"player1\",\"extra\":1}"};
const http_api_route_t routes[] = {HTTP_API_ROUTE_JOIN, HTTP_API_ROUTE_RESUME, HTTP_API_ROUTE_CONFIG,
HTTP_API_ROUTE_START, HTTP_API_ROUTE_SHOT, HTTP_API_ROUTE_REMATCH, HTTP_API_ROUTE_ABORT};
for (size_t index = 0; index < 400U; ++index) {
const char *body = corpus[index % (sizeof(corpus) / sizeof(corpus[0]))];
const game_lifecycle_t before = application.lifecycle;
http_api_response_t response;
const http_api_request_t request = {.method = HTTP_API_POST, .route = routes[index % (sizeof(routes) / sizeof(routes[0]))],
.content_type_json = true, .body = body, .body_length = strlen(body)};
assert(http_api_handle(&api, &request, &response));
assert(response.status >= 400U && response.status < 600U && response.body_length < sizeof(response.body));
assert(memcmp(&before, &application.lifecycle, sizeof(before)) == 0);
}
char oversized[kRequestBodyCapacity + 2U];
memset(oversized, 'x', sizeof(oversized) - 1U);
oversized[sizeof(oversized) - 1U] = '\0';
http_api_response_t response;
const http_api_request_t request = {.method = HTTP_API_POST, .route = HTTP_API_ROUTE_JOIN, .content_type_json = true,
.body = oversized, .body_length = strlen(oversized)};
assert(http_api_handle(&api, &request, &response));
assert(response.status == 413U);
}
static void test_websocket_fuzz_and_connection_churn(void) {
test_random_t random = {.value = 19U};
application_t application = new_application(&random);
sync_service_t service;
sync_service_init(&service, &application);
for (uint16_t attempt = 0; attempt < 600U; ++attempt) {
char frame[kWebSocketFrameCapacity + 1U];
const size_t length = (size_t)(next_random(&random) % (kWebSocketFrameCapacity + 1U));
for (size_t index = 0; index < length; ++index) frame[index] = (char)(next_random(&random) & 0x7fU);
frame[length] = '\0';
const game_lifecycle_t before = application.lifecycle;
char output[kStateMessageCapacity] = {0};
size_t output_length = 0U;
bool changed = true;
bool close = false;
assert(sync_service_open(&service, 1, attempt));
assert(sync_service_receive(&service, 1, frame, length, attempt, output, &output_length, &changed, &close));
assert(!changed && output_length < sizeof(output));
assert(memcmp(&before, &application.lifecycle, sizeof(before)) == 0);
sync_service_close(&service, 1);
}
char too_large[kWebSocketFrameCapacity + 1U] = {0};
char output[kStateMessageCapacity] = {0};
size_t output_length = 0U;
bool changed = false;
bool close = false;
assert(sync_service_open(&service, 1, 0U));
assert(sync_service_receive(&service, 1, too_large, sizeof(too_large), 0U, output, &output_length, &changed, &close));
assert(!changed && strstr(output, "MALFORMED_JSON") != NULL);
sync_service_close(&service, 1);
}
int main(void) {
test_http_malformed_inputs_do_not_mutate();
test_websocket_fuzz_and_connection_churn();
puts("robustness tests passed");
return 0;
}