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