use std::collections::HashMap;
use std::fmt::{self, Display, Formatter};
use std::sync::Mutex;
use itertools::Itertools;
use lazy_static::lazy_static;
use maplit::hashset;
use pact_models::content_types::ContentType;
use regex::Regex;
use serde::{Deserialize, Serialize};
use tracing::{debug, error, instrument, trace, warn};
use crate::content::{ContentGenerator, ContentMatcher};
use crate::plugin_models::PactPluginManifest;
use crate::proto::catalogue_entry::EntryType;
use crate::proto::CatalogueEntry as ProtoCatalogueEntry;
use crate::proto_v2::catalogue_entry::EntryType as EntryTypeV2;
lazy_static! {
static ref CATALOGUE_REGISTER: Mutex<HashMap<String, CatalogueEntry>> = Mutex::new(HashMap::new());
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[allow(non_camel_case_types)]
pub enum CatalogueEntryType {
CONTENT_MATCHER,
CONTENT_GENERATOR,
TRANSPORT,
MATCHER,
INTERACTION,
GENERATOR
}
impl CatalogueEntryType {
#[deprecated(
since = "1.2.0",
note = "Use to_proto_value, which can represent entry types that only exist on the V2 interface"
)]
pub fn to_proto_type(&self) -> EntryType {
match self {
CatalogueEntryType::CONTENT_MATCHER => EntryType::ContentMatcher,
CatalogueEntryType::CONTENT_GENERATOR => EntryType::ContentGenerator,
CatalogueEntryType::TRANSPORT => EntryType::Transport,
CatalogueEntryType::MATCHER => EntryType::Matcher,
CatalogueEntryType::INTERACTION => EntryType::Interaction,
CatalogueEntryType::GENERATOR => EntryType::Matcher
}
}
fn to_proto_enum(self) -> EntryTypeV2 {
match self {
CatalogueEntryType::CONTENT_MATCHER => EntryTypeV2::ContentMatcher,
CatalogueEntryType::CONTENT_GENERATOR => EntryTypeV2::ContentGenerator,
CatalogueEntryType::TRANSPORT => EntryTypeV2::Transport,
CatalogueEntryType::MATCHER => EntryTypeV2::Matcher,
CatalogueEntryType::INTERACTION => EntryTypeV2::Interaction,
CatalogueEntryType::GENERATOR => EntryTypeV2::Generator
}
}
fn from_proto_enum(entry_type: EntryTypeV2) -> CatalogueEntryType {
match entry_type {
EntryTypeV2::ContentMatcher => CatalogueEntryType::CONTENT_MATCHER,
EntryTypeV2::ContentGenerator => CatalogueEntryType::CONTENT_GENERATOR,
EntryTypeV2::Transport => CatalogueEntryType::TRANSPORT,
EntryTypeV2::Matcher => CatalogueEntryType::MATCHER,
EntryTypeV2::Interaction => CatalogueEntryType::INTERACTION,
EntryTypeV2::Generator => CatalogueEntryType::GENERATOR
}
}
pub fn to_proto_value(self) -> i32 {
self.to_proto_enum() as i32
}
pub fn from_proto_value(value: i32) -> Option<CatalogueEntryType> {
EntryTypeV2::try_from(value).ok().map(CatalogueEntryType::from_proto_enum)
}
pub fn as_proto_name(&self) -> &'static str {
self.to_proto_enum().as_str_name()
}
pub fn from_proto_name(name: &str) -> Option<CatalogueEntryType> {
EntryTypeV2::from_str_name(name).map(CatalogueEntryType::from_proto_enum)
}
}
impl Display for CatalogueEntryType {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
CatalogueEntryType::CONTENT_MATCHER => write!(f, "content-matcher"),
CatalogueEntryType::CONTENT_GENERATOR => write!(f, "content-generator"),
CatalogueEntryType::TRANSPORT => write!(f, "transport"),
CatalogueEntryType::MATCHER => write!(f, "matcher"),
CatalogueEntryType::INTERACTION => write!(f, "interaction"),
CatalogueEntryType::GENERATOR => write!(f, "generator"),
}
}
}
impl From<&str> for CatalogueEntryType {
fn from(s: &str) -> Self {
match s {
"content-matcher" => CatalogueEntryType::CONTENT_MATCHER,
"content-generator" => CatalogueEntryType::CONTENT_GENERATOR,
"interaction" => CatalogueEntryType::INTERACTION,
"matcher" => CatalogueEntryType::MATCHER,
"transport" => CatalogueEntryType::TRANSPORT,
"generator" => CatalogueEntryType::GENERATOR,
_ => {
let message = format!("'{}' is not a valid CatalogueEntryType value", s);
error!("{}", message);
panic!("{}", message)
}
}
}
}
impl From<String> for CatalogueEntryType {
fn from(s: String) -> Self {
Self::from(s.as_str())
}
}
impl From<EntryType> for CatalogueEntryType {
fn from(t: EntryType) -> Self {
match t {
EntryType::ContentMatcher => CatalogueEntryType::CONTENT_MATCHER,
EntryType::ContentGenerator => CatalogueEntryType::CONTENT_GENERATOR,
EntryType::Transport => CatalogueEntryType::TRANSPORT,
EntryType::Matcher => CatalogueEntryType::MATCHER,
EntryType::Interaction => CatalogueEntryType::INTERACTION
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[allow(non_camel_case_types)]
pub enum CatalogueEntryProviderType {
CORE,
PLUGIN
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CatalogueEntry {
pub entry_type: CatalogueEntryType,
pub provider_type: CatalogueEntryProviderType,
pub plugin: Option<PactPluginManifest>,
pub key: String,
pub values: HashMap<String, String>
}
pub fn register_plugin_entries(plugin: &PactPluginManifest, catalogue_list: &Vec<ProtoCatalogueEntry>) {
trace!("register_plugin_entries({:?}, {:?})", plugin, catalogue_list);
let mut guard = CATALOGUE_REGISTER.lock().unwrap();
for entry in catalogue_list {
let entry_type = match CatalogueEntryType::from_proto_value(entry.r#type) {
Some(entry_type) => entry_type,
None => {
warn!(
"Ignoring catalogue entry '{}' from plugin '{}': {} is not a catalogue entry type this driver understands",
entry.key, plugin.name, entry.r#type
);
continue;
}
};
let key = format!("plugin/{}/{}/{}", plugin.name, entry_type, entry.key);
guard.insert(key.clone(), CatalogueEntry {
entry_type,
provider_type: CatalogueEntryProviderType::PLUGIN,
plugin: Some(plugin.clone()),
key: entry.key.clone(),
values: entry.values.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
});
}
debug!("Updated catalogue entries:\n{}", guard.keys().sorted().join("\n"))
}
pub fn register_core_entries(entries: &Vec<CatalogueEntry>) {
trace!("register_core_entries({:?})", entries);
let mut inner = CATALOGUE_REGISTER.lock().unwrap();
let mut updated_keys = hashset!();
for entry in entries {
let key = format!("core/{}/{}", entry.entry_type, entry.key);
if !inner.contains_key(&key) {
inner.insert(key.clone(), entry.clone());
updated_keys.insert(key.clone());
}
}
if !updated_keys.is_empty() {
debug!("Updated catalogue entries:\n{}", updated_keys.iter().sorted().join("\n"));
}
}
pub fn lookup_entry(key: &str) -> Option<CatalogueEntry> {
let inner = CATALOGUE_REGISTER.lock().unwrap();
inner.iter()
.find(|(k, _)| names_catalogue_key(k, key))
.map(|(_, v)| v.clone())
}
#[derive(Debug, Clone)]
pub enum ResolvedCapability {
Core(String),
Plugin(Box<PactPluginManifest>)
}
fn names_catalogue_key(catalogue_key: &str, entry_key: &str) -> bool {
let key_parts = catalogue_key.split('/').collect::<Vec<_>>();
let query_parts = entry_key.split('/').collect::<Vec<_>>();
query_parts.len() <= key_parts.len()
&& key_parts[key_parts.len() - query_parts.len()..] == query_parts[..]
}
pub fn resolve_capability(entry_key: &str, expected_type: CatalogueEntryType) -> anyhow::Result<ResolvedCapability> {
let entry = resolve_capability_entry(entry_key, expected_type)?;
match entry.provider_type {
CatalogueEntryProviderType::CORE => Ok(ResolvedCapability::Core(entry.key.clone())),
CatalogueEntryProviderType::PLUGIN => entry.plugin.clone()
.map(|manifest| ResolvedCapability::Plugin(Box::new(manifest)))
.ok_or_else(|| anyhow::anyhow!("Catalogue entry '{}' has no plugin manifest", entry_key))
}
}
pub fn resolve_capability_entry(entry_key: &str, expected_type: CatalogueEntryType) -> anyhow::Result<CatalogueEntry> {
let candidates: Vec<(String, CatalogueEntry)> = {
let inner = CATALOGUE_REGISTER.lock().unwrap();
inner.iter()
.filter(|(k, _)| names_catalogue_key(k, entry_key))
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
};
let mut of_expected_type = candidates.iter().filter(|(_, entry)| entry.entry_type == expected_type);
let entry = match (of_expected_type.next(), of_expected_type.next()) {
(None, _) => return match candidates.first() {
Some((_, entry)) => Err(anyhow::anyhow!(
"Catalogue entry '{}' is a {:?}, not a {:?}", entry_key, entry.entry_type, expected_type
)),
None => Err(anyhow::anyhow!("No catalogue entry found for key '{}'", entry_key))
},
(Some(only), None) => &only.1,
(Some(first), Some(second)) => {
let mut keys: Vec<&str> = std::iter::once(first).chain(std::iter::once(second))
.chain(of_expected_type)
.map(|(k, _)| k.as_str())
.collect();
keys.sort_unstable();
return Err(anyhow::anyhow!(
"Ambiguous catalogue entry key '{}': matches multiple entries ({}) - register it under a more specific key",
entry_key, keys.join(", ")
));
}
};
Ok(entry.clone())
}
pub fn remove_plugin_entries(name: &str) {
trace!("remove_plugin_entries({})", name);
let prefix = format!("plugin/{}/", name);
let keys: Vec<String> = {
let guard = CATALOGUE_REGISTER.lock().unwrap();
guard.keys()
.filter(|key| key.starts_with(&prefix))
.cloned()
.collect()
};
let mut guard = CATALOGUE_REGISTER.lock().unwrap();
for key in keys {
guard.remove(&key);
}
debug!("Removed all catalogue entries for plugin {}", name);
}
#[instrument(level = "trace", skip(content_type))]
pub fn find_content_matcher<CT: Into<String>>(content_type: CT) -> Option<ContentMatcher> {
let content_type_str = content_type.into();
debug!("Looking for a content matcher for {}", content_type_str);
let content_type = match ContentType::parse(content_type_str.as_str()) {
Ok(ct) => ct,
Err(err) => {
error!("'{}' is not a valid content type", err);
return None;
}
};
let guard = CATALOGUE_REGISTER.lock().unwrap();
trace!("Catalogue has {} entries", guard.len());
guard.values().find(|entry| {
trace!("Catalogue entry {:?}", entry);
if entry.entry_type == CatalogueEntryType::CONTENT_MATCHER {
trace!("Catalogue entry is a content matcher for {:?}", entry.values.get("content-types"));
if let Some(content_types) = entry.values.get("content-types") {
content_types.split(";").any(|ct| matches_pattern(ct.trim(), &content_type))
} else {
false
}
} else {
false
}
}).map(|entry| ContentMatcher { catalogue_entry: entry.clone() })
}
fn matches_pattern(pattern: &str, content_type: &ContentType) -> bool {
let base_type = match &content_type.suffix {
Some(suffix) => format!("{}/{}+{}", content_type.main_type, content_type.sub_type, suffix),
None => format!("{}/{}", content_type.main_type, content_type.sub_type)
};
match Regex::new(&format!("^(?:{})$", pattern)) {
Ok(regex) => regex.is_match(base_type.as_str()),
Err(err) => {
error!("Failed to parse '{}' as a regex - {}", pattern, err);
false
}
}
}
pub fn find_content_generator(content_type: &ContentType) -> Option<ContentGenerator> {
debug!("Looking for a content generator for {}", content_type);
let guard = CATALOGUE_REGISTER.lock().unwrap();
guard.values().find(|entry| {
if entry.entry_type == CatalogueEntryType::CONTENT_GENERATOR {
if let Some(content_types) = entry.values.get("content-types") {
content_types.split(";").any(|ct| matches_pattern(ct.trim(), content_type))
} else {
false
}
} else {
false
}
}).map(|entry| ContentGenerator { catalogue_entry: entry.clone() })
}
pub fn all_entries() -> Vec<CatalogueEntry> {
let guard = CATALOGUE_REGISTER.lock().unwrap();
guard.values().cloned().collect()
}
pub fn core_entries() -> Vec<CatalogueEntry> {
let guard = CATALOGUE_REGISTER.lock().unwrap();
guard.values()
.filter(|entry| entry.provider_type == CatalogueEntryProviderType::CORE)
.cloned()
.collect()
}
#[cfg(test)]
mod tests {
use expectest::prelude::*;
use maplit::hashmap;
use crate::proto::catalogue_entry;
use super::*;
#[test]
fn sets_plugin_catalogue_entries_correctly() {
let manifest = PactPluginManifest {
name: "sets_plugin_catalogue_entries_correctly".to_string(),
.. PactPluginManifest::default()
};
let entries = vec![
ProtoCatalogueEntry {
r#type: catalogue_entry::EntryType::ContentMatcher as i32,
key: "protobuf".to_string(),
values: hashmap!{ "content-types".to_string() => "application/protobuf;application/grpc".to_string() }
},
ProtoCatalogueEntry {
r#type: catalogue_entry::EntryType::ContentGenerator as i32,
key: "protobuf".to_string(),
values: hashmap!{ "content-types".to_string() => "application/protobuf;application/grpc".to_string() }
},
ProtoCatalogueEntry {
r#type: catalogue_entry::EntryType::Transport as i32,
key: "grpc".to_string(),
values: hashmap!{}
}
];
register_plugin_entries(&manifest, &entries);
let matcher_entry = lookup_entry("content-matcher/protobuf");
let generator_entry = lookup_entry("content-generator/protobuf");
let transport_entry = lookup_entry("transport/grpc");
remove_plugin_entries("sets_plugin_catalogue_entries_correctly");
expect!(matcher_entry).to(be_some().value(CatalogueEntry {
entry_type: CatalogueEntryType::CONTENT_MATCHER,
provider_type: CatalogueEntryProviderType::PLUGIN,
plugin: Some(manifest.clone()),
key: "protobuf".to_string(),
values: hashmap!{ "content-types".to_string() => "application/protobuf;application/grpc".to_string() }
}));
expect!(generator_entry).to(be_some().value(CatalogueEntry {
entry_type: CatalogueEntryType::CONTENT_GENERATOR,
provider_type: CatalogueEntryProviderType::PLUGIN,
plugin: Some(manifest.clone()),
key: "protobuf".to_string(),
values: hashmap!{ "content-types".to_string() => "application/protobuf;application/grpc".to_string() }
}));
expect!(transport_entry).to(be_some().value(CatalogueEntry {
entry_type: CatalogueEntryType::TRANSPORT,
provider_type: CatalogueEntryProviderType::PLUGIN,
plugin: Some(manifest.clone()),
key: "grpc".to_string(),
values: hashmap!{}
}));
}
#[test]
fn entry_type_proto_values_and_names_round_trip() {
for entry_type in [
CatalogueEntryType::CONTENT_MATCHER,
CatalogueEntryType::CONTENT_GENERATOR,
CatalogueEntryType::TRANSPORT,
CatalogueEntryType::MATCHER,
CatalogueEntryType::INTERACTION,
CatalogueEntryType::GENERATOR
] {
expect!(CatalogueEntryType::from_proto_value(entry_type.to_proto_value()))
.to(be_some().value(entry_type));
expect!(CatalogueEntryType::from_proto_name(entry_type.as_proto_name()))
.to(be_some().value(entry_type));
expect!(CatalogueEntryType::from(entry_type.to_string().as_str())).to(be_equal_to(entry_type));
}
expect!(CatalogueEntryType::from_proto_value(99)).to(be_none());
expect!(CatalogueEntryType::from_proto_name("NOT_AN_ENTRY_TYPE")).to(be_none());
}
#[test]
fn entry_types_shared_with_v1_keep_their_v1_wire_values() {
expect!(CatalogueEntryType::CONTENT_MATCHER.to_proto_value())
.to(be_equal_to(EntryType::ContentMatcher as i32));
expect!(CatalogueEntryType::CONTENT_GENERATOR.to_proto_value())
.to(be_equal_to(EntryType::ContentGenerator as i32));
expect!(CatalogueEntryType::TRANSPORT.to_proto_value())
.to(be_equal_to(EntryType::Transport as i32));
expect!(CatalogueEntryType::MATCHER.to_proto_value())
.to(be_equal_to(EntryType::Matcher as i32));
expect!(CatalogueEntryType::INTERACTION.to_proto_value())
.to(be_equal_to(EntryType::Interaction as i32));
}
#[test]
fn registers_a_generator_entry_under_its_own_entry_type() {
let name = "registers_a_generator_entry_under_its_own_entry_type";
let manifest = PactPluginManifest { name: name.to_string(), .. PactPluginManifest::default() };
let entries = vec![
ProtoCatalogueEntry {
r#type: CatalogueEntryType::GENERATOR.to_proto_value(),
key: name.to_string(),
values: hashmap!{}
}
];
register_plugin_entries(&manifest, &entries);
let entry = lookup_entry(&format!("generator/{}", name));
let as_a_content_matcher = lookup_entry(&format!("content-matcher/{}", name));
remove_plugin_entries(name);
expect!(entry.map(|entry| entry.entry_type)).to(be_some().value(CatalogueEntryType::GENERATOR));
expect!(as_a_content_matcher).to(be_none());
}
#[test]
fn ignores_a_catalogue_entry_whose_type_this_driver_does_not_understand() {
let name = "ignores_a_catalogue_entry_whose_type_this_driver_does_not_understand";
let manifest = PactPluginManifest { name: name.to_string(), .. PactPluginManifest::default() };
let entries = vec![
ProtoCatalogueEntry { r#type: 99, key: name.to_string(), values: hashmap!{} }
];
register_plugin_entries(&manifest, &entries);
let registered = all_entries().into_iter().find(|entry| entry.key == name);
remove_plugin_entries(name);
expect!(registered).to(be_none());
}
#[test]
fn find_content_matcher_requires_the_whole_base_type_to_match() {
let manifest = PactPluginManifest {
name: "find_content_matcher_requires_the_whole_base_type_to_match".to_string(),
.. PactPluginManifest::default()
};
let entries = vec![
ProtoCatalogueEntry {
r#type: catalogue_entry::EntryType::ContentMatcher as i32,
key: "jwt".to_string(),
values: hashmap!{ "content-types".to_string() => "application/jwt;application/jwt\\+json".to_string() }
}
];
register_plugin_entries(&manifest, &entries);
let exact_match = find_content_matcher("application/jwt+json");
let with_params = find_content_matcher("application/jwt+json;charset=utf-8");
let longer_type = find_content_matcher("application/jwt+jsonextra");
let unrelated_type = find_content_matcher("application/json");
remove_plugin_entries("find_content_matcher_requires_the_whole_base_type_to_match");
expect!(exact_match).to(be_some());
expect!(with_params).to(be_some());
expect!(longer_type).to(be_none());
expect!(unrelated_type).to(be_none());
}
#[test]
fn resolve_capability_resolves_an_unambiguous_core_entry() {
let key = "resolve_capability_resolves_an_unambiguous_core_entry";
register_core_entries(&vec![CatalogueEntry {
entry_type: CatalogueEntryType::CONTENT_MATCHER,
provider_type: CatalogueEntryProviderType::CORE,
plugin: None,
key: key.to_string(),
values: hashmap!{}
}]);
let resolved = resolve_capability(key, CatalogueEntryType::CONTENT_MATCHER).unwrap();
let core_key = match resolved {
ResolvedCapability::Core(core_key) => core_key,
ResolvedCapability::Plugin(_) => panic!("expected a Core resolution, got Plugin")
};
expect!(core_key).to(be_equal_to(key.to_string()));
}
fn register_core_matcher_entries() {
let entries = [("equality", "V1"), ("regex", "V2"), ("type", "V2"), ("min-type", "V2"),
("max-type", "V2"), ("min-max-type", "V2"), ("include", "V3"), ("number", "V3"),
("integer", "V3"), ("decimal", "V3"), ("null", "V3"), ("date", "V3"), ("time", "V3"),
("datetime", "V3"), ("content-type", "V3"), ("values", "V3"), ("array-contains", "V4"),
("boolean", "V4"), ("status-code", "V4"), ("not-empty", "V4"), ("semver", "V4"),
("each-key", "V4"), ("each-value", "V4")]
.iter()
.map(|(key, version)| CatalogueEntry {
entry_type: CatalogueEntryType::MATCHER,
provider_type: CatalogueEntryProviderType::CORE,
plugin: None,
key: key.to_string(),
values: hashmap!{ "spec-version".to_string() => version.to_string() }
})
.collect();
register_core_entries(&entries);
}
fn resolved_core_key(entry_key: &str) -> String {
match resolve_capability(entry_key, CatalogueEntryType::MATCHER) {
Ok(ResolvedCapability::Core(key)) => key,
other => panic!("expected '{}' to resolve to a core entry, got {:?}", entry_key, other)
}
}
#[test]
fn resolve_capability_resolves_a_core_rule_by_the_name_it_is_registered_under() {
register_core_matcher_entries();
expect!(resolved_core_key("type")).to(be_equal_to("type".to_string()));
expect!(resolved_core_key("regex")).to(be_equal_to("regex".to_string()));
expect!(resolved_core_key("date")).to(be_equal_to("date".to_string()));
expect!(resolved_core_key("equality")).to(be_equal_to("equality".to_string()));
expect!(resolved_core_key("semver")).to(be_equal_to("semver".to_string()));
expect!(resolved_core_key("not-empty")).to(be_equal_to("not-empty".to_string()));
expect!(resolved_core_key("content-type")).to(be_equal_to("content-type".to_string()));
expect!(resolved_core_key("min-type")).to(be_equal_to("min-type".to_string()));
expect!(resolved_core_key("min-max-type")).to(be_equal_to("min-max-type".to_string()));
expect!(resolved_core_key("core/matcher/date")).to(be_equal_to("date".to_string()));
expect!(resolved_core_key("matcher/date")).to(be_equal_to("date".to_string()));
}
#[test]
fn resolve_capability_does_not_match_a_key_component_as_a_substring() {
expect!(names_catalogue_key("core/matcher/type", "type")).to(be_true());
expect!(names_catalogue_key("core/matcher/content-type", "type")).to(be_false());
expect!(names_catalogue_key("core/matcher/type", "matcher/type")).to(be_true());
expect!(names_catalogue_key("core/matcher/type", "core/matcher/type")).to(be_true());
expect!(names_catalogue_key("core/matcher/type", "r/type")).to(be_false());
expect!(names_catalogue_key("core/content-matcher/xml", "xml")).to(be_true());
expect!(names_catalogue_key("core/content-matcher/xml", "ml")).to(be_false());
}
#[test]
fn lookup_entry_matches_by_name_not_by_substring() {
let name = "lookup_entry_matches_by_name_not_by_substring";
let manifest = PactPluginManifest { name: name.to_string(), .. PactPluginManifest::default() };
register_plugin_entries(&manifest, &vec![
ProtoCatalogueEntry {
r#type: CatalogueEntryType::CONTENT_MATCHER.to_proto_value(),
key: name.to_string(),
values: hashmap!{}
},
ProtoCatalogueEntry {
r#type: CatalogueEntryType::MATCHER.to_proto_value(),
key: format!("other-{}", name),
values: hashmap!{}
}
]);
let by_name = lookup_entry(name).map(|entry| entry.entry_type);
let by_components = lookup_entry(&format!("content-matcher/{}", name)).map(|entry| entry.entry_type);
let fully_qualified = lookup_entry(&format!("plugin/{}/content-matcher/{}", name, name))
.map(|entry| entry.entry_type);
let by_substring = lookup_entry(&name[3..]);
remove_plugin_entries(name);
expect!(by_name).to(be_some().value(CatalogueEntryType::CONTENT_MATCHER));
expect!(by_components).to(be_some().value(CatalogueEntryType::CONTENT_MATCHER));
expect!(fully_qualified).to(be_some().value(CatalogueEntryType::CONTENT_MATCHER));
expect!(by_substring).to(be_none());
}
#[test]
fn lookup_entry_finds_a_core_rule_by_name() {
register_core_matcher_entries();
expect!(lookup_entry("type").map(|entry| entry.key)).to(be_some().value("type".to_string()));
expect!(lookup_entry("date").map(|entry| entry.key)).to(be_some().value("date".to_string()));
expect!(lookup_entry("matcher/date").map(|entry| entry.key)).to(be_some().value("date".to_string()));
expect!(lookup_entry("core/matcher/date").map(|entry| entry.key)).to(be_some().value("date".to_string()));
}
#[test]
fn resolve_capability_reports_a_plugin_rule_sharing_a_core_rule_name_as_ambiguous() {
let name = "resolve_capability_reports_a_plugin_rule_sharing_a_core_rule_name_as_ambiguous";
register_core_entries(&vec![CatalogueEntry {
entry_type: CatalogueEntryType::MATCHER,
provider_type: CatalogueEntryProviderType::CORE,
plugin: None,
key: name.to_string(),
values: hashmap!{}
}]);
let core_first = resolved_core_key(name);
let manifest = PactPluginManifest { name: name.to_string(), .. PactPluginManifest::default() };
register_plugin_entries(&manifest, &vec![ProtoCatalogueEntry {
r#type: CatalogueEntryType::MATCHER.to_proto_value(),
key: name.to_string(),
values: hashmap!{}
}]);
let ambiguous = resolve_capability(name, CatalogueEntryType::MATCHER);
let still_core = resolved_core_key(&format!("core/matcher/{}", name));
let plugin_rule = resolve_capability(
&format!("plugin/{}/matcher/{}", name, name), CatalogueEntryType::MATCHER
);
remove_plugin_entries(name);
expect!(core_first).to(be_equal_to(name.to_string()));
let error = ambiguous.expect_err("expected the shared name to be ambiguous").to_string();
expect!(error.contains("Ambiguous catalogue entry key")).to(be_true());
expect!(error.contains(&format!("core/matcher/{}", name))).to(be_true());
expect!(error.contains(&format!("plugin/{}/matcher/{}", name, name))).to(be_true());
expect!(still_core).to(be_equal_to(name.to_string()));
match plugin_rule.expect("expected the plugin's own entry to resolve when fully qualified") {
ResolvedCapability::Plugin(resolved_manifest) => expect!(resolved_manifest.name).to(be_equal_to(name.to_string())),
ResolvedCapability::Core(key) => panic!("expected the plugin entry, got core '{}'", key)
};
}
#[test]
fn resolve_capability_returns_a_clear_error_for_an_unregistered_key() {
let result = resolve_capability(
"resolve_capability_returns_a_clear_error_for_an_unregistered_key",
CatalogueEntryType::CONTENT_MATCHER
);
let err = result.expect_err("expected an error for an unregistered key");
expect!(err.to_string().contains("No catalogue entry found")).to(be_true());
}
#[test]
fn resolve_capability_returns_a_clear_error_for_the_wrong_capability_shape() {
let key = "resolve_capability_returns_a_clear_error_for_the_wrong_capability_shape";
register_core_entries(&vec![CatalogueEntry {
entry_type: CatalogueEntryType::CONTENT_GENERATOR,
provider_type: CatalogueEntryProviderType::CORE,
plugin: None,
key: key.to_string(),
values: hashmap!{}
}]);
let result = resolve_capability(key, CatalogueEntryType::CONTENT_MATCHER);
let err = result.expect_err("expected an error when the entry is a generator, not a matcher");
expect!(err.to_string().contains("is a CONTENT_GENERATOR, not a CONTENT_MATCHER")).to(be_true());
}
#[test]
fn resolve_capability_rejects_an_ambiguous_key_shared_by_a_core_and_a_plugin_entry() {
let key = "resolve_capability_rejects_an_ambiguous_key_shared_by_a_core_and_a_plugin_entry";
let manifest = PactPluginManifest {
name: "resolve_capability_rejects_an_ambiguous_key_shared_by_a_core_and_a_plugin_entry".to_string(),
.. PactPluginManifest::default()
};
register_core_entries(&vec![CatalogueEntry {
entry_type: CatalogueEntryType::CONTENT_MATCHER,
provider_type: CatalogueEntryProviderType::CORE,
plugin: None,
key: key.to_string(),
values: hashmap!{}
}]);
register_plugin_entries(&manifest, &vec![ProtoCatalogueEntry {
r#type: catalogue_entry::EntryType::ContentMatcher as i32,
key: key.to_string(),
values: hashmap!{}
}]);
let result = resolve_capability(key, CatalogueEntryType::CONTENT_MATCHER);
remove_plugin_entries(&manifest.name);
let err = result.expect_err("expected an error for a key matching more than one entry");
expect!(err.to_string().contains("Ambiguous catalogue entry key")).to(be_true());
}
}