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