diff --git a/rust/pact_ffi/src/verifier/mod.rs b/rust/pact_ffi/src/verifier/mod.rs index 499d91ef..b2304f07 100644 --- a/rust/pact_ffi/src/verifier/mod.rs +++ b/rust/pact_ffi/src/verifier/mod.rs @@ -547,6 +547,9 @@ ffi_fn! { /// If a username and password is given, then basic authentication will be used when fetching /// the pact file. If a token is provided, then bearer token authentication will be used. /// + /// This function will return zero unless any of the consumer version selectors are not valid + /// JSON, in which case, it will return -1. + /// /// # Safety /// /// All string fields must contain valid UTF-8. Invalid UTF-8 @@ -567,7 +570,7 @@ ffi_fn! { consumer_version_selectors_len: c_ushort, consumer_version_tags: *const *const c_char, consumer_version_tags_len: c_ushort - ) { + ) -> c_int { let handle = as_mut!(handle); let url = safe_str!(url); let provider_branch: Option<String> = if provider_branch.is_null() { @@ -604,7 +607,23 @@ ffi_fn! { let consumer_version_tags_vector = get_vector(consumer_version_tags, consumer_version_tags_len); let selectors = if consumer_version_selectors_vector.len() > 0 { - json_to_selectors(consumer_version_selectors_vector.iter().map(|s| &**s).collect()) + let mut selectors = vec![]; + let mut errors = false; + for s in consumer_version_selectors_vector { + match serde_json::from_str(s.as_str()) { + Ok(cvs) => selectors.push(cvs), + Err(err) => { + error!("Failed to parse consumer version selector '{}' as JSON: {}", s, err); + errors = true; + } + } + } + + if errors { + return Ok(-1); + } + + json_to_selectors(selectors) } else if consumer_version_tags_vector.len() > 0 { consumer_tags_to_selectors(consumer_version_tags_vector.iter().map(|s| &**s).collect()) } else { @@ -612,7 +631,9 @@ ffi_fn! { }; handle.add_pact_broker_source(url, enable_pending > 0, wip, provider_tags_vector, provider_branch, selectors, &auth); - } + + 0 + } { -2 } } ffi_fn! { @@ -902,6 +923,7 @@ ffi_fn! { #[cfg(test)] mod tests { use std::ffi::CString; + use std::ptr::null; use expectest::prelude::*; use libc::c_char; @@ -956,4 +978,58 @@ We are tracking events anonymously to gather important usage statistics like Pac 1) Verifying a pact between test_consumer and test_provider Given test state - test interaction - error sending request for url (http://localhost/): error trying to connect: tcp connect error: Connection refused (os error 111)\n\ \n\nThere were 1 pact failures\n\n")); } + + #[test] + fn pactffi_verifier_broker_source_with_selectors_test() { + let mut handle = VerifierHandle::new_for_application("test", "0.0.0"); + let url = CString::new("http://127.0.0.1:8080").unwrap(); + let provider_branch = CString::new("main").unwrap(); + let cvs_1 = CString::new(r#"{"mainBranch":true}"#).unwrap(); + let cvs_2 = CString::new(r#"{"deployedOrReleased":true}"#).unwrap(); + let consumer_version_selectors = [ cvs_1.as_ptr(), cvs_2.as_ptr() ]; + let result = super::pactffi_verifier_broker_source_with_selectors( + &mut handle, + url.as_ptr(), + null(), + null(), + null(), + 0, + null(), + null(), + 0, + provider_branch.as_ptr(), + consumer_version_selectors.as_ptr(), + 2, + null(), + 0 + ); + expect!(result).to(be_equal_to(0)); + } + + #[test_log::test] + fn pactffi_verifier_broker_source_with_selectors_error_test() { + let mut handle = VerifierHandle::new_for_application("test", "0.0.0"); + let url = CString::new("http://127.0.0.1:8080").unwrap(); + let provider_branch = CString::new("main").unwrap(); + let cvs_1 = CString::new(r#"{"mainBranch":true"#).unwrap(); + let cvs_2 = CString::new(r#"{"deployedOrReleased:true}"#).unwrap(); + let consumer_version_selectors = [ cvs_1.as_ptr(), cvs_2.as_ptr() ]; + let result = super::pactffi_verifier_broker_source_with_selectors( + &mut handle, + url.as_ptr(), + null(), + null(), + null(), + 0, + null(), + null(), + 0, + provider_branch.as_ptr(), + consumer_version_selectors.as_ptr(), + 2, + null(), + 0 + ); + expect!(result).to(be_equal_to(-1)); + } } diff --git a/rust/pact_ffi/src/verifier/verifier.rs b/rust/pact_ffi/src/verifier/verifier.rs index 1ee2e77a..a20951cb 100644 --- a/rust/pact_ffi/src/verifier/verifier.rs +++ b/rust/pact_ffi/src/verifier/verifier.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use clap::{AppSettings, ArgMatches, ErrorKind}; use maplit::hashmap; +use serde_json::Value; use pact_models::http_utils::HttpAuth; use pact_models::PactSpecification; use tracing::{debug, error, warn}; @@ -60,7 +61,10 @@ fn pact_source(matches: &ArgMatches) -> Vec<PactSource> { let provider_branch = matches.value_of("provider-branch").map(|v| v.to_string()); let selectors = if matches.is_present("consumer-version-selectors") { matches.values_of("consumer-version-selectors") - .map_or_else(Vec::new, |s| json_to_selectors(s.collect::<Vec<_>>())) + .map_or_else(Vec::new, |s| json_to_selectors(s + .map(|cvs| serde_json::from_str::<Value>(cvs)) + .flatten() + .collect::<Vec<_>>())) } else if matches.is_present("consumer-version-tags") { matches.values_of("consumer-version-tags") .map_or_else(Vec::new, |tags| consumer_tags_to_selectors(tags.collect::<Vec<_>>())) diff --git a/rust/pact_verifier/src/selectors.rs b/rust/pact_verifier/src/selectors.rs index 29120f62..aef97474 100644 --- a/rust/pact_verifier/src/selectors.rs +++ b/rust/pact_verifier/src/selectors.rs @@ -1,10 +1,12 @@ //! Module to deal with consumer version selectors +use serde_json::{from_value, Value}; + use crate::ConsumerVersionSelector; -/// Parses a vector of JSON strings into a vector of consumer version selectors -pub fn json_to_selectors(tags: Vec<&str>) -> Vec<ConsumerVersionSelector> { - tags.iter().map(|t| serde_json::from_str(t)) +/// Parses a vector of JSON into a vector of consumer version selectors +pub fn json_to_selectors(json: Vec<Value>) -> Vec<ConsumerVersionSelector> { + json.iter().map(|t| from_value(t.clone())) .flatten() .collect() } diff --git a/rust/pact_verifier_cli/src/args.rs b/rust/pact_verifier_cli/src/args.rs index 35b87a68..044747bf 100644 --- a/rust/pact_verifier_cli/src/args.rs +++ b/rust/pact_verifier_cli/src/args.rs @@ -2,6 +2,7 @@ use clap::{Arg, ArgAction, ArgGroup, Command, command}; use clap::builder::{FalseyValueParser, NonEmptyStringValueParser, PossibleValuesParser}; use lazy_static::lazy_static; use regex::Regex; +use serde_json::Value; fn port_value(v: &str) -> Result<u16, String> { v.parse::<u16>().map_err(|e| format!("'{}' is not a valid port value: {}", v, e) ) @@ -21,6 +22,10 @@ fn validate_regex(val: &str) -> Result<String, String> { } } +fn json_value(v: &str) -> Result<Value, String> { + serde_json::from_str(v).map_err(|err| format!("'{}' is not valid JSON: {}", v, err)) +} + lazy_static! { static ref TRANSPORT_VALUE_RE: Regex = Regex::new(r#"^(\w+):(\d+)(\/[^\s]*)?$"#).unwrap(); } @@ -223,7 +228,6 @@ pub(crate) fn setup_app() -> Command { .arg(Arg::new("custom-header") .long("header") .short('H') - .action(ArgAction::Set) .action(ArgAction::Append) .value_parser(NonEmptyStringValueParser::new()) .help("Add a custom header to be included in the calls to the provider. Values must be in the form KEY=VALUE, where KEY and VALUE contain ASCII characters (32-127) only. Can be repeated.")) @@ -278,7 +282,6 @@ pub(crate) fn setup_app() -> Command { .arg(Arg::new("filter-consumer") .short('c') .long("filter-consumer") - .action(ArgAction::Set) .action(ArgAction::Append) .value_parser(NonEmptyStringValueParser::new()) .help("Consumer name to filter the pacts to be verified (can be repeated)")) @@ -325,9 +328,8 @@ pub(crate) fn setup_app() -> Command { .help("Consumer tags to use when fetching pacts from the Broker. Accepts comma-separated values.")) .arg(Arg::new("consumer-version-selectors") .long("consumer-version-selectors") - .action(ArgAction::Set) .action(ArgAction::Append) - .value_parser(NonEmptyStringValueParser::new()) + .value_parser(json_value) .requires("broker-url") .conflicts_with("consumer-version-tags") .help("Consumer version selectors to use when fetching pacts from the Broker. Accepts a JSON string as per https://docs.pact.io/pact_broker/advanced_topics/consumer_version_selectors/. Can be repeated.")) diff --git a/rust/pact_verifier_cli/src/main.rs b/rust/pact_verifier_cli/src/main.rs index 85536177..3da911b3 100644 --- a/rust/pact_verifier_cli/src/main.rs +++ b/rust/pact_verifier_cli/src/main.rs @@ -400,6 +400,7 @@ use clap::ArgMatches; use clap::error::ErrorKind; use log::{LevelFilter}; use maplit::hashmap; +use serde_json::Value; use pact_models::{PACT_RUST_VERSION, PactSpecification}; use pact_models::prelude::HttpAuth; use tokio::time::sleep; @@ -689,8 +690,8 @@ fn pact_source(matches: &ArgMatches) -> Vec<PactSource> { let provider_branch = matches.get_one::<String>("provider-branch").cloned(); let selectors = if matches.contains_id("consumer-version-selectors") { - matches.get_many::<String>("consumer-version-selectors") - .map_or_else(Vec::new, |s| json_to_selectors(s.map(|v| v.as_str()).collect::<Vec<_>>())) + matches.get_many::<Value>("consumer-version-selectors") + .map_or_else(Vec::new, |s| json_to_selectors(s.into_iter().cloned().collect::<Vec<_>>())) } else if matches.contains_id("consumer-version-tags") { matches.get_many::<String>("consumer-version-tags") .map_or_else(Vec::new, |tags| consumer_tags_to_selectors(tags.map(|v| v.as_str()).collect::<Vec<_>>()))