trainlog/tui/tests/test_web_exercises.c

224 lines
11 KiB
C

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sqlite3.h>
#include "database_internal.h"
#include "trainlog/database.h"
#include "trainlog/web_exercises.h"
#define CHECK(expression) \
do { \
if (!(expression)) { \
(void)fprintf(stderr, "CHECK failed at %s:%d: %s\n", __FILE__, __LINE__, #expression); \
return false; \
} \
} while (0)
static int scalar(TrainlogDatabase *database, const char *sql) {
sqlite3_stmt *statement = NULL;
int value = -1;
if (sqlite3_prepare_v2(database->connection, sql, -1, &statement, NULL) == SQLITE_OK &&
sqlite3_step(statement) == SQLITE_ROW) {
value = sqlite3_column_int(statement, 0);
}
(void)sqlite3_finalize(statement);
return value;
}
static bool json_string(const char *json, const char *key, char *output, size_t capacity) {
char marker[96];
const char *start;
const char *end;
size_t length;
CHECK(snprintf(marker, sizeof(marker), "\"%s\":\"", key) > 0);
start = strstr(json, marker);
CHECK(start != NULL);
start += strlen(marker);
end = strchr(start, '"');
CHECK(end != NULL);
length = (size_t)(end - start);
CHECK(length > 0U && length < capacity);
(void)memcpy(output, start, length);
output[length] = '\0';
return true;
}
static bool lifecycle_preserves_history_and_is_causal(void) {
static const char CREATE[] =
"{\"name\":\" Test Web \u00c9paules \" ,\"recording_mode\":\"sets\","
"\"tracking_mode\":\"reps\",\"data_fields\":0,"
"\"primary_zone_id\":\"shoulders\",\"secondary_zone_ids\":[\"arms\"]}";
static const char UPDATE[] = "{\"name\":\"Test Web Renomm\u00e9\",\"recording_mode\":\"sets\","
"\"tracking_mode\":\"duration\",\"data_fields\":0,"
"\"primary_zone_id\":\"core\",\"secondary_zone_ids\":[]}";
TrainlogDatabase *database = NULL;
TrainlogWebExercisesQuery query = {0U, 24U, "test web", "sets_reps", "shoulders", false};
char *created = NULL;
char *page = NULL;
char *updated = NULL;
char *retired = NULL;
size_t size = 0U;
char exercise_id[TRAINLOG_ID_MAX + 1U];
char revision[80];
char updated_revision[80];
char sql[2048];
int initial_exercise_count;
CHECK(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK);
initial_exercise_count = scalar(database, "SELECT COUNT(*) FROM exercises");
CHECK(trainlog_web_exercises_create_json(database, CREATE, strlen(CREATE), &created, &size) ==
TRAINLOG_STATUS_OK);
CHECK(json_string(created, "exercise_id", exercise_id, sizeof(exercise_id)));
CHECK(json_string(created, "revision", revision, sizeof(revision)));
CHECK(strstr(created, "\"primary_zone_id\":\"shoulders\"") != NULL);
CHECK(strstr(created, "\"secondary_zone_ids\":[\"arms\"]") != NULL);
CHECK(strstr(created, "\"retireable\":true") != NULL);
CHECK(snprintf(sql,
sizeof(sql),
"SELECT COUNT(*) FROM exercise_profile_state s JOIN exercises e ON e.id=s."
"exercise_row_id WHERE e.exercise_id='%s'",
exercise_id) > 0);
CHECK(scalar(database, sql) == 1);
CHECK(snprintf(sql,
sizeof(sql),
"SELECT COUNT(*) FROM exercise_body_zones z JOIN exercises e ON e.id=z."
"exercise_row_id WHERE e.exercise_id='%s'",
exercise_id) > 0);
CHECK(scalar(database, sql) == 2);
CHECK(trainlog_web_exercises_list_json(database, &query, &page, &size) == TRAINLOG_STATUS_OK);
CHECK(strstr(page, exercise_id) != NULL);
free(page);
page = NULL;
CHECK(snprintf(sql,
sizeof(sql),
"INSERT INTO sessions(session_id,started_at,ended_at,session_type) VALUES("
"'se_aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa','2026-09-20T10:00:00Z',"
"'2026-09-20T11:00:00Z','training');"
"INSERT INTO session_exercises(entry_id,session_row_id,exercise_row_id,"
"recording_mode,tracking_mode,data_fields,position,load_mode,rest_seconds) "
"SELECT 'sxe_aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',s.id,e.id,'sets','reps',0,"
"0,'external',0 FROM sessions s,exercises e WHERE e.exercise_id='%s';"
"INSERT INTO performed_sets(session_exercise_row_id,position,reps,weight_kg) "
"SELECT id,0,8,42.5 FROM session_exercises;",
exercise_id) > 0);
CHECK(sqlite3_exec(database->connection, sql, NULL, NULL, NULL) == SQLITE_OK);
CHECK(trainlog_web_exercises_update_json(
database, exercise_id, revision, UPDATE, strlen(UPDATE), &updated, &size) ==
TRAINLOG_STATUS_OK);
CHECK(json_string(updated, "revision", updated_revision, sizeof(updated_revision)));
CHECK(strcmp(revision, updated_revision) != 0);
CHECK(strstr(updated, "\"tracking_mode\":\"duration\"") != NULL);
CHECK(snprintf(sql,
sizeof(sql),
"SELECT COUNT(*) FROM exercise_profile_revisions r JOIN exercises e ON e.id=r."
"exercise_row_id WHERE e.exercise_id='%s'",
exercise_id) > 0);
CHECK(scalar(database, sql) >= 2);
CHECK(snprintf(sql,
sizeof(sql),
"SELECT COUNT(*) FROM exercise_body_zones z JOIN exercises e ON e.id=z."
"exercise_row_id WHERE e.exercise_id='%s'",
exercise_id) > 0);
CHECK(scalar(database, sql) == 1);
CHECK(scalar(database,
"SELECT COUNT(*) FROM session_exercises WHERE recording_mode='sets' AND "
"tracking_mode='reps' AND data_fields=0") == 1);
CHECK(scalar(database, "SELECT COUNT(*) FROM performed_sets WHERE reps=8 AND weight_kg=42.5") ==
1);
CHECK(trainlog_web_exercises_update_json(
database, exercise_id, revision, UPDATE, strlen(UPDATE), &page, &size) ==
TRAINLOG_STATUS_CONFLICT);
CHECK(page == NULL);
CHECK(trainlog_web_exercises_retire_json(
database, exercise_id, updated_revision, &retired, &size) == TRAINLOG_STATUS_OK);
CHECK(strstr(retired, "\"state\":\"retired\"") != NULL);
CHECK(scalar(database,
"SELECT COUNT(*) FROM sync_causal_state WHERE target_kind='exercise' AND "
"deleted=1") == 1);
CHECK(scalar(database, "SELECT COUNT(*) FROM exercises") == initial_exercise_count + 1);
CHECK(scalar(database, "SELECT COUNT(*) FROM sessions") == 1);
query.search = "";
query.profile = "";
query.zone_id = NULL;
CHECK(trainlog_web_exercises_list_json(database, &query, &page, &size) == TRAINLOG_STATUS_OK);
CHECK(strstr(page, exercise_id) == NULL);
free(page);
page = NULL;
free(retired);
retired = NULL;
CHECK(trainlog_web_exercises_retire_json(
database, exercise_id, updated_revision, &retired, &size) == TRAINLOG_STATUS_OK);
CHECK(scalar(database,
"SELECT COUNT(*) FROM sync_causal_operations WHERE target_kind='exercise'") == 1);
free(created);
free(updated);
free(retired);
trainlog_database_close(database);
return true;
}
static bool validation_collision_and_builtin_protection(void) {
static const char INVALID[] = "{\"name\":\"Invalide\",\"recording_mode\":\"continuous\","
"\"tracking_mode\":\"reps\",\"data_fields\":0,"
"\"primary_zone_id\":null,\"secondary_zone_ids\":[]}";
static const char DUPLICATE[] =
"{\"name\":\" LEG EXTENSION \" ,\"recording_mode\":\"sets\","
"\"tracking_mode\":\"reps\",\"data_fields\":0,"
"\"primary_zone_id\":\"chest\",\"secondary_zone_ids\":[]}";
static const char DUPLICATE_KEY[] =
"{\"name\":\"A\",\"name\":\"B\",\"recording_mode\":\"sets\","
"\"tracking_mode\":\"reps\",\"data_fields\":0,"
"\"primary_zone_id\":\"chest\",\"secondary_zone_ids\":[]}";
static const char SETS_FIELDS[] = "{\"name\":\"Fields\",\"recording_mode\":\"sets\","
"\"tracking_mode\":\"reps\",\"data_fields\":1,"
"\"primary_zone_id\":\"chest\",\"secondary_zone_ids\":[]}";
TrainlogDatabase *database = NULL;
char *json = NULL;
size_t size = 0U;
char revision[80];
CHECK(trainlog_database_open(":memory:", &database) == TRAINLOG_STATUS_OK);
CHECK(trainlog_database_insert_exercise_profiled(database,
"ex_1872246a-39ae-44dc-b58d-f87e90ca49ab",
"Leg Extension",
"leg extension",
TRAINLOG_TRACKING_REPS,
TRAINLOG_RECORDING_SETS,
0U) == TRAINLOG_STATUS_OK);
CHECK(trainlog_web_exercises_create_json(database, INVALID, strlen(INVALID), &json, &size) ==
TRAINLOG_STATUS_INVALID_ARGUMENT);
CHECK(trainlog_web_exercises_create_json(
database, DUPLICATE_KEY, strlen(DUPLICATE_KEY), &json, &size) ==
TRAINLOG_STATUS_INVALID_ARGUMENT);
CHECK(trainlog_web_exercises_create_json(
database, SETS_FIELDS, strlen(SETS_FIELDS), &json, &size) ==
TRAINLOG_STATUS_INVALID_ARGUMENT);
CHECK(trainlog_web_exercises_create_json(
database, DUPLICATE, strlen(DUPLICATE), &json, &size) == TRAINLOG_STATUS_CONFLICT);
CHECK(trainlog_web_exercises_detail_json(
database, "ex_1872246a-39ae-44dc-b58d-f87e90ca49ab", &json, &size) ==
TRAINLOG_STATUS_OK);
CHECK(json_string(json, "revision", revision, sizeof(revision)));
CHECK(strstr(json, "\"retireable\":false") != NULL);
free(json);
json = NULL;
CHECK(trainlog_web_exercises_retire_json(
database, "ex_1872246a-39ae-44dc-b58d-f87e90ca49ab", revision, &json, &size) ==
TRAINLOG_STATUS_INVALID_ARGUMENT);
CHECK(scalar(database, "SELECT COUNT(*) FROM sync_causal_operations") == 0);
trainlog_database_close(database);
return true;
}
int main(void) {
if (!lifecycle_preserves_history_and_is_causal() ||
!validation_collision_and_builtin_protection()) {
return EXIT_FAILURE;
}
(void)puts("web exercises tests passed");
return EXIT_SUCCESS;
}