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(&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(&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 to_proto_matching_rule(rule: &MatchingRule) -> ProtoMatchingRule {
509 ProtoMatchingRule {
510 r#type: rule.name(),
511 values: Some(to_proto_struct(&rule.value_map()))
512 }
513}
514
515fn to_proto_generator(generator: &Generator) -> ProtoGenerator {
516 ProtoGenerator {
517 r#type: generator.name(),
518 values: Some(to_proto_struct(&generator.value_map()))
519 }
520}
521
522fn to_proto_plugin_config(config: PluginInteractionConfig) -> ProtoPluginConfiguration {
523 ProtoPluginConfiguration {
524 interaction_configuration: Some(to_proto_struct(&config.interaction_configuration)),
525 pact_configuration: Some(to_proto_struct(&config.pact_configuration))
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use async_trait::async_trait;
532 use expectest::prelude::*;
533 use maplit::hashmap;
534 use pact_models::matchingrules::MatchingRule;
535
536 use crate::catalogue_manager::{CatalogueEntryProviderType, register_core_entries};
537 use crate::core_capabilities::{
538 CoreFieldGenerator,
539 CoreFieldMatcher,
540 deregister_core_field_generator,
541 deregister_core_field_matcher,
542 register_core_field_generator,
543 register_core_field_matcher
544 };
545 use crate::proto_v2::ContentMismatch as ProtoContentMismatch;
546
547 use super::*;
548
549 #[test]
550 fn field_values_round_trip_through_the_proto_form() {
551 for value in [
552 FieldValue::Json(Value::String("4111111111111111".to_string())),
553 FieldValue::Json(serde_json::json!(100)),
554 FieldValue::Json(serde_json::json!(-100.5)),
555 FieldValue::Json(Value::Bool(true)),
556 FieldValue::Json(Value::Null),
557 FieldValue::Json(serde_json::json!({ "brand": "visa" })),
558 FieldValue::Binary(Bytes::from(vec![0u8, 159, 146, 150]))
560 ] {
561 expect!(FieldValue::from_proto(&value.to_proto())).to(be_equal_to(value));
562 }
563 }
564
565 #[test]
566 fn a_whole_number_stays_whole_and_a_decimal_stays_decimal() {
567 let integer = FieldValue::from_proto(&FieldValue::Json(serde_json::json!(100)).to_proto());
570 let decimal = FieldValue::from_proto(&FieldValue::Json(serde_json::json!(100.5)).to_proto());
571 let whole_decimal = FieldValue::from_proto(&FieldValue::Json(serde_json::json!(100.0)).to_proto());
572
573 expect!(integer.clone()).to(be_equal_to(FieldValue::Json(serde_json::json!(100))));
574 expect!(decimal).to(be_equal_to(FieldValue::Json(serde_json::json!(100.5))));
575 match integer {
576 FieldValue::Json(Value::Number(number)) => expect!(number.is_i64()).to(be_true()),
577 other => panic!("expected a JSON number, got {:?}", other)
578 };
579 expect!(whole_decimal.clone()).to(be_equal_to(FieldValue::Json(serde_json::json!(100.0))));
582 match whole_decimal {
583 FieldValue::Json(Value::Number(number)) => expect!(number.is_f64()).to(be_true()),
584 other => panic!("expected a JSON number, got {:?}", other)
585 };
586 }
587
588 #[test]
589 fn each_scalar_type_crosses_the_boundary_under_its_own_arm() {
590 let cases = [
591 (FieldValue::Json(Value::Null), "null"),
592 (FieldValue::Json(Value::Bool(true)), "boolean"),
593 (FieldValue::Json(serde_json::json!("4111111111111111")), "string"),
594 (FieldValue::Json(serde_json::json!(100)), "integer"),
595 (FieldValue::Json(serde_json::json!(100.5)), "decimal"),
596 (FieldValue::Binary(Bytes::from(vec![0u8, 159, 146, 150])), "binary"),
597 (FieldValue::Json(serde_json::json!({ "brand": "visa" })), "structured")
598 ];
599 for (value, expected_arm) in cases {
600 let arm = match value.to_proto().value {
601 Some(field_value::Value::NullValue(_)) => "null",
602 Some(field_value::Value::BooleanValue(_)) => "boolean",
603 Some(field_value::Value::StringValue(_)) => "string",
604 Some(field_value::Value::IntegerValue(_)) => "integer",
605 Some(field_value::Value::DecimalValue(_)) => "decimal",
606 Some(field_value::Value::BinaryValue(_)) => "binary",
607 Some(field_value::Value::StructuredValue(_)) => "structured",
608 None => "unset"
609 };
610 expect!(arm).to(be_equal_to(expected_arm));
611 }
612 }
613
614 #[test]
615 fn an_unset_proto_value_reads_as_json_null() {
616 expect!(FieldValue::from_proto(&ProtoFieldValue { value: None }))
617 .to(be_equal_to(FieldValue::Json(Value::Null)));
618 }
619
620 #[test]
621 fn a_plugin_rules_configuration_crosses_the_boundary() {
622 let rule = MatchingRule::Plugin {
626 name: "creditcard".to_string(),
627 values: serde_json::json!({ "brand": "visa" })
628 };
629
630 let proto = to_proto_matching_rule(&rule);
631 expect!(proto.r#type.as_str()).to(be_equal_to("creditcard"));
632 expect!(proto.values.unwrap().fields.get("brand").cloned()).to(
633 be_some().value(crate::utils::to_proto_value(&Value::String("visa".to_string()))));
634 }
635
636 #[test]
637 fn a_plugin_generators_configuration_crosses_the_boundary() {
638 let generator = Generator::Plugin {
639 name: "creditcard".to_string(),
640 values: serde_json::json!({ "brand": "visa" })
641 };
642
643 let proto = to_proto_generator(&generator);
644 expect!(proto.r#type.as_str()).to(be_equal_to("creditcard"));
645 expect!(proto.values.unwrap().fields.get("brand").cloned()).to(
646 be_some().value(crate::utils::to_proto_value(&Value::String("visa".to_string()))));
647 }
648
649 #[derive(Debug)]
651 struct TestCoreMatcher {
652 mismatches: Vec<ProtoContentMismatch>,
653 error: String
654 }
655
656 #[async_trait]
657 impl CoreFieldMatcher for TestCoreMatcher {
658 async fn match_field(&self, request: MatchFieldRequest) -> anyhow::Result<MatchFieldResponse> {
659 assert_eq!(request.path, "$.card.number");
661 assert_eq!(request.mismatch_type, "body");
662 assert_eq!(request.rule.as_ref().unwrap().r#type, "regex");
663 Ok(MatchFieldResponse {
664 error: self.error.clone(),
665 mismatches: self.mismatches.clone()
666 })
667 }
668 }
669
670 #[derive(Debug)]
671 struct TestCoreGenerator;
672
673 #[async_trait]
674 impl CoreFieldGenerator for TestCoreGenerator {
675 async fn generate_field(&self, request: GenerateFieldRequest) -> anyhow::Result<GenerateFieldResponse> {
676 assert_eq!(request.path, "$.card.number");
677 assert_eq!(request.test_mode, TestMode::Consumer.to_proto() as i32);
678 Ok(GenerateFieldResponse {
679 error: String::default(),
680 value: Some(FieldValue::Json(Value::String("4012888888881881".to_string())).to_proto())
681 })
682 }
683 }
684
685 fn register_core_matcher_entry(key: &str, entry_type: CatalogueEntryType) {
686 register_core_entries(&vec![CatalogueEntry {
687 entry_type,
688 provider_type: CatalogueEntryProviderType::CORE,
689 plugin: None,
690 key: key.to_string(),
691 values: hashmap!{}
692 }]);
693 }
694
695 fn a_rule() -> MatchingRule {
699 MatchingRule::Regex("\\d{16}".to_string())
700 }
701
702 fn field_context() -> FieldContext {
703 FieldContext::new(&DocPath::new("$.card.number").unwrap(), "body")
704 }
705
706 #[test_log::test(tokio::test)]
707 async fn match_field_dispatches_to_a_registered_core_handler() {
708 let key = "match_field_dispatches_to_a_registered_core_handler";
709 register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
710 register_core_field_matcher(key, Arc::new(TestCoreMatcher {
711 mismatches: vec![],
712 error: String::default()
713 }));
714
715 let matcher = find_field_matcher(key).unwrap();
716 let result = matcher.match_field(
717 &a_rule(),
718 &FieldValue::Json(Value::String("4111111111111111".to_string())),
719 &FieldValue::Json(Value::String("4012888888881881".to_string())),
720 &field_context()
721 ).await;
722
723 deregister_core_field_matcher(key);
724
725 expect!(matcher.is_core()).to(be_true());
726 expect!(result).to(be_ok());
727 }
728
729 #[test_log::test(tokio::test)]
730 async fn match_field_reports_mismatches_against_the_requested_path() {
731 let key = "match_field_reports_mismatches_against_the_requested_path";
732 register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
733 register_core_field_matcher(key, Arc::new(TestCoreMatcher {
734 mismatches: vec![ProtoContentMismatch {
735 mismatch: "fails the Luhn check".to_string(),
737 expected: Some("4111111111111111".as_bytes().to_vec()),
738 actual: Some("4111111111111112".as_bytes().to_vec()),
739 .. ProtoContentMismatch::default()
740 }],
741 error: String::default()
742 }));
743
744 let matcher = find_field_matcher(key).unwrap();
745 let result = matcher.match_field(
746 &a_rule(),
747 &FieldValue::Json(Value::String("4111111111111111".to_string())),
748 &FieldValue::Json(Value::String("4111111111111112".to_string())),
749 &field_context()
750 ).await;
751
752 deregister_core_field_matcher(key);
753
754 let mismatches = result.expect_err("expected a mismatch");
755 expect!(mismatches.len()).to(be_equal_to(1));
756 expect!(mismatches[0].mismatch.clone()).to(be_equal_to("fails the Luhn check".to_string()));
757 expect!(mismatches[0].path.clone()).to(be_equal_to("$.card.number".to_string()));
758 expect!(mismatches[0].mismatch_type.clone()).to(be_some().value("body".to_string()));
759 expect!(mismatches[0].expected.clone()).to(be_equal_to("4111111111111111".to_string()));
760 }
761
762 #[test_log::test(tokio::test)]
763 async fn match_field_turns_a_handler_error_into_a_mismatch() {
764 let key = "match_field_turns_a_handler_error_into_a_mismatch";
765 register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
766 register_core_field_matcher(key, Arc::new(TestCoreMatcher {
767 mismatches: vec![],
768 error: "'amx' is not a brand this plugin knows about".to_string()
769 }));
770
771 let matcher = find_field_matcher(key).unwrap();
772 let result = matcher.match_field(
773 &a_rule(),
774 &FieldValue::Json(Value::String("4111111111111111".to_string())),
775 &FieldValue::Json(Value::String("4111111111111111".to_string())),
776 &field_context()
777 ).await;
778
779 deregister_core_field_matcher(key);
780
781 let mismatches = result.expect_err("expected the error to surface");
782 expect!(mismatches[0].mismatch.clone())
783 .to(be_equal_to("'amx' is not a brand this plugin knows about".to_string()));
784 }
785
786 #[test_log::test(tokio::test)]
787 async fn match_field_fails_clearly_when_no_core_handler_is_registered() {
788 let key = "match_field_fails_clearly_when_no_core_handler_is_registered";
789 register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
790
791 let matcher = find_field_matcher(key).unwrap();
792 let result = matcher.match_field(
793 &a_rule(),
794 &FieldValue::Json(Value::Null),
795 &FieldValue::Json(Value::Null),
796 &field_context()
797 ).await;
798
799 let mismatches = result.expect_err("expected an error for a registered entry with no handler");
800 expect!(mismatches[0].mismatch.contains("No core field matcher registered")).to(be_true());
801 }
802
803 #[test_log::test(tokio::test)]
804 async fn generate_field_dispatches_to_a_registered_core_handler() {
805 let key = "generate_field_dispatches_to_a_registered_core_handler";
806 register_core_matcher_entry(key, CatalogueEntryType::GENERATOR);
807 register_core_field_generator(key, Arc::new(TestCoreGenerator));
808
809 let generator = find_field_generator(key).unwrap();
810 let result = generator.generate_field(
811 &Generator::RandomString(16),
812 &FieldValue::Json(Value::String("4111111111111111".to_string())),
813 TestMode::Consumer,
814 &field_context()
815 ).await;
816
817 deregister_core_field_generator(key);
818
819 expect!(generator.is_core()).to(be_true());
820 expect!(result.unwrap()).to(be_equal_to(
821 FieldValue::Json(Value::String("4012888888881881".to_string()))
822 ));
823 }
824
825 #[test]
826 fn finding_a_rule_that_is_not_registered_says_so() {
827 let err = find_field_matcher("finding_a_rule_that_is_not_registered_says_so")
828 .expect_err("expected an error for an unregistered rule");
829 expect!(err.to_string().contains("No catalogue entry found")).to(be_true());
830 }
831
832 #[test]
833 fn finding_a_rule_that_is_a_generator_says_so() {
834 let key = "finding_a_rule_that_is_a_generator_says_so";
835 register_core_matcher_entry(key, CatalogueEntryType::GENERATOR);
836
837 let err = find_field_matcher(key).expect_err("expected an error for the wrong entry type");
838 expect!(err.to_string().contains("is a GENERATOR, not a MATCHER")).to(be_true());
839 }
840
841 #[test_log::test]
842 fn the_blocking_bridge_runs_a_call_from_a_synchronous_context() {
843 let key = "the_blocking_bridge_runs_a_call_from_a_synchronous_context";
844 register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
845 register_core_field_matcher(key, Arc::new(TestCoreMatcher {
846 mismatches: vec![],
847 error: String::default()
848 }));
849
850 let matcher = find_field_matcher(key).unwrap();
851 let result = matcher.match_field_blocking(
852 &a_rule(),
853 &FieldValue::Json(Value::String("4111111111111111".to_string())),
854 &FieldValue::Json(Value::String("4012888888881881".to_string())),
855 &field_context()
856 );
857
858 deregister_core_field_matcher(key);
859
860 expect!(result).to(be_ok());
861 }
862
863 #[test_log::test(tokio::test(flavor = "multi_thread"))]
864 async fn the_blocking_bridge_works_from_inside_a_runtime() {
865 let key = "the_blocking_bridge_works_from_inside_a_runtime";
868 register_core_matcher_entry(key, CatalogueEntryType::MATCHER);
869 register_core_field_matcher(key, Arc::new(TestCoreMatcher {
870 mismatches: vec![],
871 error: String::default()
872 }));
873
874 let result = tokio::task::spawn_blocking(move || {
875 let matcher = find_field_matcher(key).unwrap();
876 matcher.match_field_blocking(
877 &a_rule(),
878 &FieldValue::Json(Value::String("4111111111111111".to_string())),
879 &FieldValue::Json(Value::String("4012888888881881".to_string())),
880 &field_context()
881 )
882 }).await.unwrap();
883
884 deregister_core_field_matcher(key);
885
886 expect!(result).to(be_ok());
887 }
888}