Skip to main content

pact_plugin_driver/
plugin_manager.rs

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