Skip to main content

camel_integration_test/
adapters.rs

1//! Partner adapters and the endpoint-keyed router (ADR-0069 §5, §7).
2//!
3//! A [`PartnerAdapter`] is the harness-owned far side of the wire: the
4//! listener or client that observes what the system under test puts on
5//! the wire. The runner talks to adapters through this trait only, so
6//! the scenario vocabulary stays transport-agnostic.
7//!
8//! Every adapter operation is bounded: `receive` carries the action's
9//! deadline, and the runner bounds `send` with a fixed timeout. No
10//! adapter call hangs (ADR-0069 §7).
11//!
12//! [`FakeAdapter`] is the in-memory test double: it records sent
13//! messages, plays a scripted incoming queue, and can fail sends and
14//! receives on demand.
15
16use std::collections::BTreeMap;
17use std::fmt;
18use std::sync::Arc;
19use std::sync::Mutex;
20use std::sync::MutexGuard;
21use std::sync::RwLock;
22use std::time::Duration;
23
24use camel_api::Body;
25use camel_api::CamelError;
26use camel_api::Exchange;
27use camel_api::Message;
28use camel_api::Value;
29use camel_component_api::NoOpComponentContext;
30use camel_core::CamelContext;
31use futures::future::BoxFuture;
32use tokio::sync::Mutex as AsyncMutex;
33use tokio::sync::mpsc;
34use tower::ServiceExt;
35
36/// The HTTP partner adapter (feature `http`): a loopback listener
37/// plus client that play both wire roles against the system under
38/// test.
39#[cfg(feature = "http")]
40pub mod http;
41
42/// A message the scenario sends to a partner endpoint.
43#[derive(Debug, Clone, PartialEq)]
44pub struct OutgoingMessage {
45    /// Message body; `Null` when the action declares none.
46    pub body: Value,
47    /// Message headers; empty when the action declares none.
48    pub headers: BTreeMap<String, Value>,
49    /// The resolved HTTP method for client-role sends (explicit from
50    /// the action's `method`, or the inferred `GET`/`POST`). Validated
51    /// as an HTTP token at parse time.
52    pub method: String,
53}
54
55/// A message the harness received from a partner endpoint.
56#[derive(Debug, Clone, PartialEq)]
57pub struct IncomingMessage {
58    /// Received body.
59    pub body: Value,
60    /// Received headers.
61    pub headers: BTreeMap<String, Value>,
62    /// Transport status code when the partner protocol carries one
63    /// (HTTP response status); `None` for transports without a status
64    /// concept.
65    pub status: Option<u16>,
66    /// Request method when the partner protocol carries a request line
67    /// (HTTP server role: the method of the request that reached the
68    /// partner listener); `None` otherwise.
69    pub method: Option<String>,
70    /// Request path (with query, when present) when the partner
71    /// protocol carries a request line; `None` otherwise.
72    pub path: Option<String>,
73    /// Monotonic wire-arrival instant — when the transport finished
74    /// receiving this message — NOT when a receive action consumed it
75    /// (ADR-0069 §5: the wire is the proof).
76    pub arrival: std::time::Instant,
77}
78
79/// A send or receive failed at the transport layer, before any
80/// assertion ran (`action-transport-failure`, ADR-0069 §7).
81///
82/// Apparatus class: the scenario never got a meaningful answer.
83#[derive(Debug, Clone, PartialEq, thiserror::Error)]
84#[non_exhaustive]
85pub enum TransportError {
86    /// No adapter is registered for the endpoint URI.
87    #[error("no partner adapter bound for endpoint {endpoint}")]
88    Unbound {
89        /// The endpoint URI the scenario referenced.
90        endpoint: String,
91    },
92    /// The transport reported a failure.
93    #[error("{message}")]
94    Other {
95        /// Transport-reported failure detail.
96        message: String,
97    },
98    /// The runner's bounded send deadline elapsed.
99    #[error("send did not complete within {after:?}")]
100    Deadline {
101        /// The bounded send deadline the call exceeded.
102        after: Duration,
103    },
104}
105
106/// Nothing reached the partner endpoint before the deadline
107/// (`receive-timeout`, ADR-0069 §7).
108///
109/// Verdict class: the scenario ran and the system under test failed
110/// it. A struct, not an enum: the taxonomy has one receive failure.
111#[derive(Debug, Clone, PartialEq)]
112pub struct ReceiveTimeout {
113    /// The endpoint URI that delivered nothing.
114    pub endpoint: String,
115    /// The deadline that elapsed.
116    pub deadline: Duration,
117    /// How long the receive actually waited before giving up.
118    pub elapsed: Duration,
119    /// The wire `path_and_query` strings that arrived in the matching
120    /// lane group, ALREADY REDACTED for diagnostics (ADR-0051
121    /// positive secret rule) by the construction site; empty when no
122    /// lane recorded an arrival. Appended to [`Display`](fmt::Display)
123    /// so byte divergence is diagnosed without external packet
124    /// capture.
125    pub lanes_recorded: Vec<String>,
126}
127
128impl fmt::Display for ReceiveTimeout {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        write!(
131            f,
132            "nothing reached {} within {:?} (waited {:?})",
133            self.endpoint, self.deadline, self.elapsed
134        )?;
135        f.write_str(&lanes_suffix(&self.lanes_recorded))
136    }
137}
138
139impl std::error::Error for ReceiveTimeout {}
140
141/// The lane-evidence suffix shared by receive-timeout diagnostics:
142/// `; no arrival matched; lanes recorded: [/a, /b]`, or empty when
143/// the construction site saw no lanes.
144pub(crate) fn lanes_suffix(lanes_recorded: &[String]) -> String {
145    if lanes_recorded.is_empty() {
146        String::new()
147    } else {
148        format!(
149            "; no arrival matched; lanes recorded: [{}]",
150            lanes_recorded.join(", ")
151        )
152    }
153}
154
155/// Why a `receive` call did not deliver a message (ADR-0069 §7).
156///
157/// The variants carry the failure class: [`ReceiveError::Timeout`] is
158/// verdict class (the system under test delivered nothing in time);
159/// [`ReceiveError::Transport`] is apparatus class (the receive failed
160/// at the transport before the scenario got a meaningful answer).
161#[derive(Debug, Clone, PartialEq, thiserror::Error)]
162#[non_exhaustive]
163pub enum ReceiveError {
164    /// Nothing reached the partner endpoint before the deadline
165    /// (`receive-timeout`, verdict class).
166    #[error("{0}")]
167    Timeout(ReceiveTimeout),
168    /// The receive failed at the transport layer
169    /// (`action-transport-failure`, apparatus class).
170    #[error("{0}")]
171    Transport(TransportError),
172}
173
174/// The harness-owned far side of the wire for one endpoint family.
175///
176/// Implementations must be `Send + Sync`; calls return boxed futures
177/// so the trait stays object-safe behind `Box<dyn PartnerAdapter>`.
178///
179/// The two-key contract splits the lane from the wire: `lane_key` is
180/// the registered router key whose lane (queue, parked roundtrip) the
181/// call belongs to, `target_uri`/`source_uri` is the resolved address
182/// the scenario referenced. Adapters that own a listener (the http
183/// partner) read their arrival lane by the registered key's request
184/// path; adapters that dial treat the target URI as the wire address.
185pub trait PartnerAdapter: Send + Sync {
186    /// Send a message to the target URI, parking any roundtrip under
187    /// the lane key. Adapters without a client role keep the default
188    /// (a transport failure naming the gap); the http partner is one
189    /// such adapter — the router's own client lane performs every
190    /// http client-role send.
191    fn send<'a>(
192        &'a self,
193        lane_key: &'a str,
194        target_uri: &'a str,
195        msg: OutgoingMessage,
196    ) -> BoxFuture<'a, Result<(), TransportError>> {
197        let _ = (lane_key, target_uri, msg);
198        Box::pin(async {
199            Err(TransportError::Other {
200                message: "adapter does not implement client-role sends".to_string(),
201            })
202        })
203    }
204
205    /// Receive a message from the source URI before the deadline
206    /// passes. Implementations must respect the deadline; they never
207    /// hang past it.
208    fn receive<'a>(
209        &'a self,
210        lane_key: &'a str,
211        source_uri: &'a str,
212        deadline: Duration,
213    ) -> BoxFuture<'a, Result<IncomingMessage, ReceiveError>>;
214
215    /// The host:port authority this adapter's listener bound, when it
216    /// owns one (the http partner); `None` otherwise. The router uses
217    /// it to resolve declared and dynamic endpoint references to real
218    /// wire addresses.
219    fn bound_authority(&self) -> Option<String> {
220        None
221    }
222
223    /// The wire requests this adapter recorded, in arrival order —
224    /// the recorded-traffic snapshot a `partner` validate asserts
225    /// against (feature `http`). The default is empty: adapters
226    /// without a listener record nothing.
227    #[cfg(feature = "http")]
228    fn recorded_requests(&self) -> Vec<http::HttpWireRequest> {
229        Vec::new()
230    }
231
232    /// Stores the query keys classified secret (ADR-0051 positive
233    /// secret rule) for the adapter's diagnostic redaction; the
234    /// router fans the set out from the booted context's component
235    /// metadata. Default noop: adapters without wire-path
236    /// diagnostics render nothing to redact.
237    fn set_secret_query_keys(&self, keys: &[String]) {
238        let _ = keys;
239    }
240}
241
242/// Dispatches adapter calls by declared endpoint key to the
243/// endpoint-keyed adapter map it wraps, and owns the shared http
244/// client lane (feature `http`).
245///
246/// Sends split into two keys — the declared endpoint string and the
247/// interpolated wire address — and http-scheme sends route through
248/// the router's own [`ClientLane`](http::ClientLane) (feature
249/// `http`): a declared `:0` harness key dials the partner's bound
250/// address, a dynamic reference resolves by interpolated authority,
251/// and a plain string dials its literal URI — no `Unbound` failure
252/// for http schemes. Non-http schemes dispatch to the registered
253/// adapter as before: an endpoint URI with no registered adapter
254/// fails the send at the transport ([`TransportError::Unbound`]) and
255/// the receive at the transport too
256/// ([`ReceiveError::Transport`]) — no partner exists that could ever
257/// deliver, the failure is apparatus class, and the call never hangs.
258///
259/// Receives are client-role-first: a roundtrip parked by the router's
260/// own client lane wins over the partner adapter's server-role
261/// arrivals.
262pub struct PartnerRouter {
263    /// Declared endpoint key to adapter.
264    adapters: BTreeMap<String, Box<dyn PartnerAdapter>>,
265    /// The shared http client lane (feature `http`): one parked
266    /// roundtrip per lane key, filled by every http-scheme send.
267    /// `Arc`-shared because a launch's spawned exchange keeps the
268    /// handle alive to park its own failure.
269    #[cfg(feature = "http")]
270    client_lane: Arc<http::ClientLane>,
271    /// The secret-marked query-key set (ADR-0051 positive secret
272    /// rule), fanned out from the booted context's component
273    /// metadata by [`set_secret_query_keys`](Self::set_secret_query_keys):
274    /// the redaction source of every wire-path diagnostic.
275    secret_query_keys: Arc<RwLock<Vec<String>>>,
276}
277
278impl PartnerRouter {
279    /// Builds a router over the given endpoint-keyed adapters.
280    pub fn new(adapters: BTreeMap<String, Box<dyn PartnerAdapter>>) -> Self {
281        Self {
282            adapters,
283            #[cfg(feature = "http")]
284            client_lane: Arc::new(http::ClientLane::new()),
285            secret_query_keys: Arc::new(RwLock::new(Vec::new())),
286        }
287    }
288
289    /// Stores the secret-marked query-key set (ADR-0051) and fans it
290    /// out to every redaction site: kept here for partner-validate
291    /// mismatch rendering, set on the router-owned http client lane
292    /// and on every registered adapter for their receive-timeout
293    /// diagnostics. The set crosses the CLI→harness boundary as plain
294    /// strings, derived from the booted context's component metadata.
295    pub fn set_secret_query_keys(&self, keys: Vec<String>) {
296        *self
297            .secret_query_keys
298            .write()
299            .unwrap_or_else(|poisoned| poisoned.into_inner()) = keys.clone();
300        #[cfg(feature = "http")]
301        self.client_lane.set_secret_query_keys(&keys);
302        for adapter in self.adapters.values() {
303            adapter.set_secret_query_keys(&keys);
304        }
305    }
306
307    /// The stored secret-marked query-key set: the redaction source
308    /// of every diagnostic that quotes a declared endpoint or wire
309    /// path (partner-validate mismatch rendering, `Unbound`
310    /// backstops, lastReceived validation subjects).
311    pub(crate) fn secret_query_keys(&self) -> Vec<String> {
312        self.secret_query_keys
313            .read()
314            .unwrap_or_else(|poisoned| poisoned.into_inner())
315            .clone()
316    }
317
318    /// The adapter registered under `key`, if any.
319    pub fn adapter(&self, key: &str) -> Option<&dyn PartnerAdapter> {
320        self.adapters.get(key).map(|boxed| boxed.as_ref())
321    }
322
323    /// The wire requests the partner registered under `key` recorded,
324    /// in arrival order (feature `http`); empty when no adapter is
325    /// registered under `key` or the registered adapter records
326    /// nothing — an unregistered key reads as an empty snapshot, so
327    /// a partner validate on it fails the count, never the run.
328    #[cfg(feature = "http")]
329    pub fn recorded_requests(&self, key: &str) -> Vec<http::HttpWireRequest> {
330        self.adapters
331            .get(key)
332            .map(|adapter| adapter.recorded_requests())
333            .unwrap_or_default()
334    }
335
336    /// Every registered partner that owns a bound authority, as
337    /// `(declared key, bound authority)` pairs.
338    pub fn authorities(&self) -> Vec<(String, String)> {
339        self.adapters
340            .iter()
341            .filter_map(|(key, adapter)| Some((key.clone(), adapter.bound_authority()?)))
342            .collect()
343    }
344
345    /// The lane key a receive under `(declared, interpolated)` reads:
346    /// the declared string itself when it names a registered partner
347    /// key (lane reads by declared key, today's behavior); otherwise
348    /// the registered key of the partner whose bound authority equals
349    /// the interpolated URI's authority. `None` when neither resolves
350    /// (plain strings): the caller falls back to the declared string.
351    pub fn lane_key_for(&self, declared: &str, interpolated: &str) -> Option<String> {
352        if self.adapters.contains_key(declared) {
353            return Some(declared.to_string());
354        }
355        let authority = uri_authority(interpolated)?;
356        self.authorities()
357            .into_iter()
358            .find(|(_, bound)| bound == authority)
359            .map(|(key, _)| key)
360    }
361
362    /// The wire target a send under `(declared_key, interpolated_uri)`
363    /// dials, when it differs from plain literal dialing.
364    ///
365    /// - `declared_key` names a registered partner AND carries the
366    ///   unroutable port-0 authority (the harness-declared form,
367    ///   ADR-0069 §8): rewrite only the authority of
368    ///   `interpolated_uri` to that partner's bound authority,
369    ///   preserving the interpolated path and query.
370    /// - `declared_key` names no partner but the interpolated URI's
371    ///   authority equals a bound partner's authority: return that
372    ///   partner's authority rewrite (path preserved) — the resolved
373    ///   URI for a dynamic reference.
374    /// - Anything else — a routable declared key (a partner registered
375    ///   under its own bound address, or under a foreign endpoint as
376    ///   a client-role vehicle) or an address no partner owns — is
377    ///   `None`: the caller dials the interpolated URI literally.
378    pub fn wire_target(&self, declared_key: &str, interpolated_uri: &str) -> Option<String> {
379        if let Some(adapter) = self.adapters.get(declared_key) {
380            let bound = adapter.bound_authority()?;
381            if !authority_is_port_zero(declared_key) {
382                return None;
383            }
384            return rewrite_authority(interpolated_uri, &bound);
385        }
386        self.partner_by_authority(interpolated_uri)
387            .and_then(|(_, bound)| rewrite_authority(interpolated_uri, &bound))
388    }
389
390    /// Sends `msg` under the two-key contract: dispatch by declared
391    /// endpoint key, dial by resolved address (see the type docs for
392    /// the http cases).
393    pub async fn send(
394        &self,
395        declared: &str,
396        interpolated: &str,
397        msg: OutgoingMessage,
398    ) -> Result<(), TransportError> {
399        #[cfg(feature = "http")]
400        if interpolated.starts_with("http://") {
401            return self.send_http(declared, interpolated, msg).await;
402        }
403        match self.adapters.get(declared) {
404            Some(adapter) => adapter.send(declared, interpolated, msg).await,
405            // Backstop (the CLI pre-validates wiring), still redacted:
406            // the router holds the secret set.
407            None => Err(TransportError::Unbound {
408                endpoint: redact_wire_path(declared, &self.secret_query_keys()),
409            }),
410        }
411    }
412
413    /// The http-scheme send dispatch (feature `http`): every case goes
414    /// through the router's own client lane.
415    #[cfg(feature = "http")]
416    async fn send_http(
417        &self,
418        declared: &str,
419        interpolated: &str,
420        msg: OutgoingMessage,
421    ) -> Result<(), TransportError> {
422        // (a) The declared key registers an http partner: the
423        // harness-declared endpoint — dial its bound address when the
424        // `:0` form resolves one, the literal URI otherwise. A
425        // non-http adapter registered under an http-scheme key keeps
426        // today's equality dispatch.
427        if let Some(adapter) = self.adapters.get(declared) {
428            if adapter.bound_authority().is_some() {
429                let target = self
430                    .wire_target(declared, interpolated)
431                    .unwrap_or_else(|| interpolated.to_string());
432                return Arc::clone(&self.client_lane)
433                    .launch(declared, &target, msg)
434                    .await;
435            }
436            return adapter.send(declared, interpolated, msg).await;
437        }
438        // (b) The declared key is not registered, but the interpolated
439        // authority resolves to a partner: dial the resolved URI under
440        // that partner's REGISTERED key, so `lane_key_for` finds the
441        // roundtrip on receive.
442        if let Some((lane_key, target)) = self
443            .partner_by_authority(interpolated)
444            .and_then(|(key, bound)| Some((key, rewrite_authority(interpolated, &bound)?)))
445        {
446            return Arc::clone(&self.client_lane)
447                .launch(&lane_key, &target, msg)
448                .await;
449        }
450        // (c) Neither: a plain-string reference dials its literal URI
451        // with no partner involved.
452        Arc::clone(&self.client_lane)
453            .launch(declared, interpolated, msg)
454            .await
455    }
456
457    /// Receives under the two-key contract, client-role-first: derive
458    /// the lane key ([`Self::lane_key_for`], falling back to the
459    /// declared string for plain strings), return a roundtrip parked
460    /// by the router's own client lane when one exists, and otherwise
461    /// delegate the server-role receive to the adapter registered
462    /// under that key. [`TransportError::Unbound`] only when neither a
463    /// parked roundtrip nor a registered adapter exists.
464    pub async fn receive(
465        &self,
466        declared: &str,
467        interpolated: &str,
468        deadline: Duration,
469    ) -> Result<IncomingMessage, ReceiveError> {
470        let lane_key = self
471            .lane_key_for(declared, interpolated)
472            .unwrap_or_else(|| declared.to_string());
473        #[cfg(feature = "http")]
474        if let Some(parked) = self.client_lane.take(&lane_key) {
475            return self
476                .client_lane
477                .await_parked(interpolated, deadline, parked)
478                .await;
479        }
480        match self.adapters.get(lane_key.as_str()) {
481            Some(adapter) => adapter.receive(&lane_key, interpolated, deadline).await,
482            // Backstop (the CLI pre-validates wiring), still redacted:
483            // the router holds the secret set.
484            None => Err(ReceiveError::Transport(TransportError::Unbound {
485                endpoint: redact_wire_path(declared, &self.secret_query_keys()),
486            })),
487        }
488    }
489
490    /// The registered partner whose bound authority equals the URI's
491    /// authority, as `(registered key, bound authority)`; the
492    /// post-interpolation resolution of a dynamic reference.
493    fn partner_by_authority(&self, uri: &str) -> Option<(String, String)> {
494        let authority = uri_authority(uri)?;
495        self.authorities()
496            .into_iter()
497            .find(|(_, bound)| bound == authority)
498    }
499}
500
501/// The authority span of an absolute URI (`scheme://authority/rest`);
502/// `None` when the string carries no `://` separator. Userinfo is not
503/// part of this grammar.
504fn uri_authority(uri: &str) -> Option<&str> {
505    let start = uri.find("://")? + 3;
506    let rest = &uri[start..];
507    let end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
508    Some(&rest[..end])
509}
510
511/// Whether the URI's authority is the unroutable port-0 placeholder —
512/// the harness-declared endpoint form (`http://127.0.0.1:0/...`,
513/// ADR-0069 §8). A declared key with any other authority addresses a
514/// routable endpoint and dials literally.
515fn authority_is_port_zero(uri: &str) -> bool {
516    let Some(authority) = uri_authority(uri) else {
517        return false;
518    };
519    match authority.rsplit_once(':') {
520        Some((_, port)) => port == "0",
521        None => false,
522    }
523}
524
525/// Rewrites the URI's authority, preserving scheme, path, and query.
526fn rewrite_authority(uri: &str, authority: &str) -> Option<String> {
527    let rest_start = uri.find("://")? + 3;
528    let rest = &uri[rest_start..];
529    let path_start = rest.find(['/', '?', '#']).unwrap_or(rest.len());
530    let mut rewritten = String::with_capacity(uri.len());
531    rewritten.push_str(&uri[..rest_start]);
532    rewritten.push_str(authority);
533    rewritten.push_str(&rest[path_start..]);
534    Some(rewritten)
535}
536
537/// One recorded send: the endpoint the scenario addressed and the
538/// message it put on the wire.
539#[derive(Debug, Clone, PartialEq)]
540pub struct RecordedSend {
541    /// Endpoint URI the message was sent to.
542    pub endpoint: String,
543    /// The message as sent.
544    pub message: OutgoingMessage,
545}
546
547/// A handle onto a [`FakeAdapter`]'s recorded sends, valid after the
548/// adapter itself moved into the router.
549#[derive(Clone)]
550pub struct FakeRecorder {
551    sent: Arc<Mutex<Vec<RecordedSend>>>,
552}
553
554impl FakeRecorder {
555    /// A snapshot of everything the scenario sent through the fake.
556    pub fn sent_messages(&self) -> Vec<RecordedSend> {
557        lock_through(&self.sent).clone()
558    }
559}
560
561/// Inner state shared between a `FakeAdapter` and its clones.
562struct FakeInner {
563    /// When set, every send fails with this reason.
564    fail_send: Option<String>,
565    /// When set, every receive fails at the transport with this
566    /// reason.
567    fail_receive: Option<String>,
568    /// Recorded sends, in order, shared with `FakeRecorder` handles.
569    sent: Arc<Mutex<Vec<RecordedSend>>>,
570    /// The receiving half of the scripted queue; the sender half is
571    /// dropped after seeding, so a drained queue reports closed and
572    /// receive maps that to a timeout.
573    queue_rx: AsyncMutex<mpsc::Receiver<IncomingMessage>>,
574}
575
576/// In-memory [`PartnerAdapter`] for tests: records sent messages,
577/// plays a scripted incoming queue, and can fail sends on demand.
578///
579/// `Clone` shares state; keep a clone (or a [`FakeRecorder`]) to
580/// inspect sends after moving the adapter into a [`PartnerRouter`].
581#[derive(Clone)]
582pub struct FakeAdapter {
583    inner: Arc<FakeInner>,
584}
585
586impl FakeAdapter {
587    /// A fake that plays the given messages, in order, on receive;
588    /// once the queue drains, further receives time out.
589    pub fn scripted(queue: Vec<IncomingMessage>) -> Self {
590        let capacity = queue.len().max(1);
591        let (queue_tx, queue_rx) = mpsc::channel(capacity);
592        for message in queue {
593            // Capacity equals the queue length, so the try-send cannot
594            // hit a full buffer; a closed receiver is impossible here.
595            if queue_tx.try_send(message).is_err() {
596                break;
597            }
598        }
599        drop(queue_tx);
600        Self {
601            inner: Arc::new(FakeInner {
602                fail_send: None,
603                fail_receive: None,
604                sent: Arc::new(Mutex::new(Vec::new())),
605                queue_rx: AsyncMutex::new(queue_rx),
606            }),
607        }
608    }
609
610    /// A fake whose every send fails at the transport.
611    pub fn failing_send(reason: impl Into<String>) -> Self {
612        Self {
613            inner: Arc::new(FakeInner {
614                fail_send: Some(reason.into()),
615                fail_receive: None,
616                sent: Arc::new(Mutex::new(Vec::new())),
617                queue_rx: AsyncMutex::new(mpsc::channel(1).1),
618            }),
619        }
620    }
621
622    /// A fake whose every receive fails at the transport, so a
623    /// mid-scenario receive transport failure is expressible.
624    pub fn failing_receive(reason: impl Into<String>) -> Self {
625        Self {
626            inner: Arc::new(FakeInner {
627                fail_send: None,
628                fail_receive: Some(reason.into()),
629                sent: Arc::new(Mutex::new(Vec::new())),
630                queue_rx: AsyncMutex::new(mpsc::channel(1).1),
631            }),
632        }
633    }
634
635    /// A handle onto this fake's recorded sends.
636    pub fn recorder(&self) -> FakeRecorder {
637        FakeRecorder {
638            sent: Arc::clone(&self.inner.sent),
639        }
640    }
641}
642
643impl PartnerAdapter for FakeAdapter {
644    fn send<'a>(
645        &'a self,
646        lane_key: &'a str,
647        _target_uri: &'a str,
648        msg: OutgoingMessage,
649    ) -> BoxFuture<'a, Result<(), TransportError>> {
650        Box::pin(async move {
651            if let Some(reason) = &self.inner.fail_send {
652                return Err(TransportError::Other {
653                    message: reason.clone(),
654                });
655            }
656            lock_through(&self.inner.sent).push(RecordedSend {
657                endpoint: lane_key.to_string(),
658                message: msg,
659            });
660            Ok(())
661        })
662    }
663
664    fn receive<'a>(
665        &'a self,
666        _lane_key: &'a str,
667        source_uri: &'a str,
668        deadline: Duration,
669    ) -> BoxFuture<'a, Result<IncomingMessage, ReceiveError>> {
670        Box::pin(async move {
671            if let Some(reason) = &self.inner.fail_receive {
672                return Err(ReceiveError::Transport(TransportError::Other {
673                    message: reason.clone(),
674                }));
675            }
676            let mut queue_rx = self.inner.queue_rx.lock().await;
677            let started = tokio::time::Instant::now();
678            let outcome = tokio::time::timeout(deadline, queue_rx.recv()).await;
679            match outcome {
680                Ok(Some(message)) => Ok(message),
681                Ok(None) | Err(_) => Err(ReceiveError::Timeout(ReceiveTimeout {
682                    endpoint: source_uri.to_string(),
683                    deadline,
684                    elapsed: started.elapsed(),
685                    lanes_recorded: Vec::new(),
686                })),
687            }
688        })
689    }
690}
691
692/// Locks, recovering the guard through poisoning: fake state is plain
693/// data, a poisoned lock carries no invariant to protect.
694fn lock_through<T>(lock: &Mutex<T>) -> MutexGuard<'_, T> {
695    lock.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
696}
697
698/// Masks secret-marked query values in a recorded wire path for
699/// diagnostics (ADR-0051 positive secret rule): a pair whose DECODED
700/// key is in `secret_keys` keeps its raw key span but has its value
701/// masked as `***`; every other pair keeps its authored bytes,
702/// unknown keys included — apparatus-internal diagnostics stay
703/// maximally informative. A percent-encoded secret key
704/// (`%61uthPassword`) matches its decoded form. When
705/// [`raw_query_pairs`](camel_component_api::raw_query_pairs) rejects
706/// the query (a malformed key escape), the ENTIRE query portion is
707/// masked fail-safe: the redactor never panics and never prints an
708/// undecodable secret.
709pub(crate) fn redact_wire_path(path_and_query: &str, secret_keys: &[String]) -> String {
710    let Some(question_mark) = path_and_query.find('?') else {
711        return path_and_query.to_string();
712    };
713    let (path, query) = path_and_query.split_at(question_mark + 1);
714    if query.is_empty() {
715        return path_and_query.to_string();
716    }
717    let rendered = match camel_component_api::raw_query_pairs(query) {
718        Ok(pairs) => pairs
719            .into_iter()
720            .map(|(decoded_key, raw_pair)| {
721                if secret_keys.contains(&decoded_key) {
722                    match raw_pair.split_once('=') {
723                        // Mask the value; the raw key span stays as
724                        // authored (an encoded secret key stays
725                        // visibly encoded).
726                        Some((raw_key, _)) => format!("{raw_key}=***"),
727                        // A bare key carries no value to mask.
728                        None => raw_pair.to_string(),
729                    }
730                } else {
731                    raw_pair.to_string()
732                }
733            })
734            .collect::<Vec<_>>()
735            .join("&"),
736        Err(_) => "***".to_string(),
737    };
738    format!("{path}{rendered}")
739}
740
741// ---------------------------------------------------------------------------
742// Route stimulus through the booted context
743// ---------------------------------------------------------------------------
744
745/// Startup-race retry sleep for `direct:` producer delivery (the
746/// camel-test / camel-run stimulus mechanism).
747const STIMULUS_RETRY_SLEEP: Duration = Duration::from_millis(20);
748/// Startup-race retry deadline for `direct:` producer delivery.
749const STIMULUS_RETRY_DEADLINE: Duration = Duration::from_secs(1);
750
751/// The route stimulus for a booted scenario (ADR-0069 section 5): a
752/// scenario `send` addressed to a CONTEXT component endpoint
753/// (`direct:`) must reach the booted system under test, not a
754/// partner. This adapter delivers the message through the context's
755/// own producer path — a fresh `direct:` endpoint and producer per
756/// send, one `oneshot` per exchange, retrying the consumer-startup
757/// race — the same mechanism `camel-test` and `camel run` use to
758/// stimulate routes.
759///
760/// Key the router map by the exact endpoint URI the scenario's `send`
761/// addresses (`direct:start`). `receive` is not a context role: it
762/// fails at the transport, apparatus class.
763pub struct DirectStimulus {
764    /// The booted context, shared with the boot-owning caller (the
765    /// caller wraps `ScenarioRun::ctx` after
766    /// [`boot_scenario`](crate::boot_scenario) returns).
767    ctx: Arc<AsyncMutex<CamelContext>>,
768}
769
770impl DirectStimulus {
771    /// Wraps the booted context the scenario sends into.
772    pub fn new(ctx: Arc<AsyncMutex<CamelContext>>) -> Self {
773        Self { ctx }
774    }
775}
776
777impl PartnerAdapter for DirectStimulus {
778    fn send<'a>(
779        &'a self,
780        lane_key: &'a str,
781        _target_uri: &'a str,
782        msg: OutgoingMessage,
783    ) -> BoxFuture<'a, Result<(), TransportError>> {
784        Box::pin(async move {
785            let exchange = stimulus_exchange(msg);
786            let transport = |detail: String| TransportError::Other { message: detail };
787            let deadline = tokio::time::Instant::now() + STIMULUS_RETRY_DEADLINE;
788            loop {
789                let producer = {
790                    let ctx = self.ctx.lock().await;
791                    let producer_ctx = ctx.producer_context();
792                    let component = ctx
793                        .registry()
794                        .get("direct")
795                        .ok_or_else(|| transport("direct component not registered".to_string()))?;
796                    let endpoint = component.create_endpoint(lane_key, &*ctx).map_err(|e| {
797                        transport(format!("failed to create endpoint {lane_key}: {e}"))
798                    })?;
799                    endpoint
800                        .create_producer(Arc::new(NoOpComponentContext), &producer_ctx)
801                        .map_err(|e| {
802                            transport(format!("failed to create producer for {lane_key}: {e}"))
803                        })?
804                };
805                match producer.oneshot(exchange.clone()).await {
806                    // The stimulus exchange completed the route; the
807                    // reply carries no scenario meaning in v1.
808                    Ok(_reply) => return Ok(()),
809                    Err(e) => {
810                        // The direct producer types the startup race as
811                        // EndpointCreationFailed at both error sites:
812                        // poll_ready ("direct endpoint '{}' not registered",
813                        // camel-direct/src/lib.rs) and call ("no consumer
814                        // registered for direct:{name}"). No string matching.
815                        let is_startup_race = matches!(e, CamelError::EndpointCreationFailed(_));
816                        if is_startup_race && tokio::time::Instant::now() < deadline {
817                            tokio::time::sleep(STIMULUS_RETRY_SLEEP).await;
818                            continue;
819                        }
820                        return Err(transport(format!("send to {lane_key} failed: {e}")));
821                    }
822                }
823            }
824        })
825    }
826
827    fn receive<'a>(
828        &'a self,
829        _lane_key: &'a str,
830        source_uri: &'a str,
831        _deadline: Duration,
832    ) -> BoxFuture<'a, Result<IncomingMessage, ReceiveError>> {
833        Box::pin(async move {
834            Err(ReceiveError::Transport(TransportError::Other {
835                message: format!(
836                    "{source_uri} is a context stimulus endpoint; receive is a partner role"
837                ),
838            }))
839        })
840    }
841}
842
843/// Builds the stimulus exchange: strings pass through as text bodies,
844/// `Null` is empty, structured values travel as JSON; headers carry
845/// over verbatim.
846fn stimulus_exchange(msg: OutgoingMessage) -> Exchange {
847    let body = match &msg.body {
848        Value::Null => Body::Empty,
849        Value::String(text) => Body::Text(text.clone()),
850        other => Body::Json(other.clone()),
851    };
852    let mut message = Message::new(body);
853    for (name, value) in &msg.headers {
854        message.set_header(name.clone(), value.clone());
855    }
856    Exchange::new(message)
857}