1use 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 instance: Arc<dyn PluginInstance + Send + Sync>,
55 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 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
126pub 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
200pub 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
217pub 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
303pub 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
402pub 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
424pub 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
447pub 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
458pub 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#[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#[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#[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
574pub 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
637fn 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
654fn 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
677fn 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
719pub 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
773pub 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
827pub 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
926pub 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
1027pub 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 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 #[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}