From e48839a3e08e6882f0ec2fea01db4890df2d4e6c Mon Sep 17 00:00:00 2001 From: Mark Janssen <20283+praseodym@users.noreply.github.com> Date: Wed, 5 Mar 2025 16:26:18 +0100 Subject: [PATCH] Move `start_server` to library And use it in integration tests. --- backend/src/bin/abacus.rs | 86 ++------------------------------------ backend/src/lib.rs | 83 +++++++++++++++++++++++++++++++++++- backend/tests/utils/mod.rs | 5 +-- 3 files changed, 87 insertions(+), 87 deletions(-) diff --git a/backend/src/bin/abacus.rs b/backend/src/bin/abacus.rs index 966c703a9..35204a9d4 100644 --- a/backend/src/bin/abacus.rs +++ b/backend/src/bin/abacus.rs @@ -1,7 +1,5 @@ #[cfg(feature = "dev-database")] -use abacus::fixtures; -use abacus::router; -use axum::serve::ListenerExt; +use abacus::{fixtures, start_server}; use clap::Parser; use sqlx::{SqlitePool, sqlite::SqliteConnectOptions}; use std::{ @@ -9,8 +7,8 @@ use std::{ net::{Ipv4Addr, SocketAddr}, str::FromStr, }; -use tokio::{net::TcpListener, signal}; -use tracing::{info, level_filters::LevelFilter, trace}; +use tokio::net::TcpListener; +use tracing::{info, level_filters::LevelFilter}; use tracing_subscriber::EnvFilter; /// Abacus API and asset server @@ -35,27 +33,6 @@ struct Args { reset_database: bool, } -/// Start the API server on the given port, using the given database pool. -async fn start_server(pool: SqlitePool, listener: TcpListener) -> Result<(), Box> { - let app = router(pool)?; - - info!("Starting API server on http://{}", listener.local_addr()?); - let listener = listener.tap_io(|tcp_stream| { - if let Err(err) = tcp_stream.set_nodelay(true) { - trace!("failed to set TCP_NODELAY on incoming connection: {err:#}"); - } - }); - - axum::serve( - listener, - app.into_make_service_with_connect_info::(), - ) - .with_graceful_shutdown(shutdown_signal()) - .await?; - - Ok(()) -} - /// Main entry point for the application. Sets up the database, and starts the /// API server and in-memory file router on port 8080. #[tokio::main] @@ -102,60 +79,3 @@ async fn create_sqlite_pool( Ok(pool) } - -/// Graceful shutdown, useful for Docker containers. -/// -/// Copied from the -/// [axum graceful-shutdown example](https://github.com/tokio-rs/axum/blob/6318b57fda6b524b4d3c7909e07946e2b246ebd2/examples/graceful-shutdown/src/main.rs) -/// (under the MIT license). -async fn shutdown_signal() { - let ctrl_c = async { - signal::ctrl_c() - .await - .expect("failed to install Ctrl+C handler"); - }; - - #[cfg(unix)] - let terminate = async { - signal::unix::signal(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 => {}, - } -} - -#[cfg(test)] -mod test { - use sqlx::SqlitePool; - use test_log::test; - use tokio::net::TcpListener; - - use super::start_server; - - #[test(sqlx::test)] - async fn test_abacus_starts(pool: SqlitePool) { - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - - let task = tokio::spawn(async move { - start_server(pool, listener).await.unwrap(); - }); - - let result = reqwest::get(format!("http://{addr}/api/user/whoami")) - .await - .unwrap(); - - assert_eq!(result.status(), 401); - - task.abort(); - let _ = task.await; - } -} diff --git a/backend/src/lib.rs b/backend/src/lib.rs index fdce92c98..f7c5164bb 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -5,12 +5,15 @@ use axum::{ extract::FromRef, middleware, routing::{get, post, put}, + serve::ListenerExt, }; #[cfg(feature = "memory-serve")] use memory_serve::MemoryServe; use sqlx::SqlitePool; -use std::error::Error; +use std::{error::Error, net::SocketAddr}; +use tokio::{net::TcpListener, signal}; use tower_http::trace::TraceLayer; +use tracing::{info, trace}; #[cfg(feature = "openapi")] use utoipa_swagger_ui::SwaggerUi; @@ -241,3 +244,81 @@ pub fn create_openapi() -> utoipa::openapi::OpenApi { struct ApiDoc; ApiDoc::openapi() } + +/// Start the API server on the given port, using the given database pool. +pub async fn start_server(pool: SqlitePool, listener: TcpListener) -> Result<(), Box> { + let app = router(pool)?; + + info!("Starting API server on http://{}", listener.local_addr()?); + let listener = listener.tap_io(|tcp_stream| { + if let Err(err) = tcp_stream.set_nodelay(true) { + trace!("failed to set TCP_NODELAY on incoming connection: {err:#}"); + } + }); + + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(shutdown_signal()) + .await?; + + Ok(()) +} + +/// Graceful shutdown, useful for Docker containers. +/// +/// Copied from the +/// [axum graceful-shutdown example](https://github.com/tokio-rs/axum/blob/6318b57fda6b524b4d3c7909e07946e2b246ebd2/examples/graceful-shutdown/src/main.rs) +/// (under the MIT license). +async fn shutdown_signal() { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(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 => {}, + } +} + +#[cfg(test)] +mod test { + use sqlx::SqlitePool; + use test_log::test; + use tokio::net::TcpListener; + + use super::start_server; + + #[test(sqlx::test)] + async fn test_abacus_starts(pool: SqlitePool) { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + + let task = tokio::spawn(async move { + start_server(pool, listener).await.unwrap(); + }); + + let result = reqwest::get(format!("http://{addr}/api/user/whoami")) + .await + .unwrap(); + + assert_eq!(result.status(), 401); + + task.abort(); + let _ = task.await; + } +} diff --git a/backend/tests/utils/mod.rs b/backend/tests/utils/mod.rs index 2efa1573f..17d11cb30 100644 --- a/backend/tests/utils/mod.rs +++ b/backend/tests/utils/mod.rs @@ -4,14 +4,13 @@ use sqlx::SqlitePool; use std::net::SocketAddr; use tokio::net::TcpListener; -use abacus::router; +use abacus::start_server; pub async fn serve_api(pool: SqlitePool) -> SocketAddr { - let app = router(pool).unwrap(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); tokio::spawn(async move { - axum::serve(listener, app).await.unwrap(); + start_server(pool, listener).await.unwrap(); }); addr }