1use std::collections::VecDeque;
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::Arc;
12use std::task::{Context, Poll};
13
14use tokio::sync::{Mutex, Notify};
15use tower::Service;
16
17use camel_component_api::{BoxProcessor, CamelError, Exchange};
18use camel_component_api::{Consumer, Endpoint, ProducerContext, RuntimeObservability};
19use tracing::debug;
20
21use crate::MockAssertionError;
22use crate::MockExpectations;
23use crate::matcher::{BodyMatcher, HeaderMatcher};
24
25pub struct MockEndpoint(pub(crate) Arc<MockEndpointInner>);
35
36pub struct MockEndpointInner {
42 pub(crate) uri: String,
43 pub(crate) name: String,
44 pub(crate) received: Arc<Mutex<VecDeque<Exchange>>>,
45 pub(crate) notify: Arc<Notify>,
46 pub(crate) max_retained: usize,
47 pub(crate) copy_on_exchange: bool,
48 pub(crate) fail_fast: bool,
49 pub(crate) fail_fast_error: Arc<std::sync::Mutex<Option<CamelError>>>,
50 pub(crate) assert_period_ms: u64,
51 pub(crate) any_order: bool,
52 pub(crate) expectations: Arc<std::sync::Mutex<MockExpectations>>,
53}
54
55impl MockEndpointInner {
56 pub async fn get_received_exchanges(&self) -> Vec<Exchange> {
58 self.received.lock().await.iter().cloned().collect()
59 }
60
61 pub async fn received_count(&self) -> usize {
63 self.received.lock().await.len()
64 }
65
66 pub async fn reset(&self) {
70 self.received.lock().await.clear();
71 let mut guard = self
72 .fail_fast_error
73 .lock()
74 .expect("fail_fast_error lock poisoned"); *guard = None;
76 }
77
78 pub async fn assert_exchange_count(&self, expected: usize) {
84 let actual = self.received.lock().await.len();
85 assert_eq!(
86 actual, expected,
87 "MockEndpoint expected {expected} exchanges, got {actual}"
88 );
89 }
90
91 pub async fn await_exchanges(&self, count: usize, timeout: std::time::Duration) {
100 let deadline = tokio::time::Instant::now() + timeout;
101 loop {
102 {
103 let received = self.received.lock().await;
104 if received.len() >= count {
105 return;
106 }
107 }
108 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
109 if remaining.is_zero() {
110 let got = self.received.lock().await.len();
113 if got >= count {
114 return;
115 }
116 panic!(
117 "MockEndpoint '{}': timed out waiting for {} exchanges (got {} after {:?})",
118 self.name, count, got, timeout
119 );
120 }
121 tokio::select! {
122 _ = self.notify.notified() => {}
123 _ = tokio::time::sleep(remaining) => {}
124 }
125 }
126 }
127
128 pub async fn await_exchanges_with_timeout(&self, count: usize, fallback: std::time::Duration) {
133 let duration = if self.assert_period_ms > 0 {
134 std::time::Duration::from_millis(self.assert_period_ms)
135 } else {
136 fallback
137 };
138 self.await_exchanges(count, duration).await;
139 }
140
141 pub fn exchange(&self, idx: usize) -> ExchangeAssert {
154 if let Ok(handle) = tokio::runtime::Handle::try_current()
155 && handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::CurrentThread
156 {
157 panic!(
158 "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()",
159 self.name
160 );
161 }
162 let received = tokio::task::block_in_place(|| self.received.blocking_lock());
163 if idx >= received.len() {
164 panic!(
165 "MockEndpoint '{}': exchange index {} out of bounds (got {} exchanges)",
166 self.name,
167 idx,
168 received.len()
169 );
170 }
171 ExchangeAssert {
172 exchange: received[idx].clone(),
173 idx,
174 endpoint_name: self.name.clone(),
175 }
176 }
177
178 pub fn expect_count(&self, n: usize) {
181 let mut guard = self
182 .expectations
183 .lock()
184 .expect("expectations lock poisoned"); guard.set_expected_count(n);
186 }
187
188 pub fn expect_minimum_count(&self, n: usize) {
191 let mut guard = self
192 .expectations
193 .lock()
194 .expect("expectations lock poisoned"); guard.set_minimum_count(n);
196 }
197
198 pub fn expect_body(&self, body: camel_component_api::Body) {
200 let mut guard = self
201 .expectations
202 .lock()
203 .expect("expectations lock poisoned"); guard.push_body(body);
205 }
206
207 pub fn expect_body_matcher(&self, matcher: BodyMatcher) {
213 let mut guard = self
214 .expectations
215 .lock()
216 .expect("expectations lock poisoned"); guard.push_body_matcher(matcher);
218 }
219
220 pub fn expect_header(&self, key: &str, value: impl Into<serde_json::Value>) {
222 let mut guard = self
223 .expectations
224 .lock()
225 .expect("expectations lock poisoned"); guard.push_header(key.to_string(), value.into());
227 }
228
229 pub fn expect_header_regex(&self, key: &str, pattern: &str) {
234 let mut guard = self
235 .expectations
236 .lock()
237 .expect("expectations lock poisoned"); guard.push_header_regex(key.to_string(), pattern.to_string());
239 }
240
241 pub fn expect_header_matcher(&self, key: &str, matcher: HeaderMatcher) {
248 let mut guard = self
249 .expectations
250 .lock()
251 .expect("expectations lock poisoned"); guard.push_header_matcher(key.to_string(), matcher);
253 }
254
255 pub async fn assert_satisfied(&self) {
267 if let Err(e) = self.evaluate_expectations().await {
268 panic!("{e}");
269 }
270 }
271
272 #[allow(clippy::result_large_err)]
288 pub async fn try_assert_satisfied(&self) -> Result<(), MockAssertionError> {
289 self.evaluate_expectations().await
290 }
291
292 pub fn fail_fast_error(&self) -> Option<CamelError> {
294 let guard = self
295 .fail_fast_error
296 .lock()
297 .expect("fail_fast_error lock poisoned"); guard.clone()
299 }
300
301 pub fn trigger_fail_fast(&self, error: CamelError) {
310 let mut guard = self
311 .fail_fast_error
312 .lock()
313 .expect("fail_fast_error lock poisoned"); *guard = Some(error);
315 }
316
317 pub(crate) fn set_fail_fast_on_mismatch(&self) {
323 if self.fail_fast {
324 let mut guard = self
325 .fail_fast_error
326 .lock()
327 .expect("fail_fast_error lock poisoned"); *guard = Some(CamelError::ProcessorError(
329 "assert_satisfied expectation mismatch".to_string(),
330 ));
331 }
332 }
333}
334
335impl Endpoint for MockEndpoint {
336 fn uri(&self) -> &str {
337 &self.0.uri
338 }
339
340 fn create_consumer(
341 &self,
342 _rt: Arc<dyn RuntimeObservability>,
343 ) -> Result<Box<dyn Consumer>, CamelError> {
344 Err(CamelError::EndpointCreationFailed(
345 "mock endpoint does not support consumers (it is a sink)".to_string(),
346 ))
347 }
348
349 fn create_producer(
350 &self,
351 _rt: Arc<dyn RuntimeObservability>,
352 _ctx: &ProducerContext,
353 ) -> Result<BoxProcessor, CamelError> {
354 Ok(BoxProcessor::new(MockProducer {
355 name: self.0.name.clone(),
356 received: Arc::clone(&self.0.received),
357 notify: Arc::clone(&self.0.notify),
358 max_retained: self.0.max_retained,
359 copy_on_exchange: self.0.copy_on_exchange,
360 fail_fast: self.0.fail_fast,
361 fail_fast_error: Arc::clone(&self.0.fail_fast_error),
362 }))
363 }
364}
365
366#[derive(Clone)]
372struct MockProducer {
373 name: String,
374 received: Arc<Mutex<VecDeque<Exchange>>>,
375 notify: Arc<Notify>,
376 max_retained: usize,
377 copy_on_exchange: bool,
378 fail_fast: bool,
379 fail_fast_error: Arc<std::sync::Mutex<Option<CamelError>>>,
380}
381
382impl Service<Exchange> for MockProducer {
383 type Response = Exchange;
384 type Error = CamelError;
385 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
386
387 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
388 if self.fail_fast
390 && let Ok(guard) = self.fail_fast_error.lock()
391 && guard.is_some()
392 {
393 return Poll::Ready(Err(CamelError::ProcessorError(
394 "mock endpoint in fail-fast mode: a previous exchange caused an error".to_string(),
395 )));
396 }
397 Poll::Ready(Ok(()))
398 }
399
400 fn call(&mut self, exchange: Exchange) -> Self::Future {
401 let name = self.name.clone();
402 let received = Arc::clone(&self.received);
403 let notify = Arc::clone(&self.notify);
404 let max_retained = self.max_retained;
405 let copy_on_exchange = self.copy_on_exchange;
406 let fail_fast = self.fail_fast;
407 let fail_fast_error = Arc::clone(&self.fail_fast_error);
408 Box::pin(async move {
409 if fail_fast
411 && let Ok(guard) = fail_fast_error.lock()
412 && guard.is_some()
413 {
414 return Err(CamelError::ProcessorError(
415 "mock endpoint in fail-fast mode: a previous exchange caused an error"
416 .to_string(),
417 ));
418 }
419
420 let correlation_id = exchange
421 .input
422 .headers
423 .get("CamelCorrelationId")
424 .and_then(|v| v.as_str())
425 .map(|s| s.to_string());
426
427 let exchange_to_store = if copy_on_exchange {
428 let mut cloned = exchange.clone();
429 cloned.input.body = clone_body(&exchange.input.body);
431 cloned
432 } else {
433 exchange.clone()
434 };
435
436 let mut guard = received.lock().await;
437 if guard.len() >= max_retained {
438 tracing::warn!(
439 endpoint_name = %name,
440 max = max_retained,
441 "max retained exchanges reached, dropping oldest"
442 );
443 guard.pop_front();
444 }
445 guard.push_back(exchange_to_store);
446 let count = guard.len();
447 drop(guard);
448
449 debug!(
450 endpoint_name = %name,
451 count = %count,
452 correlation_id = correlation_id.as_deref().unwrap_or("none"),
453 "exchange recorded on mock"
454 );
455 notify.notify_waiters();
456
457 Ok(exchange)
458 })
459 }
460}
461
462pub(crate) fn clone_body(body: &camel_component_api::Body) -> camel_component_api::Body {
464 match body {
465 camel_component_api::Body::Empty => camel_component_api::Body::Empty,
466 camel_component_api::Body::Text(s) => camel_component_api::Body::Text(s.clone()),
467 camel_component_api::Body::Json(v) => camel_component_api::Body::Json(v.clone()),
468 camel_component_api::Body::Xml(s) => camel_component_api::Body::Xml(s.clone()),
469 camel_component_api::Body::Bytes(b) => camel_component_api::Body::Bytes(b.clone()),
470 camel_component_api::Body::Stream(s) => camel_component_api::Body::Stream(s.clone()),
471 _ => camel_component_api::Body::Empty,
474 }
475}
476
477pub struct ExchangeAssert {
489 exchange: Exchange,
490 idx: usize,
491 endpoint_name: String,
492}
493
494impl ExchangeAssert {
495 fn location(&self) -> String {
496 format!(
497 "MockEndpoint '{}' exchange[{}]",
498 self.endpoint_name, self.idx
499 )
500 }
501
502 pub fn assert_body_text(self, expected: &str) -> Self {
504 match self.exchange.input.body.as_text() {
505 Some(actual) if actual == expected => {}
506 Some(actual) => panic!(
507 "{}: expected body text {:?}, got {:?}",
508 self.location(),
509 expected,
510 actual
511 ),
512 None => panic!(
513 "{}: expected body text {:?}, but body is not Body::Text (got {:?})",
514 self.location(),
515 expected,
516 self.exchange.input.body
517 ),
518 }
519 self
520 }
521
522 pub fn assert_body_json(self, expected: serde_json::Value) -> Self {
524 match &self.exchange.input.body {
525 camel_component_api::Body::Json(actual) if *actual == expected => {}
526 camel_component_api::Body::Json(actual) => panic!(
527 "{}: expected body JSON {}, got {}",
528 self.location(),
529 expected,
530 actual
531 ),
532 other => panic!(
533 "{}: expected body JSON {}, but body is not Body::Json (got {:?})",
534 self.location(),
535 expected,
536 other
537 ),
538 }
539 self
540 }
541
542 pub fn assert_body_bytes(self, expected: &[u8]) -> Self {
544 match &self.exchange.input.body {
545 camel_component_api::Body::Bytes(actual) if actual.as_ref() == expected => {}
546 camel_component_api::Body::Bytes(actual) => panic!(
547 "{}: expected body bytes {:?}, got {:?}",
548 self.location(),
549 expected,
550 actual
551 ),
552 other => panic!(
553 "{}: expected body bytes {:?}, but body is not Body::Bytes (got {:?})",
554 self.location(),
555 expected,
556 other
557 ),
558 }
559 self
560 }
561
562 pub fn assert_header(self, key: &str, expected: serde_json::Value) -> Self {
568 match self.exchange.input.headers.get(key) {
569 Some(actual) if *actual == expected => {}
570 Some(actual) => panic!(
571 "{}: expected header {:?} = {}, got {}",
572 self.location(),
573 key,
574 expected,
575 actual
576 ),
577 None => panic!(
578 "{}: expected header {:?} = {}, but header is absent",
579 self.location(),
580 key,
581 expected
582 ),
583 }
584 self
585 }
586
587 pub fn assert_header_exists(self, key: &str) -> Self {
593 if !self.exchange.input.headers.contains_key(key) {
594 panic!(
595 "{}: expected header {:?} to be present, but it was absent",
596 self.location(),
597 key
598 );
599 }
600 self
601 }
602
603 pub fn assert_has_error(self) -> Self {
609 if self.exchange.error.is_none() {
610 panic!(
611 "{}: expected exchange to have an error, but error is None",
612 self.location()
613 );
614 }
615 self
616 }
617
618 pub fn assert_no_error(self) -> Self {
624 if let Some(ref err) = self.exchange.error {
625 panic!(
626 "{}: expected exchange to have no error, but got: {}",
627 self.location(),
628 err
629 );
630 }
631 self
632 }
633}