char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
495c9a5
main
1mod common; 2 3# [ test ] 4fn revoking_network_authority_prevents_a_pending_response () { 5let output = common:: run ( 6r#" 7import {serve,pledge} from 'avril:std'; 8const server=serve({port:0}, () => { 9pledge({net:false}); 10return new Response('must not escape'); 11}); 12try { await fetch(`http://127.0.0.1:${server.addr.port}`); throw Error('authority escaped'); } 13catch(error) {if(error.message==='authority escaped') throw error;} 14await server.shutdown(); console.log('revoked'); 15"# , 16& [], 17); 18assert_eq! ( output. stdout , b"revoked\n" ); 19} 20 21# [ test ] 22fn canceling_a_response_cancels_a_stalled_source_despite_cleanup_errors () { 23let output = common:: run ( 24r#" 25import {serve} from 'avril:std'; 26let canceled, disconnected; 27const cancel=new Promise(resolve=>canceled=resolve); 28const gone=new Promise(resolve=>disconnected=resolve); 29const server=serve({port:0}, request=>{ 30request.signal.addEventListener('abort',disconnected,{once:true}); 31return new Response(new ReadableStream({ 32start(c){c.enqueue(new Uint8Array([7]));}, 33pull(){return new Promise(()=>{});}, 34cancel(){canceled(); return Promise.reject(Error('cleanup failed'));} 35})); 36}); 37const response=await fetch(`http://127.0.0.1:${server.addr.port}`); 38const reader=response.body.getReader(); 39if((await reader.read()).value[0]!==7) throw Error('first chunk'); 40await reader.cancel(); reader.releaseLock(); 41await cancel; await gone; await server.shutdown(); console.log('canceled'); 42"# , 43& [], 44); 45assert_eq! ( output. stdout , b"canceled\n" ); 46assert! ( 47 output. stderr . is_empty (), 48"{}" , 49String :: from_utf8_lossy ( & output. stderr ) 50); 51} 52 53# [ test ] 54fn invalid_stream_chunks_close_only_the_affected_connection () { 55let output = common:: run ( 56r#" 57import {serve} from 'avril:std'; 58let badChunk; 59const reports=[]; 60addEventListener('error',event=>reports.push(event.error)); 61const ready=new Promise(resolve=>badChunk=resolve); 62const server=serve({port:0}, request=>{ 63if(new URL(request.url).pathname==='/healthy') return new Response('healthy'); 64return new Response(new ReadableStream({async start(c){ 65c.enqueue(new Uint8Array([1])); 66await ready; c.enqueue('not bytes'); c.close(); 67}})); 68}); 69const url=`http://127.0.0.1:${server.addr.port}`; 70const response=await fetch(url), reader=response.body.getReader(); 71if((await reader.read()).value[0]!==1) throw Error('first chunk'); 72badChunk(); 73try {await reader.read(); throw Error('accepted bad chunk');} 74catch(error) {if(error.message==='accepted bad chunk') throw error;} 75reader.releaseLock(); 76if(await (await fetch(url+'/healthy')).text()!=='healthy') throw Error('server stopped'); 77await server.shutdown(); 78if(reports.length!==1||!(reports[0] instanceof TypeError))throw Error('stream reports'); 79console.log('bad chunk'); 80"# , 81& [], 82); 83assert_eq! ( output. stdout , b"bad chunk\n" ); 84let stderr =String :: from_utf8 ( output. stderr ). unwrap (); 85assert_eq! ( 86 stderr 87. matches ( "TypeError: Response chunks must be Uint8Array\n" ) 88. count (), 891 , 90"{stderr}" 91); 92} 93 94# [ test ] 95fn request_body_cancellation_does_not_cancel_the_response () { 96let output = common:: run ( 97r#" 98import {serve} from 'avril:std'; 99const server=serve({port:0},async request=>{ 100await request.body.cancel('not needed'); 101if(request.signal.aborted) throw Error('body cancel aborted request'); 102return new Response('accepted'); 103}); 104const response=await fetch(`http://127.0.0.1:${server.addr.port}`,{method:'POST',body:new Uint8Array(65536)}); 105if(await response.text()!=='accepted') throw Error('response canceled'); 106await server.shutdown(); console.log('body cancel'); 107"# , 108& [], 109); 110assert_eq! ( output. stdout , b"body cancel\n" ); 111} 112 113# [ test ] 114fn serving_uses_captured_intrinsics_not_guest_promise_hooks () { 115let output = common:: run ( 116r#" 117import {serve} from 'avril:std'; 118const encode=new TextEncoder(), chunk=encode.encode('safe'); 119const requestText=Request.prototype.text; 120const originalThen=Promise.prototype.then, originalPromise=Promise; 121function observe(promise) { 122Object.defineProperty(promise,'constructor',{value:undefined}); 123const result=Reflect.apply(originalThen,promise,[value=>value,error=>{throw error;}]); 124Object.defineProperty(result,'constructor',{value:originalPromise}); 125return result; 126} 127const poison=()=>{throw Error('guest hook');}; 128Object.defineProperty(Promise.prototype,'constructor',{get:poison,configurable:true}); 129Object.defineProperty(Promise,Symbol.species,{get:poison}); 130Promise.prototype.then=poison; 131Array.prototype[Symbol.iterator]=poison; 132Request.prototype.text=poison; 133Object.defineProperty(Response.prototype,'status',{get:poison}); 134const server=serve({port:0},async request=>{ 135if(request.method!=='POST') throw Error('method'); 136if(await observe(Reflect.apply(requestText,request,[]))!=='safe') throw Error('request body'); 137return new Response(new ReadableStream({start(c){c.enqueue(chunk);c.close();}})); 138}); 139const response=await observe(fetch(`http://127.0.0.1:${server.addr.port}`,{method:'POST',body:chunk})); 140if(await observe(response.text())!=='safe') throw Error('response body'); 141await observe(server.shutdown()); console.log('intrinsics'); 142"# , 143& [], 144); 145assert_eq! ( output. stdout , b"intrinsics\n" ); 146} 147 148# [ test ] 149fn content_length_completion_does_not_report_a_disconnect () { 150let output = common:: run ( 151r#" 152import {serve} from 'avril:std'; 153let signal,canceled; 154const sourceCanceled=new Promise(resolve=>canceled=resolve); 155const server=serve({port:0},request=>{ 156signal=request.signal; 157return new Response(new ReadableStream({ 158start(c){c.enqueue(new TextEncoder().encode('exact'));}, 159cancel(){canceled();} 160}),{headers:{'content-length':'5'}}); 161}); 162const response=await fetch(`http://127.0.0.1:${server.addr.port}`); 163if(await response.text()!=='exact') throw Error('content length'); 164await sourceCanceled; 165if(signal.aborted) throw Error('normal completion aborted request'); 166await server.shutdown(); console.log('length'); 167"# , 168& [], 169); 170assert_eq! ( output. stdout , b"length\n" ); 171} 172 173# [ test ] 174fn response_writes_retain_native_buffer_checks_and_release_the_reader () { 175let output = common:: run ( 176r#" 177import {serve} from 'avril:std'; 178const reports = []; 179addEventListener('error', event => {reports.push(event.error); event.preventDefault();}); 180for (const [chunk, expected] of [ 181[new Uint8Array(new SharedArrayBuffer(1)), 'buffer'], 182[new Uint8Array(new ArrayBuffer(1, {maxByteLength:4})), 'resizable'] 183]) { 184let releaseChunk, canceled, body; 185const gate = new Promise(resolve => releaseChunk = resolve); 186const cleanup = new Promise(resolve => canceled = resolve); 187const server = serve({port:0}, request => { 188if (new URL(request.url).pathname === '/healthy') return new Response('healthy'); 189body = new ReadableStream({ 190async start(controller) { 191controller.enqueue(new Uint8Array([7])); 192await gate; controller.enqueue(chunk); 193}, 194cancel() {canceled();} 195}); 196return new Response(body); 197}); 198const url = `http://127.0.0.1:${server.addr.port}`; 199const response = await fetch(url), reader = response.body.getReader(); 200if ((await reader.read()).value[0] !== 7) throw Error('first chunk'); 201releaseChunk(); 202try {await reader.read(); throw Error('accepted unsupported buffer');} 203catch (error) {if (error.message === 'accepted unsupported buffer') throw error;} 204reader.releaseLock(); 205await cleanup; 206if (body.locked) throw Error('response reader retained'); 207const error = reports.pop(); 208if (!(error instanceof TypeError) || !error.message.toLowerCase().includes(expected) || reports.length) throw Error('native write report'); 209if (await (await fetch(url+'/healthy')).text() !== 'healthy') throw Error('server stopped'); 210await server.shutdown(); 211} 212console.log('native writes and cleanup'); 213"# , 214& [], 215); 216assert_eq! ( output. stdout , b"native writes and cleanup\n" ); 217assert! ( 218 output. stderr . is_empty (), 219"{}" , 220String :: from_utf8_lossy ( & output. stderr ) 221); 222} 223 224# [ test ] 225fn invalid_framing_headers_fail_before_sending_a_response () { 226let output = common:: run ( 227r#" 228import {serve} from 'avril:std'; 229const server=serve({port:0},request=>{ 230const path=new URL(request.url).pathname; 231if(path==='/length') return new Response('short',{headers:{'content-length':'100'}}); 232if(path==='/invalid') return new Response('short',{headers:{'content-length':'5, 5'}}); 233return new Response('short',{headers:{'transfer-encoding':'chunked'}}); 234}); 235const url=`http://127.0.0.1:${server.addr.port}`; 236for(const path of ['/length','/invalid','/encoding']) { 237const response=await fetch(url+path); 238if(response.status!==500 || await response.text()!=='Internal Server Error') throw Error(path); 239} 240await server.shutdown(); console.log('framing'); 241"# , 242& [], 243); 244assert_eq! ( output. stdout , b"framing\n" ); 245} 246 247# [ test ] 248fn terminating_a_worker_releases_its_listener () { 249let dir = tempfile:: tempdir (). unwrap (); 250let path = dir. path (). join ( "worker.js" ); 251 std:: fs:: write ( 252& path, 253r#" 254import {serve} from 'avril:std'; 255const server=serve({port:0},()=>new Response('worker')); 256postMessage(server.addr.port); 257await server.finished; 258"# , 259) 260. unwrap (); 261let url = url:: Url :: from_file_path ( path). unwrap (); 262let output = common:: run ( 263& format! ( 264r#" 265import {{serve}} from 'avril:std'; 266const worker=new Worker('{url}',{{type:'module'}}); 267const port=await new Promise(resolve=>worker.onmessage=e=>resolve(e.data)); 268if(await (await fetch(`http://127.0.0.1:${{port}}`)).text()!=='worker') throw Error('worker listener'); 269worker.terminate(); 270let rebound; 271for(let i=0;i<100&&!rebound;i++) {{ 272try {{rebound=serve({{port}},()=>new Response('parent'));}} 273catch {{await new Promise(resolve=>setTimeout(resolve,5));}} 274}} 275if(!rebound) throw Error('worker leaked listener'); 276await rebound.shutdown(); console.log('terminated'); 277"# 278), 279& [], 280); 281assert_eq! ( output. stdout , b"terminated\n" ); 282}