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