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