arcature 2026.1.0

Arcature application framework: a high-level Application facade over the certified Arcature subsystems, with the low-level Axum/Tower escape hatch preserved.
Documentation
//! High-level Application engine integration — real HTTP over TCP.
//!
//! Exercises the `Application` composition root end-to-end: routes assembled
//! into the low-level `App` kernel, served via `axum::serve` on an ephemeral
//! listener, real TCP requests proving 200/404 behavior, then graceful
//! shutdown proving no task is abandoned. This is the foundation-PR serving
//! path (post-routing layers, pre-routing proxy, and subsystem lifecycle are
//! added in later PRs).

use arcature::prelude::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;

const HANG_GUARD: std::time::Duration = std::time::Duration::from_secs(10);

/// Send a raw `GET path` over TCP and return the full response text.
async fn http_get(addr: std::net::SocketAddr, path: &str) -> String {
    let mut stream = tokio::time::timeout(HANG_GUARD, tokio::net::TcpStream::connect(addr))
        .await
        .expect("connect did not hang")
        .expect("connect succeeds");
    let request = format!("GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n");
    stream
        .write_all(request.as_bytes())
        .await
        .expect("write request");
    let mut buffer = Vec::new();
    tokio::time::timeout(HANG_GUARD, stream.read_to_end(&mut buffer))
        .await
        .expect("read did not hang")
        .expect("read succeeds");
    String::from_utf8_lossy(&buffer).into_owned()
}

/// A running `Application` server on an ephemeral address with graceful
/// shutdown. Mirrors the `tests/app/common/server.rs` fixture pattern.
struct RunningApp {
    addr: std::net::SocketAddr,
    shutdown: tokio::sync::oneshot::Sender<()>,
    join: tokio::task::JoinHandle<()>,
}

impl RunningApp {
    /// Bind `127.0.0.1:0`, serve `app`, return the fixture.
    async fn start(app: Application<()>) -> RunningApp {
        let listener = TcpListener::bind("127.0.0.1:0")
            .await
            .expect("bind ephemeral listener");
        let addr = listener.local_addr().expect("read bound address");
        let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
        let join = tokio::spawn(async move {
            app.serve_with_shutdown(listener, async {
                let _ = shutdown_rx.await;
            })
            .await
            .expect("application served without engine error");
        });
        RunningApp {
            addr,
            shutdown: shutdown_tx,
            join,
        }
    }

    /// The bound address.
    fn addr(&self) -> std::net::SocketAddr {
        self.addr
    }

    /// Trigger graceful shutdown and await the server task completing.
    async fn stop(self) {
        self.shutdown
            .send(())
            .expect("server task still alive to receive shutdown");
        tokio::time::timeout(HANG_GUARD, self.join)
            .await
            .expect("server did not hang on shutdown")
            .expect("server task did not panic");
    }
}

#[tokio::test]
async fn application_serves_registered_route() {
    let app = Application::new()
        .routes(Routes::new().route("/", get(|| async { "hello from application" })))
        .build();

    let server = RunningApp::start(app).await;
    let response = http_get(server.addr(), "/").await;
    assert!(
        response.starts_with("HTTP/1.1 200 OK"),
        "expected 200 OK, got: {response}"
    );
    let (_, body) = response.split_once("\r\n\r\n").unwrap();
    assert_eq!(body, "hello from application");
    server.stop().await;
}

#[tokio::test]
async fn application_returns_404_for_unknown_route() {
    let app = Application::new()
        .routes(Routes::new().route("/known", get(|| async { "ok" })))
        .build();

    let server = RunningApp::start(app).await;
    let response = http_get(server.addr(), "/unknown").await;
    assert!(
        response.starts_with("HTTP/1.1 404"),
        "expected 404 Not Found, got: {response}"
    );
    server.stop().await;
}

#[tokio::test]
async fn application_stateful_routes_serve_with_state() {
    // A stateful application: routes use State<AppState>, then `.state(value)`
    // resolves the builder back to `()` before serving (the engine analogue
    // of axum::Router::with_state).
    #[derive(Clone)]
    struct AppState {
        greeting: &'static str,
    }

    async fn hello(State(state): State<AppState>) -> String {
        state.greeting.to_owned()
    }

    let app = Application::new()
        .routes(Routes::new().route("/", get(hello)))
        .state(AppState {
            greeting: "stateful hello",
        })
        .build();

    let server = RunningApp::start(app).await;
    let response = http_get(server.addr(), "/").await;
    assert!(
        response.starts_with("HTTP/1.1 200 OK"),
        "expected 200 OK, got: {response}"
    );
    let (_, body) = response.split_once("\r\n\r\n").unwrap();
    assert_eq!(body, "stateful hello");
    server.stop().await;
}

#[tokio::test]
async fn application_run_binds_and_serves() {
    // `Application::run()` binds a TcpListener on the configured address/port
    // and serves with ctrl_c shutdown. We bind to port 0 (ephemeral) and prove
    // `run()` reaches the serve phase by racing it against a timeout: a working
    // server blocks forever (until ctrl_c), so a timeout means it successfully
    // started serving.
    let app = Application::new()
        .routes(Routes::new().route("/", get(|| async { "running" })))
        .bind("127.0.0.1")
        .port(0) // ephemeral port — `run()` will bind it
        .build();

    let run_future = tokio::time::timeout(std::time::Duration::from_millis(500), app.run()).await;

    // It should have timed out (server is running, waiting for ctrl_c), not
    // returned an error. An immediate Ok or Err would indicate a bind failure
    // or an early return.
    assert!(
        run_future.is_err(),
        "Application::run() should block while serving, not return immediately"
    );
}

#[tokio::test]
async fn application_proxy_is_accepted_and_retained() {
    // The proxy is plumbed (stored) in PR A and executed in PR B. This test
    // proves the builder accepts a proxy function and the application still
    // serves normally (the installed proxy does not break the serving path
    // in the foundation PR).
    let app = Application::new()
        .routes(Routes::new().route("/", get(|| async { "with proxy" })))
        .proxy(|_req| ProxyAction::continue_default())
        .build();

    let server = RunningApp::start(app).await;
    let response = http_get(server.addr(), "/").await;
    assert!(
        response.starts_with("HTTP/1.1 200 OK"),
        "expected 200 OK with proxy installed, got: {response}"
    );
    server.stop().await;
}

#[tokio::test]
async fn application_bind_address_and_port_recorded() {
    let app: Application = Application::new()
        .bind("0.0.0.0")
        .port(8080)
        .routes(Routes::new().route("/", get(|| async { "ok" })))
        .build();
    assert_eq!(app.bind_address(), "0.0.0.0");
    assert_eq!(app.port(), 8080);
}

#[tokio::test]
async fn engine_error_display_preserves_source() {
    // The typed EngineError surfaces the bind address and the I/O source.
    let err = arcature::EngineError::BindListener {
        address: "127.0.0.1:3000".to_owned(),
        source: std::io::Error::new(std::io::ErrorKind::AddrInUse, "address in use"),
    };
    let msg = err.to_string();
    assert!(
        msg.contains("127.0.0.1:3000"),
        "error names the address: {msg}"
    );
    assert!(
        msg.contains("address in use"),
        "error preserves the source message: {msg}"
    );
}