From d6865b4294ec030b8dd306e22831f36968bf7ebd Mon Sep 17 00:00:00 2001 From: Ronald Holshausen Date: Mon, 26 Aug 2024 10:54:55 +1000 Subject: [PATCH] fix: HTTP Protobuf interactions can have a message defined for both the request and response parts --- src/server.rs | 155 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 144 insertions(+), 11 deletions(-) diff --git a/src/server.rs b/src/server.rs index 002103f..35340c2 100644 --- a/src/server.rs +++ b/src/server.rs @@ -242,12 +242,17 @@ impl ProtobufPactPlugin { // From the plugin configuration for the interaction, get the descriptor key. This key is used // to lookup the encoded Protobuf descriptors in the Pact level plugin configuration - let message_key = Self::lookup_message_key(&interaction_config)?; + let expected_message_type = request.expected.as_ref() + .and_then(|body| ContentType::parse(body.content_type.as_str()).ok()) + .as_ref() + .and_then(|ct| ct.attributes.get("message").clone()) + .cloned(); + let message_key = Self::lookup_message_key(&interaction_config, &expected_message_type)?; debug!("compare_contents: message_key = {}", message_key); // From the plugin configuration for the interaction, there should be either a message type name // or a service name. Check for either. - let (message, service) = Self::lookup_message_and_service(interaction_config)?; + let (message, service) = Self::lookup_message_and_service(&interaction_config, &expected_message_type)?; let descriptors = Self::lookup_descriptors(plugin_configuration, message_key)?; @@ -343,13 +348,16 @@ impl ProtobufPactPlugin { } fn lookup_message_and_service( - interaction_config: BTreeMap + interaction_config: &BTreeMap, + expected_message_type: &Option ) -> anyhow::Result<(Option, Option)> { // Both message and service will be a fully-qualified name starting with the "." if the pact // was generated by the current version of the plugin; or without the "." if it's an older version. // Example message: `.routeguide.Feature` // Service name will also include method, e.g. `.routeguide.RouteGuide/GetFeature` - let message = interaction_config.get("message").and_then(proto_value_to_string); + // Note that the message (but not the service) could be sent under a request or response key, + // in which case we need to use the expected value from the content type. + let message = Self::lookup_message_type(interaction_config, expected_message_type); let service = interaction_config.get("service").and_then(proto_value_to_string); if message.is_none() && service.is_none() { error!("Plugin configuration item with key 'message' or 'service' is required"); @@ -359,14 +367,53 @@ impl ProtobufPactPlugin { } } - fn lookup_message_key(interaction_config: &BTreeMap) -> anyhow::Result { - match interaction_config.get("descriptorKey").and_then(proto_value_to_string) { - Some(key) => Ok(key), - None => { - error!("Plugin configuration item with key 'descriptorKey' is required"); - Err(anyhow!("Plugin configuration item with key 'descriptorKey' is required")) + fn lookup_message_type( + interaction_config: &BTreeMap, + expected_message_type: &Option + ) -> Option { + interaction_config.get("message") + .and_then(proto_value_to_string) + .or_else(|| expected_message_type.clone()) + } + + fn lookup_message_key( + interaction_config: &BTreeMap, + expected_message_type: &Option + ) -> anyhow::Result { + if let Some(key) = interaction_config.get("descriptorKey").and_then(proto_value_to_string) { + return Ok(key); + } + + // The descriptor key may be stored under a request or response key. We use the message type + // from the content type to match it. + if let Some(expected_message_type) = expected_message_type { + if let Some(request_config) = interaction_config.get("request") { + if let Some(Kind::StructValue(s)) = &request_config.kind { + if let Some(message) = s.fields.get("message").and_then(proto_value_to_string) { + if message == expected_message_type.as_str() { + if let Some(key) = s.fields.get("descriptorKey").and_then(proto_value_to_string) { + return Ok(key); + } + } + } + } + } + + if let Some(response_config) = interaction_config.get("response") { + if let Some(Kind::StructValue(s)) = &response_config.kind { + if let Some(message) = s.fields.get("message").and_then(proto_value_to_string) { + if message == expected_message_type.as_str() { + if let Some(key) = s.fields.get("descriptorKey").and_then(proto_value_to_string) { + return Ok(key); + } + } + } + } } } + + error!("Plugin configuration item with key 'descriptorKey' is required"); + Err(anyhow!("Plugin configuration item with key 'descriptorKey' is required")) } /// Generate contents for the interaction. @@ -396,7 +443,12 @@ impl ProtobufPactPlugin { // From the plugin configuration for the interaction, get the descriptor key. This key is used // to lookup the encoded Protobuf descriptors in the Pact level plugin configuration - let message_key = Self::lookup_message_key(&interaction_config)?; + let expected_message_type = request.contents.as_ref() + .and_then(|body| ContentType::parse(body.content_type.as_str()).ok()) + .as_ref() + .and_then(|ct| ct.attributes.get("message").clone()) + .cloned(); + let message_key = Self::lookup_message_key(&interaction_config, &expected_message_type)?; debug!("generate_contents: message_key = {}", message_key); let descriptors = Self::lookup_descriptors(plugin_configuration, message_key)?; @@ -1592,4 +1644,85 @@ mod tests { expect!(merge_value(&json!({"additional": ["ok"]}), &json!({"additional": ["not ok"], "other": "value"})).unwrap()) .to(be_equal_to(json!({"additional": ["ok", "not ok"], "other": "value"}))); } + + #[test_log::test] + fn lookup_message_key_returns_the_descriptor_key() { + let config = btreemap!{ + "descriptorKey".to_string() => prost_types::Value { kind: Some(Kind::StringValue("1234567".to_string())) } + }; + expect!(ProtobufPactPlugin::lookup_message_key(&config, &None)) + .to(be_ok().value("1234567".to_string())); + } + + #[test_log::test] + fn lookup_message_key_returns_an_error_when_there_is_no_descriptor_key() { + expect!(ProtobufPactPlugin::lookup_message_key( + &btreemap!{}, + &None + )).to(be_err()); + } + + #[test_log::test] + fn lookup_message_key_returns_the_descriptor_key_from_the_request_if_the_message_type_matches() { + let config = btreemap!{ + "request".to_string() => prost_types::Value { + kind: Some(Kind::StructValue(prost_types::Struct { + fields: btreemap!{ + "descriptorKey".to_string() => prost_types::Value { kind: Some(Kind::StringValue("1234567".to_string())) }, + "message".to_string() => prost_types::Value { kind: Some(Kind::StringValue(".package.Type".to_string())) } + } + })) + } + }; + expect!(ProtobufPactPlugin::lookup_message_key(&config, &Some(".package.Type".to_string()))) + .to(be_ok().value("1234567".to_string())); + } + + #[test_log::test] + fn lookup_message_key_returns_an_error_if_the_request_message_type_does_not_match() { + let config = btreemap!{ + "request".to_string() => prost_types::Value { + kind: Some(Kind::StructValue(prost_types::Struct { + fields: btreemap!{ + "descriptorKey".to_string() => prost_types::Value { kind: Some(Kind::StringValue("1234567".to_string())) }, + "message".to_string() => prost_types::Value { kind: Some(Kind::StringValue(".package.OtherType".to_string())) } + } + })) + } + }; + expect!(ProtobufPactPlugin::lookup_message_key(&config, &Some(".package.Type".to_string()))) + .to(be_err()); + } + + #[test_log::test] + fn lookup_message_key_returns_the_descriptor_key_from_the_response_if_the_message_type_matches() { + let config = btreemap!{ + "response".to_string() => prost_types::Value { + kind: Some(Kind::StructValue(prost_types::Struct { + fields: btreemap!{ + "descriptorKey".to_string() => prost_types::Value { kind: Some(Kind::StringValue("1234567".to_string())) }, + "message".to_string() => prost_types::Value { kind: Some(Kind::StringValue(".package.Type".to_string())) } + } + })) + } + }; + expect!(ProtobufPactPlugin::lookup_message_key(&config, &Some(".package.Type".to_string()))) + .to(be_ok().value("1234567".to_string())); + } + + #[test_log::test] + fn lookup_message_key_returns_an_error_if_the_response_message_type_does_not_match() { + let config = btreemap!{ + "response".to_string() => prost_types::Value { + kind: Some(Kind::StructValue(prost_types::Struct { + fields: btreemap!{ + "descriptorKey".to_string() => prost_types::Value { kind: Some(Kind::StringValue("1234567".to_string())) }, + "message".to_string() => prost_types::Value { kind: Some(Kind::StringValue(".package.OtherType".to_string())) } + } + })) + } + }; + expect!(ProtobufPactPlugin::lookup_message_key(&config, &Some(".package.Type".to_string()))) + .to(be_err()); + } }