1use crate::config::{ChainSeparator, GoProxyChain, GoProxyHop, GoProxyUrl};
29use crate::types::GoVersion;
30use crate::version::{escape_module_path, escape_version, is_pseudo_version};
31use dashmap::DashMap;
32use deps_core::parser::DependencySource;
33use deps_core::{DepsError, HttpCache, Result, is_dot_segment, lsp_helpers::warn_rejected_value};
34use serde::Deserialize;
35use std::any::Any;
36use std::sync::Arc;
37
38const PROXY_BASE: &str = "https://proxy.golang.org";
39
40pub const REGISTRY: &str = "Go proxy";
43
44pub const PKG_GO_DEV_URL: &str = "https://pkg.go.dev";
46
47const MAX_ALTERNATE_REGISTRIES: usize = 256;
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58enum GoRegistryTier {
59 Public,
62 WorkspaceDeclared,
67 Terminal,
72}
73
74const MAX_MODULE_PATH_LENGTH: usize = 500;
76
77const MAX_VERSION_LENGTH: usize = 128;
79
80pub(crate) fn validate_module_path(module_path: &str) -> Result<()> {
95 if module_path.is_empty() {
96 return Err(DepsError::InvalidVersionReq("module path is empty".into()));
97 }
98
99 if module_path.len() > MAX_MODULE_PATH_LENGTH {
100 return Err(DepsError::InvalidVersionReq(format!(
101 "module path exceeds maximum length of {MAX_MODULE_PATH_LENGTH} characters"
102 )));
103 }
104
105 if module_path.split('/').any(is_dot_segment) {
112 warn_rejected_value("is_dot_segment", "Go module proxy request URL", module_path);
113 return Err(DepsError::InvalidVersionReq(format!(
114 "module path '{module_path}' contains a `.`/`..` path segment"
115 )));
116 }
117
118 Ok(())
119}
120
121fn versions_list_url_at(base: &str, module_path: &str) -> String {
127 let escaped = escape_module_path(module_path);
128 format!("{base}/{escaped}/@v/list")
129}
130
131fn validate_version_string(version: &str) -> Result<()> {
140 if version.is_empty() {
141 return Err(DepsError::InvalidVersionReq(
142 "version string is empty".into(),
143 ));
144 }
145
146 if version.len() > MAX_VERSION_LENGTH {
147 return Err(DepsError::InvalidVersionReq(format!(
148 "version string exceeds maximum length of {MAX_VERSION_LENGTH} characters"
149 )));
150 }
151
152 if version.contains("..") || version.contains('/') || version.contains('\\') {
154 return Err(DepsError::InvalidVersionReq(
155 "version string contains invalid characters".into(),
156 ));
157 }
158
159 Ok(())
160}
161
162pub fn package_url(module_path: &str) -> String {
174 let encoded = module_path
175 .split('/')
176 .map(urlencoding::encode)
177 .collect::<Vec<_>>()
178 .join("/");
179 format!("{PKG_GO_DEV_URL}/{encoded}")
180}
181
182fn version_url_at(base: &str, module_path: &str, version: &str, suffix: &str) -> String {
189 let escaped_module = escape_module_path(module_path);
190 let escaped_version = escape_version(version);
191 format!("{base}/{escaped_module}/@v/{escaped_version}.{suffix}")
192}
193
194fn not_found_or(err: DepsError, module_path: &str) -> DepsError {
204 if matches!(
205 err,
206 DepsError::HttpStatus {
207 status: 404 | 410,
208 ..
209 }
210 ) {
211 DepsError::PackageNotFound {
212 package: module_path.to_string(),
213 registry: REGISTRY,
214 }
215 } else {
216 err
217 }
218}
219
220#[derive(Clone)]
225pub struct GoRegistry {
226 cache: Arc<HttpCache>,
227 proxy_base: String,
231 tier: GoRegistryTier,
233 alternates: Arc<DashMap<String, Arc<Self>>>,
239 fallback_chain: Vec<(ChainSeparator, Arc<Self>)>,
245}
246
247impl GoRegistry {
248 pub fn new(cache: Arc<HttpCache>) -> Self {
250 Self {
251 cache,
252 proxy_base: PROXY_BASE.to_string(),
253 tier: GoRegistryTier::Public,
254 alternates: Arc::new(DashMap::new()),
255 fallback_chain: Vec::new(),
256 }
257 }
258
259 #[must_use]
267 fn with_base(
268 cache: Arc<HttpCache>,
269 url: &GoProxyUrl,
270 fallback_chain: Vec<(ChainSeparator, Arc<Self>)>,
271 ) -> Self {
272 Self {
273 cache,
274 proxy_base: url.as_str().to_string(),
275 tier: GoRegistryTier::WorkspaceDeclared,
276 alternates: Arc::new(DashMap::new()),
277 fallback_chain,
278 }
279 }
280
281 #[must_use]
286 fn terminal(cache: Arc<HttpCache>, fallback_chain: Vec<(ChainSeparator, Arc<Self>)>) -> Self {
287 Self {
288 cache,
289 proxy_base: String::new(),
290 tier: GoRegistryTier::Terminal,
291 alternates: Arc::new(DashMap::new()),
292 fallback_chain,
293 }
294 }
295
296 fn hop_client(cache: &Arc<HttpCache>, hop: &GoProxyHop) -> Arc<Self> {
299 Arc::new(match hop {
300 GoProxyHop::Url(url) => Self::with_base(Arc::clone(cache), url, Vec::new()),
301 GoProxyHop::Direct | GoProxyHop::Off => Self::terminal(Arc::clone(cache), Vec::new()),
302 })
303 }
304
305 pub fn register_chain(root: &Arc<Self>, chain: &GoProxyChain) {
310 let Some((first_hop, rest_hops)) = chain.hops.split_first() else {
311 return;
313 };
314
315 let at_capacity = root.alternates.len() >= MAX_ALTERNATE_REGISTRIES;
319
320 if let dashmap::mapref::entry::Entry::Vacant(slot) =
321 root.alternates.entry(chain.key.clone())
322 {
323 if at_capacity {
324 tracing::warn!(
325 key = %chain.key,
326 cap = MAX_ALTERNATE_REGISTRIES,
327 "Go alternate proxy cap reached; not registering a new chain"
328 );
329 return;
330 }
331
332 let fallback_chain: Vec<(ChainSeparator, Arc<Self>)> = rest_hops
337 .iter()
338 .enumerate()
339 .map(|(i, hop)| {
340 let sep = chain
341 .separators
342 .get(i)
343 .copied()
344 .unwrap_or(ChainSeparator::NotFoundOnly);
345 (sep, Self::hop_client(&root.cache, hop))
346 })
347 .collect();
348
349 let head = match first_hop {
350 GoProxyHop::Url(url) => {
351 Self::with_base(Arc::clone(&root.cache), url, fallback_chain)
352 }
353 GoProxyHop::Direct | GoProxyHop::Off => {
354 Self::terminal(Arc::clone(&root.cache), fallback_chain)
355 }
356 };
357 slot.insert(Arc::new(head));
358 }
359 }
360
361 #[must_use]
369 pub fn alternate_client(&self, index: &str) -> Option<Arc<Self>> {
370 self.alternates.get(index).map(|entry| Arc::clone(&entry))
371 }
372
373 async fn get_versions_with_latest_fallback(&self, module_path: &str) -> Result<Vec<GoVersion>> {
378 if let Ok(latest) = self.get_latest(module_path).await {
379 return Ok(vec![latest]);
380 }
381 self.get_versions(module_path).await
382 }
383
384 async fn get_versions_chained(&self, module_path: &str) -> Result<Vec<GoVersion>> {
400 let mut last_miss: Result<Vec<GoVersion>> = Err(DepsError::PackageNotFound {
401 package: module_path.to_string(),
402 registry: REGISTRY,
403 });
404
405 let hops: Vec<&Self> = std::iter::once(self)
409 .chain(self.fallback_chain.iter().map(|(_, hop)| hop.as_ref()))
410 .collect();
411 let next_seps: Vec<Option<ChainSeparator>> = self
412 .fallback_chain
413 .iter()
414 .map(|(sep, _)| Some(*sep))
415 .chain(std::iter::once(None))
416 .collect();
417
418 for (hop, next_sep) in hops.iter().zip(next_seps.iter()) {
419 match hop.get_versions_with_latest_fallback(module_path).await {
420 Ok(versions) if !versions.is_empty() => return Ok(versions),
421 Ok(empty) => last_miss = Ok(empty),
422 Err(DepsError::PackageNotFound { .. }) => {
423 last_miss = Err(DepsError::PackageNotFound {
424 package: module_path.to_string(),
425 registry: REGISTRY,
426 });
427 }
428 Err(other) => match next_sep {
429 Some(ChainSeparator::AnyError) => {
430 tracing::warn!(
431 module = module_path,
432 error = %other,
433 "Go alternate-proxy chain hop failed, but the `|` separator \
434 tolerates any error; falling through to the next hop"
435 );
436 last_miss = Err(other);
437 }
438 _ => {
439 tracing::warn!(
440 module = module_path,
441 error = %other,
442 "Go alternate-proxy chain resolution halted on a transport error — \
443 not falling back to proxy.golang.org or the next configured hop"
444 );
445 return Err(DepsError::ChainResolutionHalted);
446 }
447 },
448 }
449 }
450
451 last_miss
452 }
453
454 pub async fn get_versions(&self, module_path: &str) -> Result<Vec<GoVersion>> {
482 if self.tier == GoRegistryTier::Terminal {
483 return Err(DepsError::PackageNotFound {
484 package: module_path.to_string(),
485 registry: REGISTRY,
486 });
487 }
488 validate_module_path(module_path)?;
489
490 let url = versions_list_url_at(&self.proxy_base, module_path);
491
492 let data = match self.tier {
493 GoRegistryTier::Public => self.cache.get_cached(&url).await,
494 GoRegistryTier::WorkspaceDeclared => self.cache.get_cached_workspace(&url).await,
495 GoRegistryTier::Terminal => unreachable!("short-circuited above"),
496 }
497 .map_err(|e| not_found_or(e, module_path))?;
498
499 parse_version_list(&data)
500 }
501
502 pub async fn get_version_info(&self, module_path: &str, version: &str) -> Result<GoVersion> {
529 if self.tier == GoRegistryTier::Terminal {
530 return Err(DepsError::PackageNotFound {
531 package: module_path.to_string(),
532 registry: REGISTRY,
533 });
534 }
535 validate_module_path(module_path)?;
536 validate_version_string(version)?;
537
538 let url = version_url_at(&self.proxy_base, module_path, version, "info");
539
540 let data = match self.tier {
541 GoRegistryTier::Public => self.cache.get_cached(&url).await,
542 GoRegistryTier::WorkspaceDeclared => self.cache.get_cached_workspace(&url).await,
543 GoRegistryTier::Terminal => unreachable!("short-circuited above"),
544 }
545 .map_err(|e| not_found_or(e, module_path))?;
546
547 parse_version_info(module_path, &data)
548 }
549
550 pub async fn get_latest(&self, module_path: &str) -> Result<GoVersion> {
577 if self.tier == GoRegistryTier::Terminal {
578 return Err(DepsError::PackageNotFound {
579 package: module_path.to_string(),
580 registry: REGISTRY,
581 });
582 }
583 validate_module_path(module_path)?;
584
585 let escaped = escape_module_path(module_path);
586 let url = format!("{}/{escaped}/@latest", self.proxy_base);
587
588 let data = match self.tier {
589 GoRegistryTier::Public => self.cache.get_cached(&url).await,
590 GoRegistryTier::WorkspaceDeclared => self.cache.get_cached_workspace(&url).await,
591 GoRegistryTier::Terminal => unreachable!("short-circuited above"),
592 }
593 .map_err(|e| not_found_or(e, module_path))?;
594
595 parse_version_info(module_path, &data)
596 }
597
598 pub async fn get_go_mod(&self, module_path: &str, version: &str) -> Result<String> {
625 if self.tier == GoRegistryTier::Terminal {
626 return Err(DepsError::PackageNotFound {
627 package: module_path.to_string(),
628 registry: REGISTRY,
629 });
630 }
631 validate_module_path(module_path)?;
632 validate_version_string(version)?;
633
634 let url = version_url_at(&self.proxy_base, module_path, version, "mod");
635
636 let data = match self.tier {
637 GoRegistryTier::Public => self.cache.get_cached(&url).await,
638 GoRegistryTier::WorkspaceDeclared => self.cache.get_cached_workspace(&url).await,
639 GoRegistryTier::Terminal => unreachable!("short-circuited above"),
640 }
641 .map_err(|e| not_found_or(e, module_path))?;
642
643 std::str::from_utf8(&data)
644 .map(std::string::ToString::to_string)
645 .map_err(|e| DepsError::CacheError(format!("Invalid UTF-8 in go.mod: {e}")))
646 }
647}
648
649#[derive(Deserialize)]
651struct VersionInfo {
652 #[serde(rename = "Version")]
653 version: String,
654 #[serde(rename = "Time")]
655 time: String,
656}
657
658fn parse_version_list(data: &[u8]) -> Result<Vec<GoVersion>> {
663 let content = std::str::from_utf8(data).map_err(|e| {
664 DepsError::CacheError(format!("Invalid UTF-8 in version list response: {e}"))
665 })?;
666
667 let mut versions_with_keys: Vec<(GoVersion, Option<semver::Version>)> = content
670 .lines()
671 .filter(|line| !line.trim().is_empty())
672 .map(|line| {
673 let is_pseudo = is_pseudo_version(line);
674 let sort_key = parse_sort_key(line, is_pseudo);
675 let version = GoVersion {
676 version: line.into(),
677 published_at: None,
680 is_pseudo,
681 retracted: false,
682 };
683 (version, sort_key)
684 })
685 .collect();
686
687 versions_with_keys.sort_by(|a, b| match (&b.1, &a.1) {
689 (Some(v1), Some(v2)) => v1.cmp(v2),
690 (Some(_), None) => std::cmp::Ordering::Less,
691 (None, Some(_)) => std::cmp::Ordering::Greater,
692 (None, None) => b.0.version.as_str().cmp(a.0.version.as_str()),
693 });
694
695 Ok(versions_with_keys.into_iter().map(|(v, _)| v).collect())
696}
697
698fn parse_sort_key(version: &str, is_pseudo: bool) -> Option<semver::Version> {
701 use crate::version::base_version_from_pseudo;
702
703 let clean = version.trim_start_matches('v').replace("+incompatible", "");
704 let cmp_str = if is_pseudo {
705 base_version_from_pseudo(version).unwrap_or(clean)
706 } else {
707 clean
708 };
709
710 let base = cmp_str.split('-').next().unwrap_or(&cmp_str);
712 semver::Version::parse(base.trim_start_matches('v')).ok()
713}
714
715fn parse_version_info(module_path: &str, data: &[u8]) -> Result<GoVersion> {
717 let info: VersionInfo =
718 deps_core::parse_json_checked(data).map_err(|e| DepsError::ApiResponse {
719 package: module_path.to_string(),
720 registry: REGISTRY,
721 source: e,
722 })?;
723
724 let is_pseudo = is_pseudo_version(&info.version);
725 Ok(GoVersion {
726 version: info.version.into(),
727 published_at: deps_core::PublishTime::parse_rfc3339(&info.time),
728 is_pseudo,
729 retracted: false,
730 })
731}
732
733impl deps_core::Registry for GoRegistry {
734 fn get_versions<'a>(
735 &'a self,
736 name: &'a deps_core::PackageName,
737 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn deps_core::Version>>>>
738 {
739 Box::pin(async move {
740 let versions = self.get_versions(name.as_str()).await?;
741 Ok(versions
742 .into_iter()
743 .map(|v| Box::new(v) as Box<dyn deps_core::Version>)
744 .collect())
745 })
746 }
747
748 fn get_latest_matching<'a>(
749 &'a self,
750 name: &'a deps_core::PackageName,
751 _req: &'a deps_core::VersionReq,
752 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn deps_core::Version>>>>
753 {
754 Box::pin(async move {
755 if let Ok(version) = self.get_latest(name.as_str()).await {
757 return Ok(Some(Box::new(version) as Box<dyn deps_core::Version>));
758 }
759 let versions = self.get_versions(name.as_str()).await?;
761 let latest = versions.into_iter().find(|v| !v.is_pseudo && !v.retracted);
762 Ok(latest.map(|v| Box::new(v) as Box<dyn deps_core::Version>))
763 })
764 }
765
766 fn get_versions_from<'a>(
774 &'a self,
775 name: &'a deps_core::PackageName,
776 source: &'a DependencySource,
777 freshness: deps_core::FreshnessSettings,
778 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn deps_core::Version>>>>
779 {
780 Box::pin(async move {
781 match source {
782 DependencySource::AlternateRegistry { index, .. } => {
783 match self.alternate_client(index) {
784 Some(client) => {
785 let versions = client.get_versions_chained(name.as_str()).await?;
786 Ok(versions
787 .into_iter()
788 .map(|v| Box::new(v) as Box<dyn deps_core::Version>)
789 .collect())
790 }
791 None => Err(DepsError::PackageNotFound {
792 package: name.to_string(),
793 registry: "alternate registry (not registered)",
794 }),
795 }
796 }
797 _ => deps_core::Registry::get_versions_with(self, name, freshness).await,
798 }
799 })
800 }
801
802 fn get_latest_matching_from<'a>(
812 &'a self,
813 name: &'a deps_core::PackageName,
814 source: &'a DependencySource,
815 req: &'a deps_core::VersionReq,
816 _minimum_stability: Option<&'a str>,
817 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn deps_core::Version>>>>
818 {
819 Box::pin(async move {
820 match source {
821 DependencySource::AlternateRegistry { index, .. } => {
822 match self.alternate_client(index) {
823 Some(client) => {
824 let versions: Vec<Box<dyn deps_core::Version>> = client
825 .get_versions_chained(name.as_str())
826 .await?
827 .into_iter()
828 .map(|v| Box::new(v) as Box<dyn deps_core::Version>)
829 .collect();
830 let idx = deps_core::Registry::select_latest_matching(
831 client.as_ref(),
832 &versions,
833 req,
834 );
835 Ok(idx.and_then(|i| versions.into_iter().nth(i)))
836 }
837 None => Err(DepsError::PackageNotFound {
838 package: name.to_string(),
839 registry: "alternate registry (not registered)",
840 }),
841 }
842 }
843 _ => deps_core::Registry::get_latest_matching(self, name, req).await,
846 }
847 })
848 }
849
850 fn search<'a>(
851 &'a self,
852 _query: &'a str,
853 _limit: usize,
854 ) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn deps_core::Metadata>>>>
855 {
856 Box::pin(async move { Ok(vec![]) })
858 }
859
860 fn select_latest_matching(
861 &self,
862 versions: &[Box<dyn deps_core::Version>],
863 _req: &deps_core::VersionReq,
864 ) -> Option<usize> {
865 versions
877 .iter()
878 .position(|v| !v.is_prerelease() && !v.removal_status().blocks_resolution())
879 }
880
881 fn reports_yanked(&self) -> bool {
885 false
886 }
887
888 fn as_any(&self) -> &dyn Any {
889 self
890 }
891}
892
893#[cfg(test)]
894mod tests {
895 use super::*;
896
897 use std::assert_matches;
898
899 #[test]
900 fn test_parse_version_list() {
901 let data = b"v1.0.0\nv1.0.1\nv1.1.0\nv2.0.0\n";
902
903 let versions = parse_version_list(data).unwrap();
904 assert_eq!(versions.len(), 4);
905 assert_eq!(versions[0].version, "v2.0.0");
907 assert_eq!(versions[1].version, "v1.1.0");
908 assert_eq!(versions[2].version, "v1.0.1");
909 assert_eq!(versions[3].version, "v1.0.0");
910 assert!(!versions[0].is_pseudo);
911 }
912
913 #[test]
914 fn test_parse_version_list_with_pseudo() {
915 let data = b"v1.0.0\nv0.0.0-20191109021931-daa7c04131f5\nv1.1.0\n";
916
917 let versions = parse_version_list(data).unwrap();
918 assert_eq!(versions.len(), 3);
919 assert_eq!(versions[0].version, "v1.1.0");
921 assert!(!versions[0].is_pseudo);
922 assert_eq!(versions[1].version, "v1.0.0");
923 assert!(!versions[1].is_pseudo);
924 assert!(versions[2].is_pseudo);
925 }
926
927 #[test]
928 fn test_parse_version_list_empty() {
929 let data = b"";
930 let versions = parse_version_list(data).unwrap();
931 assert_eq!(versions.len(), 0);
932 }
933
934 #[test]
935 fn test_parse_version_list_blank_lines() {
936 let data = b"\n\n\n";
937 let versions = parse_version_list(data).unwrap();
938 assert_eq!(versions.len(), 0);
939 }
940
941 #[test]
942 fn test_parse_version_info() {
943 let json = r#"{"Version":"v1.9.1","Time":"2023-07-18T14:30:00Z"}"#;
944 let version = parse_version_info("github.com/gin-gonic/gin", json.as_bytes()).unwrap();
945 assert_eq!(version.version, "v1.9.1");
946 assert_eq!(
947 version.published_at,
948 deps_core::PublishTime::parse_rfc3339("2023-07-18T14:30:00Z")
949 );
950 assert!(!version.is_pseudo);
951 }
952
953 #[test]
954 fn test_parse_version_info_with_malformed_time() {
955 let json = r#"{"Version":"v1.9.1","Time":"not-a-timestamp"}"#;
956 let version = parse_version_info("github.com/gin-gonic/gin", json.as_bytes()).unwrap();
957 assert!(
958 version.published_at.is_none(),
959 "malformed Time degrades to None, not an error"
960 );
961 }
962
963 #[test]
964 fn test_parse_version_info_pseudo() {
965 let json =
966 r#"{"Version":"v0.0.0-20191109021931-daa7c04131f5","Time":"2019-11-09T02:19:31Z"}"#;
967 let version = parse_version_info("github.com/gin-gonic/gin", json.as_bytes()).unwrap();
968 assert_eq!(version.version, "v0.0.0-20191109021931-daa7c04131f5");
969 assert!(version.is_pseudo);
970 }
971
972 #[test]
973 fn test_parse_version_info_invalid_json() {
974 let json = b"not json";
975 let result = parse_version_info("github.com/gin-gonic/gin", json);
976 assert!(result.is_err());
977 }
978
979 #[test]
980 fn test_parse_version_info_nesting_at_max_depth_accepted() {
981 let depth = deps_core::MAX_JSON_NESTING_DEPTH;
982 let json = format!(
983 r#"{{"Version": "v1.0.0", "Time": "2024-01-01T00:00:00Z", "extra": {}1{}}}"#,
984 "[".repeat(depth - 1),
985 "]".repeat(depth - 1)
986 );
987 assert!(parse_version_info("github.com/gin-gonic/gin", json.as_bytes()).is_ok());
988 }
989
990 #[test]
991 fn test_parse_version_info_nesting_over_max_depth_rejected() {
992 let depth = deps_core::MAX_JSON_NESTING_DEPTH + 1;
993 let json = format!(
994 r#"{{"Version": "v1.0.0", "Time": "2024-01-01T00:00:00Z", "extra": {}1{}}}"#,
995 "[".repeat(depth),
996 "]".repeat(depth)
997 );
998 assert!(parse_version_info("github.com/gin-gonic/gin", json.as_bytes()).is_err());
999 }
1000
1001 #[test]
1002 fn test_not_found_or_maps_404_to_package_not_found() {
1003 let err = DepsError::HttpStatus {
1004 url: "https://proxy.golang.org/github.com/x/y/@v/list".into(),
1005 status: 404,
1006 };
1007 let result = not_found_or(err, "github.com/x/y");
1008 assert_matches!(
1009 result,
1010 DepsError::PackageNotFound { package, registry }
1011 if package == "github.com/x/y" && registry == REGISTRY
1012 );
1013 }
1014
1015 #[test]
1016 fn test_not_found_or_passes_through_non_404() {
1017 let err = DepsError::HttpStatus {
1018 url: "https://proxy.golang.org/github.com/x/y/@v/list".into(),
1019 status: 500,
1020 };
1021 let result = not_found_or(err, "github.com/x/y");
1022 assert_matches!(result, DepsError::HttpStatus { status: 500, .. });
1023 }
1024
1025 #[test]
1028 fn test_not_found_or_maps_410_to_package_not_found() {
1029 let err = DepsError::HttpStatus {
1030 url: "https://goproxy.mycorp.example/github.com/x/y/@v/list".into(),
1031 status: 410,
1032 };
1033 let result = not_found_or(err, "github.com/x/y");
1034 assert_matches!(
1035 result,
1036 DepsError::PackageNotFound { package, registry }
1037 if package == "github.com/x/y" && registry == REGISTRY
1038 );
1039 }
1040
1041 #[test]
1042 fn test_package_url() {
1043 assert_eq!(
1044 package_url("github.com/gin-gonic/gin"),
1045 "https://pkg.go.dev/github.com/gin-gonic/gin"
1046 );
1047 assert_eq!(
1048 package_url("golang.org/x/crypto"),
1049 "https://pkg.go.dev/golang.org/x/crypto"
1050 );
1051 }
1052
1053 #[test]
1054 fn test_package_url_encodes_malicious_chars() {
1055 let url = package_url("github.com/evil](https://evil.example)[pkg");
1056 assert!(!url.contains('('));
1057 assert!(!url.contains(')'));
1058 assert!(!url.contains('['));
1059 assert!(!url.contains(']'));
1060 assert!(
1061 url.contains("github.com/evil"),
1062 "legitimate path preserved: {url}"
1063 );
1064 }
1065
1066 #[test]
1067 fn test_package_url_encodes_newline_autolink_and_percent() {
1068 let url = package_url("github.com/evil\n<https://evil%zz.example>");
1069 assert!(!url.contains('\n'));
1070 assert!(!url.contains('<'));
1071 assert!(!url.contains('>'));
1072 assert!(url.contains("%25"));
1073 }
1074
1075 #[test]
1076 fn test_package_url_empty_module_path() {
1077 assert_eq!(package_url(""), "https://pkg.go.dev/");
1078 }
1079
1080 #[test]
1085 fn test_info_url_construction_legitimate_version() {
1086 let url = version_url_at(PROXY_BASE, "github.com/gin-gonic/gin", "v1.9.1", "info");
1087 assert_eq!(
1088 url,
1089 "https://proxy.golang.org/github.com/gin-gonic/gin/@v/v1.9.1.info"
1090 );
1091 }
1092
1093 #[test]
1096 fn test_info_url_construction_pseudo_version() {
1097 let url = version_url_at(
1098 PROXY_BASE,
1099 "github.com/user/repo",
1100 "v0.0.0-20210101000000-abcdef123456",
1101 "info",
1102 );
1103 assert_eq!(
1104 url,
1105 "https://proxy.golang.org/github.com/user/repo/@v/v0.0.0-20210101000000-abcdef123456.info"
1106 );
1107 }
1108
1109 #[test]
1116 fn test_info_url_construction_rejects_query_and_fragment_injection() {
1117 let cases = [
1118 ("v1?a=b", "v1%3Fa%3Db"),
1119 ("v1#frag", "v1%23frag"),
1120 ("v1 x", "v1%20x"),
1121 ("v1&x=y", "v1%26x%3Dy"),
1122 ];
1123
1124 for (raw_version, expected_escaped) in cases {
1125 let url = version_url_at(PROXY_BASE, "github.com/gin-gonic/gin", raw_version, "info");
1126 let expected_url = format!(
1127 "https://proxy.golang.org/github.com/gin-gonic/gin/@v/{expected_escaped}.info"
1128 );
1129 assert_eq!(url, expected_url, "raw version: {raw_version:?}");
1130
1131 let after_base = url
1134 .strip_prefix("https://proxy.golang.org/")
1135 .expect("URL must start with the proxy base");
1136 assert!(
1137 !after_base.contains('?'),
1138 "constructed URL must not contain a bare '?' for input {raw_version:?}: {url}"
1139 );
1140 assert!(
1141 !after_base.contains('#'),
1142 "constructed URL must not contain a bare '#' for input {raw_version:?}: {url}"
1143 );
1144 assert!(
1145 !after_base.contains(' '),
1146 "constructed URL must not contain a raw space for input {raw_version:?}: {url}"
1147 );
1148 }
1149 }
1150
1151 #[test]
1153 fn test_mod_url_construction_legitimate_version() {
1154 let url = version_url_at(PROXY_BASE, "github.com/gin-gonic/gin", "v1.9.1", "mod");
1155 assert_eq!(
1156 url,
1157 "https://proxy.golang.org/github.com/gin-gonic/gin/@v/v1.9.1.mod"
1158 );
1159 }
1160
1161 #[test]
1165 fn test_mod_url_construction_rejects_query_and_fragment_injection() {
1166 let cases = [
1167 ("v1?a=b", "v1%3Fa%3Db"),
1168 ("v1#frag", "v1%23frag"),
1169 ("v1 x", "v1%20x"),
1170 ];
1171
1172 for (raw_version, expected_escaped) in cases {
1173 let url = version_url_at(PROXY_BASE, "github.com/gin-gonic/gin", raw_version, "mod");
1174 let expected_url = format!(
1175 "https://proxy.golang.org/github.com/gin-gonic/gin/@v/{expected_escaped}.mod"
1176 );
1177 assert_eq!(url, expected_url, "raw version: {raw_version:?}");
1178
1179 let after_base = url
1180 .strip_prefix("https://proxy.golang.org/")
1181 .expect("URL must start with the proxy base");
1182 assert!(!after_base.contains('?'), "raw version: {raw_version:?}");
1183 assert!(!after_base.contains('#'), "raw version: {raw_version:?}");
1184 }
1185 }
1186
1187 #[test]
1191 fn test_info_url_construction_case_folds_uppercase_version() {
1192 let url = version_url_at(PROXY_BASE, "github.com/user/repo", "v1.7.0-RC", "info");
1193 assert_eq!(
1194 url,
1195 "https://proxy.golang.org/github.com/user/repo/@v/v1.7.0-!r!c.info"
1196 );
1197 }
1198
1199 #[tokio::test]
1200 async fn test_registry_creation() {
1201 let cache = Arc::new(HttpCache::new());
1202 let _registry = GoRegistry::new(cache);
1203 }
1204
1205 #[tokio::test]
1213 async fn test_get_versions_rejects_bare_dot_dot_segment() {
1214 let registry = GoRegistry::new(Arc::new(HttpCache::new()));
1215 let err = registry
1216 .get_versions("github.com/user/..")
1217 .await
1218 .unwrap_err();
1219 assert_matches!(err, DepsError::InvalidVersionReq(_));
1220 }
1221
1222 #[tokio::test]
1223 async fn test_registry_clone() {
1224 let cache = Arc::new(HttpCache::new());
1225 let registry = GoRegistry::new(cache);
1226 let _cloned = registry;
1227 }
1228
1229 #[tokio::test]
1230 #[ignore]
1231 async fn test_fetch_real_gin_versions() {
1232 let cache = Arc::new(HttpCache::new());
1233 let registry = GoRegistry::new(cache);
1234 let versions = registry
1235 .get_versions("github.com/gin-gonic/gin")
1236 .await
1237 .unwrap();
1238
1239 assert!(!versions.is_empty());
1240 assert!(
1241 versions
1242 .iter()
1243 .any(|v| v.version.as_str().starts_with("v1."))
1244 );
1245 }
1246
1247 #[tokio::test]
1248 #[ignore]
1249 async fn test_fetch_real_version_info() {
1250 let cache = Arc::new(HttpCache::new());
1251 let registry = GoRegistry::new(cache);
1252 let info = registry
1253 .get_version_info("github.com/gin-gonic/gin", "v1.9.1")
1254 .await
1255 .unwrap();
1256
1257 assert_eq!(info.version, "v1.9.1");
1258 assert!(info.published_at.is_some());
1259 }
1260
1261 #[tokio::test]
1262 #[ignore]
1263 async fn test_fetch_real_latest() {
1264 let cache = Arc::new(HttpCache::new());
1265 let registry = GoRegistry::new(cache);
1266 let latest = registry
1267 .get_latest("github.com/gin-gonic/gin")
1268 .await
1269 .unwrap();
1270
1271 assert!(latest.version.as_str().starts_with('v'));
1272 assert!(!latest.is_pseudo);
1273 }
1274
1275 #[tokio::test]
1276 #[ignore]
1277 async fn test_fetch_real_go_mod() {
1278 let cache = Arc::new(HttpCache::new());
1279 let registry = GoRegistry::new(cache);
1280 let go_mod = registry
1281 .get_go_mod("github.com/gin-gonic/gin", "v1.9.1")
1282 .await
1283 .unwrap();
1284
1285 assert!(go_mod.contains("module github.com/gin-gonic/gin"));
1286 }
1287
1288 #[tokio::test]
1289 #[ignore]
1290 async fn test_module_not_found() {
1291 let cache = Arc::new(HttpCache::new());
1292 let registry = GoRegistry::new(cache);
1293 let result = registry
1294 .get_versions("github.com/nonexistent/module12345")
1295 .await;
1296 assert!(result.is_err());
1297 }
1298
1299 #[test]
1300 fn test_parse_version_list_mixed_stable_and_pseudo() {
1301 let data = b"v1.0.0\nv1.1.0-0.20200101000000-abcdefabcdef\nv1.2.0\nv1.2.1-beta.1\n";
1302 let versions = parse_version_list(data).unwrap();
1303 assert_eq!(versions.len(), 4);
1304 assert_eq!(versions[0].version, "v1.2.1-beta.1");
1306 assert!(!versions[0].is_pseudo); assert_eq!(versions[1].version, "v1.2.0");
1308 assert!(!versions[1].is_pseudo);
1309 assert!(versions[2].is_pseudo); assert_eq!(versions[3].version, "v1.0.0");
1311 assert!(!versions[3].is_pseudo);
1312 }
1313
1314 #[test]
1315 fn test_parse_version_list_invalid_utf8() {
1316 let data = &[0xFF, 0xFE, 0xFD]; let result = parse_version_list(data);
1318 assert!(result.is_err());
1319 }
1320
1321 #[test]
1322 fn test_parse_version_info_missing_fields() {
1323 let json = r#"{"Version":"v1.0.0"}"#; let result = parse_version_info("github.com/gin-gonic/gin", json.as_bytes());
1325 assert!(result.is_err());
1326 }
1327
1328 #[test]
1329 fn test_validate_module_path_empty() {
1330 let result = validate_module_path("");
1331 match result {
1332 Err(DepsError::InvalidVersionReq(msg)) => assert_eq!(msg, "module path is empty"),
1333 other => panic!("expected InvalidVersionReq, got {other:?}"),
1334 }
1335 }
1336
1337 #[test]
1338 fn test_validate_module_path_too_long() {
1339 let long_path = "a".repeat(MAX_MODULE_PATH_LENGTH + 1);
1340 let result = validate_module_path(&long_path);
1341 assert!(result.is_err());
1342 assert_matches!(result, Err(DepsError::InvalidVersionReq(_)));
1343 }
1344
1345 #[test]
1346 fn test_validate_module_path_valid() {
1347 let result = validate_module_path("github.com/user/repo");
1348 assert!(result.is_ok());
1349 }
1350
1351 #[test]
1352 fn test_validate_module_path_rejects_bare_dot_dot_segment() {
1353 let result = validate_module_path("github.com/user/..");
1354 assert!(result.is_err());
1355 assert_matches!(result, Err(DepsError::InvalidVersionReq(_)));
1356 }
1357
1358 #[test]
1359 fn test_validate_module_path_rejects_bare_dot_segment() {
1360 let result = validate_module_path("./evil");
1361 assert!(result.is_err());
1362 assert_matches!(result, Err(DepsError::InvalidVersionReq(_)));
1363 }
1364
1365 #[test]
1366 fn test_validate_module_path_accepts_dots_within_a_segment() {
1367 assert!(validate_module_path("golang.org/x/mod").is_ok());
1370 }
1371
1372 #[test]
1376 fn test_versions_list_url_bare_dot_dot_normalizes_above_proxy_root() {
1377 let url = versions_list_url_at(PROXY_BASE, "..");
1378 let parsed = url::Url::parse(&url).unwrap();
1379 assert_eq!(parsed.path(), "/@v/list", "parsed path: {}", parsed.path());
1380 }
1381
1382 #[test]
1386 fn test_versions_list_url_dot_segment_sweep() {
1387 deps_core::test_util::assert_dot_segment_gated_or_contained(
1388 |seg| {
1389 validate_module_path(seg)
1390 .ok()
1391 .map(|()| versions_list_url_at(PROXY_BASE, seg))
1392 },
1393 "proxy.golang.org",
1394 "/",
1395 );
1396 }
1397
1398 #[test]
1399 fn test_validate_version_string_empty() {
1400 let result = validate_version_string("");
1401 match result {
1402 Err(DepsError::InvalidVersionReq(msg)) => assert_eq!(msg, "version string is empty"),
1403 other => panic!("expected InvalidVersionReq, got {other:?}"),
1404 }
1405 }
1406
1407 #[test]
1408 fn test_validate_version_string_too_long() {
1409 let long_version = "v".to_string() + &"1".repeat(MAX_VERSION_LENGTH);
1410 let result = validate_version_string(&long_version);
1411 assert!(result.is_err());
1412 assert_matches!(result, Err(DepsError::InvalidVersionReq(_)));
1413 }
1414
1415 #[test]
1416 fn test_validate_version_string_path_traversal() {
1417 let result = validate_version_string("v1.0.0/../etc/passwd");
1418 assert!(result.is_err());
1419 assert_matches!(result, Err(DepsError::InvalidVersionReq(_)));
1420 }
1421
1422 #[test]
1423 fn test_validate_version_string_slashes() {
1424 let result = validate_version_string("v1.0.0/malicious");
1425 assert!(result.is_err());
1426
1427 let result = validate_version_string("v1.0.0\\malicious");
1428 assert!(result.is_err());
1429 }
1430
1431 #[test]
1432 fn test_validate_version_string_valid() {
1433 let result = validate_version_string("v1.0.0");
1434 assert!(result.is_ok());
1435
1436 let result = validate_version_string("v0.0.0-20191109021931-daa7c04131f5");
1437 assert!(result.is_ok());
1438 }
1439
1440 #[test]
1445 fn test_select_latest_matching_agrees_with_get_latest_matching_list_fallback() {
1446 use deps_core::{Registry, VersionReq};
1447
1448 let cache = Arc::new(HttpCache::new());
1449 let registry = GoRegistry::new(cache);
1450 let typed = vec![
1451 GoVersion {
1452 version: "v2.0.0-pseudo".into(),
1453 published_at: None,
1454 is_pseudo: true,
1455 retracted: false,
1456 },
1457 GoVersion {
1458 version: "v1.5.0".into(),
1459 published_at: None,
1460 is_pseudo: false,
1461 retracted: true,
1462 },
1463 GoVersion {
1464 version: "v1.0.0".into(),
1465 published_at: None,
1466 is_pseudo: false,
1467 retracted: false,
1468 },
1469 ];
1470
1471 let fallback_pick = typed
1473 .iter()
1474 .find(|v| !v.is_pseudo && !v.retracted)
1475 .map(|v| v.version.to_string());
1476
1477 let boxed: Vec<Box<dyn deps_core::Version>> = typed
1478 .into_iter()
1479 .map(|v| Box::new(v) as Box<dyn deps_core::Version>)
1480 .collect();
1481 let idx = registry
1482 .select_latest_matching(&boxed, &VersionReq::new("*"))
1483 .expect("non-empty list must select an index");
1484
1485 assert_eq!(Some(boxed[idx].version_string().to_string()), fallback_pick);
1486 assert_eq!(fallback_pick.as_deref(), Some("v1.0.0"));
1487 }
1488
1489 #[test]
1490 fn test_select_latest_matching_not_default_none() {
1491 use deps_core::{Registry, VersionReq};
1492
1493 let cache = Arc::new(HttpCache::new());
1494 let registry = GoRegistry::new(cache);
1495 let versions: Vec<Box<dyn deps_core::Version>> = vec![
1496 Box::new(GoVersion {
1497 version: "v2.0.0".into(),
1498 published_at: None,
1499 is_pseudo: false,
1500 retracted: true,
1501 }),
1502 Box::new(GoVersion {
1503 version: "v1.0.0".into(),
1504 published_at: None,
1505 is_pseudo: false,
1506 retracted: false,
1507 }),
1508 ];
1509 let req = VersionReq::new("*");
1510 assert_eq!(registry.select_latest_matching(&versions, &req), Some(1));
1511 }
1512
1513 #[test]
1521 fn test_select_latest_matching_all_prerelease_stays_none_go_opts_out_of_ladder() {
1522 use deps_core::{Registry, VersionReq};
1523
1524 let cache = Arc::new(HttpCache::new());
1525 let registry = GoRegistry::new(cache);
1526 let versions: Vec<Box<dyn deps_core::Version>> = vec![
1527 Box::new(GoVersion {
1528 version: "v0.0.0-20191109021931-daa7c04131f5".into(),
1529 published_at: None,
1530 is_pseudo: true,
1531 retracted: false,
1532 }),
1533 Box::new(GoVersion {
1534 version: "v1.0.0-beta.1".into(),
1535 published_at: None,
1536 is_pseudo: false,
1537 retracted: false,
1538 }),
1539 ];
1540 let req = VersionReq::new("*");
1541 assert_eq!(
1542 registry.select_latest_matching(&versions, &req),
1543 None,
1544 "Go must not fall through to rung 3; a non-empty all-prerelease list must \
1545 still yield None so the /@latest fallback fires"
1546 );
1547 }
1548
1549 use deps_core::net_policy::{RegistryAccessPolicy, WorkspaceRegistryAccess};
1552
1553 fn all_policy() -> RegistryAccessPolicy {
1554 RegistryAccessPolicy::new(WorkspaceRegistryAccess::All)
1555 }
1556
1557 fn url_hop(raw: &str, policy: &RegistryAccessPolicy) -> GoProxyHop {
1558 GoProxyHop::Url(GoProxyUrl::new(raw, policy).unwrap())
1559 }
1560
1561 #[test]
1562 fn test_register_chain_and_alternate_client_roundtrip() {
1563 let cache = Arc::new(HttpCache::new());
1564 let root = Arc::new(GoRegistry::new(Arc::clone(&cache)));
1565 let chain = GoProxyChain {
1566 key: "go-proxy:test".to_string(),
1567 hops: vec![url_hop("https://goproxy.mycorp.example", &all_policy())],
1568 ..Default::default()
1569 };
1570 GoRegistry::register_chain(&root, &chain);
1571 assert!(root.alternate_client("go-proxy:test").is_some());
1572 assert!(root.alternate_client("nonexistent").is_none());
1573 }
1574
1575 #[test]
1576 fn test_register_chain_idempotent() {
1577 let cache = Arc::new(HttpCache::new());
1578 let root = Arc::new(GoRegistry::new(Arc::clone(&cache)));
1579 let chain = GoProxyChain {
1580 key: "go-proxy:test".to_string(),
1581 hops: vec![url_hop("https://goproxy.mycorp.example", &all_policy())],
1582 ..Default::default()
1583 };
1584 GoRegistry::register_chain(&root, &chain);
1585 let first = root.alternate_client("go-proxy:test").unwrap();
1586 GoRegistry::register_chain(&root, &chain);
1587 let second = root.alternate_client("go-proxy:test").unwrap();
1588 assert!(Arc::ptr_eq(&first, &second));
1589 }
1590
1591 #[tokio::test]
1594 async fn test_get_versions_from_routes_to_registered_alternate() {
1595 use deps_core::{FreshnessSettings, Registry};
1596
1597 let mut alt_server = mockito::Server::new_async().await;
1598 alt_server
1599 .mock("GET", "/github.com/gin-gonic/gin/@v/list")
1600 .with_status(200)
1601 .with_body("v1.9.1\n")
1602 .create_async()
1603 .await;
1604
1605 let cache = Arc::new(HttpCache::new());
1606 cache.set_registry_policy(WorkspaceRegistryAccess::All);
1607 let root = Arc::new(GoRegistry::new(Arc::clone(&cache)));
1608 let policy = all_policy();
1609 let chain = GoProxyChain {
1610 key: "go-proxy:test".to_string(),
1611 hops: vec![url_hop(&alt_server.url(), &policy)],
1612 ..Default::default()
1613 };
1614 GoRegistry::register_chain(&root, &chain);
1615
1616 let source = DependencySource::AlternateRegistry {
1617 index: "go-proxy:test".to_string(),
1618 mirrors_crates_io: false,
1619 };
1620 let versions = root
1621 .get_versions_from(
1622 &deps_core::PackageName::new("github.com/gin-gonic/gin"),
1623 &source,
1624 FreshnessSettings::default(),
1625 )
1626 .await
1627 .unwrap();
1628 assert_eq!(versions.len(), 1);
1629 }
1630
1631 #[tokio::test]
1633 async fn test_get_versions_from_falls_through_on_not_found() {
1634 use deps_core::{FreshnessSettings, Registry};
1635
1636 let mut hop0 = mockito::Server::new_async().await;
1637 hop0.mock("GET", mockito::Matcher::Any)
1638 .with_status(404)
1639 .create_async()
1640 .await;
1641 let mut hop1 = mockito::Server::new_async().await;
1642 hop1.mock("GET", "/github.com/gin-gonic/gin/@v/list")
1643 .with_status(200)
1644 .with_body("v1.9.1\n")
1645 .create_async()
1646 .await;
1647
1648 let cache = Arc::new(HttpCache::new());
1649 cache.set_registry_policy(WorkspaceRegistryAccess::All);
1650 let root = Arc::new(GoRegistry::new(Arc::clone(&cache)));
1651 let policy = all_policy();
1652 let chain = GoProxyChain {
1653 key: "go-proxy:test".to_string(),
1654 hops: vec![url_hop(&hop0.url(), &policy), url_hop(&hop1.url(), &policy)],
1655 ..Default::default()
1656 };
1657 GoRegistry::register_chain(&root, &chain);
1658
1659 let source = DependencySource::AlternateRegistry {
1660 index: "go-proxy:test".to_string(),
1661 mirrors_crates_io: false,
1662 };
1663 let versions = root
1664 .get_versions_from(
1665 &deps_core::PackageName::new("github.com/gin-gonic/gin"),
1666 &source,
1667 FreshnessSettings::default(),
1668 )
1669 .await
1670 .unwrap();
1671 assert_eq!(versions.len(), 1);
1672 }
1673
1674 #[tokio::test]
1678 async fn test_get_versions_from_falls_through_on_410() {
1679 use deps_core::{FreshnessSettings, Registry};
1680
1681 let mut hop0 = mockito::Server::new_async().await;
1682 hop0.mock("GET", mockito::Matcher::Any)
1683 .with_status(410)
1684 .create_async()
1685 .await;
1686 let mut hop1 = mockito::Server::new_async().await;
1687 hop1.mock("GET", "/github.com/gin-gonic/gin/@v/list")
1688 .with_status(200)
1689 .with_body("v1.9.1\n")
1690 .create_async()
1691 .await;
1692
1693 let cache = Arc::new(HttpCache::new());
1694 cache.set_registry_policy(WorkspaceRegistryAccess::All);
1695 let root = Arc::new(GoRegistry::new(Arc::clone(&cache)));
1696 let policy = all_policy();
1697 let chain = GoProxyChain {
1698 key: "go-proxy:test".to_string(),
1699 hops: vec![url_hop(&hop0.url(), &policy), url_hop(&hop1.url(), &policy)],
1700 ..Default::default()
1701 };
1702 GoRegistry::register_chain(&root, &chain);
1703
1704 let source = DependencySource::AlternateRegistry {
1705 index: "go-proxy:test".to_string(),
1706 mirrors_crates_io: false,
1707 };
1708 let versions = root
1709 .get_versions_from(
1710 &deps_core::PackageName::new("github.com/gin-gonic/gin"),
1711 &source,
1712 FreshnessSettings::default(),
1713 )
1714 .await
1715 .unwrap();
1716 assert_eq!(versions.len(), 1);
1717 }
1718
1719 #[tokio::test]
1722 async fn test_get_versions_from_transport_failure_is_terminal() {
1723 use deps_core::{FreshnessSettings, Registry};
1724
1725 let mut hop0 = mockito::Server::new_async().await;
1726 hop0.mock("GET", mockito::Matcher::Any)
1727 .with_status(500)
1728 .create_async()
1729 .await;
1730 let mut hop1 = mockito::Server::new_async().await;
1731 let hop1_mock = hop1
1732 .mock("GET", mockito::Matcher::Any)
1733 .with_status(200)
1734 .with_body("v1.9.1\n")
1735 .expect(0)
1736 .create_async()
1737 .await;
1738
1739 let cache = Arc::new(HttpCache::new());
1740 cache.set_registry_policy(WorkspaceRegistryAccess::All);
1741 let root = Arc::new(GoRegistry::new(Arc::clone(&cache)));
1742 let policy = all_policy();
1743 let chain = GoProxyChain {
1744 key: "go-proxy:test".to_string(),
1745 hops: vec![url_hop(&hop0.url(), &policy), url_hop(&hop1.url(), &policy)],
1746 ..Default::default()
1747 };
1748 GoRegistry::register_chain(&root, &chain);
1749
1750 let source = DependencySource::AlternateRegistry {
1751 index: "go-proxy:test".to_string(),
1752 mirrors_crates_io: false,
1753 };
1754 let result = root
1755 .get_versions_from(
1756 &deps_core::PackageName::new("github.com/gin-gonic/gin"),
1757 &source,
1758 FreshnessSettings::default(),
1759 )
1760 .await;
1761 assert_matches!(result.err(), Some(DepsError::ChainResolutionHalted));
1762 hop1_mock.assert_async().await;
1763 }
1764
1765 #[tokio::test]
1768 async fn test_pipe_separator_falls_through_on_transport_failure() {
1769 use deps_core::{FreshnessSettings, Registry};
1770
1771 let mut hop0 = mockito::Server::new_async().await;
1772 hop0.mock("GET", mockito::Matcher::Any)
1773 .with_status(500)
1774 .create_async()
1775 .await;
1776 let mut hop1 = mockito::Server::new_async().await;
1777 hop1.mock("GET", "/github.com/gin-gonic/gin/@v/list")
1778 .with_status(200)
1779 .with_body("v1.9.1\n")
1780 .create_async()
1781 .await;
1782
1783 let cache = Arc::new(HttpCache::new());
1784 cache.set_registry_policy(WorkspaceRegistryAccess::All);
1785 let root = Arc::new(GoRegistry::new(Arc::clone(&cache)));
1786 let policy = all_policy();
1787 let chain = GoProxyChain {
1788 key: "go-proxy:test".to_string(),
1789 hops: vec![url_hop(&hop0.url(), &policy), url_hop(&hop1.url(), &policy)],
1790 separators: vec![ChainSeparator::AnyError],
1791 };
1792 GoRegistry::register_chain(&root, &chain);
1793
1794 let source = DependencySource::AlternateRegistry {
1795 index: "go-proxy:test".to_string(),
1796 mirrors_crates_io: false,
1797 };
1798 let versions = root
1799 .get_versions_from(
1800 &deps_core::PackageName::new("github.com/gin-gonic/gin"),
1801 &source,
1802 FreshnessSettings::default(),
1803 )
1804 .await
1805 .unwrap();
1806 assert_eq!(versions.len(), 1);
1807 }
1808
1809 #[tokio::test]
1813 async fn test_chain_hop_falls_back_to_latest_when_list_is_empty() {
1814 use deps_core::{FreshnessSettings, Registry};
1815
1816 let mut hop = mockito::Server::new_async().await;
1817 hop.mock("GET", "/github.com/gin-gonic/gin/@latest")
1818 .with_status(200)
1819 .with_body(
1820 r#"{"Version":"v0.0.0-20191109021931-daa7c04131f5","Time":"2019-11-09T02:19:31Z"}"#,
1821 )
1822 .create_async()
1823 .await;
1824 hop.mock("GET", "/github.com/gin-gonic/gin/@v/list")
1825 .with_status(200)
1826 .with_body("")
1827 .create_async()
1828 .await;
1829
1830 let cache = Arc::new(HttpCache::new());
1831 cache.set_registry_policy(WorkspaceRegistryAccess::All);
1832 let root = Arc::new(GoRegistry::new(Arc::clone(&cache)));
1833 let policy = all_policy();
1834 let chain = GoProxyChain {
1835 key: "go-proxy:test".to_string(),
1836 hops: vec![url_hop(&hop.url(), &policy)],
1837 ..Default::default()
1838 };
1839 GoRegistry::register_chain(&root, &chain);
1840
1841 let source = DependencySource::AlternateRegistry {
1842 index: "go-proxy:test".to_string(),
1843 mirrors_crates_io: false,
1844 };
1845 let versions = root
1846 .get_versions_from(
1847 &deps_core::PackageName::new("github.com/gin-gonic/gin"),
1848 &source,
1849 FreshnessSettings::default(),
1850 )
1851 .await
1852 .unwrap();
1853 assert_eq!(versions.len(), 1);
1854 assert!(
1855 versions[0].is_prerelease(),
1856 "expected the pseudo-version from /@latest"
1857 );
1858 }
1859
1860 #[tokio::test]
1863 async fn test_direct_terminal_hop_shows_no_data_zero_requests() {
1864 use deps_core::{FreshnessSettings, Registry};
1865
1866 let mut hop0 = mockito::Server::new_async().await;
1867 hop0.mock("GET", mockito::Matcher::Any)
1868 .with_status(404)
1869 .create_async()
1870 .await;
1871
1872 let cache = Arc::new(HttpCache::new());
1873 cache.set_registry_policy(WorkspaceRegistryAccess::All);
1874 let root = Arc::new(GoRegistry::new(Arc::clone(&cache)));
1875 let policy = all_policy();
1876 let chain = GoProxyChain {
1877 key: "go-proxy:test".to_string(),
1878 hops: vec![url_hop(&hop0.url(), &policy), GoProxyHop::Direct],
1879 ..Default::default()
1880 };
1881 GoRegistry::register_chain(&root, &chain);
1882
1883 let source = DependencySource::AlternateRegistry {
1884 index: "go-proxy:test".to_string(),
1885 mirrors_crates_io: false,
1886 };
1887 let result = root
1888 .get_versions_from(
1889 &deps_core::PackageName::new("github.com/gin-gonic/gin"),
1890 &source,
1891 FreshnessSettings::default(),
1892 )
1893 .await;
1894 assert_matches!(result.err(), Some(DepsError::PackageNotFound { .. }));
1895 }
1896
1897 #[tokio::test]
1902 async fn test_goprivate_bypass_sends_zero_requests_to_goproxy() {
1903 use deps_core::{FreshnessSettings, Registry};
1904
1905 let mut public_proxy = mockito::Server::new_async().await;
1906 let public_mock = public_proxy
1907 .mock("GET", mockito::Matcher::Any)
1908 .with_status(200)
1909 .with_body("v1.9.1\n")
1910 .expect(0)
1911 .create_async()
1912 .await;
1913
1914 let content = format!(
1915 "GOPROXY={},direct\nGOPRIVATE=git.mycorp.example/*\n",
1916 public_proxy.url()
1917 );
1918 let policy = all_policy();
1919 let go_config = crate::config::GoEnvConfig::parse(&content, &policy);
1920 let source = go_config.resolve_source_for("git.mycorp.example/internal/auth");
1921 assert_eq!(
1922 source,
1923 DependencySource::AlternateRegistry {
1924 index: crate::config::GOPRIVATE_CHAIN_KEY.to_string(),
1925 mirrors_crates_io: false,
1926 }
1927 );
1928
1929 let cache = Arc::new(HttpCache::new());
1930 cache.set_registry_policy(WorkspaceRegistryAccess::All);
1931 let root = Arc::new(GoRegistry::new(Arc::clone(&cache)));
1932 for chain in go_config.resolved_chains() {
1933 GoRegistry::register_chain(&root, &chain);
1934 }
1935
1936 let result = root
1937 .get_versions_from(
1938 &deps_core::PackageName::new("git.mycorp.example/internal/auth"),
1939 &source,
1940 FreshnessSettings::default(),
1941 )
1942 .await;
1943 assert_matches!(result.err(), Some(DepsError::PackageNotFound { .. }));
1944 public_mock.assert_async().await;
1945 }
1946
1947 #[tokio::test]
1949 async fn test_off_hop_zero_requests() {
1950 use deps_core::{FreshnessSettings, Registry};
1951
1952 let cache = Arc::new(HttpCache::new());
1953 let root = Arc::new(GoRegistry::new(Arc::clone(&cache)));
1954 let chain = GoProxyChain {
1955 key: "go-proxy:off".to_string(),
1956 hops: vec![GoProxyHop::Off],
1957 ..Default::default()
1958 };
1959 GoRegistry::register_chain(&root, &chain);
1960
1961 let source = DependencySource::AlternateRegistry {
1962 index: "go-proxy:off".to_string(),
1963 mirrors_crates_io: false,
1964 };
1965 let result = root
1966 .get_versions_from(
1967 &deps_core::PackageName::new("github.com/gin-gonic/gin"),
1968 &source,
1969 FreshnessSettings::default(),
1970 )
1971 .await;
1972 assert_matches!(result.err(), Some(DepsError::PackageNotFound { .. }));
1973 }
1974
1975 #[tokio::test]
1979 async fn test_unregistered_alternate_never_falls_back_to_public() {
1980 use deps_core::{FreshnessSettings, Registry};
1981
1982 let cache = Arc::new(HttpCache::new());
1983 let root = Arc::new(GoRegistry::new(cache));
1984 let source = DependencySource::AlternateRegistry {
1985 index: "never-registered".to_string(),
1986 mirrors_crates_io: false,
1987 };
1988 let result = root
1989 .get_versions_from(
1990 &deps_core::PackageName::new("github.com/gin-gonic/gin"),
1991 &source,
1992 FreshnessSettings::default(),
1993 )
1994 .await;
1995 assert_matches!(
1996 result.err(),
1997 Some(DepsError::PackageNotFound {
1998 registry: "alternate registry (not registered)",
1999 ..
2000 })
2001 );
2002 }
2003
2004 #[tokio::test]
2007 async fn test_get_latest_matching_from_routes_to_alternate() {
2008 use deps_core::{Registry, VersionReq};
2009
2010 let mut alt_server = mockito::Server::new_async().await;
2011 alt_server
2012 .mock("GET", "/github.com/gin-gonic/gin/@v/list")
2013 .with_status(200)
2014 .with_body("v1.9.0\nv1.9.1\n")
2015 .create_async()
2016 .await;
2017
2018 let cache = Arc::new(HttpCache::new());
2019 cache.set_registry_policy(WorkspaceRegistryAccess::All);
2020 let root = Arc::new(GoRegistry::new(Arc::clone(&cache)));
2021 let policy = all_policy();
2022 let chain = GoProxyChain {
2023 key: "go-proxy:test".to_string(),
2024 hops: vec![url_hop(&alt_server.url(), &policy)],
2025 ..Default::default()
2026 };
2027 GoRegistry::register_chain(&root, &chain);
2028
2029 let source = DependencySource::AlternateRegistry {
2030 index: "go-proxy:test".to_string(),
2031 mirrors_crates_io: false,
2032 };
2033 let latest = root
2034 .get_latest_matching_from(
2035 &deps_core::PackageName::new("github.com/gin-gonic/gin"),
2036 &source,
2037 &VersionReq::new("*"),
2038 None,
2039 )
2040 .await
2041 .unwrap();
2042 assert!(latest.is_some());
2043 }
2044
2045 #[tokio::test]
2048 async fn test_get_versions_from_plain_registry_source_unchanged() {
2049 use deps_core::{FreshnessSettings, Registry};
2050
2051 let cache = Arc::new(HttpCache::new());
2052 let root = GoRegistry::new(cache);
2053 let result = root
2054 .get_versions_from(
2055 &deps_core::PackageName::new("github.com/nonexistent/module12345"),
2056 &DependencySource::Registry,
2057 FreshnessSettings::default(),
2058 )
2059 .await;
2060 assert!(result.is_err());
2063 }
2064
2065 #[test]
2066 fn test_alternate_registries_cap_enforced() {
2067 let cache = Arc::new(HttpCache::new());
2068 let root = Arc::new(GoRegistry::new(cache));
2069 let policy = all_policy();
2070 for i in 0..MAX_ALTERNATE_REGISTRIES {
2071 let chain = GoProxyChain {
2072 key: format!("go-proxy:cap-{i}"),
2073 hops: vec![url_hop("https://goproxy.mycorp.example", &policy)],
2074 ..Default::default()
2075 };
2076 GoRegistry::register_chain(&root, &chain);
2077 }
2078 assert_eq!(root.alternates.len(), MAX_ALTERNATE_REGISTRIES);
2079
2080 let overflow = GoProxyChain {
2081 key: "go-proxy:overflow".to_string(),
2082 hops: vec![url_hop("https://goproxy.mycorp.example", &policy)],
2083 ..Default::default()
2084 };
2085 GoRegistry::register_chain(&root, &overflow);
2086 assert_eq!(root.alternates.len(), MAX_ALTERNATE_REGISTRIES);
2087 assert!(root.alternate_client("go-proxy:overflow").is_none());
2088 }
2089}