//! # Server Infrastructure Utilities for Loco Framework Testing //! //! This module provides utility functions to test a server using the Loco //! framework. It includes helper functions to start the server from different //! configurations, such as from boot parameters, application context, or a //! custom route. These utilities are designed for test environments and use //! hardcoded ports and bindings. use loco_rs::{boot, controller::AppRoutes, prelude::*, tests_cfg::db::AppHook}; /// A simple asynchronous handler for GET requests. async fn get_action() -> Result { format::render().text("text response") } /// A simple asynchronous handler for POST requests. async fn post_action(_body: axum::body::Bytes) -> Result { format::render().text("text response") } /// Starts the server using the provided Loco [`boot::BootResult`] result. /// It uses hardcoded server parameters such as the port and binding address. /// /// After spawning the server task, this polls the bound address until it /// accepts a TCP connection (or a bounded timeout elapses), so callers can /// issue requests the instant the listener is actually up — no fixed sleep. pub async fn start_from_boot( boot_result: boot::BootResult, port: Option, ) -> tokio::task::JoinHandle<()> { let port = port.unwrap_or(TEST_PORT_SERVER); let handle = tokio::spawn(async move { boot::start::( boot_result, boot::ServeParams { port, binding: TEST_BINDING_SERVER.to_string(), }, false, ) .await .expect("start the server"); }); wait_until_ready(TEST_BINDING_SERVER, port).await; handle } /// Polls `binding:port` until it accepts a TCP connection so a test can proceed /// the moment the server is listening, replacing a fixed-duration sleep. Gives /// up after a bounded number of attempts (~5s) rather than hanging a broken /// boot forever — a failure then surfaces as the test's own request erroring, /// which is the correct signal. async fn wait_until_ready(binding: &str, port: i32) { let addr = format!("{binding}:{port}"); for _ in 0..200 { if tokio::net::TcpStream::connect(&addr).await.is_ok() { return; } tokio::time::sleep(tokio::time::Duration::from_millis(25)).await; } } /// Starts the server with a basic route (GET and POST) at the root (`/`), using /// the given application context. pub async fn start_from_ctx(ctx: AppContext, port: Option) -> tokio::task::JoinHandle<()> { let app_router = AppRoutes::empty() .add_route( Routes::new() .add("/", get(get_action)) .add("/", post(post_action)), ) .to_router::(ctx.clone(), axum::Router::new()) .expect("to router"); let boot = boot::BootResult { app_context: ctx, router: Some(app_router), worker: None, run_scheduler: false, }; start_from_boot(boot, port).await } /// Starts the server with a custom route specified by the URI and the HTTP /// method handler. pub async fn start_with_route( ctx: AppContext, uri: &str, method: axum::routing::MethodRouter, port: Option, ) -> tokio::task::JoinHandle<()> { let app_router = AppRoutes::empty() .add_route(Routes::new().add(uri, method)) .to_router::(ctx.clone(), axum::Router::new()) .expect("to router"); let boot = boot::BootResult { app_context: ctx, router: Some(app_router), worker: None, run_scheduler: false, }; start_from_boot(boot, port).await }