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, PreviewFeature};
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 match text_store.get_credentials(
807 url,
808 credentials
809 .as_ref()
810 .and_then(|credentials| credentials.username()),
811 ) {
812 Ok(credentials) => credentials.cloned(),
813 Err(err) => {
814 debug!("Failed to get credentials from text store: {err}");
815 None
816 }
817 }
818 }) {
819 debug!("Found credentials in plaintext store for {url}");
820 Some(credentials)
821 } else if let Some(credentials) = {
822 if self.preview.is_enabled(PreviewFeature::NativeAuth) {
823 let native_store = KeyringProvider::native();
824 let username = credentials.and_then(|credentials| credentials.username());
825 let display_username = if let Some(username) = username {
826 format!("{username}@")
827 } else {
828 String::new()
829 };
830 if let Some(index) = index {
831 debug!(
834 "Checking native store for credentials for index URL {}{}",
835 display_username, index.root_url
836 );
837 native_store.fetch(&index.root_url, username).await
838 } else {
839 debug!(
840 "Checking native store for credentials for URL {}{}",
841 display_username, url
842 );
843 native_store.fetch(url, username).await
844 }
845 } else {
847 None
848 }
849 } {
850 debug!("Found credentials in native store for {url}");
851 Some(credentials)
852 } else if let Some(credentials) = match self.keyring {
857 Some(ref keyring) => {
858 if let Some(username) = credentials.and_then(|credentials| credentials.username()) {
862 if let Some(index) = index {
863 debug!(
864 "Checking keyring for credentials for index URL {}@{}",
865 username, index.url
866 );
867 keyring
868 .fetch(DisplaySafeUrl::ref_cast(&index.url), Some(username))
869 .await
870 } else {
871 debug!(
872 "Checking keyring for credentials for full URL {}@{}",
873 username, url
874 );
875 keyring.fetch(url, Some(username)).await
876 }
877 } else if matches!(auth_policy, AuthPolicy::Always) {
878 if let Some(index) = index {
879 debug!(
880 "Checking keyring for credentials for index URL {} without username due to `authenticate = always`",
881 index.url
882 );
883 keyring
884 .fetch(DisplaySafeUrl::ref_cast(&index.url), None)
885 .await
886 } else {
887 None
888 }
889 } else {
890 debug!(
891 "Skipping keyring fetch for {url} without username; use `authenticate = always` to force"
892 );
893 None
894 }
895 }
896 None => None,
897 } {
898 debug!("Found credentials in keyring for {url}");
899 Some(credentials)
900 } else {
901 None
902 };
903
904 let credentials = credentials.map(Authentication::from).map(Arc::new);
905
906 self.cache().fetches.done(key, credentials.clone());
908
909 credentials
910 }
911}
912
913fn tracing_url(request: &Request, credentials: Option<&Authentication>) -> DisplaySafeUrl {
914 let mut url = DisplaySafeUrl::from_url(request.url().clone());
915 if let Some(Authentication::Credentials(creds)) = credentials {
916 if let Some(username) = creds.username() {
917 let _ = url.set_username(username);
918 }
919 if let Some(password) = creds.password() {
920 let _ = url.set_password(Some(password));
921 }
922 }
923 url
924}
925
926#[cfg(test)]
927mod tests {
928 use std::io::Write;
929
930 use http::Method;
931 use reqwest::Client;
932 use tempfile::NamedTempFile;
933 use test_log::test;
934
935 use url::Url;
936 use wiremock::matchers::{basic_auth, method, path_regex};
937 use wiremock::{Mock, MockServer, ResponseTemplate};
938
939 use crate::Index;
940 use crate::credentials::Password;
941
942 use super::*;
943
944 type Error = Box<dyn std::error::Error>;
945
946 async fn start_test_server(username: &'static str, password: &'static str) -> MockServer {
947 let server = MockServer::start().await;
948
949 Mock::given(method("GET"))
950 .and(basic_auth(username, password))
951 .respond_with(ResponseTemplate::new(200))
952 .mount(&server)
953 .await;
954
955 Mock::given(method("GET"))
956 .respond_with(ResponseTemplate::new(401))
957 .mount(&server)
958 .await;
959
960 server
961 }
962
963 fn test_client_builder() -> reqwest_middleware::ClientBuilder {
964 reqwest_middleware::ClientBuilder::new(
965 Client::builder()
966 .build()
967 .expect("Reqwest client should build"),
968 )
969 }
970
971 #[test(tokio::test)]
972 async fn test_no_credentials() -> Result<(), Error> {
973 let server = start_test_server("user", "password").await;
974 let client = test_client_builder()
975 .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
976 .build();
977
978 assert_eq!(
979 client
980 .get(format!("{}/foo", server.uri()))
981 .send()
982 .await?
983 .status(),
984 401
985 );
986
987 assert_eq!(
988 client
989 .get(format!("{}/bar", server.uri()))
990 .send()
991 .await?
992 .status(),
993 401
994 );
995
996 Ok(())
997 }
998
999 #[test(tokio::test)]
1001 async fn test_credentials_in_url_no_seed() -> Result<(), Error> {
1002 let username = "user";
1003 let password = "password";
1004
1005 let server = start_test_server(username, password).await;
1006 let client = test_client_builder()
1007 .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
1008 .build();
1009
1010 let base_url = Url::parse(&server.uri())?;
1011
1012 let mut url = base_url.clone();
1013 url.set_username(username).unwrap();
1014 url.set_password(Some(password)).unwrap();
1015 assert_eq!(client.get(url).send().await?.status(), 200);
1016
1017 assert_eq!(
1019 client.get(server.uri()).send().await?.status(),
1020 200,
1021 "Subsequent requests should not require credentials"
1022 );
1023
1024 assert_eq!(
1025 client
1026 .get(format!("{}/foo", server.uri()))
1027 .send()
1028 .await?
1029 .status(),
1030 200,
1031 "Requests can be to different paths in the same realm"
1032 );
1033
1034 let mut url = base_url.clone();
1035 url.set_username(username).unwrap();
1036 url.set_password(Some("invalid")).unwrap();
1037 assert_eq!(
1038 client.get(url).send().await?.status(),
1039 401,
1040 "Credentials in the URL should take precedence and fail"
1041 );
1042
1043 Ok(())
1044 }
1045
1046 #[test(tokio::test)]
1047 async fn test_credentials_in_url_seed() -> Result<(), Error> {
1048 let username = "user";
1049 let password = "password";
1050
1051 let server = start_test_server(username, password).await;
1052 let base_url = Url::parse(&server.uri())?;
1053 let cache = CredentialsCache::new();
1054 cache.insert(
1055 &base_url,
1056 Arc::new(Authentication::from(Credentials::basic(
1057 Some(username.to_string()),
1058 Some(password.to_string()),
1059 ))),
1060 );
1061
1062 let client = test_client_builder()
1063 .with(AuthMiddleware::new().with_cache(cache))
1064 .build();
1065
1066 let mut url = base_url.clone();
1067 url.set_username(username).unwrap();
1068 url.set_password(Some(password)).unwrap();
1069 assert_eq!(client.get(url).send().await?.status(), 200);
1070
1071 assert_eq!(
1073 client.get(server.uri()).send().await?.status(),
1074 200,
1075 "Requests should not require credentials"
1076 );
1077
1078 assert_eq!(
1079 client
1080 .get(format!("{}/foo", server.uri()))
1081 .send()
1082 .await?
1083 .status(),
1084 200,
1085 "Requests can be to different paths in the same realm"
1086 );
1087
1088 let mut url = base_url.clone();
1089 url.set_username(username).unwrap();
1090 url.set_password(Some("invalid")).unwrap();
1091 assert_eq!(
1092 client.get(url).send().await?.status(),
1093 401,
1094 "Credentials in the URL should take precedence and fail"
1095 );
1096
1097 Ok(())
1098 }
1099
1100 #[test(tokio::test)]
1101 async fn test_credentials_in_url_username_only() -> Result<(), Error> {
1102 let username = "user";
1103 let password = "";
1104
1105 let server = start_test_server(username, password).await;
1106 let base_url = Url::parse(&server.uri())?;
1107 let cache = CredentialsCache::new();
1108 cache.insert(
1109 &base_url,
1110 Arc::new(Authentication::from(Credentials::basic(
1111 Some(username.to_string()),
1112 None,
1113 ))),
1114 );
1115
1116 let client = test_client_builder()
1117 .with(AuthMiddleware::new().with_cache(cache))
1118 .build();
1119
1120 let mut url = base_url.clone();
1121 url.set_username(username).unwrap();
1122 url.set_password(None).unwrap();
1123 assert_eq!(client.get(url).send().await?.status(), 200);
1124
1125 assert_eq!(
1127 client.get(server.uri()).send().await?.status(),
1128 200,
1129 "Requests should not require credentials"
1130 );
1131
1132 assert_eq!(
1133 client
1134 .get(format!("{}/foo", server.uri()))
1135 .send()
1136 .await?
1137 .status(),
1138 200,
1139 "Requests can be to different paths in the same realm"
1140 );
1141
1142 let mut url = base_url.clone();
1143 url.set_username(username).unwrap();
1144 url.set_password(Some("invalid")).unwrap();
1145 assert_eq!(
1146 client.get(url).send().await?.status(),
1147 401,
1148 "Credentials in the URL should take precedence and fail"
1149 );
1150
1151 assert_eq!(
1152 client.get(server.uri()).send().await?.status(),
1153 200,
1154 "Subsequent requests should not use the invalid credentials"
1155 );
1156
1157 Ok(())
1158 }
1159
1160 #[test(tokio::test)]
1161 async fn test_netrc_file_default_host() -> Result<(), Error> {
1162 let username = "user";
1163 let password = "password";
1164
1165 let mut netrc_file = NamedTempFile::new()?;
1166 writeln!(netrc_file, "default login {username} password {password}")?;
1167
1168 let server = start_test_server(username, password).await;
1169 let client = test_client_builder()
1170 .with(
1171 AuthMiddleware::new()
1172 .with_cache(CredentialsCache::new())
1173 .with_netrc(Netrc::from_file(netrc_file.path()).ok()),
1174 )
1175 .build();
1176
1177 assert_eq!(
1178 client.get(server.uri()).send().await?.status(),
1179 200,
1180 "Credentials should be pulled from the netrc file"
1181 );
1182
1183 let mut url = Url::parse(&server.uri())?;
1184 url.set_username(username).unwrap();
1185 url.set_password(Some("invalid")).unwrap();
1186 assert_eq!(
1187 client.get(url).send().await?.status(),
1188 401,
1189 "Credentials in the URL should take precedence and fail"
1190 );
1191
1192 assert_eq!(
1193 client.get(server.uri()).send().await?.status(),
1194 200,
1195 "Subsequent requests should not use the invalid credentials"
1196 );
1197
1198 Ok(())
1199 }
1200
1201 #[test(tokio::test)]
1202 async fn test_netrc_file_matching_host() -> Result<(), Error> {
1203 let username = "user";
1204 let password = "password";
1205 let server = start_test_server(username, password).await;
1206 let base_url = Url::parse(&server.uri())?;
1207
1208 let mut netrc_file = NamedTempFile::new()?;
1209 writeln!(
1210 netrc_file,
1211 r"machine {} login {username} password {password}",
1212 base_url.host_str().unwrap()
1213 )?;
1214
1215 let client = test_client_builder()
1216 .with(
1217 AuthMiddleware::new()
1218 .with_cache(CredentialsCache::new())
1219 .with_netrc(Some(
1220 Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1221 )),
1222 )
1223 .build();
1224
1225 assert_eq!(
1226 client.get(server.uri()).send().await?.status(),
1227 200,
1228 "Credentials should be pulled from the netrc file"
1229 );
1230
1231 let mut url = base_url.clone();
1232 url.set_username(username).unwrap();
1233 url.set_password(Some("invalid")).unwrap();
1234 assert_eq!(
1235 client.get(url).send().await?.status(),
1236 401,
1237 "Credentials in the URL should take precedence and fail"
1238 );
1239
1240 assert_eq!(
1241 client.get(server.uri()).send().await?.status(),
1242 200,
1243 "Subsequent requests should not use the invalid credentials"
1244 );
1245
1246 Ok(())
1247 }
1248
1249 #[test(tokio::test)]
1250 async fn test_netrc_file_mismatched_host() -> Result<(), Error> {
1251 let username = "user";
1252 let password = "password";
1253 let server = start_test_server(username, password).await;
1254
1255 let mut netrc_file = NamedTempFile::new()?;
1256 writeln!(
1257 netrc_file,
1258 r"machine example.com login {username} password {password}",
1259 )?;
1260
1261 let client = test_client_builder()
1262 .with(
1263 AuthMiddleware::new()
1264 .with_cache(CredentialsCache::new())
1265 .with_netrc(Some(
1266 Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1267 )),
1268 )
1269 .build();
1270
1271 assert_eq!(
1272 client.get(server.uri()).send().await?.status(),
1273 401,
1274 "Credentials should not be pulled from the netrc file due to host mismatch"
1275 );
1276
1277 let mut url = Url::parse(&server.uri())?;
1278 url.set_username(username).unwrap();
1279 url.set_password(Some(password)).unwrap();
1280 assert_eq!(
1281 client.get(url).send().await?.status(),
1282 200,
1283 "Credentials in the URL should still work"
1284 );
1285
1286 Ok(())
1287 }
1288
1289 #[test(tokio::test)]
1290 async fn test_netrc_file_mismatched_username() -> Result<(), Error> {
1291 let username = "user";
1292 let password = "password";
1293 let server = start_test_server(username, password).await;
1294 let base_url = Url::parse(&server.uri())?;
1295
1296 let mut netrc_file = NamedTempFile::new()?;
1297 writeln!(
1298 netrc_file,
1299 r"machine {} login {username} password {password}",
1300 base_url.host_str().unwrap()
1301 )?;
1302
1303 let client = test_client_builder()
1304 .with(
1305 AuthMiddleware::new()
1306 .with_cache(CredentialsCache::new())
1307 .with_netrc(Some(
1308 Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"),
1309 )),
1310 )
1311 .build();
1312
1313 let mut url = base_url.clone();
1314 url.set_username("other-user").unwrap();
1315 assert_eq!(
1316 client.get(url).send().await?.status(),
1317 401,
1318 "The netrc password should not be used due to a username mismatch"
1319 );
1320
1321 let mut url = base_url.clone();
1322 url.set_username("user").unwrap();
1323 assert_eq!(
1324 client.get(url).send().await?.status(),
1325 200,
1326 "The netrc password should be used for a matching user"
1327 );
1328
1329 Ok(())
1330 }
1331
1332 #[test(tokio::test)]
1333 async fn test_keyring() -> Result<(), Error> {
1334 let username = "user";
1335 let password = "password";
1336 let server = start_test_server(username, password).await;
1337 let base_url = Url::parse(&server.uri())?;
1338
1339 let client = test_client_builder()
1340 .with(
1341 AuthMiddleware::new()
1342 .with_cache(CredentialsCache::new())
1343 .with_keyring(Some(KeyringProvider::dummy([(
1344 format!(
1345 "{}:{}",
1346 base_url.host_str().unwrap(),
1347 base_url.port().unwrap()
1348 ),
1349 username,
1350 password,
1351 )]))),
1352 )
1353 .build();
1354
1355 assert_eq!(
1356 client.get(server.uri()).send().await?.status(),
1357 401,
1358 "Credentials are not pulled from the keyring without a username"
1359 );
1360
1361 let mut url = base_url.clone();
1362 url.set_username(username).unwrap();
1363 assert_eq!(
1364 client.get(url).send().await?.status(),
1365 200,
1366 "Credentials for the username should be pulled from the keyring"
1367 );
1368
1369 let mut url = base_url.clone();
1370 url.set_username(username).unwrap();
1371 url.set_password(Some("invalid")).unwrap();
1372 assert_eq!(
1373 client.get(url).send().await?.status(),
1374 401,
1375 "Password in the URL should take precedence and fail"
1376 );
1377
1378 let mut url = base_url.clone();
1379 url.set_username(username).unwrap();
1380 assert_eq!(
1381 client.get(url.clone()).send().await?.status(),
1382 200,
1383 "Subsequent requests should not use the invalid password"
1384 );
1385
1386 let mut url = base_url.clone();
1387 url.set_username("other_user").unwrap();
1388 assert_eq!(
1389 client.get(url).send().await?.status(),
1390 401,
1391 "Credentials are not pulled from the keyring when given another username"
1392 );
1393
1394 Ok(())
1395 }
1396
1397 #[test(tokio::test)]
1398 async fn test_keyring_always_authenticate() -> Result<(), Error> {
1399 let username = "user";
1400 let password = "password";
1401 let server = start_test_server(username, password).await;
1402 let base_url = Url::parse(&server.uri())?;
1403
1404 let indexes = indexes_for(&base_url, AuthPolicy::Always);
1405 let client = test_client_builder()
1406 .with(
1407 AuthMiddleware::new()
1408 .with_cache(CredentialsCache::new())
1409 .with_keyring(Some(KeyringProvider::dummy([(
1410 format!(
1411 "{}:{}",
1412 base_url.host_str().unwrap(),
1413 base_url.port().unwrap()
1414 ),
1415 username,
1416 password,
1417 )])))
1418 .with_indexes(indexes),
1419 )
1420 .build();
1421
1422 assert_eq!(
1423 client.get(server.uri()).send().await?.status(),
1424 200,
1425 "Credentials (including a username) should be pulled from the keyring"
1426 );
1427
1428 let mut url = base_url.clone();
1429 url.set_username(username).unwrap();
1430 assert_eq!(
1431 client.get(url).send().await?.status(),
1432 200,
1433 "The password for the username should be pulled from the keyring"
1434 );
1435
1436 let mut url = base_url.clone();
1437 url.set_username(username).unwrap();
1438 url.set_password(Some("invalid")).unwrap();
1439 assert_eq!(
1440 client.get(url).send().await?.status(),
1441 401,
1442 "Password in the URL should take precedence and fail"
1443 );
1444
1445 let mut url = base_url.clone();
1446 url.set_username("other_user").unwrap();
1447 assert!(
1448 matches!(
1449 client.get(url).send().await,
1450 Err(reqwest_middleware::Error::Middleware(_))
1451 ),
1452 "If the username does not match, a password should not be fetched, and the middleware should fail eagerly since `authenticate = always` is not satisfied"
1453 );
1454
1455 Ok(())
1456 }
1457
1458 #[test(tokio::test)]
1463 async fn test_keyring_includes_non_standard_port() -> Result<(), Error> {
1464 let username = "user";
1465 let password = "password";
1466 let server = start_test_server(username, password).await;
1467 let base_url = Url::parse(&server.uri())?;
1468
1469 let client = test_client_builder()
1470 .with(
1471 AuthMiddleware::new()
1472 .with_cache(CredentialsCache::new())
1473 .with_keyring(Some(KeyringProvider::dummy([(
1474 base_url.host_str().unwrap(),
1476 username,
1477 password,
1478 )]))),
1479 )
1480 .build();
1481
1482 let mut url = base_url.clone();
1483 url.set_username(username).unwrap();
1484 assert_eq!(
1485 client.get(url).send().await?.status(),
1486 401,
1487 "We should fail because the port is not present in the keyring entry"
1488 );
1489
1490 Ok(())
1491 }
1492
1493 #[test(tokio::test)]
1494 async fn test_credentials_in_keyring_seed() -> Result<(), Error> {
1495 let username = "user";
1496 let password = "password";
1497
1498 let server = start_test_server(username, password).await;
1499 let base_url = Url::parse(&server.uri())?;
1500 let cache = CredentialsCache::new();
1501
1502 cache.insert(
1505 &base_url,
1506 Arc::new(Authentication::from(Credentials::basic(
1507 Some(username.to_string()),
1508 None,
1509 ))),
1510 );
1511 let client = test_client_builder()
1512 .with(AuthMiddleware::new().with_cache(cache).with_keyring(Some(
1513 KeyringProvider::dummy([(
1514 format!(
1515 "{}:{}",
1516 base_url.host_str().unwrap(),
1517 base_url.port().unwrap()
1518 ),
1519 username,
1520 password,
1521 )]),
1522 )))
1523 .build();
1524
1525 assert_eq!(
1526 client.get(server.uri()).send().await?.status(),
1527 200,
1528 "The username is pulled from the cache, and the password from the keyring"
1529 );
1530
1531 let mut url = base_url.clone();
1532 url.set_username(username).unwrap();
1533 assert_eq!(
1534 client.get(url).send().await?.status(),
1535 200,
1536 "Credentials for the username should be pulled from the keyring"
1537 );
1538
1539 Ok(())
1540 }
1541
1542 #[test(tokio::test)]
1543 async fn test_credentials_in_url_multiple_realms() -> Result<(), Error> {
1544 let username_1 = "user1";
1545 let password_1 = "password1";
1546 let server_1 = start_test_server(username_1, password_1).await;
1547 let base_url_1 = Url::parse(&server_1.uri())?;
1548
1549 let username_2 = "user2";
1550 let password_2 = "password2";
1551 let server_2 = start_test_server(username_2, password_2).await;
1552 let base_url_2 = Url::parse(&server_2.uri())?;
1553
1554 let cache = CredentialsCache::new();
1555 cache.insert(
1557 &base_url_1,
1558 Arc::new(Authentication::from(Credentials::basic(
1559 Some(username_1.to_string()),
1560 Some(password_1.to_string()),
1561 ))),
1562 );
1563 cache.insert(
1564 &base_url_2,
1565 Arc::new(Authentication::from(Credentials::basic(
1566 Some(username_2.to_string()),
1567 Some(password_2.to_string()),
1568 ))),
1569 );
1570
1571 let client = test_client_builder()
1572 .with(AuthMiddleware::new().with_cache(cache))
1573 .build();
1574
1575 assert_eq!(
1577 client.get(server_1.uri()).send().await?.status(),
1578 200,
1579 "Requests should not require credentials"
1580 );
1581 assert_eq!(
1582 client.get(server_2.uri()).send().await?.status(),
1583 200,
1584 "Requests should not require credentials"
1585 );
1586
1587 assert_eq!(
1588 client
1589 .get(format!("{}/foo", server_1.uri()))
1590 .send()
1591 .await?
1592 .status(),
1593 200,
1594 "Requests can be to different paths in the same realm"
1595 );
1596 assert_eq!(
1597 client
1598 .get(format!("{}/foo", server_2.uri()))
1599 .send()
1600 .await?
1601 .status(),
1602 200,
1603 "Requests can be to different paths in the same realm"
1604 );
1605
1606 Ok(())
1607 }
1608
1609 #[test(tokio::test)]
1610 async fn test_credentials_from_keyring_multiple_realms() -> Result<(), Error> {
1611 let username_1 = "user1";
1612 let password_1 = "password1";
1613 let server_1 = start_test_server(username_1, password_1).await;
1614 let base_url_1 = Url::parse(&server_1.uri())?;
1615
1616 let username_2 = "user2";
1617 let password_2 = "password2";
1618 let server_2 = start_test_server(username_2, password_2).await;
1619 let base_url_2 = Url::parse(&server_2.uri())?;
1620
1621 let client = test_client_builder()
1622 .with(
1623 AuthMiddleware::new()
1624 .with_cache(CredentialsCache::new())
1625 .with_keyring(Some(KeyringProvider::dummy([
1626 (
1627 format!(
1628 "{}:{}",
1629 base_url_1.host_str().unwrap(),
1630 base_url_1.port().unwrap()
1631 ),
1632 username_1,
1633 password_1,
1634 ),
1635 (
1636 format!(
1637 "{}:{}",
1638 base_url_2.host_str().unwrap(),
1639 base_url_2.port().unwrap()
1640 ),
1641 username_2,
1642 password_2,
1643 ),
1644 ]))),
1645 )
1646 .build();
1647
1648 assert_eq!(
1650 client.get(server_1.uri()).send().await?.status(),
1651 401,
1652 "Requests should require a username"
1653 );
1654 assert_eq!(
1655 client.get(server_2.uri()).send().await?.status(),
1656 401,
1657 "Requests should require a username"
1658 );
1659
1660 let mut url_1 = base_url_1.clone();
1661 url_1.set_username(username_1).unwrap();
1662 assert_eq!(
1663 client.get(url_1.clone()).send().await?.status(),
1664 200,
1665 "Requests with a username should succeed"
1666 );
1667 assert_eq!(
1668 client.get(server_2.uri()).send().await?.status(),
1669 401,
1670 "Credentials should not be re-used for the second server"
1671 );
1672
1673 let mut url_2 = base_url_2.clone();
1674 url_2.set_username(username_2).unwrap();
1675 assert_eq!(
1676 client.get(url_2.clone()).send().await?.status(),
1677 200,
1678 "Requests with a username should succeed"
1679 );
1680
1681 assert_eq!(
1682 client.get(format!("{url_1}/foo")).send().await?.status(),
1683 200,
1684 "Requests can be to different paths in the same realm"
1685 );
1686 assert_eq!(
1687 client.get(format!("{url_2}/foo")).send().await?.status(),
1688 200,
1689 "Requests can be to different paths in the same realm"
1690 );
1691
1692 Ok(())
1693 }
1694
1695 #[test(tokio::test)]
1696 async fn test_credentials_in_url_mixed_authentication_in_realm() -> Result<(), Error> {
1697 let username_1 = "user1";
1698 let password_1 = "password1";
1699 let username_2 = "user2";
1700 let password_2 = "password2";
1701
1702 let server = MockServer::start().await;
1703
1704 Mock::given(method("GET"))
1705 .and(path_regex("/prefix_1.*"))
1706 .and(basic_auth(username_1, password_1))
1707 .respond_with(ResponseTemplate::new(200))
1708 .mount(&server)
1709 .await;
1710
1711 Mock::given(method("GET"))
1712 .and(path_regex("/prefix_2.*"))
1713 .and(basic_auth(username_2, password_2))
1714 .respond_with(ResponseTemplate::new(200))
1715 .mount(&server)
1716 .await;
1717
1718 Mock::given(method("GET"))
1721 .and(path_regex("/prefix_3.*"))
1722 .and(basic_auth(username_1, password_1))
1723 .respond_with(ResponseTemplate::new(401))
1724 .mount(&server)
1725 .await;
1726 Mock::given(method("GET"))
1727 .and(path_regex("/prefix_3.*"))
1728 .and(basic_auth(username_2, password_2))
1729 .respond_with(ResponseTemplate::new(401))
1730 .mount(&server)
1731 .await;
1732 Mock::given(method("GET"))
1733 .and(path_regex("/prefix_3.*"))
1734 .respond_with(ResponseTemplate::new(200))
1735 .mount(&server)
1736 .await;
1737
1738 Mock::given(method("GET"))
1739 .respond_with(ResponseTemplate::new(401))
1740 .mount(&server)
1741 .await;
1742
1743 let base_url = Url::parse(&server.uri())?;
1744 let base_url_1 = base_url.join("prefix_1")?;
1745 let base_url_2 = base_url.join("prefix_2")?;
1746 let base_url_3 = base_url.join("prefix_3")?;
1747
1748 let cache = CredentialsCache::new();
1749
1750 cache.insert(
1752 &base_url_1,
1753 Arc::new(Authentication::from(Credentials::basic(
1754 Some(username_1.to_string()),
1755 Some(password_1.to_string()),
1756 ))),
1757 );
1758 cache.insert(
1759 &base_url_2,
1760 Arc::new(Authentication::from(Credentials::basic(
1761 Some(username_2.to_string()),
1762 Some(password_2.to_string()),
1763 ))),
1764 );
1765
1766 let client = test_client_builder()
1767 .with(AuthMiddleware::new().with_cache(cache))
1768 .build();
1769
1770 assert_eq!(
1772 client.get(base_url_1.clone()).send().await?.status(),
1773 200,
1774 "Requests should not require credentials"
1775 );
1776 assert_eq!(
1777 client.get(base_url_2.clone()).send().await?.status(),
1778 200,
1779 "Requests should not require credentials"
1780 );
1781 assert_eq!(
1782 client
1783 .get(base_url.join("prefix_1/foo")?)
1784 .send()
1785 .await?
1786 .status(),
1787 200,
1788 "Requests can be to different paths in the same realm"
1789 );
1790 assert_eq!(
1791 client
1792 .get(base_url.join("prefix_2/foo")?)
1793 .send()
1794 .await?
1795 .status(),
1796 200,
1797 "Requests can be to different paths in the same realm"
1798 );
1799 assert_eq!(
1800 client
1801 .get(base_url.join("prefix_1_foo")?)
1802 .send()
1803 .await?
1804 .status(),
1805 401,
1806 "Requests to paths with a matching prefix but different resource segments should fail"
1807 );
1808
1809 assert_eq!(
1810 client.get(base_url_3.clone()).send().await?.status(),
1811 200,
1812 "Requests to the 'public' prefix should not use credentials"
1813 );
1814
1815 Ok(())
1816 }
1817
1818 #[test(tokio::test)]
1819 async fn test_credentials_from_keyring_mixed_authentication_in_realm() -> Result<(), Error> {
1820 let username_1 = "user1";
1821 let password_1 = "password1";
1822 let username_2 = "user2";
1823 let password_2 = "password2";
1824
1825 let server = MockServer::start().await;
1826
1827 Mock::given(method("GET"))
1828 .and(path_regex("/prefix_1.*"))
1829 .and(basic_auth(username_1, password_1))
1830 .respond_with(ResponseTemplate::new(200))
1831 .mount(&server)
1832 .await;
1833
1834 Mock::given(method("GET"))
1835 .and(path_regex("/prefix_2.*"))
1836 .and(basic_auth(username_2, password_2))
1837 .respond_with(ResponseTemplate::new(200))
1838 .mount(&server)
1839 .await;
1840
1841 Mock::given(method("GET"))
1844 .and(path_regex("/prefix_3.*"))
1845 .and(basic_auth(username_1, password_1))
1846 .respond_with(ResponseTemplate::new(401))
1847 .mount(&server)
1848 .await;
1849 Mock::given(method("GET"))
1850 .and(path_regex("/prefix_3.*"))
1851 .and(basic_auth(username_2, password_2))
1852 .respond_with(ResponseTemplate::new(401))
1853 .mount(&server)
1854 .await;
1855 Mock::given(method("GET"))
1856 .and(path_regex("/prefix_3.*"))
1857 .respond_with(ResponseTemplate::new(200))
1858 .mount(&server)
1859 .await;
1860
1861 Mock::given(method("GET"))
1862 .respond_with(ResponseTemplate::new(401))
1863 .mount(&server)
1864 .await;
1865
1866 let base_url = Url::parse(&server.uri())?;
1867 let base_url_1 = base_url.join("prefix_1")?;
1868 let base_url_2 = base_url.join("prefix_2")?;
1869 let base_url_3 = base_url.join("prefix_3")?;
1870
1871 let client = test_client_builder()
1872 .with(
1873 AuthMiddleware::new()
1874 .with_cache(CredentialsCache::new())
1875 .with_keyring(Some(KeyringProvider::dummy([
1876 (
1877 format!(
1878 "{}:{}",
1879 base_url_1.host_str().unwrap(),
1880 base_url_1.port().unwrap()
1881 ),
1882 username_1,
1883 password_1,
1884 ),
1885 (
1886 format!(
1887 "{}:{}",
1888 base_url_2.host_str().unwrap(),
1889 base_url_2.port().unwrap()
1890 ),
1891 username_2,
1892 password_2,
1893 ),
1894 ]))),
1895 )
1896 .build();
1897
1898 assert_eq!(
1900 client.get(base_url_1.clone()).send().await?.status(),
1901 401,
1902 "Requests should require a username"
1903 );
1904 assert_eq!(
1905 client.get(base_url_2.clone()).send().await?.status(),
1906 401,
1907 "Requests should require a username"
1908 );
1909
1910 let mut url_1 = base_url_1.clone();
1911 url_1.set_username(username_1).unwrap();
1912 assert_eq!(
1913 client.get(url_1.clone()).send().await?.status(),
1914 200,
1915 "Requests with a username should succeed"
1916 );
1917 assert_eq!(
1918 client.get(base_url_2.clone()).send().await?.status(),
1919 401,
1920 "Credentials should not be re-used for the second prefix"
1921 );
1922
1923 let mut url_2 = base_url_2.clone();
1924 url_2.set_username(username_2).unwrap();
1925 assert_eq!(
1926 client.get(url_2.clone()).send().await?.status(),
1927 200,
1928 "Requests with a username should succeed"
1929 );
1930
1931 assert_eq!(
1932 client
1933 .get(base_url.join("prefix_1/foo")?)
1934 .send()
1935 .await?
1936 .status(),
1937 200,
1938 "Requests can be to different paths in the same prefix"
1939 );
1940 assert_eq!(
1941 client
1942 .get(base_url.join("prefix_2/foo")?)
1943 .send()
1944 .await?
1945 .status(),
1946 200,
1947 "Requests can be to different paths in the same prefix"
1948 );
1949 assert_eq!(
1950 client
1951 .get(base_url.join("prefix_1_foo")?)
1952 .send()
1953 .await?
1954 .status(),
1955 401,
1956 "Requests to paths with a matching prefix but different resource segments should fail"
1957 );
1958 assert_eq!(
1959 client.get(base_url_3.clone()).send().await?.status(),
1960 200,
1961 "Requests to the 'public' prefix should not use credentials"
1962 );
1963
1964 Ok(())
1965 }
1966
1967 #[test(tokio::test)]
1971 async fn test_credentials_from_keyring_mixed_authentication_in_realm_same_username()
1972 -> Result<(), Error> {
1973 let username = "user";
1974 let password_1 = "password1";
1975 let password_2 = "password2";
1976
1977 let server = MockServer::start().await;
1978
1979 Mock::given(method("GET"))
1980 .and(path_regex("/prefix_1.*"))
1981 .and(basic_auth(username, password_1))
1982 .respond_with(ResponseTemplate::new(200))
1983 .mount(&server)
1984 .await;
1985
1986 Mock::given(method("GET"))
1987 .and(path_regex("/prefix_2.*"))
1988 .and(basic_auth(username, password_2))
1989 .respond_with(ResponseTemplate::new(200))
1990 .mount(&server)
1991 .await;
1992
1993 Mock::given(method("GET"))
1994 .respond_with(ResponseTemplate::new(401))
1995 .mount(&server)
1996 .await;
1997
1998 let base_url = Url::parse(&server.uri())?;
1999 let base_url_1 = base_url.join("prefix_1")?;
2000 let base_url_2 = base_url.join("prefix_2")?;
2001
2002 let client = test_client_builder()
2003 .with(
2004 AuthMiddleware::new()
2005 .with_cache(CredentialsCache::new())
2006 .with_keyring(Some(KeyringProvider::dummy([
2007 (base_url_1.clone(), username, password_1),
2008 (base_url_2.clone(), username, password_2),
2009 ]))),
2010 )
2011 .build();
2012
2013 assert_eq!(
2015 client.get(base_url_1.clone()).send().await?.status(),
2016 401,
2017 "Requests should require a username"
2018 );
2019 assert_eq!(
2020 client.get(base_url_2.clone()).send().await?.status(),
2021 401,
2022 "Requests should require a username"
2023 );
2024
2025 let mut url_1 = base_url_1.clone();
2026 url_1.set_username(username).unwrap();
2027 assert_eq!(
2028 client.get(url_1.clone()).send().await?.status(),
2029 200,
2030 "The first request with a username will succeed"
2031 );
2032 assert_eq!(
2033 client.get(base_url_2.clone()).send().await?.status(),
2034 401,
2035 "Credentials should not be re-used for the second prefix"
2036 );
2037 assert_eq!(
2038 client
2039 .get(base_url.join("prefix_1/foo")?)
2040 .send()
2041 .await?
2042 .status(),
2043 200,
2044 "Subsequent requests can be to different paths in the same prefix"
2045 );
2046
2047 let mut url_2 = base_url_2.clone();
2048 url_2.set_username(username).unwrap();
2049 assert_eq!(
2050 client.get(url_2.clone()).send().await?.status(),
2051 401, "A request with the same username and realm for a URL that needs a different password will fail"
2053 );
2054 assert_eq!(
2055 client
2056 .get(base_url.join("prefix_2/foo")?)
2057 .send()
2058 .await?
2059 .status(),
2060 401, "Requests to other paths in the failing prefix will also fail"
2062 );
2063
2064 Ok(())
2065 }
2066
2067 #[test(tokio::test)]
2071 async fn test_credentials_from_keyring_mixed_authentication_different_indexes_same_realm()
2072 -> Result<(), Error> {
2073 let username = "user";
2074 let password_1 = "password1";
2075 let password_2 = "password2";
2076
2077 let server = MockServer::start().await;
2078
2079 Mock::given(method("GET"))
2080 .and(path_regex("/prefix_1.*"))
2081 .and(basic_auth(username, password_1))
2082 .respond_with(ResponseTemplate::new(200))
2083 .mount(&server)
2084 .await;
2085
2086 Mock::given(method("GET"))
2087 .and(path_regex("/prefix_2.*"))
2088 .and(basic_auth(username, password_2))
2089 .respond_with(ResponseTemplate::new(200))
2090 .mount(&server)
2091 .await;
2092
2093 Mock::given(method("GET"))
2094 .respond_with(ResponseTemplate::new(401))
2095 .mount(&server)
2096 .await;
2097
2098 let base_url = Url::parse(&server.uri())?;
2099 let base_url_1 = base_url.join("prefix_1")?;
2100 let base_url_2 = base_url.join("prefix_2")?;
2101 let indexes = Indexes::from_indexes(vec![
2102 Index {
2103 url: DisplaySafeUrl::from_url(base_url_1.clone()),
2104 root_url: DisplaySafeUrl::from_url(base_url_1.clone()),
2105 auth_policy: AuthPolicy::Auto,
2106 },
2107 Index {
2108 url: DisplaySafeUrl::from_url(base_url_2.clone()),
2109 root_url: DisplaySafeUrl::from_url(base_url_2.clone()),
2110 auth_policy: AuthPolicy::Auto,
2111 },
2112 ]);
2113
2114 let client = test_client_builder()
2115 .with(
2116 AuthMiddleware::new()
2117 .with_cache(CredentialsCache::new())
2118 .with_keyring(Some(KeyringProvider::dummy([
2119 (base_url_1.clone(), username, password_1),
2120 (base_url_2.clone(), username, password_2),
2121 ])))
2122 .with_indexes(indexes),
2123 )
2124 .build();
2125
2126 assert_eq!(
2128 client.get(base_url_1.clone()).send().await?.status(),
2129 401,
2130 "Requests should require a username"
2131 );
2132 assert_eq!(
2133 client.get(base_url_2.clone()).send().await?.status(),
2134 401,
2135 "Requests should require a username"
2136 );
2137
2138 let mut url_1 = base_url_1.clone();
2139 url_1.set_username(username).unwrap();
2140 assert_eq!(
2141 client.get(url_1.clone()).send().await?.status(),
2142 200,
2143 "The first request with a username will succeed"
2144 );
2145 assert_eq!(
2146 client.get(base_url_2.clone()).send().await?.status(),
2147 401,
2148 "Credentials should not be re-used for the second prefix"
2149 );
2150 assert_eq!(
2151 client
2152 .get(base_url.join("prefix_1/foo")?)
2153 .send()
2154 .await?
2155 .status(),
2156 200,
2157 "Subsequent requests can be to different paths in the same prefix"
2158 );
2159
2160 let mut url_2 = base_url_2.clone();
2161 url_2.set_username(username).unwrap();
2162 assert_eq!(
2163 client.get(url_2.clone()).send().await?.status(),
2164 200,
2165 "A request with the same username and realm for a URL will use index-specific password"
2166 );
2167 assert_eq!(
2168 client
2169 .get(base_url.join("prefix_2/foo")?)
2170 .send()
2171 .await?
2172 .status(),
2173 200,
2174 "Requests to other paths with that prefix will also succeed"
2175 );
2176
2177 Ok(())
2178 }
2179
2180 #[test(tokio::test)]
2183 async fn test_credentials_from_keyring_shared_authentication_different_indexes_same_realm()
2184 -> Result<(), Error> {
2185 let username = "user";
2186 let password = "password";
2187
2188 let server = MockServer::start().await;
2189
2190 Mock::given(method("GET"))
2191 .and(basic_auth(username, password))
2192 .respond_with(ResponseTemplate::new(200))
2193 .mount(&server)
2194 .await;
2195
2196 Mock::given(method("GET"))
2197 .and(path_regex("/prefix_1.*"))
2198 .and(basic_auth(username, password))
2199 .respond_with(ResponseTemplate::new(200))
2200 .mount(&server)
2201 .await;
2202
2203 Mock::given(method("GET"))
2204 .respond_with(ResponseTemplate::new(401))
2205 .mount(&server)
2206 .await;
2207
2208 let base_url = Url::parse(&server.uri())?;
2209 let index_url = base_url.join("prefix_1")?;
2210 let indexes = Indexes::from_indexes(vec![Index {
2211 url: DisplaySafeUrl::from_url(index_url.clone()),
2212 root_url: DisplaySafeUrl::from_url(index_url.clone()),
2213 auth_policy: AuthPolicy::Auto,
2214 }]);
2215
2216 let client = test_client_builder()
2217 .with(
2218 AuthMiddleware::new()
2219 .with_cache(CredentialsCache::new())
2220 .with_keyring(Some(KeyringProvider::dummy([(
2221 base_url.clone(),
2222 username,
2223 password,
2224 )])))
2225 .with_indexes(indexes),
2226 )
2227 .build();
2228
2229 assert_eq!(
2231 client.get(index_url.clone()).send().await?.status(),
2232 401,
2233 "Requests should require a username"
2234 );
2235
2236 let mut realm_url = base_url.clone();
2238 realm_url.set_username(username).unwrap();
2239 assert_eq!(
2240 client.get(realm_url.clone()).send().await?.status(),
2241 200,
2242 "The first realm request with a username will succeed"
2243 );
2244
2245 let mut url = index_url.clone();
2246 url.set_username(username).unwrap();
2247 assert_eq!(
2248 client.get(url.clone()).send().await?.status(),
2249 200,
2250 "A request with the same username and realm for a URL will use the realm if there is no index-specific password"
2251 );
2252 assert_eq!(
2253 client
2254 .get(base_url.join("prefix_1/foo")?)
2255 .send()
2256 .await?
2257 .status(),
2258 200,
2259 "Requests to other paths with that prefix will also succeed"
2260 );
2261
2262 Ok(())
2263 }
2264
2265 fn indexes_for(url: &Url, policy: AuthPolicy) -> Indexes {
2266 let mut url = DisplaySafeUrl::from_url(url.clone());
2267 url.set_password(None).ok();
2268 url.set_username("").ok();
2269 Indexes::from_indexes(vec![Index {
2270 url: url.clone(),
2271 root_url: url.clone(),
2272 auth_policy: policy,
2273 }])
2274 }
2275
2276 #[test(tokio::test)]
2279 async fn test_auth_policy_always_with_credentials() -> Result<(), Error> {
2280 let username = "user";
2281 let password = "password";
2282
2283 let server = start_test_server(username, password).await;
2284
2285 let base_url = Url::parse(&server.uri())?;
2286
2287 let indexes = indexes_for(&base_url, AuthPolicy::Always);
2288 let client = test_client_builder()
2289 .with(
2290 AuthMiddleware::new()
2291 .with_cache(CredentialsCache::new())
2292 .with_indexes(indexes),
2293 )
2294 .build();
2295
2296 Mock::given(method("GET"))
2297 .and(path_regex("/*"))
2298 .and(basic_auth(username, password))
2299 .respond_with(ResponseTemplate::new(200))
2300 .mount(&server)
2301 .await;
2302
2303 Mock::given(method("GET"))
2304 .respond_with(ResponseTemplate::new(401))
2305 .mount(&server)
2306 .await;
2307
2308 let mut url = base_url.clone();
2309 url.set_username(username).unwrap();
2310 url.set_password(Some(password)).unwrap();
2311 assert_eq!(client.get(url).send().await?.status(), 200);
2312
2313 assert_eq!(
2314 client
2315 .get(format!("{}/foo", server.uri()))
2316 .send()
2317 .await?
2318 .status(),
2319 200,
2320 "Requests can be to different paths with index URL as prefix"
2321 );
2322
2323 let mut url = base_url.clone();
2324 url.set_username(username).unwrap();
2325 url.set_password(Some("invalid")).unwrap();
2326 assert_eq!(
2327 client.get(url).send().await?.status(),
2328 401,
2329 "Incorrect credentials should fail"
2330 );
2331
2332 Ok(())
2333 }
2334
2335 #[test(tokio::test)]
2338 async fn test_auth_policy_always_unauthenticated() -> Result<(), Error> {
2339 let server = MockServer::start().await;
2340
2341 Mock::given(method("GET"))
2342 .and(path_regex("/*"))
2343 .respond_with(ResponseTemplate::new(200))
2344 .mount(&server)
2345 .await;
2346
2347 Mock::given(method("GET"))
2348 .respond_with(ResponseTemplate::new(401))
2349 .mount(&server)
2350 .await;
2351
2352 let base_url = Url::parse(&server.uri())?;
2353
2354 let indexes = indexes_for(&base_url, AuthPolicy::Always);
2355 let client = test_client_builder()
2356 .with(
2357 AuthMiddleware::new()
2358 .with_cache(CredentialsCache::new())
2359 .with_indexes(indexes),
2360 )
2361 .build();
2362
2363 assert!(matches!(
2365 client.get(server.uri()).send().await,
2366 Err(reqwest_middleware::Error::Middleware(_))
2367 ));
2368
2369 Ok(())
2370 }
2371
2372 #[test(tokio::test)]
2375 async fn test_auth_policy_never_with_credentials() -> Result<(), Error> {
2376 let username = "user";
2377 let password = "password";
2378
2379 let server = start_test_server(username, password).await;
2380 let base_url = Url::parse(&server.uri())?;
2381
2382 Mock::given(method("GET"))
2383 .and(path_regex("/*"))
2384 .and(basic_auth(username, password))
2385 .respond_with(ResponseTemplate::new(200))
2386 .mount(&server)
2387 .await;
2388
2389 Mock::given(method("GET"))
2390 .respond_with(ResponseTemplate::new(401))
2391 .mount(&server)
2392 .await;
2393
2394 let indexes = indexes_for(&base_url, AuthPolicy::Never);
2395 let client = test_client_builder()
2396 .with(
2397 AuthMiddleware::new()
2398 .with_cache(CredentialsCache::new())
2399 .with_indexes(indexes),
2400 )
2401 .build();
2402
2403 let mut url = base_url.clone();
2404 url.set_username(username).unwrap();
2405 url.set_password(Some(password)).unwrap();
2406
2407 assert_eq!(
2408 client
2409 .get(format!("{}/foo", server.uri()))
2410 .send()
2411 .await?
2412 .status(),
2413 401,
2414 "Requests should not be completed if credentials are required"
2415 );
2416
2417 Ok(())
2418 }
2419
2420 #[test(tokio::test)]
2423 async fn test_auth_policy_never_unauthenticated() -> Result<(), Error> {
2424 let server = MockServer::start().await;
2425
2426 Mock::given(method("GET"))
2427 .and(path_regex("/*"))
2428 .respond_with(ResponseTemplate::new(200))
2429 .mount(&server)
2430 .await;
2431
2432 Mock::given(method("GET"))
2433 .respond_with(ResponseTemplate::new(401))
2434 .mount(&server)
2435 .await;
2436
2437 let base_url = Url::parse(&server.uri())?;
2438
2439 let indexes = indexes_for(&base_url, AuthPolicy::Never);
2440 let client = test_client_builder()
2441 .with(
2442 AuthMiddleware::new()
2443 .with_cache(CredentialsCache::new())
2444 .with_indexes(indexes),
2445 )
2446 .build();
2447
2448 assert_eq!(
2449 client.get(server.uri()).send().await?.status(),
2450 200,
2451 "Requests should succeed if unauthenticated requests can succeed"
2452 );
2453
2454 Ok(())
2455 }
2456
2457 #[test]
2458 fn test_tracing_url() {
2459 let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2461 assert_eq!(
2462 tracing_url(&req, None),
2463 DisplaySafeUrl::parse("https://pypi-proxy.fly.dev/basic-auth/simple").unwrap()
2464 );
2465
2466 let creds = Authentication::from(Credentials::Basic {
2467 username: Username::new(Some(String::from("user"))),
2468 password: None,
2469 });
2470 let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2471 assert_eq!(
2472 tracing_url(&req, Some(&creds)),
2473 DisplaySafeUrl::parse("https://user@pypi-proxy.fly.dev/basic-auth/simple").unwrap()
2474 );
2475
2476 let creds = Authentication::from(Credentials::Basic {
2477 username: Username::new(Some(String::from("user"))),
2478 password: Some(Password::new(String::from("password"))),
2479 });
2480 let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple");
2481 assert_eq!(
2482 tracing_url(&req, Some(&creds)),
2483 DisplaySafeUrl::parse("https://user:password@pypi-proxy.fly.dev/basic-auth/simple")
2484 .unwrap()
2485 );
2486 }
2487
2488 #[test(tokio::test)]
2489 async fn test_text_store_basic_auth() -> Result<(), Error> {
2490 let username = "user";
2491 let password = "password";
2492
2493 let server = start_test_server(username, password).await;
2494 let base_url = Url::parse(&server.uri())?;
2495
2496 let mut store = TextCredentialStore::default();
2498 let service = crate::Service::try_from(base_url.to_string()).unwrap();
2499 let credentials =
2500 Credentials::basic(Some(username.to_string()), Some(password.to_string()));
2501 store.insert(service.clone(), credentials);
2502
2503 let client = test_client_builder()
2504 .with(
2505 AuthMiddleware::new()
2506 .with_cache(CredentialsCache::new())
2507 .with_text_store(Some(store)),
2508 )
2509 .build();
2510
2511 assert_eq!(
2512 client.get(server.uri()).send().await?.status(),
2513 200,
2514 "Credentials should be pulled from the text store"
2515 );
2516
2517 Ok(())
2518 }
2519
2520 #[test(tokio::test)]
2521 async fn test_text_store_disabled() -> Result<(), Error> {
2522 let username = "user";
2523 let password = "password";
2524 let server = start_test_server(username, password).await;
2525
2526 let client = test_client_builder()
2527 .with(
2528 AuthMiddleware::new()
2529 .with_cache(CredentialsCache::new())
2530 .with_text_store(None), )
2532 .build();
2533
2534 assert_eq!(
2535 client.get(server.uri()).send().await?.status(),
2536 401,
2537 "Credentials should not be found when text store is disabled"
2538 );
2539
2540 Ok(())
2541 }
2542
2543 #[test(tokio::test)]
2544 async fn test_text_store_by_username() -> Result<(), Error> {
2545 let username = "testuser";
2546 let password = "testpass";
2547 let wrong_username = "wronguser";
2548
2549 let server = start_test_server(username, password).await;
2550 let base_url = Url::parse(&server.uri())?;
2551
2552 let mut store = TextCredentialStore::default();
2553 let service = crate::Service::try_from(base_url.to_string()).unwrap();
2554 let credentials =
2555 crate::Credentials::basic(Some(username.to_string()), Some(password.to_string()));
2556 store.insert(service.clone(), credentials);
2557
2558 let client = test_client_builder()
2559 .with(
2560 AuthMiddleware::new()
2561 .with_cache(CredentialsCache::new())
2562 .with_text_store(Some(store)),
2563 )
2564 .build();
2565
2566 let url_with_username = format!(
2568 "{}://{}@{}",
2569 base_url.scheme(),
2570 username,
2571 base_url.host_str().unwrap()
2572 );
2573 let url_with_port = if let Some(port) = base_url.port() {
2574 format!("{}:{}{}", url_with_username, port, base_url.path())
2575 } else {
2576 format!("{}{}", url_with_username, base_url.path())
2577 };
2578
2579 assert_eq!(
2580 client.get(&url_with_port).send().await?.status(),
2581 200,
2582 "Request with matching username should succeed"
2583 );
2584
2585 let url_with_wrong_username = format!(
2587 "{}://{}@{}",
2588 base_url.scheme(),
2589 wrong_username,
2590 base_url.host_str().unwrap()
2591 );
2592 let url_with_port = if let Some(port) = base_url.port() {
2593 format!("{}:{}{}", url_with_wrong_username, port, base_url.path())
2594 } else {
2595 format!("{}{}", url_with_wrong_username, base_url.path())
2596 };
2597
2598 assert_eq!(
2599 client.get(&url_with_port).send().await?.status(),
2600 401,
2601 "Request with non-matching username should fail"
2602 );
2603
2604 assert_eq!(
2606 client.get(server.uri()).send().await?.status(),
2607 200,
2608 "Request with no username should succeed"
2609 );
2610
2611 Ok(())
2612 }
2613
2614 fn create_request(url: &str) -> Request {
2615 Request::new(Method::GET, Url::parse(url).unwrap())
2616 }
2617
2618 #[test(tokio::test)]
2623 async fn test_credentials_in_url_empty_username() -> Result<(), Error> {
2624 let username = "";
2625 let password = "token";
2626
2627 let server = MockServer::start().await;
2628
2629 Mock::given(method("GET"))
2630 .and(basic_auth(username, password))
2631 .respond_with(ResponseTemplate::new(200))
2632 .mount(&server)
2633 .await;
2634
2635 Mock::given(method("GET"))
2636 .respond_with(ResponseTemplate::new(401))
2637 .mount(&server)
2638 .await;
2639
2640 let client = test_client_builder()
2641 .with(AuthMiddleware::new().with_cache(CredentialsCache::new()))
2642 .build();
2643
2644 let base_url = Url::parse(&server.uri())?;
2645
2646 let mut url = base_url.clone();
2648 url.set_password(Some(password)).unwrap();
2649 assert_eq!(
2650 client.get(url).send().await?.status(),
2651 200,
2652 "URL with empty username but password should authenticate successfully"
2653 );
2654
2655 assert_eq!(
2657 client.get(server.uri()).send().await?.status(),
2658 200,
2659 "Subsequent requests should use cached credentials"
2660 );
2661
2662 assert_eq!(
2663 client
2664 .get(format!("{}/foo", server.uri()))
2665 .send()
2666 .await?
2667 .status(),
2668 200,
2669 "Requests to different paths in the same realm should succeed"
2670 );
2671
2672 Ok(())
2673 }
2674}