Skip to content

Commit

Permalink
fix: Header matching rules should be looked up in a case-insenstive way
Browse files Browse the repository at this point in the history
  • Loading branch information
rholshausen committed Jun 28, 2023
1 parent 52d6bfa commit 445ea1e
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 31 deletions.
2 changes: 2 additions & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion rust/pact_consumer/src/builders/http_part_builder.rs
Expand Up @@ -202,7 +202,7 @@ mod tests {
use crate::builders::{HttpPartBuilder, PactBuilder};
use crate::patterns::{Like, Term};

#[test]
#[test_log::test]
fn header_pattern() {
let application_regex = Regex::new("application/.*").unwrap();
let pattern = PactBuilder::new("C", "P")
Expand Down
24 changes: 12 additions & 12 deletions rust/pact_matching/src/headers.rs
Expand Up @@ -144,7 +144,7 @@ fn match_header_maps(
} else {
let empty = String::new();
for (index, val) in value.iter()
.pad_using(actual.len(), |_| &empty)
.pad_using(actual_values.len(), |_| &empty)
.enumerate() {
if let Some(actual_value) = actual_values.get(index) {
let comparison_result = match_header_value(key, index, val,
Expand Down Expand Up @@ -201,7 +201,7 @@ mod tests {
use pact_models::matchingrules;
use pact_models::matchingrules::MatchingRule;

use crate::{CoreMatchingContext, DiffConfig, Mismatch};
use crate::{CoreMatchingContext, DiffConfig, HeaderMatchingContext, Mismatch};
use crate::headers::{match_header_value, match_headers, parse_charset_parameters};

#[test]
Expand Down Expand Up @@ -376,30 +376,30 @@ mod tests {
expect!(result.values().flatten()).to(be_empty());
}

#[test]
#[test_log::test]
fn matching_headers_be_true_when_headers_match_by_matcher() {
let context = CoreMatchingContext::new(
let context = HeaderMatchingContext::new(CoreMatchingContext::new(
DiffConfig::AllowUnexpectedKeys,
&matchingrules! {
"header" => {
"HEADER" => [ MatchingRule::Regex("\\w+".to_string()) ]
}
}.rules_for_category("header").unwrap_or_default(), &hashmap!{}
);
));
let mismatches = match_header_value("HEADER", 0, "HEADERX", "HEADERY", &context, true);
expect!(mismatches).to(be_ok());
}

#[test]
fn matching_headers_be_false_when_headers_do_not_match_by_matcher() {
let context = CoreMatchingContext::new(
let context = HeaderMatchingContext::new(CoreMatchingContext::new(
DiffConfig::AllowUnexpectedKeys,
&matchingrules! {
"header" => {
"HEADER" => [ MatchingRule::Regex("\\d+".to_string()) ]
}
}.rules_for_category("header").unwrap_or_default(), &hashmap!{}
);
));
let mismatches = match_header_value(&"HEADER".to_string(), 0,
&"HEADER".to_string(), &"HEADER".to_string(), &context, true);
expect!(mismatches).to(be_err().value(vec![ Mismatch::HeaderMismatch {
Expand Down Expand Up @@ -434,14 +434,14 @@ mod tests {
// Issue #238
#[test_log::test]
fn matching_headers_with_an_indexed_path() {
let context = CoreMatchingContext::new(
let context = HeaderMatchingContext::new(CoreMatchingContext::new(
DiffConfig::AllowUnexpectedKeys,
&matchingrules! {
"header" => {
"HEADER[0]" => [ MatchingRule::Regex("\\w+".to_string()) ]
}
}.rules_for_category("header").unwrap_or_default(), &hashmap!{}
);
));
let mismatches = match_header_value("HEADER", 0, "HEADERX", "HEADERY", &context, true);
expect!(mismatches).to(be_ok());
}
Expand Down Expand Up @@ -622,15 +622,15 @@ mod tests {

#[test_log::test]
fn matching_last_modified_header_with_a_matcher() {
let context = CoreMatchingContext::new(
let context = HeaderMatchingContext::new(CoreMatchingContext::new(
DiffConfig::AllowUnexpectedKeys,
&matchingrules! {
"header" => {
"Last-Modified" => [ MatchingRule::Regex("^[A-Za-z]{3},\\s\\d{2}\\s[A-Za-z]{3}\\s\\d{4}\\s\\d{2}:\\d{2}:\\d{2}\\sGMT$".to_string()) ]
}
}.rules_for_category("header").unwrap_or_default(), &hashmap!{}
);
let expected = hashmap! { "Last-Modified".to_string() => vec!["Sun, 12 Mar 2023 01:21:35 GMT".to_string()] };
));
let expected = hashmap! { "last-modified".to_string() => vec!["Sun, 12 Mar 2023 01:21:35 GMT".to_string()] };
let actual = hashmap! { "Last-Modified".to_string() => vec!["Sun, 12 Mar 2023 01:21:52 GMT".to_string()]};
let result = match_headers(Some(expected), Some(actual), &context);
expect!(result.values().flatten()).to(be_empty());
Expand Down
18 changes: 9 additions & 9 deletions rust/pact_matching/src/json.rs
Expand Up @@ -600,7 +600,7 @@ mod tests {
expect!(result).to(be_ok());

let result = match_json(&val1.clone(), &val2.clone(), &CoreMatchingContext::with_config(DiffConfig::AllowUnexpectedKeys));
expect!(mismatch_message(&result).as_str()).to(be_equal_to("Expected '100' (Number) but received '200' (Number)"));
expect!(mismatch_message(&result).as_str()).to(be_equal_to("Expected 100 (Integer) but received 200 (Integer)"));
expect!(result).to(be_err().value(vec![ Mismatch::BodyMismatch {
path: "$".to_string(),
expected: val1.body.value(),
Expand All @@ -617,7 +617,7 @@ mod tests {
expect!(result).to(be_ok());

let result = match_json(&val1.clone(), &val2.clone(), &CoreMatchingContext::with_config(DiffConfig::AllowUnexpectedKeys));
expect!(mismatch_message(&result).as_str()).to(be_equal_to("Expected '100.01' (Number) but received '100.02' (Number)"));
expect!(mismatch_message(&result).as_str()).to(be_equal_to("Expected 100.01 (Decimal) but received 100.02 (Decimal)"));
expect!(result).to(be_err().value(vec![ Mismatch::BodyMismatch {
path: "$".to_string(),
expected: val1.body.value(),
Expand All @@ -634,7 +634,7 @@ mod tests {
expect!(result).to(be_ok());

let result = match_json(&val1.clone(), &val2.clone(), &CoreMatchingContext::with_config(DiffConfig::AllowUnexpectedKeys));
expect!(mismatch_message(&result).as_str()).to(be_equal_to("Expected 'true' (Boolean) but received 'false' (Boolean)"));
expect!(mismatch_message(&result).as_str()).to(be_equal_to("Expected true (Boolean) but received false (Boolean)"));
expect!(result).to(be_err().value(vec![ Mismatch::BodyMismatch {
path: "$".to_string(),
expected: val1.body.value(),
Expand All @@ -651,7 +651,7 @@ mod tests {
expect!(result).to(be_ok());

let result = match_json(&val1.clone(), &val2.clone(), &CoreMatchingContext::with_config(DiffConfig::AllowUnexpectedKeys));
expect!(mismatch_message(&result).as_str()).to(be_equal_to("Expected '' (Null) but received '33' (Number)"));
expect!(mismatch_message(&result).as_str()).to(be_equal_to("Expected null (Null) but received 33 (Integer)"));
expect!(result).to(be_err().value(vec![ Mismatch::BodyMismatch {
path: "$".to_string(),
expected: val1.clone().body.value(),
Expand Down Expand Up @@ -681,7 +681,7 @@ mod tests {
expect!(result).to(be_err());

let result = match_json(&val2.clone(), &val3.clone(), &CoreMatchingContext::with_config(DiffConfig::AllowUnexpectedKeys));
expect!(mismatch_message(&result)).to(be_equal_to("Expected '22' (Number) but received '44' (Number)".to_string()));
expect!(mismatch_message(&result)).to(be_equal_to("Expected 22 (Integer) but received 44 (Integer)".to_string()));
expect!(result).to(be_err().value(vec![ Mismatch::BodyMismatch { path: "$[1]".to_string(),
expected: Some("22".into()), actual: Some("44".into()), mismatch: "".to_string() } ]));

Expand All @@ -698,7 +698,7 @@ mod tests {
expect!(&mismatch).to(be_equal_to(&Mismatch::BodyMismatch { path: "$[1]".to_string(),
expected: Some("22".into()),
actual: Some("44".into()), mismatch: "".to_string()}));
expect!(mismatch.description()).to(be_equal_to("$[1] -> Expected '22' (Number) but received '44' (Number)".to_string()));
expect!(mismatch.description()).to(be_equal_to("$[1] -> Expected 22 (Integer) but received 44 (Integer)".to_string()));
let mismatch = mismatches[1].clone();
expect!(&mismatch).to(be_equal_to(&Mismatch::BodyMismatch { path: "$".to_string(),
expected: Some("[11,22,33]".into()),
Expand Down Expand Up @@ -736,7 +736,7 @@ mod tests {
expect!(mismatch_message(&result).as_str()).to(be_equal_to("Expected an empty Map but received {\"a\":1,\"b\":2}"));

let result = match_json(&val2.clone(), &val3.clone(), &CoreMatchingContext::with_config(DiffConfig::AllowUnexpectedKeys));
expect!(mismatch_message(&result).as_str()).to(be_equal_to("Expected '2' (Number) but received '3' (Number)"));
expect!(mismatch_message(&result).as_str()).to(be_equal_to("Expected 2 (Integer) but received 3 (Integer)"));
expect!(result).to(be_err().value(vec![ Mismatch::BodyMismatch { path: "$.b".to_string(),
expected: Some("2".into()), actual: Some("3".into()), mismatch: "".to_string() } ]));

Expand All @@ -751,7 +751,7 @@ mod tests {
} ]));

let result = match_json(&val3.clone(), &val4.clone(), &CoreMatchingContext::with_config(DiffConfig::AllowUnexpectedKeys));
expect!(mismatch_message(&result).as_str()).to(be_equal_to("Expected '3' (Number) but received '2' (Number)"));
expect!(mismatch_message(&result).as_str()).to(be_equal_to("Expected 3 (Integer) but received 2 (Integer)"));
expect!(result).to(be_err().value(vec![ Mismatch::BodyMismatch { path: "$.b".to_string(),
expected: Some("3".into()),
actual: Some("2".into()), mismatch: "".to_string() } ]));
Expand All @@ -768,7 +768,7 @@ mod tests {
expect!(&mismatch).to(be_equal_to(&Mismatch::BodyMismatch { path: "$.b".to_string(),
expected: Some("3".into()),
actual: Some("2".into()), mismatch: "".to_string()}));
expect!(mismatch.description()).to(be_equal_to("$.b -> Expected '3' (Number) but received '2' (Number)".to_string()));
expect!(mismatch.description()).to(be_equal_to("$.b -> Expected 3 (Integer) but received 2 (Integer)".to_string()));

let result = match_json(&val4.clone(), &val2.clone(), &CoreMatchingContext::with_config(DiffConfig::AllowUnexpectedKeys));
let mismatches = result.unwrap_err();
Expand Down
95 changes: 89 additions & 6 deletions rust/pact_matching/src/lib.rs
Expand Up @@ -631,6 +631,83 @@ impl MatchingContext for CoreMatchingContext {
}
}

#[derive(Debug, Clone, Default)]
/// Matching context for headers. Keys will be applied in a case-insenstive manor
pub struct HeaderMatchingContext {
inner_context: CoreMatchingContext
}

impl HeaderMatchingContext {
/// Wraps a CoreMatchingContext, downcasing all the matching path keys
pub fn new(context: CoreMatchingContext) -> Self {
HeaderMatchingContext {
inner_context: CoreMatchingContext::new(
context.config,
&MatchingRuleCategory {
name: context.matchers.name,
rules: context.matchers.rules.iter()
.map(|(path, rules)| {
// TODO: Replace this with DocPath.to_lower_case when pact_models 1.1.7 is released
let path = DocPath::new(path.to_string().to_lowercase()).unwrap_or(path.clone());
(path, rules.clone())
})
.collect()
},
&context.plugin_configuration
)
}
}
}

impl MatchingContext for HeaderMatchingContext {
fn matcher_is_defined(&self, path: &DocPath) -> bool {
self.inner_context.matcher_is_defined(path)
}

fn select_best_matcher(&self, path: &DocPath) -> RuleList {
self.inner_context.select_best_matcher(path)
}

fn type_matcher_defined(&self, path: &DocPath) -> bool {
self.inner_context.type_matcher_defined(path)
}

fn values_matcher_defined(&self, path: &DocPath) -> bool {
self.inner_context.values_matcher_defined(path)
}

fn direct_matcher_defined(&self, path: &DocPath, matchers: &HashSet<&str>) -> bool {
self.inner_context.direct_matcher_defined(path, matchers)
}

fn match_keys(&self, path: &DocPath, expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> Result<(), Vec<Mismatch>> {
self.inner_context.match_keys(path, expected, actual)
}

fn plugin_configuration(&self) -> &HashMap<String, PluginInteractionConfig> {
self.inner_context.plugin_configuration()
}

fn matchers(&self) -> &MatchingRuleCategory {
self.inner_context.matchers()
}

fn config(&self) -> DiffConfig {
self.inner_context.config()
}

fn clone_with(&self, matchers: &MatchingRuleCategory) -> Box<dyn MatchingContext> {
Box::new(HeaderMatchingContext::new(
CoreMatchingContext {
matchers: matchers.clone(),
config: self.inner_context.config.clone(),
matching_spec: self.inner_context.matching_spec,
plugin_configuration: self.inner_context.plugin_configuration.clone()
}
))
}
}

lazy_static! {
static ref BODY_MATCHERS: [
(fn(content_type: &ContentType) -> bool,
Expand Down Expand Up @@ -1414,9 +1491,12 @@ pub async fn match_request<'a>(
let query_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
&expected.matching_rules.rules_for_category("query").unwrap_or_default(),
&plugin_data);
let header_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
&expected.matching_rules.rules_for_category("header").unwrap_or_default(),
&plugin_data);
let header_context = HeaderMatchingContext::new(
CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
&expected.matching_rules.rules_for_category("header").unwrap_or_default(),
&plugin_data
)
);
let result = RequestMatchResult {
method: match_method(&expected.method, &actual.method).err(),
path: match_path(&expected.path, &actual.path, &path_context).err(),
Expand Down Expand Up @@ -1470,9 +1550,12 @@ pub async fn match_response<'a>(
let body_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
&expected.matching_rules.rules_for_category("body").unwrap_or_default(),
&plugin_data);
let header_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
&expected.matching_rules.rules_for_category("header").unwrap_or_default(),
&plugin_data);
let header_context = HeaderMatchingContext::new(
CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
&expected.matching_rules.rules_for_category("header").unwrap_or_default(),
&plugin_data
)
);

mismatches.extend_from_slice(match_body(&expected, &actual, &body_context, &header_context).await
.mismatches().as_slice());
Expand Down
4 changes: 2 additions & 2 deletions rust/pact_matching/src/tests.rs
Expand Up @@ -282,8 +282,8 @@ fn match_query_returns_a_mismatch_if_the_values_do_not_match_by_a_matcher() {
expect!(result.iter()).to_not(be_empty());
assert_eq!(result.get("a").unwrap()[0], Mismatch::QueryMismatch {
parameter: "a".to_string(),
expected: "[\"b\"]".to_string(),
actual: "[\"b\"]".to_string(),
expected: "b".to_string(),
actual: "b".to_string(),
mismatch: "Expected 'b' to match '\\d+'".to_string()
});
}
Expand Down
2 changes: 2 additions & 0 deletions rust/pact_mock_server/Cargo.toml
Expand Up @@ -42,4 +42,6 @@ quickcheck = "1.0.3"
expectest = "0.12.0"
reqwest = { version = "0.11.16", default-features = false, features = ["rustls-tls-native-roots", "blocking", "json"] }
env_logger = "0.10.0"
test-log = "0.2.12"
test-env-log = "0.2.8"
tracing-subscriber = "0.3.17"
2 changes: 1 addition & 1 deletion rust/pact_mock_server/src/tests.rs
Expand Up @@ -194,7 +194,7 @@ async fn match_request_supports_v2_matchers_with_xml() {
MatchResult::RequestMatch(interaction.request, interaction.response, request.clone())));
}

#[test]
#[test_log::test]
fn match_request_with_header_with_multiple_values() {
let pact = V4Pact {
interactions: vec![
Expand Down

0 comments on commit 445ea1e

Please sign in to comment.