1use std::collections::HashMap;
3use std::env;
4use std::fs;
5use std::fs::File;
6use std::io::{BufReader, Write};
7use std::path::PathBuf;
8use std::process::Command;
9use std::process::Stdio;
10use std::str::FromStr;
11use std::str::from_utf8;
12use std::sync::Mutex;
13use std::thread;
14
15use anyhow::{Context, anyhow, bail};
16use bytes::Bytes;
17use itertools::Either;
18use lazy_static::lazy_static;
19use log::max_level;
20use maplit::hashmap;
21use os_info::Type;
22use pact_models::PactSpecification;
23use pact_models::bodies::OptionalBody;
24use pact_models::json_utils::json_to_string;
25use pact_models::prelude::v4::V4Pact;
26use pact_models::prelude::{ContentType, Pact};
27use pact_models::v4::interaction::V4Interaction;
28use reqwest::Client;
29use semver::Version;
30use serde_json::Value;
31use sysinfo::{Pid, System};
32use tracing::{debug, info, trace, warn};
33
34use crate::catalogue_manager::{
35 CatalogueEntry, all_entries, core_entries, register_plugin_entries, remove_plugin_entries,
36};
37use crate::child_process::ChildPluginProcess;
38use crate::content::ContentMismatch;
39use crate::download::{download_json_from_github, download_plugin_executable, fetch_json_from_url};
40use crate::metrics::send_metrics;
41use crate::mock_server::{MockServerConfig, MockServerDetails, MockServerResults};
42use crate::plugin_models::{
43 PactPlugin, PactPluginManifest, PactPluginRpc, PluginDependency, PluginInitRequest,
44 PluginInterfaceVersion,
45};
46use crate::proto::*;
47use crate::proto_v2;
48use crate::repository::{USER_AGENT, fetch_repository_index};
49use crate::utils::{
50 optional_string, proto_value_to_json, to_proto_struct, to_proto_value, versions_compatible,
51};
52use crate::verification::{InteractionVerificationData, InteractionVerificationResult};
53
54lazy_static! {
55 static ref PLUGIN_MANIFEST_REGISTER: Mutex<HashMap<String, PactPluginManifest>> =
56 Mutex::new(HashMap::new());
57 static ref PLUGIN_REGISTER: Mutex<HashMap<String, PactPlugin>> = Mutex::new(HashMap::new());
58}
59
60fn host_capabilities() -> Vec<String> {
61 core_entries()
62 .into_iter()
63 .map(|entry| format!("{}/{}", entry.entry_type, entry.key))
64 .collect()
65}
66
67pub async fn load_plugin(plugin: &PluginDependency) -> anyhow::Result<PactPlugin> {
70 let thread_id = thread::current().id();
71 debug!("Loading plugin {:?}", plugin);
72 trace!(
73 "Rust plugin driver version {}",
74 option_env!("CARGO_PKG_VERSION").unwrap_or_default()
75 );
76 trace!(
77 "load_plugin {:?}: Waiting on PLUGIN_REGISTER lock",
78 thread_id
79 );
80 let mut inner = PLUGIN_REGISTER.lock().unwrap();
81 trace!("load_plugin {:?}: Got PLUGIN_REGISTER lock", thread_id);
82 let result = match lookup_plugin_inner(plugin, &mut inner) {
83 Some(plugin) => {
84 debug!("Found running plugin {:?}", plugin);
85 plugin.update_access();
86 Ok(plugin.clone())
87 }
88 None => {
89 debug!("Did not find plugin, will attempt to start it");
90 let manifest = match load_plugin_manifest(plugin) {
91 Ok(manifest) => manifest,
92 Err(err) => {
93 warn!(
94 "Could not load plugin manifest from disk, will try auto install it: {}",
95 err
96 );
97 let http_client = reqwest::ClientBuilder::new()
98 .user_agent(USER_AGENT)
99 .build()?;
100 let index = fetch_repository_index(&http_client, None).await?;
101 match index.lookup_plugin_version(&plugin.name, &plugin.version) {
102 Some(entry) => {
103 info!("Found an entry for the plugin in the plugin index, will try install that");
104 install_plugin_from_url(&http_client, entry.source.value().as_str()).await?
105 }
106 None => Err(err)?,
107 }
108 }
109 };
110 send_metrics(&manifest);
111 initialise_plugin(&manifest, &mut inner).await
112 }
113 };
114 trace!(
115 "load_plugin {:?}: Releasing PLUGIN_REGISTER lock",
116 thread_id
117 );
118 result
119}
120
121fn lookup_plugin_inner<'a>(
122 plugin: &PluginDependency,
123 plugin_register: &'a mut HashMap<String, PactPlugin>,
124) -> Option<&'a mut PactPlugin> {
125 if let Some(version) = &plugin.version {
126 plugin_register.get_mut(format!("{}/{}", plugin.name, version).as_str())
127 } else {
128 plugin_register
129 .iter_mut()
130 .filter(|(_, value)| value.manifest.name == plugin.name)
131 .max_by(|(_, v1), (_, v2)| v1.manifest.version.cmp(&v2.manifest.version))
132 .map(|(_, plugin)| plugin)
133 }
134}
135
136pub fn lookup_plugin(plugin: &PluginDependency) -> Option<PactPlugin> {
138 let thread_id = thread::current().id();
139 trace!(
140 "lookup_plugin {:?}: Waiting on PLUGIN_REGISTER lock",
141 thread_id
142 );
143 let mut inner = PLUGIN_REGISTER.lock().unwrap();
144 trace!("lookup_plugin {:?}: Got PLUGIN_REGISTER lock", thread_id);
145 let entry = lookup_plugin_inner(plugin, &mut inner);
146 trace!(
147 "lookup_plugin {:?}: Releasing PLUGIN_REGISTER lock",
148 thread_id
149 );
150 entry.cloned()
151}
152
153pub fn load_plugin_manifest(plugin_dep: &PluginDependency) -> anyhow::Result<PactPluginManifest> {
156 debug!("Loading plugin manifest for plugin {:?}", plugin_dep);
157 match lookup_plugin_manifest(plugin_dep) {
158 Some(manifest) => Ok(manifest),
159 None => load_manifest_from_disk(plugin_dep),
160 }
161}
162
163fn load_manifest_from_disk(plugin_dep: &PluginDependency) -> anyhow::Result<PactPluginManifest> {
164 let plugin_dir = pact_plugin_dir()?;
165 debug!("Looking for plugin in {:?}", plugin_dir);
166
167 if plugin_dir.exists() {
168 load_manifest_from_dir(plugin_dep, &plugin_dir)
169 } else {
170 Err(anyhow!("Plugin directory {:?} does not exist", plugin_dir))
171 }
172}
173
174fn load_manifest_from_dir(
175 plugin_dep: &PluginDependency,
176 plugin_dir: &PathBuf,
177) -> anyhow::Result<PactPluginManifest> {
178 let mut manifests = vec![];
179 for entry in fs::read_dir(plugin_dir)? {
180 let path = entry?.path();
181 trace!("Found: {:?}", path);
182
183 if path.is_dir() {
184 let manifest_file = path.join("pact-plugin.json");
185 if manifest_file.exists() && manifest_file.is_file() {
186 debug!("Found plugin manifest: {:?}", manifest_file);
187 let file = File::open(manifest_file)?;
188 let reader = BufReader::new(file);
189 let manifest: PactPluginManifest = serde_json::from_reader(reader)?;
190 trace!("Parsed plugin manifest: {:?}", manifest);
191 let version = manifest.version.clone();
192 if manifest.name == plugin_dep.name
193 && versions_compatible(version.as_str(), &plugin_dep.version)
194 {
195 let manifest = PactPluginManifest {
196 plugin_dir: path.to_string_lossy().to_string(),
197 ..manifest
198 };
199 manifests.push(manifest);
200 }
201 }
202 }
203 }
204
205 let manifest = manifests.iter().max_by(|a, b| {
206 let a = Version::parse(&a.version).unwrap_or_else(|_| Version::new(0, 0, 0));
207 let b = Version::parse(&b.version).unwrap_or_else(|_| Version::new(0, 0, 0));
208 a.cmp(&b)
209 });
210 if let Some(manifest) = manifest {
211 let key = format!("{}/{}", manifest.name, manifest.version);
212 {
213 let mut guard = PLUGIN_MANIFEST_REGISTER.lock().unwrap();
214 guard.insert(key.clone(), manifest.clone());
215 }
216 Ok(manifest.clone())
217 } else {
218 Err(anyhow!(
219 "Plugin {} was not found (in $HOME/.pact/plugins or $PACT_PLUGIN_DIR)",
220 plugin_dep
221 ))
222 }
223}
224
225pub(crate) fn pact_plugin_dir() -> anyhow::Result<PathBuf> {
226 let env_var = env::var_os("PACT_PLUGIN_DIR");
227 let plugin_dir = env_var.unwrap_or_default();
228 let plugin_dir = plugin_dir.to_string_lossy();
229 if plugin_dir.is_empty() {
230 home::home_dir().map(|dir| dir.join(".pact").join("plugins"))
231 } else {
232 PathBuf::from_str(plugin_dir.as_ref()).ok()
233 }
234 .ok_or_else(|| {
235 anyhow!("No Pact plugin directory was found (in $HOME/.pact/plugins or $PACT_PLUGIN_DIR)")
236 })
237}
238
239pub fn lookup_plugin_manifest(plugin: &PluginDependency) -> Option<PactPluginManifest> {
241 let guard = PLUGIN_MANIFEST_REGISTER.lock().unwrap();
242 if let Some(version) = &plugin.version {
243 let key = format!("{}/{}", plugin.name, version);
244 guard.get(&key).cloned()
245 } else {
246 guard
247 .iter()
248 .filter(|(_, value)| value.name == plugin.name)
249 .max_by(|(_, v1), (_, v2)| v1.version.cmp(&v2.version))
250 .map(|(_, p)| p.clone())
251 }
252}
253
254async fn initialise_plugin(
255 manifest: &PactPluginManifest,
256 plugin_register: &mut HashMap<String, PactPlugin>,
257) -> anyhow::Result<PactPlugin> {
258 let interface_version = PluginInterfaceVersion::try_from(manifest.plugin_interface_version)
259 .with_context(|| {
260 format!(
261 "Plugin {}:{} declared an invalid interface version",
262 manifest.name, manifest.version
263 )
264 })?;
265
266 match interface_version {
267 PluginInterfaceVersion::V1 | PluginInterfaceVersion::V2 => {
268 match manifest.executable_type.as_str() {
269 "exec" => {
270 let mut plugin = start_plugin_process(manifest).await?;
271 debug!(
272 "Plugin process started OK (port = {}), sending init message",
273 plugin.port()
274 );
275
276 let response = init_handshake(manifest, &mut plugin).await.map_err(|err| {
277 plugin.kill();
278 anyhow!("Failed to send init request to the plugin - {}", err)
279 })?;
280 plugin.plugin_capabilities = response.plugin_capabilities;
281
282 let key = format!("{}/{}", manifest.name, manifest.version);
283 plugin_register.insert(key, plugin.clone());
284
285 Ok(plugin)
286 }
287 _ => Err(anyhow!(
288 "Plugin executable type of {} is not supported",
289 manifest.executable_type
290 )),
291 }
292 }
293 }
294}
295
296pub async fn init_handshake(
298 manifest: &PactPluginManifest,
299 plugin: &mut (dyn PactPluginRpc + Send + Sync),
300) -> anyhow::Result<crate::plugin_models::PluginInitResponse> {
301 let request = PluginInitRequest {
302 implementation: "plugin-driver-rust".to_string(),
303 version: option_env!("CARGO_PKG_VERSION").unwrap_or("0").to_string(),
304 host_capabilities: host_capabilities(),
305 };
306 let response = plugin.init_plugin(request).await?;
307 debug!(
308 "Got init response {:?} from plugin {}",
309 response, manifest.name
310 );
311 register_plugin_entries(manifest, &response.catalogue);
312 tokio::task::spawn(publish_updated_catalogue());
313 Ok(response)
314}
315
316async fn start_plugin_process(manifest: &PactPluginManifest) -> anyhow::Result<PactPlugin> {
317 debug!("Starting plugin with manifest {:?}", manifest);
318
319 let os_info = os_info::get();
320 debug!("Detected OS: {}", os_info);
321 let mut path = if let Some(entry_point) = manifest.entry_points.get(&os_info.to_string()) {
322 PathBuf::from(entry_point)
323 } else if os_info.os_type() == Type::Windows && manifest.entry_points.contains_key("windows") {
324 PathBuf::from(manifest.entry_points.get("windows").unwrap())
325 } else {
326 PathBuf::from(&manifest.entry_point)
327 };
328 if !path.is_absolute() || !path.exists() {
329 path = PathBuf::from(manifest.plugin_dir.clone()).join(path);
330 }
331 debug!("Starting plugin using {:?}", &path);
332
333 let log_level = max_level();
334 let mut child_command = Command::new(path.clone());
335 let mut child_command = child_command
336 .env("LOG_LEVEL", log_level.to_string())
337 .env("RUST_LOG", log_level.to_string())
338 .current_dir(manifest.plugin_dir.clone());
339
340 if let Some(args) = &manifest.args {
341 child_command = child_command.args(args);
342 }
343
344 let child = child_command
345 .stdout(Stdio::piped())
346 .stderr(Stdio::piped())
347 .spawn()
348 .map_err(|err| {
349 anyhow!(
350 "Was not able to start plugin process for '{}' - {}",
351 path.to_string_lossy(),
352 err
353 )
354 })?;
355 let child_pid = child.id();
356 debug!("Plugin {} started with PID {}", manifest.name, child_pid);
357
358 match ChildPluginProcess::new(child, manifest).await {
359 Ok(child) => PactPlugin::new(manifest, child),
360 Err(err) => {
361 let mut s = System::new();
362 s.refresh_processes();
363 if let Some(process) = s.process(Pid::from_u32(child_pid)) {
364 #[cfg(not(windows))]
365 process.kill();
366 #[cfg(windows)]
368 let _ = Command::new("taskkill.exe")
369 .arg("/PID")
370 .arg(child_pid.to_string())
371 .arg("/F")
372 .arg("/T")
373 .output();
374 } else {
375 warn!("Child process with PID {} was not found", child_pid);
376 }
377 Err(err)
378 }
379 }
380}
381
382pub fn shutdown_plugins() {
384 let thread_id = thread::current().id();
385 debug!("Shutting down all plugins");
386 trace!(
387 "shutdown_plugins {:?}: Waiting on PLUGIN_REGISTER lock",
388 thread_id
389 );
390 let mut guard = PLUGIN_REGISTER.lock().unwrap();
391 trace!("shutdown_plugins {:?}: Got PLUGIN_REGISTER lock", thread_id);
392 for plugin in guard.values() {
393 debug!("Shutting down plugin {:?}", plugin);
394 plugin.kill();
395 remove_plugin_entries(&plugin.manifest.name);
396 }
397 guard.clear();
398 trace!(
399 "shutdown_plugins {:?}: Releasing PLUGIN_REGISTER lock",
400 thread_id
401 );
402}
403
404pub fn shutdown_plugin(plugin: &mut PactPlugin) {
406 debug!(
407 "Shutting down plugin {}:{}",
408 plugin.manifest.name, plugin.manifest.version
409 );
410 plugin.kill();
411 remove_plugin_entries(&plugin.manifest.name);
412}
413
414pub async fn publish_updated_catalogue() {
416 let thread_id = thread::current().id();
417
418 let request = Catalogue {
419 catalogue: all_entries()
420 .iter()
421 .map(|entry| crate::proto::CatalogueEntry {
422 r#type: entry.entry_type.to_proto_type() as i32,
423 key: entry.key.clone(),
424 values: entry.values.clone(),
425 })
426 .collect(),
427 };
428
429 let plugins = {
430 trace!(
431 "publish_updated_catalogue {:?}: Waiting on PLUGIN_REGISTER lock",
432 thread_id
433 );
434 let inner = PLUGIN_REGISTER.lock().unwrap();
435 trace!(
436 "publish_updated_catalogue {:?}: Got PLUGIN_REGISTER lock",
437 thread_id
438 );
439 let plugins = inner.values().cloned().collect::<Vec<_>>();
440 trace!(
441 "publish_updated_catalogue {:?}: Releasing PLUGIN_REGISTER lock",
442 thread_id
443 );
444 plugins
445 };
446
447 for plugin in plugins {
448 if let Err(err) = plugin.update_catalogue(request.clone()).await {
449 warn!(
450 "Failed to send updated catalogue to plugin '{}' - {}",
451 plugin.manifest.name, err
452 );
453 }
454 }
455}
456
457#[tracing::instrument]
459pub fn increment_plugin_access(plugin: &PluginDependency) {
460 let thread_id = thread::current().id();
461
462 trace!(
463 "increment_plugin_access {:?}: Waiting on PLUGIN_REGISTER lock",
464 thread_id
465 );
466 let mut inner = PLUGIN_REGISTER.lock().unwrap();
467 trace!(
468 "increment_plugin_access {:?}: Got PLUGIN_REGISTER lock",
469 thread_id
470 );
471
472 if let Some(plugin) = lookup_plugin_inner(plugin, &mut inner) {
473 plugin.update_access();
474 }
475
476 trace!(
477 "increment_plugin_access {:?}: Releasing PLUGIN_REGISTER lock",
478 thread_id
479 );
480}
481
482#[tracing::instrument]
484pub fn drop_plugin_access(plugin: &PluginDependency) {
485 let thread_id = thread::current().id();
486
487 trace!(
488 "drop_plugin_access {:?}: Waiting on PLUGIN_REGISTER lock",
489 thread_id
490 );
491 let mut inner = PLUGIN_REGISTER.lock().unwrap();
492 trace!(
493 "drop_plugin_access {:?}: Got PLUGIN_REGISTER lock",
494 thread_id
495 );
496
497 if let Some(plugin) = lookup_plugin_inner(plugin, &mut inner) {
498 let key = format!("{}/{}", plugin.manifest.name, plugin.manifest.version);
499 if plugin.drop_access() == 0 {
500 shutdown_plugin(plugin);
501 inner.remove(key.as_str());
502 }
503 }
504
505 trace!(
506 "drop_plugin_access {:?}: Releasing PLUGIN_REGISTER lock",
507 thread_id
508 );
509}
510
511#[deprecated(
513 note = "Use start_mock_server_v2 which takes a test context map",
514 since = "0.2.2"
515)]
516pub async fn start_mock_server(
517 catalogue_entry: &CatalogueEntry,
518 pact: Box<dyn Pact + Send + Sync>,
519 config: MockServerConfig,
520) -> anyhow::Result<MockServerDetails> {
521 start_mock_server_v2(catalogue_entry, pact, config, hashmap! {}).await
522}
523
524pub async fn start_mock_server_v2(
528 catalogue_entry: &CatalogueEntry,
529 pact: Box<dyn Pact + Send + Sync>,
530 config: MockServerConfig,
531 test_context: HashMap<String, Value>,
532) -> anyhow::Result<MockServerDetails> {
533 let manifest = catalogue_entry
534 .plugin
535 .as_ref()
536 .ok_or_else(|| anyhow!("Catalogue entry did not have an associated plugin manifest"))?;
537 let plugin = lookup_plugin(&manifest.as_dependency())
538 .ok_or_else(|| anyhow!("Did not find a running plugin for manifest {:?}", manifest))?;
539
540 debug!(
541 plugin_name = manifest.name.as_str(),
542 plugin_version = manifest.version.as_str(),
543 ?test_context,
544 "Sending startMockServer request to plugin"
545 );
546
547 let response = if manifest.plugin_interface_version >= 2 {
548 let v4_pact = pact.as_v4_pact().map_err(|_| anyhow!("Pact must be a V4 pact for V2 plugin interface"))?;
549 let interactions = build_v2_interaction_contents(manifest, &v4_pact);
550 let request = proto_v2::StartMockServerRequest {
551 host_interface: config.host_interface.clone().unwrap_or_default(),
552 port: config.port,
553 tls: config.tls,
554 interactions,
555 test_context: Some(to_proto_struct(&test_context)),
556 };
557 plugin.start_mock_server_v2(request).await?
558 } else {
559 let request = StartMockServerRequest {
560 host_interface: config.host_interface.unwrap_or_default(),
561 port: config.port,
562 tls: config.tls,
563 pact: pact.to_json(PactSpecification::V4)?.to_string(),
564 test_context: Some(to_proto_struct(&test_context)),
565 };
566 plugin.start_mock_server(request).await?
567 };
568
569 debug!("Got response ${response:?}");
570
571 let mock_server_response = response
572 .response
573 .ok_or_else(|| anyhow!("Did not get a valid response from the start mock server call"))?;
574 match mock_server_response {
575 start_mock_server_response::Response::Error(err) => {
576 Err(anyhow!("Mock server failed to start: {}", err))
577 }
578 start_mock_server_response::Response::Details(details) => Ok(MockServerDetails {
579 key: details.key.clone(),
580 base_url: details.address.clone(),
581 port: details.port,
582 plugin,
583 }),
584 }
585}
586
587fn to_proto_v2_interaction_data(data: InteractionData) -> proto_v2::InteractionData {
589 use prost::Message;
590 proto_v2::InteractionData::decode(data.encode_to_vec().as_slice())
591 .expect("V1 and V2 InteractionData have identical wire format")
592}
593
594fn value_to_proto_struct(v: Value) -> prost_types::Struct {
595 match v {
596 Value::Object(map) => {
597 let hmap: HashMap<String, Value> = map.into_iter().collect();
598 to_proto_struct(&hmap)
599 }
600 _ => prost_types::Struct::default(),
601 }
602}
603
604fn build_v2_interaction_contents(
606 manifest: &PactPluginManifest,
607 pact: &V4Pact,
608) -> Vec<proto_v2::InteractionContents> {
609 let plugin_name = &manifest.name;
610 let consumer = pact.consumer.name.clone();
611 let provider = pact.provider.name.clone();
612 let pact_configuration = pact.plugin_data()
613 .into_iter()
614 .find(|p| p.name == *plugin_name)
615 .and_then(|p| p.configuration.get("pactConfiguration").cloned());
616 pact.interactions.iter().map(|interaction| {
617 build_interaction_contents_inner(
618 plugin_name,
619 &consumer,
620 &provider,
621 pact_configuration.clone(),
622 interaction.as_ref(),
623 )
624 }).collect()
625}
626
627fn build_v2_single_interaction_contents(
629 manifest: &PactPluginManifest,
630 pact: &V4Pact,
631 interaction: &(dyn V4Interaction + Send + Sync),
632) -> proto_v2::InteractionContents {
633 let plugin_name = &manifest.name;
634 let pact_configuration = pact.plugin_data()
635 .into_iter()
636 .find(|p| p.name == *plugin_name)
637 .and_then(|p| p.configuration.get("pactConfiguration").cloned());
638 build_interaction_contents_inner(
639 plugin_name,
640 &pact.consumer.name,
641 &pact.provider.name,
642 pact_configuration,
643 interaction,
644 )
645}
646
647fn build_interaction_contents_inner(
648 plugin_name: &str,
649 consumer: &str,
650 provider: &str,
651 pact_configuration: Option<Value>,
652 interaction: &(dyn V4Interaction + Send + Sync),
653) -> proto_v2::InteractionContents {
654 let plugin_config = interaction.plugin_config();
655 let interaction_configuration = plugin_config
656 .get(plugin_name)
657 .map(|config| Value::Object(config.iter().map(|(k, v)| (k.clone(), v.clone())).collect()));
658 proto_v2::InteractionContents {
659 interaction_type: interaction.v4_type().to_string(),
660 plugin_configuration: Some(proto_v2::PluginConfiguration {
661 interaction_configuration: interaction_configuration.map(value_to_proto_struct),
662 pact_configuration: pact_configuration.map(value_to_proto_struct),
663 }),
664 consumer: consumer.to_string(),
665 provider: provider.to_string(),
666 }
667}
668
669pub async fn shutdown_mock_server(
671 mock_server: &MockServerDetails,
672) -> anyhow::Result<Vec<MockServerResults>> {
673 let request = ShutdownMockServerRequest {
674 server_key: mock_server.key.to_string(),
675 };
676
677 debug!(
678 plugin_name = mock_server.plugin.manifest.name.as_str(),
679 plugin_version = mock_server.plugin.manifest.version.as_str(),
680 server_key = mock_server.key.as_str(),
681 "Sending shutdownMockServer request to plugin"
682 );
683 let response = mock_server.plugin.shutdown_mock_server(request).await?;
684 debug!("Got response: {response:?}");
685
686 if response.ok {
687 Ok(vec![])
688 } else {
689 Ok(
690 response
691 .results
692 .iter()
693 .map(|result| MockServerResults {
694 path: result.path.clone(),
695 error: result.error.clone(),
696 mismatches: result
697 .mismatches
698 .iter()
699 .map(|mismatch| ContentMismatch {
700 expected: mismatch
701 .expected
702 .as_ref()
703 .map(|e| from_utf8(&e).unwrap_or_default().to_string())
704 .unwrap_or_default(),
705 actual: mismatch
706 .actual
707 .as_ref()
708 .map(|a| from_utf8(&a).unwrap_or_default().to_string())
709 .unwrap_or_default(),
710 mismatch: mismatch.mismatch.clone(),
711 path: mismatch.path.clone(),
712 diff: optional_string(&mismatch.diff),
713 mismatch_type: optional_string(&mismatch.mismatch_type),
714 })
715 .collect(),
716 })
717 .collect(),
718 )
719 }
720}
721
722pub async fn get_mock_server_results(
724 mock_server: &MockServerDetails,
725) -> anyhow::Result<Vec<MockServerResults>> {
726 let request = MockServerRequest {
727 server_key: mock_server.key.to_string(),
728 };
729
730 debug!(
731 plugin_name = mock_server.plugin.manifest.name.as_str(),
732 plugin_version = mock_server.plugin.manifest.version.as_str(),
733 server_key = mock_server.key.as_str(),
734 "Sending getMockServerResults request to plugin"
735 );
736 let response = mock_server.plugin.get_mock_server_results(request).await?;
737 debug!("Got response: {response:?}");
738
739 if response.ok {
740 Ok(vec![])
741 } else {
742 Ok(
743 response
744 .results
745 .iter()
746 .map(|result| MockServerResults {
747 path: result.path.clone(),
748 error: result.error.clone(),
749 mismatches: result
750 .mismatches
751 .iter()
752 .map(|mismatch| ContentMismatch {
753 expected: mismatch
754 .expected
755 .as_ref()
756 .map(|e| from_utf8(&e).unwrap_or_default().to_string())
757 .unwrap_or_default(),
758 actual: mismatch
759 .actual
760 .as_ref()
761 .map(|a| from_utf8(&a).unwrap_or_default().to_string())
762 .unwrap_or_default(),
763 mismatch: mismatch.mismatch.clone(),
764 path: mismatch.path.clone(),
765 diff: optional_string(&mismatch.diff),
766 mismatch_type: optional_string(&mismatch.mismatch_type),
767 })
768 .collect(),
769 })
770 .collect(),
771 )
772 }
773}
774
775pub async fn prepare_validation_for_interaction(
778 transport_entry: &CatalogueEntry,
779 pact: &V4Pact,
780 interaction: &(dyn V4Interaction + Send + Sync),
781 context: &HashMap<String, Value>,
782) -> anyhow::Result<InteractionVerificationData> {
783 let manifest = transport_entry.plugin.as_ref().ok_or_else(|| {
784 anyhow!("Transport catalogue entry did not have an associated plugin manifest")
785 })?;
786 let plugin = lookup_plugin(&manifest.as_dependency())
787 .ok_or_else(|| anyhow!("Did not find a running plugin for manifest {:?}", manifest))?;
788
789 prepare_validation_for_interaction_inner(&plugin, manifest, pact, interaction, context).await
790}
791
792pub(crate) async fn prepare_validation_for_interaction_inner(
793 plugin: &(dyn PactPluginRpc + Send + Sync),
794 manifest: &PactPluginManifest,
795 pact: &V4Pact,
796 interaction: &(dyn V4Interaction + Send + Sync),
797 context: &HashMap<String, Value>,
798) -> anyhow::Result<InteractionVerificationData> {
799 debug!(
800 plugin_name = manifest.name.as_str(),
801 plugin_version = manifest.version.as_str(),
802 "Sending prepareValidationForInteraction request to plugin"
803 );
804
805 let response = if manifest.plugin_interface_version >= 2 {
806 let interaction_contents = build_v2_single_interaction_contents(manifest, pact, interaction);
807 let request = proto_v2::VerificationPreparationRequest {
808 interaction_contents: Some(interaction_contents),
809 config: Some(to_proto_struct(context)),
810 test_context: None,
811 };
812 plugin.prepare_interaction_for_verification_v2(request).await?
813 } else {
814 let mut pact = pact.clone();
815 pact.interactions = pact
816 .interactions
817 .iter()
818 .map(|i| {
819 if i.key().is_none() {
820 i.with_unique_key()
821 } else {
822 i.boxed_v4()
823 }
824 })
825 .collect();
826 let request = VerificationPreparationRequest {
827 pact: pact.to_json(PactSpecification::V4)?.to_string(),
828 interaction_key: interaction.unique_key(),
829 config: Some(to_proto_struct(context)),
830 };
831 plugin.prepare_interaction_for_verification(request).await?
832 };
833 debug!("Got response: {response:?}");
834
835 let validation_response = response.response.ok_or_else(|| {
836 anyhow!("Did not get a valid response from the prepare interaction for verification call")
837 })?;
838 match &validation_response {
839 verification_preparation_response::Response::Error(err) => {
840 Err(anyhow!("Failed to prepare the request: {}", err))
841 }
842 verification_preparation_response::Response::InteractionData(data) => {
843 let content_type = data
844 .body
845 .as_ref()
846 .and_then(|body| ContentType::parse(body.content_type.as_str()).ok());
847 Ok(InteractionVerificationData {
848 request_data: data
849 .body
850 .as_ref()
851 .and_then(|body| body.content.as_ref())
852 .map(|body| OptionalBody::Present(Bytes::from(body.clone()), content_type, None))
853 .unwrap_or_default(),
854 metadata: data
855 .metadata
856 .iter()
857 .map(|(k, v)| {
858 let value = match &v.value {
859 Some(v) => match &v {
860 metadata_value::Value::NonBinaryValue(v) => Either::Left(proto_value_to_json(v)),
861 metadata_value::Value::BinaryValue(b) => Either::Right(Bytes::from(b.clone())),
862 },
863 None => Either::Left(Value::Null),
864 };
865 (k.clone(), value)
866 })
867 .collect(),
868 })
869 }
870 }
871}
872
873pub async fn verify_interaction(
875 transport_entry: &CatalogueEntry,
876 verification_data: &InteractionVerificationData,
877 config: &HashMap<String, Value>,
878 pact: &V4Pact,
879 interaction: &(dyn V4Interaction + Send + Sync),
880) -> anyhow::Result<InteractionVerificationResult> {
881 let manifest = transport_entry.plugin.as_ref().ok_or_else(|| {
882 anyhow!("Transport catalogue entry did not have an associated plugin manifest")
883 })?;
884 let plugin = lookup_plugin(&manifest.as_dependency())
885 .ok_or_else(|| anyhow!("Did not find a running plugin for manifest {:?}", manifest))?;
886
887 verify_interaction_inner(
888 &plugin,
889 &manifest,
890 verification_data,
891 config,
892 pact,
893 interaction,
894 )
895 .await
896}
897
898pub(crate) async fn verify_interaction_inner(
899 plugin: &(dyn PactPluginRpc + Send + Sync),
900 manifest: &PactPluginManifest,
901 verification_data: &InteractionVerificationData,
902 config: &HashMap<String, Value>,
903 pact: &V4Pact,
904 interaction: &(dyn V4Interaction + Send + Sync),
905) -> anyhow::Result<InteractionVerificationResult> {
906 debug!(
907 plugin_name = manifest.name.as_str(),
908 plugin_version = manifest.version.as_str(),
909 "Sending verifyInteraction request to plugin"
910 );
911
912 let interaction_data = InteractionData {
913 body: Some((&verification_data.request_data).into()),
914 metadata: verification_data
915 .metadata
916 .iter()
917 .map(|(k, v)| {
918 (
919 k.clone(),
920 MetadataValue {
921 value: Some(match v {
922 Either::Left(value) => metadata_value::Value::NonBinaryValue(to_proto_value(value)),
923 Either::Right(b) => metadata_value::Value::BinaryValue(b.to_vec()),
924 }),
925 },
926 )
927 })
928 .collect(),
929 };
930
931 let response = if manifest.plugin_interface_version >= 2 {
932 let interaction_contents = build_v2_single_interaction_contents(manifest, pact, interaction);
933 let request = proto_v2::VerifyInteractionRequest {
934 interaction_data: Some(to_proto_v2_interaction_data(interaction_data)),
935 config: Some(to_proto_struct(config)),
936 interaction_contents: Some(interaction_contents),
937 test_context: None,
938 };
939 plugin.verify_interaction_v2(request).await?
940 } else {
941 let mut pact = pact.clone();
942 pact.interactions = pact
943 .interactions
944 .iter()
945 .map(|i| {
946 if i.key().is_none() {
947 i.with_unique_key()
948 } else {
949 i.boxed_v4()
950 }
951 })
952 .collect();
953 let request = VerifyInteractionRequest {
954 pact: pact.to_json(PactSpecification::V4)?.to_string(),
955 interaction_key: interaction.unique_key(),
956 config: Some(to_proto_struct(config)),
957 interaction_data: Some(interaction_data),
958 };
959 plugin.verify_interaction(request).await?
960 };
961 debug!("Got response: {response:?}");
962
963 let validation_response = response
964 .response
965 .ok_or_else(|| anyhow!("Did not get a valid response from the verification call"))?;
966 match &validation_response {
967 verify_interaction_response::Response::Error(err) => {
968 Err(anyhow!("Failed to verify the request: {}", err))
969 }
970 verify_interaction_response::Response::Result(data) => Ok(data.into()),
971 }
972}
973
974pub async fn install_plugin_from_url(
977 http_client: &Client,
978 source_url: &str,
979) -> anyhow::Result<PactPluginManifest> {
980 let response = fetch_json_from_url(source_url, http_client).await?;
981 if let Some(map) = response.as_object() {
982 if let Some(tag) = map.get("tag_name") {
983 let tag = json_to_string(tag);
984 debug!(%tag, "Found tag");
985 let url = if source_url.ends_with("/latest") {
986 source_url.strip_suffix("/latest").unwrap_or(source_url)
987 } else {
988 let suffix = format!("/tag/{}", tag);
989 source_url
990 .strip_suffix(suffix.as_str())
991 .unwrap_or(source_url)
992 };
993 let manifest_json = download_json_from_github(&http_client, url, &tag, "pact-plugin.json")
994 .await
995 .context("Downloading manifest file from GitHub")?;
996 let manifest: PactPluginManifest = serde_json::from_value(manifest_json)
997 .context("Failed to parsing JSON manifest file from GitHub")?;
998 debug!(?manifest, "Loaded manifest from GitHub");
999
1000 debug!(
1001 "Installing plugin {} version {}",
1002 manifest.name, manifest.version
1003 );
1004 let plugin_dir =
1005 create_plugin_dir(&manifest).context("Failed to creating plugins directory")?;
1006 download_plugin_executable(&manifest, &plugin_dir, &http_client, url, &tag, false).await?;
1007
1008 Ok(PactPluginManifest {
1009 plugin_dir: plugin_dir.to_string_lossy().to_string(),
1010 ..manifest
1011 })
1012 } else {
1013 bail!("GitHub release page does not have a valid tag_name attribute");
1014 }
1015 } else {
1016 bail!("Response from source is not a valid JSON from a GitHub release page")
1017 }
1018}
1019
1020fn create_plugin_dir(manifest: &PactPluginManifest) -> anyhow::Result<PathBuf> {
1021 let plugins_dir = pact_plugin_dir()?;
1022 if !plugins_dir.exists() {
1023 info!(plugins_dir = %plugins_dir.display(), "Creating plugins directory");
1024 fs::create_dir_all(plugins_dir.clone())?;
1025 }
1026
1027 let plugin_dir = plugins_dir.join(format!("{}-{}", manifest.name, manifest.version));
1028 info!(plugin_dir = %plugin_dir.display(), "Creating plugin directory");
1029 fs::create_dir(plugin_dir.clone())?;
1030
1031 info!("Writing plugin manifest file");
1032 let file_name = plugin_dir.join("pact-plugin.json");
1033 let mut f = File::create(file_name)?;
1034 let json = serde_json::to_string(manifest)?;
1035 f.write_all(json.as_bytes())?;
1036
1037 Ok(plugin_dir.clone())
1038}
1039
1040#[cfg(test)]
1041mod tests {
1042 use std::collections::HashMap;
1043 use std::fs::{self, File};
1044 use std::sync::RwLock;
1045
1046 use async_trait::async_trait;
1047 use maplit::hashmap;
1048 use pact_models::prelude::v4::V4Pact;
1049 use pact_models::v4::interaction::V4Interaction;
1050 use pact_models::v4::sync_message::SynchronousMessage;
1051
1052 use expectest::prelude::*;
1053 use tempdir::TempDir;
1054
1055 use crate::plugin_manager::prepare_validation_for_interaction_inner;
1056 use crate::plugin_manager::verify_interaction_inner;
1057 use crate::plugin_models::PluginDependency;
1058 use crate::plugin_models::tests::MockPlugin;
1059 use crate::proto::*;
1060 use crate::verification::InteractionVerificationData;
1061
1062 use crate::catalogue_manager::{
1063 CatalogueEntry, CatalogueEntryProviderType, CatalogueEntryType, register_core_entries,
1064 };
1065
1066 use super::{PactPluginManifest, init_handshake, initialise_plugin, load_manifest_from_dir};
1067
1068 struct FailingInitPlugin {
1069 error: String,
1070 }
1071
1072 #[async_trait]
1073 impl crate::plugin_models::PactPluginRpc for FailingInitPlugin {
1074 async fn init_plugin(
1075 &mut self,
1076 _request: crate::plugin_models::PluginInitRequest,
1077 ) -> anyhow::Result<crate::plugin_models::PluginInitResponse> {
1078 Err(anyhow::anyhow!("{}", self.error))
1079 }
1080
1081 async fn compare_contents(
1082 &self,
1083 _request: CompareContentsRequest,
1084 ) -> anyhow::Result<CompareContentsResponse> {
1085 unimplemented!()
1086 }
1087
1088 async fn configure_interaction(
1089 &self,
1090 _request: ConfigureInteractionRequest,
1091 ) -> anyhow::Result<ConfigureInteractionResponse> {
1092 unimplemented!()
1093 }
1094
1095 async fn generate_content(
1096 &self,
1097 _request: GenerateContentRequest,
1098 ) -> anyhow::Result<GenerateContentResponse> {
1099 unimplemented!()
1100 }
1101
1102 async fn start_mock_server(
1103 &self,
1104 _request: StartMockServerRequest,
1105 ) -> anyhow::Result<StartMockServerResponse> {
1106 unimplemented!()
1107 }
1108
1109 async fn shutdown_mock_server(
1110 &self,
1111 _request: ShutdownMockServerRequest,
1112 ) -> anyhow::Result<ShutdownMockServerResponse> {
1113 unimplemented!()
1114 }
1115
1116 async fn get_mock_server_results(
1117 &self,
1118 _request: MockServerRequest,
1119 ) -> anyhow::Result<MockServerResults> {
1120 unimplemented!()
1121 }
1122
1123 async fn prepare_interaction_for_verification(
1124 &self,
1125 _request: VerificationPreparationRequest,
1126 ) -> anyhow::Result<VerificationPreparationResponse> {
1127 unimplemented!()
1128 }
1129
1130 async fn verify_interaction(
1131 &self,
1132 _request: VerifyInteractionRequest,
1133 ) -> anyhow::Result<VerifyInteractionResponse> {
1134 unimplemented!()
1135 }
1136
1137 async fn update_catalogue(&self, _request: Catalogue) -> anyhow::Result<()> {
1138 unimplemented!()
1139 }
1140 }
1141
1142 struct InitRecordingPlugin {
1143 request: RwLock<Option<crate::plugin_models::PluginInitRequest>>,
1144 }
1145
1146 impl Default for InitRecordingPlugin {
1147 fn default() -> Self {
1148 Self {
1149 request: RwLock::new(None),
1150 }
1151 }
1152 }
1153
1154 #[async_trait]
1155 impl crate::plugin_models::PactPluginRpc for InitRecordingPlugin {
1156 async fn init_plugin(
1157 &mut self,
1158 request: crate::plugin_models::PluginInitRequest,
1159 ) -> anyhow::Result<crate::plugin_models::PluginInitResponse> {
1160 *self.request.write().unwrap() = Some(request);
1161 Ok(crate::plugin_models::PluginInitResponse {
1162 catalogue: vec![],
1163 plugin_capabilities: vec!["interaction/request-response".to_string()],
1164 })
1165 }
1166
1167 async fn compare_contents(
1168 &self,
1169 _request: CompareContentsRequest,
1170 ) -> anyhow::Result<CompareContentsResponse> {
1171 unimplemented!()
1172 }
1173
1174 async fn configure_interaction(
1175 &self,
1176 _request: ConfigureInteractionRequest,
1177 ) -> anyhow::Result<ConfigureInteractionResponse> {
1178 unimplemented!()
1179 }
1180
1181 async fn generate_content(
1182 &self,
1183 _request: GenerateContentRequest,
1184 ) -> anyhow::Result<GenerateContentResponse> {
1185 unimplemented!()
1186 }
1187
1188 async fn start_mock_server(
1189 &self,
1190 _request: StartMockServerRequest,
1191 ) -> anyhow::Result<StartMockServerResponse> {
1192 unimplemented!()
1193 }
1194
1195 async fn shutdown_mock_server(
1196 &self,
1197 _request: ShutdownMockServerRequest,
1198 ) -> anyhow::Result<ShutdownMockServerResponse> {
1199 unimplemented!()
1200 }
1201
1202 async fn get_mock_server_results(
1203 &self,
1204 _request: MockServerRequest,
1205 ) -> anyhow::Result<MockServerResults> {
1206 unimplemented!()
1207 }
1208
1209 async fn prepare_interaction_for_verification(
1210 &self,
1211 _request: VerificationPreparationRequest,
1212 ) -> anyhow::Result<VerificationPreparationResponse> {
1213 unimplemented!()
1214 }
1215
1216 async fn verify_interaction(
1217 &self,
1218 _request: VerifyInteractionRequest,
1219 ) -> anyhow::Result<VerifyInteractionResponse> {
1220 unimplemented!()
1221 }
1222
1223 async fn update_catalogue(&self, _request: Catalogue) -> anyhow::Result<()> {
1224 Ok(())
1225 }
1226 }
1227
1228 #[test]
1229 fn load_manifest_from_dir_test() {
1230 let tmp_dir = TempDir::new("load_manifest_from_dir").unwrap();
1231
1232 let manifest_1 = PactPluginManifest {
1233 name: "test-plugin".to_string(),
1234 version: "0.1.5".to_string(),
1235 ..PactPluginManifest::default()
1236 };
1237 let path_1 = tmp_dir.path().join("1");
1238 fs::create_dir_all(&path_1).unwrap();
1239 let file_1 = File::create(path_1.join("pact-plugin.json")).unwrap();
1240 serde_json::to_writer(file_1, &manifest_1).unwrap();
1241
1242 let manifest_2 = PactPluginManifest {
1243 name: "test-plugin".to_string(),
1244 version: "0.1.20".to_string(),
1245 ..PactPluginManifest::default()
1246 };
1247 let path_2 = tmp_dir.path().join("2");
1248 fs::create_dir_all(&path_2).unwrap();
1249 let file_2 = File::create(path_2.join("pact-plugin.json")).unwrap();
1250 serde_json::to_writer(file_2, &manifest_2).unwrap();
1251
1252 let manifest_3 = PactPluginManifest {
1253 name: "test-plugin".to_string(),
1254 version: "0.1.7".to_string(),
1255 ..PactPluginManifest::default()
1256 };
1257 let path_3 = tmp_dir.path().join("3");
1258 fs::create_dir_all(&path_3).unwrap();
1259 let file_3 = File::create(path_3.join("pact-plugin.json")).unwrap();
1260 serde_json::to_writer(file_3, &manifest_3).unwrap();
1261
1262 let manifest_4 = PactPluginManifest {
1263 name: "test-plugin".to_string(),
1264 version: "0.1.14".to_string(),
1265 ..PactPluginManifest::default()
1266 };
1267 let path_4 = tmp_dir.path().join("4");
1268 fs::create_dir_all(&path_4).unwrap();
1269 let file_4 = File::create(path_4.join("pact-plugin.json")).unwrap();
1270 serde_json::to_writer(file_4, &manifest_4).unwrap();
1271
1272 let manifest_5 = PactPluginManifest {
1273 name: "test-plugin".to_string(),
1274 version: "0.1.12".to_string(),
1275 ..PactPluginManifest::default()
1276 };
1277 let path_5 = tmp_dir.path().join("5");
1278 fs::create_dir_all(&path_5).unwrap();
1279 let file_5 = File::create(path_5.join("pact-plugin.json")).unwrap();
1280 serde_json::to_writer(file_5, &manifest_5).unwrap();
1281
1282 let dep = PluginDependency {
1283 name: "test-plugin".to_string(),
1284 version: None,
1285 dependency_type: Default::default(),
1286 };
1287
1288 let result = load_manifest_from_dir(&dep, &tmp_dir.path().to_path_buf()).unwrap();
1289 expect!(result.version).to(be_equal_to("0.1.20"));
1290 }
1291
1292 #[test_log::test(tokio::test)]
1293 async fn initialise_plugin_rejects_unsupported_interface_versions() {
1294 let manifest = PactPluginManifest {
1295 name: "test-plugin".to_string(),
1296 version: "0.0.0".to_string(),
1297 executable_type: "exec".to_string(),
1298 plugin_interface_version: 3,
1299 ..PactPluginManifest::default()
1300 };
1301
1302 let mut plugin_register = HashMap::new();
1303 let err = initialise_plugin(&manifest, &mut plugin_register)
1304 .await
1305 .unwrap_err();
1306
1307 expect!(err.to_string()).to(be_equal_to(
1308 "Plugin test-plugin:0.0.0 declared an invalid interface version".to_string(),
1309 ));
1310 }
1311
1312 #[test_log::test(tokio::test)]
1313 async fn init_handshake_sends_host_capabilities_and_returns_plugin_capabilities() {
1314 register_core_entries(&vec![CatalogueEntry {
1315 entry_type: CatalogueEntryType::CONTENT_MATCHER,
1316 provider_type: CatalogueEntryProviderType::CORE,
1317 plugin: None,
1318 key: "test-content-type".to_string(),
1319 values: Default::default(),
1320 }]);
1321
1322 let manifest = PactPluginManifest {
1323 name: "test-plugin".to_string(),
1324 version: "0.0.0".to_string(),
1325 ..PactPluginManifest::default()
1326 };
1327 let mut plugin = InitRecordingPlugin::default();
1328
1329 let response = init_handshake(&manifest, &mut plugin).await.unwrap();
1330 let request = plugin.request.read().unwrap().clone().unwrap();
1331
1332 assert!(
1333 request.host_capabilities.contains(&"content-matcher/test-content-type".to_string()),
1334 "expected host_capabilities to contain 'content-matcher/test-content-type', got: {:?}",
1335 request.host_capabilities
1336 );
1337 expect!(response.plugin_capabilities).to(be_equal_to(vec![
1338 "interaction/request-response".to_string(),
1339 ]));
1340 }
1341
1342 #[test_log::test(tokio::test)]
1343 async fn init_handshake_propagates_plugin_init_failure() {
1344 let manifest = PactPluginManifest {
1345 name: "test-plugin".to_string(),
1346 version: "0.0.0".to_string(),
1347 ..PactPluginManifest::default()
1348 };
1349 let expected_error = "CSV plugin requires request/response-scoped interaction support \
1350 (missing host capabilities: interaction/request-response)";
1351 let mut plugin = FailingInitPlugin { error: expected_error.to_string() };
1352
1353 let err = init_handshake(&manifest, &mut plugin).await.unwrap_err();
1354
1355 expect!(err.to_string()).to(be_equal_to(expected_error.to_string()));
1356 }
1357
1358 #[test_log::test(tokio::test)]
1359 async fn prepare_validation_for_interaction_passes_in_pact_with_interaction_keys_set() {
1360 let manifest = PactPluginManifest {
1361 name: "test-plugin".to_string(),
1362 version: "0.0.0".to_string(),
1363 ..PactPluginManifest::default()
1364 };
1365 let mock_plugin = MockPlugin::default();
1366
1367 let interaction = SynchronousMessage {
1368 ..SynchronousMessage::default()
1369 };
1370 let pact = V4Pact {
1371 interactions: vec![interaction.boxed_v4()],
1372 ..V4Pact::default()
1373 };
1374 let context = hashmap! {};
1375
1376 let result = prepare_validation_for_interaction_inner(
1377 &mock_plugin,
1378 &manifest,
1379 &pact,
1380 &interaction,
1381 &context,
1382 )
1383 .await;
1384
1385 expect!(result).to(be_ok());
1386 let request = {
1387 let r = mock_plugin.prepare_request.read().unwrap();
1388 r.clone()
1389 };
1390 let pact_in =
1391 V4Pact::pact_from_json(&serde_json::from_str(request.pact.as_str()).unwrap(), "").unwrap();
1392 expect!(pact_in.interactions[0].key().unwrap()).to(be_equal_to(request.interaction_key));
1393 }
1394
1395 #[test_log::test(tokio::test)]
1396 async fn prepare_validation_for_interaction_handles_pact_with_keys_already_set() {
1397 let manifest = PactPluginManifest {
1398 name: "test-plugin".to_string(),
1399 version: "0.0.0".to_string(),
1400 ..PactPluginManifest::default()
1401 };
1402 let mock_plugin = MockPlugin::default();
1403
1404 let interaction = SynchronousMessage {
1405 key: Some("1234567890".to_string()),
1406 ..SynchronousMessage::default()
1407 };
1408 let pact = V4Pact {
1409 interactions: vec![interaction.boxed_v4()],
1410 ..V4Pact::default()
1411 };
1412 let context = hashmap! {};
1413
1414 let result = prepare_validation_for_interaction_inner(
1415 &mock_plugin,
1416 &manifest,
1417 &pact,
1418 &interaction,
1419 &context,
1420 )
1421 .await;
1422
1423 expect!(result).to(be_ok());
1424 let request = {
1425 let r = mock_plugin.prepare_request.read().unwrap();
1426 r.clone()
1427 };
1428 let pact_in =
1429 V4Pact::pact_from_json(&serde_json::from_str(request.pact.as_str()).unwrap(), "").unwrap();
1430 expect!(request.interaction_key.as_str()).to(be_equal_to("1234567890"));
1431 expect!(pact_in.interactions[0].key().unwrap()).to(be_equal_to(request.interaction_key));
1432 }
1433
1434 #[test_log::test(tokio::test)]
1435 async fn verify_interaction_passes_in_pact_with_interaction_keys_set() {
1436 let manifest = PactPluginManifest {
1437 name: "test-plugin".to_string(),
1438 version: "0.0.0".to_string(),
1439 ..PactPluginManifest::default()
1440 };
1441 let mock_plugin = MockPlugin::default();
1442
1443 let interaction = SynchronousMessage {
1444 ..SynchronousMessage::default()
1445 };
1446 let pact = V4Pact {
1447 interactions: vec![interaction.boxed_v4()],
1448 ..V4Pact::default()
1449 };
1450 let context = hashmap! {};
1451 let data = InteractionVerificationData::default();
1452
1453 let result = verify_interaction_inner(
1454 &mock_plugin,
1455 &manifest,
1456 &data,
1457 &context,
1458 &pact,
1459 &interaction,
1460 )
1461 .await;
1462
1463 expect!(result).to(be_ok());
1464 let request = {
1465 let r = mock_plugin.verify_request.read().unwrap();
1466 r.clone()
1467 };
1468 let pact_in =
1469 V4Pact::pact_from_json(&serde_json::from_str(request.pact.as_str()).unwrap(), "").unwrap();
1470 expect!(pact_in.interactions[0].key().unwrap()).to(be_equal_to(request.interaction_key));
1471 }
1472
1473 #[test_log::test(tokio::test)]
1474 async fn verify_interaction_handles_interaction_with_key_already_set() {
1475 let manifest = PactPluginManifest {
1476 name: "test-plugin".to_string(),
1477 version: "0.0.0".to_string(),
1478 ..PactPluginManifest::default()
1479 };
1480 let mock_plugin = MockPlugin::default();
1481
1482 let interaction = SynchronousMessage {
1483 key: Some("1234567890".to_string()),
1484 ..SynchronousMessage::default()
1485 };
1486 let pact = V4Pact {
1487 interactions: vec![interaction.boxed_v4()],
1488 ..V4Pact::default()
1489 };
1490 let context = hashmap! {};
1491 let data = InteractionVerificationData::default();
1492
1493 let result = verify_interaction_inner(
1494 &mock_plugin,
1495 &manifest,
1496 &data,
1497 &context,
1498 &pact,
1499 &interaction,
1500 )
1501 .await;
1502
1503 expect!(result).to(be_ok());
1504 let request = {
1505 let r = mock_plugin.verify_request.read().unwrap();
1506 r.clone()
1507 };
1508 let pact_in =
1509 V4Pact::pact_from_json(&serde_json::from_str(request.pact.as_str()).unwrap(), "").unwrap();
1510 expect!(request.interaction_key.as_str()).to(be_equal_to("1234567890"));
1511 expect!(pact_in.interactions[0].key().unwrap()).to(be_equal_to(request.interaction_key));
1512 }
1513}