use crate::MockEndpointInner;
use crate::expectations::BodyExpectation;
use crate::matcher::compact_body;
use camel_component_api::{Body, Exchange};
const DIAGNOSTIC_LIST_CAP: usize = 8;
struct HeaderDiagnostics {
received_count: usize,
actual_values: Vec<String>,
last_headers: Option<String>,
}
fn header_diagnostics(received: &[Exchange], key: &str) -> HeaderDiagnostics {
let mut actual_values: Vec<String> = received
.iter()
.filter_map(|ex| ex.input.headers.get(key))
.map(|v| format!("{v:?}"))
.collect();
let overflow = actual_values.len().saturating_sub(DIAGNOSTIC_LIST_CAP);
actual_values.truncate(DIAGNOSTIC_LIST_CAP);
if overflow > 0 {
actual_values.push(format!("+{overflow} more"));
}
let last_headers = received.last().map(|ex| {
let mut keys: Vec<&str> = ex.input.headers.keys().map(String::as_str).collect();
keys.sort_unstable();
let keys_overflow = keys.len().saturating_sub(DIAGNOSTIC_LIST_CAP);
keys.truncate(DIAGNOSTIC_LIST_CAP);
let mut rendered = keys.join(", ");
if keys_overflow > 0 {
rendered.push_str(&format!(", +{keys_overflow} more"));
}
rendered
});
HeaderDiagnostics {
received_count: received.len(),
actual_values,
last_headers,
}
}
fn write_header_received_clause(
f: &mut impl std::fmt::Write,
received_count: usize,
actual_values: &[String],
last_headers: &Option<String>,
key: &str,
) -> std::fmt::Result {
if received_count == 0 {
return write!(f, " (received 0 exchanges)");
}
if !actual_values.is_empty() {
return write!(
f,
" (received {received_count} exchanges; '{key}' present with values: [{}])",
actual_values.join(", ")
);
}
write!(
f,
" (received {received_count} exchanges; '{key}' absent from all received exchanges; last exchange headers: [{}])",
last_headers.as_deref().unwrap_or("")
)
}
#[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,
received_count: usize,
},
BodyMatcherFailed {
endpoint: String,
index: usize,
matcher: String,
received: String,
},
HeaderNotFound {
endpoint: String,
key: String,
value: serde_json::Value,
received_count: usize,
actual_values: Vec<String>,
last_headers: Option<String>,
},
HeaderRegexNotMatched {
endpoint: String,
key: String,
pattern: String,
received_count: usize,
actual_values: Vec<String>,
last_headers: Option<String>,
},
HeaderMatcherFailed {
endpoint: String,
key: String,
matcher: String,
received: String,
},
InvalidHeaderPattern {
endpoint: String,
key: String,
pattern: String,
source: Box<dyn std::error::Error + Send + Sync>,
},
InvalidBodyPattern {
endpoint: 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,
received_count,
} => write!(
f,
"MockEndpoint '{endpoint}': expected body {expected} not found in received exchanges (anyOrder mode) (received {received_count} exchanges)"
),
MockAssertionError::BodyMatcherFailed {
endpoint,
index,
matcher,
received,
} => write!(
f,
"MockEndpoint '{endpoint}': body[{index}] expected {matcher}, got {received}"
),
MockAssertionError::HeaderNotFound {
endpoint,
key,
value,
received_count,
actual_values,
last_headers,
} => {
write!(
f,
"MockEndpoint '{endpoint}': expected header '{key}' = {value} not found in any received exchange"
)?;
write_header_received_clause(f, *received_count, actual_values, last_headers, key)
}
MockAssertionError::HeaderRegexNotMatched {
endpoint,
key,
pattern,
received_count,
actual_values,
last_headers,
} => {
write!(
f,
"MockEndpoint '{endpoint}': no received exchange has header '{key}' matching regex {pattern:?}"
)?;
write_header_received_clause(f, *received_count, actual_values, last_headers, key)
}
MockAssertionError::HeaderMatcherFailed {
endpoint,
key,
matcher,
received,
} => {
write!(
f,
"MockEndpoint '{endpoint}': no received exchange has header '{key}' matching {matcher}"
)?;
write!(f, "{received}")
}
MockAssertionError::InvalidHeaderPattern {
endpoint,
pattern,
source,
..
} => write!(
f,
"MockEndpoint '{endpoint}': invalid regex pattern {pattern:?}: {source}"
),
MockAssertionError::InvalidBodyPattern {
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, .. }
| MockAssertionError::InvalidBodyPattern { source, .. } => Some(&**source),
_ => None,
}
}
}
impl MockEndpointInner {
#[allow(clippy::result_large_err)]
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() {
for expectation in &guard.expected_bodies {
if let BodyExpectation::Matcher(matcher) = expectation
&& let Some(pattern) = matcher.regex_pattern()
&& let Err(e) = regex::Regex::new(pattern)
{
return Err(MockAssertionError::InvalidBodyPattern {
endpoint: self.name.clone(),
pattern: pattern.to_string(),
source: Box::new(e),
});
}
}
let received_bodies: Vec<&Body> = 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<&Body> = received_bodies.clone();
for expectation in &guard.expected_bodies {
let idx = unmatched
.iter()
.position(|actual| expectation_matches(expectation, actual));
match idx {
Some(i) => {
unmatched.remove(i);
}
None => {
return self.latch_err(MockAssertionError::BodyNotFound {
endpoint: self.name.clone(),
expected: expectation_display(expectation),
received_count: received.len(),
});
}
}
}
} else {
for (i, expectation) in guard.expected_bodies.iter().enumerate() {
match expectation {
BodyExpectation::Exact(expected) => {
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]),
});
}
}
BodyExpectation::Matcher(matcher) => {
if !matcher.matches(received_bodies[i]) {
return self.latch_err(MockAssertionError::BodyMatcherFailed {
endpoint: self.name.clone(),
index: i,
matcher: matcher.to_string(),
received: received_body_with_note(
received_bodies[i],
matcher.mismatch_note(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 {
let diag = header_diagnostics(&received, key);
return self.latch_err(MockAssertionError::HeaderNotFound {
endpoint: self.name.clone(),
key: key.clone(),
value: value.clone(),
received_count: diag.received_count,
actual_values: diag.actual_values,
last_headers: diag.last_headers,
});
}
}
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 {
let diag = header_diagnostics(&received, key);
return self.latch_err(MockAssertionError::HeaderRegexNotMatched {
endpoint: self.name.clone(),
key: key.clone(),
pattern: pattern.clone(),
received_count: diag.received_count,
actual_values: diag.actual_values,
last_headers: diag.last_headers,
});
}
}
for (key, matcher) in &guard.expected_header_matchers {
if let Some(pattern) = matcher.regex_pattern()
&& let Err(e) = regex::Regex::new(pattern)
{
return Err(MockAssertionError::InvalidHeaderPattern {
endpoint: self.name.clone(),
key: key.clone(),
pattern: pattern.to_string(),
source: Box::new(e),
});
}
let found = received
.iter()
.any(|ex| matcher.matches(ex.input.headers.get(key)));
if !found {
let representative = received.iter().find_map(|ex| ex.input.headers.get(key));
return self.latch_err(MockAssertionError::HeaderMatcherFailed {
endpoint: self.name.clone(),
key: key.clone(),
matcher: matcher.to_string(),
received: received_header_with_note(
&received,
key,
matcher.mismatch_note(representative),
),
});
}
}
Ok(())
}
#[allow(clippy::result_large_err)]
fn latch_err(&self, err: MockAssertionError) -> Result<(), MockAssertionError> {
self.set_fail_fast_on_mismatch();
Err(err)
}
}
fn expectation_matches(expectation: &BodyExpectation, actual: &Body) -> bool {
match expectation {
BodyExpectation::Exact(expected) => body_eq(expected, actual),
BodyExpectation::Matcher(matcher) => matcher.matches(actual),
}
}
fn expectation_display(expectation: &BodyExpectation) -> String {
match expectation {
BodyExpectation::Exact(expected) => format!("{expected:?}"),
BodyExpectation::Matcher(matcher) => matcher.to_string(),
}
}
fn received_body_with_note(body: &Body, note: Option<&'static str>) -> String {
match note {
Some(note) => format!("{} ({note})", compact_body(body)),
None => compact_body(body),
}
}
fn received_header_with_note(
received: &[Exchange],
key: &str,
note: Option<&'static str>,
) -> String {
let diag = header_diagnostics(received, key);
let mut clause = String::new();
write_header_received_clause(
&mut clause,
diag.received_count,
&diag.actual_values,
&diag.last_headers,
key,
)
.expect("writing to a String cannot fail"); if let Some(note) = note {
clause.push_str(&format!(" ({note})"));
}
clause
}
pub(crate) 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,
}
}