mod test_utils;
use rust_webx_core::error::Result as LrwfResult;
use rust_webx_core::http::IHttpContext;
use rust_webx_core::middleware::IMiddleware;
use rust_webx_host::pipeline::{HandlerFn, MiddlewarePipeline};
use std::ops::ControlFlow;
use std::sync::Arc;
#[allow(dead_code)]
struct CounterMiddleware {
name: &'static str,
}
impl CounterMiddleware {
fn new(name: &'static str) -> Self {
Self { name }
}
}
#[async_trait::async_trait]
impl IMiddleware for CounterMiddleware {
async fn invoke(&self, _ctx: &mut dyn IHttpContext) -> LrwfResult<ControlFlow<()>> {
Ok(ControlFlow::Continue(()))
}
}
#[tokio::test]
async fn pipeline_empty_executes_final_handler() {
let pipeline = MiddlewarePipeline::new();
let mut ctx = test_utils::TestHttpContext::new("GET", "/test");
let final_handler: HandlerFn = Arc::new(move |ctx: &mut dyn IHttpContext| {
Box::pin(async move {
ctx.response_mut().write_text("done").await?;
Ok(())
})
});
let result = pipeline.execute(&mut ctx, final_handler).await;
assert!(result.is_ok());
let (_status, _headers, body) = ctx.into_response_parts();
assert_eq!(body.unwrap(), b"done");
}
#[tokio::test]
async fn pipeline_multiple_middleware_executed_in_order() {
let mut pipeline = MiddlewarePipeline::new();
pipeline.add_middleware(Arc::new(CounterMiddleware::new("first")));
pipeline.add_middleware(Arc::new(CounterMiddleware::new("second")));
pipeline.add_middleware(Arc::new(CounterMiddleware::new("third")));
let mut ctx = test_utils::TestHttpContext::new("GET", "/test");
let final_handler: HandlerFn = Arc::new(move |ctx: &mut dyn IHttpContext| {
Box::pin(async move {
ctx.response_mut().set_status(201);
Ok(())
})
});
let result = pipeline.execute(&mut ctx, final_handler).await;
assert!(result.is_ok());
let (status, _headers, _body) = ctx.into_response_parts();
assert_eq!(status, 201);
}
#[tokio::test]
async fn pipeline_middleware_can_modify_context() {
struct HeaderMiddleware;
#[async_trait::async_trait]
impl IMiddleware for HeaderMiddleware {
async fn invoke(&self, ctx: &mut dyn IHttpContext) -> LrwfResult<ControlFlow<()>> {
ctx.response_mut().set_header("x-powered-by", "lrwf-test");
Ok(ControlFlow::Continue(()))
}
}
let mut pipeline = MiddlewarePipeline::new();
pipeline.add_middleware(Arc::new(HeaderMiddleware));
let mut ctx = test_utils::TestHttpContext::new("GET", "/test");
let final_handler: HandlerFn = Arc::new(move |ctx: &mut dyn IHttpContext| {
Box::pin(async move {
ctx.response_mut().write_text("ok").await?;
Ok(())
})
});
pipeline.execute(&mut ctx, final_handler).await.unwrap();
let (_status, headers, _body) = ctx.into_response_parts();
let x_powered = headers
.iter()
.find(|(k, _)| k == "x-powered-by")
.map(|(_, v)| v.as_str());
assert_eq!(x_powered, Some("lrwf-test"));
}
#[tokio::test]
async fn pipeline_after_hook_executed() {
struct AfterMiddleware;
#[async_trait::async_trait]
impl IMiddleware for AfterMiddleware {
async fn invoke(&self, _ctx: &mut dyn IHttpContext) -> LrwfResult<ControlFlow<()>> {
Ok(ControlFlow::Continue(()))
}
async fn after(&self, ctx: &mut dyn IHttpContext) -> LrwfResult<()> {
ctx.response_mut().set_header("x-after-ran", "yes");
Ok(())
}
}
let mut pipeline = MiddlewarePipeline::new();
pipeline.add_middleware(Arc::new(AfterMiddleware));
let mut ctx = test_utils::TestHttpContext::new("GET", "/test");
let final_handler: HandlerFn = Arc::new(move |ctx: &mut dyn IHttpContext| {
Box::pin(async move {
ctx.response_mut().set_status(200);
ctx.response_mut().write_text("ok").await?;
Ok(())
})
});
pipeline.execute(&mut ctx, final_handler).await.unwrap();
let (_status, headers, _body) = ctx.into_response_parts();
let x_after = headers
.iter()
.find(|(k, _)| k == "x-after-ran")
.map(|(_, v)| v.as_str());
assert_eq!(x_after, Some("yes"));
}
#[tokio::test]
async fn pipeline_after_hooks_executed_in_reverse_order() {
use std::sync::Mutex;
let order = Arc::new(Mutex::new(Vec::<&'static str>::new()));
struct OrderMiddleware {
name: &'static str,
order: Arc<Mutex<Vec<&'static str>>>,
}
#[async_trait::async_trait]
impl IMiddleware for OrderMiddleware {
async fn invoke(&self, _ctx: &mut dyn IHttpContext) -> LrwfResult<ControlFlow<()>> {
self.order.lock().unwrap().push(self.name);
Ok(ControlFlow::Continue(()))
}
async fn after(&self, _ctx: &mut dyn IHttpContext) -> LrwfResult<()> {
self.order.lock().unwrap().push(self.name);
Ok(())
}
}
let mw_a = Arc::new(OrderMiddleware {
name: "A",
order: Arc::clone(&order),
});
let mw_b = Arc::new(OrderMiddleware {
name: "B",
order: Arc::clone(&order),
});
let mw_c = Arc::new(OrderMiddleware {
name: "C",
order: Arc::clone(&order),
});
let mut pipeline = MiddlewarePipeline::new();
pipeline.add_middleware(mw_a);
pipeline.add_middleware(mw_b);
pipeline.add_middleware(mw_c);
let mut ctx = test_utils::TestHttpContext::new("GET", "/test");
let final_handler: HandlerFn = Arc::new(move |ctx: &mut dyn IHttpContext| {
Box::pin(async move {
ctx.response_mut().set_status(200);
ctx.response_mut().write_text("ok").await?;
Ok(())
})
});
pipeline.execute(&mut ctx, final_handler).await.unwrap();
let result = order.lock().unwrap().clone();
assert_eq!(result, vec!["A", "B", "C", "C", "B", "A"]);
}
#[tokio::test]
async fn pipeline_short_circuit_on_invoke_error() {
struct FailingMiddleware;
#[async_trait::async_trait]
impl IMiddleware for FailingMiddleware {
async fn invoke(&self, _ctx: &mut dyn IHttpContext) -> LrwfResult<ControlFlow<()>> {
Err(rust_webx_core::error::Error::Http("blocked".into()))
}
}
let mut pipeline = MiddlewarePipeline::new();
pipeline.add_middleware(Arc::new(FailingMiddleware));
let mut ctx = test_utils::TestHttpContext::new("GET", "/test");
let final_handler: HandlerFn = Arc::new(move |ctx: &mut dyn IHttpContext| {
Box::pin(async move {
ctx.response_mut().write_text("should-not-reach").await?;
Ok(())
})
});
let result = pipeline.execute(&mut ctx, final_handler).await;
assert!(
result.is_err(),
"Error from invoke should short-circuit the pipeline"
);
let (_status, _headers, body) = ctx.into_response_parts();
assert!(
body.is_none(),
"final handler should be skipped on short-circuit"
);
}
#[tokio::test]
async fn pipeline_after_hooks_skipped_on_final_handler_error() {
struct ObserveMiddleware {
after_ran: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
#[async_trait::async_trait]
impl IMiddleware for ObserveMiddleware {
async fn invoke(&self, _ctx: &mut dyn IHttpContext) -> LrwfResult<ControlFlow<()>> {
Ok(ControlFlow::Continue(()))
}
async fn after(&self, _ctx: &mut dyn IHttpContext) -> LrwfResult<()> {
self.after_ran
.store(true, std::sync::atomic::Ordering::SeqCst);
Ok(())
}
}
let after_ran = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let mut pipeline = MiddlewarePipeline::new();
pipeline.add_middleware(Arc::new(ObserveMiddleware {
after_ran: Arc::clone(&after_ran),
}));
let mut ctx = test_utils::TestHttpContext::new("GET", "/test");
let final_handler: HandlerFn = Arc::new(move |_ctx: &mut dyn IHttpContext| {
Box::pin(async move {
Err(rust_webx_core::error::Error::Internal(
"final handler error".into(),
))
})
});
let result = pipeline.execute(&mut ctx, final_handler).await;
assert!(result.is_err());
assert!(!after_ran.load(std::sync::atomic::Ordering::SeqCst));
}
#[tokio::test]
async fn pipeline_short_circuit_via_control_flow_break() {
struct BreakMiddleware;
#[async_trait::async_trait]
impl IMiddleware for BreakMiddleware {
async fn invoke(&self, ctx: &mut dyn IHttpContext) -> LrwfResult<ControlFlow<()>> {
ctx.response_mut().set_status(204);
Ok(ControlFlow::Break(()))
}
}
let mut pipeline = MiddlewarePipeline::new();
pipeline.add_middleware(Arc::new(BreakMiddleware));
let mut ctx = test_utils::TestHttpContext::new("GET", "/test");
let final_handler: HandlerFn = Arc::new(move |ctx: &mut dyn IHttpContext| {
Box::pin(async move {
ctx.response_mut().write_text("should-not-reach").await?;
Ok(())
})
});
let result = pipeline.execute(&mut ctx, final_handler).await;
assert!(result.is_ok(), "Break is not an error");
let (status, _headers, body) = ctx.into_response_parts();
assert_eq!(status, 204, "Break should preserve the response set by middleware");
assert!(body.is_none(), "final handler should be skipped on Break");
}
#[tokio::test]
async fn pipeline_break_runs_after_hooks_on_executed_middleware() {
use std::sync::atomic::{AtomicBool, Ordering};
struct ContinueThenAfter {
after_ran: Arc<AtomicBool>,
}
#[async_trait::async_trait]
impl IMiddleware for ContinueThenAfter {
async fn invoke(&self, _ctx: &mut dyn IHttpContext) -> LrwfResult<ControlFlow<()>> {
Ok(ControlFlow::Continue(()))
}
async fn after(&self, _ctx: &mut dyn IHttpContext) -> LrwfResult<()> {
self.after_ran.store(true, Ordering::SeqCst);
Ok(())
}
}
struct BreakMiddleware;
#[async_trait::async_trait]
impl IMiddleware for BreakMiddleware {
async fn invoke(&self, _ctx: &mut dyn IHttpContext) -> LrwfResult<ControlFlow<()>> {
Ok(ControlFlow::Break(()))
}
}
let after_ran = Arc::new(AtomicBool::new(false));
let mut pipeline = MiddlewarePipeline::new();
pipeline.add_middleware(Arc::new(ContinueThenAfter { after_ran: Arc::clone(&after_ran) }));
pipeline.add_middleware(Arc::new(BreakMiddleware));
let mut ctx = test_utils::TestHttpContext::new("GET", "/test");
let final_handler: HandlerFn = Arc::new(move |ctx: &mut dyn IHttpContext| {
Box::pin(async move {
ctx.response_mut().write_text("should-not-reach").await?;
Ok(())
})
});
pipeline.execute(&mut ctx, final_handler).await.unwrap();
assert!(after_ran.load(Ordering::SeqCst), "after hooks on executed middleware should run");
}