Skip to content

Commit

Permalink
fix: HTTP Protobuf interactions can have a message defined for both t…
Browse files Browse the repository at this point in the history
…he request and response parts
  • Loading branch information
rholshausen committed Aug 26, 2024
1 parent 4997452 commit d6865b4
Showing 1 changed file with 144 additions and 11 deletions.
155 changes: 144 additions & 11 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;

Expand Down Expand Up @@ -343,13 +348,16 @@ impl ProtobufPactPlugin {
}

fn lookup_message_and_service(
interaction_config: BTreeMap<String, prost_types::Value>
interaction_config: &BTreeMap<String, prost_types::Value>,
expected_message_type: &Option<String>
) -> anyhow::Result<(Option<String>, Option<String>)> {
// 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");
Expand All @@ -359,14 +367,53 @@ impl ProtobufPactPlugin {
}
}

fn lookup_message_key(interaction_config: &BTreeMap<String, prost_types::Value>) -> anyhow::Result<String> {
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<String, prost_types::Value>,
expected_message_type: &Option<String>
) -> Option<String> {
interaction_config.get("message")
.and_then(proto_value_to_string)
.or_else(|| expected_message_type.clone())
}

fn lookup_message_key(
interaction_config: &BTreeMap<String, prost_types::Value>,
expected_message_type: &Option<String>
) -> anyhow::Result<String> {
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.
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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());
}
}

0 comments on commit d6865b4

Please sign in to comment.