use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use axol::http::request::RequestPartsRef;
use axol::http::response::Response;
use axol::http::{Method, StatusCode};
use axol::{ConnectInfo, Error, MatchedPath, Path, Query, Result, Router, Typed};
use serde::Deserialize;
mod common;
use common::*;
async fn hello() -> &'static str {
"hello"
}
#[tokio::test]
async fn serves_a_basic_get() {
let server = spawn_router(Router::new().get("/", hello)).await;
let response = reqwest::get(server.url("/")).await.unwrap();
assert_eq!(response.status().as_u16(), 200);
assert_eq!(response.text().await.unwrap(), "hello");
}
#[tokio::test]
async fn sets_content_length_and_returns_no_body_for_head() {
let server = spawn_router(Router::new().get("/", hello)).await;
let response = reqwest::Client::new()
.head(server.url("/"))
.send()
.await
.unwrap();
assert_eq!(response.status().as_u16(), 200);
assert!(response.bytes().await.unwrap().is_empty());
}
#[tokio::test]
async fn unknown_paths_return_404() {
let server = spawn_router(Router::new().get("/", hello)).await;
let response = reqwest::get(server.url("/nope")).await.unwrap();
assert_eq!(response.status().as_u16(), 404);
}
#[tokio::test]
async fn non_standard_methods_are_rejected_with_405() {
let server = spawn_router(Router::new().get("/", hello)).await;
let response = reqwest::Client::new()
.request(
reqwest::Method::from_bytes(b"PROPFIND").unwrap(),
server.url("/"),
)
.send()
.await
.unwrap();
assert_eq!(response.status().as_u16(), 405);
}
#[tokio::test]
async fn a_panicking_handler_becomes_a_500() {
async fn boom() -> &'static str {
panic!("handler exploded");
}
let server = spawn_router(Router::new().get("/boom", boom).get("/ok", hello)).await;
let response = reqwest::get(server.url("/boom")).await.unwrap();
assert_eq!(response.status().as_u16(), 500);
let response = reqwest::get(server.url("/ok")).await.unwrap();
assert_eq!(response.status().as_u16(), 200);
}
#[tokio::test]
async fn errors_map_to_their_status_codes() {
async fn not_found() -> Result<&'static str> {
Err(Error::NotFound)
}
async fn unauthorized() -> Result<&'static str> {
Err(Error::Unauthorized)
}
async fn conflict() -> Result<&'static str> {
Err(Error::Conflict)
}
async fn internal() -> Result<&'static str> {
Err(Error::Internal(anyhow::anyhow!("secret detail")))
}
let server = spawn_router(
Router::new()
.get("/not-found", not_found)
.get("/unauthorized", unauthorized)
.get("/conflict", conflict)
.get("/internal", internal),
)
.await;
for (path, expected) in [
("/not-found", 404),
("/unauthorized", 401),
("/conflict", 409),
("/internal", 500),
] {
let response = reqwest::get(server.url(path)).await.unwrap();
assert_eq!(response.status().as_u16(), expected, "path {path}");
}
}
#[tokio::test]
async fn internal_errors_do_not_leak_their_detail_to_the_client() {
async fn internal() -> Result<&'static str> {
Err(Error::Internal(anyhow::anyhow!(
"database password is /**/"
)))
}
let server = spawn_router(Router::new().get("/", internal)).await;
let response = reqwest::get(server.url("/")).await.unwrap();
assert_eq!(response.status().as_u16(), 500);
let body = response.text().await.unwrap();
assert!(
!body.contains("password"),
"internal detail leaked to client: {body}"
);
}
#[tokio::test]
async fn path_variables_are_percent_decoded_once() {
async fn echo(Path(value): Path<String>) -> String {
value
}
let server = spawn_router(Router::new().get("/echo/:value", echo)).await;
let response = reqwest::get(server.url("/echo/hello%20world"))
.await
.unwrap();
assert_eq!(response.text().await.unwrap(), "hello world");
let response = reqwest::get(server.url("/echo/a%2520b")).await.unwrap();
assert_eq!(response.text().await.unwrap(), "a%20b");
}
#[tokio::test]
async fn query_strings_deserialize() {
#[derive(Deserialize)]
struct Params {
name: String,
count: u32,
}
async fn handler(Query(params): Query<Params>) -> String {
format!("{} {}", params.name, params.count)
}
let server = spawn_router(Router::new().get("/", handler)).await;
let response = reqwest::get(server.url("/?name=ada&count=3"))
.await
.unwrap();
assert_eq!(response.text().await.unwrap(), "ada 3");
}
#[tokio::test]
async fn a_malformed_query_is_a_client_error() {
#[derive(Deserialize)]
struct Params {
count: u32,
}
async fn handler(Query(params): Query<Params>) -> String {
params.count.to_string()
}
let server = spawn_router(Router::new().get("/", handler)).await;
let response = reqwest::get(server.url("/?count=not-a-number"))
.await
.unwrap();
assert!(
response.status().is_client_error(),
"expected 4xx, got {}",
response.status()
);
}
#[tokio::test]
async fn request_bodies_are_readable() {
async fn echo(body: String) -> String {
format!("got {body}")
}
let server = spawn_router(Router::new().post("/", echo)).await;
let response = reqwest::Client::new()
.post(server.url("/"))
.body("payload")
.send()
.await
.unwrap();
assert_eq!(response.text().await.unwrap(), "got payload");
}
#[tokio::test]
async fn large_request_bodies_stream_through_intact() {
async fn size(body: Vec<u8>) -> String {
body.len().to_string()
}
let server = spawn_router(Router::new().post("/", size)).await;
let payload = vec![b'x'; 1024 * 512];
let response = reqwest::Client::new()
.post(server.url("/"))
.body(payload.clone())
.send()
.await
.unwrap();
assert_eq!(response.text().await.unwrap(), payload.len().to_string());
}
#[tokio::test]
async fn json_round_trips_in_both_directions() {
use axol::Json;
#[derive(Deserialize, serde::Serialize)]
struct Payload {
name: String,
count: u32,
}
async fn handler(Json(mut payload): Json<Payload>) -> Json<Payload> {
payload.count += 1;
Json(payload)
}
let server = spawn_router(Router::new().post("/", handler)).await;
let response = reqwest::Client::new()
.post(server.url("/"))
.json(&serde_json::json!({"name": "ada", "count": 1}))
.send()
.await
.unwrap();
let body: serde_json::Value = response.json().await.unwrap();
assert_eq!(body["name"], "ada");
assert_eq!(body["count"], 2);
}
#[tokio::test]
async fn connect_info_reports_the_peer_address() {
async fn handler(ConnectInfo(addr): ConnectInfo) -> String {
addr.ip().to_string()
}
let server = spawn_router(Router::new().get("/", handler)).await;
let response = reqwest::get(server.url("/")).await.unwrap();
assert_eq!(response.text().await.unwrap(), "127.0.0.1");
}
#[tokio::test]
async fn matched_path_is_the_route_pattern() {
async fn handler(MatchedPath(path): MatchedPath) -> String {
path.to_string()
}
let server = spawn_router(Router::new().get("/user/:id", handler)).await;
let response = reqwest::get(server.url("/user/42")).await.unwrap();
assert_eq!(response.text().await.unwrap(), "/user/:id");
}
#[tokio::test]
async fn response_headers_reach_the_client() {
async fn handler() -> ([(&'static str, &'static str); 2], &'static str) {
([("x-custom", "value"), ("x-other", "second")], "body")
}
let server = spawn_router(Router::new().get("/", handler)).await;
let response = reqwest::get(server.url("/")).await.unwrap();
assert_eq!(response.headers().get("x-custom").unwrap(), "value");
assert_eq!(response.headers().get("x-other").unwrap(), "second");
}
#[tokio::test]
async fn middleware_runs_in_registration_order_and_can_short_circuit() {
let order = Arc::new(std::sync::Mutex::new(Vec::<&'static str>::new()));
let request_order = order.clone();
let response_order = order.clone();
async fn handler() -> &'static str {
"handler"
}
let server = spawn_router(
Router::new()
.request_hook_direct(
"/",
RecordingRequestHook {
order: request_order,
},
)
.late_response_hook_direct(
"/",
RecordingResponseHook {
order: response_order,
},
)
.get("/", handler),
)
.await;
let response = reqwest::get(server.url("/")).await.unwrap();
assert_eq!(response.text().await.unwrap(), "handler");
let recorded = order.lock().unwrap().clone();
assert_eq!(recorded, vec!["request", "late_response"]);
}
struct RecordingRequestHook {
order: Arc<std::sync::Mutex<Vec<&'static str>>>,
}
#[async_trait::async_trait]
impl axol::RequestHook for RecordingRequestHook {
async fn handle_request(&self, _request: &mut axol::http::Request) -> Result<Option<Response>> {
self.order.lock().unwrap().push("request");
Ok(None)
}
}
struct RecordingResponseHook {
order: Arc<std::sync::Mutex<Vec<&'static str>>>,
}
#[async_trait::async_trait]
impl axol::LateResponseHook for RecordingResponseHook {
async fn handle_response<'a>(&self, _parts: RequestPartsRef<'a>, _response: &mut Response) {
self.order.lock().unwrap().push("late_response");
}
}
#[tokio::test]
async fn a_request_hook_can_short_circuit_the_handler() {
struct Blocker;
#[async_trait::async_trait]
impl axol::RequestHook for Blocker {
async fn handle_request(
&self,
_request: &mut axol::http::Request,
) -> Result<Option<Response>> {
Ok(Some(Response {
status: StatusCode::Forbidden,
..Default::default()
}))
}
}
let hits = Arc::new(AtomicUsize::new(0));
let handler_hits = hits.clone();
let router = Router::new()
.request_hook_direct("/", Blocker)
.get("/", move || {
let hits = handler_hits.clone();
async move {
hits.fetch_add(1, Ordering::SeqCst);
"should not run"
}
});
let server = spawn_router(router).await;
let response = reqwest::get(server.url("/")).await.unwrap();
assert_eq!(response.status().as_u16(), 403);
assert_eq!(
hits.load(Ordering::SeqCst),
0,
"handler should not have run"
);
}
#[tokio::test]
async fn late_response_hooks_run_on_error_responses_too() {
struct Tagger;
#[async_trait::async_trait]
impl axol::LateResponseHook for Tagger {
async fn handle_response<'a>(&self, _parts: RequestPartsRef<'a>, response: &mut Response) {
response.headers.insert("x-tagged", "yes");
}
}
async fn fails() -> Result<&'static str> {
Err(Error::NotFound)
}
let server = spawn_router(
Router::new()
.late_response_hook_direct("/", Tagger)
.get("/", fails),
)
.await;
let response = reqwest::get(server.url("/")).await.unwrap();
assert_eq!(response.status().as_u16(), 404);
assert_eq!(response.headers().get("x-tagged").unwrap(), "yes");
}
#[tokio::test]
async fn typed_headers_extract_and_respond() {
use axol::http::typed_headers::ContentType;
async fn handler(Typed(content_type): Typed<ContentType>) -> String {
content_type.to_string()
}
let server = spawn_router(Router::new().post("/", handler)).await;
let response = reqwest::Client::new()
.post(server.url("/"))
.header("content-type", "application/json")
.body("{}")
.send()
.await
.unwrap();
assert_eq!(response.text().await.unwrap(), "application/json");
}
#[tokio::test]
async fn concurrent_requests_are_served() {
let server = spawn_router(Router::new().get("/", hello)).await;
let url = server.url("/");
let mut handles = Vec::new();
for _ in 0..32 {
let url = url.clone();
handles.push(tokio::spawn(async move {
reqwest::get(url).await.unwrap().text().await.unwrap()
}));
}
for handle in handles {
assert_eq!(handle.await.unwrap(), "hello");
}
}
#[tokio::test]
async fn keep_alive_serves_several_requests_on_one_connection() {
let server = spawn_router(Router::new().get("/", hello)).await;
let client = reqwest::Client::builder()
.pool_max_idle_per_host(1)
.build()
.unwrap();
for _ in 0..5 {
let response = client.get(server.url("/")).send().await.unwrap();
assert_eq!(response.text().await.unwrap(), "hello");
}
}
#[tokio::test]
async fn methods_are_routed_independently() {
async fn get_handler() -> &'static str {
"get"
}
async fn post_handler() -> &'static str {
"post"
}
async fn delete_handler() -> &'static str {
"delete"
}
let server = spawn_router(
Router::new()
.get("/r", get_handler)
.post("/r", post_handler)
.delete("/r", delete_handler),
)
.await;
let client = reqwest::Client::new();
for (method, expected) in [
(Method::Get, "get"),
(Method::Post, "post"),
(Method::Delete, "delete"),
] {
let response = client
.request(
reqwest::Method::from_bytes(method.as_str().as_bytes()).unwrap(),
server.url("/r"),
)
.send()
.await
.unwrap();
assert_eq!(response.text().await.unwrap(), expected);
}
}