#![warn(missing_docs)]
#[macro_use] extern crate pact_matching;
extern crate ansi_term;
#[macro_use] extern crate log;
extern crate hyper;
extern crate tokio;
extern crate futures;
extern crate bytes;
extern crate reqwest;
extern crate mime;
#[macro_use] extern crate maplit;
extern crate itertools;
extern crate regex;
extern crate difference;
#[macro_use] extern crate serde_json;
#[cfg(test)]
#[macro_use(expect)]
extern crate expectest;
#[cfg(test)]
#[macro_use]
extern crate pact_consumer;
#[cfg(test)]
extern crate env_logger;
#[cfg(test)]
extern crate http;
mod provider_client;
mod pact_broker;
use std::path::Path;
use std::io;
use std::fs;
use std::fmt::{Display, Formatter};
use pact_matching::*;
use pact_matching::models::*;
use pact_matching::models::provider_states::*;
use pact_matching::models::http_utils::HttpAuth;
use ansi_term::*;
use ansi_term::Colour::*;
use std::collections::HashMap;
use provider_client::{make_provider_request, make_state_change_request, ProviderClientError};
use regex::Regex;
use serde_json::Value;
use tokio::runtime::current_thread::Runtime;
use pact_broker::{publish_verification_results, TestResult, Link};
#[derive(Debug, Clone)]
pub enum PactSource {
Unknown,
File(String),
Dir(String),
URL(String, Option<HttpAuth>),
BrokerUrl(String, String, Option<HttpAuth>, Vec<Link>)
}
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, _, _) => {
write!(f, "PactBroker({}, provider_name='{}')", broker_url, provider_name)
}
_ => write!(f, "Unknown")
}
}
}
#[derive(Debug, Clone)]
pub struct ProviderInfo {
pub name: String,
pub protocol: String,
pub host: String,
pub port: u16,
pub path: String,
pub state_change_url: Option<String>,
pub state_change_teardown: bool,
pub state_change_body: bool
}
impl ProviderInfo {
pub fn default() -> ProviderInfo {
ProviderInfo {
name: s!("provider"),
protocol: s!("http"),
host: s!("localhost"),
port: 8080,
path: s!("/"),
state_change_url: None,
state_change_teardown: false,
state_change_body: true
}
}
}
#[derive(Debug, Clone)]
pub enum MismatchResult {
Mismatches {
mismatches: Vec<Mismatch>,
expected: Response,
actual: Response,
interaction_id: Option<String>
},
Error(String, Option<String>)
}
impl MismatchResult {
pub fn interaction_id(&self) -> Option<String> {
match self {
&MismatchResult::Mismatches { ref interaction_id, .. } => interaction_id.clone(),
&MismatchResult::Error(_, ref interaction_id) => interaction_id.clone()
}
}
}
fn provider_client_error_to_string(err: ProviderClientError) -> String {
match err {
ProviderClientError::RequestMethodError(ref method, _) =>
format!("Invalid request method: '{}'", method),
ProviderClientError::RequestHeaderNameError(ref name, _) =>
format!("Invalid header name: '{}'", name),
ProviderClientError::RequestHeaderValueError(ref value, _) =>
format!("Invalid header value: '{}'", value),
ProviderClientError::RequestBodyError(ref message) =>
format!("Invalid request body: '{}'", message),
ProviderClientError::ResponseError(ref message) =>
format!("Invalid response: {}", message),
ProviderClientError::ResponseStatusCodeError(ref code) =>
format!("Invalid status code: {}", code)
}
}
fn verify_response_from_provider(provider: &ProviderInfo, interaction: &Interaction, runtime: &mut Runtime) -> Result<(), MismatchResult> {
let ref expected_response = interaction.response;
match runtime.block_on(make_provider_request(provider, &pact_matching::generate_request(&interaction.request, &hashmap!{}))) {
Ok(ref actual_response) => {
let mismatches = match_response(expected_response.clone(), actual_response.clone());
if mismatches.is_empty() {
Ok(())
} else {
Err(MismatchResult::Mismatches {
mismatches,
expected: expected_response.clone(),
actual: actual_response.clone(),
interaction_id: interaction.id.clone()
})
}
},
Err(err) => {
Err(MismatchResult::Error(provider_client_error_to_string(err), interaction.id.clone()))
}
}
}
fn execute_state_change(provider_state: &ProviderState, provider: &ProviderInfo, setup: bool,
runtime: &mut Runtime, interaction_id: Option<String>) -> Result<(), MismatchResult> {
if setup {
println!(" Given {}", Style::new().bold().paint(provider_state.name.clone()));
}
let result = match provider.state_change_url {
Some(_) => {
let mut state_change_request = Request { method: s!("POST"), .. Request::default() };
if provider.state_change_body {
let mut json_body = json!({
s!("state") : json!(provider_state.name.clone()),
s!("action") : json!(if setup {
s!("setup")
} else {
s!("teardown")
})
});
{
let json_body_mut = json_body.as_object_mut().unwrap();
for (k, v) in provider_state.params.clone() {
json_body_mut.insert(k, v);
}
}
state_change_request.body = OptionalBody::Present(json_body.to_string().into());
state_change_request.headers = Some(hashmap!{ s!("Content-Type") => vec![s!("application/json")] });
} else {
let mut query = hashmap!{ s!("state") => vec![provider_state.name.clone()] };
if setup {
query.insert(s!("action"), vec![s!("setup")]);
} else {
query.insert(s!("action"), vec![s!("teardown")]);
}
for (k, v) in provider_state.params.clone() {
query.insert(k, vec![match v {
Value::String(ref s) => s.clone(),
_ => v.to_string()
}]);
}
state_change_request.query = Some(query);
}
match runtime.block_on(make_state_change_request(provider, &state_change_request)) {
Ok(_) => Ok(()),
Err(err) => Err(MismatchResult::Error(provider_client_error_to_string(err), interaction_id))
}
},
None => {
if setup {
println!(" {}", Yellow.paint("WARNING: State Change ignored as there is no state change URL"));
}
Ok(())
}
};
debug!("State Change: \"{:?}\" -> {:?}", provider_state, result);
result
}
fn verify_interaction(provider: &ProviderInfo, interaction: &Interaction, runtime: &mut Runtime) -> Result<(), MismatchResult> {
for state in interaction.provider_states.clone() {
execute_state_change(&state, provider, true, runtime, interaction.id.clone())?
}
let result = verify_response_from_provider(provider, interaction, runtime);
if provider.state_change_teardown {
for state in interaction.provider_states.clone() {
execute_state_change(&state, provider, false, runtime, interaction.id.clone())?
}
}
result
}
fn display_result(status: u16, status_result: ANSIGenericString<str>,
header_results: Option<Vec<(String, String, ANSIGenericString<str>)>>,
body_result: ANSIGenericString<str>) {
println!(" returns a response which");
println!(" has status code {} ({})", Style::new().bold().paint(format!("{}", status)),
status_result);
match header_results {
Some(header_results) => {
println!(" includes headers");
for (key, value, result) in header_results {
println!(" \"{}\" with value \"{}\" ({})", Style::new().bold().paint(key),
Style::new().bold().paint(value), result);
}
},
None => ()
}
println!(" has a matching body ({})", body_result);
}
fn walkdir(dir: &Path) -> io::Result<Vec<io::Result<Pact>>> {
let mut pacts = vec![];
debug!("Scanning {:?}", dir);
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
walkdir(&path)?;
} else {
pacts.push(Pact::read_pact(&path))
}
}
Ok(pacts)
}
fn display_body_mismatch(expected: &Response, actual: &Response, path: &String) {
match expected.content_type_enum() {
DetectedContentType::Json => println!("{}", pact_matching::json::display_diff(&expected.body.str_value().to_string(),
&actual.body.str_value().to_string(), path)),
_ => ()
}
}
pub enum FilterInfo {
None,
Description(String),
State(String),
DescriptionAndState(String, String)
}
impl FilterInfo {
pub fn has_description(&self) -> bool {
match self {
&FilterInfo::Description(_) => true,
&FilterInfo::DescriptionAndState(_, _) => true,
_ => false
}
}
pub fn has_state(&self) -> bool {
match self {
&FilterInfo::State(_) => true,
&FilterInfo::DescriptionAndState(_, _) => true,
_ => false
}
}
pub fn state(&self) -> String {
match self {
&FilterInfo::State(ref s) => s.clone(),
&FilterInfo::DescriptionAndState(_, ref s) => s.clone(),
_ => s!("")
}
}
pub fn description(&self) -> String {
match self {
&FilterInfo::Description(ref s) => s.clone(),
&FilterInfo::DescriptionAndState(ref s, _) => s.clone(),
_ => s!("")
}
}
pub fn match_state(&self, interaction: &Interaction) -> bool {
if !interaction.provider_states.is_empty() {
if self.state().is_empty() {
false
} else {
let re = Regex::new(&self.state()).unwrap();
interaction.provider_states.iter().any(|state| re.is_match(&state.name))
}
} else {
self.has_state() && self.state().is_empty()
}
}
pub fn match_description(&self, interaction: &Interaction) -> bool {
let re = Regex::new(&self.description()).unwrap();
re.is_match(&interaction.description)
}
}
fn filter_interaction(interaction: &Interaction, filter: &FilterInfo) -> bool {
if filter.has_description() && filter.has_state() {
filter.match_description(interaction) && filter.match_state(interaction)
} else if filter.has_description() {
filter.match_description(interaction)
} else if filter.has_state() {
filter.match_state(interaction)
} else {
true
}
}
fn filter_consumers(consumers: &Vec<String>, res: &Result<(Pact, PactSource), String>) -> bool {
consumers.is_empty() || res.is_err() || consumers.contains(&res.clone().unwrap().0.consumer.name)
}
pub struct VerificationOptions {
pub publish: bool,
pub provider_version: Option<String>,
pub build_url: Option<String>
}
pub fn verify_provider(provider_info: &ProviderInfo, source: Vec<PactSource>, filter: &FilterInfo,
consumers: &Vec<String>, options: &VerificationOptions, runtime: &mut Runtime) -> bool {
let pacts = fetch_pacts(&source, consumers, runtime);
let mut verify_provider_result = true;
let mut all_errors: Vec<(String, MismatchResult)> = vec![];
for pact in pacts {
match pact {
Ok(ref res) => {
let pact = &res.0;
println!("\nVerifying a pact between {} and {}",
Style::new().bold().paint(pact.consumer.name.clone()),
Style::new().bold().paint(pact.provider.name.clone()));
if pact.interactions.is_empty() {
println!(" {}", Yellow.paint("WARNING: Pact file has no interactions"));
} else {
let errors = verify_pact(provider_info, filter, runtime, verify_provider_result, pact);
for error in errors.clone() {
all_errors.push(error);
}
if options.publish {
publish_result(&errors, &res.1, &options, runtime)
}
}
},
Err(err) => {
error!("Failed to load pact - {}", Red.paint(format!("{}", err)));
verify_provider_result = false;
all_errors.push((s!("Failed to load pact"), MismatchResult::Error(format!("{}", err), None)));
}
}
};
if !all_errors.is_empty() {
println!("\nFailures:\n");
for (i, &(ref description, ref mismatch)) in all_errors.iter().enumerate() {
match mismatch {
&MismatchResult::Error(ref err, _) => println!("{}) {} - {}\n", i, description, err),
&MismatchResult::Mismatches { ref mismatches, ref expected, ref actual, .. } => {
let mismatch = mismatches.first().unwrap();
println!("{}) {}{}", i, description, mismatch.summary());
for mismatch in mismatches {
println!(" {}\n", mismatch.ansi_description());
}
match mismatch {
&Mismatch::BodyMismatch{ref path, ..} => display_body_mismatch(expected, actual, path),
_ => ()
}
}
}
}
println!("\nThere were {} pact failures\n", all_errors.len());
}
verify_provider_result
}
fn fetch_pacts(source: &Vec<PactSource>, consumers: &Vec<String>, runtime: &mut Runtime) -> Vec<Result<(Pact, PactSource), String>> {
source.iter().flat_map(|s| {
match s {
&PactSource::File(ref file) => vec![Pact::read_pact(Path::new(&file))
.map_err(|err| format!("Failed to load pact '{}' - {}", file, err))
.map(|pact| (pact, s.clone()))],
&PactSource::Dir(ref dir) => match walkdir(Path::new(dir)) {
Ok(ref pacts) => pacts.iter().map(|p| {
match p {
&Ok(ref pact) => Ok((pact.clone(), s.clone())),
&Err(ref err) => Err(format!("Failed to load pact from '{}' - {}", dir, err))
}
}).collect(),
Err(err) => vec![Err(format!("Could not load pacts from directory '{}' - {}", dir, err))]
},
&PactSource::URL(ref url, ref auth) => vec![Pact::from_url(url, auth)
.map_err(|err| format!("Failed to load pact '{}' - {}", url, err))
.map(|pact| (pact, s.clone()))],
&PactSource::BrokerUrl(ref provider_name, ref broker_url, ref auth, _) => {
let future = pact_broker::fetch_pacts_from_broker(broker_url.clone(), provider_name.clone(), auth.clone());
match runtime.block_on(future) {
Ok(ref pacts) => pacts.iter().map(|p| {
match p {
&Ok((ref pact, ref links)) => {
debug!("Got pact with links {:?}", links);
Ok((pact.clone(), PactSource::BrokerUrl(provider_name.clone(), broker_url.clone(), auth.clone(), links.clone())))
},
&Err(ref err) => Err(format!("Failed to load pact from '{}' - {:?}", broker_url, err))
}
}).collect(),
Err(err) => vec![Err(format!("Could not load pacts from the pact broker '{}' - {:?}", broker_url, err))]
}
},
_ => vec![Err(format!("Could not load pacts, unknown pact source"))]
}
})
.filter(|res| filter_consumers(consumers, res))
.collect()
}
fn verify_pact(provider_info: &ProviderInfo, filter: &FilterInfo, runtime: &mut Runtime,
mut verify_provider_result: bool, pact: &Pact) -> Vec<(String, MismatchResult)> {
let mut errors = vec![];
let results: HashMap<Interaction, Result<(), MismatchResult>> = pact.interactions.iter()
.filter(|interaction| filter_interaction(interaction, filter))
.map(|interaction| {
(interaction.clone(), verify_interaction(provider_info, interaction, runtime))
}).collect();
for (interaction, result) in results.clone() {
let mut description = format!("Verifying a pact between {} and {}",
pact.consumer.name.clone(), pact.provider.name.clone());
if let Some((first, elements)) = interaction.provider_states.split_first() {
description.push_str(&format!(" Given {}", first.name));
for state in elements {
description.push_str(&format!(" And {}", state.name));
}
}
description.push_str(" - ");
description.push_str(&interaction.description);
println!(" {}", interaction.description);
match result {
Ok(()) => {
display_result(interaction.response.status, Green.paint("OK"),
interaction.response.headers.map(|h| h.iter().map(|(k, v)| {
(k.clone(), v.join(", "), Green.paint("OK"))
}).collect()), Green.paint("OK"))
},
Err(ref err) => match err {
&MismatchResult::Error(ref err_des, _) => {
println!(" {}", Red.paint(format!("Request Failed - {}", err_des)));
errors.push((description, err.clone()));
verify_provider_result = false;
},
&MismatchResult::Mismatches { ref mismatches, .. } => {
description.push_str(" returns a response which ");
let status_result = if mismatches.iter().any(|m| m.mismatch_type() == s!("StatusMismatch")) {
verify_provider_result = false;
Red.paint("FAILED")
} else {
Green.paint("OK")
};
let header_results = match interaction.response.headers {
Some(ref h) => Some(h.iter().map(|(k, v)| {
(k.clone(), v.join(", "), if mismatches.iter().any(|m| {
match m {
&Mismatch::HeaderMismatch { ref key, .. } => k == key,
_ => false
}
}) {
verify_provider_result = false;
Red.paint("FAILED")
} else {
Green.paint("OK")
})
}).collect()),
None => None
};
let body_result = if mismatches.iter().any(|m| m.mismatch_type() == s!("BodyMismatch") ||
m.mismatch_type() == s!("BodyTypeMismatch")) {
verify_provider_result = false;
Red.paint("FAILED")
} else {
Green.paint("OK")
};
display_result(interaction.response.status, status_result, header_results, body_result);
errors.push((description.clone(), err.clone()));
}
}
}
}
println!();
errors
}
fn publish_result(errors: &Vec<(String, MismatchResult)>, source: &PactSource,
options: &VerificationOptions, runtime: &mut Runtime) {
match source.clone() {
PactSource::BrokerUrl(_, broker_url, auth, links) => {
info!("Publishing verification results back to the Pact Broker");
let result = if errors.is_empty() {
debug!("Publishing a successful result to {}", source);
TestResult::Ok
} else {
debug!("Publishing a failure result to {}", source);
TestResult::Failed(errors.clone())
};
let provider_version = options.provider_version.clone().unwrap();
let future = publish_verification_results(links, broker_url.clone(), auth.clone(),
result, provider_version, options.build_url.clone());
match runtime.block_on(future) {
Ok(_) => info!("Results published to Pact Broker"),
Err(ref err) => error!("Publishing of verification results failed with an error: {}", err)
};
},
_ => ()
}
}
#[cfg(test)]
mod tests;