use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
use camel_api::body::Body;
use camel_api::{BoxProcessor, BoxProcessorExt, CamelError, Exchange};
use camel_builder::{RouteBuilder, StepAccumulator};
use camel_dsl::parse_yaml;
use camel_test::CamelTestContext;
use tower::ServiceExt;
fn test_rt() -> Arc<dyn camel_component_api::RuntimeObservability> {
Arc::new(camel_component_api::NoOpComponentContext)
}
async fn send_to_direct(h: &CamelTestContext, endpoint_uri: &str, exchange: Exchange) {
let producer = {
let ctx = h.ctx().lock().await;
let producer_ctx = ctx.producer_context();
let registry = ctx.registry();
let component = registry
.get("direct")
.expect("direct component not registered");
let endpoint = component
.create_endpoint(endpoint_uri, &*ctx)
.expect("failed to create direct endpoint");
endpoint
.create_producer(test_rt(), &producer_ctx)
.expect("failed to create direct producer")
};
producer
.oneshot(exchange)
.await
.expect("failed to send exchange to direct endpoint");
}
#[tokio::test(flavor = "multi_thread")]
async fn do_try_handled_routes_recovered_exchange_downstream() {
let h = CamelTestContext::builder()
.with_direct()
.with_mock()
.build()
.await;
let route = RouteBuilder::from("direct:handled")
.route_id("do-try-handled")
.do_try()
.process(BoxProcessor::from_fn(|_ex: Exchange| {
Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
}))
.do_catch_exception(&["ProcessorError"])
.handled()
.process(BoxProcessor::from_fn(|mut ex: Exchange| {
Box::pin(async move {
ex.input.body = Body::Text("caught".into());
Ok(ex)
})
}))
.end_do_catch()
.end_do_try()
.to("mock:handled-result")
.build()
.unwrap();
h.add_route(route).await.unwrap();
h.start().await;
tokio::time::sleep(Duration::from_millis(50)).await;
send_to_direct(&h, "direct:handled", Exchange::default()).await;
tokio::time::sleep(Duration::from_millis(200)).await;
let endpoint = h.mock().get_endpoint("handled-result").unwrap();
endpoint.assert_exchange_count(1).await;
let received = endpoint.get_received_exchanges().await;
assert_eq!(received.len(), 1);
assert_eq!(received[0].input.body.as_text(), Some("caught"));
h.stop().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn do_try_finally_runs_after_handled_catch() {
let finally_counter = Arc::new(AtomicU32::new(0));
let counter_clone = finally_counter.clone();
let h = CamelTestContext::builder()
.with_direct()
.with_mock()
.build()
.await;
let route = RouteBuilder::from("direct:finally")
.route_id("do-try-finally")
.do_try()
.process(BoxProcessor::from_fn(|_ex: Exchange| {
Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
}))
.do_catch_exception(&["ProcessorError"])
.handled()
.process(BoxProcessor::from_fn(|mut ex: Exchange| {
Box::pin(async move {
ex.input.body = Body::Text("caught".into());
Ok(ex)
})
}))
.end_do_catch()
.do_finally()
.process(BoxProcessor::from_fn(move |ex: Exchange| {
let c = counter_clone.clone();
Box::pin(async move {
c.fetch_add(1, Ordering::SeqCst);
Ok(ex)
})
}))
.end_do_finally()
.end_do_try()
.to("mock:finally-result")
.build()
.unwrap();
h.add_route(route).await.unwrap();
h.start().await;
tokio::time::sleep(Duration::from_millis(50)).await;
send_to_direct(&h, "direct:finally", Exchange::default()).await;
tokio::time::sleep(Duration::from_millis(200)).await;
assert_eq!(
finally_counter.load(Ordering::SeqCst),
1,
"doFinally must run after Handled catch recovers the exchange"
);
let endpoint = h.mock().get_endpoint("finally-result").unwrap();
endpoint.assert_exchange_count(1).await;
let received = endpoint.get_received_exchanges().await;
assert_eq!(received.len(), 1);
assert_eq!(
received[0].input.body.as_text(),
Some("caught"),
"catch branch must have set body to 'caught' before mock received"
);
h.stop().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn do_try_propagate_does_not_reach_downstream() {
let h = CamelTestContext::builder()
.with_direct()
.with_mock()
.build()
.await;
let route = RouteBuilder::from("direct:propagate")
.route_id("do-try-propagate")
.do_try()
.process(BoxProcessor::from_fn(|_ex: Exchange| {
Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
}))
.do_catch_exception(&["ProcessorError"])
.propagate()
.process(BoxProcessor::from_fn(|mut ex: Exchange| {
Box::pin(async move {
ex.input.body = Body::Text("should-not-reach-downstream".into());
Ok(ex)
})
}))
.end_do_catch()
.end_do_try()
.to("mock:propagate-result")
.build()
.unwrap();
h.add_route(route).await.unwrap();
h.start().await;
tokio::time::sleep(Duration::from_millis(50)).await;
let producer = {
let ctx = h.ctx().lock().await;
let producer_ctx = ctx.producer_context();
let registry = ctx.registry();
let component = registry
.get("direct")
.expect("direct component not registered");
let endpoint = component
.create_endpoint("direct:propagate", &*ctx)
.expect("failed to create direct endpoint");
endpoint
.create_producer(test_rt(), &producer_ctx)
.expect("failed to create direct producer")
};
let _ = producer.oneshot(Exchange::default()).await;
tokio::time::sleep(Duration::from_millis(200)).await;
let endpoint = h.mock().get_endpoint("propagate-result").unwrap();
endpoint.assert_exchange_count(0).await;
h.stop().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn do_try_yaml_on_when_predicate_filters_catch_e2e() {
let h = CamelTestContext::builder()
.with_direct()
.with_mock()
.build()
.await;
let failing_route = RouteBuilder::from("direct:failing-step")
.route_id("failing-step")
.process_fn(BoxProcessor::from_fn(|_ex: Exchange| {
Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
}))
.build()
.unwrap();
h.add_route(failing_route).await.unwrap();
let yaml = r#"
routes:
- id: "yaml-on-when-route"
from: "direct:yaml-on-when"
steps:
- set_body: "fail"
- do_try:
steps:
- to: "direct:failing-step"
catch:
- exception: ["ProcessorError"]
on_when: "${body} == 'fail'"
disposition: handled
steps:
- to: "mock:yaml-result"
"#;
let yaml_routes = parse_yaml(yaml).expect("YAML parse failed");
assert_eq!(
yaml_routes.len(),
1,
"YAML must produce exactly one route definition"
);
for route in yaml_routes {
h.add_route(route).await.unwrap();
}
h.start().await;
tokio::time::sleep(Duration::from_millis(50)).await;
send_to_direct(&h, "direct:yaml-on-when", Exchange::default()).await;
tokio::time::sleep(Duration::from_millis(200)).await;
let endpoint = h.mock().get_endpoint("yaml-result").unwrap();
endpoint.assert_exchange_count(1).await;
let received = endpoint.get_received_exchanges().await;
assert_eq!(received.len(), 1);
assert_eq!(
received[0].input.body.as_text(),
Some("fail"),
"predicate `${{body}} == 'fail'` matched → catch ran → body unchanged through to mock"
);
h.stop().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn do_try_propagate_reaches_route_on_exception() {
let h = CamelTestContext::builder()
.with_direct()
.with_mock()
.build()
.await;
let route = RouteBuilder::from("direct:propagate-to-on-exception")
.route_id("propagate-to-on-exception")
.on_exception(|e: &CamelError| matches!(e, CamelError::ProcessorError(_)))
.handled_by("mock:on-exception-caught")
.end_on_exception()
.do_try()
.process(BoxProcessor::from_fn(|_ex: Exchange| {
Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
}))
.do_catch_exception(&["ProcessorError"])
.propagate()
.process(BoxProcessor::from_fn(|mut ex: Exchange| {
Box::pin(async move {
ex.input.body = Body::Text("should-not-reach-downstream".into());
Ok(ex)
})
}))
.end_do_catch()
.end_do_try()
.to("mock:after-do-try")
.build()
.unwrap();
h.add_route(route).await.unwrap();
h.start().await;
tokio::time::sleep(Duration::from_millis(50)).await;
let producer = {
let ctx = h.ctx().lock().await;
let producer_ctx = ctx.producer_context();
let registry = ctx.registry();
let component = registry
.get("direct")
.expect("direct component not registered");
let endpoint = component
.create_endpoint("direct:propagate-to-on-exception", &*ctx)
.expect("failed to create direct endpoint");
endpoint
.create_producer(test_rt(), &producer_ctx)
.expect("failed to create direct producer")
};
let _ = producer.oneshot(Exchange::default()).await;
tokio::time::sleep(Duration::from_millis(200)).await;
let after_do_try = h.mock().get_endpoint("after-do-try").unwrap();
after_do_try.assert_exchange_count(0).await;
let on_exception_caught = h.mock().get_endpoint("on-exception-caught").unwrap();
on_exception_caught.assert_exchange_count(1).await;
h.stop().await;
}