From 205c871d310faad94eb6540c1a4c35fadbb5a051 Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Wed, 29 Apr 2026 04:08:31 +0200 Subject: [PATCH] feat(tests): add /__test__/reset endpoint and /health route (gated on TT_TEST_MODE) --- backend/src/main.rs | 18 +++++++++++--- backend/src/routes/auth_routes.rs | 4 ++-- backend/src/routes/mod.rs | 14 +++++++---- backend/src/routes/test_reset.rs | 39 +++++++++++++++++++++++++++++++ backend/src/test_helpers.rs | 4 ++-- 5 files changed, 68 insertions(+), 11 deletions(-) create mode 100644 backend/src/routes/test_reset.rs diff --git a/backend/src/main.rs b/backend/src/main.rs index 3f0ed5a..40fc6e3 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -7,7 +7,7 @@ mod routes; #[cfg(test)] mod test_helpers; -use axum::Router; +use axum::routing::get; use tracing_subscriber::EnvFilter; use tower_http::services::{ServeDir, ServeFile}; @@ -17,12 +17,24 @@ async fn main() { .with_env_filter(EnvFilter::from_default_env()) .init(); + let test_mode = std::env::var("TT_TEST_MODE").as_deref() == Ok("1"); + + if test_mode { + let seed_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("demo/demo_seed.sql"); + let seed = std::fs::read_to_string(&seed_path) + .expect("demo/demo_seed.sql not found"); + routes::test_reset::SEED_SQL.set(seed).ok(); + tracing::warn!("TT_TEST_MODE active — /__test__/reset is enabled"); + } + let pool = db::init().await.expect("db init failed"); - + let static_dir = std::env::var("STATIC_DIR") .unwrap_or_else(|_| "../frontend/build".into()); - let app = routes::build(pool) + let app = routes::build(pool, test_mode) + .route("/health", get(|| async { "ok" })) .fallback_service( ServeDir::new(&static_dir) .fallback(ServeFile::new(format!("{static_dir}/index.html"))) diff --git a/backend/src/routes/auth_routes.rs b/backend/src/routes/auth_routes.rs index 1c8e912..7cd6cf8 100644 --- a/backend/src/routes/auth_routes.rs +++ b/backend/src/routes/auth_routes.rs @@ -40,7 +40,7 @@ mod tests { .bind("Test").bind("t@test.com").bind(&hash) .execute(&pool).await.unwrap(); - let app = crate::routes::build(pool); + let app = crate::routes::build(pool, false); let (status, body) = post_json(app, "/api/auth/login", "", json!({"email":"t@test.com","password":"secret"})).await; assert_eq!(status, 200); @@ -56,7 +56,7 @@ mod tests { .bind("Test").bind("t@test.com").bind(&hash) .execute(&pool).await.unwrap(); - let app = crate::routes::build(pool); + let app = crate::routes::build(pool, false); let (status, _) = post_json(app, "/api/auth/login", "", json!({"email":"t@test.com","password":"wrong"})).await; assert_eq!(status, 401); diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs index 5d8435f..c357067 100644 --- a/backend/src/routes/mod.rs +++ b/backend/src/routes/mod.rs @@ -12,9 +12,10 @@ mod attendance; mod notes; mod export; mod tutors; +pub mod test_reset; -pub fn build(pool: SqlitePool) -> Router { - Router::new() +pub fn build(pool: SqlitePool, test_mode: bool) -> Router { + let mut router = Router::new() .merge(auth_routes::router()) .merge(checkin::router()) .merge(courses::router()) @@ -23,8 +24,13 @@ pub fn build(pool: SqlitePool) -> Router { .merge(attendance::router()) .merge(notes::router()) .merge(export::router()) - .merge(tutors::router()) - .with_state(pool) + .merge(tutors::router()); + + if test_mode { + router = router.merge(test_reset::router()); + } + + router.with_state(pool) } /// Verify that `tutor_id` is a member of `course_id` via the tutor_courses join table. diff --git a/backend/src/routes/test_reset.rs b/backend/src/routes/test_reset.rs new file mode 100644 index 0000000..102d5e7 --- /dev/null +++ b/backend/src/routes/test_reset.rs @@ -0,0 +1,39 @@ +use axum::{extract::State, http::StatusCode, routing::post, Router}; +use sqlx::SqlitePool; + +use crate::error::AppError; + +// Seed SQL loaded once at startup, reused per reset call. +pub static SEED_SQL: std::sync::OnceLock = std::sync::OnceLock::new(); + +async fn reset(State(pool): State) -> Result { + let seed = SEED_SQL.get().expect("SEED_SQL not initialised"); + + let mut tx = pool.begin().await?; + + // Delete in FK-safe order (children → parents) + sqlx::query("DELETE FROM attendances").execute(&mut *tx).await?; + sqlx::query("DELETE FROM notes").execute(&mut *tx).await?; + sqlx::query("DELETE FROM slots").execute(&mut *tx).await?; + sqlx::query("DELETE FROM sessions").execute(&mut *tx).await?; + sqlx::query("DELETE FROM tutor_courses").execute(&mut *tx).await?; + sqlx::query("DELETE FROM students").execute(&mut *tx).await?; + sqlx::query("DELETE FROM rooms").execute(&mut *tx).await?; + sqlx::query("DELETE FROM tutors").execute(&mut *tx).await?; + sqlx::query("DELETE FROM courses").execute(&mut *tx).await?; + + // Re-apply seed (multiple statements; SQLx requires executing them individually) + for stmt in seed.split(';') { + let stmt = stmt.trim(); + if !stmt.is_empty() { + sqlx::query(stmt).execute(&mut *tx).await?; + } + } + + tx.commit().await?; + Ok(StatusCode::NO_CONTENT) +} + +pub fn router() -> Router { + Router::new().route("/__test__/reset", post(reset)) +} diff --git a/backend/src/test_helpers.rs b/backend/src/test_helpers.rs index 5082d91..339be96 100644 --- a/backend/src/test_helpers.rs +++ b/backend/src/test_helpers.rs @@ -26,7 +26,7 @@ pub async fn make_token(pool: &SqlitePool, email: &str, is_superadmin: bool) -> pub async fn build_test_app(pool: SqlitePool) -> (Router, String) { unsafe { std::env::set_var("JWT_SECRET", "testsecret"); } let token = make_token(&pool, "tutor@test.com", false).await; - let app = crate::routes::build(pool); + let app = crate::routes::build(pool, false); (app, format!("Bearer {token}")) } @@ -34,7 +34,7 @@ pub async fn build_test_app(pool: SqlitePool) -> (Router, String) { pub async fn build_test_admin_app(pool: SqlitePool) -> (Router, String) { unsafe { std::env::set_var("JWT_SECRET", "testsecret"); } let token = make_token(&pool, "admin@test.com", true).await; - let app = crate::routes::build(pool); + let app = crate::routes::build(pool, false); (app, format!("Bearer {token}")) }