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