Skip to main content

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}