use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::ffi::{CStr, CString};
use std::path::PathBuf;
use std::ptr::null_mut;
use std::str::from_utf8;
use std::sync::{Arc, Mutex};
use anyhow::anyhow;
use bytes::Bytes;
use either::Either;
use itertools::Itertools;
use lazy_static::*;
use libc::{c_char, c_uint, c_ushort, size_t};
use maplit::*;
use pact_models::{Consumer, PactSpecification, Provider};
use pact_models::bodies::OptionalBody;
use pact_models::content_types::{ContentType, JSON, TEXT, XML};
use pact_models::generators::{Generator, GeneratorCategory, Generators};
use pact_models::http_parts::HttpPart;
use pact_models::interaction::Interaction;
use pact_models::json_utils::json_to_string;
use pact_models::matchingrules::{Category, MatchingRule, MatchingRuleCategory, MatchingRules, RuleLogic};
use pact_models::pact::{ReadWritePact, write_pact};
use pact_models::path_exp::DocPath;
use pact_models::prelude::Pact;
use pact_models::prelude::v4::V4Pact;
use pact_models::provider_states::ProviderState;
use pact_models::v4::async_message::AsynchronousMessage;
use pact_models::v4::interaction::V4Interaction;
use pact_models::v4::message_parts::MessageContents;
use pact_models::v4::sync_message::SynchronousMessage;
use pact_models::v4::synch_http::SynchronousHttp;
use serde_json::{json, Value};
use tracing::*;
use crate::{convert_cstr, ffi_fn, safe_str};
use crate::mock_server::{StringResult, xml};
#[allow(deprecated)]
use crate::mock_server::bodies::{
empty_multipart_body,
file_as_multipart_body,
matcher_from_integration_json,
MultipartBody,
process_array,
process_json,
process_object,
request_multipart,
response_multipart
};
use crate::models::iterators::{PactMessageIterator, PactSyncMessageIterator, PactSyncHttpIterator};
use crate::ptr;
#[derive(Debug, Clone)]
pub struct PactHandleInner {
pub(crate) pact: V4Pact,
pub(crate) mock_server_started: bool,
pub(crate) specification_version: PactSpecification
}
lazy_static! {
static ref PACT_HANDLES: Arc<Mutex<HashMap<u16, RefCell<PactHandleInner>>>> = Arc::new(Mutex::new(hashmap![]));
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy)]
pub struct PactHandle {
pact_ref: u16
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy)]
pub struct InteractionHandle {
interaction_ref: u32
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub enum InteractionPart {
Request,
Response
}
impl PactHandle {
pub fn new(consumer: &str, provider: &str) -> Self {
let mut handles = PACT_HANDLES.lock().unwrap();
let keys: HashSet<&u16> = handles.keys().collect();
let mut id: u16 = 1;
while keys.contains(&id) {
id = id + 1;
}
let mut pact = V4Pact {
consumer: Consumer { name: consumer.to_string() },
provider: Provider { name: provider.to_string() },
..V4Pact::default()
};
pact.add_md_version("ffi", option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"));
handles.insert(id, RefCell::new(PactHandleInner {
pact,
mock_server_started: false,
specification_version: PactSpecification::V3
}));
PactHandle {
pact_ref: id
}
}
pub(crate) fn with_pact<R>(&self, f: &dyn Fn(u16, &mut PactHandleInner) -> R) -> Option<R> {
let mut handles = PACT_HANDLES.lock().unwrap();
trace!("with_pact - ref = {}, keys = {:?}", self.pact_ref, handles.keys());
handles.get_mut(&self.pact_ref).map(|inner| {
trace!("with_pact before - ref = {}, inner = {:?}", self.pact_ref, inner);
let result = f(self.pact_ref - 1, &mut inner.borrow_mut());
trace!("with_pact after - ref = {}, inner = {:?}", self.pact_ref, inner);
result
})
}
}
impl InteractionHandle {
pub fn new(pact: PactHandle, interaction: u16) -> InteractionHandle {
let mut index = pact.pact_ref as u32;
index = index << 16;
index = index + interaction as u32;
InteractionHandle {
interaction_ref: index
}
}
pub fn with_pact<R>(&self, f: &dyn Fn(u16, &mut PactHandleInner) -> R) -> Option<R> {
let mut handles = PACT_HANDLES.lock().unwrap();
let index = (self.interaction_ref >> 16) as u16;
handles.get_mut(&index).map(|inner| f(index - 1, &mut inner.borrow_mut()))
}
pub fn with_interaction<R>(&self, f: &dyn Fn(u16, bool, &mut dyn V4Interaction) -> R) -> Option<R> {
let mut handles = PACT_HANDLES.lock().unwrap();
let index = (self.interaction_ref >> 16) as u16;
let interaction = (self.interaction_ref & 0x0000FFFF) as u16;
trace!("with_interaction - index = {}, interaction = {}", index, interaction);
trace!("with_interaction - keys = {:?}", handles.keys());
handles.get_mut(&index).map(|inner| {
let inner_mut = &mut *inner.borrow_mut();
trace!("with_interaction - inner = {:?}", inner_mut);
let interactions = &mut inner_mut.pact.interactions;
match interactions.get_mut((interaction - 1) as usize) {
Some(inner_i) => {
Some(f(interaction - 1, inner_mut.mock_server_started, inner_i.as_mut()))
},
None => {
debug!("Did not find interaction for index = {}, interaction = {}, pact has {} interactions",
index, interaction, interactions.len());
None
}
}
}).flatten()
}
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy)]
pub struct MessagePactHandle {
pact_ref: u16
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy)]
pub struct MessageHandle {
interaction_ref: u32
}
impl MessagePactHandle {
pub fn new(consumer: &str, provider: &str) -> Self {
let mut handles = PACT_HANDLES.lock().unwrap();
let id = (handles.len() + 1) as u16;
let mut pact = V4Pact {
consumer: Consumer { name: consumer.to_string() },
provider: Provider { name: provider.to_string() },
..V4Pact::default()
};
pact.add_md_version("ffi", option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"));
handles.insert(id, RefCell::new(PactHandleInner {
pact,
mock_server_started: false,
specification_version: PactSpecification::V3
}));
MessagePactHandle {
pact_ref: id
}
}
pub fn with_pact<R>(&self, f: &dyn Fn(u16, &mut V4Pact, PactSpecification) -> R) -> Option<R> {
let mut handles = PACT_HANDLES.lock().unwrap();
handles.get_mut(&self.pact_ref).map(|inner| {
let mut ref_mut = inner.borrow_mut();
let specification = ref_mut.specification_version;
f(self.pact_ref - 1, &mut ref_mut.pact, specification)
})
}
}
impl MessageHandle {
pub fn new(pact: MessagePactHandle, message: u16) -> MessageHandle {
let mut index = pact.pact_ref as u32;
index = index << 16;
index = index + message as u32;
MessageHandle {
interaction_ref: index
}
}
pub fn new_v4(pact: PactHandle, message: usize) -> MessageHandle {
let mut index = pact.pact_ref as u32;
index = index << 16;
index = index + message as u32;
MessageHandle {
interaction_ref: index
}
}
pub fn with_pact<R>(&self, f: &dyn Fn(u16, &mut V4Pact, PactSpecification) -> R) -> Option<R> {
let mut handles = PACT_HANDLES.lock().unwrap();
let index = self.interaction_ref as u16;
handles.get_mut(&index).map(|inner| {
let mut ref_mut = inner.borrow_mut();
let specification = ref_mut.specification_version;
f(index - 1, & mut ref_mut.pact, specification)
})
}
pub fn with_message<R>(&self, f: &dyn Fn(u16, &mut dyn V4Interaction, PactSpecification) -> R) -> Option<R> {
let mut handles = PACT_HANDLES.lock().unwrap();
let index = (self.interaction_ref >> 16) as u16;
let interaction = self.interaction_ref as u16;
handles.get_mut(&index).map(|inner| {
let mut ref_mut = inner.borrow_mut();
let specification = ref_mut.specification_version;
ref_mut.pact.interactions.get_mut((interaction - 1) as usize)
.map(|inner_i| {
if inner_i.is_message() {
Some(f(interaction - 1, inner_i.as_mut(), specification))
} else {
error!("Interaction {:#x} is not a message interaction, it is {}", self.interaction_ref, inner_i.type_of());
None
}
}).flatten()
}).flatten()
}
}
#[no_mangle]
pub extern fn pactffi_new_pact(consumer_name: *const c_char, provider_name: *const c_char) -> PactHandle {
let consumer = convert_cstr("consumer_name", consumer_name).unwrap_or("Consumer");
let provider = convert_cstr("provider_name", provider_name).unwrap_or("Provider");
PactHandle::new(consumer, provider)
}
#[no_mangle]
pub extern fn pactffi_new_interaction(pact: PactHandle, description: *const c_char) -> InteractionHandle {
if let Some(description) = convert_cstr("description", description) {
pact.with_pact(&|_, inner| {
let interaction = SynchronousHttp {
description: description.to_string(),
..SynchronousHttp::default()
};
inner.pact.interactions.push(interaction.boxed_v4());
InteractionHandle::new(pact, inner.pact.interactions.len() as u16)
}).unwrap_or_else(|| InteractionHandle::new(pact, 0))
} else {
InteractionHandle::new(pact, 0)
}
}
#[no_mangle]
pub extern fn pactffi_new_message_interaction(pact: PactHandle, description: *const c_char) -> InteractionHandle {
if let Some(description) = convert_cstr("description", description) {
pact.with_pact(&|_, inner| {
let interaction = AsynchronousMessage {
description: description.to_string(),
..AsynchronousMessage::default()
};
inner.pact.interactions.push(interaction.boxed_v4());
InteractionHandle::new(pact, inner.pact.interactions.len() as u16)
}).unwrap_or_else(|| InteractionHandle::new(pact, 0))
} else {
InteractionHandle::new(pact, 0)
}
}
#[no_mangle]
pub extern fn pactffi_new_sync_message_interaction(pact: PactHandle, description: *const c_char) -> InteractionHandle {
if let Some(description) = convert_cstr("description", description) {
pact.with_pact(&|_, inner| {
let interaction = SynchronousMessage {
description: description.to_string(),
..SynchronousMessage::default()
};
inner.pact.interactions.push(interaction.boxed_v4());
InteractionHandle::new(pact, inner.pact.interactions.len() as u16)
}).unwrap_or_else(|| InteractionHandle::new(pact, 0))
} else {
InteractionHandle::new(pact, 0)
}
}
#[no_mangle]
pub extern fn pactffi_upon_receiving(interaction: InteractionHandle, description: *const c_char) -> bool {
if let Some(description) = convert_cstr("description", description) {
interaction.with_interaction(&|_, mock_server_started, inner| {
inner.set_description(description);
!mock_server_started
}).unwrap_or(false)
} else {
false
}
}
#[no_mangle]
pub extern fn pactffi_given(interaction: InteractionHandle, description: *const c_char) -> bool {
if let Some(description) = convert_cstr("description", description) {
interaction.with_interaction(&|_, mock_server_started, inner| {
inner.provider_states_mut().push(ProviderState::default(&description.to_string()));
!mock_server_started
}).unwrap_or(false)
} else {
false
}
}
ffi_fn! {
fn pactffi_interaction_test_name(interaction: InteractionHandle, test_name: *const c_char) -> c_uint {
let test_name = safe_str!(test_name);
interaction.with_interaction(&|_, started, inner| {
if !started {
if let Some(i) = inner.as_v4_mut() {
i.comments_mut().insert("testname".to_string(), json!(test_name));
0
} else {
4
}
} else {
3
}
}).unwrap_or(2)
} {
1
}
}
#[no_mangle]
pub extern fn pactffi_given_with_param(interaction: InteractionHandle, description: *const c_char,
name: *const c_char, value: *const c_char) -> bool {
if let Some(description) = convert_cstr("description", description) {
if let Some(name) = convert_cstr("name", name) {
let value = convert_cstr("value", value).unwrap_or_default();
interaction.with_interaction(&|_, mock_server_started, inner| {
let value = match serde_json::from_str(value) {
Ok(json) => json,
Err(_) => json!(value)
};
match inner.provider_states().iter().find_position(|state| state.name == description) {
Some((index, _)) => {
inner.provider_states_mut().get_mut(index).unwrap().params.insert(name.to_string(), value);
},
None => inner.provider_states_mut().push(ProviderState {
name: description.to_string(),
params: hashmap!{ name.to_string() => value }
})
};
!mock_server_started
}).unwrap_or(false)
} else {
false
}
} else {
false
}
}
#[no_mangle]
pub extern fn pactffi_with_request(
interaction: InteractionHandle,
method: *const c_char,
path: *const c_char
) -> bool {
let method = convert_cstr("method", method).unwrap_or("GET");
let path = convert_cstr("path", path).unwrap_or("/");
interaction.with_interaction(&|_, mock_server_started, inner| {
if let Some(reqres) = inner.as_v4_http_mut() {
let path = from_integration_json_v2(&mut reqres.request.matching_rules,
&mut reqres.request.generators, &path.to_string(), DocPath::empty(), "path", 0);
reqres.request.method = method.to_string();
reqres.request.path = match path {
Either::Left(value) => value,
Either::Right(values) => {
warn!("Received multiple values for the path ({:?}), will only use the first one", values);
values.first().cloned().unwrap_or_default()
}
};
!mock_server_started
} else {
error!("Interaction is not an HTTP interaction, is {}", inner.type_of());
false
}
}).unwrap_or(false)
}
#[no_mangle]
#[deprecated]
pub extern fn pactffi_with_query_parameter(
interaction: InteractionHandle,
name: *const c_char,
index: size_t,
value: *const c_char
) -> bool {
if let Some(name) = convert_cstr("name", name) {
let value = convert_cstr("value", value).unwrap_or_default();
interaction.with_interaction(&|_, mock_server_started, inner| {
if let Some(reqres) = inner.as_v4_http_mut() {
reqres.request.query = reqres.request.query.clone().map(|mut q| {
let mut path = DocPath::root();
path.push_field(name).push_index(index);
#[allow(deprecated)]
let value = from_integration_json(&mut reqres.request.matching_rules, &mut reqres.request.generators, &value.to_string(), path, "query");
if q.contains_key(name) {
let values = q.get_mut(name).unwrap();
if index >= values.len() {
values.resize_with(index + 1, Default::default);
}
values[index] = value;
} else {
let mut values: Vec<String> = Vec::new();
values.resize_with(index + 1, Default::default);
values[index] = value;
q.insert(name.to_string(), values);
};
q
}).or_else(|| {
let mut path = DocPath::root();
path.push_field(name).push_index(index);
#[allow(deprecated)]
let value = from_integration_json(&mut reqres.request.matching_rules, &mut reqres.request.generators, &value.to_string(), path, "query");
let mut values: Vec<String> = Vec::new();
values.resize_with(index + 1, Default::default);
values[index] = value;
Some(hashmap! { name.to_string() => values })
});
!mock_server_started
} else {
error!("Interaction is not an HTTP interaction, is {}", inner.type_of());
false
}
}).unwrap_or(false)
} else {
warn!("Ignoring query parameter with empty or null name");
false
}
}
#[no_mangle]
pub extern fn pactffi_with_query_parameter_v2(
interaction: InteractionHandle,
name: *const c_char,
index: size_t,
value: *const c_char
) -> bool {
if let Some(name) = convert_cstr("name", name) {
let value = convert_cstr("value", value).unwrap_or_default();
interaction.with_interaction(&|_, mock_server_started, inner| {
if let Some(reqres) = inner.as_v4_http_mut() {
let mut path = DocPath::root();
path.push_field(name);
let value = from_integration_json_v2(
&mut reqres.request.matching_rules,
&mut reqres.request.generators,
&value.to_string(),
path,
"query",
index
);
match value {
Either::Left(value) => {
reqres.request.query = update_query_map(index, name, reqres, &value);
}
Either::Right(values) => if index == 0 {
reqres.request.query = reqres.request.query.clone().map(|mut q| {
if q.contains_key(name) {
let vec = q.get_mut(name).unwrap();
vec.extend_from_slice(&values);
} else {
q.insert(name.to_string(), values.clone());
};
q
}).or_else(|| Some(hashmap! { name.to_string() => values }))
} else {
reqres.request.query = update_query_map(index, name, reqres, &values.first().cloned().unwrap_or_default());
}
}
!mock_server_started
} else {
error!("Interaction is not an HTTP interaction, is {}", inner.type_of());
false
}
}).unwrap_or(false)
} else {
warn!("Ignoring query parameter with empty or null name");
false
}
}
fn update_query_map(index: size_t, name: &str, reqres: &mut SynchronousHttp, value: &String) -> Option<HashMap<String, Vec<String>>> {
reqres.request.query.clone().map(|mut q| {
if q.contains_key(name) {
let values = q.get_mut(name).unwrap();
if index >= values.len() {
values.resize_with(index + 1, Default::default);
}
values[index] = value.clone();
} else {
let mut values: Vec<String> = Vec::new();
values.resize_with(index + 1, Default::default);
values[index] = value.clone();
q.insert(name.to_string(), values);
};
q
}).or_else(|| {
let mut values: Vec<String> = Vec::new();
values.resize_with(index + 1, Default::default);
values[index] = value.clone();
Some(hashmap! { name.to_string() => values })
})
}
#[deprecated]
fn from_integration_json(
rules: &mut MatchingRules,
generators: &mut Generators,
value: &str,
path: DocPath,
category: &str,
) -> String {
let category = rules.add_category(category);
match serde_json::from_str(value) {
Ok(json) => match json {
Value::Object(ref map) => {
let json: Value = process_object(map, category, generators, path, false);
json_to_string(&json)
},
_ => value.to_string()
},
Err(_) => value.to_string()
}
}
fn from_integration_json_v2(
rules: &mut MatchingRules,
generators: &mut Generators,
value: &str,
path: DocPath,
category: &str,
index: usize
) -> Either<String, Vec<String>> {
let matching_rules = rules.add_category(category);
let mut path = path.clone();
match serde_json::from_str(value) {
Ok(json) => match json {
Value::Object(ref map) => {
let result = if map.contains_key("pact:matcher:type") {
debug!("detected pact:matcher:type, will configure a matcher");
#[allow(deprecated)]
let matching_rule = matcher_from_integration_json(map);
trace!("matching_rule = {matching_rule:?}");
let (path, result_value) = match map.get("value") {
Some(val) => match val {
Value::Array(array) => {
let array = process_array(&array, matching_rules, generators, path.clone(), true, false);
(path.clone(), array)
},
_ => (path.push_index(index).clone(), val.clone())
},
None => (path.push_index(index).clone(), Value::Null)
};
if let Some(rule) = &matching_rule {
let path = if matching_rules.name == Category::PATH {
path.parent().unwrap_or(DocPath::root())
} else {
path.clone()
};
matching_rules.add_rule(path, rule.clone(), RuleLogic::And);
}
if let Some(gen) = map.get("pact:generator:type") {
debug!("detected pact:generator:type, will configure a generators");
if let Some(generator) = Generator::from_map(&json_to_string(gen), map) {
let category = match matching_rules.name {
Category::BODY => &GeneratorCategory::BODY,
Category::HEADER => &GeneratorCategory::HEADER,
Category::PATH => &GeneratorCategory::PATH,
Category::QUERY => &GeneratorCategory::QUERY,
_ => {
warn!("invalid generator category {} provided, defaulting to body", matching_rules.name);
&GeneratorCategory::BODY
}
};
let path = if matching_rules.name == Category::PATH {
path.parent().unwrap_or(DocPath::root())
} else {
path.clone()
};
generators.add_generator_with_subcategory(category, path.clone(), generator);
}
}
result_value
} else {
debug!("Configuring a normal value using the 'value' attribute");
map.get("value").cloned().unwrap_or_default()
};
match result {
Value::Array(values) => Either::Right(values.iter().map(|v| json_to_string(v)).collect()),
_ => {
Either::Left(json_to_string(&result))
}
}
},
_ => Either::Left(value.to_string())
},
Err(_) => Either::Left(value.to_string())
}
}
pub(crate) fn process_xml(body: String, matching_rules: &mut MatchingRuleCategory, generators: &mut Generators) -> Result<Vec<u8>, String> {
trace!("process_xml");
match serde_json::from_str(&body) {
Ok(json) => match json {
Value::Object(ref map) => xml::generate_xml_body(map, matching_rules, generators),
_ => Err(format!("JSON document is invalid (expected an Object), have {}", json))
},
Err(err) => Err(format!("Failed to parse XML builder document: {}", err))
}
}
#[no_mangle]
pub extern fn pactffi_with_specification(pact: PactHandle, version: PactSpecification) -> bool {
pact.with_pact(&|_, inner| {
inner.specification_version = version.into();
!inner.mock_server_started
}).unwrap_or(false)
}
#[no_mangle]
pub extern fn pactffi_with_pact_metadata(
pact: PactHandle,
namespace: *const c_char,
name: *const c_char,
value: *const c_char
) -> bool {
pact.with_pact(&|_, inner| {
let namespace = convert_cstr("namespace", namespace).unwrap_or_default();
let name = convert_cstr("name", name).unwrap_or_default();
let value = convert_cstr("value", value).unwrap_or_default();
if !namespace.is_empty() {
inner.pact.metadata.insert(namespace.to_string(), json!({ name: value }));
} else {
warn!("no namespace provided for metadata {:?} => {:?}. Ignoring", name, value);
}
!inner.mock_server_started
}).unwrap_or(false)
}
#[deprecated]
#[no_mangle]
pub extern fn pactffi_with_header(
interaction: InteractionHandle,
part: InteractionPart,
name: *const c_char,
index: size_t,
value: *const c_char
) -> bool {
if let Some(name) = convert_cstr("name", name) {
let value = convert_cstr("value", value).unwrap_or_default();
interaction.with_interaction(&|_, mock_server_started, inner| {
if let Some(reqres) = inner.as_v4_http_mut() {
let headers = match part {
InteractionPart::Request => reqres.request.headers.clone(),
InteractionPart::Response => reqres.response.headers.clone()
};
let mut path = DocPath::root();
path.push_field(name);
#[allow(deprecated)]
let value = match part {
InteractionPart::Request => from_integration_json(
&mut reqres.request.matching_rules,
&mut reqres.request.generators,
&value.to_string(),
path,
"header"),
InteractionPart::Response => from_integration_json(
&mut reqres.response.matching_rules,
&mut reqres.response.generators,
&value.to_string(),
path,
"header")
};
let updated_headers = headers.map(|mut h| {
if h.contains_key(name) {
let values = h.get_mut(name).unwrap();
if index >= values.len() {
values.resize_with(index + 1, Default::default);
}
values[index] = value.to_string();
} else {
let mut values: Vec<String> = Vec::new();
values.resize_with(index + 1, Default::default);
values[index] = value.to_string();
h.insert(name.to_string(), values);
};
h
}).or_else(|| {
let mut values: Vec<String> = Vec::new();
values.resize_with(index + 1, Default::default);
values[index] = value.to_string();
Some(hashmap! { name.to_string() => values })
});
match part {
InteractionPart::Request => reqres.request.headers = updated_headers,
InteractionPart::Response => reqres.response.headers = updated_headers
};
!mock_server_started
} else {
error!("Interaction is not an HTTP interaction, is {}", inner.type_of());
false
}
}).unwrap_or(false)
} else {
warn!("Ignoring header with empty or null name");
false
}
}
#[no_mangle]
pub extern fn pactffi_with_header_v2(
interaction: InteractionHandle,
part: InteractionPart,
name: *const c_char,
index: size_t,
value: *const c_char
) -> bool {
if let Some(name) = convert_cstr("name", name) {
let value = convert_cstr("value", value).unwrap_or_default();
interaction.with_interaction(&|_, mock_server_started, inner| {
if let Some(reqres) = inner.as_v4_http_mut() {
let headers = match part {
InteractionPart::Request => reqres.request.headers.clone(),
InteractionPart::Response => reqres.response.headers.clone()
};
let mut path = DocPath::root();
path.push_field(name);
let value = match part {
InteractionPart::Request => from_integration_json_v2(
&mut reqres.request.matching_rules,
&mut reqres.request.generators,
&value.to_string(),
path,
"header",
index
),
InteractionPart::Response => from_integration_json_v2(
&mut reqres.response.matching_rules,
&mut reqres.response.generators,
&value.to_string(),
path,
"header",
index
)
};
let updated_headers = headers.map(|mut h| {
let entry = h.entry(name.to_string()).or_default();
match &value {
Either::Left(value) => {
if index >= entry.len() {
entry.resize_with(index + 1, Default::default);
}
entry[index] = value.clone();
}
Either::Right(values) => {
entry.extend_from_slice(values);
}
}
h
}).or_else(|| {
let values = match &value {
Either::Left(value) => vec![value.clone()],
Either::Right(values) => values.clone()
};
Some(hashmap! { name.to_string() => values })
});
match part {
InteractionPart::Request => reqres.request.headers = updated_headers,
InteractionPart::Response => reqres.response.headers = updated_headers
};
!mock_server_started
} else {
error!("Interaction is not an HTTP interaction, is {}", inner.type_of());
false
}
}).unwrap_or(false)
} else {
warn!("Ignoring header with empty or null name");
false
}
}
#[no_mangle]
pub extern fn pactffi_response_status(interaction: InteractionHandle, status: c_ushort) -> bool {
interaction.with_interaction(&|_, mock_server_started, inner| {
if let Some(reqres) = inner.as_v4_http_mut() {
reqres.response.status = status;
!mock_server_started
} else {
error!("Interaction is not an HTTP interaction, is {}", inner.type_of());
false
}
}).unwrap_or(false)
}
#[no_mangle]
pub extern fn pactffi_with_body(
interaction: InteractionHandle,
part: InteractionPart,
content_type: *const c_char,
body: *const c_char
) -> bool {
let content_type = convert_cstr("content_type", content_type).unwrap_or("text/plain");
let body = convert_cstr("body", body).unwrap_or_default();
let content_type_header = "Content-Type".to_string();
interaction.with_interaction(&|_, mock_server_started, inner| {
if let Some(reqres) = inner.as_v4_http_mut() {
match part {
InteractionPart::Request => {
if !reqres.request.has_header(&content_type_header) {
match reqres.request.headers {
Some(ref mut headers) => {
headers.insert(content_type_header.clone(), vec![content_type.to_string()]);
},
None => {
reqres.request.headers = Some(hashmap! { content_type_header.clone() => vec![ content_type.to_string() ]});
}
}
}
let body = if reqres.request.content_type().unwrap_or_default().is_json() {
let category = reqres.request.matching_rules.add_category("body");
OptionalBody::Present(Bytes::from(process_json(body.to_string(), category, &mut reqres.request.generators)),
Some(ContentType::parse(content_type).unwrap()), None)
} else if reqres.request.content_type().unwrap_or_default().is_xml() {
let category = reqres.request.matching_rules.add_category("body");
OptionalBody::Present(Bytes::from(process_xml(body.to_string(), category, &mut reqres.request.generators).unwrap_or(vec![])),
Some(XML.clone()), None)
} else {
OptionalBody::from(body)
};
reqres.request.body = body;
},
InteractionPart::Response => {
if !reqres.response.has_header(&content_type_header) {
match reqres.response.headers {
Some(ref mut headers) => {
headers.insert(content_type_header.clone(), vec![content_type.to_string()]);
},
None => {
reqres.response.headers = Some(hashmap! { content_type_header.clone() => vec![ content_type.to_string() ]});
}
}
}
let body = if reqres.response.content_type().unwrap_or_default().is_json() {
let category = reqres.response.matching_rules.add_category("body");
OptionalBody::Present(Bytes::from(process_json(body.to_string(), category, &mut reqres.response.generators)),
Some(JSON.clone()), None)
} else if reqres.response.content_type().unwrap_or_default().is_xml() {
let category = reqres.response.matching_rules.add_category("body");
OptionalBody::Present(Bytes::from(process_xml(body.to_string(), category, &mut reqres.response.generators).unwrap_or(vec![])),
Some(XML.clone()), None)
} else {
OptionalBody::from(body)
};
reqres.response.body = body;
}
};
!mock_server_started
} else if let Some(message) = inner.as_v4_async_message_mut() {
let ct = ContentType::parse(content_type).unwrap_or_else(|_| TEXT.clone());
let body = if ct.is_json() {
let category = message.contents.matching_rules.add_category("body");
OptionalBody::Present(Bytes::from(process_json(body.to_string(), category, &mut message.contents.generators)),
Some(JSON.clone()), None)
} else if ct.is_xml() {
let category = message.contents.matching_rules.add_category("body");
OptionalBody::Present(Bytes::from(process_xml(body.to_string(), category, &mut message.contents.generators).unwrap_or(vec![])),
Some(XML.clone()), None)
} else {
OptionalBody::from(body)
};
message.contents.contents = body;
message.contents.metadata.insert("contentType".to_string(), json!(content_type));
true
} else if let Some(message) = inner.as_v4_sync_message_mut() {
let ct = ContentType::parse(content_type).unwrap_or_else(|_| TEXT.clone());
match part {
InteractionPart::Request => {
let category = message.request.matching_rules.add_category("body");
let body = if ct.is_json() {
OptionalBody::Present(Bytes::from(process_json(body.to_string(), category, &mut message.request.generators)),
Some(JSON.clone()), None)
} else if ct.is_xml() {
OptionalBody::Present(Bytes::from(process_xml(body.to_string(), category, &mut message.request.generators).unwrap_or(vec![])),
Some(XML.clone()), None)
} else {
OptionalBody::from(body)
};
message.request.contents = body;
message.request.metadata.insert("contentType".to_string(), json!(content_type));
}
InteractionPart::Response => {
let mut response = MessageContents::default();
let category = response.matching_rules.add_category("body");
let body = if ct.is_json() {
OptionalBody::Present(Bytes::from(process_json(body.to_string(), category, &mut response.generators)),
Some(JSON.clone()), None)
} else if ct.is_xml() {
OptionalBody::Present(Bytes::from(process_xml(body.to_string(), category, &mut response.generators).unwrap_or(vec![])),
Some(XML.clone()), None)
} else {
OptionalBody::from(body)
};
response.contents = body;
response.metadata.insert("contentType".to_string(), json!(content_type));
message.response.push(response);
}
}
true
} else {
error!("Interaction is an unknown type, is {}", inner.type_of());
false
}
}).unwrap_or(false)
}
#[no_mangle]
pub extern fn pactffi_with_binary_file(
interaction: InteractionHandle,
part: InteractionPart,
content_type: *const c_char,
body: *const u8,
size: size_t
) -> bool {
let content_type_header = "Content-Type".to_string();
match convert_cstr("content_type", content_type) {
Some(content_type) => {
interaction.with_interaction(&|_, mock_server_started, inner| {
if let Some(reqres) = inner.as_v4_http_mut() {
match part {
InteractionPart::Request => {
reqres.request.body = convert_ptr_to_body(body, size);
if !reqres.request.has_header(&content_type_header) {
match reqres.request.headers {
Some(ref mut headers) => {
headers.insert(content_type_header.clone(), vec![content_type.to_string()]);
},
None => {
reqres.request.headers = Some(hashmap! { content_type_header.clone() => vec![content_type.to_string()]});
}
}
};
reqres.request.matching_rules.add_category("body").add_rule(
DocPath::root(), MatchingRule::ContentType(content_type.into()), RuleLogic::And);
},
InteractionPart::Response => {
reqres.response.body = convert_ptr_to_body(body, size);
if !reqres.response.has_header(&content_type_header) {
match reqres.response.headers {
Some(ref mut headers) => {
headers.insert(content_type_header.clone(), vec![content_type.to_string()]);
},
None => {
reqres.response.headers = Some(hashmap! { content_type_header.clone() => vec![content_type.to_string()]});
}
}
}
reqres.response.matching_rules.add_category("body").add_rule(
DocPath::root(), MatchingRule::ContentType(content_type.into()), RuleLogic::And);
}
};
!mock_server_started
} else if let Some(message) = inner.as_v4_async_message_mut() {
message.contents.contents = convert_ptr_to_body(body, size);
message.contents.matching_rules.add_category("body").add_rule(
DocPath::root(), MatchingRule::ContentType(content_type.into()), RuleLogic::And);
message.contents.metadata.insert("contentType".to_string(), json!(content_type));
true
} else if let Some(sync_message) = inner.as_v4_sync_message_mut() {
match part {
InteractionPart::Request => {
sync_message.request.contents = convert_ptr_to_body(body, size);
sync_message.request.matching_rules.add_category("body").add_rule(
DocPath::root(), MatchingRule::ContentType(content_type.into()), RuleLogic::And);
sync_message.request.metadata.insert("contentType".to_string(), json!(content_type));
},
InteractionPart::Response => {
let mut response = MessageContents::default();
response.contents = convert_ptr_to_body(body, size);
response.matching_rules.add_category("body").add_rule(
DocPath::root(), MatchingRule::ContentType(content_type.into()), RuleLogic::And);
response.metadata.insert("contentType".to_string(), json!(content_type));
sync_message.response.push(response);
}
};
true
} else {
error!("Interaction is an unknown type, is {}", inner.type_of());
false
}
}).unwrap_or(false)
},
None => {
error!("with_binary_file: Content type value is not valid (NULL or non-UTF-8)");
false
}
}
}
#[no_mangle]
pub extern fn pactffi_with_multipart_file(
interaction: InteractionHandle,
part: InteractionPart,
content_type: *const c_char,
file: *const c_char,
part_name: *const c_char
) -> StringResult {
let part_name = convert_cstr("part_name", part_name).unwrap_or("file");
match convert_cstr("content_type", content_type) {
Some(content_type) => {
let result = interaction.with_interaction(&|_, mock_server_started, inner| {
match convert_ptr_to_mime_part_body(file, part_name) {
Ok(body) => {
if let Some(reqres) = inner.as_v4_http_mut() {
match part {
InteractionPart::Request => request_multipart(&mut reqres.request, &body.boundary, body.body, content_type, part_name),
InteractionPart::Response => response_multipart(&mut reqres.response, &body.boundary, body.body, content_type, part_name)
};
if mock_server_started {
Err("with_multipart_file: This Pact can not be modified, as the mock server has already started".to_string())
} else {
Ok(())
}
} else {
error!("Interaction is not an HTTP interaction, is {}", inner.type_of());
Err(format!("with_multipart_file: Interaction is not an HTTP interaction, is {}", inner.type_of()))
}
},
Err(err) => Err(format!("with_multipart_file: failed to generate multipart body - {}", err))
}
});
match result {
Some(inner_result) => match inner_result {
Ok(_) => StringResult::Ok(null_mut()),
Err(err) => {
let error = CString::new(err).unwrap();
StringResult::Failed(error.into_raw())
}
},
None => {
let error = CString::new("with_multipart_file: Interaction handle is invalid").unwrap();
StringResult::Failed(error.into_raw())
}
}
},
None => {
error!("with_multipart_file: Content type value is not valid (NULL or non-UTF-8)");
let error = CString::new("with_multipart_file: Content type value is not valid (NULL or non-UTF-8)").unwrap();
StringResult::Failed(error.into_raw())
}
}
}
fn convert_ptr_to_body(body: *const u8, size: size_t) -> OptionalBody {
if body.is_null() {
OptionalBody::Null
} else if size == 0 {
OptionalBody::Empty
} else {
OptionalBody::Present(Bytes::from(unsafe { std::slice::from_raw_parts(body, size) }), None, None)
}
}
fn convert_ptr_to_mime_part_body(file: *const c_char, part_name: &str) -> Result<MultipartBody, String> {
if file.is_null() {
empty_multipart_body()
} else {
let c_str = unsafe { CStr::from_ptr(file) };
let file = match c_str.to_str() {
Ok(str) => Ok(str),
Err(err) => {
error!("convert_ptr_to_mime_part_body: Failed to parse file name as a UTF-8 string: {}", err);
Err(format!("convert_ptr_to_mime_part_body: Failed to parse file name as a UTF-8 string: {}", err))
}
}?;
file_as_multipart_body(file, part_name)
}
}
ffi_fn! {
fn pactffi_pact_handle_get_message_iter(pact: PactHandle) -> *mut PactMessageIterator {
let message_pact = pact.with_pact(&|_, inner| {
inner.pact.as_message_pact().unwrap()
}).ok_or_else(|| anyhow!("Pact handle is not valid"))?;
let iter = PactMessageIterator::new(message_pact);
ptr::raw_to(iter)
} {
ptr::null_mut_to::<PactMessageIterator>()
}
}
ffi_fn! {
fn pactffi_pact_handle_get_sync_message_iter(pact: PactHandle) -> *mut PactSyncMessageIterator {
let v4_pact = pact.with_pact(&|_, inner| {
inner.pact.as_v4_pact().unwrap()
}).ok_or_else(|| anyhow!("Pact handle is not valid"))?;
let iter = PactSyncMessageIterator::new(v4_pact);
ptr::raw_to(iter)
} {
ptr::null_mut_to::<PactSyncMessageIterator>()
}
}
ffi_fn! {
fn pactffi_pact_handle_get_sync_http_iter(pact: PactHandle) -> *mut PactSyncHttpIterator {
let v4_pact = pact.with_pact(&|_, inner| {
inner.pact.as_v4_pact().unwrap()
}).ok_or_else(|| anyhow!("Pact handle is not valid"))?;
let iter = PactSyncHttpIterator::new(v4_pact);
ptr::raw_to(iter)
} {
ptr::null_mut_to::<PactSyncHttpIterator>()
}
}
#[no_mangle]
pub extern fn pactffi_new_message_pact(consumer_name: *const c_char, provider_name: *const c_char) -> MessagePactHandle {
let consumer = convert_cstr("consumer_name", consumer_name).unwrap_or("Consumer");
let provider = convert_cstr("provider_name", provider_name).unwrap_or("Provider");
MessagePactHandle::new(consumer, provider)
}
#[no_mangle]
pub extern fn pactffi_new_message(pact: MessagePactHandle, description: *const c_char) -> MessageHandle {
if let Some(description) = convert_cstr("description", description) {
pact.with_pact(&|_, inner, _| {
let message = AsynchronousMessage {
description: description.to_string(),
..AsynchronousMessage::default()
};
inner.interactions.push(message.boxed_v4());
MessageHandle::new(pact, inner.interactions.len() as u16)
}).unwrap_or_else(|| MessageHandle::new(pact, 0))
} else {
MessageHandle::new(pact, 0)
}
}
#[no_mangle]
pub extern fn pactffi_message_expects_to_receive(message: MessageHandle, description: *const c_char) {
if let Some(description) = convert_cstr("description", description) {
message.with_message(&|_, inner, _| {
inner.set_description(description);
});
}
}
#[no_mangle]
pub extern fn pactffi_message_given(message: MessageHandle, description: *const c_char) {
if let Some(description) = convert_cstr("description", description) {
message.with_message(&|_, inner, _| {
inner.provider_states_mut().push(ProviderState::default(&description.to_string()));
});
}
}
#[no_mangle]
pub extern fn pactffi_message_given_with_param(message: MessageHandle, description: *const c_char,
name: *const c_char, value: *const c_char) {
if let Some(description) = convert_cstr("description", description) {
if let Some(name) = convert_cstr("name", name) {
let value = convert_cstr("value", value).unwrap_or_default();
message.with_message(&|_, inner, _| {
let value = match serde_json::from_str(value) {
Ok(json) => json,
Err(_) => json!(value)
};
match inner.provider_states().iter().find_position(|state| state.name == description) {
Some((index, _)) => {
inner.provider_states_mut().get_mut(index).unwrap().params.insert(name.to_string(), value);
},
None => inner.provider_states_mut().push(ProviderState {
name: description.to_string(),
params: hashmap!{ name.to_string() => value }
})
};
});
}
}
}
#[no_mangle]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern fn pactffi_message_with_contents(message_handle: MessageHandle, content_type: *const c_char, body: *const u8, size: size_t) {
let content_type = convert_cstr("content_type", content_type).unwrap_or("text/plain");
trace!("pactffi_message_with_contents(message_handle: {:?}, content_type: {:?}, body: {:?}, size: {})", message_handle, content_type, body, size);
message_handle.with_message(&|_, inner, _| {
let content_type = ContentType::parse(content_type).ok();
if let Some(message) = inner.as_v4_async_message_mut() {
let body = if let Some(content_type) = content_type {
let category = message.contents.matching_rules.add_category("body");
let body_str = convert_cstr("body", body as *const c_char).unwrap_or_default();
if content_type.is_xml() {
OptionalBody::Present(Bytes::from(process_xml(body_str.to_string(), category, &mut message.contents.generators).unwrap_or(vec![])), Some(content_type), None)
} else if content_type.is_text() || content_type.is_json() {
OptionalBody::Present(Bytes::from(process_json(body_str.to_string(), category, &mut message.contents.generators)), Some(content_type), None)
} else {
OptionalBody::Present(Bytes::from(unsafe { std::slice::from_raw_parts(body, size) }), Some(content_type), None)
}
} else {
OptionalBody::Present(Bytes::from(unsafe { std::slice::from_raw_parts(body, size) }), None, None)
};
message.contents.contents = body;
}
});
}
#[no_mangle]
pub extern fn pactffi_message_with_metadata(message_handle: MessageHandle, key: *const c_char, value: *const c_char) {
if let Some(key) = convert_cstr("key", key) {
let value = convert_cstr("value", value).unwrap_or_default();
message_handle.with_message(&|_, inner, _| {
if let Some(message) = inner.as_v4_async_message_mut() {
message.contents.metadata.insert(key.to_string(), Value::String(value.to_string()));
}
});
}
}
#[no_mangle]
pub extern fn pactffi_message_reify(message_handle: MessageHandle) -> *const c_char {
let res = message_handle.with_message(&|_, inner, spec_version| {
trace!("pactffi_message_reify(message: {:?}, spec_version: {})", inner, spec_version);
if let Some(message) = inner.as_v4_async_message() {
match message.contents.contents {
OptionalBody::Null => "null".to_string(),
OptionalBody::Present(_, _, _) => if spec_version <= pact_models::PactSpecification::V3 {
message.as_message().unwrap_or_default().to_json(&spec_version).to_string()
} else {
message.to_json().to_string()
},
_ => "".to_string()
}
} else {
"".to_string()
}
});
match res {
Some(res) => {
let string = CString::new(res).unwrap();
string.into_raw() as *const c_char
},
None => CString::default().into_raw() as *const c_char
}
}
#[no_mangle]
pub extern fn pactffi_write_message_pact_file(pact: MessagePactHandle, directory: *const c_char, overwrite: bool) -> i32 {
let result = pact.with_pact(&|_, inner, spec_version| {
let filename = path_from_dir(directory, Some(inner.default_file_name().as_str()));
write_pact(inner.boxed(), &filename.unwrap(), spec_version, overwrite)
});
match result {
Some(write_result) => match write_result {
Ok(_) => 0,
Err(e) => {
error!("unable to write the pact file: {:}", e);
1
}
},
None => {
error!("unable to write the pact file, message pact for handle {:?} not found", &pact);
2
}
}
}
#[no_mangle]
pub extern fn pactffi_with_message_pact_metadata(pact: MessagePactHandle, namespace: *const c_char, name: *const c_char, value: *const c_char) {
pact.with_pact(&|_, inner, _| {
let namespace = convert_cstr("namespace", namespace).unwrap_or_default();
let name = convert_cstr("name", name).unwrap_or_default();
let value = convert_cstr("value", value).unwrap_or_default();
if !namespace.is_empty() {
inner.metadata.insert(namespace.to_string(), json!({ name: value }));
} else {
warn!("no namespace provided for metadata {:?} => {:?}. Ignoring", name, value);
}
});
}
pub(crate) fn path_from_dir(directory: *const c_char, file_name: Option<&str>) -> Option<PathBuf> {
let dir = unsafe {
if directory.is_null() {
warn!("Directory to write to is NULL, defaulting to the current working directory");
None
} else {
let c_str = CStr::from_ptr(directory);
let dir_str = from_utf8(c_str.to_bytes()).unwrap();
if dir_str.is_empty() {
None
} else {
Some(dir_str.to_string())
}
}
};
dir.map(|path| {
let mut full_path = PathBuf::from(path);
if let Some(pact_file_name) = file_name {
full_path.push(pact_file_name);
}
full_path
})
}
ffi_fn! {
fn pactffi_pact_handle_write_file(pact: PactHandle, directory: *const c_char, overwrite: bool) -> i32 {
let result = pact.with_pact(&|_, inner| {
let pact_file = inner.pact.default_file_name();
let filename = path_from_dir(directory, Some(pact_file.as_str()));
write_pact(inner.pact.boxed(), &filename.unwrap_or_else(|| PathBuf::from(pact_file.as_str())), inner.specification_version, overwrite)
});
match result {
Some(write_result) => match write_result {
Ok(_) => 0,
Err(e) => {
error!("unable to write the pact file: {:}", e);
2
}
},
None => {
error!("unable to write the pact file, message pact for handle {:?} not found", &pact);
3
}
}
} {
1
}
}
#[no_mangle]
pub extern fn pactffi_new_async_message(pact: PactHandle, description: *const c_char) -> MessageHandle {
if let Some(description) = convert_cstr("description", description) {
pact.with_pact(&|_, inner| {
let message = AsynchronousMessage {
description: description.to_string(),
..AsynchronousMessage::default()
};
inner.pact.interactions.push(message.boxed_v4());
MessageHandle::new_v4(pact, inner.pact.interactions.len())
}).unwrap_or_else(|| MessageHandle::new_v4(pact, 0))
} else {
MessageHandle::new_v4(pact, 0)
}
}
#[no_mangle]
pub extern fn pactffi_free_pact_handle(pact: PactHandle) -> c_uint {
let mut handles = PACT_HANDLES.lock().unwrap();
trace!("pactffi_free_pact_handle - removing pact with index {}", pact.pact_ref);
handles.remove(&pact.pact_ref).map(|_| 0).unwrap_or(1)
}
#[no_mangle]
pub extern fn pactffi_free_message_pact_handle(pact: MessagePactHandle) -> c_uint {
let mut handles = PACT_HANDLES.lock().unwrap();
handles.remove(&pact.pact_ref).map(|_| 0).unwrap_or(1)
}
#[cfg(test)]
mod tests {
use std::ffi::CString;
use either::Either;
use expectest::prelude::*;
use maplit::hashmap;
use pact_models::content_types::JSON;
use pact_models::matchingrules;
use pact_models::matchingrules::{Category, MatchingRule};
use pact_models::path_exp::DocPath;
use pact_models::prelude::{Generators, MatchingRules};
use crate::mock_server::handles::*;
use super::from_integration_json_v2;
#[test]
fn pact_handles() {
let pact_handle = PactHandle::new("TestHandlesC", "TestHandlesP");
let description = CString::new("first interaction").unwrap();
let i_handle = pactffi_new_interaction(pact_handle, description.as_ptr());
let description2 = CString::new("second interaction").unwrap();
let i_handle2 = pactffi_new_async_message(pact_handle, description2.as_ptr());
expect!(i_handle.interaction_ref).to(be_equal_to(((pact_handle.pact_ref as u32) << 16) + 1));
expect!(i_handle2.interaction_ref).to(be_equal_to(((pact_handle.pact_ref as u32) << 16) + 2));
pact_handle.with_pact(&|pact_ref, inner| {
expect!(pact_ref).to(be_equal_to(pact_handle.pact_ref - 1));
expect!(inner.pact.consumer.name.as_str()).to(be_equal_to("TestHandlesC"));
expect!(inner.pact.provider.name.as_str()).to(be_equal_to("TestHandlesP"));
expect!(inner.pact.interactions.len()).to(be_equal_to(2));
});
i_handle.with_interaction(&|i_ref, _, inner| {
expect!(i_ref).to(be_equal_to(0));
expect!(inner.description().as_str()).to(be_equal_to("first interaction"));
expect!(inner.type_of().as_str()).to(be_equal_to("V4 Synchronous/HTTP"));
});
i_handle2.with_message(&|i_ref, inner, _| {
expect!(i_ref).to(be_equal_to(1));
expect!(inner.description().as_str()).to(be_equal_to("second interaction"));
expect!(inner.type_of().as_str()).to(be_equal_to("V4 Asynchronous/Messages"));
});
pactffi_free_pact_handle(pact_handle);
}
#[test]
fn simple_query_parameter() {
let pact_handle = PactHandle::new("TestC1", "TestP");
let description = CString::new("simple_query_parameter").unwrap();
let handle = pactffi_new_interaction(pact_handle, description.as_ptr());
let name = CString::new("id").unwrap();
let value = CString::new("100").unwrap();
pactffi_with_query_parameter_v2(handle, name.as_ptr(), 0, value.as_ptr());
let interaction = handle.with_interaction(&|_, _, inner| {
inner.as_v4_http().unwrap()
}).unwrap();
pactffi_free_pact_handle(pact_handle);
expect!(interaction.request.query.clone()).to(be_some().value(hashmap!{
"id".to_string() => vec!["100".to_string()]
}));
expect!(interaction.request.matching_rules.rules.get(&Category::QUERY).cloned().unwrap_or_default().is_empty()).to(be_true());
}
#[test]
fn query_parameter_with_matcher() {
let pact_handle = PactHandle::new("TestC2", "TestP");
let description = CString::new("query_parameter_with_matcher").unwrap();
let handle = pactffi_new_interaction(pact_handle, description.as_ptr());
let name = CString::new("id").unwrap();
let value = CString::new("{\"value\": \"100\", \"pact:matcher:type\": \"regex\", \"regex\": \"\\\\d+\"}").unwrap();
pactffi_with_query_parameter_v2(handle, name.as_ptr(), 0, value.as_ptr());
let interaction = handle.with_interaction(&|_, _, inner| {
inner.as_v4_http().unwrap()
}).unwrap();
pactffi_free_pact_handle(pact_handle);
expect!(interaction.request.query.clone()).to(be_some().value(hashmap!{
"id".to_string() => vec!["100".to_string()]
}));
expect!(&interaction.request.matching_rules).to(be_equal_to(&matchingrules! {
"query" => { "$.id[0]" => [ MatchingRule::Regex("\\d+".to_string()) ] }
}));
}
#[test]
fn query_parameter_with_multiple_values() {
let pact_handle = PactHandle::new("TestC3", "TestP");
let description = CString::new("query_parameter_with_multiple_values").unwrap();
let handle = pactffi_new_interaction(pact_handle, description.as_ptr());
let name = CString::new("id").unwrap();
let value = CString::new("{\"value\": [\"1\", \"2\"]}").unwrap();
pactffi_with_query_parameter_v2(handle, name.as_ptr(), 0, value.as_ptr());
let interaction = handle.with_interaction(&|_, _, inner| {
inner.as_v4_http().unwrap()
}).unwrap();
pactffi_free_pact_handle(pact_handle);
expect!(interaction.request.query.clone()).to(be_some().value(hashmap!{
"id".to_string() => vec!["1".to_string(), "2".to_string()]
}));
expect!(interaction.request.matching_rules.rules.get(&Category::QUERY).cloned().unwrap_or_default().is_empty()).to(be_true());
}
#[test]
fn query_parameter_with_multiple_values_with_matchers() {
let pact_handle = PactHandle::new("TestC4", "TestP");
let description = CString::new("query_parameter_with_multiple_values_with_matchers").unwrap();
let handle = pactffi_new_interaction(pact_handle, description.as_ptr());
let name = CString::new("id").unwrap();
let value = CString::new("{\"value\": \"100\", \"pact:matcher:type\": \"regex\", \"regex\": \"\\\\d+\"}").unwrap();
pactffi_with_query_parameter_v2(handle, name.as_ptr(), 0, value.as_ptr());
let value = CString::new("{\"value\": \"abc\", \"pact:matcher:type\": \"regex\", \"regex\": \"\\\\w+\"}").unwrap();
pactffi_with_query_parameter_v2(handle, name.as_ptr(), 1, value.as_ptr());
let interaction = handle.with_interaction(&|_, _, inner| {
inner.as_v4_http().unwrap()
}).unwrap();
pactffi_free_pact_handle(pact_handle);
expect!(interaction.request.query.clone()).to(be_some().value(hashmap!{
"id".to_string() => vec!["100".to_string(), "abc".to_string()]
}));
expect!(&interaction.request.matching_rules).to(be_equal_to(&matchingrules! {
"query" => {
"$.id[0]" => [ MatchingRule::Regex("\\d+".to_string()) ],
"$.id[1]" => [ MatchingRule::Regex("\\w+".to_string()) ]
}
}));
}
#[test]
fn query_parameter_with_multiple_values_in_json() {
let pact_handle = PactHandle::new("TestC5", "TestP");
let description = CString::new("query_parameter_with_multiple_values").unwrap();
let handle = pactffi_new_interaction(pact_handle, description.as_ptr());
let name = CString::new("catId[]").unwrap();
let value = CString::new("{\"value\": [\"1\"], \"pact:matcher:type\": \"type\", \"min\": 1}").unwrap();
pactffi_with_query_parameter_v2(handle, name.as_ptr(), 0, value.as_ptr());
let interaction = handle.with_interaction(&|_, _, inner| {
inner.as_v4_http().unwrap()
}).unwrap();
pactffi_free_pact_handle(pact_handle);
expect!(interaction.request.query.clone()).to(be_some().value(hashmap!{
"catId[]".to_string() => vec!["1".to_string()]
}));
expect!(&interaction.request.matching_rules).to(be_equal_to(&matchingrules! {
"query" => { "$['catId[]']" => [ MatchingRule::MinType(1) ] }
}));
}
#[test]
fn from_integration_json_test() {
let mut rules = MatchingRules::default();
let mut generators = Generators::default();
let path = DocPath::root();
expect!(from_integration_json_v2(&mut rules, &mut generators, "100", path.clone(), "query", 0))
.to(be_equal_to(Either::Left("100".to_string())));
expect!(from_integration_json_v2(&mut rules, &mut generators, "kjhaksdhj", path.clone(), "query", 0))
.to(be_equal_to(Either::Left("kjhaksdhj".to_string())));
expect!(from_integration_json_v2(&mut rules, &mut generators, r#"{"value":"100"}"#, path.clone(), "query", 0))
.to(be_equal_to(Either::Left("100".to_string())));
expect!(from_integration_json_v2(&mut rules, &mut generators, r#"{"value":["100"]}"#, path.clone(), "query", 0))
.to(be_equal_to(Either::Right(vec!["100".to_string()])));
expect!(from_integration_json_v2(&mut rules, &mut generators, r#"{"value":["100","200"]}"#, path.clone(), "query", 0))
.to(be_equal_to(Either::Right(vec!["100".to_string(), "200".to_string()])));
}
#[test]
fn simple_header() {
let pact_handle = PactHandle::new("TestHC1", "TestHP");
let description = CString::new("simple_header").unwrap();
let handle = pactffi_new_interaction(pact_handle, description.as_ptr());
let name = CString::new("x-id").unwrap();
let value = CString::new("100").unwrap();
pactffi_with_header_v2(handle, InteractionPart::Request, name.as_ptr(), 0, value.as_ptr());
let interaction = handle.with_interaction(&|_, _, inner| {
inner.as_v4_http().unwrap()
}).unwrap();
pactffi_free_pact_handle(pact_handle);
expect!(interaction.request.headers.clone()).to(be_some().value(hashmap!{
"x-id".to_string() => vec!["100".to_string()]
}));
expect!(interaction.request.matching_rules.rules.get(&Category::HEADER).cloned().unwrap_or_default().is_empty()).to(be_true());
}
#[test]
fn header_with_matcher() {
let pact_handle = PactHandle::new("TestHC2", "TestHP");
let description = CString::new("header_with_matcher").unwrap();
let handle = pactffi_new_interaction(pact_handle, description.as_ptr());
let name = CString::new("x-id").unwrap();
let value = CString::new("{\"value\": \"100\", \"pact:matcher:type\": \"regex\", \"regex\": \"\\\\d+\"}").unwrap();
pactffi_with_header_v2(handle, InteractionPart::Request, name.as_ptr(), 0, value.as_ptr());
let interaction = handle.with_interaction(&|_, _, inner| {
inner.as_v4_http().unwrap()
}).unwrap();
pactffi_free_pact_handle(pact_handle);
expect!(interaction.request.headers.clone()).to(be_some().value(hashmap!{
"x-id".to_string() => vec!["100".to_string()]
}));
expect!(&interaction.request.matching_rules).to(be_equal_to(&matchingrules! {
"header" => { "$['x-id'][0]" => [ MatchingRule::Regex("\\d+".to_string()) ] }
}));
}
#[test]
fn header_with_multiple_values() {
let pact_handle = PactHandle::new("TestHC3", "TestHP");
let description = CString::new("header_with_multiple_values").unwrap();
let handle = pactffi_new_interaction(pact_handle, description.as_ptr());
let name = CString::new("x-id").unwrap();
let value = CString::new("{\"value\": [\"1\", \"2\"]}").unwrap();
pactffi_with_header_v2(handle, InteractionPart::Request, name.as_ptr(), 0, value.as_ptr());
let interaction = handle.with_interaction(&|_, _, inner| {
inner.as_v4_http().unwrap()
}).unwrap();
pactffi_free_pact_handle(pact_handle);
expect!(interaction.request.headers.clone()).to(be_some().value(hashmap!{
"x-id".to_string() => vec!["1".to_string(), "2".to_string()]
}));
expect!(interaction.request.matching_rules.rules.get(&Category::HEADER).cloned().unwrap_or_default().is_empty()).to(be_true());
}
#[test]
fn header_with_multiple_values_with_matchers() {
let pact_handle = PactHandle::new("TestHC4", "TestHP");
let description = CString::new("header_with_multiple_values_with_matchers").unwrap();
let handle = pactffi_new_interaction(pact_handle, description.as_ptr());
let name = CString::new("x-id").unwrap();
let value = CString::new("{\"value\": \"100\", \"pact:matcher:type\": \"regex\", \"regex\": \"\\\\d+\"}").unwrap();
pactffi_with_header_v2(handle, InteractionPart::Request, name.as_ptr(), 0, value.as_ptr());
let value = CString::new("{\"value\": \"abc\", \"pact:matcher:type\": \"regex\", \"regex\": \"\\\\w+\"}").unwrap();
pactffi_with_header_v2(handle, InteractionPart::Request, name.as_ptr(), 1, value.as_ptr());
let interaction = handle.with_interaction(&|_, _, inner| {
inner.as_v4_http().unwrap()
}).unwrap();
pactffi_free_pact_handle(pact_handle);
expect!(interaction.request.headers.clone()).to(be_some().value(hashmap!{
"x-id".to_string() => vec!["100".to_string(), "abc".to_string()]
}));
expect!(&interaction.request.matching_rules).to(be_equal_to(&matchingrules! {
"header" => {
"$['x-id'][0]" => [ MatchingRule::Regex("\\d+".to_string()) ],
"$['x-id'][1]" => [ MatchingRule::Regex("\\w+".to_string()) ]
}
}));
}
#[test]
fn simple_path() {
let pact_handle = PactHandle::new("TestPC1", "TestPP");
let description = CString::new("simple_path").unwrap();
let handle = pactffi_new_interaction(pact_handle, description.as_ptr());
let method = CString::new("PUT").unwrap();
let path = CString::new("/path/to/100").unwrap();
pactffi_with_request(handle, method.as_ptr(), path.as_ptr());
let interaction = handle.with_interaction(&|_, _, inner| {
inner.as_v4_http().unwrap()
}).unwrap();
pactffi_free_pact_handle(pact_handle);
expect!(interaction.request.method).to(be_equal_to("PUT"));
expect!(interaction.request.path).to(be_equal_to("/path/to/100"));
expect!(interaction.request.matching_rules.rules.get(&Category::PATH).cloned().unwrap_or_default().is_empty()).to(be_true());
}
#[test]
fn path_with_matcher() {
let pact_handle = PactHandle::new("TestPC2", "TestPP");
let description = CString::new("path_with_matcher").unwrap();
let handle = pactffi_new_interaction(pact_handle, description.as_ptr());
let method = CString::new("PUT").unwrap();
let path = CString::new("{\"value\": \"/path/to/100\", \"pact:matcher:type\": \"regex\", \"regex\": \"\\\\/path\\\\/to\\\\/\\\\d+\"}").unwrap();
pactffi_with_request(handle, method.as_ptr(), path.as_ptr());
let interaction = handle.with_interaction(&|_, _, inner| {
inner.as_v4_http().unwrap()
}).unwrap();
pactffi_free_pact_handle(pact_handle);
expect!(interaction.request.method).to(be_equal_to("PUT"));
expect!(interaction.request.path).to(be_equal_to("/path/to/100"));
expect!(&interaction.request.matching_rules).to(be_equal_to(&matchingrules! {
"path" => { "$" => [ MatchingRule::Regex("\\/path\\/to\\/\\d+".to_string()) ] }
}));
}
#[test]
fn pactffi_with_body_test() {
let pact_handle = PactHandle::new("WithBodyC", "WithBodyP");
let description = CString::new("first interaction").unwrap();
let i_handle = pactffi_new_interaction(pact_handle, description.as_ptr());
let json_ct = CString::new(JSON.to_string()).unwrap();
let json = "{\"test\":true}";
let body = CString::new(json).unwrap();
let result = pactffi_with_body(i_handle, InteractionPart::Request, json_ct.as_ptr(), body.as_ptr());
let description2 = CString::new("second interaction").unwrap();
let i_handle2 = pactffi_new_message_interaction(pact_handle, description2.as_ptr());
let result2 = pactffi_with_body(i_handle2, InteractionPart::Request, json_ct.as_ptr(), body.as_ptr());
let description3 = CString::new("third interaction").unwrap();
let i_handle3 = pactffi_new_sync_message_interaction(pact_handle, description3.as_ptr());
let result3 = pactffi_with_body(i_handle3, InteractionPart::Request, json_ct.as_ptr(), body.as_ptr());
let interaction1 = i_handle.with_interaction(&|_, _, inner| {
inner.as_v4_http().unwrap()
}).unwrap();
let interaction2 = i_handle2.with_interaction(&|_, _, inner| {
inner.as_v4_async_message().unwrap()
}).unwrap();
let interaction3 = i_handle3.with_interaction(&|_, _, inner| {
inner.as_v4_sync_message().unwrap()
}).unwrap();
pactffi_free_pact_handle(pact_handle);
expect!(result).to(be_true());
expect!(result2).to(be_true());
expect!(result3).to(be_true());
let body1 = interaction1.request.body.value().unwrap();
expect!(body1.len()).to(be_equal_to(json.len()));
let headers = interaction1.request.headers.unwrap();
expect!(headers.get("Content-Type").unwrap().first().unwrap()).to(be_equal_to(&JSON.to_string()));
let body2 = interaction2.contents.contents.value().unwrap();
expect!(body2.len()).to(be_equal_to(json.len()));
expect!(interaction2.contents.metadata.get("contentType").unwrap().to_string()).to(be_equal_to("\"application/json\""));
let body3 = interaction3.request.contents.value().unwrap();
expect!(body3.len()).to(be_equal_to(json.len()));
expect!(interaction3.request.metadata.get("contentType").unwrap().to_string()).to(be_equal_to("\"application/json\""));
}
#[test]
fn pactffi_with_body_for_non_default_json_test() {
let pact_handle = PactHandle::new("WithBodyC", "WithBodyP");
let description = CString::new("first interaction").unwrap();
let i_handle = pactffi_new_interaction(pact_handle, description.as_ptr());
let json_ct = CString::new("application/vnd.schemaregistry.v1+json").unwrap();
let json = "{\"test\":true}";
let body = CString::new(json).unwrap();
let result = pactffi_with_body(i_handle, InteractionPart::Request, json_ct.as_ptr(), body.as_ptr());
let description2 = CString::new("second interaction").unwrap();
let i_handle2 = pactffi_new_message_interaction(pact_handle, description2.as_ptr());
let result2 = pactffi_with_body(i_handle2, InteractionPart::Request, json_ct.as_ptr(), body.as_ptr());
let description3 = CString::new("third interaction").unwrap();
let i_handle3 = pactffi_new_sync_message_interaction(pact_handle, description3.as_ptr());
let result3 = pactffi_with_body(i_handle3, InteractionPart::Request, json_ct.as_ptr(), body.as_ptr());
let interaction1 = i_handle.with_interaction(&|_, _, inner| {
inner.as_v4_http().unwrap()
}).unwrap();
let interaction2 = i_handle2.with_interaction(&|_, _, inner| {
inner.as_v4_async_message().unwrap()
}).unwrap();
let interaction3 = i_handle3.with_interaction(&|_, _, inner| {
inner.as_v4_sync_message().unwrap()
}).unwrap();
pactffi_free_pact_handle(pact_handle);
expect!(result).to(be_true());
expect!(result2).to(be_true());
expect!(result3).to(be_true());
let body1 = interaction1.request.body.value().unwrap();
expect!(body1.len()).to(be_equal_to(json.len()));
let headers = interaction1.request.headers.unwrap();
expect!(headers.get("Content-Type").unwrap().first().unwrap()).to(be_equal_to("application/vnd.schemaregistry.v1+json"));
let body2 = interaction2.contents.contents.value().unwrap();
expect!(body2.len()).to(be_equal_to(json.len()));
expect!(interaction2.contents.metadata.get("contentType").unwrap().to_string()).to(be_equal_to("\"application/vnd.schemaregistry.v1+json\""));
let body3 = interaction3.request.contents.value().unwrap();
expect!(body3.len()).to(be_equal_to(json.len()));
expect!(interaction3.request.metadata.get("contentType").unwrap().to_string()).to(be_equal_to("\"application/vnd.schemaregistry.v1+json\""));
}
#[test]
fn pactffi_with_binary_file_test() {
let pact_handle = PactHandle::new("CBin", "PBin");
let description = CString::new("first interaction").unwrap();
let i_handle = pactffi_new_interaction(pact_handle, description.as_ptr());
let json_ct = CString::new(JSON.to_string()).unwrap();
let json = "{\"test\":true}";
let result = pactffi_with_binary_file(i_handle, InteractionPart::Request,
json_ct.as_ptr(), json.as_ptr(), json.len());
let description2 = CString::new("second interaction").unwrap();
let i_handle2 = pactffi_new_message_interaction(pact_handle, description2.as_ptr());
let result2 = pactffi_with_binary_file(i_handle2, InteractionPart::Request,
json_ct.as_ptr(), json.as_ptr(), json.len());
let description3 = CString::new("third interaction").unwrap();
let i_handle3 = pactffi_new_sync_message_interaction(pact_handle, description3.as_ptr());
let result3 = pactffi_with_binary_file(i_handle3, InteractionPart::Request,
json_ct.as_ptr(), json.as_ptr(), json.len());
let interaction1 = i_handle.with_interaction(&|_, _, inner| {
inner.as_v4_http().unwrap()
}).unwrap();
let interaction2 = i_handle2.with_interaction(&|_, _, inner| {
inner.as_v4_async_message().unwrap()
}).unwrap();
let interaction3 = i_handle3.with_interaction(&|_, _, inner| {
inner.as_v4_sync_message().unwrap()
}).unwrap();
pactffi_free_pact_handle(pact_handle);
expect!(result).to(be_true());
expect!(result2).to(be_true());
expect!(result3).to(be_true());
let body1 = interaction1.request.body.value().unwrap();
expect!(body1.len()).to(be_equal_to(json.len()));
let headers = interaction1.request.headers.unwrap();
expect!(headers.get("Content-Type").unwrap().first().unwrap()).to(be_equal_to(&JSON.to_string()));
let body2 = interaction2.contents.contents.value().unwrap();
expect!(body2.len()).to(be_equal_to(json.len()));
expect!(interaction2.contents.metadata.get("contentType").unwrap().to_string()).to(be_equal_to("\"application/json\""));
let body3 = interaction3.request.contents.value().unwrap();
expect!(body3.len()).to(be_equal_to(json.len()));
expect!(interaction3.request.metadata.get("contentType").unwrap().to_string()).to(be_equal_to("\"application/json\""));
}
#[test]
fn process_json_with_nested_rules() {
let mut rules = MatchingRules::default();
let mut category = rules.add_category("body");
let mut generators = Generators::default();
let json = json!({
"pact:matcher:type": "values",
"value": {
"some-string": {
"pact:matcher:type": "values",
"value": {
"some-string": {
"pact:matcher:type": "values",
"value": {
"some-string": {
"pact:matcher:type": "type",
"value": "some string"
}
}
}
}
}
}
});
let result = process_json(json.to_string(), &mut category, &mut generators);
expect!(result).to(be_equal_to("{\"some-string\":{\"some-string\":{\"some-string\":\"some string\"}}}"));
expect!(&rules).to(be_equal_to(&matchingrules! {
"body" => {
"$" => [ MatchingRule::Values ],
"$.*" => [ MatchingRule::Values ],
"$.*.*" => [ MatchingRule::Values ],
"$.*.*.*" => [ MatchingRule::Type ]
}
}));
}
}