use aro_web::App;
use axum::Router;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use axum::routing::get;
use tower::ServiceExt;
#[expect(
clippy::unwrap_used,
reason = "test helper — constructing requests from literals cannot fail"
)]
async fn response(router: Router, method: &str, uri: &str) -> (StatusCode, Vec<u8>) {
let req = Request::builder()
.method(method)
.uri(uri)
.body(Body::empty())
.unwrap();
let resp = router.oneshot(req).await.unwrap();
let status = resp.status();
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap()
.to_vec();
(status, body)
}
#[cfg(feature = "test-utils")]
mod with_test_client {
use aro_web::App;
use aro_web::test::TestClient;
use axum::Router;
use axum::routing::get;
#[tokio::test]
async fn build_produces_working_service() {
let router = Router::new().route("/hello", get(|| async { "world" }));
let client = TestClient::new(App::new().routes(router).build());
let resp = client.get("/hello").send().await;
resp.assert_status(200);
assert_eq!(resp.text(), "world");
}
#[tokio::test]
async fn default_404_returns_json() {
let client = TestClient::new(App::new().build());
let resp = client.get("/nonexistent").send().await;
resp.assert_status(404);
let json: serde_json::Value = resp.json();
assert_eq!(json["error"], "not found");
assert_eq!(json["status"], 404);
}
#[tokio::test]
async fn multiple_routes_calls_merge() {
let router_a = Router::new().route("/a", get(|| async { "a" }));
let router_b = Router::new().route("/b", get(|| async { "b" }));
let client = TestClient::new(App::new().routes(router_a).routes(router_b).build());
let resp = client.get("/a").send().await;
resp.assert_status(200);
assert_eq!(resp.text(), "a");
let resp = client.get("/b").send().await;
resp.assert_status(200);
assert_eq!(resp.text(), "b");
}
}
#[tokio::test]
async fn without_defaults_disables_trace_layer() {
let router = Router::new().route("/ok", get(|| async { "ok" }));
let app = App::new().routes(router).without_defaults().build();
let (status, body) = response(app, "GET", "/ok").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body, b"ok");
}
#[tokio::test]
async fn without_defaults_still_has_fallback() {
let app = App::new().without_defaults().build();
let (status, body) = response(app, "GET", "/nonexistent").await;
assert_eq!(status, StatusCode::NOT_FOUND);
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["error"], "not found");
assert_eq!(json["status"], 404);
}
#[tokio::test]
async fn empty_app_builds_without_routes() {
let app = App::new().build();
let (status, _) = response(app, "GET", "/anything").await;
assert_eq!(status, StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn state_with_dep_extractor() {
use aro_web::dep::Dep;
use aro_web::state::AroState;
use std::sync::Arc;
trait Greeter: Send + Sync {
fn greet(&self) -> &'static str;
}
struct HelloGreeter;
impl Greeter for HelloGreeter {
fn greet(&self) -> &'static str {
"hi from dep"
}
}
let state = AroState::builder()
.register::<dyn Greeter>(Arc::new(HelloGreeter))
.build();
let stateful_routes: Router<AroState> = Router::new().route(
"/greet",
get(|dep: Dep<dyn Greeter>| async move { dep.greet().to_string() }),
);
let app = App::new()
.routes_with_state(stateful_routes)
.state(state)
.build();
let (status, body) = response(app, "GET", "/greet").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body, b"hi from dep");
}
#[tokio::test]
async fn register_provides_deps_without_external_state() {
use aro_web::dep::Dep;
use aro_web::state::AroState;
use std::sync::Arc;
trait Greeter: Send + Sync {
fn greet(&self) -> &'static str;
}
struct HelloGreeter;
impl Greeter for HelloGreeter {
fn greet(&self) -> &'static str {
"hi from register"
}
}
let stateful_routes: Router<AroState> = Router::new().route(
"/greet",
get(|dep: Dep<dyn Greeter>| async move { dep.greet().to_string() }),
);
let app = App::new()
.register::<dyn Greeter>(Arc::new(HelloGreeter))
.routes_with_state(stateful_routes)
.build();
let (status, body) = response(app, "GET", "/greet").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body, b"hi from register");
}
#[tokio::test]
async fn register_merges_with_external_state() {
use aro_web::dep::Dep;
use aro_web::state::AroState;
use std::sync::Arc;
trait Greeter: Send + Sync {
fn greet(&self) -> &'static str;
}
struct HelloGreeter;
impl Greeter for HelloGreeter {
fn greet(&self) -> &'static str {
"from external"
}
}
trait Counter: Send + Sync {
fn count(&self) -> u32;
}
struct FixedCounter;
impl Counter for FixedCounter {
fn count(&self) -> u32 {
99
}
}
let external = AroState::builder()
.register::<dyn Counter>(Arc::new(FixedCounter))
.build();
let stateful_routes: Router<AroState> = Router::new()
.route(
"/greet",
get(|dep: Dep<dyn Greeter>| async move { dep.greet().to_string() }),
)
.route(
"/count",
get(|dep: Dep<dyn Counter>| async move { dep.count().to_string() }),
);
let app = App::new()
.state(external)
.register::<dyn Greeter>(Arc::new(HelloGreeter))
.routes_with_state(stateful_routes)
.build();
let (status, body) = response(app.clone(), "GET", "/greet").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body, b"from external");
let (status, body) = response(app, "GET", "/count").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body, b"99");
}
#[tokio::test]
async fn register_multiple_deps() {
use aro_web::dep::Dep;
use aro_web::state::AroState;
use std::sync::Arc;
trait Greeter: Send + Sync {
fn greet(&self) -> &'static str;
}
struct HelloGreeter;
impl Greeter for HelloGreeter {
fn greet(&self) -> &'static str {
"hello"
}
}
trait Counter: Send + Sync {
fn count(&self) -> u32;
}
struct FixedCounter;
impl Counter for FixedCounter {
fn count(&self) -> u32 {
7
}
}
let stateful_routes: Router<AroState> = Router::new()
.route(
"/greet",
get(|dep: Dep<dyn Greeter>| async move { dep.greet().to_string() }),
)
.route(
"/count",
get(|dep: Dep<dyn Counter>| async move { dep.count().to_string() }),
);
let app = App::new()
.register::<dyn Greeter>(Arc::new(HelloGreeter))
.register::<dyn Counter>(Arc::new(FixedCounter))
.routes_with_state(stateful_routes)
.build();
let (status, body) = response(app.clone(), "GET", "/greet").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body, b"hello");
let (status, body) = response(app, "GET", "/count").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body, b"7");
}
#[tokio::test]
async fn mixed_stateful_and_stateless_routes() {
use aro_web::dep::Dep;
use aro_web::state::AroState;
use std::sync::Arc;
trait Counter: Send + Sync {
fn count(&self) -> u32;
}
struct FixedCounter;
impl Counter for FixedCounter {
fn count(&self) -> u32 {
42
}
}
let state = AroState::builder()
.register::<dyn Counter>(Arc::new(FixedCounter))
.build();
let plain_routes = Router::new().route("/ping", get(|| async { "pong" }));
let stateful_routes: Router<AroState> = Router::new().route(
"/count",
get(|dep: Dep<dyn Counter>| async move { dep.count().to_string() }),
);
let app = App::new()
.routes(plain_routes)
.routes_with_state(stateful_routes)
.state(state)
.build();
let (status, body) = response(app.clone(), "GET", "/ping").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body, b"pong");
let (status, body) = response(app, "GET", "/count").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body, b"42");
}
#[tokio::test]
async fn layer_applies_to_stateful_routes() {
use aro_web::dep::Dep;
use aro_web::middleware::cache_control;
use aro_web::state::AroState;
use std::sync::Arc;
trait Marker: Send + Sync {
fn value(&self) -> &'static str;
}
struct TestMarker;
impl Marker for TestMarker {
fn value(&self) -> &'static str {
"marked"
}
}
let stateful_routes: Router<AroState> = Router::new().route(
"/marked",
get(|dep: Dep<dyn Marker>| async move { dep.value().to_string() }),
);
let app = App::new()
.register::<dyn Marker>(Arc::new(TestMarker))
.routes_with_state(stateful_routes)
.layer(cache_control("max-age=60"))
.build();
let req = Request::builder()
.method("GET")
.uri("/marked")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(resp.headers().get("cache-control").unwrap(), "max-age=60");
}
#[tokio::test]
async fn method_not_allowed_returns_json_405() {
let router = Router::new().route("/only-get", get(|| async { "ok" }));
let app = App::new().routes(router).build();
let (status, body) = response(app, "POST", "/only-get").await;
assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED);
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["error"], "method not allowed");
assert_eq!(json["status"], 405);
}