1use std::borrow::Cow;
2use std::fmt;
3use std::io::Read;
4use std::io::Write;
5use std::str::FromStr;
6
7use base64::prelude::BASE64_STANDARD;
8use base64::read::DecoderReader;
9use base64::write::EncoderWriter;
10use http::Uri;
11use reqsign::aws::DefaultSigner as AwsDefaultSigner;
12use reqsign::azure::DefaultSigner as AzureDefaultSigner;
13use reqsign::google::DefaultSigner as GcsDefaultSigner;
14use reqwest::Request;
15use reqwest::header::{HeaderName, HeaderValue};
16use serde::{Deserialize, Serialize};
17use thiserror::Error;
18use url::Url;
19
20use uv_netrc::Netrc;
21use uv_redacted::DisplaySafeUrl;
22use uv_static::EnvVars;
23
24const AZURE_STORAGE_VERSION: &str = "2023-11-03";
25
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub enum Credentials {
28 Basic {
30 username: Username,
32 password: Option<Password>,
34 },
35 Bearer {
37 token: Token,
39 },
40}
41
42#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Serialize, Deserialize)]
43#[serde(transparent)]
44pub struct Username(Option<String>);
45
46impl Username {
47 pub(crate) fn new(value: Option<String>) -> Self {
51 Self(value.filter(|s| !s.is_empty()))
53 }
54
55 pub(crate) fn none() -> Self {
56 Self::new(None)
57 }
58
59 pub(crate) fn is_none(&self) -> bool {
60 self.0.is_none()
61 }
62
63 pub(crate) fn is_some(&self) -> bool {
64 self.0.is_some()
65 }
66
67 pub(crate) fn as_deref(&self) -> Option<&str> {
68 self.0.as_deref()
69 }
70}
71
72impl From<String> for Username {
73 fn from(value: String) -> Self {
74 Self::new(Some(value))
75 }
76}
77
78impl From<Option<String>> for Username {
79 fn from(value: Option<String>) -> Self {
80 Self::new(value)
81 }
82}
83
84#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Serialize, Deserialize)]
85#[serde(transparent)]
86pub struct Password(String);
87
88impl Password {
89 pub fn new(password: String) -> Self {
90 Self(password)
91 }
92
93 fn as_str(&self) -> &str {
95 self.0.as_str()
96 }
97}
98
99impl fmt::Debug for Password {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 write!(f, "****")
102 }
103}
104
105#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default, Deserialize)]
106#[serde(transparent)]
107pub struct Token(Vec<u8>);
108
109impl Token {
110 pub(crate) fn new(token: Vec<u8>) -> Self {
111 Self(token)
112 }
113
114 fn as_slice(&self) -> &[u8] {
116 self.0.as_slice()
117 }
118
119 pub(crate) fn into_bytes(self) -> Vec<u8> {
121 self.0
122 }
123
124 fn is_empty(&self) -> bool {
126 self.0.is_empty()
127 }
128}
129
130impl fmt::Debug for Token {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 write!(f, "****")
133 }
134}
135impl Credentials {
136 #[allow(dead_code)]
138 pub fn basic(username: Option<String>, password: Option<String>) -> Self {
139 Self::Basic {
140 username: Username::new(username),
141 password: password.map(Password),
142 }
143 }
144
145 #[allow(dead_code)]
147 pub fn bearer(token: Vec<u8>) -> Self {
148 Self::Bearer {
149 token: Token::new(token),
150 }
151 }
152
153 pub fn username(&self) -> Option<&str> {
154 match self {
155 Self::Basic { username, .. } => username.as_deref(),
156 Self::Bearer { .. } => None,
157 }
158 }
159
160 fn to_username(&self) -> Username {
161 match self {
162 Self::Basic { username, .. } => username.clone(),
163 Self::Bearer { .. } => Username::none(),
164 }
165 }
166
167 fn as_username(&self) -> Cow<'_, Username> {
168 match self {
169 Self::Basic { username, .. } => Cow::Borrowed(username),
170 Self::Bearer { .. } => Cow::Owned(Username::none()),
171 }
172 }
173
174 pub fn password(&self) -> Option<&str> {
175 match self {
176 Self::Basic { password, .. } => password.as_ref().map(Password::as_str),
177 Self::Bearer { .. } => None,
178 }
179 }
180
181 fn is_authenticated(&self) -> bool {
182 match self {
183 Self::Basic {
184 username: _,
185 password,
186 } => password.is_some(),
187 Self::Bearer { token } => !token.is_empty(),
188 }
189 }
190
191 fn is_empty(&self) -> bool {
192 match self {
193 Self::Basic { username, password } => username.is_none() && password.is_none(),
194 Self::Bearer { token } => token.is_empty(),
195 }
196 }
197
198 pub(crate) fn from_netrc(
202 netrc: &Netrc,
203 url: &DisplaySafeUrl,
204 username: Option<&str>,
205 ) -> Option<Self> {
206 let host = url.host_str()?;
207 let entry = netrc
208 .hosts
209 .get(host)
210 .or_else(|| netrc.hosts.get("default"))?;
211
212 if username.is_some_and(|username| username != entry.login) {
214 return None;
215 }
216
217 Some(Self::Basic {
218 username: Username::new(Some(entry.login.clone())),
219 password: Some(Password(entry.password.clone())),
220 })
221 }
222
223 pub fn from_url(url: &Url) -> Option<Self> {
227 if url.username().is_empty() && url.password().is_none() {
228 return None;
229 }
230 Some(Self::Basic {
231 username: if url.username().is_empty() {
234 None
235 } else {
236 Some(
237 percent_encoding::percent_decode_str(url.username())
238 .decode_utf8_lossy()
239 .into_owned(),
240 )
241 }
242 .into(),
243 password: url.password().map(|password| {
244 Password(
245 percent_encoding::percent_decode_str(password)
246 .decode_utf8_lossy()
247 .into_owned(),
248 )
249 }),
250 })
251 }
252
253 pub fn from_env(name: impl AsRef<str>) -> Option<Self> {
258 let username = std::env::var(EnvVars::index_username(name.as_ref())).ok();
259 let password = std::env::var(EnvVars::index_password(name.as_ref())).ok();
260 if username.is_none() && password.is_none() {
261 None
262 } else {
263 Some(Self::basic(username, password))
264 }
265 }
266
267 pub(crate) fn from_request(request: &Request) -> Option<Self> {
271 Self::from_url(request.url()).or(
273 request
275 .headers()
276 .get(reqwest::header::AUTHORIZATION)
277 .map(Self::from_header_value)?,
278 )
279 }
280
281 fn from_header_value(header: &HeaderValue) -> Option<Self> {
290 if let Some(mut value) = header.as_bytes().strip_prefix(b"Basic ") {
292 let mut decoder = DecoderReader::new(&mut value, &BASE64_STANDARD);
293 let mut buf = String::new();
294 decoder
295 .read_to_string(&mut buf)
296 .expect("HTTP Basic Authentication should be base64 encoded");
297 let (username, password) = buf
298 .split_once(':')
299 .expect("HTTP Basic Authentication should include a `:` separator");
300 let username = if username.is_empty() {
301 None
302 } else {
303 Some(username.to_string())
304 };
305 let password = if password.is_empty() {
306 None
307 } else {
308 Some(password.to_string())
309 };
310 return Some(Self::Basic {
311 username: Username::new(username),
312 password: password.map(Password),
313 });
314 }
315
316 if let Some(token) = header.as_bytes().strip_prefix(b"Bearer ") {
318 return Some(Self::Bearer {
319 token: Token::new(token.to_vec()),
320 });
321 }
322
323 None
324 }
325
326 pub fn to_header_value(&self) -> HeaderValue {
330 match self {
331 Self::Basic { .. } => {
332 let mut buf = b"Basic ".to_vec();
334 {
335 let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
336 write!(encoder, "{}:", self.username().unwrap_or_default())
337 .expect("Write to base64 encoder should succeed");
338 if let Some(password) = self.password() {
339 write!(encoder, "{password}")
340 .expect("Write to base64 encoder should succeed");
341 }
342 }
343 let mut header =
344 HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue");
345 header.set_sensitive(true);
346 header
347 }
348 Self::Bearer { token } => {
349 let mut header = HeaderValue::from_bytes(&[b"Bearer ", token.as_slice()].concat())
350 .expect("Bearer token is always valid HeaderValue");
351 header.set_sensitive(true);
352 header
353 }
354 }
355 }
356
357 #[must_use]
361 pub fn apply(&self, mut url: DisplaySafeUrl) -> DisplaySafeUrl {
362 if let Some(username) = self.username() {
363 let _ = url.set_username(username);
364 }
365 if let Some(password) = self.password() {
366 let _ = url.set_password(Some(password));
367 }
368 url
369 }
370
371 #[must_use]
375 pub fn authenticate(&self, mut request: Request) -> Request {
376 request
377 .headers_mut()
378 .insert(reqwest::header::AUTHORIZATION, Self::to_header_value(self));
379 request
380 }
381}
382
383#[derive(Clone, Debug)]
384pub(crate) enum Authentication {
385 Credentials(Credentials),
387
388 AwsSigner(AwsDefaultSigner),
390
391 GcsSigner(GcsDefaultSigner),
393
394 AzureSigner(AzureDefaultSigner),
396}
397
398#[derive(Debug, Error)]
399pub(crate) enum AuthenticationError {
400 #[error("Failed to convert request URL to URI")]
401 InvalidUri(#[from] http::uri::InvalidUri),
402
403 #[error("Failed to build request for {provider} signing")]
404 BuildRequest {
405 provider: &'static str,
406 #[source]
407 source: http::Error,
408 },
409
410 #[error("Failed to sign request with {provider} credentials")]
411 Sign {
412 provider: &'static str,
413 #[source]
414 source: reqsign::Error,
415 },
416}
417
418impl PartialEq for Authentication {
419 fn eq(&self, other: &Self) -> bool {
420 match (self, other) {
421 (Self::Credentials(a), Self::Credentials(b)) => a == b,
422 (Self::AwsSigner(..), Self::AwsSigner(..)) => true,
423 (Self::GcsSigner(..), Self::GcsSigner(..)) => true,
424 (Self::AzureSigner(..), Self::AzureSigner(..)) => true,
425 _ => false,
426 }
427 }
428}
429
430impl Eq for Authentication {}
431
432impl From<Credentials> for Authentication {
433 fn from(credentials: Credentials) -> Self {
434 Self::Credentials(credentials)
435 }
436}
437
438impl From<AwsDefaultSigner> for Authentication {
439 fn from(signer: AwsDefaultSigner) -> Self {
440 Self::AwsSigner(signer)
441 }
442}
443
444impl From<GcsDefaultSigner> for Authentication {
445 fn from(signer: GcsDefaultSigner) -> Self {
446 Self::GcsSigner(signer)
447 }
448}
449
450impl From<AzureDefaultSigner> for Authentication {
451 fn from(signer: AzureDefaultSigner) -> Self {
452 Self::AzureSigner(signer)
453 }
454}
455
456impl Authentication {
457 pub(crate) fn password(&self) -> Option<&str> {
459 match self {
460 Self::Credentials(credentials) => credentials.password(),
461 Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => None,
462 }
463 }
464
465 pub(crate) fn username(&self) -> Option<&str> {
467 match self {
468 Self::Credentials(credentials) => credentials.username(),
469 Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => None,
470 }
471 }
472
473 pub(crate) fn as_username(&self) -> Cow<'_, Username> {
475 match self {
476 Self::Credentials(credentials) => credentials.as_username(),
477 Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => {
478 Cow::Owned(Username::none())
479 }
480 }
481 }
482
483 pub(crate) fn to_username(&self) -> Username {
485 match self {
486 Self::Credentials(credentials) => credentials.to_username(),
487 Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => Username::none(),
488 }
489 }
490
491 pub(crate) fn is_authenticated(&self) -> bool {
493 match self {
494 Self::Credentials(credentials) => credentials.is_authenticated(),
495 Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => true,
496 }
497 }
498
499 pub(crate) fn is_empty(&self) -> bool {
501 match self {
502 Self::Credentials(credentials) => credentials.is_empty(),
503 Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => false,
504 }
505 }
506
507 pub(crate) async fn authenticate(
511 &self,
512 mut request: Request,
513 ) -> Result<Request, AuthenticationError> {
514 match self {
515 Self::Credentials(credentials) => Ok(credentials.authenticate(request)),
516 Self::AwsSigner(signer) => {
517 let uri = Uri::from_str(request.url().as_str())?;
519 let mut http_req = http::Request::builder()
520 .method(request.method().clone())
521 .uri(uri)
522 .body(())
523 .map_err(|source| AuthenticationError::BuildRequest {
524 provider: "AWS",
525 source,
526 })?;
527 *http_req.headers_mut() = request.headers().clone();
528
529 let (mut parts, ()) = http_req.into_parts();
531 signer.sign(&mut parts, None).await.map_err(|source| {
532 AuthenticationError::Sign {
533 provider: "AWS",
534 source,
535 }
536 })?;
537
538 request.headers_mut().extend(parts.headers);
540
541 if let Some(path_and_query) = parts.uri.path_and_query() {
543 request.url_mut().set_path(path_and_query.path());
544 request.url_mut().set_query(path_and_query.query());
545 }
546 Ok(request)
547 }
548 Self::GcsSigner(signer) => {
549 let uri = Uri::from_str(request.url().as_str())?;
551 let mut http_req = http::Request::builder()
552 .method(request.method().clone())
553 .uri(uri)
554 .body(())
555 .map_err(|source| AuthenticationError::BuildRequest {
556 provider: "GCS",
557 source,
558 })?;
559 *http_req.headers_mut() = request.headers().clone();
560
561 let (mut parts, ()) = http_req.into_parts();
563 signer.sign(&mut parts, None).await.map_err(|source| {
564 AuthenticationError::Sign {
565 provider: "GCS",
566 source,
567 }
568 })?;
569
570 request.headers_mut().extend(parts.headers);
572
573 if let Some(path_and_query) = parts.uri.path_and_query() {
575 request.url_mut().set_path(path_and_query.path());
576 request.url_mut().set_query(path_and_query.query());
577 }
578 Ok(request)
579 }
580 Self::AzureSigner(signer) => {
581 let uri = Uri::from_str(request.url().as_str())?;
583 let mut http_req = http::Request::builder()
584 .method(request.method().clone())
585 .uri(uri)
586 .body(())
587 .map_err(|source| AuthenticationError::BuildRequest {
588 provider: "Azure",
589 source,
590 })?;
591 *http_req.headers_mut() = request.headers().clone();
592 http_req
593 .headers_mut()
594 .entry(HeaderName::from_static("x-ms-version"))
595 .or_insert(HeaderValue::from_static(AZURE_STORAGE_VERSION));
596
597 let (mut parts, ()) = http_req.into_parts();
599 signer.sign(&mut parts, None).await.map_err(|source| {
600 AuthenticationError::Sign {
601 provider: "Azure",
602 source,
603 }
604 })?;
605
606 request.headers_mut().extend(parts.headers);
608
609 if let Some(path_and_query) = parts.uri.path_and_query() {
611 request.url_mut().set_path(path_and_query.path());
612 request.url_mut().set_query(path_and_query.query());
613 }
614 Ok(request)
615 }
616 }
617 }
618}
619
620#[cfg(test)]
621mod tests {
622 use insta::assert_debug_snapshot;
623 use reqsign::aws::Credential as AwsCredential;
624 use reqsign::azure::Credential as AzureCredential;
625 use reqsign::{Context, ProvideCredential};
626
627 use super::*;
628
629 #[derive(Debug)]
630 struct EmptyAwsCredentialProvider;
631
632 impl ProvideCredential for EmptyAwsCredentialProvider {
633 type Credential = AwsCredential;
634
635 async fn provide_credential(
636 &self,
637 _ctx: &Context,
638 ) -> reqsign::Result<Option<Self::Credential>> {
639 Ok(None)
640 }
641 }
642
643 #[derive(Debug)]
644 struct EmptyAzureCredentialProvider;
645
646 impl ProvideCredential for EmptyAzureCredentialProvider {
647 type Credential = AzureCredential;
648
649 async fn provide_credential(
650 &self,
651 _ctx: &Context,
652 ) -> reqsign::Result<Option<Self::Credential>> {
653 Ok(None)
654 }
655 }
656
657 #[test]
658 fn from_url_no_credentials() {
659 let url = &Url::parse("https://example.com/simple/first/").unwrap();
660 assert_eq!(Credentials::from_url(url), None);
661 }
662
663 #[test]
664 fn from_url_username_and_password() {
665 let url = &Url::parse("https://example.com/simple/first/").unwrap();
666 let mut auth_url = url.clone();
667 auth_url.set_username("user").unwrap();
668 auth_url.set_password(Some("password")).unwrap();
669 let credentials = Credentials::from_url(&auth_url).unwrap();
670 assert_eq!(credentials.username(), Some("user"));
671 assert_eq!(credentials.password(), Some("password"));
672 }
673
674 #[test]
675 fn from_url_invalid_utf8_username() {
676 let url = Url::parse("https://%FF:password@example.com/simple/first/").unwrap();
677 let credentials = Credentials::from_url(&url).unwrap();
678 assert_eq!(credentials.username(), Some("\u{fffd}"));
679 assert_eq!(credentials.password(), Some("password"));
680 }
681
682 #[test]
683 fn from_url_invalid_utf8_password() {
684 let url = Url::parse("https://user:%FF@example.com/simple/first/").unwrap();
685 let credentials = Credentials::from_url(&url).unwrap();
686 assert_eq!(credentials.username(), Some("user"));
687 assert_eq!(credentials.password(), Some("\u{fffd}"));
688 }
689
690 #[test]
691 fn from_url_no_username() {
692 let url = &Url::parse("https://example.com/simple/first/").unwrap();
693 let mut auth_url = url.clone();
694 auth_url.set_password(Some("password")).unwrap();
695 let credentials = Credentials::from_url(&auth_url).unwrap();
696 assert_eq!(credentials.username(), None);
697 assert_eq!(credentials.password(), Some("password"));
698 }
699
700 #[test]
705 fn from_url_empty_username_with_password() {
706 let url = Url::parse("https://:token@example.com/simple/first/").unwrap();
708 let credentials = Credentials::from_url(&url).unwrap();
709 assert_eq!(credentials.username(), None);
710 assert_eq!(credentials.password(), Some("token"));
711 assert!(
712 credentials.is_authenticated(),
713 "URL with empty username but password should be considered authenticated"
714 );
715 }
716
717 #[test]
718 fn from_url_no_password() {
719 let url = &Url::parse("https://example.com/simple/first/").unwrap();
720 let mut auth_url = url.clone();
721 auth_url.set_username("user").unwrap();
722 let credentials = Credentials::from_url(&auth_url).unwrap();
723 assert_eq!(credentials.username(), Some("user"));
724 assert_eq!(credentials.password(), None);
725 }
726
727 #[test]
728 fn authenticated_request_from_url() {
729 let url = Url::parse("https://example.com/simple/first/").unwrap();
730 let mut auth_url = url.clone();
731 auth_url.set_username("user").unwrap();
732 auth_url.set_password(Some("password")).unwrap();
733 let credentials = Credentials::from_url(&auth_url).unwrap();
734
735 let mut request = Request::new(reqwest::Method::GET, url);
736 request = credentials.authenticate(request);
737
738 let mut header = request
739 .headers()
740 .get(reqwest::header::AUTHORIZATION)
741 .expect("Authorization header should be set")
742 .clone();
743 header.set_sensitive(false);
744
745 assert_debug_snapshot!(header, @r#""Basic dXNlcjpwYXNzd29yZA==""#);
746 assert_eq!(Credentials::from_header_value(&header), Some(credentials));
747 }
748
749 #[test]
750 fn authenticated_request_from_url_with_percent_encoded_user() {
751 let url = Url::parse("https://example.com/simple/first/").unwrap();
752 let mut auth_url = url.clone();
753 auth_url.set_username("user@domain").unwrap();
754 auth_url.set_password(Some("password")).unwrap();
755 let credentials = Credentials::from_url(&auth_url).unwrap();
756
757 let mut request = Request::new(reqwest::Method::GET, url);
758 request = credentials.authenticate(request);
759
760 let mut header = request
761 .headers()
762 .get(reqwest::header::AUTHORIZATION)
763 .expect("Authorization header should be set")
764 .clone();
765 header.set_sensitive(false);
766
767 assert_debug_snapshot!(header, @r#""Basic dXNlckBkb21haW46cGFzc3dvcmQ=""#);
768 assert_eq!(Credentials::from_header_value(&header), Some(credentials));
769 }
770
771 #[test]
772 fn authenticated_request_from_url_with_percent_encoded_password() {
773 let url = Url::parse("https://example.com/simple/first/").unwrap();
774 let mut auth_url = url.clone();
775 auth_url.set_username("user").unwrap();
776 auth_url.set_password(Some("password==")).unwrap();
777 let credentials = Credentials::from_url(&auth_url).unwrap();
778
779 let mut request = Request::new(reqwest::Method::GET, url);
780 request = credentials.authenticate(request);
781
782 let mut header = request
783 .headers()
784 .get(reqwest::header::AUTHORIZATION)
785 .expect("Authorization header should be set")
786 .clone();
787 header.set_sensitive(false);
788
789 assert_debug_snapshot!(header, @r#""Basic dXNlcjpwYXNzd29yZD09""#);
790 assert_eq!(Credentials::from_header_value(&header), Some(credentials));
791 }
792
793 #[tokio::test]
794 async fn authenticated_request_with_azure_signer() {
795 let signer = reqsign::azure::default_signer().with_credential_provider(
796 reqsign::azure::StaticCredentialProvider::new_bearer_token("token"),
797 );
798 let authentication = Authentication::from(signer);
799
800 let request = Request::new(
801 reqwest::Method::GET,
802 Url::parse("https://account.blob.core.windows.net/container/blob.whl").unwrap(),
803 );
804 let request = authentication.authenticate(request).await.unwrap();
805
806 let authorization = request
807 .headers()
808 .get(reqwest::header::AUTHORIZATION)
809 .expect("Authorization header should be set");
810 assert_eq!(authorization.to_str().unwrap(), "Bearer token");
811 assert!(request.headers().contains_key("x-ms-date"));
812 assert_eq!(
813 request
814 .headers()
815 .get("x-ms-version")
816 .expect("x-ms-version header should be set")
817 .to_str()
818 .unwrap(),
819 AZURE_STORAGE_VERSION
820 );
821 }
822
823 #[tokio::test]
824 async fn authenticated_request_with_aws_signer_missing_credentials() {
825 let signer = reqsign::aws::default_signer("s3", "us-east-1")
826 .with_credential_provider(EmptyAwsCredentialProvider);
827 let authentication = Authentication::from(signer);
828
829 let request = Request::new(
830 reqwest::Method::GET,
831 Url::parse("https://s3.amazonaws.com/bucket/blob.whl").unwrap(),
832 );
833 let err = authentication.authenticate(request).await.unwrap_err();
834
835 insta::assert_snapshot!(
836 err.to_string(),
837 @"Failed to sign request with AWS credentials"
838 );
839 }
840
841 #[tokio::test]
842 async fn authenticated_request_with_azure_signer_missing_credentials() {
843 let signer =
844 reqsign::azure::default_signer().with_credential_provider(EmptyAzureCredentialProvider);
845 let authentication = Authentication::from(signer);
846
847 let request = Request::new(
848 reqwest::Method::GET,
849 Url::parse("https://account.blob.core.windows.net/container/blob.whl").unwrap(),
850 );
851 let err = authentication.authenticate(request).await.unwrap_err();
852
853 insta::assert_snapshot!(
854 err.to_string(),
855 @"Failed to sign request with Azure credentials"
856 );
857 }
858
859 #[test]
861 fn test_password_redaction() {
862 let credentials =
863 Credentials::basic(Some(String::from("user")), Some(String::from("password")));
864 insta::assert_compact_debug_snapshot!(credentials, @r#"Basic { username: Username(Some("user")), password: Some(****) }"#);
865 }
866
867 #[test]
869 fn test_bearer_token_redaction() {
870 let token = "super_secret_token";
871 let credentials = Credentials::bearer(token.into());
872 insta::assert_compact_debug_snapshot!(credentials, @"Bearer { token: **** }");
873 }
874}