#[path = "common/http.rs"] mod http_fixture; use http_fixture::Http; fn cli(directory: &std::path::Path, args: &[&str]) -> std::process::Output { std::process::Command::new(env!("CARGO_BIN_EXE_avril")) .current_dir(directory) .arg(args[0]) .arg("--cache-dir=cache") .args(&args[1..]) .output() .unwrap() } fn success(output: std::process::Output) -> String { assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); String::from_utf8(output.stdout).unwrap() } #[test] fn http_graphs_resolve_maps_cycles_queries_and_concurrent_dynamic_imports() { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); http.source("lib/mod.ts?version=1", "import type {Missing} from './missing.ts'; import './cycle.ts'; export {value} from './dep.mts'; export const count: number = globalThis.count = (globalThis.count ?? 0) + 1; export const url = import.meta.url; export const self = () => import(import.meta.url);"); http.source( "lib/cycle.ts", "import {count} from './mod.ts?version=1'; export const read = () => count;", ); http.source("lib/dep.mts", "export const value: number = 42;"); http.source( "lib/lazy.ts", "import {value} from './dep.mts'; export {value};", ); let name = http.name("lib/mod.ts?version=1"); std::fs::write( dir.path().join("avril.conf"), format!("imports: {{lib: '{name}'}}"), ) .unwrap(); std::fs::write(dir.path().join("main.ts"), format!("import * as a from 'lib'; import * as b from '{name}'; const lazy = '{}'; const [x,y] = await Promise.all([import(lazy),import(lazy)]); console.log(a === b, a.value, a.count, (await a.self()) === a, x === y, x.value, a.url);", http.name("lib/lazy.ts"))).unwrap(); let expected = format!("true 42 1 true true 42 {name}\n"); assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), expected); let requests = http.requests.lock().unwrap().clone(); assert_eq!(requests.len(), 4, "{requests:?}"); assert_eq!( success(cli( dir.path(), &["run", "--offline", "--frozen", "main.ts"] )), expected ); assert_eq!(*http.requests.lock().unwrap(), requests); let info: serde_json::Value = serde_json::from_str(&success(cli(dir.path(), &["info", "--offline", "main.ts"]))).unwrap(); assert_eq!(info["modules"][&name]["url"], name); assert_eq!( info["modules"][&name]["imports"]["./dep.mts"], http.name("lib/dep.mts") ); } #[test] fn redirected_modules_use_the_final_base_without_authorizing_redirect_targets() { let http = Http::new(); let target = Http::new(); let dir = tempfile::tempdir().unwrap(); http.redirect("start", &target.name("nested/mod.ts")); target.source("nested/mod.ts", "export const value: number = 7; export const url = import.meta.url; export const self = () => import(import.meta.url);"); let name = http.name("start"); std::fs::write( dir.path().join("main.ts"), format!( "import * as a from '{name}'; console.log(a.value, a.url, (await a.self()) === a);" ), ) .unwrap(); let allow = format!("--allow-js-exec={name}"); let deny = format!("--deny-js-exec={}", target.url); let args = ["run", "-0", &allow, &deny, "main.ts"]; let expected = format!("7 {name} true\n"); assert_eq!(success(cli(dir.path(), &args)), expected); let mut offline = args.to_vec(); offline.insert(1, "--offline"); assert_eq!(success(cli(dir.path(), &offline)), expected); target.source("nested/mod.ts", "import {value} from './dep.ts'; export {value}; export const url = import.meta.url; export const self = () => import(import.meta.url);"); target.source("nested/dep.ts", "export const value: number = 9;"); let output = cli(dir.path(), &["update", "-0", &allow, &deny, "main.ts"]); assert!(!output.status.success()); assert!(String::from_utf8_lossy(&output.stderr).contains("PermissionDenied")); assert!( !target .requests .lock() .unwrap() .iter() .any(|path| path.ends_with("dep.ts")) ); let dep = target.name("nested/dep.ts"); let allow_dep = format!("--allow-js-exec={dep}"); success(cli( dir.path(), &["update", "-0", &allow, &allow_dep, "main.ts"], )); assert_eq!( success(cli( dir.path(), &[ "run", "--offline", "--frozen", "-0", &allow, &allow_dep, "main.ts" ] )), expected.replace("7 ", "9 ") ); } #[test] fn update_refreshes_cli_graphs_and_http_urls_can_be_entries() { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); let name = http.name("entry.ts?x=1"); http.source( "entry.ts?x=1", "console.log(1, import.meta.main, import.meta.url); export {};", ); let expected = format!("1 true {name}\n"); assert_eq!(success(cli(dir.path(), &["run", "-0", &name])), expected); http.source( "entry.ts?x=1", "console.log(2, import.meta.main, import.meta.url); export {};", ); assert_eq!(success(cli(dir.path(), &["run", "-0", &name])), expected); success(cli(dir.path(), &["update", &name])); assert_eq!( success(cli( dir.path(), &["run", "--offline", "--frozen", "-0", &name] )), expected.replace("1 true", "2 true") ); let deny = format!("--deny-js-exec={}", http.url); let output = cli(dir.path(), &["run", "--offline", "-0", &deny, &name]); assert!(!output.status.success()); assert!(String::from_utf8_lossy(&output.stderr).contains("PermissionDenied")); } #[test] fn http_modules_do_not_inherit_local_aliases_or_resolve_file_urls() { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); std::fs::write(dir.path().join("avril.conf"), "imports: {dep: './dep.ts'}").unwrap(); std::fs::write( dir.path().join("dep.ts"), "throw new Error('local file executed');", ) .unwrap(); for (specifier, expected) in [ ("dep", "Unsupported module specifier: dep"), ( "file:///tmp/dep.ts", "Unsupported module specifier: file:///tmp/dep.ts", ), ( "node:fs", "node: builtin modules are not supported: node:fs", ), ] { http.source("mod.js", &format!("import '{specifier}';")); std::fs::write( dir.path().join("main.ts"), format!("import '{}';", http.name("mod.js")), ) .unwrap(); let output = cli(dir.path(), &["update", "main.ts"]); assert!(!output.status.success()); assert!( String::from_utf8_lossy(&output.stderr).contains(expected), "{specifier}: {}", String::from_utf8_lossy(&output.stderr) ); } } #[test] fn http_sources_are_vendored_and_usable_offline() { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); let name = http.name("start"); http.redirect("start", "/nested/mod.ts"); http.source("nested/mod.ts", "export {value} from './dep.ts';"); http.source("nested/dep.ts", "export const value: number = 42;"); std::fs::write( dir.path().join("main.ts"), format!("import {{value}} from '{name}'; console.log(value);"), ) .unwrap(); success(cli(dir.path(), &["vendor", "main.ts"])); std::fs::remove_dir_all(dir.path().join("cache")).unwrap(); assert_eq!( success(cli( dir.path(), &["run", "--vendor=vendor", "--offline", "--frozen", "main.ts"] )), "42\n" ); } #[test] fn complete_http_modules_obey_live_revocation_of_the_requested_url() { let http = Http::new(); let target = Http::new(); let dir = tempfile::tempdir().unwrap(); let name = http.name("start"); http.redirect("start", &target.name("mod.ts")); target.source("mod.ts", "export const value = 42;"); std::fs::write(dir.path().join("main.ts"), format!("import {{pledge}} from 'avril:std'; const a = await import('{name}'); pledge({{jsExec: {{deny: ['{}']}}}}); console.log((await import('{name}')) === a); pledge({{jsExec: {{deny: ['{name}']}}}}); try {{await import('{name}'); console.log('allowed');}} catch(e) {{console.log(e.message.includes('PermissionDenied'));}}", target.url)).unwrap(); assert_eq!( success(cli(dir.path(), &["run", "main.ts"])), "true\ntrue\n" ); }