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