Skip to main content

pact_plugin_driver/
plugin_models.rs

1//! Models for representing plugins
2
3use std::collections::HashMap;
4use std::fmt::{Display, Formatter};
5use std::sync::Arc;
6use std::sync::atomic::{AtomicUsize, Ordering};
7
8use anyhow::anyhow;
9use async_trait::async_trait;
10use pact_models::v4::V4InteractionType;
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use tracing::trace;
14
15use crate::child_process::ChildPluginProcess;
16use crate::proto::*;
17use crate::proto_v2;
18
19/// Type of plugin dependencies
20#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
21pub enum PluginDependencyType {
22  /// Required operating system package
23  OSPackage,
24  /// Dependency on another plugin
25  Plugin,
26  /// Dependency on a shared library
27  Library,
28  /// Dependency on an executable
29  Executable,
30}
31
32impl Default for PluginDependencyType {
33  fn default() -> Self {
34    PluginDependencyType::Plugin
35  }
36}
37
38/// Plugin dependency
39#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
40#[serde(rename_all = "camelCase")]
41pub struct PluginDependency {
42  /// Dependency name
43  pub name: String,
44  /// Dependency version (semver format)
45  pub version: Option<String>,
46  /// Type of dependency
47  #[serde(default)]
48  pub dependency_type: PluginDependencyType,
49}
50
51impl Display for PluginDependency {
52  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
53    if let Some(version) = &self.version {
54      write!(f, "{}:{}", self.name, version)
55    } else {
56      write!(f, "{}:*", self.name)
57    }
58  }
59}
60
61/// Manifest of a plugin
62#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
63#[serde(rename_all = "camelCase")]
64pub struct PactPluginManifest {
65  /// Directory were the plugin was loaded from
66  #[serde(skip)]
67  pub plugin_dir: String,
68
69  /// Interface version supported by the plugin
70  pub plugin_interface_version: u8,
71
72  /// Plugin name
73  pub name: String,
74
75  /// Plugin version in semver format
76  pub version: String,
77
78  /// Type if executable of the plugin
79  pub executable_type: String,
80
81  /// Minimum required version for the executable type
82  pub minimum_required_version: Option<String>,
83
84  /// How to invoke the plugin
85  pub entry_point: String,
86
87  /// Additional entry points for other operating systems (i.e. requiring a .bat file for Windows)
88  #[serde(default)]
89  pub entry_points: HashMap<String, String>,
90
91  /// Parameters to pass into the command line
92  pub args: Option<Vec<String>>,
93
94  /// Dependencies required to invoke the plugin
95  pub dependencies: Option<Vec<PluginDependency>>,
96
97  /// Plugin specific config
98  #[serde(default)]
99  pub plugin_config: HashMap<String, Value>,
100}
101
102impl PactPluginManifest {
103  pub fn as_dependency(&self) -> PluginDependency {
104    PluginDependency {
105      name: self.name.clone(),
106      version: Some(self.version.clone()),
107      dependency_type: PluginDependencyType::Plugin,
108    }
109  }
110}
111
112impl Default for PactPluginManifest {
113  fn default() -> Self {
114    PactPluginManifest {
115      plugin_dir: "".to_string(),
116      plugin_interface_version: 1,
117      name: "".to_string(),
118      version: "".to_string(),
119      executable_type: "".to_string(),
120      minimum_required_version: None,
121      entry_point: "".to_string(),
122      entry_points: Default::default(),
123      args: None,
124      dependencies: None,
125      plugin_config: Default::default(),
126    }
127  }
128}
129
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub enum PluginInterfaceVersion {
132  V1,
133  V2,
134}
135
136#[derive(Clone, Debug, PartialEq, Eq)]
137pub struct PluginInitRequest {
138  pub implementation: String,
139  pub version: String,
140  pub host_capabilities: Vec<String>,
141  pub plugin_instance_id: String,
142}
143
144#[derive(Clone, Debug, PartialEq)]
145pub struct PluginInitResponse {
146  pub catalogue: Vec<CatalogueEntry>,
147  pub plugin_capabilities: Vec<String>,
148}
149
150impl TryFrom<u8> for PluginInterfaceVersion {
151  type Error = anyhow::Error;
152
153  fn try_from(value: u8) -> Result<Self, Self::Error> {
154    match value {
155      1 => Ok(PluginInterfaceVersion::V1),
156      2 => Ok(PluginInterfaceVersion::V2),
157      _ => Err(anyhow!("Unsupported plugin interface version {}", value)),
158    }
159  }
160}
161
162/// Capability name a V2 plugin declares to say it can handle interactions of this type.
163///
164/// There is one per interaction type a Pact file can record, and they are the same names the host
165/// registers as `INTERACTION` catalogue entries and advertises in `hostCapabilities`. The transport
166/// an interaction is carried over is a separate concern - it is only history that the
167/// request/response interaction is the original Pact one carried over HTTP or HTTPS.
168pub fn interaction_type_capability(interaction_type: &V4InteractionType) -> &'static str {
169  match interaction_type {
170    V4InteractionType::Synchronous_HTTP => "interaction/request-response",
171    V4InteractionType::Asynchronous_Messages => "interaction/message",
172    V4InteractionType::Synchronous_Messages => "interaction/synchronous-message",
173  }
174}
175
176/// Every interaction type a Pact file can record.
177pub const ALL_INTERACTION_TYPES: [V4InteractionType; 3] = [
178  V4InteractionType::Synchronous_HTTP,
179  V4InteractionType::Asynchronous_Messages,
180  V4InteractionType::Synchronous_Messages,
181];
182
183/// Check that a plugin declared it can handle interactions of the given type.
184///
185/// This is the plugin→driver half of the capability negotiation from proposal 005: a V2 plugin
186/// declares an `interaction/*` capability for each interaction type it understands, and the driver
187/// refuses to hand it an interaction of a type it did not declare rather than letting the plugin
188/// fail further in with a less obvious error.
189///
190/// A plugin that declares no interaction capability at all is treated as supporting every type, so
191/// V2 plugins written before these capabilities existed keep working unchanged. Declaring one is
192/// therefore opting in to the check for all three.
193pub fn check_interaction_type_capability(
194  plugin: &dyn PluginInstance,
195  interaction_type: V4InteractionType,
196) -> anyhow::Result<()> {
197  let declares_any = ALL_INTERACTION_TYPES
198    .iter()
199    .any(|interaction_type| plugin.has_capability(interaction_type_capability(interaction_type)));
200  if !declares_any {
201    return Ok(());
202  }
203
204  let required = interaction_type_capability(&interaction_type);
205  if plugin.has_capability(required) {
206    Ok(())
207  } else {
208    let manifest = plugin.manifest();
209    Err(anyhow!(
210      "Plugin {}/{} does not support {} interactions - it did not declare the '{}' capability",
211      manifest.name,
212      manifest.version,
213      interaction_type,
214      required
215    ))
216  }
217}
218
219/// Trait for the plugin init handshake only (used by anything that can handle the init message)
220#[async_trait]
221pub trait PactPluginRpc {
222  /// Send an init request to the plugin process
223  async fn init_plugin(&mut self, request: PluginInitRequest)
224    -> anyhow::Result<PluginInitResponse>;
225}
226
227/// Trait for a running plugin instance.
228///
229/// Implementations include [`crate::grpc_plugin::GrpcPactPlugin`] for exec-type plugins
230/// that communicate via gRPC, and future embedded runtimes (Lua, Python, …).
231#[async_trait]
232pub trait PluginInstance: std::fmt::Debug + Send + Sync {
233  /// Return the manifest for this plugin.
234  fn manifest(&self) -> &PactPluginManifest;
235
236  /// Return the instance ID assigned to this plugin at startup.
237  fn instance_id(&self) -> &str;
238
239  /// Check whether the plugin declared a specific capability.
240  fn has_capability(&self, capability: &str) -> bool;
241
242  /// Terminate the running plugin. The default no-op suits embedded runtimes
243  /// that are not managed as a child process.
244  fn kill(&self) {}
245
246  /// Send a compare contents request to the plugin process
247  async fn compare_contents(
248    &self,
249    request: CompareContentsRequest,
250  ) -> anyhow::Result<CompareContentsResponse>;
251
252  /// Send a compare contents request to the plugin process, propagating call-chain cycle
253  /// detection and deadline metadata (see [`crate::call_chain`]) for transports that support it.
254  /// The default implementation ignores `chain_id`/`deadline_ms` and delegates to
255  /// [`PluginInstance::compare_contents`], which suits in-process runtimes (Lua, WASM) where a
256  /// cycle is already caught by the native call stack; [`crate::grpc_plugin::GrpcPactPlugin`]
257  /// overrides this to send the metadata over gRPC.
258  async fn compare_contents_with_chain(
259    &self,
260    request: CompareContentsRequest,
261    chain_id: &str,
262    deadline_ms: u64,
263  ) -> anyhow::Result<CompareContentsResponse> {
264    let _ = (chain_id, deadline_ms);
265    self.compare_contents(request).await
266  }
267
268  /// Send a configure contents request to the plugin process
269  async fn configure_interaction(
270    &self,
271    request: ConfigureInteractionRequest,
272  ) -> anyhow::Result<ConfigureInteractionResponse>;
273
274  /// Send a generate content request to the plugin
275  async fn generate_content(
276    &self,
277    request: GenerateContentRequest,
278  ) -> anyhow::Result<GenerateContentResponse>;
279
280  /// Send a generate content request to the plugin, propagating call-chain cycle detection and
281  /// deadline metadata (see [`crate::call_chain`]) for transports that support it. See
282  /// [`PluginInstance::compare_contents_with_chain`] for the default/override split.
283  async fn generate_content_with_chain(
284    &self,
285    request: GenerateContentRequest,
286    chain_id: &str,
287    deadline_ms: u64,
288  ) -> anyhow::Result<GenerateContentResponse> {
289    let _ = (chain_id, deadline_ms);
290    self.generate_content(request).await
291  }
292
293  /// Apply a plugin-provided matching rule to a single value (see proposal 006). Field-level
294  /// operations exist only on the V2 interface, so the default reports that this plugin cannot
295  /// provide them - the same shape as the other V2-only operations below.
296  async fn match_field(
297    &self,
298    request: proto_v2::MatchFieldRequest,
299  ) -> anyhow::Result<proto_v2::MatchFieldResponse> {
300    let _ = request;
301    Err(anyhow!("Field-level matching rules are not supported by this plugin"))
302  }
303
304  /// Apply a plugin-provided matching rule to a single value, propagating call-chain cycle
305  /// detection and deadline metadata (see [`crate::call_chain`]) for transports that support it.
306  /// See [`PluginInstance::compare_contents_with_chain`] for the default/override split.
307  async fn match_field_with_chain(
308    &self,
309    request: proto_v2::MatchFieldRequest,
310    chain_id: &str,
311    deadline_ms: u64,
312  ) -> anyhow::Result<proto_v2::MatchFieldResponse> {
313    let _ = (chain_id, deadline_ms);
314    self.match_field(request).await
315  }
316
317  /// Apply a plugin-provided generator to a single value (see proposal 006). See
318  /// [`PluginInstance::match_field`].
319  async fn generate_field(
320    &self,
321    request: proto_v2::GenerateFieldRequest,
322  ) -> anyhow::Result<proto_v2::GenerateFieldResponse> {
323    let _ = request;
324    Err(anyhow!("Field-level generators are not supported by this plugin"))
325  }
326
327  /// Apply a plugin-provided generator to a single value, propagating call-chain cycle detection
328  /// and deadline metadata. See [`PluginInstance::match_field_with_chain`].
329  async fn generate_field_with_chain(
330    &self,
331    request: proto_v2::GenerateFieldRequest,
332    chain_id: &str,
333    deadline_ms: u64,
334  ) -> anyhow::Result<proto_v2::GenerateFieldResponse> {
335    let _ = (chain_id, deadline_ms);
336    self.generate_field(request).await
337  }
338
339  /// Start a mock server
340  async fn start_mock_server(
341    &self,
342    request: StartMockServerRequest,
343  ) -> anyhow::Result<StartMockServerResponse>;
344
345  /// Start a mock server using V2 structured interaction data (no pact JSON).
346  async fn start_mock_server_v2(
347    &self,
348    request: proto_v2::StartMockServerRequest,
349  ) -> anyhow::Result<StartMockServerResponse> {
350    let _ = request;
351    Err(anyhow!("V2 interface not supported by this plugin"))
352  }
353
354  /// Shutdown a running mock server
355  async fn shutdown_mock_server(
356    &self,
357    request: ShutdownMockServerRequest,
358  ) -> anyhow::Result<ShutdownMockServerResponse>;
359
360  /// Get the matching results from a running mock server
361  async fn get_mock_server_results(
362    &self,
363    request: MockServerRequest,
364  ) -> anyhow::Result<MockServerResults>;
365
366  /// Prepare an interaction for verification.
367  async fn prepare_interaction_for_verification(
368    &self,
369    request: VerificationPreparationRequest,
370  ) -> anyhow::Result<VerificationPreparationResponse>;
371
372  /// Prepare an interaction for verification using V2 structured interaction data.
373  async fn prepare_interaction_for_verification_v2(
374    &self,
375    request: proto_v2::VerificationPreparationRequest,
376  ) -> anyhow::Result<VerificationPreparationResponse> {
377    let _ = request;
378    Err(anyhow!("V2 interface not supported by this plugin"))
379  }
380
381  /// Execute the verification for the interaction.
382  async fn verify_interaction(
383    &self,
384    request: VerifyInteractionRequest,
385  ) -> anyhow::Result<VerifyInteractionResponse>;
386
387  /// Execute the verification for the interaction using V2 structured interaction data.
388  async fn verify_interaction_v2(
389    &self,
390    request: proto_v2::VerifyInteractionRequest,
391  ) -> anyhow::Result<VerifyInteractionResponse> {
392    let _ = request;
393    Err(anyhow!("V2 interface not supported by this plugin"))
394  }
395
396  /// Updates the catalogue.
397  async fn update_catalogue(&self, request: Catalogue) -> anyhow::Result<()>;
398}
399
400/// Running plugin details
401#[derive(Debug, Clone)]
402pub struct PactPlugin {
403  /// Manifest for this plugin
404  pub manifest: PactPluginManifest,
405
406  /// Interface version supported by the plugin
407  pub interface_version: PluginInterfaceVersion,
408
409  /// Running child process.
410  ///
411  /// Deprecated: not all plugin types have a child process; this field is now
412  /// owned by the gRPC layer. Access plugin lifecycle through [`PluginInstance`] methods instead.
413  #[deprecated(
414    note = "Not all plugin types have a child process; use PluginInstance methods for plugin lifecycle"
415  )]
416  pub child: Arc<ChildPluginProcess>,
417
418  /// Optional capabilities negotiated for this plugin instance
419  pub plugin_capabilities: Vec<String>,
420
421  /// UUID assigned by the driver at process start; used to correlate log output from this instance
422  pub instance_id: String,
423
424  /// Count of access to the plugin. If this is ever zero, the plugin process will be shutdown
425  access_count: Arc<AtomicUsize>,
426}
427
428impl PactPlugin {
429  /// Create a new Plugin
430  #[allow(deprecated)]
431  pub fn new(manifest: &PactPluginManifest, child: ChildPluginProcess) -> anyhow::Result<Self> {
432    let instance_id = child.instance_id.clone();
433    Ok(PactPlugin {
434      manifest: manifest.clone(),
435      interface_version: PluginInterfaceVersion::try_from(manifest.plugin_interface_version)?,
436      instance_id,
437      child: Arc::new(child),
438      plugin_capabilities: vec![],
439      access_count: Arc::new(AtomicUsize::new(1)),
440    })
441  }
442
443  pub fn has_plugin_capability(&self, capability: &str) -> bool {
444    self.plugin_capabilities.iter().any(|value| value == capability)
445  }
446
447  /// Check if this plugin has the given capability
448  pub fn has_capability(&self, capability: &str) -> bool {
449    self.plugin_capabilities.iter().any(|c| c == capability)
450  }
451
452  /// Return the instance ID for this plugin
453  pub fn instance_id(&self) -> &str {
454    &self.instance_id
455  }
456
457  /// Port the plugin is running on.
458  ///
459  /// Deprecated: port is a gRPC-specific concept; use `GrpcPactPlugin` directly if you need it.
460  #[deprecated(note = "Port is specific to gRPC plugins; access it via GrpcPactPlugin")]
461  #[allow(deprecated)]
462  pub fn port(&self) -> u16 {
463    self.child.port()
464  }
465
466  /// Kill the running plugin process.
467  ///
468  /// Deprecated: use [`PluginInstance::kill`] instead so non-gRPC plugin types are handled correctly.
469  #[deprecated(note = "Use PluginInstance::kill() instead")]
470  #[allow(deprecated)]
471  pub fn kill(&self) {
472    self.child.kill();
473  }
474
475  /// Update the access count of the plugin
476  pub fn update_access(&self) {
477    let count = self.access_count.fetch_add(1, Ordering::SeqCst);
478    trace!(
479      "update_access: Plugin {}/{} access is now {}",
480      self.manifest.name,
481      self.manifest.version,
482      count + 1
483    );
484  }
485
486  /// Decrement and return the access count for the plugin
487  pub fn drop_access(&self) -> usize {
488    let check = self
489      .access_count
490      .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| {
491        if count > 0 { Some(count - 1) } else { None }
492      });
493    let count = if let Ok(v) = check {
494      if v > 0 { v - 1 } else { v }
495    } else {
496      0
497    };
498    trace!(
499      "drop_access: Plugin {}/{} access is now {}",
500      self.manifest.name, self.manifest.version, count
501    );
502    count
503  }
504}
505
506/// Plugin configuration to add to the matching context for an interaction
507#[derive(Clone, Debug, PartialEq)]
508pub struct PluginInteractionConfig {
509  /// Global plugin config (Pact level)
510  pub pact_configuration: HashMap<String, Value>,
511  /// Interaction plugin config
512  pub interaction_configuration: HashMap<String, Value>,
513}
514
515#[cfg(test)]
516pub(crate) mod tests {
517  use std::sync::RwLock;
518
519  use async_trait::async_trait;
520  use expectest::prelude::*;
521  use pact_models::v4::V4InteractionType;
522
523  use crate::plugin_models::{
524    ALL_INTERACTION_TYPES, PactPluginManifest, PluginInitRequest, PluginInitResponse,
525    PluginInstance, check_interaction_type_capability, interaction_type_capability,
526  };
527  use crate::proto::verification_preparation_response::Response;
528  use crate::proto::*;
529  use crate::proto_v2;
530
531  pub(crate) struct MockPlugin {
532    pub manifest: PactPluginManifest,
533    pub capabilities: Vec<String>,
534    pub prepare_request: RwLock<VerificationPreparationRequest>,
535    pub verify_request: RwLock<VerifyInteractionRequest>,
536    pub prepare_request_v2: RwLock<Option<proto_v2::VerificationPreparationRequest>>,
537    pub verify_request_v2: RwLock<Option<proto_v2::VerifyInteractionRequest>>,
538  }
539
540  impl std::fmt::Debug for MockPlugin {
541    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
542      f.debug_struct("MockPlugin")
543        .field("manifest", &self.manifest)
544        .finish()
545    }
546  }
547
548  impl Default for MockPlugin {
549    fn default() -> Self {
550      MockPlugin {
551        manifest: PactPluginManifest::default(),
552        capabilities: vec![],
553        prepare_request: RwLock::new(VerificationPreparationRequest::default()),
554        verify_request: RwLock::new(VerifyInteractionRequest::default()),
555        prepare_request_v2: RwLock::new(None),
556        verify_request_v2: RwLock::new(None),
557      }
558    }
559  }
560
561  #[async_trait]
562  impl PluginInstance for MockPlugin {
563    fn manifest(&self) -> &PactPluginManifest {
564      &self.manifest
565    }
566
567    fn instance_id(&self) -> &str {
568      "test-instance"
569    }
570
571    fn has_capability(&self, capability: &str) -> bool {
572      self.capabilities.iter().any(|c| c == capability)
573    }
574
575    async fn compare_contents(
576      &self,
577      _request: CompareContentsRequest,
578    ) -> anyhow::Result<CompareContentsResponse> {
579      unimplemented!()
580    }
581
582    async fn configure_interaction(
583      &self,
584      _request: ConfigureInteractionRequest,
585    ) -> anyhow::Result<ConfigureInteractionResponse> {
586      unimplemented!()
587    }
588
589    async fn generate_content(
590      &self,
591      _request: GenerateContentRequest,
592    ) -> anyhow::Result<GenerateContentResponse> {
593      unimplemented!()
594    }
595
596    async fn start_mock_server(
597      &self,
598      _request: StartMockServerRequest,
599    ) -> anyhow::Result<StartMockServerResponse> {
600      unimplemented!()
601    }
602
603    async fn shutdown_mock_server(
604      &self,
605      _request: ShutdownMockServerRequest,
606    ) -> anyhow::Result<ShutdownMockServerResponse> {
607      unimplemented!()
608    }
609
610    async fn get_mock_server_results(
611      &self,
612      _request: MockServerRequest,
613    ) -> anyhow::Result<MockServerResults> {
614      unimplemented!()
615    }
616
617    async fn prepare_interaction_for_verification(
618      &self,
619      request: VerificationPreparationRequest,
620    ) -> anyhow::Result<VerificationPreparationResponse> {
621      let mut w = self.prepare_request.write().unwrap();
622      *w = request;
623      let data = InteractionData {
624        body: None,
625        metadata: Default::default(),
626      };
627      Ok(VerificationPreparationResponse {
628        response: Some(Response::InteractionData(data)),
629      })
630    }
631
632    async fn prepare_interaction_for_verification_v2(
633      &self,
634      request: proto_v2::VerificationPreparationRequest,
635    ) -> anyhow::Result<VerificationPreparationResponse> {
636      let mut w = self.prepare_request_v2.write().unwrap();
637      *w = Some(request);
638      let data = InteractionData {
639        body: None,
640        metadata: Default::default(),
641      };
642      Ok(VerificationPreparationResponse {
643        response: Some(Response::InteractionData(data)),
644      })
645    }
646
647    async fn verify_interaction(
648      &self,
649      request: VerifyInteractionRequest,
650    ) -> anyhow::Result<VerifyInteractionResponse> {
651      let mut w = self.verify_request.write().unwrap();
652      *w = request;
653      let result = VerificationResult {
654        success: false,
655        response_data: None,
656        mismatches: vec![],
657        output: vec![],
658      };
659      Ok(VerifyInteractionResponse {
660        response: Some(verify_interaction_response::Response::Result(result)),
661      })
662    }
663
664    async fn verify_interaction_v2(
665      &self,
666      request: proto_v2::VerifyInteractionRequest,
667    ) -> anyhow::Result<VerifyInteractionResponse> {
668      let mut w = self.verify_request_v2.write().unwrap();
669      *w = Some(request);
670      let result = VerificationResult {
671        success: false,
672        response_data: None,
673        mismatches: vec![],
674        output: vec![],
675      };
676      Ok(VerifyInteractionResponse {
677        response: Some(verify_interaction_response::Response::Result(result)),
678      })
679    }
680
681    async fn update_catalogue(&self, _request: Catalogue) -> anyhow::Result<()> {
682      unimplemented!()
683    }
684  }
685
686  pub(crate) struct FailingInitPlugin {
687    pub error: String,
688  }
689
690  #[async_trait]
691  impl crate::plugin_models::PactPluginRpc for FailingInitPlugin {
692    async fn init_plugin(
693      &mut self,
694      _request: PluginInitRequest,
695    ) -> anyhow::Result<PluginInitResponse> {
696      Err(anyhow::anyhow!("{}", self.error))
697    }
698  }
699
700  pub(crate) struct InitRecordingPlugin {
701    pub request: RwLock<Option<PluginInitRequest>>,
702  }
703
704  impl Default for InitRecordingPlugin {
705    fn default() -> Self {
706      Self {
707        request: RwLock::new(None),
708      }
709    }
710  }
711
712  #[async_trait]
713  impl crate::plugin_models::PactPluginRpc for InitRecordingPlugin {
714    async fn init_plugin(
715      &mut self,
716      request: PluginInitRequest,
717    ) -> anyhow::Result<PluginInitResponse> {
718      *self.request.write().unwrap() = Some(request);
719      Ok(PluginInitResponse {
720        catalogue: vec![],
721        plugin_capabilities: vec!["interaction/request-response".to_string()],
722      })
723    }
724  }
725
726  /// One capability per interaction type a Pact file can record, named after the interaction and
727  /// not the transport it happens to be carried over. These are the same names the host registers
728  /// as INTERACTION catalogue entries, so the two halves of the negotiation agree.
729  #[test]
730  fn interaction_type_capability_covers_every_interaction_type() {
731    let capabilities = ALL_INTERACTION_TYPES
732      .iter()
733      .map(|interaction_type| {
734        (
735          interaction_type.to_string(),
736          interaction_type_capability(interaction_type),
737        )
738      })
739      .collect::<Vec<_>>();
740
741    expect!(capabilities).to(be_equal_to(vec![
742      ("Synchronous/HTTP".to_string(), "interaction/request-response"),
743      ("Asynchronous/Messages".to_string(), "interaction/message"),
744      (
745        "Synchronous/Messages".to_string(),
746        "interaction/synchronous-message",
747      ),
748    ]));
749  }
750
751  #[test]
752  fn check_interaction_type_capability_allows_a_plugin_that_declared_none() {
753    let plugin = MockPlugin::default();
754    expect!(check_interaction_type_capability(
755      &plugin,
756      V4InteractionType::Synchronous_HTTP
757    ))
758    .to(be_ok());
759  }
760
761  #[test]
762  fn check_interaction_type_capability_rejects_an_undeclared_type() {
763    let plugin = MockPlugin {
764      capabilities: vec!["interaction/message".to_string()],
765      ..MockPlugin::default()
766    };
767
768    expect!(check_interaction_type_capability(
769      &plugin,
770      V4InteractionType::Asynchronous_Messages
771    ))
772    .to(be_ok());
773    expect!(
774      check_interaction_type_capability(&plugin, V4InteractionType::Synchronous_HTTP).is_err()
775    )
776    .to(be_true());
777  }
778}