test(web): cover dashboard failure states

This commit is contained in:
fy59 2026-09-16 13:39:20 +02:00
parent f894f744f3
commit 6f34233751
3 changed files with 53 additions and 3 deletions

View file

@ -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.

View file

@ -1,7 +1,11 @@
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sqlite3.h>
#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;}

View file

@ -13,6 +13,8 @@
#include <time.h>
#include <unistd.h>
#include <sqlite3.h>
#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;