1use std::sync::{
4 Arc,
5 atomic::{AtomicBool, Ordering},
6};
7
8use camel_api::CamelError;
9use camel_component_direct::DirectComponent;
10use camel_component_log::LogComponent;
11use camel_component_mock::MockComponent;
12use camel_component_timer::TimerComponent;
13use camel_core::CamelContext;
14use camel_core::route::RouteDefinition;
15use tokio::sync::Mutex;
16
17use crate::time::TimeController;
18
19pub struct NoTimeControl;
24pub struct WithTimeControl;
25
26type Registration = Box<dyn FnOnce(&mut CamelContext) + Send>;
31
32pub struct CamelTestContextBuilder<S = NoTimeControl> {
36 registrations: Vec<Registration>,
37 mock: MockComponent,
38 _state: std::marker::PhantomData<S>,
39}
40
41impl CamelTestContextBuilder<NoTimeControl> {
42 pub(crate) fn new() -> Self {
43 Self {
44 registrations: Vec::new(),
45 mock: MockComponent::new(),
46 _state: std::marker::PhantomData,
47 }
48 }
49
50 pub fn with_time_control(self) -> CamelTestContextBuilder<WithTimeControl> {
53 CamelTestContextBuilder {
54 registrations: self.registrations,
55 mock: self.mock,
56 _state: std::marker::PhantomData,
57 }
58 }
59
60 pub async fn build(self) -> CamelTestContext {
62 build_context(self.registrations, self.mock).await
63 }
64}
65
66impl CamelTestContextBuilder<WithTimeControl> {
67 pub async fn build(self) -> (CamelTestContext, TimeController) {
72 tokio::time::pause();
73 let ctx = build_context(self.registrations, self.mock).await;
74 (ctx, TimeController)
75 }
76}
77
78macro_rules! impl_builder_methods {
79 ($S:ty) => {
80 impl CamelTestContextBuilder<$S> {
81 pub fn with_mock(self) -> Self {
84 self
85 }
86
87 pub fn with_timer(mut self) -> Self {
89 self.registrations.push(Box::new(|ctx: &mut CamelContext| {
90 ctx.register_component(TimerComponent::new());
91 }));
92 self
93 }
94
95 pub fn with_log(mut self) -> Self {
97 self.registrations.push(Box::new(|ctx: &mut CamelContext| {
98 ctx.register_component(LogComponent::new());
99 }));
100 self
101 }
102
103 pub fn with_direct(mut self) -> Self {
105 self.registrations.push(Box::new(|ctx: &mut CamelContext| {
106 ctx.register_component(DirectComponent::new());
107 }));
108 self
109 }
110
111 pub fn with_seda(mut self) -> Self {
113 self.registrations.push(Box::new(|ctx: &mut CamelContext| {
114 ctx.register_component(camel_component_seda::SedaComponent::new());
115 }));
116 self
117 }
118
119 pub fn with_component<C>(mut self, component: C) -> Self
121 where
122 C: camel_component_api::Component + 'static,
123 {
124 self.registrations
125 .push(Box::new(move |ctx: &mut CamelContext| {
126 ctx.register_component(component);
127 }));
128 self
129 }
130 }
131 };
132}
133
134impl_builder_methods!(NoTimeControl);
135impl_builder_methods!(WithTimeControl);
136
137async fn build_context(registrations: Vec<Registration>, mock: MockComponent) -> CamelTestContext {
142 let mut ctx = CamelContext::builder().build().await.unwrap(); ctx.register_component(mock.clone());
146
147 for register in registrations {
149 register(&mut ctx);
150 }
151
152 let ctx = Arc::new(Mutex::new(ctx));
153 let stopped = Arc::new(AtomicBool::new(false));
154
155 CamelTestContext {
156 ctx: ctx.clone(),
157 mock,
158 stopped: stopped.clone(),
159 _guard: TestGuard { ctx, stopped },
160 }
161}
162
163pub(crate) struct TestGuard {
168 ctx: Arc<Mutex<CamelContext>>,
169 stopped: Arc<AtomicBool>,
170}
171
172impl Drop for TestGuard {
173 fn drop(&mut self) {
174 if self.stopped.swap(true, Ordering::SeqCst) {
175 return;
177 }
178 let ctx = self.ctx.clone();
179 if let Ok(handle) = tokio::runtime::Handle::try_current() {
180 match handle.runtime_flavor() {
181 tokio::runtime::RuntimeFlavor::MultiThread => {
182 tokio::task::block_in_place(|| {
184 handle.block_on(async move {
185 let mut ctx = ctx.lock().await;
186 let _ = ctx.stop().await;
187 });
188 });
189 }
190 tokio::runtime::RuntimeFlavor::CurrentThread => {
191 handle.spawn(async move {
194 let mut ctx = ctx.lock().await;
195 let _ = ctx.stop().await;
196 });
197 }
198 _ => {
199 handle.spawn(async move {
200 let mut ctx = ctx.lock().await;
201 let _ = ctx.stop().await;
202 });
203 }
204 }
205 }
206 }
207}
208
209pub struct CamelTestContext {
235 ctx: Arc<Mutex<CamelContext>>,
236 mock: MockComponent,
237 stopped: Arc<AtomicBool>,
238 _guard: TestGuard,
239}
240
241impl CamelTestContext {
242 pub fn builder() -> CamelTestContextBuilder<NoTimeControl> {
244 CamelTestContextBuilder::new()
245 }
246
247 pub async fn add_route(&self, route: RouteDefinition) -> Result<(), CamelError> {
249 let ctx = self.ctx.lock().await;
250 ctx.add_route_definition(route).await
251 }
252
253 pub async fn start(&self) {
255 let mut ctx = self.ctx.lock().await;
256 ctx.start().await.expect("CamelTestContext: start failed"); }
258
259 pub async fn stop(&self) {
262 if self.stopped.swap(true, Ordering::SeqCst) {
263 return; }
265 let mut ctx = self.ctx.lock().await;
266 ctx.stop().await.expect("CamelTestContext: stop failed"); }
268
269 pub async fn shutdown(self) {
271 self.stop().await;
272 }
273
274 pub fn mock(&self) -> &MockComponent {
276 &self.mock
277 }
278
279 pub fn ctx(&self) -> &Arc<Mutex<CamelContext>> {
281 &self.ctx
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use camel_builder::{RouteBuilder, StepAccumulator};
289 use std::time::Duration;
290
291 #[tokio::test]
292 async fn builder_without_time_control_builds_context() {
293 let harness = CamelTestContext::builder()
294 .with_mock()
295 .with_timer()
296 .with_log()
297 .build()
298 .await;
299
300 assert!(harness.mock().get_endpoint("result").is_none());
301 let guard = harness.ctx().lock().await;
302 let _ = &*guard;
303 }
304
305 #[tokio::test]
306 async fn builder_with_time_control_builds_and_advances_clock() {
307 let (_harness, time) = CamelTestContext::builder()
308 .with_mock()
309 .with_timer()
310 .with_time_control()
311 .build()
312 .await;
313
314 time.advance(Duration::from_millis(1)).await;
315 time.resume();
316 }
317
318 #[tokio::test]
319 async fn stop_is_idempotent_and_shutdown_is_safe() {
320 let harness = CamelTestContext::builder().with_mock().build().await;
321 harness.stop().await;
322 harness.stop().await;
323 harness.shutdown().await;
324 }
325
326 #[tokio::test]
327 async fn add_route_returns_error_for_invalid_step_uri() {
328 let harness = CamelTestContext::builder().with_mock().build().await;
329
330 let route = RouteBuilder::from("direct:start")
331 .route_id("bad-route")
332 .to("not-a-uri")
333 .build()
334 .unwrap();
335
336 let err = harness.add_route(route).await.expect_err("must fail");
337 assert!(err.to_string().contains("Invalid") || err.to_string().contains("invalid"));
338 }
339
340 #[tokio::test]
341 async fn with_component_registers_custom_component() {
342 let harness = CamelTestContext::builder()
343 .with_component(camel_component_direct::DirectComponent::new())
344 .with_mock()
345 .build()
346 .await;
347
348 let route = RouteBuilder::from("direct:start")
349 .route_id("direct-route")
350 .to("mock:out")
351 .build()
352 .unwrap();
353
354 harness.add_route(route).await.unwrap();
355 harness.start().await;
356 harness.stop().await;
357
358 let _guard = harness.ctx().lock().await;
360 }
361
362 #[tokio::test]
365 async fn tst004_route_lifecycle_start_stop_restart() {
366 let harness = CamelTestContext::builder()
367 .with_direct()
368 .with_mock()
369 .build()
370 .await;
371
372 let route = RouteBuilder::from("direct:lifecycle")
373 .route_id("lifecycle-route")
374 .to("mock:lifecycle-out")
375 .build()
376 .unwrap();
377
378 harness.add_route(route).await.unwrap();
379
380 harness.start().await;
382
383 harness.stop().await;
385
386 {
388 let mut ctx = harness.ctx().lock().await;
389 ctx.start().await.expect("restart should succeed");
390 ctx.stop().await.expect("stop after restart should succeed");
391 }
392 }
393
394 #[tokio::test]
397 async fn tst005_concurrent_exchange_processing() {
398 use camel_api::{BoxProcessor, BoxProcessorExt, Exchange, Message};
399 use camel_core::route::{CompiledStep, PipelineRuntimeCtx, compose_pipeline};
400 use std::sync::Arc;
401 use std::sync::atomic::{AtomicU32, Ordering};
402 use tower::ServiceExt;
403
404 let counter = Arc::new(AtomicU32::new(0));
405 let processor: BoxProcessor = {
406 let c = Arc::clone(&counter);
407 BoxProcessor::from_fn(move |ex: Exchange| {
408 let c = Arc::clone(&c);
409 Box::pin(async move {
410 c.fetch_add(1, Ordering::Relaxed);
411 tokio::task::yield_now().await;
412 Ok(ex)
413 })
414 })
415 };
416
417 let pipeline = compose_pipeline(
418 vec![CompiledStep::Process {
419 processor,
420 body_contract: None,
421 lifecycle: None,
422 }],
423 PipelineRuntimeCtx::compile_time(),
424 );
425
426 let concurrency: u32 = 10;
427 let mut handles = Vec::with_capacity(concurrency as usize);
428 for i in 0..concurrency {
429 let p = pipeline.clone();
430 handles.push(tokio::spawn(async move {
431 let ex = Exchange::new(Message::new(format!("msg-{i}")));
432 p.oneshot(ex).await.unwrap()
433 }));
434 }
435
436 for h in handles {
437 let _ = h.await.unwrap();
438 }
439
440 assert_eq!(counter.load(Ordering::Relaxed), concurrency);
441 }
442
443 #[tokio::test]
446 #[allow(deprecated)]
447 async fn tst006_error_handler_invoked_on_failure() {
448 use camel_api::error_handler::ExceptionPolicy;
449 use camel_api::{BoxProcessor, BoxProcessorExt, CamelError, Exchange, Message};
450 use camel_processor::ErrorHandlerService;
451 use std::sync::Arc;
452 use tower::ServiceExt;
453
454 let error_received = Arc::new(std::sync::Mutex::new(false));
455 let error_received_clone = Arc::clone(&error_received);
456
457 let handler: BoxProcessor = BoxProcessor::from_fn(move |ex: Exchange| {
459 let r = Arc::clone(&error_received_clone);
460 Box::pin(async move {
461 *r.lock().unwrap() = true;
462 Ok(ex)
463 })
464 });
465
466 let failing: BoxProcessor = BoxProcessor::from_fn(|_| {
468 Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
469 });
470
471 let policy = ExceptionPolicy::new(|_| true);
472 let svc = ErrorHandlerService::new(failing, Some(handler), vec![(policy, None)]);
473 let ex = Exchange::new(Message::new("test"));
474 let result = svc.oneshot(ex).await;
475
476 assert!(result.is_ok(), "error handler should absorb the error");
477 assert!(
478 result.unwrap().has_error(),
479 "exchange should have error set"
480 );
481 assert!(
482 *error_received.lock().unwrap(),
483 "error handler processor should have been invoked"
484 );
485 }
486
487 #[tokio::test]
490 #[allow(deprecated)]
491 async fn tst007_dead_letter_channel_receives_failed_exchange() {
492 use camel_api::{BoxProcessor, BoxProcessorExt, CamelError, Exchange, Message};
493 use camel_processor::ErrorHandlerService;
494 use std::sync::Arc;
495 use tower::ServiceExt;
496
497 let dlc_received = Arc::new(std::sync::Mutex::new(Vec::<Exchange>::new()));
498 let dlc_received_clone = Arc::clone(&dlc_received);
499
500 let dlc: BoxProcessor = BoxProcessor::from_fn(move |ex: Exchange| {
502 let r = Arc::clone(&dlc_received_clone);
503 Box::pin(async move {
504 r.lock().unwrap().push(ex.clone());
505 Ok(ex)
506 })
507 });
508
509 let failing: BoxProcessor = BoxProcessor::from_fn(|_| {
510 Box::pin(async { Err(CamelError::ProcessorError("fail".into())) })
511 });
512
513 let svc = ErrorHandlerService::new(failing, Some(dlc), vec![]);
514 let ex = Exchange::new(Message::new("dlc-test"));
515 let result = svc.oneshot(ex).await;
516
517 assert!(result.is_ok());
518 let exchanges = dlc_received.lock().unwrap();
519 assert_eq!(
520 exchanges.len(),
521 1,
522 "DLC should have received exactly one exchange"
523 );
524 assert!(exchanges[0].has_error());
525 }
526
527 #[tokio::test]
530 async fn tst008_header_propagation_across_processors() {
531 use camel_api::{Body, BoxProcessor, BoxProcessorExt, Exchange, Message, Value};
532 use camel_core::route::{CompiledStep, PipelineRuntimeCtx, compose_pipeline};
533 use tower::ServiceExt;
534
535 let step1: BoxProcessor = BoxProcessor::from_fn(|mut ex: Exchange| {
536 Box::pin(async move {
537 ex.input
538 .set_header("trace-id", Value::String("abc-123".into()));
539 Ok(ex)
540 })
541 });
542
543 let step2: BoxProcessor = BoxProcessor::from_fn(|mut ex: Exchange| {
544 Box::pin(async move {
545 ex.input.body = Body::Text("processed".to_string());
547 Ok(ex)
548 })
549 });
550
551 let pipeline = compose_pipeline(
552 vec![
553 CompiledStep::Process {
554 processor: step1,
555 body_contract: None,
556 lifecycle: None,
557 },
558 CompiledStep::Process {
559 processor: step2,
560 body_contract: None,
561 lifecycle: None,
562 },
563 ],
564 PipelineRuntimeCtx::compile_time(),
565 );
566 let ex = Exchange::new(Message::new("input"));
567 let result = pipeline.oneshot(ex).await.unwrap();
568
569 assert_eq!(
570 result.input.header("trace-id"),
571 Some(&Value::String("abc-123".into())),
572 "header should survive across processors"
573 );
574 assert_eq!(result.input.body.as_text(), Some("processed"));
575 }
576
577 #[tokio::test]
580 async fn tst009_exchange_body_type_conversion() {
581 use camel_api::body::Body;
582 use camel_api::body_converter::{BodyType, convert};
583
584 let text_body = Body::Text("hello".to_string());
586 let bytes_body = convert(text_body, BodyType::Bytes).unwrap();
587 assert!(matches!(bytes_body, Body::Bytes(_)));
588 if let Body::Bytes(ref b) = bytes_body {
589 assert_eq!(b.as_ref(), b"hello");
590 }
591
592 let text_body_back = convert(bytes_body, BodyType::Text).unwrap();
594 assert!(matches!(text_body_back, Body::Text(_)));
595 assert_eq!(text_body_back.as_text(), Some("hello"));
596 }
597
598 #[tokio::test]
601 async fn tst010_multicast_delivers_to_multiple_endpoints() {
602 use camel_api::multicast::{MulticastConfig, MulticastStrategy};
603 use camel_api::{BoxProcessor, BoxProcessorExt, Exchange, Message};
604 use camel_processor::MulticastService;
605 use std::sync::Arc;
606 use tower::ServiceExt;
607
608 let received_a = Arc::new(std::sync::Mutex::new(Vec::<Exchange>::new()));
609 let received_b = Arc::new(std::sync::Mutex::new(Vec::<Exchange>::new()));
610
611 let endpoint_a: BoxProcessor = {
612 let r = Arc::clone(&received_a);
613 BoxProcessor::from_fn(move |ex: Exchange| {
614 let r = Arc::clone(&r);
615 Box::pin(async move {
616 r.lock().unwrap().push(ex.clone());
617 Ok(ex)
618 })
619 })
620 };
621
622 let endpoint_b: BoxProcessor = {
623 let r = Arc::clone(&received_b);
624 BoxProcessor::from_fn(move |ex: Exchange| {
625 let r = Arc::clone(&r);
626 Box::pin(async move {
627 r.lock().unwrap().push(ex.clone());
628 Ok(ex)
629 })
630 })
631 };
632
633 let config = MulticastConfig::new().aggregation(MulticastStrategy::LastWins);
634
635 let svc = MulticastService::new(vec![endpoint_a, endpoint_b], config)
636 .expect("multicast service creation should succeed");
637 let ex = Exchange::new(Message::new("multicast-test"));
638 let _result = svc.oneshot(ex).await.unwrap();
639
640 assert_eq!(
641 received_a.lock().unwrap().len(),
642 1,
643 "endpoint A should receive exactly one exchange"
644 );
645 assert_eq!(
646 received_b.lock().unwrap().len(),
647 1,
648 "endpoint B should receive exactly one exchange"
649 );
650 }
651}