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