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;
23
24pub struct MockEndpoint(pub(crate) Arc<MockEndpointInner>);
34
35pub struct MockEndpointInner {
41 pub(crate) uri: String,
42 pub(crate) name: String,
43 pub(crate) received: Arc<Mutex<VecDeque<Exchange>>>,
44 pub(crate) notify: Arc<Notify>,
45 pub(crate) max_retained: usize,
46 pub(crate) copy_on_exchange: bool,
47 pub(crate) fail_fast: bool,
48 pub(crate) fail_fast_error: Arc<std::sync::Mutex<Option<CamelError>>>,
49 pub(crate) assert_period_ms: u64,
50 pub(crate) any_order: bool,
51 pub(crate) expectations: Arc<std::sync::Mutex<MockExpectations>>,
52}
53
54impl MockEndpointInner {
55 pub async fn get_received_exchanges(&self) -> Vec<Exchange> {
57 self.received.lock().await.iter().cloned().collect()
58 }
59
60 pub async fn received_count(&self) -> usize {
62 self.received.lock().await.len()
63 }
64
65 pub async fn reset(&self) {
69 self.received.lock().await.clear();
70 let mut guard = self
71 .fail_fast_error
72 .lock()
73 .expect("fail_fast_error lock poisoned"); *guard = None;
75 }
76
77 pub async fn assert_exchange_count(&self, expected: usize) {
83 let actual = self.received.lock().await.len();
84 assert_eq!(
85 actual, expected,
86 "MockEndpoint expected {expected} exchanges, got {actual}"
87 );
88 }
89
90 pub async fn await_exchanges(&self, count: usize, timeout: std::time::Duration) {
99 let deadline = tokio::time::Instant::now() + timeout;
100 loop {
101 {
102 let received = self.received.lock().await;
103 if received.len() >= count {
104 return;
105 }
106 }
107 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
108 if remaining.is_zero() {
109 let got = self.received.lock().await.len();
112 if got >= count {
113 return;
114 }
115 panic!(
116 "MockEndpoint '{}': timed out waiting for {} exchanges (got {} after {:?})",
117 self.name, count, got, timeout
118 );
119 }
120 tokio::select! {
121 _ = self.notify.notified() => {}
122 _ = tokio::time::sleep(remaining) => {}
123 }
124 }
125 }
126
127 pub async fn await_exchanges_with_timeout(&self, count: usize, fallback: std::time::Duration) {
132 let duration = if self.assert_period_ms > 0 {
133 std::time::Duration::from_millis(self.assert_period_ms)
134 } else {
135 fallback
136 };
137 self.await_exchanges(count, duration).await;
138 }
139
140 pub fn exchange(&self, idx: usize) -> ExchangeAssert {
153 if let Ok(handle) = tokio::runtime::Handle::try_current()
154 && handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::CurrentThread
155 {
156 panic!(
157 "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()",
158 self.name
159 );
160 }
161 let received = tokio::task::block_in_place(|| self.received.blocking_lock());
162 if idx >= received.len() {
163 panic!(
164 "MockEndpoint '{}': exchange index {} out of bounds (got {} exchanges)",
165 self.name,
166 idx,
167 received.len()
168 );
169 }
170 ExchangeAssert {
171 exchange: received[idx].clone(),
172 idx,
173 endpoint_name: self.name.clone(),
174 }
175 }
176
177 pub fn expect_count(&self, n: usize) {
180 let mut guard = self
181 .expectations
182 .lock()
183 .expect("expectations lock poisoned"); guard.set_expected_count(n);
185 }
186
187 pub fn expect_minimum_count(&self, n: usize) {
190 let mut guard = self
191 .expectations
192 .lock()
193 .expect("expectations lock poisoned"); guard.set_minimum_count(n);
195 }
196
197 pub fn expect_body(&self, body: camel_component_api::Body) {
199 let mut guard = self
200 .expectations
201 .lock()
202 .expect("expectations lock poisoned"); guard.push_body(body);
204 }
205
206 pub fn expect_header(&self, key: &str, value: impl Into<serde_json::Value>) {
208 let mut guard = self
209 .expectations
210 .lock()
211 .expect("expectations lock poisoned"); guard.push_header(key.to_string(), value.into());
213 }
214
215 pub fn expect_header_regex(&self, key: &str, pattern: &str) {
220 let mut guard = self
221 .expectations
222 .lock()
223 .expect("expectations lock poisoned"); guard.push_header_regex(key.to_string(), pattern.to_string());
225 }
226
227 pub async fn assert_satisfied(&self) {
238 if let Err(e) = self.evaluate_expectations().await {
239 panic!("{e}");
240 }
241 }
242
243 #[allow(clippy::result_large_err)]
259 pub async fn try_assert_satisfied(&self) -> Result<(), MockAssertionError> {
260 self.evaluate_expectations().await
261 }
262
263 pub fn fail_fast_error(&self) -> Option<CamelError> {
265 let guard = self
266 .fail_fast_error
267 .lock()
268 .expect("fail_fast_error lock poisoned"); guard.clone()
270 }
271
272 pub fn trigger_fail_fast(&self, error: CamelError) {
281 let mut guard = self
282 .fail_fast_error
283 .lock()
284 .expect("fail_fast_error lock poisoned"); *guard = Some(error);
286 }
287
288 pub(crate) fn set_fail_fast_on_mismatch(&self) {
294 if self.fail_fast {
295 let mut guard = self
296 .fail_fast_error
297 .lock()
298 .expect("fail_fast_error lock poisoned"); *guard = Some(CamelError::ProcessorError(
300 "assert_satisfied expectation mismatch".to_string(),
301 ));
302 }
303 }
304}
305
306impl Endpoint for MockEndpoint {
307 fn uri(&self) -> &str {
308 &self.0.uri
309 }
310
311 fn create_consumer(
312 &self,
313 _rt: Arc<dyn RuntimeObservability>,
314 ) -> Result<Box<dyn Consumer>, CamelError> {
315 Err(CamelError::EndpointCreationFailed(
316 "mock endpoint does not support consumers (it is a sink)".to_string(),
317 ))
318 }
319
320 fn create_producer(
321 &self,
322 _rt: Arc<dyn RuntimeObservability>,
323 _ctx: &ProducerContext,
324 ) -> Result<BoxProcessor, CamelError> {
325 Ok(BoxProcessor::new(MockProducer {
326 name: self.0.name.clone(),
327 received: Arc::clone(&self.0.received),
328 notify: Arc::clone(&self.0.notify),
329 max_retained: self.0.max_retained,
330 copy_on_exchange: self.0.copy_on_exchange,
331 fail_fast: self.0.fail_fast,
332 fail_fast_error: Arc::clone(&self.0.fail_fast_error),
333 }))
334 }
335}
336
337#[derive(Clone)]
343struct MockProducer {
344 name: String,
345 received: Arc<Mutex<VecDeque<Exchange>>>,
346 notify: Arc<Notify>,
347 max_retained: usize,
348 copy_on_exchange: bool,
349 fail_fast: bool,
350 fail_fast_error: Arc<std::sync::Mutex<Option<CamelError>>>,
351}
352
353impl Service<Exchange> for MockProducer {
354 type Response = Exchange;
355 type Error = CamelError;
356 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
357
358 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
359 if self.fail_fast
361 && let Ok(guard) = self.fail_fast_error.lock()
362 && guard.is_some()
363 {
364 return Poll::Ready(Err(CamelError::ProcessorError(
365 "mock endpoint in fail-fast mode: a previous exchange caused an error".to_string(),
366 )));
367 }
368 Poll::Ready(Ok(()))
369 }
370
371 fn call(&mut self, exchange: Exchange) -> Self::Future {
372 let name = self.name.clone();
373 let received = Arc::clone(&self.received);
374 let notify = Arc::clone(&self.notify);
375 let max_retained = self.max_retained;
376 let copy_on_exchange = self.copy_on_exchange;
377 let fail_fast = self.fail_fast;
378 let fail_fast_error = Arc::clone(&self.fail_fast_error);
379 Box::pin(async move {
380 if fail_fast
382 && let Ok(guard) = fail_fast_error.lock()
383 && guard.is_some()
384 {
385 return Err(CamelError::ProcessorError(
386 "mock endpoint in fail-fast mode: a previous exchange caused an error"
387 .to_string(),
388 ));
389 }
390
391 let correlation_id = exchange
392 .input
393 .headers
394 .get("CamelCorrelationId")
395 .and_then(|v| v.as_str())
396 .map(|s| s.to_string());
397
398 let exchange_to_store = if copy_on_exchange {
399 let mut cloned = exchange.clone();
400 cloned.input.body = clone_body(&exchange.input.body);
402 cloned
403 } else {
404 exchange.clone()
405 };
406
407 let mut guard = received.lock().await;
408 if guard.len() >= max_retained {
409 tracing::warn!(
410 endpoint_name = %name,
411 max = max_retained,
412 "max retained exchanges reached, dropping oldest"
413 );
414 guard.pop_front();
415 }
416 guard.push_back(exchange_to_store);
417 let count = guard.len();
418 drop(guard);
419
420 debug!(
421 endpoint_name = %name,
422 count = %count,
423 correlation_id = correlation_id.as_deref().unwrap_or("none"),
424 "exchange recorded on mock"
425 );
426 notify.notify_waiters();
427
428 Ok(exchange)
429 })
430 }
431}
432
433pub(crate) fn clone_body(body: &camel_component_api::Body) -> camel_component_api::Body {
435 match body {
436 camel_component_api::Body::Empty => camel_component_api::Body::Empty,
437 camel_component_api::Body::Text(s) => camel_component_api::Body::Text(s.clone()),
438 camel_component_api::Body::Json(v) => camel_component_api::Body::Json(v.clone()),
439 camel_component_api::Body::Xml(s) => camel_component_api::Body::Xml(s.clone()),
440 camel_component_api::Body::Bytes(b) => camel_component_api::Body::Bytes(b.clone()),
441 camel_component_api::Body::Stream(s) => camel_component_api::Body::Stream(s.clone()),
442 _ => camel_component_api::Body::Empty,
445 }
446}
447
448pub struct ExchangeAssert {
460 exchange: Exchange,
461 idx: usize,
462 endpoint_name: String,
463}
464
465impl ExchangeAssert {
466 fn location(&self) -> String {
467 format!(
468 "MockEndpoint '{}' exchange[{}]",
469 self.endpoint_name, self.idx
470 )
471 }
472
473 pub fn assert_body_text(self, expected: &str) -> Self {
475 match self.exchange.input.body.as_text() {
476 Some(actual) if actual == expected => {}
477 Some(actual) => panic!(
478 "{}: expected body text {:?}, got {:?}",
479 self.location(),
480 expected,
481 actual
482 ),
483 None => panic!(
484 "{}: expected body text {:?}, but body is not Body::Text (got {:?})",
485 self.location(),
486 expected,
487 self.exchange.input.body
488 ),
489 }
490 self
491 }
492
493 pub fn assert_body_json(self, expected: serde_json::Value) -> Self {
495 match &self.exchange.input.body {
496 camel_component_api::Body::Json(actual) if *actual == expected => {}
497 camel_component_api::Body::Json(actual) => panic!(
498 "{}: expected body JSON {}, got {}",
499 self.location(),
500 expected,
501 actual
502 ),
503 other => panic!(
504 "{}: expected body JSON {}, but body is not Body::Json (got {:?})",
505 self.location(),
506 expected,
507 other
508 ),
509 }
510 self
511 }
512
513 pub fn assert_body_bytes(self, expected: &[u8]) -> Self {
515 match &self.exchange.input.body {
516 camel_component_api::Body::Bytes(actual) if actual.as_ref() == expected => {}
517 camel_component_api::Body::Bytes(actual) => panic!(
518 "{}: expected body bytes {:?}, got {:?}",
519 self.location(),
520 expected,
521 actual
522 ),
523 other => panic!(
524 "{}: expected body bytes {:?}, but body is not Body::Bytes (got {:?})",
525 self.location(),
526 expected,
527 other
528 ),
529 }
530 self
531 }
532
533 pub fn assert_header(self, key: &str, expected: serde_json::Value) -> Self {
539 match self.exchange.input.headers.get(key) {
540 Some(actual) if *actual == expected => {}
541 Some(actual) => panic!(
542 "{}: expected header {:?} = {}, got {}",
543 self.location(),
544 key,
545 expected,
546 actual
547 ),
548 None => panic!(
549 "{}: expected header {:?} = {}, but header is absent",
550 self.location(),
551 key,
552 expected
553 ),
554 }
555 self
556 }
557
558 pub fn assert_header_exists(self, key: &str) -> Self {
564 if !self.exchange.input.headers.contains_key(key) {
565 panic!(
566 "{}: expected header {:?} to be present, but it was absent",
567 self.location(),
568 key
569 );
570 }
571 self
572 }
573
574 pub fn assert_has_error(self) -> Self {
580 if self.exchange.error.is_none() {
581 panic!(
582 "{}: expected exchange to have an error, but error is None",
583 self.location()
584 );
585 }
586 self
587 }
588
589 pub fn assert_no_error(self) -> Self {
595 if let Some(ref err) = self.exchange.error {
596 panic!(
597 "{}: expected exchange to have no error, but got: {}",
598 self.location(),
599 err
600 );
601 }
602 self
603 }
604}