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