1use std::collections::HashMap;
4use std::ops::Not;
5use std::panic::RefUnwindSafe;
6use std::str::from_utf8;
7
8use anyhow::anyhow;
9use futures::stream::*;
10
11use itertools::Itertools;
12use maplit::hashmap;
13
14use pact_models::http_utils;
15use pact_models::http_utils::HttpAuth;
16use pact_models::json_utils::json_to_string;
17
18#[derive(Debug, Clone)]
19pub struct CustomHeaders {
20 pub headers: std::collections::HashMap<String, String>,
21}
22use pact_models::pact::{Pact, load_pact_from_json};
23use regex::{Captures, Regex};
24use reqwest::{Method, Url};
25use serde::{Deserialize, Serialize};
26use serde_json::{Value, json};
27use serde_with::skip_serializing_none;
28use tracing::{debug, error, info, trace, warn};
29pub mod branches;
30pub mod can_i_deploy;
31pub mod deployments;
32pub mod environments;
33pub mod pact_publish;
34pub mod pacticipants;
35pub mod pacts;
36pub mod provider_states;
37pub mod subcommands;
38pub mod tags;
39#[cfg(test)]
40pub mod test_utils;
41pub mod types;
42pub mod utils;
43pub mod verification;
44pub mod versions;
45pub mod webhooks;
46use utils::with_retries;
47use crate::cli::utils::{CYAN, GREEN, RED, YELLOW};
49use http::Extensions;
50use opentelemetry::Context;
51use opentelemetry::global;
52use opentelemetry_http::HeaderInjector;
53use reqwest::Request;
54use reqwest::Response;
55use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
56use reqwest_middleware::{Middleware, Next};
57use reqwest_tracing::TracingMiddleware;
58
59use crate::cli::pact_broker::main::types::SslOptions;
60
61pub fn process_notices(notices: &[Notice]) {
62 for notice in notices {
63 let notice_text = notice.text.to_string();
64 let formatted_text = notice_text
65 .split_whitespace()
66 .map(|word| {
67 if word.starts_with("https") || word.starts_with("http") {
68 format!("{}", CYAN.apply_to(word))
69 } else {
70 match notice.type_field.as_str() {
71 "success" => format!("{}", GREEN.apply_to(word)),
72 "warning" | "prompt" => format!("{}", YELLOW.apply_to(word)),
73 "error" | "danger" => format!("{}", RED.apply_to(word)),
74 _ => word.to_string(),
75 }
76 }
77 })
78 .collect::<Vec<String>>()
79 .join(" ");
80 println!("{}", formatted_text);
81 }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
85#[serde(rename_all = "camelCase")]
86pub struct Notice {
87 pub text: String,
88 #[serde(rename = "type")]
89 pub type_field: String,
90}
91
92fn is_true(object: &serde_json::Map<String, Value>, field: &str) -> bool {
93 match object.get(field) {
94 Some(json) => match *json {
95 serde_json::Value::Bool(b) => b,
96 _ => false,
97 },
98 None => false,
99 }
100}
101
102fn as_string(json: &Value) -> String {
103 match *json {
104 serde_json::Value::String(ref s) => s.clone(),
105 _ => format!("{}", json),
106 }
107}
108
109fn content_type(response: &reqwest::Response) -> String {
110 match response.headers().get("content-type") {
111 Some(value) => value.to_str().unwrap_or("text/plain").into(),
112 None => "text/plain".to_string(),
113 }
114}
115
116fn json_content_type(response: &reqwest::Response) -> bool {
117 match content_type(response).parse::<mime::Mime>() {
118 Ok(mime) => {
119 match (
120 mime.type_().as_str(),
121 mime.subtype().as_str(),
122 mime.suffix(),
123 ) {
124 ("application", "json", None) => true,
125 ("application", "hal", Some(mime::JSON)) => true,
126 _ => false,
127 }
128 }
129 Err(_) => false,
130 }
131}
132
133fn find_entry(map: &serde_json::Map<String, Value>, key: &str) -> Option<(String, Value)> {
134 match map.keys().find(|k| k.to_lowercase() == key.to_lowercase()) {
135 Some(k) => map.get(k).map(|v| (key.to_string(), v.clone())),
136 None => None,
137 }
138}
139
140#[derive(Debug, Clone, thiserror::Error)]
142pub enum PactBrokerError {
143 #[error("Error with a HAL link - {0}")]
145 LinkError(String),
146 #[error("Error with the content of a HAL resource - {0}")]
148 ContentError(String),
149 #[error("IO Error - {0}")]
150 IoError(String),
152 #[error("Link/Resource was not found - {0}")]
154 NotFound(String),
155 #[error("Invalid URL - {0}")]
157 UrlError(String),
158 #[error("failed validation - {0:?}")]
160 ValidationError(Vec<String>),
161 #[error("failed validation - {0:?}")]
163 ValidationErrorWithNotices(Vec<String>, Vec<Notice>),
164}
165
166impl PartialEq<String> for PactBrokerError {
167 fn eq(&self, other: &String) -> bool {
168 let mut buffer = String::new();
169 match self {
170 PactBrokerError::LinkError(s) => buffer.push_str(s),
171 PactBrokerError::ContentError(s) => buffer.push_str(s),
172 PactBrokerError::IoError(s) => buffer.push_str(s),
173 PactBrokerError::NotFound(s) => buffer.push_str(s),
174 PactBrokerError::UrlError(s) => buffer.push_str(s),
175 PactBrokerError::ValidationError(errors) => {
176 buffer.push_str(errors.iter().join(", ").as_str())
177 }
178 PactBrokerError::ValidationErrorWithNotices(errors, _) => {
179 buffer.push_str(errors.iter().join(", ").as_str())
180 }
181 };
182 buffer == *other
183 }
184}
185
186impl<'a> PartialEq<&'a str> for PactBrokerError {
187 fn eq(&self, other: &&str) -> bool {
188 let message = match self {
189 PactBrokerError::LinkError(s) => s.clone(),
190 PactBrokerError::ContentError(s) => s.clone(),
191 PactBrokerError::IoError(s) => s.clone(),
192 PactBrokerError::NotFound(s) => s.clone(),
193 PactBrokerError::UrlError(s) => s.clone(),
194 PactBrokerError::ValidationError(errors) => errors.iter().join(", "),
195 PactBrokerError::ValidationErrorWithNotices(errors, _) => errors.iter().join(", "),
196 };
197 message.as_str() == *other
198 }
199}
200
201impl From<url::ParseError> for PactBrokerError {
202 fn from(err: url::ParseError) -> Self {
203 PactBrokerError::UrlError(format!("{}", err))
204 }
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
208#[serde(default)]
209pub struct Link {
211 pub name: String,
213 pub href: Option<String>,
215 pub templated: bool,
217 pub title: Option<String>,
219}
220
221impl Link {
222 pub fn from_json(link: &str, link_data: &serde_json::Map<String, serde_json::Value>) -> Link {
224 Link {
225 name: link.to_string(),
226 href: find_entry(link_data, &"href".to_string()).map(|(_, href)| as_string(&href)),
227 templated: is_true(link_data, "templated"),
228 title: link_data.get("title").map(|title| as_string(title)),
229 }
230 }
231
232 pub fn as_json(&self) -> serde_json::Value {
234 match (self.href.clone(), self.title.clone()) {
235 (Some(href), Some(title)) => json!({
236 "href": href,
237 "title": title,
238 "templated": self.templated
239 }),
240 (Some(href), None) => json!({
241 "href": href,
242 "templated": self.templated
243 }),
244 (None, Some(title)) => json!({
245 "title": title,
246 "templated": self.templated
247 }),
248 (None, None) => json!({
249 "templated": self.templated
250 }),
251 }
252 }
253}
254
255impl Default for Link {
256 fn default() -> Self {
257 Link {
258 name: "link".to_string(),
259 href: None,
260 templated: false,
261 title: None,
262 }
263 }
264}
265
266#[derive(Clone)]
268pub struct HALClient {
269 pub url: String,
270 pub client: ClientWithMiddleware,
271 path_info: Option<Value>,
272 auth: Option<HttpAuth>,
273 custom_headers: Option<CustomHeaders>,
274 ssl_options: SslOptions,
275 pub retries: u8,
276}
277
278struct OtelPropagatorMiddleware;
279
280#[async_trait::async_trait]
281impl Middleware for OtelPropagatorMiddleware {
282 async fn handle(
283 &self,
284 mut req: Request,
285 extensions: &mut Extensions,
286 next: Next<'_>,
287 ) -> reqwest_middleware::Result<Response> {
288 let cx = Context::current();
289 let mut headers = reqwest::header::HeaderMap::new();
290 global::get_text_map_propagator(|propagator| {
291 propagator.inject_context(&cx, &mut HeaderInjector(&mut headers))
292 });
293 headers.append(
294 "baggage",
295 reqwest::header::HeaderValue::from_static("is_synthetic=true"),
296 );
297
298 for (key, value) in headers.iter() {
299 req.headers_mut().append(key, value.clone());
300 }
301 let res = next.run(req, extensions).await;
302 res
303 }
304}
305
306pub trait WithCurrentSpan {
307 fn with_current_span<F, R>(&self, f: F) -> R
308 where
309 F: FnOnce() -> R;
310}
311
312impl<T> WithCurrentSpan for T {
313 fn with_current_span<F, R>(&self, f: F) -> R
314 where
315 F: FnOnce() -> R,
316 {
317 let span = tracing::Span::current();
318 let _enter = span.enter();
319 f()
320 }
321}
322
323impl HALClient {
324 fn apply_custom_headers(
326 &self,
327 mut builder: reqwest_middleware::RequestBuilder,
328 ) -> reqwest_middleware::RequestBuilder {
329 if let Some(ref custom_headers) = self.custom_headers {
330 for (name, value) in &custom_headers.headers {
331 builder = builder.header(name, value);
332 }
333 }
334 builder
335 }
336
337 pub fn with_url(
339 url: &str,
340 auth: Option<HttpAuth>,
341 ssl_options: SslOptions,
342 custom_headers: Option<CustomHeaders>,
343 ) -> HALClient {
344 HALClient {
345 url: url.to_string(),
346 auth: auth.clone(),
347 custom_headers,
348 ssl_options: ssl_options.clone(),
349 ..HALClient::setup(url, auth, ssl_options)
350 }
351 }
352
353 fn update_path_info(self, path_info: serde_json::Value) -> HALClient {
354 HALClient {
355 client: self.client.clone(),
356 url: self.url.clone(),
357 path_info: Some(path_info),
358 auth: self.auth,
359 custom_headers: self.custom_headers,
360 retries: self.retries,
361 ssl_options: self.ssl_options,
362 }
363 }
364
365 pub async fn navigate(
367 self,
368 link: &'static str,
369 template_values: &HashMap<String, String>,
370 ) -> Result<HALClient, PactBrokerError> {
371 trace!(
372 "navigate(link='{}', template_values={:?})",
373 link, template_values
374 );
375
376 let client = if self.path_info.is_none() {
377 let path_info = self.fetch("").await?;
378 self.update_path_info(path_info)
379 } else {
380 self
381 };
382
383 let path_info = client.clone().fetch_link(link, template_values).await?;
384 let client = client.update_path_info(path_info);
385
386 Ok(client)
387 }
388
389 fn find_link(&self, link: &'static str) -> Result<Link, PactBrokerError> {
390 match self.path_info {
391 None => Err(PactBrokerError::LinkError(format!("No previous resource has been fetched from the pact broker. URL: '{}', LINK: '{}'",
392 self.url, link))),
393 Some(ref json) => match json.get("_links") {
394 Some(json) => match json.get(link) {
395 Some(link_data) => link_data.as_object()
396 .map(|link_data| Link::from_json(&link.to_string(), &link_data))
397 .ok_or_else(|| PactBrokerError::LinkError(format!("Link is malformed, expected an object but got {}. URL: '{}', LINK: '{}'",
398 link_data, self.url, link))),
399 None => Err(PactBrokerError::LinkError(format!("Link '{}' was not found in the response, only the following links where found: {:?}. URL: '{}', LINK: '{}'",
400 link, json.as_object().unwrap_or(&json!({}).as_object().unwrap()).keys().join(", "), self.url, link)))
401 },
402 None => Err(PactBrokerError::LinkError(format!("Expected a HAL+JSON response from the pact broker, but got a response with no '_links'. URL: '{}', LINK: '{}'",
403 self.url, link)))
404 }
405 }
406 }
407
408 async fn fetch_link(
409 self,
410 link: &'static str,
411 template_values: &HashMap<String, String>,
412 ) -> Result<Value, PactBrokerError> {
413 trace!(
414 "fetch_link(link='{}', template_values={:?})",
415 link, template_values
416 );
417
418 let link_data = self.find_link(link)?;
419
420 self.fetch_url(&link_data, template_values).await
421 }
422
423 pub async fn fetch_url(
425 self,
426 link: &Link,
427 template_values: &HashMap<String, String>,
428 ) -> Result<Value, PactBrokerError> {
429 debug!(
430 "fetch_url(link={:?}, template_values={:?})",
431 link, template_values
432 );
433
434 let link_url = if link.templated {
435 debug!("Link URL is templated");
436 self.parse_link_url(&link, &template_values)
437 } else {
438 link.href.clone().ok_or_else(|| {
439 PactBrokerError::LinkError(format!(
440 "Link is malformed, there is no href. URL: '{}', LINK: '{}'",
441 self.url, link.name
442 ))
443 })
444 }?;
445
446 let base_url = self.url.parse::<Url>()?;
447 let joined_url = base_url.join(&link_url)?;
448 self.fetch(joined_url.path().into()).await
449 }
450 pub async fn delete_url(
451 self,
452 link: &Link,
453 template_values: &HashMap<String, String>,
454 ) -> Result<Value, PactBrokerError> {
455 debug!(
456 "fetch_url(link={:?}, template_values={:?})",
457 link, template_values
458 );
459
460 let link_url = if link.templated {
461 debug!("Link URL is templated");
462 self.parse_link_url(&link, &template_values)
463 } else {
464 link.href.clone().ok_or_else(|| {
465 PactBrokerError::LinkError(format!(
466 "Link is malformed, there is no href. URL: '{}', LINK: '{}'",
467 self.url, link.name
468 ))
469 })
470 }?;
471
472 let base_url = self.url.parse::<Url>()?;
473 debug!("base_url: {}", base_url);
474 debug!("link_url: {}", link_url);
475 let joined_url = base_url.join(&link_url)?;
476 debug!("joined_url: {}", joined_url);
477 self.delete(joined_url.path().into()).await
478 }
479
480 pub async fn fetch(&self, path: &str) -> Result<Value, PactBrokerError> {
481 info!("Fetching path '{}' from pact broker", path);
482 trace!(%path, broker_url = %self.url, ">> fetch");
483 let url = self.resolve_path(path)?;
484 debug!("Final broker URL: {}", url);
485
486 let mut request_builder = match self.auth {
487 Some(ref auth) => match auth {
488 HttpAuth::User(username, password) => {
489 self.client.get(url).basic_auth(username, password.clone())
490 }
491 HttpAuth::Token(token) => self.client.get(url).bearer_auth(token),
492 _ => self.client.get(url),
493 },
494 None => self.client.get(url),
495 }
496 .header("accept", "application/hal+json, application/json");
497
498 request_builder = self.apply_custom_headers(request_builder);
500
501 let response = utils::with_retries(self.retries, request_builder)
502 .await
503 .map_err(|err| {
504 PactBrokerError::IoError(format!(
505 "Failed to access pact broker path '{}' - {}. URL: '{}'",
506 &path, err, &self.url,
507 ))
508 })?;
509
510 self.parse_broker_response(path.to_string(), response).await
511 }
512
513 fn resolve_path(&self, path: &str) -> Result<Url, PactBrokerError> {
514 let broker_url = self.url.parse::<Url>()?;
515 let context_path = broker_url.path();
516 let url = if path.is_empty() {
517 broker_url
518 } else if !context_path.is_empty() && context_path != "/" {
519 if path.starts_with(context_path) {
520 let mut base_url = broker_url.clone();
521 base_url.set_path("/");
522 base_url.join(path)?
523 } else if path.starts_with("/") {
524 let mut base_url = broker_url.clone();
525 base_url.set_path(path);
526 base_url
527 } else {
528 let mut base_url = broker_url.clone();
529 let mut cp = context_path.to_string();
530 cp.push('/');
531 base_url.set_path(cp.as_str());
532 base_url.join(path)?
533 }
534 } else {
535 broker_url.join(path)?
536 };
537 Ok(url)
538 }
539
540 pub async fn delete(self, path: &str) -> Result<Value, PactBrokerError> {
541 info!("Deleting path '{}' from pact broker", path);
542
543 let broker_url = self.url.parse::<Url>()?;
544 let context_path = broker_url.path();
545 let url = if context_path.is_empty().not()
546 && context_path != "/"
547 && path.starts_with(context_path)
548 {
549 let mut base_url = broker_url.clone();
550 base_url.set_path("/");
551 base_url.join(path)?
552 } else {
553 broker_url.join(path)?
554 };
555
556 let request_builder = match self.auth {
557 Some(ref auth) => match auth {
558 HttpAuth::User(username, password) => self
559 .client
560 .delete(url)
561 .basic_auth(username, password.clone()),
562 HttpAuth::Token(token) => self.client.delete(url).bearer_auth(token),
563 _ => self.client.delete(url),
564 },
565 None => self.client.delete(url),
566 }
567 .header("Accept", "application/hal+json");
568
569 let response = utils::with_retries(self.retries, request_builder)
570 .await
571 .map_err(|err| {
572 PactBrokerError::IoError(format!(
573 "Failed to delete pact broker path '{}' - {}. URL: '{}'",
574 &path, err, &self.url,
575 ))
576 })?;
577
578 self.parse_broker_response(path.to_string(), response).await
579 }
580
581 async fn parse_broker_response(
582 &self,
583 path: String,
584 response: reqwest::Response,
585 ) -> Result<Value, PactBrokerError> {
586 let is_json_content_type = json_content_type(&response);
587 let content_type = content_type(&response);
588 let status_code = response.status();
589
590 if status_code.is_success() {
591 if is_json_content_type {
592 response.json::<Value>()
593 .await
594 .map_err(|err| PactBrokerError::ContentError(
595 format!("Did not get a valid HAL response body from pact broker path '{}' - {}. URL: '{}'",
596 path, err, self.url)
597 ))
598 } else if status_code.as_u16() == 204 {
599 Ok(json!({}))
600 } else {
601 debug!("Request from broker was a success, but the response body was not JSON");
602 Err(PactBrokerError::ContentError(format!(
603 "Did not get a valid HAL response body from pact broker path '{}', content type is '{}'. URL: '{}'",
604 path, content_type, self.url
605 )))
606 }
607 } else if status_code.as_u16() == 404 {
608 Err(PactBrokerError::NotFound(format!(
609 "Request to pact broker path '{}' failed: {}. URL: '{}'",
610 path, status_code, self.url
611 )))
612 } else {
613 let body = response.bytes().await.map_err(|_| {
615 PactBrokerError::IoError(format!(
616 "Failed to download response body for path '{}'. URL: '{}'",
617 &path, self.url
618 ))
619 })?;
620
621 if is_json_content_type {
622 match serde_json::from_slice::<Value>(&body) {
623 Ok(json_body) => {
624 if json_body.get("errors").is_some() || json_body.get("notices").is_some() {
625 Err(handle_validation_errors(json_body))
626 } else {
627 Err(PactBrokerError::IoError(format!(
628 "Request to pact broker path '{}' failed: {}. Response: {}. URL: '{}'",
629 path, status_code, json_body, self.url
630 )))
631 }
632 }
633 Err(_) => {
634 let body_text = from_utf8(&body)
635 .map(|b| b.to_string())
636 .unwrap_or_else(|err| format!("could not read body: {}", err));
637 error!(
638 "Request to pact broker path '{}' failed: {}",
639 path, body_text
640 );
641 Err(PactBrokerError::IoError(format!(
642 "Request to pact broker path '{}' failed: {}. URL: '{}'",
643 path, status_code, self.url
644 )))
645 }
646 }
647 } else {
648 let body_text = from_utf8(&body)
649 .map(|b| b.to_string())
650 .unwrap_or_else(|err| format!("could not read body: {}", err));
651 error!(
652 "Request to pact broker path '{}' failed: {}",
653 path, body_text
654 );
655 Err(PactBrokerError::IoError(format!(
656 "Request to pact broker path '{}' failed: {}. URL: '{}'",
657 path, status_code, self.url
658 )))
659 }
660 }
661 }
662
663 fn parse_link_url(
664 &self,
665 link: &Link,
666 values: &HashMap<String, String>,
667 ) -> Result<String, PactBrokerError> {
668 match link.href {
669 Some(ref href) => {
670 debug!("templated URL = {}", href);
671 let re = Regex::new(r"\{(\w+)}").unwrap();
672 let final_url = re.replace_all(href, |caps: &Captures| {
673 let lookup = caps.get(1).unwrap().as_str();
674 trace!("Looking up value for key '{}'", lookup);
675 match values.get(lookup) {
676 Some(val) => urlencoding::encode(val.as_str()).to_string(),
677 None => {
678 warn!(
679 "No value was found for key '{}', mapped values are {:?}",
680 lookup, values
681 );
682 format!("{{{}}}", lookup)
683 }
684 }
685 });
686 debug!("final URL = {}", final_url);
687 Ok(final_url.to_string())
688 }
689 None => Err(PactBrokerError::LinkError(format!(
690 "Expected a HAL+JSON response from the pact broker, but got a link with no HREF. URL: '{}', LINK: '{}'",
691 self.url, link.name
692 ))),
693 }
694 }
695
696 pub fn iter_links(&self, link: &str) -> Result<Vec<Link>, PactBrokerError> {
698 match self.path_info {
699 None => Err(PactBrokerError::LinkError(format!("No previous resource has been fetched from the pact broker. URL: '{}', LINK: '{}'",
700 self.url, link))),
701 Some(ref json) => match json.get("_links") {
702 Some(json) => match json.get(&link) {
703 Some(link_data) => link_data.as_array()
704 .map(|link_data| link_data.iter().map(|link_json| match link_json {
705 Value::Object(data) => Link::from_json(&link, data),
706 Value::String(s) => Link { name: link.to_string(), href: Some(s.clone()), templated: false, title: None },
707 _ => Link { name: link.to_string(), href: Some(link_json.to_string()), templated: false, title: None }
708 }).collect())
709 .ok_or_else(|| PactBrokerError::LinkError(format!("Link is malformed, expected an object but got {}. URL: '{}', LINK: '{}'",
710 link_data, self.url, link))),
711 None => Err(PactBrokerError::LinkError(format!("Link '{}' was not found in the response, only the following links where found: {:?}. URL: '{}', LINK: '{}'",
712 link, json.as_object().unwrap_or(&json!({}).as_object().unwrap()).keys().join(", "), self.url, link)))
713 },
714 None => Err(PactBrokerError::LinkError(format!("Expected a HAL+JSON response from the pact broker, but got a response with no '_links'. URL: '{}', LINK: '{}'",
715 self.url, link)))
716 }
717 }
718 }
719
720 pub async fn post_json(
721 &self,
722 url: &str,
723 body: &str,
724 headers: Option<HashMap<String, String>>,
725 ) -> Result<serde_json::Value, PactBrokerError> {
726 trace!("post_json(url='{}', body='{}')", url, body);
727
728 self.send_document(url, body, Method::POST, headers).await
729 }
730
731 pub async fn put_json(
732 &self,
733 url: &str,
734 body: &str,
735 headers: Option<HashMap<String, String>>,
736 ) -> Result<serde_json::Value, PactBrokerError> {
737 trace!("put_json(url='{}', body='{}')", url, body);
738
739 self.send_document(url, body, Method::PUT, headers).await
740 }
741 pub async fn patch_json(
742 &self,
743 url: &str,
744 body: &str,
745 headers: Option<HashMap<String, String>>,
746 ) -> Result<serde_json::Value, PactBrokerError> {
747 trace!("put_json(url='{}', body='{}')", url, body);
748
749 self.send_document(url, body, Method::PATCH, headers).await
750 }
751
752 async fn send_document(
753 &self,
754 url: &str,
755 body: &str,
756 method: Method,
757 headers: Option<HashMap<String, String>>,
758 ) -> Result<Value, PactBrokerError> {
759 let method_type = method.clone();
760 debug!("Sending JSON to {} using {}: {}", url, method, body);
761
762 let base_url = &self.url.parse::<Url>()?;
763 let url = if url.starts_with("/") {
764 base_url.join(url)?
765 } else {
766 let url = url.parse::<Url>()?;
767 base_url.join(&url.path())?
768 };
769
770 let request_builder = match self.auth {
771 Some(ref auth) => match auth {
772 HttpAuth::User(username, password) => self
773 .client
774 .request(method, url.clone())
775 .basic_auth(username, password.clone()),
776 HttpAuth::Token(token) => {
777 self.client.request(method, url.clone()).bearer_auth(token)
778 }
779 _ => self.client.request(method, url.clone()),
780 },
781 None => self.client.request(method, url.clone()),
782 }
783 .header("Accept", "application/hal+json")
784 .body(body.to_string());
785
786 let request_builder = if let Some(ref headers) = headers {
789 headers
790 .iter()
791 .fold(request_builder, |builder, (key, value)| {
792 builder.header(key.as_str(), value.as_str())
793 })
794 } else {
795 request_builder
796 };
797
798 let request_builder = if method_type == Method::PATCH {
799 request_builder.header("Content-Type", "application/merge-patch+json")
800 } else {
801 request_builder.header("Content-Type", "application/json")
802 };
803 let response = with_retries(self.retries, request_builder).await;
804 match response {
805 Ok(res) => {
806 self.parse_broker_response(url.path().to_string(), res)
807 .await
808 }
809 Err(err) => Err(PactBrokerError::IoError(format!(
810 "Failed to send JSON to the pact broker URL '{}' - IoError {}",
811 url, err
812 ))),
813 }
814 }
815
816 fn with_doc_context(self, doc_attributes: &[Link]) -> Result<HALClient, PactBrokerError> {
817 let links: serde_json::Map<String, serde_json::Value> = doc_attributes
818 .iter()
819 .map(|link| (link.name.clone(), link.as_json()))
820 .collect();
821 let links_json = json!({
822 "_links": json!(links)
823 });
824 Ok(self.update_path_info(links_json))
825 }
826}
827
828fn handle_validation_errors(body: Value) -> PactBrokerError {
829 match &body {
830 Value::Object(attrs) => {
831 let notices: Vec<Notice> = attrs
833 .get("notices")
834 .and_then(|n| n.as_array())
835 .map(|notices_array| {
836 notices_array
837 .iter()
838 .filter_map(|notice| serde_json::from_value::<Notice>(notice.clone()).ok())
839 .collect()
840 })
841 .unwrap_or_default();
842
843 if let Some(errors) = attrs.get("errors") {
844 let error_messages = match errors {
845 Value::Array(values) => values.iter().map(|v| json_to_string(v)).collect(),
846 Value::Object(errors) => errors
847 .iter()
848 .map(|(field, errors)| match errors {
849 Value::String(error) => format!("{}: {}", field, error),
850 Value::Array(errors) => format!(
851 "{}: {}",
852 field,
853 errors.iter().map(|err| json_to_string(err)).join(", ")
854 ),
855 _ => format!("{}: {}", field, errors),
856 })
857 .collect(),
858 Value::String(s) => vec![s.clone()],
859 _ => vec![errors.to_string()],
860 };
861
862 if !notices.is_empty() {
863 PactBrokerError::ValidationErrorWithNotices(error_messages, notices)
864 } else {
865 PactBrokerError::ValidationError(error_messages)
866 }
867 } else if !notices.is_empty() {
868 let notice_messages = notices.iter().map(|n| n.text.clone()).collect();
870 PactBrokerError::ValidationErrorWithNotices(notice_messages, notices)
871 } else {
872 PactBrokerError::ValidationError(vec![body.to_string()])
873 }
874 }
875 Value::String(s) => PactBrokerError::ValidationError(vec![s.clone()]),
876 _ => PactBrokerError::ValidationError(vec![body.to_string()]),
877 }
878}
879
880impl HALClient {
881 pub fn setup(url: &str, auth: Option<HttpAuth>, ssl_options: SslOptions) -> HALClient {
882 let mut builder = reqwest::Client::builder().user_agent(format!(
883 "{}/{}",
884 env!("CARGO_PKG_NAME"),
885 env!("CARGO_PKG_VERSION")
886 ));
887
888 debug!("Using ssl_options: {:?}", ssl_options);
889 if let Some(ref path) = ssl_options.ssl_cert_path {
890 if let Ok(cert_bytes) = std::fs::read(path) {
891 if let Ok(cert) = reqwest::Certificate::from_pem_bundle(&cert_bytes) {
892 debug!("Adding SSL certificate from path: {}", path);
893 for c in cert {
894 builder = builder.add_root_certificate(c.clone());
895 }
896 }
897 } else {
898 debug!(
899 "Could not read SSL certificate from provided path: {}",
900 path
901 );
902 }
903 }
904 if ssl_options.skip_ssl {
905 builder = builder.danger_accept_invalid_certs(true);
906 debug!("Skipping SSL certificate validation");
907 }
908 if !ssl_options.use_root_trust_store {
909 builder = builder.tls_built_in_root_certs(false);
910 debug!("Disabling root trust store for SSL");
911 }
912
913 let built_client = builder.build().unwrap();
914 let client = ClientBuilder::new(built_client)
915 .with(TracingMiddleware::default())
916 .with(OtelPropagatorMiddleware)
917 .build();
918
919 HALClient {
920 client,
921 url: url.to_string(),
922 path_info: None,
923 auth,
924 custom_headers: None,
925 retries: 3,
926 ssl_options,
927 }
928 }
929}
930
931pub fn links_from_json(json: &Value) -> Vec<Link> {
932 match json.get("_links") {
933 Some(json) => match json {
934 Value::Object(v) => v
935 .iter()
936 .map(|(link, json)| match json {
937 Value::Object(attr) => Link::from_json(link, attr),
938 _ => Link {
939 name: link.clone(),
940 ..Link::default()
941 },
942 })
943 .collect(),
944 _ => vec![],
945 },
946 None => vec![],
947 }
948}
949
950pub async fn fetch_pacts_from_broker(
952 broker_url: &str,
953 provider_name: &str,
954 auth: Option<HttpAuth>,
955 ssl_options: SslOptions,
956 custom_headers: Option<CustomHeaders>,
957) -> anyhow::Result<
958 Vec<
959 anyhow::Result<(
960 Box<dyn Pact + Send + Sync + RefUnwindSafe>,
961 Option<PactVerificationContext>,
962 Vec<Link>,
963 )>,
964 >,
965> {
966 trace!(
967 "fetch_pacts_from_broker(broker_url='{}', provider_name='{}', auth={})",
968 broker_url,
969 provider_name,
970 auth.clone().unwrap_or_default()
971 );
972
973 let mut hal_client = HALClient::with_url(broker_url, auth, ssl_options, custom_headers);
974 let template_values = hashmap! { "provider".to_string() => provider_name.to_string() };
975
976 hal_client = hal_client
977 .navigate("pb:latest-provider-pacts", &template_values)
978 .await
979 .map_err(move |err| match err {
980 PactBrokerError::NotFound(_) => PactBrokerError::NotFound(format!(
981 "No pacts for provider '{}' where found in the pact broker. URL: '{}'",
982 provider_name, broker_url
983 )),
984 _ => err,
985 })?;
986
987 let pact_links = hal_client.clone().iter_links("pacts")?;
988
989 let results: Vec<_> = futures::stream::iter(pact_links)
990 .map(|ref pact_link| {
991 match pact_link.href {
992 Some(_) => Ok((hal_client.clone(), pact_link.clone())),
993 None => Err(
994 PactBrokerError::LinkError(
995 format!(
996 "Expected a HAL+JSON response from the pact broker, but got a link with no HREF. URL: '{}', LINK: '{:?}'",
997 &hal_client.url,
998 pact_link
999 )
1000 )
1001 )
1002 }
1003 })
1004 .and_then(|(hal_client, pact_link)| async {
1005 let pact_json = hal_client.fetch_url(
1006 &pact_link.clone(),
1007 &template_values.clone()
1008 ).await?;
1009 Ok((pact_link, pact_json))
1010 })
1011 .map(|result| {
1012 match result {
1013 Ok((pact_link, pact_json)) => {
1014 let href = pact_link.href.unwrap_or_default();
1015 let links = links_from_json(&pact_json);
1016 load_pact_from_json(href.as_str(), &pact_json)
1017 .map(|pact| (pact, None, links))
1018 },
1019 Err(err) => Err(err.into())
1020 }
1021 })
1022 .into_stream()
1023 .collect()
1024 .await;
1025
1026 Ok(results)
1027}
1028
1029pub async fn fetch_pacts_dynamically_from_broker(
1031 broker_url: &str,
1032 provider_name: String,
1033 pending: bool,
1034 include_wip_pacts_since: Option<String>,
1035 provider_tags: Vec<String>,
1036 provider_branch: Option<String>,
1037 consumer_version_selectors: Vec<ConsumerVersionSelector>,
1038 auth: Option<HttpAuth>,
1039 ssl_options: SslOptions,
1040 headers: Option<HashMap<String, String>>,
1041 custom_headers: Option<CustomHeaders>,
1042) -> anyhow::Result<
1043 Vec<
1044 Result<
1045 (
1046 Box<dyn Pact + Send + Sync + RefUnwindSafe>,
1047 Option<PactVerificationContext>,
1048 Vec<Link>,
1049 ),
1050 PactBrokerError,
1051 >,
1052 >,
1053> {
1054 trace!(
1055 "fetch_pacts_dynamically_from_broker(broker_url='{}', provider_name='{}', pending={}, \
1056 include_wip_pacts_since={:?}, provider_tags: {:?}, consumer_version_selectors: {:?}, auth={})",
1057 broker_url,
1058 provider_name,
1059 pending,
1060 include_wip_pacts_since,
1061 provider_tags,
1062 consumer_version_selectors,
1063 auth.clone().unwrap_or_default()
1064 );
1065
1066 let mut hal_client = HALClient::with_url(broker_url, auth, ssl_options, custom_headers);
1067 let template_values = hashmap! { "provider".to_string() => provider_name.clone() };
1068
1069 hal_client = hal_client
1070 .navigate("pb:provider-pacts-for-verification", &template_values)
1071 .await
1072 .map_err(move |err| match err {
1073 PactBrokerError::NotFound(_) => PactBrokerError::NotFound(format!(
1074 "No pacts for provider '{}' were found in the pact broker. URL: '{}'",
1075 provider_name.clone(),
1076 broker_url
1077 )),
1078 _ => err,
1079 })?;
1080
1081 let pacts_for_verification = PactsForVerificationRequest {
1083 provider_version_tags: provider_tags,
1084 provider_version_branch: provider_branch,
1085 include_wip_pacts_since,
1086 consumer_version_selectors,
1087 include_pending_status: pending,
1088 };
1089 let request_body = serde_json::to_string(&pacts_for_verification).unwrap();
1090
1091 let response = match hal_client.find_link("self") {
1093 Ok(link) => {
1094 let link = hal_client.clone().parse_link_url(&link, &hashmap! {})?;
1095 match hal_client
1096 .clone()
1097 .post_json(link.as_str(), request_body.as_str(), headers)
1098 .await
1099 {
1100 Ok(res) => Some(res),
1101 Err(err) => {
1102 info!("error response for pacts for verification: {} ", err);
1103 return Err(anyhow!(err));
1104 }
1105 }
1106 }
1107 Err(e) => return Err(anyhow!(e)),
1108 };
1109
1110 let pact_links = match response {
1112 Some(v) => {
1113 let pfv: PactsForVerificationResponse = serde_json::from_value(v)
1114 .map_err(|err| {
1115 trace!(
1116 "Failed to deserialise PactsForVerificationResponse: {}",
1117 err
1118 );
1119 err
1120 })
1121 .unwrap_or(PactsForVerificationResponse {
1122 embedded: PactsForVerificationBody { pacts: vec![] },
1123 });
1124 trace!(?pfv, "got pacts for verification response");
1125
1126 if pfv.embedded.pacts.len() == 0 {
1127 return Err(anyhow!(PactBrokerError::NotFound(format!(
1128 "No pacts were found for this provider"
1129 ))));
1130 };
1131
1132 let links: Result<Vec<(Link, PactVerificationContext)>, PactBrokerError> = pfv.embedded.pacts.iter().map(| p| {
1133 match p.links.get("self") {
1134 Some(l) => Ok((l.clone(), p.into())),
1135 None => Err(
1136 PactBrokerError::LinkError(
1137 format!(
1138 "Expected a HAL+JSON response from the pact broker, but got a link with no HREF. URL: '{}', PATH: '{:?}'",
1139 &hal_client.url,
1140 &p.links,
1141 )
1142 )
1143 )
1144 }
1145 }).collect();
1146
1147 links
1148 }
1149 None => Err(PactBrokerError::NotFound(format!(
1150 "No pacts were found for this provider"
1151 ))),
1152 }?;
1153
1154 let results: Vec<_> = futures::stream::iter(pact_links)
1155 .map(|(ref pact_link, ref context)| {
1156 match pact_link.href {
1157 Some(_) => Ok((hal_client.clone(), pact_link.clone(), context.clone())),
1158 None => Err(
1159 PactBrokerError::LinkError(
1160 format!(
1161 "Expected a HAL+JSON response from the pact broker, but got a link with no HREF. URL: '{}', LINK: '{:?}'",
1162 &hal_client.url,
1163 pact_link
1164 )
1165 )
1166 )
1167 }
1168 })
1169 .and_then(|(hal_client, pact_link, context)| async {
1170 let pact_json = hal_client.fetch_url(
1171 &pact_link.clone(),
1172 &template_values.clone()
1173 ).await?;
1174 Ok((pact_link, pact_json, context))
1175 })
1176 .map(|result| {
1177 match result {
1178 Ok((pact_link, pact_json, context)) => {
1179 let href = pact_link.href.unwrap_or_default();
1180 let links = links_from_json(&pact_json);
1181 load_pact_from_json(href.as_str(), &pact_json)
1182 .map(|pact| (pact, Some(context), links))
1183 .map_err(|err| PactBrokerError::ContentError(format!("{}", err)))
1184 },
1185 Err(err) => Err(err)
1186 }
1187 })
1188 .into_stream()
1189 .collect()
1190 .await;
1191
1192 Ok(results)
1193}
1194
1195pub async fn fetch_pact_from_url(
1199 url: &str,
1200 auth: &Option<HttpAuth>,
1201) -> anyhow::Result<(Box<dyn Pact + Send + Sync + RefUnwindSafe>, Vec<Link>)> {
1202 let url = url.to_string();
1203 let auth = auth.clone();
1204 let (url, pact_json) =
1205 tokio::task::spawn_blocking(move || http_utils::fetch_json_from_url(&url, &auth)).await??;
1206 let pact = load_pact_from_json(&url, &pact_json)?;
1207 let links = links_from_json(&pact_json);
1208 Ok((pact, links))
1209}
1210
1211async fn publish_provider_tags(
1212 hal_client: &HALClient,
1213 links: &[Link],
1214 provider_tags: Vec<String>,
1215 version: &str,
1216 headers: Option<HashMap<String, String>>,
1217) -> Result<(), PactBrokerError> {
1218 let hal_client = hal_client
1219 .clone()
1220 .with_doc_context(links)?
1221 .navigate("pb:provider", &hashmap! {})
1222 .await?;
1223 match hal_client.find_link("pb:version-tag") {
1224 Ok(link) => {
1225 for tag in &provider_tags {
1226 let template_values = hashmap! {
1227 "version".to_string() => version.to_string(),
1228 "tag".to_string() => tag.clone()
1229 };
1230 match hal_client
1231 .clone()
1232 .put_json(
1233 hal_client
1234 .clone()
1235 .parse_link_url(&link, &template_values)?
1236 .as_str(),
1237 "{}",
1238 headers.clone(),
1239 )
1240 .await
1241 {
1242 Ok(_) => debug!("Pushed tag {} for provider version {}", tag, version),
1243 Err(err) => {
1244 error!(
1245 "Failed to push tag {} for provider version {}",
1246 tag, version
1247 );
1248 return Err(err);
1249 }
1250 }
1251 }
1252 Ok(())
1253 }
1254 Err(_) => Err(PactBrokerError::LinkError(
1255 "Can't publish provider tags as there is no 'pb:version-tag' link".to_string(),
1256 )),
1257 }
1258}
1259
1260async fn publish_provider_branch(
1261 hal_client: &HALClient,
1262 links: &[Link],
1263 branch: &str,
1264 version: &str,
1265 headers: Option<HashMap<String, String>>,
1266) -> Result<(), PactBrokerError> {
1267 let hal_client = hal_client
1268 .clone()
1269 .with_doc_context(links)?
1270 .navigate("pb:provider", &hashmap! {})
1271 .await?;
1272
1273 match hal_client.find_link("pb:branch-version") {
1274 Ok(link) => {
1275 let template_values = hashmap! {
1276 "branch".to_string() => branch.to_string(),
1277 "version".to_string() => version.to_string(),
1278 };
1279 match hal_client.clone().put_json(hal_client.clone().parse_link_url(&link, &template_values)?.as_str(), "{}",headers).await {
1280 Ok(_) => debug!("Pushed branch {} for provider version {}", branch, version),
1281 Err(err) => {
1282 error!("Failed to push branch {} for provider version {}", branch, version);
1283 return Err(err);
1284 }
1285 }
1286 Ok(())
1287 },
1288 Err(_) => Err(PactBrokerError::LinkError("Can't publish provider branch as there is no 'pb:branch-version' link. Please ugrade to Pact Broker version 2.86.0 or later for branch support".to_string()))
1289 }
1290}
1291
1292#[skip_serializing_none]
1293#[derive(Serialize, Deserialize, Debug, Clone)]
1294#[serde(rename_all = "camelCase")]
1295pub struct ConsumerVersionSelector {
1297 pub consumer: Option<String>,
1299 pub tag: Option<String>,
1301 pub fallback_tag: Option<String>,
1303 pub latest: Option<bool>,
1305 pub deployed_or_released: Option<bool>,
1307 pub deployed: Option<bool>,
1309 pub released: Option<bool>,
1311 pub environment: Option<String>,
1313 pub main_branch: Option<bool>,
1315 pub branch: Option<String>,
1317 pub matching_branch: Option<bool>,
1319}
1320
1321#[derive(Serialize, Deserialize, Debug, Clone)]
1322#[serde(rename_all = "camelCase")]
1323struct PactsForVerificationResponse {
1324 #[serde(rename(deserialize = "_embedded"))]
1325 pub embedded: PactsForVerificationBody,
1326}
1327
1328#[derive(Serialize, Deserialize, Debug, Clone)]
1329#[serde(rename_all = "camelCase")]
1330struct PactsForVerificationBody {
1331 pub pacts: Vec<PactForVerification>,
1332}
1333
1334#[derive(Serialize, Deserialize, Debug, Clone)]
1335#[serde(rename_all = "camelCase")]
1336struct PactForVerification {
1337 pub short_description: String,
1338 #[serde(rename(deserialize = "_links"))]
1339 pub links: HashMap<String, Link>,
1340 pub verification_properties: Option<PactVerificationProperties>,
1341}
1342
1343#[skip_serializing_none]
1344#[derive(Serialize, Deserialize, Debug, Clone)]
1345#[serde(rename_all = "camelCase")]
1346pub struct PactsForVerificationRequest {
1348 #[serde(skip_serializing_if = "Vec::is_empty")]
1350 pub provider_version_tags: Vec<String>,
1351 pub include_pending_status: bool,
1353 pub include_wip_pacts_since: Option<String>,
1355 pub consumer_version_selectors: Vec<ConsumerVersionSelector>,
1357 pub provider_version_branch: Option<String>,
1359}
1360
1361#[skip_serializing_none]
1362#[derive(Serialize, Deserialize, Debug, Clone)]
1363#[serde(rename_all = "camelCase")]
1364pub struct PactVerificationContext {
1366 pub short_description: String,
1368 pub verification_properties: PactVerificationProperties,
1370}
1371
1372impl From<&PactForVerification> for PactVerificationContext {
1373 fn from(value: &PactForVerification) -> Self {
1374 PactVerificationContext {
1375 short_description: value.short_description.clone(),
1376 verification_properties: value.verification_properties.clone().unwrap_or_default(),
1377 }
1378 }
1379}
1380
1381#[skip_serializing_none]
1382#[derive(Serialize, Deserialize, Debug, Clone, Default)]
1383#[serde(rename_all = "camelCase")]
1384pub struct PactVerificationProperties {
1386 #[serde(default)]
1387 pub pending: bool,
1389 pub notices: Vec<HashMap<String, String>>,
1391}
1392
1393#[cfg(test)]
1394mod hal_client_custom_headers_tests {
1395 use super::*;
1396 use crate::cli::pact_broker::main::types::SslOptions;
1397 use std::collections::HashMap;
1398
1399 fn create_test_custom_headers() -> CustomHeaders {
1400 let mut headers = HashMap::new();
1401 headers.insert("Authorization".to_string(), "Bearer test-token".to_string());
1402 headers.insert("X-API-Key".to_string(), "secret-key".to_string());
1403 CustomHeaders { headers }
1404 }
1405
1406 fn create_cloudflare_custom_headers() -> CustomHeaders {
1407 let mut headers = HashMap::new();
1408 headers.insert(
1409 "CF-Access-Client-Id".to_string(),
1410 "client-id-123".to_string(),
1411 );
1412 headers.insert(
1413 "CF-Access-Client-Secret".to_string(),
1414 "secret-456".to_string(),
1415 );
1416 CustomHeaders { headers }
1417 }
1418
1419 #[test]
1420 fn test_hal_client_with_custom_headers() {
1421 let custom_headers = Some(create_test_custom_headers());
1422 let ssl_options = SslOptions::default();
1423
1424 let client = HALClient::with_url(
1425 "https://test.example.com",
1426 None,
1427 ssl_options,
1428 custom_headers.clone(),
1429 );
1430
1431 assert_eq!(client.url, "https://test.example.com");
1432 assert!(client.custom_headers.is_some());
1433
1434 let headers = client.custom_headers.unwrap();
1435 assert_eq!(headers.headers.len(), 2);
1436 assert_eq!(
1437 headers.headers.get("Authorization"),
1438 Some(&"Bearer test-token".to_string())
1439 );
1440 assert_eq!(
1441 headers.headers.get("X-API-Key"),
1442 Some(&"secret-key".to_string())
1443 );
1444 }
1445
1446 #[test]
1447 fn test_hal_client_with_cloudflare_headers() {
1448 let custom_headers = Some(create_cloudflare_custom_headers());
1449 let ssl_options = SslOptions::default();
1450
1451 let client = HALClient::with_url(
1452 "https://pact-broker.example.com",
1453 None,
1454 ssl_options,
1455 custom_headers.clone(),
1456 );
1457
1458 assert!(client.custom_headers.is_some());
1459
1460 let headers = client.custom_headers.unwrap();
1461 assert_eq!(headers.headers.len(), 2);
1462 assert_eq!(
1463 headers.headers.get("CF-Access-Client-Id"),
1464 Some(&"client-id-123".to_string())
1465 );
1466 assert_eq!(
1467 headers.headers.get("CF-Access-Client-Secret"),
1468 Some(&"secret-456".to_string())
1469 );
1470 }
1471
1472 #[test]
1473 fn test_hal_client_without_custom_headers() {
1474 let ssl_options = SslOptions::default();
1475
1476 let client = HALClient::with_url("https://test.example.com", None, ssl_options, None);
1477
1478 assert!(client.custom_headers.is_none());
1479 }
1480
1481 #[test]
1482 fn test_hal_client_with_auth_and_custom_headers() {
1483 let auth = Some(HttpAuth::Token("bearer-token".to_string()));
1484 let custom_headers = Some(create_test_custom_headers());
1485 let ssl_options = SslOptions::default();
1486
1487 let client = HALClient::with_url(
1488 "https://test.example.com",
1489 auth.clone(),
1490 ssl_options,
1491 custom_headers,
1492 );
1493
1494 assert!(client.auth.is_some());
1495 assert!(client.custom_headers.is_some());
1496
1497 if let Some(HttpAuth::Token(token)) = client.auth {
1498 assert_eq!(token, "bearer-token");
1499 }
1500 }
1501
1502 #[test]
1503 fn test_apply_custom_headers_with_mock_request() {
1504 use reqwest::Client;
1505 use reqwest_middleware::ClientBuilder;
1506
1507 let custom_headers = Some(create_test_custom_headers());
1508 let ssl_options = SslOptions::default();
1509
1510 let client = HALClient::with_url(
1511 "https://test.example.com",
1512 None,
1513 ssl_options,
1514 custom_headers,
1515 );
1516
1517 let reqwest_client = Client::new();
1519 let middleware_client = ClientBuilder::new(reqwest_client).build();
1520 let request_builder = middleware_client.get("https://test.example.com/test");
1521
1522 let modified_builder = client.apply_custom_headers(request_builder);
1524
1525 let request = modified_builder.build().unwrap();
1527
1528 assert!(request.headers().contains_key("authorization"));
1530 assert!(request.headers().contains_key("x-api-key"));
1531
1532 assert_eq!(
1533 request
1534 .headers()
1535 .get("authorization")
1536 .unwrap()
1537 .to_str()
1538 .unwrap(),
1539 "Bearer test-token"
1540 );
1541 assert_eq!(
1542 request
1543 .headers()
1544 .get("x-api-key")
1545 .unwrap()
1546 .to_str()
1547 .unwrap(),
1548 "secret-key"
1549 );
1550 }
1551
1552 #[test]
1553 fn test_apply_custom_headers_without_headers() {
1554 use reqwest::Client;
1555 use reqwest_middleware::ClientBuilder;
1556
1557 let ssl_options = SslOptions::default();
1558
1559 let client = HALClient::with_url("https://test.example.com", None, ssl_options, None);
1560
1561 let reqwest_client = Client::new();
1563 let middleware_client = ClientBuilder::new(reqwest_client).build();
1564 let request_builder = middleware_client.get("https://test.example.com/test");
1565
1566 let modified_builder = client.apply_custom_headers(request_builder);
1568
1569 let request = modified_builder.build().unwrap();
1571
1572 assert!(!request.headers().contains_key("authorization"));
1574 assert!(!request.headers().contains_key("x-api-key"));
1575 }
1576
1577 #[test]
1578 fn test_custom_headers_struct_creation() {
1579 let mut headers = HashMap::new();
1580 headers.insert("Test-Header".to_string(), "test-value".to_string());
1581
1582 let custom_headers = CustomHeaders { headers };
1583
1584 assert_eq!(custom_headers.headers.len(), 1);
1585 assert_eq!(
1586 custom_headers.headers.get("Test-Header"),
1587 Some(&"test-value".to_string())
1588 );
1589 }
1590
1591 #[test]
1592 fn test_custom_headers_empty() {
1593 let headers = HashMap::new();
1594 let custom_headers = CustomHeaders { headers };
1595
1596 assert_eq!(custom_headers.headers.len(), 0);
1597 assert!(custom_headers.headers.is_empty());
1598 }
1599
1600 #[test]
1601 fn test_custom_headers_case_sensitivity() {
1602 let mut headers = HashMap::new();
1603 headers.insert("content-type".to_string(), "application/json".to_string());
1604 headers.insert("Content-Type".to_string(), "text/plain".to_string());
1605
1606 let custom_headers = CustomHeaders { headers };
1607
1608 assert_eq!(custom_headers.headers.len(), 2);
1610 assert_eq!(
1611 custom_headers.headers.get("content-type"),
1612 Some(&"application/json".to_string())
1613 );
1614 assert_eq!(
1615 custom_headers.headers.get("Content-Type"),
1616 Some(&"text/plain".to_string())
1617 );
1618 }
1619}
1620
1621#[cfg(test)]
1622mod tests
1623{
1624 use expectest::expect;
1625 use expectest::prelude::*;
1626 use pact_models::{Consumer, PactSpecification, Provider};
1627 use pact_models::prelude::RequestResponsePact;
1628 use pact_models::sync_interaction::RequestResponseInteraction;
1629 use pretty_assertions::assert_eq;
1630
1631 use pact_consumer::*;
1632 use pact_consumer::prelude::*;
1633
1634 use super::*;
1635 use super::{content_type, json_content_type};
1636
1637 #[test]
1638 fn resolve_path_test() {
1639 let client = HALClient::with_url("not a URL", None, SslOptions::default(), None);
1640 expect!(client.resolve_path("/any")).to(be_err());
1641
1642 let client = HALClient::with_url("http://localhost-ip4:1234", None, SslOptions::default(), None);
1643 expect!(client.resolve_path("")).to(be_ok().value(Url::parse("http://localhost-ip4:1234").unwrap()));
1644 expect!(client.resolve_path("/")).to(be_ok().value(Url::parse("http://localhost-ip4:1234").unwrap()));
1645 expect!(client.resolve_path("/any")).to(be_ok().value(Url::parse("http://localhost-ip4:1234/any").unwrap()));
1646 expect!(client.resolve_path("any")).to(be_ok().value(Url::parse("http://localhost-ip4:1234/any").unwrap()));
1647 expect!(client.resolve_path("any/sub-path")).to(be_ok().value(Url::parse("http://localhost-ip4:1234/any/sub-path").unwrap()));
1648 expect!(client.resolve_path("/base-path")).to(be_ok().value(Url::parse("http://localhost-ip4:1234/base-path").unwrap()));
1649 expect!(client.resolve_path("/base-path/")).to(be_ok().value(Url::parse("http://localhost-ip4:1234/base-path/").unwrap()));
1650 expect!(client.resolve_path("/base-path/sub-path")).to(be_ok().value(Url::parse("http://localhost-ip4:1234/base-path/sub-path").unwrap()));
1651
1652 let client = HALClient::with_url("http://localhost-ip4:1234/base-path", None, SslOptions::default(), None);
1653 expect!(client.resolve_path("")).to(be_ok().value(Url::parse("http://localhost-ip4:1234/base-path").unwrap()));
1654 expect!(client.resolve_path("/")).to(be_ok().value(Url::parse("http://localhost-ip4:1234").unwrap()));
1655 expect!(client.resolve_path("/any")).to(be_ok().value(Url::parse("http://localhost-ip4:1234/any").unwrap()));
1656 expect!(client.resolve_path("any")).to(be_ok().value(Url::parse("http://localhost-ip4:1234/base-path/any").unwrap()));
1657 expect!(client.resolve_path("any/sub-path")).to(be_ok().value(Url::parse("http://localhost-ip4:1234/base-path/any/sub-path").unwrap()));
1658 expect!(client.resolve_path("/base-path")).to(be_ok().value(Url::parse("http://localhost-ip4:1234/base-path").unwrap()));
1659 expect!(client.resolve_path("/base-path/")).to(be_ok().value(Url::parse("http://localhost-ip4:1234/base-path/").unwrap()));
1660 expect!(client.resolve_path("/base-path/sub-path")).to(be_ok().value(Url::parse("http://localhost-ip4:1234/base-path/sub-path").unwrap()));
1661 }
1662
1663 #[test_log::test(tokio::test)]
1664 async fn navigate_first_retrieves_the_root_resource() {
1665 let pact_broker = PactBuilderAsync::new("RustPactVerifier", "PactBrokerStub")
1666 .interaction("a request to a hal resource", "", |mut i| async move {
1667 i.request.path("/");
1668 i.response
1669 .header("Content-Type", "application/hal+json")
1670 .body("{\"_links\":{\"next\":{\"href\":\"/abc\"},\"prev\":{\"href\":\"/def\"}}}");
1671 i
1672 })
1673 .await
1674 .interaction("a request to next", "", |mut i| async move {
1675 i.request.path("/abc");
1676 i.response
1677 .header("Content-Type", "application/json")
1678 .json_body(json_pattern!("Yay! You found your way here"));
1679 i
1680 })
1681 .await
1682 .start_mock_server(None, Some(MockServerConfig::default()));
1683
1684 let client = HALClient::with_url(pact_broker.url().as_str(), None, SslOptions::default(), None);
1685 let result = client.navigate("next", &hashmap!{}).await.unwrap();
1686 expect!(result.path_info).to(be_some().value(serde_json::Value::String("Yay! You found your way here".to_string())));
1687 }
1688
1689 #[test_log::test(tokio::test)]
1690 async fn navigate_will_not_retrieve_the_root_resource_if_a_path_is_already_set() {
1691 let pact_broker = PactBuilderAsync::new("RustPactVerifier", "PactBrokerStub")
1692 .interaction("a request to next", "", |mut i| async move {
1693 i.request.path("/abc");
1694 i.response
1695 .header("Content-Type", "application/json")
1696 .json_body(json_pattern!("Yay! You found your way here"));
1697 i
1698 })
1699 .await
1700 .start_mock_server(None, Some(MockServerConfig::default()));
1701
1702 let mut client = HALClient::with_url(pact_broker.url().as_str(), None, SslOptions::default(), None);
1703 client.path_info = Some(json!({
1704 "_links": {
1705 "next": { "href": "/abc" },
1706 "prev": { "href": "/def" }
1707 }
1708 }));
1709 let result = client.navigate("next", &hashmap!{}).await.unwrap();
1710 expect!(result.path_info).to(be_some().value(serde_json::Value::String("Yay! You found your way here".to_string())));
1711 }
1712
1713 #[test_log::test(tokio::test)]
1714 async fn navigate_takes_context_paths_into_account() {
1715 let pact_broker = PactBuilderAsync::new("RustPactVerifier", "PactBrokerStub")
1716 .interaction("a request to a hal resource with base path", "", |mut i| async move {
1717 i.request.path("/base-path");
1718 i.response
1719 .header("Content-Type", "application/hal+json")
1720 .body("{\"_links\":{\"next\":{\"href\":\"/base-path/abc\"},\"prev\":{\"href\":\"/base-path/def\"}}}");
1721 i
1722 })
1723 .await
1724 .interaction("a request to next with a base path", "", |mut i| async move {
1725 i.request.path("/base-path/abc");
1726 i.response
1727 .header("Content-Type", "application/json")
1728 .json_body(json_pattern!("Yay! You found your way here"));
1729 i
1730 })
1731 .await
1732 .start_mock_server(None, Some(MockServerConfig::default()));
1733
1734 let client = HALClient::with_url(pact_broker.url().join("/base-path").unwrap().as_str(), None, SslOptions::default(), None);
1735 let result = client.navigate("next", &hashmap!{}).await.unwrap();
1736 expect!(result.path_info).to(be_some().value(serde_json::Value::String("Yay! You found your way here".to_string())));
1737 }
1738}