char/avril

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

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

Charlotte Somsnapshot: embed shared main and worker startup in the CLI688e96e

main
16.4 KiB470 linesraw
1#[path = "common/registry.rs"]
2mod fixture;
3
4#[path = "common/http.rs"]
5mod http_fixture;
6
7use http_fixture::{Http, Response};
8
9use avril_runtime::caps::{Capabilities, Category, Initial};
10use avril_runtime::registry::{Lockfile, PackageStore, checksum};
11use fixture::Registry;
12use futures::executor::block_on;
13
14fn read_lock(path: &std::path::Path) -> Lockfile {
15    let bytes = std::fs::read(path).unwrap();
16    let source = std::str::from_utf8(&bytes).unwrap();
17    serde_json::from_value(serde_fjson::parse(source).unwrap()).unwrap()
18}
19
20#[test]
21fn redirects_pin_the_final_url_and_only_authorize_the_requested_url() {
22    let registry = Registry::new();
23    let http = Http::new();
24    let target = Http::new();
25    http.redirect("mod.ts", &target.name("nested/final.ts"));
26    target.source("nested/final.ts", "export const value = 1;");
27    target.source("nested/dep.ts", "export {};");
28    let dir = tempfile::tempdir().unwrap();
29    let options = registry.options(dir.path());
30    let name = http.name("mod.ts");
31    let mut initial = Initial::new(true);
32    initial
33        .grant(Category::JsExec, false, &name, dir.path())
34        .unwrap();
35    initial
36        .grant(Category::JsExec, true, target.url.as_str(), dir.path())
37        .unwrap();
38    let caps = Capabilities::new(initial, dir.path().to_owned());
39    let store = PackageStore::new(fixture::runtime_handle(), options.clone())
40        .unwrap()
41        .with_capabilities(caps.clone());
42    assert!(store.remote_base(&name).is_err());
43    let (a, b) = block_on(futures::future::join(
44        store.remote_source(&name),
45        store.remote_source(&name),
46    ));
47    assert_eq!(a.unwrap(), "export const value = 1;");
48    assert_eq!(b.unwrap(), "export const value = 1;");
49    assert_eq!(
50        store.remote_base(&name).unwrap(),
51        target.name("nested/final.ts")
52    );
53    assert!(
54        block_on(store.remote_source(&target.name("nested/dep.ts")))
55            .unwrap_err()
56            .to_string()
57            .contains("PermissionDenied")
58    );
59    assert_eq!(http.requests.lock().unwrap().len(), 1);
60    assert_eq!(target.requests.lock().unwrap().len(), 1);
61    store.save().unwrap();
62    let lock = read_lock(&options.lock);
63    assert_eq!(
64        lock.remote[&name].checksum,
65        checksum(b"export const value = 1;")
66    );
67    assert_eq!(lock.remote[&name].url, target.name("nested/final.ts"));
68    caps.pledge(vec![(Category::JsExec, Some(vec![]), vec![])])
69        .unwrap();
70    assert!(
71        block_on(store.remote_source(&name))
72            .unwrap_err()
73            .to_string()
74            .contains("PermissionDenied")
75    );
76    let mut offline = options;
77    offline.offline = true;
78    offline.frozen = true;
79    let denied = PackageStore::new(fixture::runtime_handle(), offline.clone())
80        .unwrap()
81        .with_capabilities(caps);
82    assert!(
83        block_on(denied.remote_source(&name))
84            .unwrap_err()
85            .to_string()
86            .contains("PermissionDenied")
87    );
88    let store = PackageStore::new(fixture::runtime_handle(), offline).unwrap();
89    block_on(store.remote_source(&name)).unwrap();
90    assert_eq!(
91        store.remote_base(&name).unwrap(),
92        target.name("nested/final.ts")
93    );
94    assert_eq!(http.requests.lock().unwrap().len(), 1);
95}
96
97#[test]
98fn updates_refresh_once_and_preserve_other_projects_content_pins() {
99    let registry = Registry::new();
100    let http = Http::new();
101    http.source("mod.js", "export const value = 1;");
102    let dir = tempfile::tempdir().unwrap();
103    let options = registry.options(dir.path());
104    let name = http.name("mod.js");
105    let original = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
106    block_on(original.remote_source(&name)).unwrap();
107    original.save().unwrap();
108    http.source("mod.js", "export const value = 2;");
109    let stable = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
110    assert_eq!(
111        block_on(stable.remote_source(&name)).unwrap(),
112        "export const value = 1;"
113    );
114    let mut other = options.clone();
115    other.lock = dir.path().join("other.lock");
116    other.update = true;
117    let update = PackageStore::new(fixture::runtime_handle(), other.clone()).unwrap();
118    let (a, b) = block_on(futures::future::join(
119        update.remote_source(&name),
120        update.remote_source(&name),
121    ));
122    assert_eq!(a.unwrap(), "export const value = 2;");
123    assert_eq!(b.unwrap(), "export const value = 2;");
124    http.source("mod.js", "export const value = 3;");
125    assert_eq!(
126        block_on(update.remote_source(&name)).unwrap(),
127        "export const value = 2;"
128    );
129    update.save().unwrap();
130    assert_eq!(http.requests.lock().unwrap().len(), 2);
131    let mut offline = options;
132    offline.offline = true;
133    offline.frozen = true;
134    let store = PackageStore::new(fixture::runtime_handle(), offline).unwrap();
135    assert_eq!(
136        block_on(store.remote_source(&name)).unwrap(),
137        "export const value = 1;"
138    );
139    other.update = false;
140    other.lock = dir.path().join("unlocked.lock");
141    let store = PackageStore::new(fixture::runtime_handle(), other).unwrap();
142    assert_eq!(
143        block_on(store.remote_source(&name)).unwrap(),
144        "export const value = 2;"
145    );
146}
147
148#[test]
149fn vendored_sources_need_only_the_lock_and_vendor_directory() {
150    let registry = Registry::new();
151    let http = Http::new();
152    http.redirect("mod.ts?version=1", "/nested/final.ts");
153    http.source("nested/final.ts", "export {};");
154    let dir = tempfile::tempdir().unwrap();
155    let mut options = registry.options(dir.path());
156    let name = http.name("mod.ts?version=1");
157    let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
158    block_on(store.remote_source(&name)).unwrap();
159    store.save().unwrap();
160    let vendor = dir.path().join("vendor");
161    store.vendor(&vendor).unwrap();
162    let relative = store.vendor_relative(&name).unwrap();
163    assert!(relative.starts_with("http"));
164    assert_eq!(relative.extension().unwrap(), "ts");
165    assert_eq!(std::fs::read(vendor.join(relative)).unwrap(), b"export {};");
166    options.vendor = Some(vendor);
167    options.cache = dir.path().join("empty-cache");
168    options.offline = true;
169    options.frozen = true;
170    let store = PackageStore::new(fixture::runtime_handle(), options).unwrap();
171    assert_eq!(block_on(store.remote_source(&name)).unwrap(), "export {};");
172    assert_eq!(
173        store.remote_base(&name).unwrap(),
174        http.name("nested/final.ts")
175    );
176    assert_eq!(http.requests.lock().unwrap().len(), 2);
177}
178
179#[test]
180fn graph_saves_merge_filter_conflict_and_prune_remote_entries() {
181    let registry = Registry::new();
182    let http = Http::new();
183    http.source("a.js", "export const value = 1;");
184    http.source("b.js", "export {};");
185    let dir = tempfile::tempdir().unwrap();
186    let options = registry.options(dir.path());
187    let a = http.name("a.js");
188    let b = http.name("b.js");
189    let first = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
190    let second = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
191    block_on(first.remote_source(&a)).unwrap();
192    block_on(first.remote_source(&b)).unwrap();
193    first
194        .save_graph(&Default::default(), &[a.clone()].into())
195        .unwrap();
196    assert_eq!(read_lock(&options.lock).remote.len(), 1);
197    block_on(second.remote_source(&b)).unwrap();
198    second.save().unwrap();
199    assert_eq!(read_lock(&options.lock).remote.len(), 2);
200    let stale = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
201    block_on(stale.remote_source(&a)).unwrap();
202    let mut updated = options.clone();
203    updated.update = true;
204    http.source("a.js", "export const value = 2;");
205    let update = PackageStore::new(fixture::runtime_handle(), updated).unwrap();
206    block_on(update.remote_source(&a)).unwrap();
207    update.save().unwrap();
208    assert!(
209        stale
210            .save()
211            .unwrap_err()
212            .to_string()
213            .contains("changed concurrently")
214    );
215    stale
216        .save_graph(&Default::default(), &[b.clone()].into())
217        .unwrap();
218    let prune = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
219    block_on(prune.remote_source(&b)).unwrap();
220    prune.prune().unwrap();
221    let lock = read_lock(&options.lock);
222    assert_eq!(lock.remote.len(), 1);
223    assert!(lock.remote.contains_key(&b));
224}
225
226#[test]
227fn rejects_html_credentials_unsupported_redirects_and_redirect_loops() {
228    let registry = Registry::new();
229    let http = Http::new();
230    let dir = tempfile::tempdir().unwrap();
231    let store = PackageStore::new(fixture::runtime_handle(), registry.options(dir.path())).unwrap();
232    http.source("html.js", "<!DOCTYPE html><html></html>");
233    http.source("mime.js", "export {};");
234    http.routes
235        .lock()
236        .unwrap()
237        .get_mut("/mime.js")
238        .unwrap()
239        .headers = "Content-Type: TEXT/HTML; charset=utf-8\r\n".into();
240    http.redirect("loop.js", "/loop.js");
241    http.redirect("file.js", "file:///tmp/module.js");
242    http.redirect(
243        "credentials.js",
244        &http
245            .name("secret.js")
246            .replace("http://", "http://user:pass@"),
247    );
248    http.source("secret.js", "export {};");
249    for (path, message) in [
250        ("html.js", "HTML"),
251        ("mime.js", "HTML"),
252        ("loop.js", "Too many"),
253        ("file.js", "HTTP(S)"),
254        ("credentials.js", "credentials"),
255    ] {
256        assert!(
257            block_on(store.remote_source(&http.name(path)))
258                .unwrap_err()
259                .to_string()
260                .contains(message),
261            "{path}"
262        );
263    }
264    assert!(!http.requests.lock().unwrap().contains(&"/secret.js".into()));
265    assert_eq!(
266        http.requests
267            .lock()
268            .unwrap()
269            .iter()
270            .filter(|path| path.as_str() == "/loop.js")
271            .count(),
272        11
273    );
274    let credentialed = http
275        .name("secret.js")
276        .replace("http://", "http://user:pass@");
277    assert!(
278        block_on(store.remote_source(&credentialed))
279            .unwrap_err()
280            .to_string()
281            .contains("credentials")
282    );
283    assert!(block_on(store.remote_source("file:///tmp/mod.js")).is_err());
284}
285
286#[test]
287fn rejects_revocation_during_download_and_missing_frozen_pins() {
288    let registry = Registry::new();
289    let http = Http::new();
290    http.source("mod.js", "export {};");
291    let dir = tempfile::tempdir().unwrap();
292    let options = registry.options(dir.path());
293    let name = http.name("mod.js");
294    let mut initial = Initial::new(true);
295    initial
296        .grant(Category::JsExec, false, &name, dir.path())
297        .unwrap();
298    let caps = Capabilities::new(initial, dir.path().to_owned());
299    let store = PackageStore::new(fixture::runtime_handle(), options.clone())
300        .unwrap()
301        .with_capabilities(caps.clone());
302    *http.hook.lock().unwrap() = Some(Box::new(move || {
303        caps.pledge(vec![(Category::JsExec, Some(vec![]), vec![])])
304            .unwrap();
305    }));
306    assert!(
307        block_on(store.remote_source(&name))
308            .unwrap_err()
309            .to_string()
310            .contains("PermissionDenied")
311    );
312    store.save().unwrap();
313    assert!(!options.lock.exists());
314    let lock = format!(
315        "version: 1\njsrRegistry: \"{}\"\nspecifiers: {{}}\npackages: {{}}\n",
316        registry.url
317    );
318    std::fs::write(&options.lock, lock).unwrap();
319    let mut frozen = options;
320    frozen.frozen = true;
321    let store = PackageStore::new(fixture::runtime_handle(), frozen).unwrap();
322    assert!(
323        block_on(store.remote_source(&name))
324            .unwrap_err()
325            .to_string()
326            .contains("Frozen")
327    );
328    assert_eq!(http.requests.lock().unwrap().len(), 1);
329}
330
331#[test]
332fn pinned_downloads_reject_changed_final_urls_even_with_identical_bytes() {
333    let registry = Registry::new();
334    let http = Http::new();
335    http.redirect("mod.js", "/a.js");
336    http.source("a.js", "export {};");
337    http.source("b.js", "export {};");
338    let dir = tempfile::tempdir().unwrap();
339    let mut options = registry.options(dir.path());
340    let name = http.name("mod.js");
341    let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
342    block_on(store.remote_source(&name)).unwrap();
343    store.save().unwrap();
344    options.cache = dir.path().join("empty-cache");
345    http.redirect("mod.js", "/b.js");
346    let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
347    assert!(
348        block_on(store.remote_source(&name))
349            .unwrap_err()
350            .to_string()
351            .contains("final URL mismatch")
352    );
353    http.redirect("mod.js", "/a.js");
354    http.source("a.js", "export const changed = true;");
355    let store = PackageStore::new(fixture::runtime_handle(), options).unwrap();
356    assert!(
357        block_on(store.remote_source(&name))
358            .unwrap_err()
359            .to_string()
360            .contains("integrity mismatch")
361    );
362}
363
364#[test]
365fn registry_prefix_sources_still_require_the_manifest() {
366    let registry = Registry::new();
367    registry.package(
368        "@test/pkg",
369        "1.0.0",
370        &[("mod.ts", "export {};")],
371        &[(".", "./mod.ts")],
372    );
373    let dir = tempfile::tempdir().unwrap();
374    let options = registry.options(dir.path());
375    let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
376    let name = block_on(store.resolve("jsr:@test/pkg@1")).unwrap();
377    assert_eq!(block_on(store.remote_source(&name)).unwrap(), "export {};");
378    store.save().unwrap();
379    assert!(read_lock(&options.lock).remote.is_empty());
380    assert!(
381        registry
382            .requests
383            .lock()
384            .unwrap()
385            .contains(&"/@test/pkg/1.0.0_meta.json".into())
386    );
387    let private = registry
388        .url
389        .join("@test/pkg/1.0.0/private.ts")
390        .unwrap()
391        .to_string();
392    assert!(
393        block_on(store.remote_source(&private))
394            .unwrap_err()
395            .to_string()
396            .contains("manifest")
397    );
398}
399
400#[test]
401fn rejects_oversized_and_non_utf8_sources() {
402    let registry = Registry::new();
403    let http = Http::new();
404    let dir = tempfile::tempdir().unwrap();
405    let store = PackageStore::new(fixture::runtime_handle(), registry.options(dir.path())).unwrap();
406    for (path, body, message) in [
407        (
408            "large.js",
409            vec![b' '; avril_runtime::registry::MAX_ARTIFACT_SIZE + 1],
410            "too large",
411        ),
412        ("invalid.js", vec![255], "UTF-8"),
413    ] {
414        http.routes.lock().unwrap().insert(
415            format!("/{path}"),
416            Response {
417                status: 200,
418                headers: String::new(),
419                body,
420            },
421        );
422        assert!(
423            block_on(store.remote_source(&http.name(path)))
424                .unwrap_err()
425                .to_string()
426                .contains(message)
427        );
428    }
429}
430
431#[test]
432fn corrupted_cached_http_source_fails_integrity_verification() {
433    let registry = Registry::new();
434    let http = Http::new();
435    http.source("mod.js", "export {};");
436    let dir = tempfile::tempdir().unwrap();
437    let options = registry.options(dir.path());
438    let name = http.name("mod.js");
439    let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
440    block_on(store.remote_source(&name)).unwrap();
441    store.save().unwrap();
442    let hash = read_lock(&options.lock).remote[&name].checksum.clone();
443    std::fs::write(options.cache.join("http/blobs").join(hash), b"tampered").unwrap();
444    let store = PackageStore::new(fixture::runtime_handle(), options).unwrap();
445    assert!(
446        block_on(store.remote_source(&name))
447            .unwrap_err()
448            .to_string()
449            .contains("integrity mismatch")
450    );
451}
452
453#[test]
454fn missing_unpinned_blobs_are_reacquired_without_enforcing_stale_metadata() {
455    let registry = Registry::new();
456    let http = Http::new();
457    let dir = tempfile::tempdir().unwrap();
458    let options = registry.options(dir.path());
459    let name = http.name("mod.ts");
460    http.source("mod.ts", "export const value = 1;");
461    let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
462    block_on(store.remote_source(&name)).unwrap();
463    std::fs::remove_dir_all(options.cache.join("http/blobs")).unwrap();
464    http.source("mod.ts", "export const value = 2;");
465    let store = PackageStore::new(fixture::runtime_handle(), options).unwrap();
466    assert_eq!(
467        block_on(store.remote_source(&name)).unwrap(),
468        "export const value = 2;"
469    );
470}