277 lines
9.8 KiB
Rust
277 lines
9.8 KiB
Rust
use super::Fixture;
|
|
use dever::compiler::project::dependency::library::npm::{self, Kind, Omission};
|
|
use serde_json::json;
|
|
use std::collections::BTreeMap;
|
|
|
|
#[test]
|
|
fn npm_scoped_metadata_uses_strict_official_url_policy() {
|
|
let mut fixture = Fixture::new();
|
|
fixture.package(
|
|
json!({"name":"app","version":"1.0.0","dependencies":{"@types/node":"^22"}}),
|
|
BTreeMap::new(),
|
|
);
|
|
fixture.package(
|
|
json!({"name":"@types/node","version":"22.10.0"}),
|
|
BTreeMap::new(),
|
|
);
|
|
fixture.resolve(&["npm:app@1.0.0"]).unwrap();
|
|
for address in [
|
|
"https://registry.npmjs.org/@types/node",
|
|
"https://registry.npmjs.org/@types/node/22.10.0",
|
|
"https://registry.npmjs.org/@types/node/-/archive-22.10.0.tgz",
|
|
] {
|
|
assert!(fixture.calls.borrow().iter().any(|url| url == address));
|
|
}
|
|
let graph = fixture.graph();
|
|
let node = graph
|
|
.instances
|
|
.iter()
|
|
.find(|node| node.spec.name == "@types/node")
|
|
.unwrap();
|
|
let files = fixture.install().unwrap();
|
|
assert_eq!(
|
|
files[&format!("{}/index.js", node.path)],
|
|
b"module.exports = 42;"
|
|
);
|
|
let lib = fixture
|
|
.lock
|
|
.libs
|
|
.iter()
|
|
.find(|lib| lib.spec.name == "@types/node")
|
|
.unwrap();
|
|
let mut source = lib.artifacts[0].source.clone().unwrap();
|
|
for locator in [
|
|
"https://registry.npmjs.org/@types%2fnode/-/archive-22.10.0.tgz",
|
|
"https://registry.npmjs.org/@types%2Fnode/-/archive-22.10.0.tgz",
|
|
"https://registry.npmjs.org/@types/node/%2e%2e/archive-22.10.0.tgz",
|
|
] {
|
|
source.locator = locator.into();
|
|
assert!(source.validate(&lib.spec).is_err());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn npm_nested_instances_keep_peer_context_and_install_offline() {
|
|
let mut fixture = Fixture::new();
|
|
for document in [
|
|
json!({"name":"left","version":"1.0.0","dependencies":{"plugin":"1.0.0","engine":"1.0.0"}}),
|
|
json!({"name":"right","version":"1.0.0","dependencies":{"plugin":"1.0.0","engine":"2.0.0"}}),
|
|
json!({"name":"plugin","version":"1.0.0","peerDependencies":{"engine":">=1 <3"}}),
|
|
json!({"name":"engine","version":"1.0.0"}),
|
|
json!({"name":"engine","version":"2.0.0"}),
|
|
] {
|
|
fixture.package(document, BTreeMap::new());
|
|
}
|
|
fixture
|
|
.resolve(&["npm:left@1.0.0", "npm:right@1.0.0"])
|
|
.unwrap();
|
|
let graph = fixture.graph();
|
|
for root in ["left", "right"] {
|
|
let plugin = graph
|
|
.instances
|
|
.iter()
|
|
.find(|node| node.path == format!("node_modules/{root}/node_modules/plugin"))
|
|
.unwrap();
|
|
assert_eq!(plugin.edges[0].kind, Kind::Peer);
|
|
assert_eq!(
|
|
plugin.edges[0].target.as_deref(),
|
|
Some(format!("node_modules/{root}/node_modules/engine").as_str())
|
|
);
|
|
}
|
|
fixture.documents.clear();
|
|
let calls = fixture.calls.borrow().len();
|
|
let files = fixture.install().unwrap();
|
|
assert_eq!(files.len(), 12);
|
|
assert_eq!(fixture.calls.borrow().len(), calls);
|
|
assert_eq!(fixture.lock.libs.len(), 5);
|
|
}
|
|
|
|
#[test]
|
|
fn npm_peer_conflict_backtracks_versions_and_incompatible_roots_fail_atomically() {
|
|
let mut fixture = Fixture::new();
|
|
for document in [
|
|
json!({"name":"app","version":"1.0.0","dependencies":{"plugin":"*","engine":"1.0.0"}}),
|
|
json!({"name":"plugin","version":"2.0.0","peerDependencies":{"engine":"2.0.0"}}),
|
|
json!({"name":"plugin","version":"1.0.0","peerDependencies":{"engine":"1.0.0"}}),
|
|
json!({"name":"engine","version":"1.0.0"}),
|
|
json!({"name":"engine","version":"2.0.0"}),
|
|
] {
|
|
fixture.package(document, BTreeMap::new());
|
|
}
|
|
fixture.resolve(&["npm:app@1.0.0"]).unwrap();
|
|
assert_eq!(
|
|
fixture
|
|
.graph()
|
|
.instances
|
|
.iter()
|
|
.find(|node| node.spec.name == "plugin")
|
|
.unwrap()
|
|
.spec
|
|
.version,
|
|
"1.0.0"
|
|
);
|
|
let before = fixture.lock.encode().unwrap();
|
|
assert!(
|
|
fixture
|
|
.resolve(&["npm:engine@1.0.0", "npm:engine@2.0.0"])
|
|
.unwrap_err()
|
|
.contains("incompatible")
|
|
);
|
|
assert_eq!(fixture.lock.encode().unwrap(), before);
|
|
}
|
|
|
|
#[test]
|
|
fn npm_optional_target_and_absent_peer_are_explicit_but_integrity_errors_are_fatal() {
|
|
let mut fixture = Fixture::new();
|
|
fixture.package(json!({"name":"app","version":"1.0.0","optionalDependencies":{"wrong":"*","missing":"*"},"peerDependencies":{"peer":"*"},"peerDependenciesMeta":{"peer":{"optional":true}}}),BTreeMap::new());
|
|
fixture.package(
|
|
json!({"name":"wrong","version":"1.0.0","os":["!linux"]}),
|
|
BTreeMap::new(),
|
|
);
|
|
fixture.resolve(&["npm:app@1.0.0"]).unwrap();
|
|
let graph = fixture.graph();
|
|
let edges = &graph.instances[0].edges;
|
|
for (name, reason) in [
|
|
("wrong", Omission::Target),
|
|
("missing", Omission::Unavailable),
|
|
("peer", Omission::OptionalPeerAbsent),
|
|
] {
|
|
assert_eq!(
|
|
edges
|
|
.iter()
|
|
.find(|edge| edge.name == name)
|
|
.unwrap()
|
|
.omission,
|
|
Some(reason)
|
|
);
|
|
}
|
|
fixture.install().unwrap();
|
|
let before = fixture.lock.encode().unwrap();
|
|
assert!(
|
|
fixture
|
|
.resolve(&["npm:missing@1.0.0"])
|
|
.unwrap_err()
|
|
.contains("no npm version")
|
|
);
|
|
assert_eq!(fixture.lock.encode().unwrap(), before);
|
|
fixture.documents.insert(
|
|
"https://registry.npmjs.org/wrong/-/archive-1.0.0.tgz".into(),
|
|
b"corrupt optional archive".to_vec(),
|
|
);
|
|
assert!(
|
|
fixture
|
|
.resolve(&["npm:app@1.0.0"])
|
|
.unwrap_err()
|
|
.contains("integrity")
|
|
);
|
|
assert_eq!(fixture.lock.encode().unwrap(), before);
|
|
}
|
|
|
|
#[test]
|
|
fn npm_one_version_cannot_change_bytes_between_worker_groups() {
|
|
let mut fixture = Fixture::new();
|
|
let metadata = json!({"name":"shared","version":"1.0.0"});
|
|
fixture.package(metadata.clone(), BTreeMap::new());
|
|
fixture.resolve(&["npm:shared@1.0.0"]).unwrap();
|
|
let before = fixture.lock.encode().unwrap();
|
|
fixture.package(
|
|
metadata,
|
|
BTreeMap::from([("package/changed.js".into(), b"different bytes".to_vec())]),
|
|
);
|
|
let error = fixture.resolve(&["npm:shared@1.0.0"]).unwrap_err();
|
|
assert!(error.contains("artifact differs"), "{error}");
|
|
assert_eq!(fixture.lock.encode().unwrap(), before);
|
|
}
|
|
|
|
#[test]
|
|
fn npm_bundled_packages_have_one_owner_and_tampered_manifest_or_graph_fails() {
|
|
let mut fixture = Fixture::new();
|
|
fixture.package(json!({"name":"app","version":"1.0.0","dependencies":{"tiny":"1.0.0"},"bundleDependencies":["tiny"]}),BTreeMap::from([
|
|
("package/node_modules/tiny/package.json".into(),serde_json::to_vec(&json!({"name":"tiny","version":"1.0.0"})).unwrap()),
|
|
("package/node_modules/tiny/index.js".into(),b"module.exports=7".to_vec()),
|
|
]));
|
|
fixture.resolve(&["npm:app@1.0.0"]).unwrap();
|
|
assert_eq!(fixture.lock.libs.len(), 1);
|
|
assert_eq!(fixture.install().unwrap().len(), 4);
|
|
let original = fixture.lock.npm[0].clone();
|
|
fixture.lock.npm[0]["instances"][0]["edges"][0]["requirement"] = json!("2.0.0");
|
|
assert!(fixture.install().unwrap_err().contains("range"));
|
|
fixture.lock.npm[0] = original;
|
|
let mut graph = fixture.graph();
|
|
graph.instances[1].source.prefix = "package/".into();
|
|
assert!(
|
|
npm::validate(&graph, &fixture.lock)
|
|
.unwrap_err()
|
|
.contains("owned")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn npm_registry_and_archive_metadata_must_agree_and_urls_are_official() {
|
|
let mut fixture = Fixture::new();
|
|
fixture.package(json!({"name":"app","version":"1.0.0"}), BTreeMap::new());
|
|
let address = "https://registry.npmjs.org/app/1.0.0";
|
|
let original = fixture.documents[address].clone();
|
|
let mut metadata: serde_json::Value = serde_json::from_slice(&original).unwrap();
|
|
metadata["dependencies"] = json!({"hidden":"1.0.0"});
|
|
fixture
|
|
.documents
|
|
.insert(address.into(), serde_json::to_vec(&metadata).unwrap());
|
|
assert!(
|
|
fixture
|
|
.resolve(&["npm:app@1.0.0"])
|
|
.unwrap_err()
|
|
.contains("metadata")
|
|
);
|
|
let mut metadata: serde_json::Value = serde_json::from_slice(&original).unwrap();
|
|
metadata["dist"]["tarball"] = json!("https://attacker.invalid/app.tgz");
|
|
fixture
|
|
.documents
|
|
.insert(address.into(), serde_json::to_vec(&metadata).unwrap());
|
|
assert!(
|
|
fixture
|
|
.resolve(&["npm:app@1.0.0"])
|
|
.unwrap_err()
|
|
.contains("official")
|
|
);
|
|
assert!(
|
|
!fixture
|
|
.calls
|
|
.borrow()
|
|
.iter()
|
|
.any(|url| url.contains("attacker"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn npm_cycles_close_without_infinite_nested_copies_and_missing_build_receipt_fails() {
|
|
let mut fixture = Fixture::new();
|
|
fixture.package(
|
|
json!({"name":"alpha","version":"1.0.0","dependencies":{"beta":"1.0.0"}}),
|
|
BTreeMap::new(),
|
|
);
|
|
fixture.package(
|
|
json!({"name":"beta","version":"1.0.0","dependencies":{"alpha":"1.0.0"}}),
|
|
BTreeMap::new(),
|
|
);
|
|
fixture.resolve(&["npm:alpha@1.0.0"]).unwrap();
|
|
assert_eq!(fixture.graph().instances.len(), 2);
|
|
assert_eq!(fixture.install().unwrap().len(), 4);
|
|
fixture.package(
|
|
json!({"name":"hook","version":"1.0.0","scripts":{"install":"must not run"}}),
|
|
BTreeMap::new(),
|
|
);
|
|
fixture.resolve(&["npm:hook@1.0.0"]).unwrap();
|
|
let graph = fixture
|
|
.lock
|
|
.npm
|
|
.iter()
|
|
.find(|value| value["roots"][0]["name"] == "hook")
|
|
.unwrap()
|
|
.clone();
|
|
fixture.lock.npm = vec![graph];
|
|
assert!(fixture.install().unwrap_err().contains("lifecycle"));
|
|
fixture.lock.npm[0]["build"] = json!("0".repeat(64));
|
|
assert!(fixture.install().unwrap_err().contains("receipt"));
|
|
}
|