use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Arc;
use crate::app::App;
use crate::application::Application;
use crate::core::New;
use crate::http::VERSION;
use crate::middleware::{Middleware, WithMiddleware};
use crate::request::{METHOD, Request};
use crate::response::Response;
use crate::server::{Address, ConnectionInfo};
fn conn() -> ConnectionInfo {
ConnectionInfo {
client: Address { ip: "127.0.0.1".to_string(), port: 0 },
server: Address { ip: "127.0.0.1".to_string(), port: 7878 },
request_size: 16000,
sni_hostname: None,
tls: false,
}
}
fn get(uri: &str) -> Request {
Request {
method: METHOD.get.to_string(),
request_uri: uri.to_string(),
http_version: VERSION.http_1_1.to_string(),
headers: vec![],
body: Default::default(),
}
}
struct CountingMiddleware {
count: Arc<AtomicU32>,
}
impl Middleware for CountingMiddleware {
fn handle(&self, request: &Request, connection: &ConnectionInfo, next: &dyn Application) -> Result<Response, String> {
self.count.fetch_add(1, Ordering::Relaxed);
next.execute(request, connection)
}
}
struct ShortCircuitMiddleware {
status: i16,
}
impl Middleware for ShortCircuitMiddleware {
fn handle(&self, _request: &Request, _connection: &ConnectionInfo, _next: &dyn Application) -> Result<Response, String> {
let mut r = Response::new();
r.status_code = self.status;
r.reason_phrase = "Short-Circuit".to_string();
Ok(r)
}
}
struct AddHeaderMiddleware {
name: &'static str,
value: &'static str,
}
impl Middleware for AddHeaderMiddleware {
fn handle(&self, request: &Request, connection: &ConnectionInfo, next: &dyn Application) -> Result<Response, String> {
let mut response = next.execute(request, connection)?;
response.headers.push(crate::header::Header { name: self.name.to_string().into(), value: self.value.to_string().into() });
Ok(response)
}
}
struct OrderRecordingMiddleware {
id: &'static str,
log: Arc<std::sync::Mutex<Vec<String>>>,
}
impl Middleware for OrderRecordingMiddleware {
fn handle(&self, request: &Request, connection: &ConnectionInfo, next: &dyn Application) -> Result<Response, String> {
self.log.lock().unwrap().push(format!("{}-before", self.id));
let resp = next.execute(request, connection)?;
self.log.lock().unwrap().push(format!("{}-after", self.id));
Ok(resp)
}
}
#[test]
fn middleware_runs_before_inner_app() {
let ran = Arc::new(AtomicBool::new(false));
let ran_clone = Arc::clone(&ran);
struct FlagMiddleware(Arc<AtomicBool>);
impl Middleware for FlagMiddleware {
fn handle(&self, request: &Request, connection: &ConnectionInfo, next: &dyn Application) -> Result<Response, String> {
self.0.store(true, Ordering::Relaxed);
next.execute(request, connection)
}
}
let app = WithMiddleware::new(App::new()).wrap(FlagMiddleware(ran_clone));
app.execute(&get("/healthz"), &conn()).unwrap();
assert!(ran.load(Ordering::Relaxed));
}
#[test]
fn middleware_can_short_circuit() {
let app = WithMiddleware::new(App::new()).wrap(ShortCircuitMiddleware { status: 418i16 });
let resp = app.execute(&get("/healthz"), &conn()).unwrap();
assert_eq!(418, resp.status_code);
}
#[test]
fn multiple_middlewares_run_in_order() {
let log: Arc<std::sync::Mutex<Vec<String>>> = Arc::new(std::sync::Mutex::new(vec![]));
let app = WithMiddleware::new(App::new())
.wrap(OrderRecordingMiddleware { id: "A", log: Arc::clone(&log) })
.wrap(OrderRecordingMiddleware { id: "B", log: Arc::clone(&log) });
app.execute(&get("/healthz"), &conn()).unwrap();
let order = log.lock().unwrap().clone();
assert_eq!(order, vec!["A-before", "B-before", "B-after", "A-after"]);
}
#[test]
fn middleware_can_add_response_header() {
let app = WithMiddleware::new(App::new())
.wrap(AddHeaderMiddleware { name: "X-Custom", value: "injected" });
let resp = app.execute(&get("/healthz"), &conn()).unwrap();
let found = resp.headers.iter().any(|h| h.name == "X-Custom" && h.value == "injected");
assert!(found, "X-Custom header not found in response headers");
}
#[test]
fn counting_middleware_counts_requests() {
let count = Arc::new(AtomicU32::new(0));
let app = WithMiddleware::new(App::new()).wrap(CountingMiddleware { count: Arc::clone(&count) });
for _ in 0..5 {
app.execute(&get("/healthz"), &conn()).unwrap();
}
assert_eq!(5, count.load(Ordering::Relaxed));
}
#[test]
fn inner_app_still_serves_requests_normally() {
let app = WithMiddleware::new(App::new()).wrap(CountingMiddleware { count: Arc::new(AtomicU32::new(0)) });
let resp = app.execute(&get("/healthz"), &conn()).unwrap();
assert_eq!(200, resp.status_code);
}
#[test]
fn second_middleware_short_circuits_does_not_reach_inner_app() {
let count = Arc::new(AtomicU32::new(0));
let app = WithMiddleware::new(App::new())
.wrap(CountingMiddleware { count: Arc::clone(&count) })
.wrap(ShortCircuitMiddleware { status: 503i16 });
let resp = app.execute(&get("/healthz"), &conn()).unwrap();
assert_eq!(503, resp.status_code);
assert_eq!(1, count.load(Ordering::Relaxed));
}
#[test]
fn no_middleware_passes_through_to_inner_app() {
let app = WithMiddleware::new(App::new());
let resp = app.execute(&get("/healthz"), &conn()).unwrap();
assert_eq!(200, resp.status_code);
}
#[test]
fn middleware_sees_request_method_and_uri() {
struct AssertMiddleware;
impl Middleware for AssertMiddleware {
fn handle(&self, request: &Request, connection: &ConnectionInfo, next: &dyn Application) -> Result<Response, String> {
assert_eq!("GET", request.method);
assert_eq!("/healthz", request.request_uri);
next.execute(request, connection)
}
}
let app = WithMiddleware::new(App::new()).wrap(AssertMiddleware);
app.execute(&get("/healthz"), &conn()).unwrap();
}
#[test]
fn clones_share_the_layer_list_and_wrapping_a_clone_leaves_the_original_alone() {
let calls = Arc::new(AtomicU32::new(0));
let base = WithMiddleware::new(App::new()).wrap(CountingMiddleware { count: Arc::clone(&calls) });
let copy = base.clone();
assert!(Arc::ptr_eq(&base.layers, ©.layers), "a clone must not copy the layers");
let wider = copy.wrap(CountingMiddleware { count: Arc::clone(&calls) });
assert_eq!(1, base.layers.len());
assert_eq!(2, wider.layers.len());
let _ = base.execute(&get("/healthz"), &conn());
assert_eq!(1, calls.load(Ordering::SeqCst));
let _ = wider.execute(&get("/healthz"), &conn());
assert_eq!(3, calls.load(Ordering::SeqCst));
}
struct Generic<T>(std::marker::PhantomData<T>);
impl<T: Send + Sync> Middleware for Generic<T> {
fn handle(&self, request: &Request, connection: &ConnectionInfo, next: &dyn Application) -> Result<Response, String> {
next.execute(request, connection)
}
}
struct Named;
impl Middleware for Named {
fn handle(&self, request: &Request, connection: &ConnectionInfo, next: &dyn Application) -> Result<Response, String> {
next.execute(request, connection)
}
fn layer_name(&self) -> &'static str {
"custom-name"
}
}
#[test]
fn a_layer_is_named_by_its_type_without_path_or_generics() {
assert_eq!(crate::middleware::RateLimitLayer.layer_name(), "RateLimitLayer");
assert_eq!(Generic::<Vec<String>>(std::marker::PhantomData).layer_name(), "Generic");
let boxed: Arc<dyn Middleware> = Arc::new(crate::maintenance::MaintenanceLayer);
assert_eq!(boxed.layer_name(), "MaintenanceLayer", "through dyn too");
assert_eq!(Named.layer_name(), "custom-name");
assert_eq!(super::short_type_name("Plain"), "Plain");
}
#[test]
fn with_middleware_reports_its_layers_and_the_inner_routes() {
fn ok(_: &Request, _: &crate::router::PathParams, _: &ConnectionInfo, _: &()) -> Response {
Response::new()
}
let routes = App::with_state(()).get("/a/:id", ok).post("/b", ok);
let app = WithMiddleware::new(routes.wrap(Named)).wrap(crate::middleware::RateLimitLayer);
let entries: Vec<(String, String)> = app.registered_routes().into_iter().map(|r| (r.method, r.pattern)).collect();
assert_eq!(entries, vec![("GET".to_string(), "/a/:id".to_string()), ("POST".to_string(), "/b".to_string())]);
assert_eq!(app.middleware_names(), vec!["RateLimitLayer", "custom-name"], "outer stack first");
let mcp = crate::mcp::McpServer::new("t", "1").wrap(app);
assert_eq!(mcp.registered_routes().len(), 2);
assert_eq!(mcp.middleware_names(), vec!["RateLimitLayer", "custom-name"]);
assert!(App::new().registered_routes().is_empty());
assert!(crate::mcp::McpServer::new("t", "1").middleware_names().is_empty());
}