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