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`](http::ClientLane) (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, and otherwise
519    /// delegate the server-role receive to the adapter registered
520    /// under that key. [`TransportError::Unbound`] only when neither a
521    /// parked roundtrip nor a registered adapter exists.
522    pub async fn receive(
523        &self,
524        declared: &str,
525        interpolated: &str,
526        deadline: Duration,
527    ) -> Result<IncomingMessage, ReceiveError> {
528        let lane_key = self
529            .lane_key_for(declared, interpolated)
530            .unwrap_or_else(|| declared.to_string());
531        #[cfg(feature = "http")]
532        if let Some(parked) = self.client_lane.take(&lane_key) {
533            return self
534                .client_lane
535                .await_parked(interpolated, deadline, parked)
536                .await;
537        }
538        match self.adapters.get(lane_key.as_str()) {
539            Some(adapter) => adapter.receive(&lane_key, interpolated, deadline).await,
540            // Backstop (the CLI pre-validates wiring), still redacted:
541            // the router holds the secret set.
542            None => Err(ReceiveError::Transport(TransportError::Unbound {
543                endpoint: redact_wire_path(declared, &self.secret_query_keys()),
544            })),
545        }
546    }
547
548    /// The registered partner whose bound authority equals the URI's
549    /// authority, as `(registered key, bound authority)`; the
550    /// post-interpolation resolution of a dynamic reference.
551    fn partner_by_authority(&self, uri: &str) -> Option<(String, String)> {
552        let authority = uri_authority(uri)?;
553        self.authorities()
554            .into_iter()
555            .find(|(_, bound)| bound == authority)
556    }
557}
558
559/// The authority span of an absolute URI (`scheme://authority/rest`);
560/// `None` when the string carries no `://` separator. Userinfo is not
561/// part of this grammar.
562fn uri_authority(uri: &str) -> Option<&str> {
563    let start = uri.find("://")? + 3;
564    let rest = &uri[start..];
565    let end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
566    Some(&rest[..end])
567}
568
569/// Whether the URI's authority is the unroutable port-0 placeholder —
570/// the harness-declared endpoint form (`http://127.0.0.1:0/...`,
571/// ADR-0069 §8). A declared key with any other authority addresses a
572/// routable endpoint and dials literally.
573fn authority_is_port_zero(uri: &str) -> bool {
574    let Some(authority) = uri_authority(uri) else {
575        return false;
576    };
577    match authority.rsplit_once(':') {
578        Some((_, port)) => port == "0",
579        None => false,
580    }
581}
582
583/// Rewrites the URI's authority, preserving scheme, path, and query.
584fn rewrite_authority(uri: &str, authority: &str) -> Option<String> {
585    let rest_start = uri.find("://")? + 3;
586    let rest = &uri[rest_start..];
587    let path_start = rest.find(['/', '?', '#']).unwrap_or(rest.len());
588    let mut rewritten = String::with_capacity(uri.len());
589    rewritten.push_str(&uri[..rest_start]);
590    rewritten.push_str(authority);
591    rewritten.push_str(&rest[path_start..]);
592    Some(rewritten)
593}
594
595/// One recorded send: the endpoint the scenario addressed and the
596/// message it put on the wire.
597#[derive(Debug, Clone, PartialEq)]
598pub struct RecordedSend {
599    /// Endpoint URI the message was sent to.
600    pub endpoint: String,
601    /// The message as sent.
602    pub message: OutgoingMessage,
603}
604
605/// A handle onto a [`FakeAdapter`]'s recorded sends, valid after the
606/// adapter itself moved into the router.
607#[derive(Clone)]
608pub struct FakeRecorder {
609    sent: Arc<Mutex<Vec<RecordedSend>>>,
610}
611
612impl FakeRecorder {
613    /// A snapshot of everything the scenario sent through the fake.
614    pub fn sent_messages(&self) -> Vec<RecordedSend> {
615        lock_through(&self.sent).clone()
616    }
617}
618
619/// Inner state shared between a `FakeAdapter` and its clones.
620struct FakeInner {
621    /// When set, every send fails with this reason.
622    fail_send: Option<String>,
623    /// When set, every receive fails at the transport with this
624    /// reason.
625    fail_receive: Option<String>,
626    /// Recorded sends, in order, shared with `FakeRecorder` handles.
627    sent: Arc<Mutex<Vec<RecordedSend>>>,
628    /// The receiving half of the scripted queue; the sender half is
629    /// dropped after seeding, so a drained queue reports closed and
630    /// receive maps that to a timeout.
631    queue_rx: AsyncMutex<mpsc::Receiver<IncomingMessage>>,
632}
633
634/// In-memory [`PartnerAdapter`] for tests: records sent messages,
635/// plays a scripted incoming queue, and can fail sends on demand.
636///
637/// `Clone` shares state; keep a clone (or a [`FakeRecorder`]) to
638/// inspect sends after moving the adapter into a [`PartnerRouter`].
639#[derive(Clone)]
640pub struct FakeAdapter {
641    inner: Arc<FakeInner>,
642}
643
644impl FakeAdapter {
645    /// A fake that plays the given messages, in order, on receive;
646    /// once the queue drains, further receives time out.
647    pub fn scripted(queue: Vec<IncomingMessage>) -> Self {
648        let capacity = queue.len().max(1);
649        let (queue_tx, queue_rx) = mpsc::channel(capacity);
650        for message in queue {
651            // Capacity equals the queue length, so the try-send cannot
652            // hit a full buffer; a closed receiver is impossible here.
653            if queue_tx.try_send(message).is_err() {
654                break;
655            }
656        }
657        drop(queue_tx);
658        Self {
659            inner: Arc::new(FakeInner {
660                fail_send: None,
661                fail_receive: None,
662                sent: Arc::new(Mutex::new(Vec::new())),
663                queue_rx: AsyncMutex::new(queue_rx),
664            }),
665        }
666    }
667
668    /// A fake whose every send fails at the transport.
669    pub fn failing_send(reason: impl Into<String>) -> Self {
670        Self {
671            inner: Arc::new(FakeInner {
672                fail_send: Some(reason.into()),
673                fail_receive: None,
674                sent: Arc::new(Mutex::new(Vec::new())),
675                queue_rx: AsyncMutex::new(mpsc::channel(1).1),
676            }),
677        }
678    }
679
680    /// A fake whose every receive fails at the transport, so a
681    /// mid-scenario receive transport failure is expressible.
682    pub fn failing_receive(reason: impl Into<String>) -> Self {
683        Self {
684            inner: Arc::new(FakeInner {
685                fail_send: None,
686                fail_receive: Some(reason.into()),
687                sent: Arc::new(Mutex::new(Vec::new())),
688                queue_rx: AsyncMutex::new(mpsc::channel(1).1),
689            }),
690        }
691    }
692
693    /// A handle onto this fake's recorded sends.
694    pub fn recorder(&self) -> FakeRecorder {
695        FakeRecorder {
696            sent: Arc::clone(&self.inner.sent),
697        }
698    }
699}
700
701impl PartnerAdapter for FakeAdapter {
702    fn send<'a>(
703        &'a self,
704        lane_key: &'a str,
705        _target_uri: &'a str,
706        msg: OutgoingMessage,
707    ) -> BoxFuture<'a, Result<Option<Exchange>, TransportError>> {
708        Box::pin(async move {
709            if let Some(reason) = &self.inner.fail_send {
710                return Err(TransportError::Other {
711                    message: reason.clone(),
712                });
713            }
714            lock_through(&self.inner.sent).push(RecordedSend {
715                endpoint: lane_key.to_string(),
716                message: msg,
717            });
718            // The fake records sends; it produces no synchronous
719            // reply for an `expectReply` assertion to read.
720            Ok(None)
721        })
722    }
723
724    fn receive<'a>(
725        &'a self,
726        _lane_key: &'a str,
727        source_uri: &'a str,
728        deadline: Duration,
729    ) -> BoxFuture<'a, Result<IncomingMessage, ReceiveError>> {
730        Box::pin(async move {
731            if let Some(reason) = &self.inner.fail_receive {
732                return Err(ReceiveError::Transport(TransportError::Other {
733                    message: reason.clone(),
734                }));
735            }
736            let mut queue_rx = self.inner.queue_rx.lock().await;
737            let started = tokio::time::Instant::now();
738            let outcome = tokio::time::timeout(deadline, queue_rx.recv()).await;
739            match outcome {
740                Ok(Some(message)) => Ok(message),
741                Ok(None) | Err(_) => Err(ReceiveError::Timeout(ReceiveTimeout {
742                    endpoint: source_uri.to_string(),
743                    deadline,
744                    elapsed: started.elapsed(),
745                    lanes_recorded: Vec::new(),
746                })),
747            }
748        })
749    }
750}
751
752/// Locks, recovering the guard through poisoning: fake state is plain
753/// data, a poisoned lock carries no invariant to protect.
754fn lock_through<T>(lock: &Mutex<T>) -> MutexGuard<'_, T> {
755    lock.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
756}
757
758/// Masks secret-marked query values in a recorded wire path for
759/// diagnostics (ADR-0051 positive secret rule): a pair whose DECODED
760/// key is in `secret_keys` keeps its raw key span but has its value
761/// masked as `***`; every other pair keeps its authored bytes,
762/// unknown keys included — apparatus-internal diagnostics stay
763/// maximally informative. A percent-encoded secret key
764/// (`%61uthPassword`) matches its decoded form. When
765/// [`raw_query_pairs`](camel_component_api::raw_query_pairs) rejects
766/// the query (a malformed key escape), the ENTIRE query portion is
767/// masked fail-safe: the redactor never panics and never prints an
768/// undecodable secret.
769pub(crate) fn redact_wire_path(path_and_query: &str, secret_keys: &[String]) -> String {
770    let Some(question_mark) = path_and_query.find('?') else {
771        return path_and_query.to_string();
772    };
773    let (path, query) = path_and_query.split_at(question_mark + 1);
774    if query.is_empty() {
775        return path_and_query.to_string();
776    }
777    let rendered = match camel_component_api::raw_query_pairs(query) {
778        Ok(pairs) => pairs
779            .into_iter()
780            .map(|(decoded_key, raw_pair)| {
781                if secret_keys.contains(&decoded_key) {
782                    match raw_pair.split_once('=') {
783                        // Mask the value; the raw key span stays as
784                        // authored (an encoded secret key stays
785                        // visibly encoded).
786                        Some((raw_key, _)) => format!("{raw_key}=***"),
787                        // A bare key carries no value to mask.
788                        None => raw_pair.to_string(),
789                    }
790                } else {
791                    raw_pair.to_string()
792                }
793            })
794            .collect::<Vec<_>>()
795            .join("&"),
796        Err(_) => "***".to_string(),
797    };
798    format!("{path}{rendered}")
799}
800
801// ---------------------------------------------------------------------------
802// Route stimulus through the booted context
803// ---------------------------------------------------------------------------
804
805/// Startup-race retry sleep for `direct:` producer delivery (the
806/// camel-test / camel-run stimulus mechanism).
807const STIMULUS_RETRY_SLEEP: Duration = Duration::from_millis(20);
808/// Startup-race retry deadline for `direct:` producer delivery.
809const STIMULUS_RETRY_DEADLINE: Duration = Duration::from_secs(1);
810
811/// The route stimulus for a booted scenario (ADR-0069 section 5): a
812/// scenario `send` addressed to a CONTEXT component endpoint
813/// (`direct:`) must reach the booted system under test, not a
814/// partner. This adapter delivers the message through the context's
815/// own producer path — a fresh `direct:` endpoint and producer per
816/// send, one `oneshot` per exchange, retrying the consumer-startup
817/// race — the same mechanism `camel-test` and `camel run` use to
818/// stimulate routes.
819///
820/// Key the router map by the exact endpoint URI the scenario's `send`
821/// addresses (`direct:start`). `receive` is not a context role: it
822/// fails at the transport, apparatus class.
823pub struct DirectStimulus {
824    /// The booted context, shared with the boot-owning caller (the
825    /// caller wraps `ScenarioRun::ctx` after
826    /// [`boot_scenario`](crate::boot_scenario) returns).
827    ctx: Arc<AsyncMutex<CamelContext>>,
828}
829
830impl DirectStimulus {
831    /// Wraps the booted context the scenario sends into.
832    pub fn new(ctx: Arc<AsyncMutex<CamelContext>>) -> Self {
833        Self { ctx }
834    }
835}
836
837impl PartnerAdapter for DirectStimulus {
838    fn send<'a>(
839        &'a self,
840        lane_key: &'a str,
841        _target_uri: &'a str,
842        msg: OutgoingMessage,
843    ) -> BoxFuture<'a, Result<Option<Exchange>, TransportError>> {
844        Box::pin(async move {
845            let exchange = stimulus_exchange(msg);
846            let transport = |detail: String| TransportError::Other { message: detail };
847            let deadline = tokio::time::Instant::now() + STIMULUS_RETRY_DEADLINE;
848            loop {
849                let producer = {
850                    let ctx = self.ctx.lock().await;
851                    let producer_ctx = ctx.producer_context();
852                    let component = ctx
853                        .registry()
854                        .get("direct")
855                        .ok_or_else(|| transport("direct component not registered".to_string()))?;
856                    let endpoint = component.create_endpoint(lane_key, &*ctx).map_err(|e| {
857                        transport(format!("failed to create endpoint {lane_key}: {e}"))
858                    })?;
859                    endpoint
860                        .create_producer(Arc::new(NoOpComponentContext), &producer_ctx)
861                        .map_err(|e| {
862                            transport(format!("failed to create producer for {lane_key}: {e}"))
863                        })?
864                };
865                match producer.oneshot(exchange.clone()).await {
866                    // The stimulus exchange completed the route; the
867                    // routed exchange is the synchronous reply an
868                    // `expectReply` assertion reads (rc-qvz6).
869                    Ok(reply) => return Ok(Some(reply)),
870                    Err(e) => {
871                        // The direct producer types the startup race as
872                        // EndpointCreationFailed at both error sites:
873                        // poll_ready ("direct endpoint '{}' not registered",
874                        // camel-direct/src/lib.rs) and call ("no consumer
875                        // registered for direct:{name}"). No string matching.
876                        let is_startup_race = matches!(e, CamelError::EndpointCreationFailed(_));
877                        if is_startup_race && tokio::time::Instant::now() < deadline {
878                            tokio::time::sleep(STIMULUS_RETRY_SLEEP).await;
879                            continue;
880                        }
881                        return Err(transport(format!("send to {lane_key} failed: {e}")));
882                    }
883                }
884            }
885        })
886    }
887
888    fn receive<'a>(
889        &'a self,
890        _lane_key: &'a str,
891        source_uri: &'a str,
892        _deadline: Duration,
893    ) -> BoxFuture<'a, Result<IncomingMessage, ReceiveError>> {
894        Box::pin(async move {
895            Err(ReceiveError::Transport(TransportError::Other {
896                message: format!(
897                    "{source_uri} is a context stimulus endpoint; receive is a partner role"
898                ),
899            }))
900        })
901    }
902}
903
904/// Builds the stimulus exchange: strings pass through as text bodies,
905/// `Null` is empty, structured values travel as JSON; headers carry
906/// over verbatim.
907fn stimulus_exchange(msg: OutgoingMessage) -> Exchange {
908    let body = match &msg.body {
909        Value::Null => Body::Empty,
910        Value::String(text) => Body::Text(text.clone()),
911        other => Body::Json(other.clone()),
912    };
913    let mut message = Message::new(body);
914    for (name, value) in &msg.headers {
915        message.set_header(name.clone(), value.clone());
916    }
917    Exchange::new(message)
918}