1use std::collections::VecDeque;
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::Arc;
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::task::{Context, Poll};
14
15use tokio::sync::{Mutex, Notify};
16use tower::Service;
17
18use camel_component_api::{BoxProcessor, CamelError, Exchange};
19use camel_component_api::{Consumer, Endpoint, ProducerContext, RuntimeObservability};
20use camel_matchers::CountBound;
21use tracing::debug;
22
23use crate::MockAssertionError;
24use crate::MockExpectations;
25use crate::matcher::{BodyMatcher, HeaderMatcher};
26
27pub struct MockEndpoint(pub(crate) Arc<MockEndpointInner>);
37
38pub struct MockEndpointInner {
44 pub(crate) uri: String,
45 pub(crate) name: String,
46 pub(crate) received: Arc<Mutex<VecDeque<Exchange>>>,
47 pub(crate) notify: Arc<Notify>,
48 pub(crate) max_retained: usize,
49 pub(crate) copy_on_exchange: bool,
50 pub(crate) fail_fast: bool,
51 pub(crate) fail_fast_error: Arc<std::sync::Mutex<Option<CamelError>>>,
52 pub(crate) assert_period_ms: u64,
53 pub(crate) any_order: bool,
54 pub(crate) expectations: Arc<std::sync::Mutex<MockExpectations>>,
55 pub(crate) arrival_counter: Arc<AtomicU64>,
59 pub(crate) arrival_indices: Arc<Mutex<Vec<u64>>>,
65}
66
67impl MockEndpointInner {
68 pub async fn get_received_exchanges(&self) -> Vec<Exchange> {
70 self.received.lock().await.iter().cloned().collect()
71 }
72
73 pub async fn get_arrival_indices(&self) -> Vec<u64> {
83 self.arrival_indices.lock().await.clone()
84 }
85
86 pub async fn received_count(&self) -> usize {
88 self.received.lock().await.len()
89 }
90
91 pub async fn reset(&self) {
97 self.received.lock().await.clear();
98 self.arrival_indices.lock().await.clear();
99 let mut guard = self
100 .fail_fast_error
101 .lock()
102 .expect("fail_fast_error lock poisoned"); *guard = None;
104 }
105
106 pub async fn assert_exchange_count(&self, expected: usize) {
112 let actual = self.received.lock().await.len();
113 assert_eq!(
114 actual, expected,
115 "MockEndpoint expected {expected} exchanges, got {actual}"
116 );
117 }
118
119 pub async fn await_exchanges(&self, count: usize, timeout: std::time::Duration) {
128 let deadline = tokio::time::Instant::now() + timeout;
129 loop {
130 {
131 let received = self.received.lock().await;
132 if received.len() >= count {
133 return;
134 }
135 }
136 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
137 if remaining.is_zero() {
138 let got = self.received.lock().await.len();
141 if got >= count {
142 return;
143 }
144 panic!(
145 "MockEndpoint '{}': timed out waiting for {} exchanges (got {} after {:?})",
146 self.name, count, got, timeout
147 );
148 }
149 tokio::select! {
150 _ = self.notify.notified() => {}
151 _ = tokio::time::sleep(remaining) => {}
152 }
153 }
154 }
155
156 pub async fn await_exchanges_with_timeout(&self, count: usize, fallback: std::time::Duration) {
161 let duration = if self.assert_period_ms > 0 {
162 std::time::Duration::from_millis(self.assert_period_ms)
163 } else {
164 fallback
165 };
166 self.await_exchanges(count, duration).await;
167 }
168
169 pub fn exchange(&self, idx: usize) -> ExchangeAssert {
182 if let Ok(handle) = tokio::runtime::Handle::try_current()
183 && handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::CurrentThread
184 {
185 panic!(
186 "MockEndpoint '{}': exchange(idx) cannot be used from a current-thread tokio runtime; use #[tokio::test(flavor = \"multi_thread\")] or the async accessors get_received_exchanges()/await_exchanges()",
187 self.name
188 );
189 }
190 let received = tokio::task::block_in_place(|| self.received.blocking_lock());
191 if idx >= received.len() {
192 panic!(
193 "MockEndpoint '{}': exchange index {} out of bounds (got {} exchanges)",
194 self.name,
195 idx,
196 received.len()
197 );
198 }
199 ExchangeAssert {
200 exchange: received[idx].clone(),
201 idx,
202 endpoint_name: self.name.clone(),
203 }
204 }
205
206 pub fn expect_count(&self, n: usize) {
212 self.expect_bound(CountBound::Exact(n as u64));
213 }
214
215 pub fn expect_minimum_count(&self, n: usize) {
221 self.expect_bound(CountBound::AtLeast(n as u64));
222 }
223
224 pub fn expect_maximum_count(&self, n: usize) {
230 self.expect_bound(CountBound::AtMost(n as u64));
231 }
232
233 pub fn expect_bound(&self, bound: CountBound) {
238 let mut guard = self
239 .expectations
240 .lock()
241 .expect("expectations lock poisoned"); if guard.count_bound.is_some() {
243 debug!(
244 endpoint_name = %self.name,
245 new = ?bound,
246 "a later setter replaced an earlier count bound"
247 );
248 }
249 guard.set_count_bound(bound);
250 }
251
252 pub fn expect_body(&self, body: camel_component_api::Body) {
254 let mut guard = self
255 .expectations
256 .lock()
257 .expect("expectations lock poisoned"); guard.push_body(body);
259 }
260
261 pub fn expect_body_matcher(&self, matcher: BodyMatcher) {
267 let mut guard = self
268 .expectations
269 .lock()
270 .expect("expectations lock poisoned"); guard.push_body_matcher(matcher);
272 }
273
274 pub fn expect_header(&self, key: &str, value: impl Into<serde_json::Value>) {
276 let mut guard = self
277 .expectations
278 .lock()
279 .expect("expectations lock poisoned"); guard.push_header(key.to_string(), value.into());
281 }
282
283 pub fn expect_header_regex(&self, key: &str, pattern: &str) {
288 let mut guard = self
289 .expectations
290 .lock()
291 .expect("expectations lock poisoned"); guard.push_header_regex(key.to_string(), pattern.to_string());
293 }
294
295 pub fn expect_header_matcher(&self, key: &str, matcher: HeaderMatcher) {
302 let mut guard = self
303 .expectations
304 .lock()
305 .expect("expectations lock poisoned"); guard.push_header_matcher(key.to_string(), matcher);
307 }
308
309 pub async fn assert_satisfied(&self) {
323 if let Err(e) = self.evaluate_expectations().await {
324 panic!("{e}");
325 }
326 }
327
328 #[allow(clippy::result_large_err)]
344 pub async fn try_assert_satisfied(&self) -> Result<(), MockAssertionError> {
345 self.evaluate_expectations().await
346 }
347
348 pub fn fail_fast_error(&self) -> Option<CamelError> {
350 let guard = self
351 .fail_fast_error
352 .lock()
353 .expect("fail_fast_error lock poisoned"); guard.clone()
355 }
356
357 pub fn trigger_fail_fast(&self, error: CamelError) {
366 let mut guard = self
367 .fail_fast_error
368 .lock()
369 .expect("fail_fast_error lock poisoned"); *guard = Some(error);
371 }
372
373 pub(crate) fn set_fail_fast_on_mismatch(&self) {
379 if self.fail_fast {
380 let mut guard = self
381 .fail_fast_error
382 .lock()
383 .expect("fail_fast_error lock poisoned"); *guard = Some(CamelError::ProcessorError(
385 "assert_satisfied expectation mismatch".to_string(),
386 ));
387 }
388 }
389}
390
391impl Endpoint for MockEndpoint {
392 fn uri(&self) -> &str {
393 &self.0.uri
394 }
395
396 fn create_consumer(
397 &self,
398 _rt: Arc<dyn RuntimeObservability>,
399 ) -> Result<Box<dyn Consumer>, CamelError> {
400 Err(CamelError::EndpointCreationFailed(
401 "mock endpoint does not support consumers (it is a sink)".to_string(),
402 ))
403 }
404
405 fn create_producer(
406 &self,
407 _rt: Arc<dyn RuntimeObservability>,
408 _ctx: &ProducerContext,
409 ) -> Result<BoxProcessor, CamelError> {
410 Ok(BoxProcessor::new(MockProducer {
411 name: self.0.name.clone(),
412 received: Arc::clone(&self.0.received),
413 notify: Arc::clone(&self.0.notify),
414 max_retained: self.0.max_retained,
415 copy_on_exchange: self.0.copy_on_exchange,
416 fail_fast: self.0.fail_fast,
417 fail_fast_error: Arc::clone(&self.0.fail_fast_error),
418 arrival_counter: Arc::clone(&self.0.arrival_counter),
419 arrival_indices: Arc::clone(&self.0.arrival_indices),
420 }))
421 }
422}
423
424#[derive(Clone)]
430struct MockProducer {
431 name: String,
432 received: Arc<Mutex<VecDeque<Exchange>>>,
433 notify: Arc<Notify>,
434 max_retained: usize,
435 copy_on_exchange: bool,
436 fail_fast: bool,
437 fail_fast_error: Arc<std::sync::Mutex<Option<CamelError>>>,
438 arrival_counter: Arc<AtomicU64>,
439 arrival_indices: Arc<Mutex<Vec<u64>>>,
440}
441
442impl Service<Exchange> for MockProducer {
443 type Response = Exchange;
444 type Error = CamelError;
445 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
446
447 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
448 if self.fail_fast
450 && let Ok(guard) = self.fail_fast_error.lock()
451 && guard.is_some()
452 {
453 return Poll::Ready(Err(CamelError::ProcessorError(
454 "mock endpoint in fail-fast mode: a previous exchange caused an error".to_string(),
455 )));
456 }
457 Poll::Ready(Ok(()))
458 }
459
460 fn call(&mut self, exchange: Exchange) -> Self::Future {
461 let name = self.name.clone();
462 let received = Arc::clone(&self.received);
463 let notify = Arc::clone(&self.notify);
464 let max_retained = self.max_retained;
465 let copy_on_exchange = self.copy_on_exchange;
466 let fail_fast = self.fail_fast;
467 let fail_fast_error = Arc::clone(&self.fail_fast_error);
468 let arrival_counter = Arc::clone(&self.arrival_counter);
469 let arrival_indices = Arc::clone(&self.arrival_indices);
470 Box::pin(async move {
471 if fail_fast
473 && let Ok(guard) = fail_fast_error.lock()
474 && guard.is_some()
475 {
476 return Err(CamelError::ProcessorError(
477 "mock endpoint in fail-fast mode: a previous exchange caused an error"
478 .to_string(),
479 ));
480 }
481
482 let correlation_id = exchange
483 .input
484 .headers
485 .get("CamelCorrelationId")
486 .and_then(|v| v.as_str())
487 .map(|s| s.to_string());
488
489 let exchange_to_store = if copy_on_exchange {
490 let mut cloned = exchange.clone();
491 cloned.input.body = clone_body(&exchange.input.body);
493 cloned
494 } else {
495 exchange.clone()
496 };
497
498 let mut guard = received.lock().await;
499 let arrival = arrival_counter.fetch_add(1, Ordering::Relaxed);
504 let mut indices = arrival_indices.lock().await;
505 if guard.len() >= max_retained {
506 tracing::warn!(
507 endpoint_name = %name,
508 max = max_retained,
509 "max retained exchanges reached, dropping oldest"
510 );
511 guard.pop_front();
512 indices.remove(0);
515 }
516 guard.push_back(exchange_to_store);
517 indices.push(arrival);
518 let count = guard.len();
519 drop(indices);
520 drop(guard);
521
522 debug!(
523 endpoint_name = %name,
524 count = %count,
525 correlation_id = correlation_id.as_deref().unwrap_or("none"),
526 "exchange recorded on mock"
527 );
528 notify.notify_waiters();
529
530 Ok(exchange)
531 })
532 }
533}
534
535pub(crate) fn clone_body(body: &camel_component_api::Body) -> camel_component_api::Body {
537 match body {
538 camel_component_api::Body::Empty => camel_component_api::Body::Empty,
539 camel_component_api::Body::Text(s) => camel_component_api::Body::Text(s.clone()),
540 camel_component_api::Body::Json(v) => camel_component_api::Body::Json(v.clone()),
541 camel_component_api::Body::Xml(s) => camel_component_api::Body::Xml(s.clone()),
542 camel_component_api::Body::Bytes(b) => camel_component_api::Body::Bytes(b.clone()),
543 camel_component_api::Body::Stream(s) => camel_component_api::Body::Stream(s.clone()),
544 _ => camel_component_api::Body::Empty,
547 }
548}
549
550pub struct ExchangeAssert {
562 exchange: Exchange,
563 idx: usize,
564 endpoint_name: String,
565}
566
567impl ExchangeAssert {
568 fn location(&self) -> String {
569 format!(
570 "MockEndpoint '{}' exchange[{}]",
571 self.endpoint_name, self.idx
572 )
573 }
574
575 pub fn assert_body_text(self, expected: &str) -> Self {
577 match self.exchange.input.body.as_text() {
578 Some(actual) if actual == expected => {}
579 Some(actual) => panic!(
580 "{}: expected body text {:?}, got {:?}",
581 self.location(),
582 expected,
583 actual
584 ),
585 None => panic!(
586 "{}: expected body text {:?}, but body is not Body::Text (got {:?})",
587 self.location(),
588 expected,
589 self.exchange.input.body
590 ),
591 }
592 self
593 }
594
595 pub fn assert_body_json(self, expected: serde_json::Value) -> Self {
597 match &self.exchange.input.body {
598 camel_component_api::Body::Json(actual) if *actual == expected => {}
599 camel_component_api::Body::Json(actual) => panic!(
600 "{}: expected body JSON {}, got {}",
601 self.location(),
602 expected,
603 actual
604 ),
605 other => panic!(
606 "{}: expected body JSON {}, but body is not Body::Json (got {:?})",
607 self.location(),
608 expected,
609 other
610 ),
611 }
612 self
613 }
614
615 pub fn assert_body_bytes(self, expected: &[u8]) -> Self {
617 match &self.exchange.input.body {
618 camel_component_api::Body::Bytes(actual) if actual.as_ref() == expected => {}
619 camel_component_api::Body::Bytes(actual) => panic!(
620 "{}: expected body bytes {:?}, got {:?}",
621 self.location(),
622 expected,
623 actual
624 ),
625 other => panic!(
626 "{}: expected body bytes {:?}, but body is not Body::Bytes (got {:?})",
627 self.location(),
628 expected,
629 other
630 ),
631 }
632 self
633 }
634
635 pub fn assert_header(self, key: &str, expected: serde_json::Value) -> Self {
641 match self.exchange.input.headers.get(key) {
642 Some(actual) if *actual == expected => {}
643 Some(actual) => panic!(
644 "{}: expected header {:?} = {}, got {}",
645 self.location(),
646 key,
647 expected,
648 actual
649 ),
650 None => panic!(
651 "{}: expected header {:?} = {}, but header is absent",
652 self.location(),
653 key,
654 expected
655 ),
656 }
657 self
658 }
659
660 pub fn assert_header_exists(self, key: &str) -> Self {
666 if !self.exchange.input.headers.contains_key(key) {
667 panic!(
668 "{}: expected header {:?} to be present, but it was absent",
669 self.location(),
670 key
671 );
672 }
673 self
674 }
675
676 pub fn assert_has_error(self) -> Self {
682 if self.exchange.error.is_none() {
683 panic!(
684 "{}: expected exchange to have an error, but error is None",
685 self.location()
686 );
687 }
688 self
689 }
690
691 pub fn assert_no_error(self) -> Self {
697 if let Some(ref err) = self.exchange.error {
698 panic!(
699 "{}: expected exchange to have no error, but got: {}",
700 self.location(),
701 err
702 );
703 }
704 self
705 }
706}
707
708#[cfg(test)]
713mod tests {
714 use camel_component_api::test_support::PanicRuntimeObservability;
715 use camel_component_api::{Exchange, Message, NoOpComponentContext, ProducerContext};
716 use tower::Service;
717
718 use crate::MockComponent;
719 use camel_component_api::Component;
720
721 fn rt() -> std::sync::Arc<dyn camel_component_api::RuntimeObservability> {
722 std::sync::Arc::new(PanicRuntimeObservability)
723 }
724
725 #[tokio::test]
726 async fn arrival_indices_strictly_increasing_across_endpoints() {
727 let ctx = ProducerContext::new();
728 let component = MockComponent::new();
729 let ep_a = component
730 .create_endpoint("mock:a", &NoOpComponentContext)
731 .unwrap();
732 let ep_b = component
733 .create_endpoint("mock:b", &NoOpComponentContext)
734 .unwrap();
735 let mut pa = ep_a.create_producer(rt(), &ctx).unwrap();
736 let mut pb = ep_b.create_producer(rt(), &ctx).unwrap();
737
738 pa.call(Exchange::new(Message::new("a0"))).await.unwrap();
739 pb.call(Exchange::new(Message::new("b0"))).await.unwrap();
740 pa.call(Exchange::new(Message::new("a1"))).await.unwrap();
741 pb.call(Exchange::new(Message::new("b1"))).await.unwrap();
742
743 let a = component
744 .get_endpoint("a")
745 .unwrap()
746 .get_arrival_indices()
747 .await;
748 let b = component
749 .get_endpoint("b")
750 .unwrap()
751 .get_arrival_indices()
752 .await;
753 assert_eq!(a, vec![0, 2]);
754 assert_eq!(b, vec![1, 3]);
755
756 let mut merged = a;
757 merged.extend(b);
758 merged.sort_unstable();
759 assert!(
760 merged.windows(2).all(|w| w[0] < w[1]),
761 "merged indices must be strictly increasing, got {merged:?}"
762 );
763 }
764
765 #[tokio::test]
766 async fn arrival_indices_truncate_in_lockstep_with_retention() {
767 let ctx = ProducerContext::new();
768 let component = MockComponent::new();
769 let endpoint = component
770 .create_endpoint("mock:x?retain=2", &NoOpComponentContext)
771 .unwrap();
772 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
773
774 for body in ["first", "second", "third"] {
775 producer
776 .call(Exchange::new(Message::new(body)))
777 .await
778 .unwrap();
779 }
780
781 let inner = component.get_endpoint("x").unwrap();
782 let indices = inner.get_arrival_indices().await;
783 assert_eq!(indices, vec![1, 2]);
785
786 let received = inner.get_received_exchanges().await;
787 assert_eq!(received.len(), indices.len());
788 assert_eq!(received[0].input.body.as_text(), Some("second"));
789 assert_eq!(received[1].input.body.as_text(), Some("third"));
790 }
791
792 #[tokio::test]
793 async fn reset_clears_indices_but_counter_stays_monotonic() {
794 let ctx = ProducerContext::new();
795 let component = MockComponent::new();
796 let endpoint = component
797 .create_endpoint("mock:r", &NoOpComponentContext)
798 .unwrap();
799 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
800
801 producer
802 .call(Exchange::new(Message::new("one")))
803 .await
804 .unwrap();
805 producer
806 .call(Exchange::new(Message::new("two")))
807 .await
808 .unwrap();
809
810 let inner = component.get_endpoint("r").unwrap();
811 assert_eq!(inner.get_arrival_indices().await, vec![0, 1]);
812
813 inner.reset().await;
814 assert!(inner.get_arrival_indices().await.is_empty());
815
816 producer
817 .call(Exchange::new(Message::new("three")))
818 .await
819 .unwrap();
820 assert_eq!(inner.get_arrival_indices().await, vec![2]);
822 }
823
824 #[tokio::test(flavor = "multi_thread")]
825 async fn concurrent_sends_preserve_per_endpoint_order() {
826 let ctx = ProducerContext::new();
827 let component = MockComponent::new();
828 let endpoint = component
829 .create_endpoint("mock:c", &NoOpComponentContext)
830 .unwrap();
831
832 let mut producers: Vec<_> = (0..32)
833 .map(|_| endpoint.create_producer(rt(), &ctx).unwrap())
834 .collect();
835 let sends = producers
836 .iter_mut()
837 .enumerate()
838 .map(|(i, p)| p.call(Exchange::new(Message::new(format!("m{i}")))));
839 let _ = futures::future::join_all(sends).await;
840
841 let inner = component.get_endpoint("c").unwrap();
842 let indices = inner.get_arrival_indices().await;
843 assert_eq!(indices.len(), 32);
844 assert!(
845 indices.windows(2).all(|w| w[0] < w[1]),
846 "per-endpoint indices must be strictly increasing, got {indices:?}"
847 );
848 }
849}