use std::fs; use std::path::Path; use std::process::{Command, Output}; use avril_runtime::import_map::ImportMap; use url::Url; fn run(root: &Path, entry: &str, files: &[(&str, &str)], args: &[&str]) -> Output { for (name, source) in files { let path = root.join(name); fs::create_dir_all(path.parent().unwrap()).unwrap(); fs::write(path, source).unwrap(); } Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(root) .args(args) .arg(entry) .output() .unwrap() } #[test] fn sibling_libraries_own_their_maps_without_workspace_registration() { let dir = tempfile::tempdir().unwrap(); let output = run( dir.path(), "app/main.ts", &[ ("app/avril.conf", "imports: {dep: './dep.ts'}"), ( "app/main.ts", "import {value} from 'dep'; import {value as other} from '../library/mod.ts'; console.log(value, other, (await import('../library/mod.ts')).value);", ), ("app/dep.ts", "export const value = 'app';"), ("library/avril.conf", "imports: {dep: './dep.ts'}"), ( "library/mod.ts", "import {value as dependency} from 'dep'; export const value = dependency;", ), ("library/dep.ts", "export const value = 'library';"), ], &[], ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"app library library\n"); let output = Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(dir.path().join("library")) .arg("mod.ts") .output() .unwrap(); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); } #[test] fn nested_boundaries_do_not_inherit_consumer_aliases() { let dir = tempfile::tempdir().unwrap(); let output = run( dir.path(), "main.js", &[ ("avril.conf", "imports: {consumerOnly: './dep.js'}"), ("main.js", "import './library/mod.js';"), ("dep.js", "export {};"), ("library/avril.conf", ""), ("library/mod.js", "import 'consumerOnly';"), ], &[], ); assert!(!output.status.success()); assert!( String::from_utf8_lossy(&output.stderr) .contains("Unsupported module specifier: consumerOnly") ); } #[test] fn aliases_and_prefixes_share_canonical_module_identity() { let dir = tempfile::tempdir().unwrap(); let output = run( dir.path(), "sub/main.ts", &[ ( "avril.conf", "imports: {shared: './lib/shared.ts', 'lib/': './lib/'}", ), ( "sub/main.ts", "import * as a from 'shared'; import * as b from 'lib/shared.ts'; import * as c from '../lib/./shared.ts'; console.log(a === b, b === c, a.count, (await import('shared')) === a);", ), ( "lib/shared.ts", "export const count = globalThis.count = (globalThis.count ?? 0) + 1;", ), ], &[], ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"true true 1 true\n"); } #[test] fn scopes_use_longest_matches_and_fall_back_to_the_package_map() { let base = Url::parse("file:///project/avril.conf").unwrap(); let map = ImportMap::parse(r#" imports: {dep: './default.ts', common: './common.ts', 'lib/': './lib/', 'lib/special/': './special/'}, scopes: { './src/': {dep: './src.ts'}, './src/nested/': {dep: './nested.ts', blocked: null}, }, "#, &base).unwrap(); let referrer = Url::parse("file:///project/src/nested/main.ts").unwrap(); assert_eq!( map.resolve("dep", &referrer).unwrap().as_deref(), Some("file:///project/nested.ts") ); assert_eq!( map.resolve("common", &referrer).unwrap().as_deref(), Some("file:///project/common.ts") ); assert_eq!( map.resolve("lib/special/mod.ts", &referrer) .unwrap() .as_deref(), Some("file:///project/special/mod.ts") ); assert!( map.resolve("blocked", &referrer) .unwrap_err() .to_string() .contains("blocked") ); assert!( map.resolve("lib/../outside.ts", &referrer) .unwrap_err() .to_string() .contains("escapes") ); assert!(map.resolve("lib/%2e%2e/outside.ts", &referrer).is_err()); } #[test] fn normalizes_url_like_keys_and_does_not_recursively_resolve_aliases() { let base = Url::parse("file:///project/avril.conf").unwrap(); let map = ImportMap::parse( "imports: {'./dep.ts': './other.ts', other: './dep.ts'},", &base, ) .unwrap(); let referrer = Url::parse("file:///project/sub/main.ts").unwrap(); assert_eq!( map.resolve("../dep.ts", &referrer).unwrap().as_deref(), Some("file:///project/other.ts") ); assert_eq!( map.resolve("other", &referrer).unwrap().as_deref(), Some("file:///project/dep.ts") ); } #[test] fn unknown_options_do_not_affect_import_resolution() { let base = Url::parse("file:///project/avril.conf").unwrap(); let map = ImportMap::parse( "custom: {imports: false, nested: [null, 42]}, imports: {dep: './dep.ts'}, scopes: {'./tests/': {dep: './test-dep.ts'}}", &base, ) .unwrap(); for (referrer, target) in [ ("./main.ts", "./dep.ts"), ("./tests/main.ts", "./test-dep.ts"), ] { assert_eq!( map.resolve("dep", &base.join(referrer).unwrap()).unwrap(), Some(base.join(target).unwrap().to_string()) ); } } #[test] fn invalid_schema_reports_the_offending_source_location() { let base = Url::parse("file:///project/avril.conf").unwrap(); for (source, expected) in [ ("imports: []", "Expected an object"), ("imports: {dep: true}", "strings or null"), ("imports: {dep: 'bare'}", "URLs or relative paths"), ("imports: {'lib/': './lib'}", "end with '/'"), ("imports: {'': './dep.ts'}", "must not be empty"), ( "imports: {'./dep.ts': './a.ts', 'file:///project/dep.ts': './b.ts'}", "Duplicate normalized", ), ] { let error = ImportMap::parse(source, &base).unwrap_err(); assert!(error.message.contains(expected), "{source}: {error}"); assert!(source.get(error.span).is_some()); } let dir = tempfile::tempdir().unwrap(); let output = run( dir.path(), "main.js", &[ ("main.js", "import 'dep';"), ("avril.conf", "// heading\nimports: {dep: true}"), ], &[], ); assert!(!output.status.success()); assert!(String::from_utf8_lossy(&output.stderr).contains("avril.conf:2:16")); } #[test] fn discovery_and_mapped_imports_do_not_expand_execution_authority() { let dir = tempfile::tempdir().unwrap(); let output = run( dir.path(), "app/main.js", &[ ("avril.conf", "imports: {secret: './secret.js'}"), ("secret.js", "console.log('secret');"), ("app/main.js", "import 'secret';"), ], &["-0", "--allow-js-exec=app"], ); assert!(!output.status.success()); assert!( String::from_utf8_lossy(&output.stderr).contains("Unsupported module specifier: secret") ); let output = run( dir.path(), "app/main.js", &[("app/avril.conf", "imports: {secret: '../secret.js'}")], &["-0", "--allow-js-exec=app"], ); assert!(!output.status.success()); assert!(String::from_utf8_lossy(&output.stderr).contains("PermissionDenied: js-exec")); assert!(output.stdout.is_empty()); } #[test] fn runtime_revocation_still_applies_to_mapped_cached_modules() { let dir = tempfile::tempdir().unwrap(); let output = run( dir.path(), "main.js", &[ ("avril.conf", "imports: {dep: './dep.js'}"), ( "main.js", "import * as Avril from 'avril:std'; await import('dep'); Avril.pledge({jsExec: {deny: ['./dep.js']}}); try {await import('dep'); throw new Error('allowed');} catch (e) {console.log(e.message.includes('PermissionDenied'));}", ), ("dep.js", "export {};"), ], &[], ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"true\n"); } #[test] fn scope_file_keys_are_exact_and_colons_can_appear_in_bare_suffixes() { let base = Url::parse("file:///project/avril.conf").unwrap(); let map = ImportMap::parse( "imports: {dep: './default.ts', 'lib/': './lib/', 'lib/a:b.ts': './lib/a:b.ts'}, scopes: {'./src': {dep: './scoped.ts'}}", &base, ) .unwrap(); assert_eq!( map.resolve("dep", &Url::parse("file:///project/src").unwrap()) .unwrap() .as_deref(), Some("file:///project/scoped.ts") ); assert_eq!( map.resolve( "dep", &Url::parse("file:///project/src-legacy/main.ts").unwrap() ) .unwrap() .as_deref(), Some("file:///project/default.ts") ); assert_eq!( map.resolve("lib/a:b.ts", &base).unwrap().as_deref(), Some("file:///project/lib/a:b.ts") ); assert!(map.resolve("lib/..%2fsecret.ts", &base).is_err()); } #[test] fn cached_maps_do_not_skip_revoked_ancestor_authority() { let dir = tempfile::tempdir().unwrap(); let output = run( dir.path(), "app/main.js", &[ ("avril.conf", "imports: {dep: './app/dep.js'}"), ("app/dep.js", "export {};"), ( "app/main.js", "import {pledge} from 'avril:std'; await import('dep'); pledge({jsExec: {allow: ['./app', './avril.conf']}}); try {await import('dep'); throw new Error('allowed');} catch(e) {console.log(e.message.includes('Unsupported module specifier: dep'));}", ), ], &[], ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"true\n"); } #[test] fn denied_config_existence_is_not_observable() { let dir = tempfile::tempdir().unwrap(); let files = [("main.js", "import 'dep';")]; let args = ["-0", "--allow-js-exec=.", "--deny-js-exec=avril.conf"]; let missing = run(dir.path(), "main.js", &files, &args); let present = run( dir.path(), "main.js", &[("avril.conf", "not: valid: syntax")], &args, ); assert!(!missing.status.success()); assert_eq!(present.stderr, missing.stderr); } #[cfg(unix)] #[test] fn config_symlinks_are_rejected_without_probing_referents() { use std::os::unix::fs::symlink; let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("main.js"), "import 'dep';").unwrap(); fs::write(dir.path().join("existing"), "").unwrap(); let config = dir.path().join("avril.conf"); symlink(dir.path().join("existing"), &config).unwrap(); let present = Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(dir.path()) .args(["-0", "--allow-js-exec=.", "main.js"]) .output() .unwrap(); fs::remove_file(&config).unwrap(); symlink(dir.path().join("missing"), &config).unwrap(); let missing = Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(dir.path()) .args(["-0", "--allow-js-exec=.", "main.js"]) .output() .unwrap(); assert!(!present.status.success()); assert_eq!(present.stderr, missing.stderr); assert!(String::from_utf8_lossy(&present.stderr).contains("non-symlink")); } #[cfg(unix)] #[test] fn discovery_stops_before_shared_ancestors() { use std::os::unix::fs::PermissionsExt; let dir = tempfile::tempdir().unwrap(); let output = run( dir.path(), "shared/app/main.js", &[ ("shared/avril.conf", "imports: {dep: './dep.js'}"), ("shared/dep.js", "console.log('hijacked');"), ("shared/app/main.js", "import 'dep';"), ], &[], ); assert!(output.status.success()); fs::set_permissions(dir.path().join("shared"), fs::Permissions::from_mode(0o777)).unwrap(); let output = run(dir.path(), "shared/app/main.js", &[], &[]); assert!(!output.status.success()); assert!(String::from_utf8_lossy(&output.stderr).contains("Unsupported module specifier")); assert!(output.stdout.is_empty()); }