use crate::MockEndpointInner;
#[non_exhaustive]
#[derive(Debug)]
pub enum MockAssertionError {
CountMismatch {
endpoint: String,
expected: usize,
actual: usize,
},
MinimumCountNotMet {
endpoint: String,
minimum: usize,
actual: usize,
},
BodyCountMismatch {
endpoint: String,
expected: usize,
actual: usize,
},
BodyMismatch {
endpoint: String,
index: usize,
expected: String,
actual: String,
},
BodyNotFound {
endpoint: String,
expected: String,
},
HeaderNotFound {
endpoint: String,
key: String,
value: serde_json::Value,
},
HeaderRegexNotMatched {
endpoint: String,
key: String,
pattern: String,
},
InvalidHeaderPattern {
endpoint: String,
key: String,
pattern: String,
source: Box<dyn std::error::Error + Send + Sync>,
},
}
impl std::fmt::Display for MockAssertionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MockAssertionError::CountMismatch {
endpoint,
expected,
actual,
} => write!(
f,
"MockEndpoint '{endpoint}': expected {expected} exchanges, got {actual}"
),
MockAssertionError::MinimumCountNotMet {
endpoint,
minimum,
actual,
} => write!(
f,
"MockEndpoint '{endpoint}': expected at least {minimum} exchanges, got {actual}"
),
MockAssertionError::BodyCountMismatch {
endpoint,
expected,
actual,
} => write!(
f,
"MockEndpoint '{endpoint}': expected {expected} bodies, got {actual}"
),
MockAssertionError::BodyMismatch {
endpoint,
index,
expected,
actual,
} => write!(
f,
"MockEndpoint '{endpoint}': body[{index}] expected {expected}, got {actual}"
),
MockAssertionError::BodyNotFound { endpoint, expected } => write!(
f,
"MockEndpoint '{endpoint}': expected body {expected} not found in received exchanges (anyOrder mode)"
),
MockAssertionError::HeaderNotFound {
endpoint,
key,
value,
} => write!(
f,
"MockEndpoint '{endpoint}': expected header '{key}' = {value} not found in any received exchange"
),
MockAssertionError::HeaderRegexNotMatched {
endpoint,
key,
pattern,
} => write!(
f,
"MockEndpoint '{endpoint}': no received exchange has header '{key}' matching regex {pattern:?}"
),
MockAssertionError::InvalidHeaderPattern {
endpoint,
pattern,
source,
..
} => write!(
f,
"MockEndpoint '{endpoint}': invalid regex pattern {pattern:?}: {source}"
),
}
}
}
impl std::error::Error for MockAssertionError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
MockAssertionError::InvalidHeaderPattern { source, .. } => Some(&**source),
_ => None,
}
}
}
impl MockEndpointInner {
pub(crate) async fn evaluate_expectations(&self) -> Result<(), MockAssertionError> {
let received = self.get_received_exchanges().await;
let guard = self
.expectations
.lock()
.expect("expectations lock poisoned");
if let Some(n) = guard.expected_count
&& received.len() != n
{
return self.latch_err(MockAssertionError::CountMismatch {
endpoint: self.name.clone(),
expected: n,
actual: received.len(),
});
}
if let Some(m) = guard.minimum_count
&& received.len() < m
{
return self.latch_err(MockAssertionError::MinimumCountNotMet {
endpoint: self.name.clone(),
minimum: m,
actual: received.len(),
});
}
if !guard.expected_bodies.is_empty() {
let received_bodies: Vec<_> = received.iter().map(|e| &e.input.body).collect();
if guard.expected_bodies.len() != received_bodies.len() {
return self.latch_err(MockAssertionError::BodyCountMismatch {
endpoint: self.name.clone(),
expected: guard.expected_bodies.len(),
actual: received_bodies.len(),
});
}
if self.any_order {
let mut unmatched: Vec<_> = received_bodies.iter().collect();
for expected in &guard.expected_bodies {
let idx = unmatched
.iter()
.position(|actual| body_eq(expected, actual));
match idx {
Some(i) => {
unmatched.remove(i);
}
None => {
return self.latch_err(MockAssertionError::BodyNotFound {
endpoint: self.name.clone(),
expected: format!("{expected:?}"),
});
}
}
}
} else {
for (i, expected) in guard.expected_bodies.iter().enumerate() {
if !body_eq(expected, received_bodies[i]) {
return self.latch_err(MockAssertionError::BodyMismatch {
endpoint: self.name.clone(),
index: i,
expected: format!("{expected:?}"),
actual: format!("{:?}", received_bodies[i]),
});
}
}
}
}
for (key, value) in &guard.expected_headers {
let found = received
.iter()
.any(|ex| ex.input.headers.get(key).is_some_and(|v| v == value));
if !found {
return self.latch_err(MockAssertionError::HeaderNotFound {
endpoint: self.name.clone(),
key: key.clone(),
value: value.clone(),
});
}
}
for (key, pattern) in &guard.expected_header_regexes {
let re = match regex::Regex::new(pattern) {
Ok(re) => re,
Err(e) => {
return Err(MockAssertionError::InvalidHeaderPattern {
endpoint: self.name.clone(),
key: key.clone(),
pattern: pattern.clone(),
source: Box::new(e),
});
}
};
let found = received.iter().any(|ex| {
ex.input.headers.get(key).is_some_and(|v| {
let s = match v {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
};
re.is_match(&s)
})
});
if !found {
return self.latch_err(MockAssertionError::HeaderRegexNotMatched {
endpoint: self.name.clone(),
key: key.clone(),
pattern: pattern.clone(),
});
}
}
Ok(())
}
fn latch_err(&self, err: MockAssertionError) -> Result<(), MockAssertionError> {
self.set_fail_fast_on_mismatch();
Err(err)
}
}
fn body_eq(a: &camel_component_api::Body, b: &camel_component_api::Body) -> bool {
match (a, b) {
(camel_component_api::Body::Empty, camel_component_api::Body::Empty) => true,
(camel_component_api::Body::Text(a), camel_component_api::Body::Text(b)) => a == b,
(camel_component_api::Body::Json(a), camel_component_api::Body::Json(b)) => a == b,
(camel_component_api::Body::Xml(a), camel_component_api::Body::Xml(b)) => a == b,
(camel_component_api::Body::Bytes(a), camel_component_api::Body::Bytes(b)) => a == b,
_ => false,
}
}