use std::time::Duration;
use axum::Router;
use axum::routing::get;
use crate::server::EmbeddedServer;
fn hello() -> Router {
Router::new().route("/", get(|| async { "hello" }))
}
async fn fetch(addr: std::net::SocketAddr, path: &str) -> std::io::Result<String> {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut stream = tokio::net::TcpStream::connect(addr).await?;
let request = format!("GET {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n");
stream.write_all(request.as_bytes()).await?;
let mut response = String::new();
stream.read_to_string(&mut response).await?;
Ok(response)
}
#[tokio::test]
async fn the_listener_is_loopback_only() {
let server = EmbeddedServer::bind().await.expect("a loopback listener");
let addr = server.addr();
assert!(addr.ip().is_loopback(), "bound {addr}");
assert!(!addr.ip().is_unspecified(), "bound {addr}");
assert_eq!(addr.ip().to_string(), "127.0.0.1");
}
#[tokio::test]
async fn the_port_is_assigned_by_the_operating_system() {
let one = EmbeddedServer::bind().await.expect("a listener");
let two = EmbeddedServer::bind().await.expect("a second listener");
assert_ne!(one.port(), 0, "port 0 is a request, not an assignment");
assert_ne!(one.port(), two.port());
}
#[tokio::test]
async fn the_base_url_names_the_port_that_actually_bound() {
let server = EmbeddedServer::bind().await.expect("a listener");
assert_eq!(
server.base_url(),
format!("http://127.0.0.1:{}", server.addr().port())
);
assert!(!server.base_url().contains("localhost"));
}
#[tokio::test]
async fn the_port_accepts_before_anything_is_told_to_go_there() {
let server = EmbeddedServer::bind().await.expect("a listener");
let addr = server.addr();
assert!(
tokio::net::TcpStream::connect(addr).await.is_ok(),
"the bound port refused a connection before `serve` was called"
);
let running = server.serve(hello());
running.wait_until_ready().await.expect("a ready server");
let response = fetch(addr, "/").await.expect("a response");
assert!(response.starts_with("HTTP/1.1 200"), "{response}");
assert!(response.ends_with("hello"), "{response}");
running
.shutdown(Duration::from_secs(2))
.await
.expect("a clean stop");
}
#[tokio::test]
async fn a_shutdown_stops_the_server_accepting() {
let server = EmbeddedServer::bind().await.expect("a listener");
let addr = server.addr();
let running = server.serve(hello());
running.wait_until_ready().await.expect("a ready server");
fetch(addr, "/").await.expect("a response while running");
running
.shutdown(Duration::from_secs(5))
.await
.expect("a clean stop");
let after = fetch(addr, "/").await;
let stopped = match after {
Err(_) => true,
Ok(body) => !body.starts_with("HTTP/1.1 200"),
};
assert!(stopped, "the server was still answering after shutdown");
}
#[tokio::test]
async fn dropping_the_handle_stops_the_server_too() {
let server = EmbeddedServer::bind().await.expect("a listener");
let addr = server.addr();
{
let running = server.serve(hello());
running.wait_until_ready().await.expect("a ready server");
fetch(addr, "/").await.expect("a response while running");
}
tokio::time::sleep(Duration::from_millis(100)).await;
let after = fetch(addr, "/").await;
let stopped = match after {
Err(_) => true,
Ok(body) => !body.starts_with("HTTP/1.1 200"),
};
assert!(stopped, "the server outlived its handle");
}
#[tokio::test]
async fn a_bounded_shutdown_gives_up_rather_than_hanging() {
let router = Router::new().route(
"/forever",
get(|| async {
tokio::time::sleep(Duration::from_secs(120)).await;
"never"
}),
);
let server = EmbeddedServer::bind().await.expect("a listener");
let addr = server.addr();
let running = server.serve(router);
running.wait_until_ready().await.expect("a ready server");
let held = tokio::spawn(async move { fetch(addr, "/forever").await });
tokio::time::sleep(Duration::from_millis(150)).await;
let started = std::time::Instant::now();
let result = running.shutdown(Duration::from_millis(300)).await;
let waited = started.elapsed();
assert!(result.is_err(), "an abandoned connection is reported");
assert!(
waited < Duration::from_secs(5),
"shutdown waited {waited:?} for a connection that was never going to end"
);
held.abort();
}