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