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