1use crate::config::{AuthToken, RegistryIndex};
28use crate::sparse::SparseIndexClient;
29use crate::types::{CargoVersion, CrateInfo};
30use deps_core::parser::DependencySource;
31use deps_core::{DepsError, HttpCache, PackageName, Result};
32use semver::{Version, VersionReq};
33use serde::Deserialize;
34use std::any::Any;
35use std::sync::Arc;
36
37const SPARSE_INDEX_BASE: &str = "https://index.crates.io";
38const SEARCH_API_BASE: &str = "https://crates.io/api/v1";
39
40pub const REGISTRY: &str = "crates.io";
42
43pub const CRATES_IO_URL: &str = "https://crates.io/crates";
45
46pub fn crate_url(name: &str) -> String {
53 format!("{CRATES_IO_URL}/{}", urlencoding::encode(name))
54}
55
56#[derive(Clone)]
61pub struct CratesIoRegistry {
62 sparse: SparseIndexClient,
63 cache: Arc<HttpCache>,
64}
65
66impl CratesIoRegistry {
67 pub fn new(cache: Arc<HttpCache>) -> Self {
69 Self {
70 sparse: SparseIndexClient::new(
71 RegistryIndex::builtin(SPARSE_INDEX_BASE),
72 Arc::clone(&cache),
73 ),
74 cache,
75 }
76 }
77
78 pub async fn get_versions(&self, name: &str) -> Result<Vec<CargoVersion>> {
105 self.sparse.get_versions(name).await
106 }
107
108 pub async fn get_versions_with(
114 &self,
115 name: &str,
116 _freshness: deps_core::freshness::FreshnessSettings,
117 ) -> Result<Vec<CargoVersion>> {
118 self.get_versions(name).await
119 }
120
121 pub async fn get_latest_matching(
147 &self,
148 name: &str,
149 req_str: &str,
150 ) -> Result<Option<CargoVersion>> {
151 self.sparse.get_latest_matching(name, req_str).await
152 }
153
154 pub async fn search(&self, query: &str, limit: usize) -> Result<Vec<CrateInfo>> {
180 let url = format!(
181 "{}/crates?q={}&per_page={}&sort=downloads",
182 SEARCH_API_BASE,
183 urlencoding::encode(query),
184 limit
185 );
186
187 let data = self.cache.get_cached(&url).await?;
188 parse_search_response(&data)
189 }
190}
191
192#[derive(Deserialize)]
194struct SearchResponse {
195 crates: Vec<SearchCrate>,
196}
197
198#[derive(Deserialize)]
200struct SearchCrate {
201 name: String,
202 #[serde(default)]
203 description: Option<String>,
204 #[serde(default)]
205 repository: Option<String>,
206 #[serde(default)]
207 documentation: Option<String>,
208 max_version: String,
209}
210
211fn parse_search_response(data: &[u8]) -> Result<Vec<CrateInfo>> {
213 let response: SearchResponse = deps_core::parse_json_checked(data)?;
214
215 Ok(response
216 .crates
217 .into_iter()
218 .map(|c| CrateInfo {
219 name: c.name.into(),
220 description: c.description,
221 repository: c.repository,
222 documentation: c.documentation,
223 max_version: c.max_version.into(),
224 })
225 .collect())
226}
227
228fn select_latest_matching_impl(
233 versions: &[Box<dyn deps_core::Version>],
234 req: &deps_core::VersionReq,
235) -> Option<usize> {
236 if deps_core::is_existence_wildcard(req) {
237 return deps_core::select_latest_for_existence(versions, |v| v.as_ref());
238 }
239 let parsed_req: VersionReq = req.as_str().parse().ok()?;
240 versions.iter().position(|v| {
241 v.version_string()
242 .as_str()
243 .parse::<Version>()
244 .is_ok_and(|ver| parsed_req.matches(&ver) && !v.removal_status().blocks_resolution())
245 })
246}
247
248impl deps_core::Registry for CratesIoRegistry {
249 fn get_versions<'a>(
250 &'a self,
251 name: &'a deps_core::PackageName,
252 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn deps_core::Version>>>> {
253 Box::pin(async move {
254 let versions = self.get_versions(name.as_str()).await?;
255 Ok(versions
256 .into_iter()
257 .map(|v| Box::new(v) as Box<dyn deps_core::Version>)
258 .collect())
259 })
260 }
261
262 fn get_latest_matching<'a>(
263 &'a self,
264 name: &'a deps_core::PackageName,
265 req: &'a deps_core::VersionReq,
266 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Option<Box<dyn deps_core::Version>>>> {
267 Box::pin(async move {
268 let version = self
269 .get_latest_matching(name.as_str(), req.as_str())
270 .await?;
271 Ok(version.map(|v| Box::new(v) as Box<dyn deps_core::Version>))
272 })
273 }
274
275 fn select_latest_matching(
276 &self,
277 versions: &[Box<dyn deps_core::Version>],
278 req: &deps_core::VersionReq,
279 ) -> Option<usize> {
280 select_latest_matching_impl(versions, req)
281 }
282
283 fn search<'a>(
284 &'a self,
285 query: &'a str,
286 limit: usize,
287 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn deps_core::Metadata>>>> {
288 Box::pin(async move {
289 let results = self.search(query, limit).await?;
290 Ok(results
291 .into_iter()
292 .map(|m| Box::new(m) as Box<dyn deps_core::Metadata>)
293 .collect())
294 })
295 }
296
297 fn as_any(&self) -> &dyn Any {
298 self
299 }
300}
301
302impl deps_core::Registry for SparseIndexClient {
311 fn get_versions<'a>(
312 &'a self,
313 name: &'a deps_core::PackageName,
314 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn deps_core::Version>>>> {
315 Box::pin(async move {
316 let versions = self.get_versions(name.as_str()).await?;
317 Ok(versions
318 .into_iter()
319 .map(|v| Box::new(v) as Box<dyn deps_core::Version>)
320 .collect())
321 })
322 }
323
324 fn get_latest_matching<'a>(
325 &'a self,
326 name: &'a deps_core::PackageName,
327 req: &'a deps_core::VersionReq,
328 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Option<Box<dyn deps_core::Version>>>> {
329 Box::pin(async move {
330 let version = self
331 .get_latest_matching(name.as_str(), req.as_str())
332 .await?;
333 Ok(version.map(|v| Box::new(v) as Box<dyn deps_core::Version>))
334 })
335 }
336
337 fn select_latest_matching(
338 &self,
339 versions: &[Box<dyn deps_core::Version>],
340 req: &deps_core::VersionReq,
341 ) -> Option<usize> {
342 select_latest_matching_impl(versions, req)
343 }
344
345 fn search<'a>(
346 &'a self,
347 _query: &'a str,
348 _limit: usize,
349 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn deps_core::Metadata>>>> {
350 Box::pin(async move { Ok(Vec::new()) })
351 }
352
353 fn as_any(&self) -> &dyn Any {
354 self
355 }
356}
357
358const MAX_ALTERNATE_REGISTRIES: usize = 256;
366
367pub struct CargoRegistry {
388 crates_io: CratesIoRegistry,
389 alternates: dashmap::DashMap<String, Arc<SparseIndexClient>>,
404 cache: Arc<HttpCache>,
405}
406
407impl CargoRegistry {
408 pub fn new(cache: Arc<HttpCache>) -> Self {
411 Self {
412 crates_io: CratesIoRegistry::new(Arc::clone(&cache)),
413 alternates: dashmap::DashMap::new(),
414 cache,
415 }
416 }
417
418 pub fn register_alternate(&self, index: RegistryIndex, auth: Option<AuthToken>) {
443 let at_capacity = self.alternates.len() >= MAX_ALTERNATE_REGISTRIES;
447 let key = index.as_str().to_string();
448 let incoming_trust = index.trust();
449 let index_display = index.to_string();
450
451 match self.alternates.entry(key) {
452 dashmap::mapref::entry::Entry::Occupied(mut slot) => {
453 let stored_trust = slot.get().trust();
454 let folded = stored_trust.min(incoming_trust);
455 if folded != stored_trust {
456 tracing::warn!(
457 index = %index_display,
458 "re-registration folds an alternate registry to the stricter trust \
459 tier; dropping any stored credential"
460 );
461 slot.insert(Arc::new(SparseIndexClient::with_auth(
462 index,
463 Arc::clone(&self.cache),
464 None,
465 "alternate registry",
466 )));
467 }
468 }
469 dashmap::mapref::entry::Entry::Vacant(slot) => {
470 if at_capacity {
471 tracing::warn!(
472 index = %index_display,
473 cap = MAX_ALTERNATE_REGISTRIES,
474 "alternate registry cap reached; not registering a new index"
475 );
476 return;
477 }
478 slot.insert(Arc::new(SparseIndexClient::with_auth(
479 index,
480 Arc::clone(&self.cache),
481 auth,
482 "alternate registry",
483 )));
484 }
485 }
486 }
487
488 #[must_use]
499 pub fn alternate_client(&self, index: &str) -> Option<Arc<SparseIndexClient>> {
500 self.alternates.get(index).map(|entry| Arc::clone(&entry))
501 }
502
503 async fn get_versions_for_source(
504 &self,
505 name: &PackageName,
506 source: &DependencySource,
507 freshness: deps_core::freshness::FreshnessSettings,
508 ) -> Result<Vec<CargoVersion>> {
509 match source {
510 DependencySource::AlternateRegistry {
511 index,
512 mirrors_crates_io,
513 } => match self.alternate_client(index) {
514 Some(client) => client.get_versions(name.as_str()).await,
515 None if *mirrors_crates_io => {
521 self.crates_io
522 .get_versions_with(name.as_str(), freshness)
523 .await
524 }
525 None => Err(DepsError::PackageNotFound {
526 package: name.to_string(),
527 registry: "alternate registry (not registered)",
528 }),
529 },
530 _ => {
531 self.crates_io
532 .get_versions_with(name.as_str(), freshness)
533 .await
534 }
535 }
536 }
537
538 async fn get_latest_matching_for_source(
539 &self,
540 name: &PackageName,
541 source: &DependencySource,
542 req: &deps_core::VersionReq,
543 ) -> Result<Option<CargoVersion>> {
544 match source {
545 DependencySource::AlternateRegistry {
546 index,
547 mirrors_crates_io,
548 } => match self.alternate_client(index) {
549 Some(client) => {
550 client
551 .get_latest_matching(name.as_str(), req.as_str())
552 .await
553 }
554 None if *mirrors_crates_io => {
558 self.crates_io
559 .get_latest_matching(name.as_str(), req.as_str())
560 .await
561 }
562 None => Err(DepsError::PackageNotFound {
563 package: name.to_string(),
564 registry: "alternate registry (not registered)",
565 }),
566 },
567 _ => {
568 self.crates_io
569 .get_latest_matching(name.as_str(), req.as_str())
570 .await
571 }
572 }
573 }
574}
575
576impl deps_core::Registry for CargoRegistry {
577 fn get_versions<'a>(
578 &'a self,
579 name: &'a PackageName,
580 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn deps_core::Version>>>> {
581 deps_core::Registry::get_versions(&self.crates_io, name)
582 }
583
584 fn get_versions_with<'a>(
585 &'a self,
586 name: &'a PackageName,
587 freshness: deps_core::freshness::FreshnessSettings,
588 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn deps_core::Version>>>> {
589 deps_core::Registry::get_versions_with(&self.crates_io, name, freshness)
590 }
591
592 fn get_versions_from<'a>(
593 &'a self,
594 name: &'a PackageName,
595 source: &'a DependencySource,
596 freshness: deps_core::freshness::FreshnessSettings,
597 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn deps_core::Version>>>> {
598 Box::pin(async move {
599 let versions = self
600 .get_versions_for_source(name, source, freshness)
601 .await?;
602 Ok(versions
603 .into_iter()
604 .map(|v| Box::new(v) as Box<dyn deps_core::Version>)
605 .collect())
606 })
607 }
608
609 fn get_latest_matching<'a>(
610 &'a self,
611 name: &'a PackageName,
612 req: &'a deps_core::VersionReq,
613 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Option<Box<dyn deps_core::Version>>>> {
614 deps_core::Registry::get_latest_matching(&self.crates_io, name, req)
615 }
616
617 fn get_latest_matching_with_context<'a>(
618 &'a self,
619 name: &'a PackageName,
620 req: &'a deps_core::VersionReq,
621 minimum_stability: Option<&'a str>,
622 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Option<Box<dyn deps_core::Version>>>> {
623 deps_core::Registry::get_latest_matching_with_context(
624 &self.crates_io,
625 name,
626 req,
627 minimum_stability,
628 )
629 }
630
631 fn get_latest_matching_from<'a>(
632 &'a self,
633 name: &'a PackageName,
634 source: &'a DependencySource,
635 req: &'a deps_core::VersionReq,
636 _minimum_stability: Option<&'a str>,
637 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Option<Box<dyn deps_core::Version>>>> {
638 Box::pin(async move {
639 let version = self
640 .get_latest_matching_for_source(name, source, req)
641 .await?;
642 Ok(version.map(|v| Box::new(v) as Box<dyn deps_core::Version>))
643 })
644 }
645
646 fn select_latest_matching(
647 &self,
648 versions: &[Box<dyn deps_core::Version>],
649 req: &deps_core::VersionReq,
650 ) -> Option<usize> {
651 select_latest_matching_impl(versions, req)
652 }
653
654 fn search<'a>(
657 &'a self,
658 query: &'a str,
659 limit: usize,
660 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Vec<Box<dyn deps_core::Metadata>>>> {
661 deps_core::Registry::search(&self.crates_io, query, limit)
662 }
663
664 fn as_any(&self) -> &dyn Any {
665 self
666 }
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672 use crate::config::IndexTrust;
673 use deps_core::net_policy::RegistryAccessPolicy;
674 use std::assert_matches;
675 use std::collections::HashMap;
676
677 fn test_index(raw: &str) -> RegistryIndex {
680 let policy = RegistryAccessPolicy::default();
681 RegistryIndex::new(raw, IndexTrust::Trusted, &policy).unwrap()
682 }
683
684 fn test_index_with_trust(raw: &str, trust: IndexTrust) -> RegistryIndex {
688 let policy = RegistryAccessPolicy::new(deps_core::net_policy::WorkspaceRegistryAccess::All);
689 RegistryIndex::new(raw, trust, &policy).unwrap()
690 }
691
692 #[tokio::test]
697 #[ignore]
698 async fn test_search_real() {
699 let cache = Arc::new(HttpCache::new());
700 let registry = CratesIoRegistry::new(cache);
701 let results = registry.search("serde", 5).await.unwrap();
702
703 assert!(!results.is_empty());
704 assert!(results.iter().any(|r| r.name == "serde"));
705 }
706
707 #[test]
708 fn test_parse_search_response() {
709 let json = r#"{
710 "crates": [
711 {
712 "name": "serde",
713 "description": "A serialization framework",
714 "repository": "https://github.com/serde-rs/serde",
715 "documentation": "https://docs.rs/serde",
716 "max_version": "1.0.214"
717 }
718 ]
719 }"#;
720
721 let results = parse_search_response(json.as_bytes()).unwrap();
722 assert_eq!(results.len(), 1);
723 assert_eq!(results[0].name, "serde");
724 assert_eq!(results[0].max_version, "1.0.214");
725 }
726
727 #[test]
728 fn test_parse_search_response_empty() {
729 let json = r#"{"crates": []}"#;
730 let results = parse_search_response(json.as_bytes()).unwrap();
731 assert_eq!(results.len(), 0);
732 }
733
734 #[test]
735 fn test_parse_search_response_missing_optional_fields() {
736 let json = r#"{
737 "crates": [
738 {
739 "name": "minimal",
740 "max_version": "1.0.0"
741 }
742 ]
743 }"#;
744
745 let results = parse_search_response(json.as_bytes()).unwrap();
746 assert_eq!(results.len(), 1);
747 assert_eq!(results[0].name, "minimal");
748 assert_eq!(results[0].description, None);
749 assert_eq!(results[0].repository, None);
750 }
751
752 #[test]
753 fn test_parse_search_response_nesting_at_max_depth_accepted() {
754 let depth = deps_core::MAX_JSON_NESTING_DEPTH;
755 let json = format!(
756 r#"{{"crates": [], "extra": {}1{}}}"#,
757 "[".repeat(depth - 1),
758 "]".repeat(depth - 1)
759 );
760 assert!(parse_search_response(json.as_bytes()).is_ok());
761 }
762
763 #[test]
764 fn test_parse_search_response_nesting_over_max_depth_rejected() {
765 let depth = deps_core::MAX_JSON_NESTING_DEPTH + 1;
766 let json = format!(
767 r#"{{"crates": [], "extra": {}1{}}}"#,
768 "[".repeat(depth),
769 "]".repeat(depth)
770 );
771 assert!(parse_search_response(json.as_bytes()).is_err());
772 }
773
774 #[test]
775 fn test_crate_url() {
776 assert_eq!(crate_url("serde"), "https://crates.io/crates/serde");
777 assert_eq!(crate_url("tokio"), "https://crates.io/crates/tokio");
778 }
779
780 #[test]
781 fn test_crate_url_with_hyphens() {
782 assert_eq!(
783 crate_url("serde-json"),
784 "https://crates.io/crates/serde-json"
785 );
786 }
787
788 #[test]
789 fn test_crate_url_encodes_malicious_name() {
790 let url = crate_url("evil](https://evil.example)[pkg");
791 assert!(!url.contains('('));
792 assert!(!url.contains(')'));
793 assert!(!url.contains('['));
794 assert!(!url.contains(']'));
795 }
796
797 #[test]
798 fn test_crate_url_encodes_newline_autolink_and_percent() {
799 let url = crate_url("evil\n<https://evil%zz.example>");
800 assert!(!url.contains('\n'));
801 assert!(!url.contains('<'));
802 assert!(!url.contains('>'));
803 assert!(url.contains("%25"));
804 }
805
806 #[test]
807 fn test_crate_url_empty_name() {
808 assert_eq!(crate_url(""), "https://crates.io/crates/");
809 }
810
811 #[tokio::test]
812 async fn test_registry_creation() {
813 let cache = Arc::new(HttpCache::new());
814 let _registry = CratesIoRegistry::new(cache);
815 }
816
817 #[tokio::test]
818 async fn test_registry_clone() {
819 let cache = Arc::new(HttpCache::new());
820 let registry = CratesIoRegistry::new(cache);
821 let _cloned = registry;
822 }
823
824 #[test]
825 fn test_select_latest_matching_not_default_none() {
826 use deps_core::{Registry, VersionReq};
827
828 let cache = Arc::new(HttpCache::new());
829 let registry = CratesIoRegistry::new(cache);
830 let versions: Vec<Box<dyn deps_core::Version>> = vec![
831 Box::new(CargoVersion {
832 num: "2.0.0".into(),
833 yanked: true,
834 features: HashMap::new(),
835 published_at: None,
836 }),
837 Box::new(CargoVersion {
838 num: "1.0.0".into(),
839 yanked: false,
840 features: HashMap::new(),
841 published_at: None,
842 }),
843 ];
844 let req = VersionReq::new("*");
845 assert_eq!(registry.select_latest_matching(&versions, &req), Some(1));
846 }
847
848 #[test]
849 fn test_select_latest_matching_all_yanked_returns_newest_yanked() {
850 use deps_core::{Registry, VersionReq};
851
852 let cache = Arc::new(HttpCache::new());
853 let registry = CratesIoRegistry::new(cache);
854 let versions: Vec<Box<dyn deps_core::Version>> = vec![
855 Box::new(CargoVersion {
856 num: "2.0.0".into(),
857 yanked: true,
858 features: HashMap::new(),
859 published_at: None,
860 }),
861 Box::new(CargoVersion {
862 num: "1.0.0".into(),
863 yanked: true,
864 features: HashMap::new(),
865 published_at: None,
866 }),
867 ];
868 let req = VersionReq::new("*");
869 assert_eq!(registry.select_latest_matching(&versions, &req), Some(0));
870 }
871
872 #[test]
873 fn test_select_latest_matching_all_prerelease_returns_newest_prerelease() {
874 use deps_core::{Registry, VersionReq};
875
876 let cache = Arc::new(HttpCache::new());
877 let registry = CratesIoRegistry::new(cache);
878 let versions: Vec<Box<dyn deps_core::Version>> = vec![
879 Box::new(CargoVersion {
880 num: "2.0.0-beta.1".into(),
881 yanked: false,
882 features: HashMap::new(),
883 published_at: None,
884 }),
885 Box::new(CargoVersion {
886 num: "1.0.0-alpha.1".into(),
887 yanked: false,
888 features: HashMap::new(),
889 published_at: None,
890 }),
891 ];
892 let req = VersionReq::new("*");
893 assert_eq!(registry.select_latest_matching(&versions, &req), Some(0));
894 }
895
896 #[tokio::test]
897 async fn test_cargo_registry_creation() {
898 let cache = Arc::new(HttpCache::new());
899 let _registry = CargoRegistry::new(cache);
900 }
901
902 #[tokio::test]
903 async fn test_cargo_registry_unregistered_alternate_returns_not_found() {
904 use deps_core::Registry;
905
906 let cache = Arc::new(HttpCache::new());
907 let registry = CargoRegistry::new(cache);
908 let source = DependencySource::AlternateRegistry {
909 index: "https://index.mycorp.dev".to_string(),
910 mirrors_crates_io: false,
911 };
912 let name = PackageName::new("internal-crate");
913 let result = registry
914 .get_versions_from(
915 &name,
916 &source,
917 deps_core::freshness::FreshnessSettings::default(),
918 )
919 .await;
920 assert_matches!(result.err(), Some(DepsError::PackageNotFound { .. }));
921 }
922
923 fn cargo_registry_with_mocked_crates_io(
929 mockito_url: &str,
930 cache: Arc<HttpCache>,
931 ) -> CargoRegistry {
932 let sparse = SparseIndexClient::new(test_index(mockito_url), Arc::clone(&cache));
933 CargoRegistry {
934 crates_io: CratesIoRegistry {
935 sparse,
936 cache: Arc::clone(&cache),
937 },
938 alternates: dashmap::DashMap::new(),
939 cache,
940 }
941 }
942
943 #[tokio::test]
947 async fn test_get_versions_for_source_unregistered_mirror_falls_back_to_crates_io() {
948 let mut server = mockito::Server::new_async().await;
949 let mock = server
950 .mock("GET", "/se/rd/serde")
951 .with_status(200)
952 .with_body(r#"{"name":"serde","vers":"1.0.0","yanked":false,"features":{},"deps":[]}"#)
953 .create_async()
954 .await;
955
956 let cache = Arc::new(HttpCache::new());
957 let registry = cargo_registry_with_mocked_crates_io(&server.url(), cache);
958
959 let source = DependencySource::AlternateRegistry {
960 index: "https://index.never-registered.example".to_string(),
961 mirrors_crates_io: true,
962 };
963 let name = PackageName::new("serde");
964 let versions = registry
965 .get_versions_for_source(
966 &name,
967 &source,
968 deps_core::freshness::FreshnessSettings::default(),
969 )
970 .await
971 .expect("an unregistered mirror must fall back to crates.io, not error");
972 assert_eq!(versions.len(), 1);
973 assert_eq!(versions[0].num, "1.0.0");
974 mock.assert_async().await;
975 }
976
977 #[tokio::test]
985 async fn test_get_versions_from_threads_freshness_through_default_arm() {
986 use deps_core::Registry as _;
987
988 let mut server = mockito::Server::new_async().await;
989 let mock = server
990 .mock("GET", "/se/rd/serde")
991 .with_status(200)
992 .with_body(r#"{"name":"serde","vers":"1.0.0","yanked":false,"features":{},"deps":[]}"#)
993 .expect(2)
994 .create_async()
995 .await;
996
997 let cache = Arc::new(HttpCache::new());
998 let registry = cargo_registry_with_mocked_crates_io(&server.url(), cache);
999 let name = PackageName::new("serde");
1000 let freshness = deps_core::freshness::FreshnessSettings::default();
1001
1002 let via_from = registry
1003 .get_versions_from(&name, &DependencySource::Registry, freshness)
1004 .await
1005 .unwrap();
1006 let via_with = registry.get_versions_with(&name, freshness).await.unwrap();
1007
1008 assert_eq!(via_from.len(), via_with.len());
1009 assert_eq!(
1010 via_from[0].version_string().as_str(),
1011 via_with[0].version_string().as_str()
1012 );
1013 mock.assert_async().await;
1014 }
1015
1016 #[tokio::test]
1020 async fn test_get_latest_matching_for_source_unregistered_mirror_falls_back_to_crates_io() {
1021 let mut server = mockito::Server::new_async().await;
1022 let mock = server
1023 .mock("GET", "/se/rd/serde")
1024 .with_status(200)
1025 .with_body(r#"{"name":"serde","vers":"1.0.0","yanked":false,"features":{},"deps":[]}"#)
1026 .create_async()
1027 .await;
1028
1029 let cache = Arc::new(HttpCache::new());
1030 let registry = cargo_registry_with_mocked_crates_io(&server.url(), cache);
1031
1032 let source = DependencySource::AlternateRegistry {
1033 index: "https://index.never-registered.example".to_string(),
1034 mirrors_crates_io: true,
1035 };
1036 let name = PackageName::new("serde");
1037 let req = deps_core::VersionReq::new("^1.0");
1038 let latest = registry
1039 .get_latest_matching_for_source(&name, &source, &req)
1040 .await
1041 .expect("an unregistered mirror must fall back to crates.io, not error");
1042 assert_eq!(latest.expect("a matching version").num, "1.0.0");
1043 mock.assert_async().await;
1044 }
1045
1046 #[tokio::test]
1050 async fn test_get_latest_matching_for_source_unregistered_non_mirror_stays_not_found() {
1051 let cache = Arc::new(HttpCache::new());
1052 let registry = CargoRegistry::new(cache);
1053 let source = DependencySource::AlternateRegistry {
1054 index: "https://index.never-registered.example".to_string(),
1055 mirrors_crates_io: false,
1056 };
1057 let name = PackageName::new("internal-crate");
1058 let req = deps_core::VersionReq::new("^1.0");
1059 let result = registry
1060 .get_latest_matching_for_source(&name, &source, &req)
1061 .await;
1062 assert_matches!(result, Err(DepsError::PackageNotFound { .. }));
1063 }
1064
1065 #[tokio::test]
1066 async fn test_cargo_registry_register_alternate_is_idempotent() {
1067 let cache = Arc::new(HttpCache::new());
1068 let registry = CargoRegistry::new(cache);
1069 let index = test_index("https://index.mycorp.dev");
1070
1071 registry.register_alternate(index.clone(), None);
1072 assert!(
1073 registry
1074 .alternate_client("https://index.mycorp.dev/")
1075 .is_some()
1076 );
1077
1078 registry.register_alternate(index, Some(AuthToken::new("token".to_string())));
1081 assert!(
1082 registry
1083 .alternate_client("https://index.mycorp.dev/")
1084 .is_some()
1085 );
1086 }
1087
1088 #[tokio::test]
1094 async fn test_cargo_registry_register_alternate_folds_to_stricter_trust_trusted_then_workspace()
1095 {
1096 let cache = Arc::new(HttpCache::new());
1097 let registry = CargoRegistry::new(cache);
1098 let url = "https://index.mycorp.dev";
1099
1100 registry.register_alternate(
1101 test_index_with_trust(url, IndexTrust::Trusted),
1102 Some(AuthToken::new("token".to_string())),
1103 );
1104 let client = registry
1105 .alternate_client("https://index.mycorp.dev/")
1106 .expect("registered");
1107 assert_eq!(client.trust(), IndexTrust::Trusted);
1108 assert!(client.has_auth());
1109
1110 registry.register_alternate(
1111 test_index_with_trust(url, IndexTrust::WorkspaceDeclared),
1112 None,
1113 );
1114 let client = registry
1115 .alternate_client("https://index.mycorp.dev/")
1116 .expect("still registered");
1117 assert_eq!(client.trust(), IndexTrust::WorkspaceDeclared);
1118 assert!(
1119 !client.has_auth(),
1120 "the fold must drop the stored credential"
1121 );
1122 }
1123
1124 #[tokio::test]
1128 async fn test_cargo_registry_register_alternate_folds_to_stricter_trust_workspace_then_trusted()
1129 {
1130 let cache = Arc::new(HttpCache::new());
1131 let registry = CargoRegistry::new(cache);
1132 let url = "https://index.mycorp.dev";
1133
1134 registry.register_alternate(
1135 test_index_with_trust(url, IndexTrust::WorkspaceDeclared),
1136 None,
1137 );
1138 registry.register_alternate(
1139 test_index_with_trust(url, IndexTrust::Trusted),
1140 Some(AuthToken::new("token".to_string())),
1141 );
1142
1143 let client = registry
1144 .alternate_client("https://index.mycorp.dev/")
1145 .expect("still registered");
1146 assert_eq!(client.trust(), IndexTrust::WorkspaceDeclared);
1147 assert!(
1148 !client.has_auth(),
1149 "a WorkspaceDeclared registration must not gain a credential from a later Trusted \
1150 re-registration attempt"
1151 );
1152 }
1153
1154 #[tokio::test]
1158 async fn test_cargo_registry_alternate_cap_skips_registration() {
1159 let cache = Arc::new(HttpCache::new());
1160 let registry = CargoRegistry::new(cache);
1161 for i in 0..MAX_ALTERNATE_REGISTRIES {
1162 let index = test_index(&format!("https://index{i}.example"));
1163 registry.register_alternate(index, None);
1164 }
1165 assert_eq!(registry.alternates.len(), MAX_ALTERNATE_REGISTRIES);
1166
1167 let overflow = test_index("https://overflow.example");
1168 registry.register_alternate(overflow, None);
1169 assert_eq!(registry.alternates.len(), MAX_ALTERNATE_REGISTRIES);
1170 assert!(
1171 registry
1172 .alternate_client("https://overflow.example/")
1173 .is_none()
1174 );
1175 }
1176}