feat(tests): add /__test__/reset endpoint and /health route (gated on TT_TEST_MODE)

This commit is contained in:
2026-04-29 04:08:31 +02:00
parent 4dce11dd26
commit 205c871d31
5 changed files with 68 additions and 11 deletions

View File

@@ -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")))

View File

@@ -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);

View File

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

View File

@@ -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<String> = std::sync::OnceLock::new();
async fn reset(State(pool): State<SqlitePool>) -> Result<StatusCode, AppError> {
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<SqlitePool> {
Router::new().route("/__test__/reset", post(reset))
}

View File

@@ -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}"))
}