1#![warn(missing_docs)]
354
355use std::collections::{BTreeSet, HashMap, HashSet};
356use std::fmt::{Debug, Display};
357use std::fmt::Formatter;
358use std::hash::Hash;
359use std::panic::RefUnwindSafe;
360use std::str;
361use std::str::from_utf8;
362
363use ansi_term::*;
364use ansi_term::Colour::*;
365use anyhow::anyhow;
366use bytes::Bytes;
367use itertools::{Either, Itertools};
368use lazy_static::*;
369use maplit::{hashmap, hashset};
370#[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))] use pact_plugin_driver::catalogue_manager::find_content_matcher;
371#[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))] use pact_plugin_driver::plugin_models::PluginInteractionConfig;
372use serde_json::{json, Value};
373#[allow(unused_imports)] use tracing::{debug, error, info, instrument, trace, warn};
374
375use pact_models::bodies::OptionalBody;
376use pact_models::content_types::ContentType;
377use pact_models::generators::{apply_generators, GenerateValue, GeneratorCategory, GeneratorTestMode, VariantMatcher};
378use pact_models::http_parts::HttpPart;
379use pact_models::interaction::Interaction;
380use pact_models::json_utils::json_to_string;
381use pact_models::matchingrules::{Category, MatchingRule, MatchingRuleCategory, RuleList};
382use pact_models::pact::Pact;
383use pact_models::PactSpecification;
384use pact_models::path_exp::DocPath;
385use pact_models::v4::http_parts::{HttpRequest, HttpResponse};
386use pact_models::v4::message_parts::MessageContents;
387use pact_models::v4::sync_message::SynchronousMessage;
388
389use crate::engine::{
390 body_mismatches,
391 build_message_plan,
392 build_request_plan,
393 build_response_plan,
394 execute_message_plan,
395 execute_request_plan,
396 execute_response_plan,
397 ExecutionPlan,
398 header_mismatches,
399 metadata_mismatches,
400 method_mismatch,
401 path_mismatch,
402 query_mismatches
403};
404use crate::engine::context::{MatchingConfiguration, PlanMatchingContext};
405use crate::generators::bodies::generators_process_body;
406use crate::generators::DefaultVariantMatcher;
407use crate::headers::{match_header_value, match_headers};
408#[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))] use crate::json::match_json;
409use crate::matchingrules::{DisplayForMismatch, DoMatch, match_values};
410#[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))] use crate::plugin_support::{InteractionPart, setup_plugin_config};
411use crate::query::match_query_maps;
412
413#[macro_export]
415macro_rules! s {
416 ($e:expr) => ($e.to_string())
417}
418
419pub const PACT_RUST_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
421
422pub mod json;
423pub mod matchingrules;
424#[cfg(not(target_family = "wasm"))] pub mod metrics;
425pub mod generators;
426pub mod engine;
427
428#[cfg(feature = "xml")] mod xml;
429pub mod binary_utils;
430pub mod headers;
431pub mod query;
432pub mod form_urlencoded;
433#[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))] mod plugin_support;
434
435#[cfg(not(feature = "plugins"))]
436#[derive(Clone, Debug, PartialEq)]
437pub struct PluginInteractionConfig {}
439
440pub trait MatchingContext: Debug {
442 fn matcher_is_defined(&self, path: &DocPath) -> bool;
444
445 fn select_best_matcher(&self, path: &DocPath) -> RuleList;
447
448 fn type_matcher_defined(&self, path: &DocPath) -> bool;
450
451 fn values_matcher_defined(&self, path: &DocPath) -> bool;
453
454 fn direct_matcher_defined(&self, path: &DocPath, matchers: &HashSet<&str>) -> bool;
456
457 fn match_keys(&self, path: &DocPath, expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> Result<(), Vec<CommonMismatch>>;
459
460 #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
462 fn plugin_configuration(&self) -> &HashMap<String, PluginInteractionConfig>;
463
464 fn matchers(&self) -> &MatchingRuleCategory;
466
467 fn config(&self) -> DiffConfig;
469
470 fn clone_with(&self, matchers: &MatchingRuleCategory) -> Box<dyn MatchingContext + Send + Sync>;
472}
473
474#[derive(Debug, Clone)]
475pub struct CoreMatchingContext {
477 pub matchers: MatchingRuleCategory,
479 pub config: DiffConfig,
481 pub matching_spec: PactSpecification,
483 #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
485 pub plugin_configuration: HashMap<String, PluginInteractionConfig>
486}
487
488impl CoreMatchingContext {
489 pub fn new(
491 config: DiffConfig,
492 matchers: &MatchingRuleCategory,
493 #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
494 plugin_configuration: &HashMap<String, PluginInteractionConfig>
495 ) -> Self {
496 #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
497 {
498 CoreMatchingContext {
499 matchers: matchers.clone(),
500 config,
501 plugin_configuration: plugin_configuration.clone(),
502 ..CoreMatchingContext::default()
503 }
504 }
505
506 #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
507 {
508 CoreMatchingContext {
509 matchers: matchers.clone(),
510 config,
511 ..CoreMatchingContext::default()
512 }
513 }
514 }
515
516 pub fn with_config(config: DiffConfig) -> Self {
518 CoreMatchingContext {
519 config,
520 .. CoreMatchingContext::default()
521 }
522 }
523
524 fn matchers_for_exact_path(&self, path: &DocPath) -> MatchingRuleCategory {
525 match self.matchers.name {
526 Category::HEADER | Category::QUERY => self.matchers.filter(|&(val, _)| {
527 path.len() == 1 && path.first_field() == val.first_field()
528 }),
529 Category::BODY => self.matchers.filter(|&(val, _)| {
530 let p = path.to_vec();
531 let p_slice = p.iter().map(|p| p.as_str()).collect_vec();
532 val.matches_path_exactly(p_slice.as_slice())
533 }),
534 _ => self.matchers.filter(|_| false)
535 }
536 }
537
538 #[allow(dead_code)]
539 pub(crate) fn clone_from(context: &(dyn MatchingContext + Send + Sync)) -> Self {
540 #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
541 {
542 CoreMatchingContext {
543 matchers: context.matchers().clone(),
544 config: context.config().clone(),
545 plugin_configuration: context.plugin_configuration().clone(),
546 .. CoreMatchingContext::default()
547 }
548 }
549
550 #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
551 {
552 CoreMatchingContext {
553 matchers: context.matchers().clone(),
554 config: context.config().clone(),
555 .. CoreMatchingContext::default()
556 }
557 }
558 }
559}
560
561impl Default for CoreMatchingContext {
562 fn default() -> Self {
563 #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
564 {
565 CoreMatchingContext {
566 matchers: Default::default(),
567 config: DiffConfig::AllowUnexpectedKeys,
568 matching_spec: PactSpecification::V3,
569 plugin_configuration: Default::default()
570 }
571 }
572
573 #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
574 {
575 CoreMatchingContext {
576 matchers: Default::default(),
577 config: DiffConfig::AllowUnexpectedKeys,
578 matching_spec: PactSpecification::V3
579 }
580 }
581 }
582}
583
584impl MatchingContext for CoreMatchingContext {
585 #[instrument(level = "trace", ret, skip_all, fields(path, matchers = ?self.matchers))]
586 fn matcher_is_defined(&self, path: &DocPath) -> bool {
587 let path = path.to_vec();
588 let path_slice = path.iter().map(|p| p.as_str()).collect_vec();
589 self.matchers.matcher_is_defined(path_slice.as_slice())
590 }
591
592 fn select_best_matcher(&self, path: &DocPath) -> RuleList {
593 let path = path.to_vec();
594 let path_slice = path.iter().map(|p| p.as_str()).collect_vec();
595 self.matchers.select_best_matcher(path_slice.as_slice())
596 }
597
598 fn type_matcher_defined(&self, path: &DocPath) -> bool {
599 let path = path.to_vec();
600 let path_slice = path.iter().map(|p| p.as_str()).collect_vec();
601 self.matchers.resolve_matchers_for_path(path_slice.as_slice()).type_matcher_defined()
602 }
603
604 fn values_matcher_defined(&self, path: &DocPath) -> bool {
605 self.matchers_for_exact_path(path).values_matcher_defined()
606 }
607
608 fn direct_matcher_defined(&self, path: &DocPath, matchers: &HashSet<&str>) -> bool {
609 let actual = self.matchers_for_exact_path(path);
610 if matchers.is_empty() {
611 actual.is_not_empty()
612 } else {
613 actual.as_rule_list().rules.iter().any(|r| matchers.contains(r.name().as_str()))
614 }
615 }
616
617 fn match_keys(
618 &self,
619 path: &DocPath,
620 expected: &BTreeSet<String>,
621 actual: &BTreeSet<String>
622 ) -> Result<(), Vec<CommonMismatch>> {
623 let mut expected_keys = expected.iter().cloned().collect::<Vec<String>>();
624 expected_keys.sort();
625 let mut actual_keys = actual.iter().cloned().collect::<Vec<String>>();
626 actual_keys.sort();
627 let missing_keys: Vec<String> = expected.iter().filter(|key| !actual.contains(*key)).cloned().collect();
628 let mut result = vec![];
629
630 if !self.direct_matcher_defined(path, &hashset! { "values", "each-value", "each-key" }) {
631 match self.config {
632 DiffConfig::AllowUnexpectedKeys if !missing_keys.is_empty() => {
633 result.push(CommonMismatch {
634 path: path.to_string(),
635 expected: expected.for_mismatch(),
636 actual: actual.for_mismatch(),
637 description: format!("Actual map is missing the following keys: {}", missing_keys.join(", ")),
638 });
639 }
640 DiffConfig::NoUnexpectedKeys if expected_keys != actual_keys => {
641 result.push(CommonMismatch {
642 path: path.to_string(),
643 expected: expected.for_mismatch(),
644 actual: actual.for_mismatch(),
645 description: format!("Expected a Map with keys [{}] but received one with keys [{}]",
646 expected_keys.join(", "), actual_keys.join(", ")),
647 });
648 }
649 _ => {}
650 }
651 }
652
653 if self.direct_matcher_defined(path, &Default::default()) {
654 let matchers = self.select_best_matcher(path);
655 for matcher in matchers.rules {
656 match matcher {
657 MatchingRule::EachKey(definition) => {
658 for sub_matcher in definition.rules {
659 match sub_matcher {
660 Either::Left(rule) => {
661 for key in &actual_keys {
662 let key_path = path.join(key);
663 if let Err(err) = rule.match_value("", key.as_str(), false, false) {
664 result.push(CommonMismatch {
665 path: key_path.to_string(),
666 expected: "".to_string(),
667 actual: key.clone(),
668 description: err.to_string(),
669 });
670 }
671 }
672 }
673 Either::Right(name) => {
674 result.push(CommonMismatch {
675 path: path.to_string(),
676 expected: expected.for_mismatch(),
677 actual: actual.for_mismatch(),
678 description: format!("Expected a matching rule, found an unresolved reference '{}'",
679 name.name),
680 });
681 }
682 }
683 }
684 }
685 _ => {}
686 }
687 }
688 }
689
690 if result.is_empty() {
691 Ok(())
692 } else {
693 Err(result)
694 }
695 }
696
697 #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
698 fn plugin_configuration(&self) -> &HashMap<String, PluginInteractionConfig> {
699 &self.plugin_configuration
700 }
701
702 fn matchers(&self) -> &MatchingRuleCategory {
703 &self.matchers
704 }
705
706 fn config(&self) -> DiffConfig {
707 self.config
708 }
709
710 fn clone_with(&self, matchers: &MatchingRuleCategory) -> Box<dyn MatchingContext + Send + Sync> {
711 #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
712 {
713 Box::new(CoreMatchingContext {
714 matchers: matchers.clone(),
715 config: self.config.clone(),
716 matching_spec: self.matching_spec,
717 plugin_configuration: self.plugin_configuration.clone()
718 })
719 }
720
721 #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
722 {
723 Box::new(CoreMatchingContext {
724 matchers: matchers.clone(),
725 config: self.config.clone(),
726 matching_spec: self.matching_spec
727 })
728 }
729 }
730}
731
732#[derive(Debug, Clone, Default)]
733pub struct HeaderMatchingContext {
735 inner_context: CoreMatchingContext
736}
737
738impl HeaderMatchingContext {
739 pub fn new(context: &(dyn MatchingContext + Send + Sync)) -> Self {
741 let matchers = context.matchers();
742
743 #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
744 {
745 HeaderMatchingContext {
746 inner_context: CoreMatchingContext::new(
747 context.config(),
748 &MatchingRuleCategory {
749 name: matchers.name.clone(),
750 rules: matchers.rules.iter()
751 .map(|(path, rules)| {
752 (path.to_lower_case(), rules.clone())
753 })
754 .collect()
755 },
756 &context.plugin_configuration()
757 )
758 }
759 }
760
761 #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
762 {
763 HeaderMatchingContext {
764 inner_context: CoreMatchingContext::new(
765 context.config(),
766 &MatchingRuleCategory {
767 name: matchers.name.clone(),
768 rules: matchers.rules.iter()
769 .map(|(path, rules)| {
770 (path.to_lower_case(), rules.clone())
771 })
772 .collect()
773 }
774 )
775 }
776 }
777 }
778}
779
780impl MatchingContext for HeaderMatchingContext {
781 fn matcher_is_defined(&self, path: &DocPath) -> bool {
782 self.inner_context.matcher_is_defined(path)
783 }
784
785 fn select_best_matcher(&self, path: &DocPath) -> RuleList {
786 self.inner_context.select_best_matcher(path)
787 }
788
789 fn type_matcher_defined(&self, path: &DocPath) -> bool {
790 self.inner_context.type_matcher_defined(path)
791 }
792
793 fn values_matcher_defined(&self, path: &DocPath) -> bool {
794 self.inner_context.values_matcher_defined(path)
795 }
796
797 fn direct_matcher_defined(&self, path: &DocPath, matchers: &HashSet<&str>) -> bool {
798 self.inner_context.direct_matcher_defined(path, matchers)
799 }
800
801 fn match_keys(&self, path: &DocPath, expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> Result<(), Vec<CommonMismatch>> {
802 self.inner_context.match_keys(path, expected, actual)
803 }
804
805 #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
806 fn plugin_configuration(&self) -> &HashMap<String, PluginInteractionConfig> {
807 self.inner_context.plugin_configuration()
808 }
809
810 fn matchers(&self) -> &MatchingRuleCategory {
811 self.inner_context.matchers()
812 }
813
814 fn config(&self) -> DiffConfig {
815 self.inner_context.config()
816 }
817
818 fn clone_with(&self, matchers: &MatchingRuleCategory) -> Box<dyn MatchingContext + Send + Sync> {
819 #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
820 {
821 Box::new(HeaderMatchingContext::new(
822 &CoreMatchingContext {
823 matchers: matchers.clone(),
824 config: self.inner_context.config.clone(),
825 matching_spec: self.inner_context.matching_spec,
826 plugin_configuration: self.inner_context.plugin_configuration.clone()
827 }
828 ))
829 }
830
831 #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
832 {
833 Box::new(HeaderMatchingContext::new(
834 &CoreMatchingContext {
835 matchers: matchers.clone(),
836 config: self.inner_context.config.clone(),
837 matching_spec: self.inner_context.matching_spec
838 }
839 ))
840 }
841 }
842}
843
844lazy_static! {
845 static ref BODY_MATCHERS: [
846 (fn(content_type: &ContentType) -> bool,
847 fn(expected: &(dyn HttpPart + Send + Sync), actual: &(dyn HttpPart + Send + Sync), context: &(dyn MatchingContext + Send + Sync)) -> Result<(), Vec<Mismatch>>); 5]
848 = [
849 (|content_type| { content_type.is_json() }, json::match_json),
850 (|content_type| { content_type.is_xml() }, match_xml),
851 (|content_type| { content_type.main_type == "multipart" }, binary_utils::match_mime_multipart),
852 (|content_type| { content_type.base_type() == "application/x-www-form-urlencoded" }, form_urlencoded::match_form_urlencoded),
853 (|content_type| { content_type.is_binary() || content_type.base_type() == "application/octet-stream" }, binary_utils::match_octet_stream)
854 ];
855}
856
857fn match_xml(
858 expected: &(dyn HttpPart + Send + Sync),
859 actual: &(dyn HttpPart + Send + Sync),
860 context: &(dyn MatchingContext + Send + Sync)
861) -> Result<(), Vec<Mismatch>> {
862 #[cfg(feature = "xml")]
863 {
864 xml::match_xml(expected, actual, context)
865 }
866 #[cfg(not(feature = "xml"))]
867 {
868 warn!("Matching XML documents requires the xml feature to be enabled");
869 match_text(&expected.body().value(), &actual.body().value(), context)
870 }
871}
872
873#[derive(Debug, Clone, PartialOrd, Ord, Eq)]
875pub struct CommonMismatch {
876 pub path: String,
878 expected: String,
880 actual: String,
882 description: String
884}
885
886impl CommonMismatch {
887 pub fn to_body_mismatch(&self) -> Mismatch {
889 Mismatch::BodyMismatch {
890 path: self.path.clone(),
891 expected: Some(self.expected.clone().into()),
892 actual: Some(self.actual.clone().into()),
893 mismatch: self.description.clone()
894 }
895 }
896
897 pub fn to_query_mismatch(&self) -> Mismatch {
899 Mismatch::QueryMismatch {
900 parameter: self.path.clone(),
901 expected: self.expected.clone(),
902 actual: self.actual.clone(),
903 mismatch: self.description.clone()
904 }
905 }
906
907 pub fn to_header_mismatch(&self) -> Mismatch {
909 Mismatch::HeaderMismatch {
910 key: self.path.clone(),
911 expected: self.expected.clone().into(),
912 actual: self.actual.clone().into(),
913 mismatch: self.description.clone()
914 }
915 }
916}
917
918impl Display for CommonMismatch {
919 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
920 write!(f, "{}", self.description)
921 }
922}
923
924impl PartialEq for CommonMismatch {
925 fn eq(&self, other: &CommonMismatch) -> bool {
926 self.path == other.path && self.expected == other.expected && self.actual == other.actual
927 }
928}
929
930impl From<Mismatch> for CommonMismatch {
931 fn from(value: Mismatch) -> Self {
932 match value {
933 Mismatch::MethodMismatch { expected, actual , mismatch} => CommonMismatch {
934 path: "".to_string(),
935 expected: expected.clone(),
936 actual: actual.clone(),
937 description: mismatch.clone()
938 },
939 Mismatch::PathMismatch { expected, actual, mismatch } => CommonMismatch {
940 path: "".to_string(),
941 expected: expected.clone(),
942 actual: actual.clone(),
943 description: mismatch.clone()
944 },
945 Mismatch::StatusMismatch { expected, actual, mismatch } => CommonMismatch {
946 path: "".to_string(),
947 expected: expected.to_string(),
948 actual: actual.to_string(),
949 description: mismatch.clone()
950 },
951 Mismatch::QueryMismatch { parameter, expected, actual, mismatch } => CommonMismatch {
952 path: parameter.clone(),
953 expected: expected.clone(),
954 actual: actual.clone(),
955 description: mismatch.clone()
956 },
957 Mismatch::HeaderMismatch { key, expected, actual, mismatch } => CommonMismatch {
958 path: key.clone(),
959 expected: expected.clone(),
960 actual: actual.clone(),
961 description: mismatch.clone()
962 },
963 Mismatch::BodyTypeMismatch { expected, actual, mismatch, .. } => CommonMismatch {
964 path: "".to_string(),
965 expected: expected.clone(),
966 actual: actual.clone(),
967 description: mismatch.clone()
968 },
969 Mismatch::BodyMismatch { path, expected, actual, mismatch } => CommonMismatch {
970 path: path.clone(),
971 expected: String::from_utf8_lossy(expected.unwrap_or_default().as_ref()).to_string(),
972 actual: String::from_utf8_lossy(actual.unwrap_or_default().as_ref()).to_string(),
973 description: mismatch.clone()
974 },
975 Mismatch::MetadataMismatch { key, expected, actual, mismatch } => CommonMismatch {
976 path: key.clone(),
977 expected: expected.clone(),
978 actual: actual.clone(),
979 description: mismatch.clone()
980 }
981 }
982 }
983}
984
985#[derive(Debug, Clone, PartialOrd, Ord, Eq)]
987pub enum Mismatch {
988 MethodMismatch {
990 expected: String,
992 actual: String,
994 mismatch: String
996 },
997 PathMismatch {
999 expected: String,
1001 actual: String,
1003 mismatch: String
1005 },
1006 StatusMismatch {
1008 expected: u16,
1010 actual: u16,
1012 mismatch: String
1014 },
1015 QueryMismatch {
1017 parameter: String,
1019 expected: String,
1021 actual: String,
1023 mismatch: String
1025 },
1026 HeaderMismatch {
1028 key: String,
1030 expected: String,
1032 actual: String,
1034 mismatch: String
1036 },
1037 BodyTypeMismatch {
1039 expected: String,
1041 actual: String,
1043 mismatch: String,
1045 expected_body: Option<Bytes>,
1047 actual_body: Option<Bytes>
1049 },
1050 BodyMismatch {
1052 path: String,
1054 expected: Option<Bytes>,
1056 actual: Option<Bytes>,
1058 mismatch: String
1060 },
1061 MetadataMismatch {
1063 key: String,
1065 expected: String,
1067 actual: String,
1069 mismatch: String
1071 }
1072}
1073
1074impl Mismatch {
1075 pub fn to_json(&self) -> serde_json::Value {
1077 match self {
1078 Mismatch::MethodMismatch { expected: e, actual: a, mismatch: m } => {
1079 json!({
1080 "type" : "MethodMismatch",
1081 "expected" : e,
1082 "actual" : a,
1083 "mismatch" : m
1084 })
1085 },
1086 Mismatch::PathMismatch { expected: e, actual: a, mismatch: m } => {
1087 json!({
1088 "type" : "PathMismatch",
1089 "expected" : e,
1090 "actual" : a,
1091 "mismatch" : m
1092 })
1093 },
1094 Mismatch::StatusMismatch { expected: e, actual: a, mismatch: m } => {
1095 json!({
1096 "type" : "StatusMismatch",
1097 "expected" : e,
1098 "actual" : a,
1099 "mismatch": m
1100 })
1101 },
1102 Mismatch::QueryMismatch { parameter: p, expected: e, actual: a, mismatch: m } => {
1103 json!({
1104 "type" : "QueryMismatch",
1105 "parameter" : p,
1106 "expected" : e,
1107 "actual" : a,
1108 "mismatch" : m
1109 })
1110 },
1111 Mismatch::HeaderMismatch { key: k, expected: e, actual: a, mismatch: m } => {
1112 json!({
1113 "type" : "HeaderMismatch",
1114 "key" : k,
1115 "expected" : e,
1116 "actual" : a,
1117 "mismatch" : m
1118 })
1119 },
1120 Mismatch::BodyTypeMismatch {
1121 expected,
1122 actual,
1123 mismatch,
1124 expected_body,
1125 actual_body
1126 } => {
1127 json!({
1128 "type" : "BodyTypeMismatch",
1129 "expected" : expected,
1130 "actual" : actual,
1131 "mismatch" : mismatch,
1132 "expectedBody": match expected_body {
1133 Some(v) => serde_json::Value::String(str::from_utf8(v)
1134 .unwrap_or("ERROR: could not convert to UTF-8 from bytes").into()),
1135 None => serde_json::Value::Null
1136 },
1137 "actualBody": match actual_body {
1138 Some(v) => serde_json::Value::String(str::from_utf8(v)
1139 .unwrap_or("ERROR: could not convert to UTF-8 from bytes").into()),
1140 None => serde_json::Value::Null
1141 }
1142 })
1143 },
1144 Mismatch::BodyMismatch { path, expected, actual, mismatch } => {
1145 json!({
1146 "type" : "BodyMismatch",
1147 "path" : path,
1148 "expected" : match expected {
1149 Some(v) => serde_json::Value::String(str::from_utf8(v).unwrap_or("ERROR: could not convert from bytes").into()),
1150 None => serde_json::Value::Null
1151 },
1152 "actual" : match actual {
1153 Some(v) => serde_json::Value::String(str::from_utf8(v).unwrap_or("ERROR: could not convert from bytes").into()),
1154 None => serde_json::Value::Null
1155 },
1156 "mismatch" : mismatch
1157 })
1158 }
1159 Mismatch::MetadataMismatch { key, expected, actual, mismatch } => {
1160 json!({
1161 "type" : "MetadataMismatch",
1162 "key" : key,
1163 "expected" : expected,
1164 "actual" : actual,
1165 "mismatch" : mismatch
1166 })
1167 }
1168 }
1169 }
1170
1171 pub fn mismatch_type(&self) -> &str {
1173 match *self {
1174 Mismatch::MethodMismatch { .. } => "MethodMismatch",
1175 Mismatch::PathMismatch { .. } => "PathMismatch",
1176 Mismatch::StatusMismatch { .. } => "StatusMismatch",
1177 Mismatch::QueryMismatch { .. } => "QueryMismatch",
1178 Mismatch::HeaderMismatch { .. } => "HeaderMismatch",
1179 Mismatch::BodyTypeMismatch { .. } => "BodyTypeMismatch",
1180 Mismatch::BodyMismatch { .. } => "BodyMismatch",
1181 Mismatch::MetadataMismatch { .. } => "MetadataMismatch"
1182 }
1183 }
1184
1185 pub fn summary(&self) -> String {
1187 match *self {
1188 Mismatch::MethodMismatch { expected: ref e, .. } => format!("is a {} request", e),
1189 Mismatch::PathMismatch { expected: ref e, .. } => format!("to path '{}'", e),
1190 Mismatch::StatusMismatch { expected: ref e, .. } => format!("has status code {}", e),
1191 Mismatch::QueryMismatch { ref parameter, expected: ref e, .. } => format!("includes parameter '{}' with value '{}'", parameter, e),
1192 Mismatch::HeaderMismatch { ref key, expected: ref e, .. } => format!("includes header '{}' with value '{}'", key, e),
1193 Mismatch::BodyTypeMismatch { .. } => "has a matching body".to_string(),
1194 Mismatch::BodyMismatch { .. } => "has a matching body".to_string(),
1195 Mismatch::MetadataMismatch { .. } => "has matching metadata".to_string()
1196 }
1197 }
1198
1199 pub fn description(&self) -> String {
1201 match self {
1202 Mismatch::MethodMismatch { expected: e, actual: a, mismatch: m } => if m.is_empty() {
1203 format!("expected {} but was {}", e, a)
1204 } else {
1205 m.clone()
1206 },
1207 Mismatch::PathMismatch { mismatch, .. } => mismatch.clone(),
1208 Mismatch::StatusMismatch { mismatch, .. } => mismatch.clone(),
1209 Mismatch::QueryMismatch { mismatch, .. } => mismatch.clone(),
1210 Mismatch::HeaderMismatch { mismatch, .. } => mismatch.clone(),
1211 Mismatch::BodyTypeMismatch { expected: e, actual: a, .. } =>
1212 format!("Expected a body of '{}' but the actual content type was '{}'", e, a),
1213 Mismatch::BodyMismatch { path, mismatch, .. } => format!("{} -> {}", path, mismatch),
1214 Mismatch::MetadataMismatch { mismatch, .. } => mismatch.clone()
1215 }
1216 }
1217
1218 pub fn ansi_description(&self) -> String {
1220 match self {
1221 Mismatch::MethodMismatch { expected: e, actual: a, .. } => format!("expected {} but was {}", Red.paint(e.clone()), Green.paint(a.clone())),
1222 Mismatch::PathMismatch { expected: e, actual: a, .. } => format!("expected '{}' but was '{}'", Red.paint(e.clone()), Green.paint(a.clone())),
1223 Mismatch::StatusMismatch { expected: e, actual: a, .. } => format!("expected {} but was {}", Red.paint(e.to_string()), Green.paint(a.to_string())),
1224 Mismatch::QueryMismatch { expected: e, actual: a, parameter: p, .. } => format!("Expected '{}' but received '{}' for query parameter '{}'",
1225 Red.paint(e.to_string()), Green.paint(a.to_string()), Style::new().bold().paint(p.clone())),
1226 Mismatch::HeaderMismatch { expected: e, actual: a, key: k, .. } => format!("Expected header '{}' to have value '{}' but was '{}'",
1227 Style::new().bold().paint(k.clone()), Red.paint(e.to_string()), Green.paint(a.to_string())),
1228 Mismatch::BodyTypeMismatch { expected: e, actual: a, .. } =>
1229 format!("expected a body of '{}' but the actual content type was '{}'", Red.paint(e.clone()), Green.paint(a.clone())),
1230 Mismatch::BodyMismatch { path, mismatch, .. } => format!("{} -> {}", Style::new().bold().paint(path.clone()), mismatch),
1231 Mismatch::MetadataMismatch { expected: e, actual: a, key: k, .. } => format!("Expected message metadata '{}' to have value '{}' but was '{}'",
1232 Style::new().bold().paint(k.clone()), Red.paint(e.to_string()), Green.paint(a.to_string()))
1233 }
1234 }
1235}
1236
1237impl PartialEq for Mismatch {
1238 fn eq(&self, other: &Mismatch) -> bool {
1239 match (self, other) {
1240 (Mismatch::MethodMismatch { expected: e1, actual: a1, .. },
1241 Mismatch::MethodMismatch { expected: e2, actual: a2, .. }) => {
1242 e1 == e2 && a1 == a2
1243 },
1244 (Mismatch::PathMismatch { expected: e1, actual: a1, .. },
1245 Mismatch::PathMismatch { expected: e2, actual: a2, .. }) => {
1246 e1 == e2 && a1 == a2
1247 },
1248 (Mismatch::StatusMismatch { expected: e1, actual: a1, .. },
1249 Mismatch::StatusMismatch { expected: e2, actual: a2, .. }) => {
1250 e1 == e2 && a1 == a2
1251 },
1252 (Mismatch::BodyTypeMismatch { expected: e1, actual: a1, .. },
1253 Mismatch::BodyTypeMismatch { expected: e2, actual: a2, .. }) => {
1254 e1 == e2 && a1 == a2
1255 },
1256 (Mismatch::QueryMismatch { parameter: p1, expected: e1, actual: a1, .. },
1257 Mismatch::QueryMismatch { parameter: p2, expected: e2, actual: a2, .. }) => {
1258 p1 == p2 && e1 == e2 && a1 == a2
1259 },
1260 (Mismatch::HeaderMismatch { key: p1, expected: e1, actual: a1, .. },
1261 Mismatch::HeaderMismatch { key: p2, expected: e2, actual: a2, .. }) => {
1262 p1 == p2 && e1 == e2 && a1 == a2
1263 },
1264 (Mismatch::BodyMismatch { path: p1, expected: e1, actual: a1, .. },
1265 Mismatch::BodyMismatch { path: p2, expected: e2, actual: a2, .. }) => {
1266 p1 == p2 && e1 == e2 && a1 == a2
1267 },
1268 (Mismatch::MetadataMismatch { key: p1, expected: e1, actual: a1, .. },
1269 Mismatch::MetadataMismatch { key: p2, expected: e2, actual: a2, .. }) => {
1270 p1 == p2 && e1 == e2 && a1 == a2
1271 },
1272 (_, _) => false
1273 }
1274 }
1275}
1276
1277impl Display for Mismatch {
1278 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1279 write!(f, "{}", self.description())
1280 }
1281}
1282
1283fn merge_result<T: Clone>(res1: Result<(), Vec<T>>, res2: Result<(), Vec<T>>) -> Result<(), Vec<T>> {
1284 match (&res1, &res2) {
1285 (Ok(_), Ok(_)) => res1.clone(),
1286 (Err(_), Ok(_)) => res1.clone(),
1287 (Ok(_), Err(_)) => res2.clone(),
1288 (Err(m1), Err(m2)) => {
1289 let mut mismatches = m1.clone();
1290 mismatches.extend_from_slice(&*m2);
1291 Err(mismatches)
1292 }
1293 }
1294}
1295
1296#[derive(Debug, Default, Clone, PartialEq)]
1298pub enum BodyMatchResult {
1299 #[default]
1301 Ok,
1302 BodyTypeMismatch {
1304 expected_type: String,
1306 actual_type: String,
1308 message: String,
1310 expected: Option<Bytes>,
1312 actual: Option<Bytes>
1314 },
1315 BodyMismatches(HashMap<String, Vec<Mismatch>>)
1317}
1318
1319impl BodyMatchResult {
1320 pub fn mismatches(&self) -> Vec<Mismatch> {
1322 match self {
1323 BodyMatchResult::BodyTypeMismatch { expected_type, actual_type, message, expected, actual } => {
1324 vec![Mismatch::BodyTypeMismatch {
1325 expected: expected_type.clone(),
1326 actual: actual_type.clone(),
1327 mismatch: message.clone(),
1328 expected_body: expected.clone(),
1329 actual_body: actual.clone()
1330 }]
1331 },
1332 BodyMatchResult::BodyMismatches(results) =>
1333 results.values().flatten().cloned().collect(),
1334 _ => vec![]
1335 }
1336 }
1337
1338 pub fn all_matched(&self) -> bool {
1340 match self {
1341 BodyMatchResult::BodyTypeMismatch { .. } => false,
1342 BodyMatchResult::BodyMismatches(results) =>
1343 results.values().all(|m| m.is_empty()),
1344 _ => true
1345 }
1346 }
1347}
1348
1349#[derive(Debug, Default, Clone, PartialEq)]
1351pub struct RequestMatchResult {
1352 pub method: Option<Mismatch>,
1354 pub path: Option<Vec<Mismatch>>,
1356 pub body: BodyMatchResult,
1358 pub query: HashMap<String, Vec<Mismatch>>,
1360 pub headers: HashMap<String, Vec<Mismatch>>
1362}
1363
1364impl RequestMatchResult {
1365 pub fn mismatches(&self) -> Vec<Mismatch> {
1367 let mut m = vec![];
1368
1369 if let Some(ref mismatch) = self.method {
1370 m.push(mismatch.clone());
1371 }
1372 if let Some(ref mismatches) = self.path {
1373 m.extend_from_slice(mismatches.as_slice());
1374 }
1375 for mismatches in self.query.values() {
1376 m.extend_from_slice(mismatches.as_slice());
1377 }
1378 for mismatches in self.headers.values() {
1379 m.extend_from_slice(mismatches.as_slice());
1380 }
1381 m.extend_from_slice(self.body.mismatches().as_slice());
1382
1383 m
1384 }
1385
1386 pub fn score(&self) -> i8 {
1388 let mut score = 0;
1389 if self.method.is_none() {
1390 score += 1;
1391 } else {
1392 score -= 1;
1393 }
1394 if self.path.is_none() {
1395 score += 1
1396 } else {
1397 score -= 1
1398 }
1399 for mismatches in self.query.values() {
1400 if mismatches.is_empty() {
1401 score += 1;
1402 } else {
1403 score -= 1;
1404 }
1405 }
1406 for mismatches in self.headers.values() {
1407 if mismatches.is_empty() {
1408 score += 1;
1409 } else {
1410 score -= 1;
1411 }
1412 }
1413 match &self.body {
1414 BodyMatchResult::BodyTypeMismatch { .. } => {
1415 score -= 1;
1416 },
1417 BodyMatchResult::BodyMismatches(results) => {
1418 for mismatches in results.values() {
1419 if mismatches.is_empty() {
1420 score += 1;
1421 } else {
1422 score -= 1;
1423 }
1424 }
1425 },
1426 _ => ()
1427 }
1428 score
1429 }
1430
1431 pub fn all_matched(&self) -> bool {
1433 self.method.is_none() && self.path.is_none() &&
1434 self.query.values().all(|m| m.is_empty()) &&
1435 self.headers.values().all(|m| m.is_empty()) &&
1436 self.body.all_matched()
1437 }
1438
1439 pub fn method_or_path_mismatch(&self) -> bool {
1441 self.method.is_some() || self.path.is_some()
1442 }
1443}
1444
1445impl From<ExecutionPlan> for RequestMatchResult {
1446 fn from(plan: ExecutionPlan) -> Self {
1447 let request = plan.fetch_node(&[":request"]).unwrap_or_default();
1448 let method = method_mismatch(&request);
1449 let path = path_mismatch(&request);
1450 let query = query_mismatches(&request);
1451 let headers = header_mismatches(&request);
1452 let body = body_mismatches(&request);
1453 RequestMatchResult {
1454 method,
1455 path,
1456 body,
1457 query,
1458 headers
1459 }
1460 }
1461}
1462
1463#[derive(Debug, Clone, Copy, PartialEq)]
1465pub enum DiffConfig {
1466 AllowUnexpectedKeys,
1468 NoUnexpectedKeys
1470}
1471
1472pub fn match_text(expected: &Option<Bytes>, actual: &Option<Bytes>, context: &dyn MatchingContext) -> Result<(), Vec<Mismatch>> {
1474 let path = DocPath::root();
1475 if context.matcher_is_defined(&path) {
1476 let mut mismatches = vec![];
1477 let empty = Bytes::default();
1478 let expected_str = match from_utf8(expected.as_ref().unwrap_or(&empty)) {
1479 Ok(expected) => expected,
1480 Err(err) => {
1481 mismatches.push(Mismatch::BodyMismatch {
1482 path: "$".to_string(),
1483 expected: expected.clone(),
1484 actual: actual.clone(),
1485 mismatch: format!("Could not parse expected value as UTF-8 text: {}", err)
1486 });
1487 ""
1488 }
1489 };
1490 let actual_str = match from_utf8(actual.as_ref().unwrap_or(&empty)) {
1491 Ok(actual) => actual,
1492 Err(err) => {
1493 mismatches.push(Mismatch::BodyMismatch {
1494 path: "$".to_string(),
1495 expected: expected.clone(),
1496 actual: actual.clone(),
1497 mismatch: format!("Could not parse actual value as UTF-8 text: {}", err)
1498 });
1499 ""
1500 }
1501 };
1502 if let Err(messages) = match_values(&path, &context.select_best_matcher(&path), expected_str, actual_str) {
1503 for message in messages {
1504 mismatches.push(Mismatch::BodyMismatch {
1505 path: "$".to_string(),
1506 expected: expected.clone(),
1507 actual: actual.clone(),
1508 mismatch: message.clone()
1509 })
1510 }
1511 };
1512 if mismatches.is_empty() {
1513 Ok(())
1514 } else {
1515 Err(mismatches)
1516 }
1517 } else if expected != actual {
1518 let expected = expected.clone().unwrap_or_default();
1519 let actual = actual.clone().unwrap_or_default();
1520 let e = String::from_utf8_lossy(&expected);
1521 let a = String::from_utf8_lossy(&actual);
1522 let mismatch = format!("Expected body '{}' to match '{}' using equality but did not match", e, a);
1523 Err(vec![
1524 Mismatch::BodyMismatch {
1525 path: "$".to_string(),
1526 expected: Some(expected.clone()),
1527 actual: Some(actual.clone()),
1528 mismatch
1529 }
1530 ])
1531 } else {
1532 Ok(())
1533 }
1534}
1535
1536pub fn match_method(expected: &str, actual: &str) -> Result<(), Mismatch> {
1538 if expected.to_lowercase() != actual.to_lowercase() {
1539 Err(Mismatch::MethodMismatch { expected: expected.to_string(), actual: actual.to_string(), mismatch: "".to_string() })
1540 } else {
1541 Ok(())
1542 }
1543}
1544
1545pub fn match_path(expected: &str, actual: &str, context: &(dyn MatchingContext + Send + Sync)) -> Result<(), Vec<Mismatch>> {
1547 let path = DocPath::empty();
1548 let matcher_result = if context.matcher_is_defined(&path) {
1549 match_values(&path, &context.select_best_matcher(&path), expected.to_string(), actual.to_string())
1550 } else {
1551 MatchingRule::Equality.match_value(expected, actual, false, false)
1552 .map_err(|err| vec![err.to_string()])
1553 };
1554 matcher_result.map_err(|messages| messages.iter().map(|message| {
1555 Mismatch::PathMismatch {
1556 expected: expected.to_string(),
1557 actual: actual.to_string(), mismatch: message.clone()
1558 }
1559 }).collect())
1560}
1561
1562pub fn match_query(
1564 expected: Option<HashMap<String, Vec<Option<String>>>>,
1565 actual: Option<HashMap<String, Vec<Option<String>>>>,
1566 context: &(dyn MatchingContext + Send + Sync)
1567) -> HashMap<String, Vec<Mismatch>> {
1568 match (actual, expected) {
1569 (Some(aqm), Some(eqm)) => match_query_maps(eqm, aqm, context),
1570 (Some(aqm), None) => aqm.iter().map(|(key, value)| {
1571 let actual_value = value.iter().map(|v| v.clone().unwrap_or_default()).collect_vec();
1572 (key.clone(), vec![Mismatch::QueryMismatch {
1573 parameter: key.clone(),
1574 expected: "".to_string(),
1575 actual: format!("{:?}", actual_value),
1576 mismatch: format!("Unexpected query parameter '{}' received", key)
1577 }])
1578 }).collect(),
1579 (None, Some(eqm)) => eqm.iter().map(|(key, value)| {
1580 let expected_value = value.iter().map(|v| v.clone().unwrap_or_default()).collect_vec();
1581 (key.clone(), vec![Mismatch::QueryMismatch {
1582 parameter: key.clone(),
1583 expected: format!("{:?}", expected_value),
1584 actual: "".to_string(),
1585 mismatch: format!("Expected query parameter '{}' but was missing", key)
1586 }])
1587 }).collect(),
1588 (None, None) => hashmap!{}
1589 }
1590}
1591
1592fn group_by<I, F, K>(items: I, f: F) -> HashMap<K, Vec<I::Item>>
1593 where I: IntoIterator, F: Fn(&I::Item) -> K, K: Eq + Hash {
1594 let mut m = hashmap!{};
1595 for item in items {
1596 let key = f(&item);
1597 let values = m.entry(key).or_insert_with(Vec::new);
1598 values.push(item);
1599 }
1600 m
1601}
1602
1603#[instrument(level = "trace", ret, skip_all)]
1604pub(crate) async fn compare_bodies(
1605 content_type: &ContentType,
1606 expected: &(dyn HttpPart + Send + Sync),
1607 actual: &(dyn HttpPart + Send + Sync),
1608 context: &(dyn MatchingContext + Send + Sync)
1609) -> BodyMatchResult {
1610 let mut mismatches = vec![];
1611
1612 trace!(?content_type, "Comparing bodies");
1613
1614 #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
1615 {
1616 match find_content_matcher(content_type) {
1617 Some(matcher) => {
1618 debug!("Using content matcher {} for content type '{}'", matcher.catalogue_entry_key(), content_type);
1619 if matcher.is_core() {
1620 if let Err(m) = match matcher.catalogue_entry_key().as_str() {
1621 "core/content-matcher/form-urlencoded" => form_urlencoded::match_form_urlencoded(expected, actual, context),
1622 "core/content-matcher/json" => match_json(expected, actual, context),
1623 "core/content-matcher/multipart-form-data" => binary_utils::match_mime_multipart(expected, actual, context),
1624 "core/content-matcher/text" => match_text(&expected.body().value(), &actual.body().value(), context),
1625 "core/content-matcher/xml" => {
1626 #[cfg(feature = "xml")]
1627 {
1628 xml::match_xml(expected, actual, context)
1629 }
1630 #[cfg(not(feature = "xml"))]
1631 {
1632 warn!("Matching XML bodies requires the xml feature to be enabled");
1633 match_text(&expected.body().value(), &actual.body().value(), context)
1634 }
1635 },
1636 "core/content-matcher/binary" => binary_utils::match_octet_stream(expected, actual, context),
1637 _ => {
1638 warn!("There is no core content matcher for entry {}", matcher.catalogue_entry_key());
1639 match_text(&expected.body().value(), &actual.body().value(), context)
1640 }
1641 } {
1642 mismatches.extend_from_slice(&*m);
1643 }
1644 } else {
1645 trace!(plugin_name = matcher.plugin_name(),"Content matcher is provided via a plugin");
1646 let plugin_config = context.plugin_configuration().get(&matcher.plugin_name()).cloned();
1647 trace!("Plugin config = {:?}", plugin_config);
1648 if pact_plugin_driver::test_context::current_test_run_id().is_none() {
1649 pact_plugin_driver::test_context::set_test_run_id(Some(uuid::Uuid::new_v4().to_string()));
1650 }
1651 if let Err(map) = matcher.match_contents(expected.body(), actual.body(), &context.matchers(),
1652 context.config() == DiffConfig::AllowUnexpectedKeys, plugin_config).await {
1653 for (_key, list) in map {
1655 for mismatch in list {
1656 mismatches.push(Mismatch::BodyMismatch {
1657 path: mismatch.path.clone(),
1658 expected: Some(Bytes::from(mismatch.expected)),
1659 actual: Some(Bytes::from(mismatch.actual)),
1660 mismatch: mismatch.mismatch.clone()
1661 });
1662 }
1663 }
1664 }
1665 }
1666 }
1667 None => {
1668 debug!("No content matcher defined for content type '{}', using core matcher implementation", content_type);
1669 mismatches.extend(compare_bodies_core(content_type, expected, actual, context));
1670 }
1671 }
1672 }
1673
1674 #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
1675 {
1676 mismatches.extend(compare_bodies_core(content_type, expected, actual, context));
1677 }
1678
1679 if mismatches.is_empty() {
1680 BodyMatchResult::Ok
1681 } else {
1682 BodyMatchResult::BodyMismatches(group_by(mismatches, |m| match m {
1683 Mismatch::BodyMismatch { path: m, ..} => m.to_string(),
1684 _ => String::default()
1685 }))
1686 }
1687}
1688
1689fn compare_bodies_core(
1690 content_type: &ContentType,
1691 expected: &(dyn HttpPart + Send + Sync),
1692 actual: &(dyn HttpPart + Send + Sync),
1693 context: &(dyn MatchingContext + Send + Sync)
1694) -> Vec<Mismatch> {
1695 let mut mismatches = vec![];
1696 match BODY_MATCHERS.iter().find(|mt| mt.0(content_type)) {
1697 Some(match_fn) => {
1698 debug!("Using body matcher for content type '{}'", content_type);
1699 if let Err(m) = match_fn.1(expected, actual, context) {
1700 mismatches.extend_from_slice(&*m);
1701 }
1702 },
1703 None => {
1704 debug!("No body matcher defined for content type '{}', checking for a content type matcher", content_type);
1705 let path = DocPath::root();
1706 if context.matcher_is_defined(&path) && context.select_best_matcher(&path).rules
1707 .iter().any(|rule| if let MatchingRule::ContentType(_) = rule { true } else { false }) {
1708 debug!("Found a content type matcher");
1709 if let Err(m) = binary_utils::match_octet_stream(expected, actual, context) {
1710 mismatches.extend_from_slice(&*m);
1711 }
1712 } else {
1713 debug!("No body matcher defined for content type '{}', using plain text matcher", content_type);
1714 if let Err(m) = match_text(&expected.body().value(), &actual.body().value(), context) {
1715 mismatches.extend_from_slice(&*m);
1716 }
1717 }
1718 }
1719 };
1720 mismatches
1721}
1722
1723#[instrument(level = "trace", ret, skip_all, fields(%content_type, ?context))]
1724async fn match_body_content(
1725 content_type: &ContentType,
1726 expected: &(dyn HttpPart + Send + Sync),
1727 actual: &(dyn HttpPart + Send + Sync),
1728 context: &(dyn MatchingContext + Send + Sync)
1729) -> BodyMatchResult {
1730 let expected_body = expected.body();
1731 let actual_body = actual.body();
1732 match (expected_body, actual_body) {
1733 (&OptionalBody::Missing, _) => BodyMatchResult::Ok,
1734 (&OptionalBody::Null, &OptionalBody::Present(ref b, _, _)) => {
1735 BodyMatchResult::BodyMismatches(hashmap!{ "$".into() => vec![Mismatch::BodyMismatch { expected: None, actual: Some(b.clone()),
1736 mismatch: format!("Expected empty body but received {}", actual_body),
1737 path: s!("/")}]})
1738 },
1739 (&OptionalBody::Empty, &OptionalBody::Present(ref b, _, _)) => {
1740 BodyMatchResult::BodyMismatches(hashmap!{ "$".into() => vec![Mismatch::BodyMismatch { expected: None, actual: Some(b.clone()),
1741 mismatch: format!("Expected empty body but received {}", actual_body),
1742 path: s!("/")}]})
1743 },
1744 (&OptionalBody::Null, _) => BodyMatchResult::Ok,
1745 (&OptionalBody::Empty, _) => BodyMatchResult::Ok,
1746 (e, &OptionalBody::Missing) => {
1747 BodyMatchResult::BodyMismatches(hashmap!{ "$".into() => vec![Mismatch::BodyMismatch {
1748 expected: e.value(),
1749 actual: None,
1750 mismatch: format!("Expected body {} but was missing", e),
1751 path: s!("/")}]})
1752 },
1753 (e, &OptionalBody::Empty) => {
1754 BodyMatchResult::BodyMismatches(hashmap!{ "$".into() => vec![Mismatch::BodyMismatch {
1755 expected: e.value(),
1756 actual: None,
1757 mismatch: format!("Expected body {} but was empty", e),
1758 path: s!("/")}]})
1759 },
1760 (_, _) => compare_bodies(content_type, expected, actual, context).await
1761 }
1762}
1763
1764pub async fn match_body(
1766 expected: &(dyn HttpPart + Send + Sync),
1767 actual: &(dyn HttpPart + Send + Sync),
1768 context: &(dyn MatchingContext + Send + Sync),
1769 header_context: &(dyn MatchingContext + Send + Sync)
1770) -> BodyMatchResult {
1771 let expected_content_type = expected.content_type().unwrap_or_default();
1772 let actual_content_type = actual.content_type().unwrap_or_default();
1773 debug!("expected content type = '{}', actual content type = '{}'", expected_content_type,
1774 actual_content_type);
1775 let content_type_matcher = header_context.select_best_matcher(&DocPath::root().join("content-type"));
1776 debug!("content type header matcher = '{:?}'", content_type_matcher);
1777 if expected_content_type.is_unknown() || actual_content_type.is_unknown() ||
1778 expected_content_type.is_equivalent_to(&actual_content_type) ||
1779 expected_content_type.is_equivalent_to(&actual_content_type.base_type()) ||
1780 (!content_type_matcher.is_empty() &&
1781 match_header_value("Content-Type", 0, expected_content_type.to_string().as_str(),
1782 actual_content_type.to_string().as_str(), header_context, true
1783 ).is_ok()) {
1784 match_body_content(&expected_content_type, expected, actual, context).await
1785 } else if expected.body().is_present() {
1786 BodyMatchResult::BodyTypeMismatch {
1787 expected_type: expected_content_type.to_string(),
1788 actual_type: actual_content_type.to_string(),
1789 message: format!("Expected a body of '{}' but the actual content type was '{}'", expected_content_type,
1790 actual_content_type),
1791 expected: expected.body().value(),
1792 actual: actual.body().value()
1793 }
1794 } else {
1795 BodyMatchResult::Ok
1796 }
1797}
1798
1799#[allow(unused_variables)]
1801pub async fn match_request<'a>(
1802 expected: HttpRequest,
1803 actual: HttpRequest,
1804 pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>,
1805 interaction: &Box<dyn Interaction + Send + Sync + RefUnwindSafe>
1806) -> anyhow::Result<RequestMatchResult> {
1807 debug!("comparing to expected {}", expected);
1808 debug!(" body: '{}'", expected.body.display_string());
1809 debug!(" matching_rules:\n{}", expected.matching_rules);
1810 debug!(" generators: {:?}", expected.generators);
1811
1812 let use_v2_engine = std::env::var("PACT_MATCHING_ENGINE")
1813 .map(|val| val.to_lowercase() == "v2")
1814 .unwrap_or(false);
1815 if use_v2_engine {
1816 let config = MatchingConfiguration {
1817 allow_unexpected_entries: false,
1818 .. MatchingConfiguration::init_from_env()
1819 };
1820 let mut context = PlanMatchingContext {
1821 pact: pact.as_v4_pact().unwrap_or_default(),
1822 interaction: interaction.as_v4().unwrap(),
1823 matching_rules: Default::default(),
1824 config
1825 };
1826
1827 let plan = build_request_plan(&expected, &mut context)?;
1828 let executed_plan = execute_request_plan(&plan, &actual, &mut context)?;
1829
1830 if config.log_executed_plan {
1831 debug!("config = {:?}", config);
1832 debug!("\n{}", executed_plan.pretty_form());
1833 }
1834 if config.log_plan_summary {
1835 info!("\n{}", executed_plan.generate_summary(config.coloured_output));
1836 }
1837 Ok(executed_plan.into())
1838 } else {
1839 let result;
1840
1841 #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
1842 {
1843 let plugin_data = setup_plugin_config(pact, interaction, InteractionPart::Request);
1844 trace!("plugin_data = {:?}", plugin_data);
1845
1846 let path_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1847 &expected.matching_rules.rules_for_category("path").unwrap_or_default(),
1848 &plugin_data
1849 );
1850 let body_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1851 &expected.matching_rules.rules_for_category("body").unwrap_or_default(),
1852 &plugin_data
1853 );
1854 let query_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1855 &expected.matching_rules.rules_for_category("query").unwrap_or_default(),
1856 &plugin_data
1857 );
1858 let header_context = HeaderMatchingContext::new(
1859 &CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1860 &expected.matching_rules.rules_for_category("header").unwrap_or_default(),
1861 &plugin_data
1862 )
1863 );
1864 result = RequestMatchResult {
1865 method: match_method(&expected.method, &actual.method).err(),
1866 path: match_path(&expected.path, &actual.path, &path_context).err(),
1867 body: match_body(&expected, &actual, &body_context, &header_context).await,
1868 query: match_query(expected.query, actual.query, &query_context),
1869 headers: match_headers(expected.headers, actual.headers, &header_context)
1870 };
1871 }
1872
1873 #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
1874 {
1875 let path_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1876 &expected.matching_rules.rules_for_category("path").unwrap_or_default()
1877 );
1878 let body_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1879 &expected.matching_rules.rules_for_category("body").unwrap_or_default()
1880 );
1881 let query_context = CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1882 &expected.matching_rules.rules_for_category("query").unwrap_or_default()
1883 );
1884 let header_context = HeaderMatchingContext::new(
1885 &CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1886 &expected.matching_rules.rules_for_category("header").unwrap_or_default()
1887 )
1888 );
1889 result = RequestMatchResult {
1890 method: match_method(&expected.method, &actual.method).err(),
1891 path: match_path(&expected.path, &actual.path, &path_context).err(),
1892 body: match_body(&expected, &actual, &body_context, &header_context).await,
1893 query: match_query(expected.query, actual.query, &query_context),
1894 headers: match_headers(expected.headers, actual.headers, &header_context)
1895 };
1896 }
1897
1898 debug!("--> Mismatches: {:?}", result.mismatches());
1899 Ok(result)
1900 }
1901}
1902
1903#[instrument(level = "trace")]
1905pub fn match_status(expected: u16, actual: u16, context: &dyn MatchingContext) -> Result<(), Vec<Mismatch>> {
1906 let path = DocPath::empty();
1907 let result = if context.matcher_is_defined(&path) {
1908 match_values(&path, &context.select_best_matcher(&path), expected, actual)
1909 .map_err(|messages| messages.iter().map(|message| {
1910 Mismatch::StatusMismatch {
1911 expected,
1912 actual,
1913 mismatch: message.clone()
1914 }
1915 }).collect())
1916 } else if expected != actual {
1917 Err(vec![Mismatch::StatusMismatch {
1918 expected,
1919 actual,
1920 mismatch: format!("expected {} but was {}", expected, actual)
1921 }])
1922 } else {
1923 Ok(())
1924 };
1925 trace!(?result, "matching response status");
1926 result
1927}
1928
1929#[allow(unused_variables)]
1931pub async fn match_response<'a>(
1932 expected: HttpResponse,
1933 actual: HttpResponse,
1934 pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>,
1935 interaction: &Box<dyn Interaction + Send + Sync + RefUnwindSafe>
1936) -> anyhow::Result<Vec<Mismatch>> {
1937 let mut mismatches = vec![];
1938
1939 debug!("comparing to expected response: {}", expected);
1940
1941 let use_v2_engine = std::env::var("PACT_MATCHING_ENGINE")
1942 .map(|val| val.to_lowercase() == "v2")
1943 .unwrap_or(false);
1944 if use_v2_engine {
1945 let config = MatchingConfiguration {
1946 allow_unexpected_entries: true,
1947 .. MatchingConfiguration::init_from_env()
1948 };
1949 let mut context = PlanMatchingContext {
1950 pact: pact.as_v4_pact().unwrap_or_default(),
1951 interaction: interaction.as_v4().unwrap(),
1952 matching_rules: Default::default(),
1953 config
1954 };
1955
1956 let plan = build_response_plan(&expected, &mut context)?;
1957 let executed_plan = execute_response_plan(&plan, &actual, &mut context)?;
1958
1959 if config.log_executed_plan {
1960 debug!("config = {:?}", config);
1961 debug!("\n{}", executed_plan.pretty_form());
1962 }
1963 if config.log_plan_summary {
1964 info!("\n{}", executed_plan.generate_summary(config.coloured_output));
1965 }
1966 Ok(executed_plan.into())
1967 } else {
1968
1969 #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
1970 {
1971 let plugin_data = setup_plugin_config(pact, interaction, InteractionPart::Response);
1972 trace!("plugin_data = {:?}", plugin_data);
1973
1974 let status_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
1975 &expected.matching_rules.rules_for_category("status").unwrap_or_default(),
1976 &plugin_data);
1977 let body_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
1978 &expected.matching_rules.rules_for_category("body").unwrap_or_default(),
1979 &plugin_data);
1980 let header_context = HeaderMatchingContext::new(
1981 &CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
1982 &expected.matching_rules.rules_for_category("header").unwrap_or_default(),
1983 &plugin_data
1984 )
1985 );
1986
1987 mismatches.extend_from_slice(match_body(&expected, &actual, &body_context, &header_context).await
1988 .mismatches().as_slice());
1989 if let Err(m) = match_status(expected.status, actual.status, &status_context) {
1990 mismatches.extend_from_slice(&m);
1991 }
1992 let result = match_headers(expected.headers, actual.headers,
1993 &header_context);
1994 for values in result.values() {
1995 mismatches.extend_from_slice(values.as_slice());
1996 }
1997 }
1998
1999 #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
2000 {
2001 let status_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2002 &expected.matching_rules.rules_for_category("status").unwrap_or_default());
2003 let body_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2004 &expected.matching_rules.rules_for_category("body").unwrap_or_default());
2005 let header_context = HeaderMatchingContext::new(
2006 &CoreMatchingContext::new(DiffConfig::NoUnexpectedKeys,
2007 &expected.matching_rules.rules_for_category("header").unwrap_or_default()
2008 )
2009 );
2010
2011 mismatches.extend_from_slice(match_body(&expected, &actual, &body_context, &header_context).await
2012 .mismatches().as_slice());
2013 if let Err(m) = match_status(expected.status, actual.status, &status_context) {
2014 mismatches.extend_from_slice(&m);
2015 }
2016 let result = match_headers(expected.headers, actual.headers,
2017 &header_context);
2018 for values in result.values() {
2019 mismatches.extend_from_slice(values.as_slice());
2020 }
2021 }
2022
2023 trace!(?mismatches, "match response");
2024
2025 Ok(mismatches)
2026 }
2027}
2028
2029#[instrument(level = "trace")]
2031pub async fn match_message_contents(
2032 expected: &MessageContents,
2033 actual: &MessageContents,
2034 context: &(dyn MatchingContext + Send + Sync)
2035) -> Result<(), Vec<Mismatch>> {
2036 let use_v2_engine = std::env::var("PACT_MATCHING_ENGINE")
2037 .map(|val| val.to_lowercase() == "v2")
2038 .unwrap_or(false);
2039 if use_v2_engine {
2040 let config = MatchingConfiguration {
2041 allow_unexpected_entries: true,
2042 show_types_in_errors: true,
2043 .. MatchingConfiguration::init_from_env()
2044 };
2045 let plan_context = PlanMatchingContext {
2046 config,
2047 .. PlanMatchingContext::default()
2048 };
2049 match build_message_plan(expected, &plan_context) {
2050 Ok(plan) => match execute_message_plan(&plan, actual, &plan_context) {
2051 Ok(executed_plan) => {
2052 if config.log_executed_plan {
2053 debug!("config = {:?}", config);
2054 debug!("\n{}", executed_plan.pretty_form());
2055 }
2056 if config.log_plan_summary {
2057 info!("\n{}", executed_plan.generate_summary(config.coloured_output));
2058 }
2059 if let Some(message_node) = executed_plan.fetch_node(&[":message"]) {
2060 return match body_mismatches(&message_node) {
2061 BodyMatchResult::Ok => Ok(()),
2062 BodyMatchResult::BodyTypeMismatch { expected_type, actual_type, message, expected: e, actual: a } => {
2063 Err(vec![Mismatch::BodyTypeMismatch {
2064 expected: expected_type,
2065 actual: actual_type,
2066 mismatch: message,
2067 expected_body: e,
2068 actual_body: a
2069 }])
2070 }
2071 BodyMatchResult::BodyMismatches(results) => {
2072 let mismatches: Vec<Mismatch> = results.values()
2073 .flat_map(|values| values.iter().cloned())
2074 .collect();
2075 if mismatches.is_empty() { Ok(()) } else { Err(mismatches) }
2076 }
2077 };
2078 }
2079 return Ok(());
2080 }
2081 Err(err) => warn!("Failed to execute message plan: {}", err)
2082 },
2083 Err(err) => warn!("Failed to build message plan: {}", err)
2084 }
2085 }
2086
2087 let expected_content_type = expected.message_content_type().unwrap_or_default();
2088 let actual_content_type = actual.message_content_type().unwrap_or_default();
2089 debug!("expected content type = '{}', actual content type = '{}'", expected_content_type,
2090 actual_content_type);
2091 if expected_content_type.is_equivalent_to(&actual_content_type) {
2092 let result = match_body_content(&expected_content_type, expected, actual, context).await;
2093 match result {
2094 BodyMatchResult::BodyTypeMismatch { expected_type, actual_type, message, expected, actual } => {
2095 Err(vec![ Mismatch::BodyTypeMismatch {
2096 expected: expected_type,
2097 actual: actual_type,
2098 mismatch: message,
2099 expected_body: expected,
2100 actual_body: actual
2101 } ])
2102 },
2103 BodyMatchResult::BodyMismatches(results) => {
2104 Err(results.values().flat_map(|values| values.iter().cloned()).collect())
2105 },
2106 _ => Ok(())
2107 }
2108 } else if expected.contents.is_present() {
2109 Err(vec![ Mismatch::BodyTypeMismatch {
2110 expected: expected_content_type.to_string(),
2111 actual: actual_content_type.to_string(),
2112 mismatch: format!("Expected message with content type {} but was {}",
2113 expected_content_type, actual_content_type),
2114 expected_body: expected.contents.value(),
2115 actual_body: actual.contents.value()
2116 } ])
2117 } else {
2118 Ok(())
2119 }
2120}
2121
2122#[instrument(level = "trace")]
2124pub fn match_message_metadata(
2125 expected: &MessageContents,
2126 actual: &MessageContents,
2127 context: &dyn MatchingContext
2128) -> HashMap<String, Vec<Mismatch>> {
2129 let use_v2_engine = std::env::var("PACT_MATCHING_ENGINE")
2130 .map(|val| val.to_lowercase() == "v2")
2131 .unwrap_or(false);
2132 if use_v2_engine {
2133 let config = MatchingConfiguration {
2134 allow_unexpected_entries: true,
2135 .. MatchingConfiguration::init_from_env()
2136 };
2137 let plan_context = PlanMatchingContext {
2138 config,
2139 .. PlanMatchingContext::default()
2140 };
2141 match build_message_plan(expected, &plan_context) {
2142 Ok(plan) => match execute_message_plan(&plan, actual, &plan_context) {
2143 Ok(executed_plan) => {
2144 if config.log_executed_plan {
2145 debug!("config = {:?}", config);
2146 debug!("\n{}", executed_plan.pretty_form());
2147 }
2148 if config.log_plan_summary {
2149 info!("\n{}", executed_plan.generate_summary(config.coloured_output));
2150 }
2151 if let Some(message_node) = executed_plan.fetch_node(&[":message"]) {
2152 return metadata_mismatches(&message_node);
2153 }
2154 return hashmap!{};
2155 }
2156 Err(err) => warn!("Failed to execute message plan: {}", err)
2157 },
2158 Err(err) => warn!("Failed to build message plan: {}", err)
2159 }
2160 }
2161
2162 debug!("Matching message metadata");
2163 let mut result = hashmap!{};
2164 let expected_metadata = &expected.metadata;
2165 let actual_metadata = &actual.metadata;
2166 debug!("Matching message metadata. Expected '{:?}', Actual '{:?}'", expected_metadata, actual_metadata);
2167
2168 if !expected_metadata.is_empty() || context.config() == DiffConfig::NoUnexpectedKeys {
2169 for (key, value) in expected_metadata {
2170 match actual_metadata.get(key) {
2171 Some(actual_value) => {
2172 result.insert(key.clone(), match_metadata_value(key, value,
2173 actual_value, context).err().unwrap_or_default());
2174 },
2175 None => {
2176 result.insert(key.clone(), vec![Mismatch::MetadataMismatch { key: key.clone(),
2177 expected: json_to_string(&value),
2178 actual: "".to_string(),
2179 mismatch: format!("Expected message metadata '{}' but was missing", key) }]);
2180 }
2181 }
2182 }
2183 }
2184 result
2185}
2186
2187#[instrument(level = "trace")]
2188fn match_metadata_value(
2189 key: &str,
2190 expected: &Value,
2191 actual: &Value,
2192 context: &dyn MatchingContext
2193) -> Result<(), Vec<Mismatch>> {
2194 debug!("Comparing metadata values for key '{}'", key);
2195 let path = DocPath::root().join(key);
2196 let matcher_result = if context.matcher_is_defined(&path) {
2197 match_values(&path, &context.select_best_matcher(&path), expected, actual)
2198 } else if key.to_ascii_lowercase() == "contenttype" || key.to_ascii_lowercase() == "content-type" {
2199 debug!("Comparing message context type '{}' => '{}'", expected, actual);
2200 headers::match_parameter_header(expected.as_str().unwrap_or_default(), actual.as_str().unwrap_or_default(),
2201 key, "metadata", 0, true)
2202 } else {
2203 MatchingRule::Equality.match_value(expected, actual, false, false).map_err(|err| vec![err.to_string()])
2204 };
2205 matcher_result.map_err(|messages| {
2206 messages.iter().map(|message| {
2207 Mismatch::MetadataMismatch {
2208 key: key.to_string(),
2209 expected: expected.to_string(),
2210 actual: actual.to_string(),
2211 mismatch: format!("Expected metadata key '{}' to have value '{}' but was '{}' - {}", key, expected, actual, message)
2212 }
2213 }).collect()
2214 })
2215}
2216
2217#[allow(unused_variables)]
2219pub async fn match_message<'a>(
2220 expected: &Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2221 actual: &Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2222 pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>) -> Vec<Mismatch> {
2223 let mut mismatches = vec![];
2224
2225 if expected.is_message() && actual.is_message() {
2226 debug!("comparing to expected message: {:?}", expected);
2227 let expected_message = expected.as_message().unwrap();
2228 let actual_message = actual.as_message().unwrap();
2229
2230 let matching_rules = &expected_message.matching_rules;
2231
2232 #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
2233 {
2234 let plugin_data = setup_plugin_config(pact, expected, InteractionPart::None);
2235
2236 let body_context = if expected.is_v4() {
2237 CoreMatchingContext {
2238 matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
2239 config: DiffConfig::AllowUnexpectedKeys,
2240 matching_spec: PactSpecification::V4,
2241 plugin_configuration: plugin_data.clone()
2242 }
2243 } else {
2244 CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2245 &matching_rules.rules_for_category("body").unwrap_or_default(),
2246 &plugin_data)
2247 };
2248
2249 let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2250 &matching_rules.rules_for_category("metadata").unwrap_or_default(),
2251 &plugin_data);
2252 let result = match_message_contents(&expected_message.as_message_content(), &actual_message.as_message_content(), &body_context).await;
2253 mismatches.extend_from_slice(result.err().unwrap_or_default().as_slice());
2254 for values in match_message_metadata(&expected_message.as_message_content(), &actual_message.as_message_content(), &metadata_context).values() {
2255 mismatches.extend_from_slice(values.as_slice());
2256 }
2257 }
2258
2259 #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
2260 {
2261 let body_context = if expected.is_v4() {
2262 CoreMatchingContext {
2263 matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
2264 config: DiffConfig::AllowUnexpectedKeys,
2265 matching_spec: PactSpecification::V4
2266 }
2267 } else {
2268 CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2269 &matching_rules.rules_for_category("body").unwrap_or_default())
2270 };
2271
2272 let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2273 &matching_rules.rules_for_category("metadata").unwrap_or_default());
2274 let result = crate::match_message_contents(&expected_message.as_message_content(), &actual_message.as_message_content(), &body_context).await;
2275 mismatches.extend_from_slice(result.err().unwrap_or_default().as_slice());
2276 for values in crate::match_message_metadata(&expected_message.as_message_content(), &actual_message.as_message_content(), &metadata_context).values() {
2277 mismatches.extend_from_slice(values.as_slice());
2278 }
2279 }
2280 } else {
2281 mismatches.push(Mismatch::BodyTypeMismatch {
2282 expected: "message".into(),
2283 actual: actual.type_of(),
2284 mismatch: format!("Cannot compare a {} with a {}", expected.type_of(), actual.type_of()),
2285 expected_body: None,
2286 actual_body: None
2287 });
2288 }
2289
2290 mismatches
2291}
2292
2293pub async fn match_sync_message<'a>(expected: SynchronousMessage, actual: SynchronousMessage, pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>) -> Vec<Mismatch> {
2295 let mut mismatches = match_sync_message_request(&expected, &actual, pact).await;
2296 let response_result = match_sync_message_response(&expected, &expected.response, &actual.response, pact).await;
2297 mismatches.extend_from_slice(&*response_result);
2298 mismatches
2299}
2300
2301#[allow(unused_variables)]
2303pub async fn match_sync_message_request<'a>(
2304 expected: &SynchronousMessage,
2305 actual: &SynchronousMessage,
2306 pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>
2307) -> Vec<Mismatch> {
2308 debug!("comparing to expected message request: {:?}", expected);
2309
2310 let mut mismatches = vec![];
2311 let matching_rules = &expected.request.matching_rules;
2312
2313 #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
2314 {
2315 let plugin_data = setup_plugin_config(pact, &expected.boxed(), InteractionPart::None);
2316
2317 let body_context = CoreMatchingContext {
2318 matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
2319 config: DiffConfig::AllowUnexpectedKeys,
2320 matching_spec: PactSpecification::V4,
2321 plugin_configuration: plugin_data.clone()
2322 };
2323
2324 let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2325 &matching_rules.rules_for_category("metadata").unwrap_or_default(),
2326 &plugin_data);
2327 let contents = match_message_contents(&expected.request, &actual.request, &body_context).await;
2328
2329 mismatches.extend_from_slice(contents.err().unwrap_or_default().as_slice());
2330 for values in match_message_metadata(&expected.request, &actual.request, &metadata_context).values() {
2331 mismatches.extend_from_slice(values.as_slice());
2332 }
2333 }
2334
2335 #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
2336 {
2337 let body_context = CoreMatchingContext {
2338 matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
2339 config: DiffConfig::AllowUnexpectedKeys,
2340 matching_spec: PactSpecification::V4
2341 };
2342
2343 let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2344 &matching_rules.rules_for_category("metadata").unwrap_or_default());
2345 let contents = match_message_contents(&expected.request, &actual.request, &body_context).await;
2346
2347 mismatches.extend_from_slice(contents.err().unwrap_or_default().as_slice());
2348 for values in match_message_metadata(&expected.request, &actual.request, &metadata_context).values() {
2349 mismatches.extend_from_slice(values.as_slice());
2350 }
2351 }
2352
2353 mismatches
2354}
2355
2356#[allow(unused_variables)]
2358pub async fn match_sync_message_response<'a>(
2359 expected: &SynchronousMessage,
2360 expected_responses: &[MessageContents],
2361 actual_responses: &[MessageContents],
2362 pact: &Box<dyn Pact + Send + Sync + RefUnwindSafe + 'a>
2363) -> Vec<Mismatch> {
2364 debug!("comparing to expected message responses: {:?}", expected_responses);
2365
2366 let mut mismatches = vec![];
2367
2368 if expected_responses.len() != actual_responses.len() {
2369 if !expected_responses.is_empty() && actual_responses.is_empty() {
2370 mismatches.push(Mismatch::BodyTypeMismatch {
2371 expected: "message response".into(),
2372 actual: "".into(),
2373 mismatch: "Expected a message with a response, but the actual response was empty".into(),
2374 expected_body: None,
2375 actual_body: None
2376 });
2377 } else if !expected_responses.is_empty() {
2378 mismatches.push(Mismatch::BodyTypeMismatch {
2379 expected: "message response".into(),
2380 actual: "".into(),
2381 mismatch: format!("Expected a message with {} responses, but the actual response had {}",
2382 expected_responses.len(), actual_responses.len()),
2383 expected_body: None,
2384 actual_body: None
2385 });
2386 }
2387 } else {
2388
2389 #[cfg(feature = "plugins")] #[cfg(not(target_family = "wasm"))]
2390 {
2391 let plugin_data = setup_plugin_config(pact, &expected.boxed(), InteractionPart::None);
2392 for (expected_response, actual_response) in expected_responses.iter().zip(actual_responses) {
2393 let matching_rules = &expected_response.matching_rules;
2394 let body_context = CoreMatchingContext {
2395 matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
2396 config: DiffConfig::AllowUnexpectedKeys,
2397 matching_spec: PactSpecification::V4,
2398 plugin_configuration: plugin_data.clone()
2399 };
2400
2401 let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2402 &matching_rules.rules_for_category("metadata").unwrap_or_default(),
2403 &plugin_data);
2404 let contents = match_message_contents(expected_response, actual_response, &body_context).await;
2405
2406 mismatches.extend_from_slice(contents.err().unwrap_or_default().as_slice());
2407 for values in match_message_metadata(expected_response, actual_response, &metadata_context).values() {
2408 mismatches.extend_from_slice(values.as_slice());
2409 }
2410 }
2411 }
2412
2413 #[cfg(any(not(feature = "plugins"), target_family = "wasm"))]
2414 {
2415 for (expected_response, actual_response) in expected_responses.iter().zip(actual_responses) {
2416 let matching_rules = &expected_response.matching_rules;
2417 let body_context = CoreMatchingContext {
2418 matchers: matching_rules.rules_for_category("content").unwrap_or_default(),
2419 config: DiffConfig::AllowUnexpectedKeys,
2420 matching_spec: PactSpecification::V4
2421 };
2422
2423 let metadata_context = CoreMatchingContext::new(DiffConfig::AllowUnexpectedKeys,
2424 &matching_rules.rules_for_category("metadata").unwrap_or_default());
2425 let contents = match_message_contents(expected_response, actual_response, &body_context).await;
2426
2427 mismatches.extend_from_slice(contents.err().unwrap_or_default().as_slice());
2428 for values in match_message_metadata(expected_response, actual_response, &metadata_context).values() {
2429 mismatches.extend_from_slice(values.as_slice());
2430 }
2431 }
2432 }
2433 }
2434 mismatches
2435}
2436
2437#[instrument(level = "trace")]
2440pub async fn generate_request(request: &HttpRequest, mode: &GeneratorTestMode, context: &HashMap<&str, Value>) -> HttpRequest {
2441 trace!(?request, ?mode, ?context, "generate_request");
2442 let mut request = request.clone();
2443
2444 let generators = request.build_generators(&GeneratorCategory::PATH);
2445 if !generators.is_empty() {
2446 debug!("Applying path generator...");
2447 apply_generators(mode, &generators, &mut |_, generator| {
2448 if let Ok(v) = generator.generate_value(&request.path, context, &DefaultVariantMatcher.boxed()) {
2449 request.path = v;
2450 }
2451 });
2452 }
2453
2454 let generators = request.build_generators(&GeneratorCategory::HEADER);
2455 if !generators.is_empty() {
2456 debug!("Applying header generators...");
2457 apply_generators(mode, &generators, &mut |key, generator| {
2458 if let Some(header) = key.first_field() {
2459 if let Some(ref mut headers) = request.headers {
2460 if headers.contains_key(header) {
2461 if let Ok(v) = generator.generate_value(&headers.get(header).unwrap().clone(), context, &DefaultVariantMatcher.boxed()) {
2462 headers.insert(header.to_string(), v);
2463 }
2464 } else {
2465 if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2466 headers.insert(header.to_string(), vec![ v.to_string() ]);
2467 }
2468 }
2469 } else {
2470 if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2471 request.headers = Some(hashmap!{
2472 header.to_string() => vec![ v.to_string() ]
2473 })
2474 }
2475 }
2476 }
2477 });
2478 }
2479
2480 let generators = request.build_generators(&GeneratorCategory::QUERY);
2481 if !generators.is_empty() {
2482 debug!("Applying query generators...");
2483 apply_generators(mode, &generators, &mut |key, generator| {
2484 if let Some(param) = key.first_field() {
2485 if let Some(ref mut parameters) = request.query {
2486 if let Some(parameter) = parameters.get_mut(param) {
2487 let mut generated = parameter.clone();
2488 for (index, val) in parameter.iter().enumerate() {
2489 let value = val.clone().unwrap_or_default();
2490 if let Ok(v) = generator.generate_value(&value, context, &DefaultVariantMatcher.boxed()) {
2491 generated[index] = Some(v);
2492 }
2493 }
2494 *parameter = generated;
2495 } else if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2496 parameters.insert(param.to_string(), vec![ Some(v.to_string()) ]);
2497 }
2498 } else if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2499 request.query = Some(hashmap!{
2500 param.to_string() => vec![ Some(v.to_string()) ]
2501 })
2502 }
2503 }
2504 });
2505 }
2506
2507 let generators = request.build_generators(&GeneratorCategory::BODY);
2508 if !generators.is_empty() && request.body.is_present() {
2509 debug!("Applying body generators...");
2510 match generators_process_body(mode, &request.body, request.content_type(),
2511 context, &generators, &DefaultVariantMatcher {}, &vec![], &hashmap!{}).await {
2512 Ok(body) => request.body = body,
2513 Err(err) => error!("Failed to generate the body, will use the original: {}", err)
2514 }
2515 }
2516
2517 request
2518}
2519
2520pub async fn generate_response(response: &HttpResponse, mode: &GeneratorTestMode, context: &HashMap<&str, Value>) -> HttpResponse {
2523 trace!(?response, ?mode, ?context, "generate_response");
2524 let mut response = response.clone();
2525 let generators = response.build_generators(&GeneratorCategory::STATUS);
2526 if !generators.is_empty() {
2527 debug!("Applying status generator...");
2528 apply_generators(mode, &generators, &mut |_, generator| {
2529 if let Ok(v) = generator.generate_value(&response.status, context, &DefaultVariantMatcher.boxed()) {
2530 debug!("Generated value for status: {}", v);
2531 response.status = v;
2532 }
2533 });
2534 }
2535 let generators = response.build_generators(&GeneratorCategory::HEADER);
2536 if !generators.is_empty() {
2537 debug!("Applying header generators...");
2538 apply_generators(mode, &generators, &mut |key, generator| {
2539 if let Some(header) = key.first_field() {
2540 if let Some(ref mut headers) = response.headers {
2541 if headers.contains_key(header) {
2542 if let Ok(v) = generator.generate_value(&headers.get(header).unwrap().clone(), context, &DefaultVariantMatcher.boxed()) {
2543 headers.insert(header.to_string(), v);
2544 }
2545 } else {
2546 if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2547 headers.insert(header.to_string(), vec![ v.to_string() ]);
2548 }
2549 }
2550 } else {
2551 if let Ok(v) = generator.generate_value(&"".to_string(), context, &DefaultVariantMatcher.boxed()) {
2552 response.headers = Some(hashmap!{
2553 header.to_string() => vec![ v.to_string() ]
2554 })
2555 }
2556 }
2557 }
2558 });
2559 }
2560 let generators = response.build_generators(&GeneratorCategory::BODY);
2561 if !generators.is_empty() && response.body.is_present() {
2562 debug!("Applying body generators...");
2563 match generators_process_body(mode, &response.body, response.content_type(),
2564 context, &generators, &DefaultVariantMatcher{}, &vec![], &hashmap!{}).await {
2565 Ok(body) => response.body = body,
2566 Err(err) => error!("Failed to generate the body, will use the original: {}", err)
2567 }
2568 }
2569 response
2570}
2571
2572pub async fn match_interaction_request(
2574 expected: Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2575 actual: Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2576 pact: Box<dyn Pact + Send + Sync + RefUnwindSafe>,
2577 _spec_version: &PactSpecification
2578) -> anyhow::Result<RequestMatchResult> {
2579 if let Some(http_interaction) = expected.as_v4_http() {
2580 let request = actual.as_v4_http()
2581 .ok_or_else(|| anyhow!("Could not unpack actual request as a V4 Http Request"))?.request;
2582 match_request(http_interaction.request, request, &pact, &expected).await
2583 } else {
2584 Err(anyhow!("match_interaction_request must be called with HTTP request/response interactions, got {}", expected.type_of()))
2585 }
2586}
2587
2588pub async fn match_interaction_response(
2590 expected: Box<dyn Interaction + Sync + RefUnwindSafe>,
2591 actual: Box<dyn Interaction + Sync + RefUnwindSafe>,
2592 pact: Box<dyn Pact + Send + Sync + RefUnwindSafe>,
2593 _spec_version: &PactSpecification
2594) -> anyhow::Result<Vec<Mismatch>> {
2595 if let Some(expected) = expected.as_v4_http() {
2596 let expected_response = expected.response.clone();
2597 let expected = expected.boxed();
2598 let response = actual.as_v4_http()
2599 .ok_or_else(|| anyhow!("Could not unpack actual response as a V4 Http Response"))?.response;
2600 match_response(expected_response, response, &pact, &expected).await
2601 } else {
2602 Err(anyhow!("match_interaction_response must be called with HTTP request/response interactions, got {}", expected.type_of()))
2603 }
2604}
2605
2606pub async fn match_interaction(
2608 expected: Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2609 actual: Box<dyn Interaction + Send + Sync + RefUnwindSafe>,
2610 pact: Box<dyn Pact + Send + Sync + RefUnwindSafe>,
2611 _spec_version: &PactSpecification
2612) -> anyhow::Result<Vec<Mismatch>> {
2613 if let Some(expected) = expected.as_v4_http() {
2614 let expected_request = expected.request.clone();
2615 let expected_response = expected.response.clone();
2616 let expected = expected.boxed();
2617 let request = actual.as_v4_http()
2618 .ok_or_else(|| anyhow!("Could not unpack actual request as a V4 Http Request"))?.request;
2619 let request_result = match_request(expected_request, request, &pact, &expected).await?;
2620 let response = actual.as_v4_http()
2621 .ok_or_else(|| anyhow!("Could not unpack actual response as a V4 Http Response"))?.response;
2622 let response_result = match_response(expected_response, response, &pact, &expected).await?;
2623 let mut mismatches = request_result.mismatches();
2624 mismatches.extend_from_slice(&*response_result);
2625 Ok(mismatches)
2626 } else if expected.is_message() || expected.is_v4() {
2627 Ok(match_message(&expected, &actual, &pact).await)
2628 } else {
2629 Err(anyhow!("match_interaction must be called with either an HTTP request/response interaction or a Message, got {}", expected.type_of()))
2630 }
2631}
2632
2633#[cfg(test)]
2634mod tests;
2635#[cfg(test)]
2636mod generator_tests;