diff --git a/rust/pact_models/src/v4/async_message.rs b/rust/pact_models/src/v4/async_message.rs index 7cc8495b..21eee6e0 100644 --- a/rust/pact_models/src/v4/async_message.rs +++ b/rust/pact_models/src/v4/async_message.rs @@ -522,6 +522,23 @@ mod tests { use crate::v4::interaction::V4Interaction; use crate::v4::message_parts::MessageContents; + #[test] + fn message_contents_without_a_content_attribute_are_not_dropped() { + // A hand-written pact may put the payload where the body attributes belong. Reading it as a + // missing body made an interaction verify successfully whatever the provider produced. + // See https://github.com/pact-foundation/pact-python/issues/1103 + let json = json!({ + "type": "Asynchronous/Messages", + "description": "a message", + "contents": { + "Payload": "exists" + } + }); + let message = AsynchronousMessage::from_json(&json, 0).unwrap(); + expect!(message.contents.contents).to(be_equal_to( + OptionalBody::Present("{\"Payload\":\"exists\"}".into(), None, None))); + } + #[test] fn when_downgrading_message_to_v3_rename_the_matching_rules_from_content_to_body() { let message = AsynchronousMessage { diff --git a/rust/pact_models/src/v4/http_parts.rs b/rust/pact_models/src/v4/http_parts.rs index 3ad490e8..b3b34f32 100644 --- a/rust/pact_models/src/v4/http_parts.rs +++ b/rust/pact_models/src/v4/http_parts.rs @@ -269,6 +269,9 @@ impl HttpPart for HttpRequest { } } +/// Attributes a V4 body fragment may hold alongside its content. +const BODY_ATTRIBUTES: [&str; 4] = ["content", "contentType", "contentTypeHint", "encoded"]; + /// Set up an OptionalBody from a JSON fragment. The contents for the body will be looked up from /// the attribute given by `attr_name`. The headers will be used to work out the content type, /// if required. @@ -361,8 +364,16 @@ pub fn body_from_json(json: &Value, attr_name: &str, headers: &Option OptionalBody::Missing + None => if body_attrs.keys().all(|k| BODY_ATTRIBUTES.contains(&k.as_str())) { + // Only body attributes, so the body really is missing + OptionalBody::Missing + } else { + // The payload has been set where the body attributes belong. Treat it the same as a + // body that is not a JSON object, rather than silently dropping it. + warn!("Body in attribute '{}' from JSON file has no 'content' attribute, will load the \ + whole value as the body", attr_name); + OptionalBody::Present(body.to_string().into(), None, None) + } } }, @@ -993,6 +1004,35 @@ mod tests { expect!(body).to(be_equal_to(OptionalBody::Null)); } + #[test] + fn body_from_json_returns_missing_if_the_body_only_has_body_attributes() { + let json = json!({ + "body": { + "contentType": "application/json", + "encoded": false + } + }); + expect!(body_from_json(&json, "body", &None)).to(be_equal_to(OptionalBody::Missing)); + + let json = json!({ + "body": {} + }); + expect!(body_from_json(&json, "body", &None)).to(be_equal_to(OptionalBody::Missing)); + } + + #[test] + fn body_from_json_loads_the_whole_value_if_the_body_has_no_content_attribute() { + // The payload has been set where the body attributes belong. Previously this was read as a + // missing body, so an interaction with a mismatched payload verified successfully. + let json = json!({ + "body": { + "Payload": "exists" + } + }); + expect!(body_from_json(&json, "body", &None)).to(be_equal_to( + OptionalBody::Present("{\"Payload\":\"exists\"}".into(), None, None))); + } + #[test] fn body_from_json_returns_json_string_if_the_body_is_json_but_not_a_string() { let json = json!({