#![cfg(feature = "test-utils")]
use aro_web::App;
use aro_web::test::TestClient;
use axum::http::StatusCode;
use axum::routing::{get, post};
use axum::{Json, Router};
use serde::{Deserialize, Serialize};
fn app() -> Router {
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Item {
name: String,
}
Router::new()
.route("/hello", get(|| async { "world" }))
.route("/status", get(|| async { StatusCode::NO_CONTENT }))
.route(
"/echo",
post(|Json(item): Json<Item>| async move { Json(item) }),
)
}
#[tokio::test]
async fn get_returns_text_body() {
let client = TestClient::new(App::new().routes(app()).build());
let resp = client.get("/hello").send().await;
resp.assert_status(200);
assert_eq!(resp.text(), "world");
}
#[tokio::test]
async fn post_json_roundtrip() {
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Item {
name: String,
}
let client = TestClient::new(App::new().routes(app()).build());
let resp = client
.post("/echo")
.json(&Item {
name: "widget".into(),
})
.send()
.await;
resp.assert_status(200);
let item: Item = resp.json();
assert_eq!(
item,
Item {
name: "widget".into()
}
);
}
#[tokio::test]
async fn assert_json_returns_deserialized_value() {
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Item {
name: String,
}
let client = TestClient::new(App::new().routes(app()).build());
let resp = client
.post("/echo")
.json(&Item {
name: "gadget".into(),
})
.send()
.await;
let item: Item = resp.assert_json();
assert_eq!(
item,
Item {
name: "gadget".into()
}
);
}
#[tokio::test]
async fn custom_header_is_sent() {
let router = Router::new().route(
"/check-header",
get(|headers: axum::http::HeaderMap| async move {
headers
.get("x-custom")
.map_or_else(|| "missing".into(), |v| v.to_str().unwrap().to_string())
}),
);
let client = TestClient::new(App::new().routes(router).build());
let resp = client
.get("/check-header")
.header("x-custom", "hello")
.send()
.await;
resp.assert_status(200);
assert_eq!(resp.text(), "hello");
}
#[tokio::test]
async fn status_code_non_200() {
let client = TestClient::new(App::new().routes(app()).build());
let resp = client.get("/status").send().await;
resp.assert_status(204);
assert_eq!(resp.status(), StatusCode::NO_CONTENT);
}
#[tokio::test]
async fn not_found_fallback() {
let client = TestClient::new(App::new().routes(app()).build());
let resp = client.get("/nonexistent").send().await;
resp.assert_status(404);
let body: serde_json::Value = resp.json();
assert_eq!(body["error"], "not found");
assert_eq!(body["status"], 404);
}
#[tokio::test]
async fn put_delete_patch_methods() {
let router = Router::new().route(
"/resource",
axum::routing::put(|| async { "updated" })
.delete(|| async { "deleted" })
.patch(|| async { "patched" }),
);
let client = TestClient::new(App::new().routes(router).build());
let resp = client.put("/resource").send().await;
resp.assert_status(200);
assert_eq!(resp.text(), "updated");
let resp = client.delete("/resource").send().await;
resp.assert_status(200);
assert_eq!(resp.text(), "deleted");
let resp = client.patch("/resource").send().await;
resp.assert_status(200);
assert_eq!(resp.text(), "patched");
}
#[tokio::test]
async fn raw_body() {
let router = Router::new().route("/raw", post(|body: axum::body::Bytes| async move { body }));
let client = TestClient::new(App::new().routes(router).build());
let resp = client
.post("/raw")
.body(b"raw bytes".as_slice())
.send()
.await;
resp.assert_status(200);
assert_eq!(resp.text(), "raw bytes");
}
#[tokio::test]
async fn response_headers_accessible() {
let router = Router::new().route(
"/with-header",
get(|| async { ([(axum::http::header::CONTENT_TYPE, "text/plain")], "hello") }),
);
let client = TestClient::new(App::new().routes(router).build());
let resp = client.get("/with-header").send().await;
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
"text/plain"
);
}
#[tokio::test]
async fn from_app_runs_startup_hooks() {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let counter = Arc::new(AtomicU32::new(0));
let counter_clone = counter.clone();
let client = TestClient::from_app(
App::new()
.on_startup(move |_state| {
Box::pin(async move {
counter_clone.fetch_add(1, Ordering::SeqCst);
Ok(())
})
})
.routes(Router::new().route("/ping", get(|| async { "pong" }))),
)
.await
.unwrap();
assert_eq!(counter.load(Ordering::SeqCst), 1);
let resp = client.get("/ping").send().await;
resp.assert_status(200);
assert_eq!(resp.text(), "pong");
}
#[tokio::test]
async fn from_app_propagates_hook_error() {
let result = TestClient::from_app(
App::new().on_startup(|_state| Box::pin(async { Err("hook failed".into()) })),
)
.await;
let err = result.err().expect("expected an error");
assert_eq!(err.to_string(), "hook failed");
}