#[path = "common/registry.rs"] mod fixture; use avril_runtime::{ caps::{Capabilities, Category, Initial}, registry::{ PackageStore, npm::resolution::{NpmResolver, NpmRootRequest, ResolutionMode}, }, }; use fixture::Registry; use futures::executor::block_on; use std::collections::BTreeMap; use std::sync::Arc; #[test] fn frozen_replay_accepts_the_canonical_locked_graph() { let server = Registry::new(); server.publish_package("util", "1.0.0", serde_json::json!({}), &[]); server.publish_package( "app", "1.0.0", serde_json::json!({"dependencies": {"util": "^1"}}), &[], ); let root = tempfile::tempdir().unwrap(); let app = NpmRootRequest { request: "npm:app@1".into(), context: BTreeMap::new(), }; let warm = server.resolver(root.path()); let expected = block_on(warm.prepare(std::slice::from_ref(&app))).unwrap(); warm.store().save().unwrap(); let frozen = server.frozen_resolver(root.path()); let lock_before = std::fs::read(root.path().join("avril.lock")).unwrap(); let replay = block_on(frozen.prepare(&[app])).unwrap(); assert_eq!(replay.roots, expected.roots); frozen.store().save().unwrap(); assert_eq!( std::fs::read(root.path().join("avril.lock")).unwrap(), lock_before ); } #[test] fn locked_roots_restore_the_recorded_peer_context() { let server = Registry::new(); for version in ["1.0.0", "1.1.0"] { server.publish_package("host", version, serde_json::json!({}), &[]); } server.publish_package( "plugin", "1.0.0", serde_json::json!({"peerDependencies": {"host": "^1"}}), &[], ); let root = tempfile::tempdir().unwrap(); let request = |version: &str| NpmRootRequest { request: "npm:plugin@1".into(), context: BTreeMap::from([("host".into(), format!("{}|host|{version}", server.url))]), }; let warm = server.resolver(root.path()); let first = block_on(warm.prepare(&[request("1.0.0")])).unwrap(); let second = block_on(warm.prepare(&[request("1.1.0")])).unwrap(); assert_ne!(first.roots.values().next(), second.roots.values().next()); warm.store().save().unwrap(); let valid = server.frozen_resolver(root.path()); block_on(valid.prepare(&[request("1.0.0")])).unwrap(); let mut lock = fixture::read_lock(root.path()); let first_key = first.roots.keys().next().unwrap(); let second_instance = second.roots.values().next().unwrap(); lock["npm"]["roots"][first_key] = serde_json::json!(second_instance); std::fs::write(root.path().join("avril.lock"), lock.to_string()).unwrap(); let cold = server.frozen_resolver(root.path()); let replay = block_on(cold.prepare(&[request("1.0.0")])).unwrap(); assert_eq!(replay.roots[first_key], *second_instance); let plugin = block_on(cold.resolve_request("npm:plugin@1", ResolutionMode::Import)).unwrap(); let host = cold .resolve_from_file("host", &plugin.path, ResolutionMode::Import) .unwrap(); assert!(host.path.to_string_lossy().contains("/host/1.1.0/")); } #[test] fn locked_dependency_edges_override_request_selections() { let server = Registry::new(); server.publish_package("util", "1.0.0", serde_json::json!({}), &[]); server.publish_package( "app", "1.0.0", serde_json::json!({"dependencies": {"util": "^1"}, "imports": {"#renamed": "renamed"}}), &[], ); let root = tempfile::tempdir().unwrap(); let app = || NpmRootRequest { request: "npm:app@1".into(), context: BTreeMap::new(), }; let warm = server.resolver(root.path()); block_on(warm.prepare(&[app()])).unwrap(); warm.store().save().unwrap(); server.publish_package("util", "2.0.0", serde_json::json!({}), &[]); let expanded = server.resolver(root.path()); let newer = block_on(expanded.prepare(&[NpmRootRequest { request: "npm:util@2.0.0".into(), context: BTreeMap::new(), }])) .unwrap(); expanded.store().save().unwrap(); let valid = server.frozen_resolver(root.path()); block_on(valid.prepare(&[app()])).unwrap(); let mut lock = fixture::read_lock(root.path()); assert_eq!(lock["npm"]["specifiers"]["npm:util@^1"], "1.0.0"); let instances = lock["npm"]["instances"].as_object_mut().unwrap(); let app_pin = instances .values_mut() .find(|pin| pin["package"].as_str().unwrap().ends_with("|app|1.0.0")) .unwrap(); app_pin["deps"]["util"] = serde_json::json!(newer.roots.values().next().unwrap()); app_pin["deps"]["renamed"] = app_pin["deps"]["util"].clone(); std::fs::write(root.path().join("avril.lock"), lock.to_string()).unwrap(); let cold = server.frozen_resolver(root.path()); block_on(cold.prepare(&[app()])).unwrap(); let app = block_on(cold.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap(); let util = cold .resolve_from_file("util", &app.path, ResolutionMode::Import) .unwrap(); assert!(util.path.to_string_lossy().contains("/util/2.0.0/")); for alias in ["renamed", "#renamed"] { assert_eq!( cold.resolve_from_file(alias, &app.path, ResolutionMode::Import) .unwrap() .path, util.path ); } } #[test] fn replay_loads_only_root_metadata_until_a_dependency_is_visited() { let server = Registry::new(); server.publish_package("util", "1.0.0", serde_json::json!({}), &[]); server.publish_package( "app", "1.0.0", serde_json::json!({"dependencies": {"util": "^1"}}), &[("index.js", "export default 1;")], ); let root = tempfile::tempdir().unwrap(); let app = NpmRootRequest { request: "npm:app@1".into(), context: BTreeMap::new(), }; let warm = server.resolver(root.path()); block_on(warm.prepare(std::slice::from_ref(&app))).unwrap(); warm.store().save().unwrap(); let app_module = block_on(warm.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap(); let util = warm .resolve_from_file("util", &app_module.path, ResolutionMode::Import) .unwrap(); std::fs::write(util.path.with_file_name("package.json"), "not json").unwrap(); let frozen = server.frozen_resolver(root.path()); block_on(frozen.prepare(&[app])).unwrap(); let app_module = block_on(frozen.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap(); assert_eq!( frozen.npm_source(&app_module.path).unwrap(), "export default 1;" ); let error = frozen .resolve_from_file("util", &app_module.path, ResolutionMode::Import) .unwrap_err() .to_string(); assert!(error.contains("package.json"), "{error}"); assert!( frozen .audit() .unwrap_err() .to_string() .contains("package.json") ); } #[test] fn locked_graph_can_omit_a_manifest_dependency() { let server = Registry::new(); server.publish_package("util", "1.0.0", serde_json::json!({}), &[]); server.publish_package( "app", "1.0.0", serde_json::json!({"dependencies": {"util": "^1"}}), &[], ); let root = tempfile::tempdir().unwrap(); let warm = server.resolver(root.path()); block_on(warm.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap(); warm.store().save().unwrap(); let mut lock = fixture::read_lock(root.path()); let app_pin = lock["npm"]["instances"] .as_object_mut() .unwrap() .values_mut() .find(|pin| pin["package"].as_str().unwrap().ends_with("|app|1.0.0")) .unwrap(); app_pin["deps"].as_object_mut().unwrap().remove("util"); std::fs::write(root.path().join("avril.lock"), lock.to_string()).unwrap(); let frozen = server.frozen_resolver(root.path()); let app = block_on(frozen.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap(); let error = frozen .resolve_from_file("util", &app.path, ResolutionMode::Import) .unwrap_err(); assert!( error.to_string().contains("not a declared dependency"), "{error}" ); } #[test] fn frozen_replay_restores_missing_storage_before_sync_require() { let server = Registry::new(); server.publish_package( "util", "1.0.0", serde_json::json!({}), &[("index.js", "export default 1;")], ); server.publish_package( "app", "1.0.0", serde_json::json!({"dependencies": {"util": "^1"}}), &[], ); let root = tempfile::tempdir().unwrap(); let warm = server.resolver(root.path()); let app = block_on(warm.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap(); let util = warm .resolve_from_file("util", &app.path, ResolutionMode::Import) .unwrap(); warm.store().save().unwrap(); assert!( fixture::read_lock(root.path())["npm"]["specifiers"] .get("npm:util@1.0.0") .is_none() ); let before = std::fs::read(root.path().join("avril.lock")).unwrap(); let requests = server.requests.lock().unwrap().len(); std::fs::remove_dir_all(util.path.parent().unwrap()).unwrap(); let frozen = server.frozen_resolver(root.path()); let app = block_on(frozen.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap(); let util = frozen .resolve_from_file("util", &app.path, ResolutionMode::Require) .unwrap(); assert_eq!(frozen.npm_source(&util.path).unwrap(), "export default 1;"); frozen.store().save().unwrap(); assert_eq!( std::fs::read(root.path().join("avril.lock")).unwrap(), before ); assert_eq!(server.requests.lock().unwrap().len(), requests); } #[test] fn frozen_replay_restores_backing_storage_for_existing_peer_instances() { let server = Registry::new(); server.publish_package( "app", "1.0.0", serde_json::json!({"dependencies": {"app": "1.0.0"}}), &[("index.js", "export default 1;")], ); let root = tempfile::tempdir().unwrap(); let warm = server.resolver(root.path()); let app = block_on(warm.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap(); let backing = block_on(warm.store().npm_acquire("npm:app@1")).unwrap(); assert_ne!(app.path.parent().unwrap(), backing.dir); warm.store().save().unwrap(); std::fs::remove_dir_all(&backing.dir).unwrap(); let frozen = server.frozen_resolver(root.path()); let replay = block_on(frozen.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap(); assert_eq!(replay.path, app.path); assert_eq!( frozen.npm_source(&replay.path).unwrap(), "export default 1;" ); frozen.audit().unwrap(); } #[test] fn frozen_replay_requires_the_recorded_contextual_root() { let server = Registry::new(); server.publish_package("app", "1.0.0", serde_json::json!({}), &[]); let root = tempfile::tempdir().unwrap(); let warm = server.resolver(root.path()); block_on(warm.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap(); warm.store().save().unwrap(); let mut lock = fixture::read_lock(root.path()); lock["npm"]["roots"] = serde_json::json!({}); std::fs::write(root.path().join("avril.lock"), lock.to_string()).unwrap(); let frozen = server.frozen_resolver(root.path()); let error = block_on(frozen.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap_err(); assert!( error .to_string() .contains("Frozen lockfile is missing npm:app@1"), "{error}" ); } #[test] fn replay_preserves_different_root_versions_of_the_same_package() { let server = Registry::new(); for version in ["1.0.0", "2.0.0"] { server.publish_package("app", version, serde_json::json!({}), &[]); } let root = tempfile::tempdir().unwrap(); let requests: Vec<_> = ["npm:app@1", "npm:app@2"] .into_iter() .map(|request| NpmRootRequest { request: request.to_owned(), context: BTreeMap::new(), }) .collect(); let warm = server.resolver(root.path()); let expected = block_on(warm.prepare(&requests)).unwrap(); warm.store().save().unwrap(); let frozen = server.frozen_resolver(root.path()); assert_eq!( block_on(frozen.prepare(&requests)).unwrap().roots, expected.roots ); for (request, version) in [("npm:app@1", "1.0.0"), ("npm:app@2", "2.0.0")] { let module = block_on(frozen.resolve_request(request, ResolutionMode::Import)).unwrap(); assert_eq!( frozen.describe_instance(&module.instance).unwrap().version, version ); } } #[test] fn warm_replay_rejects_foreign_archive_provenance() { let server = Registry::new(); server.publish_package("app", "1.0.0", serde_json::json!({}), &[]); let root = tempfile::tempdir().unwrap(); let warm = server.resolver(root.path()); block_on(warm.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap(); warm.store().save().unwrap(); let mut lock = fixture::read_lock(root.path()); let pin = lock["npm"]["packages"] .as_object_mut() .unwrap() .values_mut() .next() .unwrap(); pin["tarball"] = serde_json::json!("https://unrelated.invalid/app.tgz"); std::fs::write(root.path().join("avril.lock"), lock.to_string()).unwrap(); let error = PackageStore::new(fixture::runtime_handle(), server.options(root.path())) .err() .expect("foreign archive provenance must fail"); assert!( matches!( error, avril_runtime::registry::RegistryError::Npm( avril_runtime::registry::npm::NpmError::NpmTarballEscapesRegistry { .. } ) ), "{error}" ); } #[test] fn frozen_replay_rechecks_live_source_authority() { let server = Registry::new(); server.publish_package("guarded", "1.0.0", serde_json::json!({}), &[]); let root = tempfile::tempdir().unwrap(); let version_url = format!("{}guarded/1.0.0/", server.url); let mut initial = Initial::new(false); initial .grant(Category::JsExec, false, &version_url, root.path()) .unwrap(); let capabilities = Capabilities::new(initial, root.path().to_owned()); let store = PackageStore::new(fixture::runtime_handle(), server.options(root.path())) .unwrap() .with_capabilities(capabilities.clone()); let warm = NpmResolver::new(Arc::new(store)); let request = NpmRootRequest { request: "npm:guarded@1".into(), context: BTreeMap::new(), }; block_on(warm.prepare(std::slice::from_ref(&request))).unwrap(); warm.store().save().unwrap(); capabilities .pledge(vec![(Category::JsExec, None, vec![version_url])]) .unwrap(); let mut options = server.options(root.path()); options.frozen = true; options.offline = true; let replay = NpmResolver::new(Arc::new( PackageStore::new(fixture::runtime_handle(), options) .unwrap() .with_capabilities(capabilities), )); let error = block_on(replay.prepare(&[request])) .unwrap_err() .to_string(); assert!(error.contains("PermissionDenied"), "{error}"); } #[test] fn frozen_replay_rejects_invalid_graph_structure() { let server = Registry::new(); server.publish_package("util", "1.0.0", serde_json::json!({}), &[]); server.publish_package( "app", "1.0.0", serde_json::json!({"dependencies": {"util": "^1"}}), &[], ); let root = tempfile::tempdir().unwrap(); let app = NpmRootRequest { request: "npm:app@1".into(), context: BTreeMap::new(), }; let warm = server.resolver(root.path()); block_on(warm.prepare(std::slice::from_ref(&app))).unwrap(); warm.store().save().unwrap(); let pristine = fixture::read_lock(root.path()); let mut missing_instance = pristine.clone(); let instances = missing_instance["npm"]["instances"] .as_object_mut() .unwrap(); let dependency_instance = instances .iter() .find(|(_, pin)| pin["package"].as_str().unwrap().ends_with("|util|1.0.0")) .map(|(id, _)| id.clone()) .unwrap(); instances.remove(&dependency_instance); let mut missing_edge = pristine.clone(); let app_pin = missing_edge["npm"]["instances"] .as_object_mut() .unwrap() .values_mut() .find(|pin| pin["package"].as_str().unwrap().ends_with("|app|1.0.0")) .unwrap(); app_pin["deps"]["util"] = serde_json::json!("p-0000000000000000"); let mut missing_package = pristine.clone(); let package = missing_package["npm"]["instances"] .as_object() .unwrap() .values() .find(|pin| pin["package"].as_str().unwrap().ends_with("|util|1.0.0")) .unwrap()["package"] .as_str() .unwrap() .to_owned(); missing_package["npm"]["packages"] .as_object_mut() .unwrap() .remove(&package); for damaged in [missing_instance, missing_edge, missing_package] { std::fs::write(root.path().join("avril.lock"), damaged.to_string()).unwrap(); let mut options = server.options(root.path()); options.frozen = true; options.offline = true; let error = match PackageStore::new(fixture::runtime_handle(), options) { Ok(store) => { let cold = NpmResolver::new(Arc::new(store)); block_on(cold.prepare(std::slice::from_ref(&app))) .unwrap_err() .to_string() } Err(error) => error.to_string(), }; assert!(error.contains("Lockfile is missing npm"), "{error}"); } let alias = package.replacen("http://", "HTTP://", 1); let noncanonical = pristine.to_string().replace(&package, &alias); std::fs::write(root.path().join("avril.lock"), noncanonical).unwrap(); let error = PackageStore::new(fixture::runtime_handle(), server.options(root.path())) .err() .expect("distinct package identities must not alias the same storage"); assert!( matches!( error, avril_runtime::registry::RegistryError::Npm( avril_runtime::registry::npm::NpmError::InvalidNpmLockIdentity ) ), "{error}" ); }