/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
libs/npmrc/lib.rs
1 721 строка
55 KB
Nathan Whitaker
fix(npmrc): match auth configs by authority and path (#36359)
31 июл 2026, 00:41
Не верифицирован
31 июл 2026, 00:41
c026afe
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. #![deny(clippy::print_stderr)] #![deny(clippy::print_stdout)] #![deny(clippy::unused_async)] use std::borrow::Cow; use std::collections::HashMap; use std::sync::Arc; use monch::*; use sys_traits::EnvVar; use url::Url; use self::ini::Key; use self::ini::KeyValueOrSection; use self::ini::Value; mod ini; /// The default npm registry URL. pub static NPM_DEFAULT_REGISTRY: &str = "https://registry.npmjs.org"; const NPM_DEFAULT_REGISTRY_HOST: &str = "registry.npmjs.org"; #[derive(Debug, thiserror::Error)] pub enum ResolveError { #[error("failed parsing npm registry url for scope '{scope}'")] UrlScope { scope: String, #[source] source: url::ParseError, }, #[error("failed parsing npm registry url")] Url(#[source] url::ParseError), } pub type NpmRcParseError = monch::ParseErrorFailureError; #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct RegistryConfig { pub auth: Option<String>, pub auth_token: Option<String>, pub username: Option<String>, pub password: Option<String>, pub email: Option<String>, pub certfile: Option<String>, pub keyfile: Option<String>, } impl RegistryConfig { /// Whether this config carries credentials usable for authentication. /// /// Mirrors the cases that `maybe_auth_header_value_for_npm_registry` turns /// into a header, including treating `email` as a username substitute, so the /// two never disagree. pub fn has_auth(&self) -> bool { self.auth_token.is_some() || self.auth.is_some() || ((self.username.is_some() || self.email.is_some()) && self.password.is_some()) } } /// `trust-policy` value. Controls whether a resolved npm version may have /// weaker publishing-trust evidence than an earlier-published version of the /// same package. Mirrors pnpm's `trustPolicy`. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum TrustPolicyConfig { /// Trust evidence is ignored during resolution (the default). #[default] Off, /// Refuse to resolve a version whose publishing-trust evidence is weaker /// than the strongest evidence on any earlier-published version of the same /// package. NoDowngrade, } /// Controls when the configured registry replaces the registry host in a /// package tarball URL. This mirrors npm's `replace-registry-host` setting. #[derive(Debug, Default, Clone, PartialEq, Eq)] pub enum ReplaceRegistryHost { /// Replace tarball URLs hosted by the public npm registry. #[default] NpmJs, /// Never replace a tarball URL. Never, /// Replace every tarball URL. Always, /// Replace tarball URLs hosted by this hostname. Hostname(String), /// Replace tarball URLs whose hostname and path match this URL prefix. Url(Url), } impl ReplaceRegistryHost { fn parse(value: &str) -> Self { let value = value.trim(); match value { "" | "npmjs" => Self::NpmJs, "never" => Self::Never, "always" => Self::Always, _ => match Url::parse(value) { Ok(url) if url.host_str().is_some() => Self::Url(url), _ => Self::Hostname(value.to_string()), }, } } pub fn for_npm(sys: &impl EnvVar) -> Option<Self> { for env_var_name in [ "NPM_CONFIG_REPLACE_REGISTRY_HOST", "npm_config_replace_registry_host", ] { if let Ok(value) = sys.env_var(env_var_name) { return Some(Self::parse(&value)); } } None } fn matches(&self, tarball_url: &Url) -> Option<Option<&str>> { match self { Self::NpmJs => (tarball_url.host_str() == Some(NPM_DEFAULT_REGISTRY_HOST)) .then_some(None), Self::Never => None, Self::Always => Some(None), Self::Hostname(hostname) => { (tarball_url.host_str() == Some(hostname.as_str())).then_some(None) } Self::Url(url) => { let host_matches = url.host_str() == tarball_url.host_str(); let match_path = url.path().trim_end_matches('/'); let path_matches = match_path.is_empty() || path_has_prefix(tarball_url.path(), match_path); (host_matches && path_matches) .then_some((!match_path.is_empty()).then_some(match_path)) } } } fn replace(&self, mut tarball_url: Url, registry_url: &Url) -> Url { let Some(maybe_match_path) = self.matches(&tarball_url) else { return tarball_url; }; let original_url = tarball_url.clone(); if tarball_url.set_scheme(registry_url.scheme()).is_err() || tarball_url.set_host(registry_url.host_str()).is_err() || tarball_url.set_port(registry_url.port()).is_err() { return original_url; } let registry_path = registry_url.path().trim_end_matches('/'); let tarball_path = original_url.path(); let replaced_path = if let Some(match_path) = maybe_match_path { format!("{}{}", registry_path, &tarball_path[match_path.len()..]) } else if !registry_path.is_empty() && !path_has_prefix(tarball_path, registry_path) { format!("{}{}", registry_path, tarball_path) } else { tarball_path.to_string() }; tarball_url.set_path(&replaced_path); tarball_url } } fn path_has_prefix(path: &str, prefix: &str) -> bool { path == prefix || path .strip_prefix(prefix) .is_some_and(|suffix| suffix.starts_with('/')) } #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct NpmRc { pub registry: Option<String>, pub scope_registries: HashMap<String, String>, pub registry_configs: HashMap<String, Arc<RegistryConfig>>, pub replace_registry_host: Option<ReplaceRegistryHost>, /// `min-release-age` value in days. See /// https://docs.npmjs.com/cli/v11/using-npm/config#min-release-age pub min_release_age_days: Option<u64>, /// `trust-policy` value (`off` or `no-downgrade`). pub trust_policy: TrustPolicyConfig, /// `trust-policy-ignore-after` value in minutes: skip the `no-downgrade` /// check for versions published more than this many minutes ago. Mirrors /// pnpm's `trustPolicyIgnoreAfter`. pub trust_policy_ignore_after_minutes: Option<u64>, /// `trust-policy-exclude[]` values: package names exempted from the /// `no-downgrade` trust policy. Mirrors pnpm's `trustPolicyExclude`. Set via /// repeated `trust-policy-exclude[]=<package>` entries in `.npmrc`. pub trust_policy_exclude: Vec<String>, } impl NpmRc { pub fn parse( sys: &impl EnvVar, input: &str, ) -> Result<Self, NpmRcParseError> { let kv_or_sections = ini::parse_ini(input)?; let mut registry = None; let mut scope_registries: HashMap<String, String> = HashMap::new(); let mut registry_configs: HashMap<String, RegistryConfig> = HashMap::new(); let replace_registry_host_from_env = ReplaceRegistryHost::for_npm(sys); let mut replace_registry_host = None; let mut min_release_age_days = min_release_age_days_from_env(sys); let mut trust_policy = TrustPolicyConfig::default(); let mut trust_policy_ignore_after_minutes: Option<u64> = None; let mut trust_policy_exclude: Vec<String> = Vec::new(); for kv_or_section in kv_or_sections { match kv_or_section { KeyValueOrSection::KeyValue(kv) => { if let Key::Plain(key) = &kv.key { if let Some((left, right)) = key.rsplit_once(':') { if let Some(scope) = left.strip_prefix('@') { if right == "registry" && let Value::String(text) = &kv.value { let value = expand_vars(text, sys); scope_registries.insert(scope.to_string(), value); } } else if let Some(host_and_path) = left.strip_prefix("//") && let Value::String(text) = &kv.value { let value = expand_vars(text, sys); let config = registry_configs .entry(host_and_path.to_string()) .or_default(); match right { "_auth" => { config.auth = Some(value); } "_authToken" => { config.auth_token = Some(value); } "username" => { config.username = Some(value); } "_password" => { config.password = Some(value); } "email" => { config.email = Some(value); } "certfile" => { config.certfile = Some(value); } "keyfile" => { config.keyfile = Some(value); } _ => {} } } } else if key == "registry" && let Value::String(text) = &kv.value { let value = expand_vars(text, sys); registry = Some(value); } else if key == "replace-registry-host" && let Value::String(text) = &kv.value { let value = expand_vars(text, sys); replace_registry_host = Some(ReplaceRegistryHost::parse(&value)); } else if key == "min-release-age" { // npm interprets the value as a number of days. Ignore values // that can't be parsed rather than erroring (npm is lenient // about unknown/invalid config values). match &kv.value { Value::Number(n) if *n >= 0 => { min_release_age_days = Some(*n as u64); } Value::String(text) => { let value = expand_vars(text, sys); if let Ok(days) = value.trim().parse::<u64>() { min_release_age_days = Some(days); } } _ => {} } } else if key == "trust-policy" && let Value::String(text) = &kv.value { let value = expand_vars(text, sys); trust_policy = match value.trim() { "no-downgrade" => TrustPolicyConfig::NoDowngrade, // unknown/`off` values fall back to off (npm is lenient about // unknown config values) _ => TrustPolicyConfig::Off, }; } else if key == "trust-policy-ignore-after" { // a number of minutes; ignore unparsable values (npm is lenient // about unknown/invalid config values) match &kv.value { Value::Number(n) if *n >= 0 => { trust_policy_ignore_after_minutes = Some(*n as u64); } Value::String(text) => { let value = expand_vars(text, sys); if let Ok(minutes) = value.trim().parse::<u64>() { trust_policy_ignore_after_minutes = Some(minutes); } } _ => {} } } } else if let Key::Array(key) = &kv.key && key == "trust-policy-exclude" && let Value::String(text) = &kv.value { // repeated `trust-policy-exclude[]=<package>` entries, each adding // one package name to exempt from the `no-downgrade` policy let value = expand_vars(text, sys); let value = value.trim(); if !value.is_empty() { trust_policy_exclude.push(value.to_string()); } } } KeyValueOrSection::Section(_) => { // ignore } } } Ok(NpmRc { registry, scope_registries, registry_configs: registry_configs .into_iter() .map(|(k, v)| (k, Arc::new(v))) .collect(), replace_registry_host: replace_registry_host_from_env .or(replace_registry_host), min_release_age_days, trust_policy, trust_policy_ignore_after_minutes, trust_policy_exclude, }) } pub fn as_resolved( &self, registry_url: &NpmRegistryUrl, ) -> Result<ResolvedNpmRc, ResolveError> { let mut scopes = HashMap::with_capacity(self.scope_registries.len()); for scope in self.scope_registries.keys() { let (url, config) = self.registry_url_and_config_for_maybe_scope( Some(scope.as_str()), registry_url, ); let url = Url::parse(&url).map_err(|e| ResolveError::UrlScope { scope: scope.clone(), source: e, })?; scopes.insert( scope.clone(), RegistryConfigWithUrl { registry_url: url, config, }, ); } let (default_url, default_config) = self.registry_url_and_config_for_maybe_scope(None, registry_url); let default_url = Url::parse(&default_url).map_err(ResolveError::Url)?; Ok(ResolvedNpmRc { default_config: RegistryConfigWithUrl { registry_url: default_url, config: default_config, }, scopes, registry_configs: self.registry_configs.clone(), replace_registry_host: self .replace_registry_host .clone() .unwrap_or_default(), min_release_age_days: self.min_release_age_days, trust_policy: self.trust_policy, trust_policy_ignore_after_minutes: self.trust_policy_ignore_after_minutes, trust_policy_exclude: self.trust_policy_exclude.clone(), }) } fn registry_url_and_config_for_maybe_scope( &self, maybe_scope_name: Option<&str>, registry_url: &NpmRegistryUrl, ) -> (String, Arc<RegistryConfig>) { let registry_url = maybe_scope_name .and_then(|scope| self.scope_registries.get(scope).map(|s| s.as_str())) .unwrap_or_else(|| { // NPM_CONFIG_REGISTRY env var should take priority over .npmrc registry setting. // Only use .npmrc registry if NPM_CONFIG_REGISTRY was not explicitly set. if registry_url.from_env { registry_url.url.as_str() } else { self .registry .as_deref() .unwrap_or(registry_url.url.as_str()) } }); let original_registry_url = if registry_url.ends_with('/') { Cow::Borrowed(registry_url) } else { Cow::Owned(format!("{}/", registry_url)) }; // https://example.com/ -> example.com/ let Some((_, registry_url)) = original_registry_url .split_once("//") .filter(|(_, url)| !url.is_empty()) else { return ( original_registry_url.into_owned(), Arc::new(RegistryConfig::default()), ); }; let mut url: &str = registry_url; loop { if let Some(config) = self.registry_configs.get(url) { return (original_registry_url.into_owned(), config.clone()); } let Some(next_slash_index) = url[..url.len() - 1].rfind('/') else { return ( original_registry_url.into_owned(), Arc::new(RegistryConfig::default()), ); }; url = &url[..next_slash_index + 1]; } } } pub fn min_release_age_days_from_env(sys: &impl EnvVar) -> Option<u64> { for env_var_name in ["NPM_CONFIG_MIN_RELEASE_AGE", "npm_config_min_release_age"] { if let Ok(value) = sys.env_var(env_var_name) && let Ok(days) = value.trim().parse::<u64>() { return Some(days); } } None } fn get_scope_name(package_name: &str) -> Option<&str> { let no_at_pkg_name = package_name.strip_prefix('@')?; no_at_pkg_name.split_once('/').map(|(scope, _)| scope) } #[derive(Debug, Clone, PartialEq, Eq)] pub struct RegistryConfigWithUrl { pub registry_url: Url, pub config: Arc<RegistryConfig>, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolvedNpmRc { pub default_config: RegistryConfigWithUrl, pub scopes: HashMap<String, RegistryConfigWithUrl>, pub registry_configs: HashMap<String, Arc<RegistryConfig>>, pub replace_registry_host: ReplaceRegistryHost, /// `min-release-age` value in days. See /// https://docs.npmjs.com/cli/v11/using-npm/config#min-release-age pub min_release_age_days: Option<u64>, /// `trust-policy` value (`off` or `no-downgrade`). pub trust_policy: TrustPolicyConfig, /// `trust-policy-ignore-after` value in minutes. pub trust_policy_ignore_after_minutes: Option<u64>, /// `trust-policy-exclude[]` package names exempted from the `no-downgrade` /// trust policy. pub trust_policy_exclude: Vec<String>, } impl ResolvedNpmRc { pub fn get_registry_url(&self, package_name: &str) -> &Url { let Some(scope_name) = get_scope_name(package_name) else { return &self.default_config.registry_url; }; match self.scopes.get(scope_name) { Some(registry_config) => ®istry_config.registry_url, None => &self.default_config.registry_url, } } pub fn get_registry_config( &self, package_name: &str, ) -> &Arc<RegistryConfig> { let Some(scope_name) = get_scope_name(package_name) else { return &self.default_config.config; }; match self.scopes.get(scope_name) { Some(registry_config) => ®istry_config.config, None => &self.default_config.config, } } pub fn get_all_known_registries_urls(&self) -> Vec<Url> { let mut urls = Vec::with_capacity(1 + self.scopes.len()); urls.push(self.default_config.registry_url.clone()); for scope_config in self.scopes.values() { urls.push(scope_config.registry_url.clone()); } urls } /// Applies npm's `replace-registry-host` policy to a package tarball URL. pub fn replace_tarball_url( &self, tarball_url: Url, package_name: &str, ) -> Url { self .replace_registry_host .replace(tarball_url, self.get_registry_url(package_name)) } pub fn tarball_config( &self, tarball_url: &Url, ) -> Option<&Arc<RegistryConfig>> { let mut best_match: Option<(usize, &Arc<RegistryConfig>)> = None; for (config_url, config) in &self.registry_configs { if let Some(match_len) = registry_config_match_len(tarball_url, config_url) && best_match .is_none_or(|(current_match_len, _)| match_len > current_match_len) { best_match = Some((match_len, config)); } } best_match.map(|(_, config)| config) } /// Like [`Self::tarball_config`], but falls back to the scoped registry's /// config for `package_name` when the tarball is served from the same origin /// as that registry. /// /// Some registries (e.g. GitLab instance-level npm registries) serve tarballs /// from a different path than the registry endpoint, so a plain path-prefix /// match against the tarball URL misses the auth that is configured for the /// registry. See https://github.com/denoland/deno/issues/27759 pub fn tarball_config_for_package( &self, tarball_url: &Url, package_name: &str, ) -> Option<&Arc<RegistryConfig>> { if let Some(config) = self.tarball_config(tarball_url) { return Some(config); } // Mirror get_registry_config/get_registry_url: a scoped-but-unconfigured // package resolves through the default registry, so its tarball auth must // come from default_config too (still gated by same-origin + has_auth // below). Bailing out here would re-introduce the 404 this method fixes for // a default instance-level registry. let scope_registry = get_scope_name(package_name) .and_then(|scope| self.scopes.get(scope)) .unwrap_or(&self.default_config); // Only fall back when the tarball is served from the same origin as the // registry the package was resolved from, and that registry actually has // credentials. This keeps the token from leaking to unrelated hosts. let registry_url = &scope_registry.registry_url; let same_origin = registry_url.scheme() == tarball_url.scheme() && registry_url.host_str() == tarball_url.host_str() && registry_url.port_or_known_default() == tarball_url.port_or_known_default(); if same_origin && scope_registry.config.has_auth() { Some(&scope_registry.config) } else { None } } } fn registry_config_match_len( tarball_url: &Url, config_url: &str, ) -> Option<usize> { let (config_authority, config_path) = config_url .find('/') .map(|index| config_url.split_at(index)) .unwrap_or((config_url, "")); if config_authority.is_empty() || tarball_url.host().is_none() { return None; } // npm auth keys are scheme-relative. Compare the complete serialized // authority so a host without a port does not match that host on a // non-default port (and so similarly-prefixed host names stay distinct). let tarball_authority = &tarball_url[url::Position::BeforeHost..url::Position::AfterPort]; if !config_authority.eq_ignore_ascii_case(tarball_authority) { return None; } let tarball_path = tarball_url.path(); let path_matches = if config_path.is_empty() { true } else if config_path.ends_with('/') { tarball_path.starts_with(config_path) } else { tarball_path == config_path || tarball_path .strip_prefix(config_path) .is_some_and(|rest| rest.starts_with('/')) }; path_matches.then_some(config_path.len()) } fn expand_vars(input: &str, sys: &impl EnvVar) -> String { fn escaped_char(input: &str) -> ParseResult<'_, char> { preceded(ch('\\'), next_char)(input) } fn env_var(input: &str) -> ParseResult<'_, &str> { let (input, _) = tag("${")(input)?; let (input, var_name) = take_while_byte(|b| b != b'}')(input)?; if var_name.chars().any(|c| matches!(c, '$' | '{' | '\\')) { return ParseError::backtrace(); } let (input, _) = ch('}')(input)?; Ok((input, var_name)) } let (input, results) = many0(or3( map(escaped_char, |c| c.to_string()), map(env_var, |var_name| { if let Ok(var_value) = sys.env_var(var_name) { var_value } else { format!("${{{}}}", var_name) } }), map(next_char, |c| c.to_string()), ))(input) .unwrap(); assert!(input.is_empty()); results.join("") } #[derive(Debug, Clone)] pub struct NpmRegistryUrl { pub url: Url, /// Whether the URL was read from an environment variable. pub from_env: bool, } impl NpmRegistryUrl { /// Gets the NPM_CONFIG_REGISTRY or falls back to https://registry.npmjs.org pub fn for_npm(sys: &impl EnvVar) -> Self { Self::from_env(sys, "NPM_CONFIG_REGISTRY", NPM_DEFAULT_REGISTRY) } /// Gets the JSR_NPM_URL or falls back to https://npm.jsr.io pub fn for_jsr(sys: &impl EnvVar) -> Self { // unfortunately we can't use NPM_CONFIG_JSR_REGISTRY because npm // will complain about an unknown configuration value Self::from_env(sys, "JSR_NPM_URL", "https://npm.jsr.io") } fn from_env( sys: &impl EnvVar, env_var_name: &str, fallback_url: &str, ) -> Self { fn ensure_trailing_slash(value: &str) -> Cow<'_, str> { if value.ends_with('/') { Cow::Borrowed(value) } else { Cow::Owned(format!("{}/", value)) } } if let Ok(registry_url) = sys.env_var(env_var_name) { // ensure there is a trailing slash for the directory let registry_url = ensure_trailing_slash(®istry_url); match Url::parse(®istry_url) { Ok(url) => { return NpmRegistryUrl { url, from_env: true, }; } Err(err) => { log::debug!( "Invalid {} environment variable: {:#}", env_var_name, err, ); } } } Self { url: Url::parse(fallback_url).unwrap(), from_env: false, } } } #[cfg(test)] mod test { use pretty_assertions::assert_eq; use sys_traits::EnvSetVar; use sys_traits::impls::InMemorySys; use super::*; fn replace_tarball_url( config: &str, registry: &str, tarball: &str, package_name: &str, ) -> String { let npm_rc = NpmRc::parse( &InMemorySys::default(), &format!("registry={registry}\n{config}"), ) .unwrap() .as_resolved(&npm_url(NPM_DEFAULT_REGISTRY)) .unwrap(); npm_rc .replace_tarball_url(Url::parse(tarball).unwrap(), package_name) .to_string() } #[test] fn test_replace_registry_host_default() { assert_eq!( replace_tarball_url( "", "https://artifactory.example.com/api/npm/npm-remote/", "https://registry.npmjs.org/@scope/pkg/-/pkg-1.0.0.tgz?x=1#fragment", "@scope/pkg", ), "https://artifactory.example.com/api/npm/npm-remote/@scope/pkg/-/pkg-1.0.0.tgz?x=1#fragment", ); assert_eq!( replace_tarball_url( "", "https://artifactory.example.com/api/npm/npm-remote/", "https://cdn.example.com/pkg-1.0.0.tgz", "pkg", ), "https://cdn.example.com/pkg-1.0.0.tgz", ); } #[test] fn test_replace_registry_host_never_and_always() { let tarball = "https://cdn.example.com/pkg/-/pkg-1.0.0.tgz"; assert_eq!( replace_tarball_url( "replace-registry-host=never", "https://mirror.example.com/npm/", tarball, "pkg", ), tarball, ); assert_eq!( replace_tarball_url( "replace-registry-host=always", "https://mirror.example.com/npm/", tarball, "pkg", ), "https://mirror.example.com/npm/pkg/-/pkg-1.0.0.tgz", ); } #[test] fn test_replace_registry_host_hostname() { assert_eq!( replace_tarball_url( "replace-registry-host=old.example.com", "https://mirror.example.com/npm/", "http://old.example.com/pkg/-/pkg-1.0.0.tgz", "pkg", ), "https://mirror.example.com/npm/pkg/-/pkg-1.0.0.tgz", ); } #[test] fn test_replace_registry_host_url_prefix() { assert_eq!( replace_tarball_url( "replace-registry-host=https://old.example.com/api/npm/", "https://mirror.example.com/npm/", "https://old.example.com/api/npm/pkg/-/pkg-1.0.0.tgz", "pkg", ), "https://mirror.example.com/npm/pkg/-/pkg-1.0.0.tgz", ); let non_matching = "https://old.example.com/api/npm-other/pkg/-/pkg-1.0.0.tgz"; assert_eq!( replace_tarball_url( "replace-registry-host=https://old.example.com/api/npm/", "https://mirror.example.com/npm/", non_matching, "pkg", ), non_matching, ); } #[test] fn test_replace_registry_host_does_not_duplicate_registry_path() { assert_eq!( replace_tarball_url( "replace-registry-host=old.example.com", "https://mirror.example.com/npm/", "https://old.example.com/npm/pkg/-/pkg-1.0.0.tgz", "pkg", ), "https://mirror.example.com/npm/pkg/-/pkg-1.0.0.tgz", ); } #[test] fn test_replace_registry_host_uses_scoped_registry() { assert_eq!( replace_tarball_url( "@scope:registry=https://scope.example.com/npm/", "https://default.example.com/", "https://registry.npmjs.org/@scope/pkg/-/pkg-1.0.0.tgz", "@scope/pkg", ), "https://scope.example.com/npm/@scope/pkg/-/pkg-1.0.0.tgz", ); } #[test] fn test_replace_registry_host_env_overrides_npmrc() { let sys = InMemorySys::default(); sys.env_set_var("NPM_CONFIG_REPLACE_REGISTRY_HOST", "never"); let npm_rc = NpmRc::parse(&sys, "replace-registry-host=always").unwrap(); assert_eq!( npm_rc.replace_registry_host, Some(ReplaceRegistryHost::Never) ); } #[test] fn test_parse_basic() { // https://docs.npmjs.com/cli/v10/configuring-npm/npmrc#auth-related-configuration let npm_rc = NpmRc::parse( &InMemorySys::default(), r#" @myorg:registry=https://example.com/myorg @another:registry=https://example.com/another @example:registry=https://example.com/example @yet_another:registry=https://yet.another.com/ //registry.npmjs.org/:_authToken=MYTOKEN ; would apply to both @myorg and @another //example.com/:_authToken=MYTOKEN0 //example.com/:_auth=AUTH //example.com/:username=USERNAME //example.com/:_password=PASSWORD //example.com/:email=EMAIL //example.com/:certfile=CERTFILE //example.com/:keyfile=KEYFILE ; would apply only to @myorg //example.com/myorg/:_authToken=MYTOKEN1 ; would apply only to @another //example.com/another/:_authToken=MYTOKEN2 ; this should not apply to `@yet_another`, because the URL contains the name of the scope ; and not the URL of the registry root specified above //yet.another.com/yet_another/:_authToken=MYTOKEN3 registry=https://registry.npmjs.org/ "#, ) .unwrap(); assert_eq!( npm_rc, NpmRc { registry: Some("https://registry.npmjs.org/".to_string()), scope_registries: HashMap::from([ ("myorg".to_string(), "https://example.com/myorg".to_string()), ( "another".to_string(), "https://example.com/another".to_string() ), ( "example".to_string(), "https://example.com/example".to_string() ), ( "yet_another".to_string(), "https://yet.another.com/".to_string() ), ]), registry_configs: HashMap::from([ ( "example.com/".to_string(), Arc::new(RegistryConfig { auth: Some("AUTH".to_string()), auth_token: Some("MYTOKEN0".to_string()), username: Some("USERNAME".to_string()), password: Some("PASSWORD".to_string()), email: Some("EMAIL".to_string()), certfile: Some("CERTFILE".to_string()), keyfile: Some("KEYFILE".to_string()), }) ), ( "example.com/another/".to_string(), Arc::new(RegistryConfig { auth_token: Some("MYTOKEN2".to_string()), ..Default::default() }) ), ( "example.com/myorg/".to_string(), Arc::new(RegistryConfig { auth_token: Some("MYTOKEN1".to_string()), ..Default::default() }) ), ( "yet.another.com/yet_another/".to_string(), Arc::new(RegistryConfig { auth_token: Some("MYTOKEN3".to_string()), ..Default::default() }) ), ( "registry.npmjs.org/".to_string(), Arc::new(RegistryConfig { auth_token: Some("MYTOKEN".to_string()), ..Default::default() }) ), ]), replace_registry_host: None, min_release_age_days: None, trust_policy: Default::default(), trust_policy_ignore_after_minutes: None, trust_policy_exclude: Vec::new(), } ); let resolved_npm_rc = npm_rc .as_resolved(&npm_url("https://deno.land/npm/")) .unwrap(); assert_eq!( resolved_npm_rc, ResolvedNpmRc { default_config: RegistryConfigWithUrl { registry_url: Url::parse("https://registry.npmjs.org/").unwrap(), config: Arc::new(RegistryConfig { auth_token: Some("MYTOKEN".to_string()), ..Default::default() }), }, scopes: HashMap::from([ ( "myorg".to_string(), RegistryConfigWithUrl { registry_url: Url::parse("https://example.com/myorg/").unwrap(), config: Arc::new(RegistryConfig { auth_token: Some("MYTOKEN1".to_string()), ..Default::default() }) } ), ( "another".to_string(), RegistryConfigWithUrl { registry_url: Url::parse("https://example.com/another/").unwrap(), config: Arc::new(RegistryConfig { auth_token: Some("MYTOKEN2".to_string()), ..Default::default() }) } ), ( "example".to_string(), RegistryConfigWithUrl { registry_url: Url::parse("https://example.com/example/").unwrap(), config: Arc::new(RegistryConfig { auth: Some("AUTH".to_string()), auth_token: Some("MYTOKEN0".to_string()), username: Some("USERNAME".to_string()), password: Some("PASSWORD".to_string()), email: Some("EMAIL".to_string()), certfile: Some("CERTFILE".to_string()), keyfile: Some("KEYFILE".to_string()), }) } ), ( "yet_another".to_string(), RegistryConfigWithUrl { registry_url: Url::parse("https://yet.another.com/").unwrap(), config: Default::default() } ), ]), registry_configs: npm_rc.registry_configs.clone(), replace_registry_host: ReplaceRegistryHost::default(), min_release_age_days: None, trust_policy: Default::default(), trust_policy_ignore_after_minutes: None, trust_policy_exclude: Vec::new(), } ); // no matching scoped package { let registry_url = resolved_npm_rc.get_registry_url("test"); let config = resolved_npm_rc.get_registry_config("test"); assert_eq!(registry_url.as_str(), "https://registry.npmjs.org/"); assert_eq!(config.auth_token, Some("MYTOKEN".to_string())); } // matching scoped package { let registry_url = resolved_npm_rc.get_registry_url("@example/pkg"); let config = resolved_npm_rc.get_registry_config("@example/pkg"); assert_eq!(registry_url.as_str(), "https://example.com/example/"); assert_eq!(config.auth_token, Some("MYTOKEN0".to_string())); } // matching scoped package with specific token { let registry_url = resolved_npm_rc.get_registry_url("@myorg/pkg"); let config = resolved_npm_rc.get_registry_config("@myorg/pkg"); assert_eq!(registry_url.as_str(), "https://example.com/myorg/"); assert_eq!(config.auth_token, Some("MYTOKEN1".to_string())); } // This should not return the token - the configuration is borked for `@yet_another` scope - // it defines the registry url as root + scope_name and instead it should be matching the // registry root. { let registry_url = resolved_npm_rc.get_registry_url("@yet_another/pkg"); let config = resolved_npm_rc.get_registry_config("@yet_another/pkg"); assert_eq!(registry_url.as_str(), "https://yet.another.com/"); assert_eq!(config.auth_token, None); } assert_eq!( resolved_npm_rc.get_registry_url("@deno/test").as_str(), "https://registry.npmjs.org/" ); assert_eq!( resolved_npm_rc .get_registry_config("@deno/test") .auth_token .as_ref() .unwrap(), "MYTOKEN" ); assert_eq!( resolved_npm_rc.get_registry_url("@myorg/test").as_str(), "https://example.com/myorg/" ); assert_eq!( resolved_npm_rc .get_registry_config("@myorg/test") .auth_token .as_ref() .unwrap(), "MYTOKEN1" ); assert_eq!( resolved_npm_rc.get_registry_url("@another/test").as_str(), "https://example.com/another/" ); assert_eq!( resolved_npm_rc .get_registry_config("@another/test") .auth_token .as_ref() .unwrap(), "MYTOKEN2" ); assert_eq!( resolved_npm_rc.get_registry_url("@example/test").as_str(), "https://example.com/example/" ); let config = resolved_npm_rc.get_registry_config("@example/test"); assert_eq!(config.auth.as_ref().unwrap(), "AUTH"); assert_eq!(config.auth_token.as_ref().unwrap(), "MYTOKEN0"); assert_eq!(config.username.as_ref().unwrap(), "USERNAME"); assert_eq!(config.password.as_ref().unwrap(), "PASSWORD"); assert_eq!(config.email.as_ref().unwrap(), "EMAIL"); assert_eq!(config.certfile.as_ref().unwrap(), "CERTFILE"); assert_eq!(config.keyfile.as_ref().unwrap(), "KEYFILE"); // tarball uri { assert_eq!( resolved_npm_rc .tarball_config( &Url::parse("https://example.com/example/chalk.tgz").unwrap(), ) .unwrap() .auth_token .as_ref() .unwrap(), "MYTOKEN0" ); assert_eq!( resolved_npm_rc .tarball_config( &Url::parse("https://example.com/myorg/chalk.tgz").unwrap(), ) .unwrap() .auth_token .as_ref() .unwrap(), "MYTOKEN1" ); assert_eq!( resolved_npm_rc .tarball_config( &Url::parse("https://example.com/another/chalk.tgz").unwrap(), ) .unwrap() .auth_token .as_ref() .unwrap(), "MYTOKEN2" ); assert_eq!( resolved_npm_rc.tarball_config( &Url::parse("https://yet.another.com/example/chalk.tgz").unwrap(), ), None, ); assert_eq!( resolved_npm_rc .tarball_config( &Url::parse( "https://yet.another.com/yet_another/example/chalk.tgz" ) .unwrap(), ) .unwrap() .auth_token .as_ref() .unwrap(), "MYTOKEN3" ); } } #[test] fn test_tarball_config_matches_authority_and_path_boundaries() { let npm_rc = NpmRc::parse( &InMemorySys::default(), r#" //example.com:_authToken=HOST //example.com:8443/:_authToken=PORT //example.com/private:_authToken=PRIVATE //example.com/private/nested/:_authToken=NESTED //[::1]:8443/:_authToken=IPV6 "#, ) .unwrap(); let resolved_npm_rc = npm_rc .as_resolved(&npm_url("https://registry.npmjs.org/")) .unwrap(); let auth_token = |url: &str| { resolved_npm_rc .tarball_config(&Url::parse(url).unwrap()) .and_then(|config| config.auth_token.as_deref()) }; // Auth keys are scheme-relative, but the complete host and port must // match. assert_eq!(auth_token("https://example.com/pkg.tgz"), Some("HOST")); assert_eq!(auth_token("http://example.com/pkg.tgz"), Some("HOST")); assert_eq!(auth_token("https://example.com:443/pkg.tgz"), Some("HOST")); assert_eq!(auth_token("https://example.com.evil/pkg.tgz"), None); assert_eq!(auth_token("https://example.com:8443/pkg.tgz"), Some("PORT")); assert_eq!(auth_token("https://example.com:8444/pkg.tgz"), None); assert_eq!(auth_token("https://example.com:18443/pkg.tgz"), None); assert_eq!(auth_token("https://[::1]:8443/pkg.tgz"), Some("IPV6")); assert_eq!(auth_token("https://[::1]:8444/pkg.tgz"), None); // Path matches stop at segment boundaries and the longest valid path wins. assert_eq!( auth_token("https://example.com/private/pkg.tgz"), Some("PRIVATE") ); assert_eq!(auth_token("https://example.com/private"), Some("PRIVATE")); assert_eq!( auth_token("https://example.com/privateevil/pkg.tgz"), Some("HOST") ); assert_eq!( auth_token("https://example.com/private:evil/pkg.tgz"), Some("HOST") ); assert_eq!( auth_token("https://example.com/private/nested/pkg.tgz"), Some("NESTED") ); assert_eq!( auth_token("https://example.com/private/nested"), Some("PRIVATE") ); assert_eq!( auth_token("https://example.com/private/nested/?download=1"), Some("NESTED") ); } #[test] fn test_parse_env_vars() { let sys = InMemorySys::default(); sys.env_set_var("VAR_FOUND", "SOME_VALUE"); let npm_rc = NpmRc::parse( &sys, r#" @myorg:registry=${VAR_FOUND} @another:registry=${VAR_NOT_FOUND} @a:registry=\${VAR_FOUND} //registry.npmjs.org/:_authToken=${VAR_FOUND} registry=${VAR_FOUND} "#, ) .unwrap(); assert_eq!( npm_rc, NpmRc { registry: Some("SOME_VALUE".to_string()), scope_registries: HashMap::from([ ("a".to_string(), "${VAR_FOUND}".to_string()), ("myorg".to_string(), "SOME_VALUE".to_string()), ("another".to_string(), "${VAR_NOT_FOUND}".to_string()), ]), registry_configs: HashMap::from([( "registry.npmjs.org/".to_string(), Arc::new(RegistryConfig { auth_token: Some("SOME_VALUE".to_string()), ..Default::default() }) ),]), replace_registry_host: None, min_release_age_days: None, trust_policy: Default::default(), trust_policy_ignore_after_minutes: None, trust_policy_exclude: Vec::new(), } ) } #[test] fn test_expand_vars() { let sys = InMemorySys::default(); sys.env_set_var("VAR", "VALUE"); assert_eq!(expand_vars("test${VAR}test", &sys), "testVALUEtest"); let sys = InMemorySys::default(); sys.env_set_var("A", "1"); sys.env_set_var("B", "2"); sys.env_set_var("C", "3"); assert_eq!(expand_vars("${A}${B}${C}", &sys), "123"); let sys = InMemorySys::default(); sys.env_set_var("VAR", "VALUE"); assert_eq!(expand_vars("test\\${VAR}test", &sys), "test${VAR}test"); let sys = InMemorySys::default(); // npm ignores values with $ in them assert_eq!(expand_vars("test${VA$R}test", &sys), "test${VA$R}test"); // npm ignores values with { in them assert_eq!(expand_vars("test${VA{R}test", &sys), "test${VA{R}test"); } #[test] fn test_parse_min_release_age() { let sys = InMemorySys::default(); let npm_rc = NpmRc::parse(&sys, "min-release-age=30").unwrap(); assert_eq!(npm_rc.min_release_age_days, Some(30)); let resolved = npm_rc .as_resolved(&npm_url("https://registry.npmjs.org/")) .unwrap(); assert_eq!(resolved.min_release_age_days, Some(30)); // not set let npm_rc = NpmRc::parse(&sys, "").unwrap(); assert_eq!(npm_rc.min_release_age_days, None); // invalid value is ignored let npm_rc = NpmRc::parse(&sys, "min-release-age=invalid").unwrap(); assert_eq!(npm_rc.min_release_age_days, None); // env var expansion sys.env_set_var("MIN_AGE", "7"); let npm_rc = NpmRc::parse(&sys, "min-release-age=${MIN_AGE}").unwrap(); assert_eq!(npm_rc.min_release_age_days, Some(7)); // npm config environment variable let sys = InMemorySys::default(); sys.env_set_var("NPM_CONFIG_MIN_RELEASE_AGE", "4"); let npm_rc = NpmRc::parse(&sys, "").unwrap(); assert_eq!(npm_rc.min_release_age_days, Some(4)); // .npmrc value takes precedence over the environment fallback. let npm_rc = NpmRc::parse(&sys, "min-release-age=5").unwrap(); assert_eq!(npm_rc.min_release_age_days, Some(5)); } #[test] fn test_parse_trust_policy() { let sys = InMemorySys::default(); // default is off let npm_rc = NpmRc::parse(&sys, "").unwrap(); assert_eq!(npm_rc.trust_policy, TrustPolicyConfig::Off); let npm_rc = NpmRc::parse(&sys, "trust-policy=no-downgrade").unwrap(); assert_eq!(npm_rc.trust_policy, TrustPolicyConfig::NoDowngrade); let resolved = npm_rc .as_resolved(&npm_url("https://registry.npmjs.org/")) .unwrap(); assert_eq!(resolved.trust_policy, TrustPolicyConfig::NoDowngrade); // unknown values fall back to off let npm_rc = NpmRc::parse(&sys, "trust-policy=bogus").unwrap(); assert_eq!(npm_rc.trust_policy, TrustPolicyConfig::Off); // trust-policy-ignore-after parses as a number of minutes and propagates // through to the resolved npmrc let npm_rc = NpmRc::parse( &sys, "trust-policy=no-downgrade\ntrust-policy-ignore-after=4320", ) .unwrap(); assert_eq!(npm_rc.trust_policy_ignore_after_minutes, Some(4320)); let resolved = npm_rc .as_resolved(&npm_url("https://registry.npmjs.org/")) .unwrap(); assert_eq!(resolved.trust_policy_ignore_after_minutes, Some(4320)); // unparsable values are ignored let npm_rc = NpmRc::parse(&sys, "trust-policy-ignore-after=soon").unwrap(); assert_eq!(npm_rc.trust_policy_ignore_after_minutes, None); // repeated `trust-policy-exclude[]` entries accumulate into the exclude // list and propagate through to the resolved npmrc let npm_rc = NpmRc::parse( &sys, "trust-policy=no-downgrade\ntrust-policy-exclude[]=@scope/pkg\ntrust-policy-exclude[]=other", ) .unwrap(); assert_eq!( npm_rc.trust_policy_exclude, vec!["@scope/pkg".to_string(), "other".to_string()] ); let resolved = npm_rc .as_resolved(&npm_url("https://registry.npmjs.org/")) .unwrap(); assert_eq!( resolved.trust_policy_exclude, vec!["@scope/pkg".to_string(), "other".to_string()] ); // default is an empty exclude list let npm_rc = NpmRc::parse(&sys, "").unwrap(); assert!(npm_rc.trust_policy_exclude.is_empty()); } #[test] fn test_scope_registry_url_only() { let npm_rc = NpmRc::parse( &InMemorySys::default(), r#" @example:registry=https://example.com/ "#, ) .unwrap(); let npm_rc = npm_rc .as_resolved(&npm_url("https://deno.land/npm/")) .unwrap(); { let registry_url = npm_rc.get_registry_url("@example/test"); let config = npm_rc.get_registry_config("@example/test"); assert_eq!(registry_url.as_str(), "https://example.com/"); assert_eq!(config.as_ref(), &RegistryConfig::default()); } { let registry_url = npm_rc.get_registry_url("test"); let config = npm_rc.get_registry_config("test"); assert_eq!(registry_url.as_str(), "https://deno.land/npm/"); assert_eq!(config.as_ref(), &Default::default()); } } #[test] fn test_scope_with_auth() { let npm_rc = NpmRc::parse( &InMemorySys::default(), r#" @example:registry=https://example.com/foo @example2:registry=https://example2.com/ //example.com/foo/:_authToken=MY_AUTH_TOKEN ; This one is borked - the URL must match registry URL exactly //example.com2/example/:_authToken=MY_AUTH_TOKEN2 "#, ) .unwrap(); let npm_rc = npm_rc .as_resolved(&npm_url("https://deno.land/npm/")) .unwrap(); { let registry_url = npm_rc.get_registry_url("@example/test"); let config = npm_rc.get_registry_config("@example/test"); assert_eq!(registry_url.as_str(), "https://example.com/foo/"); assert_eq!( config.as_ref(), &RegistryConfig { auth_token: Some("MY_AUTH_TOKEN".to_string()), ..Default::default() } ); } { let registry_url = npm_rc.get_registry_url("@example2/test"); let config = npm_rc.get_registry_config("@example2/test"); assert_eq!(registry_url.as_str(), "https://example2.com/"); assert_eq!(config.as_ref(), &Default::default()); } } #[test] fn test_scope_registry_same_as_env_registry() { // a scope registry that matches the env registry url should still // be included in the resolved npmrc. This is important because scopes // that are overridden by Deno like the @jsr scope might have the registry // set to the default registry like this and so we want to ensure it's // still used and not overwritten let npm_rc = NpmRc::parse( &InMemorySys::default(), r#" @jsr:registry=https://registry.npmjs.org/ "#, ) .unwrap(); let npm_rc = npm_rc .as_resolved(&npm_url("https://registry.npmjs.org/")) .unwrap(); assert!(npm_rc.scopes.contains_key("jsr")); assert_eq!( npm_rc.scopes.get("jsr").unwrap().registry_url.as_str(), "https://registry.npmjs.org/" ); } #[test] fn test_npm_config_registry_overrides_npmrc() { // NPM_CONFIG_REGISTRY should override the registry in .npmrc files let npm_rc = NpmRc::parse( &InMemorySys::default(), "registry=http://wrong.registry.example.com/", ) .unwrap(); // This simulates what npm_registry_url() would return when NPM_CONFIG_REGISTRY is set let env_registry_url = Url::parse("http://env.registry.example.com/").unwrap(); let resolved = npm_rc .as_resolved(&NpmRegistryUrl { url: env_registry_url, from_env: true, }) .unwrap(); // Should use the env var registry, not the .npmrc one assert_eq!( resolved.default_config.registry_url.as_str(), "http://env.registry.example.com/" ); } #[test] fn test_npmrc_registry_used_when_no_env_var() { // When NPM_CONFIG_REGISTRY is not set, should use .npmrc registry let npm_rc = NpmRc::parse( &InMemorySys::default(), "registry=http://npmrc.registry.example.com/", ) .unwrap(); let resolved = npm_rc .as_resolved(&NpmRegistryUrl { url: Url::parse("https://registry.npmjs.org/").unwrap(), from_env: false, }) .unwrap(); // Should use the .npmrc registry assert_eq!( resolved.default_config.registry_url.as_str(), "http://npmrc.registry.example.com/" ); } #[test] fn test_gitlab_instance_level_tarball_auth() { // GitLab "instance-level" npm registries serve tarballs from a different // path than the registry endpoint: // registry: https://gitlab.example.com/api/v4/packages/npm/ // tarball: https://gitlab.example.com/api/v4/projects/4055/packages/npm/@scope/pkg/-/...tgz // The auth token is scoped to the registry path, so a plain path-prefix // match against the tarball URL fails. See // https://github.com/denoland/deno/issues/27759 let npm_rc = NpmRc::parse( &InMemorySys::default(), r#" @myscope:registry=https://gitlab.example.com/api/v4/packages/npm/ //gitlab.example.com/api/v4/packages/npm/:_authToken=GITLABTOKEN "#, ) .unwrap(); let resolved_npm_rc = npm_rc .as_resolved(&npm_url("https://registry.npmjs.org/")) .unwrap(); let tarball_url = Url::parse( "https://gitlab.example.com/api/v4/projects/4055/packages/npm/@myscope/pkg/-/@myscope/pkg-1.0.0.tgz", ) .unwrap(); // Plain path-prefix matching (npm-compatible) does not find the auth, // because the tarball path differs from the registry path. assert_eq!(resolved_npm_rc.tarball_config(&tarball_url), None); // Package-aware lookup falls back to the scoped registry's auth because the // tarball is served from the same host as the scope's registry. assert_eq!( resolved_npm_rc .tarball_config_for_package(&tarball_url, "@myscope/pkg") .unwrap() .auth_token .as_deref(), Some("GITLABTOKEN"), ); // The fallback must not apply a scope's token to an unrelated host. let other_host = Url::parse( "https://evil.example.org/api/v4/projects/4055/packages/npm/@myscope/pkg/-/pkg-1.0.0.tgz", ) .unwrap(); assert_eq!( resolved_npm_rc.tarball_config_for_package(&other_host, "@myscope/pkg"), None, ); // Same host but a different port is a different origin: no fallback. let other_port = Url::parse( "https://gitlab.example.com:8443/api/v4/projects/4055/packages/npm/@myscope/pkg/-/pkg-1.0.0.tgz", ) .unwrap(); assert_eq!( resolved_npm_rc.tarball_config_for_package(&other_port, "@myscope/pkg"), None, ); // Same host but a downgraded scheme is a different origin: the token must // not be sent over http when the registry is https. let other_scheme = Url::parse( "http://gitlab.example.com/api/v4/projects/4055/packages/npm/@myscope/pkg/-/pkg-1.0.0.tgz", ) .unwrap(); assert_eq!( resolved_npm_rc.tarball_config_for_package(&other_scheme, "@myscope/pkg"), None, ); // A package whose scope has no configured registry does not fall back to an // unrelated scope's auth. assert_eq!( resolved_npm_rc.tarball_config_for_package(&tarball_url, "@other/pkg"), None, ); } #[test] fn test_tarball_config_for_package_default_scope() { // An instance-level registry configured as the default (unscoped) registry // serves tarballs from a different path; the fallback resolves through // `default_config` for unscoped packages. let npm_rc = NpmRc::parse( &InMemorySys::default(), r#" registry=https://gitlab.example.com/api/v4/packages/npm/ //gitlab.example.com/api/v4/packages/npm/:_authToken=GITLABTOKEN "#, ) .unwrap(); let resolved_npm_rc = npm_rc .as_resolved(&npm_url("https://registry.npmjs.org/")) .unwrap(); let tarball_url = Url::parse( "https://gitlab.example.com/api/v4/projects/4055/packages/npm/pkg/-/pkg-1.0.0.tgz", ) .unwrap(); assert_eq!(resolved_npm_rc.tarball_config(&tarball_url), None); assert_eq!( resolved_npm_rc .tarball_config_for_package(&tarball_url, "pkg") .unwrap() .auth_token .as_deref(), Some("GITLABTOKEN"), ); } #[test] fn test_tarball_config_for_package_scoped_unconfigured_default() { // A scoped package whose scope is not separately configured resolves // through the default instance-level registry, so its same-origin tarball // auth must fall back to `default_config` rather than bailing out (which // would re-introduce the 404 this method fixes). let npm_rc = NpmRc::parse( &InMemorySys::default(), r#" registry=https://gitlab.example.com/api/v4/packages/npm/ //gitlab.example.com/api/v4/packages/npm/:_authToken=GITLABTOKEN "#, ) .unwrap(); let resolved_npm_rc = npm_rc .as_resolved(&npm_url("https://registry.npmjs.org/")) .unwrap(); let tarball_url = Url::parse( "https://gitlab.example.com/api/v4/projects/4055/packages/npm/@foo/bar/-/bar-1.0.0.tgz", ) .unwrap(); assert_eq!(resolved_npm_rc.tarball_config(&tarball_url), None); assert_eq!( resolved_npm_rc .tarball_config_for_package(&tarball_url, "@foo/bar") .unwrap() .auth_token .as_deref(), Some("GITLABTOKEN"), ); } #[test] fn test_has_auth() { let with = |f: fn(&mut RegistryConfig)| { let mut config = RegistryConfig::default(); f(&mut config); config.has_auth() }; assert!(with(|c| c.auth_token = Some("t".into()))); assert!(with(|c| c.auth = Some("a".into()))); assert!(with(|c| { c.username = Some("u".into()); c.password = Some("p".into()); })); // email substitutes for username, matching // maybe_auth_header_value_for_npm_registry. assert!(with(|c| { c.email = Some("e".into()); c.password = Some("p".into()); })); // Incomplete credentials don't count. assert!(!with(|_| {})); assert!(!with(|c| c.username = Some("u".into()))); assert!(!with(|c| c.password = Some("p".into()))); assert!(!with(|c| c.email = Some("e".into()))); } #[test] fn test_tarball_config_for_package_no_auth() { // Same-origin tarball but the registry carries no credentials: there is // nothing to fall back to, so no config is returned. let npm_rc = NpmRc::parse( &InMemorySys::default(), r#" @myscope:registry=https://gitlab.example.com/api/v4/packages/npm/ "#, ) .unwrap(); let resolved_npm_rc = npm_rc .as_resolved(&npm_url("https://registry.npmjs.org/")) .unwrap(); let tarball_url = Url::parse( "https://gitlab.example.com/api/v4/projects/4055/packages/npm/@myscope/pkg/-/pkg-1.0.0.tgz", ) .unwrap(); assert_eq!( resolved_npm_rc.tarball_config_for_package(&tarball_url, "@myscope/pkg"), None, ); } fn npm_url(url: &str) -> NpmRegistryUrl { NpmRegistryUrl { url: Url::parse(url).unwrap(), from_env: false, } } }