#[path = "common/registry.rs"] mod fixture; use avril_runtime::caps::{Capabilities, Category, Initial}; use avril_runtime::registry::{PackageStore, ProgressEvent, artifact_key}; use fixture::Registry; use futures::executor::block_on; use serde_json::json; use std::sync::{Arc, Mutex}; #[test] fn selects_versions_exports_and_verifies_manifest_sources() { let server = Registry::new(); server.package( "@test/pkg", "1.0.0", &[("mod.ts", "export const value = 1;")], &[(".", "./mod.ts")], ); server.package( "@test/pkg", "1.1.0", &[ ("mod.ts", "export const value = 2;"), ("sub.ts", "export {};"), ], &[(".", "./mod.ts"), ("./sub", "./sub.ts")], ); server.package( "@test/pkg", "2.0.0", &[("mod.ts", "export const value = 3;")], &[(".", "./mod.ts")], ); let dir = tempfile::tempdir().unwrap(); let store = PackageStore::new(fixture::runtime_handle(), server.options(dir.path())).unwrap(); let module = block_on(store.resolve("jsr:@test/pkg@^1")).unwrap(); assert!(module.ends_with("/@test/pkg/1.1.0/mod.ts")); assert_eq!( block_on(store.source(&module)).unwrap(), "export const value = 2;" ); assert!( block_on(store.resolve("jsr:@test/pkg@^1/sub")) .unwrap() .ends_with("/sub.ts") ); assert!( block_on(store.resolve("jsr:@test/pkg@^2")) .unwrap() .contains("/2.0.0/") ); assert!( block_on(store.resolve("jsr:@test/pkg@^1/private")) .unwrap_err() .to_string() .contains("no export") ); assert!(block_on(store.resolve("jsr:@test/pkg@latest")).is_err()); assert!(block_on(store.resolve("jsr:unscoped@1")).is_err()); store.save().unwrap(); let lock = fixture::read_lock(dir.path()); assert_eq!(lock["version"], 1); assert_eq!(lock["specifiers"]["jsr:@test/pkg@^1"], "1.1.0"); assert_eq!(lock["specifiers"]["jsr:@test/pkg@^2"], "2.0.0"); } #[test] fn written_locks_are_plain_fjson_with_a_final_newline() { let server = Registry::new(); server.package( "@test/pkg", "1.0.0", &[("mod.js", "export {};")], &[(".", "./mod.js")], ); let dir = tempfile::tempdir().unwrap(); let store = PackageStore::new(fixture::runtime_handle(), server.options(dir.path())).unwrap(); block_on(store.resolve("jsr:@test/pkg@1")).unwrap(); store.save().unwrap(); let text = std::fs::read_to_string(dir.path().join("avril.lock")).unwrap(); let lock = fixture::read_lock(dir.path()); assert_eq!( text, format!("{}\n", serde_fjson::stringify(&lock).unwrap()) ); } #[test] fn ephemeral_stores_resolve_and_save_without_writing_a_lock() { let server = Registry::new(); server.package( "@test/pkg", "1.0.0", &[("mod.ts", "export const value = 1;")], &[(".", "./mod.ts")], ); server.package( "@test/pkg", "1.1.0", &[("mod.ts", "export const value = 2;")], &[(".", "./mod.ts")], ); let dir = tempfile::tempdir().unwrap(); let mut options = server.options(dir.path()); options.ephemeral_lock = true; let store = PackageStore::new(fixture::runtime_handle(), options).unwrap(); let module = block_on(store.resolve("jsr:@test/pkg@^1")).unwrap(); assert!(module.ends_with("/@test/pkg/1.1.0/mod.ts")); assert_eq!( block_on(store.source(&module)).unwrap(), "export const value = 2;" ); store.save().unwrap(); store .save_graph(&["jsr:@test/pkg@^1".into()].into(), &[module].into()) .unwrap(); assert!(!dir.path().join("avril.lock").exists()); } #[test] fn pinned_versions_are_stable_until_an_explicit_update_and_yanks_are_skipped() { let server = Registry::new(); server.package( "@test/pkg", "1.0.0", &[("mod.js", "export {};")], &[(".", "./mod.js")], ); let dir = tempfile::tempdir().unwrap(); let options = server.options(dir.path()); let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); assert!( block_on(store.resolve("jsr:@test/pkg@^1")) .unwrap() .contains("/1.0.0/") ); store.save().unwrap(); drop(store); server.package( "@test/pkg", "1.1.0", &[("mod.js", "export {};")], &[(".", "./mod.js")], ); server.package( "@test/pkg", "1.2.0", &[("mod.js", "export {};")], &[(".", "./mod.js")], ); let mut index: serde_json::Value = serde_json::from_slice( server .files .lock() .unwrap() .get("/@test/pkg/meta.json") .unwrap(), ) .unwrap(); index["versions"]["1.2.0"] = json!({"yanked": true}); server.files.lock().unwrap().insert( "/@test/pkg/meta.json".into(), serde_json::to_vec(&index).unwrap(), ); let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); assert!( block_on(store.resolve("jsr:@test/pkg@^1")) .unwrap() .contains("/1.0.0/") ); drop(store); let mut options = options; options.update = true; let store = PackageStore::new(fixture::runtime_handle(), options).unwrap(); assert!( block_on(store.resolve("jsr:@test/pkg@^1")) .unwrap() .contains("/1.1.0/") ); store.save().unwrap(); } #[test] fn concurrent_acquisitions_share_downloads_and_independent_saves_merge() { let server = Registry::new(); for name in ["@test/a", "@test/b"] { server.package( name, "1.0.0", &[("mod.ts", "export {};")], &[(".", "./mod.ts")], ); } let dir = tempfile::tempdir().unwrap(); let options = server.options(dir.path()); let a = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); let b = PackageStore::new(fixture::runtime_handle(), options).unwrap(); let (first, second) = block_on(futures::future::join( a.resolve("jsr:@test/a@1"), a.resolve("jsr:@test/a@1"), )); assert_eq!(first.as_ref().unwrap(), second.as_ref().unwrap()); let name = first.unwrap(); let (first, second) = block_on(futures::future::join(a.source(&name), a.source(&name))); assert_eq!(first.unwrap(), second.unwrap()); block_on(b.resolve("jsr:@test/b@1")).unwrap(); a.save().unwrap(); b.save().unwrap(); let requests = server.requests.lock().unwrap(); for path in [ "/@test/a/meta.json", "/@test/a/1.0.0_meta.json", "/@test/a/1.0.0/mod.ts", ] { assert_eq!( requests .iter() .filter(|request| request.as_str() == path) .count(), 1, "{requests:?}" ); } let lock = fixture::read_lock(dir.path()); assert_eq!(lock["specifiers"].as_object().unwrap().len(), 2); } #[test] fn offline_frozen_and_vendored_runs_need_no_network() { let server = Registry::new(); server.package( "@test/pkg", "1.0.0", &[("mod.ts", "export {};")], &[(".", "./mod.ts")], ); let dir = tempfile::tempdir().unwrap(); let mut options = server.options(dir.path()); let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); let module = block_on(store.resolve("jsr:@test/pkg@1")).unwrap(); block_on(store.source(&module)).unwrap(); store.save().unwrap(); let vendor = dir.path().join("vendor"); store.vendor(&vendor).unwrap(); drop(store); server.requests.lock().unwrap().clear(); options.offline = true; options.frozen = true; options.vendor = Some(vendor); options.cache = dir.path().join("empty-cache"); let store = PackageStore::new(fixture::runtime_handle(), options).unwrap(); assert_eq!(block_on(store.resolve("jsr:@test/pkg@1")).unwrap(), module); assert_eq!(block_on(store.source(&module)).unwrap(), "export {};"); assert!( block_on(store.resolve("jsr:@test/pkg@2")) .unwrap_err() .to_string() .contains("Frozen") ); assert!(server.requests.lock().unwrap().is_empty()); } #[test] fn corrupted_sources_and_locked_metadata_fail_closed() { let server = Registry::new(); server.package( "@test/pkg", "1.0.0", &[("mod.ts", "export {};")], &[(".", "./mod.ts")], ); let dir = tempfile::tempdir().unwrap(); let options = server.options(dir.path()); let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); let name = block_on(store.resolve("jsr:@test/pkg@1")).unwrap(); block_on(store.source(&name)).unwrap(); store.save().unwrap(); drop(store); std::fs::write( options.cache.join("files").join(artifact_key(&name)), "console.log('tampered');", ) .unwrap(); let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); assert!( block_on(store.source(&name)) .unwrap_err() .to_string() .contains("integrity mismatch") ); drop(store); let metadata_url = server.url.join("@test/pkg/1.0.0_meta.json").unwrap(); std::fs::write( options .cache .join("files") .join(artifact_key(metadata_url.as_str())), "{}", ) .unwrap(); let store = PackageStore::new(fixture::runtime_handle(), options).unwrap(); assert!( block_on(store.resolve("jsr:@test/pkg@1")) .unwrap_err() .to_string() .contains("integrity mismatch") ); } #[test] fn rejects_unexported_or_escaping_files_and_registry_changes() { let server = Registry::new(); server.package( "@test/pkg", "1.0.0", &[("mod.ts", "export {};")], &[(".", "../other.ts")], ); let dir = tempfile::tempdir().unwrap(); let options = server.options(dir.path()); let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); assert!(block_on(store.resolve("jsr:@test/pkg@1")).is_err()); assert!( block_on( store.source( server .url .join("@test/pkg/1.0.0/private.ts") .unwrap() .as_str() ) ) .unwrap_err() .to_string() .contains("manifest") ); assert!(store.module_base("https://example.com/a.js").is_err()); assert!( store .module_base(&format!("{}@test/pkg/1.0.0/mod.ts?query", server.url)) .is_err() ); let mut invalid = options.clone(); invalid.jsr_registry = url::Url::parse("http://example.com/").unwrap(); assert!( PackageStore::new(fixture::runtime_handle(), invalid) .err() .unwrap() .to_string() .contains("HTTPS") ); let mut frozen = options; frozen.frozen = true; assert!( PackageStore::new(fixture::runtime_handle(), frozen) .err() .unwrap() .to_string() .contains("does not exist") ); } #[test] fn lock_saves_ignore_untouched_stale_entries_but_detect_used_conflicts() { let server = Registry::new(); for name in ["@test/a", "@test/b"] { server.package( name, "1.0.0", &[("mod.js", "export {};")], &[(".", "./mod.js")], ); } let dir = tempfile::tempdir().unwrap(); let options = server.options(dir.path()); let initial = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); block_on(initial.resolve("jsr:@test/a@1")).unwrap(); initial.save().unwrap(); let stale = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); let used = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); block_on(used.resolve("jsr:@test/a@1")).unwrap(); server.package( "@test/a", "1.1.0", &[("mod.js", "export {};")], &[(".", "./mod.js")], ); let mut update_options = options.clone(); update_options.update = true; let update = PackageStore::new(fixture::runtime_handle(), update_options).unwrap(); block_on(update.resolve("jsr:@test/a@1")).unwrap(); update.save().unwrap(); block_on(stale.resolve("jsr:@test/b@1")).unwrap(); stale.save().unwrap(); assert!( used.save() .unwrap_err() .to_string() .contains("changed concurrently") ); let lock = fixture::read_lock(dir.path()); assert_eq!(lock["specifiers"]["jsr:@test/a@1"], "1.1.0"); assert_eq!(lock["specifiers"]["jsr:@test/b@1"], "1.0.0"); } #[test] fn relative_registry_imports_cannot_cross_package_or_version_boundaries() { let server = Registry::new(); let dir = tempfile::tempdir().unwrap(); let store = PackageStore::new(fixture::runtime_handle(), server.options(dir.path())).unwrap(); let referrer = server .url .join("@test/pkg/1.0.0/sub/main.ts") .unwrap() .to_string(); assert!( store .resolve_relative("../dep.ts", &referrer) .unwrap() .ends_with("/1.0.0/dep.ts") ); for specifier in [ "../../2.0.0/mod.ts", "../../../other/1.0.0/mod.ts", "../../../../@other/pkg/1.0.0/mod.ts", "../..%2fsecret.ts", "https://example.com/module.ts", "./dep.ts?query", ] { assert!( store.resolve_relative(specifier, &referrer).is_err(), "{specifier}" ); } } #[cfg(unix)] #[test] fn atomic_outputs_preserve_existing_permissions_and_reject_special_files() { use std::os::unix::fs::PermissionsExt; let dir = tempfile::tempdir().unwrap(); let output = dir.path().join("avril.lock"); std::fs::write(&output, "old").unwrap(); std::fs::set_permissions(&output, std::fs::Permissions::from_mode(0o640)).unwrap(); avril_runtime::registry::atomic_write(&output, b"new").unwrap(); assert_eq!( std::fs::metadata(&output).unwrap().permissions().mode() & 0o777, 0o640 ); let link = dir.path().join("link"); std::os::unix::fs::symlink(&output, &link).unwrap(); assert!(avril_runtime::registry::atomic_write(&link, b"replaced").is_err()); assert_eq!(std::fs::read(&output).unwrap(), b"new"); use std::os::unix::ffi::OsStrExt; let fifo = dir.path().join("fifo"); let path = std::ffi::CString::new(fifo.as_os_str().as_bytes()).unwrap(); assert_eq!(unsafe { libc::mkfifo(path.as_ptr(), 0o600) }, 0); let server = Registry::new(); let mut options = server.options(dir.path()); options.lock = fifo; assert!( PackageStore::new(fixture::runtime_handle(), options) .err() .unwrap() .to_string() .contains("regular") ); } #[test] fn graph_scoped_saves_ignore_prior_walks_changed_by_other_processes() { let server = Registry::new(); for name in ["@test/a", "@test/b"] { server.package( name, "1.0.0", &[("mod.ts", "export {};")], &[(".", "./mod.ts")], ); } let dir = tempfile::tempdir().unwrap(); let options = server.options(dir.path()); let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); let a = block_on(store.resolve("jsr:@test/a@^1")).unwrap(); store .save_graph(&["jsr:@test/a@^1".into()].into(), &[a].into()) .unwrap(); server.package( "@test/a", "1.1.0", &[("mod.ts", "export {};")], &[(".", "./mod.ts")], ); let mut update_options = options.clone(); update_options.update = true; let update = PackageStore::new(fixture::runtime_handle(), update_options).unwrap(); block_on(update.resolve("jsr:@test/a@^1")).unwrap(); update.save().unwrap(); store .save_graph(&Default::default(), &["/local/mod.ts".into()].into()) .unwrap(); let b = block_on(store.resolve("jsr:@test/b@^1")).unwrap(); store .save_graph(&["jsr:@test/b@^1".into()].into(), &[b.clone()].into()) .unwrap(); let lock = fixture::read_lock(dir.path()); assert_eq!(lock["specifiers"]["jsr:@test/a@^1"], "1.1.0"); assert_eq!(lock["specifiers"]["jsr:@test/b@^1"], "1.0.0"); std::fs::remove_file(options.lock).unwrap(); store .save_graph(&["jsr:@test/b@^1".into()].into(), &[b].into()) .unwrap(); } #[test] fn shared_caller_runtime_survives_sibling_drop() { let server = Registry::new(); server.package( "@test/pkg", "1.0.0", &[("mod.ts", "export {};")], &[(".", "./mod.ts")], ); let dir = tempfile::tempdir().unwrap(); let options = server.options(dir.path()); let runtime = tokio::runtime::Builder::new_multi_thread() .worker_threads(2) .enable_all() .build() .unwrap(); let handle = runtime.handle().clone(); let first = PackageStore::new(handle.clone(), options.clone()).unwrap(); let second = PackageStore::new(handle, options).unwrap(); drop(first); let module = block_on(second.resolve("jsr:@test/pkg@1")).unwrap(); assert_eq!(block_on(second.source(&module)).unwrap(), "export {};"); } #[test] fn source_download_callbacks_cannot_bypass_live_authority() { let registry = Registry::new(); registry.package( "@test/progress", "1.0.0", &[("mod.ts", "export {};")], &[(".", "./mod.ts")], ); let module = format!("{}@test/progress/1.0.0/mod.ts", registry.url); for jsr in [false, true] { let directory = tempfile::tempdir().unwrap(); let mut options = registry.options(directory.path()); if !jsr { options.jsr_registry = url::Url::parse("https://jsr.example/").unwrap(); } let caps = Capabilities::new(Initial::new(false), directory.path().to_owned()); let revoke = caps.clone(); let events = Arc::new(Mutex::new(Vec::new())); let recorded = events.clone(); let store = PackageStore::new(fixture::runtime_handle(), options) .unwrap() .with_capabilities(caps) .with_progress(Arc::new(move |event| { recorded.lock().unwrap().push(event.clone()); revoke .pledge(vec![(Category::JsExec, Some(vec![]), vec![])]) .unwrap(); })); assert!( block_on(store.remote_source(&module)) .unwrap_err() .to_string() .contains("PermissionDenied") ); assert!(matches!( events.lock().unwrap().as_slice(), [ProgressEvent::SourceDownload { .. }] )); assert!(registry.requests.lock().unwrap().is_empty()); } }