use super::*;
use crate::config::types::NpmrcSource;
use base64::Engine as _;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
#[test]
fn parse_npmrc_strips_utf8_bom() {
let dir = tempfile::tempdir().unwrap();
let rc = dir.path().join(".npmrc");
std::fs::write(&rc, "\u{feff}registry=https://r.example.com\n").unwrap();
let entries = parse_npmrc(&rc).unwrap();
assert_eq!(
entries,
vec![("registry".to_string(), "https://r.example.com".to_string())]
);
}
#[test]
fn scoped_registry_lookup_is_case_insensitive() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join(".npmrc"),
"@MyOrg:registry=https://myorg.example.com/\n",
)
.unwrap();
let cfg = NpmConfig::load_isolated(dir.path());
assert_eq!(cfg.registry_for("@myorg/pkg"), "https://myorg.example.com/");
}
#[test]
fn test_parse_npmrc_basic() {
let dir = tempfile::tempdir().unwrap();
let rc = dir.path().join(".npmrc");
std::fs::write(
&rc,
"registry=https://registry.example.com\n_authToken=secret123\n",
)
.unwrap();
let entries = parse_npmrc(&rc).unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(
entries[0],
(
"registry".to_string(),
"https://registry.example.com".to_string()
)
);
assert_eq!(
entries[1],
("_authToken".to_string(), "secret123".to_string())
);
}
#[test]
fn test_parse_npmrc_comments_and_blanks() {
let dir = tempfile::tempdir().unwrap();
let rc = dir.path().join(".npmrc");
std::fs::write(
&rc,
"# comment\n\n; another comment\nregistry=https://r.com\n",
)
.unwrap();
let entries = parse_npmrc(&rc).unwrap();
assert_eq!(entries.len(), 1);
}
#[test]
fn test_substitute_env() {
unsafe { std::env::set_var("AUBE_TEST_TOKEN_CFG", "mytoken") };
assert_eq!(substitute_env("${AUBE_TEST_TOKEN_CFG}"), "mytoken");
assert_eq!(
substitute_env("prefix-${AUBE_TEST_TOKEN_CFG}-suffix"),
"prefix-mytoken-suffix"
);
assert_eq!(substitute_env("no-vars-here"), "no-vars-here");
unsafe { std::env::remove_var("AUBE_TEST_TOKEN_CFG") };
}
#[test]
fn test_substitute_env_missing_var() {
assert_eq!(substitute_env("${AUBE_DEFINITELY_NOT_SET}"), "");
}
#[test]
fn parse_npmrc_strips_surrounding_quotes() {
let dir = tempfile::tempdir().unwrap();
let rc = dir.path().join(".npmrc");
std::fs::write(
&rc,
"//artifactory.example.com/api/npm/virtual-npm/:_auth=\"token==\"\n\
//registry.example.com/:_authToken='single-quoted'\n\
registry=\"https://r.example.com/\"\n\
unmatched=\"only-leading\n\
plain=value\n",
)
.unwrap();
let entries = parse_npmrc(&rc).unwrap();
assert_eq!(
entries,
vec![
(
"//artifactory.example.com/api/npm/virtual-npm/:_auth".to_string(),
"token==".to_string()
),
(
"//registry.example.com/:_authToken".to_string(),
"single-quoted".to_string()
),
("registry".to_string(), "https://r.example.com/".to_string()),
("unmatched".to_string(), "\"only-leading".to_string()),
("plain".to_string(), "value".to_string()),
]
);
}
#[test]
fn parse_npmrc_expands_env_in_keys_for_per_uri_auth() {
struct EnvVars(&'static [&'static str]);
impl Drop for EnvVars {
fn drop(&mut self) {
for name in self.0 {
unsafe { std::env::remove_var(name) };
}
}
}
let _vars = EnvVars(&["AUBE_TEST_NEXUS_HOST_CFG", "AUBE_TEST_NEXUS_TOKEN_CFG"]);
unsafe {
std::env::set_var(
"AUBE_TEST_NEXUS_HOST_CFG",
"//nexus.example.com/repository/npm/",
);
std::env::set_var("AUBE_TEST_NEXUS_TOKEN_CFG", "dXNlcjpwYXNz");
}
let dir = tempfile::tempdir().unwrap();
let rc = dir.path().join(".npmrc");
std::fs::write(
&rc,
"${AUBE_TEST_NEXUS_HOST_CFG}:_auth=${AUBE_TEST_NEXUS_TOKEN_CFG}\n",
)
.unwrap();
let entries = parse_npmrc(&rc).unwrap();
assert_eq!(
entries,
vec![(
"//nexus.example.com/repository/npm/:_auth".to_string(),
"dXNlcjpwYXNz".to_string(),
)]
);
let mut config = NpmConfig::default();
config.apply(entries);
assert_eq!(
config
.basic_auth_for("https://nexus.example.com/repository/npm/@scope/pkg/-/pkg-1.0.0.tgz"),
Some("dXNlcjpwYXNz".to_string()),
"tarball URL under the env-templated host must pick up _auth",
);
}
#[test]
fn test_package_scope() {
assert_eq!(package_scope("@myorg/pkg"), Some("@myorg"));
assert_eq!(package_scope("lodash"), None);
assert_eq!(package_scope("@types/node"), Some("@types"));
}
#[test]
fn test_registry_uri_key() {
assert_eq!(
registry_uri_key("https://registry.example.com/"),
"//registry.example.com/"
);
assert_eq!(
registry_uri_key("http://localhost:4873/"),
"//localhost:4873/"
);
}
#[test]
fn test_registry_uri_key_strips_default_port() {
assert_eq!(
registry_uri_key("https://registry.example.com:443/"),
"//registry.example.com/"
);
assert_eq!(
registry_uri_key("http://registry.example.com:80/artifactory/npm/"),
"//registry.example.com/artifactory/npm/"
);
assert_eq!(
registry_uri_key("https://registry.example.com:8443/"),
"//registry.example.com:8443/"
);
}
#[test]
fn test_registry_uri_key_only_strips_matching_default_port() {
assert_eq!(registry_uri_key("https://host:80/x/"), "//host:80/x/",);
assert_eq!(registry_uri_key("http://host:443/x/"), "//host:443/x/",);
}
#[test]
fn test_lookup_by_uri_prefix_longest_match() {
let mut map: BTreeMap<String, &'static str> = BTreeMap::new();
map.insert("//host/artifactory/npm/".to_string(), "scoped-token");
map.insert("//host/".to_string(), "root-token");
assert_eq!(
lookup_by_uri_prefix(&map, "//host/artifactory/npm/lodash/-/lodash-4.17.21.tgz"),
Some(&"scoped-token"),
);
assert_eq!(
lookup_by_uri_prefix(&map, "//host/other/pkg.tgz"),
Some(&"root-token"),
);
assert_eq!(lookup_by_uri_prefix(&map, "//other/foo"), None);
}
#[test]
fn auth_token_resolves_for_path_scoped_registry_with_default_port() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join(".npmrc"),
"registry=https://registry.example.com/artifactory/npm/\n\
//registry.example.com/artifactory/npm/:_authToken=scoped-secret\n",
)
.unwrap();
let config = NpmConfig::load_isolated(dir.path());
assert_eq!(
config.auth_token_for(
"https://registry.example.com:443/artifactory/npm/lodash/-/lodash-4.17.21.tgz"
),
Some("scoped-secret"),
);
assert_eq!(
config.auth_token_for(
"https://registry.example.com/artifactory/npm/lodash/-/lodash-4.17.21.tgz"
),
Some("scoped-secret"),
);
}
#[test]
fn npmrc_key_with_default_port_is_normalized_on_ingest() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join(".npmrc"),
"//registry.example.com:443/:_authToken=via-443\n",
)
.unwrap();
let config = NpmConfig::load_isolated(dir.path());
assert_eq!(
config.auth_token_for("https://registry.example.com/"),
Some("via-443"),
);
}
#[test]
fn test_normalize_registry_url() {
assert_eq!(normalize_registry_url("https://r.com"), "https://r.com/");
assert_eq!(normalize_registry_url("https://r.com/"), "https://r.com/");
}
#[test]
fn test_config_load_project_npmrc() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join(".npmrc"),
"registry=https://custom.registry.com\n\
@myorg:registry=https://myorg.registry.com\n\
//myorg.registry.com/:_authToken=org-secret\n\
//custom.registry.com/:_authToken=custom-secret\n",
)
.unwrap();
let config = NpmConfig::load_isolated(dir.path());
assert_eq!(config.registry, "https://custom.registry.com/");
assert_eq!(
config.registry_for("@myorg/pkg"),
"https://myorg.registry.com/"
);
assert_eq!(
config.registry_for("lodash"),
"https://custom.registry.com/"
);
assert_eq!(
config.auth_token_for("https://myorg.registry.com/"),
Some("org-secret")
);
assert_eq!(
config.auth_token_for("https://custom.registry.com/"),
Some("custom-secret")
);
}
#[test]
fn split_username_password_auth_resolves_to_basic_header_payload() {
let dir = tempfile::tempdir().unwrap();
let encoded_password = base64::engine::general_purpose::STANDARD.encode("s3cr3t");
std::fs::write(
dir.path().join(".npmrc"),
format!(
"//registry.example.com/:username=alice\n\
//registry.example.com/:_password={encoded_password}\n"
),
)
.unwrap();
let config = NpmConfig::load_isolated(dir.path());
let expected = base64::engine::general_purpose::STANDARD.encode("alice:s3cr3t");
assert_eq!(
config.basic_auth_for("https://registry.example.com/"),
Some(expected),
);
}
#[test]
fn token_helper_from_project_npmrc_is_refused_kebab_case() {
let project = tempfile::tempdir().unwrap();
std::fs::write(
project.path().join(".npmrc"),
"//registry.example.com/:token-helper=/tmp/evil.sh\n",
)
.unwrap();
let home = tempfile::tempdir().unwrap();
let mut config = NpmConfig::default();
config.apply_tagged(load_npmrc_entries_tagged_with_home(
Some(home.path()),
None,
project.path(),
None,
));
assert_eq!(
config.token_helper_for("https://registry.example.com/"),
None,
"project-scope token-helper (kebab-case) must be refused"
);
}
#[test]
fn token_helper_from_project_npmrc_is_refused() {
let project = tempfile::tempdir().unwrap();
std::fs::write(
project.path().join(".npmrc"),
"//registry.example.com/:tokenHelper=/tmp/evil.sh\n",
)
.unwrap();
let home = tempfile::tempdir().unwrap();
let mut config = NpmConfig::default();
config.apply_tagged(load_npmrc_entries_tagged_with_home(
Some(home.path()),
None,
project.path(),
None,
));
assert_eq!(
config.token_helper_for("https://registry.example.com/"),
None,
"project-scope tokenHelper must be refused"
);
}
#[test]
fn token_helper_from_user_npmrc_is_accepted() {
let home = tempfile::tempdir().unwrap();
let helper_path = if cfg!(windows) {
"C:\\opt\\aube\\helper.exe"
} else {
"/opt/aube/helper"
};
std::fs::write(
home.path().join(".npmrc"),
format!("//registry.example.com/:tokenHelper={helper_path}\n"),
)
.unwrap();
let project = tempfile::tempdir().unwrap();
let mut config = NpmConfig::default();
config.apply_tagged(load_npmrc_entries_tagged_with_home(
Some(home.path()),
None,
project.path(),
None,
));
assert_eq!(
config.token_helper_for("https://registry.example.com/"),
Some(helper_path)
);
}
#[test]
fn token_helper_from_npmrc_auth_file_is_refused() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let auth = project.path().join("auth.rc");
std::fs::write(&auth, "//registry.example.com/:tokenHelper=/tmp/evil.sh\n").unwrap();
std::fs::write(
project.path().join(".npmrc"),
format!(
"npmrc-auth-file={}\n",
auth.to_string_lossy().replace('\\', "/")
),
)
.unwrap();
let mut config = NpmConfig::default();
config.apply_tagged(load_npmrc_entries_tagged_with_home(
Some(home.path()),
None,
project.path(),
None,
));
assert_eq!(
config.token_helper_for("https://registry.example.com/"),
None,
"tokenHelper from an auth file reachable via project `.npmrc` must be refused"
);
}
#[test]
fn sanitize_token_helper_accepts_absolute_path() {
assert_eq!(
sanitize_token_helper("/usr/local/bin/aws-npm-helper"),
Some("/usr/local/bin/aws-npm-helper".to_string())
);
assert_eq!(
sanitize_token_helper("C:\\Program.Files\\auth.exe"),
Some("C:\\Program.Files\\auth.exe".to_string())
);
assert_eq!(
sanitize_token_helper("C:/tools/auth.exe"),
Some("C:/tools/auth.exe".to_string())
);
assert_eq!(
sanitize_token_helper("\\\\server\\share\\auth.exe"),
Some("\\\\server\\share\\auth.exe".to_string())
);
}
#[test]
fn sanitize_token_helper_rejects_relative_path() {
assert!(sanitize_token_helper("aws-helper").is_none());
assert!(sanitize_token_helper("./aws-helper").is_none());
assert!(sanitize_token_helper("bin/aws-helper").is_none());
}
#[test]
fn sanitize_token_helper_rejects_shell_metacharacters() {
for v in [
"/bin/helper;rm",
"/bin/helper|rm",
"/bin/helper&rm",
"/bin/helper`rm`",
"/bin/helper$(rm)",
"/bin/helper>log",
"/bin/helper<log",
"/bin/helper*glob",
"/bin/helper?glob",
"/bin/helper\"evil",
"/bin/helper'evil",
] {
assert!(sanitize_token_helper(v).is_none(), "should reject {v:?}");
}
}
#[test]
fn sanitize_token_helper_rejects_whitespace() {
assert!(sanitize_token_helper("/bin/helper --flag").is_none());
assert!(sanitize_token_helper("/bin/helper\targ").is_none());
assert!(sanitize_token_helper("/bin/helper\nevil").is_none());
}
#[test]
fn sanitize_token_helper_rejects_empty_and_nul() {
assert!(sanitize_token_helper("").is_none());
assert!(sanitize_token_helper(" ").is_none());
assert!(sanitize_token_helper("/bin/helper\0evil").is_none());
}
#[test]
fn sanitize_token_helper_rejects_env_substitution_markers() {
assert!(sanitize_token_helper("/bin/helper-${EVIL}").is_none());
assert!(sanitize_token_helper("/bin/$EVIL").is_none());
}
#[test]
fn per_registry_tls_config_is_parsed() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join(".npmrc"),
"//registry.example.com/:ca=-----BEGIN CERTIFICATE-----\\nca\\n-----END CERTIFICATE-----\n\
//registry.example.com/:cafile=corp-ca.pem\n\
//registry.example.com/:cert=-----BEGIN CERTIFICATE-----\\nclient\\n-----END CERTIFICATE-----\n\
//registry.example.com/:key=-----BEGIN PRIVATE KEY-----\\nkey\\n-----END PRIVATE KEY-----\n",
)
.unwrap();
let config = NpmConfig::load_isolated(dir.path());
let tls = &config
.registry_config_for("https://registry.example.com/")
.expect("registry config")
.tls;
assert_eq!(tls.ca.len(), 1);
assert!(tls.ca[0].contains("\nca\n"));
assert!(!tls.ca[0].contains("\\n"));
assert_eq!(tls.cafile.as_deref(), Some(Path::new("corp-ca.pem")));
assert!(tls.cert.as_deref().unwrap().contains("\nclient\n"));
assert!(tls.key.as_deref().unwrap().contains("\nkey\n"));
}
#[test]
fn top_level_cafile_and_ca_are_parsed() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join(".npmrc"),
"cafile=/etc/ssl/corp-bundle.pem\n\
ca=-----BEGIN CERTIFICATE-----\\nfirst\\n-----END CERTIFICATE-----\n\
ca[]=-----BEGIN CERTIFICATE-----\\nsecond\\n-----END CERTIFICATE-----\n",
)
.unwrap();
let config = NpmConfig::load_isolated(dir.path());
assert_eq!(
config.cafile.as_deref(),
Some(Path::new("/etc/ssl/corp-bundle.pem"))
);
assert_eq!(config.ca.len(), 2);
assert!(config.ca[0].contains("\nfirst\n"));
assert!(config.ca[1].contains("\nsecond\n"));
assert!(
config
.registry_config_for("https://registry.npmjs.org/")
.is_none()
);
}
#[test]
fn unscoped_auth_token_is_pinned_to_same_source_registry() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join(".npmrc"), "_authToken=global-token\n").unwrap();
let config = NpmConfig::load_isolated(dir.path());
assert_eq!(
config.auth_token_for("https://registry.npmjs.org/"),
Some("global-token")
);
assert_eq!(config.auth_token_for("https://registry.example.com/"), None);
}
#[test]
fn unscoped_auth_uses_registry_from_same_source() {
let home = tempfile::tempdir().unwrap();
std::fs::write(
home.path().join(".npmrc"),
"registry=https://registry.npmjs.org/\n_authToken=user-token\n",
)
.unwrap();
let project = tempfile::tempdir().unwrap();
std::fs::write(
project.path().join(".npmrc"),
"registry=https://registry.internal.example/\n",
)
.unwrap();
let mut config = NpmConfig::default();
config.apply_tagged(load_npmrc_entries_tagged_with_home(
Some(home.path()),
None,
project.path(),
None,
));
assert_eq!(
config.registry, "https://registry.internal.example/",
"project registry still wins as the effective default"
);
assert_eq!(
config.auth_token_for("https://registry.npmjs.org/"),
Some("user-token")
);
assert_eq!(
config.auth_token_for("https://registry.internal.example/"),
None,
"project registry must not inherit user source's unscoped token"
);
}
#[test]
fn unscoped_auth_uses_same_source_registry_regardless_of_order() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join(".npmrc"),
"_authToken=private-token\nregistry=https://registry.private.example/\n",
)
.unwrap();
let config = NpmConfig::load_isolated(dir.path());
assert_eq!(
config.auth_token_for("https://registry.private.example/"),
Some("private-token")
);
assert_eq!(config.auth_token_for("https://registry.npmjs.org/"), None);
}
#[test]
fn uri_scoped_auth_beats_later_rescoped_bare_auth() {
let home = tempfile::tempdir().unwrap();
std::fs::write(
home.path().join(".npmrc"),
"//registry.npmjs.org/:_authToken=user-token\n",
)
.unwrap();
let project = tempfile::tempdir().unwrap();
std::fs::write(project.path().join(".npmrc"), "_authToken=project-token\n").unwrap();
let mut config = NpmConfig::default();
config.apply_tagged(load_npmrc_entries_tagged_with_home(
Some(home.path()),
None,
project.path(),
None,
));
assert_eq!(
config.auth_token_for("https://registry.npmjs.org/"),
Some("user-token")
);
}
#[test]
fn later_bare_auth_can_override_earlier_bare_auth() {
let mut config = NpmConfig::default();
config.apply_tagged(vec![
(
NpmrcSource::User,
"_authToken".to_string(),
"user-token".to_string(),
),
(
NpmrcSource::Env,
"_authToken".to_string(),
"env-token".to_string(),
),
]);
assert_eq!(
config.auth_token_for("https://registry.npmjs.org/"),
Some("env-token")
);
}
#[test]
fn later_uri_scoped_auth_can_override_earlier_bare_auth() {
let mut config = NpmConfig::default();
config.apply_tagged(vec![
(
NpmrcSource::User,
"_authToken".to_string(),
"user-token".to_string(),
),
(
NpmrcSource::Project,
"//registry.npmjs.org/:_authToken".to_string(),
"project-token".to_string(),
),
]);
assert_eq!(
config.auth_token_for("https://registry.npmjs.org/"),
Some("project-token")
);
}
#[test]
fn unscoped_tls_client_credentials_are_registry_scoped() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join(".npmrc"),
"registry=https://registry.example.com/\n\
cert=-----BEGIN CERTIFICATE-----\\nclient\\n-----END CERTIFICATE-----\n\
key=-----BEGIN PRIVATE KEY-----\\nkey\\n-----END PRIVATE KEY-----\n",
)
.unwrap();
let config = NpmConfig::load_isolated(dir.path());
let tls = &config
.registry_config_for("https://registry.example.com/")
.unwrap()
.tls;
assert!(tls.cert.as_deref().unwrap().contains("\nclient\n"));
assert!(tls.key.as_deref().unwrap().contains("\nkey\n"));
assert!(
config
.registry_config_for("https://registry.npmjs.org/")
.is_none()
);
}
#[test]
fn test_config_defaults() {
let dir = tempfile::tempdir().unwrap();
let config = NpmConfig::load_isolated(dir.path());
assert_eq!(config.registry, "https://registry.npmjs.org/");
assert!(
config
.auth_token_for("https://registry.npmjs.org/")
.is_none()
);
}
#[test]
fn test_config_scoped_registry_without_auth() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join(".npmrc"),
"@private:registry=https://private.registry.com\n",
)
.unwrap();
let config = NpmConfig::load_isolated(dir.path());
assert_eq!(
config.registry_for("@private/my-lib"),
"https://private.registry.com/"
);
assert!(
config
.auth_token_for("https://private.registry.com/")
.is_none()
);
}
#[test]
fn test_http_proxy_inherits_https_proxy() {
let mut config = NpmConfig {
https_proxy: Some("http://corp.proxy:8080".to_string()),
..Default::default()
};
config.apply_proxy_env();
assert_eq!(
config.http_proxy.as_deref(),
Some("http://corp.proxy:8080"),
"http_proxy must inherit https_proxy"
);
}
#[test]
fn test_npmrc_proxy_key_feeds_https_proxy() {
let mut config = NpmConfig {
npmrc_proxy: Some("http://legacy:3128".to_string()),
..Default::default()
};
config.apply_proxy_env();
assert_eq!(
config.https_proxy.as_deref(),
Some("http://legacy:3128"),
"legacy `proxy=` key must resolve into https_proxy"
);
assert_eq!(
config.http_proxy.as_deref(),
Some("http://legacy:3128"),
"http_proxy then inherits the resolved https_proxy"
);
}
#[test]
fn test_explicit_https_proxy_wins_over_npmrc_proxy() {
let mut config = NpmConfig {
https_proxy: Some("http://explicit:1".to_string()),
npmrc_proxy: Some("http://fallback:2".to_string()),
..Default::default()
};
config.apply_proxy_env();
assert_eq!(config.https_proxy.as_deref(), Some("http://explicit:1"));
}
#[test]
fn test_default_strict_ssl_is_true() {
let c = NpmConfig::default();
assert!(c.strict_ssl);
}
#[test]
fn test_parses_proxy_and_ssl_settings() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join(".npmrc"),
"https-proxy=http://proxy.example.com:8080\n\
proxy=http://plain.example.com:3128\n\
noproxy=localhost,.internal\n\
strict-ssl=false\n\
local-address=127.0.0.1\n\
maxsockets=12\n",
)
.unwrap();
let home = tempfile::tempdir().unwrap();
let mut config = NpmConfig {
registry: "https://registry.npmjs.org/".to_string(),
strict_ssl: true,
..Default::default()
};
config.apply(load_npmrc_entries_with_home(
Some(home.path()),
None,
dir.path(),
None,
));
assert_eq!(
config.https_proxy.as_deref(),
Some("http://proxy.example.com:8080")
);
assert_eq!(
config.npmrc_proxy.as_deref(),
Some("http://plain.example.com:3128")
);
assert!(config.http_proxy.is_none());
assert_eq!(config.no_proxy.as_deref(), Some("localhost,.internal"));
assert!(!config.strict_ssl);
assert_eq!(
config.local_address,
Some("127.0.0.1".parse::<std::net::IpAddr>().unwrap())
);
assert_eq!(config.max_sockets, Some(12));
}
#[test]
fn test_strict_ssl_default_true() {
let dir = tempfile::tempdir().unwrap();
let home = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join(".npmrc"), "").unwrap();
let mut config = NpmConfig {
strict_ssl: true,
..Default::default()
};
config.apply(load_npmrc_entries_with_home(
Some(home.path()),
None,
dir.path(),
None,
));
assert!(config.strict_ssl);
}
#[test]
fn test_camel_case_proxy_aliases() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join(".npmrc"),
"httpsProxy=http://a\nhttpProxy=http://b\nnoProxy=foo\nstrictSsl=false\nlocalAddress=::1\n",
)
.unwrap();
let home = tempfile::tempdir().unwrap();
let mut config = NpmConfig {
strict_ssl: true,
..Default::default()
};
config.apply(load_npmrc_entries_with_home(
Some(home.path()),
None,
dir.path(),
None,
));
assert_eq!(config.https_proxy.as_deref(), Some("http://a"));
assert_eq!(config.http_proxy.as_deref(), Some("http://b"));
assert_eq!(config.no_proxy.as_deref(), Some("foo"));
assert!(!config.strict_ssl);
assert_eq!(
config.local_address,
Some("::1".parse::<std::net::IpAddr>().unwrap())
);
}
#[test]
fn test_invalid_proxy_values_dropped() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join(".npmrc"),
"local-address=not-an-ip\nmaxsockets=zero\nstrict-ssl=perhaps\n",
)
.unwrap();
let home = tempfile::tempdir().unwrap();
let mut config = NpmConfig {
strict_ssl: true,
..Default::default()
};
config.apply(load_npmrc_entries_with_home(
Some(home.path()),
None,
dir.path(),
None,
));
assert!(config.local_address.is_none());
assert!(config.max_sockets.is_none());
assert!(config.strict_ssl);
}
#[test]
fn test_load_npmrc_entries_orders_user_before_project() {
let home_dir = tempfile::tempdir().unwrap();
let proj_dir = tempfile::tempdir().unwrap();
std::fs::write(
home_dir.path().join(".npmrc"),
"auto-install-peers=true\nfoo=user-only\n",
)
.unwrap();
std::fs::write(
proj_dir.path().join(".npmrc"),
"auto-install-peers=false\nbar=project-only\n",
)
.unwrap();
let entries = load_npmrc_entries_with_home(Some(home_dir.path()), None, proj_dir.path(), None);
assert!(entries.iter().any(|(k, v)| k == "foo" && v == "user-only"));
assert!(
entries
.iter()
.any(|(k, v)| k == "bar" && v == "project-only")
);
let positions: Vec<_> = entries
.iter()
.filter(|(k, _)| k == "auto-install-peers")
.map(|(_, v)| v.as_str())
.collect();
assert_eq!(
positions.len(),
2,
"expected both user and project entries for shared key: {entries:?}"
);
assert_eq!(
positions[0], "true",
"user entry must come first (precedence is last-write-wins downstream)"
);
assert_eq!(
positions[1], "false",
"project entry must come second so it overrides the user entry"
);
}
#[test]
fn pnpm_global_auth_ini_loads_and_overrides_user_rc() {
let home_dir = tempfile::tempdir().unwrap();
let proj_dir = tempfile::tempdir().unwrap();
std::fs::write(
home_dir.path().join(".npmrc"),
"//registry.example.com/:_authToken=stale-npmrc\n",
)
.unwrap();
let auth_ini = home_dir.path().join(".config/pnpm/auth.ini");
std::fs::create_dir_all(auth_ini.parent().unwrap()).unwrap();
std::fs::write(
&auth_ini,
"//registry.example.com/:_authToken=fresh-auth-ini\n\
//other.example.com/:_authToken=other-token\n",
)
.unwrap();
let entries = load_npmrc_entries_with_home(Some(home_dir.path()), None, proj_dir.path(), None);
let mut cfg = NpmConfig::default();
cfg.apply(entries);
assert_eq!(
cfg.auth_token_for("https://registry.example.com/"),
Some("fresh-auth-ini"),
"auth.ini token should override stale ~/.npmrc token",
);
assert_eq!(
cfg.auth_token_for("https://other.example.com/"),
Some("other-token"),
"additional auth.ini entries should be picked up",
);
}
#[test]
fn pnpm_global_auth_ini_honors_xdg_config_home_override() {
let home_dir = tempfile::tempdir().unwrap();
let xdg_dir = tempfile::tempdir().unwrap();
let proj_dir = tempfile::tempdir().unwrap();
let auth_ini = xdg_dir.path().join("pnpm/auth.ini");
std::fs::create_dir_all(auth_ini.parent().unwrap()).unwrap();
std::fs::write(&auth_ini, "//registry.example.com/:_authToken=xdg-token\n").unwrap();
let decoy = home_dir.path().join(".config/pnpm/auth.ini");
std::fs::create_dir_all(decoy.parent().unwrap()).unwrap();
std::fs::write(&decoy, "//registry.example.com/:_authToken=decoy\n").unwrap();
let entries = load_npmrc_entries_with_home(
Some(home_dir.path()),
Some(xdg_dir.path()),
proj_dir.path(),
None,
);
let mut cfg = NpmConfig::default();
cfg.apply(entries);
assert_eq!(
cfg.auth_token_for("https://registry.example.com/"),
Some("xdg-token"),
);
}
#[test]
fn pnpm_global_auth_ini_loses_to_project_npmrc() {
let home_dir = tempfile::tempdir().unwrap();
let proj_dir = tempfile::tempdir().unwrap();
let auth_ini = home_dir.path().join(".config/pnpm/auth.ini");
std::fs::create_dir_all(auth_ini.parent().unwrap()).unwrap();
std::fs::write(
&auth_ini,
"//registry.example.com/:_authToken=global-auth-ini\n",
)
.unwrap();
std::fs::write(
proj_dir.path().join(".npmrc"),
"//registry.example.com/:_authToken=project-pin\n",
)
.unwrap();
let entries = load_npmrc_entries_with_home(Some(home_dir.path()), None, proj_dir.path(), None);
let mut cfg = NpmConfig::default();
cfg.apply(entries);
assert_eq!(
cfg.auth_token_for("https://registry.example.com/"),
Some("project-pin"),
);
}
#[test]
fn npmrc_auth_file_overrides_user_token() {
let home_dir = tempfile::tempdir().unwrap();
let proj_dir = tempfile::tempdir().unwrap();
let auth_file = proj_dir.path().join("auth.npmrc");
std::fs::write(
home_dir.path().join(".npmrc"),
"//registry.example.com/:_authToken=stale-user-token\n",
)
.unwrap();
std::fs::write(
&auth_file,
"//registry.example.com/:_authToken=fresh-from-auth-file\n",
)
.unwrap();
std::fs::write(
proj_dir.path().join(".npmrc"),
format!("npmrc-auth-file={}\n", auth_file.display()),
)
.unwrap();
let entries = load_npmrc_entries_with_home(Some(home_dir.path()), None, proj_dir.path(), None);
let mut cfg = NpmConfig::default();
cfg.apply(entries);
assert_eq!(
cfg.auth_token_for("https://registry.example.com/"),
Some("fresh-from-auth-file"),
);
}
#[test]
fn npmrc_auth_file_resolves_relative_to_project_root() {
let home_dir = tempfile::tempdir().unwrap();
let proj_dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(proj_dir.path().join("secrets")).unwrap();
std::fs::write(
proj_dir.path().join("secrets/npm"),
"//registry.example.com/:_authToken=relative-path-token\n",
)
.unwrap();
std::fs::write(
proj_dir.path().join(".npmrc"),
"npmrc-auth-file=secrets/npm\n",
)
.unwrap();
let entries = load_npmrc_entries_with_home(Some(home_dir.path()), None, proj_dir.path(), None);
assert!(
entries
.iter()
.any(|(k, v)| k == "//registry.example.com/:_authToken" && v == "relative-path-token"),
"auth file entries missing — got {entries:?}",
);
}
#[test]
fn npmrc_auth_file_camel_case_alias_works() {
let home_dir = tempfile::tempdir().unwrap();
let proj_dir = tempfile::tempdir().unwrap();
let auth_file = proj_dir.path().join("auth.npmrc");
std::fs::write(
&auth_file,
"//registry.example.com/:_authToken=camel-token\n",
)
.unwrap();
std::fs::write(
proj_dir.path().join(".npmrc"),
format!("npmrcAuthFile={}\n", auth_file.display()),
)
.unwrap();
let entries = load_npmrc_entries_with_home(Some(home_dir.path()), None, proj_dir.path(), None);
assert!(
entries
.iter()
.any(|(k, v)| k == "//registry.example.com/:_authToken" && v == "camel-token"),
"camelCase alias did not load auth file — got {entries:?}",
);
}
#[test]
fn npmrc_auth_file_expands_tilde_against_home() {
let home_dir = tempfile::tempdir().unwrap();
let proj_dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(home_dir.path().join("secrets")).unwrap();
std::fs::write(
home_dir.path().join("secrets/npm"),
"//registry.example.com/:_authToken=tilde-token\n",
)
.unwrap();
std::fs::write(
proj_dir.path().join(".npmrc"),
"npmrc-auth-file=~/secrets/npm\n",
)
.unwrap();
let entries = load_npmrc_entries_with_home(Some(home_dir.path()), None, proj_dir.path(), None);
assert!(
entries
.iter()
.any(|(k, v)| k == "//registry.example.com/:_authToken" && v == "tilde-token"),
"tilde expansion failed — got {entries:?}",
);
}
#[test]
fn userconfig_override_replaces_default_user_npmrc() {
let home_dir = tempfile::tempdir().unwrap();
let proj_dir = tempfile::tempdir().unwrap();
let override_dir = tempfile::tempdir().unwrap();
let override_rc = override_dir.path().join("npmrc");
std::fs::write(
home_dir.path().join(".npmrc"),
"registry=https://decoy.example/\n",
)
.unwrap();
std::fs::write(&override_rc, "registry=https://override.example/\n").unwrap();
let entries = load_npmrc_entries_with_home(
Some(home_dir.path()),
None,
proj_dir.path(),
Some(&override_rc),
);
assert!(
entries
.iter()
.any(|(k, v)| k == "registry" && v == "https://override.example/"),
"override file was not loaded — got {entries:?}",
);
assert!(
!entries.iter().any(|(_, v)| v == "https://decoy.example/"),
"default ~/.npmrc must be skipped when override is set — got {entries:?}",
);
}
#[test]
fn expand_userconfig_path_handles_tilde_absolute_and_empty() {
let home = PathBuf::from("/fake/home");
assert_eq!(
expand_userconfig_path("~/config/npm/npmrc", Some(&home)),
Some(PathBuf::from("/fake/home/config/npm/npmrc"))
);
assert_eq!(
expand_userconfig_path("~", Some(&home)),
Some(PathBuf::from("/fake/home"))
);
assert_eq!(
expand_userconfig_path("/etc/npmrc", Some(&home)),
Some(PathBuf::from("/etc/npmrc"))
);
assert_eq!(expand_userconfig_path("~/x", None), None);
assert_eq!(expand_userconfig_path("", Some(&home)), None);
assert_eq!(expand_userconfig_path(" ", Some(&home)), None);
}
#[test]
fn userconfig_override_from_env_prefers_screaming_casing() {
let home = PathBuf::from("/h");
let upper = vec![(
"NPM_CONFIG_USERCONFIG".to_string(),
"/tmp/upper-rc".to_string(),
)];
assert_eq!(
userconfig_override_from_env(&upper, Some(&home)),
Some(PathBuf::from("/tmp/upper-rc"))
);
let lower = vec![(
"npm_config_userconfig".to_string(),
"/tmp/lower-rc".to_string(),
)];
assert_eq!(
userconfig_override_from_env(&lower, Some(&home)),
Some(PathBuf::from("/tmp/lower-rc"))
);
let upper_first = vec![
(
"NPM_CONFIG_USERCONFIG".to_string(),
"/tmp/upper".to_string(),
),
(
"npm_config_userconfig".to_string(),
"/tmp/lower".to_string(),
),
];
assert_eq!(
userconfig_override_from_env(&upper_first, Some(&home)),
Some(PathBuf::from("/tmp/upper")),
);
let lower_first = vec![
(
"npm_config_userconfig".to_string(),
"/tmp/lower".to_string(),
),
(
"NPM_CONFIG_USERCONFIG".to_string(),
"/tmp/upper".to_string(),
),
];
assert_eq!(
userconfig_override_from_env(&lower_first, Some(&home)),
Some(PathBuf::from("/tmp/upper")),
"SCREAMING form must win regardless of slice position",
);
let none_case = vec![("HOME".to_string(), "/h".to_string())];
assert_eq!(userconfig_override_from_env(&none_case, Some(&home)), None);
}
#[test]
fn load_with_env_honors_npm_config_userconfig() {
let proj_dir = tempfile::tempdir().unwrap();
let override_dir = tempfile::tempdir().unwrap();
let override_rc = override_dir.path().join("custom-npmrc");
std::fs::write(
&override_rc,
"//userconfig-test.example/:_authToken=from-userconfig-file\n",
)
.unwrap();
let env = vec![(
"NPM_CONFIG_USERCONFIG".to_string(),
override_rc.display().to_string(),
)];
let config = NpmConfig::load_with_env(proj_dir.path(), &env);
assert_eq!(
config.auth_token_for("https://userconfig-test.example/"),
Some("from-userconfig-file"),
);
}
#[test]
fn fetch_policy_default_matches_settings_toml_declared_defaults() {
let p = FetchPolicy::default();
assert_eq!(p.timeout_ms, 300_000);
assert_eq!(p.retries, 2);
assert_eq!(p.retry_factor, 10);
assert_eq!(p.retry_min_timeout_ms, 10_000);
assert_eq!(p.retry_max_timeout_ms, 60_000);
}
#[test]
fn fetch_policy_backoff_sequence_matches_make_fetch_happen() {
let p = FetchPolicy::default();
assert_eq!(
p.backoff_for_attempt(1),
std::time::Duration::from_millis(10_000)
);
assert_eq!(
p.backoff_for_attempt(2),
std::time::Duration::from_millis(60_000)
);
assert_eq!(
p.backoff_for_attempt(3),
std::time::Duration::from_millis(60_000)
);
}
#[test]
fn fetch_policy_backoff_clamps_on_huge_factor() {
let p = FetchPolicy {
timeout_ms: 60_000,
retries: 5,
retry_factor: u32::MAX,
retry_min_timeout_ms: 100,
retry_max_timeout_ms: 5_000,
..FetchPolicy::default()
};
assert_eq!(
p.backoff_for_attempt(1),
std::time::Duration::from_millis(100),
"first attempt is the min (no multiplier applied yet)",
);
assert_eq!(
p.backoff_for_attempt(2),
std::time::Duration::from_millis(5_000),
);
assert_eq!(
p.backoff_for_attempt(10),
std::time::Duration::from_millis(5_000),
"deep retries still clamp; no overflow panic",
);
}
#[test]
fn fetch_policy_from_ctx_reads_npmrc_overrides() {
let entries = vec![
("fetch-timeout".to_string(), "1234".to_string()),
("fetch-retries".to_string(), "5".to_string()),
("fetch-retry-factor".to_string(), "3".to_string()),
("fetch-retry-mintimeout".to_string(), "250".to_string()),
("fetch-retry-maxtimeout".to_string(), "9_999".to_string()),
];
let ws: std::collections::BTreeMap<String, yaml_serde::Value> =
std::collections::BTreeMap::new();
let ctx = aube_settings::ResolveCtx::files_only(&entries, &ws);
let p = FetchPolicy::from_ctx(&ctx);
assert_eq!(p.timeout_ms, 1234);
assert_eq!(p.retries, 5);
assert_eq!(p.retry_factor, 3);
assert_eq!(p.retry_min_timeout_ms, 250);
assert_eq!(p.retry_max_timeout_ms, 60_000);
}
#[test]
fn fetch_policy_from_ctx_reads_warn_timeout_and_min_speed() {
let entries = vec![
("fetchWarnTimeoutMs".to_string(), "500".to_string()),
("fetchMinSpeedKiBps".to_string(), "123".to_string()),
];
let ws: std::collections::BTreeMap<String, yaml_serde::Value> =
std::collections::BTreeMap::new();
let ctx = aube_settings::ResolveCtx::files_only(&entries, &ws);
let p = FetchPolicy::from_ctx(&ctx);
assert_eq!(p.warn_timeout_ms, 500);
assert_eq!(p.min_speed_kibps, 123);
}
#[test]
fn fetch_policy_default_includes_observability_thresholds() {
let p = FetchPolicy::default();
assert_eq!(p.warn_timeout_ms, 10_000);
assert_eq!(p.min_speed_kibps, 50);
}
#[test]
fn translate_npm_config_env_maps_default_registry() {
assert_eq!(
translate_npm_config_env("NPM_CONFIG_REGISTRY", "https://r.example/"),
Some(("registry".to_string(), "https://r.example/".to_string()))
);
assert_eq!(
translate_npm_config_env("npm_config_registry", "https://r.example/"),
Some(("registry".to_string(), "https://r.example/".to_string()))
);
assert_eq!(translate_npm_config_env("HOME", "/tmp"), None);
}
#[test]
fn translate_npm_config_env_maps_proxy_and_tls_knobs() {
let cases = [
("NPM_CONFIG_HTTPS_PROXY", "http://p:8", "https-proxy"),
("NPM_CONFIG_HTTP_PROXY", "http://p:9", "http-proxy"),
("NPM_CONFIG_PROXY", "http://p:0", "proxy"),
("NPM_CONFIG_NOPROXY", "localhost,.internal", "noproxy"),
("NPM_CONFIG_STRICT_SSL", "false", "strict-ssl"),
("NPM_CONFIG_LOCAL_ADDRESS", "127.0.0.1", "local-address"),
("NPM_CONFIG_MAXSOCKETS", "16", "maxsockets"),
];
for (name, value, expected_key) in cases {
assert_eq!(
translate_npm_config_env(name, value),
Some((expected_key.to_string(), value.to_string())),
"mapping failed for {name}"
);
}
}
#[test]
fn translate_npm_config_env_maps_scoped_registry() {
assert_eq!(
translate_npm_config_env("NPM_CONFIG_@MYORG:REGISTRY", "https://r.mycorp/"),
Some((
"@myorg:registry".to_string(),
"https://r.mycorp/".to_string()
))
);
assert_eq!(
translate_npm_config_env("npm_config_@myorg:registry", "https://r.mycorp/"),
Some((
"@myorg:registry".to_string(),
"https://r.mycorp/".to_string()
))
);
}
#[test]
fn translate_npm_config_env_passes_uri_auth_through_verbatim() {
assert_eq!(
translate_npm_config_env(
"NPM_CONFIG_//registry.example.com/:_authToken",
"secret-token"
),
Some((
"//registry.example.com/:_authToken".to_string(),
"secret-token".to_string()
))
);
}
#[test]
fn load_with_env_npm_config_registry_overrides_project_file() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join(".npmrc"),
"registry=https://file.registry.example/\n",
)
.unwrap();
let env = vec![(
"NPM_CONFIG_REGISTRY".to_string(),
"https://env.registry.example/".to_string(),
)];
let config = NpmConfig::load_with_env(dir.path(), &env);
assert_eq!(config.registry, "https://env.registry.example/");
}
#[test]
fn env_registry_overrides_project_npmrc() {
let mut config = NpmConfig {
registry: "https://registry.npmjs.org/".to_string(),
..Default::default()
};
config.apply(vec![(
"registry".to_string(),
"https://file.registry/".to_string(),
)]);
assert_eq!(config.registry, "https://file.registry/");
let env = translate_npm_config_env("NPM_CONFIG_REGISTRY", "https://env.registry/")
.map(|e| vec![e])
.unwrap_or_default();
config.apply(env);
assert_eq!(
config.registry, "https://env.registry/",
"env var must override file-based registry"
);
}
#[test]
fn is_public_npmjs_matches_canonical_and_normalised_urls() {
for url in [
"https://registry.npmjs.org/",
"https://registry.npmjs.org",
"https://Registry.NPMJS.org/",
"http://registry.npmjs.org/",
"//registry.npmjs.org/",
"HTTPS://registry.npmjs.org/",
"Http://registry.npmjs.org/",
] {
assert!(is_public_npmjs_url(url), "expected public for {url}");
}
}
#[test]
fn is_public_npmjs_rejects_private_and_mirror_urls() {
for url in [
"https://internal.example.com/",
"https://npm.pkg.github.com/",
"https://registry.yarnpkg.com/",
"https://example.com/registry.npmjs.org/",
"ftp://registry.npmjs.org/",
] {
assert!(!is_public_npmjs_url(url), "expected private for {url}");
}
}
#[test]
fn is_public_npmjs_does_not_panic_on_multibyte_char_at_prefix_boundary() {
assert!(!is_public_npmjs_url("https:/ñregistry.npmjs.org/"));
assert!(!is_public_npmjs_url("htñps://registry.npmjs.org/"));
}
#[test]
fn is_public_npmjs_via_npm_config_uses_scope_override() {
let mut cfg = NpmConfig {
registry: "https://registry.npmjs.org/".to_string(),
..Default::default()
};
cfg.scoped_registries.insert(
"@myorg".to_string(),
"https://npm.internal.example/".to_string(),
);
assert!(cfg.is_public_npmjs("lodash"));
assert!(!cfg.is_public_npmjs("@myorg/utils"));
}
#[test]
fn fetch_policy_clamps_giant_retry_counts_into_u32() {
let entries = vec![("fetch-retries".to_string(), "99999999999999".to_string())];
let ws: std::collections::BTreeMap<String, yaml_serde::Value> =
std::collections::BTreeMap::new();
let ctx = aube_settings::ResolveCtx::files_only(&entries, &ws);
let p = FetchPolicy::from_ctx(&ctx);
assert_eq!(p.retries, u32::MAX);
}