1use std::collections::{BTreeMap, HashMap};
3use std::str::from_utf8;
4
5use anyhow::anyhow;
6use bytes::Bytes;
7use maplit::hashmap;
8use pact_models::bodies::OptionalBody;
9use pact_models::content_types::ContentTypeHint;
10use pact_models::matchingrules::{Category, MatchingRule, MatchingRuleCategory, RuleList};
11use pact_models::path_exp::DocPath;
12use pact_models::plugins::PluginData;
13use pact_models::prelude::{ContentType, Generator, GeneratorCategory, Generators, RuleLogic};
14use serde_json::Value;
15use tracing::{debug, error};
16
17use crate::catalogue_manager::{CatalogueEntry, CatalogueEntryProviderType};
18use crate::core_capabilities;
19use crate::plugin_manager::lookup_plugin;
20use crate::plugin_models::{PactPluginManifest, PluginInteractionConfig};
21use crate::proto::{
22 Body,
23 CompareContentsRequest,
24 ConfigureInteractionRequest,
25 ConfigureInteractionResponse,
26 GenerateContentRequest,
27 PluginConfiguration as ProtoPluginConfiguration
28};
29use crate::proto::body;
30use crate::proto::interaction_response::MarkupType;
31use crate::utils::{proto_struct_to_hashmap, proto_struct_to_json, proto_struct_to_map, to_proto_struct};
32
33#[derive(Clone, Debug)]
35pub struct ContentMatcher {
36 pub catalogue_entry: CatalogueEntry
38}
39
40impl ContentMatcher {
41 pub fn plugin(&self) -> Option<PactPluginManifest> {
43 self.catalogue_entry.plugin.clone()
44 }
45}
46
47#[derive(Clone, Debug)]
49pub struct ContentMismatch {
50 pub expected: String,
53 pub actual: String,
56 pub mismatch: String,
58 pub path: String,
60 pub diff: Option<String>,
62 pub mismatch_type: Option<String>
64}
65
66#[derive(Clone, Debug, PartialEq)]
68pub struct InteractionContents {
69 pub part_name: String,
72
73 pub body: OptionalBody,
75
76 pub rules: Option<MatchingRuleCategory>,
78
79 pub generators: Option<Generators>,
81
82 pub metadata: Option<BTreeMap<String, Value>>,
84
85 pub metadata_rules: Option<MatchingRuleCategory>,
87
88 pub plugin_config: PluginConfiguration,
90
91 pub interaction_markup: String,
93
94 pub interaction_markup_type: String
96}
97
98impl Default for InteractionContents {
99 fn default() -> Self {
100 InteractionContents {
101 part_name: Default::default(),
102 body: Default::default(),
103 rules: None,
104 generators: None,
105 metadata: None,
106 metadata_rules: None,
107 plugin_config: Default::default(),
108 interaction_markup: Default::default(),
109 interaction_markup_type: Default::default()
110 }
111 }
112}
113
114#[derive(Clone, Debug, PartialEq)]
116pub struct PluginConfiguration {
117 pub interaction_configuration: HashMap<String, Value>,
119 pub pact_configuration: HashMap<String, Value>
121}
122
123impl PluginConfiguration {
124 pub fn is_empty(&self) -> bool {
126 self.pact_configuration.is_empty() && self.interaction_configuration.is_empty()
127 }
128}
129
130impl Default for PluginConfiguration {
131 fn default() -> Self {
132 PluginConfiguration {
133 interaction_configuration: Default::default(),
134 pact_configuration: Default::default()
135 }
136 }
137}
138
139impl From<ProtoPluginConfiguration> for PluginConfiguration {
140 fn from(config: ProtoPluginConfiguration) -> Self {
141 PluginConfiguration {
142 interaction_configuration: config.interaction_configuration
143 .as_ref()
144 .map(|c| proto_struct_to_hashmap(c))
145 .unwrap_or_default(),
146 pact_configuration: config.pact_configuration
147 .as_ref()
148 .map(|c| proto_struct_to_hashmap(c))
149 .unwrap_or_default()
150 }
151 }
152}
153
154impl ContentMatcher {
155 pub fn is_core(&self) -> bool {
157 self.catalogue_entry.provider_type == CatalogueEntryProviderType::CORE
158 }
159
160 pub fn catalogue_entry_key(&self) -> String {
162 if self.is_core() {
163 format!("core/content-matcher/{}", self.catalogue_entry.key)
164 } else {
165 format!("plugin/{}/content-matcher/{}", self.plugin_name(), self.catalogue_entry.key)
166 }
167 }
168
169 pub fn plugin_name(&self) -> String {
171 self.catalogue_entry.plugin.as_ref()
172 .map(|p| p.name.clone())
173 .unwrap_or("core".to_string())
174 }
175
176 pub fn plugin_version(&self) -> String {
178 self.catalogue_entry.plugin.as_ref()
179 .map(|p| p.version.clone())
180 .unwrap_or_default()
181 }
182
183 #[deprecated(note = "Use the version that is spelled correctly")]
186 pub async fn configure_interation(
187 &self,
188 content_type: &ContentType,
189 definition: HashMap<String, Value>
190 ) -> anyhow::Result<(Vec<InteractionContents>, Option<PluginConfiguration>)> {
191 self.configure_interaction(content_type, definition).await
192 }
193
194 pub async fn configure_interaction(
197 &self,
198 content_type: &ContentType,
199 definition: HashMap<String, Value>
200 ) -> anyhow::Result<(Vec<InteractionContents>, Option<PluginConfiguration>)> {
201 debug!("Sending ConfigureContents request to plugin {:?}", self.catalogue_entry);
202 let request = ConfigureInteractionRequest {
203 content_type: content_type.to_string(),
204 contents_config: Some(to_proto_struct(&definition)),
205 };
206
207 let plugin_manifest = self.catalogue_entry.plugin.as_ref()
208 .expect("Plugin type is required");
209 match lookup_plugin(&plugin_manifest.as_dependency()) {
210 Some(plugin) => match plugin.configure_interaction(request).await {
211 Ok(response) => {
212 debug!("Got response: {:?}", response);
213 if response.error.is_empty() {
214 let results = Self::build_interaction_contents(&response)?;
215 Ok((results, response.plugin_configuration.map(|config| PluginConfiguration::from(config))))
216 } else {
217 Err(anyhow!("Request to configure interaction failed: {}", response.error))
218 }
219 }
220 Err(err) => {
221 error!("Call to plugin failed - {}", err);
222 Err(anyhow!("Call to plugin failed - {}", err))
223 }
224 },
225 None => {
226 error!("Plugin for {:?} was not found in the plugin register", self.catalogue_entry);
227 Err(anyhow!("Plugin for {:?} was not found in the plugin register", self.catalogue_entry))
228 }
229 }
230 }
231
232 pub(crate) fn build_interaction_contents(
233 response: &ConfigureInteractionResponse
234 ) -> anyhow::Result<Vec<InteractionContents>> {
235 let mut results = vec![];
236
237 for response in &response.interaction {
238 let body = match &response.contents {
239 Some(body) => {
240 let contents = body.content.as_ref().cloned().unwrap_or_default();
241 if contents.is_empty() {
242 OptionalBody::Empty
243 } else {
244 let returned_content_type = ContentType::parse(body.content_type.as_str()).ok();
245 OptionalBody::Present(Bytes::from(contents), returned_content_type,
246 Some(match body.content_type_hint() {
247 body::ContentTypeHint::Text => ContentTypeHint::TEXT,
248 body::ContentTypeHint::Binary => ContentTypeHint::BINARY,
249 body::ContentTypeHint::Default => ContentTypeHint::DEFAULT,
250 }))
251 }
252 },
253 None => OptionalBody::Missing
254 };
255
256 let rules = Self::setup_matching_rules(&response.rules)?;
257
258 let generators = if !response.generators.is_empty() || !response.metadata_generators.is_empty() {
259 let mut categories = hashmap! {};
260
261 if !response.generators.is_empty() {
262 let mut generators = hashmap! {};
263 for (k, g) in &response.generators {
264 generators.insert(DocPath::new(k)?,
265 Generator::create(g.r#type.as_str(),
266 &g.values.as_ref().map(|attr| proto_struct_to_json(attr)).unwrap_or_default())?);
267 }
268 categories.insert(GeneratorCategory::BODY, generators);
269 }
270
271 if !response.metadata_generators.is_empty() {
272 let mut generators = hashmap! {};
273 for (k, g) in &response.metadata_generators {
274 generators.insert(DocPath::new(k)?,
275 Generator::create(g.r#type.as_str(),
276 &g.values.as_ref().map(|attr| proto_struct_to_json(attr)).unwrap_or_default())?);
277 }
278 categories.insert(GeneratorCategory::METADATA, generators);
279 }
280
281 Some(Generators { categories })
282 } else {
283 None
284 };
285
286 let metadata = response.message_metadata.as_ref().map(|md| proto_struct_to_map(md));
287 let metadata_rules = Self::setup_matching_rules(&response.metadata_rules)?;
288
289 let plugin_config = if let Some(plugin_configuration) = &response.plugin_configuration {
290 PluginConfiguration {
291 interaction_configuration: plugin_configuration.interaction_configuration.as_ref()
292 .map(|val| proto_struct_to_hashmap(val)).unwrap_or_default(),
293 pact_configuration: plugin_configuration.pact_configuration.as_ref()
294 .map(|val| proto_struct_to_hashmap(val)).unwrap_or_default()
295 }
296 } else {
297 PluginConfiguration::default()
298 };
299
300 debug!("body={}", body);
301 debug!("rules={:?}", rules);
302 debug!("generators={:?}", generators);
303 debug!("metadata={:?}", metadata);
304 debug!("metadata_rules={:?}", metadata_rules);
305 debug!("pluginConfig={:?}", plugin_config);
306
307 results.push(InteractionContents {
308 part_name: response.part_name.clone(),
309 body,
310 rules,
311 generators,
312 metadata,
313 metadata_rules,
314 plugin_config,
315 interaction_markup: response.interaction_markup.clone(),
316 interaction_markup_type: match response.interaction_markup_type() {
317 MarkupType::Html => "HTML".to_string(),
318 _ => "COMMON_MARK".to_string(),
319 }
320 })
321 }
322
323 Ok(results)
324 }
325
326 fn setup_matching_rules(rules_map: &HashMap<String, crate::proto::MatchingRules>) -> anyhow::Result<Option<MatchingRuleCategory>> {
327 if !rules_map.is_empty() {
328 let mut rules = hashmap!{};
329 for (k, rule_list) in rules_map {
330 let mut vec = vec![];
331 for rule in &rule_list.rule {
332 let mr = MatchingRule::create(rule.r#type.as_str(), &rule.values.as_ref().map(|rule| {
333 proto_struct_to_json(rule)
334 }).unwrap_or_default())?;
335 vec.push(mr);
336 }
337 rules.insert(DocPath::new(k)?, RuleList {
338 rules: vec,
339 rule_logic: RuleLogic::And,
340 cascaded: false
341 });
342 }
343 Ok(Some(MatchingRuleCategory { name: Category::BODY, rules }))
344 } else {
345 Ok(None)
346 }
347 }
348
349 pub async fn match_contents(
355 &self,
356 expected: &OptionalBody,
357 actual: &OptionalBody,
358 context: &MatchingRuleCategory,
359 allow_unexpected_keys: bool,
360 plugin_config: Option<PluginInteractionConfig>
361 ) -> Result<(), HashMap<String, Vec<ContentMismatch>>> {
362 let request = CompareContentsRequest {
363 expected: Some(Body {
364 content_type: expected.content_type().unwrap_or_default().to_string(),
365 content: expected.value().map(|b| b.to_vec()),
366 content_type_hint: body::ContentTypeHint::Default as i32
367 }),
368 actual: Some(Body {
369 content_type: actual.content_type().unwrap_or_default().to_string(),
370 content: actual.value().map(|b| b.to_vec()),
371 content_type_hint: body::ContentTypeHint::Default as i32
372 }),
373 allow_unexpected_keys,
374 rules: context.rules.iter().map(|(k, r)| {
375 (k.to_string(), crate::proto::MatchingRules {
376 rule: r.rules.iter().map(|rule|{
377 crate::proto::MatchingRule {
378 r#type: rule.name(),
379 values: Some(to_proto_struct(&rule.values().iter().map(|(k, v)| (k.to_string(), v.clone())).collect())),
380 }
381 }).collect()
382 })
383 }).collect(),
384 plugin_configuration: plugin_config.map(|config| ProtoPluginConfiguration {
385 interaction_configuration: Some(to_proto_struct(&config.interaction_configuration)),
386 pact_configuration: Some(to_proto_struct(&config.pact_configuration))
387 })
388 };
389
390 if self.is_core() {
391 return match core_capabilities::lookup_core_content_matcher(&self.catalogue_entry.key) {
392 Some(handler) => Self::process_compare_contents_response(handler.compare_contents(request).await),
393 None => {
394 error!("No core content matcher registered for {:?}", self.catalogue_entry);
395 Err(hashmap! {
396 String::default() => vec![
397 ContentMismatch {
398 expected: "".to_string(),
399 actual: "".to_string(),
400 mismatch: format!("No core content matcher registered for {:?}", self.catalogue_entry),
401 path: "".to_string(),
402 diff: None,
403 mismatch_type: None
404 }
405 ]
406 })
407 }
408 };
409 }
410
411 let plugin_manifest = self.catalogue_entry.plugin.as_ref()
412 .expect("Plugin type is required");
413 match lookup_plugin(&plugin_manifest.as_dependency()) {
414 Some(plugin) => {
415 let chain_id = crate::call_chain::new_call_chain_id();
416 let deadline_ms = crate::call_chain::default_deadline_ms();
417 Self::process_compare_contents_response(
418 plugin.compare_contents_with_chain(request, &chain_id, deadline_ms).await
419 )
420 }
421 None => {
422 error!("Plugin for {:?} was not found in the plugin register", self.catalogue_entry);
423 Err(hashmap! {
424 String::default() => vec![
425 ContentMismatch {
426 expected: "".to_string(),
427 actual: "".to_string(),
428 mismatch: format!("Plugin for {:?} was not found in the plugin register", self.catalogue_entry),
429 path: "".to_string(),
430 diff: None,
431 mismatch_type: None
432 }
433 ]
434 })
435 }
436 }
437 }
438
439 fn process_compare_contents_response(
442 response: anyhow::Result<crate::proto::CompareContentsResponse>
443 ) -> Result<(), HashMap<String, Vec<ContentMismatch>>> {
444 match response {
445 Ok(response) => if let Some(mismatch) = response.type_mismatch {
446 Err(hashmap!{
447 String::default() => vec![
448 ContentMismatch {
449 expected: mismatch.expected.clone(),
450 actual: mismatch.actual.clone(),
451 mismatch: format!("Expected content type '{}' but got '{}'", mismatch.expected, mismatch.actual),
452 path: "".to_string(),
453 diff: None,
454 mismatch_type: None
455 }
456 ]
457 })
458 } else if !response.error.is_empty() {
459 Err(hashmap! {
460 String::default() => vec![
461 ContentMismatch {
462 expected: Default::default(),
463 actual: Default::default(),
464 mismatch: response.error.clone(),
465 path: "".to_string(),
466 diff: None,
467 mismatch_type: None
468 }
469 ]
470 })
471 } else if !response.results.is_empty() {
472 Err(response.results.iter().map(|(k, v)| {
473 (k.clone(), v.mismatches.iter().map(|mismatch| {
474 ContentMismatch {
475 expected: mismatch.expected.as_ref()
476 .map(|e| from_utf8(e).unwrap_or_default().to_string())
477 .unwrap_or_default(),
478 actual: mismatch.actual.as_ref()
479 .map(|a| from_utf8(a).unwrap_or_default().to_string())
480 .unwrap_or_default(),
481 mismatch: mismatch.mismatch.clone(),
482 path: mismatch.path.clone(),
483 diff: if mismatch.diff.is_empty() {
484 None
485 } else {
486 Some(mismatch.diff.clone())
487 },
488 mismatch_type: Some(mismatch.mismatch_type.clone())
489 }
490 }).collect())
491 }).collect())
492 } else {
493 Ok(())
494 }
495 Err(err) => {
496 error!("Call to compare contents handler failed - {}", err);
497 Err(hashmap! {
498 String::default() => vec![
499 ContentMismatch {
500 expected: "".to_string(),
501 actual: "".to_string(),
502 mismatch: format!("Call to compare contents handler failed = {}", err),
503 path: "".to_string(),
504 diff: None,
505 mismatch_type: None
506 }
507 ]
508 })
509 }
510 }
511 }
512}
513
514#[derive(Clone, Debug)]
516pub struct ContentGenerator {
517 pub catalogue_entry: CatalogueEntry
519}
520
521impl ContentGenerator {
522 pub fn is_core(&self) -> bool {
524 self.catalogue_entry.provider_type == CatalogueEntryProviderType::CORE
525 }
526
527 pub fn catalogue_entry_key(&self) -> String {
529 if self.is_core() {
530 format!("core/content-generator/{}", self.catalogue_entry.key)
531 } else {
532 format!("plugin/{}/content-generator/{}", self.plugin_name(), self.catalogue_entry.key)
533 }
534 }
535
536 pub fn plugin_name(&self) -> String {
538 self.catalogue_entry.plugin.as_ref()
539 .map(|p| p.name.clone())
540 .unwrap_or("core".to_string())
541 }
542
543 pub async fn generate_content(
545 &self,
546 content_type: &ContentType,
547 generators: &HashMap<String, Generator>,
548 body: &OptionalBody,
549 plugin_data: &Vec<PluginData>,
550 interaction_data: &HashMap<String, HashMap<String, Value>>,
551 context: &HashMap<&str, Value>
552 ) -> anyhow::Result<OptionalBody> {
553 let pact_plugin_manifest = self.catalogue_entry.plugin.clone().unwrap_or_default();
554 let plugin_data = plugin_data.iter().find_map(|pd| {
555 if pact_plugin_manifest.name == pd.name {
556 Some(pd.configuration.clone())
557 } else {
558 None
559 }
560 });
561 let interaction_data = interaction_data.get(&pact_plugin_manifest.name);
562
563 let request = GenerateContentRequest {
564 contents: Some(crate::proto::Body {
565 content_type: content_type.to_string(),
566 content: Some(body.value().unwrap_or_default().to_vec()),
567 content_type_hint: body::ContentTypeHint::Default as i32
568 }),
569 generators: generators.iter().map(|(k, v)| {
570 (k.clone(), crate::proto::Generator {
571 r#type: v.name(),
572 values: Some(to_proto_struct(&v.values().iter()
573 .map(|(k, v)| (k.to_string(), v.clone())).collect())),
574 })
575 }).collect(),
576 plugin_configuration: Some(ProtoPluginConfiguration {
577 pact_configuration: plugin_data.as_ref().map(to_proto_struct),
578 interaction_configuration: interaction_data.map(to_proto_struct),
579 .. ProtoPluginConfiguration::default()
580 }),
581 test_context: Some(to_proto_struct(&context.iter().map(|(k, v)| (k.to_string(), v.clone())).collect())),
582 .. GenerateContentRequest::default()
583 };
584
585 if self.is_core() {
586 let handler = core_capabilities::lookup_core_content_generator(&self.catalogue_entry.key)
587 .ok_or_else(|| anyhow!("No core content generator registered for {:?}", self.catalogue_entry))?;
588 debug!("Calling core content generator for {:?}", self.catalogue_entry);
589 return Self::response_to_body(handler.generate_content(request).await?.contents);
590 }
591
592 let plugin_manifest = self.catalogue_entry.plugin.as_ref()
593 .expect("Plugin type is required");
594 match lookup_plugin(&plugin_manifest.as_dependency()) {
595 Some(plugin) => {
596 debug!("Sending generateContent request to plugin {:?}", plugin_manifest);
597 let chain_id = crate::call_chain::new_call_chain_id();
598 let deadline_ms = crate::call_chain::default_deadline_ms();
599 Self::response_to_body(plugin.generate_content_with_chain(request, &chain_id, deadline_ms).await?.contents)
600 },
601 None => {
602 error!("Plugin for {:?} was not found in the plugin register", self.catalogue_entry);
603 Err(anyhow!("Plugin for {:?} was not found in the plugin register", self.catalogue_entry))
604 }
605 }
606 }
607
608 fn response_to_body(contents: Option<crate::proto::Body>) -> anyhow::Result<OptionalBody> {
611 match contents {
612 Some(contents) => Ok(OptionalBody::Present(
613 Bytes::from(contents.content.unwrap_or_default()),
614 ContentType::parse(contents.content_type.as_str()).ok(),
615 None
616 )),
617 None => Ok(OptionalBody::Empty)
618 }
619 }
620}
621
622#[cfg(test)]
623mod tests {
624 use std::collections::HashMap;
625 use std::sync::Arc;
626
627 use async_trait::async_trait;
628 use bytes::Bytes;
629 use maplit::btreemap;
630 use pact_models::bodies::OptionalBody;
631 use pact_models::content_types::{ContentType, ContentTypeHint};
632 use pact_models::matchingrules::{Category, MatchingRuleCategory};
633 use pretty_assertions::assert_eq;
634 use prost_types::value::Kind::StringValue;
635 use serde_json::Value;
636
637 use crate::catalogue_manager::{CatalogueEntry, CatalogueEntryProviderType, CatalogueEntryType};
638 use crate::core_capabilities::{self, CoreContentGenerator, CoreContentMatcher};
639 use crate::proto::{
640 Body, body, CompareContentsRequest, CompareContentsResponse, ConfigureInteractionResponse,
641 GenerateContentRequest, GenerateContentResponse, InteractionResponse
642 };
643
644 use super::{ContentGenerator, ContentMatcher, InteractionContents};
645
646 #[test_log::test]
648 fn build_interaction_contents_deals_with_empty_contents() {
649 let response = ConfigureInteractionResponse {
650 interaction: vec![
651 InteractionResponse {
652 contents: Some(Body {
653 content_type: "application/protobuf; message=.area_calculator.ShapeMessage".to_string(),
654 content: Some(b"\x12\n\r\0\0@@\x15\0\0\x80@".to_vec()),
655 content_type_hint: body::ContentTypeHint::Binary.into()
656 }),
657 message_metadata: Some(prost_types::Struct {
658 fields: btreemap!{
659 "contentType".to_string() => prost_types::Value {
660 kind: Some(StringValue("application/protobuf;message=.area_calculator.ShapeMessage".to_string()))
661 }
662 }
663 }),
664 part_name: "request".to_string(),
665 .. InteractionResponse::default()
666 },
667 InteractionResponse {
668 contents: Some(Body {
669 content_type: "application/protobuf; message=.area_calculator.AreaResponse".to_string(),
670 content: Some(vec![]),
671 content_type_hint: body::ContentTypeHint::Binary.into()
672 }),
673 message_metadata: Some(prost_types::Struct {
674 fields: btreemap!{
675 "grpc-message".to_string() => prost_types::Value {
676 kind: Some(StringValue("Not implemented".to_string()))
677 },
678 "grpc-status".to_string() => prost_types::Value {
679 kind: Some(StringValue("UNIMPLEMENTED".to_string()))
680 },
681 "contentType".to_string() => prost_types::Value {
682 kind: Some(StringValue("application/protobuf;message=.area_calculator.AreaResponse".to_string()))
683 }
684 }
685 }),
686 part_name: "response".to_string(),
687 .. InteractionResponse::default()
688 }
689 ],
690 .. ConfigureInteractionResponse::default()
691 };
692 let result = ContentMatcher::build_interaction_contents(&response).unwrap();
693
694 assert_eq!(result, vec![
695 InteractionContents {
696 part_name: "request".to_string(),
697 interaction_markup_type: "COMMON_MARK".to_string(),
698 body: OptionalBody::Present(Bytes::from(b"\x12\n\r\0\0@@\x15\0\0\x80@".to_vec()),
699 Some(ContentType::parse("application/protobuf;message=.area_calculator.ShapeMessage").unwrap()),
700 Some(ContentTypeHint::BINARY)),
701 metadata: Some(btreemap!{
702 "contentType".to_string() => Value::String("application/protobuf;message=.area_calculator.ShapeMessage".to_string())
703 }),
704 .. InteractionContents::default()
705 },
706
707 InteractionContents {
708 part_name: "response".to_string(),
709 interaction_markup_type: "COMMON_MARK".to_string(),
710 body: OptionalBody::Empty,
711 metadata: Some(btreemap!{
712 "grpc-status".to_string() => Value::String("UNIMPLEMENTED".to_string()),
713 "grpc-message".to_string() => Value::String("Not implemented".to_string()),
714 "contentType".to_string() => Value::String("application/protobuf;message=.area_calculator.AreaResponse".to_string())
715 }),
716 .. InteractionContents::default()
717 }
718 ]);
719 }
720
721 struct SuccessfulCoreMatcher;
722
723 #[async_trait]
724 impl CoreContentMatcher for SuccessfulCoreMatcher {
725 async fn compare_contents(&self, _request: CompareContentsRequest) -> anyhow::Result<CompareContentsResponse> {
726 Ok(CompareContentsResponse::default())
727 }
728 }
729
730 fn core_content_matcher(key: &str) -> ContentMatcher {
731 ContentMatcher {
732 catalogue_entry: CatalogueEntry {
733 entry_type: CatalogueEntryType::CONTENT_MATCHER,
734 provider_type: CatalogueEntryProviderType::CORE,
735 plugin: None,
736 key: key.to_string(),
737 values: Default::default()
738 }
739 }
740 }
741
742 #[test_log::test(tokio::test)]
743 async fn match_contents_calls_the_registered_core_handler_for_a_core_catalogue_entry() {
744 let key = "match_contents_calls_the_registered_core_handler_for_a_core_catalogue_entry";
745 core_capabilities::register_core_content_matcher(key, Arc::new(SuccessfulCoreMatcher));
746
747 let result = core_content_matcher(key).match_contents(
748 &OptionalBody::Present(Bytes::from("expected"), None, None),
749 &OptionalBody::Present(Bytes::from("actual"), None, None),
750 &MatchingRuleCategory { name: Category::BODY, rules: Default::default() },
751 true,
752 None
753 ).await;
754
755 core_capabilities::deregister_core_content_matcher(key);
756
757 assert!(result.is_ok());
758 }
759
760 #[test_log::test(tokio::test)]
761 async fn match_contents_returns_a_clear_error_when_no_core_handler_is_registered() {
762 let key = "match_contents_returns_a_clear_error_when_no_core_handler_is_registered";
763
764 let result = core_content_matcher(key).match_contents(
765 &OptionalBody::Present(Bytes::from("expected"), None, None),
766 &OptionalBody::Present(Bytes::from("actual"), None, None),
767 &MatchingRuleCategory { name: Category::BODY, rules: Default::default() },
768 true,
769 None
770 ).await;
771
772 let mismatches = result.expect_err("expected an error when no core handler is registered");
773 let messages: Vec<String> = mismatches.values().flatten().map(|m| m.mismatch.clone()).collect();
774 assert!(
775 messages.iter().any(|m| m.contains("No core content matcher registered")),
776 "expected a 'No core content matcher registered' mismatch, got: {:?}", messages
777 );
778 }
779
780 struct FixedCoreGenerator;
781
782 #[async_trait]
783 impl CoreContentGenerator for FixedCoreGenerator {
784 async fn generate_content(&self, _request: GenerateContentRequest) -> anyhow::Result<GenerateContentResponse> {
785 Ok(GenerateContentResponse {
786 contents: Some(Body {
787 content_type: "text/plain".to_string(),
788 content: Some(b"generated".to_vec()),
789 content_type_hint: body::ContentTypeHint::Default as i32
790 })
791 })
792 }
793 }
794
795 fn core_content_generator(key: &str) -> ContentGenerator {
796 ContentGenerator {
797 catalogue_entry: CatalogueEntry {
798 entry_type: CatalogueEntryType::CONTENT_GENERATOR,
799 provider_type: CatalogueEntryProviderType::CORE,
800 plugin: None,
801 key: key.to_string(),
802 values: Default::default()
803 }
804 }
805 }
806
807 #[test_log::test(tokio::test)]
808 async fn generate_content_calls_the_registered_core_handler_for_a_core_catalogue_entry() {
809 let key = "generate_content_calls_the_registered_core_handler_for_a_core_catalogue_entry";
810 core_capabilities::register_core_content_generator(key, Arc::new(FixedCoreGenerator));
811
812 let result = core_content_generator(key).generate_content(
813 &ContentType::parse("text/plain").unwrap(),
814 &HashMap::new(),
815 &OptionalBody::Empty,
816 &vec![],
817 &HashMap::new(),
818 &HashMap::new()
819 ).await;
820
821 core_capabilities::deregister_core_content_generator(key);
822
823 let body = result.expect("expected the core generator's content to be returned");
824 assert_eq!(body, OptionalBody::Present(Bytes::from("generated"), ContentType::parse("text/plain").ok(), None));
825 }
826
827 #[test_log::test(tokio::test)]
828 async fn generate_content_returns_a_clear_error_when_no_core_handler_is_registered() {
829 let key = "generate_content_returns_a_clear_error_when_no_core_handler_is_registered";
830
831 let result = core_content_generator(key).generate_content(
832 &ContentType::parse("text/plain").unwrap(),
833 &HashMap::new(),
834 &OptionalBody::Empty,
835 &vec![],
836 &HashMap::new(),
837 &HashMap::new()
838 ).await;
839
840 let err = result.expect_err("expected an error when no core handler is registered");
841 assert!(
842 err.to_string().contains("No core content generator registered"),
843 "expected a 'No core content generator registered' error, got: {}", err
844 );
845 }
846}