Files
tutortool/backend/src/main.rs
s0wlz (Matthias Puchstein) 681b43174b
Some checks failed
CI / test (pull_request) Has been cancelled
CI / test (push) Failing after 4m51s
fix: implement random-port discovery for CI E2E backend
When PORT=0, the backend now writes its actual bound port to
data/test/.port. test-env.sh reads that file when TT_TEST_PORT=0
so all targets (test-up, test-reset, test-down) resolve the real URL.
test-up waits for .port to appear before the health-check loop.
2026-05-05 02:24:29 +02:00

131 lines
4.2 KiB
Rust

mod auth;
mod db;
mod error;
mod models;
mod routes;
#[cfg(test)]
mod test_helpers;
use axum::routing::get;
use std::net::SocketAddr;
use tower_http::services::{ServeDir, ServeFile};
use tracing_subscriber::EnvFilter;
#[derive(Clone)]
pub struct AppState {
pub pool: sqlx::SqlitePool,
pub jwt_secret: String,
pub test_mode: bool,
}
impl axum::extract::FromRef<AppState> for sqlx::SqlitePool {
fn from_ref(state: &AppState) -> Self {
state.pool.clone()
}
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
let jwt_secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
#[cfg(debug_assertions)]
let test_mode = std::env::var("TT_TEST_MODE").as_deref() == Ok("1");
#[cfg(not(debug_assertions))]
let test_mode = false;
if test_mode {
// Extra safeguard: panic if someone tries to enable test mode in what looks like production
if std::env::var("APP_ENV").as_deref() == Ok("production") {
panic!("TT_TEST_MODE cannot be active when APP_ENV=production");
}
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");
db::maybe_seed_demo(&pool).await;
let state = AppState {
pool,
jwt_secret,
test_mode,
};
let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "../frontend/build".into());
let app = routes::build(state, test_mode)
.route("/health", get(|| async { "ok" }))
.fallback_service(
ServeDir::new(&static_dir).fallback(ServeFile::new(format!("{static_dir}/index.html"))),
)
.layer(tower_http::set_header::SetResponseHeaderLayer::overriding(
axum::http::header::CONTENT_SECURITY_POLICY,
axum::http::HeaderValue::from_static("default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self';"),
))
.layer(tower_http::set_header::SetResponseHeaderLayer::overriding(
axum::http::header::X_CONTENT_TYPE_OPTIONS,
axum::http::HeaderValue::from_static("nosniff"),
))
.layer(tower_http::set_header::SetResponseHeaderLayer::overriding(
axum::http::header::X_FRAME_OPTIONS,
axum::http::HeaderValue::from_static("DENY"),
))
.layer(tower_http::trace::TraceLayer::new_for_http());
let port = std::env::var("PORT").unwrap_or_else(|_| "3000".into());
let addr: SocketAddr = format!("0.0.0.0:{port}").parse().expect("invalid address");
let listener = tokio::net::TcpListener::bind(&addr)
.await
.expect("failed to bind");
let actual_addr = listener.local_addr().expect("failed to get local addr");
tracing::info!("listening on {}", actual_addr);
// When started with PORT=0 (random), write actual port so the test harness can discover it
if port == "0" {
let _ = std::fs::create_dir_all("data/test");
let _ = std::fs::write("data/test/.port", actual_addr.port().to_string());
}
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.with_graceful_shutdown(shutdown_signal())
.await
.expect("failed to serve");
}
async fn shutdown_signal() {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
tracing::info!("signal received, starting graceful shutdown");
}