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