1use std::sync::{Arc, LazyLock};
2
3use anyhow::{anyhow, format_err};
4use http::{Extensions, StatusCode};
5use reqwest::{Request, Response};
6use reqwest_middleware::{Error, Middleware, Next};
7use tokio::sync::Mutex;
8use tracing::{debug, trace, warn};
9
10use uv_netrc::Netrc;
11use uv_preview::{Preview, PreviewFeature};
12use uv_redacted::DisplaySafeUrl;
13use uv_static::EnvVars;
14
15use crate::providers::{
16 AzureEndpointProvider, GcsEndpointProvider, HuggingFaceProvider, S3EndpointProvider,
17};
18use crate::{
19 CredentialsCache, KeyringProvider,
20 cache::FetchUrl,
21 credentials::{
22 Authentication, AuthenticationError, Credentials, CredentialsFromUrlError, Username,
23 },
24 index::{AuthPolicy, Indexes},
25 realm::Realm,
26};
27use crate::{Index, TextCredentialStore};
28
29static IS_DEPENDABOT: LazyLock<bool> =
31 LazyLock::new(|| std::env::var(EnvVars::DEPENDABOT).is_ok_and(|value| value == "true"));
32
33impl From<AuthenticationError> for Error {
34 fn from(err: AuthenticationError) -> Self {
35 Self::middleware(err)
36 }
37}
38
39impl From<CredentialsFromUrlError> for Error {
40 fn from(err: CredentialsFromUrlError) -> Self {
41 Self::middleware(err)
42 }
43}
44
45enum NetrcMode {
47 Automatic(LazyLock<Option<Netrc>>),
48 #[cfg(test)]
49 Enabled(Netrc),
50 #[cfg(test)]
51 Disabled,
52}
53
54impl Default for NetrcMode {
55 fn default() -> Self {
56 Self::Automatic(LazyLock::new(|| match Netrc::new() {
57 Ok(netrc) => Some(netrc),
58 Err(uv_netrc::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
59 debug!("No netrc file found");
60 None
61 }
62 Err(err) => {
63 warn!("Error reading netrc file: {err}");
64 None
65 }
66 }))
67 }
68}
69
70impl NetrcMode {
71 fn get(&self) -> Option<&Netrc> {
73 match self {
74 Self::Automatic(lock) => lock.as_ref(),
75 #[cfg(test)]
76 Self::Enabled(netrc) => Some(netrc),
77 #[cfg(test)]
78 Self::Disabled => None,
79 }
80 }
81}
82
83enum TextStoreMode {
85 Automatic(tokio::sync::OnceCell<Option<TextCredentialStore>>),
86 #[cfg(test)]
87 Enabled(TextCredentialStore),
88 #[cfg(test)]
89 Disabled,
90}
91
92impl Default for TextStoreMode {
93 fn default() -> Self {
94 Self::Automatic(tokio::sync::OnceCell::new())
95 }
96}
97
98impl TextStoreMode {
99 async fn load_default_store() -> Option<TextCredentialStore> {
100 let path = TextCredentialStore::default_file()
101 .inspect_err(|err| {
102 warn!("Failed to determine credentials file path: {}", err);
103 })
104 .ok()?;
105
106 match TextCredentialStore::read(&path).await {
107 Ok((store, _lock)) => {
108 debug!("Loaded credential file {}", path.display());
109 Some(store)
110 }
111 Err(err)
112 if err
113 .as_io_error()
114 .is_some_and(|err| err.kind() == std::io::ErrorKind::NotFound) =>
115 {
116 debug!("No credentials file found at {}", path.display());
117 None
118 }
119 Err(err) => {
120 warn!(
121 "Failed to load credentials from {}: {}",
122 path.display(),
123 err
124 );
125 None
126 }
127 }
128 }
129
130 async fn get(&self) -> Option<&TextCredentialStore> {
132 match self {
133 Self::Automatic(lock) => lock.get_or_init(Self::load_default_store).await.as_ref(),
136 #[cfg(test)]
137 Self::Enabled(store) => Some(store),
138 #[cfg(test)]
139 Self::Disabled => None,
140 }
141 }
142}
143
144#[derive(Clone)]
145enum S3CredentialState {
146 Uninitialized,
148 Initialized(Option<Arc<Authentication>>),
151}
152
153#[derive(Clone)]
154enum GcsCredentialState {
155 Uninitialized,
157 Initialized(Option<Arc<Authentication>>),
160}
161
162#[derive(Clone)]
163enum AzureCredentialState {
164 Uninitialized,
166 Initialized(Option<Arc<Authentication>>),
169}
170
171pub struct AuthMiddleware {
176 netrc: NetrcMode,
177 text_store: TextStoreMode,
178 keyring: Option<KeyringProvider>,
179 cache: Arc<CredentialsCache>,
181 indexes: Indexes,
183 only_authenticated: bool,
186 s3_credential_state: Mutex<S3CredentialState>,
188 gcs_credential_state: Mutex<GcsCredentialState>,
190 azure_credential_state: Mutex<AzureCredentialState>,
192 preview: Preview,
193}
194
195impl Default for AuthMiddleware {
196 fn default() -> Self {
197 Self::new()
198 }
199}
200
201impl AuthMiddleware {
202 pub fn new() -> Self {
203 Self {
204 netrc: NetrcMode::default(),
205 text_store: TextStoreMode::default(),
206 keyring: None,
207 cache: Arc::new(CredentialsCache::default()),
209 indexes: Indexes::new(),
210 only_authenticated: false,
211 s3_credential_state: Mutex::new(S3CredentialState::Uninitialized),
212 gcs_credential_state: Mutex::new(GcsCredentialState::Uninitialized),
213 azure_credential_state: Mutex::new(AzureCredentialState::Uninitialized),
214 preview: Preview::default(),
215 }
216 }
217
218 #[must_use]
222 #[cfg(test)]
223 fn with_netrc(mut self, netrc: Option<Netrc>) -> Self {
224 self.netrc = if let Some(netrc) = netrc {
225 NetrcMode::Enabled(netrc)
226 } else {
227 NetrcMode::Disabled
228 };
229 self
230 }
231
232 #[must_use]
236 #[cfg(test)]
237 fn with_text_store(mut self, store: Option<TextCredentialStore>) -> Self {
238 self.text_store = if let Some(store) = store {
239 TextStoreMode::Enabled(store)
240 } else {
241 TextStoreMode::Disabled
242 };
243 self
244 }
245
246 #[must_use]
248 pub fn with_keyring(mut self, keyring: Option<KeyringProvider>) -> Self {
249 self.keyring = keyring;
250 self
251 }
252
253 #[must_use]
255 pub fn with_preview(mut self, preview: Preview) -> Self {
256 self.preview = preview;
257 self
258 }
259
260 #[must_use]
262 #[cfg(test)]
263 fn with_cache(mut self, cache: CredentialsCache) -> Self {
264 self.cache = Arc::new(cache);
265 self
266 }
267
268 #[must_use]
270 pub fn with_cache_arc(mut self, cache: Arc<CredentialsCache>) -> Self {
271 self.cache = cache;
272 self
273 }
274
275 #[must_use]
277 pub fn with_indexes(mut self, indexes: Indexes) -> Self {
278 self.indexes = indexes;
279 self
280 }
281
282 #[must_use]
285 pub fn with_only_authenticated(mut self, only_authenticated: bool) -> Self {
286 self.only_authenticated = only_authenticated;
287 self
288 }
289
290 fn cache(&self) -> &CredentialsCache {
292 &self.cache
293 }
294}
295
296#[async_trait::async_trait]
297impl Middleware for AuthMiddleware {
298 async fn handle(
335 &self,
336 mut request: Request,
337 extensions: &mut Extensions,
338 next: Next<'_>,
339 ) -> reqwest_middleware::Result<Response> {
340 let request_credentials = Credentials::from_request(&request)?.map(Authentication::from);
342
343 let url = tracing_url(&request, request_credentials.as_ref());
346 let index = self.indexes.index_for(request.url());
347 let auth_policy = self.indexes.auth_policy_for(request.url());
348 trace!("Handling request for {url} with authentication policy {auth_policy}");
349
350 let credentials: Option<Arc<Authentication>> = if matches!(auth_policy, AuthPolicy::Never) {
351 None
352 } else {
353 if let Some(request_credentials) = request_credentials {
354 return self
355 .complete_request_with_request_credentials(
356 request_credentials,
357 request,
358 extensions,
359 next,
360 &url,
361 index,
362 auth_policy,
363 )
364 .await;
365 }
366
367 trace!("Request for {url} is unauthenticated, checking cache");
369
370 let credentials = self
373 .cache()
374 .get_url(DisplaySafeUrl::ref_cast(request.url()), &Username::none());
375 if let Some(credentials) = credentials.as_ref() {
376 request = credentials.authenticate(request).await?;
377
378 if credentials.is_authenticated() {
380 trace!("Request for {url} is fully authenticated");
381 return self
382 .complete_request(None, request, extensions, next, auth_policy)
383 .await;
384 }
385
386 trace!("Found username for {url} in cache, attempting request");
389 }
390 credentials
391 };
392 let attempt_has_username = credentials
393 .as_ref()
394 .is_some_and(|credentials| credentials.username().is_some());
395
396 let must_authenticate = self.only_authenticated
397 || (matches!(auth_policy, AuthPolicy::Always)
398 && !*IS_DEPENDABOT);
402
403 let (mut retry_request, response) = if !must_authenticate {
404 let url = tracing_url(&request, credentials.as_deref());
405 if credentials.is_none() {
406 trace!("Attempting unauthenticated request for {url}");
407 } else {
408 trace!("Attempting partially authenticated request for {url}");
409 }
410
411 let retry_request = request.try_clone().ok_or_else(|| {
414 Error::Middleware(anyhow!(
415 "Request object is not cloneable. Are you passing a streaming body?"
416 .to_string()
417 ))
418 })?;
419
420 let response = next.clone().run(request, extensions).await?;
421
422 if !matches!(
425 response.status(),
426 StatusCode::FORBIDDEN | StatusCode::NOT_FOUND | StatusCode::UNAUTHORIZED
427 ) || matches!(auth_policy, AuthPolicy::Never)
428 {
429 return Ok(response);
430 }
431
432 trace!(
434 "Request for {url} failed with {}, checking for credentials",
435 response.status()
436 );
437
438 (retry_request, Some(response))
439 } else {
440 trace!("Checking for credentials for {url}");
443 (request, None)
444 };
445 let retry_request_url = DisplaySafeUrl::ref_cast(retry_request.url());
446
447 let username = credentials
448 .as_ref()
449 .map(|credentials| credentials.to_username())
450 .unwrap_or(Username::none());
451 let credentials = if let Some(index) = index {
452 self.cache().get_url(&index.url, &username).or_else(|| {
453 self.cache()
454 .get_realm(Realm::from(&**retry_request_url), username)
455 })
456 } else {
457 self.cache()
460 .get_realm(Realm::from(&**retry_request_url), username)
461 }
462 .or(credentials);
463
464 if let Some(credentials) = credentials.as_ref() {
465 if credentials.is_authenticated() {
466 trace!("Retrying request for {url} with credentials from cache {credentials:?}");
467 retry_request = credentials.authenticate(retry_request).await?;
468 return self
469 .complete_request(None, retry_request, extensions, next, auth_policy)
470 .await;
471 }
472 }
473
474 if let Some(credentials) = self
477 .fetch_credentials(
478 credentials.as_deref(),
479 retry_request_url,
480 index,
481 auth_policy,
482 )
483 .await?
484 {
485 retry_request = credentials.authenticate(retry_request).await?;
486 trace!("Retrying request for {url} with {credentials:?}");
487 return self
488 .complete_request(
489 Some(credentials),
490 retry_request,
491 extensions,
492 next,
493 auth_policy,
494 )
495 .await;
496 }
497
498 if let Some(credentials) = credentials.as_ref() {
499 if !attempt_has_username {
500 trace!("Retrying request for {url} with username from cache {credentials:?}");
501 retry_request = credentials.authenticate(retry_request).await?;
502 return self
503 .complete_request(None, retry_request, extensions, next, auth_policy)
504 .await;
505 }
506 }
507
508 if let Some(response) = response {
509 Ok(response)
510 } else {
511 Err(Error::Middleware(format_err!(
512 "Missing credentials for {url}"
513 )))
514 }
515 }
516}
517
518impl AuthMiddleware {
519 async fn complete_request(
523 &self,
524 credentials: Option<Arc<Authentication>>,
525 request: Request,
526 extensions: &mut Extensions,
527 next: Next<'_>,
528 auth_policy: AuthPolicy,
529 ) -> reqwest_middleware::Result<Response> {
530 let Some(credentials) = credentials else {
531 return next.run(request, extensions).await;
533 };
534 let url = DisplaySafeUrl::from_url(request.url().clone());
535 if matches!(auth_policy, AuthPolicy::Always) && !credentials.is_authenticated() {
536 return Err(Error::Middleware(format_err!(
537 "Incomplete credentials for {url}"
538 )));
539 }
540 let result = next.run(request, extensions).await;
541
542 if result
544 .as_ref()
545 .is_ok_and(|response| response.error_for_status_ref().is_ok())
546 {
547 trace!("Updating cached credentials for {url} to {credentials:?}");
549 self.cache().insert(&url, credentials);
550 }
551
552 result
553 }
554
555 async fn complete_request_with_request_credentials(
557 &self,
558 credentials: Authentication,
559 mut request: Request,
560 extensions: &mut Extensions,
561 next: Next<'_>,
562 url: &DisplaySafeUrl,
563 index: Option<&Index>,
564 auth_policy: AuthPolicy,
565 ) -> reqwest_middleware::Result<Response> {
566 let credentials = Arc::new(credentials);
567
568 if credentials.is_authenticated() {
570 trace!("Request for {url} already contains complete authentication");
571 return self
572 .complete_request(Some(credentials), request, extensions, next, auth_policy)
573 .await;
574 }
575
576 trace!("Request for {url} is missing a password, looking for credentials");
577
578 let maybe_cached_credentials = if let Some(index) = index {
582 self.cache()
583 .get_url(&index.url, credentials.as_username().as_ref())
584 .or_else(|| {
585 self.cache()
586 .get_url(&index.root_url, credentials.as_username().as_ref())
587 })
588 } else {
589 self.cache()
590 .get_realm(Realm::from(request.url()), credentials.to_username())
591 };
592 if let Some(credentials) = maybe_cached_credentials {
593 request = credentials.authenticate(request).await?;
594 let credentials = None;
596 return self
597 .complete_request(credentials, request, extensions, next, auth_policy)
598 .await;
599 }
600
601 let credentials = if let Some(credentials) = self.cache().get_url(
602 DisplaySafeUrl::ref_cast(request.url()),
603 credentials.as_username().as_ref(),
604 ) {
605 request = credentials.authenticate(request).await?;
606 None
608 } else if let Some(credentials) = self
609 .fetch_credentials(
610 Some(&credentials),
611 DisplaySafeUrl::ref_cast(request.url()),
612 index,
613 auth_policy,
614 )
615 .await?
616 {
617 request = credentials.authenticate(request).await?;
618 Some(credentials)
619 } else if index.is_some() {
620 if let Some(credentials) = self
622 .cache()
623 .get_realm(Realm::from(request.url()), credentials.to_username())
624 {
625 request = credentials.authenticate(request).await?;
626 Some(credentials)
627 } else {
628 Some(credentials)
629 }
630 } else {
631 Some(credentials)
633 };
634
635 self.complete_request(credentials, request, extensions, next, auth_policy)
636 .await
637 }
638
639 async fn fetch_credentials(
643 &self,
644 credentials: Option<&Authentication>,
645 url: &DisplaySafeUrl,
646 index: Option<&Index>,
647 auth_policy: AuthPolicy,
648 ) -> reqwest_middleware::Result<Option<Arc<Authentication>>> {
649 let is_s3_endpoint =
650 S3EndpointProvider::is_s3_endpoint(url, self.preview).map_err(Error::Middleware)?;
651 let is_gcs_endpoint =
652 GcsEndpointProvider::is_gcs_endpoint(url, self.preview).map_err(Error::Middleware)?;
653 let is_azure_endpoint = AzureEndpointProvider::is_azure_endpoint(url, self.preview)
654 .map_err(Error::Middleware)?;
655 let username = Username::from(
656 credentials.map(|credentials| credentials.username().unwrap_or_default().to_string()),
657 );
658
659 let key = if let Some(index) = index {
662 (FetchUrl::Index(index.url.clone()), username)
663 } else {
664 (FetchUrl::Realm(Realm::from(&**url)), username)
665 };
666 if let Some(credentials) = self.cache().fetches.register_or_wait(&key).await {
667 if credentials.is_some() {
668 trace!("Using credentials from previous fetch for {}", key.0);
669 } else {
670 trace!(
671 "Skipping fetch of credentials for {}, previous attempt failed",
672 key.0
673 );
674 }
675
676 return Ok(credentials);
677 }
678
679 if let Some(credentials) = HuggingFaceProvider::credentials_for(url)
681 .map(Authentication::from)
682 .map(Arc::new)
683 {
684 debug!("Found Hugging Face credentials for {url}");
685 self.cache().fetches.done(key, Some(credentials.clone()));
686 return Ok(Some(credentials));
687 }
688
689 if is_s3_endpoint {
690 let mut s3_state = self.s3_credential_state.lock().await;
691
692 let credentials = match &*s3_state {
694 S3CredentialState::Uninitialized => {
695 trace!("Initializing S3 credentials for {url}");
696 let signer = S3EndpointProvider::create_signer();
697 let credentials = Arc::new(Authentication::from(signer));
698 *s3_state = S3CredentialState::Initialized(Some(credentials.clone()));
699 Some(credentials)
700 }
701 S3CredentialState::Initialized(credentials) => credentials.clone(),
702 };
703
704 if let Some(credentials) = credentials {
705 debug!("Found S3 credentials for {url}");
706 self.cache().fetches.done(key, Some(credentials.clone()));
707 return Ok(Some(credentials));
708 }
709 }
710
711 if is_gcs_endpoint {
712 let mut gcs_state = self.gcs_credential_state.lock().await;
713
714 let credentials = match &*gcs_state {
716 GcsCredentialState::Uninitialized => {
717 trace!("Initializing GCS credentials for {url}");
718 let signer = GcsEndpointProvider::create_signer();
719 let credentials = Arc::new(Authentication::from(signer));
720 *gcs_state = GcsCredentialState::Initialized(Some(credentials.clone()));
721 Some(credentials)
722 }
723 GcsCredentialState::Initialized(credentials) => credentials.clone(),
724 };
725
726 if let Some(credentials) = credentials {
727 debug!("Found GCS credentials for {url}");
728 self.cache().fetches.done(key, Some(credentials.clone()));
729 return Ok(Some(credentials));
730 }
731 }
732
733 if is_azure_endpoint {
734 let mut azure_state = self.azure_credential_state.lock().await;
735
736 let credentials = match &*azure_state {
738 AzureCredentialState::Uninitialized => {
739 trace!("Initializing Azure credentials for {url}");
740 let signer = AzureEndpointProvider::create_signer();
741 let credentials = Arc::new(Authentication::from(signer));
742 *azure_state = AzureCredentialState::Initialized(Some(credentials.clone()));
743 Some(credentials)
744 }
745 AzureCredentialState::Initialized(credentials) => credentials.clone(),
746 };
747
748 if let Some(credentials) = credentials {
749 debug!("Found Azure credentials for {url}");
750 self.cache().fetches.done(key, Some(credentials.clone()));
751 return Ok(Some(credentials));
752 }
753 }
754
755 let credentials = if let Some(credentials) = self.netrc.get().and_then(|netrc| {
757 debug!("Checking netrc for credentials for {url}");
758 Credentials::from_netrc(
759 netrc,
760 url,
761 credentials
762 .as_ref()
763 .and_then(|credentials| credentials.username()),
764 )
765 }) {
766 debug!("Found credentials in netrc file for {url}");
767 Some(credentials)
768
769 } else if let Some(credentials) = self.text_store.get().await.and_then(|text_store| {
771 debug!("Checking text store for credentials for {url}");
772 match text_store.get_credentials(
773 url,
774 credentials
775 .as_ref()
776 .and_then(|credentials| credentials.username()),
777 ) {
778 Ok(credentials) => credentials.cloned(),
779 Err(err) => {
780 debug!("Failed to get credentials from text store: {err}");
781 None
782 }
783 }
784 }) {
785 debug!("Found credentials in plaintext store for {url}");
786 Some(credentials)
787 } else if let Some(credentials) = {
788 if self.preview.is_enabled(PreviewFeature::NativeAuth) {
789 let native_store = KeyringProvider::native();
790 let username = credentials.and_then(|credentials| credentials.username());
791 let display_username = if let Some(username) = username {
792 format!("{username}@")
793 } else {
794 String::new()
795 };
796 if let Some(index) = index {
797 debug!(
800 "Checking native store for credentials for index URL {}{}",
801 display_username, index.root_url
802 );
803 native_store.fetch(&index.root_url, username).await
804 } else {
805 debug!(
806 "Checking native store for credentials for URL {}{}",
807 display_username, url
808 );
809 native_store.fetch(url, username).await
810 }
811 } else {
813 None
814 }
815 } {
816 debug!("Found credentials in native store for {url}");
817 Some(credentials)
818 } else if let Some(credentials) = match self.keyring {
823 Some(ref keyring) => {
824 if let Some(username) = credentials.and_then(|credentials| credentials.username()) {
828 if let Some(index) = index {
829 debug!(
830 "Checking keyring for credentials for index URL {}@{}",
831 username, index.url
832 );
833 keyring
834 .fetch(DisplaySafeUrl::ref_cast(&index.url), Some(username))
835 .await
836 } else {
837 debug!(
838 "Checking keyring for credentials for full URL {}@{}",
839 username, url
840 );
841 keyring.fetch(url, Some(username)).await
842 }
843 } else if matches!(auth_policy, AuthPolicy::Always) {
844 if let Some(index) = index {
845 debug!(
846 "Checking keyring for credentials for index URL {} without username due to `authenticate = always`",
847 index.url
848 );
849 keyring
850 .fetch(DisplaySafeUrl::ref_cast(&index.url), None)
851 .await
852 } else {
853 None
854 }
855 } else {
856 debug!(
857 "Skipping keyring fetch for {url} without username; use `authenticate = always` to force"
858 );
859 None
860 }
861 }
862 None => None,
863 } {
864 debug!("Found credentials in keyring for {url}");
865 Some(credentials)
866 } else {
867 None
868 };
869
870 let credentials = credentials.map(Authentication::from).map(Arc::new);
871
872 self.cache().fetches.done(key, credentials.clone());
874
875 Ok(credentials)
876 }
877}
878
879fn tracing_url(request: &Request, credentials: Option<&Authentication>) -> DisplaySafeUrl {
880 let mut url = DisplaySafeUrl::from_url(request.url().clone());
881 if let Some(Authentication::Credentials(creds)) = credentials {
882 if let Some(username) = creds.username() {
883 let _ = url.set_username(username);
884 }
885 if let Some(password) = creds.password() {
886 let _ = url.set_password(Some(password));
887 }
888 }
889 url
890}
891
892#[cfg(test)]
893mod tests {
894 use std::assert_matches;
895 use std::io::Write;
896
897 use http::Method;
898 use reqwest::Client;
899 use tempfile::NamedTempFile;
900 use test_log::test;
901
902 use url::Url;
903 use wiremock::matchers::{basic_auth, method, path_regex};
904 use wiremock::{Mock, MockServer, ResponseTemplate};
905
906 use crate::Index;
907 use crate::credentials::Password;
908
909 use super::*;
910
911 type Error = Box<dyn std::error::Error>;
912
913 async fn start_test_server(username: &'static str, password: &'static str) -> MockServer {
914 let server = MockServer::start().await;
915
916 Mock::given(method("GET"))
917 .and(basic_auth(username, password))
918 .respond_with(ResponseTemplate::new(200))
919 .mount(&server)
920 .await;
921
922 Mock::given(method("GET"))
923 .respond_with(ResponseTemplate::new(401))
924 .mount(&server)
925 .await;
926
927 server
928 }
929
930 fn test_client_builder() -> reqwest_middleware::ClientBuilder {
931 reqwest_middleware::ClientBuilder::new(
932 Client::builder()
933 .build()
934 .expect("Reqwest client should build"),
935 )
936 }
937
938 #[test(tokio::test)]
939 async fn test_no_credentials() -> Result<(), Error> {
940 let server = start_test_server("user", "password").await;
941 let client = test_client_builder()
942 .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
943 .build();
944
945 assert_eq!(
946 client
947 .get(format!("{}/foo", server.uri()))
948 .send()
949 .await?
950 .status(),
951 401
952 );
953
954 assert_eq!(
955 client
956 .get(format!("{}/bar", server.uri()))
957 .send()
958 .await?
959 .status(),
960 401
961 );
962
963 Ok(())
964 }
965
966 #[test(tokio::test)]
968 async fn test_credentials_in_url_no_seed() -> Result<(), Error> {
969 let username = "user";
970 let password = "password";
971
972 let server = start_test_server(username, password).await;
973 let client = test_client_builder()
974 .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
975 .build();
976
977 let base_url = Url::parse(&server.uri())?;
978
979 let mut url = base_url.clone();
980 url.set_username(username).unwrap();
981 url.set_password(Some(password)).unwrap();
982 assert_eq!(client.get(url).send().await?.status(), 200);
983
984 assert_eq!(
986 client.get(server.uri()).send().await?.status(),
987 200,
988 "Subsequent requests should not require credentials"
989 );
990
991 assert_eq!(
992 client
993 .get(format!("{}/foo", server.uri()))
994 .send()
995 .await?
996 .status(),
997 200,
998 "Requests can be to different paths in the same realm"
999 );
1000
1001 let mut url = base_url.clone();
1002 url.set_username(username).unwrap();
1003 url.set_password(Some("invalid")).unwrap();
1004 assert_eq!(
1005 client.get(url).send().await?.status(),
1006 401,
1007 "Credentials in the URL should take precedence and fail"
1008 );
1009
1010 Ok(())
1011 }
1012
1013 #[test(tokio::test)]
1014 async fn test_credentials_in_url_seed() -> Result<(), Error> {
1015 let username = "user";
1016 let password = "password";
1017
1018 let server = start_test_server(username, password).await;
1019 let base_url = Url::parse(&server.uri())?;
1020 let cache = CredentialsCache::new();
1021 cache.insert(
1022 DisplaySafeUrl::ref_cast(&base_url),
1023 Arc::new(Authentication::from(Credentials::basic(
1024 Some(username.to_string()),
1025 Some(password.to_string()),
1026 ))),
1027 );
1028
1029 let client = test_client_builder()
1030 .with(AuthMiddleware::new().with_cache(cache))
1031 .build();
1032
1033 let mut url = base_url.clone();
1034 url.set_username(username).unwrap();
1035 url.set_password(Some(password)).unwrap();
1036 assert_eq!(client.get(url).send().await?.status(), 200);
1037
1038 assert_eq!(
1040 client.get(server.uri()).send().await?.status(),
1041 200,
1042 "Requests should not require credentials"
1043 );
1044
1045 assert_eq!(
1046 client
1047 .get(format!("{}/foo", server.uri()))
1048 .send()
1049 .await?
1050 .status(),
1051 200,
1052 "Requests can be to different paths in the same realm"
1053 );
1054
1055 let mut url = base_url.clone();
1056 url.set_username(username).unwrap();
1057 url.set_password(Some("invalid")).unwrap();
1058 assert_eq!(
1059 client.get(url).send().await?.status(),
1060 401,
1061 "Credentials in the URL should take precedence and fail"
1062 );
1063
1064 Ok(())
1065 }
1066
1067 #[test(tokio::test)]
1068 async fn test_credentials_in_url_username_only() -> Result<(), Error> {
1069 let username = "user";
1070 let password = "";
1071
1072 let server = start_test_server(username, password).await;
1073 let base_url = Url::parse(&server.uri())?;
1074 let cache = CredentialsCache::new();
1075 cache.insert(
1076 DisplaySafeUrl::ref_cast(&base_url),
1077 Arc::new(Authentication::from(Credentials::basic(
1078 Some(username.to_string()),
1079 None,
1080 ))),
1081 );
1082
1083 let client = test_client_builder()
1084 .with(AuthMiddleware::new().with_cache(cache))
1085 .build();
1086
1087 let mut url = base_url.clone();
1088 url.set_username(username).unwrap();
1089 url.set_password(None).unwrap();
1090 assert_eq!(client.get(url).send().await?.status(), 200);
1091
1092 assert_eq!(
1094 client.get(server.uri()).send().await?.status(),
1095 200,
1096 "Requests should not require credentials"
1097 );
1098
1099 assert_eq!(
1100 client
1101 .get(format!("{}/foo", server.uri()))
1102 .send()
1103 .await?
1104 .status(),
1105 200,
1106 "Requests can be to different paths in the same realm"
1107 );
1108
1109 let mut url = base_url.clone();
1110 url.set_username(username).unwrap();
1111 url.set_password(Some("invalid")).unwrap();
1112 assert_eq!(
1113 client.get(url).send().await?.status(),
1114 401,
1115 "Credentials in the URL should take precedence and fail"
1116 );
1117
1118 assert_eq!(
1119 client.get(server.uri()).send().await?.status(),
1120 200,
1121 "Subsequent requests should not use the invalid credentials"
1122 );
1123
1124 Ok(())
1125 }
1126
1127 #[test(tokio::test)]
1128 async fn test_netrc_file_default_host() -> Result<(), Error> {
1129 let username = "user";
1130 let password = "password";
1131
1132 let mut netrc_file = NamedTempFile::new()?;
1133 writeln!(netrc_file, "default login {username} password {password}")?;
1134
1135 let server = start_test_server(username, password).await;
1136 let client = test_client_builder()
1137 .with(
1138 AuthMiddleware::new()
1139 .with_cache(CredentialsCache::new())
1140 .with_netrc(Netrc::from_file(netrc_file.path()).ok()),
1141 )
1142 .build();
1143
1144 assert_eq!(
1145 client.get(server.uri()).send().await?.status(),
1146 200,
1147 "Credentials should be pulled from the netrc file"
1148 );
1149
1150 let mut url = Url::parse(&server.uri())?;
1151 url.set_username(username).unwrap();
1152 url.set_password(Some("invalid")).unwrap();
1153 assert_eq!(
1154 client.get(url).send().await?.status(),
1155 401,
1156 "Credentials in the URL should take precedence and fail"
1157 );
1158
1159 assert_eq!(
1160 client.get(server.uri()).send().await?.status(),
1161 200,
1162 "Subsequent requests should not use the invalid credentials"
1163 );
1164
1165 Ok(())
1166 }
1167
1168 #[test(tokio::test)]
1169 async fn test_netrc_file_matching_host() -> Result<(), Error> {
1170 let username = "user";
1171 let password = "password";
1172 let server = start_test_server(username, password).await;
1173 let base_url = Url::parse(&server.uri())?;
1174
1175 let mut netrc_file = NamedTempFile::new()?;
1176 writeln!(
1177 netrc_file,
1178 r"machine {} login {username} password {password}",
1179 base_url.host_str().unwrap()
1180 )?;
1181
1182 let client = test_client_builder()
1183 .with(
1184 AuthMiddleware::new()
1185 .with_cache(CredentialsCache::new())
1186 .with_netrc(Some(
1187 Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1188 )),
1189 )
1190 .build();
1191
1192 assert_eq!(
1193 client.get(server.uri()).send().await?.status(),
1194 200,
1195 "Credentials should be pulled from the netrc file"
1196 );
1197
1198 let mut url = base_url.clone();
1199 url.set_username(username).unwrap();
1200 url.set_password(Some("invalid")).unwrap();
1201 assert_eq!(
1202 client.get(url).send().await?.status(),
1203 401,
1204 "Credentials in the URL should take precedence and fail"
1205 );
1206
1207 assert_eq!(
1208 client.get(server.uri()).send().await?.status(),
1209 200,
1210 "Subsequent requests should not use the invalid credentials"
1211 );
1212
1213 Ok(())
1214 }
1215
1216 #[test(tokio::test)]
1217 async fn test_netrc_file_mismatched_host() -> Result<(), Error> {
1218 let username = "user";
1219 let password = "password";
1220 let server = start_test_server(username, password).await;
1221
1222 let mut netrc_file = NamedTempFile::new()?;
1223 writeln!(
1224 netrc_file,
1225 r"machine example.com login {username} password {password}",
1226 )?;
1227
1228 let client = test_client_builder()
1229 .with(
1230 AuthMiddleware::new()
1231 .with_cache(CredentialsCache::new())
1232 .with_netrc(Some(
1233 Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1234 )),
1235 )
1236 .build();
1237
1238 assert_eq!(
1239 client.get(server.uri()).send().await?.status(),
1240 401,
1241 "Credentials should not be pulled from the netrc file due to host mismatch"
1242 );
1243
1244 let mut url = Url::parse(&server.uri())?;
1245 url.set_username(username).unwrap();
1246 url.set_password(Some(password)).unwrap();
1247 assert_eq!(
1248 client.get(url).send().await?.status(),
1249 200,
1250 "Credentials in the URL should still work"
1251 );
1252
1253 Ok(())
1254 }
1255
1256 #[test(tokio::test)]
1257 async fn test_netrc_file_mismatched_username() -> Result<(), Error> {
1258 let username = "user";
1259 let password = "password";
1260 let server = start_test_server(username, password).await;
1261 let base_url = Url::parse(&server.uri())?;
1262
1263 let mut netrc_file = NamedTempFile::new()?;
1264 writeln!(
1265 netrc_file,
1266 r"machine {} login {username} password {password}",
1267 base_url.host_str().unwrap()
1268 )?;
1269
1270 let client = test_client_builder()
1271 .with(
1272 AuthMiddleware::new()
1273 .with_cache(CredentialsCache::new())
1274 .with_netrc(Some(
1275 Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1276 )),
1277 )
1278 .build();
1279
1280 let mut url = base_url.clone();
1281 url.set_username("other-user").unwrap();
1282 assert_eq!(
1283 client.get(url).send().await?.status(),
1284 401,
1285 "The netrc password should not be used due to a username mismatch"
1286 );
1287
1288 let mut url = base_url.clone();
1289 url.set_username("user").unwrap();
1290 assert_eq!(
1291 client.get(url).send().await?.status(),
1292 200,
1293 "The netrc password should be used for a matching user"
1294 );
1295
1296 Ok(())
1297 }
1298
1299 #[test(tokio::test)]
1300 async fn test_keyring() -> Result<(), Error> {
1301 let username = "user";
1302 let password = "password";
1303 let server = start_test_server(username, password).await;
1304 let base_url = Url::parse(&server.uri())?;
1305
1306 let client = test_client_builder()
1307 .with(
1308 AuthMiddleware::new()
1309 .with_cache(CredentialsCache::new())
1310 .with_keyring(Some(KeyringProvider::dummy([(
1311 format!(
1312 "{}:{}",
1313 base_url.host_str().unwrap(),
1314 base_url.port().unwrap()
1315 ),
1316 username,
1317 password,
1318 )]))),
1319 )
1320 .build();
1321
1322 assert_eq!(
1323 client.get(server.uri()).send().await?.status(),
1324 401,
1325 "Credentials are not pulled from the keyring without a username"
1326 );
1327
1328 let mut url = base_url.clone();
1329 url.set_username(username).unwrap();
1330 assert_eq!(
1331 client.get(url).send().await?.status(),
1332 200,
1333 "Credentials for the username should be pulled from the keyring"
1334 );
1335
1336 let mut url = base_url.clone();
1337 url.set_username(username).unwrap();
1338 url.set_password(Some("invalid")).unwrap();
1339 assert_eq!(
1340 client.get(url).send().await?.status(),
1341 401,
1342 "Password in the URL should take precedence and fail"
1343 );
1344
1345 let mut url = base_url.clone();
1346 url.set_username(username).unwrap();
1347 assert_eq!(
1348 client.get(url.clone()).send().await?.status(),
1349 200,
1350 "Subsequent requests should not use the invalid password"
1351 );
1352
1353 let mut url = base_url.clone();
1354 url.set_username("other_user").unwrap();
1355 assert_eq!(
1356 client.get(url).send().await?.status(),
1357 401,
1358 "Credentials are not pulled from the keyring when given another username"
1359 );
1360
1361 Ok(())
1362 }
1363
1364 #[test(tokio::test)]
1365 async fn test_keyring_always_authenticate() -> Result<(), Error> {
1366 let username = "user";
1367 let password = "password";
1368 let server = start_test_server(username, password).await;
1369 let base_url = Url::parse(&server.uri())?;
1370
1371 let indexes = indexes_for(&base_url, AuthPolicy::Always);
1372 let client = test_client_builder()
1373 .with(
1374 AuthMiddleware::new()
1375 .with_cache(CredentialsCache::new())
1376 .with_keyring(Some(KeyringProvider::dummy([(
1377 format!(
1378 "{}:{}",
1379 base_url.host_str().unwrap(),
1380 base_url.port().unwrap()
1381 ),
1382 username,
1383 password,
1384 )])))
1385 .with_indexes(indexes),
1386 )
1387 .build();
1388
1389 assert_eq!(
1390 client.get(server.uri()).send().await?.status(),
1391 200,
1392 "Credentials (including a username) should be pulled from the keyring"
1393 );
1394
1395 let mut url = base_url.clone();
1396 url.set_username(username).unwrap();
1397 assert_eq!(
1398 client.get(url).send().await?.status(),
1399 200,
1400 "The password for the username should be pulled from the keyring"
1401 );
1402
1403 let mut url = base_url.clone();
1404 url.set_username(username).unwrap();
1405 url.set_password(Some("invalid")).unwrap();
1406 assert_eq!(
1407 client.get(url).send().await?.status(),
1408 401,
1409 "Password in the URL should take precedence and fail"
1410 );
1411
1412 let mut url = base_url.clone();
1413 url.set_username("other_user").unwrap();
1414 assert_matches!(
1415 client.get(url).send().await,
1416 Err(reqwest_middleware::Error::Middleware(_)),
1417 "If the username does not match, a password should not be fetched, and the middleware should fail eagerly since `authenticate = always` is not satisfied"
1418 );
1419
1420 Ok(())
1421 }
1422
1423 #[test(tokio::test)]
1428 async fn test_keyring_includes_non_standard_port() -> Result<(), Error> {
1429 let username = "user";
1430 let password = "password";
1431 let server = start_test_server(username, password).await;
1432 let base_url = Url::parse(&server.uri())?;
1433
1434 let client = test_client_builder()
1435 .with(
1436 AuthMiddleware::new()
1437 .with_cache(CredentialsCache::new())
1438 .with_keyring(Some(KeyringProvider::dummy([(
1439 base_url.host_str().unwrap(),
1441 username,
1442 password,
1443 )]))),
1444 )
1445 .build();
1446
1447 let mut url = base_url.clone();
1448 url.set_username(username).unwrap();
1449 assert_eq!(
1450 client.get(url).send().await?.status(),
1451 401,
1452 "We should fail because the port is not present in the keyring entry"
1453 );
1454
1455 Ok(())
1456 }
1457
1458 #[test(tokio::test)]
1459 async fn test_credentials_in_keyring_seed() -> Result<(), Error> {
1460 let username = "user";
1461 let password = "password";
1462
1463 let server = start_test_server(username, password).await;
1464 let base_url = Url::parse(&server.uri())?;
1465 let cache = CredentialsCache::new();
1466
1467 cache.insert(
1470 DisplaySafeUrl::ref_cast(&base_url),
1471 Arc::new(Authentication::from(Credentials::basic(
1472 Some(username.to_string()),
1473 None,
1474 ))),
1475 );
1476 let client = test_client_builder()
1477 .with(AuthMiddleware::new().with_cache(cache).with_keyring(Some(
1478 KeyringProvider::dummy([(
1479 format!(
1480 "{}:{}",
1481 base_url.host_str().unwrap(),
1482 base_url.port().unwrap()
1483 ),
1484 username,
1485 password,
1486 )]),
1487 )))
1488 .build();
1489
1490 assert_eq!(
1491 client.get(server.uri()).send().await?.status(),
1492 200,
1493 "The username is pulled from the cache, and the password from the keyring"
1494 );
1495
1496 let mut url = base_url.clone();
1497 url.set_username(username).unwrap();
1498 assert_eq!(
1499 client.get(url).send().await?.status(),
1500 200,
1501 "Credentials for the username should be pulled from the keyring"
1502 );
1503
1504 Ok(())
1505 }
1506
1507 #[test(tokio::test)]
1508 async fn test_credentials_in_url_multiple_realms() -> Result<(), Error> {
1509 let username_1 = "user1";
1510 let password_1 = "password1";
1511 let server_1 = start_test_server(username_1, password_1).await;
1512 let base_url_1 = Url::parse(&server_1.uri())?;
1513
1514 let username_2 = "user2";
1515 let password_2 = "password2";
1516 let server_2 = start_test_server(username_2, password_2).await;
1517 let base_url_2 = Url::parse(&server_2.uri())?;
1518
1519 let cache = CredentialsCache::new();
1520 cache.insert(
1522 DisplaySafeUrl::ref_cast(&base_url_1),
1523 Arc::new(Authentication::from(Credentials::basic(
1524 Some(username_1.to_string()),
1525 Some(password_1.to_string()),
1526 ))),
1527 );
1528 cache.insert(
1529 DisplaySafeUrl::ref_cast(&base_url_2),
1530 Arc::new(Authentication::from(Credentials::basic(
1531 Some(username_2.to_string()),
1532 Some(password_2.to_string()),
1533 ))),
1534 );
1535
1536 let client = test_client_builder()
1537 .with(AuthMiddleware::new().with_cache(cache))
1538 .build();
1539
1540 assert_eq!(
1542 client.get(server_1.uri()).send().await?.status(),
1543 200,
1544 "Requests should not require credentials"
1545 );
1546 assert_eq!(
1547 client.get(server_2.uri()).send().await?.status(),
1548 200,
1549 "Requests should not require credentials"
1550 );
1551
1552 assert_eq!(
1553 client
1554 .get(format!("{}/foo", server_1.uri()))
1555 .send()
1556 .await?
1557 .status(),
1558 200,
1559 "Requests can be to different paths in the same realm"
1560 );
1561 assert_eq!(
1562 client
1563 .get(format!("{}/foo", server_2.uri()))
1564 .send()
1565 .await?
1566 .status(),
1567 200,
1568 "Requests can be to different paths in the same realm"
1569 );
1570
1571 Ok(())
1572 }
1573
1574 #[test(tokio::test)]
1575 async fn test_credentials_from_keyring_multiple_realms() -> Result<(), Error> {
1576 let username_1 = "user1";
1577 let password_1 = "password1";
1578 let server_1 = start_test_server(username_1, password_1).await;
1579 let base_url_1 = Url::parse(&server_1.uri())?;
1580
1581 let username_2 = "user2";
1582 let password_2 = "password2";
1583 let server_2 = start_test_server(username_2, password_2).await;
1584 let base_url_2 = Url::parse(&server_2.uri())?;
1585
1586 let client = test_client_builder()
1587 .with(
1588 AuthMiddleware::new()
1589 .with_cache(CredentialsCache::new())
1590 .with_keyring(Some(KeyringProvider::dummy([
1591 (
1592 format!(
1593 "{}:{}",
1594 base_url_1.host_str().unwrap(),
1595 base_url_1.port().unwrap()
1596 ),
1597 username_1,
1598 password_1,
1599 ),
1600 (
1601 format!(
1602 "{}:{}",
1603 base_url_2.host_str().unwrap(),
1604 base_url_2.port().unwrap()
1605 ),
1606 username_2,
1607 password_2,
1608 ),
1609 ]))),
1610 )
1611 .build();
1612
1613 assert_eq!(
1615 client.get(server_1.uri()).send().await?.status(),
1616 401,
1617 "Requests should require a username"
1618 );
1619 assert_eq!(
1620 client.get(server_2.uri()).send().await?.status(),
1621 401,
1622 "Requests should require a username"
1623 );
1624
1625 let mut url_1 = base_url_1.clone();
1626 url_1.set_username(username_1).unwrap();
1627 assert_eq!(
1628 client.get(url_1.clone()).send().await?.status(),
1629 200,
1630 "Requests with a username should succeed"
1631 );
1632 assert_eq!(
1633 client.get(server_2.uri()).send().await?.status(),
1634 401,
1635 "Credentials should not be re-used for the second server"
1636 );
1637
1638 let mut url_2 = base_url_2.clone();
1639 url_2.set_username(username_2).unwrap();
1640 assert_eq!(
1641 client.get(url_2.clone()).send().await?.status(),
1642 200,
1643 "Requests with a username should succeed"
1644 );
1645
1646 assert_eq!(
1647 client.get(format!("{url_1}/foo")).send().await?.status(),
1648 200,
1649 "Requests can be to different paths in the same realm"
1650 );
1651 assert_eq!(
1652 client.get(format!("{url_2}/foo")).send().await?.status(),
1653 200,
1654 "Requests can be to different paths in the same realm"
1655 );
1656
1657 Ok(())
1658 }
1659
1660 #[test(tokio::test)]
1661 async fn test_credentials_in_url_mixed_authentication_in_realm() -> Result<(), Error> {
1662 let username_1 = "user1";
1663 let password_1 = "password1";
1664 let username_2 = "user2";
1665 let password_2 = "password2";
1666
1667 let server = MockServer::start().await;
1668
1669 Mock::given(method("GET"))
1670 .and(path_regex("/prefix_1.*"))
1671 .and(basic_auth(username_1, password_1))
1672 .respond_with(ResponseTemplate::new(200))
1673 .mount(&server)
1674 .await;
1675
1676 Mock::given(method("GET"))
1677 .and(path_regex("/prefix_2.*"))
1678 .and(basic_auth(username_2, password_2))
1679 .respond_with(ResponseTemplate::new(200))
1680 .mount(&server)
1681 .await;
1682
1683 Mock::given(method("GET"))
1686 .and(path_regex("/prefix_3.*"))
1687 .and(basic_auth(username_1, password_1))
1688 .respond_with(ResponseTemplate::new(401))
1689 .mount(&server)
1690 .await;
1691 Mock::given(method("GET"))
1692 .and(path_regex("/prefix_3.*"))
1693 .and(basic_auth(username_2, password_2))
1694 .respond_with(ResponseTemplate::new(401))
1695 .mount(&server)
1696 .await;
1697 Mock::given(method("GET"))
1698 .and(path_regex("/prefix_3.*"))
1699 .respond_with(ResponseTemplate::new(200))
1700 .mount(&server)
1701 .await;
1702
1703 Mock::given(method("GET"))
1704 .respond_with(ResponseTemplate::new(401))
1705 .mount(&server)
1706 .await;
1707
1708 let base_url = Url::parse(&server.uri())?;
1709 let base_url_1 = base_url.join("prefix_1")?;
1710 let base_url_2 = base_url.join("prefix_2")?;
1711 let base_url_3 = base_url.join("prefix_3")?;
1712
1713 let cache = CredentialsCache::new();
1714
1715 cache.insert(
1717 DisplaySafeUrl::ref_cast(&base_url_1),
1718 Arc::new(Authentication::from(Credentials::basic(
1719 Some(username_1.to_string()),
1720 Some(password_1.to_string()),
1721 ))),
1722 );
1723 cache.insert(
1724 DisplaySafeUrl::ref_cast(&base_url_2),
1725 Arc::new(Authentication::from(Credentials::basic(
1726 Some(username_2.to_string()),
1727 Some(password_2.to_string()),
1728 ))),
1729 );
1730
1731 let client = test_client_builder()
1732 .with(AuthMiddleware::new().with_cache(cache))
1733 .build();
1734
1735 assert_eq!(
1737 client.get(base_url_1.clone()).send().await?.status(),
1738 200,
1739 "Requests should not require credentials"
1740 );
1741 assert_eq!(
1742 client.get(base_url_2.clone()).send().await?.status(),
1743 200,
1744 "Requests should not require credentials"
1745 );
1746 assert_eq!(
1747 client
1748 .get(base_url.join("prefix_1/foo")?)
1749 .send()
1750 .await?
1751 .status(),
1752 200,
1753 "Requests can be to different paths in the same realm"
1754 );
1755 assert_eq!(
1756 client
1757 .get(base_url.join("prefix_2/foo")?)
1758 .send()
1759 .await?
1760 .status(),
1761 200,
1762 "Requests can be to different paths in the same realm"
1763 );
1764 assert_eq!(
1765 client
1766 .get(base_url.join("prefix_1_foo")?)
1767 .send()
1768 .await?
1769 .status(),
1770 401,
1771 "Requests to paths with a matching prefix but different resource segments should fail"
1772 );
1773
1774 assert_eq!(
1775 client.get(base_url_3.clone()).send().await?.status(),
1776 200,
1777 "Requests to the 'public' prefix should not use credentials"
1778 );
1779
1780 Ok(())
1781 }
1782
1783 #[test(tokio::test)]
1784 async fn test_credentials_from_keyring_mixed_authentication_in_realm() -> Result<(), Error> {
1785 let username_1 = "user1";
1786 let password_1 = "password1";
1787 let username_2 = "user2";
1788 let password_2 = "password2";
1789
1790 let server = MockServer::start().await;
1791
1792 Mock::given(method("GET"))
1793 .and(path_regex("/prefix_1.*"))
1794 .and(basic_auth(username_1, password_1))
1795 .respond_with(ResponseTemplate::new(200))
1796 .mount(&server)
1797 .await;
1798
1799 Mock::given(method("GET"))
1800 .and(path_regex("/prefix_2.*"))
1801 .and(basic_auth(username_2, password_2))
1802 .respond_with(ResponseTemplate::new(200))
1803 .mount(&server)
1804 .await;
1805
1806 Mock::given(method("GET"))
1809 .and(path_regex("/prefix_3.*"))
1810 .and(basic_auth(username_1, password_1))
1811 .respond_with(ResponseTemplate::new(401))
1812 .mount(&server)
1813 .await;
1814 Mock::given(method("GET"))
1815 .and(path_regex("/prefix_3.*"))
1816 .and(basic_auth(username_2, password_2))
1817 .respond_with(ResponseTemplate::new(401))
1818 .mount(&server)
1819 .await;
1820 Mock::given(method("GET"))
1821 .and(path_regex("/prefix_3.*"))
1822 .respond_with(ResponseTemplate::new(200))
1823 .mount(&server)
1824 .await;
1825
1826 Mock::given(method("GET"))
1827 .respond_with(ResponseTemplate::new(401))
1828 .mount(&server)
1829 .await;
1830
1831 let base_url = Url::parse(&server.uri())?;
1832 let base_url_1 = base_url.join("prefix_1")?;
1833 let base_url_2 = base_url.join("prefix_2")?;
1834 let base_url_3 = base_url.join("prefix_3")?;
1835
1836 let client = test_client_builder()
1837 .with(
1838 AuthMiddleware::new()
1839 .with_cache(CredentialsCache::new())
1840 .with_keyring(Some(KeyringProvider::dummy([
1841 (
1842 format!(
1843 "{}:{}",
1844 base_url_1.host_str().unwrap(),
1845 base_url_1.port().unwrap()
1846 ),
1847 username_1,
1848 password_1,
1849 ),
1850 (
1851 format!(
1852 "{}:{}",
1853 base_url_2.host_str().unwrap(),
1854 base_url_2.port().unwrap()
1855 ),
1856 username_2,
1857 password_2,
1858 ),
1859 ]))),
1860 )
1861 .build();
1862
1863 assert_eq!(
1865 client.get(base_url_1.clone()).send().await?.status(),
1866 401,
1867 "Requests should require a username"
1868 );
1869 assert_eq!(
1870 client.get(base_url_2.clone()).send().await?.status(),
1871 401,
1872 "Requests should require a username"
1873 );
1874
1875 let mut url_1 = base_url_1.clone();
1876 url_1.set_username(username_1).unwrap();
1877 assert_eq!(
1878 client.get(url_1.clone()).send().await?.status(),
1879 200,
1880 "Requests with a username should succeed"
1881 );
1882 assert_eq!(
1883 client.get(base_url_2.clone()).send().await?.status(),
1884 401,
1885 "Credentials should not be re-used for the second prefix"
1886 );
1887
1888 let mut url_2 = base_url_2.clone();
1889 url_2.set_username(username_2).unwrap();
1890 assert_eq!(
1891 client.get(url_2.clone()).send().await?.status(),
1892 200,
1893 "Requests with a username should succeed"
1894 );
1895
1896 assert_eq!(
1897 client
1898 .get(base_url.join("prefix_1/foo")?)
1899 .send()
1900 .await?
1901 .status(),
1902 200,
1903 "Requests can be to different paths in the same prefix"
1904 );
1905 assert_eq!(
1906 client
1907 .get(base_url.join("prefix_2/foo")?)
1908 .send()
1909 .await?
1910 .status(),
1911 200,
1912 "Requests can be to different paths in the same prefix"
1913 );
1914 assert_eq!(
1915 client
1916 .get(base_url.join("prefix_1_foo")?)
1917 .send()
1918 .await?
1919 .status(),
1920 401,
1921 "Requests to paths with a matching prefix but different resource segments should fail"
1922 );
1923 assert_eq!(
1924 client.get(base_url_3.clone()).send().await?.status(),
1925 200,
1926 "Requests to the 'public' prefix should not use credentials"
1927 );
1928
1929 Ok(())
1930 }
1931
1932 #[test(tokio::test)]
1936 async fn test_credentials_from_keyring_mixed_authentication_in_realm_same_username()
1937 -> Result<(), Error> {
1938 let username = "user";
1939 let password_1 = "password1";
1940 let password_2 = "password2";
1941
1942 let server = MockServer::start().await;
1943
1944 Mock::given(method("GET"))
1945 .and(path_regex("/prefix_1.*"))
1946 .and(basic_auth(username, password_1))
1947 .respond_with(ResponseTemplate::new(200))
1948 .mount(&server)
1949 .await;
1950
1951 Mock::given(method("GET"))
1952 .and(path_regex("/prefix_2.*"))
1953 .and(basic_auth(username, password_2))
1954 .respond_with(ResponseTemplate::new(200))
1955 .mount(&server)
1956 .await;
1957
1958 Mock::given(method("GET"))
1959 .respond_with(ResponseTemplate::new(401))
1960 .mount(&server)
1961 .await;
1962
1963 let base_url = Url::parse(&server.uri())?;
1964 let base_url_1 = base_url.join("prefix_1")?;
1965 let base_url_2 = base_url.join("prefix_2")?;
1966
1967 let client = test_client_builder()
1968 .with(
1969 AuthMiddleware::new()
1970 .with_cache(CredentialsCache::new())
1971 .with_keyring(Some(KeyringProvider::dummy([
1972 (base_url_1.clone(), username, password_1),
1973 (base_url_2.clone(), username, password_2),
1974 ]))),
1975 )
1976 .build();
1977
1978 assert_eq!(
1980 client.get(base_url_1.clone()).send().await?.status(),
1981 401,
1982 "Requests should require a username"
1983 );
1984 assert_eq!(
1985 client.get(base_url_2.clone()).send().await?.status(),
1986 401,
1987 "Requests should require a username"
1988 );
1989
1990 let mut url_1 = base_url_1.clone();
1991 url_1.set_username(username).unwrap();
1992 assert_eq!(
1993 client.get(url_1.clone()).send().await?.status(),
1994 200,
1995 "The first request with a username will succeed"
1996 );
1997 assert_eq!(
1998 client.get(base_url_2.clone()).send().await?.status(),
1999 401,
2000 "Credentials should not be re-used for the second prefix"
2001 );
2002 assert_eq!(
2003 client
2004 .get(base_url.join("prefix_1/foo")?)
2005 .send()
2006 .await?
2007 .status(),
2008 200,
2009 "Subsequent requests can be to different paths in the same prefix"
2010 );
2011
2012 let mut url_2 = base_url_2.clone();
2013 url_2.set_username(username).unwrap();
2014 assert_eq!(
2015 client.get(url_2.clone()).send().await?.status(),
2016 401, "A request with the same username and realm for a URL that needs a different password will fail"
2018 );
2019 assert_eq!(
2020 client
2021 .get(base_url.join("prefix_2/foo")?)
2022 .send()
2023 .await?
2024 .status(),
2025 401, "Requests to other paths in the failing prefix will also fail"
2027 );
2028
2029 Ok(())
2030 }
2031
2032 #[test(tokio::test)]
2036 async fn test_credentials_from_keyring_mixed_authentication_different_indexes_same_realm()
2037 -> Result<(), Error> {
2038 let username = "user";
2039 let password_1 = "password1";
2040 let password_2 = "password2";
2041
2042 let server = MockServer::start().await;
2043
2044 Mock::given(method("GET"))
2045 .and(path_regex("/prefix_1.*"))
2046 .and(basic_auth(username, password_1))
2047 .respond_with(ResponseTemplate::new(200))
2048 .mount(&server)
2049 .await;
2050
2051 Mock::given(method("GET"))
2052 .and(path_regex("/prefix_2.*"))
2053 .and(basic_auth(username, password_2))
2054 .respond_with(ResponseTemplate::new(200))
2055 .mount(&server)
2056 .await;
2057
2058 Mock::given(method("GET"))
2059 .respond_with(ResponseTemplate::new(401))
2060 .mount(&server)
2061 .await;
2062
2063 let base_url = Url::parse(&server.uri())?;
2064 let base_url_1 = base_url.join("prefix_1")?;
2065 let base_url_2 = base_url.join("prefix_2")?;
2066 let indexes = Indexes::from_indexes(vec![
2067 Index {
2068 url: DisplaySafeUrl::from_url(base_url_1.clone()),
2069 root_url: DisplaySafeUrl::from_url(base_url_1.clone()),
2070 auth_policy: AuthPolicy::Auto,
2071 },
2072 Index {
2073 url: DisplaySafeUrl::from_url(base_url_2.clone()),
2074 root_url: DisplaySafeUrl::from_url(base_url_2.clone()),
2075 auth_policy: AuthPolicy::Auto,
2076 },
2077 ]);
2078
2079 let client = test_client_builder()
2080 .with(
2081 AuthMiddleware::new()
2082 .with_cache(CredentialsCache::new())
2083 .with_keyring(Some(KeyringProvider::dummy([
2084 (base_url_1.clone(), username, password_1),
2085 (base_url_2.clone(), username, password_2),
2086 ])))
2087 .with_indexes(indexes),
2088 )
2089 .build();
2090
2091 assert_eq!(
2093 client.get(base_url_1.clone()).send().await?.status(),
2094 401,
2095 "Requests should require a username"
2096 );
2097 assert_eq!(
2098 client.get(base_url_2.clone()).send().await?.status(),
2099 401,
2100 "Requests should require a username"
2101 );
2102
2103 let mut url_1 = base_url_1.clone();
2104 url_1.set_username(username).unwrap();
2105 assert_eq!(
2106 client.get(url_1.clone()).send().await?.status(),
2107 200,
2108 "The first request with a username will succeed"
2109 );
2110 assert_eq!(
2111 client.get(base_url_2.clone()).send().await?.status(),
2112 401,
2113 "Credentials should not be re-used for the second prefix"
2114 );
2115 assert_eq!(
2116 client
2117 .get(base_url.join("prefix_1/foo")?)
2118 .send()
2119 .await?
2120 .status(),
2121 200,
2122 "Subsequent requests can be to different paths in the same prefix"
2123 );
2124
2125 let mut url_2 = base_url_2.clone();
2126 url_2.set_username(username).unwrap();
2127 assert_eq!(
2128 client.get(url_2.clone()).send().await?.status(),
2129 200,
2130 "A request with the same username and realm for a URL will use index-specific password"
2131 );
2132 assert_eq!(
2133 client
2134 .get(base_url.join("prefix_2/foo")?)
2135 .send()
2136 .await?
2137 .status(),
2138 200,
2139 "Requests to other paths with that prefix will also succeed"
2140 );
2141
2142 Ok(())
2143 }
2144
2145 #[test(tokio::test)]
2148 async fn test_credentials_from_keyring_shared_authentication_different_indexes_same_realm()
2149 -> Result<(), Error> {
2150 let username = "user";
2151 let password = "password";
2152
2153 let server = MockServer::start().await;
2154
2155 Mock::given(method("GET"))
2156 .and(basic_auth(username, password))
2157 .respond_with(ResponseTemplate::new(200))
2158 .mount(&server)
2159 .await;
2160
2161 Mock::given(method("GET"))
2162 .and(path_regex("/prefix_1.*"))
2163 .and(basic_auth(username, password))
2164 .respond_with(ResponseTemplate::new(200))
2165 .mount(&server)
2166 .await;
2167
2168 Mock::given(method("GET"))
2169 .respond_with(ResponseTemplate::new(401))
2170 .mount(&server)
2171 .await;
2172
2173 let base_url = Url::parse(&server.uri())?;
2174 let index_url = base_url.join("prefix_1")?;
2175 let indexes = Indexes::from_indexes(vec![Index {
2176 url: DisplaySafeUrl::from_url(index_url.clone()),
2177 root_url: DisplaySafeUrl::from_url(index_url.clone()),
2178 auth_policy: AuthPolicy::Auto,
2179 }]);
2180
2181 let client = test_client_builder()
2182 .with(
2183 AuthMiddleware::new()
2184 .with_cache(CredentialsCache::new())
2185 .with_keyring(Some(KeyringProvider::dummy([(
2186 base_url.clone(),
2187 username,
2188 password,
2189 )])))
2190 .with_indexes(indexes),
2191 )
2192 .build();
2193
2194 assert_eq!(
2196 client.get(index_url.clone()).send().await?.status(),
2197 401,
2198 "Requests should require a username"
2199 );
2200
2201 let mut realm_url = base_url.clone();
2203 realm_url.set_username(username).unwrap();
2204 assert_eq!(
2205 client.get(realm_url.clone()).send().await?.status(),
2206 200,
2207 "The first realm request with a username will succeed"
2208 );
2209
2210 let mut url = index_url.clone();
2211 url.set_username(username).unwrap();
2212 assert_eq!(
2213 client.get(url.clone()).send().await?.status(),
2214 200,
2215 "A request with the same username and realm for a URL will use the realm if there is no index-specific password"
2216 );
2217 assert_eq!(
2218 client
2219 .get(base_url.join("prefix_1/foo")?)
2220 .send()
2221 .await?
2222 .status(),
2223 200,
2224 "Requests to other paths with that prefix will also succeed"
2225 );
2226
2227 Ok(())
2228 }
2229
2230 fn indexes_for(url: &Url, policy: AuthPolicy) -> Indexes {
2231 let mut url = DisplaySafeUrl::from_url(url.clone());
2232 url.set_password(None).ok();
2233 url.set_username("").ok();
2234 Indexes::from_indexes(vec![Index {
2235 url: url.clone(),
2236 root_url: url.clone(),
2237 auth_policy: policy,
2238 }])
2239 }
2240
2241 #[test(tokio::test)]
2244 async fn test_auth_policy_always_with_credentials() -> Result<(), Error> {
2245 let username = "user";
2246 let password = "password";
2247
2248 let server = start_test_server(username, password).await;
2249
2250 let base_url = Url::parse(&server.uri())?;
2251
2252 let indexes = indexes_for(&base_url, AuthPolicy::Always);
2253 let client = test_client_builder()
2254 .with(
2255 AuthMiddleware::new()
2256 .with_cache(CredentialsCache::new())
2257 .with_indexes(indexes),
2258 )
2259 .build();
2260
2261 Mock::given(method("GET"))
2262 .and(path_regex("/*"))
2263 .and(basic_auth(username, password))
2264 .respond_with(ResponseTemplate::new(200))
2265 .mount(&server)
2266 .await;
2267
2268 Mock::given(method("GET"))
2269 .respond_with(ResponseTemplate::new(401))
2270 .mount(&server)
2271 .await;
2272
2273 let mut url = base_url.clone();
2274 url.set_username(username).unwrap();
2275 url.set_password(Some(password)).unwrap();
2276 assert_eq!(client.get(url).send().await?.status(), 200);
2277
2278 assert_eq!(
2279 client
2280 .get(format!("{}/foo", server.uri()))
2281 .send()
2282 .await?
2283 .status(),
2284 200,
2285 "Requests can be to different paths with index URL as prefix"
2286 );
2287
2288 let mut url = base_url.clone();
2289 url.set_username(username).unwrap();
2290 url.set_password(Some("invalid")).unwrap();
2291 assert_eq!(
2292 client.get(url).send().await?.status(),
2293 401,
2294 "Incorrect credentials should fail"
2295 );
2296
2297 Ok(())
2298 }
2299
2300 #[test(tokio::test)]
2303 async fn test_auth_policy_always_unauthenticated() -> Result<(), Error> {
2304 let server = MockServer::start().await;
2305
2306 Mock::given(method("GET"))
2307 .and(path_regex("/*"))
2308 .respond_with(ResponseTemplate::new(200))
2309 .mount(&server)
2310 .await;
2311
2312 Mock::given(method("GET"))
2313 .respond_with(ResponseTemplate::new(401))
2314 .mount(&server)
2315 .await;
2316
2317 let base_url = Url::parse(&server.uri())?;
2318
2319 let indexes = indexes_for(&base_url, AuthPolicy::Always);
2320 let client = test_client_builder()
2321 .with(
2322 AuthMiddleware::new()
2323 .with_cache(CredentialsCache::new())
2324 .with_indexes(indexes),
2325 )
2326 .build();
2327
2328 assert_matches!(
2330 client.get(server.uri()).send().await,
2331 Err(reqwest_middleware::Error::Middleware(_))
2332 );
2333
2334 Ok(())
2335 }
2336
2337 #[test(tokio::test)]
2340 async fn test_auth_policy_never_with_credentials() -> Result<(), Error> {
2341 let username = "user";
2342 let password = "password";
2343
2344 let server = start_test_server(username, password).await;
2345 let base_url = Url::parse(&server.uri())?;
2346
2347 Mock::given(method("GET"))
2348 .and(path_regex("/*"))
2349 .and(basic_auth(username, password))
2350 .respond_with(ResponseTemplate::new(200))
2351 .mount(&server)
2352 .await;
2353
2354 Mock::given(method("GET"))
2355 .respond_with(ResponseTemplate::new(401))
2356 .mount(&server)
2357 .await;
2358
2359 let indexes = indexes_for(&base_url, AuthPolicy::Never);
2360 let client = test_client_builder()
2361 .with(
2362 AuthMiddleware::new()
2363 .with_cache(CredentialsCache::new())
2364 .with_indexes(indexes),
2365 )
2366 .build();
2367
2368 let mut url = base_url.clone();
2369 url.set_username(username).unwrap();
2370 url.set_password(Some(password)).unwrap();
2371
2372 assert_eq!(
2373 client
2374 .get(format!("{}/foo", server.uri()))
2375 .send()
2376 .await?
2377 .status(),
2378 401,
2379 "Requests should not be completed if credentials are required"
2380 );
2381
2382 Ok(())
2383 }
2384
2385 #[test(tokio::test)]
2388 async fn test_auth_policy_never_unauthenticated() -> Result<(), Error> {
2389 let server = MockServer::start().await;
2390
2391 Mock::given(method("GET"))
2392 .and(path_regex("/*"))
2393 .respond_with(ResponseTemplate::new(200))
2394 .mount(&server)
2395 .await;
2396
2397 Mock::given(method("GET"))
2398 .respond_with(ResponseTemplate::new(401))
2399 .mount(&server)
2400 .await;
2401
2402 let base_url = Url::parse(&server.uri())?;
2403
2404 let indexes = indexes_for(&base_url, AuthPolicy::Never);
2405 let client = test_client_builder()
2406 .with(
2407 AuthMiddleware::new()
2408 .with_cache(CredentialsCache::new())
2409 .with_indexes(indexes),
2410 )
2411 .build();
2412
2413 assert_eq!(
2414 client.get(server.uri()).send().await?.status(),
2415 200,
2416 "Requests should succeed if unauthenticated requests can succeed"
2417 );
2418
2419 Ok(())
2420 }
2421
2422 #[test]
2423 fn test_tracing_url() {
2424 let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2426 assert_eq!(
2427 tracing_url(&req, None),
2428 DisplaySafeUrl::parse("https://pypi-proxy.fly.dev/basic-auth/simple").unwrap()
2429 );
2430
2431 let creds = Authentication::from(Credentials::Basic {
2432 username: Username::new(Some(String::from("user"))),
2433 password: None,
2434 });
2435 let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2436 assert_eq!(
2437 tracing_url(&req, Some(&creds)),
2438 DisplaySafeUrl::parse("https://user@pypi-proxy.fly.dev/basic-auth/simple").unwrap()
2439 );
2440
2441 let creds = Authentication::from(Credentials::Basic {
2442 username: Username::new(Some(String::from("user"))),
2443 password: Some(Password::new(String::from("password"))),
2444 });
2445 let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2446 assert_eq!(
2447 tracing_url(&req, Some(&creds)),
2448 DisplaySafeUrl::parse("https://user:password@pypi-proxy.fly.dev/basic-auth/simple")
2449 .unwrap()
2450 );
2451 }
2452
2453 #[test(tokio::test)]
2454 async fn test_text_store_basic_auth() -> Result<(), Error> {
2455 let username = "user";
2456 let password = "password";
2457
2458 let server = start_test_server(username, password).await;
2459 let base_url = Url::parse(&server.uri())?;
2460
2461 let mut store = TextCredentialStore::default();
2463 let service = crate::Service::try_from(base_url.to_string()).unwrap();
2464 let credentials =
2465 Credentials::basic(Some(username.to_string()), Some(password.to_string()));
2466 store.insert(service.clone(), credentials);
2467
2468 let client = test_client_builder()
2469 .with(
2470 AuthMiddleware::new()
2471 .with_cache(CredentialsCache::new())
2472 .with_text_store(Some(store)),
2473 )
2474 .build();
2475
2476 assert_eq!(
2477 client.get(server.uri()).send().await?.status(),
2478 200,
2479 "Credentials should be pulled from the text store"
2480 );
2481
2482 Ok(())
2483 }
2484
2485 #[test(tokio::test)]
2486 async fn test_text_store_disabled() -> Result<(), Error> {
2487 let username = "user";
2488 let password = "password";
2489 let server = start_test_server(username, password).await;
2490
2491 let client = test_client_builder()
2492 .with(
2493 AuthMiddleware::new()
2494 .with_cache(CredentialsCache::new())
2495 .with_text_store(None), )
2497 .build();
2498
2499 assert_eq!(
2500 client.get(server.uri()).send().await?.status(),
2501 401,
2502 "Credentials should not be found when text store is disabled"
2503 );
2504
2505 Ok(())
2506 }
2507
2508 #[test(tokio::test)]
2509 async fn test_text_store_by_username() -> Result<(), Error> {
2510 let username = "testuser";
2511 let password = "testpass";
2512 let wrong_username = "wronguser";
2513
2514 let server = start_test_server(username, password).await;
2515 let base_url = Url::parse(&server.uri())?;
2516
2517 let mut store = TextCredentialStore::default();
2518 let service = crate::Service::try_from(base_url.to_string()).unwrap();
2519 let credentials =
2520 crate::Credentials::basic(Some(username.to_string()), Some(password.to_string()));
2521 store.insert(service.clone(), credentials);
2522
2523 let client = test_client_builder()
2524 .with(
2525 AuthMiddleware::new()
2526 .with_cache(CredentialsCache::new())
2527 .with_text_store(Some(store)),
2528 )
2529 .build();
2530
2531 let url_with_username = format!(
2533 "{}://{}@{}",
2534 base_url.scheme(),
2535 username,
2536 base_url.host_str().unwrap()
2537 );
2538 let url_with_port = if let Some(port) = base_url.port() {
2539 format!("{}:{}{}", url_with_username, port, base_url.path())
2540 } else {
2541 format!("{}{}", url_with_username, base_url.path())
2542 };
2543
2544 assert_eq!(
2545 client.get(&url_with_port).send().await?.status(),
2546 200,
2547 "Request with matching username should succeed"
2548 );
2549
2550 let url_with_wrong_username = format!(
2552 "{}://{}@{}",
2553 base_url.scheme(),
2554 wrong_username,
2555 base_url.host_str().unwrap()
2556 );
2557 let url_with_port = if let Some(port) = base_url.port() {
2558 format!("{}:{}{}", url_with_wrong_username, port, base_url.path())
2559 } else {
2560 format!("{}{}", url_with_wrong_username, base_url.path())
2561 };
2562
2563 assert_eq!(
2564 client.get(&url_with_port).send().await?.status(),
2565 401,
2566 "Request with non-matching username should fail"
2567 );
2568
2569 assert_eq!(
2571 client.get(server.uri()).send().await?.status(),
2572 200,
2573 "Request with no username should succeed"
2574 );
2575
2576 Ok(())
2577 }
2578
2579 fn create_request(url: &str) -> Request {
2580 Request::new(Method::GET, Url::parse(url).unwrap())
2581 }
2582
2583 #[test(tokio::test)]
2588 async fn test_credentials_in_url_empty_username() -> Result<(), Error> {
2589 let username = "";
2590 let password = "token";
2591
2592 let server = MockServer::start().await;
2593
2594 Mock::given(method("GET"))
2595 .and(basic_auth(username, password))
2596 .respond_with(ResponseTemplate::new(200))
2597 .mount(&server)
2598 .await;
2599
2600 Mock::given(method("GET"))
2601 .respond_with(ResponseTemplate::new(401))
2602 .mount(&server)
2603 .await;
2604
2605 let client = test_client_builder()
2606 .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
2607 .build();
2608
2609 let base_url = Url::parse(&server.uri())?;
2610
2611 let mut url = base_url.clone();
2613 url.set_password(Some(password)).unwrap();
2614 assert_eq!(
2615 client.get(url).send().await?.status(),
2616 200,
2617 "URL with empty username but password should authenticate successfully"
2618 );
2619
2620 assert_eq!(
2622 client.get(server.uri()).send().await?.status(),
2623 200,
2624 "Subsequent requests should use cached credentials"
2625 );
2626
2627 assert_eq!(
2628 client
2629 .get(format!("{}/foo", server.uri()))
2630 .send()
2631 .await?
2632 .status(),
2633 200,
2634 "Requests to different paths in the same realm should succeed"
2635 );
2636
2637 Ok(())
2638 }
2639}