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.clone().fetch("/".into()).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.clone().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.clone().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
483 let broker_url = self.url.parse::<Url>()?;
484 let context_path = broker_url.path();
485 let url = if context_path.is_empty().not()
486 && context_path != "/"
487 && path.starts_with(context_path)
488 {
489 let mut base_url = broker_url.clone();
490 base_url.set_path("/");
491 base_url.join(path)?
492 } else {
493 broker_url.join(path)?
494 };
495
496 let mut request_builder = match self.auth {
497 Some(ref auth) => match auth {
498 HttpAuth::User(username, password) => {
499 self.client.get(url).basic_auth(username, password.clone())
500 }
501 HttpAuth::Token(token) => self.client.get(url).bearer_auth(token),
502 _ => self.client.get(url),
503 },
504 None => self.client.get(url),
505 }
506 .header("accept", "application/hal+json, application/json");
507
508 request_builder = self.apply_custom_headers(request_builder);
510
511 let response = utils::with_retries(self.retries, request_builder)
512 .await
513 .map_err(|err| {
514 PactBrokerError::IoError(format!(
515 "Failed to access pact broker path '{}' - {}. URL: '{}'",
516 &path, err, &self.url,
517 ))
518 })?;
519
520 self.parse_broker_response(path.to_string(), response).await
521 }
522
523 pub async fn delete(self, path: &str) -> Result<Value, PactBrokerError> {
524 info!("Deleting path '{}' from pact broker", path);
525
526 let broker_url = self.url.parse::<Url>()?;
527 let context_path = broker_url.path();
528 let url = if context_path.is_empty().not()
529 && context_path != "/"
530 && path.starts_with(context_path)
531 {
532 let mut base_url = broker_url.clone();
533 base_url.set_path("/");
534 base_url.join(path)?
535 } else {
536 broker_url.join(path)?
537 };
538
539 let request_builder = match self.auth {
540 Some(ref auth) => match auth {
541 HttpAuth::User(username, password) => self
542 .client
543 .delete(url)
544 .basic_auth(username, password.clone()),
545 HttpAuth::Token(token) => self.client.delete(url).bearer_auth(token),
546 _ => self.client.delete(url),
547 },
548 None => self.client.delete(url),
549 }
550 .header("Accept", "application/hal+json");
551
552 let response = utils::with_retries(self.retries, request_builder)
553 .await
554 .map_err(|err| {
555 PactBrokerError::IoError(format!(
556 "Failed to delete pact broker path '{}' - {}. URL: '{}'",
557 &path, err, &self.url,
558 ))
559 })?;
560
561 self.parse_broker_response(path.to_string(), response).await
562 }
563
564 async fn parse_broker_response(
565 &self,
566 path: String,
567 response: reqwest::Response,
568 ) -> Result<Value, PactBrokerError> {
569 let is_json_content_type = json_content_type(&response);
570 let content_type = content_type(&response);
571 let status_code = response.status();
572
573 if status_code.is_success() {
574 if is_json_content_type {
575 response.json::<Value>()
576 .await
577 .map_err(|err| PactBrokerError::ContentError(
578 format!("Did not get a valid HAL response body from pact broker path '{}' - {}. URL: '{}'",
579 path, err, self.url)
580 ))
581 } else if status_code.as_u16() == 204 {
582 Ok(json!({}))
583 } else {
584 debug!("Request from broker was a success, but the response body was not JSON");
585 Err(PactBrokerError::ContentError(format!(
586 "Did not get a valid HAL response body from pact broker path '{}', content type is '{}'. URL: '{}'",
587 path, content_type, self.url
588 )))
589 }
590 } else if status_code.as_u16() == 404 {
591 Err(PactBrokerError::NotFound(format!(
592 "Request to pact broker path '{}' failed: {}. URL: '{}'",
593 path, status_code, self.url
594 )))
595 } else {
596 let body = response.bytes().await.map_err(|_| {
598 PactBrokerError::IoError(format!(
599 "Failed to download response body for path '{}'. URL: '{}'",
600 &path, self.url
601 ))
602 })?;
603
604 if is_json_content_type {
605 match serde_json::from_slice::<Value>(&body) {
606 Ok(json_body) => {
607 if json_body.get("errors").is_some() || json_body.get("notices").is_some() {
608 Err(handle_validation_errors(json_body))
609 } else {
610 Err(PactBrokerError::IoError(format!(
611 "Request to pact broker path '{}' failed: {}. Response: {}. URL: '{}'",
612 path, status_code, json_body, self.url
613 )))
614 }
615 }
616 Err(_) => {
617 let body_text = from_utf8(&body)
618 .map(|b| b.to_string())
619 .unwrap_or_else(|err| format!("could not read body: {}", err));
620 error!(
621 "Request to pact broker path '{}' failed: {}",
622 path, body_text
623 );
624 Err(PactBrokerError::IoError(format!(
625 "Request to pact broker path '{}' failed: {}. URL: '{}'",
626 path, status_code, self.url
627 )))
628 }
629 }
630 } else {
631 let body_text = from_utf8(&body)
632 .map(|b| b.to_string())
633 .unwrap_or_else(|err| format!("could not read body: {}", err));
634 error!(
635 "Request to pact broker path '{}' failed: {}",
636 path, body_text
637 );
638 Err(PactBrokerError::IoError(format!(
639 "Request to pact broker path '{}' failed: {}. URL: '{}'",
640 path, status_code, self.url
641 )))
642 }
643 }
644 }
645
646 fn parse_link_url(
647 &self,
648 link: &Link,
649 values: &HashMap<String, String>,
650 ) -> Result<String, PactBrokerError> {
651 match link.href {
652 Some(ref href) => {
653 debug!("templated URL = {}", href);
654 let re = Regex::new(r"\{(\w+)}").unwrap();
655 let final_url = re.replace_all(href, |caps: &Captures| {
656 let lookup = caps.get(1).unwrap().as_str();
657 trace!("Looking up value for key '{}'", lookup);
658 match values.get(lookup) {
659 Some(val) => urlencoding::encode(val.as_str()).to_string(),
660 None => {
661 warn!(
662 "No value was found for key '{}', mapped values are {:?}",
663 lookup, values
664 );
665 format!("{{{}}}", lookup)
666 }
667 }
668 });
669 debug!("final URL = {}", final_url);
670 Ok(final_url.to_string())
671 }
672 None => Err(PactBrokerError::LinkError(format!(
673 "Expected a HAL+JSON response from the pact broker, but got a link with no HREF. URL: '{}', LINK: '{}'",
674 self.url, link.name
675 ))),
676 }
677 }
678
679 pub fn iter_links(&self, link: &str) -> Result<Vec<Link>, PactBrokerError> {
681 match self.path_info {
682 None => Err(PactBrokerError::LinkError(format!("No previous resource has been fetched from the pact broker. URL: '{}', LINK: '{}'",
683 self.url, link))),
684 Some(ref json) => match json.get("_links") {
685 Some(json) => match json.get(&link) {
686 Some(link_data) => link_data.as_array()
687 .map(|link_data| link_data.iter().map(|link_json| match link_json {
688 Value::Object(data) => Link::from_json(&link, data),
689 Value::String(s) => Link { name: link.to_string(), href: Some(s.clone()), templated: false, title: None },
690 _ => Link { name: link.to_string(), href: Some(link_json.to_string()), templated: false, title: None }
691 }).collect())
692 .ok_or_else(|| PactBrokerError::LinkError(format!("Link is malformed, expected an object but got {}. URL: '{}', LINK: '{}'",
693 link_data, self.url, link))),
694 None => Err(PactBrokerError::LinkError(format!("Link '{}' was not found in the response, only the following links where found: {:?}. URL: '{}', LINK: '{}'",
695 link, json.as_object().unwrap_or(&json!({}).as_object().unwrap()).keys().join(", "), self.url, link)))
696 },
697 None => Err(PactBrokerError::LinkError(format!("Expected a HAL+JSON response from the pact broker, but got a response with no '_links'. URL: '{}', LINK: '{}'",
698 self.url, link)))
699 }
700 }
701 }
702
703 pub async fn post_json(
704 &self,
705 url: &str,
706 body: &str,
707 headers: Option<HashMap<String, String>>,
708 ) -> Result<serde_json::Value, PactBrokerError> {
709 trace!("post_json(url='{}', body='{}')", url, body);
710
711 self.send_document(url, body, Method::POST, headers).await
712 }
713
714 pub async fn put_json(
715 &self,
716 url: &str,
717 body: &str,
718 headers: Option<HashMap<String, String>>,
719 ) -> Result<serde_json::Value, PactBrokerError> {
720 trace!("put_json(url='{}', body='{}')", url, body);
721
722 self.send_document(url, body, Method::PUT, headers).await
723 }
724 pub async fn patch_json(
725 &self,
726 url: &str,
727 body: &str,
728 headers: Option<HashMap<String, String>>,
729 ) -> Result<serde_json::Value, PactBrokerError> {
730 trace!("put_json(url='{}', body='{}')", url, body);
731
732 self.send_document(url, body, Method::PATCH, headers).await
733 }
734
735 async fn send_document(
736 &self,
737 url: &str,
738 body: &str,
739 method: Method,
740 headers: Option<HashMap<String, String>>,
741 ) -> Result<Value, PactBrokerError> {
742 let method_type = method.clone();
743 debug!("Sending JSON to {} using {}: {}", url, method, body);
744
745 let base_url = &self.url.parse::<Url>()?;
746 let url = if url.starts_with("/") {
747 base_url.join(url)?
748 } else {
749 let url = url.parse::<Url>()?;
750 base_url.join(&url.path())?
751 };
752
753 let request_builder = match self.auth {
754 Some(ref auth) => match auth {
755 HttpAuth::User(username, password) => self
756 .client
757 .request(method, url.clone())
758 .basic_auth(username, password.clone()),
759 HttpAuth::Token(token) => {
760 self.client.request(method, url.clone()).bearer_auth(token)
761 }
762 _ => self.client.request(method, url.clone()),
763 },
764 None => self.client.request(method, url.clone()),
765 }
766 .header("Accept", "application/hal+json")
767 .body(body.to_string());
768
769 let request_builder = if let Some(ref headers) = headers {
772 headers
773 .iter()
774 .fold(request_builder, |builder, (key, value)| {
775 builder.header(key.as_str(), value.as_str())
776 })
777 } else {
778 request_builder
779 };
780
781 let request_builder = if method_type == Method::PATCH {
782 request_builder.header("Content-Type", "application/merge-patch+json")
783 } else {
784 request_builder.header("Content-Type", "application/json")
785 };
786 let response = with_retries(self.retries, request_builder).await;
787 match response {
788 Ok(res) => {
789 self.parse_broker_response(url.path().to_string(), res)
790 .await
791 }
792 Err(err) => Err(PactBrokerError::IoError(format!(
793 "Failed to send JSON to the pact broker URL '{}' - IoError {}",
794 url, err
795 ))),
796 }
797 }
798
799 fn with_doc_context(self, doc_attributes: &[Link]) -> Result<HALClient, PactBrokerError> {
800 let links: serde_json::Map<String, serde_json::Value> = doc_attributes
801 .iter()
802 .map(|link| (link.name.clone(), link.as_json()))
803 .collect();
804 let links_json = json!({
805 "_links": json!(links)
806 });
807 Ok(self.update_path_info(links_json))
808 }
809}
810
811fn handle_validation_errors(body: Value) -> PactBrokerError {
812 match &body {
813 Value::Object(attrs) => {
814 let notices: Vec<Notice> = attrs
816 .get("notices")
817 .and_then(|n| n.as_array())
818 .map(|notices_array| {
819 notices_array
820 .iter()
821 .filter_map(|notice| serde_json::from_value::<Notice>(notice.clone()).ok())
822 .collect()
823 })
824 .unwrap_or_default();
825
826 if let Some(errors) = attrs.get("errors") {
827 let error_messages = match errors {
828 Value::Array(values) => values.iter().map(|v| json_to_string(v)).collect(),
829 Value::Object(errors) => errors
830 .iter()
831 .map(|(field, errors)| match errors {
832 Value::String(error) => format!("{}: {}", field, error),
833 Value::Array(errors) => format!(
834 "{}: {}",
835 field,
836 errors.iter().map(|err| json_to_string(err)).join(", ")
837 ),
838 _ => format!("{}: {}", field, errors),
839 })
840 .collect(),
841 Value::String(s) => vec![s.clone()],
842 _ => vec![errors.to_string()],
843 };
844
845 if !notices.is_empty() {
846 PactBrokerError::ValidationErrorWithNotices(error_messages, notices)
847 } else {
848 PactBrokerError::ValidationError(error_messages)
849 }
850 } else if !notices.is_empty() {
851 let notice_messages = notices.iter().map(|n| n.text.clone()).collect();
853 PactBrokerError::ValidationErrorWithNotices(notice_messages, notices)
854 } else {
855 PactBrokerError::ValidationError(vec![body.to_string()])
856 }
857 }
858 Value::String(s) => PactBrokerError::ValidationError(vec![s.clone()]),
859 _ => PactBrokerError::ValidationError(vec![body.to_string()]),
860 }
861}
862
863impl HALClient {
864 pub fn setup(url: &str, auth: Option<HttpAuth>, ssl_options: SslOptions) -> HALClient {
865 let mut builder = reqwest::Client::builder().user_agent(format!(
866 "{}/{}",
867 env!("CARGO_PKG_NAME"),
868 env!("CARGO_PKG_VERSION")
869 ));
870
871 debug!("Using ssl_options: {:?}", ssl_options);
872 if let Some(ref path) = ssl_options.ssl_cert_path {
873 if let Ok(cert_bytes) = std::fs::read(path) {
874 if let Ok(cert) = reqwest::Certificate::from_pem_bundle(&cert_bytes) {
875 debug!("Adding SSL certificate from path: {}", path);
876 for c in cert {
877 builder = builder.add_root_certificate(c.clone());
878 }
879 }
880 } else {
881 debug!(
882 "Could not read SSL certificate from provided path: {}",
883 path
884 );
885 }
886 }
887 if ssl_options.skip_ssl {
888 builder = builder.danger_accept_invalid_certs(true);
889 debug!("Skipping SSL certificate validation");
890 }
891 if !ssl_options.use_root_trust_store {
892 builder = builder.tls_built_in_root_certs(false);
893 debug!("Disabling root trust store for SSL");
894 }
895
896 let built_client = builder.build().unwrap();
897 let client = ClientBuilder::new(built_client)
898 .with(TracingMiddleware::default())
899 .with(OtelPropagatorMiddleware)
900 .build();
901
902 HALClient {
903 client,
904 url: url.to_string(),
905 path_info: None,
906 auth,
907 custom_headers: None,
908 retries: 3,
909 ssl_options,
910 }
911 }
912}
913
914pub fn links_from_json(json: &Value) -> Vec<Link> {
915 match json.get("_links") {
916 Some(json) => match json {
917 Value::Object(v) => v
918 .iter()
919 .map(|(link, json)| match json {
920 Value::Object(attr) => Link::from_json(link, attr),
921 _ => Link {
922 name: link.clone(),
923 ..Link::default()
924 },
925 })
926 .collect(),
927 _ => vec![],
928 },
929 None => vec![],
930 }
931}
932
933pub async fn fetch_pacts_from_broker(
935 broker_url: &str,
936 provider_name: &str,
937 auth: Option<HttpAuth>,
938 ssl_options: SslOptions,
939 custom_headers: Option<CustomHeaders>,
940) -> anyhow::Result<
941 Vec<
942 anyhow::Result<(
943 Box<dyn Pact + Send + Sync + RefUnwindSafe>,
944 Option<PactVerificationContext>,
945 Vec<Link>,
946 )>,
947 >,
948> {
949 trace!(
950 "fetch_pacts_from_broker(broker_url='{}', provider_name='{}', auth={})",
951 broker_url,
952 provider_name,
953 auth.clone().unwrap_or_default()
954 );
955
956 let mut hal_client = HALClient::with_url(broker_url, auth, ssl_options, custom_headers);
957 let template_values = hashmap! { "provider".to_string() => provider_name.to_string() };
958
959 hal_client = hal_client
960 .navigate("pb:latest-provider-pacts", &template_values)
961 .await
962 .map_err(move |err| match err {
963 PactBrokerError::NotFound(_) => PactBrokerError::NotFound(format!(
964 "No pacts for provider '{}' where found in the pact broker. URL: '{}'",
965 provider_name, broker_url
966 )),
967 _ => err,
968 })?;
969
970 let pact_links = hal_client.clone().iter_links("pacts")?;
971
972 let results: Vec<_> = futures::stream::iter(pact_links)
973 .map(|ref pact_link| {
974 match pact_link.href {
975 Some(_) => Ok((hal_client.clone(), pact_link.clone())),
976 None => Err(
977 PactBrokerError::LinkError(
978 format!(
979 "Expected a HAL+JSON response from the pact broker, but got a link with no HREF. URL: '{}', LINK: '{:?}'",
980 &hal_client.url,
981 pact_link
982 )
983 )
984 )
985 }
986 })
987 .and_then(|(hal_client, pact_link)| async {
988 let pact_json = hal_client.fetch_url(
989 &pact_link.clone(),
990 &template_values.clone()
991 ).await?;
992 Ok((pact_link, pact_json))
993 })
994 .map(|result| {
995 match result {
996 Ok((pact_link, pact_json)) => {
997 let href = pact_link.href.unwrap_or_default();
998 let links = links_from_json(&pact_json);
999 load_pact_from_json(href.as_str(), &pact_json)
1000 .map(|pact| (pact, None, links))
1001 },
1002 Err(err) => Err(err.into())
1003 }
1004 })
1005 .into_stream()
1006 .collect()
1007 .await;
1008
1009 Ok(results)
1010}
1011
1012pub async fn fetch_pacts_dynamically_from_broker(
1014 broker_url: &str,
1015 provider_name: String,
1016 pending: bool,
1017 include_wip_pacts_since: Option<String>,
1018 provider_tags: Vec<String>,
1019 provider_branch: Option<String>,
1020 consumer_version_selectors: Vec<ConsumerVersionSelector>,
1021 auth: Option<HttpAuth>,
1022 ssl_options: SslOptions,
1023 headers: Option<HashMap<String, String>>,
1024 custom_headers: Option<CustomHeaders>,
1025) -> anyhow::Result<
1026 Vec<
1027 Result<
1028 (
1029 Box<dyn Pact + Send + Sync + RefUnwindSafe>,
1030 Option<PactVerificationContext>,
1031 Vec<Link>,
1032 ),
1033 PactBrokerError,
1034 >,
1035 >,
1036> {
1037 trace!(
1038 "fetch_pacts_dynamically_from_broker(broker_url='{}', provider_name='{}', pending={}, \
1039 include_wip_pacts_since={:?}, provider_tags: {:?}, consumer_version_selectors: {:?}, auth={})",
1040 broker_url,
1041 provider_name,
1042 pending,
1043 include_wip_pacts_since,
1044 provider_tags,
1045 consumer_version_selectors,
1046 auth.clone().unwrap_or_default()
1047 );
1048
1049 let mut hal_client = HALClient::with_url(broker_url, auth, ssl_options, custom_headers);
1050 let template_values = hashmap! { "provider".to_string() => provider_name.clone() };
1051
1052 hal_client = hal_client
1053 .navigate("pb:provider-pacts-for-verification", &template_values)
1054 .await
1055 .map_err(move |err| match err {
1056 PactBrokerError::NotFound(_) => PactBrokerError::NotFound(format!(
1057 "No pacts for provider '{}' were found in the pact broker. URL: '{}'",
1058 provider_name.clone(),
1059 broker_url
1060 )),
1061 _ => err,
1062 })?;
1063
1064 let pacts_for_verification = PactsForVerificationRequest {
1066 provider_version_tags: provider_tags,
1067 provider_version_branch: provider_branch,
1068 include_wip_pacts_since,
1069 consumer_version_selectors,
1070 include_pending_status: pending,
1071 };
1072 let request_body = serde_json::to_string(&pacts_for_verification).unwrap();
1073
1074 let response = match hal_client.find_link("self") {
1076 Ok(link) => {
1077 let link = hal_client.clone().parse_link_url(&link, &hashmap! {})?;
1078 match hal_client
1079 .clone()
1080 .post_json(link.as_str(), request_body.as_str(), headers)
1081 .await
1082 {
1083 Ok(res) => Some(res),
1084 Err(err) => {
1085 info!("error response for pacts for verification: {} ", err);
1086 return Err(anyhow!(err));
1087 }
1088 }
1089 }
1090 Err(e) => return Err(anyhow!(e)),
1091 };
1092
1093 let pact_links = match response {
1095 Some(v) => {
1096 let pfv: PactsForVerificationResponse = serde_json::from_value(v)
1097 .map_err(|err| {
1098 trace!(
1099 "Failed to deserialise PactsForVerificationResponse: {}",
1100 err
1101 );
1102 err
1103 })
1104 .unwrap_or(PactsForVerificationResponse {
1105 embedded: PactsForVerificationBody { pacts: vec![] },
1106 });
1107 trace!(?pfv, "got pacts for verification response");
1108
1109 if pfv.embedded.pacts.len() == 0 {
1110 return Err(anyhow!(PactBrokerError::NotFound(format!(
1111 "No pacts were found for this provider"
1112 ))));
1113 };
1114
1115 let links: Result<Vec<(Link, PactVerificationContext)>, PactBrokerError> = pfv.embedded.pacts.iter().map(| p| {
1116 match p.links.get("self") {
1117 Some(l) => Ok((l.clone(), p.into())),
1118 None => Err(
1119 PactBrokerError::LinkError(
1120 format!(
1121 "Expected a HAL+JSON response from the pact broker, but got a link with no HREF. URL: '{}', PATH: '{:?}'",
1122 &hal_client.url,
1123 &p.links,
1124 )
1125 )
1126 )
1127 }
1128 }).collect();
1129
1130 links
1131 }
1132 None => Err(PactBrokerError::NotFound(format!(
1133 "No pacts were found for this provider"
1134 ))),
1135 }?;
1136
1137 let results: Vec<_> = futures::stream::iter(pact_links)
1138 .map(|(ref pact_link, ref context)| {
1139 match pact_link.href {
1140 Some(_) => Ok((hal_client.clone(), pact_link.clone(), context.clone())),
1141 None => Err(
1142 PactBrokerError::LinkError(
1143 format!(
1144 "Expected a HAL+JSON response from the pact broker, but got a link with no HREF. URL: '{}', LINK: '{:?}'",
1145 &hal_client.url,
1146 pact_link
1147 )
1148 )
1149 )
1150 }
1151 })
1152 .and_then(|(hal_client, pact_link, context)| async {
1153 let pact_json = hal_client.fetch_url(
1154 &pact_link.clone(),
1155 &template_values.clone()
1156 ).await?;
1157 Ok((pact_link, pact_json, context))
1158 })
1159 .map(|result| {
1160 match result {
1161 Ok((pact_link, pact_json, context)) => {
1162 let href = pact_link.href.unwrap_or_default();
1163 let links = links_from_json(&pact_json);
1164 load_pact_from_json(href.as_str(), &pact_json)
1165 .map(|pact| (pact, Some(context), links))
1166 .map_err(|err| PactBrokerError::ContentError(format!("{}", err)))
1167 },
1168 Err(err) => Err(err)
1169 }
1170 })
1171 .into_stream()
1172 .collect()
1173 .await;
1174
1175 Ok(results)
1176}
1177
1178pub async fn fetch_pact_from_url(
1182 url: &str,
1183 auth: &Option<HttpAuth>,
1184) -> anyhow::Result<(Box<dyn Pact + Send + Sync + RefUnwindSafe>, Vec<Link>)> {
1185 let url = url.to_string();
1186 let auth = auth.clone();
1187 let (url, pact_json) =
1188 tokio::task::spawn_blocking(move || http_utils::fetch_json_from_url(&url, &auth)).await??;
1189 let pact = load_pact_from_json(&url, &pact_json)?;
1190 let links = links_from_json(&pact_json);
1191 Ok((pact, links))
1192}
1193
1194async fn publish_provider_tags(
1195 hal_client: &HALClient,
1196 links: &[Link],
1197 provider_tags: Vec<String>,
1198 version: &str,
1199 headers: Option<HashMap<String, String>>,
1200) -> Result<(), PactBrokerError> {
1201 let hal_client = hal_client
1202 .clone()
1203 .with_doc_context(links)?
1204 .navigate("pb:provider", &hashmap! {})
1205 .await?;
1206 match hal_client.find_link("pb:version-tag") {
1207 Ok(link) => {
1208 for tag in &provider_tags {
1209 let template_values = hashmap! {
1210 "version".to_string() => version.to_string(),
1211 "tag".to_string() => tag.clone()
1212 };
1213 match hal_client
1214 .clone()
1215 .put_json(
1216 hal_client
1217 .clone()
1218 .parse_link_url(&link, &template_values)?
1219 .as_str(),
1220 "{}",
1221 headers.clone(),
1222 )
1223 .await
1224 {
1225 Ok(_) => debug!("Pushed tag {} for provider version {}", tag, version),
1226 Err(err) => {
1227 error!(
1228 "Failed to push tag {} for provider version {}",
1229 tag, version
1230 );
1231 return Err(err);
1232 }
1233 }
1234 }
1235 Ok(())
1236 }
1237 Err(_) => Err(PactBrokerError::LinkError(
1238 "Can't publish provider tags as there is no 'pb:version-tag' link".to_string(),
1239 )),
1240 }
1241}
1242
1243async fn publish_provider_branch(
1244 hal_client: &HALClient,
1245 links: &[Link],
1246 branch: &str,
1247 version: &str,
1248 headers: Option<HashMap<String, String>>,
1249) -> Result<(), PactBrokerError> {
1250 let hal_client = hal_client
1251 .clone()
1252 .with_doc_context(links)?
1253 .navigate("pb:provider", &hashmap! {})
1254 .await?;
1255
1256 match hal_client.find_link("pb:branch-version") {
1257 Ok(link) => {
1258 let template_values = hashmap! {
1259 "branch".to_string() => branch.to_string(),
1260 "version".to_string() => version.to_string(),
1261 };
1262 match hal_client.clone().put_json(hal_client.clone().parse_link_url(&link, &template_values)?.as_str(), "{}",headers).await {
1263 Ok(_) => debug!("Pushed branch {} for provider version {}", branch, version),
1264 Err(err) => {
1265 error!("Failed to push branch {} for provider version {}", branch, version);
1266 return Err(err);
1267 }
1268 }
1269 Ok(())
1270 },
1271 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()))
1272 }
1273}
1274
1275#[skip_serializing_none]
1276#[derive(Serialize, Deserialize, Debug, Clone)]
1277#[serde(rename_all = "camelCase")]
1278pub struct ConsumerVersionSelector {
1280 pub consumer: Option<String>,
1282 pub tag: Option<String>,
1284 pub fallback_tag: Option<String>,
1286 pub latest: Option<bool>,
1288 pub deployed_or_released: Option<bool>,
1290 pub deployed: Option<bool>,
1292 pub released: Option<bool>,
1294 pub environment: Option<String>,
1296 pub main_branch: Option<bool>,
1298 pub branch: Option<String>,
1300 pub matching_branch: Option<bool>,
1302}
1303
1304#[derive(Serialize, Deserialize, Debug, Clone)]
1305#[serde(rename_all = "camelCase")]
1306struct PactsForVerificationResponse {
1307 #[serde(rename(deserialize = "_embedded"))]
1308 pub embedded: PactsForVerificationBody,
1309}
1310
1311#[derive(Serialize, Deserialize, Debug, Clone)]
1312#[serde(rename_all = "camelCase")]
1313struct PactsForVerificationBody {
1314 pub pacts: Vec<PactForVerification>,
1315}
1316
1317#[derive(Serialize, Deserialize, Debug, Clone)]
1318#[serde(rename_all = "camelCase")]
1319struct PactForVerification {
1320 pub short_description: String,
1321 #[serde(rename(deserialize = "_links"))]
1322 pub links: HashMap<String, Link>,
1323 pub verification_properties: Option<PactVerificationProperties>,
1324}
1325
1326#[skip_serializing_none]
1327#[derive(Serialize, Deserialize, Debug, Clone)]
1328#[serde(rename_all = "camelCase")]
1329pub struct PactsForVerificationRequest {
1331 #[serde(skip_serializing_if = "Vec::is_empty")]
1333 pub provider_version_tags: Vec<String>,
1334 pub include_pending_status: bool,
1336 pub include_wip_pacts_since: Option<String>,
1338 pub consumer_version_selectors: Vec<ConsumerVersionSelector>,
1340 pub provider_version_branch: Option<String>,
1342}
1343
1344#[skip_serializing_none]
1345#[derive(Serialize, Deserialize, Debug, Clone)]
1346#[serde(rename_all = "camelCase")]
1347pub struct PactVerificationContext {
1349 pub short_description: String,
1351 pub verification_properties: PactVerificationProperties,
1353}
1354
1355impl From<&PactForVerification> for PactVerificationContext {
1356 fn from(value: &PactForVerification) -> Self {
1357 PactVerificationContext {
1358 short_description: value.short_description.clone(),
1359 verification_properties: value.verification_properties.clone().unwrap_or_default(),
1360 }
1361 }
1362}
1363
1364#[skip_serializing_none]
1365#[derive(Serialize, Deserialize, Debug, Clone, Default)]
1366#[serde(rename_all = "camelCase")]
1367pub struct PactVerificationProperties {
1369 #[serde(default)]
1370 pub pending: bool,
1372 pub notices: Vec<HashMap<String, String>>,
1374}
1375
1376#[cfg(test)]
1377mod hal_client_custom_headers_tests {
1378 use super::*;
1379 use crate::cli::pact_broker::main::types::SslOptions;
1380 use std::collections::HashMap;
1381
1382 fn create_test_custom_headers() -> CustomHeaders {
1383 let mut headers = HashMap::new();
1384 headers.insert("Authorization".to_string(), "Bearer test-token".to_string());
1385 headers.insert("X-API-Key".to_string(), "secret-key".to_string());
1386 CustomHeaders { headers }
1387 }
1388
1389 fn create_cloudflare_custom_headers() -> CustomHeaders {
1390 let mut headers = HashMap::new();
1391 headers.insert(
1392 "CF-Access-Client-Id".to_string(),
1393 "client-id-123".to_string(),
1394 );
1395 headers.insert(
1396 "CF-Access-Client-Secret".to_string(),
1397 "secret-456".to_string(),
1398 );
1399 CustomHeaders { headers }
1400 }
1401
1402 #[test]
1403 fn test_hal_client_with_custom_headers() {
1404 let custom_headers = Some(create_test_custom_headers());
1405 let ssl_options = SslOptions::default();
1406
1407 let client = HALClient::with_url(
1408 "https://test.example.com",
1409 None,
1410 ssl_options,
1411 custom_headers.clone(),
1412 );
1413
1414 assert_eq!(client.url, "https://test.example.com");
1415 assert!(client.custom_headers.is_some());
1416
1417 let headers = client.custom_headers.unwrap();
1418 assert_eq!(headers.headers.len(), 2);
1419 assert_eq!(
1420 headers.headers.get("Authorization"),
1421 Some(&"Bearer test-token".to_string())
1422 );
1423 assert_eq!(
1424 headers.headers.get("X-API-Key"),
1425 Some(&"secret-key".to_string())
1426 );
1427 }
1428
1429 #[test]
1430 fn test_hal_client_with_cloudflare_headers() {
1431 let custom_headers = Some(create_cloudflare_custom_headers());
1432 let ssl_options = SslOptions::default();
1433
1434 let client = HALClient::with_url(
1435 "https://pact-broker.example.com",
1436 None,
1437 ssl_options,
1438 custom_headers.clone(),
1439 );
1440
1441 assert!(client.custom_headers.is_some());
1442
1443 let headers = client.custom_headers.unwrap();
1444 assert_eq!(headers.headers.len(), 2);
1445 assert_eq!(
1446 headers.headers.get("CF-Access-Client-Id"),
1447 Some(&"client-id-123".to_string())
1448 );
1449 assert_eq!(
1450 headers.headers.get("CF-Access-Client-Secret"),
1451 Some(&"secret-456".to_string())
1452 );
1453 }
1454
1455 #[test]
1456 fn test_hal_client_without_custom_headers() {
1457 let ssl_options = SslOptions::default();
1458
1459 let client = HALClient::with_url("https://test.example.com", None, ssl_options, None);
1460
1461 assert!(client.custom_headers.is_none());
1462 }
1463
1464 #[test]
1465 fn test_hal_client_with_auth_and_custom_headers() {
1466 let auth = Some(HttpAuth::Token("bearer-token".to_string()));
1467 let custom_headers = Some(create_test_custom_headers());
1468 let ssl_options = SslOptions::default();
1469
1470 let client = HALClient::with_url(
1471 "https://test.example.com",
1472 auth.clone(),
1473 ssl_options,
1474 custom_headers,
1475 );
1476
1477 assert!(client.auth.is_some());
1478 assert!(client.custom_headers.is_some());
1479
1480 if let Some(HttpAuth::Token(token)) = client.auth {
1481 assert_eq!(token, "bearer-token");
1482 }
1483 }
1484
1485 #[test]
1486 fn test_apply_custom_headers_with_mock_request() {
1487 use reqwest::Client;
1488 use reqwest_middleware::ClientBuilder;
1489
1490 let custom_headers = Some(create_test_custom_headers());
1491 let ssl_options = SslOptions::default();
1492
1493 let client = HALClient::with_url(
1494 "https://test.example.com",
1495 None,
1496 ssl_options,
1497 custom_headers,
1498 );
1499
1500 let reqwest_client = Client::new();
1502 let middleware_client = ClientBuilder::new(reqwest_client).build();
1503 let request_builder = middleware_client.get("https://test.example.com/test");
1504
1505 let modified_builder = client.apply_custom_headers(request_builder);
1507
1508 let request = modified_builder.build().unwrap();
1510
1511 assert!(request.headers().contains_key("authorization"));
1513 assert!(request.headers().contains_key("x-api-key"));
1514
1515 assert_eq!(
1516 request
1517 .headers()
1518 .get("authorization")
1519 .unwrap()
1520 .to_str()
1521 .unwrap(),
1522 "Bearer test-token"
1523 );
1524 assert_eq!(
1525 request
1526 .headers()
1527 .get("x-api-key")
1528 .unwrap()
1529 .to_str()
1530 .unwrap(),
1531 "secret-key"
1532 );
1533 }
1534
1535 #[test]
1536 fn test_apply_custom_headers_without_headers() {
1537 use reqwest::Client;
1538 use reqwest_middleware::ClientBuilder;
1539
1540 let ssl_options = SslOptions::default();
1541
1542 let client = HALClient::with_url("https://test.example.com", None, ssl_options, None);
1543
1544 let reqwest_client = Client::new();
1546 let middleware_client = ClientBuilder::new(reqwest_client).build();
1547 let request_builder = middleware_client.get("https://test.example.com/test");
1548
1549 let modified_builder = client.apply_custom_headers(request_builder);
1551
1552 let request = modified_builder.build().unwrap();
1554
1555 assert!(!request.headers().contains_key("authorization"));
1557 assert!(!request.headers().contains_key("x-api-key"));
1558 }
1559
1560 #[test]
1561 fn test_custom_headers_struct_creation() {
1562 let mut headers = HashMap::new();
1563 headers.insert("Test-Header".to_string(), "test-value".to_string());
1564
1565 let custom_headers = CustomHeaders { headers };
1566
1567 assert_eq!(custom_headers.headers.len(), 1);
1568 assert_eq!(
1569 custom_headers.headers.get("Test-Header"),
1570 Some(&"test-value".to_string())
1571 );
1572 }
1573
1574 #[test]
1575 fn test_custom_headers_empty() {
1576 let headers = HashMap::new();
1577 let custom_headers = CustomHeaders { headers };
1578
1579 assert_eq!(custom_headers.headers.len(), 0);
1580 assert!(custom_headers.headers.is_empty());
1581 }
1582
1583 #[test]
1584 fn test_custom_headers_case_sensitivity() {
1585 let mut headers = HashMap::new();
1586 headers.insert("content-type".to_string(), "application/json".to_string());
1587 headers.insert("Content-Type".to_string(), "text/plain".to_string());
1588
1589 let custom_headers = CustomHeaders { headers };
1590
1591 assert_eq!(custom_headers.headers.len(), 2);
1593 assert_eq!(
1594 custom_headers.headers.get("content-type"),
1595 Some(&"application/json".to_string())
1596 );
1597 assert_eq!(
1598 custom_headers.headers.get("Content-Type"),
1599 Some(&"text/plain".to_string())
1600 );
1601 }
1602}