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