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