Skip to main content

camel_component_mock/
inner.rs

1//! Endpoint internals for the mock component.
2//!
3//! Holds the shared per-endpoint state ([`MockEndpointInner`]), the thin
4//! [`MockEndpoint`] wrapper, the recording [`MockProducer`], and the
5//! synchronous [`ExchangeAssert`] handle. These types are re-exported from the
6//! crate root; the public API is unchanged.
7
8use std::collections::VecDeque;
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::Arc;
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::task::{Context, Poll};
14
15use tokio::sync::{Mutex, Notify};
16use tower::Service;
17
18use camel_component_api::{BoxProcessor, CamelError, Exchange};
19use camel_component_api::{Consumer, Endpoint, ProducerContext, RuntimeObservability};
20use camel_matchers::CountBound;
21use tracing::debug;
22
23use crate::MockAssertionError;
24use crate::MockExpectations;
25use crate::matcher::{BodyMatcher, HeaderMatcher};
26
27// ---------------------------------------------------------------------------
28// MockEndpoint / MockEndpointInner
29// ---------------------------------------------------------------------------
30
31/// A mock endpoint that records all exchanges sent to it.
32///
33/// This is a thin wrapper around `Arc<MockEndpointInner>`. Multiple
34/// `MockEndpoint` instances created with the same name share the same inner
35/// storage.
36pub struct MockEndpoint(pub(crate) Arc<MockEndpointInner>);
37
38/// The actual data behind a mock endpoint. Shared across all `MockEndpoint`
39/// instances created with the same name via `MockComponent`.
40///
41/// Use `get_received_exchanges` and `assert_exchange_count` to inspect
42/// recorded exchanges in tests.
43pub struct MockEndpointInner {
44    pub(crate) uri: String,
45    pub(crate) name: String,
46    pub(crate) received: Arc<Mutex<VecDeque<Exchange>>>,
47    pub(crate) notify: Arc<Notify>,
48    pub(crate) max_retained: usize,
49    pub(crate) copy_on_exchange: bool,
50    pub(crate) fail_fast: bool,
51    pub(crate) fail_fast_error: Arc<std::sync::Mutex<Option<CamelError>>>,
52    pub(crate) assert_period_ms: u64,
53    pub(crate) any_order: bool,
54    pub(crate) expectations: Arc<std::sync::Mutex<MockExpectations>>,
55    /// Component-wide monotonic counter shared by every endpoint of the
56    /// same `MockComponent`. Stamped inside the `received`-lock critical
57    /// section on every record; never cleared.
58    pub(crate) arrival_counter: Arc<AtomicU64>,
59    /// Arrival indices paired positionally with the retained exchanges;
60    /// the two lists truncate and clear in lockstep. Guarded by its own
61    /// mutex; on the record path it is acquired while already holding
62    /// the `received` lock — keep this nesting order:
63    /// `arrival_indices` is never locked before `received`.
64    pub(crate) arrival_indices: Arc<Mutex<Vec<u64>>>,
65}
66
67impl MockEndpointInner {
68    /// Return a snapshot of all exchanges retained so far.
69    pub async fn get_received_exchanges(&self) -> Vec<Exchange> {
70        self.received.lock().await.iter().cloned().collect()
71    }
72
73    /// Return a snapshot of the arrival indices paired with the currently
74    /// retained exchanges, in arrival order.
75    ///
76    /// Indices come from the component-wide monotonic counter shared by
77    /// every endpoint of the same `MockComponent`: merging the indices of
78    /// several endpoints and sorting yields the global arrival order.
79    /// [`reset`](Self::reset) clears the list but not the counter, so
80    /// post-reset indices keep increasing (no index reuse). Bounded
81    /// retention truncates indices in lockstep with exchanges.
82    pub async fn get_arrival_indices(&self) -> Vec<u64> {
83        self.arrival_indices.lock().await.clone()
84    }
85
86    /// Return the number of currently retained exchanges.
87    pub async fn received_count(&self) -> usize {
88        self.received.lock().await.len()
89    }
90
91    /// Clear all retained exchanges and reset internal counters.
92    ///
93    /// Useful between test cases to reuse the same mock endpoint. Clears
94    /// the arrival indices but not the component-wide arrival counter —
95    /// post-reset arrivals keep increasing, so indices are never reused.
96    pub async fn reset(&self) {
97        self.received.lock().await.clear();
98        self.arrival_indices.lock().await.clear();
99        let mut guard = self
100            .fail_fast_error
101            .lock()
102            .expect("fail_fast_error lock poisoned"); // allow-unwrap
103        *guard = None;
104    }
105
106    /// Assert that exactly `expected` exchanges have been received.
107    ///
108    /// # Panics
109    ///
110    /// Panics if the count does not match.
111    pub async fn assert_exchange_count(&self, expected: usize) {
112        let actual = self.received.lock().await.len();
113        assert_eq!(
114            actual, expected,
115            "MockEndpoint expected {expected} exchanges, got {actual}"
116        );
117    }
118
119    /// Wait until at least `count` exchanges have been received, or panic on timeout.
120    ///
121    /// Uses `tokio::sync::Notify` — no polling. Returns immediately if `count`
122    /// exchanges are already present.
123    ///
124    /// # Panics
125    ///
126    /// Panics if `timeout` elapses before `count` exchanges arrive.
127    pub async fn await_exchanges(&self, count: usize, timeout: std::time::Duration) {
128        let deadline = tokio::time::Instant::now() + timeout;
129        loop {
130            {
131                let received = self.received.lock().await;
132                if received.len() >= count {
133                    return;
134                }
135            }
136            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
137            if remaining.is_zero() {
138                // Re-check in case the final exchange arrived between the lock drop
139                // above and entering the select — Notify does not buffer permits.
140                let got = self.received.lock().await.len();
141                if got >= count {
142                    return;
143                }
144                panic!(
145                    "MockEndpoint '{}': timed out waiting for {} exchanges (got {} after {:?})",
146                    self.name, count, got, timeout
147                );
148            }
149            tokio::select! {
150                _ = self.notify.notified() => {}
151                _ = tokio::time::sleep(remaining) => {}
152            }
153        }
154    }
155
156    /// Wait for exchanges with a configurable timeout derived from `assert_period_ms`.
157    ///
158    /// If `assert_period_ms` is 0, uses the provided `fallback` duration.
159    /// Otherwise, waits for `assert_period_ms` milliseconds before checking.
160    pub async fn await_exchanges_with_timeout(&self, count: usize, fallback: std::time::Duration) {
161        let duration = if self.assert_period_ms > 0 {
162            std::time::Duration::from_millis(self.assert_period_ms)
163        } else {
164            fallback
165        };
166        self.await_exchanges(count, duration).await;
167    }
168
169    /// Return an [`ExchangeAssert`] for the exchange at `idx`.
170    ///
171    /// # Panics
172    ///
173    /// Panics if `idx` is out of bounds. Always call [`await_exchanges`] first
174    /// to ensure the exchange has been received.
175    ///
176    /// Panics immediately if called from a current-thread tokio runtime.
177    /// Use `#[tokio::test(flavor = "multi_thread")]` or the async accessors
178    /// [`get_received_exchanges`](MockEndpointInner::get_received_exchanges) / [`await_exchanges`] instead.
179    ///
180    /// [`await_exchanges`]: MockEndpointInner::await_exchanges
181    pub fn exchange(&self, idx: usize) -> ExchangeAssert {
182        if let Ok(handle) = tokio::runtime::Handle::try_current()
183            && handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::CurrentThread
184        {
185            panic!(
186                "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()",
187                self.name
188            );
189        }
190        let received = tokio::task::block_in_place(|| self.received.blocking_lock());
191        if idx >= received.len() {
192            panic!(
193                "MockEndpoint '{}': exchange index {} out of bounds (got {} exchanges)",
194                self.name,
195                idx,
196                received.len()
197            );
198        }
199        ExchangeAssert {
200            exchange: received[idx].clone(),
201            idx,
202            endpoint_name: self.name.clone(),
203        }
204    }
205
206    /// Set an exact count expectation: `assert_satisfied` panics unless the
207    /// number of retained exchanges equals `n`.
208    ///
209    /// Sugar for [`expect_bound`](Self::expect_bound) with
210    /// [`CountBound::Exact`].
211    pub fn expect_count(&self, n: usize) {
212        self.expect_bound(CountBound::Exact(n as u64));
213    }
214
215    /// Set a minimum count expectation: `assert_satisfied` panics unless at
216    /// least `n` exchanges are retained.
217    ///
218    /// Sugar for [`expect_bound`](Self::expect_bound) with
219    /// [`CountBound::AtLeast`].
220    pub fn expect_minimum_count(&self, n: usize) {
221        self.expect_bound(CountBound::AtLeast(n as u64));
222    }
223
224    /// Set a maximum count expectation (an absence claim): `assert_satisfied`
225    /// panics unless at most `n` exchanges are retained.
226    ///
227    /// Sugar for [`expect_bound`](Self::expect_bound) with
228    /// [`CountBound::AtMost`].
229    pub fn expect_maximum_count(&self, n: usize) {
230        self.expect_bound(CountBound::AtMost(n as u64));
231    }
232
233    /// Set a count bound from the shared matcher algebra: `assert_satisfied`
234    /// panics unless the number of retained exchanges satisfies `bound`
235    /// (evaluated on the post-settle snapshot). Exactly one bound is kept per
236    /// endpoint — a later setter replaces the earlier one.
237    pub fn expect_bound(&self, bound: CountBound) {
238        let mut guard = self
239            .expectations
240            .lock()
241            .expect("expectations lock poisoned"); // allow-unwrap
242        if guard.count_bound.is_some() {
243            debug!(
244                endpoint_name = %self.name,
245                new = ?bound,
246                "a later setter replaced an earlier count bound"
247            );
248        }
249        guard.set_count_bound(bound);
250    }
251
252    /// Add an expected body to the expectations list.
253    pub fn expect_body(&self, body: camel_component_api::Body) {
254        let mut guard = self
255            .expectations
256            .lock()
257            .expect("expectations lock poisoned"); // allow-unwrap
258        guard.push_body(body);
259    }
260
261    /// Add a body matcher to the expectations list.
262    ///
263    /// Matchers share the ordered slot list with
264    /// [`expect_body`](Self::expect_body): mixed sequences keep their
265    /// insertion order.
266    pub fn expect_body_matcher(&self, matcher: BodyMatcher) {
267        let mut guard = self
268            .expectations
269            .lock()
270            .expect("expectations lock poisoned"); // allow-unwrap
271        guard.push_body_matcher(matcher);
272    }
273
274    /// Add an expected header key-value pair to the expectations list.
275    pub fn expect_header(&self, key: &str, value: impl Into<serde_json::Value>) {
276        let mut guard = self
277            .expectations
278            .lock()
279            .expect("expectations lock poisoned"); // allow-unwrap
280        guard.push_header(key.to_string(), value.into());
281    }
282
283    /// Add an expected header regex pattern to the expectations list.
284    ///
285    /// After `await_exchanges()`, `assert_satisfied()` checks whether any
286    /// received exchange has the named header matching the given regex pattern.
287    pub fn expect_header_regex(&self, key: &str, pattern: &str) {
288        let mut guard = self
289            .expectations
290            .lock()
291            .expect("expectations lock poisoned"); // allow-unwrap
292        guard.push_header_regex(key.to_string(), pattern.to_string());
293    }
294
295    /// Add a header matcher to the expectations list.
296    ///
297    /// After `await_exchanges()`, `assert_satisfied()` checks whether any
298    /// received exchange has the named header satisfying the matcher.
299    /// Unlike [`expect_header_regex`](Self::expect_header_regex), a
300    /// [`HeaderMatcher::Regex`] requires the received value to be a string.
301    pub fn expect_header_matcher(&self, key: &str, matcher: HeaderMatcher) {
302        let mut guard = self
303            .expectations
304            .lock()
305            .expect("expectations lock poisoned"); // allow-unwrap
306        guard.push_header_matcher(key.to_string(), matcher);
307    }
308
309    /// Assert that all registered expectations are satisfied.
310    ///
311    /// # Panics
312    ///
313    /// Panics if the recorded count bound (see
314    /// [`expect_bound`](Self::expect_bound) and its
315    /// [`expect_count`](Self::expect_count) /
316    /// [`expect_minimum_count`](Self::expect_minimum_count) /
317    /// [`expect_maximum_count`](Self::expect_maximum_count) sugar) is not
318    /// met, if expected bodies or body matchers do not match received bodies
319    /// (in order or any order depending on `any_order` config), if expected
320    /// headers are missing, if header regex patterns do not match, or if
321    /// header matchers fail.
322    pub async fn assert_satisfied(&self) {
323        if let Err(e) = self.evaluate_expectations().await {
324            panic!("{e}");
325        }
326    }
327
328    /// Assert that all registered expectations are satisfied without
329    /// panicking.
330    ///
331    /// Performs the same checks as [`assert_satisfied`](Self::assert_satisfied)
332    /// (see it for the full list) and evaluates the same fail-fast latch
333    /// rules, but returns the mismatch as [`MockAssertionError`] instead of
334    /// panicking.
335    ///
336    /// # Errors
337    ///
338    /// Returns `Err(MockAssertionError)` when any expectation is not met or
339    /// an expectation is malformed (e.g. a header regex pattern that fails
340    /// to compile). Diagnostic payloads keep the error above the
341    /// `result_large_err` size threshold (clippy allow mirrors
342    /// `do_try_segment.rs`).
343    #[allow(clippy::result_large_err)]
344    pub async fn try_assert_satisfied(&self) -> Result<(), MockAssertionError> {
345        self.evaluate_expectations().await
346    }
347
348    /// Return the stored fail-fast error, if any.
349    pub fn fail_fast_error(&self) -> Option<CamelError> {
350        let guard = self
351            .fail_fast_error
352            .lock()
353            .expect("fail_fast_error lock poisoned"); // allow-unwrap
354        guard.clone()
355    }
356
357    /// Manually trip the fail-fast latch.
358    ///
359    /// Sets the internal `fail_fast_error` to `Some(error)`. The `MockProducer`
360    /// treats the presence of any error here as a sentinel — the actual
361    /// `CamelError` value is never propagated to the caller; a fixed
362    /// "fail-fast mode" message is returned instead. Use this hook when a
363    /// downstream component wants to short-circuit further processing on this
364    /// endpoint.
365    pub fn trigger_fail_fast(&self, error: CamelError) {
366        let mut guard = self
367            .fail_fast_error
368            .lock()
369            .expect("fail_fast_error lock poisoned"); // allow-unwrap
370        *guard = Some(error);
371    }
372
373    /// When `fail_fast` is enabled, record the assertion-mismatch sentinel
374    /// before panicking. This ensures any concurrent or subsequent
375    /// `MockProducer::poll_ready` / `call` invocation rejects with the fixed
376    /// "fail-fast mode" message instead of being blocked on a panic-orphaned
377    /// lock or a stale `None` sentinel.
378    pub(crate) fn set_fail_fast_on_mismatch(&self) {
379        if self.fail_fast {
380            let mut guard = self
381                .fail_fast_error
382                .lock()
383                .expect("fail_fast_error lock poisoned"); // allow-unwrap
384            *guard = Some(CamelError::ProcessorError(
385                "assert_satisfied expectation mismatch".to_string(),
386            ));
387        }
388    }
389}
390
391impl Endpoint for MockEndpoint {
392    fn uri(&self) -> &str {
393        &self.0.uri
394    }
395
396    fn create_consumer(
397        &self,
398        _rt: Arc<dyn RuntimeObservability>,
399    ) -> Result<Box<dyn Consumer>, CamelError> {
400        Err(CamelError::EndpointCreationFailed(
401            "mock endpoint does not support consumers (it is a sink)".to_string(),
402        ))
403    }
404
405    fn create_producer(
406        &self,
407        _rt: Arc<dyn RuntimeObservability>,
408        _ctx: &ProducerContext,
409    ) -> Result<BoxProcessor, CamelError> {
410        Ok(BoxProcessor::new(MockProducer {
411            name: self.0.name.clone(),
412            received: Arc::clone(&self.0.received),
413            notify: Arc::clone(&self.0.notify),
414            max_retained: self.0.max_retained,
415            copy_on_exchange: self.0.copy_on_exchange,
416            fail_fast: self.0.fail_fast,
417            fail_fast_error: Arc::clone(&self.0.fail_fast_error),
418            arrival_counter: Arc::clone(&self.0.arrival_counter),
419            arrival_indices: Arc::clone(&self.0.arrival_indices),
420        }))
421    }
422}
423
424// ---------------------------------------------------------------------------
425// MockProducer
426// ---------------------------------------------------------------------------
427
428/// A producer that simply records each exchange it processes.
429#[derive(Clone)]
430struct MockProducer {
431    name: String,
432    received: Arc<Mutex<VecDeque<Exchange>>>,
433    notify: Arc<Notify>,
434    max_retained: usize,
435    copy_on_exchange: bool,
436    fail_fast: bool,
437    fail_fast_error: Arc<std::sync::Mutex<Option<CamelError>>>,
438    arrival_counter: Arc<AtomicU64>,
439    arrival_indices: Arc<Mutex<Vec<u64>>>,
440}
441
442impl Service<Exchange> for MockProducer {
443    type Response = Exchange;
444    type Error = CamelError;
445    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
446
447    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
448        // In fail-fast mode, reject new exchanges if a previous one failed
449        if self.fail_fast
450            && let Ok(guard) = self.fail_fast_error.lock()
451            && guard.is_some()
452        {
453            return Poll::Ready(Err(CamelError::ProcessorError(
454                "mock endpoint in fail-fast mode: a previous exchange caused an error".to_string(),
455            )));
456        }
457        Poll::Ready(Ok(()))
458    }
459
460    fn call(&mut self, exchange: Exchange) -> Self::Future {
461        let name = self.name.clone();
462        let received = Arc::clone(&self.received);
463        let notify = Arc::clone(&self.notify);
464        let max_retained = self.max_retained;
465        let copy_on_exchange = self.copy_on_exchange;
466        let fail_fast = self.fail_fast;
467        let fail_fast_error = Arc::clone(&self.fail_fast_error);
468        let arrival_counter = Arc::clone(&self.arrival_counter);
469        let arrival_indices = Arc::clone(&self.arrival_indices);
470        Box::pin(async move {
471            // In fail-fast mode, check if a previous error was recorded
472            if fail_fast
473                && let Ok(guard) = fail_fast_error.lock()
474                && guard.is_some()
475            {
476                return Err(CamelError::ProcessorError(
477                    "mock endpoint in fail-fast mode: a previous exchange caused an error"
478                        .to_string(),
479                ));
480            }
481
482            let correlation_id = exchange
483                .input
484                .headers
485                .get("CamelCorrelationId")
486                .and_then(|v| v.as_str())
487                .map(|s| s.to_string());
488
489            let exchange_to_store = if copy_on_exchange {
490                let mut cloned = exchange.clone();
491                // Deep-clone the body to break aliasing
492                cloned.input.body = clone_body(&exchange.input.body);
493                cloned
494            } else {
495                exchange.clone()
496            };
497
498            let mut guard = received.lock().await;
499            // Stamp the arrival index while holding the `received` lock:
500            // concurrent sends to this endpoint serialize here, so the
501            // index order matches the push order by construction. Stamping
502            // outside the lock would let two sends push [7, 6].
503            let arrival = arrival_counter.fetch_add(1, Ordering::Relaxed);
504            let mut indices = arrival_indices.lock().await;
505            if guard.len() >= max_retained {
506                tracing::warn!(
507                    endpoint_name = %name,
508                    max = max_retained,
509                    "max retained exchanges reached, dropping oldest"
510                );
511                guard.pop_front();
512                // Drop the index paired with the truncated exchange so
513                // indices always pair positionally with retained exchanges.
514                indices.remove(0);
515            }
516            guard.push_back(exchange_to_store);
517            indices.push(arrival);
518            let count = guard.len();
519            drop(indices);
520            drop(guard);
521
522            debug!(
523                endpoint_name = %name,
524                count = %count,
525                correlation_id = correlation_id.as_deref().unwrap_or("none"),
526                "exchange recorded on mock"
527            );
528            notify.notify_waiters();
529
530            Ok(exchange)
531        })
532    }
533}
534
535/// Deep-clone a `Body` value.
536pub(crate) fn clone_body(body: &camel_component_api::Body) -> camel_component_api::Body {
537    match body {
538        camel_component_api::Body::Empty => camel_component_api::Body::Empty,
539        camel_component_api::Body::Text(s) => camel_component_api::Body::Text(s.clone()),
540        camel_component_api::Body::Json(v) => camel_component_api::Body::Json(v.clone()),
541        camel_component_api::Body::Xml(s) => camel_component_api::Body::Xml(s.clone()),
542        camel_component_api::Body::Bytes(b) => camel_component_api::Body::Bytes(b.clone()),
543        camel_component_api::Body::Stream(s) => camel_component_api::Body::Stream(s.clone()),
544        // Safety net for future #[non_exhaustive] variants; all current variants
545        // are handled explicitly above.
546        _ => camel_component_api::Body::Empty,
547    }
548}
549
550// ---------------------------------------------------------------------------
551// ExchangeAssert
552// ---------------------------------------------------------------------------
553
554/// A handle for making synchronous assertions on a recorded exchange.
555///
556/// Obtain one via [`MockEndpointInner::exchange`] after calling
557/// [`MockEndpointInner::await_exchanges`].
558///
559/// All methods panic with descriptive messages on failure, making test output
560/// self-explanatory without additional context.
561pub struct ExchangeAssert {
562    exchange: Exchange,
563    idx: usize,
564    endpoint_name: String,
565}
566
567impl ExchangeAssert {
568    fn location(&self) -> String {
569        format!(
570            "MockEndpoint '{}' exchange[{}]",
571            self.endpoint_name, self.idx
572        )
573    }
574
575    /// Assert that the body is `Body::Text` equal to `expected`.
576    pub fn assert_body_text(self, expected: &str) -> Self {
577        match self.exchange.input.body.as_text() {
578            Some(actual) if actual == expected => {}
579            Some(actual) => panic!(
580                "{}: expected body text {:?}, got {:?}",
581                self.location(),
582                expected,
583                actual
584            ),
585            None => panic!(
586                "{}: expected body text {:?}, but body is not Body::Text (got {:?})",
587                self.location(),
588                expected,
589                self.exchange.input.body
590            ),
591        }
592        self
593    }
594
595    /// Assert that the body is `Body::Json` equal to `expected`.
596    pub fn assert_body_json(self, expected: serde_json::Value) -> Self {
597        match &self.exchange.input.body {
598            camel_component_api::Body::Json(actual) if *actual == expected => {}
599            camel_component_api::Body::Json(actual) => panic!(
600                "{}: expected body JSON {}, got {}",
601                self.location(),
602                expected,
603                actual
604            ),
605            other => panic!(
606                "{}: expected body JSON {}, but body is not Body::Json (got {:?})",
607                self.location(),
608                expected,
609                other
610            ),
611        }
612        self
613    }
614
615    /// Assert that the body is `Body::Bytes` equal to `expected`.
616    pub fn assert_body_bytes(self, expected: &[u8]) -> Self {
617        match &self.exchange.input.body {
618            camel_component_api::Body::Bytes(actual) if actual.as_ref() == expected => {}
619            camel_component_api::Body::Bytes(actual) => panic!(
620                "{}: expected body bytes {:?}, got {:?}",
621                self.location(),
622                expected,
623                actual
624            ),
625            other => panic!(
626                "{}: expected body bytes {:?}, but body is not Body::Bytes (got {:?})",
627                self.location(),
628                expected,
629                other
630            ),
631        }
632        self
633    }
634
635    /// Assert that header `key` exists and equals `expected`.
636    ///
637    /// # Panics
638    ///
639    /// Panics if the header is missing or its value does not match `expected`.
640    pub fn assert_header(self, key: &str, expected: serde_json::Value) -> Self {
641        match self.exchange.input.headers.get(key) {
642            Some(actual) if *actual == expected => {}
643            Some(actual) => panic!(
644                "{}: expected header {:?} = {}, got {}",
645                self.location(),
646                key,
647                expected,
648                actual
649            ),
650            None => panic!(
651                "{}: expected header {:?} = {}, but header is absent",
652                self.location(),
653                key,
654                expected
655            ),
656        }
657        self
658    }
659
660    /// Assert that header `key` is present (any value).
661    ///
662    /// # Panics
663    ///
664    /// Panics if the header key is absent.
665    pub fn assert_header_exists(self, key: &str) -> Self {
666        if !self.exchange.input.headers.contains_key(key) {
667            panic!(
668                "{}: expected header {:?} to be present, but it was absent",
669                self.location(),
670                key
671            );
672        }
673        self
674    }
675
676    /// Assert that the exchange has an error (`exchange.error` is `Some`).
677    ///
678    /// # Panics
679    ///
680    /// Panics if `exchange.error` is `None`.
681    pub fn assert_has_error(self) -> Self {
682        if self.exchange.error.is_none() {
683            panic!(
684                "{}: expected exchange to have an error, but error is None",
685                self.location()
686            );
687        }
688        self
689    }
690
691    /// Assert that the exchange has no error (`exchange.error` is `None`).
692    ///
693    /// # Panics
694    ///
695    /// Panics if `exchange.error` is `Some`.
696    pub fn assert_no_error(self) -> Self {
697        if let Some(ref err) = self.exchange.error {
698            panic!(
699                "{}: expected exchange to have no error, but got: {}",
700                self.location(),
701                err
702            );
703        }
704        self
705    }
706}
707
708// ---------------------------------------------------------------------------
709// Tests — global arrival indices
710// ---------------------------------------------------------------------------
711
712#[cfg(test)]
713mod tests {
714    use camel_component_api::test_support::PanicRuntimeObservability;
715    use camel_component_api::{Exchange, Message, NoOpComponentContext, ProducerContext};
716    use tower::Service;
717
718    use crate::MockComponent;
719    use camel_component_api::Component;
720
721    fn rt() -> std::sync::Arc<dyn camel_component_api::RuntimeObservability> {
722        std::sync::Arc::new(PanicRuntimeObservability)
723    }
724
725    #[tokio::test]
726    async fn arrival_indices_strictly_increasing_across_endpoints() {
727        let ctx = ProducerContext::new();
728        let component = MockComponent::new();
729        let ep_a = component
730            .create_endpoint("mock:a", &NoOpComponentContext)
731            .unwrap();
732        let ep_b = component
733            .create_endpoint("mock:b", &NoOpComponentContext)
734            .unwrap();
735        let mut pa = ep_a.create_producer(rt(), &ctx).unwrap();
736        let mut pb = ep_b.create_producer(rt(), &ctx).unwrap();
737
738        pa.call(Exchange::new(Message::new("a0"))).await.unwrap();
739        pb.call(Exchange::new(Message::new("b0"))).await.unwrap();
740        pa.call(Exchange::new(Message::new("a1"))).await.unwrap();
741        pb.call(Exchange::new(Message::new("b1"))).await.unwrap();
742
743        let a = component
744            .get_endpoint("a")
745            .unwrap()
746            .get_arrival_indices()
747            .await;
748        let b = component
749            .get_endpoint("b")
750            .unwrap()
751            .get_arrival_indices()
752            .await;
753        assert_eq!(a, vec![0, 2]);
754        assert_eq!(b, vec![1, 3]);
755
756        let mut merged = a;
757        merged.extend(b);
758        merged.sort_unstable();
759        assert!(
760            merged.windows(2).all(|w| w[0] < w[1]),
761            "merged indices must be strictly increasing, got {merged:?}"
762        );
763    }
764
765    #[tokio::test]
766    async fn arrival_indices_truncate_in_lockstep_with_retention() {
767        let ctx = ProducerContext::new();
768        let component = MockComponent::new();
769        let endpoint = component
770            .create_endpoint("mock:x?retain=2", &NoOpComponentContext)
771            .unwrap();
772        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
773
774        for body in ["first", "second", "third"] {
775            producer
776                .call(Exchange::new(Message::new(body)))
777                .await
778                .unwrap();
779        }
780
781        let inner = component.get_endpoint("x").unwrap();
782        let indices = inner.get_arrival_indices().await;
783        // Stamped 0, 1, 2 — the first index is dropped with its exchange.
784        assert_eq!(indices, vec![1, 2]);
785
786        let received = inner.get_received_exchanges().await;
787        assert_eq!(received.len(), indices.len());
788        assert_eq!(received[0].input.body.as_text(), Some("second"));
789        assert_eq!(received[1].input.body.as_text(), Some("third"));
790    }
791
792    #[tokio::test]
793    async fn reset_clears_indices_but_counter_stays_monotonic() {
794        let ctx = ProducerContext::new();
795        let component = MockComponent::new();
796        let endpoint = component
797            .create_endpoint("mock:r", &NoOpComponentContext)
798            .unwrap();
799        let mut producer = endpoint.create_producer(rt(), &ctx).unwrap();
800
801        producer
802            .call(Exchange::new(Message::new("one")))
803            .await
804            .unwrap();
805        producer
806            .call(Exchange::new(Message::new("two")))
807            .await
808            .unwrap();
809
810        let inner = component.get_endpoint("r").unwrap();
811        assert_eq!(inner.get_arrival_indices().await, vec![0, 1]);
812
813        inner.reset().await;
814        assert!(inner.get_arrival_indices().await.is_empty());
815
816        producer
817            .call(Exchange::new(Message::new("three")))
818            .await
819            .unwrap();
820        // Component-wide counter is not reset: the post-reset index is 2.
821        assert_eq!(inner.get_arrival_indices().await, vec![2]);
822    }
823
824    #[tokio::test(flavor = "multi_thread")]
825    async fn concurrent_sends_preserve_per_endpoint_order() {
826        let ctx = ProducerContext::new();
827        let component = MockComponent::new();
828        let endpoint = component
829            .create_endpoint("mock:c", &NoOpComponentContext)
830            .unwrap();
831
832        let mut producers: Vec<_> = (0..32)
833            .map(|_| endpoint.create_producer(rt(), &ctx).unwrap())
834            .collect();
835        let sends = producers
836            .iter_mut()
837            .enumerate()
838            .map(|(i, p)| p.call(Exchange::new(Message::new(format!("m{i}")))));
839        let _ = futures::future::join_all(sends).await;
840
841        let inner = component.get_endpoint("c").unwrap();
842        let indices = inner.get_arrival_indices().await;
843        assert_eq!(indices.len(), 32);
844        assert!(
845            indices.windows(2).all(|w| w[0] < w[1]),
846            "per-endpoint indices must be strictly increasing, got {indices:?}"
847        );
848    }
849}