char/avril

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

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

Charlotte Somadd progress to module imports from registries7b01146

main
18.8 KiB581 linesraw
1#[path = "common/registry.rs"]
2mod fixture;
3
4use avril_runtime::caps::{Capabilities, Category, Initial};
5use avril_runtime::registry::{PackageStore, ProgressEvent, artifact_key};
6use fixture::Registry;
7use futures::executor::block_on;
8use serde_json::json;
9use std::sync::{Arc, Mutex};
10
11#[test]
12fn selects_versions_exports_and_verifies_manifest_sources() {
13    let server = Registry::new();
14    server.package(
15        "@test/pkg",
16        "1.0.0",
17        &[("mod.ts", "export const value = 1;")],
18        &[(".", "./mod.ts")],
19    );
20    server.package(
21        "@test/pkg",
22        "1.1.0",
23        &[
24            ("mod.ts", "export const value = 2;"),
25            ("sub.ts", "export {};"),
26        ],
27        &[(".", "./mod.ts"), ("./sub", "./sub.ts")],
28    );
29    server.package(
30        "@test/pkg",
31        "2.0.0",
32        &[("mod.ts", "export const value = 3;")],
33        &[(".", "./mod.ts")],
34    );
35    let dir = tempfile::tempdir().unwrap();
36    let store = PackageStore::new(fixture::runtime_handle(), server.options(dir.path())).unwrap();
37    let module = block_on(store.resolve("jsr:@test/pkg@^1")).unwrap();
38    assert!(module.ends_with("/@test/pkg/1.1.0/mod.ts"));
39    assert_eq!(
40        block_on(store.source(&module)).unwrap(),
41        "export const value = 2;"
42    );
43    assert!(
44        block_on(store.resolve("jsr:@test/pkg@^1/sub"))
45            .unwrap()
46            .ends_with("/sub.ts")
47    );
48    assert!(
49        block_on(store.resolve("jsr:@test/pkg@^2"))
50            .unwrap()
51            .contains("/2.0.0/")
52    );
53    assert!(
54        block_on(store.resolve("jsr:@test/pkg@^1/private"))
55            .unwrap_err()
56            .to_string()
57            .contains("no export")
58    );
59    assert!(block_on(store.resolve("jsr:@test/pkg@latest")).is_err());
60    assert!(block_on(store.resolve("jsr:unscoped@1")).is_err());
61    store.save().unwrap();
62    let lock = fixture::read_lock(dir.path());
63    assert_eq!(lock["version"], 1);
64    assert_eq!(lock["specifiers"]["jsr:@test/pkg@^1"], "1.1.0");
65    assert_eq!(lock["specifiers"]["jsr:@test/pkg@^2"], "2.0.0");
66}
67
68#[test]
69fn written_locks_are_plain_fjson_with_a_final_newline() {
70    let server = Registry::new();
71    server.package(
72        "@test/pkg",
73        "1.0.0",
74        &[("mod.js", "export {};")],
75        &[(".", "./mod.js")],
76    );
77    let dir = tempfile::tempdir().unwrap();
78    let store = PackageStore::new(fixture::runtime_handle(), server.options(dir.path())).unwrap();
79    block_on(store.resolve("jsr:@test/pkg@1")).unwrap();
80    store.save().unwrap();
81    let text = std::fs::read_to_string(dir.path().join("avril.lock")).unwrap();
82    let lock = fixture::read_lock(dir.path());
83    assert_eq!(
84        text,
85        format!("{}\n", serde_fjson::stringify(&lock).unwrap())
86    );
87}
88
89#[test]
90fn ephemeral_stores_resolve_and_save_without_writing_a_lock() {
91    let server = Registry::new();
92    server.package(
93        "@test/pkg",
94        "1.0.0",
95        &[("mod.ts", "export const value = 1;")],
96        &[(".", "./mod.ts")],
97    );
98    server.package(
99        "@test/pkg",
100        "1.1.0",
101        &[("mod.ts", "export const value = 2;")],
102        &[(".", "./mod.ts")],
103    );
104    let dir = tempfile::tempdir().unwrap();
105    let mut options = server.options(dir.path());
106    options.ephemeral_lock = true;
107    let store = PackageStore::new(fixture::runtime_handle(), options).unwrap();
108    let module = block_on(store.resolve("jsr:@test/pkg@^1")).unwrap();
109    assert!(module.ends_with("/@test/pkg/1.1.0/mod.ts"));
110    assert_eq!(
111        block_on(store.source(&module)).unwrap(),
112        "export const value = 2;"
113    );
114    store.save().unwrap();
115    store
116        .save_graph(&["jsr:@test/pkg@^1".into()].into(), &[module].into())
117        .unwrap();
118    assert!(!dir.path().join("avril.lock").exists());
119}
120
121#[test]
122fn pinned_versions_are_stable_until_an_explicit_update_and_yanks_are_skipped() {
123    let server = Registry::new();
124    server.package(
125        "@test/pkg",
126        "1.0.0",
127        &[("mod.js", "export {};")],
128        &[(".", "./mod.js")],
129    );
130    let dir = tempfile::tempdir().unwrap();
131    let options = server.options(dir.path());
132    let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
133    assert!(
134        block_on(store.resolve("jsr:@test/pkg@^1"))
135            .unwrap()
136            .contains("/1.0.0/")
137    );
138    store.save().unwrap();
139    drop(store);
140    server.package(
141        "@test/pkg",
142        "1.1.0",
143        &[("mod.js", "export {};")],
144        &[(".", "./mod.js")],
145    );
146    server.package(
147        "@test/pkg",
148        "1.2.0",
149        &[("mod.js", "export {};")],
150        &[(".", "./mod.js")],
151    );
152    let mut index: serde_json::Value = serde_json::from_slice(
153        server
154            .files
155            .lock()
156            .unwrap()
157            .get("/@test/pkg/meta.json")
158            .unwrap(),
159    )
160    .unwrap();
161    index["versions"]["1.2.0"] = json!({"yanked": true});
162    server.files.lock().unwrap().insert(
163        "/@test/pkg/meta.json".into(),
164        serde_json::to_vec(&index).unwrap(),
165    );
166    let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
167    assert!(
168        block_on(store.resolve("jsr:@test/pkg@^1"))
169            .unwrap()
170            .contains("/1.0.0/")
171    );
172    drop(store);
173    let mut options = options;
174    options.update = true;
175    let store = PackageStore::new(fixture::runtime_handle(), options).unwrap();
176    assert!(
177        block_on(store.resolve("jsr:@test/pkg@^1"))
178            .unwrap()
179            .contains("/1.1.0/")
180    );
181    store.save().unwrap();
182}
183
184#[test]
185fn concurrent_acquisitions_share_downloads_and_independent_saves_merge() {
186    let server = Registry::new();
187    for name in ["@test/a", "@test/b"] {
188        server.package(
189            name,
190            "1.0.0",
191            &[("mod.ts", "export {};")],
192            &[(".", "./mod.ts")],
193        );
194    }
195    let dir = tempfile::tempdir().unwrap();
196    let options = server.options(dir.path());
197    let a = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
198    let b = PackageStore::new(fixture::runtime_handle(), options).unwrap();
199    let (first, second) = block_on(futures::future::join(
200        a.resolve("jsr:@test/a@1"),
201        a.resolve("jsr:@test/a@1"),
202    ));
203    assert_eq!(first.as_ref().unwrap(), second.as_ref().unwrap());
204    let name = first.unwrap();
205    let (first, second) = block_on(futures::future::join(a.source(&name), a.source(&name)));
206    assert_eq!(first.unwrap(), second.unwrap());
207    block_on(b.resolve("jsr:@test/b@1")).unwrap();
208    a.save().unwrap();
209    b.save().unwrap();
210    let requests = server.requests.lock().unwrap();
211    for path in [
212        "/@test/a/meta.json",
213        "/@test/a/1.0.0_meta.json",
214        "/@test/a/1.0.0/mod.ts",
215    ] {
216        assert_eq!(
217            requests
218                .iter()
219                .filter(|request| request.as_str() == path)
220                .count(),
221            1,
222            "{requests:?}"
223        );
224    }
225    let lock = fixture::read_lock(dir.path());
226    assert_eq!(lock["specifiers"].as_object().unwrap().len(), 2);
227}
228
229#[test]
230fn offline_frozen_and_vendored_runs_need_no_network() {
231    let server = Registry::new();
232    server.package(
233        "@test/pkg",
234        "1.0.0",
235        &[("mod.ts", "export {};")],
236        &[(".", "./mod.ts")],
237    );
238    let dir = tempfile::tempdir().unwrap();
239    let mut options = server.options(dir.path());
240    let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
241    let module = block_on(store.resolve("jsr:@test/pkg@1")).unwrap();
242    block_on(store.source(&module)).unwrap();
243    store.save().unwrap();
244    let vendor = dir.path().join("vendor");
245    store.vendor(&vendor).unwrap();
246    drop(store);
247    server.requests.lock().unwrap().clear();
248    options.offline = true;
249    options.frozen = true;
250    options.vendor = Some(vendor);
251    options.cache = dir.path().join("empty-cache");
252    let store = PackageStore::new(fixture::runtime_handle(), options).unwrap();
253    assert_eq!(block_on(store.resolve("jsr:@test/pkg@1")).unwrap(), module);
254    assert_eq!(block_on(store.source(&module)).unwrap(), "export {};");
255    assert!(
256        block_on(store.resolve("jsr:@test/pkg@2"))
257            .unwrap_err()
258            .to_string()
259            .contains("Frozen")
260    );
261    assert!(server.requests.lock().unwrap().is_empty());
262}
263
264#[test]
265fn corrupted_sources_and_locked_metadata_fail_closed() {
266    let server = Registry::new();
267    server.package(
268        "@test/pkg",
269        "1.0.0",
270        &[("mod.ts", "export {};")],
271        &[(".", "./mod.ts")],
272    );
273    let dir = tempfile::tempdir().unwrap();
274    let options = server.options(dir.path());
275    let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
276    let name = block_on(store.resolve("jsr:@test/pkg@1")).unwrap();
277    block_on(store.source(&name)).unwrap();
278    store.save().unwrap();
279    drop(store);
280    std::fs::write(
281        options.cache.join("files").join(artifact_key(&name)),
282        "console.log('tampered');",
283    )
284    .unwrap();
285    let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
286    assert!(
287        block_on(store.source(&name))
288            .unwrap_err()
289            .to_string()
290            .contains("integrity mismatch")
291    );
292    drop(store);
293    let metadata_url = server.url.join("@test/pkg/1.0.0_meta.json").unwrap();
294    std::fs::write(
295        options
296            .cache
297            .join("files")
298            .join(artifact_key(metadata_url.as_str())),
299        "{}",
300    )
301    .unwrap();
302    let store = PackageStore::new(fixture::runtime_handle(), options).unwrap();
303    assert!(
304        block_on(store.resolve("jsr:@test/pkg@1"))
305            .unwrap_err()
306            .to_string()
307            .contains("integrity mismatch")
308    );
309}
310
311#[test]
312fn rejects_unexported_or_escaping_files_and_registry_changes() {
313    let server = Registry::new();
314    server.package(
315        "@test/pkg",
316        "1.0.0",
317        &[("mod.ts", "export {};")],
318        &[(".", "../other.ts")],
319    );
320    let dir = tempfile::tempdir().unwrap();
321    let options = server.options(dir.path());
322    let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
323    assert!(block_on(store.resolve("jsr:@test/pkg@1")).is_err());
324    assert!(
325        block_on(
326            store.source(
327                server
328                    .url
329                    .join("@test/pkg/1.0.0/private.ts")
330                    .unwrap()
331                    .as_str()
332            )
333        )
334        .unwrap_err()
335        .to_string()
336        .contains("manifest")
337    );
338    assert!(store.module_base("https://example.com/a.js").is_err());
339    assert!(
340        store
341            .module_base(&format!("{}@test/pkg/1.0.0/mod.ts?query", server.url))
342            .is_err()
343    );
344    let mut invalid = options.clone();
345    invalid.jsr_registry = url::Url::parse("http://example.com/").unwrap();
346    assert!(
347        PackageStore::new(fixture::runtime_handle(), invalid)
348            .err()
349            .unwrap()
350            .to_string()
351            .contains("HTTPS")
352    );
353    let mut frozen = options;
354    frozen.frozen = true;
355    assert!(
356        PackageStore::new(fixture::runtime_handle(), frozen)
357            .err()
358            .unwrap()
359            .to_string()
360            .contains("does not exist")
361    );
362}
363
364#[test]
365fn lock_saves_ignore_untouched_stale_entries_but_detect_used_conflicts() {
366    let server = Registry::new();
367    for name in ["@test/a", "@test/b"] {
368        server.package(
369            name,
370            "1.0.0",
371            &[("mod.js", "export {};")],
372            &[(".", "./mod.js")],
373        );
374    }
375    let dir = tempfile::tempdir().unwrap();
376    let options = server.options(dir.path());
377    let initial = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
378    block_on(initial.resolve("jsr:@test/a@1")).unwrap();
379    initial.save().unwrap();
380    let stale = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
381    let used = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
382    block_on(used.resolve("jsr:@test/a@1")).unwrap();
383    server.package(
384        "@test/a",
385        "1.1.0",
386        &[("mod.js", "export {};")],
387        &[(".", "./mod.js")],
388    );
389    let mut update_options = options.clone();
390    update_options.update = true;
391    let update = PackageStore::new(fixture::runtime_handle(), update_options).unwrap();
392    block_on(update.resolve("jsr:@test/a@1")).unwrap();
393    update.save().unwrap();
394    block_on(stale.resolve("jsr:@test/b@1")).unwrap();
395    stale.save().unwrap();
396    assert!(
397        used.save()
398            .unwrap_err()
399            .to_string()
400            .contains("changed concurrently")
401    );
402    let lock = fixture::read_lock(dir.path());
403    assert_eq!(lock["specifiers"]["jsr:@test/a@1"], "1.1.0");
404    assert_eq!(lock["specifiers"]["jsr:@test/b@1"], "1.0.0");
405}
406
407#[test]
408fn relative_registry_imports_cannot_cross_package_or_version_boundaries() {
409    let server = Registry::new();
410    let dir = tempfile::tempdir().unwrap();
411    let store = PackageStore::new(fixture::runtime_handle(), server.options(dir.path())).unwrap();
412    let referrer = server
413        .url
414        .join("@test/pkg/1.0.0/sub/main.ts")
415        .unwrap()
416        .to_string();
417    assert!(
418        store
419            .resolve_relative("../dep.ts", &referrer)
420            .unwrap()
421            .ends_with("/1.0.0/dep.ts")
422    );
423    for specifier in [
424        "../../2.0.0/mod.ts",
425        "../../../other/1.0.0/mod.ts",
426        "../../../../@other/pkg/1.0.0/mod.ts",
427        "../..%2fsecret.ts",
428        "https://example.com/module.ts",
429        "./dep.ts?query",
430    ] {
431        assert!(
432            store.resolve_relative(specifier, &referrer).is_err(),
433            "{specifier}"
434        );
435    }
436}
437
438#[cfg(unix)]
439#[test]
440fn atomic_outputs_preserve_existing_permissions_and_reject_special_files() {
441    use std::os::unix::fs::PermissionsExt;
442    let dir = tempfile::tempdir().unwrap();
443    let output = dir.path().join("avril.lock");
444    std::fs::write(&output, "old").unwrap();
445    std::fs::set_permissions(&output, std::fs::Permissions::from_mode(0o640)).unwrap();
446    avril_runtime::registry::atomic_write(&output, b"new").unwrap();
447    assert_eq!(
448        std::fs::metadata(&output).unwrap().permissions().mode() & 0o777,
449        0o640
450    );
451    let link = dir.path().join("link");
452    std::os::unix::fs::symlink(&output, &link).unwrap();
453    assert!(avril_runtime::registry::atomic_write(&link, b"replaced").is_err());
454    assert_eq!(std::fs::read(&output).unwrap(), b"new");
455    use std::os::unix::ffi::OsStrExt;
456    let fifo = dir.path().join("fifo");
457    let path = std::ffi::CString::new(fifo.as_os_str().as_bytes()).unwrap();
458    assert_eq!(unsafe { libc::mkfifo(path.as_ptr(), 0o600) }, 0);
459    let server = Registry::new();
460    let mut options = server.options(dir.path());
461    options.lock = fifo;
462    assert!(
463        PackageStore::new(fixture::runtime_handle(), options)
464            .err()
465            .unwrap()
466            .to_string()
467            .contains("regular")
468    );
469}
470
471#[test]
472fn graph_scoped_saves_ignore_prior_walks_changed_by_other_processes() {
473    let server = Registry::new();
474    for name in ["@test/a", "@test/b"] {
475        server.package(
476            name,
477            "1.0.0",
478            &[("mod.ts", "export {};")],
479            &[(".", "./mod.ts")],
480        );
481    }
482    let dir = tempfile::tempdir().unwrap();
483    let options = server.options(dir.path());
484    let store = PackageStore::new(fixture::runtime_handle(), options.clone()).unwrap();
485    let a = block_on(store.resolve("jsr:@test/a@^1")).unwrap();
486    store
487        .save_graph(&["jsr:@test/a@^1".into()].into(), &[a].into())
488        .unwrap();
489    server.package(
490        "@test/a",
491        "1.1.0",
492        &[("mod.ts", "export {};")],
493        &[(".", "./mod.ts")],
494    );
495    let mut update_options = options.clone();
496    update_options.update = true;
497    let update = PackageStore::new(fixture::runtime_handle(), update_options).unwrap();
498    block_on(update.resolve("jsr:@test/a@^1")).unwrap();
499    update.save().unwrap();
500    store
501        .save_graph(&Default::default(), &["/local/mod.ts".into()].into())
502        .unwrap();
503    let b = block_on(store.resolve("jsr:@test/b@^1")).unwrap();
504    store
505        .save_graph(&["jsr:@test/b@^1".into()].into(), &[b.clone()].into())
506        .unwrap();
507    let lock = fixture::read_lock(dir.path());
508    assert_eq!(lock["specifiers"]["jsr:@test/a@^1"], "1.1.0");
509    assert_eq!(lock["specifiers"]["jsr:@test/b@^1"], "1.0.0");
510    std::fs::remove_file(options.lock).unwrap();
511    store
512        .save_graph(&["jsr:@test/b@^1".into()].into(), &[b].into())
513        .unwrap();
514}
515
516#[test]
517fn shared_caller_runtime_survives_sibling_drop() {
518    let server = Registry::new();
519    server.package(
520        "@test/pkg",
521        "1.0.0",
522        &[("mod.ts", "export {};")],
523        &[(".", "./mod.ts")],
524    );
525    let dir = tempfile::tempdir().unwrap();
526    let options = server.options(dir.path());
527    let runtime = tokio::runtime::Builder::new_multi_thread()
528        .worker_threads(2)
529        .enable_all()
530        .build()
531        .unwrap();
532    let handle = runtime.handle().clone();
533    let first = PackageStore::new(handle.clone(), options.clone()).unwrap();
534    let second = PackageStore::new(handle, options).unwrap();
535    drop(first);
536    let module = block_on(second.resolve("jsr:@test/pkg@1")).unwrap();
537    assert_eq!(block_on(second.source(&module)).unwrap(), "export {};");
538}
539
540#[test]
541fn source_download_callbacks_cannot_bypass_live_authority() {
542    let registry = Registry::new();
543    registry.package(
544        "@test/progress",
545        "1.0.0",
546        &[("mod.ts", "export {};")],
547        &[(".", "./mod.ts")],
548    );
549    let module = format!("{}@test/progress/1.0.0/mod.ts", registry.url);
550    for jsr in [false, true] {
551        let directory = tempfile::tempdir().unwrap();
552        let mut options = registry.options(directory.path());
553        if !jsr {
554            options.jsr_registry = url::Url::parse("https://jsr.example/").unwrap();
555        }
556        let caps = Capabilities::new(Initial::new(false), directory.path().to_owned());
557        let revoke = caps.clone();
558        let events = Arc::new(Mutex::new(Vec::new()));
559        let recorded = events.clone();
560        let store = PackageStore::new(fixture::runtime_handle(), options)
561            .unwrap()
562            .with_capabilities(caps)
563            .with_progress(Arc::new(move |event| {
564                recorded.lock().unwrap().push(event.clone());
565                revoke
566                    .pledge(vec![(Category::JsExec, Some(vec![]), vec![])])
567                    .unwrap();
568            }));
569        assert!(
570            block_on(store.remote_source(&module))
571                .unwrap_err()
572                .to_string()
573                .contains("PermissionDenied")
574        );
575        assert!(matches!(
576            events.lock().unwrap().as_slice(),
577            [ProgressEvent::SourceDownload { .. }]
578        ));
579        assert!(registry.requests.lock().unwrap().is_empty());
580    }
581}