#[path = "common/registry.rs"] mod fixture; #[path = "common/http.rs"] mod http_fixture; use http_fixture::{Http, Response}; use avril_runtime::caps::{Capabilities, Category, Initial}; use avril_runtime::registry::{Lockfile, PackageStore, checksum}; use fixture::Registry; use futures::executor::block_on; fn read_lock(path: &std::path::Path) -> Lockfile { let bytes = std::fs::read(path).unwrap(); let source = std::str::from_utf8(&bytes).unwrap(); serde_json::from_value(serde_fjson::parse(source).unwrap()).unwrap() } #[test] fn redirects_pin_the_final_url_and_only_authorize_the_requested_url() { let registry = Registry::new(); let http = Http::new(); let target = Http::new(); http.redirect("mod.ts", &target.name("nested/final.ts")); target.source("nested/final.ts", "export const value = 1;"); target.source("nested/dep.ts", "export {};"); let dir = tempfile::tempdir().unwrap(); let options = registry.options(dir.path()); let name = http.name("mod.ts"); let mut initial = Initial::new(true); initial .grant(Category::JsExec, false, &name, dir.path()) .unwrap(); initial .grant(Category::JsExec, true, target.url.as_str(), dir.path()) .unwrap(); let caps = Capabilities::new(initial, dir.path().to_owned()); let store = PackageStore::new(fixture::runtime_handle(), options.clone()) .unwrap() .with_capabilities(caps.clone()); assert!(store.remote_base(&name).is_err()); let (a, b) = block_on(futures::future::join( store.remote_source(&name), store.remote_source(&name), )); assert_eq!(a.unwrap(), "export const value = 1;"); assert_eq!(b.unwrap(), "export const value = 1;"); assert_eq!( store.remote_base(&name).unwrap(), target.name("nested/final.ts") ); assert!( block_on(store.remote_source(&target.name("nested/dep.ts"))) .unwrap_err() .to_string() .contains("PermissionDenied") ); assert_eq!(http.requests.lock().unwrap().len(), 1); assert_eq!(target.requests.lock().unwrap().len(), 1); store.save().unwrap(); let lock = read_lock(&options.lock); assert_eq!( lock.remote[&name].checksum, checksum(b"export const value = 1;") ); assert_eq!(lock.remote[&name].url, target.name("nested/final.ts")); caps.pledge(vec![(Category::JsExec, Some(vec![]), vec![])]) .unwrap(); assert!( block_on(store.remote_source(&name)) .unwrap_err() .to_string() .contains("PermissionDenied") ); let mut offline = options; offline.offline = true; offline.frozen = true; let denied = PackageStore::new(fixture::runtime_handle(), offline.clone()) .unwrap() .with_capabilities(caps); assert!( block_on(denied.remote_source(&name)) .unwrap_err() .to_string() .contains("PermissionDenied") ); let store = PackageStore::new(fixture::runtime_handle(), offline).unwrap(); block_on(store.remote_source(&name)).unwrap(); assert_eq!( store.remote_base(&name).unwrap(), target.name("nested/final.ts") ); assert_eq!(http.requests.lock().unwrap().len(), 1); } #[test] fn updates_refresh_once_and_preserve_other_projects_content_pins() { let registry = Registry::new(); let http = Http::new(); http.source("mod.js", "export const value = 1;"); let dir = tempfile::tempdir().unwrap(); let options = registry.options(dir.path()); let name = http.name("mod.js"); let original = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); block_on(original.remote_source(&name)).unwrap(); original.save().unwrap(); http.source("mod.js", "export const value = 2;"); let stable = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); assert_eq!( block_on(stable.remote_source(&name)).unwrap(), "export const value = 1;" ); let mut other = options.clone(); other.lock = dir.path().join("other.lock"); other.update = true; let update = PackageStore::new(fixture::runtime_handle(), other.clone()).unwrap(); let (a, b) = block_on(futures::future::join( update.remote_source(&name), update.remote_source(&name), )); assert_eq!(a.unwrap(), "export const value = 2;"); assert_eq!(b.unwrap(), "export const value = 2;"); http.source("mod.js", "export const value = 3;"); assert_eq!( block_on(update.remote_source(&name)).unwrap(), "export const value = 2;" ); update.save().unwrap(); assert_eq!(http.requests.lock().unwrap().len(), 2); let mut offline = options; offline.offline = true; offline.frozen = true; let store = PackageStore::new(fixture::runtime_handle(), offline).unwrap(); assert_eq!( block_on(store.remote_source(&name)).unwrap(), "export const value = 1;" ); other.update = false; other.lock = dir.path().join("unlocked.lock"); let store = PackageStore::new(fixture::runtime_handle(), other).unwrap(); assert_eq!( block_on(store.remote_source(&name)).unwrap(), "export const value = 2;" ); } #[test] fn vendored_sources_need_only_the_lock_and_vendor_directory() { let registry = Registry::new(); let http = Http::new(); http.redirect("mod.ts?version=1", "/nested/final.ts"); http.source("nested/final.ts", "export {};"); let dir = tempfile::tempdir().unwrap(); let mut options = registry.options(dir.path()); let name = http.name("mod.ts?version=1"); let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); block_on(store.remote_source(&name)).unwrap(); store.save().unwrap(); let vendor = dir.path().join("vendor"); store.vendor(&vendor).unwrap(); let relative = store.vendor_relative(&name).unwrap(); assert!(relative.starts_with("http")); assert_eq!(relative.extension().unwrap(), "ts"); assert_eq!(std::fs::read(vendor.join(relative)).unwrap(), b"export {};"); options.vendor = Some(vendor); options.cache = dir.path().join("empty-cache"); options.offline = true; options.frozen = true; let store = PackageStore::new(fixture::runtime_handle(), options).unwrap(); assert_eq!(block_on(store.remote_source(&name)).unwrap(), "export {};"); assert_eq!( store.remote_base(&name).unwrap(), http.name("nested/final.ts") ); assert_eq!(http.requests.lock().unwrap().len(), 2); } #[test] fn graph_saves_merge_filter_conflict_and_prune_remote_entries() { let registry = Registry::new(); let http = Http::new(); http.source("a.js", "export const value = 1;"); http.source("b.js", "export {};"); let dir = tempfile::tempdir().unwrap(); let options = registry.options(dir.path()); let a = http.name("a.js"); let b = http.name("b.js"); let first = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); let second = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); block_on(first.remote_source(&a)).unwrap(); block_on(first.remote_source(&b)).unwrap(); first .save_graph(&Default::default(), &[a.clone()].into()) .unwrap(); assert_eq!(read_lock(&options.lock).remote.len(), 1); block_on(second.remote_source(&b)).unwrap(); second.save().unwrap(); assert_eq!(read_lock(&options.lock).remote.len(), 2); let stale = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); block_on(stale.remote_source(&a)).unwrap(); let mut updated = options.clone(); updated.update = true; http.source("a.js", "export const value = 2;"); let update = PackageStore::new(fixture::runtime_handle(), updated).unwrap(); block_on(update.remote_source(&a)).unwrap(); update.save().unwrap(); assert!( stale .save() .unwrap_err() .to_string() .contains("changed concurrently") ); stale .save_graph(&Default::default(), &[b.clone()].into()) .unwrap(); let prune = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); block_on(prune.remote_source(&b)).unwrap(); prune.prune().unwrap(); let lock = read_lock(&options.lock); assert_eq!(lock.remote.len(), 1); assert!(lock.remote.contains_key(&b)); } #[test] fn rejects_html_credentials_unsupported_redirects_and_redirect_loops() { let registry = Registry::new(); let http = Http::new(); let dir = tempfile::tempdir().unwrap(); let store = PackageStore::new(fixture::runtime_handle(), registry.options(dir.path())).unwrap(); http.source("html.js", ""); http.source("mime.js", "export {};"); http.routes .lock() .unwrap() .get_mut("/mime.js") .unwrap() .headers = "Content-Type: TEXT/HTML; charset=utf-8\r\n".into(); http.redirect("loop.js", "/loop.js"); http.redirect("file.js", "file:///tmp/module.js"); http.redirect( "credentials.js", &http .name("secret.js") .replace("http://", "http://user:pass@"), ); http.source("secret.js", "export {};"); for (path, message) in [ ("html.js", "HTML"), ("mime.js", "HTML"), ("loop.js", "Too many"), ("file.js", "HTTP(S)"), ("credentials.js", "credentials"), ] { assert!( block_on(store.remote_source(&http.name(path))) .unwrap_err() .to_string() .contains(message), "{path}" ); } assert!(!http.requests.lock().unwrap().contains(&"/secret.js".into())); assert_eq!( http.requests .lock() .unwrap() .iter() .filter(|path| path.as_str() == "/loop.js") .count(), 11 ); let credentialed = http .name("secret.js") .replace("http://", "http://user:pass@"); assert!( block_on(store.remote_source(&credentialed)) .unwrap_err() .to_string() .contains("credentials") ); assert!(block_on(store.remote_source("file:///tmp/mod.js")).is_err()); } #[test] fn rejects_revocation_during_download_and_missing_frozen_pins() { let registry = Registry::new(); let http = Http::new(); http.source("mod.js", "export {};"); let dir = tempfile::tempdir().unwrap(); let options = registry.options(dir.path()); let name = http.name("mod.js"); let mut initial = Initial::new(true); initial .grant(Category::JsExec, false, &name, dir.path()) .unwrap(); let caps = Capabilities::new(initial, dir.path().to_owned()); let store = PackageStore::new(fixture::runtime_handle(), options.clone()) .unwrap() .with_capabilities(caps.clone()); *http.hook.lock().unwrap() = Some(Box::new(move || { caps.pledge(vec![(Category::JsExec, Some(vec![]), vec![])]) .unwrap(); })); assert!( block_on(store.remote_source(&name)) .unwrap_err() .to_string() .contains("PermissionDenied") ); store.save().unwrap(); assert!(!options.lock.exists()); let lock = format!( "version: 1\njsrRegistry: \"{}\"\nspecifiers: {{}}\npackages: {{}}\n", registry.url ); std::fs::write(&options.lock, lock).unwrap(); let mut frozen = options; frozen.frozen = true; let store = PackageStore::new(fixture::runtime_handle(), frozen).unwrap(); assert!( block_on(store.remote_source(&name)) .unwrap_err() .to_string() .contains("Frozen") ); assert_eq!(http.requests.lock().unwrap().len(), 1); } #[test] fn pinned_downloads_reject_changed_final_urls_even_with_identical_bytes() { let registry = Registry::new(); let http = Http::new(); http.redirect("mod.js", "/a.js"); http.source("a.js", "export {};"); http.source("b.js", "export {};"); let dir = tempfile::tempdir().unwrap(); let mut options = registry.options(dir.path()); let name = http.name("mod.js"); let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); block_on(store.remote_source(&name)).unwrap(); store.save().unwrap(); options.cache = dir.path().join("empty-cache"); http.redirect("mod.js", "/b.js"); let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); assert!( block_on(store.remote_source(&name)) .unwrap_err() .to_string() .contains("final URL mismatch") ); http.redirect("mod.js", "/a.js"); http.source("a.js", "export const changed = true;"); let store = PackageStore::new(fixture::runtime_handle(), options).unwrap(); assert!( block_on(store.remote_source(&name)) .unwrap_err() .to_string() .contains("integrity mismatch") ); } #[test] fn registry_prefix_sources_still_require_the_manifest() { let registry = Registry::new(); registry.package( "@test/pkg", "1.0.0", &[("mod.ts", "export {};")], &[(".", "./mod.ts")], ); let dir = tempfile::tempdir().unwrap(); let options = registry.options(dir.path()); let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); let name = block_on(store.resolve("jsr:@test/pkg@1")).unwrap(); assert_eq!(block_on(store.remote_source(&name)).unwrap(), "export {};"); store.save().unwrap(); assert!(read_lock(&options.lock).remote.is_empty()); assert!( registry .requests .lock() .unwrap() .contains(&"/@test/pkg/1.0.0_meta.json".into()) ); let private = registry .url .join("@test/pkg/1.0.0/private.ts") .unwrap() .to_string(); assert!( block_on(store.remote_source(&private)) .unwrap_err() .to_string() .contains("manifest") ); } #[test] fn rejects_oversized_and_non_utf8_sources() { let registry = Registry::new(); let http = Http::new(); let dir = tempfile::tempdir().unwrap(); let store = PackageStore::new(fixture::runtime_handle(), registry.options(dir.path())).unwrap(); for (path, body, message) in [ ( "large.js", vec![b' '; avril_runtime::registry::MAX_ARTIFACT_SIZE + 1], "too large", ), ("invalid.js", vec![255], "UTF-8"), ] { http.routes.lock().unwrap().insert( format!("/{path}"), Response { status: 200, headers: String::new(), body, }, ); assert!( block_on(store.remote_source(&http.name(path))) .unwrap_err() .to_string() .contains(message) ); } } #[test] fn corrupted_cached_http_source_fails_integrity_verification() { let registry = Registry::new(); let http = Http::new(); http.source("mod.js", "export {};"); let dir = tempfile::tempdir().unwrap(); let options = registry.options(dir.path()); let name = http.name("mod.js"); let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); block_on(store.remote_source(&name)).unwrap(); store.save().unwrap(); let hash = read_lock(&options.lock).remote[&name].checksum.clone(); std::fs::write(options.cache.join("http/blobs").join(hash), b"tampered").unwrap(); let store = PackageStore::new(fixture::runtime_handle(), options).unwrap(); assert!( block_on(store.remote_source(&name)) .unwrap_err() .to_string() .contains("integrity mismatch") ); } #[test] fn missing_unpinned_blobs_are_reacquired_without_enforcing_stale_metadata() { let registry = Registry::new(); let http = Http::new(); let dir = tempfile::tempdir().unwrap(); let options = registry.options(dir.path()); let name = http.name("mod.ts"); http.source("mod.ts", "export const value = 1;"); let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap(); block_on(store.remote_source(&name)).unwrap(); std::fs::remove_dir_all(options.cache.join("http/blobs")).unwrap(); http.source("mod.ts", "export const value = 2;"); let store = PackageStore::new(fixture::runtime_handle(), options).unwrap(); assert_eq!( block_on(store.remote_source(&name)).unwrap(), "export const value = 2;" ); }