Skip to content

Commit

Permalink
feat(verifier): Add option to only verify pact from a webhook URL #350
Browse files Browse the repository at this point in the history
  • Loading branch information
rholshausen committed Dec 20, 2023
1 parent 302a5e8 commit 7a170fa
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 73 deletions.
55 changes: 40 additions & 15 deletions rust/pact_verifier/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,32 +113,47 @@ pub enum PactSource {
links: Vec<Link>
},
/// Load the Pact from some JSON (used for testing purposed)
String(String)
String(String),
/// Load the given pact with the URL via a webhook callback
WebhookCallbackUrl {
/// URL to the Pact to verify
pact_url: String,
/// Base URL of the Pact Broker
broker_url: String,
/// HTTP authentication details for accessing the Pact Broker
auth: Option<HttpAuth>
}
}

impl Display for PactSource {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match *self {
PactSource::File(ref file) => write!(f, "File({})", file),
PactSource::Dir(ref dir) => write!(f, "Dir({})", dir),
PactSource::URL(ref url, _) => write!(f, "URL({})", url),
PactSource::BrokerUrl(ref provider_name, ref broker_url, _, _) => {
match self {
PactSource::File(file) => write!(f, "File({})", file),
PactSource::Dir(dir) => write!(f, "Dir({})", dir),
PactSource::URL(url, _) => write!(f, "URL({})", url),
PactSource::BrokerUrl(provider_name, broker_url, _, _) => {
write!(f, "PactBroker({}, provider_name='{}')", broker_url, provider_name)
}
PactSource::BrokerWithDynamicConfiguration {
ref provider_name,
ref broker_url,
ref enable_pending,
ref include_wip_pacts_since,
ref provider_branch,
ref provider_tags,
ref selectors,
ref auth, .. } => {
provider_name,
broker_url,
enable_pending,
include_wip_pacts_since,
provider_branch,
provider_tags,
selectors,
auth, .. } => {
if let Some(auth) = auth {
write!(f, "PactBrokerWithDynamicConfiguration({}, provider_name='{}', enable_pending={}, include_wip_since={:?}, provider_tags={:?}, provider_branch={:?}, consumer_version_selectors='{:?}, auth={}')", broker_url, provider_name, enable_pending, include_wip_pacts_since, provider_tags, provider_branch, selectors, auth)
} else {
write!(f, "PactBrokerWithDynamicConfiguration({}, provider_name='{}', enable_pending={}, include_wip_since={:?}, provider_tags={:?}, provider_branch={:?}, consumer_version_selectors='{:?}, auth=None')", broker_url, provider_name, enable_pending, include_wip_pacts_since, provider_tags, provider_branch, selectors)

}
}
PactSource::WebhookCallbackUrl { pact_url, auth, .. } => {
if let Some(auth) = auth {
write!(f, "WebhookCallbackUrl({}, auth={}')", pact_url, auth)
} else {
write!(f, "WebhookCallbackUrl({}, auth=None')", pact_url)
}
}
_ => write!(f, "Unknown")
Expand Down Expand Up @@ -1315,6 +1330,16 @@ async fn fetch_pact(
(pact, None, source.clone(), tm)
})
],
PactSource::WebhookCallbackUrl { pact_url, broker_url, auth, .. } => vec![
timeit_async(pact_broker::fetch_pact_from_url(pact_url, auth)).await
.map_err(|err| anyhow!("Failed to load pact '{}' - {}", pact_url, err))
.map(|((pact, links), tm)| {
trace!(%pact_url, duration = ?tm, "Loaded pact from url");
let provider = pact.provider();
(pact, None, PactSource::BrokerUrl(provider.name.clone(), broker_url.clone(),
auth.clone(), links.clone()), tm)
})
],
_ => vec![Err(anyhow!("Could not load pacts, unknown pact source {}", source))]
}
}
Expand Down
15 changes: 15 additions & 0 deletions rust/pact_verifier_cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub(crate) fn setup_app() -> Command {
.long("version")
.action(ArgAction::Version)
.help("Print version information and exit"))

.group(ArgGroup::new("logging").multiple(true))
.next_help_heading("Logging options")
.arg(Arg::new("loglevel")
Expand Down Expand Up @@ -83,6 +84,7 @@ pub(crate) fn setup_app() -> Command {
.action(ArgAction::SetTrue)
.visible_alias("no-color")
.help("Disables ANSI escape codes in the output"))

.group(ArgGroup::new("source").multiple(true))
.next_help_heading("Loading pacts options")
.arg(Arg::new("file")
Expand Down Expand Up @@ -115,10 +117,18 @@ pub(crate) fn setup_app() -> Command {
.action(ArgAction::Set)
.value_parser(NonEmptyStringValueParser::new())
.help("URL of the pact broker to fetch pacts from to verify (requires the provider name parameter)"))
.arg(Arg::new("webhook-callback-url")
.long("webhook-callback-url")
.requires("broker-url")
.conflicts_with_all(&["file", "dir", "url"])
.action(ArgAction::Set)
.value_parser(NonEmptyStringValueParser::new())
.help("URL of a Pact to verify via a webhook callback. Requires the broker-url to be set."))
.arg(Arg::new("ignore-no-pacts-error")
.long("ignore-no-pacts-error")
.action(ArgAction::SetTrue)
.help("Do not fail if no pacts are found to verify"))

.group(ArgGroup::new("auth").multiple(true))
.next_help_heading("Authentication options")
.arg(Arg::new("user")
Expand All @@ -143,6 +153,7 @@ pub(crate) fn setup_app() -> Command {
.value_parser(NonEmptyStringValueParser::new())
.conflicts_with("user")
.help("Bearer token to use when fetching pacts from URLS"))

.group(ArgGroup::new("provider").multiple(true))
.next_help_heading("Provider options")
.arg(Arg::new("hostname")
Expand Down Expand Up @@ -198,6 +209,7 @@ pub(crate) fn setup_app() -> Command {
.long("disable-ssl-verification")
.action(ArgAction::SetTrue)
.help("Disables validation of SSL certificates"))

.group(ArgGroup::new("states").multiple(true))
.next_help_heading("Provider state options")
.arg(Arg::new("state-change-url")
Expand All @@ -214,6 +226,7 @@ pub(crate) fn setup_app() -> Command {
.long("state-change-teardown")
.action(ArgAction::SetTrue)
.help("State change teardown requests are to be made after each interaction"))

.group(ArgGroup::new("filtering").multiple(true))
.next_help_heading("Filtering interactions")
.arg(Arg::new("filter-description")
Expand Down Expand Up @@ -242,6 +255,7 @@ pub(crate) fn setup_app() -> Command {
.action(ArgAction::Append)
.value_parser(NonEmptyStringValueParser::new())
.help("Consumer name to filter the pacts to be verified (can be repeated)"))

.group(ArgGroup::new("publish-options").multiple(true))
.next_help_heading("Publishing options")
.arg(Arg::new("publish")
Expand Down Expand Up @@ -271,6 +285,7 @@ pub(crate) fn setup_app() -> Command {
.action(ArgAction::Set)
.value_parser(NonEmptyStringValueParser::new())
.help("Provider branch to use when publishing results"))

.group(ArgGroup::new("broker").multiple(true))
.next_help_heading("Pact Broker options")
.arg(Arg::new("consumer-version-tags")
Expand Down
119 changes: 66 additions & 53 deletions rust/pact_verifier_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -514,66 +514,79 @@ fn print_version(version: &str) {
fn pact_source(matches: &ArgMatches) -> Vec<PactSource> {
let mut sources = vec![];

if let Some(values) = matches.get_many::<String>("file") {
sources.extend(values.map(|v| PactSource::File(v.clone())).collect::<Vec<PactSource>>());
};

if let Some(values) = matches.get_many::<String>("dir") {
sources.extend(values.map(|v| PactSource::Dir(v.clone())).collect::<Vec<PactSource>>());
};

if let Some(values) = matches.get_many::<String>("url") {
sources.extend(values.map(|v| {
if let Some(user) = matches.get_one::<String>("user") {
PactSource::URL(v.clone(), Some(HttpAuth::User(user.clone(),
matches.get_one::<String>("password").map(|p| p.clone()))))
} else if let Some(token) = matches.get_one::<String>("token") {
PactSource::URL(v.clone(), Some(HttpAuth::Token(token.clone())))
} else {
PactSource::URL(v.clone(), None)
}
}).collect::<Vec<PactSource>>());
};

if let Some(broker_url) = matches.get_one::<String>("broker-url") {
let name = matches.get_one::<String>("provider-name").cloned().unwrap_or_default();
if let Some(webhook_url) = matches.get_one::<String>("webhook-callback-url") {
let broker_url = matches.get_one::<String>("broker-url").unwrap();
let auth = matches.get_one::<String>("user").map(|user| {
HttpAuth::User(user.clone(), matches.get_one::<String>("password").cloned())
}).or_else(|| matches.get_one::<String>("token").map(|t| HttpAuth::Token(t.clone())));
sources.push(PactSource::WebhookCallbackUrl {
pact_url: webhook_url.clone(),
broker_url: broker_url.clone(),
auth
});
} else {
if let Some(values) = matches.get_many::<String>("file") {
sources.extend(values.map(|v| PactSource::File(v.clone())).collect::<Vec<PactSource>>());
};

if let Some(values) = matches.get_many::<String>("dir") {
sources.extend(values.map(|v| PactSource::Dir(v.clone())).collect::<Vec<PactSource>>());
};

if let Some(values) = matches.get_many::<String>("url") {
sources.extend(values.map(|v| {
if let Some(user) = matches.get_one::<String>("user") {
PactSource::URL(v.clone(), Some(HttpAuth::User(user.clone(),
matches.get_one::<String>("password").map(|p| p.clone()))))
} else if let Some(token) = matches.get_one::<String>("token") {
PactSource::URL(v.clone(), Some(HttpAuth::Token(token.clone())))
} else {
PactSource::URL(v.clone(), None)
}
}).collect::<Vec<PactSource>>());
};

let source = if matches.contains_id("consumer-version-selectors") || matches.contains_id("consumer-version-tags") {
let pending = matches.get_flag("enable-pending");
let wip = matches.get_one::<String>("include-wip-pacts-since").cloned();
let provider_tags = matches.get_many::<String>("provider-tags")
.map_or_else(Vec::new, |tags| tags.map(|tag| tag.clone()).collect());
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<_>>()))
} 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<_>>()))
if let Some(broker_url) = matches.get_one::<String>("broker-url") {
let name = matches.get_one::<String>("provider-name").cloned().unwrap_or_default();
let auth = matches.get_one::<String>("user").map(|user| {
HttpAuth::User(user.clone(), matches.get_one::<String>("password").cloned())
}).or_else(|| matches.get_one::<String>("token").map(|t| HttpAuth::Token(t.clone())));

let source = if matches.contains_id("consumer-version-selectors") || matches.contains_id("consumer-version-tags") {
let pending = matches.get_flag("enable-pending");
let wip = matches.get_one::<String>("include-wip-pacts-since").cloned();
let provider_tags = matches.get_many::<String>("provider-tags")
.map_or_else(Vec::new, |tags| tags.map(|tag| tag.clone()).collect());
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<_>>()))
} 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<_>>()))
} else {
vec![]
};

PactSource::BrokerWithDynamicConfiguration {
provider_name: name,
broker_url: broker_url.into(),
enable_pending: pending,
include_wip_pacts_since: wip,
provider_tags,
provider_branch,
selectors,
auth,
links: vec![]
}
} else {
vec![]
PactSource::BrokerUrl(name, broker_url.to_string(), auth, vec![])
};

PactSource::BrokerWithDynamicConfiguration {
provider_name: name,
broker_url: broker_url.into(),
enable_pending: pending,
include_wip_pacts_since: wip,
provider_tags,
provider_branch,
selectors,
auth,
links: vec![]
}
} else {
PactSource::BrokerUrl(name, broker_url.to_string(), auth, vec![])
sources.push(source);
};
sources.push(source);
};
}

sources
}

Expand Down
17 changes: 12 additions & 5 deletions rust/pact_verifier_cli/tests/cmd/main.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,18 @@ Logging options:
--no-colour Disables ANSI escape codes in the output [aliases: no-color]

Loading pacts options:
-f, --file <file> Pact file to verify (can be repeated)
-d, --dir <dir> Directory of pact files to verify (can be repeated)
-u, --url <url> URL of pact file to verify (can be repeated)
-b, --broker-url <broker-url> URL of the pact broker to fetch pacts from to verify (requires the provider name parameter) [env: PACT_BROKER_BASE_URL=]
--ignore-no-pacts-error Do not fail if no pacts are found to verify
-f, --file <file>
Pact file to verify (can be repeated)
-d, --dir <dir>
Directory of pact files to verify (can be repeated)
-u, --url <url>
URL of pact file to verify (can be repeated)
-b, --broker-url <broker-url>
URL of the pact broker to fetch pacts from to verify (requires the provider name parameter) [env: PACT_BROKER_BASE_URL=]
--webhook-callback-url <webhook-callback-url>
URL of a Pact to verify via a webhook callback. Requires the broker-url to be set.
--ignore-no-pacts-error
Do not fail if no pacts are found to verify

Authentication options:
--user <user> Username to use when fetching pacts from URLS [env: PACT_BROKER_USERNAME=]
Expand Down

0 comments on commit 7a170fa

Please sign in to comment.