diff --git a/compatibility-suite/Cargo.toml b/compatibility-suite/Cargo.toml index 19e75b1a2..fce2c411a 100644 --- a/compatibility-suite/Cargo.toml +++ b/compatibility-suite/Cargo.toml @@ -38,3 +38,11 @@ harness = false [[test]] name = "v2_provider" harness = false + +#[[test]] +#name = "v3" +#harness = false + +[[test]] +name = "v3_provider" +harness = false diff --git a/compatibility-suite/tests/shared_steps/provider.rs b/compatibility-suite/tests/shared_steps/provider.rs index c7ee39d49..53481fe0d 100644 --- a/compatibility-suite/tests/shared_steps/provider.rs +++ b/compatibility-suite/tests/shared_steps/provider.rs @@ -51,6 +51,7 @@ use crate::shared_steps::{setup_body, setup_common_interactions}; #[derive(Debug, World)] pub struct ProviderWorld { + pub spec_version: PactSpecification, pub interactions: Vec, pub provider_key: String, pub provider_server: Arc>, @@ -81,6 +82,7 @@ impl ProviderWorld { impl Default for ProviderWorld { fn default() -> Self { ProviderWorld { + spec_version: PactSpecification::V1, interactions: vec![], provider_key: "".to_string(), provider_server: Default::default(), @@ -120,6 +122,20 @@ impl MockProviderStateExecutor { state.name == state_name && *setup == is_setup }).is_some() } + + pub fn was_called_for_state_with_params( + &self, + state_name: &str, + state_params: &HashMap, + is_setup: bool + ) -> bool { + let params = self.params.lock().unwrap(); + params.iter().find(|(state, setup)| { + state.name == state_name && + state.params == *state_params && + *setup == is_setup + }).is_some() + } } #[derive(Debug, Default, Clone)] @@ -238,12 +254,12 @@ async fn a_provider_is_started_that_returns_the_response_from_interaction(world: consumer: Consumer { name: "v1-compatibility-suite-c".to_string() }, provider: Provider { name: "p".to_string() }, interactions: vec![ world.interactions.get(num - 1).unwrap().clone() ], - specification_version: PactSpecification::V1, + specification_version: world.spec_version, .. RequestResponsePact::default() }; world.provider_key = Uuid::new_v4().to_string(); let config = MockServerConfig { - pact_specification: PactSpecification::V1, + pact_specification: world.spec_version, .. MockServerConfig::default() }; let (mock_server, future) = MockServer::new( @@ -316,12 +332,12 @@ async fn a_provider_is_started_that_returns_the_response_from_interaction_with_t consumer: Consumer { name: "v1-compatibility-suite-c".to_string() }, provider: Provider { name: "p".to_string() }, interactions: vec![interaction], - specification_version: PactSpecification::V1, + specification_version: world.spec_version, .. RequestResponsePact::default() }; world.provider_key = Uuid::new_v4().to_string(); let config = MockServerConfig { - pact_specification: PactSpecification::V1, + pact_specification: world.spec_version, .. MockServerConfig::default() }; let (mock_server, future) = MockServer::new( @@ -351,10 +367,10 @@ fn a_pact_file_for_interaction_is_to_be_verified(world: &mut ProviderWorld, num: consumer: Consumer { name: format!("c_{}", num) }, provider: Provider { name: "p".to_string() }, interactions: vec![ world.interactions.get(num - 1).unwrap().clone() ], - specification_version: PactSpecification::V1, + specification_version: world.spec_version, .. RequestResponsePact::default() }; - world.sources.push(PactSource::String(pact.to_json(PactSpecification::V1)?.to_string())); + world.sources.push(PactSource::String(pact.to_json(world.spec_version)?.to_string())); Ok(()) } @@ -373,10 +389,55 @@ fn a_pact_file_for_interaction_is_to_be_verified_with_a_provider_state( consumer: Consumer { name: format!("c_{}", num) }, provider: Provider { name: "p".to_string() }, interactions: vec![interaction], - specification_version: PactSpecification::V1, + specification_version: world.spec_version, .. RequestResponsePact::default() }; - world.sources.push(PactSource::String(pact.to_json(PactSpecification::V1)?.to_string())); + world.sources.push(PactSource::String(pact.to_json(world.spec_version)?.to_string())); + Ok(()) +} + +#[given(expr = "a Pact file for interaction {int} is to be verified with the following provider states defined:")] +fn a_pact_file_for_interaction_is_to_be_verified_with_the_following_provider_states_defined( + world: &mut ProviderWorld, + step: &Step, + num: usize +) -> anyhow::Result<()> { + let mut interaction = world.interactions.get(num - 1).unwrap().clone(); + + if let Some(table) = step.table.as_ref() { + let headers = table.rows.first().unwrap().iter() + .enumerate() + .map(|(index, h)| (index, h.clone())) + .collect::>(); + for values in table.rows.iter().skip(1) { + let data = values.iter().enumerate() + .map(|(index, v)| (headers.get(&index).unwrap().as_str(), v.clone())) + .collect::>(); + if let Some(parameters) = data.get("Parameters") { + let json: Value = serde_json::from_str(parameters.as_str()).unwrap(); + interaction.provider_states.push(ProviderState { + name: data.get("State Name").unwrap().clone(), + params: json.as_object().unwrap().iter().map(|(k, v)| (k.clone(), v.clone())).collect() + }); + } else { + interaction.provider_states.push(ProviderState { + name: data.get("State Name").unwrap().clone(), + params: Default::default(), + }); + } + } + } else { + return Err(anyhow!("No data table defined")); + } + + let pact = RequestResponsePact { + consumer: Consumer { name: format!("c_{}", num) }, + provider: Provider { name: "p".to_string() }, + interactions: vec![interaction], + specification_version: world.spec_version, + .. RequestResponsePact::default() + }; + world.sources.push(PactSource::String(pact.to_json(world.spec_version)?.to_string())); Ok(()) } @@ -419,12 +480,12 @@ async fn a_provider_is_started_that_returns_the_responses_from_interactions( consumer: Consumer { name: "v1-compatibility-suite-c".to_string() }, provider: Provider { name: "p".to_string() }, interactions, - specification_version: PactSpecification::V1, + specification_version: world.spec_version, .. RequestResponsePact::default() }; world.provider_key = Uuid::new_v4().to_string(); let config = MockServerConfig { - pact_specification: PactSpecification::V1, + pact_specification: world.spec_version, .. MockServerConfig::default() }; let (mock_server, future) = MockServer::new( @@ -496,10 +557,10 @@ async fn a_pact_file_for_interaction_is_to_be_verified_from_a_pact_broker( consumer: Consumer { name: format!("c_{}", num) }, provider: Provider { name: "p".to_string() }, interactions: vec![interaction.clone()], - specification_version: PactSpecification::V1, + specification_version: world.spec_version, .. RequestResponsePact::default() }; - let mut pact_json = pact.to_json(PactSpecification::V1)?; + let mut pact_json = pact.to_json(world.spec_version)?; let pact_json_inner = pact_json.as_object_mut().unwrap(); pact_json_inner.insert("_links".to_string(), json!({ "pb:publish-verification-results": { @@ -730,6 +791,51 @@ fn the_provider_state_callback_will_receive_a_setup_call_with_as_the_provider_st } } +#[then(expr = "the provider state callback will receive a setup call with {string} and the following parameters:")] +fn the_provider_state_callback_will_receive_a_setup_call_with_and_the_following_parameters( + world: &mut ProviderWorld, + step: &Step, + state: String +) -> anyhow::Result<()> { + validate_state_call(world, step, state, true) +} + +#[then(expr = "the provider state callback will receive a teardown call {string} and the following parameters:")] +fn the_provider_state_callback_will_receive_a_teardown_call_with_and_the_following_parameters( + world: &mut ProviderWorld, + step: &Step, + state: String +) -> anyhow::Result<()> { + validate_state_call(world, step, state, false) +} + +fn validate_state_call(world: &mut ProviderWorld, step: &Step, state: String, is_setup: bool) -> anyhow::Result<()> { + if let Some(table) = step.table.as_ref() { + let headers = table.rows.first().unwrap().iter() + .enumerate() + .map(|(index, h)| (index, h.clone())) + .collect::>(); + if let Some(values) = table.rows.get(1) { + let parameters = values.iter().enumerate() + .map(|(index, v)| { + let key = headers.get(&index).unwrap(); + let value = serde_json::from_str(v).unwrap(); + (key.clone(), value) + }) + .collect::>(); + if world.provider_state_executor.was_called_for_state_with_params(state.as_str(), ¶meters, is_setup) { + Ok(()) + } else { + Err(anyhow!("Provider state callback was not called for state '{}' with params {:?}", state, parameters)) + } + } else { + Err(anyhow!("No data table defined")) + } + } else { + Err(anyhow!("No data table defined")) + } +} + #[then("the provider state callback will be called after the verification is run")] fn the_provider_state_callback_will_be_called_after_the_verification_is_run(world: &mut ProviderWorld) -> anyhow::Result<()> { if world.provider_state_executor.was_called(false) { diff --git a/compatibility-suite/tests/v3_provider.rs b/compatibility-suite/tests/v3_provider.rs new file mode 100644 index 000000000..fd4f9ebff --- /dev/null +++ b/compatibility-suite/tests/v3_provider.rs @@ -0,0 +1,39 @@ +use cucumber::World; +use pact_models::PactSpecification; +use tracing_subscriber::EnvFilter; + +use crate::shared_steps::provider::ProviderWorld; + +pub mod shared_steps; + +#[tokio::main] +async fn main() { + let format = tracing_subscriber::fmt::format().pretty(); + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .event_format(format) + .init(); + + ProviderWorld::cucumber() + .fail_on_skipped() + .max_concurrent_scenarios(1) + .before(|_, _, _, world| Box::pin(async move { + world.spec_version = PactSpecification::V3; + })) + .after(|_feature, _, _scenario, _status, world| Box::pin(async move { + if let Some(world) = world { + { + let mut ms = world.provider_server.lock().unwrap(); + let _ = ms.shutdown(); + } + for broker in &world.mock_brokers { + let mut ms = broker.lock().unwrap(); + let _ = ms.shutdown(); + } + } + })) + .filter_run_and_exit("pact-compatibility-suite/features/V3", |feature, _rule, _scenario| { + feature.tags.iter().any(|tag| tag == "provider") + }) + .await; +}