Skip to main content

camel_component_mock/
lib.rs

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