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