use std::time::Duration;
use camel_api::body::Body;
use camel_api::{Exchange, Message, Value};
use camel_component_api::test_support::acquire_deadline;
use camel_core::LanguageRegistryError;
use camel_dsl::parse_yaml;
use camel_language_xpath::XPathLanguage;
use camel_test::CamelTestContext;
use tower::ServiceExt;
fn test_rt() -> std::sync::Arc<dyn camel_component_api::RuntimeObservability> {
std::sync::Arc::new(camel_component_api::NoOpComponentContext)
}
fn ensure_xpath_registered(ctx: &mut camel_core::CamelContext) {
match ctx.register_language("xpath", Box::new(XPathLanguage::new())) {
Ok(()) | Err(LanguageRegistryError::AlreadyRegistered { .. }) => {}
}
}
async fn send_to_direct(h: &CamelTestContext, endpoint_uri: &str, exchange: Exchange) {
let producer = {
let ctx = acquire_deadline(
h.ctx(),
"camel context (send_to_direct)",
Duration::from_secs(10),
)
.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 xpath_filter_with_xml_body() {
let h = CamelTestContext::builder()
.with_direct()
.with_mock()
.build()
.await;
let mut guard = h.ctx().lock().await;
ensure_xpath_registered(&mut guard);
drop(guard);
let yaml = r#"
routes:
- id: "xpath-filter"
from: "direct:start"
steps:
- filter:
# Strict-bool predicate contract (language-value-boundary, sealed Q3):
# a bare nodeset like /order[@status='active'] is a TypeMismatch, so
# wrap the match test in boolean() to yield a bool result.
xpath: "boolean(/order[@status='active'])"
steps:
- to: "mock:filtered"
"#;
let routes = parse_yaml(yaml).expect("YAML parse failed");
for route in routes {
h.add_route(route).await.unwrap();
}
h.start().await;
tokio::time::sleep(Duration::from_millis(50)).await;
let xml = r#"<order status="active"><item>widget</item></order>"#;
let exchange = Exchange::new(Message::new(Body::Xml(xml.to_string())));
send_to_direct(&h, "direct:start", exchange).await;
tokio::time::sleep(Duration::from_millis(100)).await;
h.stop().await;
let endpoint = h.mock().get_endpoint("filtered").unwrap();
endpoint.assert_exchange_count(1).await;
let exchanges = endpoint.get_received_exchanges().await;
assert_eq!(exchanges.len(), 1);
}
#[tokio::test(flavor = "multi_thread")]
async fn xpath_set_header_from_body() {
let h = CamelTestContext::builder()
.with_direct()
.with_mock()
.build()
.await;
let mut guard = h.ctx().lock().await;
ensure_xpath_registered(&mut guard);
drop(guard);
let yaml = r#"
routes:
- id: "xpath-header"
from: "direct:start"
steps:
- set_header:
key: "bookTitle"
xpath: "/books/book[1]/title"
- to: "mock:header-out"
"#;
let routes = parse_yaml(yaml).expect("YAML parse failed");
for route in routes {
h.add_route(route).await.unwrap();
}
h.start().await;
tokio::time::sleep(Duration::from_millis(50)).await;
let xml = r#"<books><book><title>Rust in Action</title></book><book><title>Programming Rust</title></book></books>"#;
let exchange = Exchange::new(Message::new(Body::Xml(xml.to_string())));
send_to_direct(&h, "direct:start", exchange).await;
tokio::time::sleep(Duration::from_millis(100)).await;
h.stop().await;
let endpoint = h.mock().get_endpoint("header-out").unwrap();
endpoint.assert_exchange_count(1).await;
let exchanges = endpoint.get_received_exchanges().await;
let ex = &exchanges[0];
assert_eq!(
ex.input.header("bookTitle"),
Some(&Value::String("Rust in Action".into())),
"Header 'bookTitle' should be 'Rust in Action'"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn xpath_set_body_from_query() {
let h = CamelTestContext::builder()
.with_direct()
.with_mock()
.build()
.await;
let mut guard = h.ctx().lock().await;
ensure_xpath_registered(&mut guard);
drop(guard);
let yaml = r#"
routes:
- id: "xpath-body"
from: "direct:start"
steps:
- set_body:
xpath: "/root/value"
- to: "mock:body-out"
"#;
let routes = parse_yaml(yaml).expect("YAML parse failed");
for route in routes {
h.add_route(route).await.unwrap();
}
h.start().await;
tokio::time::sleep(Duration::from_millis(50)).await;
let xml = r#"<root><value>hello world</value></root>"#;
let exchange = Exchange::new(Message::new(Body::Xml(xml.to_string())));
send_to_direct(&h, "direct:start", exchange).await;
tokio::time::sleep(Duration::from_millis(100)).await;
h.stop().await;
let endpoint = h.mock().get_endpoint("body-out").unwrap();
endpoint.assert_exchange_count(1).await;
let exchanges = endpoint.get_received_exchanges().await;
let ex = &exchanges[0];
match &ex.input.body {
Body::Text(t) | Body::Xml(t) => {
assert_eq!(t, "hello world");
}
other => panic!("expected text body with 'hello world', got {:?}", other),
}
}
fn expect_raw_text<'a>(body: &'a Body, ctx: &str) -> &'a str {
match body {
Body::Text(t) => t,
other => panic!("{ctx}: expected Body::Text (raw link string), got {other:?}"),
}
}
#[tokio::test(flavor = "multi_thread")]
async fn xpath_split_three_matches_fragments_are_raw_text() {
let h = CamelTestContext::builder()
.with_direct()
.with_mock()
.build()
.await;
let mut guard = h.ctx().lock().await; ensure_xpath_registered(&mut guard);
drop(guard);
let yaml = r#"
routes:
- id: "xpath-split-three"
from: "direct:start"
steps:
- split:
expression:
xpath: "//item/link"
steps:
- to: "mock:fragments"
"#;
let routes = parse_yaml(yaml).expect("YAML parse failed");
for route in routes {
h.add_route(route).await.unwrap();
}
h.start().await;
let xml = r#"<items><item><link>https://example.org/1.xml</link></item><item><link>https://example.org/2.xml</link></item><item><link>https://example.org/3.xml</link></item></items>"#;
let exchange = Exchange::new(Message::new(Body::Xml(xml.to_string())));
send_to_direct(&h, "direct:start", exchange).await;
let endpoint = h.mock().get_endpoint("fragments").unwrap();
endpoint.await_exchanges(3, Duration::from_secs(2)).await;
h.stop().await;
let expected = [
"https://example.org/1.xml",
"https://example.org/2.xml",
"https://example.org/3.xml",
];
let exchanges = endpoint.get_received_exchanges().await;
assert_eq!(
exchanges.len(),
3,
"one fragment per //item/link match expected"
);
for (i, ex) in exchanges.iter().enumerate() {
let t = expect_raw_text(&ex.input.body, &format!("fragment {i}"));
assert_eq!(t, expected[i], "fragment {i} must be the raw link string");
assert!(
!t.contains('"'),
"fragment {i} must not carry a literal quote character"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn xpath_split_match_count_parity() {
let h = CamelTestContext::builder()
.with_direct()
.with_mock()
.build()
.await;
let mut guard = h.ctx().lock().await; ensure_xpath_registered(&mut guard);
drop(guard);
let yaml = r#"
routes:
- id: "xpath-split-parity-one"
from: "direct:one"
steps:
- split:
expression:
xpath: "//item/link"
steps:
- to: "mock:parity-one"
- id: "xpath-split-parity-many"
from: "direct:many"
steps:
- split:
expression:
xpath: "//item/link"
steps:
- to: "mock:parity-many"
"#;
let routes = parse_yaml(yaml).expect("YAML parse failed");
for route in routes {
h.add_route(route).await.unwrap();
}
h.start().await;
let one_link = r#"<items><item><link>https://example.org/bar.xml</link></item></items>"#;
let three_links = r#"<items><item><link>https://example.org/bar.xml</link></item><item><link>https://example.org/foo.xml</link></item><item><link>https://example.org/quux.xml</link></item></items>"#;
send_to_direct(
&h,
"direct:one",
Exchange::new(Message::new(Body::Xml(one_link.to_string()))),
)
.await;
send_to_direct(
&h,
"direct:many",
Exchange::new(Message::new(Body::Xml(three_links.to_string()))),
)
.await;
let one_ep = h.mock().get_endpoint("parity-one").unwrap();
let many_ep = h.mock().get_endpoint("parity-many").unwrap();
one_ep.await_exchanges(1, Duration::from_secs(2)).await;
many_ep.await_exchanges(3, Duration::from_secs(2)).await;
h.stop().await;
let one_exchanges = one_ep.get_received_exchanges().await;
let many_exchanges = many_ep.get_received_exchanges().await;
assert_eq!(one_exchanges.len(), 1, "1-link run yields one fragment");
assert_eq!(many_exchanges.len(), 3, "3-link run yields three fragments");
let one_first = expect_raw_text(&one_exchanges[0].input.body, "1-link run fragment[0]");
let many_first = expect_raw_text(&many_exchanges[0].input.body, "3-link run fragment[0]");
assert_eq!(
one_first, "https://example.org/bar.xml",
"1-link run fragment[0] must be the raw link string"
);
assert_eq!(
many_first, "https://example.org/bar.xml",
"3-link run fragment[0] must be the raw link string"
);
assert_eq!(
one_first, many_first,
"fragment[0] of both runs must be byte-identical"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn xpath_split_collect_all_aggregate_byte_identical() {
let h = CamelTestContext::builder()
.with_direct()
.with_mock()
.build()
.await;
let mut guard = h.ctx().lock().await; ensure_xpath_registered(&mut guard);
drop(guard);
let yaml = r#"
routes:
- id: "xpath-split-collect-all"
from: "direct:start"
steps:
- split:
expression:
xpath: "//item/link"
aggregation: collect_all
- to: "mock:agg"
"#;
let routes = parse_yaml(yaml).expect("YAML parse failed");
for route in routes {
h.add_route(route).await.unwrap();
}
h.start().await;
let xml = r#"<items><item><link>https://example.org/1.xml</link></item><item><link>https://example.org/2.xml</link></item><item><link>https://example.org/3.xml</link></item></items>"#;
let exchange = Exchange::new(Message::new(Body::Xml(xml.to_string())));
send_to_direct(&h, "direct:start", exchange).await;
let endpoint = h.mock().get_endpoint("agg").unwrap();
endpoint.await_exchanges(1, Duration::from_secs(2)).await;
h.stop().await;
let exchanges = endpoint.get_received_exchanges().await;
assert_eq!(exchanges.len(), 1, "collect_all emits one aggregate");
match &exchanges[0].input.body {
Body::Json(v) => assert_eq!(
v.to_string(),
r#"["https://example.org/1.xml","https://example.org/2.xml","https://example.org/3.xml"]"#,
"aggregate must be a string array byte-identical to the link set"
),
other => panic!("expected JSON array aggregate body, got {other:?}"),
}
}