feat(tests): add /__test__/reset endpoint and /health route (gated on TT_TEST_MODE)
This commit is contained in:
@@ -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")))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
39
backend/src/routes/test_reset.rs
Normal file
39
backend/src/routes/test_reset.rs
Normal 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))
|
||||
}
|
||||
@@ -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}"))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user