use std::collections::HashMap;
use bytes::Bytes;
#[cfg(test)]
#[allow(unused_imports)]
use env_logger;
use maplit::*;
use pact_models::bodies::OptionalBody;
use pact_models::content_types::ContentType;
use pact_models::expression_parser::DataType;
use pact_models::generators::{Generator, GeneratorCategory, Generators};
use pact_models::http_parts::HttpPart;
use pact_models::json_utils::body_from_json;
use pact_models::matchingrules::{Category, MatchingRules};
use pact_models::path_exp::DocPath;
use pact_models::request::Request;
use pact_models::v4::http_parts::HttpRequest;
use pact_models::v4::interaction::InteractionMarkup;
use pact_plugin_driver::catalogue_manager::find_content_matcher;
use pact_plugin_driver::content::PluginConfiguration;
#[cfg(test)]
use regex::Regex;
#[cfg(test)]
use serde_json::json;
use serde_json::Value;
use tracing::debug;
use crate::prelude::*;
use crate::util::GetDefaulting;
#[derive(Clone, Debug)]
pub struct RequestBuilder {
request: HttpRequest,
plugin_config: HashMap<String, PluginConfiguration>,
interaction_markup: InteractionMarkup
}
impl RequestBuilder {
pub fn method<M: Into<String>>(&mut self, method: M) -> &mut Self {
self.request.method = method.into();
self
}
pub fn get(&mut self) -> &mut Self {
self.method("GET")
}
pub fn post(&mut self) -> &mut Self {
self.method("POST")
}
pub fn put(&mut self) -> &mut Self {
self.method("PUT")
}
pub fn delete(&mut self) -> &mut Self {
self.method("DELETE")
}
pub fn path<P: Into<StringPattern>>(&mut self, path: P) -> &mut Self {
let path = path.into();
self.request.path = path.to_example();
path.extract_matching_rules(
DocPath::empty(),
self.request.matching_rules.add_category(Category::PATH),
);
self
}
pub fn path_from_provider_state<E, P: Into<StringPattern>>(&mut self, expression: E, path: P) -> &mut Self
where
E: Into<String>
{
let path = path.into();
let expression = expression.into();
self.path(path);
{
let generators = self.generators();
generators.add_generator(&GeneratorCategory::PATH, Generator::ProviderStateGenerator(expression, Some(DataType::STRING)))
}
self
}
pub fn query_param<K, V>(&mut self, key: K, value: V) -> &mut Self
where
K: Into<String>,
V: Into<StringPattern>,
{
let key = key.into();
let value = value.into();
self.request
.query
.get_defaulting()
.entry(key.clone())
.or_insert_with(Default::default)
.push(value.to_example());
let mut path = DocPath::root();
path.push_field(key);
value.extract_matching_rules(
path,
self.request.matching_rules.add_category("query"),
);
self
}
pub fn build(&self) -> Request {
self.request.as_v3_request()
}
pub fn build_v4(&self) -> HttpRequest {
self.request.clone()
}
fn setup_core_matcher(&mut self, content_type: &ContentType, definition: Value) {
match definition {
Value::String(s) => self.request.body = OptionalBody::Present(Bytes::from(s), Some(content_type.clone()), None),
Value::Object(ref o) => if o.contains_key("contents") {
self.request.body = body_from_json(&definition, "contents", &None);
}
_ => {}
}
}
pub async fn contents(&mut self, content_type: ContentType, definition: Value) -> &mut Self {
match find_content_matcher(&content_type) {
Some(matcher) => {
debug!("Found a matcher for '{}': {:?}", content_type, matcher);
if matcher.is_core() {
debug!("Matcher is from the core framework");
self.setup_core_matcher(&content_type, definition);
} else {
let request = &mut self.request;
debug!("Plugin matcher, will get the plugin to provide the request contents");
match definition {
Value::Object(attributes) => {
let map = attributes.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
match matcher.configure_interation(&content_type, map).await {
Ok((contents, plugin_config)) => {
debug!("Interaction contents = {:?}", contents);
debug!("Interaction plugin_config = {:?}", plugin_config);
if let Some(contents) = contents.first() {
request.body = contents.body.clone();
if !request.has_header("content-type") {
request.add_header("content-type", vec![content_type.to_string().as_str()]);
}
if let Some(rules) = &contents.rules {
request.matching_rules.add_rules("body", rules.clone());
}
if let Some(generators) = &contents.generators {
request.generators.add_generators(generators.clone());
}
if !contents.plugin_config.is_empty() {
self.plugin_config.insert(matcher.plugin_name(), contents.plugin_config.clone());
}
self.interaction_markup = InteractionMarkup {
markup: contents.interaction_markup.clone(),
markup_type: contents.interaction_markup_type.clone()
};
}
if let Some(plugin_config) = plugin_config {
let plugin_name = matcher.plugin_name();
if self.plugin_config.contains_key(&*plugin_name) {
let entry = self.plugin_config.get_mut(&*plugin_name).unwrap();
for (k, v) in plugin_config.pact_configuration {
entry.pact_configuration.insert(k.clone(), v.clone());
}
} else {
self.plugin_config.insert(plugin_name.to_string(), plugin_config.clone());
}
}
}
Err(err) => panic!("Failed to call out to plugin - {}", err)
}
}
_ => panic!("{} is not a valid value for contents", definition)
}
}
}
None => {
debug!("No matcher was found, will default to the core framework");
self.setup_core_matcher(&content_type, definition);
}
}
self
}
pub(crate) fn plugin_config(&self) -> HashMap<String, PluginConfiguration> {
self.plugin_config.clone()
}
pub(crate) fn interaction_markup(&self) -> InteractionMarkup {
self.interaction_markup.clone()
}
}
impl Default for RequestBuilder {
fn default() -> Self {
RequestBuilder {
request: HttpRequest::default(),
plugin_config: Default::default(),
interaction_markup: Default::default()
}
}
}
impl HttpPartBuilder for RequestBuilder {
fn headers_and_matching_rules_mut(&mut self) -> (&mut HashMap<String, Vec<String>>, &mut MatchingRules) {
(
self.request.headers.get_or_insert(hashmap!{}),
&mut self.request.matching_rules,
)
}
fn generators(&mut self) -> &mut Generators {
&mut self.request.generators
}
fn body_and_matching_rules_mut(&mut self) -> (&mut OptionalBody, &mut MatchingRules) {
(
&mut self.request.body,
&mut self.request.matching_rules,
)
}
}
#[test]
fn path_pattern() {
let greeting_regex = Regex::new("/greeting/.*").unwrap();
let pattern = PactBuilder::new("C", "P")
.interaction("I", "", |mut i| {
i.request.path(Term::new(greeting_regex, "/greeting/hello"));
i
})
.build();
let good = PactBuilder::new("C", "P")
.interaction("I", "", |mut i| { i.request.path("/greeting/hi"); i })
.build();
let bad = PactBuilder::new("C", "P")
.interaction("I", "", |mut i| { i.request.path("/farewell/bye"); i })
.build();
assert_requests_match!(good, pattern);
assert_requests_do_not_match!(bad, pattern);
}
#[test]
fn path_generator() {
let actual = PactBuilder::new("C", "P")
.interaction("I", "", |mut i| {
i.request.path_from_provider_state("/greeting/${greeting}", "/greeting/hi");
i
})
.build();
let expected = PactBuilder::new("C", "P")
.interaction("I", "", |mut i| {
i.request.path("/greeting/hello");
i
})
.build();
let good_context = &mut HashMap::new();
good_context.insert("greeting", json!("hello"));
assert_requests_with_context_match!(actual, expected, good_context);
let bad_context = &mut HashMap::new();
bad_context.insert("greeting", json!("goodbye"));
assert_requests_with_context_do_not_match!(actual, expected, bad_context);
}
#[test]
fn query_param_pattern() {
let pattern = PactBuilder::new("C", "P")
.interaction("I", "", |mut i| {
i.request.query_param("greeting", term!("^h.*$", "hello"));
i
})
.build();
let good = PactBuilder::new("C", "P")
.interaction("I", "", |mut i| { i.request.query_param("greeting", "hi"); i })
.build();
let bad = PactBuilder::new("C", "P")
.interaction("I", "", |mut i| { i.request.query_param("greeting", "bye"); i })
.build();
assert_requests_match!(good, pattern);
assert_requests_do_not_match!(bad, pattern);
}
#[test]
fn query_param_with_underscore() {
let pattern = PactBuilder::new("C", "P")
.interaction("get a user", "", |mut i| {
i.request
.path("/users")
.query_param("user_id", term!("^[0-9]+$", "1"));
i
})
.build();
let good = PactBuilder::new("C", "P")
.interaction("I", "", |mut i| {
i.request
.path("/users")
.query_param("user_id", "2");
i
})
.build();
assert_requests_match!(good, pattern);
}
#[test]
fn term_does_not_require_anchors() {
use crate::prelude::*;
let pattern = PactBuilder::new("C", "P")
.interaction("I", "", |mut i| {
i.request.path(term!("^/users/[0-9]+$", "/users/12"));
i
})
.build();
let good = PactBuilder::new("C", "P")
.interaction("I", "", |mut i| { i.request.path("/users/2"); i })
.build();
let bad1 = PactBuilder::new("C", "P")
.interaction("I", "", |mut i| { i.request.path("/users/2/posts"); i })
.build();
let bad2 = PactBuilder::new("C", "P")
.interaction("I", "", |mut i| { i.request.path("/account/1/users/2"); i })
.build();
assert_requests_match!(good, pattern);
assert_requests_do_not_match!(bad1, pattern);
assert_requests_do_not_match!(bad2, pattern);
}