1use std::collections::HashMap;
12use std::sync::{Arc, Mutex};
13
14use anyhow::anyhow;
15use bytes::Bytes;
16use lazy_static::lazy_static;
17use pact_models::matchingrules::MatchingRule;
18use pact_models::path_exp::DocPath;
19use pact_models::prelude::Generator;
20use serde_json::Value;
21use tokio::runtime::Runtime;
22use tracing::{debug, error};
23
24use crate::catalogue_manager::{CatalogueEntry, CatalogueEntryProviderType, CatalogueEntryType, resolve_capability_entry};
25use crate::content::ContentMismatch;
26use crate::core_capabilities;
27use crate::plugin_manager::lookup_plugin;
28use crate::plugin_models::{PactPluginManifest, PluginInteractionConfig};
29use crate::proto_v2::{
30 FieldValue as ProtoFieldValue,
31 GenerateFieldRequest,
32 GenerateFieldResponse,
33 MatchFieldRequest,
34 MatchFieldResponse,
35 MatchingRule as ProtoMatchingRule,
36 Generator as ProtoGenerator,
37 PluginConfiguration as ProtoPluginConfiguration,
38 field_value
39};
40use crate::utils::{proto_value_to_json, to_proto_struct, to_proto_value};
41
42#[derive(Clone, Debug, PartialEq)]
50pub enum FieldValue {
51 Json(Value),
53 Binary(Bytes)
55}
56
57impl FieldValue {
58 pub fn to_proto(&self) -> ProtoFieldValue {
64 ProtoFieldValue {
65 value: Some(match self {
66 FieldValue::Binary(bytes) => field_value::Value::BinaryValue(bytes.to_vec()),
67 FieldValue::Json(Value::Null) => field_value::Value::NullValue(0),
68 FieldValue::Json(Value::Bool(value)) => field_value::Value::BooleanValue(*value),
69 FieldValue::Json(Value::String(value)) => field_value::Value::StringValue(value.clone()),
70 FieldValue::Json(Value::Number(number)) => match number.as_i64() {
71 Some(value) => field_value::Value::IntegerValue(value),
72 None => field_value::Value::DecimalValue(number.as_f64().unwrap_or_default())
74 },
75 FieldValue::Json(value) => field_value::Value::StructuredValue(to_proto_value(value))
76 })
77 }
78 }
79
80 pub fn from_proto(value: &ProtoFieldValue) -> FieldValue {
83 match &value.value {
84 Some(field_value::Value::NullValue(_)) | None => FieldValue::Json(Value::Null),
85 Some(field_value::Value::BooleanValue(value)) => FieldValue::Json(Value::Bool(*value)),
86 Some(field_value::Value::StringValue(value)) => FieldValue::Json(Value::String(value.clone())),
87 Some(field_value::Value::IntegerValue(value)) => FieldValue::Json(Value::Number((*value).into())),
88 Some(field_value::Value::DecimalValue(value)) => FieldValue::Json(
89 serde_json::Number::from_f64(*value)
90 .map(Value::Number)
91 .unwrap_or(Value::Null)
93 ),
94 Some(field_value::Value::BinaryValue(bytes)) => FieldValue::Binary(Bytes::from(bytes.clone())),
95 Some(field_value::Value::StructuredValue(value)) => FieldValue::Json(proto_value_to_json(value))
96 }
97 }
98}
99
100impl From<Value> for FieldValue {
101 fn from(value: Value) -> Self {
102 FieldValue::Json(value)
103 }
104}
105
106impl From<Bytes> for FieldValue {
107 fn from(bytes: Bytes) -> Self {
108 FieldValue::Binary(bytes)
109 }
110}
111
112#[derive(Clone, Debug)]
114pub struct FieldContext {
115 pub path: DocPath,
117 pub category: String,
120 pub plugin_config: Option<PluginInteractionConfig>,
122 pub test_context: HashMap<String, Value>
124}
125
126impl Default for FieldContext {
127 fn default() -> Self {
128 FieldContext {
129 path: DocPath::root(),
130 category: "body".to_string(),
131 plugin_config: None,
132 test_context: HashMap::default()
133 }
134 }
135}
136
137impl FieldContext {
138 pub fn new(path: &DocPath, category: &str) -> FieldContext {
140 FieldContext {
141 path: path.clone(),
142 category: category.to_string(),
143 .. FieldContext::default()
144 }
145 }
146
147 pub fn with_plugin_config(self, plugin_config: Option<PluginInteractionConfig>) -> FieldContext {
149 FieldContext { plugin_config, .. self }
150 }
151
152 pub fn with_test_context(self, test_context: HashMap<String, Value>) -> FieldContext {
154 FieldContext { test_context, .. self }
155 }
156}
157
158#[derive(Clone, Debug)]
161pub struct FieldMatcher {
162 pub catalogue_entry: CatalogueEntry
164}
165
166#[derive(Clone, Debug)]
168pub struct FieldGenerator {
169 pub catalogue_entry: CatalogueEntry
171}
172
173pub fn find_field_matcher(name: &str) -> anyhow::Result<FieldMatcher> {
179 resolve_capability_entry(name, CatalogueEntryType::MATCHER)
180 .map(|catalogue_entry| FieldMatcher { catalogue_entry })
181}
182
183pub fn find_field_generator(name: &str) -> anyhow::Result<FieldGenerator> {
185 resolve_capability_entry(name, CatalogueEntryType::GENERATOR)
186 .map(|catalogue_entry| FieldGenerator { catalogue_entry })
187}
188
189impl FieldMatcher {
190 pub fn is_core(&self) -> bool {
192 self.catalogue_entry.provider_type == CatalogueEntryProviderType::CORE
193 }
194
195 pub fn catalogue_entry_key(&self) -> String {
197 if self.is_core() {
198 format!("core/matcher/{}", self.catalogue_entry.key)
199 } else {
200 format!("plugin/{}/matcher/{}", self.plugin_name(), self.catalogue_entry.key)
201 }
202 }
203
204 pub fn plugin(&self) -> Option<PactPluginManifest> {
206 self.catalogue_entry.plugin.clone()
207 }
208
209 pub fn plugin_name(&self) -> String {
211 self.catalogue_entry.plugin.as_ref()
212 .map(|plugin| plugin.name.clone())
213 .unwrap_or("core".to_string())
214 }
215
216 pub async fn match_field(
222 &self,
223 rule: &MatchingRule,
224 expected: &FieldValue,
225 actual: &FieldValue,
226 context: &FieldContext
227 ) -> Result<(), Vec<ContentMismatch>> {
228 let request = MatchFieldRequest {
229 key: self.catalogue_entry.key.clone(),
230 rule: Some(to_proto_matching_rule(rule)),
231 path: context.path.to_string(),
232 mismatch_type: context.category.clone(),
233 expected: Some(expected.to_proto()),
234 actual: Some(actual.to_proto()),
235 plugin_configuration: context.plugin_config.clone().map(to_proto_plugin_config),
236 test_context: Some(to_proto_struct(&with_test_run_id(&context.test_context)))
237 };
238
239 let response = if self.is_core() {
240 match core_capabilities::lookup_core_field_matcher(&self.catalogue_entry.key) {
241 Some(handler) => handler.match_field(request).await,
242 None => Err(anyhow!("No core field matcher registered for '{}'", self.catalogue_entry.key))
243 }
244 } else {
245 self.call_plugin(request).await
246 };
247
248 process_match_field_response(response, context)
249 }
250
251 async fn call_plugin(&self, request: MatchFieldRequest) -> anyhow::Result<MatchFieldResponse> {
252 let manifest = self.catalogue_entry.plugin.as_ref()
253 .ok_or_else(|| anyhow!("Catalogue entry '{}' has no plugin manifest", self.catalogue_entry_key()))?;
254 let plugin = lookup_plugin(&manifest.as_dependency())
255 .ok_or_else(|| anyhow!("Plugin '{}' for matching rule '{}' is not currently running",
256 manifest.name, self.catalogue_entry.key))?;
257 debug!("Sending MatchField request to plugin {:?}", manifest.name);
258 let chain_id = crate::call_chain::new_call_chain_id();
259 let deadline_ms = crate::call_chain::default_deadline_ms();
260 plugin.match_field_with_chain(request, &chain_id, deadline_ms).await
261 }
262
263 pub fn match_field_blocking(
270 &self,
271 rule: &MatchingRule,
272 expected: &FieldValue,
273 actual: &FieldValue,
274 context: &FieldContext
275 ) -> Result<(), Vec<ContentMismatch>> {
276 let matcher = self.clone();
277 let rule = rule.clone();
278 let expected = expected.clone();
279 let actual = actual.clone();
280 let call_context = context.clone();
281
282 block_on_field_call(async move {
283 matcher.match_field(&rule, &expected, &actual, &call_context).await
284 })
285 .unwrap_or_else(|err| Err(vec![mismatch_for(err.to_string(), context)]))
286 }
287}
288
289impl FieldGenerator {
290 pub fn is_core(&self) -> bool {
292 self.catalogue_entry.provider_type == CatalogueEntryProviderType::CORE
293 }
294
295 pub fn catalogue_entry_key(&self) -> String {
297 if self.is_core() {
298 format!("core/generator/{}", self.catalogue_entry.key)
299 } else {
300 format!("plugin/{}/generator/{}", self.plugin_name(), self.catalogue_entry.key)
301 }
302 }
303
304 pub fn plugin(&self) -> Option<PactPluginManifest> {
306 self.catalogue_entry.plugin.clone()
307 }
308
309 pub fn plugin_name(&self) -> String {
311 self.catalogue_entry.plugin.as_ref()
312 .map(|plugin| plugin.name.clone())
313 .unwrap_or("core".to_string())
314 }
315
316 pub async fn generate_field(
318 &self,
319 generator: &Generator,
320 example: &FieldValue,
321 mode: TestMode,
322 context: &FieldContext
323 ) -> anyhow::Result<FieldValue> {
324 let request = GenerateFieldRequest {
325 key: self.catalogue_entry.key.clone(),
326 generator: Some(to_proto_generator(generator)),
327 path: context.path.to_string(),
328 example_value: Some(example.to_proto()),
329 plugin_configuration: context.plugin_config.clone().map(to_proto_plugin_config),
330 test_context: Some(to_proto_struct(&with_test_run_id(&context.test_context))),
331 test_mode: mode.to_proto() as i32
332 };
333
334 let response = if self.is_core() {
335 let handler = core_capabilities::lookup_core_field_generator(&self.catalogue_entry.key)
336 .ok_or_else(|| anyhow!("No core field generator registered for '{}'", self.catalogue_entry.key))?;
337 handler.generate_field(request).await?
338 } else {
339 self.call_plugin(request).await?
340 };
341
342 if !response.error.is_empty() {
343 return Err(anyhow!("Generator '{}' failed: {}", self.catalogue_entry.key, response.error));
344 }
345 match &response.value {
346 Some(value) => Ok(FieldValue::from_proto(value)),
347 None => Err(anyhow!("Generator '{}' returned no value", self.catalogue_entry.key))
348 }
349 }
350
351 async fn call_plugin(&self, request: GenerateFieldRequest) -> anyhow::Result<GenerateFieldResponse> {
352 let manifest = self.catalogue_entry.plugin.as_ref()
353 .ok_or_else(|| anyhow!("Catalogue entry '{}' has no plugin manifest", self.catalogue_entry_key()))?;
354 let plugin = lookup_plugin(&manifest.as_dependency())
355 .ok_or_else(|| anyhow!("Plugin '{}' for generator '{}' is not currently running",
356 manifest.name, self.catalogue_entry.key))?;
357 debug!("Sending GenerateField request to plugin {:?}", manifest.name);
358 let chain_id = crate::call_chain::new_call_chain_id();
359 let deadline_ms = crate::call_chain::default_deadline_ms();
360 plugin.generate_field_with_chain(request, &chain_id, deadline_ms).await
361 }
362
363 pub fn generate_field_blocking(
366 &self,
367 generator: &Generator,
368 example: &FieldValue,
369 mode: TestMode,
370 context: &FieldContext
371 ) -> anyhow::Result<FieldValue> {
372 let field_generator = self.clone();
373 let generator = generator.clone();
374 let example = example.clone();
375 let context = context.clone();
376
377 block_on_field_call(async move {
378 field_generator.generate_field(&generator, &example, mode, &context).await
379 })?
380 }
381}
382
383#[derive(Clone, Copy, Debug, PartialEq, Eq)]
386pub enum TestMode {
387 Consumer,
389 Provider,
391 Unknown
393}
394
395impl TestMode {
396 fn to_proto(self) -> crate::proto_v2::generate_content_request::TestMode {
397 use crate::proto_v2::generate_content_request::TestMode as ProtoTestMode;
398 match self {
399 TestMode::Consumer => ProtoTestMode::Consumer,
400 TestMode::Provider => ProtoTestMode::Provider,
401 TestMode::Unknown => ProtoTestMode::Unknown
402 }
403 }
404}
405
406lazy_static! {
407 static ref FIELD_RUNTIME: Mutex<Option<Arc<Runtime>>> = Mutex::new(None);
411}
412
413fn field_runtime() -> anyhow::Result<Arc<Runtime>> {
414 let mut guard = FIELD_RUNTIME.lock()
415 .map_err(|err| anyhow!("FIELD_RUNTIME mutex poisoned - {}", err))?;
416 match guard.as_ref() {
417 Some(runtime) => Ok(runtime.clone()),
418 None => {
419 let runtime = Arc::new(tokio::runtime::Builder::new_multi_thread()
420 .worker_threads(1)
421 .enable_all()
422 .thread_name("pact-plugin-field")
423 .build()?);
424 *guard = Some(runtime.clone());
425 Ok(runtime)
426 }
427 }
428}
429
430fn block_on_field_call<F, T>(future: F) -> anyhow::Result<T>
443where
444 F: std::future::Future<Output = T> + Send + 'static,
445 T: Send + 'static
446{
447 let runtime = field_runtime()?;
448 let deadline_ms = crate::call_chain::default_deadline_ms();
449 let (sender, receiver) = std::sync::mpsc::channel();
450 runtime.spawn(async move {
451 let _ = sender.send(future.await);
453 });
454 receiver.recv_timeout(crate::call_chain::remaining(deadline_ms))
455 .map_err(|err| {
456 error!("Timed out waiting for a field-level plugin call to complete - {}", err);
457 anyhow!("Timed out waiting for the plugin call to complete - {}", err)
458 })
459}
460
461fn process_match_field_response(
462 response: anyhow::Result<MatchFieldResponse>,
463 context: &FieldContext
464) -> Result<(), Vec<ContentMismatch>> {
465 let path = context.path.to_string();
466 match response {
467 Ok(response) => if !response.error.is_empty() {
468 Err(vec![mismatch_for(response.error, context)])
469 } else if response.mismatches.is_empty() {
470 Ok(())
471 } else {
472 Err(response.mismatches.iter().map(|mismatch| ContentMismatch {
473 expected: mismatch.expected.as_ref()
474 .map(|bytes| String::from_utf8_lossy(bytes).to_string())
475 .unwrap_or_default(),
476 actual: mismatch.actual.as_ref()
477 .map(|bytes| String::from_utf8_lossy(bytes).to_string())
478 .unwrap_or_default(),
479 mismatch: mismatch.mismatch.clone(),
480 path: if mismatch.path.is_empty() { path.clone() } else { mismatch.path.clone() },
482 diff: if mismatch.diff.is_empty() { None } else { Some(mismatch.diff.clone()) },
483 mismatch_type: if mismatch.mismatch_type.is_empty() {
484 Some(context.category.clone())
485 } else {
486 Some(mismatch.mismatch_type.clone())
487 }
488 }).collect())
489 },
490 Err(err) => {
491 error!("Field-level match call failed - {}", err);
492 Err(vec![mismatch_for(err.to_string(), context)])
493 }
494 }
495}
496
497fn mismatch_for(message: String, context: &FieldContext) -> ContentMismatch {
498 ContentMismatch {
499 expected: Default::default(),
500 actual: Default::default(),
501 mismatch: message,
502 path: context.path.to_string(),
503 diff: None,
504 mismatch_type: Some(context.category.clone())
505 }
506}
507
508fn with_test_run_id(test_context: &HashMap<String, Value>) -> HashMap<String, Value> {
519 let mut context = test_context.clone();
520 if let Some(id) = crate::test_context::current_test_run_id() {
521 context.entry("testRunId".to_string()).or_insert_with(|| Value::String(id));
522 }
523 context
524}
525
526fn to_proto_matching_rule(rule: &MatchingRule) -> ProtoMatchingRule {
527 ProtoMatchingRule {
528 r#type: rule.name(),
529 values: Some(to_proto_struct(&rule.value_map()))
530 }
531}
532
533fn to_proto_generator(generator: &Generator) -> ProtoGenerator {
534 ProtoGenerator {
535 r#type: generator.name(),
536 values: Some(to_proto_struct(&generator.value_map()))
537 }
538}
539
540fn to_proto_plugin_config(config: PluginInteractionConfig) -> ProtoPluginConfiguration {
541 ProtoPluginConfiguration {
542 interaction_configuration: Some(to_proto_struct(&config.interaction_configuration)),
543 pact_configuration: Some(to_proto_struct(&config.pact_configuration))
544 }
545}
546
547#[cfg(test)]
548mod tests {
549 use async_trait::async_trait;
550 use expectest::prelude::*;
551 use maplit::hashmap;
552 use pact_models::matchingrules::MatchingRule;
553
554 use crate::catalogue_manager::{CatalogueEntryProviderType, register_core_entries};
555 use crate::core_capabilities::{
556 CoreFieldGenerator,
557 CoreFieldMatcher,
558 deregister_core_field_generator,
559 deregister_core_field_matcher,
560 register_core_field_generator,
561 register_core_field_matcher
562 };
563 use crate::proto_v2::ContentMismatch as ProtoContentMismatch;
564
565 use super::*;
566
567 #[test]
568 fn field_values_round_trip_through_the_proto_form() {
569 for value in [
570 FieldValue::Json(Value::String("4111111111111111".to_string())),
571 FieldValue::Json(serde_json::json!(100)),
572 FieldValue::Json(serde_json::json!(-100.5)),
573 FieldValue::Json(Value::Bool(true)),
574 FieldValue::Json(Value::Null),
575 FieldValue::Json(serde_json::json!({ "brand": "visa" })),
576 FieldValue::Binary(Bytes::from(vec![0u8, 159, 146, 150]))
578 ] {
579 expect!(FieldValue::from_proto(&value.to_proto())).to(be_equal_to(value));
580 }
581 }
582
583 #[test]
584 fn a_whole_number_stays_whole_and_a_decimal_stays_decimal() {
585 let integer = FieldValue::from_proto(&FieldValue::Json(serde_json::json!(100)).to_proto());
588 let decimal = FieldValue::from_proto(&FieldValue::Json(serde_json::json!(100.5)).to_proto());
589 let whole_decimal = FieldValue::from_proto(&FieldValue::Json(serde_json::json!(100.0)).to_proto());
590
591 expect!(integer.clone()).to(be_equal_to(FieldValue::Json(serde_json::json!(100))));
592 expect!(decimal).to(be_equal_to(FieldValue::Json(serde_json::json!(100.5))));
593 match integer {
594 FieldValue::Json(Value::Number(number)) => expect!(number.is_i64()).to(be_true()),
595 other => panic!("expected a JSON number, got {:?}", other)
596 };
597 expect!(whole_decimal.clone()).to(be_equal_to(FieldValue::Json(serde_json::json!(100.0))));
600 match whole_decimal {
601 FieldValue::Json(Value::Number(number)) => expect!(number.is_f64()).to(be_true()),
602 other => panic!("expected a JSON number, got {:?}", other)
603 };
604 }
605
606 #[test]
607 fn each_scalar_type_crosses_the_boundary_under_its_own_arm() {
608 let cases = [
609 (FieldValue::Json(Value::Null), "null"),
610 (FieldValue::Json(Value::Bool(true)), "boolean"),
611 (FieldValue::Json(serde_json::json!("4111111111111111")), "string"),
612 (FieldValue::Json(serde_json::json!(100)), "integer"),
613 (FieldValue::Json(serde_json::json!(100.5)), "decimal"),
614 (FieldValue::Binary(Bytes::from(vec![0u8, 159, 146, 150])), "binary"),
615 (FieldValue::Json(serde_json::json!({ "brand": "visa" })), "structured")
616 ];
617 for (value, expected_arm) in cases {
618 let arm = match value.to_proto().value {
619 Some(field_value::Value::NullValue(_)) => "null",
620 Some(field_value::Value::BooleanValue(_)) => "boolean",
621 Some(field_value::Value::StringValue(_)) => "string",
622 Some(field_value::Value::IntegerValue(_)) => "integer",
623 Some(field_value::Value::DecimalValue(_)) => "decimal",
624 Some(field_value::Value::BinaryValue(_)) => "binary",
625 Some(field_value::Value::StructuredValue(_)) => "structured",
626 None => "unset"
627 };
628 expect!(arm).to(be_equal_to(expected_arm));
629 }
630 }
631
632 #[test]
633 fn an_unset_proto_value_reads_as_json_null() {
634 expect!(FieldValue::from_proto(&ProtoFieldValue { value: None }))
635 .to(be_equal_to(FieldValue::Json(Value::Null)));
636 }
637
638 #[test]
639 fn a_plugin_rules_configuration_crosses_the_boundary() {
640 let rule = MatchingRule::Plugin {
644 name: "creditcard".to_string(),
645 values: serde_json::json!({ "brand": "visa" })
646 };
647
648 let proto = to_proto_matching_rule(&rule);
649 expect!(proto.r#type.as_str()).to(be_equal_to("creditcard"));
650 expect!(proto.values.unwrap().fields.get("brand").cloned()).to(
651 be_some().value(crate::utils::to_proto_value(&Value::String("visa".to_string()))));
652 }
653
654 #[test]
655 fn a_plugin_generators_configuration_crosses_the_boundary() {
656 let generator = Generator::Plugin {
657 name: "creditcard".to_string(),
658 values: serde_json::json!({ "brand": "visa" })
659 };
660
661 let proto = to_proto_generator(&generator);
662 expect!(proto.r#type.as_str()).to(be_equal_to("creditcard"));
663 expect!(proto.values.unwrap().fields.get("brand").cloned()).to(
664 be_some().value(crate::utils::to_proto_value(&Value::String("visa".to_string()))));
665 }
666
667 #[tokio::test]
671 async fn a_field_request_carries_the_current_test_run_id() {
672 #[derive(Debug)]
673 struct CapturingMatcher {
674 test_run_ids: Arc<Mutex<Vec<Option<String>>>>
675 }
676
677 #[async_trait]
678 impl CoreFieldMatcher for CapturingMatcher {
679 async fn match_field(&self, request: MatchFieldRequest) -> anyhow::Result<MatchFieldResponse> {
680 let id = request.test_context.as_ref()
681 .and_then(|context| context.fields.get("testRunId"))
682 .and_then(|value| match &value.kind {
683 Some(prost_types::value::Kind::StringValue(value)) => Some(value.clone()),
684 _ => None
685 });
686 self.test_run_ids.lock().unwrap().push(id);
687 Ok(MatchFieldResponse::default())
688 }
689 }
690
691 let key = "a_field_request_carries_the_current_test_run_id";
692 let test_run_ids = Arc::new(Mutex::new(vec![]));
693 register_core_field_matcher(key, Arc::new(CapturingMatcher { test_run_ids: test_run_ids.clone() }));
694 register_core_entries(&vec![CatalogueEntry {
695 entry_type: CatalogueEntryType::MATCHER,
696 provider_type: CatalogueEntryProviderType::CORE,
697 plugin: None,
698 key: key.to_string(),
699 values: hashmap!{}
700 }]);
701 let matcher = find_field_matcher(key).unwrap();
702 let context = FieldContext::new(&DocPath::new_unwrap("$.one"), "body");
703
704 crate::test_context::set_test_run_id(Some("test-run-1".to_string()));
705 let _ = matcher.match_field(&MatchingRule::Type, &FieldValue::Json(Value::Null),
706 &FieldValue::Json(Value::Null), &context).await;
707 crate::test_context::set_test_run_id(None);
708 let _ = matcher.match_field(&MatchingRule::Type, &FieldValue::Json(Value::Null),
709 &FieldValue::Json(Value::Null), &context).await;
710
711 deregister_core_field_matcher(key);
712
713 let ids = test_run_ids.lock().unwrap().clone();
714 expect!(ids).to(be_equal_to(vec![Some("test-run-1".to_string()), None]));
715 }
716
717 #[derive(Debug)]
719 struct TestCoreMatcher {
720 mismatches: Vec<ProtoContentMismatch>,
721 error: String
722 }
723
724 #[async_trait]
725 impl CoreFieldMatcher for TestCoreMatcher {
726 async fn match_field(&self, request: MatchFieldRequest) -> anyhow::Result<MatchFieldResponse> {
727 assert_eq!(request.path, "$.card.number");
729 assert_eq!(request.mismatch_type, "body");
730 assert_eq!(request.rule.as_ref().unwrap().r#type, "regex");
731 Ok(MatchFieldResponse {
732 error: self.error.clone(),
733 mismatches: self.mismatches.clone()
734 })
735 }
736 }
737
738 #[derive(Debug)]
739 struct TestCoreGenerator;
740
741 #[async_trait]
742 impl CoreFieldGenerator for TestCoreGenerator {
743 async fn generate_field(&self, request: GenerateFieldRequest) -> anyhow::Result<GenerateFieldResponse> {
744 assert_eq!(request.path, "$.card.number");
745 assert_eq!(request.test_mode, TestMode::Consumer.to_proto() as i32);
746 Ok(GenerateFieldResponse {
747 error: String::default(),
748 value: Some(FieldValue::Json(Value::String("4012888888881881".to_string())).to_proto())
749 })
750 }
751 }
752
753 fn register_core_matcher_entry(key: &str, entry_type: CatalogueEntryType) {
754 register_core_entries(&vec![CatalogueEntry {
755 entry_type,
756 provider_type: CatalogueEntryProviderType::CORE,
757 plugin: None,
758 key: key.to_string(),
759 values: hashmap!{}
760 }]);
761 }
762
763 fn a_rule() -> MatchingRule {
767 MatchingRule::Regex("\\d{16}".to_string())
768 }
769
770 fn field_context() -> FieldContext {
771 FieldContext::new(&DocPath::new("$.card.number").unwrap(), "body")
772 }
773
774 #[test_log::test(tokio::test)]
775 async fn match_field_dispatches_to_a_registered_core_handler() {
776 let key = "match_field_dispatches_to_a_registered_core_handler";
777 register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
778 register_core_field_matcher(key, Arc::new(TestCoreMatcher {
779 mismatches: vec![],
780 error: String::default()
781 }));
782
783 let matcher = find_field_matcher(key).unwrap();
784 let result = matcher.match_field(
785 &a_rule(),
786 &FieldValue::Json(Value::String("4111111111111111".to_string())),
787 &FieldValue::Json(Value::String("4012888888881881".to_string())),
788 &field_context()
789 ).await;
790
791 deregister_core_field_matcher(key);
792
793 expect!(matcher.is_core()).to(be_true());
794 expect!(result).to(be_ok());
795 }
796
797 #[test_log::test(tokio::test)]
798 async fn match_field_reports_mismatches_against_the_requested_path() {
799 let key = "match_field_reports_mismatches_against_the_requested_path";
800 register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
801 register_core_field_matcher(key, Arc::new(TestCoreMatcher {
802 mismatches: vec![ProtoContentMismatch {
803 mismatch: "fails the Luhn check".to_string(),
805 expected: Some("4111111111111111".as_bytes().to_vec()),
806 actual: Some("4111111111111112".as_bytes().to_vec()),
807 .. ProtoContentMismatch::default()
808 }],
809 error: String::default()
810 }));
811
812 let matcher = find_field_matcher(key).unwrap();
813 let result = matcher.match_field(
814 &a_rule(),
815 &FieldValue::Json(Value::String("4111111111111111".to_string())),
816 &FieldValue::Json(Value::String("4111111111111112".to_string())),
817 &field_context()
818 ).await;
819
820 deregister_core_field_matcher(key);
821
822 let mismatches = result.expect_err("expected a mismatch");
823 expect!(mismatches.len()).to(be_equal_to(1));
824 expect!(mismatches[0].mismatch.clone()).to(be_equal_to("fails the Luhn check".to_string()));
825 expect!(mismatches[0].path.clone()).to(be_equal_to("$.card.number".to_string()));
826 expect!(mismatches[0].mismatch_type.clone()).to(be_some().value("body".to_string()));
827 expect!(mismatches[0].expected.clone()).to(be_equal_to("4111111111111111".to_string()));
828 }
829
830 #[test_log::test(tokio::test)]
831 async fn match_field_turns_a_handler_error_into_a_mismatch() {
832 let key = "match_field_turns_a_handler_error_into_a_mismatch";
833 register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
834 register_core_field_matcher(key, Arc::new(TestCoreMatcher {
835 mismatches: vec![],
836 error: "'amx' is not a brand this plugin knows about".to_string()
837 }));
838
839 let matcher = find_field_matcher(key).unwrap();
840 let result = matcher.match_field(
841 &a_rule(),
842 &FieldValue::Json(Value::String("4111111111111111".to_string())),
843 &FieldValue::Json(Value::String("4111111111111111".to_string())),
844 &field_context()
845 ).await;
846
847 deregister_core_field_matcher(key);
848
849 let mismatches = result.expect_err("expected the error to surface");
850 expect!(mismatches[0].mismatch.clone())
851 .to(be_equal_to("'amx' is not a brand this plugin knows about".to_string()));
852 }
853
854 #[test_log::test(tokio::test)]
855 async fn match_field_fails_clearly_when_no_core_handler_is_registered() {
856 let key = "match_field_fails_clearly_when_no_core_handler_is_registered";
857 register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
858
859 let matcher = find_field_matcher(key).unwrap();
860 let result = matcher.match_field(
861 &a_rule(),
862 &FieldValue::Json(Value::Null),
863 &FieldValue::Json(Value::Null),
864 &field_context()
865 ).await;
866
867 let mismatches = result.expect_err("expected an error for a registered entry with no handler");
868 expect!(mismatches[0].mismatch.contains("No core field matcher registered")).to(be_true());
869 }
870
871 #[test_log::test(tokio::test)]
872 async fn generate_field_dispatches_to_a_registered_core_handler() {
873 let key = "generate_field_dispatches_to_a_registered_core_handler";
874 register_core_matcher_entry(key, CatalogueEntryType::GENERATOR);
875 register_core_field_generator(key, Arc::new(TestCoreGenerator));
876
877 let generator = find_field_generator(key).unwrap();
878 let result = generator.generate_field(
879 &Generator::RandomString(16),
880 &FieldValue::Json(Value::String("4111111111111111".to_string())),
881 TestMode::Consumer,
882 &field_context()
883 ).await;
884
885 deregister_core_field_generator(key);
886
887 expect!(generator.is_core()).to(be_true());
888 expect!(result.unwrap()).to(be_equal_to(
889 FieldValue::Json(Value::String("4012888888881881".to_string()))
890 ));
891 }
892
893 #[test]
894 fn finding_a_rule_that_is_not_registered_says_so() {
895 let err = find_field_matcher("finding_a_rule_that_is_not_registered_says_so")
896 .expect_err("expected an error for an unregistered rule");
897 expect!(err.to_string().contains("No catalogue entry found")).to(be_true());
898 }
899
900 #[test]
901 fn finding_a_rule_that_is_a_generator_says_so() {
902 let key = "finding_a_rule_that_is_a_generator_says_so";
903 register_core_matcher_entry(key, CatalogueEntryType::GENERATOR);
904
905 let err = find_field_matcher(key).expect_err("expected an error for the wrong entry type");
906 expect!(err.to_string().contains("is a GENERATOR, not a MATCHER")).to(be_true());
907 }
908
909 #[test_log::test]
910 fn the_blocking_bridge_runs_a_call_from_a_synchronous_context() {
911 let key = "the_blocking_bridge_runs_a_call_from_a_synchronous_context";
912 register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
913 register_core_field_matcher(key, Arc::new(TestCoreMatcher {
914 mismatches: vec![],
915 error: String::default()
916 }));
917
918 let matcher = find_field_matcher(key).unwrap();
919 let result = matcher.match_field_blocking(
920 &a_rule(),
921 &FieldValue::Json(Value::String("4111111111111111".to_string())),
922 &FieldValue::Json(Value::String("4012888888881881".to_string())),
923 &field_context()
924 );
925
926 deregister_core_field_matcher(key);
927
928 expect!(result).to(be_ok());
929 }
930
931 #[test_log::test(tokio::test(flavor = "multi_thread"))]
932 async fn the_blocking_bridge_works_from_inside_a_runtime() {
933 let key = "the_blocking_bridge_works_from_inside_a_runtime";
936 register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
937 register_core_field_matcher(key, Arc::new(TestCoreMatcher {
938 mismatches: vec![],
939 error: String::default()
940 }));
941
942 let result = tokio::task::spawn_blocking(move || {
943 let matcher = find_field_matcher(key).unwrap();
944 matcher.match_field_blocking(
945 &a_rule(),
946 &FieldValue::Json(Value::String("4111111111111111".to_string())),
947 &FieldValue::Json(Value::String("4012888888881881".to_string())),
948 &field_context()
949 )
950 }).await.unwrap();
951
952 deregister_core_field_matcher(key);
953
954 expect!(result).to(be_ok());
955 }
956}