Skip to content

Commit

Permalink
feat: Implemented the remaining V1 HTTP consumer compatability suite …
Browse files Browse the repository at this point in the history
…feature
  • Loading branch information
rholshausen committed May 18, 2023
1 parent 4347375 commit f72f819
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 46 deletions.
1 change: 1 addition & 0 deletions compatibility-suite/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions compatibility-suite/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ bytes = "1.4.0"
cucumber = "0.19"
futures = "0.3"
pact_models = "1.1.2"
pact_matching = { version = "1.0.8", path = "../rust/pact_matching" }
pact_mock_server = { version = "1.0.2", path = "../rust/pact_mock_server" }
pact_verifier = { version = "0.15.3", path = "../rust/pact_verifier" }
reqwest = { version = "0.11.17", features = ["rustls-tls-native-roots", "json"] }
Expand Down
260 changes: 251 additions & 9 deletions compatibility-suite/tests/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ use cucumber::{given, then, when, World, Parameter};
use cucumber::gherkin::Step;
use pact_models::{Consumer, PactSpecification, Provider};
use pact_models::bodies::OptionalBody;
use pact_models::content_types::ContentType;
use pact_models::content_types::{ContentType, JSON, XML};
use pact_models::headers::parse_header;
use pact_models::http_parts::HttpPart;
use pact_models::pact::{Pact, read_pact};
use pact_models::query_strings::parse_query_string;
Expand All @@ -21,6 +22,7 @@ use pact_models::sync_pact::RequestResponsePact;
use pact_models::v4::http_parts::HttpResponse;
use serde_json::Value;
use uuid::Uuid;
use pact_matching::Mismatch;
use pact_mock_server::matching::MatchResult;

use pact_mock_server::mock_server::{MockServer, MockServerConfig};
Expand Down Expand Up @@ -98,6 +100,56 @@ fn the_following_http_interactions_have_been_setup(world: &mut CompatibilitySuit
}
}

if let Some(index) = headers.get("headers") {
if let Some(headers) = values.get(*index) {
if !headers.is_empty() {
let headers = headers.split(",")
.map(|header| {
let key_value = header.strip_prefix("'").unwrap_or(header)
.strip_suffix("'").unwrap_or(header)
.splitn(2, ":")
.map(|v| v.trim())
.collect::<Vec<_>>();
(key_value[0].to_string(), parse_header(key_value[0], key_value[1]))
}).collect();
interaction.request.headers = Some(headers);
}
}
}

if let Some(index) = headers.get("body") {
if let Some(body) = values.get(*index) {
if !body.is_empty() {
if body.starts_with("JSON:") {
interaction.request.add_header("content-type", vec!["application/json"]);
interaction.request.body = OptionalBody::Present(Bytes::from(body.strip_prefix("JSON:").unwrap_or(body).to_string()),
Some(JSON.clone()), None);
} else if body.starts_with("XML:") {
interaction.request.add_header("content-type", vec!["application/xml"]);
interaction.request.body = OptionalBody::Present(Bytes::from(body.strip_prefix("XML:").unwrap_or(body).to_string()),
Some(XML.clone()), None);
} else {
let ct = if body.ends_with(".json") {
"application/json"
} else if body.ends_with(".xml") {
"application/xml"
} else {
"text/plain"
};
interaction.request.headers_mut().insert("content-type".to_string(), vec![ct.to_string()]);

let mut f = File::open(format!("pact-compatibility-suite/fixtures/{}", body))
.expect(format!("could not load fixture '{}'", body).as_str());
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)
.expect(format!("could not read fixture '{}'", body).as_str());
interaction.request.body = OptionalBody::Present(Bytes::from(buffer),
ContentType::parse(ct).ok(), None);
}
}
}
}

if let Some(index) = headers.get("response") {
if let Some(response) = values.get(*index) {
interaction.response.status = response.parse().unwrap();
Expand Down Expand Up @@ -223,6 +275,46 @@ async fn request_is_made_to_the_mock_server_with_the_following_changes(
"method" => request.method = value.clone(),
"path" => request.path = value.clone(),
"query" => request.query = parse_query_string(value),
"headers" => {
let headers = value.split(",")
.map(|header| {
let key_value = header.strip_prefix("'").unwrap_or(header)
.strip_suffix("'").unwrap_or(header)
.splitn(2, ":")
.map(|v| v.trim())
.collect::<Vec<_>>();
(key_value[0].to_string(), parse_header(key_value[0], key_value[1]))
}).collect();
request.headers = Some(headers);
},
"body" => {
if value.starts_with("JSON:") {
request.add_header("content-type", vec!["application/json"]);
request.body = OptionalBody::Present(Bytes::from(value.strip_prefix("JSON:").unwrap_or(value).to_string()),
Some(JSON.clone()), None);
} else if value.starts_with("XML:") {
request.add_header("content-type", vec!["application/xml"]);
request.body = OptionalBody::Present(Bytes::from(value.strip_prefix("XML:").unwrap_or(value).to_string()),
Some(XML.clone()), None);
} else {
let ct = if value.ends_with(".json") {
"application/json"
} else if value.ends_with(".xml") {
"application/xml"
} else {
"text/plain"
};
request.headers_mut().insert("content-type".to_string(), vec![ct.to_string()]);

let mut f = File::open(format!("pact-compatibility-suite/fixtures/{}", value))
.expect(format!("could not load fixture '{}'", value).as_str());
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)
.expect(format!("could not read fixture '{}'", value).as_str());
request.body = OptionalBody::Present(Bytes::from(buffer),
ContentType::parse(ct).ok(), None);
}
},
_ => {}
}
}
Expand Down Expand Up @@ -394,13 +486,20 @@ fn the_interaction_request_will_be_for_a(world: &mut CompatibilitySuiteWorld, nu
fn the_interaction_response_will_contain_the_document(world: &mut CompatibilitySuiteWorld, num: IndexType, fixture: String) -> anyhow::Result<()> {
if let Some(interaction) = world.pact.interactions().get(num.0) {
if let Some(reqres) = interaction.as_request_response() {
let mut fixture = File::open(format!("pact-compatibility-suite/fixtures/{}", fixture))?;
let mut fixture_file = File::open(format!("pact-compatibility-suite/fixtures/{}", fixture))?;
let mut buffer = Vec::new();
fixture.read_to_end(&mut buffer)?;
let json: Value = serde_json::from_slice(&buffer)?;
let json_str = json.to_string();
fixture_file.read_to_end(&mut buffer)?;

let mut expected = Vec::new();
if fixture.ends_with(".json") {
let json: Value = serde_json::from_slice(&buffer)?;
let string = json.to_string();
expected.extend_from_slice(string.as_bytes());
} else {
expected.extend_from_slice(&buffer);
}
let actual_body = reqres.response.body.value().unwrap_or_default();
if &actual_body == json_str.as_bytes() {
if &actual_body == expected.as_slice() {
Ok(())
} else {
let body = OptionalBody::Present(Bytes::from(buffer), None, None);
Expand Down Expand Up @@ -481,23 +580,30 @@ fn the_mismatches_will_contain_a_mismatch_with_error(
_ => vec![]
})
.collect();
if mismatches.iter().find(|ms| ms.mismatch_type().to_lowercase().starts_with(mismatch_type.as_str()) && ms.description() == error).is_some() {
if mismatches.iter().find(|ms| {
let correct_type = match ms {
Mismatch::BodyTypeMismatch { .. } => mismatch_type == "body-content-type",
_ => ms.mismatch_type().to_lowercase().starts_with(mismatch_type.as_str())
};
correct_type && ms.description() == error
}).is_some() {
Ok(())
} else {
Err(anyhow!("Did not find a {} mismatch with error {}", mismatch_type, error))
}
}

#[then(expr = "the mock server status will be an unexpected request received error for interaction \\{{int}}")]
#[then(expr = "the mock server status will be an unexpected {string} request received error for interaction \\{{int}}")]
fn the_mock_server_status_will_be_an_unexpected_request_received_error_for_interaction(
world: &mut CompatibilitySuiteWorld,
method: String,
num: usize
) -> anyhow::Result<()> {
let mock_server = { world.mock_server.lock().unwrap().clone() };
if let Some(interaction) = world.interactions.get(num - 1) {
if let Some(_) = mock_server.mismatches().iter().find(|mismatch| {
match mismatch {
MatchResult::RequestNotFound(request) => request.method == interaction.request.method &&
MatchResult::RequestNotFound(request) => request.method == method &&
request.path == interaction.request.path && request.query == interaction.request.query,
_ => false
}
Expand All @@ -511,6 +617,142 @@ fn the_mock_server_status_will_be_an_unexpected_request_received_error_for_inter
}
}

#[then(expr = "the mock server status will be an unexpected {string} request received error for path {string}")]
fn the_mock_server_status_will_be_an_unexpected_request_received_error(
world: &mut CompatibilitySuiteWorld,
method: String,
path: String
) -> anyhow::Result<()> {
let mock_server = { world.mock_server.lock().unwrap().clone() };
if let Some(_) = mock_server.mismatches().iter().find(|mismatch| {
match mismatch {
MatchResult::RequestNotFound(request) => request.method == method &&
request.path == path,
_ => false
}
}) {
Ok(())
} else {
Err(anyhow!("Did not find a RequestNotFound mismatch for path {}", path))
}
}

#[then(expr = "the \\{{numType}} interaction request will contain the header {string} with value {string}")]
fn the_interaction_request_will_contain_the_header_with_value(
world: &mut CompatibilitySuiteWorld,
num: IndexType,
key: String,
value: String
) -> anyhow::Result<()> {
if let Some(interaction) = world.pact.interactions().get(num.0) {
if let Some(reqres) = interaction.as_request_response() {
if let Some(header_value) = reqres.request.lookup_header_value(&key) {
if header_value == value {
Ok(())
} else {
Err(anyhow!("Expected interaction {} request to have a header {} with value {} but got {}", num.0 + 1, key, value, header_value))
}
} else {
Err(anyhow!("Expected interaction {} request to have a header {} with value {}", num.0 + 1, key, value))
}
} else {
Err(anyhow!("Interaction {} is not a RequestResponseInteraction", num.0 + 1))
}
} else {
Err(anyhow!("Did not find interaction {} in the Pact", num.0 + 1))
}
}

#[then(expr = "the \\{{numType}} interaction request content type will be {string}")]
fn the_interaction_request_content_type_will_be(
world: &mut CompatibilitySuiteWorld,
num: IndexType,
content_type: String
) -> anyhow::Result<()> {
if let Some(interaction) = world.pact.interactions().get(num.0) {
if let Some(reqres) = interaction.as_request_response() {
if let Some(ct) = reqres.request.content_type() {
if ct.to_string() == content_type {
Ok(())
} else {
Err(anyhow!("Expected interaction {} request to have a content type of {} but got {}", num.0 + 1, content_type, ct))
}
} else {
Err(anyhow!("Interaction {} request does not have a content type set", num.0 + 1))
}
} else {
Err(anyhow!("Interaction {} is not a RequestResponseInteraction", num.0 + 1))
}
} else {
Err(anyhow!("Did not find interaction {} in the Pact", num.0 + 1))
}
}

#[then(expr = "the \\{{numType}} interaction request will contain the {string} document")]
fn the_interaction_request_will_contain_the_document(
world: &mut CompatibilitySuiteWorld,
num: IndexType,
fixture: String,
) -> anyhow::Result<()> {
if let Some(interaction) = world.pact.interactions().get(num.0) {
if let Some(reqres) = interaction.as_request_response() {
let mut fixture_file = File::open(format!("pact-compatibility-suite/fixtures/{}", fixture))?;
let mut buffer = Vec::new();
fixture_file.read_to_end(&mut buffer)?;

let mut expected = Vec::new();
if fixture.ends_with(".json") {
let json: Value = serde_json::from_slice(&buffer)?;
let string = json.to_string();
expected.extend_from_slice(string.as_bytes());
} else {
expected.extend_from_slice(&buffer);
}
let actual_body = reqres.request.body.value().unwrap_or_default();
if &actual_body == expected.as_slice() {
Ok(())
} else {
let body = OptionalBody::Present(Bytes::from(buffer), None, None);
Err(anyhow!("Expected Interaction {} request with body {} but got {}", num.0 + 1,
reqres.request.body.display_string(), body.display_string()))
}
} else {
Err(anyhow!("Interaction {} is not a RequestResponseInteraction", num.0 + 1))
}
} else {
Err(anyhow!("Did not find interaction {} in the Pact", num.0 + 1))
}
}

#[then(expr = "the mismatches will contain a {string} mismatch with path {string} with error {string}")]
fn the_mismatches_will_contain_a_mismatch_with_path_with_error(
world: &mut CompatibilitySuiteWorld,
mismatch_type: String,
error_path: String,
error: String
) -> anyhow::Result<()> {
let mock_server = world.mock_server.lock().unwrap();
let mismatches: Vec<_> = mock_server.mismatches().iter()
.flat_map(|m| match m {
MatchResult::RequestMismatch(_, mismatches) => mismatches.clone(),
_ => vec![]
})
.collect();
if mismatches.iter().find(|ms| {
let correct_type = match ms {
Mismatch::QueryMismatch { parameter, .. } => mismatch_type == "query" && parameter == &error_path,
Mismatch::HeaderMismatch { key, .. } => mismatch_type == "header" && key == &error_path,
Mismatch::BodyMismatch { path, .. } => mismatch_type == "body" && path == &error_path,
_ => false
};
correct_type && ms.description().contains(&error)
}).is_some() {
Ok(())
} else {
Err(anyhow!("Did not find a {} mismatch for path {} with error {}", mismatch_type, error_path, error))
}
}

#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
Expand Down
1 change: 1 addition & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rust/pact_ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ quickcheck = "1.0.3"
test-log = "0.2.11"
tempfile = "3.5.0"
home = "0.5.4"
pretty_assertions = "1.3.0"

[lib]
crate-type = ["cdylib", "staticlib", "rlib"]
Expand Down
4 changes: 3 additions & 1 deletion rust/pact_ffi/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use libc::c_char;
use maplit::*;
use reqwest::blocking::Client;
use reqwest::header::CONTENT_TYPE;
use pretty_assertions::assert_eq;

#[allow(deprecated)]
use pact_ffi::mock_server::{
Expand Down Expand Up @@ -67,7 +68,8 @@ fn post_to_mock_server_with_mismatches() {

pactffi_cleanup_mock_server(port);

expect!(mismatches).to(be_equal_to("[{\"method\":\"POST\",\"mismatches\":[{\"actual\":\"\\\"no-very-bar\\\"\",\"expected\":\"\\\"bar\\\"\",\"mismatch\":\"Expected \'bar\' to be equal to \'no-very-bar\'\",\"path\":\"$.foo\",\"type\":\"BodyMismatch\"}],\"path\":\"/path\",\"type\":\"request-mismatch\"}]"));
assert_eq!("[{\"method\":\"POST\",\"mismatches\":[{\"actual\":\"\\\"no-very-bar\\\"\",\"expected\":\"\\\"bar\\\"\",\"mismatch\":\"Expected 'bar' (String) but received 'no-very-bar' (String)\",\"path\":\"$.foo\",\"type\":\"BodyMismatch\"}],\"path\":\"/path\",\"type\":\"request-mismatch\"}]",
mismatches);
}

#[test]
Expand Down
Loading

0 comments on commit f72f819

Please sign in to comment.