From 6f34233751dad52f6dade135cdc2846736f2224c Mon Sep 17 00:00:00 2001 From: fy59 Date: Wed, 16 Sep 2026 13:39:20 +0200 Subject: [PATCH] test(web): cover dashboard failure states --- docs/tests.md | 3 ++- tui/tests/test_web_dashboard.c | 8 +++++- tui/tests/test_web_server.c | 45 +++++++++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/docs/tests.md b/docs/tests.md index 741cc87..4ca430c 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -61,7 +61,8 @@ day, an unfinished session, continuous activity, MAX-only sessions, primary zone deduplication, catalogue-without-history exclusion, 90/30-day windows, recent-MAX truncation/`partial`, Footer facts and JSON escaping/overflow. The HTTP lifecycle test additionally proves `GET /api/v1/dashboard`, JSON content -type/version, unavailable reasons and `POST` rejection without weakening Host, +type/version, unavailable reasons, corrupt-timestamp `invalid_data`, Core-error +translation to bounded HTTP 500 and `POST` rejection without weakening Host, CSP, CORS or unknown-route behavior. Vitest validates strict parsing, real Footer population, empty fallback, malformed snapshots and absence of invented tile metrics; the seven tile bodies remain placeholders. diff --git a/tui/tests/test_web_dashboard.c b/tui/tests/test_web_dashboard.c index bfd6b1a..c43b3da 100644 --- a/tui/tests/test_web_dashboard.c +++ b/tui/tests/test_web_dashboard.c @@ -1,7 +1,11 @@ #include #include #include +#include #include +#include + +#include #include "trainlog/web_dashboard.h" @@ -36,4 +40,6 @@ static bool test_empty_and_real_facts(void){ static bool test_json_escaping_and_limit(void){TrainlogWebDashboardSnapshot s;char json[TRAINLOG_WEB_JSON_CAPACITY];char tiny[8];size_t size;(void)memset(&s,0,sizeof(s));(void)snprintf(s.user_display_name,sizeof(s.user_display_name),"A\"B\\C\nÉ");(void)snprintf(s.generated_at,sizeof(s.generated_at),"2026-09-16T12:00:00Z");s.next_session_reason="no_persisted_executable_plan";s.cardio_reason="no_cardio_data_source";CHECK(trainlog_web_dashboard_serialize(&s,json,sizeof(json),&size)==TRAINLOG_STATUS_OK&&size>0U);CHECK(strstr(json,"A\\\"B\\\\C\\nÉ")!=NULL);CHECK(trainlog_web_dashboard_serialize(&s,tiny,sizeof(tiny),&size)==TRAINLOG_STATUS_SYSTEM_ERROR);s.user_display_name[0]=(char)0xc3;s.user_display_name[1]='\0';CHECK(trainlog_web_dashboard_serialize(&s,json,sizeof(json),&size)==TRAINLOG_STATUS_SYSTEM_ERROR);return true;} -int main(void){return test_empty_and_real_facts()&&test_json_escaping_and_limit()?0:1;} +static bool test_corrupt_timestamp_sets_invalid_data(void){char path[]="/tmp/trainlog-web-dashboard-XXXXXX";int fd=mkstemp(path);TrainlogDatabase*d=NULL;sqlite3*raw=NULL;TrainlogWebDashboardSnapshot s;TrainlogWebDashboardQuery q={INT64_C(1789560000)};CHECK(fd>=0);CHECK(close(fd)==0);CHECK(trainlog_database_open(path,&d)==TRAINLOG_STATUS_OK);CHECK(insert_fact(d,"se_92000000-0000-4000-8000-000000000001","en_92000000-0000-4000-8000-000000000001","2026-09-15T10:00:00+02:00",0)==false);CHECK(trainlog_database_insert_exercise_profiled(d,"ex_90000000-0000-4000-8000-000000000001","Corruptible","corruptible",TRAINLOG_TRACKING_REPS,TRAINLOG_RECORDING_SETS,(TrainlogExerciseDataFields)0)==TRAINLOG_STATUS_OK);CHECK(insert_fact(d,"se_92000000-0000-4000-8000-000000000001","en_92000000-0000-4000-8000-000000000001","2026-09-15T10:00:00+02:00",0));trainlog_database_close(d);d=NULL;CHECK(sqlite3_open(path,&raw)==SQLITE_OK);CHECK(sqlite3_exec(raw,"UPDATE sessions SET started_at='corrupt';",NULL,NULL,NULL)==SQLITE_OK);CHECK(sqlite3_close(raw)==SQLITE_OK);raw=NULL;CHECK(trainlog_database_open(path,&d)==TRAINLOG_STATUS_OK);CHECK(trainlog_web_dashboard_load(d,&q,&s)==TRAINLOG_STATUS_OK);CHECK(s.invalid_data);trainlog_database_close(d);CHECK(unlink(path)==0);return true;} + +int main(void){return test_empty_and_real_facts()&&test_json_escaping_and_limit()&&test_corrupt_timestamp_sets_invalid_data()?0:1;} diff --git a/tui/tests/test_web_server.c b/tui/tests/test_web_server.c index 9b4be8c..62a9c74 100644 --- a/tui/tests/test_web_server.c +++ b/tui/tests/test_web_server.c @@ -13,6 +13,8 @@ #include #include +#include + #include "trainlog/database.h" #include "trainlog/web_server.h" #include "web_assets.h" @@ -252,6 +254,46 @@ static bool test_port_in_use(TrainlogDatabase *database) return true; } +static bool test_dashboard_core_error_translation(void) +{ + char path[] = "/tmp/trainlog-web-error-XXXXXX"; + char response[4096]; + TrainlogDatabase *database = NULL; + sqlite3 *raw = NULL; + uint16_t port; + pid_t child; + int fd = mkstemp(path); + int status; + CHECK(fd >= 0 && close(fd) == 0); + CHECK(trainlog_database_open(path, &database) == TRAINLOG_STATUS_OK); + trainlog_database_close(database); + database = NULL; + CHECK(sqlite3_open(path, &raw) == SQLITE_OK); + CHECK(sqlite3_exec(raw, "DROP TABLE sessions;", NULL, NULL, NULL) == SQLITE_OK); + CHECK(sqlite3_close(raw) == SQLITE_OK); + raw = NULL; + CHECK(trainlog_database_open(path, &database) == TRAINLOG_STATUS_OK); + CHECK(reserve_port(&port, 0) == -2); + child = fork(); + CHECK(child >= 0); + if (child == 0) { + volatile sig_atomic_t stop = 0; + char diagnostic[256]; + _exit(trainlog_web_server_run(database, port, &stop, diagnostic, + sizeof(diagnostic)) == 0 ? 0 : 1); + } + CHECK(exchange(port, + "GET /api/v1/dashboard HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n", + response, sizeof(response))); + CHECK(strstr(response, "HTTP/1.1 500") != NULL); + CHECK(strstr(response, "\"dashboard_unavailable\"") != NULL); + CHECK(kill(child, SIGTERM) == 0); + CHECK(waitpid(child, &status, 0) == child); + trainlog_database_close(database); + CHECK(unlink(path) == 0); + return true; +} + int main(void) { TrainlogDatabase *database = NULL; @@ -264,7 +306,8 @@ int main(void) passed = trainlog_web_server_run(database, 8080U, &stop, diagnostic, sizeof(diagnostic)) != 0 && strstr(diagnostic, "frontend") != NULL; } else { - passed = test_http_contract(database) && test_port_in_use(database); + passed = test_http_contract(database) && test_port_in_use(database) && + test_dashboard_core_error_translation(); } trainlog_database_close(database); return passed ? 0 : 1;