1use std::collections::{HashMap, VecDeque, hash_map::Entry};
38use std::future::Future;
39use std::pin::Pin;
40use std::sync::Arc;
41use std::task::{Context, Poll};
42
43use tokio::sync::{Mutex, Notify};
44use tower::Service;
45
46use camel_api::component_metadata::ComponentMetadata;
47use camel_component_api::UriConfig;
48use camel_component_api::parse_uri;
49use camel_component_api::{BoxProcessor, CamelError, Exchange};
50use camel_component_api::{Component, Consumer, Endpoint, ProducerContext, RuntimeObservability};
51use tracing::debug;
52
53const DEFAULT_MAX_RETAINED: usize = 10_000;
55
56#[derive(Clone, Debug)]
79pub struct MockConfig {
80 pub max_retained: usize,
83 pub copy_on_exchange: bool,
87 pub fail_fast: bool,
90 pub assert_period_ms: u64,
93 pub any_order: bool,
96}
97
98#[derive(Debug, Clone, UriConfig)]
120#[allow(dead_code)]
121#[uri_scheme = "mock"]
122#[uri_config(
123 skip_impl,
124 metadata(
125 scheme = "mock",
126 description = "Records exchanges for test assertions",
127 producer
128 ),
129 crate = "camel_component_api"
130)]
131struct MockUriConfig {
132 #[uri_param(name = "retain")]
133 pub _retain: Option<String>,
134
135 #[uri_param(name = "copy")]
136 pub _copy: Option<String>,
137
138 #[uri_param(name = "failFast")]
139 pub _fail_fast: Option<String>,
140
141 #[uri_param(name = "expectedCount")]
142 pub _expected_count: Option<String>,
143
144 #[uri_param(name = "anyOrder")]
145 pub _any_order: Option<String>,
146}
147
148impl Default for MockConfig {
149 fn default() -> Self {
150 Self {
151 max_retained: DEFAULT_MAX_RETAINED,
152 copy_on_exchange: false,
153 fail_fast: false,
154 assert_period_ms: 0,
155 any_order: false,
156 }
157 }
158}
159
160impl MockConfig {
161 pub fn new(max_retained: usize) -> Self {
163 Self {
164 max_retained,
165 ..Self::default()
166 }
167 }
168
169 pub fn metadata() -> ComponentMetadata {
172 MockUriConfig::metadata()
173 }
174}
175
176mod assert;
181mod expectations;
182
183pub use assert::MockAssertionError;
184pub use expectations::MockExpectations;
185
186#[derive(Clone)]
206pub struct MockComponent {
207 registry: Arc<std::sync::Mutex<HashMap<String, Arc<MockEndpointInner>>>>,
208 config: MockConfig,
209}
210
211impl MockComponent {
212 pub fn new() -> Self {
213 Self::with_config(MockConfig::default())
214 }
215
216 pub fn with_config(config: MockConfig) -> Self {
218 Self {
219 registry: Arc::new(std::sync::Mutex::new(HashMap::new())),
220 config,
221 }
222 }
223
224 pub fn get_endpoint(&self, name: &str) -> Option<Arc<MockEndpointInner>> {
228 let registry = self
229 .registry
230 .lock()
231 .expect("mutex poisoned: another thread panicked while holding this lock"); registry.get(name).cloned()
233 }
234}
235
236impl Default for MockComponent {
237 fn default() -> Self {
238 Self::new()
239 }
240}
241
242fn parse_usize_param(uri_value: &str, name: &str) -> Result<usize, CamelError> {
244 uri_value.parse::<usize>().map_err(|_| {
245 CamelError::EndpointCreationFailed(format!(
246 "mock: invalid value for URI parameter '{name}': '{uri_value}' is not a non-negative integer"
247 ))
248 })
249}
250
251fn parse_bool_param(uri_value: &str, name: &str) -> Result<bool, CamelError> {
254 match uri_value.to_ascii_lowercase().as_str() {
255 "true" => Ok(true),
256 "false" => Ok(false),
257 _ => Err(CamelError::EndpointCreationFailed(format!(
258 "mock: invalid value for URI parameter '{name}': '{uri_value}' is not a boolean (true|false)"
259 ))),
260 }
261}
262
263impl Component for MockComponent {
264 fn scheme(&self) -> &str {
265 "mock"
266 }
267
268 fn metadata(&self) -> ComponentMetadata {
269 MockConfig::metadata()
270 }
271
272 fn create_endpoint(
273 &self,
274 uri: &str,
275 _ctx: &dyn camel_component_api::ComponentContext,
276 ) -> Result<Box<dyn Endpoint>, CamelError> {
277 let parts = parse_uri(uri)?;
278 if parts.scheme != "mock" {
279 return Err(CamelError::InvalidUri(format!(
280 "expected scheme 'mock', got '{}'",
281 parts.scheme
282 )));
283 }
284
285 let name = parts.path;
286 if name.is_empty() {
287 return Err(CamelError::InvalidUri(
288 "mock endpoint name must be non-empty (use 'mock:<name>')".to_string(),
289 ));
290 }
291
292 let max_retained = match parts.params.get("retain") {
296 Some(v) => {
297 let n = parse_usize_param(v, "retain")?;
298 if n == 0 {
299 return Err(CamelError::EndpointCreationFailed(
300 "mock: URI parameter 'retain' must be >= 1, got 0".to_string(),
301 ));
302 }
303 n
304 }
305 None => self.config.max_retained,
306 };
307 let copy_on_exchange = match parts.params.get("copy") {
308 Some(v) => parse_bool_param(v, "copy")?,
309 None => self.config.copy_on_exchange,
310 };
311 let fail_fast = match parts.params.get("failFast") {
312 Some(v) => parse_bool_param(v, "failFast")?,
313 None => self.config.fail_fast,
314 };
315 let any_order = match parts.params.get("anyOrder") {
316 Some(v) => parse_bool_param(v, "anyOrder")?,
317 None => self.config.any_order,
318 };
319 let expected_count = match parts.params.get("expectedCount") {
322 Some(v) => Some(parse_usize_param(v, "expectedCount")?),
323 None => None,
324 };
325
326 let mut registry = self.registry.lock().map_err(|e| {
327 CamelError::EndpointCreationFailed(format!("mock registry lock poisoned: {e}"))
328 })?;
329 let assert_period_ms = self.config.assert_period_ms;
330 let (inner, fresh) = match registry.entry(name.clone()) {
335 Entry::Vacant(vacant) => {
336 let created = vacant.insert(Arc::new(MockEndpointInner {
337 uri: uri.to_string(),
338 name,
339 received: Arc::new(Mutex::new(VecDeque::new())),
340 notify: Arc::new(Notify::new()),
341 max_retained,
342 copy_on_exchange,
343 fail_fast,
344 fail_fast_error: Arc::new(std::sync::Mutex::new(None)),
345 assert_period_ms,
346 any_order,
347 expectations: Arc::new(std::sync::Mutex::new(MockExpectations::new())),
348 }));
349 (Arc::clone(created), true)
350 }
351 Entry::Occupied(occupied) => (Arc::clone(occupied.get()), false),
352 };
353
354 if fresh && let Some(n) = expected_count {
359 inner.expect_count(n);
360 }
361
362 debug!(endpoint_name = %inner.name, "mock endpoint created");
363 Ok(Box::new(MockEndpoint(inner)))
364 }
365}
366
367pub struct MockEndpoint(Arc<MockEndpointInner>);
377
378pub struct MockEndpointInner {
384 uri: String,
385 pub(crate) name: String,
386 received: Arc<Mutex<VecDeque<Exchange>>>,
387 notify: Arc<Notify>,
388 max_retained: usize,
389 copy_on_exchange: bool,
390 fail_fast: bool,
391 fail_fast_error: Arc<std::sync::Mutex<Option<CamelError>>>,
392 assert_period_ms: u64,
393 pub(crate) any_order: bool,
394 pub(crate) expectations: Arc<std::sync::Mutex<MockExpectations>>,
395}
396
397impl MockEndpointInner {
398 pub async fn get_received_exchanges(&self) -> Vec<Exchange> {
400 self.received.lock().await.iter().cloned().collect()
401 }
402
403 pub async fn received_count(&self) -> usize {
405 self.received.lock().await.len()
406 }
407
408 pub async fn reset(&self) {
412 self.received.lock().await.clear();
413 if let Ok(mut guard) = self.fail_fast_error.lock() {
414 *guard = None;
415 }
416 }
417
418 pub async fn assert_exchange_count(&self, expected: usize) {
424 let actual = self.received.lock().await.len();
425 assert_eq!(
426 actual, expected,
427 "MockEndpoint expected {expected} exchanges, got {actual}"
428 );
429 }
430
431 pub async fn await_exchanges(&self, count: usize, timeout: std::time::Duration) {
440 let deadline = tokio::time::Instant::now() + timeout;
441 loop {
442 {
443 let received = self.received.lock().await;
444 if received.len() >= count {
445 return;
446 }
447 }
448 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
449 if remaining.is_zero() {
450 let got = self.received.lock().await.len();
453 if got >= count {
454 return;
455 }
456 panic!(
457 "MockEndpoint '{}': timed out waiting for {} exchanges (got {} after {:?})",
458 self.name, count, got, timeout
459 );
460 }
461 tokio::select! {
462 _ = self.notify.notified() => {}
463 _ = tokio::time::sleep(remaining) => {}
464 }
465 }
466 }
467
468 pub async fn await_exchanges_with_timeout(&self, count: usize, fallback: std::time::Duration) {
473 let duration = if self.assert_period_ms > 0 {
474 std::time::Duration::from_millis(self.assert_period_ms)
475 } else {
476 fallback
477 };
478 self.await_exchanges(count, duration).await;
479 }
480
481 pub fn exchange(&self, idx: usize) -> ExchangeAssert {
494 if let Ok(handle) = tokio::runtime::Handle::try_current()
495 && handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::CurrentThread
496 {
497 panic!(
498 "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()",
499 self.name
500 );
501 }
502 let received = tokio::task::block_in_place(|| self.received.blocking_lock());
503 if idx >= received.len() {
504 panic!(
505 "MockEndpoint '{}': exchange index {} out of bounds (got {} exchanges)",
506 self.name,
507 idx,
508 received.len()
509 );
510 }
511 ExchangeAssert {
512 exchange: received[idx].clone(),
513 idx,
514 endpoint_name: self.name.clone(),
515 }
516 }
517
518 pub fn expect_count(&self, n: usize) {
521 if let Ok(mut guard) = self.expectations.lock() {
522 guard.set_expected_count(n);
523 }
524 }
525
526 pub fn expect_minimum_count(&self, n: usize) {
529 if let Ok(mut guard) = self.expectations.lock() {
530 guard.set_minimum_count(n);
531 }
532 }
533
534 pub fn expect_body(&self, body: camel_component_api::Body) {
536 if let Ok(mut guard) = self.expectations.lock() {
537 guard.push_body(body);
538 }
539 }
540
541 pub fn expect_header(&self, key: &str, value: impl Into<serde_json::Value>) {
543 if let Ok(mut guard) = self.expectations.lock() {
544 guard.push_header(key.to_string(), value.into());
545 }
546 }
547
548 pub fn expect_header_regex(&self, key: &str, pattern: &str) {
553 if let Ok(mut guard) = self.expectations.lock() {
554 guard.push_header_regex(key.to_string(), pattern.to_string());
555 }
556 }
557
558 pub async fn assert_satisfied(&self) {
569 if let Err(e) = self.evaluate_expectations().await {
570 panic!("{e}");
571 }
572 }
573
574 pub async fn try_assert_satisfied(&self) -> Result<(), MockAssertionError> {
588 self.evaluate_expectations().await
589 }
590
591 pub fn fail_fast_error(&self) -> Option<CamelError> {
593 self.fail_fast_error.lock().ok().and_then(|g| g.clone())
594 }
595
596 pub fn trigger_fail_fast(&self, error: CamelError) {
605 if let Ok(mut guard) = self.fail_fast_error.lock() {
606 *guard = Some(error);
607 }
608 }
609
610 pub(crate) fn set_fail_fast_on_mismatch(&self) {
616 if self.fail_fast
617 && let Ok(mut guard) = self.fail_fast_error.lock()
618 {
619 *guard = Some(CamelError::ProcessorError(
620 "assert_satisfied expectation mismatch".to_string(),
621 ));
622 }
623 }
624}
625
626impl Endpoint for MockEndpoint {
627 fn uri(&self) -> &str {
628 &self.0.uri
629 }
630
631 fn create_consumer(
632 &self,
633 _rt: Arc<dyn RuntimeObservability>,
634 ) -> Result<Box<dyn Consumer>, CamelError> {
635 Err(CamelError::EndpointCreationFailed(
636 "mock endpoint does not support consumers (it is a sink)".to_string(),
637 ))
638 }
639
640 fn create_producer(
641 &self,
642 _rt: Arc<dyn RuntimeObservability>,
643 _ctx: &ProducerContext,
644 ) -> Result<BoxProcessor, CamelError> {
645 Ok(BoxProcessor::new(MockProducer {
646 name: self.0.name.clone(),
647 received: Arc::clone(&self.0.received),
648 notify: Arc::clone(&self.0.notify),
649 max_retained: self.0.max_retained,
650 copy_on_exchange: self.0.copy_on_exchange,
651 fail_fast: self.0.fail_fast,
652 fail_fast_error: Arc::clone(&self.0.fail_fast_error),
653 }))
654 }
655}
656
657#[derive(Clone)]
663struct MockProducer {
664 name: String,
665 received: Arc<Mutex<VecDeque<Exchange>>>,
666 notify: Arc<Notify>,
667 max_retained: usize,
668 copy_on_exchange: bool,
669 fail_fast: bool,
670 fail_fast_error: Arc<std::sync::Mutex<Option<CamelError>>>,
671}
672
673impl Service<Exchange> for MockProducer {
674 type Response = Exchange;
675 type Error = CamelError;
676 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
677
678 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
679 if self.fail_fast
681 && let Ok(guard) = self.fail_fast_error.lock()
682 && guard.is_some()
683 {
684 return Poll::Ready(Err(CamelError::ProcessorError(
685 "mock endpoint in fail-fast mode: a previous exchange caused an error".to_string(),
686 )));
687 }
688 Poll::Ready(Ok(()))
689 }
690
691 fn call(&mut self, exchange: Exchange) -> Self::Future {
692 let name = self.name.clone();
693 let received = Arc::clone(&self.received);
694 let notify = Arc::clone(&self.notify);
695 let max_retained = self.max_retained;
696 let copy_on_exchange = self.copy_on_exchange;
697 let fail_fast = self.fail_fast;
698 let fail_fast_error = Arc::clone(&self.fail_fast_error);
699 Box::pin(async move {
700 if fail_fast
702 && let Ok(guard) = fail_fast_error.lock()
703 && guard.is_some()
704 {
705 return Err(CamelError::ProcessorError(
706 "mock endpoint in fail-fast mode: a previous exchange caused an error"
707 .to_string(),
708 ));
709 }
710
711 let correlation_id = exchange
712 .input
713 .headers
714 .get("CamelCorrelationId")
715 .and_then(|v| v.as_str())
716 .map(|s| s.to_string());
717
718 let exchange_to_store = if copy_on_exchange {
719 let mut cloned = exchange.clone();
720 cloned.input.body = clone_body(&exchange.input.body);
722 cloned
723 } else {
724 exchange.clone()
725 };
726
727 let mut guard = received.lock().await;
728 if guard.len() >= max_retained {
729 tracing::warn!(
730 endpoint_name = %name,
731 max = max_retained,
732 "max retained exchanges reached, dropping oldest"
733 );
734 guard.pop_front();
735 }
736 guard.push_back(exchange_to_store);
737 let count = guard.len();
738 drop(guard);
739
740 debug!(
741 endpoint_name = %name,
742 count = %count,
743 correlation_id = correlation_id.as_deref().unwrap_or("none"),
744 "exchange recorded on mock"
745 );
746 notify.notify_waiters();
747
748 Ok(exchange)
749 })
750 }
751}
752
753fn clone_body(body: &camel_component_api::Body) -> camel_component_api::Body {
755 match body {
756 camel_component_api::Body::Empty => camel_component_api::Body::Empty,
757 camel_component_api::Body::Text(s) => camel_component_api::Body::Text(s.clone()),
758 camel_component_api::Body::Json(v) => camel_component_api::Body::Json(v.clone()),
759 camel_component_api::Body::Xml(s) => camel_component_api::Body::Xml(s.clone()),
760 camel_component_api::Body::Bytes(b) => camel_component_api::Body::Bytes(b.clone()),
761 camel_component_api::Body::Stream(s) => camel_component_api::Body::Stream(s.clone()),
762 _ => camel_component_api::Body::Empty,
765 }
766}
767
768pub struct ExchangeAssert {
780 exchange: Exchange,
781 idx: usize,
782 endpoint_name: String,
783}
784
785impl ExchangeAssert {
786 fn location(&self) -> String {
787 format!(
788 "MockEndpoint '{}' exchange[{}]",
789 self.endpoint_name, self.idx
790 )
791 }
792
793 pub fn assert_body_text(self, expected: &str) -> Self {
795 match self.exchange.input.body.as_text() {
796 Some(actual) if actual == expected => {}
797 Some(actual) => panic!(
798 "{}: expected body text {:?}, got {:?}",
799 self.location(),
800 expected,
801 actual
802 ),
803 None => panic!(
804 "{}: expected body text {:?}, but body is not Body::Text (got {:?})",
805 self.location(),
806 expected,
807 self.exchange.input.body
808 ),
809 }
810 self
811 }
812
813 pub fn assert_body_json(self, expected: serde_json::Value) -> Self {
815 match &self.exchange.input.body {
816 camel_component_api::Body::Json(actual) if *actual == expected => {}
817 camel_component_api::Body::Json(actual) => panic!(
818 "{}: expected body JSON {}, got {}",
819 self.location(),
820 expected,
821 actual
822 ),
823 other => panic!(
824 "{}: expected body JSON {}, but body is not Body::Json (got {:?})",
825 self.location(),
826 expected,
827 other
828 ),
829 }
830 self
831 }
832
833 pub fn assert_body_bytes(self, expected: &[u8]) -> Self {
835 match &self.exchange.input.body {
836 camel_component_api::Body::Bytes(actual) if actual.as_ref() == expected => {}
837 camel_component_api::Body::Bytes(actual) => panic!(
838 "{}: expected body bytes {:?}, got {:?}",
839 self.location(),
840 expected,
841 actual
842 ),
843 other => panic!(
844 "{}: expected body bytes {:?}, but body is not Body::Bytes (got {:?})",
845 self.location(),
846 expected,
847 other
848 ),
849 }
850 self
851 }
852
853 pub fn assert_header(self, key: &str, expected: serde_json::Value) -> Self {
859 match self.exchange.input.headers.get(key) {
860 Some(actual) if *actual == expected => {}
861 Some(actual) => panic!(
862 "{}: expected header {:?} = {}, got {}",
863 self.location(),
864 key,
865 expected,
866 actual
867 ),
868 None => panic!(
869 "{}: expected header {:?} = {}, but header is absent",
870 self.location(),
871 key,
872 expected
873 ),
874 }
875 self
876 }
877
878 pub fn assert_header_exists(self, key: &str) -> Self {
884 if !self.exchange.input.headers.contains_key(key) {
885 panic!(
886 "{}: expected header {:?} to be present, but it was absent",
887 self.location(),
888 key
889 );
890 }
891 self
892 }
893
894 pub fn assert_has_error(self) -> Self {
900 if self.exchange.error.is_none() {
901 panic!(
902 "{}: expected exchange to have an error, but error is None",
903 self.location()
904 );
905 }
906 self
907 }
908
909 pub fn assert_no_error(self) -> Self {
915 if let Some(ref err) = self.exchange.error {
916 panic!(
917 "{}: expected exchange to have no error, but got: {}",
918 self.location(),
919 err
920 );
921 }
922 self
923 }
924}
925
926#[cfg(test)]
931mod tests {
932 use camel_component_api::test_support::PanicRuntimeObservability;
933 fn rt() -> std::sync::Arc<dyn camel_component_api::RuntimeObservability> {
934 std::sync::Arc::new(PanicRuntimeObservability)
935 }
936
937 use super::*;
938 use camel_component_api::Message;
939 use camel_component_api::NoOpComponentContext;
940 use tower::ServiceExt;
941
942 fn test_producer_ctx() -> ProducerContext {
943 ProducerContext::new()
944 }
945
946 #[test]
947 fn test_mock_component_scheme() {
948 let component = MockComponent::new();
949 assert_eq!(component.scheme(), "mock");
950 }
951
952 #[test]
953 fn test_mock_component_default() {
954 let component = MockComponent::default();
955 assert_eq!(component.scheme(), "mock");
956 assert!(component.get_endpoint("missing").is_none());
957 }
958
959 #[test]
960 fn test_mock_creates_endpoint() {
961 let component = MockComponent::new();
962 let endpoint = component.create_endpoint("mock:result", &NoOpComponentContext);
963 assert!(endpoint.is_ok());
964 }
965
966 #[test]
967 fn test_mock_wrong_scheme() {
968 let component = MockComponent::new();
969 let result = component.create_endpoint("timer:tick", &NoOpComponentContext);
970 assert!(result.is_err());
971 }
972
973 #[test]
974 fn test_empty_mock_endpoint_name_rejected() {
975 let component = MockComponent::new();
976 let result = component.create_endpoint("mock:", &NoOpComponentContext);
977 assert!(result.is_err(), "empty mock name should be rejected");
978 }
979
980 #[test]
981 fn test_valid_mock_endpoint_name_accepted() {
982 let component = MockComponent::new();
983 let result = component.create_endpoint("mock:result", &NoOpComponentContext);
984 assert!(result.is_ok());
985 }
986
987 #[test]
988 fn test_mock_endpoint_no_consumer() {
989 let component = MockComponent::new();
990 let endpoint = component
991 .create_endpoint("mock:result", &NoOpComponentContext)
992 .unwrap();
993 assert!(endpoint.create_consumer(rt()).is_err());
994 }
995
996 #[test]
997 fn test_mock_endpoint_creates_producer() {
998 let ctx = test_producer_ctx();
999 let component = MockComponent::new();
1000 let endpoint = component
1001 .create_endpoint("mock:result", &NoOpComponentContext)
1002 .unwrap();
1003 assert!(endpoint.create_producer(rt(), &ctx).is_ok());
1004 }
1005
1006 #[test]
1007 fn test_mock_endpoint_uri() {
1008 let component = MockComponent::new();
1009 let endpoint = component
1010 .create_endpoint("mock:uri-check", &NoOpComponentContext)
1011 .unwrap();
1012 assert_eq!(endpoint.uri(), "mock:uri-check");
1013 }
1014
1015 #[test]
1016 fn test_mock_get_endpoint_returns_same_inner_for_same_name() {
1017 let component = MockComponent::new();
1018 let _ = component
1019 .create_endpoint("mock:shared-inner", &NoOpComponentContext)
1020 .unwrap();
1021 let _ = component
1022 .create_endpoint("mock:shared-inner", &NoOpComponentContext)
1023 .unwrap();
1024
1025 let first = component.get_endpoint("shared-inner").unwrap();
1026 let second = component.get_endpoint("shared-inner").unwrap();
1027 assert!(Arc::ptr_eq(&first, &second));
1028 }
1029
1030 #[tokio::test]
1031 async fn test_mock_producer_records_exchange() {
1032 let ctx = test_producer_ctx();
1033 let component = MockComponent::new();
1034 let endpoint = component
1035 .create_endpoint("mock:test", &NoOpComponentContext)
1036 .unwrap();
1037
1038 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1039
1040 let ex1 = Exchange::new(Message::new("first"));
1041 let ex2 = Exchange::new(Message::new("second"));
1042
1043 producer.call(ex1).await.unwrap();
1044 producer.call(ex2).await.unwrap();
1045
1046 let inner = component.get_endpoint("test").unwrap();
1047 inner.assert_exchange_count(2).await;
1048
1049 let received = inner.get_received_exchanges().await;
1050 assert_eq!(received[0].input.body.as_text(), Some("first"));
1051 assert_eq!(received[1].input.body.as_text(), Some("second"));
1052 }
1053
1054 #[tokio::test]
1055 async fn test_mock_producer_passes_through_exchange() {
1056 let ctx = test_producer_ctx();
1057 let component = MockComponent::new();
1058 let endpoint = component
1059 .create_endpoint("mock:passthrough", &NoOpComponentContext)
1060 .unwrap();
1061
1062 let producer = endpoint.create_producer(rt(), &ctx).unwrap();
1063 let exchange = Exchange::new(Message::new("hello"));
1064 let result = producer.oneshot(exchange).await.unwrap();
1065
1066 assert_eq!(result.input.body.as_text(), Some("hello"));
1068 }
1069
1070 #[tokio::test]
1071 async fn test_mock_assert_count_passes() {
1072 let component = MockComponent::new();
1073 let endpoint = component
1074 .create_endpoint("mock:count", &NoOpComponentContext)
1075 .unwrap();
1076 let inner = component.get_endpoint("count").unwrap();
1077
1078 inner.assert_exchange_count(0).await;
1079
1080 let ctx = test_producer_ctx();
1081 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1082 producer
1083 .call(Exchange::new(Message::new("one")))
1084 .await
1085 .unwrap();
1086
1087 inner.assert_exchange_count(1).await;
1088 }
1089
1090 #[tokio::test]
1091 #[should_panic(expected = "MockEndpoint expected 5 exchanges, got 0")]
1092 async fn test_mock_assert_count_fails() {
1093 let component = MockComponent::new();
1094 let _endpoint = component
1097 .create_endpoint("mock:fail", &NoOpComponentContext)
1098 .unwrap();
1099 let inner = component.get_endpoint("fail").unwrap();
1100
1101 inner.assert_exchange_count(5).await;
1102 }
1103
1104 #[tokio::test]
1105 async fn test_mock_component_shared_registry() {
1106 let component = MockComponent::new();
1107 let ep1 = component
1108 .create_endpoint("mock:shared", &NoOpComponentContext)
1109 .unwrap();
1110 let ep2 = component
1111 .create_endpoint("mock:shared", &NoOpComponentContext)
1112 .unwrap();
1113
1114 let ctx = test_producer_ctx();
1116 let mut p1 = ep1.create_producer(rt(), &ctx).unwrap();
1117 p1.call(Exchange::new(Message::new("from-ep1")))
1118 .await
1119 .unwrap();
1120
1121 let mut p2 = ep2.create_producer(rt(), &ctx).unwrap();
1123 p2.call(Exchange::new(Message::new("from-ep2")))
1124 .await
1125 .unwrap();
1126
1127 let inner = component.get_endpoint("shared").unwrap();
1129 inner.assert_exchange_count(2).await;
1130
1131 let received = inner.get_received_exchanges().await;
1132 assert_eq!(received[0].input.body.as_text(), Some("from-ep1"));
1133 assert_eq!(received[1].input.body.as_text(), Some("from-ep2"));
1134 }
1135
1136 #[tokio::test]
1137 async fn await_exchanges_resolves_immediately() {
1138 let ctx = test_producer_ctx();
1140 let component = MockComponent::new();
1141 let endpoint = component
1142 .create_endpoint("mock:immediate", &NoOpComponentContext)
1143 .unwrap();
1144 let inner = component.get_endpoint("immediate").unwrap();
1145
1146 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1147 producer
1148 .call(Exchange::new(Message::new("a")))
1149 .await
1150 .unwrap();
1151 producer
1152 .call(Exchange::new(Message::new("b")))
1153 .await
1154 .unwrap();
1155
1156 inner
1158 .await_exchanges(2, std::time::Duration::from_millis(100))
1159 .await;
1160 }
1161
1162 #[tokio::test]
1163 async fn await_exchanges_waits_then_resolves() {
1164 let ctx = test_producer_ctx();
1166 let component = MockComponent::new();
1167 let endpoint = component
1168 .create_endpoint("mock:waiter", &NoOpComponentContext)
1169 .unwrap();
1170 let inner = component.get_endpoint("waiter").unwrap();
1171
1172 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1174 tokio::spawn(async move {
1175 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1176 producer
1177 .call(Exchange::new(Message::new("delayed")))
1178 .await
1179 .unwrap();
1180 });
1181
1182 inner
1184 .await_exchanges(1, std::time::Duration::from_millis(500))
1185 .await;
1186
1187 let received = inner.get_received_exchanges().await;
1188 assert_eq!(received.len(), 1);
1189 assert_eq!(received[0].input.body.as_text(), Some("delayed"));
1190 }
1191
1192 #[tokio::test]
1193 #[should_panic(expected = "timed out waiting for 5 exchanges")]
1194 async fn await_exchanges_times_out() {
1195 let component = MockComponent::new();
1196 let _endpoint = component
1197 .create_endpoint("mock:timeout", &NoOpComponentContext)
1198 .unwrap();
1199 let inner = component.get_endpoint("timeout").unwrap();
1200
1201 inner
1203 .await_exchanges(5, std::time::Duration::from_millis(50))
1204 .await;
1205 }
1206
1207 #[tokio::test(flavor = "multi_thread")]
1208 async fn exchange_idx_returns_assert() {
1209 let ctx = test_producer_ctx();
1210 let component = MockComponent::new();
1211 let endpoint = component
1212 .create_endpoint("mock:assert-idx", &NoOpComponentContext)
1213 .unwrap();
1214 let inner = component.get_endpoint("assert-idx").unwrap();
1215
1216 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1217 producer
1218 .call(Exchange::new(Message::new("hello")))
1219 .await
1220 .unwrap();
1221
1222 inner
1223 .await_exchanges(1, std::time::Duration::from_millis(500))
1224 .await;
1225 let _assert = inner.exchange(0);
1227 }
1228
1229 #[tokio::test]
1230 async fn exchange_current_thread_clear_panic() {
1231 let ctx = test_producer_ctx();
1232 let component = MockComponent::new();
1233 let endpoint = component
1234 .create_endpoint("mock:current-thread", &NoOpComponentContext)
1235 .unwrap();
1236 let inner = component.get_endpoint("current-thread").unwrap();
1237 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1238 producer
1239 .call(Exchange::new(Message::new("recorded")))
1240 .await
1241 .unwrap();
1242
1243 let panic =
1244 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| inner.exchange(0))) {
1245 Ok(_) => panic!("exchange must panic on a current-thread runtime"),
1246 Err(payload) => payload,
1247 };
1248 let message = panic
1249 .downcast_ref::<String>()
1250 .map(String::as_str)
1251 .or_else(|| panic.downcast_ref::<&str>().copied())
1252 .expect("panic payload should be a string");
1253 assert!(message.contains("current-thread"), "panic: {message}");
1254 assert!(message.contains("multi_thread"), "panic: {message}");
1255 }
1256
1257 #[tokio::test(flavor = "multi_thread")]
1258 async fn exchange_multi_thread_unchanged() {
1259 let ctx = test_producer_ctx();
1260 let component = MockComponent::new();
1261 let endpoint = component
1262 .create_endpoint("mock:multi-thread", &NoOpComponentContext)
1263 .unwrap();
1264 let inner = component.get_endpoint("multi-thread").unwrap();
1265 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1266 producer
1267 .call(Exchange::new(Message::new("first")))
1268 .await
1269 .unwrap();
1270 producer
1271 .call(Exchange::new(Message::new("second")))
1272 .await
1273 .unwrap();
1274
1275 let _assert = inner.exchange(1);
1276 }
1277
1278 #[test]
1279 fn exchange_no_runtime_returns_assert() {
1280 let component = MockComponent::new();
1281 let endpoint = component
1282 .create_endpoint("mock:no-runtime", &NoOpComponentContext)
1283 .unwrap();
1284 let inner = component.get_endpoint("no-runtime").unwrap();
1285 let ctx = test_producer_ctx();
1286 let runtime = tokio::runtime::Runtime::new().unwrap();
1287 runtime.block_on(async {
1288 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1289 producer
1290 .call(Exchange::new(Message::new("recorded")))
1291 .await
1292 .unwrap();
1293 });
1294 drop(runtime);
1295
1296 let _assert = inner.exchange(0);
1297 }
1298
1299 #[tokio::test(flavor = "multi_thread")]
1300 #[should_panic(expected = "exchange index 5 out of bounds")]
1301 async fn exchange_idx_out_of_bounds() {
1302 let ctx = test_producer_ctx();
1303 let component = MockComponent::new();
1304 let endpoint = component
1305 .create_endpoint("mock:oob", &NoOpComponentContext)
1306 .unwrap();
1307 let inner = component.get_endpoint("oob").unwrap();
1308
1309 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1310 producer
1311 .call(Exchange::new(Message::new("only-one")))
1312 .await
1313 .unwrap();
1314
1315 inner
1316 .await_exchanges(1, std::time::Duration::from_millis(500))
1317 .await;
1318 let _assert = inner.exchange(5);
1320 }
1321
1322 #[tokio::test(flavor = "multi_thread")]
1323 async fn assert_body_text_pass() {
1324 let ctx = test_producer_ctx();
1325 let component = MockComponent::new();
1326 let endpoint = component
1327 .create_endpoint("mock:body-text-pass", &NoOpComponentContext)
1328 .unwrap();
1329 let inner = component.get_endpoint("body-text-pass").unwrap();
1330 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1331 producer
1332 .call(Exchange::new(Message::new("hello")))
1333 .await
1334 .unwrap();
1335 inner
1336 .await_exchanges(1, std::time::Duration::from_millis(500))
1337 .await;
1338 inner.exchange(0).assert_body_text("hello");
1339 }
1340
1341 #[tokio::test(flavor = "multi_thread")]
1342 #[should_panic(expected = "expected body text")]
1343 async fn assert_body_text_fail() {
1344 let ctx = test_producer_ctx();
1345 let component = MockComponent::new();
1346 let endpoint = component
1347 .create_endpoint("mock:body-text-fail", &NoOpComponentContext)
1348 .unwrap();
1349 let inner = component.get_endpoint("body-text-fail").unwrap();
1350 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1351 producer
1352 .call(Exchange::new(Message::new("hello")))
1353 .await
1354 .unwrap();
1355 inner
1356 .await_exchanges(1, std::time::Duration::from_millis(500))
1357 .await;
1358 inner.exchange(0).assert_body_text("world");
1359 }
1360
1361 #[tokio::test(flavor = "multi_thread")]
1362 async fn assert_body_json_pass() {
1363 use camel_component_api::Body;
1364 let ctx = test_producer_ctx();
1365 let component = MockComponent::new();
1366 let endpoint = component
1367 .create_endpoint("mock:body-json-pass", &NoOpComponentContext)
1368 .unwrap();
1369 let inner = component.get_endpoint("body-json-pass").unwrap();
1370 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1371 let mut msg = Message::new("");
1372 msg.body = Body::Json(serde_json::json!({"key": "value"}));
1373 producer.call(Exchange::new(msg)).await.unwrap();
1374 inner
1375 .await_exchanges(1, std::time::Duration::from_millis(500))
1376 .await;
1377 inner
1378 .exchange(0)
1379 .assert_body_json(serde_json::json!({"key": "value"}));
1380 }
1381
1382 #[tokio::test(flavor = "multi_thread")]
1383 #[should_panic(expected = "expected body JSON")]
1384 async fn assert_body_json_fail() {
1385 use camel_component_api::Body;
1386 let ctx = test_producer_ctx();
1387 let component = MockComponent::new();
1388 let endpoint = component
1389 .create_endpoint("mock:body-json-fail", &NoOpComponentContext)
1390 .unwrap();
1391 let inner = component.get_endpoint("body-json-fail").unwrap();
1392 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1393 let mut msg = Message::new("");
1394 msg.body = Body::Json(serde_json::json!({"key": "value"}));
1395 producer.call(Exchange::new(msg)).await.unwrap();
1396 inner
1397 .await_exchanges(1, std::time::Duration::from_millis(500))
1398 .await;
1399 inner
1400 .exchange(0)
1401 .assert_body_json(serde_json::json!({"key": "other"}));
1402 }
1403
1404 #[tokio::test(flavor = "multi_thread")]
1405 async fn assert_body_bytes_pass() {
1406 use bytes::Bytes;
1407 use camel_component_api::Body;
1408 let ctx = test_producer_ctx();
1409 let component = MockComponent::new();
1410 let endpoint = component
1411 .create_endpoint("mock:body-bytes-pass", &NoOpComponentContext)
1412 .unwrap();
1413 let inner = component.get_endpoint("body-bytes-pass").unwrap();
1414 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1415 let mut msg = Message::new("");
1416 msg.body = Body::Bytes(Bytes::from_static(b"binary"));
1417 producer.call(Exchange::new(msg)).await.unwrap();
1418 inner
1419 .await_exchanges(1, std::time::Duration::from_millis(500))
1420 .await;
1421 inner.exchange(0).assert_body_bytes(b"binary");
1422 }
1423
1424 #[tokio::test(flavor = "multi_thread")]
1425 #[should_panic(expected = "expected body bytes")]
1426 async fn assert_body_bytes_fail() {
1427 use bytes::Bytes;
1428 use camel_component_api::Body;
1429 let ctx = test_producer_ctx();
1430 let component = MockComponent::new();
1431 let endpoint = component
1432 .create_endpoint("mock:body-bytes-fail", &NoOpComponentContext)
1433 .unwrap();
1434 let inner = component.get_endpoint("body-bytes-fail").unwrap();
1435 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1436 let mut msg = Message::new("");
1437 msg.body = Body::Bytes(Bytes::from_static(b"binary"));
1438 producer.call(Exchange::new(msg)).await.unwrap();
1439 inner
1440 .await_exchanges(1, std::time::Duration::from_millis(500))
1441 .await;
1442 inner.exchange(0).assert_body_bytes(b"different");
1443 }
1444
1445 #[tokio::test(flavor = "multi_thread")]
1446 async fn assert_header_pass() {
1447 let ctx = test_producer_ctx();
1448 let component = MockComponent::new();
1449 let endpoint = component
1450 .create_endpoint("mock:hdr-pass", &NoOpComponentContext)
1451 .unwrap();
1452 let inner = component.get_endpoint("hdr-pass").unwrap();
1453 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1454 let mut msg = Message::new("body");
1455 msg.headers
1456 .insert("x-key".to_string(), serde_json::json!("value"));
1457 producer.call(Exchange::new(msg)).await.unwrap();
1458 inner
1459 .await_exchanges(1, std::time::Duration::from_millis(500))
1460 .await;
1461 inner
1462 .exchange(0)
1463 .assert_header("x-key", serde_json::json!("value"));
1464 }
1465
1466 #[tokio::test(flavor = "multi_thread")]
1467 #[should_panic(expected = "expected header")]
1468 async fn assert_header_fail() {
1469 let ctx = test_producer_ctx();
1470 let component = MockComponent::new();
1471 let endpoint = component
1472 .create_endpoint("mock:hdr-fail", &NoOpComponentContext)
1473 .unwrap();
1474 let inner = component.get_endpoint("hdr-fail").unwrap();
1475 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1476 let mut msg = Message::new("body");
1477 msg.headers
1478 .insert("x-key".to_string(), serde_json::json!("value"));
1479 producer.call(Exchange::new(msg)).await.unwrap();
1480 inner
1481 .await_exchanges(1, std::time::Duration::from_millis(500))
1482 .await;
1483 inner
1484 .exchange(0)
1485 .assert_header("x-key", serde_json::json!("other"));
1486 }
1487
1488 #[tokio::test(flavor = "multi_thread")]
1489 async fn assert_header_exists_pass() {
1490 let ctx = test_producer_ctx();
1491 let component = MockComponent::new();
1492 let endpoint = component
1493 .create_endpoint("mock:hdr-exists-pass", &NoOpComponentContext)
1494 .unwrap();
1495 let inner = component.get_endpoint("hdr-exists-pass").unwrap();
1496 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1497 let mut msg = Message::new("body");
1498 msg.headers
1499 .insert("x-present".to_string(), serde_json::json!(42));
1500 producer.call(Exchange::new(msg)).await.unwrap();
1501 inner
1502 .await_exchanges(1, std::time::Duration::from_millis(500))
1503 .await;
1504 inner.exchange(0).assert_header_exists("x-present");
1505 }
1506
1507 #[tokio::test(flavor = "multi_thread")]
1508 #[should_panic(expected = "expected header")]
1509 async fn assert_header_exists_fail() {
1510 let ctx = test_producer_ctx();
1511 let component = MockComponent::new();
1512 let endpoint = component
1513 .create_endpoint("mock:hdr-exists-fail", &NoOpComponentContext)
1514 .unwrap();
1515 let inner = component.get_endpoint("hdr-exists-fail").unwrap();
1516 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1517 producer
1518 .call(Exchange::new(Message::new("body")))
1519 .await
1520 .unwrap();
1521 inner
1522 .await_exchanges(1, std::time::Duration::from_millis(500))
1523 .await;
1524 inner.exchange(0).assert_header_exists("x-missing");
1525 }
1526
1527 #[tokio::test(flavor = "multi_thread")]
1528 async fn assert_has_error_pass() {
1529 let ctx = test_producer_ctx();
1530 let component = MockComponent::new();
1531 let endpoint = component
1532 .create_endpoint("mock:err-pass", &NoOpComponentContext)
1533 .unwrap();
1534 let inner = component.get_endpoint("err-pass").unwrap();
1535 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1536 let mut ex = Exchange::new(Message::new("body"));
1537 ex.set_error(camel_component_api::CamelError::ProcessorError(
1538 "oops".to_string(),
1539 ));
1540 producer.call(ex).await.unwrap();
1541 inner
1542 .await_exchanges(1, std::time::Duration::from_millis(500))
1543 .await;
1544 inner.exchange(0).assert_has_error();
1545 }
1546
1547 #[tokio::test(flavor = "multi_thread")]
1548 #[should_panic(expected = "expected exchange to have an error")]
1549 async fn assert_has_error_fail() {
1550 let ctx = test_producer_ctx();
1551 let component = MockComponent::new();
1552 let endpoint = component
1553 .create_endpoint("mock:has-err-fail", &NoOpComponentContext)
1554 .unwrap();
1555 let inner = component.get_endpoint("has-err-fail").unwrap();
1556 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1557 producer
1558 .call(Exchange::new(Message::new("body")))
1559 .await
1560 .unwrap();
1561 inner
1562 .await_exchanges(1, std::time::Duration::from_millis(500))
1563 .await;
1564 inner.exchange(0).assert_has_error();
1565 }
1566
1567 #[tokio::test(flavor = "multi_thread")]
1568 async fn assert_no_error_pass() {
1569 let ctx = test_producer_ctx();
1570 let component = MockComponent::new();
1571 let endpoint = component
1572 .create_endpoint("mock:no-err-pass", &NoOpComponentContext)
1573 .unwrap();
1574 let inner = component.get_endpoint("no-err-pass").unwrap();
1575 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1576 producer
1577 .call(Exchange::new(Message::new("body")))
1578 .await
1579 .unwrap();
1580 inner
1581 .await_exchanges(1, std::time::Duration::from_millis(500))
1582 .await;
1583 inner.exchange(0).assert_no_error();
1584 }
1585
1586 #[tokio::test]
1591 async fn test_mock_reset_clears_exchanges() {
1592 let component = MockComponent::new();
1593 let endpoint = component
1594 .create_endpoint("mock:reset-test", &NoOpComponentContext)
1595 .unwrap();
1596 let inner = component.get_endpoint("reset-test").unwrap();
1597
1598 let ctx = test_producer_ctx();
1599 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1600 producer
1601 .call(Exchange::new(Message::new("a")))
1602 .await
1603 .unwrap();
1604 producer
1605 .call(Exchange::new(Message::new("b")))
1606 .await
1607 .unwrap();
1608
1609 assert_eq!(inner.received_count().await, 2);
1610 inner.reset().await;
1611 assert_eq!(inner.received_count().await, 0);
1612 }
1613
1614 #[tokio::test]
1615 async fn test_mock_bounded_retention_drops_oldest() {
1616 let config = MockConfig {
1617 max_retained: 3,
1618 ..Default::default()
1619 };
1620 let component = MockComponent::with_config(config);
1621 let endpoint = component
1622 .create_endpoint("mock:bounded", &NoOpComponentContext)
1623 .unwrap();
1624 let inner = component.get_endpoint("bounded").unwrap();
1625
1626 let ctx = test_producer_ctx();
1627 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1628
1629 for i in 0..5 {
1631 producer
1632 .call(Exchange::new(Message::new(format!("msg-{i}"))))
1633 .await
1634 .unwrap();
1635 }
1636
1637 assert_eq!(inner.received_count().await, 3);
1638 let received = inner.get_received_exchanges().await;
1639 assert_eq!(received[0].input.body.as_text(), Some("msg-2"));
1641 assert_eq!(received[1].input.body.as_text(), Some("msg-3"));
1642 assert_eq!(received[2].input.body.as_text(), Some("msg-4"));
1643 }
1644
1645 #[tokio::test]
1646 async fn test_mock_reset_then_record_again() {
1647 let component = MockComponent::new();
1648 let endpoint = component
1649 .create_endpoint("mock:reset-reuse", &NoOpComponentContext)
1650 .unwrap();
1651 let inner = component.get_endpoint("reset-reuse").unwrap();
1652
1653 let ctx = test_producer_ctx();
1654 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1655 producer
1656 .call(Exchange::new(Message::new("before-reset")))
1657 .await
1658 .unwrap();
1659 inner.reset().await;
1660
1661 producer
1662 .call(Exchange::new(Message::new("after-reset")))
1663 .await
1664 .unwrap();
1665
1666 let received = inner.get_received_exchanges().await;
1667 assert_eq!(received.len(), 1);
1668 assert_eq!(received[0].input.body.as_text(), Some("after-reset"));
1669 }
1670
1671 #[tokio::test(flavor = "multi_thread")]
1672 #[should_panic(expected = "expected exchange to have no error")]
1673 async fn assert_no_error_fail() {
1674 let ctx = test_producer_ctx();
1675 let component = MockComponent::new();
1676 let endpoint = component
1677 .create_endpoint("mock:no-err-fail", &NoOpComponentContext)
1678 .unwrap();
1679 let inner = component.get_endpoint("no-err-fail").unwrap();
1680 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1681 let mut ex = Exchange::new(Message::new("body"));
1682 ex.set_error(camel_component_api::CamelError::ProcessorError(
1683 "oops".to_string(),
1684 ));
1685 producer.call(ex).await.unwrap();
1686 inner
1687 .await_exchanges(1, std::time::Duration::from_millis(500))
1688 .await;
1689 inner.exchange(0).assert_no_error();
1690 }
1691
1692 #[tokio::test]
1697 async fn test_copy_on_exchange_stores_cloned_body() {
1698 let config = MockConfig {
1699 copy_on_exchange: true,
1700 ..Default::default()
1701 };
1702 let component = MockComponent::with_config(config);
1703 let endpoint = component
1704 .create_endpoint("mock:copy", &NoOpComponentContext)
1705 .unwrap();
1706 let inner = component.get_endpoint("copy").unwrap();
1707
1708 let ctx = test_producer_ctx();
1709 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1710
1711 let mut msg = Message::new("original");
1712 msg.headers.insert("x-test".into(), serde_json::json!(1));
1713 let ex = Exchange::new(msg);
1714 producer.call(ex).await.unwrap();
1715
1716 let received = inner.get_received_exchanges().await;
1717 assert_eq!(received[0].input.body.as_text(), Some("original"));
1718 }
1719
1720 #[tokio::test]
1721 async fn test_copy_on_exchange_false_shares_storage() {
1722 let config = MockConfig {
1723 copy_on_exchange: false,
1724 ..Default::default()
1725 };
1726 let component = MockComponent::with_config(config);
1727 let endpoint = component
1728 .create_endpoint("mock:no-copy", &NoOpComponentContext)
1729 .unwrap();
1730 let inner = component.get_endpoint("no-copy").unwrap();
1731
1732 let ctx = test_producer_ctx();
1733 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1734
1735 producer
1736 .call(Exchange::new(Message::new("direct")))
1737 .await
1738 .unwrap();
1739
1740 let received = inner.get_received_exchanges().await;
1741 assert_eq!(received[0].input.body.as_text(), Some("direct"));
1742 }
1743
1744 #[tokio::test]
1749 async fn test_clone_body_preserves_stream() {
1750 use bytes::Bytes;
1751 use camel_component_api::{Body, StreamBody, StreamMetadata};
1752 use futures::stream;
1753 use std::sync::Arc;
1754 use tokio::sync::Mutex;
1755
1756 let chunks: Vec<Result<Bytes, camel_component_api::CamelError>> =
1757 vec![Ok(Bytes::from("data"))];
1758 let body = Body::Stream(StreamBody {
1759 stream: Arc::new(Mutex::new(Some(Box::pin(stream::iter(chunks))))),
1760 metadata: StreamMetadata::default(),
1761 });
1762
1763 let config = MockConfig {
1764 copy_on_exchange: true,
1765 ..Default::default()
1766 };
1767 let component = MockComponent::with_config(config);
1768 let endpoint = component
1769 .create_endpoint("mock:stream-test", &NoOpComponentContext)
1770 .unwrap();
1771 let inner = component.get_endpoint("stream-test").unwrap();
1772
1773 let ctx = test_producer_ctx();
1774 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1775
1776 let msg = Message::new(body);
1777 let ex = Exchange::new(msg);
1778 producer.call(ex).await.unwrap();
1779
1780 let received = inner.get_received_exchanges().await;
1781 assert!(
1782 matches!(received[0].input.body, Body::Stream(_)),
1783 "expected Body::Stream, got {:?}",
1784 received[0].input.body
1785 );
1786 }
1787
1788 #[tokio::test]
1789 async fn test_clone_body_stream_shares_arc() {
1790 use bytes::Bytes;
1791 use camel_component_api::{Body, StreamBody, StreamMetadata};
1792 use futures::stream;
1793 use std::sync::Arc;
1794 use tokio::sync::Mutex;
1795
1796 let chunks: Vec<Result<Bytes, camel_component_api::CamelError>> =
1797 vec![Ok(Bytes::from("data"))];
1798 let original = Body::Stream(StreamBody {
1799 stream: Arc::new(Mutex::new(Some(Box::pin(stream::iter(chunks))))),
1800 metadata: StreamMetadata::default(),
1801 });
1802
1803 let clone = clone_body(&original);
1804
1805 let _ = original.into_bytes(100).await.unwrap();
1807
1808 let result = clone.into_bytes(100).await;
1810 assert!(
1811 matches!(
1812 result,
1813 Err(camel_component_api::CamelError::AlreadyConsumed)
1814 ),
1815 "expected AlreadyConsumed, got {:?}",
1816 result
1817 );
1818 }
1819
1820 #[tokio::test]
1825 async fn test_assert_satisfied_bodies_in_order() {
1826 let component = MockComponent::new();
1827 let endpoint = component
1828 .create_endpoint("mock:sat-bodies", &NoOpComponentContext)
1829 .unwrap();
1830 let inner = component.get_endpoint("sat-bodies").unwrap();
1831
1832 inner.expect_body(camel_component_api::Body::Text("alpha".into()));
1833 inner.expect_body(camel_component_api::Body::Text("beta".into()));
1834
1835 let ctx = test_producer_ctx();
1836 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1837 producer
1838 .call(Exchange::new(Message::new("alpha")))
1839 .await
1840 .unwrap();
1841 producer
1842 .call(Exchange::new(Message::new("beta")))
1843 .await
1844 .unwrap();
1845
1846 inner.assert_satisfied().await;
1847 }
1848
1849 #[tokio::test]
1850 #[should_panic(expected = "body[0] expected")]
1851 async fn test_assert_satisfied_bodies_wrong_order_fails() {
1852 let component = MockComponent::new();
1853 let endpoint = component
1854 .create_endpoint("mock:sat-bodies-fail", &NoOpComponentContext)
1855 .unwrap();
1856 let inner = component.get_endpoint("sat-bodies-fail").unwrap();
1857
1858 inner.expect_body(camel_component_api::Body::Text("alpha".into()));
1859 inner.expect_body(camel_component_api::Body::Text("beta".into()));
1860
1861 let ctx = test_producer_ctx();
1862 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1863 producer
1864 .call(Exchange::new(Message::new("beta")))
1865 .await
1866 .unwrap();
1867 producer
1868 .call(Exchange::new(Message::new("alpha")))
1869 .await
1870 .unwrap();
1871
1872 inner.assert_satisfied().await;
1873 }
1874
1875 #[tokio::test]
1876 async fn test_assert_satisfied_headers() {
1877 let component = MockComponent::new();
1878 let endpoint = component
1879 .create_endpoint("mock:sat-hdr", &NoOpComponentContext)
1880 .unwrap();
1881 let inner = component.get_endpoint("sat-hdr").unwrap();
1882
1883 inner.expect_header("status", serde_json::json!("ok"));
1884
1885 let ctx = test_producer_ctx();
1886 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1887 let mut msg = Message::new("body");
1888 msg.headers.insert("status".into(), serde_json::json!("ok"));
1889 producer.call(Exchange::new(msg)).await.unwrap();
1890
1891 inner.assert_satisfied().await;
1892 }
1893
1894 #[tokio::test]
1895 #[should_panic(expected = "expected header 'missing' =")]
1896 async fn test_assert_satisfied_headers_missing() {
1897 let component = MockComponent::new();
1898 let endpoint = component
1899 .create_endpoint("mock:sat-hdr-missing", &NoOpComponentContext)
1900 .unwrap();
1901 let inner = component.get_endpoint("sat-hdr-missing").unwrap();
1902
1903 inner.expect_header("missing", serde_json::json!("value"));
1904
1905 let ctx = test_producer_ctx();
1906 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1907 producer
1908 .call(Exchange::new(Message::new("body")))
1909 .await
1910 .unwrap();
1911
1912 inner.assert_satisfied().await;
1913 }
1914
1915 #[tokio::test]
1920 async fn test_fail_fast_rejects_after_first_call() {
1921 let config = MockConfig {
1922 fail_fast: true,
1923 ..Default::default()
1924 };
1925 let component = MockComponent::with_config(config);
1926 let endpoint = component
1927 .create_endpoint("mock:ff", &NoOpComponentContext)
1928 .unwrap();
1929
1930 let ctx = test_producer_ctx();
1931 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1932
1933 producer
1935 .call(Exchange::new(Message::new("ok")))
1936 .await
1937 .unwrap();
1938 }
1939
1940 #[tokio::test]
1941 async fn test_fail_fast_no_error_when_all_good() {
1942 let config = MockConfig {
1943 fail_fast: true,
1944 ..Default::default()
1945 };
1946 let component = MockComponent::with_config(config);
1947 let endpoint = component
1948 .create_endpoint("mock:ff-good", &NoOpComponentContext)
1949 .unwrap();
1950 let inner = component.get_endpoint("ff-good").unwrap();
1951
1952 let ctx = test_producer_ctx();
1953 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1954
1955 producer
1956 .call(Exchange::new(Message::new("a")))
1957 .await
1958 .unwrap();
1959 producer
1960 .call(Exchange::new(Message::new("b")))
1961 .await
1962 .unwrap();
1963
1964 assert!(inner.fail_fast_error().is_none());
1965 inner.assert_exchange_count(2).await;
1966 }
1967
1968 #[tokio::test]
1973 async fn test_await_exchanges_with_timeout_uses_config_period() {
1974 let config = MockConfig {
1975 assert_period_ms: 100,
1976 ..Default::default()
1977 };
1978 let component = MockComponent::with_config(config);
1979 let endpoint = component
1980 .create_endpoint("mock:ap", &NoOpComponentContext)
1981 .unwrap();
1982 let inner = component.get_endpoint("ap").unwrap();
1983
1984 let ctx = test_producer_ctx();
1985 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1986 producer
1987 .call(Exchange::new(Message::new("x")))
1988 .await
1989 .unwrap();
1990
1991 inner
1992 .await_exchanges_with_timeout(1, std::time::Duration::from_millis(1))
1993 .await;
1994 }
1995
1996 #[tokio::test]
1997 async fn test_await_exchanges_with_timeout_uses_fallback_when_zero() {
1998 let config = MockConfig {
1999 assert_period_ms: 0,
2000 ..Default::default()
2001 };
2002 let component = MockComponent::with_config(config);
2003 let endpoint = component
2004 .create_endpoint("mock:ap-fb", &NoOpComponentContext)
2005 .unwrap();
2006 let inner = component.get_endpoint("ap-fb").unwrap();
2007
2008 let ctx = test_producer_ctx();
2009 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2010 producer
2011 .call(Exchange::new(Message::new("y")))
2012 .await
2013 .unwrap();
2014
2015 inner
2016 .await_exchanges_with_timeout(1, std::time::Duration::from_millis(200))
2017 .await;
2018 }
2019
2020 #[tokio::test]
2025 async fn test_expect_header_regex_match() {
2026 let component = MockComponent::new();
2027 let endpoint = component
2028 .create_endpoint("mock:re-hdr", &NoOpComponentContext)
2029 .unwrap();
2030 let inner = component.get_endpoint("re-hdr").unwrap();
2031
2032 inner.expect_header_regex("x-trace-id", r"^[a-f0-9]{8}$");
2033
2034 let ctx = test_producer_ctx();
2035 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2036 let mut msg = Message::new("body");
2037 msg.headers
2038 .insert("x-trace-id".into(), serde_json::json!("deadbeef"));
2039 producer.call(Exchange::new(msg)).await.unwrap();
2040
2041 inner.assert_satisfied().await;
2042 }
2043
2044 #[tokio::test]
2045 #[should_panic(expected = "no received exchange has header")]
2046 async fn test_expect_header_regex_no_match() {
2047 let component = MockComponent::new();
2048 let endpoint = component
2049 .create_endpoint("mock:re-hdr-fail", &NoOpComponentContext)
2050 .unwrap();
2051 let inner = component.get_endpoint("re-hdr-fail").unwrap();
2052
2053 inner.expect_header_regex("x-trace-id", r"^\d+$");
2054
2055 let ctx = test_producer_ctx();
2056 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2057 let mut msg = Message::new("body");
2058 msg.headers
2059 .insert("x-trace-id".into(), serde_json::json!("abc"));
2060 producer.call(Exchange::new(msg)).await.unwrap();
2061
2062 inner.assert_satisfied().await;
2063 }
2064
2065 #[tokio::test]
2070 async fn test_any_order_bodies_match() {
2071 let config = MockConfig {
2072 any_order: true,
2073 ..Default::default()
2074 };
2075 let component = MockComponent::with_config(config);
2076 let endpoint = component
2077 .create_endpoint("mock:anyorder", &NoOpComponentContext)
2078 .unwrap();
2079 let inner = component.get_endpoint("anyorder").unwrap();
2080
2081 inner.expect_body(camel_component_api::Body::Text("beta".into()));
2082 inner.expect_body(camel_component_api::Body::Text("alpha".into()));
2083
2084 let ctx = test_producer_ctx();
2085 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2086 producer
2087 .call(Exchange::new(Message::new("alpha")))
2088 .await
2089 .unwrap();
2090 producer
2091 .call(Exchange::new(Message::new("beta")))
2092 .await
2093 .unwrap();
2094
2095 inner.assert_satisfied().await;
2096 }
2097
2098 #[tokio::test]
2099 #[should_panic(expected = "not found in received exchanges (anyOrder mode)")]
2100 async fn test_any_order_bodies_missing() {
2101 let config = MockConfig {
2102 any_order: true,
2103 ..Default::default()
2104 };
2105 let component = MockComponent::with_config(config);
2106 let endpoint = component
2107 .create_endpoint("mock:anyorder-fail", &NoOpComponentContext)
2108 .unwrap();
2109 let inner = component.get_endpoint("anyorder-fail").unwrap();
2110
2111 inner.expect_body(camel_component_api::Body::Text("gamma".into()));
2112 inner.expect_body(camel_component_api::Body::Text("alpha".into()));
2113
2114 let ctx = test_producer_ctx();
2115 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2116 producer
2117 .call(Exchange::new(Message::new("alpha")))
2118 .await
2119 .unwrap();
2120 producer
2121 .call(Exchange::new(Message::new("beta")))
2122 .await
2123 .unwrap();
2124
2125 inner.assert_satisfied().await;
2126 }
2127
2128 #[tokio::test]
2133 async fn test_tracing_logs_exchange_received() {
2134 let ctx = test_producer_ctx();
2136 let component = MockComponent::new();
2137 let endpoint = component
2138 .create_endpoint("mock:trace", &NoOpComponentContext)
2139 .unwrap();
2140 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2141 producer
2142 .call(Exchange::new(Message::new("traced")))
2143 .await
2144 .unwrap();
2145
2146 let inner = component.get_endpoint("trace").unwrap();
2147 inner.assert_exchange_count(1).await;
2148 }
2149
2150 #[test]
2155 fn test_mock_config_new() {
2156 let cfg = MockConfig::new(42);
2157 assert_eq!(cfg.max_retained, 42);
2158 assert!(!cfg.copy_on_exchange);
2159 assert!(!cfg.fail_fast);
2160 assert!(!cfg.any_order);
2161 }
2162
2163 use futures::FutureExt;
2168
2169 #[tokio::test]
2170 async fn test_trigger_fail_fast_rejects_subsequent_producer() {
2171 use camel_component_api::CamelError;
2172 use std::panic::AssertUnwindSafe;
2173 let config = MockConfig {
2174 fail_fast: true,
2175 ..Default::default()
2176 };
2177 let component = MockComponent::with_config(config);
2178 let endpoint = component
2179 .create_endpoint("mock:test", &NoOpComponentContext)
2180 .unwrap();
2181 let inner = component.get_endpoint("test").unwrap();
2182
2183 inner.trigger_fail_fast(CamelError::ProcessorError("boom".to_string()));
2184
2185 let ctx = test_producer_ctx();
2186 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2187 assert!(producer.ready().await.is_err());
2189 let result = AssertUnwindSafe(producer.call(Exchange::default()))
2191 .catch_unwind()
2192 .await
2193 .expect("call should not panic");
2194 match result {
2195 Err(CamelError::ProcessorError(msg)) => {
2196 assert!(
2197 msg.contains("fail-fast mode"),
2198 "message should contain 'fail-fast mode', got: {msg}"
2199 );
2200 assert!(
2201 !msg.contains("boom"),
2202 "supplied error must NOT be in fixed message, got: {msg}"
2203 );
2204 }
2205 other => panic!("expected ProcessorError, got {other:?}"),
2206 }
2207 }
2208
2209 #[tokio::test]
2210 async fn test_trigger_fail_fast_noop_when_fail_fast_false() {
2211 use camel_component_api::CamelError;
2212 use std::panic::AssertUnwindSafe;
2213 let config = MockConfig {
2214 fail_fast: false,
2215 ..Default::default()
2216 };
2217 let component = MockComponent::with_config(config);
2218 let endpoint = component
2219 .create_endpoint("mock:test", &NoOpComponentContext)
2220 .unwrap();
2221 let inner = component.get_endpoint("test").unwrap();
2222
2223 inner.trigger_fail_fast(CamelError::ProcessorError("boom".to_string()));
2224
2225 let ctx = test_producer_ctx();
2226 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2227 let result = AssertUnwindSafe(producer.call(Exchange::default()))
2228 .catch_unwind()
2229 .await
2230 .expect("call should not panic");
2231 assert!(
2232 result.is_ok(),
2233 "fail_fast=false must let the call through even with stored error"
2234 );
2235 }
2236
2237 #[tokio::test]
2238 async fn test_reset_clears_trigger_fail_fast() {
2239 use camel_component_api::CamelError;
2240 use std::panic::AssertUnwindSafe;
2241 let config = MockConfig {
2242 fail_fast: true,
2243 ..Default::default()
2244 };
2245 let component = MockComponent::with_config(config);
2246 let endpoint = component
2247 .create_endpoint("mock:test", &NoOpComponentContext)
2248 .unwrap();
2249 let inner = component.get_endpoint("test").unwrap();
2250
2251 inner.trigger_fail_fast(CamelError::ProcessorError("boom".to_string()));
2252 assert!(inner.fail_fast_error().is_some());
2253 inner.reset().await;
2254 assert!(inner.fail_fast_error().is_none());
2255
2256 let ctx = test_producer_ctx();
2258 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2259 let result = AssertUnwindSafe(producer.call(Exchange::default()))
2260 .catch_unwind()
2261 .await
2262 .expect("call should not panic");
2263 assert!(result.is_ok());
2264 }
2265
2266 #[tokio::test]
2267 async fn test_assert_satisfied_body_count_mismatch_sets_fail_fast() {
2268 use std::panic::AssertUnwindSafe;
2269 let config = MockConfig {
2270 fail_fast: true,
2271 ..Default::default()
2272 };
2273 let component = MockComponent::with_config(config);
2274 let endpoint = component
2275 .create_endpoint("mock:test", &NoOpComponentContext)
2276 .unwrap();
2277 let inner = component.get_endpoint("test").unwrap();
2278
2279 inner.expect_body(camel_component_api::Body::Text("a".to_string()));
2280 inner.expect_body(camel_component_api::Body::Text("b".to_string()));
2281
2282 let ctx = test_producer_ctx();
2284 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2285 producer
2286 .call(Exchange::new(Message::new("a")))
2287 .await
2288 .unwrap();
2289
2290 let panic_result = AssertUnwindSafe(inner.assert_satisfied())
2292 .catch_unwind()
2293 .await;
2294 assert!(
2295 panic_result.is_err(),
2296 "expected panic from assert_satisfied"
2297 );
2298 assert!(
2299 inner.fail_fast_error().is_some(),
2300 "fail_fast_error must be set when fail_fast=true and assertion panics"
2301 );
2302 }
2303
2304 #[tokio::test]
2305 async fn test_assert_satisfied_body_mismatch_sets_fail_fast() {
2306 use std::panic::AssertUnwindSafe;
2307 let config = MockConfig {
2308 fail_fast: true,
2309 ..Default::default()
2310 };
2311 let component = MockComponent::with_config(config);
2312 let endpoint = component
2313 .create_endpoint("mock:test", &NoOpComponentContext)
2314 .unwrap();
2315 let inner = component.get_endpoint("test").unwrap();
2316
2317 inner.expect_body(camel_component_api::Body::Text("expected".to_string()));
2318
2319 let ctx = test_producer_ctx();
2320 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2321 producer
2322 .call(Exchange::new(Message::new("actual")))
2323 .await
2324 .unwrap();
2325
2326 let panic_result = AssertUnwindSafe(inner.assert_satisfied())
2327 .catch_unwind()
2328 .await;
2329 assert!(
2330 panic_result.is_err(),
2331 "expected panic from assert_satisfied"
2332 );
2333 assert!(
2334 inner.fail_fast_error().is_some(),
2335 "fail_fast_error must be set on body mismatch when fail_fast=true"
2336 );
2337 }
2338
2339 #[tokio::test]
2340 async fn test_assert_satisfied_no_set_error_when_fail_fast_false() {
2341 use std::panic::AssertUnwindSafe;
2342 let config = MockConfig {
2343 fail_fast: false,
2344 ..Default::default()
2345 };
2346 let component = MockComponent::with_config(config);
2347 let endpoint = component
2348 .create_endpoint("mock:test", &NoOpComponentContext)
2349 .unwrap();
2350 let inner = component.get_endpoint("test").unwrap();
2351
2352 inner.expect_body(camel_component_api::Body::Text("a".to_string()));
2353 inner.expect_body(camel_component_api::Body::Text("b".to_string()));
2354
2355 let ctx = test_producer_ctx();
2357 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2358 producer
2359 .call(Exchange::new(Message::new("a")))
2360 .await
2361 .unwrap();
2362
2363 let panic_result = AssertUnwindSafe(inner.assert_satisfied())
2364 .catch_unwind()
2365 .await;
2366 assert!(
2367 panic_result.is_err(),
2368 "expected panic from assert_satisfied"
2369 );
2370 assert!(
2371 inner.fail_fast_error().is_none(),
2372 "fail_fast_error must remain None when fail_fast=false"
2373 );
2374 }
2375
2376 #[tokio::test]
2377 async fn test_assert_satisfied_any_order_body_mismatch_sets_fail_fast() {
2378 use std::panic::AssertUnwindSafe;
2379 let config = MockConfig {
2380 fail_fast: true,
2381 any_order: true,
2382 ..Default::default()
2383 };
2384 let component = MockComponent::with_config(config);
2385 let endpoint = component
2386 .create_endpoint("mock:test", &NoOpComponentContext)
2387 .unwrap();
2388 let inner = component.get_endpoint("test").unwrap();
2389
2390 inner.expect_body(camel_component_api::Body::Text("a".to_string()));
2391 inner.expect_body(camel_component_api::Body::Text("b".to_string()));
2392
2393 let ctx = test_producer_ctx();
2395 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2396 producer
2397 .call(Exchange::new(Message::new("a")))
2398 .await
2399 .unwrap();
2400 producer
2401 .call(Exchange::new(Message::new("c")))
2402 .await
2403 .unwrap();
2404
2405 let panic_result = AssertUnwindSafe(inner.assert_satisfied())
2406 .catch_unwind()
2407 .await;
2408 assert!(
2409 panic_result.is_err(),
2410 "expected panic from assert_satisfied (any-order body not found)"
2411 );
2412 assert!(
2413 inner.fail_fast_error().is_some(),
2414 "fail_fast_error must be set when fail_fast=true and any-order body is missing"
2415 );
2416 }
2417
2418 #[tokio::test]
2419 async fn test_assert_satisfied_header_missing_sets_fail_fast() {
2420 use std::panic::AssertUnwindSafe;
2421 let config = MockConfig {
2422 fail_fast: true,
2423 ..Default::default()
2424 };
2425 let component = MockComponent::with_config(config);
2426 let endpoint = component
2427 .create_endpoint("mock:test", &NoOpComponentContext)
2428 .unwrap();
2429 let inner = component.get_endpoint("test").unwrap();
2430
2431 inner.expect_header("x-missing", serde_json::json!("value"));
2432
2433 let ctx = test_producer_ctx();
2435 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2436 producer
2437 .call(Exchange::new(Message::new("body")))
2438 .await
2439 .unwrap();
2440
2441 let panic_result = AssertUnwindSafe(inner.assert_satisfied())
2442 .catch_unwind()
2443 .await;
2444 assert!(
2445 panic_result.is_err(),
2446 "expected panic from assert_satisfied (header missing)"
2447 );
2448 assert!(
2449 inner.fail_fast_error().is_some(),
2450 "fail_fast_error must be set when fail_fast=true and expected header is missing"
2451 );
2452 }
2453
2454 fn panic_message(payload: Box<dyn std::any::Any + Send>) -> String {
2461 payload
2462 .downcast_ref::<String>()
2463 .cloned()
2464 .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
2465 .unwrap_or_else(|| "<non-string panic payload>".to_string())
2466 }
2467
2468 #[tokio::test]
2469 async fn count_exact_mismatch_fails() {
2470 use std::panic::AssertUnwindSafe;
2471 let component = MockComponent::new();
2472 let endpoint = component
2473 .create_endpoint("mock:count-exact-mismatch", &NoOpComponentContext)
2474 .unwrap();
2475 let inner = component.get_endpoint("count-exact-mismatch").unwrap();
2476
2477 inner.expect_count(3);
2478
2479 let ctx = test_producer_ctx();
2480 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2481 producer
2482 .call(Exchange::new(Message::new("a")))
2483 .await
2484 .unwrap();
2485 producer
2486 .call(Exchange::new(Message::new("b")))
2487 .await
2488 .unwrap();
2489
2490 let payload = AssertUnwindSafe(inner.assert_satisfied())
2491 .catch_unwind()
2492 .await
2493 .expect_err("assert_satisfied should panic on exact count mismatch");
2494 let msg = panic_message(payload);
2495 assert!(
2496 msg.contains("count-exact-mismatch"),
2497 "message should contain endpoint name, got: {msg}"
2498 );
2499 assert!(
2500 msg.contains("expected 3 exchanges") && msg.contains("got 2"),
2501 "message should report expected 3 / got 2, got: {msg}"
2502 );
2503 }
2504
2505 #[tokio::test]
2506 async fn count_exact_satisfied_passes() {
2507 let component = MockComponent::new();
2508 let endpoint = component
2509 .create_endpoint("mock:count-exact-pass", &NoOpComponentContext)
2510 .unwrap();
2511 let inner = component.get_endpoint("count-exact-pass").unwrap();
2512
2513 inner.expect_count(2);
2514
2515 let ctx = test_producer_ctx();
2516 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2517 producer
2518 .call(Exchange::new(Message::new("one")))
2519 .await
2520 .unwrap();
2521 producer
2522 .call(Exchange::new(Message::new("two")))
2523 .await
2524 .unwrap();
2525
2526 inner.assert_satisfied().await;
2527 }
2528
2529 #[tokio::test]
2530 async fn count_minimum_satisfied_by_more() {
2531 let component = MockComponent::new();
2532 let endpoint = component
2533 .create_endpoint("mock:count-min-more", &NoOpComponentContext)
2534 .unwrap();
2535 let inner = component.get_endpoint("count-min-more").unwrap();
2536
2537 inner.expect_minimum_count(2);
2538
2539 let ctx = test_producer_ctx();
2540 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2541 for i in 0..5 {
2542 producer
2543 .call(Exchange::new(Message::new(format!("m{i}"))))
2544 .await
2545 .unwrap();
2546 }
2547
2548 inner.assert_satisfied().await;
2549 }
2550
2551 #[tokio::test]
2552 async fn count_minimum_violated_fails() {
2553 use std::panic::AssertUnwindSafe;
2554 let component = MockComponent::new();
2555 let endpoint = component
2556 .create_endpoint("mock:count-min-violated", &NoOpComponentContext)
2557 .unwrap();
2558 let inner = component.get_endpoint("count-min-violated").unwrap();
2559
2560 inner.expect_minimum_count(4);
2561
2562 let ctx = test_producer_ctx();
2563 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2564 producer
2565 .call(Exchange::new(Message::new("only-one")))
2566 .await
2567 .unwrap();
2568
2569 let payload = AssertUnwindSafe(inner.assert_satisfied())
2570 .catch_unwind()
2571 .await
2572 .expect_err("assert_satisfied should panic on minimum count violation");
2573 let msg = panic_message(payload);
2574 assert!(
2575 msg.contains("at least 4"),
2576 "message should state at least 4 exchanges were expected, got: {msg}"
2577 );
2578 }
2579
2580 #[tokio::test]
2581 async fn count_exact_and_minimum_enforced_together() {
2582 use std::panic::AssertUnwindSafe;
2583 let component = MockComponent::new();
2584 let endpoint = component
2585 .create_endpoint("mock:count-both", &NoOpComponentContext)
2586 .unwrap();
2587 let inner = component.get_endpoint("count-both").unwrap();
2588
2589 inner.expect_count(2);
2590 inner.expect_minimum_count(1);
2591
2592 let ctx = test_producer_ctx();
2593 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2594 for i in 0..3 {
2595 producer
2596 .call(Exchange::new(Message::new(format!("m{i}"))))
2597 .await
2598 .unwrap();
2599 }
2600
2601 let payload = AssertUnwindSafe(inner.assert_satisfied())
2602 .catch_unwind()
2603 .await
2604 .expect_err("assert_satisfied should panic on exact count mismatch");
2605 let msg = panic_message(payload);
2606 assert!(
2607 msg.contains("expected 2 exchanges") && msg.contains("got 3"),
2608 "message should report the exact mismatch, got: {msg}"
2609 );
2610 assert!(
2611 !msg.contains("at least"),
2612 "exact mismatch must be reported even though the minimum was satisfied, got: {msg}"
2613 );
2614 }
2615
2616 #[tokio::test]
2617 async fn count_checked_before_bodies() {
2618 use std::panic::AssertUnwindSafe;
2619 let component = MockComponent::new();
2620 let endpoint = component
2621 .create_endpoint("mock:count-first", &NoOpComponentContext)
2622 .unwrap();
2623 let inner = component.get_endpoint("count-first").unwrap();
2624
2625 inner.expect_count(5);
2626 inner.expect_body(camel_component_api::Body::Text("x".into()));
2627
2628 let ctx = test_producer_ctx();
2629 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2630 producer
2631 .call(Exchange::new(Message::new("y")))
2632 .await
2633 .unwrap();
2634 producer
2635 .call(Exchange::new(Message::new("z")))
2636 .await
2637 .unwrap();
2638
2639 let payload = AssertUnwindSafe(inner.assert_satisfied())
2640 .catch_unwind()
2641 .await
2642 .expect_err("assert_satisfied should panic on count mismatch");
2643 let msg = panic_message(payload);
2644 assert!(
2645 msg.contains("expected 5 exchanges") && msg.contains("got 2"),
2646 "count mismatch must be reported, got: {msg}"
2647 );
2648 assert!(
2649 !msg.contains("bodies"),
2650 "body checks must not run after a count mismatch, got: {msg}"
2651 );
2652 }
2653
2654 #[tokio::test]
2655 async fn count_coexists_with_bodies_pass() {
2656 let component = MockComponent::new();
2657 let endpoint = component
2658 .create_endpoint("mock:count-with-bodies", &NoOpComponentContext)
2659 .unwrap();
2660 let inner = component.get_endpoint("count-with-bodies").unwrap();
2661
2662 inner.expect_count(2);
2663 inner.expect_body(camel_component_api::Body::Text("alpha".into()));
2664 inner.expect_body(camel_component_api::Body::Text("beta".into()));
2665
2666 let ctx = test_producer_ctx();
2667 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2668 producer
2669 .call(Exchange::new(Message::new("alpha")))
2670 .await
2671 .unwrap();
2672 producer
2673 .call(Exchange::new(Message::new("beta")))
2674 .await
2675 .unwrap();
2676
2677 inner.assert_satisfied().await;
2678 }
2679
2680 #[tokio::test]
2681 async fn count_evaluates_retained_snapshot_under_truncation() {
2682 let component = MockComponent::with_config(MockConfig::new(3));
2683 let endpoint = component
2684 .create_endpoint("mock:count-truncated", &NoOpComponentContext)
2685 .unwrap();
2686 let inner = component.get_endpoint("count-truncated").unwrap();
2687
2688 inner.expect_count(3);
2689
2690 let ctx = test_producer_ctx();
2691 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2692 for i in 0..5 {
2693 producer
2694 .call(Exchange::new(Message::new(format!("msg-{i}"))))
2695 .await
2696 .unwrap();
2697 }
2698
2699 assert_eq!(inner.received_count().await, 3);
2700 inner.assert_satisfied().await;
2701 }
2702
2703 #[tokio::test]
2709 async fn try_assert_satisfied_ok_when_satisfied() {
2710 let component = MockComponent::new();
2711 let endpoint = component
2712 .create_endpoint("mock:try-ok", &NoOpComponentContext)
2713 .unwrap();
2714 let inner = component.get_endpoint("try-ok").unwrap();
2715
2716 inner.expect_count(1);
2717 inner.expect_body(camel_component_api::Body::Text("payload".into()));
2718
2719 let ctx = test_producer_ctx();
2720 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2721 producer
2722 .call(Exchange::new(Message::new("payload")))
2723 .await
2724 .unwrap();
2725
2726 let result = inner.try_assert_satisfied().await;
2727 assert!(result.is_ok(), "expected Ok(()), got: {result:?}");
2728 }
2729
2730 #[tokio::test]
2731 async fn try_assert_satisfied_err_with_details() {
2732 let component = MockComponent::new();
2733 let _endpoint = component
2734 .create_endpoint("mock:try-err", &NoOpComponentContext)
2735 .unwrap();
2736 let inner = component.get_endpoint("try-err").unwrap();
2737
2738 inner.expect_count(2);
2739 let err = inner
2743 .try_assert_satisfied()
2744 .await
2745 .expect_err("expected Err on unmet expect_count");
2746 let msg = err.to_string();
2747 assert!(
2748 msg.contains("try-err"),
2749 "message should contain endpoint name, got: {msg}"
2750 );
2751 assert!(
2752 msg.contains("expected 2"),
2753 "message should contain 'expected 2', got: {msg}"
2754 );
2755 }
2756
2757 #[tokio::test]
2758 async fn try_assert_satisfied_sets_fail_fast_latch() {
2759 let config = MockConfig {
2760 fail_fast: true,
2761 ..Default::default()
2762 };
2763 let component = MockComponent::with_config(config);
2764 let _endpoint = component
2765 .create_endpoint("mock:try-latch", &NoOpComponentContext)
2766 .unwrap();
2767 let inner = component.get_endpoint("try-latch").unwrap();
2768
2769 inner.expect_count(2);
2770 let result = inner.try_assert_satisfied().await;
2773 assert!(result.is_err(), "expected Err, got: {result:?}");
2774 assert!(
2775 inner.fail_fast_error().is_some(),
2776 "fail-fast latch must be set on mismatch (parity with assert_satisfied)"
2777 );
2778 }
2779
2780 #[tokio::test]
2781 async fn invalid_header_regex_returns_err_not_panic() {
2782 let config = MockConfig {
2783 fail_fast: true,
2784 ..Default::default()
2785 };
2786 let component = MockComponent::with_config(config);
2787 let endpoint = component
2788 .create_endpoint("mock:try-bad-re", &NoOpComponentContext)
2789 .unwrap();
2790 let inner = component.get_endpoint("try-bad-re").unwrap();
2791
2792 inner.expect_header_regex("k", "(unclosed");
2793
2794 let ctx = test_producer_ctx();
2795 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2796 producer
2797 .call(Exchange::new(Message::new("body")))
2798 .await
2799 .unwrap();
2800
2801 let err = inner
2803 .try_assert_satisfied()
2804 .await
2805 .expect_err("invalid regex must produce Err, not a panic");
2806 assert!(
2807 matches!(err, MockAssertionError::InvalidHeaderPattern { .. }),
2808 "expected InvalidHeaderPattern, got: {err:?}"
2809 );
2810 assert!(
2811 inner.fail_fast_error().is_none(),
2812 "malformed expectation is a caller programming error — latch must NOT trip"
2813 );
2814 }
2815
2816 #[tokio::test]
2817 async fn display_equals_panicking_variant_message() {
2818 use std::panic::AssertUnwindSafe;
2819
2820 let component = MockComponent::new();
2823 let _endpoint_a = component
2824 .create_endpoint("mock:parity", &NoOpComponentContext)
2825 .unwrap();
2826 let endpoint_b = component
2827 .create_endpoint("mock:parity", &NoOpComponentContext)
2828 .unwrap();
2829 let inner_a = component.get_endpoint("parity").unwrap();
2830 let inner_b = component.get_endpoint("parity").unwrap();
2831
2832 inner_a.expect_count(3);
2833
2834 let ctx = test_producer_ctx();
2835 let mut producer = endpoint_b.create_producer(rt(), &ctx).unwrap();
2836 producer
2837 .call(Exchange::new(Message::new("only-one")))
2838 .await
2839 .unwrap();
2840
2841 let payload = AssertUnwindSafe(inner_a.assert_satisfied())
2842 .catch_unwind()
2843 .await
2844 .expect_err("assert_satisfied should panic on count mismatch");
2845 let panicking = panic_message(payload);
2846
2847 let display = inner_b
2848 .try_assert_satisfied()
2849 .await
2850 .expect_err("try_assert_satisfied should Err on count mismatch")
2851 .to_string();
2852
2853 assert_eq!(panicking, display);
2854 }
2855
2856 #[tokio::test]
2857 async fn no_expected_bodies_with_received_exchanges_still_ok() {
2858 let component = MockComponent::new();
2859 let endpoint = component
2860 .create_endpoint("mock:try-no-bodies", &NoOpComponentContext)
2861 .unwrap();
2862 let inner = component.get_endpoint("try-no-bodies").unwrap();
2863
2864 let ctx = test_producer_ctx();
2866 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2867 for i in 0..3 {
2868 producer
2869 .call(Exchange::new(Message::new(format!("m{i}"))))
2870 .await
2871 .unwrap();
2872 }
2873
2874 let result = inner.try_assert_satisfied().await;
2875 assert!(result.is_ok(), "no expectations set, got: {result:?}");
2876 }
2877
2878 #[tokio::test]
2883 async fn uri_retain_override_truncates() {
2884 let component = MockComponent::new();
2885 let endpoint = component
2886 .create_endpoint("mock:cap?retain=50", &NoOpComponentContext)
2887 .unwrap();
2888 let ctx = test_producer_ctx();
2889 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2890 for i in 0..55 {
2891 producer
2892 .call(Exchange::new(Message::new(format!("m{i}"))))
2893 .await
2894 .unwrap();
2895 }
2896 let inner = component.get_endpoint("cap").unwrap();
2897 assert_eq!(
2898 inner.received_count().await,
2899 50,
2900 "retain=50 must cap stored exchanges at 50 (default 10_000 would retain all 55)"
2901 );
2902 }
2903
2904 #[tokio::test]
2905 async fn uri_any_order_overrides_matching() {
2906 let component = MockComponent::new();
2907 let endpoint = component
2908 .create_endpoint("mock:relaxed?anyOrder=true", &NoOpComponentContext)
2909 .unwrap();
2910 let inner = component.get_endpoint("relaxed").unwrap();
2911
2912 inner.expect_body(camel_component_api::Body::Text("a".to_string()));
2913 inner.expect_body(camel_component_api::Body::Text("b".to_string()));
2914
2915 let ctx = test_producer_ctx();
2916 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2917 producer
2918 .call(Exchange::new(Message::new("b")))
2919 .await
2920 .unwrap();
2921 producer
2922 .call(Exchange::new(Message::new("a")))
2923 .await
2924 .unwrap();
2925
2926 inner.assert_satisfied().await;
2929 }
2930
2931 #[tokio::test]
2932 async fn uri_fail_fast_overrides_latching() {
2933 use std::panic::AssertUnwindSafe;
2934
2935 let component = MockComponent::new();
2936 let _endpoint = component
2937 .create_endpoint("mock:tight?failFast=true", &NoOpComponentContext)
2938 .unwrap();
2939 let inner = component.get_endpoint("tight").unwrap();
2940
2941 inner.expect_count(1);
2942 let _payload = AssertUnwindSafe(inner.assert_satisfied())
2945 .catch_unwind()
2946 .await
2947 .expect_err("assert_satisfied should panic on unmet expect_count");
2948 assert!(
2949 inner.fail_fast_error().is_some(),
2950 "failFast=true from URI must latch the mismatch (component default false leaves None)"
2951 );
2952 }
2953
2954 #[tokio::test]
2955 async fn uri_absent_params_fallback_to_config() {
2956 use std::panic::AssertUnwindSafe;
2957
2958 let config = MockConfig {
2959 fail_fast: true,
2960 ..Default::default()
2961 };
2962 let component = MockComponent::with_config(config);
2963 let _endpoint = component
2964 .create_endpoint("mock:audit", &NoOpComponentContext)
2965 .unwrap();
2966 let inner = component.get_endpoint("audit").unwrap();
2967
2968 inner.expect_count(1);
2969 let _payload = AssertUnwindSafe(inner.assert_satisfied())
2972 .catch_unwind()
2973 .await
2974 .expect_err("assert_satisfied should panic on unmet expect_count");
2975 assert!(
2976 inner.fail_fast_error().is_some(),
2977 "component-level fail_fast=true must apply when the URI param is absent"
2978 );
2979
2980 let _fresh = component
2983 .create_endpoint("mock:audit-fresh", &NoOpComponentContext)
2984 .unwrap();
2985 let fresh = component.get_endpoint("audit-fresh").unwrap();
2986 let result = fresh.try_assert_satisfied().await;
2987 assert!(result.is_ok(), "no expectations set, got: {result:?}");
2988 }
2989
2990 #[test]
2991 fn uri_malformed_numeric_rejected() {
2992 let component = MockComponent::new();
2993 let err = component
2994 .create_endpoint("mock:x?retain=abc", &NoOpComponentContext)
2995 .err()
2996 .expect("retain=abc must be rejected");
2997 let msg = err.to_string();
2998 assert!(
2999 msg.contains("retain"),
3000 "message should name 'retain', got: {msg}"
3001 );
3002 }
3003
3004 #[test]
3005 fn uri_malformed_expected_count_rejected() {
3006 let component = MockComponent::new();
3007 let err = component
3008 .create_endpoint("mock:x?expectedCount=abc", &NoOpComponentContext)
3009 .err()
3010 .expect("expectedCount=abc must be rejected");
3011 let msg = err.to_string();
3012 assert!(
3013 msg.contains("expectedCount"),
3014 "message should name 'expectedCount', got: {msg}"
3015 );
3016 }
3017
3018 #[test]
3019 fn uri_zero_retain_rejected() {
3020 let component = MockComponent::new();
3021 let err = component
3022 .create_endpoint("mock:x?retain=0", &NoOpComponentContext)
3023 .err()
3024 .expect("retain=0 must be rejected");
3025 let msg = err.to_string();
3026 assert!(
3027 msg.contains("retain"),
3028 "message should name 'retain', got: {msg}"
3029 );
3030 assert!(
3031 msg.contains(">= 1"),
3032 "message should state the >= 1 constraint, got: {msg}"
3033 );
3034 }
3035
3036 #[test]
3037 fn uri_malformed_boolean_rejected() {
3038 let component = MockComponent::new();
3039 let err = component
3040 .create_endpoint("mock:x?copy=maybe", &NoOpComponentContext)
3041 .err()
3042 .expect("copy=maybe must be rejected");
3043 let msg = err.to_string();
3044 assert!(
3045 msg.contains("copy"),
3046 "message should name 'copy', got: {msg}"
3047 );
3048 }
3049
3050 #[tokio::test]
3051 async fn uri_first_creation_wins_on_conflict() {
3052 let component = MockComponent::new();
3053 let _first = component
3054 .create_endpoint("mock:single?retain=5", &NoOpComponentContext)
3055 .unwrap();
3056 let second = component
3057 .create_endpoint("mock:single?retain=100", &NoOpComponentContext)
3058 .unwrap();
3059
3060 let ctx = test_producer_ctx();
3061 let mut producer = second.create_producer(rt(), &ctx).unwrap();
3062 for i in 0..7 {
3063 producer
3064 .call(Exchange::new(Message::new(format!("m{i}"))))
3065 .await
3066 .unwrap();
3067 }
3068 let inner = component.get_endpoint("single").unwrap();
3069 assert_eq!(
3070 inner.received_count().await,
3071 5,
3072 "first creation's retain=5 must still bind; second creation must not reconfigure"
3073 );
3074 }
3075
3076 #[test]
3077 fn catalog_parity_five_params() {
3078 let meta = MockConfig::metadata();
3079 let mut names: Vec<&str> = meta.uri_options.iter().map(|o| o.name.as_str()).collect();
3080 names.sort();
3081 assert_eq!(
3082 names,
3083 ["anyOrder", "copy", "expectedCount", "failFast", "retain"],
3084 "metadata uri_options names must match parser keys"
3085 );
3086
3087 for opt in &meta.uri_options {
3093 assert!(
3094 !opt.required,
3095 "{} must be optional (falls back to MockConfig)",
3096 opt.name
3097 );
3098 }
3099 }
3100
3101 #[tokio::test]
3106 async fn expected_count_never_rejects_live_exchanges() {
3107 let component = MockComponent::new();
3108 let endpoint = component
3109 .create_endpoint(
3110 "mock:sink?expectedCount=2&failFast=true",
3111 &NoOpComponentContext,
3112 )
3113 .unwrap();
3114 let ctx = test_producer_ctx();
3115 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
3116 for i in 0..7 {
3117 let result = producer
3118 .call(Exchange::new(Message::new(format!("m{i}"))))
3119 .await;
3120 assert!(
3121 result.is_ok(),
3122 "live exchange {i} must not be rejected by expectedCount"
3123 );
3124 }
3125 let inner = component.get_endpoint("sink").unwrap();
3126 assert_eq!(inner.received_count().await, 7);
3127 assert!(
3128 inner.fail_fast_error().is_none(),
3129 "expectedCount alone must never trip the fail-fast latch before an assertion runs"
3130 );
3131 }
3132
3133 #[tokio::test]
3134 async fn expected_count_enforced_only_at_assertion() {
3135 let component = MockComponent::new();
3136 let endpoint = component
3137 .create_endpoint("mock:sink?expectedCount=2", &NoOpComponentContext)
3138 .unwrap();
3139 let ctx = test_producer_ctx();
3140 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
3141 for i in 0..3 {
3142 producer
3143 .call(Exchange::new(Message::new(format!("m{i}"))))
3144 .await
3145 .unwrap();
3146 }
3147 let inner = component.get_endpoint("sink").unwrap();
3148 let result = inner.try_assert_satisfied().await;
3149 assert!(
3150 result.is_err(),
3151 "expectedCount=2 vs 3 received must fail at assertion time, got: {result:?}"
3152 );
3153 }
3154
3155 #[tokio::test]
3156 async fn failed_assertion_then_applies_normal_fail_fast() {
3157 use camel_component_api::CamelError;
3158
3159 let component = MockComponent::new();
3160 let endpoint = component
3161 .create_endpoint(
3162 "mock:sink?expectedCount=2&failFast=true",
3163 &NoOpComponentContext,
3164 )
3165 .unwrap();
3166 let ctx = test_producer_ctx();
3167 let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
3168 for i in 0..3 {
3169 producer
3170 .call(Exchange::new(Message::new(format!("m{i}"))))
3171 .await
3172 .unwrap();
3173 }
3174 let inner = component.get_endpoint("sink").unwrap();
3175 assert!(
3176 inner.try_assert_satisfied().await.is_err(),
3177 "2-vs-3 mismatch must Err and trip the fail-fast latch"
3178 );
3179 let result = producer.call(Exchange::default()).await;
3180 match result {
3181 Err(CamelError::ProcessorError(msg)) => assert!(
3182 msg.contains("fail-fast mode"),
3183 "fixed fail-fast message expected, got: {msg}"
3184 ),
3185 other => panic!("expected ProcessorError, got {other:?}"),
3186 }
3187 }
3188
3189 #[tokio::test]
3190 async fn expected_count_not_reset_on_second_creation() {
3191 let component = MockComponent::new();
3192 let _first = component
3193 .create_endpoint("mock:once", &NoOpComponentContext)
3194 .unwrap();
3195 let second = component
3196 .create_endpoint("mock:once?expectedCount=5", &NoOpComponentContext)
3197 .unwrap();
3198 let ctx = test_producer_ctx();
3199 let mut producer = second.create_producer(rt(), &ctx).unwrap();
3200 for i in 0..2 {
3201 producer
3202 .call(Exchange::new(Message::new(format!("m{i}"))))
3203 .await
3204 .unwrap();
3205 }
3206 let inner = component.get_endpoint("once").unwrap();
3207 let result = inner.try_assert_satisfied().await;
3208 assert!(
3209 result.is_ok(),
3210 "first creation registered no count expectation; second creation must not \
3211 reconfigure it, got: {result:?}"
3212 );
3213 }
3214}