camel_component_mock/assert.rs
1//! Non-panicking assertion surface for mock endpoints.
2//!
3//! [`MockAssertionError`] maps every assertion branch of
4//! [`crate::MockEndpointInner::assert_satisfied`] to one variant whose
5//! `Display` output is byte-identical to the panic message the panicking
6//! variant produces for the same condition.
7
8use crate::MockEndpointInner;
9
10/// Error returned by [`MockEndpointInner::try_assert_satisfied`] when a
11/// recorded expectation is not satisfied (or is malformed).
12///
13/// Every assertion branch of [`MockEndpointInner::assert_satisfied`]
14/// corresponds to exactly one variant. The `Display` output of each variant
15/// equals the panic message the panicking variant produces for the same
16/// condition. Body detail fields are pre-formatted (`{:?}`) strings.
17#[non_exhaustive]
18#[derive(Debug)]
19pub enum MockAssertionError {
20 /// Exact count expectation (`expect_count`) not met.
21 CountMismatch {
22 /// Endpoint name.
23 endpoint: String,
24 /// Expected number of exchanges.
25 expected: usize,
26 /// Actual number of retained exchanges.
27 actual: usize,
28 },
29 /// Minimum count expectation (`expect_minimum_count`) not met.
30 MinimumCountNotMet {
31 /// Endpoint name.
32 endpoint: String,
33 /// Minimum number of exchanges expected.
34 minimum: usize,
35 /// Actual number of retained exchanges.
36 actual: usize,
37 },
38 /// Number of expected bodies differs from the number of received bodies.
39 BodyCountMismatch {
40 /// Endpoint name.
41 endpoint: String,
42 /// Expected number of bodies.
43 expected: usize,
44 /// Actual number of bodies.
45 actual: usize,
46 },
47 /// Ordered body at `index` does not match the expected body.
48 BodyMismatch {
49 /// Endpoint name.
50 endpoint: String,
51 /// Index of the mismatching body.
52 index: usize,
53 /// `{:?}`-formatted expected body.
54 expected: String,
55 /// `{:?}`-formatted actual body.
56 actual: String,
57 },
58 /// Expected body not found in any received exchange (anyOrder mode).
59 BodyNotFound {
60 /// Endpoint name.
61 endpoint: String,
62 /// `{:?}`-formatted expected body.
63 expected: String,
64 },
65 /// Expected header key/value pair not found in any received exchange.
66 HeaderNotFound {
67 /// Endpoint name.
68 endpoint: String,
69 /// Header key.
70 key: String,
71 /// Expected header value.
72 value: serde_json::Value,
73 },
74 /// No received exchange has the named header matching the regex pattern.
75 HeaderRegexNotMatched {
76 /// Endpoint name.
77 endpoint: String,
78 /// Header key.
79 key: String,
80 /// Regex pattern.
81 pattern: String,
82 },
83 /// Header regex pattern failed to compile.
84 ///
85 /// A malformed expectation is a caller programming error, not an
86 /// expectation mismatch: it does not trip the fail-fast latch.
87 InvalidHeaderPattern {
88 /// Endpoint name.
89 endpoint: String,
90 /// Header key.
91 key: String,
92 /// Regex pattern that failed to compile.
93 pattern: String,
94 /// Underlying regex compile error.
95 source: Box<dyn std::error::Error + Send + Sync>,
96 },
97}
98
99impl std::fmt::Display for MockAssertionError {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 match self {
102 MockAssertionError::CountMismatch {
103 endpoint,
104 expected,
105 actual,
106 } => write!(
107 f,
108 "MockEndpoint '{endpoint}': expected {expected} exchanges, got {actual}"
109 ),
110 MockAssertionError::MinimumCountNotMet {
111 endpoint,
112 minimum,
113 actual,
114 } => write!(
115 f,
116 "MockEndpoint '{endpoint}': expected at least {minimum} exchanges, got {actual}"
117 ),
118 MockAssertionError::BodyCountMismatch {
119 endpoint,
120 expected,
121 actual,
122 } => write!(
123 f,
124 "MockEndpoint '{endpoint}': expected {expected} bodies, got {actual}"
125 ),
126 MockAssertionError::BodyMismatch {
127 endpoint,
128 index,
129 expected,
130 actual,
131 } => write!(
132 f,
133 "MockEndpoint '{endpoint}': body[{index}] expected {expected}, got {actual}"
134 ),
135 MockAssertionError::BodyNotFound { endpoint, expected } => write!(
136 f,
137 "MockEndpoint '{endpoint}': expected body {expected} not found in received exchanges (anyOrder mode)"
138 ),
139 MockAssertionError::HeaderNotFound {
140 endpoint,
141 key,
142 value,
143 } => write!(
144 f,
145 "MockEndpoint '{endpoint}': expected header '{key}' = {value} not found in any received exchange"
146 ),
147 MockAssertionError::HeaderRegexNotMatched {
148 endpoint,
149 key,
150 pattern,
151 } => write!(
152 f,
153 "MockEndpoint '{endpoint}': no received exchange has header '{key}' matching regex {pattern:?}"
154 ),
155 MockAssertionError::InvalidHeaderPattern {
156 endpoint,
157 pattern,
158 source,
159 ..
160 } => write!(
161 f,
162 "MockEndpoint '{endpoint}': invalid regex pattern {pattern:?}: {source}"
163 ),
164 }
165 }
166}
167
168impl std::error::Error for MockAssertionError {
169 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
170 match self {
171 MockAssertionError::InvalidHeaderPattern { source, .. } => Some(&**source),
172 _ => None,
173 }
174 }
175}
176
177impl MockEndpointInner {
178 /// Evaluate all recorded expectations against the received snapshot.
179 ///
180 /// Single evaluation path shared by
181 /// [`assert_satisfied`](crate::MockEndpointInner::assert_satisfied) and
182 /// [`try_assert_satisfied`](crate::MockEndpointInner::try_assert_satisfied):
183 /// exact count, minimum count, then — only when expected bodies are
184 /// registered — body-count and per-body checks, then header and
185 /// header-regex checks (independent of the body gate).
186 ///
187 /// On a mismatch-class error the fail-fast latch is tripped first (when
188 /// `fail_fast` is enabled), then the error is returned. A malformed
189 /// expectation ([`MockAssertionError::InvalidHeaderPattern`]) is returned
190 /// without touching the latch.
191 pub(crate) async fn evaluate_expectations(&self) -> Result<(), MockAssertionError> {
192 let received = self.get_received_exchanges().await;
193
194 let guard = self
195 .expectations
196 .lock()
197 .expect("expectations lock poisoned"); // allow-unwrap
198
199 // Exact count expectation — checked before bodies; a mismatch
200 // short-circuits all later checks.
201 if let Some(n) = guard.expected_count
202 && received.len() != n
203 {
204 return self.latch_err(MockAssertionError::CountMismatch {
205 endpoint: self.name.clone(),
206 expected: n,
207 actual: received.len(),
208 });
209 }
210
211 // Minimum count expectation.
212 if let Some(m) = guard.minimum_count
213 && received.len() < m
214 {
215 return self.latch_err(MockAssertionError::MinimumCountNotMet {
216 endpoint: self.name.clone(),
217 minimum: m,
218 actual: received.len(),
219 });
220 }
221
222 // Body expectations — gated: no expected bodies ⇒ body-count and
223 // per-body checks are skipped.
224 if !guard.expected_bodies.is_empty() {
225 let received_bodies: Vec<_> = received.iter().map(|e| &e.input.body).collect();
226 if guard.expected_bodies.len() != received_bodies.len() {
227 return self.latch_err(MockAssertionError::BodyCountMismatch {
228 endpoint: self.name.clone(),
229 expected: guard.expected_bodies.len(),
230 actual: received_bodies.len(),
231 });
232 }
233 if self.any_order {
234 // Match in any order — each expected body must appear exactly once.
235 let mut unmatched: Vec<_> = received_bodies.iter().collect();
236 for expected in &guard.expected_bodies {
237 let idx = unmatched
238 .iter()
239 .position(|actual| body_eq(expected, actual));
240 match idx {
241 Some(i) => {
242 unmatched.remove(i);
243 }
244 None => {
245 return self.latch_err(MockAssertionError::BodyNotFound {
246 endpoint: self.name.clone(),
247 expected: format!("{expected:?}"),
248 });
249 }
250 }
251 }
252 } else {
253 for (i, expected) in guard.expected_bodies.iter().enumerate() {
254 if !body_eq(expected, received_bodies[i]) {
255 return self.latch_err(MockAssertionError::BodyMismatch {
256 endpoint: self.name.clone(),
257 index: i,
258 expected: format!("{expected:?}"),
259 actual: format!("{:?}", received_bodies[i]),
260 });
261 }
262 }
263 }
264 }
265
266 // Expected headers (must all be present on at least one exchange).
267 for (key, value) in &guard.expected_headers {
268 let found = received
269 .iter()
270 .any(|ex| ex.input.headers.get(key).is_some_and(|v| v == value));
271 if !found {
272 return self.latch_err(MockAssertionError::HeaderNotFound {
273 endpoint: self.name.clone(),
274 key: key.clone(),
275 value: value.clone(),
276 });
277 }
278 }
279
280 // Expected header regexes.
281 for (key, pattern) in &guard.expected_header_regexes {
282 let re = match regex::Regex::new(pattern) {
283 Ok(re) => re,
284 // Malformed expectation: caller programming error, not a
285 // mismatch — the latch is not tripped.
286 Err(e) => {
287 return Err(MockAssertionError::InvalidHeaderPattern {
288 endpoint: self.name.clone(),
289 key: key.clone(),
290 pattern: pattern.clone(),
291 source: Box::new(e),
292 });
293 }
294 };
295 let found = received.iter().any(|ex| {
296 ex.input.headers.get(key).is_some_and(|v| {
297 let s = match v {
298 serde_json::Value::String(s) => s.clone(),
299 other => other.to_string(),
300 };
301 re.is_match(&s)
302 })
303 });
304 if !found {
305 return self.latch_err(MockAssertionError::HeaderRegexNotMatched {
306 endpoint: self.name.clone(),
307 key: key.clone(),
308 pattern: pattern.clone(),
309 });
310 }
311 }
312
313 Ok(())
314 }
315
316 /// Trip the fail-fast latch (when enabled) and wrap `err` for return.
317 ///
318 /// Single latch call site for every expectation-mismatch branch;
319 /// [`MockAssertionError::InvalidHeaderPattern`] deliberately bypasses it.
320 fn latch_err(&self, err: MockAssertionError) -> Result<(), MockAssertionError> {
321 self.set_fail_fast_on_mismatch();
322 Err(err)
323 }
324}
325
326/// Compare two `Body` values for equality (used by expectation evaluation).
327fn body_eq(a: &camel_component_api::Body, b: &camel_component_api::Body) -> bool {
328 match (a, b) {
329 (camel_component_api::Body::Empty, camel_component_api::Body::Empty) => true,
330 (camel_component_api::Body::Text(a), camel_component_api::Body::Text(b)) => a == b,
331 (camel_component_api::Body::Json(a), camel_component_api::Body::Json(b)) => a == b,
332 (camel_component_api::Body::Xml(a), camel_component_api::Body::Xml(b)) => a == b,
333 (camel_component_api::Body::Bytes(a), camel_component_api::Body::Bytes(b)) => a == b,
334 _ => false,
335 }
336}