Skip to main content

pact_plugin_driver/
plugin_manager.rs

1//! Manages interactions with Pact plugins
2use 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
67/// Load the plugin defined by the dependency information. Will first look in the global
68/// plugin registry.
69pub 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
136/// Look up the plugin in the global plugin register
137pub 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
153/// Return the plugin manifest for the given plugin. Will first look in the global plugin manifest
154/// registry.
155pub 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
239/// Lookup the plugin manifest in the global plugin manifest registry.
240pub 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
296/// Internal function: public for testing
297pub 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        // revert windows specific logic once https://github.com/GuillaumeGomez/sysinfo/pull/1341/files is merged/released
367        #[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
382/// Shut down all plugin processes
383pub 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
404/// Shutdown the given plugin
405pub 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
414/// Publish the current catalogue to all plugins
415pub 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/// Increment access to the plugin.
458#[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/// Decrement access to the plugin. If the current access count is zero, shut down the plugin
483#[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/// Starts a mock server given the catalog entry for it and a Pact
512#[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
524/// Starts a mock server given the catalog entry for it and a Pact.
525/// Any transport specific configuration must be passed in the `test_context` field under
526/// the `transport_config` key.
527pub 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
587/// Convert a V1 InteractionData to a V2 InteractionData (structurally identical; binary conversion is safe).
588fn 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
604/// Build a list of V2 InteractionContents from a V4 pact and plugin manifest.
605fn 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
627/// Build V2 InteractionContents for a single interaction.
628fn 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
669/// Shutdowns a running mock server. Will return any errors from the mock server.
670pub 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
722/// Gets the results from a running mock server.
723pub 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
775/// Sets up a transport request to be made. This is the first phase when verifying, and it allows the
776/// users to add additional values to any requests that are made.
777pub 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
873/// Executes the verification of the interaction that was configured with the prepare_validation_for_interaction call
874pub 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
974/// Tries to download and install the plugin from the given URL, returning the manifest for the
975/// plugin if successful.
976pub 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}