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}