char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somport http extension to typescript495c9a5

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