use crate::behaviors::{extract_jsonpath, extract_xpath_with_ns};
use crate::imposter::types::{Predicate, PredicateOperation, PredicateSelector};
use std::collections::HashMap;
#[allow(clippy::too_many_arguments)]
pub fn stub_matches(
predicates: &[Predicate],
method: &str,
path: &str,
query: Option<&str>,
headers: &HashMap<String, String>,
body: Option<&str>,
request_from: Option<&str>,
client_ip: Option<&str>,
form: Option<&HashMap<String, String>>,
imposter_port: u16,
) -> bool {
if predicates.is_empty() {
return true;
}
for predicate in predicates {
if !predicate_matches(
predicate,
method,
path,
query,
headers,
body,
request_from,
client_ip,
form,
imposter_port,
) {
return false;
}
}
true
}
pub fn parse_query(query: Option<&str>) -> HashMap<String, String> {
query.map_or_else(HashMap::new, parse_query_string)
}
#[allow(clippy::too_many_arguments)]
pub fn predicate_matches(
predicate: &Predicate,
method: &str,
path: &str,
query: Option<&str>,
headers: &HashMap<String, String>,
body: Option<&str>,
request_from: Option<&str>,
client_ip: Option<&str>,
form: Option<&HashMap<String, String>>,
imposter_port: u16,
) -> bool {
let case_sensitive = predicate.parameters.case_sensitive.unwrap_or(false);
let key_case_sensitive = predicate
.parameters
.key_case_sensitive
.unwrap_or(case_sensitive);
let except_pattern = Some(predicate.parameters.except.as_str()).filter(|s| !s.is_empty());
let apply_except = |value: &str| -> String {
if let Some(pattern) = except_pattern {
if let Ok(re) = regex::Regex::new(pattern) {
return re.replace_all(value, "").to_string();
}
}
value.to_string()
};
let str_equals = |expected: &str, actual: &str| -> bool {
if case_sensitive {
expected == actual
} else {
expected.eq_ignore_ascii_case(actual)
}
};
let str_contains = |haystack: &str, needle: &str| -> bool {
if case_sensitive {
haystack.contains(needle)
} else {
haystack.to_lowercase().contains(&needle.to_lowercase())
}
};
let str_starts_with = |haystack: &str, needle: &str| -> bool {
if case_sensitive {
haystack.starts_with(needle)
} else {
haystack.to_lowercase().starts_with(&needle.to_lowercase())
}
};
let str_ends_with = |haystack: &str, needle: &str| -> bool {
if case_sensitive {
haystack.ends_with(needle)
} else {
haystack.to_lowercase().ends_with(&needle.to_lowercase())
}
};
let query_map = parse_query(query);
let body_str = body.unwrap_or("");
let extracted_body: String;
let effective_body = match &predicate.parameters.selector {
Some(PredicateSelector::JsonPath { selector }) => {
extracted_body = extract_jsonpath(body_str, selector).unwrap_or_default();
&extracted_body
}
Some(PredicateSelector::XPath {
selector,
namespaces,
}) => {
extracted_body =
extract_xpath_with_ns(body_str, selector, namespaces.as_ref()).unwrap_or_default();
&extracted_body
}
None => body_str,
};
match &predicate.operation {
PredicateOperation::Equals(fields) => {
check_predicate_fields(
fields,
method,
path,
&query_map,
headers,
effective_body,
&apply_except,
str_equals,
false, request_from,
client_ip,
form,
key_case_sensitive,
)
}
PredicateOperation::DeepEquals(fields) => {
check_predicate_fields(
fields,
method,
path,
&query_map,
headers,
effective_body,
&apply_except,
str_equals,
true, request_from,
client_ip,
form,
key_case_sensitive,
)
}
PredicateOperation::Contains(fields) => check_predicate_fields(
fields,
method,
path,
&query_map,
headers,
effective_body,
&apply_except,
|expected, actual| str_contains(actual, expected),
false,
request_from,
client_ip,
form,
key_case_sensitive,
),
PredicateOperation::StartsWith(fields) => check_predicate_fields(
fields,
method,
path,
&query_map,
headers,
effective_body,
&apply_except,
|expected, actual| str_starts_with(actual, expected),
false,
request_from,
client_ip,
form,
key_case_sensitive,
),
PredicateOperation::EndsWith(fields) => check_predicate_fields(
fields,
method,
path,
&query_map,
headers,
effective_body,
&apply_except,
|expected, actual| str_ends_with(actual, expected),
false,
request_from,
client_ip,
form,
key_case_sensitive,
),
PredicateOperation::Matches(fields) => check_predicate_fields_regex(
fields,
method,
path,
&query_map,
headers,
effective_body,
&apply_except,
case_sensitive,
request_from,
client_ip,
form,
key_case_sensitive,
),
PredicateOperation::Exists(fields) => check_exists_predicate(
fields,
method,
path,
&query_map,
headers,
effective_body,
request_from,
client_ip,
form,
key_case_sensitive,
),
PredicateOperation::Not(inner) => !predicate_matches(
inner,
method,
path,
query,
headers,
body,
request_from,
client_ip,
form,
imposter_port,
),
PredicateOperation::Or(children) => children.iter().any(|p| {
predicate_matches(
p,
method,
path,
query,
headers,
body,
request_from,
client_ip,
form,
imposter_port,
)
}),
PredicateOperation::And(children) => children.iter().all(|p| {
predicate_matches(
p,
method,
path,
query,
headers,
body,
request_from,
client_ip,
form,
imposter_port,
)
}),
PredicateOperation::Inject(inject_fn) => {
#[cfg(feature = "javascript")]
{
use crate::scripting::{execute_predicate_inject, MountebankRequest};
let query_map = parse_query(query);
let mb_request = MountebankRequest {
method: method.to_string(),
path: path.to_string(),
query: query_map,
headers: headers.clone(),
body: body.map(|b| b.to_string()),
};
execute_predicate_inject(inject_fn, &mb_request, imposter_port)
}
#[cfg(not(feature = "javascript"))]
{
tracing::warn!(
"inject predicate requires the 'javascript' feature; predicate will not match"
);
let _ = inject_fn;
false
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn check_predicate_fields<F>(
obj: &HashMap<String, serde_json::Value>,
method: &str,
path: &str,
query: &HashMap<String, String>,
headers: &HashMap<String, String>,
body: &str,
apply_except: &impl Fn(&str) -> String,
compare: F,
deep_equals: bool,
request_from: Option<&str>,
client_ip: Option<&str>,
form: Option<&HashMap<String, String>>,
key_case_sensitive: bool,
) -> bool
where
F: Fn(&str, &str) -> bool,
{
let key_matches = |expected_key: &str, actual_key: &str| -> bool {
if key_case_sensitive {
expected_key == actual_key
} else {
expected_key.eq_ignore_ascii_case(actual_key)
}
};
let check_string_field = |expected: &serde_json::Value, actual: &str| -> bool {
match expected {
serde_json::Value::Object(_) | serde_json::Value::Array(_) => compare_json_recursive(
expected,
actual,
&compare,
deep_equals,
key_case_sensitive,
&apply_except,
),
_ => {
let actual = apply_except(actual);
match expected {
serde_json::Value::String(s) => compare(s, &actual),
_ => {
let expected_str = expected.to_string();
compare(&expected_str, &actual)
}
}
}
}
};
if let Some(expected) = obj.get("method") {
if !check_string_field(expected, method) {
return false;
}
}
if let Some(expected) = obj.get("path") {
if !check_string_field(expected, path) {
return false;
}
}
if let Some(expected) = obj.get("body") {
if !check_string_field(expected, body) {
return false;
}
}
if let Some(expected) = obj.get("requestFrom") {
let actual = request_from.unwrap_or("");
if !check_string_field(expected, actual) {
return false;
}
}
if let Some(expected) = obj.get("ip") {
let actual = client_ip.unwrap_or("");
if !check_string_field(expected, actual) {
return false;
}
}
if let Some(expected_form) = obj.get("form") {
if let Some(expected_obj) = expected_form.as_object() {
let actual_form = form.cloned().unwrap_or_default();
if deep_equals && expected_obj.len() != actual_form.len() {
return false;
}
for (key, expected_val) in expected_obj {
let actual = actual_form
.iter()
.find(|(k, _)| key_matches(key, k))
.map(|(_, v)| v.as_str());
match actual {
Some(actual) => {
if !check_string_field(expected_val, actual) {
return false;
}
}
None => return false,
}
}
}
}
if let Some(expected_query) = obj.get("query") {
if let Some(expected_obj) = expected_query.as_object() {
if deep_equals && expected_obj.len() != query.len() {
return false;
}
for (key, expected_val) in expected_obj {
let actual = query
.iter()
.find(|(k, _)| key_matches(key, k))
.map(|(_, v)| v.as_str());
match actual {
Some(actual) => {
if !check_string_field(expected_val, actual) {
return false;
}
}
None => return false,
}
}
}
}
if let Some(expected_headers) = obj.get("headers") {
if let Some(expected_obj) = expected_headers.as_object() {
if deep_equals && expected_obj.len() != headers.len() {
return false;
}
for (key, expected_val) in expected_obj {
let actual = headers
.iter()
.find(|(k, _)| key_matches(key, k))
.map(|(_, v)| v.as_str());
match actual {
Some(actual) => {
if !check_string_field(expected_val, actual) {
return false;
}
}
None => return false,
}
}
}
}
true
}
#[allow(clippy::too_many_arguments)]
fn check_predicate_fields_regex(
obj: &HashMap<String, serde_json::Value>,
method: &str,
path: &str,
query: &HashMap<String, String>,
headers: &HashMap<String, String>,
body: &str,
apply_except: &impl Fn(&str) -> String,
case_sensitive: bool,
request_from: Option<&str>,
client_ip: Option<&str>,
form: Option<&HashMap<String, String>>,
key_case_sensitive: bool,
) -> bool {
let build_regex = |pattern: &str| -> Result<regex::Regex, regex::Error> {
if case_sensitive {
regex::Regex::new(pattern)
} else {
regex::RegexBuilder::new(pattern)
.case_insensitive(true)
.build()
}
};
let key_matches = |expected_key: &str, actual_key: &str| -> bool {
if key_case_sensitive {
expected_key == actual_key
} else {
expected_key.eq_ignore_ascii_case(actual_key)
}
};
let check_regex_field = |expected: &serde_json::Value, actual: &str| -> bool {
match expected {
serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
let regex_compare = |pattern: &str, actual: &str| -> bool {
match build_regex(pattern) {
Ok(re) => re.is_match(actual),
Err(_) => false,
}
};
compare_json_recursive(
expected,
actual,
®ex_compare,
false,
key_case_sensitive,
&apply_except,
)
}
_ => {
let pattern = match expected {
serde_json::Value::String(s) => s.as_str().to_string(),
_ => expected.to_string(),
};
match build_regex(&pattern) {
Ok(re) => {
let actual = apply_except(actual);
re.is_match(&actual)
}
Err(_) => false,
}
}
}
};
if let Some(expected) = obj.get("method") {
if !check_regex_field(expected, method) {
return false;
}
}
if let Some(expected) = obj.get("path") {
if !check_regex_field(expected, path) {
return false;
}
}
if let Some(expected) = obj.get("body") {
if !check_regex_field(expected, body) {
return false;
}
}
if let Some(expected) = obj.get("requestFrom") {
if !check_regex_field(expected, request_from.unwrap_or("")) {
return false;
}
}
if let Some(expected) = obj.get("ip") {
if !check_regex_field(expected, client_ip.unwrap_or("")) {
return false;
}
}
if let Some(expected_form) = obj.get("form").and_then(|v| v.as_object()) {
let actual_form = form.cloned().unwrap_or_default();
for (key, pattern_val) in expected_form {
let actual = actual_form
.iter()
.find(|(k, _)| key_matches(key, k))
.map(|(_, v)| v.as_str());
match actual {
Some(actual) => {
if !check_regex_field(pattern_val, actual) {
return false;
}
}
None => return false,
}
}
}
if let Some(expected_query) = obj.get("query").and_then(|v| v.as_object()) {
for (key, pattern_val) in expected_query {
let actual = query
.iter()
.find(|(k, _)| key_matches(key, k))
.map(|(_, v)| v.as_str());
match actual {
Some(actual) => {
if !check_regex_field(pattern_val, actual) {
return false;
}
}
None => return false,
}
}
}
if let Some(expected_headers) = obj.get("headers").and_then(|v| v.as_object()) {
for (key, pattern_val) in expected_headers {
let actual = headers
.iter()
.find(|(k, _)| key_matches(key, k))
.map(|(_, v)| v.as_str());
match actual {
Some(actual) => {
if !check_regex_field(pattern_val, actual) {
return false;
}
}
None => return false,
}
}
}
true
}
fn json_value_to_string(val: &serde_json::Value) -> String {
match val {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Null => String::new(),
_ => val.to_string(),
}
}
fn check_exists_json_recursive(expected: &serde_json::Value, actual_str: &str) -> bool {
match expected {
serde_json::Value::Bool(should_exist) => {
let exists = !actual_str.is_empty();
exists == *should_exist
}
serde_json::Value::Object(expected_obj) => {
let actual_json: serde_json::Value = match serde_json::from_str(actual_str) {
Ok(v) => v,
Err(_) => {
return expected_obj
.values()
.all(|v| v == &serde_json::Value::Bool(false));
}
};
for (key, expected_val) in expected_obj {
match expected_val {
serde_json::Value::Bool(should_exist) => {
let exists = actual_json.get(key).is_some();
if exists != *should_exist {
return false;
}
}
serde_json::Value::Object(_) => {
let nested_str = match actual_json.get(key) {
Some(v) => json_value_to_string(v),
None => return false,
};
if !check_exists_json_recursive(expected_val, &nested_str) {
return false;
}
}
_ => {
if actual_json.get(key).is_none() {
return false;
}
}
}
}
true
}
_ => {
!actual_str.is_empty()
}
}
}
#[allow(clippy::too_many_arguments)]
fn check_exists_predicate(
obj: &HashMap<String, serde_json::Value>,
method: &str,
path: &str,
query: &HashMap<String, String>,
headers: &HashMap<String, String>,
body: &str,
request_from: Option<&str>,
client_ip: Option<&str>,
form: Option<&HashMap<String, String>>,
key_case_sensitive: bool,
) -> bool {
let key_matches = |expected_key: &str, actual_key: &str| -> bool {
if key_case_sensitive {
expected_key == actual_key
} else {
expected_key.eq_ignore_ascii_case(actual_key)
}
};
if let Some(expected) = obj.get("method") {
let should_exist = expected.as_bool().unwrap_or(true);
let exists = !method.is_empty();
if exists != should_exist {
return false;
}
}
if let Some(expected) = obj.get("path") {
let should_exist = expected.as_bool().unwrap_or(true);
let exists = !path.is_empty();
if exists != should_exist {
return false;
}
}
if let Some(expected) = obj.get("requestFrom") {
let should_exist = expected.as_bool().unwrap_or(true);
let exists = request_from.is_some_and(|v| !v.is_empty());
if exists != should_exist {
return false;
}
}
if let Some(expected) = obj.get("ip") {
let should_exist = expected.as_bool().unwrap_or(true);
let exists = client_ip.is_some_and(|v| !v.is_empty());
if exists != should_exist {
return false;
}
}
if let Some(expected) = obj.get("body") {
if !check_exists_json_recursive(expected, body) {
return false;
}
}
if let Some(expected_query) = obj.get("query").and_then(|v| v.as_object()) {
for (key, should_exist_val) in expected_query {
let should_exist = should_exist_val.as_bool().unwrap_or(true);
let exists = query.iter().any(|(k, _)| key_matches(key, k));
if exists != should_exist {
return false;
}
}
}
if let Some(expected_headers) = obj.get("headers").and_then(|v| v.as_object()) {
for (key, should_exist_val) in expected_headers {
let should_exist = should_exist_val.as_bool().unwrap_or(true);
let exists = headers.iter().any(|(k, _)| key_matches(key, k));
if exists != should_exist {
return false;
}
}
}
if let Some(expected_form) = obj.get("form").and_then(|v| v.as_object()) {
let actual_form = form.cloned().unwrap_or_default();
for (key, should_exist_val) in expected_form {
let should_exist = should_exist_val.as_bool().unwrap_or(true);
let exists = actual_form.iter().any(|(k, _)| key_matches(key, k));
if exists != should_exist {
return false;
}
}
}
true
}
fn compare_json_recursive<F>(
expected: &serde_json::Value,
actual_str: &str,
compare: &F,
deep_equals: bool,
key_case_sensitive: bool,
apply_except: &dyn Fn(&str) -> String,
) -> bool
where
F: Fn(&str, &str) -> bool,
{
match expected {
serde_json::Value::Object(expected_obj) => {
let actual_json: serde_json::Value = match serde_json::from_str(actual_str) {
Ok(v) => v,
Err(_) => return false,
};
let actual_obj = match actual_json.as_object() {
Some(obj) => obj,
None => return false,
};
if deep_equals && expected_obj.len() != actual_obj.len() {
return false;
}
for (key, expected_val) in expected_obj {
let actual_val = if key_case_sensitive {
actual_obj.get(key)
} else {
actual_obj
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(key))
.map(|(_, v)| v)
};
let actual_val = match actual_val {
Some(v) => v,
None => return false,
};
let actual_val_str = json_value_to_string(actual_val);
if !compare_json_recursive(
expected_val,
&actual_val_str,
compare,
deep_equals,
key_case_sensitive,
apply_except,
) {
return false;
}
}
true
}
serde_json::Value::Array(expected_arr) => {
let actual_json: serde_json::Value = match serde_json::from_str(actual_str) {
Ok(v) => v,
Err(_) => return false,
};
let actual_arr = match actual_json.as_array() {
Some(arr) => arr,
None => return false,
};
if expected_arr.len() != actual_arr.len() {
return false;
}
for (expected_elem, actual_elem) in expected_arr.iter().zip(actual_arr.iter()) {
let actual_elem_str = json_value_to_string(actual_elem);
if !compare_json_recursive(
expected_elem,
&actual_elem_str,
compare,
deep_equals,
key_case_sensitive,
apply_except,
) {
return false;
}
}
true
}
_ => {
let expected_str = json_value_to_string(expected);
let actual_str = apply_except(actual_str);
compare(&expected_str, &actual_str)
}
}
}
pub fn parse_query_string(query: &str) -> HashMap<String, String> {
let mut map = HashMap::new();
for pair in query.split('&').filter(|s| !s.is_empty()) {
let (key, value) = match pair.split_once('=') {
Some((k, v)) => (k, v),
None => (pair, ""),
};
let decoded_key = urlencoding::decode(key).unwrap_or_default().into_owned();
let decoded_value = urlencoding::decode(value).unwrap_or_default().into_owned();
map.entry(decoded_key)
.and_modify(|existing: &mut String| {
existing.push(',');
existing.push_str(&decoded_value);
})
.or_insert(decoded_value);
}
map
}
#[cfg(test)]
mod tests {
use super::*;
use crate::imposter::types::{Predicate, PredicateOperation, PredicateParameters};
use serde_json::json;
fn make_predicate(op: PredicateOperation) -> Predicate {
Predicate {
parameters: PredicateParameters::default(),
operation: op,
}
}
fn make_predicate_with_params(
op: PredicateOperation,
params: PredicateParameters,
) -> Predicate {
Predicate {
parameters: params,
operation: op,
}
}
fn empty_headers() -> HashMap<String, String> {
HashMap::new()
}
#[test]
fn test_parse_query_string_multi_valued_comma_joined() {
let result = parse_query_string("key=first&key=second");
assert_eq!(result.len(), 1);
assert_eq!(
result.get("key"),
Some(&"first,second".to_string()),
"Multi-valued query params should be comma-joined"
);
}
#[test]
fn test_equals_query_multi_valued_param() {
let fields: HashMap<String, serde_json::Value> =
[("query".to_string(), json!({"key": "first,second"}))]
.into_iter()
.collect();
let pred = make_predicate(PredicateOperation::Equals(fields));
let result = predicate_matches(
&pred,
"GET",
"/test",
Some("key=first&key=second"),
&empty_headers(),
None,
None,
None,
None,
0,
);
assert!(
result,
"equals should match comma-joined multi-valued query params"
);
}
#[test]
fn test_parse_query_string_bare_param() {
let result = parse_query_string("flag");
assert_eq!(
result.get("flag"),
Some(&String::new()),
"Bare query param 'flag' should be present with empty value"
);
}
#[test]
fn test_parse_query_string_mixed_bare_and_valued() {
let result = parse_query_string("a=1&flag&b=2");
assert_eq!(result.get("a"), Some(&"1".to_string()));
assert_eq!(result.get("b"), Some(&"2".to_string()));
assert!(
result.contains_key("flag"),
"Bare param 'flag' should be present in mixed query string"
);
}
#[test]
fn test_exists_query_bare_param() {
let fields: HashMap<String, serde_json::Value> =
[("query".to_string(), json!({"flag": true}))]
.into_iter()
.collect();
let pred = make_predicate(PredicateOperation::Exists(fields));
let result = predicate_matches(
&pred,
"GET",
"/test",
Some("flag"),
&empty_headers(),
None,
None,
None,
None,
0,
);
assert!(
result,
"exists predicate should match bare query param '?flag'"
);
}
#[test]
fn test_deep_equals_body_json_key_order_independence() {
let fields: HashMap<String, serde_json::Value> =
[("body".to_string(), json!({"a": 1, "b": 2}))]
.into_iter()
.collect();
let pred = make_predicate(PredicateOperation::DeepEquals(fields));
let body = r#"{"b":2,"a":1}"#;
let result = predicate_matches(
&pred,
"GET",
"/test",
None,
&empty_headers(),
Some(body),
None,
None,
None,
0,
);
assert!(
result,
"deepEquals should match JSON bodies regardless of key order"
);
}
#[test]
fn test_deep_equals_body_string_match() {
let fields: HashMap<String, serde_json::Value> =
[("body".to_string(), json!("hello world"))]
.into_iter()
.collect();
let pred = make_predicate(PredicateOperation::DeepEquals(fields));
let result = predicate_matches(
&pred,
"GET",
"/test",
None,
&empty_headers(),
Some("hello world"),
None,
None,
None,
0,
);
assert!(result, "deepEquals should match identical string bodies");
}
#[test]
fn test_exists_query_key_case_sensitive_false() {
let fields: HashMap<String, serde_json::Value> =
[("query".to_string(), json!({"Key": true}))]
.into_iter()
.collect();
let pred = make_predicate(PredicateOperation::Exists(fields));
let result = predicate_matches(
&pred,
"GET",
"/test",
Some("key=value"),
&empty_headers(),
None,
None,
None,
None,
0,
);
assert!(
result,
"exists with keyCaseSensitive=false should match case-insensitively"
);
}
#[test]
fn test_exists_header_key_case_sensitive_true() {
let fields: HashMap<String, serde_json::Value> =
[("headers".to_string(), json!({"Content-Type": true}))]
.into_iter()
.collect();
let params = PredicateParameters {
key_case_sensitive: Some(true),
..Default::default()
};
let pred = make_predicate_with_params(PredicateOperation::Exists(fields), params);
let mut headers = HashMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
let result = predicate_matches(
&pred, "GET", "/test", None, &headers, None, None, None, None, 0,
);
assert!(
result,
"exists with keyCaseSensitive=true should match exact case header key"
);
}
#[test]
fn test_exists_form_key_case_sensitive() {
let fields: HashMap<String, serde_json::Value> =
[("form".to_string(), json!({"Name": true}))]
.into_iter()
.collect();
let pred = make_predicate(PredicateOperation::Exists(fields));
let mut form = HashMap::new();
form.insert("name".to_string(), "John".to_string());
let result = predicate_matches(
&pred,
"POST",
"/test",
None,
&empty_headers(),
Some("name=John"),
None,
None,
Some(&form),
0,
);
assert!(
result,
"exists with keyCaseSensitive=false should match form key case-insensitively"
);
}
#[test]
fn test_header_key_case_sensitive_true_with_title_case() {
let fields: HashMap<String, serde_json::Value> = [(
"headers".to_string(),
json!({"Content-Type": "application/json"}),
)]
.into_iter()
.collect();
let params = PredicateParameters {
case_sensitive: Some(true),
key_case_sensitive: Some(true),
..Default::default()
};
let pred = make_predicate_with_params(PredicateOperation::Equals(fields), params);
let mut headers = HashMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
let result = predicate_matches(
&pred, "GET", "/test", None, &headers, None, None, None, None, 0,
);
assert!(
result,
"keyCaseSensitive=true with Title-Case header key should match"
);
}
#[test]
fn test_header_key_case_sensitive_false_default() {
let fields: HashMap<String, serde_json::Value> = [(
"headers".to_string(),
json!({"Content-Type": "application/json"}),
)]
.into_iter()
.collect();
let pred = make_predicate(PredicateOperation::Equals(fields));
let mut headers = HashMap::new();
headers.insert("content-type".to_string(), "application/json".to_string());
let result = predicate_matches(
&pred,
"GET",
"/test",
None,
&headers,
Some(""),
None,
None,
None,
0,
);
assert!(
result,
"Default keyCaseSensitive=false should match case-insensitively"
);
}
#[test]
fn test_equals_method() {
let fields: HashMap<String, serde_json::Value> = [("method".to_string(), json!("POST"))]
.into_iter()
.collect();
let pred = make_predicate(PredicateOperation::Equals(fields));
assert!(predicate_matches(
&pred,
"POST",
"/test",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
assert!(predicate_matches(
&pred,
"post",
"/test",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
assert!(!predicate_matches(
&pred,
"GET",
"/test",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
}
#[test]
fn test_equals_path() {
let fields: HashMap<String, serde_json::Value> =
[("path".to_string(), json!("/api/users"))]
.into_iter()
.collect();
let pred = make_predicate(PredicateOperation::Equals(fields));
assert!(predicate_matches(
&pred,
"GET",
"/api/users",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
assert!(!predicate_matches(
&pred,
"GET",
"/api/other",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
}
#[test]
fn test_contains_body() {
let fields: HashMap<String, serde_json::Value> =
[("body".to_string(), json!("hello"))].into_iter().collect();
let pred = make_predicate(PredicateOperation::Contains(fields));
assert!(predicate_matches(
&pred,
"POST",
"/",
None,
&empty_headers(),
Some("say hello world"),
None,
None,
None,
0,
));
assert!(!predicate_matches(
&pred,
"POST",
"/",
None,
&empty_headers(),
Some("goodbye"),
None,
None,
None,
0,
));
}
#[test]
fn test_starts_with_path() {
let fields: HashMap<String, serde_json::Value> =
[("path".to_string(), json!("/api/"))].into_iter().collect();
let pred = make_predicate(PredicateOperation::StartsWith(fields));
assert!(predicate_matches(
&pred,
"GET",
"/api/users",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
assert!(!predicate_matches(
&pred,
"GET",
"/web/page",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
}
#[test]
fn test_ends_with_path() {
let fields: HashMap<String, serde_json::Value> =
[("path".to_string(), json!(".json"))].into_iter().collect();
let pred = make_predicate(PredicateOperation::EndsWith(fields));
assert!(predicate_matches(
&pred,
"GET",
"/data/file.json",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
assert!(!predicate_matches(
&pred,
"GET",
"/data/file.xml",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
}
#[test]
fn test_matches_regex() {
let fields: HashMap<String, serde_json::Value> =
[("path".to_string(), json!("^/api/users/\\d+$"))]
.into_iter()
.collect();
let pred = make_predicate(PredicateOperation::Matches(fields));
assert!(predicate_matches(
&pred,
"GET",
"/api/users/123",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
assert!(!predicate_matches(
&pred,
"GET",
"/api/users/abc",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
}
#[test]
fn test_not_predicate() {
let inner_fields: HashMap<String, serde_json::Value> =
[("method".to_string(), json!("GET"))].into_iter().collect();
let inner = make_predicate(PredicateOperation::Equals(inner_fields));
let pred = make_predicate(PredicateOperation::Not(Box::new(inner)));
assert!(!predicate_matches(
&pred,
"GET",
"/",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
assert!(predicate_matches(
&pred,
"POST",
"/",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
}
#[test]
fn test_or_predicate() {
let eq_get: HashMap<String, serde_json::Value> =
[("method".to_string(), json!("GET"))].into_iter().collect();
let eq_post: HashMap<String, serde_json::Value> = [("method".to_string(), json!("POST"))]
.into_iter()
.collect();
let pred = make_predicate(PredicateOperation::Or(vec![
make_predicate(PredicateOperation::Equals(eq_get)),
make_predicate(PredicateOperation::Equals(eq_post)),
]));
assert!(predicate_matches(
&pred,
"GET",
"/",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
assert!(predicate_matches(
&pred,
"POST",
"/",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
assert!(!predicate_matches(
&pred,
"DELETE",
"/",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
}
#[test]
fn test_and_predicate() {
let eq_get: HashMap<String, serde_json::Value> =
[("method".to_string(), json!("GET"))].into_iter().collect();
let eq_path: HashMap<String, serde_json::Value> =
[("path".to_string(), json!("/api"))].into_iter().collect();
let pred = make_predicate(PredicateOperation::And(vec![
make_predicate(PredicateOperation::Equals(eq_get)),
make_predicate(PredicateOperation::Equals(eq_path)),
]));
assert!(predicate_matches(
&pred,
"GET",
"/api",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
assert!(!predicate_matches(
&pred,
"POST",
"/api",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
assert!(!predicate_matches(
&pred,
"GET",
"/other",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
}
#[test]
fn test_case_sensitive_equals() {
let fields: HashMap<String, serde_json::Value> = [("method".to_string(), json!("POST"))]
.into_iter()
.collect();
let params = PredicateParameters {
case_sensitive: Some(true),
..Default::default()
};
let pred = make_predicate_with_params(PredicateOperation::Equals(fields), params);
assert!(predicate_matches(
&pred,
"POST",
"/",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
assert!(!predicate_matches(
&pred,
"post",
"/",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
}
#[test]
fn test_except_pattern() {
let fields: HashMap<String, serde_json::Value> =
[("path".to_string(), json!("/api/users"))]
.into_iter()
.collect();
let params = PredicateParameters {
except: "/api".to_string(),
..Default::default()
};
let pred = make_predicate_with_params(PredicateOperation::Equals(fields), params);
assert!(!predicate_matches(
&pred,
"GET",
"/api/users",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
}
#[test]
fn test_exists_body() {
let fields_true: HashMap<String, serde_json::Value> =
[("body".to_string(), json!(true))].into_iter().collect();
let fields_false: HashMap<String, serde_json::Value> =
[("body".to_string(), json!(false))].into_iter().collect();
let pred_true = make_predicate(PredicateOperation::Exists(fields_true));
let pred_false = make_predicate(PredicateOperation::Exists(fields_false));
assert!(predicate_matches(
&pred_true,
"POST",
"/",
None,
&empty_headers(),
Some("content"),
None,
None,
None,
0,
));
assert!(!predicate_matches(
&pred_true,
"GET",
"/",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
assert!(predicate_matches(
&pred_false,
"GET",
"/",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
}
#[test]
fn test_exists_header() {
let fields: HashMap<String, serde_json::Value> =
[("headers".to_string(), json!({"content-type": true}))]
.into_iter()
.collect();
let pred = make_predicate(PredicateOperation::Exists(fields));
let mut headers = HashMap::new();
headers.insert("content-type".to_string(), "application/json".to_string());
assert!(predicate_matches(
&pred, "GET", "/", None, &headers, None, None, None, None, 0
));
assert!(!predicate_matches(
&pred,
"GET",
"/",
None,
&empty_headers(),
None,
None,
None,
None,
0,
));
}
#[test]
fn test_deep_equals_query_extra_params_mismatch() {
let fields: HashMap<String, serde_json::Value> = [("query".to_string(), json!({"a": "1"}))]
.into_iter()
.collect();
let pred = make_predicate(PredicateOperation::DeepEquals(fields));
assert!(predicate_matches(
&pred,
"GET",
"/",
Some("a=1"),
&empty_headers(),
None,
None,
None,
None,
0,
));
assert!(!predicate_matches(
&pred,
"GET",
"/",
Some("a=1&b=2"),
&empty_headers(),
None,
None,
None,
None,
0,
));
}
#[test]
fn test_deep_equals_headers_extra_mismatch() {
let fields: HashMap<String, serde_json::Value> =
[("headers".to_string(), json!({"x-custom": "value"}))]
.into_iter()
.collect();
let pred = make_predicate(PredicateOperation::DeepEquals(fields));
let mut exact_headers = HashMap::new();
exact_headers.insert("x-custom".to_string(), "value".to_string());
let mut extra_headers = HashMap::new();
extra_headers.insert("x-custom".to_string(), "value".to_string());
extra_headers.insert("x-other".to_string(), "other".to_string());
assert!(predicate_matches(
&pred,
"GET",
"/",
None,
&exact_headers,
None,
None,
None,
None,
0,
));
assert!(!predicate_matches(
&pred,
"GET",
"/",
None,
&extra_headers,
None,
None,
None,
None,
0,
));
}
#[test]
fn test_stub_matches_empty_predicates() {
assert!(stub_matches(
&[],
"GET",
"/anything",
None,
&empty_headers(),
None,
None,
None,
None,
0
));
}
#[test]
fn test_stub_matches_all_must_match() {
let predicates = vec![
make_predicate(PredicateOperation::Equals(
[("method".to_string(), json!("GET"))].into_iter().collect(),
)),
make_predicate(PredicateOperation::Equals(
[("path".to_string(), json!("/api"))].into_iter().collect(),
)),
];
assert!(stub_matches(
&predicates,
"GET",
"/api",
None,
&empty_headers(),
None,
None,
None,
None,
0
));
assert!(!stub_matches(
&predicates,
"POST",
"/api",
None,
&empty_headers(),
None,
None,
None,
None,
0
));
}
#[test]
fn test_parse_query_string_url_encoded() {
let result = parse_query_string("key=hello%20world&name=caf%C3%A9");
assert_eq!(result.get("key"), Some(&"hello world".to_string()));
assert_eq!(result.get("name"), Some(&"café".to_string()));
}
#[test]
fn test_parse_query_string_empty() {
let result = parse_query_string("");
assert!(result.is_empty());
}
#[test]
fn test_equals_json_body_key_case_insensitive_by_default() {
let fields: HashMap<String, serde_json::Value> =
[("body".to_string(), json!({"Name": "John"}))]
.into_iter()
.collect();
let pred = make_predicate(PredicateOperation::Equals(fields));
let result = predicate_matches(
&pred,
"POST",
"/test",
None,
&empty_headers(),
Some(r#"{"name": "John"}"#),
None,
None,
None,
0,
);
assert!(
result,
"caseSensitive=false (default) should match JSON body keys case-insensitively"
);
}
#[test]
fn test_except_applied_to_leaf_values_not_raw_json() {
let fields: HashMap<String, serde_json::Value> =
[("body".to_string(), json!({"greeting": "hello"}))]
.into_iter()
.collect();
let params = PredicateParameters {
except: "\\d+".to_string(),
..Default::default()
};
let pred = make_predicate_with_params(PredicateOperation::Equals(fields), params);
let result = predicate_matches(
&pred,
"POST",
"/test",
None,
&empty_headers(),
Some(r#"{"greeting": "hello", "count": 42}"#),
None,
None,
None,
0,
);
assert!(
result,
"except should apply to leaf values after JSON parsing, not break raw JSON structure"
);
}
#[test]
fn test_matches_invalid_regex_returns_false() {
let fields: HashMap<String, serde_json::Value> = [("path".to_string(), json!("[unclosed"))]
.into_iter()
.collect();
let pred = make_predicate(PredicateOperation::Matches(fields));
let result = predicate_matches(
&pred,
"GET",
"/any/path/at/all",
None,
&empty_headers(),
None,
None,
None,
None,
0,
);
assert!(
!result,
"Invalid regex pattern should cause the predicate to not match"
);
}
#[test]
fn test_matches_method_with_except_applied() {
let fields: HashMap<String, serde_json::Value> = [("method".to_string(), json!("^OST$"))]
.into_iter()
.collect();
let params = PredicateParameters {
except: "P".to_string(),
..Default::default()
};
let pred = make_predicate_with_params(PredicateOperation::Matches(fields), params);
let result = predicate_matches(
&pred,
"POST",
"/test",
None,
&empty_headers(),
None,
None,
None,
None,
0,
);
assert!(
result,
"except should be applied to method before regex matching"
);
}
#[test]
fn test_equals_body_array_rejects_longer_actual() {
let fields: HashMap<String, serde_json::Value> =
[("body".to_string(), json!([1, 2]))].into_iter().collect();
let pred = make_predicate(PredicateOperation::Equals(fields));
let result = predicate_matches(
&pred,
"POST",
"/test",
None,
&empty_headers(),
Some("[1, 2, 3]"),
None,
None,
None,
0,
);
assert!(
!result,
"equals [1, 2] should not match [1, 2, 3] — arrays have different lengths"
);
}
#[test]
fn test_equals_body_array_rejects_shorter_actual() {
let fields: HashMap<String, serde_json::Value> = [("body".to_string(), json!([1, 2, 3]))]
.into_iter()
.collect();
let pred = make_predicate(PredicateOperation::Equals(fields));
let result = predicate_matches(
&pred,
"POST",
"/test",
None,
&empty_headers(),
Some("[1, 2]"),
None,
None,
None,
0,
);
assert!(
!result,
"equals [1, 2, 3] should not match [1, 2] — arrays have different lengths"
);
}
#[test]
fn test_exists_method_false_fails_when_present() {
let fields: HashMap<String, serde_json::Value> =
[("method".to_string(), json!(false))].into_iter().collect();
let pred = make_predicate(PredicateOperation::Exists(fields));
let result = predicate_matches(
&pred,
"GET",
"/test",
None,
&empty_headers(),
None,
None,
None,
None,
0,
);
assert!(
!result,
"exists method=false should fail when method is present"
);
}
#[test]
fn test_exists_path_false_fails_when_present() {
let fields: HashMap<String, serde_json::Value> =
[("path".to_string(), json!(false))].into_iter().collect();
let pred = make_predicate(PredicateOperation::Exists(fields));
let result = predicate_matches(
&pred,
"GET",
"/test",
None,
&empty_headers(),
None,
None,
None,
None,
0,
);
assert!(
!result,
"exists path=false should fail when path is present"
);
}
#[cfg(feature = "javascript")]
#[test]
fn test_inject_predicate_matches_true() {
let pred = make_predicate(PredicateOperation::Inject(
"function(request) { return request.path === '/api'; }".to_string(),
));
let result = predicate_matches(
&pred,
"GET",
"/api",
None,
&empty_headers(),
None,
None,
None,
None,
0,
);
assert!(result, "inject predicate returning true should match");
}
#[cfg(feature = "javascript")]
#[test]
fn test_inject_predicate_matches_false() {
let pred = make_predicate(PredicateOperation::Inject(
"function(request) { return request.path === '/other'; }".to_string(),
));
let result = predicate_matches(
&pred,
"GET",
"/api",
None,
&empty_headers(),
None,
None,
None,
None,
0,
);
assert!(!result, "inject predicate returning false should not match");
}
#[cfg(feature = "javascript")]
#[test]
fn test_inject_predicate_checks_method() {
let pred = make_predicate(PredicateOperation::Inject(
"function(request) { return request.method === 'POST'; }".to_string(),
));
let headers = HashMap::new();
let post_result = predicate_matches(
&pred, "POST", "/", None, &headers, None, None, None, None, 0,
);
let get_result =
predicate_matches(&pred, "GET", "/", None, &headers, None, None, None, None, 0);
assert!(post_result, "inject predicate should match POST");
assert!(!get_result, "inject predicate should not match GET");
}
#[cfg(feature = "javascript")]
#[test]
fn test_inject_predicate_accesses_body() {
let pred = make_predicate(PredicateOperation::Inject(
r#"function(request) { return request.body && request.body.indexOf('hello') >= 0; }"#
.to_string(),
));
let result = predicate_matches(
&pred,
"POST",
"/",
None,
&empty_headers(),
Some("hello world"),
None,
None,
None,
0,
);
assert!(result, "inject predicate should access request body");
}
#[test]
fn test_inject_predicate_deserializes() {
let json = r#"{"inject": "function(request) { return true; }"}"#;
let op: PredicateOperation = serde_json::from_str(json).expect("should deserialize inject");
assert!(matches!(op, PredicateOperation::Inject(_)));
}
}