//! Test-only admin routes. **Compiled in always, but only registered when //! `EVENTSNAP_TEST_MODE=1` is set in the environment.** The route returns a hard //! 404 in production builds because [`crate::main`] skips registering the handler. //! //! These exist to give the Playwright E2E suite a quick "reset everything" //! escape hatch without forcing tests to maintain raw SQL fixtures or spin up a //! fresh database container per test. use axum::extract::State; use axum::http::StatusCode; use crate::auth::middleware::RequireAdmin; use crate::error::AppError; use crate::state::AppState; /// Truncates every event-scoped table, wipes media on disk, and reseeds the /// `config` table from migration defaults. Requires an admin JWT — even with /// `EVENTSNAP_TEST_MODE=1` it cannot be hit anonymously. pub async fn truncate_all( State(state): State, RequireAdmin(_auth): RequireAdmin, ) -> Result { // Truncate in dependency order doesn't matter with CASCADE, but listing the // tables explicitly makes the blast radius obvious in code review. sqlx::query( r#"TRUNCATE comment_hashtag, upload_hashtag, hashtag, "like", comment, export_job, upload, session, "user", event, config RESTART IDENTITY CASCADE"#, ) .execute(&state.pool) .await?; // Reseed config — mirrors migrations 005 and 009. Kept in sync by hand // because pulling SQL out of the migration files at runtime is fragile. sqlx::query( r#"INSERT INTO config (key, value) VALUES ('max_image_size_mb', '20'), ('max_video_size_mb', '500'), ('upload_rate_per_hour', '10'), ('feed_rate_per_min', '60'), ('export_rate_per_day', '3'), ('quota_tolerance', '0.75'), ('estimated_guest_count', '100'), ('compression_concurrency', '2'), ('rate_limits_enabled', 'false'), ('upload_rate_enabled', 'false'), ('feed_rate_enabled', 'false'), ('export_rate_enabled', 'false'), ('join_rate_enabled', 'false'), ('quota_enabled', 'false'), ('storage_quota_enabled', 'false'), ('upload_count_quota_enabled', 'false'), ('privacy_note', '') ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value"#, ) .execute(&state.pool) .await?; // Wipe media directory. Best-effort: if it doesn't exist, that's fine. let _ = tokio::fs::remove_dir_all(&state.config.media_path).await; let _ = tokio::fs::create_dir_all(&state.config.media_path).await; // The rate limiter holds an in-memory HashMap; clear it so a previous test's // counters don't leak into the next one. state.rate_limiter.clear(); Ok(StatusCode::NO_CONTENT) } /// Returns whether the truncate endpoint is enabled. Used by the e2e harness /// during global-setup to fail loud if the test backend was started without /// `EVENTSNAP_TEST_MODE=1`. pub fn is_test_mode() -> bool { std::env::var("EVENTSNAP_TEST_MODE").as_deref() == Ok("1") }