char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
688e96e
main
1# [ path = "common/registry.rs" ] 2mod fixture; 3 4use std:: process::{ Command , Output }; 5 6fn run ( source : & str , files : & [( & str , & [ u8 ])], args : & [ & str ], values : & [( & str , & str )]) ->Output { 7let dir = tempfile:: tempdir (). unwrap (); 8 std:: fs:: create_dir ( dir. path (). join ( "app" )). unwrap (); 9 std:: fs:: write ( dir. path (). join ( "app/main.ts" ), source). unwrap (); 10for ( name, content) in files{ 11 std:: fs:: write ( dir. path (). join ( name), content). unwrap (); 12} 13Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 14. current_dir ( dir. path ()) 15. env_clear () 16. envs ( values. iter (). copied ()) 17. arg ( "run" ) 18. args ( args) 19. arg ( "app/main.ts" ) 20. output () 21. unwrap () 22} 23 24fn success ( output : Output ) ->String { 25assert! ( 26 output. status . success (), 27"{}" , 28String :: from_utf8_lossy ( & output. stderr ) 29); 30String :: from_utf8 ( output. stdout ). unwrap () 31} 32 33# [ test ] 34fn dotenv_syntax_supports_quotes_comments_exports_multiline_and_literal_values () { 35let file =concat! ( 36"\u{feff}" , 37r#" 38# ignored = comment 39no assignment here 40= ignored 41BASIC = basic # comment 42HASH=before#after 43EQUAL=first=second 44export EXPORTED = yes 45export EXPORTED_EMPTY= 46EMPTY= 47SPACE_EMPTY= 48AFTER_EMPTY=next 49SINGLE=' literal\n # and "quotes" ' 50DOUBLE="expanded\nnewline # and 'quotes'" 51BACKTICK=`literal\n # and "double" and 'single'` 52MULTI="first 53second" 54MULTI_SINGLE='first 55second' 56MULTI_BACKTICK=`first 57second` 58EMPTY_SINGLE='' 59EMPTY_DOUBLE="" 60EMPTY_BACKTICK=`` 61TRAILING="kept" # ignored "other" 62JSON={"key":"value"} 63REFERENCE=$BASIC ${EXPORTED} 64UNQUOTED_ESCAPE=literal\nnewline 65__proto__=ordinary 66UNICODE=héllo 🌸 67DUPLICATE=old 68DUPLICATE=new 69LAST=no final newline"# 70) 71. replace ( '\n' , "\r\n" ); 72let output =run ( 73"import {env} from 'avril:std'; console.log(JSON.stringify(env.toObject()));" , 74& [( "settings.env" , file. as_bytes ())], 75& [ "--env-file=settings.env" ], 76& [], 77); 78let snapshot: serde_json:: Value = serde_json:: from_str ( & success ( output)). unwrap (); 79assert_eq! ( 80 snapshot, 81 serde_json:: json!({ 82"BASIC" : "basic" , "HASH" : "before" , "EQUAL" : "first=second" , "EXPORTED" : "yes" , 83"EXPORTED_EMPTY" : "" , "EMPTY" : "" , "SPACE_EMPTY" : "" , "AFTER_EMPTY" : "next" , 84"SINGLE" : " literal\\n # and \"quotes\" " , 85"DOUBLE" : "expanded\nnewline # and 'quotes'" , 86"BACKTICK" : "literal\\n # and \"double\" and 'single'" , 87"MULTI" : "first\nsecond" , "MULTI_SINGLE" : "first\nsecond" , "MULTI_BACKTICK" : "first\nsecond" , 88"EMPTY_SINGLE" : "" , "EMPTY_DOUBLE" : "" , "EMPTY_BACKTICK" : "" , 89"TRAILING" : "kept" , "JSON" : "{\"key\":\"value\"}" , "REFERENCE" : "$BASIC ${EXPORTED}" , 90"UNQUOTED_ESCAPE" : "literal\\nnewline" , "__proto__" : "ordinary" , "UNICODE" : "héllo 🌸" , 91"DUPLICATE" : "new" , "LAST" : "no final newline" 92}) 93); 94} 95 96# [ test ] 97fn inherited_values_win_and_later_files_override_earlier_files () { 98let output =run ( 99r#" 100import {env} from 'avril:std'; 101console.log(env.get('SHARED'), env.get('HOST_EMPTY') === '', env.get('FILE'), env.get('NEW')); 102console.log(Object.keys(env.toObject()).sort().join(',')); 103"# , 104& [ 105( 106"first.env" , 107b"SHARED=first\nHOST_EMPTY=first\nFILE=first\nNEW=first\n" , 108), 109( 110"second.env" , 111b"SHARED=second\nHOST_EMPTY=second\nFILE=second\nNEW=\n" , 112), 113], 114& [ 115"-0" , 116"--allow-env=SHARED" , 117"--allow-env=HOST_EMPTY" , 118"--allow-env=FILE" , 119"--allow-env=NEW" , 120"--env-file=first.env" , 121"--env-file" , 122"second.env" , 123], 124& [ 125( "SHARED" , "inherited" ), 126( "HOST_EMPTY" , "" ), 127( "HIDDEN" , "secret" ), 128], 129); 130assert_eq! ( 131success ( output), 132"inherited true second \nFILE,HOST_EMPTY,NEW,SHARED\n" 133); 134} 135 136# [ test ] 137fn explicit_env_grants_and_pledges_apply_to_file_values_without_granting_read_access () { 138let output =run ( 139r#" 140import {env, errors, pledge} from 'avril:std'; 141import {readTextFile} from 'avril:fs'; 142console.log(env.get('ALLOWED'), env.has('EMPTY'), Object.keys(env.toObject()).sort().join(',')); 143for (const name of ['DENIED', 'AMBIENT']) { 144try {env.get(name); throw Error('Allowed');} 145catch (error) {if (!(error instanceof errors.PermissionDenied)) throw error;} 146} 147try {await readTextFile(new URL('../settings.env', import.meta.url)); throw Error('File allowed');} 148catch (error) {if (!(error instanceof errors.PermissionDenied)) throw error;} 149pledge({env: false}); 150try {env.get('ALLOWED'); throw Error('Pledge ignored');} 151catch (error) {if (!(error instanceof errors.PermissionDenied)) throw error;} 152console.log(Object.keys(env.toObject()).length); 153"# , 154& [( "settings.env" , b"ALLOWED=value\nEMPTY=\nDENIED=secret\n" )], 155& [ 156"--deny-env=DENIED" , 157"--env-file=settings.env" , 158"-0" , 159"--allow-env=ALLOWED" , 160"--allow-env=EMPTY" , 161"--allow-env=DENIED" , 162"--deny-read=settings.env" , 163], 164& [( "AMBIENT" , "secret" )], 165); 166assert_eq! ( success ( output), "value true ALLOWED,EMPTY\n0\n" ); 167} 168 169# [ test ] 170fn env_files_never_grant_environment_authority () { 171for argsin [ 172vec! [ "-0" , "--env-file=settings.env" ], 173vec! [ "--env-file" , "settings.env" , "-0" ], 174] { 175let output =run ( 176r#" 177import {env, errors} from 'avril:std'; 178for (const name of ['FILE', 'EMPTY', 'COLLISION', 'AMBIENT']) { 179for (const action of [() => env.get(name), () => env.has(name), 180() => env.set(name, 'replacement'), () => env.delete(name)]) { 181try {action(); throw Error('Allowed');} 182catch (error) {if (!(error instanceof errors.PermissionDenied)) throw error;} 183} 184} 185console.log(Object.keys(env.toObject()).length); 186"# , 187& [( 188"settings.env" , 189b"FILE=value\nEMPTY=\nCOLLISION=file-value\n" , 190)], 191& args, 192& [( "COLLISION" , "host-secret" ), ( "AMBIENT" , "host-secret" )], 193); 194assert_eq! ( success ( output), "0\n" ); 195} 196} 197 198# [ test ] 199fn file_values_have_no_node_options_special_handling () { 200let output =run ( r#" 201import {env} from 'avril:std'; 202console.log(env.get('NODE_OPTIONS'), env.get('AVRIL_CACHE_DIR'), env.get('HOME')); 203"# , & [( "settings.env" , b"NODE_OPTIONS=--not-a-real-option\nAVRIL_CACHE_DIR=/nonexistent/forbidden\nHOME=/file-home\n" )], 204& [ "--env-file=settings.env" ], & []); 205assert_eq! ( 206success ( output), 207"--not-a-real-option /nonexistent/forbidden /file-home\n" 208); 209} 210 211# [ test ] 212fn file_values_do_not_reconfigure_the_package_cache () { 213let server = fixture:: Registry :: new (); 214 server. package ( 215"@test/env" , 216"1.0.0" , 217& [( "mod.ts" , "export const value = 'cached';" )], 218& [( "." , "./mod.ts" )], 219); 220let dir = tempfile:: tempdir (). unwrap (); 221 std:: fs:: write ( 222 dir. path (). join ( "main.ts" ), 223"import {value} from 'jsr:@test/env@1'; console.log(value);" , 224) 225. unwrap (); 226 std:: fs:: write ( 227 dir. path (). join ( "settings.env" ), 228"AVRIL_CACHE_DIR=file-cache\nHOME=file-home\n" , 229) 230. unwrap (); 231let output =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 232. current_dir ( dir. path ()) 233. env_clear () 234. args ([ "run" , "--env-file=settings.env" ]) 235. arg ( format! ( "--jsr-registry={}" , server. url )) 236. arg ( "main.ts" ) 237. output () 238. unwrap (); 239assert_eq! ( success ( output), "cached\n" ); 240assert! ( dir. path (). join ( ".avril-cache" ). is_dir ()); 241assert! ( !dir. path (). join ( "file-cache" ). exists ()); 242assert! ( !dir. path (). join ( "file-home" ). exists ()); 243} 244 245# [ test ] 246fn invalid_files_fail_before_javascript_executes_without_echoing_contents () { 247for ( files, args, expected) in [ 248( vec! [], vec! [ "--env-file=missing.env" ], "missing.env" ), 249( 250vec! [( "bad.env" , & b"TOKEN=secret\0tail" [ ..])], 251vec! [ "--env-file=bad.env" ], 252"NUL" , 253), 254( 255vec! [( "bad.env" , & b"TOKEN=secret\xff" [ ..])], 256vec! [ "--env-file=bad.env" ], 257"UTF-8" , 258), 259( 260vec! [( "good.env" , & b"TOKEN=valid" [ ..])], 261vec! [ "--env-file=good.env" , "--env-file=missing.env" ], 262"missing.env" , 263), 264] { 265let output =run ( "console.log('executed');" , & files, & args, & []); 266assert! ( !output. status . success ()); 267assert! ( output. stdout . is_empty ()); 268let error =String :: from_utf8_lossy ( & output. stderr ); 269assert! ( error. contains ( expected), "{error}" ); 270assert! ( !error. contains ( "secret" ), "{error}" ); 271} 272} 273 274# [ test ] 275fn empty_comment_only_and_unclosed_quoted_files_are_handled_without_hanging () { 276let output =run ( 277"import {env} from 'avril:std'; console.log(JSON.stringify(env.toObject()));" , 278& [ 279( "empty.env" , b"" ), 280( "comments.env" , b" \t\n #comment\ninvalid\n" ), 281( 282"unclosed.env" , 283b"QUOTE=\"not closed # literal\nNEXT=parsed\nFINAL=" , 284), 285], 286& [ 287"--env-file=empty.env" , 288"--env-file=comments.env" , 289"--env-file=unclosed.env" , 290], 291& [], 292); 293let snapshot: serde_json:: Value = serde_json:: from_str ( & success ( output)). unwrap (); 294assert_eq! ( 295 snapshot, 296 serde_json:: json!({ "QUOTE" : "\"not closed # literal" , "NEXT" : "parsed" , "FINAL" : "" }) 297); 298} 299 300# [ test ] 301fn env_file_is_run_only_and_requires_a_nonempty_path () { 302for commandin [ "repl" , "cache" , "info" , "update" , "vendor" , "add" ] { 303let output =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 304. args ([ command, "--env-file=missing.env" ]) 305. output () 306. unwrap (); 307assert! ( !output. status . success ()); 308let error =String :: from_utf8_lossy ( & output. stderr ); 309assert! ( 310 error. contains ( & format! ( "--env-file is not an option of `avril {command}`" )), 311"{error}" 312); 313} 314for argsin [ 315vec! [ "--env-file=" ], 316vec! [ "--env-file" , "" ], 317vec! [ "--env-file" , "--offline" ], 318] { 319let output =run ( "console.log('executed');" , & [], & args, & []); 320assert! ( !output. status . success ()); 321assert! ( output. stdout . is_empty ()); 322assert! ( String :: from_utf8_lossy ( & output. stderr ). contains ( "Expected" )); 323} 324let output =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 325. args ([ "run" , "--env-file" ]) 326. output () 327. unwrap (); 328assert! ( !output. status . success ()); 329assert! ( String :: from_utf8_lossy ( & output. stderr ). contains ( "Expected a path" )); 330let help =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 331. args ([ "run" , "--help" ]) 332. output () 333. unwrap (); 334assert! ( success ( help). contains ( "--env-file=PATH" )); 335} 336 337# [ test ] 338fn env_flags_after_double_dash_are_entry_paths_not_options () { 339let dir = tempfile:: tempdir (). unwrap (); 340 std:: fs:: write ( 341 dir. path (). join ( "--env-file=missing.env" ), 342"console.log('entry');" , 343) 344. unwrap (); 345let output =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 346. current_dir ( dir. path ()) 347. args ([ "run" , "--" , "--env-file=missing.env" ]) 348. output () 349. unwrap (); 350assert_eq! ( success ( output), "entry\n" ); 351} 352 353# [ test ] 354fn equals_form_accepts_dash_prefixed_paths_without_changing_zero_permissions () { 355let output =run ( 356"import {env} from 'avril:std'; console.log(Object.keys(env.toObject()).join(','));" , 357& [( "-0" , b"FILE=value" )], 358& [ "--env-file=-0" , "-0" , "--allow-env=FILE" ], 359& [( "HOST" , "hidden" )], 360); 361assert_eq! ( success ( output), "FILE\n" ); 362}