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