Skip to main content

camel_component_mock/
lib.rs

1//! # camel-component-mock
2//!
3//! Mock component for rust-camel — testing utility that records received
4//! exchanges for later assertion, useful for verifying route output in tests.
5//!
6//! Main types: `MockComponent`, `MockEndpoint`, `MockProducer`, `MockExpectations`.
7//!
8//! # Example
9//!
10//! ```rust,no_run
11//! use camel_component_mock::MockComponent;
12//! use camel_component_api::{Component, NoOpComponentContext, Exchange, Message};
13//!
14//! // Create a mock component and endpoint
15//! let component = MockComponent::new();
16//! let endpoint = component
17//!     .create_endpoint("mock:result", &NoOpComponentContext)
18//!     .unwrap();
19//!
20//! // In a real route, the producer would be used as a Tower service.
21//! // After sending exchanges, you can inspect them:
22//! let inner = component.get_endpoint("result").unwrap();
23//! // inner.assert_exchange_count(1).await;
24//! // inner.exchange(0).assert_body_text("hello");
25//! ```
26
27use std::collections::{HashMap, VecDeque};
28use std::future::Future;
29use std::pin::Pin;
30use std::sync::Arc;
31use std::task::{Context, Poll};
32
33use tokio::sync::{Mutex, Notify};
34use tower::Service;
35
36use camel_api::component_metadata::ComponentMetadata;
37use camel_component_api::UriConfig;
38use camel_component_api::parse_uri;
39use camel_component_api::{BoxProcessor, CamelError, Exchange};
40use camel_component_api::{Component, Consumer, Endpoint, ProducerContext, RuntimeObservability};
41use tracing::debug;
42
43/// Default maximum number of exchanges retained by a mock endpoint.
44const DEFAULT_MAX_RETAINED: usize = 10_000;
45
46// ---------------------------------------------------------------------------
47// MockConfig
48// ---------------------------------------------------------------------------
49
50/// Configuration for [`MockComponent`].
51///
52/// Controls how many exchanges are retained before the oldest are dropped,
53/// and other behavioural flags for assertions.
54///
55/// # Examples
56///
57/// ```rust
58/// use camel_component_mock::MockConfig;
59///
60/// let config = MockConfig {
61///     max_retained: 100,
62///     copy_on_exchange: true,
63///     fail_fast: false,
64///     assert_period_ms: 0,
65///     any_order: false,
66/// };
67/// ```
68#[derive(Clone, Debug)]
69pub struct MockConfig {
70    /// Maximum number of exchanges to retain. When exceeded, the oldest
71    /// exchange is dropped. Defaults to 10 000.
72    pub max_retained: usize,
73    /// When `true`, clone the exchange body before storing it in the received
74    /// exchanges list. This prevents aliasing when the caller mutates the
75    /// original exchange after sending. Defaults to `false`.
76    pub copy_on_exchange: bool,
77    /// When `true`, after the first failing assertion the mock stops processing
78    /// exchanges and records the error. Defaults to `false`.
79    pub fail_fast: bool,
80    /// Time in milliseconds to wait before asserting expectations (to allow
81    /// async processing to complete). Defaults to `0` (no wait).
82    pub assert_period_ms: u64,
83    /// When `true`, [`MockEndpointInner::assert_satisfied`] matches expected
84    /// bodies in any order rather than strict sequence. Defaults to `false`.
85    pub any_order: bool,
86}
87
88/// Private container for macro-derived `metadata()`.
89///
90/// Mock has zero real URI params — empty `uri_options` is legitimate.
91/// This inner struct exists solely to anchor the metadata derivation.
92#[derive(Debug, Clone, UriConfig)]
93#[allow(dead_code)]
94#[uri_scheme = "mock"]
95#[uri_config(
96    skip_impl,
97    metadata(
98        scheme = "mock",
99        description = "Records exchanges for test assertions",
100        producer
101    ),
102    crate = "camel_component_api"
103)]
104struct MockUriConfig {
105    #[allow(dead_code)]
106    _name: String,
107}
108
109impl Default for MockConfig {
110    fn default() -> Self {
111        Self {
112            max_retained: DEFAULT_MAX_RETAINED,
113            copy_on_exchange: false,
114            fail_fast: false,
115            assert_period_ms: 0,
116            any_order: false,
117        }
118    }
119}
120
121impl MockConfig {
122    /// Create a config with a custom retention limit.
123    pub fn new(max_retained: usize) -> Self {
124        Self {
125            max_retained,
126            ..Self::default()
127        }
128    }
129
130    /// Component metadata for the mock scheme, derived from
131    /// `#[uri_config(metadata(..))]` on `MockUriConfig`.
132    pub fn metadata() -> ComponentMetadata {
133        MockUriConfig::metadata()
134    }
135}
136
137// ---------------------------------------------------------------------------
138// MockExpectations
139// ---------------------------------------------------------------------------
140
141/// Expectations set on a mock endpoint for batch-style assertion.
142///
143/// Use [`MockEndpointInner::expect_body`] and
144/// [`MockEndpointInner::expect_header`] to populate expectations, then call
145/// [`MockEndpointInner::assert_satisfied`] after exchanges have been received.
146pub struct MockExpectations {
147    expected_bodies: Vec<camel_component_api::Body>,
148    expected_headers: Vec<(String, serde_json::Value)>,
149    expected_header_regexes: Vec<(String, String)>,
150}
151
152impl Default for MockExpectations {
153    fn default() -> Self {
154        Self::new()
155    }
156}
157
158impl MockExpectations {
159    /// Create an empty set of expectations.
160    pub fn new() -> Self {
161        Self {
162            expected_bodies: Vec::new(),
163            expected_headers: Vec::new(),
164            expected_header_regexes: Vec::new(),
165        }
166    }
167
168    /// Add an expected body value.
169    pub fn push_body(&mut self, body: camel_component_api::Body) {
170        self.expected_bodies.push(body);
171    }
172
173    /// Add an expected header key-value pair.
174    pub fn push_header(&mut self, key: String, value: serde_json::Value) {
175        self.expected_headers.push((key, value));
176    }
177
178    /// Add an expected header regex pattern.
179    pub fn push_header_regex(&mut self, key: String, pattern: String) {
180        self.expected_header_regexes.push((key, pattern));
181    }
182}
183
184// ---------------------------------------------------------------------------
185// MockComponent
186// ---------------------------------------------------------------------------
187
188/// The Mock component is a testing utility that records every exchange it
189/// receives via its producer.  It exposes helpers to inspect and assert on
190/// the recorded exchanges.
191///
192/// URI format: `mock:name`
193///
194/// When `create_endpoint` is called multiple times with the same name, the
195/// returned endpoints share the same received-exchanges storage. This enables
196/// test assertions: create mock, register it, run routes, then inspect via
197/// `component.get_endpoint("name")`.
198#[derive(Clone)]
199pub struct MockComponent {
200    registry: Arc<std::sync::Mutex<HashMap<String, Arc<MockEndpointInner>>>>,
201    config: MockConfig,
202}
203
204impl MockComponent {
205    pub fn new() -> Self {
206        Self::with_config(MockConfig::default())
207    }
208
209    /// Create a `MockComponent` with a custom [`MockConfig`].
210    pub fn with_config(config: MockConfig) -> Self {
211        Self {
212            registry: Arc::new(std::sync::Mutex::new(HashMap::new())),
213            config,
214        }
215    }
216
217    /// Retrieve a previously created endpoint's inner data by name.
218    ///
219    /// This is the primary way to inspect recorded exchanges in tests.
220    pub fn get_endpoint(&self, name: &str) -> Option<Arc<MockEndpointInner>> {
221        let registry = self
222            .registry
223            .lock()
224            .expect("mutex poisoned: another thread panicked while holding this lock"); // allow-unwrap
225        registry.get(name).cloned()
226    }
227}
228
229impl Default for MockComponent {
230    fn default() -> Self {
231        Self::new()
232    }
233}
234
235impl Component for MockComponent {
236    fn scheme(&self) -> &str {
237        "mock"
238    }
239
240    fn metadata(&self) -> ComponentMetadata {
241        MockConfig::metadata()
242    }
243
244    fn create_endpoint(
245        &self,
246        uri: &str,
247        _ctx: &dyn camel_component_api::ComponentContext,
248    ) -> Result<Box<dyn Endpoint>, CamelError> {
249        let parts = parse_uri(uri)?;
250        if parts.scheme != "mock" {
251            return Err(CamelError::InvalidUri(format!(
252                "expected scheme 'mock', got '{}'",
253                parts.scheme
254            )));
255        }
256
257        let name = parts.path;
258        if name.is_empty() {
259            return Err(CamelError::InvalidUri(
260                "mock endpoint name must be non-empty (use 'mock:<name>')".to_string(),
261            ));
262        }
263        let mut registry = self.registry.lock().map_err(|e| {
264            CamelError::EndpointCreationFailed(format!("mock registry lock poisoned: {e}"))
265        })?;
266        let max_retained = self.config.max_retained;
267        let copy_on_exchange = self.config.copy_on_exchange;
268        let fail_fast = self.config.fail_fast;
269        let assert_period_ms = self.config.assert_period_ms;
270        let any_order = self.config.any_order;
271        let inner = registry
272            .entry(name.clone())
273            .or_insert_with(|| {
274                Arc::new(MockEndpointInner {
275                    uri: uri.to_string(),
276                    name,
277                    received: Arc::new(Mutex::new(VecDeque::new())),
278                    notify: Arc::new(Notify::new()),
279                    max_retained,
280                    copy_on_exchange,
281                    fail_fast,
282                    fail_fast_error: Arc::new(std::sync::Mutex::new(None)),
283                    assert_period_ms,
284                    any_order,
285                    expectations: Arc::new(std::sync::Mutex::new(MockExpectations::new())),
286                })
287            })
288            .clone();
289
290        debug!(endpoint_name = %inner.name, "mock endpoint created");
291        Ok(Box::new(MockEndpoint(inner)))
292    }
293}
294
295// ---------------------------------------------------------------------------
296// MockEndpoint / MockEndpointInner
297// ---------------------------------------------------------------------------
298
299/// A mock endpoint that records all exchanges sent to it.
300///
301/// This is a thin wrapper around `Arc<MockEndpointInner>`. Multiple
302/// `MockEndpoint` instances created with the same name share the same inner
303/// storage.
304pub struct MockEndpoint(Arc<MockEndpointInner>);
305
306/// The actual data behind a mock endpoint. Shared across all `MockEndpoint`
307/// instances created with the same name via `MockComponent`.
308///
309/// Use `get_received_exchanges` and `assert_exchange_count` to inspect
310/// recorded exchanges in tests.
311pub struct MockEndpointInner {
312    uri: String,
313    pub name: String,
314    received: Arc<Mutex<VecDeque<Exchange>>>,
315    notify: Arc<Notify>,
316    max_retained: usize,
317    copy_on_exchange: bool,
318    fail_fast: bool,
319    fail_fast_error: Arc<std::sync::Mutex<Option<CamelError>>>,
320    assert_period_ms: u64,
321    any_order: bool,
322    expectations: Arc<std::sync::Mutex<MockExpectations>>,
323}
324
325impl MockEndpointInner {
326    /// Return a snapshot of all exchanges retained so far.
327    pub async fn get_received_exchanges(&self) -> Vec<Exchange> {
328        self.received.lock().await.iter().cloned().collect()
329    }
330
331    /// Return the number of currently retained exchanges.
332    pub async fn received_count(&self) -> usize {
333        self.received.lock().await.len()
334    }
335
336    /// Clear all retained exchanges and reset internal counters.
337    ///
338    /// Useful between test cases to reuse the same mock endpoint.
339    pub async fn reset(&self) {
340        self.received.lock().await.clear();
341        if let Ok(mut guard) = self.fail_fast_error.lock() {
342            *guard = None;
343        }
344    }
345
346    /// Assert that exactly `expected` exchanges have been received.
347    ///
348    /// # Panics
349    ///
350    /// Panics if the count does not match.
351    pub async fn assert_exchange_count(&self, expected: usize) {
352        let actual = self.received.lock().await.len();
353        assert_eq!(
354            actual, expected,
355            "MockEndpoint expected {expected} exchanges, got {actual}"
356        );
357    }
358
359    /// Wait until at least `count` exchanges have been received, or panic on timeout.
360    ///
361    /// Uses `tokio::sync::Notify` — no polling. Returns immediately if `count`
362    /// exchanges are already present.
363    ///
364    /// # Panics
365    ///
366    /// Panics if `timeout` elapses before `count` exchanges arrive.
367    pub async fn await_exchanges(&self, count: usize, timeout: std::time::Duration) {
368        let deadline = tokio::time::Instant::now() + timeout;
369        loop {
370            {
371                let received = self.received.lock().await;
372                if received.len() >= count {
373                    return;
374                }
375            }
376            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
377            if remaining.is_zero() {
378                // Re-check in case the final exchange arrived between the lock drop
379                // above and entering the select — Notify does not buffer permits.
380                let got = self.received.lock().await.len();
381                if got >= count {
382                    return;
383                }
384                panic!(
385                    "MockEndpoint '{}': timed out waiting for {} exchanges (got {} after {:?})",
386                    self.name, count, got, timeout
387                );
388            }
389            tokio::select! {
390                _ = self.notify.notified() => {}
391                _ = tokio::time::sleep(remaining) => {}
392            }
393        }
394    }
395
396    /// Wait for exchanges with a configurable timeout derived from `assert_period_ms`.
397    ///
398    /// If `assert_period_ms` is 0, uses the provided `fallback` duration.
399    /// Otherwise, waits for `assert_period_ms` milliseconds before checking.
400    pub async fn await_exchanges_with_timeout(&self, count: usize, fallback: std::time::Duration) {
401        let duration = if self.assert_period_ms > 0 {
402            std::time::Duration::from_millis(self.assert_period_ms)
403        } else {
404            fallback
405        };
406        self.await_exchanges(count, duration).await;
407    }
408
409    /// Return an [`ExchangeAssert`] for the exchange at `idx`.
410    ///
411    /// # Panics
412    ///
413    /// Panics if `idx` is out of bounds. Always call [`await_exchanges`] first
414    /// to ensure the exchange has been received.
415    ///
416    /// Panics if called from a single-threaded tokio runtime. Use
417    /// `#[tokio::test(flavor = "multi_thread")]` for tests that call this method.
418    ///
419    /// [`await_exchanges`]: MockEndpointInner::await_exchanges
420    // NOTE: requires multi-threaded Tokio runtime (current_thread will deadlock)
421    // due to `block_in_place` used for blocking_lock.
422    pub fn exchange(&self, idx: usize) -> ExchangeAssert {
423        let received = tokio::task::block_in_place(|| self.received.blocking_lock());
424        if idx >= received.len() {
425            panic!(
426                "MockEndpoint '{}': exchange index {} out of bounds (got {} exchanges)",
427                self.name,
428                idx,
429                received.len()
430            );
431        }
432        ExchangeAssert {
433            exchange: received[idx].clone(),
434            idx,
435            endpoint_name: self.name.clone(),
436        }
437    }
438
439    /// Add an expected body to the expectations list.
440    pub fn expect_body(&self, body: camel_component_api::Body) {
441        if let Ok(mut guard) = self.expectations.lock() {
442            guard.push_body(body);
443        }
444    }
445
446    /// Add an expected header key-value pair to the expectations list.
447    pub fn expect_header(&self, key: &str, value: impl Into<serde_json::Value>) {
448        if let Ok(mut guard) = self.expectations.lock() {
449            guard.push_header(key.to_string(), value.into());
450        }
451    }
452
453    /// Add an expected header regex pattern to the expectations list.
454    ///
455    /// After `await_exchanges()`, `assert_satisfied()` checks whether any
456    /// received exchange has the named header matching the given regex pattern.
457    pub fn expect_header_regex(&self, key: &str, pattern: &str) {
458        if let Ok(mut guard) = self.expectations.lock() {
459            guard.push_header_regex(key.to_string(), pattern.to_string());
460        }
461    }
462
463    /// Assert that all registered expectations are satisfied.
464    ///
465    /// # Panics
466    ///
467    /// Panics if expected bodies do not match received bodies (in order or any
468    /// order depending on `any_order` config), if expected headers are missing,
469    /// or if header regex patterns do not match.
470    pub async fn assert_satisfied(&self) {
471        let received = self.get_received_exchanges().await;
472
473        // Check expected bodies
474        {
475            let guard = self
476                .expectations
477                .lock()
478                .expect("expectations lock poisoned"); // allow-unwrap
479            if !guard.expected_bodies.is_empty() {
480                let received_bodies: Vec<_> = received.iter().map(|e| &e.input.body).collect();
481                if guard.expected_bodies.len() != received_bodies.len() {
482                    self.set_fail_fast_on_mismatch();
483                    panic!(
484                        "MockEndpoint '{}': expected {} bodies, got {}",
485                        self.name,
486                        guard.expected_bodies.len(),
487                        received_bodies.len()
488                    );
489                }
490                if self.any_order {
491                    // Match in any order — each expected body must appear exactly once
492                    let mut unmatched: Vec<_> = received_bodies.iter().collect();
493                    for expected in &guard.expected_bodies {
494                        let idx = unmatched
495                            .iter()
496                            .position(|actual| body_eq(expected, actual));
497                        match idx {
498                            Some(i) => {
499                                unmatched.remove(i);
500                            }
501                            None => {
502                                self.set_fail_fast_on_mismatch();
503                                panic!(
504                                    "MockEndpoint '{}': expected body {:?} not found in received exchanges (anyOrder mode)",
505                                    self.name, expected
506                                );
507                            }
508                        }
509                    }
510                } else {
511                    for (i, expected) in guard.expected_bodies.iter().enumerate() {
512                        if !body_eq(expected, received_bodies[i]) {
513                            self.set_fail_fast_on_mismatch();
514                            panic!(
515                                "MockEndpoint '{}': body[{}] expected {:?}, got {:?}",
516                                self.name, i, expected, received_bodies[i]
517                            );
518                        }
519                    }
520                }
521            }
522
523            // Check expected headers (must all be present on at least one exchange)
524            for (key, value) in &guard.expected_headers {
525                let found = received
526                    .iter()
527                    .any(|ex| ex.input.headers.get(key).is_some_and(|v| v == value));
528                if !found {
529                    self.set_fail_fast_on_mismatch();
530                    panic!(
531                        "MockEndpoint '{}': expected header '{}' = {} not found in any received exchange",
532                        self.name, key, value
533                    );
534                }
535            }
536
537            // Check expected header regexes
538            for (key, pattern) in &guard.expected_header_regexes {
539                let re = regex::Regex::new(pattern).unwrap_or_else(|e| {
540                    panic!(
541                        "MockEndpoint '{}': invalid regex pattern {:?}: {e}",
542                        self.name, pattern
543                    )
544                });
545                let found = received.iter().any(|ex| {
546                    ex.input.headers.get(key).is_some_and(|v| {
547                        let s = match v {
548                            serde_json::Value::String(s) => s.clone(),
549                            other => other.to_string(),
550                        };
551                        re.is_match(&s)
552                    })
553                });
554                if !found {
555                    self.set_fail_fast_on_mismatch();
556                    panic!(
557                        "MockEndpoint '{}': no received exchange has header '{}' matching regex {:?}",
558                        self.name, key, pattern
559                    );
560                }
561            }
562        }
563    }
564
565    /// Return the stored fail-fast error, if any.
566    pub fn fail_fast_error(&self) -> Option<CamelError> {
567        self.fail_fast_error.lock().ok().and_then(|g| g.clone())
568    }
569
570    /// Manually trip the fail-fast latch.
571    ///
572    /// Sets the internal `fail_fast_error` to `Some(error)`. The `MockProducer`
573    /// treats the presence of any error here as a sentinel — the actual
574    /// `CamelError` value is never propagated to the caller; a fixed
575    /// "fail-fast mode" message is returned instead. Use this hook when a
576    /// downstream component wants to short-circuit further processing on this
577    /// endpoint.
578    pub fn trigger_fail_fast(&self, error: CamelError) {
579        if let Ok(mut guard) = self.fail_fast_error.lock() {
580            *guard = Some(error);
581        }
582    }
583
584    /// When `fail_fast` is enabled, record the assertion-mismatch sentinel
585    /// before panicking. This ensures any concurrent or subsequent
586    /// `MockProducer::poll_ready` / `call` invocation rejects with the fixed
587    /// "fail-fast mode" message instead of being blocked on a panic-orphaned
588    /// lock or a stale `None` sentinel.
589    fn set_fail_fast_on_mismatch(&self) {
590        if self.fail_fast
591            && let Ok(mut guard) = self.fail_fast_error.lock()
592        {
593            *guard = Some(CamelError::ProcessorError(
594                "assert_satisfied expectation mismatch".to_string(),
595            ));
596        }
597    }
598}
599
600/// Compare two `Body` values for equality (used by assert_satisfied).
601fn body_eq(a: &camel_component_api::Body, b: &camel_component_api::Body) -> bool {
602    match (a, b) {
603        (camel_component_api::Body::Empty, camel_component_api::Body::Empty) => true,
604        (camel_component_api::Body::Text(a), camel_component_api::Body::Text(b)) => a == b,
605        (camel_component_api::Body::Json(a), camel_component_api::Body::Json(b)) => a == b,
606        (camel_component_api::Body::Xml(a), camel_component_api::Body::Xml(b)) => a == b,
607        (camel_component_api::Body::Bytes(a), camel_component_api::Body::Bytes(b)) => a == b,
608        _ => false,
609    }
610}
611
612impl Endpoint for MockEndpoint {
613    fn uri(&self) -> &str {
614        &self.0.uri
615    }
616
617    fn create_consumer(
618        &self,
619        _rt: Arc<dyn RuntimeObservability>,
620    ) -> Result<Box<dyn Consumer>, CamelError> {
621        Err(CamelError::EndpointCreationFailed(
622            "mock endpoint does not support consumers (it is a sink)".to_string(),
623        ))
624    }
625
626    fn create_producer(
627        &self,
628        _rt: Arc<dyn RuntimeObservability>,
629        _ctx: &ProducerContext,
630    ) -> Result<BoxProcessor, CamelError> {
631        Ok(BoxProcessor::new(MockProducer {
632            name: self.0.name.clone(),
633            received: Arc::clone(&self.0.received),
634            notify: Arc::clone(&self.0.notify),
635            max_retained: self.0.max_retained,
636            copy_on_exchange: self.0.copy_on_exchange,
637            fail_fast: self.0.fail_fast,
638            fail_fast_error: Arc::clone(&self.0.fail_fast_error),
639        }))
640    }
641}
642
643// ---------------------------------------------------------------------------
644// MockProducer
645// ---------------------------------------------------------------------------
646
647/// A producer that simply records each exchange it processes.
648#[derive(Clone)]
649struct MockProducer {
650    name: String,
651    received: Arc<Mutex<VecDeque<Exchange>>>,
652    notify: Arc<Notify>,
653    max_retained: usize,
654    copy_on_exchange: bool,
655    fail_fast: bool,
656    fail_fast_error: Arc<std::sync::Mutex<Option<CamelError>>>,
657}
658
659impl Service<Exchange> for MockProducer {
660    type Response = Exchange;
661    type Error = CamelError;
662    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
663
664    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
665        // In fail-fast mode, reject new exchanges if a previous one failed
666        if self.fail_fast
667            && let Ok(guard) = self.fail_fast_error.lock()
668            && guard.is_some()
669        {
670            return Poll::Ready(Err(CamelError::ProcessorError(
671                "mock endpoint in fail-fast mode: a previous exchange caused an error".to_string(),
672            )));
673        }
674        Poll::Ready(Ok(()))
675    }
676
677    fn call(&mut self, exchange: Exchange) -> Self::Future {
678        let name = self.name.clone();
679        let received = Arc::clone(&self.received);
680        let notify = Arc::clone(&self.notify);
681        let max_retained = self.max_retained;
682        let copy_on_exchange = self.copy_on_exchange;
683        let fail_fast = self.fail_fast;
684        let fail_fast_error = Arc::clone(&self.fail_fast_error);
685        Box::pin(async move {
686            // In fail-fast mode, check if a previous error was recorded
687            if fail_fast
688                && let Ok(guard) = fail_fast_error.lock()
689                && guard.is_some()
690            {
691                return Err(CamelError::ProcessorError(
692                    "mock endpoint in fail-fast mode: a previous exchange caused an error"
693                        .to_string(),
694                ));
695            }
696
697            let correlation_id = exchange
698                .input
699                .headers
700                .get("CamelCorrelationId")
701                .and_then(|v| v.as_str())
702                .map(|s| s.to_string());
703
704            let exchange_to_store = if copy_on_exchange {
705                let mut cloned = exchange.clone();
706                // Deep-clone the body to break aliasing
707                cloned.input.body = clone_body(&exchange.input.body);
708                cloned
709            } else {
710                exchange.clone()
711            };
712
713            let mut guard = received.lock().await;
714            if guard.len() >= max_retained {
715                tracing::warn!(
716                    endpoint_name = %name,
717                    max = max_retained,
718                    "max retained exchanges reached, dropping oldest"
719                );
720                guard.pop_front();
721            }
722            guard.push_back(exchange_to_store);
723            let count = guard.len();
724            drop(guard);
725
726            debug!(
727                endpoint_name = %name,
728                count = %count,
729                correlation_id = correlation_id.as_deref().unwrap_or("none"),
730                "exchange recorded on mock"
731            );
732            notify.notify_waiters();
733
734            Ok(exchange)
735        })
736    }
737}
738
739/// Deep-clone a `Body` value.
740fn clone_body(body: &camel_component_api::Body) -> camel_component_api::Body {
741    match body {
742        camel_component_api::Body::Empty => camel_component_api::Body::Empty,
743        camel_component_api::Body::Text(s) => camel_component_api::Body::Text(s.clone()),
744        camel_component_api::Body::Json(v) => camel_component_api::Body::Json(v.clone()),
745        camel_component_api::Body::Xml(s) => camel_component_api::Body::Xml(s.clone()),
746        camel_component_api::Body::Bytes(b) => camel_component_api::Body::Bytes(b.clone()),
747        camel_component_api::Body::Stream(s) => camel_component_api::Body::Stream(s.clone()),
748        // Safety net for future #[non_exhaustive] variants; all current variants
749        // are handled explicitly above.
750        _ => camel_component_api::Body::Empty,
751    }
752}
753
754// ---------------------------------------------------------------------------
755// ExchangeAssert
756// ---------------------------------------------------------------------------
757
758/// A handle for making synchronous assertions on a recorded exchange.
759///
760/// Obtain one via [`MockEndpointInner::exchange`] after calling
761/// [`MockEndpointInner::await_exchanges`].
762///
763/// All methods panic with descriptive messages on failure, making test output
764/// self-explanatory without additional context.
765pub struct ExchangeAssert {
766    exchange: Exchange,
767    idx: usize,
768    endpoint_name: String,
769}
770
771impl ExchangeAssert {
772    fn location(&self) -> String {
773        format!(
774            "MockEndpoint '{}' exchange[{}]",
775            self.endpoint_name, self.idx
776        )
777    }
778
779    /// Assert that the body is `Body::Text` equal to `expected`.
780    pub fn assert_body_text(self, expected: &str) -> Self {
781        match self.exchange.input.body.as_text() {
782            Some(actual) if actual == expected => {}
783            Some(actual) => panic!(
784                "{}: expected body text {:?}, got {:?}",
785                self.location(),
786                expected,
787                actual
788            ),
789            None => panic!(
790                "{}: expected body text {:?}, but body is not Body::Text (got {:?})",
791                self.location(),
792                expected,
793                self.exchange.input.body
794            ),
795        }
796        self
797    }
798
799    /// Assert that the body is `Body::Json` equal to `expected`.
800    pub fn assert_body_json(self, expected: serde_json::Value) -> Self {
801        match &self.exchange.input.body {
802            camel_component_api::Body::Json(actual) if *actual == expected => {}
803            camel_component_api::Body::Json(actual) => panic!(
804                "{}: expected body JSON {}, got {}",
805                self.location(),
806                expected,
807                actual
808            ),
809            other => panic!(
810                "{}: expected body JSON {}, but body is not Body::Json (got {:?})",
811                self.location(),
812                expected,
813                other
814            ),
815        }
816        self
817    }
818
819    /// Assert that the body is `Body::Bytes` equal to `expected`.
820    pub fn assert_body_bytes(self, expected: &[u8]) -> Self {
821        match &self.exchange.input.body {
822            camel_component_api::Body::Bytes(actual) if actual.as_ref() == expected => {}
823            camel_component_api::Body::Bytes(actual) => panic!(
824                "{}: expected body bytes {:?}, got {:?}",
825                self.location(),
826                expected,
827                actual
828            ),
829            other => panic!(
830                "{}: expected body bytes {:?}, but body is not Body::Bytes (got {:?})",
831                self.location(),
832                expected,
833                other
834            ),
835        }
836        self
837    }
838
839    /// Assert that header `key` exists and equals `expected`.
840    ///
841    /// # Panics
842    ///
843    /// Panics if the header is missing or its value does not match `expected`.
844    pub fn assert_header(self, key: &str, expected: serde_json::Value) -> Self {
845        match self.exchange.input.headers.get(key) {
846            Some(actual) if *actual == expected => {}
847            Some(actual) => panic!(
848                "{}: expected header {:?} = {}, got {}",
849                self.location(),
850                key,
851                expected,
852                actual
853            ),
854            None => panic!(
855                "{}: expected header {:?} = {}, but header is absent",
856                self.location(),
857                key,
858                expected
859            ),
860        }
861        self
862    }
863
864    /// Assert that header `key` is present (any value).
865    ///
866    /// # Panics
867    ///
868    /// Panics if the header key is absent.
869    pub fn assert_header_exists(self, key: &str) -> Self {
870        if !self.exchange.input.headers.contains_key(key) {
871            panic!(
872                "{}: expected header {:?} to be present, but it was absent",
873                self.location(),
874                key
875            );
876        }
877        self
878    }
879
880    /// Assert that the exchange has an error (`exchange.error` is `Some`).
881    ///
882    /// # Panics
883    ///
884    /// Panics if `exchange.error` is `None`.
885    pub fn assert_has_error(self) -> Self {
886        if self.exchange.error.is_none() {
887            panic!(
888                "{}: expected exchange to have an error, but error is None",
889                self.location()
890            );
891        }
892        self
893    }
894
895    /// Assert that the exchange has no error (`exchange.error` is `None`).
896    ///
897    /// # Panics
898    ///
899    /// Panics if `exchange.error` is `Some`.
900    pub fn assert_no_error(self) -> Self {
901        if let Some(ref err) = self.exchange.error {
902            panic!(
903                "{}: expected exchange to have no error, but got: {}",
904                self.location(),
905                err
906            );
907        }
908        self
909    }
910}
911
912// ---------------------------------------------------------------------------
913// Tests
914// ---------------------------------------------------------------------------
915
916#[cfg(test)]
917mod tests {
918    use camel_component_api::test_support::PanicRuntimeObservability;
919    fn rt() -> std::sync::Arc<dyn camel_component_api::RuntimeObservability> {
920        std::sync::Arc::new(PanicRuntimeObservability)
921    }
922
923    use super::*;
924    use camel_component_api::Message;
925    use camel_component_api::NoOpComponentContext;
926    use tower::ServiceExt;
927
928    fn test_producer_ctx() -> ProducerContext {
929        ProducerContext::new()
930    }
931
932    #[test]
933    fn test_mock_component_scheme() {
934        let component = MockComponent::new();
935        assert_eq!(component.scheme(), "mock");
936    }
937
938    #[test]
939    fn test_mock_component_default() {
940        let component = MockComponent::default();
941        assert_eq!(component.scheme(), "mock");
942        assert!(component.get_endpoint("missing").is_none());
943    }
944
945    #[test]
946    fn test_mock_creates_endpoint() {
947        let component = MockComponent::new();
948        let endpoint = component.create_endpoint("mock:result", &NoOpComponentContext);
949        assert!(endpoint.is_ok());
950    }
951
952    #[test]
953    fn test_mock_wrong_scheme() {
954        let component = MockComponent::new();
955        let result = component.create_endpoint("timer:tick", &NoOpComponentContext);
956        assert!(result.is_err());
957    }
958
959    #[test]
960    fn test_empty_mock_endpoint_name_rejected() {
961        let component = MockComponent::new();
962        let result = component.create_endpoint("mock:", &NoOpComponentContext);
963        assert!(result.is_err(), "empty mock name should be rejected");
964    }
965
966    #[test]
967    fn test_valid_mock_endpoint_name_accepted() {
968        let component = MockComponent::new();
969        let result = component.create_endpoint("mock:result", &NoOpComponentContext);
970        assert!(result.is_ok());
971    }
972
973    #[test]
974    fn test_mock_endpoint_no_consumer() {
975        let component = MockComponent::new();
976        let endpoint = component
977            .create_endpoint("mock:result", &NoOpComponentContext)
978            .unwrap();
979        assert!(endpoint.create_consumer(rt()).is_err());
980    }
981
982    #[test]
983    fn test_mock_endpoint_creates_producer() {
984        let ctx = test_producer_ctx();
985        let component = MockComponent::new();
986        let endpoint = component
987            .create_endpoint("mock:result", &NoOpComponentContext)
988            .unwrap();
989        assert!(endpoint.create_producer(rt(), &ctx).is_ok());
990    }
991
992    #[test]
993    fn test_mock_endpoint_uri() {
994        let component = MockComponent::new();
995        let endpoint = component
996            .create_endpoint("mock:uri-check", &NoOpComponentContext)
997            .unwrap();
998        assert_eq!(endpoint.uri(), "mock:uri-check");
999    }
1000
1001    #[test]
1002    fn test_mock_get_endpoint_returns_same_inner_for_same_name() {
1003        let component = MockComponent::new();
1004        let _ = component
1005            .create_endpoint("mock:shared-inner", &NoOpComponentContext)
1006            .unwrap();
1007        let _ = component
1008            .create_endpoint("mock:shared-inner", &NoOpComponentContext)
1009            .unwrap();
1010
1011        let first = component.get_endpoint("shared-inner").unwrap();
1012        let second = component.get_endpoint("shared-inner").unwrap();
1013        assert!(Arc::ptr_eq(&first, &second));
1014    }
1015
1016    #[tokio::test]
1017    async fn test_mock_producer_records_exchange() {
1018        let ctx = test_producer_ctx();
1019        let component = MockComponent::new();
1020        let endpoint = component
1021            .create_endpoint("mock:test", &NoOpComponentContext)
1022            .unwrap();
1023
1024        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1025
1026        let ex1 = Exchange::new(Message::new("first"));
1027        let ex2 = Exchange::new(Message::new("second"));
1028
1029        producer.call(ex1).await.unwrap();
1030        producer.call(ex2).await.unwrap();
1031
1032        let inner = component.get_endpoint("test").unwrap();
1033        inner.assert_exchange_count(2).await;
1034
1035        let received = inner.get_received_exchanges().await;
1036        assert_eq!(received[0].input.body.as_text(), Some("first"));
1037        assert_eq!(received[1].input.body.as_text(), Some("second"));
1038    }
1039
1040    #[tokio::test]
1041    async fn test_mock_producer_passes_through_exchange() {
1042        let ctx = test_producer_ctx();
1043        let component = MockComponent::new();
1044        let endpoint = component
1045            .create_endpoint("mock:passthrough", &NoOpComponentContext)
1046            .unwrap();
1047
1048        let producer = endpoint.create_producer(rt(), &ctx).unwrap();
1049        let exchange = Exchange::new(Message::new("hello"));
1050        let result = producer.oneshot(exchange).await.unwrap();
1051
1052        // Producer should return the exchange unchanged
1053        assert_eq!(result.input.body.as_text(), Some("hello"));
1054    }
1055
1056    #[tokio::test]
1057    async fn test_mock_assert_count_passes() {
1058        let component = MockComponent::new();
1059        let endpoint = component
1060            .create_endpoint("mock:count", &NoOpComponentContext)
1061            .unwrap();
1062        let inner = component.get_endpoint("count").unwrap();
1063
1064        inner.assert_exchange_count(0).await;
1065
1066        let ctx = test_producer_ctx();
1067        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1068        producer
1069            .call(Exchange::new(Message::new("one")))
1070            .await
1071            .unwrap();
1072
1073        inner.assert_exchange_count(1).await;
1074    }
1075
1076    #[tokio::test]
1077    #[should_panic(expected = "MockEndpoint expected 5 exchanges, got 0")]
1078    async fn test_mock_assert_count_fails() {
1079        let component = MockComponent::new();
1080        // Endpoint not created yet, so get_endpoint returns None.
1081        // Create it first, then assert.
1082        let _endpoint = component
1083            .create_endpoint("mock:fail", &NoOpComponentContext)
1084            .unwrap();
1085        let inner = component.get_endpoint("fail").unwrap();
1086
1087        inner.assert_exchange_count(5).await;
1088    }
1089
1090    #[tokio::test]
1091    async fn test_mock_component_shared_registry() {
1092        let component = MockComponent::new();
1093        let ep1 = component
1094            .create_endpoint("mock:shared", &NoOpComponentContext)
1095            .unwrap();
1096        let ep2 = component
1097            .create_endpoint("mock:shared", &NoOpComponentContext)
1098            .unwrap();
1099
1100        // Producing via ep1's producer...
1101        let ctx = test_producer_ctx();
1102        let mut p1 = ep1.create_producer(rt(), &ctx).unwrap();
1103        p1.call(Exchange::new(Message::new("from-ep1")))
1104            .await
1105            .unwrap();
1106
1107        // ...and via ep2's producer...
1108        let mut p2 = ep2.create_producer(rt(), &ctx).unwrap();
1109        p2.call(Exchange::new(Message::new("from-ep2")))
1110            .await
1111            .unwrap();
1112
1113        // ...both should be visible via the shared storage
1114        let inner = component.get_endpoint("shared").unwrap();
1115        inner.assert_exchange_count(2).await;
1116
1117        let received = inner.get_received_exchanges().await;
1118        assert_eq!(received[0].input.body.as_text(), Some("from-ep1"));
1119        assert_eq!(received[1].input.body.as_text(), Some("from-ep2"));
1120    }
1121
1122    #[tokio::test]
1123    async fn await_exchanges_resolves_immediately() {
1124        // If exchanges are already present, await_exchanges returns without timeout.
1125        let ctx = test_producer_ctx();
1126        let component = MockComponent::new();
1127        let endpoint = component
1128            .create_endpoint("mock:immediate", &NoOpComponentContext)
1129            .unwrap();
1130        let inner = component.get_endpoint("immediate").unwrap();
1131
1132        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1133        producer
1134            .call(Exchange::new(Message::new("a")))
1135            .await
1136            .unwrap();
1137        producer
1138            .call(Exchange::new(Message::new("b")))
1139            .await
1140            .unwrap();
1141
1142        // Should return immediately — both exchanges already received.
1143        inner
1144            .await_exchanges(2, std::time::Duration::from_millis(100))
1145            .await;
1146    }
1147
1148    #[tokio::test]
1149    async fn await_exchanges_waits_then_resolves() {
1150        // await_exchanges unblocks when a producer sends after the call.
1151        let ctx = test_producer_ctx();
1152        let component = MockComponent::new();
1153        let endpoint = component
1154            .create_endpoint("mock:waiter", &NoOpComponentContext)
1155            .unwrap();
1156        let inner = component.get_endpoint("waiter").unwrap();
1157
1158        // Spawn producer that sends after a short delay.
1159        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1160        tokio::spawn(async move {
1161            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1162            producer
1163                .call(Exchange::new(Message::new("delayed")))
1164                .await
1165                .unwrap();
1166        });
1167
1168        // This should block until the spawned task delivers the exchange.
1169        inner
1170            .await_exchanges(1, std::time::Duration::from_millis(500))
1171            .await;
1172
1173        let received = inner.get_received_exchanges().await;
1174        assert_eq!(received.len(), 1);
1175        assert_eq!(received[0].input.body.as_text(), Some("delayed"));
1176    }
1177
1178    #[tokio::test]
1179    #[should_panic(expected = "timed out waiting for 5 exchanges")]
1180    async fn await_exchanges_times_out() {
1181        let component = MockComponent::new();
1182        let _endpoint = component
1183            .create_endpoint("mock:timeout", &NoOpComponentContext)
1184            .unwrap();
1185        let inner = component.get_endpoint("timeout").unwrap();
1186
1187        // Nobody sends — should panic after timeout.
1188        inner
1189            .await_exchanges(5, std::time::Duration::from_millis(50))
1190            .await;
1191    }
1192
1193    #[tokio::test(flavor = "multi_thread")]
1194    async fn exchange_idx_returns_assert() {
1195        let ctx = test_producer_ctx();
1196        let component = MockComponent::new();
1197        let endpoint = component
1198            .create_endpoint("mock:assert-idx", &NoOpComponentContext)
1199            .unwrap();
1200        let inner = component.get_endpoint("assert-idx").unwrap();
1201
1202        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1203        producer
1204            .call(Exchange::new(Message::new("hello")))
1205            .await
1206            .unwrap();
1207
1208        inner
1209            .await_exchanges(1, std::time::Duration::from_millis(500))
1210            .await;
1211        // Should not panic — index 0 exists.
1212        let _assert = inner.exchange(0);
1213    }
1214
1215    #[tokio::test(flavor = "multi_thread")]
1216    #[should_panic(expected = "exchange index 5 out of bounds")]
1217    async fn exchange_idx_out_of_bounds() {
1218        let ctx = test_producer_ctx();
1219        let component = MockComponent::new();
1220        let endpoint = component
1221            .create_endpoint("mock:oob", &NoOpComponentContext)
1222            .unwrap();
1223        let inner = component.get_endpoint("oob").unwrap();
1224
1225        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1226        producer
1227            .call(Exchange::new(Message::new("only-one")))
1228            .await
1229            .unwrap();
1230
1231        inner
1232            .await_exchanges(1, std::time::Duration::from_millis(500))
1233            .await;
1234        // Only 1 exchange, index 5 should panic.
1235        let _assert = inner.exchange(5);
1236    }
1237
1238    #[tokio::test(flavor = "multi_thread")]
1239    async fn assert_body_text_pass() {
1240        let ctx = test_producer_ctx();
1241        let component = MockComponent::new();
1242        let endpoint = component
1243            .create_endpoint("mock:body-text-pass", &NoOpComponentContext)
1244            .unwrap();
1245        let inner = component.get_endpoint("body-text-pass").unwrap();
1246        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1247        producer
1248            .call(Exchange::new(Message::new("hello")))
1249            .await
1250            .unwrap();
1251        inner
1252            .await_exchanges(1, std::time::Duration::from_millis(500))
1253            .await;
1254        inner.exchange(0).assert_body_text("hello");
1255    }
1256
1257    #[tokio::test(flavor = "multi_thread")]
1258    #[should_panic(expected = "expected body text")]
1259    async fn assert_body_text_fail() {
1260        let ctx = test_producer_ctx();
1261        let component = MockComponent::new();
1262        let endpoint = component
1263            .create_endpoint("mock:body-text-fail", &NoOpComponentContext)
1264            .unwrap();
1265        let inner = component.get_endpoint("body-text-fail").unwrap();
1266        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1267        producer
1268            .call(Exchange::new(Message::new("hello")))
1269            .await
1270            .unwrap();
1271        inner
1272            .await_exchanges(1, std::time::Duration::from_millis(500))
1273            .await;
1274        inner.exchange(0).assert_body_text("world");
1275    }
1276
1277    #[tokio::test(flavor = "multi_thread")]
1278    async fn assert_body_json_pass() {
1279        use camel_component_api::Body;
1280        let ctx = test_producer_ctx();
1281        let component = MockComponent::new();
1282        let endpoint = component
1283            .create_endpoint("mock:body-json-pass", &NoOpComponentContext)
1284            .unwrap();
1285        let inner = component.get_endpoint("body-json-pass").unwrap();
1286        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1287        let mut msg = Message::new("");
1288        msg.body = Body::Json(serde_json::json!({"key": "value"}));
1289        producer.call(Exchange::new(msg)).await.unwrap();
1290        inner
1291            .await_exchanges(1, std::time::Duration::from_millis(500))
1292            .await;
1293        inner
1294            .exchange(0)
1295            .assert_body_json(serde_json::json!({"key": "value"}));
1296    }
1297
1298    #[tokio::test(flavor = "multi_thread")]
1299    #[should_panic(expected = "expected body JSON")]
1300    async fn assert_body_json_fail() {
1301        use camel_component_api::Body;
1302        let ctx = test_producer_ctx();
1303        let component = MockComponent::new();
1304        let endpoint = component
1305            .create_endpoint("mock:body-json-fail", &NoOpComponentContext)
1306            .unwrap();
1307        let inner = component.get_endpoint("body-json-fail").unwrap();
1308        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1309        let mut msg = Message::new("");
1310        msg.body = Body::Json(serde_json::json!({"key": "value"}));
1311        producer.call(Exchange::new(msg)).await.unwrap();
1312        inner
1313            .await_exchanges(1, std::time::Duration::from_millis(500))
1314            .await;
1315        inner
1316            .exchange(0)
1317            .assert_body_json(serde_json::json!({"key": "other"}));
1318    }
1319
1320    #[tokio::test(flavor = "multi_thread")]
1321    async fn assert_body_bytes_pass() {
1322        use bytes::Bytes;
1323        use camel_component_api::Body;
1324        let ctx = test_producer_ctx();
1325        let component = MockComponent::new();
1326        let endpoint = component
1327            .create_endpoint("mock:body-bytes-pass", &NoOpComponentContext)
1328            .unwrap();
1329        let inner = component.get_endpoint("body-bytes-pass").unwrap();
1330        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1331        let mut msg = Message::new("");
1332        msg.body = Body::Bytes(Bytes::from_static(b"binary"));
1333        producer.call(Exchange::new(msg)).await.unwrap();
1334        inner
1335            .await_exchanges(1, std::time::Duration::from_millis(500))
1336            .await;
1337        inner.exchange(0).assert_body_bytes(b"binary");
1338    }
1339
1340    #[tokio::test(flavor = "multi_thread")]
1341    #[should_panic(expected = "expected body bytes")]
1342    async fn assert_body_bytes_fail() {
1343        use bytes::Bytes;
1344        use camel_component_api::Body;
1345        let ctx = test_producer_ctx();
1346        let component = MockComponent::new();
1347        let endpoint = component
1348            .create_endpoint("mock:body-bytes-fail", &NoOpComponentContext)
1349            .unwrap();
1350        let inner = component.get_endpoint("body-bytes-fail").unwrap();
1351        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1352        let mut msg = Message::new("");
1353        msg.body = Body::Bytes(Bytes::from_static(b"binary"));
1354        producer.call(Exchange::new(msg)).await.unwrap();
1355        inner
1356            .await_exchanges(1, std::time::Duration::from_millis(500))
1357            .await;
1358        inner.exchange(0).assert_body_bytes(b"different");
1359    }
1360
1361    #[tokio::test(flavor = "multi_thread")]
1362    async fn assert_header_pass() {
1363        let ctx = test_producer_ctx();
1364        let component = MockComponent::new();
1365        let endpoint = component
1366            .create_endpoint("mock:hdr-pass", &NoOpComponentContext)
1367            .unwrap();
1368        let inner = component.get_endpoint("hdr-pass").unwrap();
1369        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1370        let mut msg = Message::new("body");
1371        msg.headers
1372            .insert("x-key".to_string(), serde_json::json!("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_header("x-key", serde_json::json!("value"));
1380    }
1381
1382    #[tokio::test(flavor = "multi_thread")]
1383    #[should_panic(expected = "expected header")]
1384    async fn assert_header_fail() {
1385        let ctx = test_producer_ctx();
1386        let component = MockComponent::new();
1387        let endpoint = component
1388            .create_endpoint("mock:hdr-fail", &NoOpComponentContext)
1389            .unwrap();
1390        let inner = component.get_endpoint("hdr-fail").unwrap();
1391        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1392        let mut msg = Message::new("body");
1393        msg.headers
1394            .insert("x-key".to_string(), serde_json::json!("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_header("x-key", serde_json::json!("other"));
1402    }
1403
1404    #[tokio::test(flavor = "multi_thread")]
1405    async fn assert_header_exists_pass() {
1406        let ctx = test_producer_ctx();
1407        let component = MockComponent::new();
1408        let endpoint = component
1409            .create_endpoint("mock:hdr-exists-pass", &NoOpComponentContext)
1410            .unwrap();
1411        let inner = component.get_endpoint("hdr-exists-pass").unwrap();
1412        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1413        let mut msg = Message::new("body");
1414        msg.headers
1415            .insert("x-present".to_string(), serde_json::json!(42));
1416        producer.call(Exchange::new(msg)).await.unwrap();
1417        inner
1418            .await_exchanges(1, std::time::Duration::from_millis(500))
1419            .await;
1420        inner.exchange(0).assert_header_exists("x-present");
1421    }
1422
1423    #[tokio::test(flavor = "multi_thread")]
1424    #[should_panic(expected = "expected header")]
1425    async fn assert_header_exists_fail() {
1426        let ctx = test_producer_ctx();
1427        let component = MockComponent::new();
1428        let endpoint = component
1429            .create_endpoint("mock:hdr-exists-fail", &NoOpComponentContext)
1430            .unwrap();
1431        let inner = component.get_endpoint("hdr-exists-fail").unwrap();
1432        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1433        producer
1434            .call(Exchange::new(Message::new("body")))
1435            .await
1436            .unwrap();
1437        inner
1438            .await_exchanges(1, std::time::Duration::from_millis(500))
1439            .await;
1440        inner.exchange(0).assert_header_exists("x-missing");
1441    }
1442
1443    #[tokio::test(flavor = "multi_thread")]
1444    async fn assert_has_error_pass() {
1445        let ctx = test_producer_ctx();
1446        let component = MockComponent::new();
1447        let endpoint = component
1448            .create_endpoint("mock:err-pass", &NoOpComponentContext)
1449            .unwrap();
1450        let inner = component.get_endpoint("err-pass").unwrap();
1451        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1452        let mut ex = Exchange::new(Message::new("body"));
1453        ex.set_error(camel_component_api::CamelError::ProcessorError(
1454            "oops".to_string(),
1455        ));
1456        producer.call(ex).await.unwrap();
1457        inner
1458            .await_exchanges(1, std::time::Duration::from_millis(500))
1459            .await;
1460        inner.exchange(0).assert_has_error();
1461    }
1462
1463    #[tokio::test(flavor = "multi_thread")]
1464    #[should_panic(expected = "expected exchange to have an error")]
1465    async fn assert_has_error_fail() {
1466        let ctx = test_producer_ctx();
1467        let component = MockComponent::new();
1468        let endpoint = component
1469            .create_endpoint("mock:has-err-fail", &NoOpComponentContext)
1470            .unwrap();
1471        let inner = component.get_endpoint("has-err-fail").unwrap();
1472        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1473        producer
1474            .call(Exchange::new(Message::new("body")))
1475            .await
1476            .unwrap();
1477        inner
1478            .await_exchanges(1, std::time::Duration::from_millis(500))
1479            .await;
1480        inner.exchange(0).assert_has_error();
1481    }
1482
1483    #[tokio::test(flavor = "multi_thread")]
1484    async fn assert_no_error_pass() {
1485        let ctx = test_producer_ctx();
1486        let component = MockComponent::new();
1487        let endpoint = component
1488            .create_endpoint("mock:no-err-pass", &NoOpComponentContext)
1489            .unwrap();
1490        let inner = component.get_endpoint("no-err-pass").unwrap();
1491        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1492        producer
1493            .call(Exchange::new(Message::new("body")))
1494            .await
1495            .unwrap();
1496        inner
1497            .await_exchanges(1, std::time::Duration::from_millis(500))
1498            .await;
1499        inner.exchange(0).assert_no_error();
1500    }
1501
1502    // -----------------------------------------------------------------------
1503    // A-13: reset() and bounded retention tests
1504    // -----------------------------------------------------------------------
1505
1506    #[tokio::test]
1507    async fn test_mock_reset_clears_exchanges() {
1508        let component = MockComponent::new();
1509        let endpoint = component
1510            .create_endpoint("mock:reset-test", &NoOpComponentContext)
1511            .unwrap();
1512        let inner = component.get_endpoint("reset-test").unwrap();
1513
1514        let ctx = test_producer_ctx();
1515        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1516        producer
1517            .call(Exchange::new(Message::new("a")))
1518            .await
1519            .unwrap();
1520        producer
1521            .call(Exchange::new(Message::new("b")))
1522            .await
1523            .unwrap();
1524
1525        assert_eq!(inner.received_count().await, 2);
1526        inner.reset().await;
1527        assert_eq!(inner.received_count().await, 0);
1528    }
1529
1530    #[tokio::test]
1531    async fn test_mock_bounded_retention_drops_oldest() {
1532        let config = MockConfig {
1533            max_retained: 3,
1534            ..Default::default()
1535        };
1536        let component = MockComponent::with_config(config);
1537        let endpoint = component
1538            .create_endpoint("mock:bounded", &NoOpComponentContext)
1539            .unwrap();
1540        let inner = component.get_endpoint("bounded").unwrap();
1541
1542        let ctx = test_producer_ctx();
1543        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1544
1545        // Send 5 exchanges, but max_retained is 3
1546        for i in 0..5 {
1547            producer
1548                .call(Exchange::new(Message::new(format!("msg-{i}"))))
1549                .await
1550                .unwrap();
1551        }
1552
1553        assert_eq!(inner.received_count().await, 3);
1554        let received = inner.get_received_exchanges().await;
1555        // Oldest (msg-0, msg-1) should be dropped
1556        assert_eq!(received[0].input.body.as_text(), Some("msg-2"));
1557        assert_eq!(received[1].input.body.as_text(), Some("msg-3"));
1558        assert_eq!(received[2].input.body.as_text(), Some("msg-4"));
1559    }
1560
1561    #[tokio::test]
1562    async fn test_mock_reset_then_record_again() {
1563        let component = MockComponent::new();
1564        let endpoint = component
1565            .create_endpoint("mock:reset-reuse", &NoOpComponentContext)
1566            .unwrap();
1567        let inner = component.get_endpoint("reset-reuse").unwrap();
1568
1569        let ctx = test_producer_ctx();
1570        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1571        producer
1572            .call(Exchange::new(Message::new("before-reset")))
1573            .await
1574            .unwrap();
1575        inner.reset().await;
1576
1577        producer
1578            .call(Exchange::new(Message::new("after-reset")))
1579            .await
1580            .unwrap();
1581
1582        let received = inner.get_received_exchanges().await;
1583        assert_eq!(received.len(), 1);
1584        assert_eq!(received[0].input.body.as_text(), Some("after-reset"));
1585    }
1586
1587    #[tokio::test(flavor = "multi_thread")]
1588    #[should_panic(expected = "expected exchange to have no error")]
1589    async fn assert_no_error_fail() {
1590        let ctx = test_producer_ctx();
1591        let component = MockComponent::new();
1592        let endpoint = component
1593            .create_endpoint("mock:no-err-fail", &NoOpComponentContext)
1594            .unwrap();
1595        let inner = component.get_endpoint("no-err-fail").unwrap();
1596        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1597        let mut ex = Exchange::new(Message::new("body"));
1598        ex.set_error(camel_component_api::CamelError::ProcessorError(
1599            "oops".to_string(),
1600        ));
1601        producer.call(ex).await.unwrap();
1602        inner
1603            .await_exchanges(1, std::time::Duration::from_millis(500))
1604            .await;
1605        inner.exchange(0).assert_no_error();
1606    }
1607
1608    // -----------------------------------------------------------------------
1609    // MOCK-003: copy_on_exchange tests
1610    // -----------------------------------------------------------------------
1611
1612    #[tokio::test]
1613    async fn test_copy_on_exchange_stores_cloned_body() {
1614        let config = MockConfig {
1615            copy_on_exchange: true,
1616            ..Default::default()
1617        };
1618        let component = MockComponent::with_config(config);
1619        let endpoint = component
1620            .create_endpoint("mock:copy", &NoOpComponentContext)
1621            .unwrap();
1622        let inner = component.get_endpoint("copy").unwrap();
1623
1624        let ctx = test_producer_ctx();
1625        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1626
1627        let mut msg = Message::new("original");
1628        msg.headers.insert("x-test".into(), serde_json::json!(1));
1629        let ex = Exchange::new(msg);
1630        producer.call(ex).await.unwrap();
1631
1632        let received = inner.get_received_exchanges().await;
1633        assert_eq!(received[0].input.body.as_text(), Some("original"));
1634    }
1635
1636    #[tokio::test]
1637    async fn test_copy_on_exchange_false_shares_storage() {
1638        let config = MockConfig {
1639            copy_on_exchange: false,
1640            ..Default::default()
1641        };
1642        let component = MockComponent::with_config(config);
1643        let endpoint = component
1644            .create_endpoint("mock:no-copy", &NoOpComponentContext)
1645            .unwrap();
1646        let inner = component.get_endpoint("no-copy").unwrap();
1647
1648        let ctx = test_producer_ctx();
1649        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1650
1651        producer
1652            .call(Exchange::new(Message::new("direct")))
1653            .await
1654            .unwrap();
1655
1656        let received = inner.get_received_exchanges().await;
1657        assert_eq!(received[0].input.body.as_text(), Some("direct"));
1658    }
1659
1660    // -----------------------------------------------------------------------
1661    // MOCK-003b: clone_body preserves Body::Stream
1662    // -----------------------------------------------------------------------
1663
1664    #[tokio::test]
1665    async fn test_clone_body_preserves_stream() {
1666        use bytes::Bytes;
1667        use camel_component_api::{Body, StreamBody, StreamMetadata};
1668        use futures::stream;
1669        use std::sync::Arc;
1670        use tokio::sync::Mutex;
1671
1672        let chunks: Vec<Result<Bytes, camel_component_api::CamelError>> =
1673            vec![Ok(Bytes::from("data"))];
1674        let body = Body::Stream(StreamBody {
1675            stream: Arc::new(Mutex::new(Some(Box::pin(stream::iter(chunks))))),
1676            metadata: StreamMetadata::default(),
1677        });
1678
1679        let config = MockConfig {
1680            copy_on_exchange: true,
1681            ..Default::default()
1682        };
1683        let component = MockComponent::with_config(config);
1684        let endpoint = component
1685            .create_endpoint("mock:stream-test", &NoOpComponentContext)
1686            .unwrap();
1687        let inner = component.get_endpoint("stream-test").unwrap();
1688
1689        let ctx = test_producer_ctx();
1690        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1691
1692        let msg = Message::new(body);
1693        let ex = Exchange::new(msg);
1694        producer.call(ex).await.unwrap();
1695
1696        let received = inner.get_received_exchanges().await;
1697        assert!(
1698            matches!(received[0].input.body, Body::Stream(_)),
1699            "expected Body::Stream, got {:?}",
1700            received[0].input.body
1701        );
1702    }
1703
1704    #[tokio::test]
1705    async fn test_clone_body_stream_shares_arc() {
1706        use bytes::Bytes;
1707        use camel_component_api::{Body, StreamBody, StreamMetadata};
1708        use futures::stream;
1709        use std::sync::Arc;
1710        use tokio::sync::Mutex;
1711
1712        let chunks: Vec<Result<Bytes, camel_component_api::CamelError>> =
1713            vec![Ok(Bytes::from("data"))];
1714        let original = Body::Stream(StreamBody {
1715            stream: Arc::new(Mutex::new(Some(Box::pin(stream::iter(chunks))))),
1716            metadata: StreamMetadata::default(),
1717        });
1718
1719        let clone = clone_body(&original);
1720
1721        // Consume the original first
1722        let _ = original.into_bytes(100).await.unwrap();
1723
1724        // Clone should fail with AlreadyConsumed (shared Arc semantics)
1725        let result = clone.into_bytes(100).await;
1726        assert!(
1727            matches!(
1728                result,
1729                Err(camel_component_api::CamelError::AlreadyConsumed)
1730            ),
1731            "expected AlreadyConsumed, got {:?}",
1732            result
1733        );
1734    }
1735
1736    // -----------------------------------------------------------------------
1737    // MOCK-004: expect_body / expect_header / assert_satisfied tests
1738    // -----------------------------------------------------------------------
1739
1740    #[tokio::test]
1741    async fn test_assert_satisfied_bodies_in_order() {
1742        let component = MockComponent::new();
1743        let endpoint = component
1744            .create_endpoint("mock:sat-bodies", &NoOpComponentContext)
1745            .unwrap();
1746        let inner = component.get_endpoint("sat-bodies").unwrap();
1747
1748        inner.expect_body(camel_component_api::Body::Text("alpha".into()));
1749        inner.expect_body(camel_component_api::Body::Text("beta".into()));
1750
1751        let ctx = test_producer_ctx();
1752        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1753        producer
1754            .call(Exchange::new(Message::new("alpha")))
1755            .await
1756            .unwrap();
1757        producer
1758            .call(Exchange::new(Message::new("beta")))
1759            .await
1760            .unwrap();
1761
1762        inner.assert_satisfied().await;
1763    }
1764
1765    #[tokio::test]
1766    #[should_panic(expected = "body[0] expected")]
1767    async fn test_assert_satisfied_bodies_wrong_order_fails() {
1768        let component = MockComponent::new();
1769        let endpoint = component
1770            .create_endpoint("mock:sat-bodies-fail", &NoOpComponentContext)
1771            .unwrap();
1772        let inner = component.get_endpoint("sat-bodies-fail").unwrap();
1773
1774        inner.expect_body(camel_component_api::Body::Text("alpha".into()));
1775        inner.expect_body(camel_component_api::Body::Text("beta".into()));
1776
1777        let ctx = test_producer_ctx();
1778        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1779        producer
1780            .call(Exchange::new(Message::new("beta")))
1781            .await
1782            .unwrap();
1783        producer
1784            .call(Exchange::new(Message::new("alpha")))
1785            .await
1786            .unwrap();
1787
1788        inner.assert_satisfied().await;
1789    }
1790
1791    #[tokio::test]
1792    async fn test_assert_satisfied_headers() {
1793        let component = MockComponent::new();
1794        let endpoint = component
1795            .create_endpoint("mock:sat-hdr", &NoOpComponentContext)
1796            .unwrap();
1797        let inner = component.get_endpoint("sat-hdr").unwrap();
1798
1799        inner.expect_header("status", serde_json::json!("ok"));
1800
1801        let ctx = test_producer_ctx();
1802        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1803        let mut msg = Message::new("body");
1804        msg.headers.insert("status".into(), serde_json::json!("ok"));
1805        producer.call(Exchange::new(msg)).await.unwrap();
1806
1807        inner.assert_satisfied().await;
1808    }
1809
1810    #[tokio::test]
1811    #[should_panic(expected = "expected header 'missing' =")]
1812    async fn test_assert_satisfied_headers_missing() {
1813        let component = MockComponent::new();
1814        let endpoint = component
1815            .create_endpoint("mock:sat-hdr-missing", &NoOpComponentContext)
1816            .unwrap();
1817        let inner = component.get_endpoint("sat-hdr-missing").unwrap();
1818
1819        inner.expect_header("missing", serde_json::json!("value"));
1820
1821        let ctx = test_producer_ctx();
1822        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1823        producer
1824            .call(Exchange::new(Message::new("body")))
1825            .await
1826            .unwrap();
1827
1828        inner.assert_satisfied().await;
1829    }
1830
1831    // -----------------------------------------------------------------------
1832    // MOCK-005: fail_fast tests
1833    // -----------------------------------------------------------------------
1834
1835    #[tokio::test]
1836    async fn test_fail_fast_rejects_after_first_call() {
1837        let config = MockConfig {
1838            fail_fast: true,
1839            ..Default::default()
1840        };
1841        let component = MockComponent::with_config(config);
1842        let endpoint = component
1843            .create_endpoint("mock:ff", &NoOpComponentContext)
1844            .unwrap();
1845
1846        let ctx = test_producer_ctx();
1847        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1848
1849        // First call succeeds
1850        producer
1851            .call(Exchange::new(Message::new("ok")))
1852            .await
1853            .unwrap();
1854    }
1855
1856    #[tokio::test]
1857    async fn test_fail_fast_no_error_when_all_good() {
1858        let config = MockConfig {
1859            fail_fast: true,
1860            ..Default::default()
1861        };
1862        let component = MockComponent::with_config(config);
1863        let endpoint = component
1864            .create_endpoint("mock:ff-good", &NoOpComponentContext)
1865            .unwrap();
1866        let inner = component.get_endpoint("ff-good").unwrap();
1867
1868        let ctx = test_producer_ctx();
1869        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1870
1871        producer
1872            .call(Exchange::new(Message::new("a")))
1873            .await
1874            .unwrap();
1875        producer
1876            .call(Exchange::new(Message::new("b")))
1877            .await
1878            .unwrap();
1879
1880        assert!(inner.fail_fast_error().is_none());
1881        inner.assert_exchange_count(2).await;
1882    }
1883
1884    // -----------------------------------------------------------------------
1885    // MOCK-008: await_exchanges_with_timeout tests
1886    // -----------------------------------------------------------------------
1887
1888    #[tokio::test]
1889    async fn test_await_exchanges_with_timeout_uses_config_period() {
1890        let config = MockConfig {
1891            assert_period_ms: 100,
1892            ..Default::default()
1893        };
1894        let component = MockComponent::with_config(config);
1895        let endpoint = component
1896            .create_endpoint("mock:ap", &NoOpComponentContext)
1897            .unwrap();
1898        let inner = component.get_endpoint("ap").unwrap();
1899
1900        let ctx = test_producer_ctx();
1901        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1902        producer
1903            .call(Exchange::new(Message::new("x")))
1904            .await
1905            .unwrap();
1906
1907        inner
1908            .await_exchanges_with_timeout(1, std::time::Duration::from_millis(1))
1909            .await;
1910    }
1911
1912    #[tokio::test]
1913    async fn test_await_exchanges_with_timeout_uses_fallback_when_zero() {
1914        let config = MockConfig {
1915            assert_period_ms: 0,
1916            ..Default::default()
1917        };
1918        let component = MockComponent::with_config(config);
1919        let endpoint = component
1920            .create_endpoint("mock:ap-fb", &NoOpComponentContext)
1921            .unwrap();
1922        let inner = component.get_endpoint("ap-fb").unwrap();
1923
1924        let ctx = test_producer_ctx();
1925        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1926        producer
1927            .call(Exchange::new(Message::new("y")))
1928            .await
1929            .unwrap();
1930
1931        inner
1932            .await_exchanges_with_timeout(1, std::time::Duration::from_millis(200))
1933            .await;
1934    }
1935
1936    // -----------------------------------------------------------------------
1937    // MOCK-009: expect_header_regex tests
1938    // -----------------------------------------------------------------------
1939
1940    #[tokio::test]
1941    async fn test_expect_header_regex_match() {
1942        let component = MockComponent::new();
1943        let endpoint = component
1944            .create_endpoint("mock:re-hdr", &NoOpComponentContext)
1945            .unwrap();
1946        let inner = component.get_endpoint("re-hdr").unwrap();
1947
1948        inner.expect_header_regex("x-trace-id", r"^[a-f0-9]{8}$");
1949
1950        let ctx = test_producer_ctx();
1951        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1952        let mut msg = Message::new("body");
1953        msg.headers
1954            .insert("x-trace-id".into(), serde_json::json!("deadbeef"));
1955        producer.call(Exchange::new(msg)).await.unwrap();
1956
1957        inner.assert_satisfied().await;
1958    }
1959
1960    #[tokio::test]
1961    #[should_panic(expected = "no received exchange has header")]
1962    async fn test_expect_header_regex_no_match() {
1963        let component = MockComponent::new();
1964        let endpoint = component
1965            .create_endpoint("mock:re-hdr-fail", &NoOpComponentContext)
1966            .unwrap();
1967        let inner = component.get_endpoint("re-hdr-fail").unwrap();
1968
1969        inner.expect_header_regex("x-trace-id", r"^\d+$");
1970
1971        let ctx = test_producer_ctx();
1972        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
1973        let mut msg = Message::new("body");
1974        msg.headers
1975            .insert("x-trace-id".into(), serde_json::json!("abc"));
1976        producer.call(Exchange::new(msg)).await.unwrap();
1977
1978        inner.assert_satisfied().await;
1979    }
1980
1981    // -----------------------------------------------------------------------
1982    // MOCK-010: any_order tests
1983    // -----------------------------------------------------------------------
1984
1985    #[tokio::test]
1986    async fn test_any_order_bodies_match() {
1987        let config = MockConfig {
1988            any_order: true,
1989            ..Default::default()
1990        };
1991        let component = MockComponent::with_config(config);
1992        let endpoint = component
1993            .create_endpoint("mock:anyorder", &NoOpComponentContext)
1994            .unwrap();
1995        let inner = component.get_endpoint("anyorder").unwrap();
1996
1997        inner.expect_body(camel_component_api::Body::Text("beta".into()));
1998        inner.expect_body(camel_component_api::Body::Text("alpha".into()));
1999
2000        let ctx = test_producer_ctx();
2001        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2002        producer
2003            .call(Exchange::new(Message::new("alpha")))
2004            .await
2005            .unwrap();
2006        producer
2007            .call(Exchange::new(Message::new("beta")))
2008            .await
2009            .unwrap();
2010
2011        inner.assert_satisfied().await;
2012    }
2013
2014    #[tokio::test]
2015    #[should_panic(expected = "not found in received exchanges (anyOrder mode)")]
2016    async fn test_any_order_bodies_missing() {
2017        let config = MockConfig {
2018            any_order: true,
2019            ..Default::default()
2020        };
2021        let component = MockComponent::with_config(config);
2022        let endpoint = component
2023            .create_endpoint("mock:anyorder-fail", &NoOpComponentContext)
2024            .unwrap();
2025        let inner = component.get_endpoint("anyorder-fail").unwrap();
2026
2027        inner.expect_body(camel_component_api::Body::Text("gamma".into()));
2028        inner.expect_body(camel_component_api::Body::Text("alpha".into()));
2029
2030        let ctx = test_producer_ctx();
2031        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2032        producer
2033            .call(Exchange::new(Message::new("alpha")))
2034            .await
2035            .unwrap();
2036        producer
2037            .call(Exchange::new(Message::new("beta")))
2038            .await
2039            .unwrap();
2040
2041        inner.assert_satisfied().await;
2042    }
2043
2044    // -----------------------------------------------------------------------
2045    // MOCK-012: tracing instrumentation tests (compilation + basic)
2046    // -----------------------------------------------------------------------
2047
2048    #[tokio::test]
2049    async fn test_tracing_logs_exchange_received() {
2050        // Verify the producer doesn't panic and the debug trace fires
2051        let ctx = test_producer_ctx();
2052        let component = MockComponent::new();
2053        let endpoint = component
2054            .create_endpoint("mock:trace", &NoOpComponentContext)
2055            .unwrap();
2056        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2057        producer
2058            .call(Exchange::new(Message::new("traced")))
2059            .await
2060            .unwrap();
2061
2062        let inner = component.get_endpoint("trace").unwrap();
2063        inner.assert_exchange_count(1).await;
2064    }
2065
2066    // -----------------------------------------------------------------------
2067    // MOCK-006 / MOCK-007: doctest exists on MockConfig
2068    // -----------------------------------------------------------------------
2069
2070    #[test]
2071    fn test_mock_config_new() {
2072        let cfg = MockConfig::new(42);
2073        assert_eq!(cfg.max_retained, 42);
2074        assert!(!cfg.copy_on_exchange);
2075        assert!(!cfg.fail_fast);
2076        assert!(!cfg.any_order);
2077    }
2078
2079    // -----------------------------------------------------------------------
2080    // M1: fail-fast trigger + assert_satisfied wires fail_fast_error
2081    // -----------------------------------------------------------------------
2082
2083    use futures::FutureExt;
2084
2085    #[tokio::test]
2086    async fn test_trigger_fail_fast_rejects_subsequent_producer() {
2087        use camel_component_api::CamelError;
2088        use std::panic::AssertUnwindSafe;
2089        let config = MockConfig {
2090            fail_fast: true,
2091            ..Default::default()
2092        };
2093        let component = MockComponent::with_config(config);
2094        let endpoint = component
2095            .create_endpoint("mock:test", &NoOpComponentContext)
2096            .unwrap();
2097        let inner = component.get_endpoint("test").unwrap();
2098
2099        inner.trigger_fail_fast(CamelError::ProcessorError("boom".to_string()));
2100
2101        let ctx = test_producer_ctx();
2102        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2103        // poll_ready must reject in fail-fast mode.
2104        assert!(producer.ready().await.is_err());
2105        // The next call must reject with the fixed "fail-fast mode" message.
2106        let result = AssertUnwindSafe(producer.call(Exchange::default()))
2107            .catch_unwind()
2108            .await
2109            .expect("call should not panic");
2110        match result {
2111            Err(CamelError::ProcessorError(msg)) => {
2112                assert!(
2113                    msg.contains("fail-fast mode"),
2114                    "message should contain 'fail-fast mode', got: {msg}"
2115                );
2116                assert!(
2117                    !msg.contains("boom"),
2118                    "supplied error must NOT be in fixed message, got: {msg}"
2119                );
2120            }
2121            other => panic!("expected ProcessorError, got {other:?}"),
2122        }
2123    }
2124
2125    #[tokio::test]
2126    async fn test_trigger_fail_fast_noop_when_fail_fast_false() {
2127        use camel_component_api::CamelError;
2128        use std::panic::AssertUnwindSafe;
2129        let config = MockConfig {
2130            fail_fast: false,
2131            ..Default::default()
2132        };
2133        let component = MockComponent::with_config(config);
2134        let endpoint = component
2135            .create_endpoint("mock:test", &NoOpComponentContext)
2136            .unwrap();
2137        let inner = component.get_endpoint("test").unwrap();
2138
2139        inner.trigger_fail_fast(CamelError::ProcessorError("boom".to_string()));
2140
2141        let ctx = test_producer_ctx();
2142        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2143        let result = AssertUnwindSafe(producer.call(Exchange::default()))
2144            .catch_unwind()
2145            .await
2146            .expect("call should not panic");
2147        assert!(
2148            result.is_ok(),
2149            "fail_fast=false must let the call through even with stored error"
2150        );
2151    }
2152
2153    #[tokio::test]
2154    async fn test_reset_clears_trigger_fail_fast() {
2155        use camel_component_api::CamelError;
2156        use std::panic::AssertUnwindSafe;
2157        let config = MockConfig {
2158            fail_fast: true,
2159            ..Default::default()
2160        };
2161        let component = MockComponent::with_config(config);
2162        let endpoint = component
2163            .create_endpoint("mock:test", &NoOpComponentContext)
2164            .unwrap();
2165        let inner = component.get_endpoint("test").unwrap();
2166
2167        inner.trigger_fail_fast(CamelError::ProcessorError("boom".to_string()));
2168        assert!(inner.fail_fast_error().is_some());
2169        inner.reset().await;
2170        assert!(inner.fail_fast_error().is_none());
2171
2172        // Producer should accept the call now that reset cleared the error.
2173        let ctx = test_producer_ctx();
2174        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2175        let result = AssertUnwindSafe(producer.call(Exchange::default()))
2176            .catch_unwind()
2177            .await
2178            .expect("call should not panic");
2179        assert!(result.is_ok());
2180    }
2181
2182    #[tokio::test]
2183    async fn test_assert_satisfied_body_count_mismatch_sets_fail_fast() {
2184        use std::panic::AssertUnwindSafe;
2185        let config = MockConfig {
2186            fail_fast: true,
2187            ..Default::default()
2188        };
2189        let component = MockComponent::with_config(config);
2190        let endpoint = component
2191            .create_endpoint("mock:test", &NoOpComponentContext)
2192            .unwrap();
2193        let inner = component.get_endpoint("test").unwrap();
2194
2195        inner.expect_body(camel_component_api::Body::Text("a".to_string()));
2196        inner.expect_body(camel_component_api::Body::Text("b".to_string()));
2197
2198        // Send only 1 exchange (expects 2 -> mismatch).
2199        let ctx = test_producer_ctx();
2200        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2201        producer
2202            .call(Exchange::new(Message::new("a")))
2203            .await
2204            .unwrap();
2205
2206        // Wrap in catch_unwind so the test does not abort on panic.
2207        let panic_result = AssertUnwindSafe(inner.assert_satisfied())
2208            .catch_unwind()
2209            .await;
2210        assert!(
2211            panic_result.is_err(),
2212            "expected panic from assert_satisfied"
2213        );
2214        assert!(
2215            inner.fail_fast_error().is_some(),
2216            "fail_fast_error must be set when fail_fast=true and assertion panics"
2217        );
2218    }
2219
2220    #[tokio::test]
2221    async fn test_assert_satisfied_body_mismatch_sets_fail_fast() {
2222        use std::panic::AssertUnwindSafe;
2223        let config = MockConfig {
2224            fail_fast: true,
2225            ..Default::default()
2226        };
2227        let component = MockComponent::with_config(config);
2228        let endpoint = component
2229            .create_endpoint("mock:test", &NoOpComponentContext)
2230            .unwrap();
2231        let inner = component.get_endpoint("test").unwrap();
2232
2233        inner.expect_body(camel_component_api::Body::Text("expected".to_string()));
2234
2235        let ctx = test_producer_ctx();
2236        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2237        producer
2238            .call(Exchange::new(Message::new("actual")))
2239            .await
2240            .unwrap();
2241
2242        let panic_result = AssertUnwindSafe(inner.assert_satisfied())
2243            .catch_unwind()
2244            .await;
2245        assert!(
2246            panic_result.is_err(),
2247            "expected panic from assert_satisfied"
2248        );
2249        assert!(
2250            inner.fail_fast_error().is_some(),
2251            "fail_fast_error must be set on body mismatch when fail_fast=true"
2252        );
2253    }
2254
2255    #[tokio::test]
2256    async fn test_assert_satisfied_no_set_error_when_fail_fast_false() {
2257        use std::panic::AssertUnwindSafe;
2258        let config = MockConfig {
2259            fail_fast: false,
2260            ..Default::default()
2261        };
2262        let component = MockComponent::with_config(config);
2263        let endpoint = component
2264            .create_endpoint("mock:test", &NoOpComponentContext)
2265            .unwrap();
2266        let inner = component.get_endpoint("test").unwrap();
2267
2268        inner.expect_body(camel_component_api::Body::Text("a".to_string()));
2269        inner.expect_body(camel_component_api::Body::Text("b".to_string()));
2270
2271        // Send only 1 exchange.
2272        let ctx = test_producer_ctx();
2273        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2274        producer
2275            .call(Exchange::new(Message::new("a")))
2276            .await
2277            .unwrap();
2278
2279        let panic_result = AssertUnwindSafe(inner.assert_satisfied())
2280            .catch_unwind()
2281            .await;
2282        assert!(
2283            panic_result.is_err(),
2284            "expected panic from assert_satisfied"
2285        );
2286        assert!(
2287            inner.fail_fast_error().is_none(),
2288            "fail_fast_error must remain None when fail_fast=false"
2289        );
2290    }
2291
2292    #[tokio::test]
2293    async fn test_assert_satisfied_any_order_body_mismatch_sets_fail_fast() {
2294        use std::panic::AssertUnwindSafe;
2295        let config = MockConfig {
2296            fail_fast: true,
2297            any_order: true,
2298            ..Default::default()
2299        };
2300        let component = MockComponent::with_config(config);
2301        let endpoint = component
2302            .create_endpoint("mock:test", &NoOpComponentContext)
2303            .unwrap();
2304        let inner = component.get_endpoint("test").unwrap();
2305
2306        inner.expect_body(camel_component_api::Body::Text("a".to_string()));
2307        inner.expect_body(camel_component_api::Body::Text("b".to_string()));
2308
2309        // Send 2 exchanges: "a" and "c" — "b" is missing.
2310        let ctx = test_producer_ctx();
2311        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2312        producer
2313            .call(Exchange::new(Message::new("a")))
2314            .await
2315            .unwrap();
2316        producer
2317            .call(Exchange::new(Message::new("c")))
2318            .await
2319            .unwrap();
2320
2321        let panic_result = AssertUnwindSafe(inner.assert_satisfied())
2322            .catch_unwind()
2323            .await;
2324        assert!(
2325            panic_result.is_err(),
2326            "expected panic from assert_satisfied (any-order body not found)"
2327        );
2328        assert!(
2329            inner.fail_fast_error().is_some(),
2330            "fail_fast_error must be set when fail_fast=true and any-order body is missing"
2331        );
2332    }
2333
2334    #[tokio::test]
2335    async fn test_assert_satisfied_header_missing_sets_fail_fast() {
2336        use std::panic::AssertUnwindSafe;
2337        let config = MockConfig {
2338            fail_fast: true,
2339            ..Default::default()
2340        };
2341        let component = MockComponent::with_config(config);
2342        let endpoint = component
2343            .create_endpoint("mock:test", &NoOpComponentContext)
2344            .unwrap();
2345        let inner = component.get_endpoint("test").unwrap();
2346
2347        inner.expect_header("x-missing", serde_json::json!("value"));
2348
2349        // Send 1 exchange without the expected header.
2350        let ctx = test_producer_ctx();
2351        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
2352        producer
2353            .call(Exchange::new(Message::new("body")))
2354            .await
2355            .unwrap();
2356
2357        let panic_result = AssertUnwindSafe(inner.assert_satisfied())
2358            .catch_unwind()
2359            .await;
2360        assert!(
2361            panic_result.is_err(),
2362            "expected panic from assert_satisfied (header missing)"
2363        );
2364        assert!(
2365            inner.fail_fast_error().is_some(),
2366            "fail_fast_error must be set when fail_fast=true and expected header is missing"
2367        );
2368    }
2369}