use arcature::prelude::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
const HANG_GUARD: std::time::Duration = std::time::Duration::from_secs(10);
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()
}
struct RunningApp {
addr: std::net::SocketAddr,
shutdown: tokio::sync::oneshot::Sender<()>,
join: tokio::task::JoinHandle<()>,
}
impl RunningApp {
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,
}
}
fn addr(&self) -> std::net::SocketAddr {
self.addr
}
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() {
#[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() {
let app = Application::new()
.routes(Routes::new().route("/", get(|| async { "running" })))
.bind("127.0.0.1")
.port(0) .build();
let run_future = tokio::time::timeout(std::time::Duration::from_millis(500), app.run()).await;
assert!(
run_future.is_err(),
"Application::run() should block while serving, not return immediately"
);
}
#[tokio::test]
async fn application_proxy_is_accepted_and_retained() {
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() {
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}"
);
}