From f72f81913091d2c49c38b14dd467f96d2378c3af Mon Sep 17 00:00:00 2001 From: Ronald Holshausen Date: Thu, 18 May 2023 14:12:40 +1000 Subject: [PATCH] feat: Implemented the remaining V1 HTTP consumer compatability suite feature --- compatibility-suite/Cargo.lock | 1 + compatibility-suite/Cargo.toml | 1 + compatibility-suite/tests/v1.rs | 260 ++++++++++++++++++++++++++++-- rust/Cargo.lock | 1 + rust/pact_ffi/Cargo.toml | 1 + rust/pact_ffi/tests/tests.rs | 4 +- rust/pact_matching/src/headers.rs | 8 +- rust/pact_matching/src/json.rs | 53 +++--- rust/pact_matching/src/lib.rs | 12 +- 9 files changed, 295 insertions(+), 46 deletions(-) diff --git a/compatibility-suite/Cargo.lock b/compatibility-suite/Cargo.lock index eef180053..7c54c33bc 100644 --- a/compatibility-suite/Cargo.lock +++ b/compatibility-suite/Cargo.lock @@ -473,6 +473,7 @@ dependencies = [ "bytes", "cucumber", "futures", + "pact_matching", "pact_mock_server", "pact_models", "pact_verifier", diff --git a/compatibility-suite/Cargo.toml b/compatibility-suite/Cargo.toml index 7ae853988..5c13053b4 100644 --- a/compatibility-suite/Cargo.toml +++ b/compatibility-suite/Cargo.toml @@ -9,6 +9,7 @@ bytes = "1.4.0" cucumber = "0.19" futures = "0.3" pact_models = "1.1.2" +pact_matching = { version = "1.0.8", path = "../rust/pact_matching" } pact_mock_server = { version = "1.0.2", path = "../rust/pact_mock_server" } pact_verifier = { version = "0.15.3", path = "../rust/pact_verifier" } reqwest = { version = "0.11.17", features = ["rustls-tls-native-roots", "json"] } diff --git a/compatibility-suite/tests/v1.rs b/compatibility-suite/tests/v1.rs index 167c0a4b0..5817c6c9e 100644 --- a/compatibility-suite/tests/v1.rs +++ b/compatibility-suite/tests/v1.rs @@ -12,7 +12,8 @@ use cucumber::{given, then, when, World, Parameter}; use cucumber::gherkin::Step; use pact_models::{Consumer, PactSpecification, Provider}; use pact_models::bodies::OptionalBody; -use pact_models::content_types::ContentType; +use pact_models::content_types::{ContentType, JSON, XML}; +use pact_models::headers::parse_header; use pact_models::http_parts::HttpPart; use pact_models::pact::{Pact, read_pact}; use pact_models::query_strings::parse_query_string; @@ -21,6 +22,7 @@ use pact_models::sync_pact::RequestResponsePact; use pact_models::v4::http_parts::HttpResponse; use serde_json::Value; use uuid::Uuid; +use pact_matching::Mismatch; use pact_mock_server::matching::MatchResult; use pact_mock_server::mock_server::{MockServer, MockServerConfig}; @@ -98,6 +100,56 @@ fn the_following_http_interactions_have_been_setup(world: &mut CompatibilitySuit } } + if let Some(index) = headers.get("headers") { + if let Some(headers) = values.get(*index) { + if !headers.is_empty() { + let headers = headers.split(",") + .map(|header| { + let key_value = header.strip_prefix("'").unwrap_or(header) + .strip_suffix("'").unwrap_or(header) + .splitn(2, ":") + .map(|v| v.trim()) + .collect::>(); + (key_value[0].to_string(), parse_header(key_value[0], key_value[1])) + }).collect(); + interaction.request.headers = Some(headers); + } + } + } + + if let Some(index) = headers.get("body") { + if let Some(body) = values.get(*index) { + if !body.is_empty() { + if body.starts_with("JSON:") { + interaction.request.add_header("content-type", vec!["application/json"]); + interaction.request.body = OptionalBody::Present(Bytes::from(body.strip_prefix("JSON:").unwrap_or(body).to_string()), + Some(JSON.clone()), None); + } else if body.starts_with("XML:") { + interaction.request.add_header("content-type", vec!["application/xml"]); + interaction.request.body = OptionalBody::Present(Bytes::from(body.strip_prefix("XML:").unwrap_or(body).to_string()), + Some(XML.clone()), None); + } else { + let ct = if body.ends_with(".json") { + "application/json" + } else if body.ends_with(".xml") { + "application/xml" + } else { + "text/plain" + }; + interaction.request.headers_mut().insert("content-type".to_string(), vec![ct.to_string()]); + + let mut f = File::open(format!("pact-compatibility-suite/fixtures/{}", body)) + .expect(format!("could not load fixture '{}'", body).as_str()); + let mut buffer = Vec::new(); + f.read_to_end(&mut buffer) + .expect(format!("could not read fixture '{}'", body).as_str()); + interaction.request.body = OptionalBody::Present(Bytes::from(buffer), + ContentType::parse(ct).ok(), None); + } + } + } + } + if let Some(index) = headers.get("response") { if let Some(response) = values.get(*index) { interaction.response.status = response.parse().unwrap(); @@ -223,6 +275,46 @@ async fn request_is_made_to_the_mock_server_with_the_following_changes( "method" => request.method = value.clone(), "path" => request.path = value.clone(), "query" => request.query = parse_query_string(value), + "headers" => { + let headers = value.split(",") + .map(|header| { + let key_value = header.strip_prefix("'").unwrap_or(header) + .strip_suffix("'").unwrap_or(header) + .splitn(2, ":") + .map(|v| v.trim()) + .collect::>(); + (key_value[0].to_string(), parse_header(key_value[0], key_value[1])) + }).collect(); + request.headers = Some(headers); + }, + "body" => { + if value.starts_with("JSON:") { + request.add_header("content-type", vec!["application/json"]); + request.body = OptionalBody::Present(Bytes::from(value.strip_prefix("JSON:").unwrap_or(value).to_string()), + Some(JSON.clone()), None); + } else if value.starts_with("XML:") { + request.add_header("content-type", vec!["application/xml"]); + request.body = OptionalBody::Present(Bytes::from(value.strip_prefix("XML:").unwrap_or(value).to_string()), + Some(XML.clone()), None); + } else { + let ct = if value.ends_with(".json") { + "application/json" + } else if value.ends_with(".xml") { + "application/xml" + } else { + "text/plain" + }; + request.headers_mut().insert("content-type".to_string(), vec![ct.to_string()]); + + let mut f = File::open(format!("pact-compatibility-suite/fixtures/{}", value)) + .expect(format!("could not load fixture '{}'", value).as_str()); + let mut buffer = Vec::new(); + f.read_to_end(&mut buffer) + .expect(format!("could not read fixture '{}'", value).as_str()); + request.body = OptionalBody::Present(Bytes::from(buffer), + ContentType::parse(ct).ok(), None); + } + }, _ => {} } } @@ -394,13 +486,20 @@ fn the_interaction_request_will_be_for_a(world: &mut CompatibilitySuiteWorld, nu fn the_interaction_response_will_contain_the_document(world: &mut CompatibilitySuiteWorld, num: IndexType, fixture: String) -> anyhow::Result<()> { if let Some(interaction) = world.pact.interactions().get(num.0) { if let Some(reqres) = interaction.as_request_response() { - let mut fixture = File::open(format!("pact-compatibility-suite/fixtures/{}", fixture))?; + let mut fixture_file = File::open(format!("pact-compatibility-suite/fixtures/{}", fixture))?; let mut buffer = Vec::new(); - fixture.read_to_end(&mut buffer)?; - let json: Value = serde_json::from_slice(&buffer)?; - let json_str = json.to_string(); + fixture_file.read_to_end(&mut buffer)?; + + let mut expected = Vec::new(); + if fixture.ends_with(".json") { + let json: Value = serde_json::from_slice(&buffer)?; + let string = json.to_string(); + expected.extend_from_slice(string.as_bytes()); + } else { + expected.extend_from_slice(&buffer); + } let actual_body = reqres.response.body.value().unwrap_or_default(); - if &actual_body == json_str.as_bytes() { + if &actual_body == expected.as_slice() { Ok(()) } else { let body = OptionalBody::Present(Bytes::from(buffer), None, None); @@ -481,23 +580,30 @@ fn the_mismatches_will_contain_a_mismatch_with_error( _ => vec![] }) .collect(); - if mismatches.iter().find(|ms| ms.mismatch_type().to_lowercase().starts_with(mismatch_type.as_str()) && ms.description() == error).is_some() { + if mismatches.iter().find(|ms| { + let correct_type = match ms { + Mismatch::BodyTypeMismatch { .. } => mismatch_type == "body-content-type", + _ => ms.mismatch_type().to_lowercase().starts_with(mismatch_type.as_str()) + }; + correct_type && ms.description() == error + }).is_some() { Ok(()) } else { Err(anyhow!("Did not find a {} mismatch with error {}", mismatch_type, error)) } } -#[then(expr = "the mock server status will be an unexpected request received error for interaction \\{{int}}")] +#[then(expr = "the mock server status will be an unexpected {string} request received error for interaction \\{{int}}")] fn the_mock_server_status_will_be_an_unexpected_request_received_error_for_interaction( world: &mut CompatibilitySuiteWorld, + method: String, num: usize ) -> anyhow::Result<()> { let mock_server = { world.mock_server.lock().unwrap().clone() }; if let Some(interaction) = world.interactions.get(num - 1) { if let Some(_) = mock_server.mismatches().iter().find(|mismatch| { match mismatch { - MatchResult::RequestNotFound(request) => request.method == interaction.request.method && + MatchResult::RequestNotFound(request) => request.method == method && request.path == interaction.request.path && request.query == interaction.request.query, _ => false } @@ -511,6 +617,142 @@ fn the_mock_server_status_will_be_an_unexpected_request_received_error_for_inter } } +#[then(expr = "the mock server status will be an unexpected {string} request received error for path {string}")] +fn the_mock_server_status_will_be_an_unexpected_request_received_error( + world: &mut CompatibilitySuiteWorld, + method: String, + path: String +) -> anyhow::Result<()> { + let mock_server = { world.mock_server.lock().unwrap().clone() }; + if let Some(_) = mock_server.mismatches().iter().find(|mismatch| { + match mismatch { + MatchResult::RequestNotFound(request) => request.method == method && + request.path == path, + _ => false + } + }) { + Ok(()) + } else { + Err(anyhow!("Did not find a RequestNotFound mismatch for path {}", path)) + } +} + +#[then(expr = "the \\{{numType}} interaction request will contain the header {string} with value {string}")] +fn the_interaction_request_will_contain_the_header_with_value( + world: &mut CompatibilitySuiteWorld, + num: IndexType, + key: String, + value: String +) -> anyhow::Result<()> { + if let Some(interaction) = world.pact.interactions().get(num.0) { + if let Some(reqres) = interaction.as_request_response() { + if let Some(header_value) = reqres.request.lookup_header_value(&key) { + if header_value == value { + Ok(()) + } else { + Err(anyhow!("Expected interaction {} request to have a header {} with value {} but got {}", num.0 + 1, key, value, header_value)) + } + } else { + Err(anyhow!("Expected interaction {} request to have a header {} with value {}", num.0 + 1, key, value)) + } + } else { + Err(anyhow!("Interaction {} is not a RequestResponseInteraction", num.0 + 1)) + } + } else { + Err(anyhow!("Did not find interaction {} in the Pact", num.0 + 1)) + } +} + +#[then(expr = "the \\{{numType}} interaction request content type will be {string}")] +fn the_interaction_request_content_type_will_be( + world: &mut CompatibilitySuiteWorld, + num: IndexType, + content_type: String +) -> anyhow::Result<()> { + if let Some(interaction) = world.pact.interactions().get(num.0) { + if let Some(reqres) = interaction.as_request_response() { + if let Some(ct) = reqres.request.content_type() { + if ct.to_string() == content_type { + Ok(()) + } else { + Err(anyhow!("Expected interaction {} request to have a content type of {} but got {}", num.0 + 1, content_type, ct)) + } + } else { + Err(anyhow!("Interaction {} request does not have a content type set", num.0 + 1)) + } + } else { + Err(anyhow!("Interaction {} is not a RequestResponseInteraction", num.0 + 1)) + } + } else { + Err(anyhow!("Did not find interaction {} in the Pact", num.0 + 1)) + } +} + +#[then(expr = "the \\{{numType}} interaction request will contain the {string} document")] +fn the_interaction_request_will_contain_the_document( + world: &mut CompatibilitySuiteWorld, + num: IndexType, + fixture: String, +) -> anyhow::Result<()> { + if let Some(interaction) = world.pact.interactions().get(num.0) { + if let Some(reqres) = interaction.as_request_response() { + let mut fixture_file = File::open(format!("pact-compatibility-suite/fixtures/{}", fixture))?; + let mut buffer = Vec::new(); + fixture_file.read_to_end(&mut buffer)?; + + let mut expected = Vec::new(); + if fixture.ends_with(".json") { + let json: Value = serde_json::from_slice(&buffer)?; + let string = json.to_string(); + expected.extend_from_slice(string.as_bytes()); + } else { + expected.extend_from_slice(&buffer); + } + let actual_body = reqres.request.body.value().unwrap_or_default(); + if &actual_body == expected.as_slice() { + Ok(()) + } else { + let body = OptionalBody::Present(Bytes::from(buffer), None, None); + Err(anyhow!("Expected Interaction {} request with body {} but got {}", num.0 + 1, + reqres.request.body.display_string(), body.display_string())) + } + } else { + Err(anyhow!("Interaction {} is not a RequestResponseInteraction", num.0 + 1)) + } + } else { + Err(anyhow!("Did not find interaction {} in the Pact", num.0 + 1)) + } +} + +#[then(expr = "the mismatches will contain a {string} mismatch with path {string} with error {string}")] +fn the_mismatches_will_contain_a_mismatch_with_path_with_error( + world: &mut CompatibilitySuiteWorld, + mismatch_type: String, + error_path: String, + error: String +) -> anyhow::Result<()> { + let mock_server = world.mock_server.lock().unwrap(); + let mismatches: Vec<_> = mock_server.mismatches().iter() + .flat_map(|m| match m { + MatchResult::RequestMismatch(_, mismatches) => mismatches.clone(), + _ => vec![] + }) + .collect(); + if mismatches.iter().find(|ms| { + let correct_type = match ms { + Mismatch::QueryMismatch { parameter, .. } => mismatch_type == "query" && parameter == &error_path, + Mismatch::HeaderMismatch { key, .. } => mismatch_type == "header" && key == &error_path, + Mismatch::BodyMismatch { path, .. } => mismatch_type == "body" && path == &error_path, + _ => false + }; + correct_type && ms.description().contains(&error) + }).is_some() { + Ok(()) + } else { + Err(anyhow!("Did not find a {} mismatch for path {} with error {}", mismatch_type, error_path, error)) + } +} + #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 86b25108e..22ecfbde0 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1924,6 +1924,7 @@ dependencies = [ "pact_models 1.1.2", "pact_verifier", "panic-message", + "pretty_assertions", "quickcheck", "rand", "rand_regex", diff --git a/rust/pact_ffi/Cargo.toml b/rust/pact_ffi/Cargo.toml index 591a85458..cdc1846ce 100644 --- a/rust/pact_ffi/Cargo.toml +++ b/rust/pact_ffi/Cargo.toml @@ -59,6 +59,7 @@ quickcheck = "1.0.3" test-log = "0.2.11" tempfile = "3.5.0" home = "0.5.4" +pretty_assertions = "1.3.0" [lib] crate-type = ["cdylib", "staticlib", "rlib"] diff --git a/rust/pact_ffi/tests/tests.rs b/rust/pact_ffi/tests/tests.rs index ecc517f83..6eb2b7857 100644 --- a/rust/pact_ffi/tests/tests.rs +++ b/rust/pact_ffi/tests/tests.rs @@ -11,6 +11,7 @@ use libc::c_char; use maplit::*; use reqwest::blocking::Client; use reqwest::header::CONTENT_TYPE; +use pretty_assertions::assert_eq; #[allow(deprecated)] use pact_ffi::mock_server::{ @@ -67,7 +68,8 @@ fn post_to_mock_server_with_mismatches() { pactffi_cleanup_mock_server(port); - expect!(mismatches).to(be_equal_to("[{\"method\":\"POST\",\"mismatches\":[{\"actual\":\"\\\"no-very-bar\\\"\",\"expected\":\"\\\"bar\\\"\",\"mismatch\":\"Expected \'bar\' to be equal to \'no-very-bar\'\",\"path\":\"$.foo\",\"type\":\"BodyMismatch\"}],\"path\":\"/path\",\"type\":\"request-mismatch\"}]")); + assert_eq!("[{\"method\":\"POST\",\"mismatches\":[{\"actual\":\"\\\"no-very-bar\\\"\",\"expected\":\"\\\"bar\\\"\",\"mismatch\":\"Expected 'bar' (String) but received 'no-very-bar' (String)\",\"path\":\"$.foo\",\"type\":\"BodyMismatch\"}],\"path\":\"/path\",\"type\":\"request-mismatch\"}]", + mismatches); } #[test] diff --git a/rust/pact_matching/src/headers.rs b/rust/pact_matching/src/headers.rs index 91920d9cf..8435cfc92 100644 --- a/rust/pact_matching/src/headers.rs +++ b/rust/pact_matching/src/headers.rs @@ -165,7 +165,7 @@ fn match_header_maps( result.insert(key.clone(), vec![Mismatch::HeaderMismatch { key: key.clone(), expected: format!("{:?}", value.join(", ")), actual: "".to_string(), - mismatch: format!("Expected header '{}' but was missing", key) }]); + mismatch: format!("Expected a header '{}' but was missing", key) }]); } } } @@ -185,7 +185,7 @@ pub fn match_headers( (key.clone(), vec![Mismatch::HeaderMismatch { key: key.clone(), expected: format!("{:?}", value.join(", ")), actual: "".to_string(), - mismatch: format!("Expected header '{}' but was missing", key) }]) + mismatch: format!("Expected a header '{}' but was missing", key) }]) }).collect(), (None, None) => hashmap!{} } @@ -492,7 +492,7 @@ mod tests { key: "a".to_string(), expected: "\"b\"".to_string(), actual: "".to_string(), - mismatch: "Expected header 'a' but was missing".to_string() + mismatch: "Expected a header 'a' but was missing".to_string() }); } @@ -512,7 +512,7 @@ mod tests { key: "a".to_string(), expected: "\"b\"".to_string(), actual: "".to_string(), - mismatch: "Expected header 'a' but was missing".to_string(), + mismatch: "Expected a header 'a' but was missing".to_string(), }); } diff --git a/rust/pact_matching/src/json.rs b/rust/pact_matching/src/json.rs index c809666c8..46c7b5ac8 100644 --- a/rust/pact_matching/src/json.rs +++ b/rust/pact_matching/src/json.rs @@ -138,7 +138,8 @@ impl Matches<&Value> for Value { if self == actual { Ok(()) } else { - Err(anyhow!("Expected '{}' to be equal to '{}'", json_to_string(self), json_to_string(actual))) + Err(anyhow!("Expected '{}' ({}) but received '{}' ({})", + json_to_string(self), type_of(self), json_to_string(actual), type_of(actual))) } }, MatchingRule::Null => match actual { @@ -563,12 +564,12 @@ mod tests { expect!(result).to(be_ok()); let result = match_json(&val1.clone(), &val2.clone(), &CoreMatchingContext::with_config(DiffConfig::AllowUnexpectedKeys)); - expect!(mismatch_message(&result)).to(be_equal_to(s!("Expected 'string value' to be equal to 'other value'"))); + expect!(mismatch_message(&result).as_str()).to(be_equal_to("Expected 'string value' (String) but received 'other value' (String)")); expect!(result).to(be_err().value(vec![ Mismatch::BodyMismatch { - path: s!("$"), + path: "$".to_string(), expected: val1.body.value(), actual: val2.body.value(), - mismatch: s!("") + mismatch: "".to_string() } ])); } @@ -580,12 +581,12 @@ mod tests { expect!(result).to(be_ok()); let result = match_json(&val1.clone(), &val2.clone(), &CoreMatchingContext::with_config(DiffConfig::AllowUnexpectedKeys)); - expect!(mismatch_message(&result)).to(be_equal_to(s!("Expected '100' to be equal to '200'"))); + expect!(mismatch_message(&result).as_str()).to(be_equal_to("Expected '100' (Number) but received '200' (Number)")); expect!(result).to(be_err().value(vec![ Mismatch::BodyMismatch { - path: s!("$"), + path: "$".to_string(), expected: val1.body.value(), actual: val2.body.value(), - mismatch: s!("") + mismatch: "".to_string() } ])); } @@ -597,12 +598,12 @@ mod tests { expect!(result).to(be_ok()); let result = match_json(&val1.clone(), &val2.clone(), &CoreMatchingContext::with_config(DiffConfig::AllowUnexpectedKeys)); - expect!(mismatch_message(&result)).to(be_equal_to(s!("Expected '100.01' to be equal to '100.02'"))); + expect!(mismatch_message(&result).as_str()).to(be_equal_to("Expected '100.01' (Number) but received '100.02' (Number)")); expect!(result).to(be_err().value(vec![ Mismatch::BodyMismatch { - path: s!("$"), + path: "$".to_string(), expected: val1.body.value(), actual: val2.body.value(), - mismatch: s!("") + mismatch: "".to_string() } ])); } @@ -614,12 +615,12 @@ mod tests { expect!(result).to(be_ok()); let result = match_json(&val1.clone(), &val2.clone(), &CoreMatchingContext::with_config(DiffConfig::AllowUnexpectedKeys)); - expect!(mismatch_message(&result)).to(be_equal_to(s!("Expected 'true' to be equal to 'false'"))); + 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: s!("$"), + path: "$".to_string(), expected: val1.body.value(), actual: val2.body.value(), - mismatch: s!("") + mismatch: "".to_string() } ])); } @@ -631,12 +632,12 @@ mod tests { expect!(result).to(be_ok()); let result = match_json(&val1.clone(), &val2.clone(), &CoreMatchingContext::with_config(DiffConfig::AllowUnexpectedKeys)); - expect!(mismatch_message(&result)).to(be_equal_to(s!("Expected '' to be equal to '33'"))); + expect!(mismatch_message(&result).as_str()).to(be_equal_to("Expected '' (Null) but received '33' (Number)")); expect!(result).to(be_err().value(vec![ Mismatch::BodyMismatch { - path: s!("$"), + path: "$".to_string(), expected: val1.clone().body.value(), actual: val2.clone().body.value(), - mismatch: s!("") + mismatch: "".to_string() } ])); } @@ -661,7 +662,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' to be equal to '44'".to_string())); + expect!(mismatch_message(&result)).to(be_equal_to("Expected '22' (Number) but received '44' (Number)".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() } ])); @@ -678,7 +679,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' to be equal to '44'".to_string())); + expect!(mismatch.description()).to(be_equal_to("$[1] -> Expected '22' (Number) but received '44' (Number)".to_string())); let mismatch = mismatches[1].clone(); expect!(&mismatch).to(be_equal_to(&Mismatch::BodyMismatch { path: "$".to_string(), expected: Some("[11,22,33]".into()), @@ -713,12 +714,12 @@ mod tests { expect!(result).to(be_ok()); let result = match_json(&val1.clone(), &val2.clone(), &CoreMatchingContext::with_config(DiffConfig::NoUnexpectedKeys)); - expect!(mismatch_message(&result)).to(be_equal_to(s!("Expected an empty Map but received {\"a\":1,\"b\":2}"))); + 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)).to(be_equal_to(s!("Expected '2' to be equal to '3'"))); - expect!(result).to(be_err().value(vec![ Mismatch::BodyMismatch { path: s!("$.b"), - expected: Some("2".into()), actual: Some("3".into()), mismatch: s!("") } ])); + expect!(mismatch_message(&result).as_str()).to(be_equal_to("Expected '2' (Number) but received '3' (Number)")); + expect!(result).to(be_err().value(vec![ Mismatch::BodyMismatch { path: "$.b".to_string(), + expected: Some("2".into()), actual: Some("3".into()), mismatch: "".to_string() } ])); let result = match_json(&val2.clone(), &val4.clone(), &CoreMatchingContext::with_config(DiffConfig::AllowUnexpectedKeys)); expect!(result).to(be_ok()); @@ -731,10 +732,10 @@ mod tests { } ])); let result = match_json(&val3.clone(), &val4.clone(), &CoreMatchingContext::with_config(DiffConfig::AllowUnexpectedKeys)); - expect!(mismatch_message(&result)).to(be_equal_to(s!("Expected '3' to be equal to '2'"))); - expect!(result).to(be_err().value(vec![ Mismatch::BodyMismatch { path: s!("$.b"), + expect!(mismatch_message(&result).as_str()).to(be_equal_to("Expected '3' (Number) but received '2' (Number)")); + expect!(result).to(be_err().value(vec![ Mismatch::BodyMismatch { path: "$.b".to_string(), expected: Some("3".into()), - actual: Some("2".into()), mismatch: s!("") } ])); + actual: Some("2".into()), mismatch: "".to_string() } ])); let result = match_json(&val3.clone(), &val4.clone(), &CoreMatchingContext::with_config(DiffConfig::NoUnexpectedKeys)); let mismatches = result.unwrap_err(); @@ -748,7 +749,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' to be equal to '2'".to_string())); + expect!(mismatch.description()).to(be_equal_to("$.b -> Expected '3' (Number) but received '2' (Number)".to_string())); let result = match_json(&val4.clone(), &val2.clone(), &CoreMatchingContext::with_config(DiffConfig::AllowUnexpectedKeys)); let mismatches = result.unwrap_err(); diff --git a/rust/pact_matching/src/lib.rs b/rust/pact_matching/src/lib.rs index ebd07213c..c98aabf4d 100644 --- a/rust/pact_matching/src/lib.rs +++ b/rust/pact_matching/src/lib.rs @@ -848,9 +848,9 @@ impl Mismatch { Mismatch::StatusMismatch { expected: ref e, .. } => format!("has status code {}", e), Mismatch::QueryMismatch { ref parameter, expected: ref e, .. } => format!("includes parameter '{}' with value '{}'", parameter, e), Mismatch::HeaderMismatch { ref key, expected: ref e, .. } => format!("includes header '{}' with value '{}'", key, e), - Mismatch::BodyTypeMismatch { .. } => s!("has a matching body"), - Mismatch::BodyMismatch { .. } => s!("has a matching body"), - Mismatch::MetadataMismatch { .. } => s!("has matching metadata") + Mismatch::BodyTypeMismatch { .. } => "has a matching body".to_string(), + Mismatch::BodyMismatch { .. } => "has a matching body".to_string(), + Mismatch::MetadataMismatch { .. } => "has matching metadata".to_string() } } @@ -862,7 +862,7 @@ impl Mismatch { Mismatch::StatusMismatch { mismatch, .. } => mismatch.clone(), Mismatch::QueryMismatch { mismatch, .. } => mismatch.clone(), Mismatch::HeaderMismatch { mismatch, .. } => mismatch.clone(), - Mismatch::BodyTypeMismatch { expected: e, actual: a, .. } => format!("expected '{}' body but was '{}'", e, a), + Mismatch::BodyTypeMismatch { expected: e, actual: a, .. } => format!("Expected a body of '{}' but the actual content type was '{}'", e, a), Mismatch::BodyMismatch { path, mismatch, .. } => format!("{} -> {}", path, mismatch), Mismatch::MetadataMismatch { mismatch, .. } => mismatch.clone() } @@ -878,7 +878,7 @@ impl Mismatch { Red.paint(e.to_string()), Green.paint(a.to_string()), Style::new().bold().paint(p.clone())), Mismatch::HeaderMismatch { expected: e, actual: a, key: k, .. } => format!("Expected header '{}' to have value '{}' but was '{}'", Style::new().bold().paint(k.clone()), Red.paint(e.to_string()), Green.paint(a.to_string())), - Mismatch::BodyTypeMismatch { expected: e, actual: a, .. } => format!("expected '{}' body but was '{}'", Red.paint(e.clone()), Green.paint(a.clone())), + Mismatch::BodyTypeMismatch { expected: e, actual: a, .. } => format!("expected a body of '{}' but the actual content type was '{}'", Red.paint(e.clone()), Green.paint(a.clone())), Mismatch::BodyMismatch { path, mismatch, .. } => format!("{} -> {}", Style::new().bold().paint(path.clone()), mismatch), Mismatch::MetadataMismatch { expected: e, actual: a, key: k, .. } => format!("Expected message metadata '{}' to have value '{}' but was '{}'", Style::new().bold().paint(k.clone()), Red.paint(e.to_string()), Green.paint(a.to_string())) @@ -1380,7 +1380,7 @@ pub async fn match_body( BodyMatchResult::BodyTypeMismatch { expected_type: expected_content_type.to_string(), actual_type: actual_content_type.to_string(), - message: format!("Expected body with content type {} but was {}", expected_content_type, + message: format!("Expected a body of '{}' but the actual content type was '{}'", expected_content_type, actual_content_type), expected: expected.body().value(), actual: actual.body().value()