Skip to content

Commit

Permalink
fix(pact_matching): EachValue matcher was not applying the associated…
Browse files Browse the repository at this point in the history
… matching rules correctly #299
  • Loading branch information
rholshausen committed Jul 12, 2023
1 parent 64a862e commit 4f448b1
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 40 deletions.
8 changes: 0 additions & 8 deletions rust/pact_consumer/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,28 +297,20 @@ async fn post_json_with_incorrect_content_type() {
// Issue #300
#[test_log::test(tokio::test)]
async fn multi_value_headers() {
// Define the Pact for the test, specify the names of the consuming
// application and the provider application.
let alice_service = PactBuilder::new_v4("Consumer", "Alice Service")
// Start a new interaction. We can add as many interactions as we want.
.interaction("a retrieve Mallory request", "", |mut i| {
// Defines a provider state. It is optional.
i.given("there is some good mallory");
// Define the request, a GET (default) request to '/mallory'.
i.request.path("/mallory");
i.request.header("accept", "application/problem+json, application/json, text/plain, */*");
// Define the response we want returned.
i.response
.ok()
.content_type("text/plain")
.body("That is some good Mallory.");

// Return the interaction back to the pact framework
i.clone()
})
.start_mock_server(None);

// You would use your actual client code here.
let mallory_url = alice_service.path("/mallory");
let client = reqwest::Client::new();
let response = client.get(mallory_url)
Expand Down
64 changes: 64 additions & 0 deletions rust/pact_ffi/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use pretty_assertions::assert_eq;
use reqwest::blocking::Client;
use reqwest::header::CONTENT_TYPE;
use tempfile::TempDir;
use serde_json::json;

#[allow(deprecated)]
use pact_ffi::mock_server::{
Expand Down Expand Up @@ -482,3 +483,66 @@ fn test_missing_plugin() {
expect!(result).to(be_equal_to(2));
expect!(output.to_string_lossy().contains("Verification execution failed: Plugin missing-csv:0.0 was not found")).to(be_true());
}

// Issue #299
#[test_log::test]
#[allow(deprecated)]
fn each_value_matcher() {
let consumer_name = CString::new("each_value_matcher-consumer").unwrap();
let provider_name = CString::new("each_value_matcher-provider").unwrap();
let pact_handle = pactffi_new_pact(consumer_name.as_ptr(), provider_name.as_ptr());
let description = CString::new("each_value_matcher").unwrap();
let interaction = pactffi_new_interaction(pact_handle.clone(), description.as_ptr());

let content_type = CString::new("application/json").unwrap();
let path = CString::new("/book").unwrap();
let json = json!({
"pact:matcher:type": "each-value",
"value": {
"id1": "book1"
},
"rules": [
{
"pact:matcher:type": "regex",
"regex": "\\w+\\d+"
}
]
});
let body = CString::new(json.to_string()).unwrap();
let address = CString::new("127.0.0.1:0").unwrap();
let method = CString::new("PUT").unwrap();

pactffi_upon_receiving(interaction.clone(), description.as_ptr());
pactffi_with_request(interaction.clone(), method.as_ptr(), path.as_ptr());
pactffi_with_body(interaction.clone(), InteractionPart::Request, content_type.as_ptr(), body.as_ptr());
pactffi_response_status(interaction.clone(), 200);

let port = pactffi_create_mock_server_for_pact(pact_handle.clone(), address.as_ptr(), false);

expect!(port).to(be_greater_than(0));

let client = Client::default();
let result = client.put(format!("http://127.0.0.1:{}/book", port).as_str())
.header("Content-Type", "application/json")
.body(r#"{"id1": "book100", "id2": "book2"}"#)
.send();

match result {
Ok(res) => {
expect!(res.status()).to(be_eq(200));
},
Err(err) => {
panic!("expected 200 response but request failed: {}", err);
}
};

let mismatches = unsafe {
CStr::from_ptr(pactffi_mock_server_mismatches(port)).to_string_lossy().into_owned()
};

expect!(mismatches).to(be_equal_to("[]"));

let file_path = CString::new("/tmp/pact").unwrap();
pactffi_write_pact_file(port, file_path.as_ptr(), true);
pactffi_cleanup_mock_server(port);
}
4 changes: 2 additions & 2 deletions rust/pact_matching/src/generators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ pub async fn generators_process_body(
pub(crate) fn find_matching_variant<T>(
value: &T,
variants: &[(usize, MatchingRuleCategory, HashMap<DocPath, Generator>)],
callback: &dyn Fn(&DocPath, &T, &dyn MatchingContext) -> bool
callback: &dyn Fn(&DocPath, &T, &(dyn MatchingContext + Send + Sync)) -> bool
) -> Option<(usize, HashMap<DocPath, Generator>)>
where T: Clone + std::fmt::Debug {
let result = variants.iter()
Expand All @@ -103,7 +103,7 @@ impl VariantMatcher for DefaultVariantMatcher {
value: &Value,
variants: &Vec<(usize, MatchingRuleCategory, HashMap<DocPath, Generator>)>
) -> Option<(usize, HashMap<DocPath, Generator>)> {
let callback = |path: &DocPath, value: &Value, context: &dyn MatchingContext| {
let callback = |path: &DocPath, value: &Value, context: &(dyn MatchingContext + Send + Sync)| {
compare_json(path, value, value, context).is_ok()
};
find_matching_variant(value, variants, &callback)
Expand Down
54 changes: 47 additions & 7 deletions rust/pact_matching/src/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,12 @@ pub fn display_diff(expected: &str, actual: &str, path: &str, indent: &str) -> S
}

/// Compares the actual JSON to the expected one
pub fn compare_json(path: &DocPath, expected: &Value, actual: &Value, context: &dyn MatchingContext) -> Result<(), Vec<Mismatch>> {
pub fn compare_json(
path: &DocPath,
expected: &Value,
actual: &Value,
context: &(dyn MatchingContext + Send + Sync)
) -> Result<(), Vec<Mismatch>> {
debug!("compare: Comparing path {}", path);
match (expected, actual) {
(&Value::Object(ref emap), &Value::Object(ref amap)) => compare_maps(path, emap, amap, context),
Expand Down Expand Up @@ -383,7 +388,7 @@ fn compare_maps(
path: &DocPath,
expected: &serde_json::Map<String, Value>,
actual: &serde_json::Map<String, Value>,
context: &dyn MatchingContext
context: &(dyn MatchingContext + Send + Sync)
) -> Result<(), Vec<Mismatch>> {
let spath = path.to_string();
debug!("compare_maps: Comparing maps at {}: {:?} -> {:?}", spath, expected, actual);
Expand All @@ -404,9 +409,10 @@ fn compare_maps(
debug!("compare_maps: Matcher is defined for path {}", path);
let rule_list = context.select_best_matcher(path);
for matcher in rule_list.rules {
result = merge_result(result,compare_maps_with_matchingrule(&matcher, rule_list.cascaded, path, &expected, &actual, context, &mut |p, expected, actual| {
let result1 = compare_maps_with_matchingrule(&matcher, rule_list.cascaded, path, &expected, &actual, context, &mut |p, expected, actual, context| {
compare_json(p, expected, actual, context)
}));
});
result = merge_result(result, result1);
}
} else {
let expected_keys = expected.keys().cloned().collect();
Expand All @@ -427,7 +433,7 @@ fn compare_lists(
path: &DocPath,
expected: &[Value],
actual: &[Value],
context: &dyn MatchingContext
context: &(dyn MatchingContext + Send + Sync)
) -> Result<(), Vec<Mismatch>> {
let spath = path.to_string();
if context.matcher_is_defined(path) {
Expand Down Expand Up @@ -468,7 +474,7 @@ fn compare_list_content(
path: &DocPath,
expected: &[Value],
actual: &[Value],
context: &dyn MatchingContext
context: &(dyn MatchingContext + Send + Sync)
) -> Result<(), Vec<Mismatch>> {
let mut result = Ok(());
for (index, value) in expected.iter().enumerate() {
Expand All @@ -492,7 +498,7 @@ fn compare_values(
path: &DocPath,
expected: &Value,
actual: &Value,
context: &dyn MatchingContext
context: &(dyn MatchingContext + Send + Sync)
) -> Result<(), Vec<Mismatch>> {
let matcher_result = if context.matcher_is_defined(path) {
debug!("compare_values: Calling match_values for path {}", path);
Expand Down Expand Up @@ -524,6 +530,7 @@ mod tests {
use pact_models::{matchingrules, matchingrules_list};
use pact_models::bodies::OptionalBody;
use pact_models::matchingrules::{MatchingRule, MatchingRuleCategory};
use pact_models::matchingrules::expressions::{MatchingRuleDefinition, ValueType};
use pact_models::request::Request;

use crate::{CoreMatchingContext, DiffConfig};
Expand Down Expand Up @@ -1177,4 +1184,37 @@ mod tests {
let result = compare_maps(&DocPath::root(), expected, actual, &context);
expect!(result).to(be_err());
}

#[test_log::test]
fn compare_maps_with_each_value_matcher() {
let expected_json = json!({
"id1": "book1"
});
let expected = expected_json.as_object().unwrap();
let actual_json = json!({
"id1001": "book1100",
"id2": "book2"
});
let actual = actual_json.as_object().unwrap();

let matchingrules = matchingrules_list! {
"body"; "$" => [
MatchingRule::EachValue(MatchingRuleDefinition::new("{\"id1\":\"book1\"}".to_string(),
ValueType::Unknown, MatchingRule::Regex("\\w+\\d+".to_string()), None))
]
};

let context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
&matchingrules, &hashmap!{});
let result = compare_maps(&DocPath::root(), expected, actual, &context);
expect!(result).to(be_ok());

let invalid_json = json!({
"id1001": "book1100",
"id2": 1
});
let invalid = invalid_json.as_object().unwrap();
let result = compare_maps(&DocPath::root(), expected, invalid, &context);
expect!(result).to(be_err());
}
}
Loading

0 comments on commit 4f448b1

Please sign in to comment.