char/avril

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

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

Charlotte Somdeslop: consolidate public type and registry test utils7325e8c

main
18.2 KiB501 linesraw
1#[path = "common/registry.rs"]
2mod fixture;
3
4use avril_runtime::{
5    caps::{Capabilities, Category, Initial},
6    registry::{
7        PackageStore,
8        npm::resolution::{NpmResolver, NpmRootRequest, ResolutionMode},
9    },
10};
11use fixture::Registry;
12use futures::executor::block_on;
13use std::collections::BTreeMap;
14use std::sync::Arc;
15
16#[test]
17fn frozen_replay_accepts_the_canonical_locked_graph() {
18    let server = Registry::new();
19    server.publish_package("util", "1.0.0", serde_json::json!({}), &[]);
20    server.publish_package(
21        "app",
22        "1.0.0",
23        serde_json::json!({"dependencies": {"util": "^1"}}),
24        &[],
25    );
26    let root = tempfile::tempdir().unwrap();
27    let app = NpmRootRequest {
28        request: "npm:app@1".into(),
29        context: BTreeMap::new(),
30    };
31    let warm = server.resolver(root.path());
32    let expected = block_on(warm.prepare(std::slice::from_ref(&app))).unwrap();
33    warm.store().save().unwrap();
34
35    let frozen = server.frozen_resolver(root.path());
36    let lock_before = std::fs::read(root.path().join("avril.lock")).unwrap();
37    let replay = block_on(frozen.prepare(&[app])).unwrap();
38    assert_eq!(replay.roots, expected.roots);
39    frozen.store().save().unwrap();
40    assert_eq!(
41        std::fs::read(root.path().join("avril.lock")).unwrap(),
42        lock_before
43    );
44}
45
46#[test]
47fn locked_roots_restore_the_recorded_peer_context() {
48    let server = Registry::new();
49    for version in ["1.0.0", "1.1.0"] {
50        server.publish_package("host", version, serde_json::json!({}), &[]);
51    }
52    server.publish_package(
53        "plugin",
54        "1.0.0",
55        serde_json::json!({"peerDependencies": {"host": "^1"}}),
56        &[],
57    );
58    let root = tempfile::tempdir().unwrap();
59    let request = |version: &str| NpmRootRequest {
60        request: "npm:plugin@1".into(),
61        context: BTreeMap::from([("host".into(), format!("{}|host|{version}", server.url))]),
62    };
63    let warm = server.resolver(root.path());
64    let first = block_on(warm.prepare(&[request("1.0.0")])).unwrap();
65    let second = block_on(warm.prepare(&[request("1.1.0")])).unwrap();
66    assert_ne!(first.roots.values().next(), second.roots.values().next());
67    warm.store().save().unwrap();
68    let valid = server.frozen_resolver(root.path());
69    block_on(valid.prepare(&[request("1.0.0")])).unwrap();
70    let mut lock = fixture::read_lock(root.path());
71    let first_key = first.roots.keys().next().unwrap();
72    let second_instance = second.roots.values().next().unwrap();
73    lock["npm"]["roots"][first_key] = serde_json::json!(second_instance);
74    std::fs::write(root.path().join("avril.lock"), lock.to_string()).unwrap();
75
76    let cold = server.frozen_resolver(root.path());
77    let replay = block_on(cold.prepare(&[request("1.0.0")])).unwrap();
78    assert_eq!(replay.roots[first_key], *second_instance);
79    let plugin = block_on(cold.resolve_request("npm:plugin@1", ResolutionMode::Import)).unwrap();
80    let host = cold
81        .resolve_from_file("host", &plugin.path, ResolutionMode::Import)
82        .unwrap();
83    assert!(host.path.to_string_lossy().contains("/host/1.1.0/"));
84}
85
86#[test]
87fn locked_dependency_edges_override_request_selections() {
88    let server = Registry::new();
89    server.publish_package("util", "1.0.0", serde_json::json!({}), &[]);
90    server.publish_package(
91        "app",
92        "1.0.0",
93        serde_json::json!({"dependencies": {"util": "^1"}, "imports": {"#renamed": "renamed"}}),
94        &[],
95    );
96    let root = tempfile::tempdir().unwrap();
97    let app = || NpmRootRequest {
98        request: "npm:app@1".into(),
99        context: BTreeMap::new(),
100    };
101    let warm = server.resolver(root.path());
102    block_on(warm.prepare(&[app()])).unwrap();
103    warm.store().save().unwrap();
104    server.publish_package("util", "2.0.0", serde_json::json!({}), &[]);
105    let expanded = server.resolver(root.path());
106    let newer = block_on(expanded.prepare(&[NpmRootRequest {
107        request: "npm:util@2.0.0".into(),
108        context: BTreeMap::new(),
109    }]))
110    .unwrap();
111    expanded.store().save().unwrap();
112    let valid = server.frozen_resolver(root.path());
113    block_on(valid.prepare(&[app()])).unwrap();
114    let mut lock = fixture::read_lock(root.path());
115    assert_eq!(lock["npm"]["specifiers"]["npm:util@^1"], "1.0.0");
116    let instances = lock["npm"]["instances"].as_object_mut().unwrap();
117    let app_pin = instances
118        .values_mut()
119        .find(|pin| pin["package"].as_str().unwrap().ends_with("|app|1.0.0"))
120        .unwrap();
121    app_pin["deps"]["util"] = serde_json::json!(newer.roots.values().next().unwrap());
122    app_pin["deps"]["renamed"] = app_pin["deps"]["util"].clone();
123    std::fs::write(root.path().join("avril.lock"), lock.to_string()).unwrap();
124
125    let cold = server.frozen_resolver(root.path());
126    block_on(cold.prepare(&[app()])).unwrap();
127    let app = block_on(cold.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap();
128    let util = cold
129        .resolve_from_file("util", &app.path, ResolutionMode::Import)
130        .unwrap();
131    assert!(util.path.to_string_lossy().contains("/util/2.0.0/"));
132    for alias in ["renamed", "#renamed"] {
133        assert_eq!(
134            cold.resolve_from_file(alias, &app.path, ResolutionMode::Import)
135                .unwrap()
136                .path,
137            util.path
138        );
139    }
140}
141
142#[test]
143fn replay_loads_only_root_metadata_until_a_dependency_is_visited() {
144    let server = Registry::new();
145    server.publish_package("util", "1.0.0", serde_json::json!({}), &[]);
146    server.publish_package(
147        "app",
148        "1.0.0",
149        serde_json::json!({"dependencies": {"util": "^1"}}),
150        &[("index.js", "export default 1;")],
151    );
152    let root = tempfile::tempdir().unwrap();
153    let app = NpmRootRequest {
154        request: "npm:app@1".into(),
155        context: BTreeMap::new(),
156    };
157    let warm = server.resolver(root.path());
158    block_on(warm.prepare(std::slice::from_ref(&app))).unwrap();
159    warm.store().save().unwrap();
160
161    let app_module = block_on(warm.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap();
162    let util = warm
163        .resolve_from_file("util", &app_module.path, ResolutionMode::Import)
164        .unwrap();
165    std::fs::write(util.path.with_file_name("package.json"), "not json").unwrap();
166
167    let frozen = server.frozen_resolver(root.path());
168    block_on(frozen.prepare(&[app])).unwrap();
169    let app_module = block_on(frozen.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap();
170    assert_eq!(
171        frozen.npm_source(&app_module.path).unwrap(),
172        "export default 1;"
173    );
174    let error = frozen
175        .resolve_from_file("util", &app_module.path, ResolutionMode::Import)
176        .unwrap_err()
177        .to_string();
178    assert!(error.contains("package.json"), "{error}");
179    assert!(
180        frozen
181            .audit()
182            .unwrap_err()
183            .to_string()
184            .contains("package.json")
185    );
186}
187
188#[test]
189fn locked_graph_can_omit_a_manifest_dependency() {
190    let server = Registry::new();
191    server.publish_package("util", "1.0.0", serde_json::json!({}), &[]);
192    server.publish_package(
193        "app",
194        "1.0.0",
195        serde_json::json!({"dependencies": {"util": "^1"}}),
196        &[],
197    );
198    let root = tempfile::tempdir().unwrap();
199    let warm = server.resolver(root.path());
200    block_on(warm.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap();
201    warm.store().save().unwrap();
202    let mut lock = fixture::read_lock(root.path());
203    let app_pin = lock["npm"]["instances"]
204        .as_object_mut()
205        .unwrap()
206        .values_mut()
207        .find(|pin| pin["package"].as_str().unwrap().ends_with("|app|1.0.0"))
208        .unwrap();
209    app_pin["deps"].as_object_mut().unwrap().remove("util");
210    std::fs::write(root.path().join("avril.lock"), lock.to_string()).unwrap();
211    let frozen = server.frozen_resolver(root.path());
212    let app = block_on(frozen.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap();
213    let error = frozen
214        .resolve_from_file("util", &app.path, ResolutionMode::Import)
215        .unwrap_err();
216    assert!(
217        error.to_string().contains("not a declared dependency"),
218        "{error}"
219    );
220}
221
222#[test]
223fn frozen_replay_restores_missing_storage_before_sync_require() {
224    let server = Registry::new();
225    server.publish_package(
226        "util",
227        "1.0.0",
228        serde_json::json!({}),
229        &[("index.js", "export default 1;")],
230    );
231    server.publish_package(
232        "app",
233        "1.0.0",
234        serde_json::json!({"dependencies": {"util": "^1"}}),
235        &[],
236    );
237    let root = tempfile::tempdir().unwrap();
238    let warm = server.resolver(root.path());
239    let app = block_on(warm.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap();
240    let util = warm
241        .resolve_from_file("util", &app.path, ResolutionMode::Import)
242        .unwrap();
243    warm.store().save().unwrap();
244    assert!(
245        fixture::read_lock(root.path())["npm"]["specifiers"]
246            .get("npm:util@1.0.0")
247            .is_none()
248    );
249    let before = std::fs::read(root.path().join("avril.lock")).unwrap();
250    let requests = server.requests.lock().unwrap().len();
251    std::fs::remove_dir_all(util.path.parent().unwrap()).unwrap();
252    let frozen = server.frozen_resolver(root.path());
253    let app = block_on(frozen.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap();
254    let util = frozen
255        .resolve_from_file("util", &app.path, ResolutionMode::Require)
256        .unwrap();
257    assert_eq!(frozen.npm_source(&util.path).unwrap(), "export default 1;");
258    frozen.store().save().unwrap();
259    assert_eq!(
260        std::fs::read(root.path().join("avril.lock")).unwrap(),
261        before
262    );
263    assert_eq!(server.requests.lock().unwrap().len(), requests);
264}
265
266#[test]
267fn frozen_replay_restores_backing_storage_for_existing_peer_instances() {
268    let server = Registry::new();
269    server.publish_package(
270        "app",
271        "1.0.0",
272        serde_json::json!({"dependencies": {"app": "1.0.0"}}),
273        &[("index.js", "export default 1;")],
274    );
275    let root = tempfile::tempdir().unwrap();
276    let warm = server.resolver(root.path());
277    let app = block_on(warm.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap();
278    let backing = block_on(warm.store().npm_acquire("npm:app@1")).unwrap();
279    assert_ne!(app.path.parent().unwrap(), backing.dir);
280    warm.store().save().unwrap();
281    std::fs::remove_dir_all(&backing.dir).unwrap();
282    let frozen = server.frozen_resolver(root.path());
283    let replay = block_on(frozen.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap();
284    assert_eq!(replay.path, app.path);
285    assert_eq!(
286        frozen.npm_source(&replay.path).unwrap(),
287        "export default 1;"
288    );
289    frozen.audit().unwrap();
290}
291
292#[test]
293fn frozen_replay_requires_the_recorded_contextual_root() {
294    let server = Registry::new();
295    server.publish_package("app", "1.0.0", serde_json::json!({}), &[]);
296    let root = tempfile::tempdir().unwrap();
297    let warm = server.resolver(root.path());
298    block_on(warm.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap();
299    warm.store().save().unwrap();
300    let mut lock = fixture::read_lock(root.path());
301    lock["npm"]["roots"] = serde_json::json!({});
302    std::fs::write(root.path().join("avril.lock"), lock.to_string()).unwrap();
303    let frozen = server.frozen_resolver(root.path());
304    let error = block_on(frozen.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap_err();
305    assert!(
306        error
307            .to_string()
308            .contains("Frozen lockfile is missing npm:app@1"),
309        "{error}"
310    );
311}
312
313#[test]
314fn replay_preserves_different_root_versions_of_the_same_package() {
315    let server = Registry::new();
316    for version in ["1.0.0", "2.0.0"] {
317        server.publish_package("app", version, serde_json::json!({}), &[]);
318    }
319    let root = tempfile::tempdir().unwrap();
320    let requests: Vec<_> = ["npm:app@1", "npm:app@2"]
321        .into_iter()
322        .map(|request| NpmRootRequest {
323            request: request.to_owned(),
324            context: BTreeMap::new(),
325        })
326        .collect();
327    let warm = server.resolver(root.path());
328    let expected = block_on(warm.prepare(&requests)).unwrap();
329    warm.store().save().unwrap();
330    let frozen = server.frozen_resolver(root.path());
331    assert_eq!(
332        block_on(frozen.prepare(&requests)).unwrap().roots,
333        expected.roots
334    );
335    for (request, version) in [("npm:app@1", "1.0.0"), ("npm:app@2", "2.0.0")] {
336        let module = block_on(frozen.resolve_request(request, ResolutionMode::Import)).unwrap();
337        assert_eq!(
338            frozen.describe_instance(&module.instance).unwrap().version,
339            version
340        );
341    }
342}
343
344#[test]
345fn warm_replay_rejects_foreign_archive_provenance() {
346    let server = Registry::new();
347    server.publish_package("app", "1.0.0", serde_json::json!({}), &[]);
348    let root = tempfile::tempdir().unwrap();
349    let warm = server.resolver(root.path());
350    block_on(warm.resolve_request("npm:app@1", ResolutionMode::Import)).unwrap();
351    warm.store().save().unwrap();
352    let mut lock = fixture::read_lock(root.path());
353    let pin = lock["npm"]["packages"]
354        .as_object_mut()
355        .unwrap()
356        .values_mut()
357        .next()
358        .unwrap();
359    pin["tarball"] = serde_json::json!("https://unrelated.invalid/app.tgz");
360    std::fs::write(root.path().join("avril.lock"), lock.to_string()).unwrap();
361    let error = PackageStore::new(fixture::runtime_handle(), server.options(root.path()))
362        .err()
363        .expect("foreign archive provenance must fail");
364    assert!(
365        matches!(
366            error,
367            avril_runtime::registry::RegistryError::Npm(
368                avril_runtime::registry::npm::NpmError::NpmTarballEscapesRegistry { .. }
369            )
370        ),
371        "{error}"
372    );
373}
374
375#[test]
376fn frozen_replay_rechecks_live_source_authority() {
377    let server = Registry::new();
378    server.publish_package("guarded", "1.0.0", serde_json::json!({}), &[]);
379    let root = tempfile::tempdir().unwrap();
380    let version_url = format!("{}guarded/1.0.0/", server.url);
381    let mut initial = Initial::new(false);
382    initial
383        .grant(Category::JsExec, false, &version_url, root.path())
384        .unwrap();
385    let capabilities = Capabilities::new(initial, root.path().to_owned());
386    let store = PackageStore::new(fixture::runtime_handle(), server.options(root.path()))
387        .unwrap()
388        .with_capabilities(capabilities.clone());
389    let warm = NpmResolver::new(Arc::new(store));
390    let request = NpmRootRequest {
391        request: "npm:guarded@1".into(),
392        context: BTreeMap::new(),
393    };
394    block_on(warm.prepare(std::slice::from_ref(&request))).unwrap();
395    warm.store().save().unwrap();
396
397    capabilities
398        .pledge(vec![(Category::JsExec, None, vec![version_url])])
399        .unwrap();
400    let mut options = server.options(root.path());
401    options.frozen = true;
402    options.offline = true;
403    let replay = NpmResolver::new(Arc::new(
404        PackageStore::new(fixture::runtime_handle(), options)
405            .unwrap()
406            .with_capabilities(capabilities),
407    ));
408    let error = block_on(replay.prepare(&[request]))
409        .unwrap_err()
410        .to_string();
411    assert!(error.contains("PermissionDenied"), "{error}");
412}
413
414#[test]
415fn frozen_replay_rejects_invalid_graph_structure() {
416    let server = Registry::new();
417    server.publish_package("util", "1.0.0", serde_json::json!({}), &[]);
418    server.publish_package(
419        "app",
420        "1.0.0",
421        serde_json::json!({"dependencies": {"util": "^1"}}),
422        &[],
423    );
424    let root = tempfile::tempdir().unwrap();
425    let app = NpmRootRequest {
426        request: "npm:app@1".into(),
427        context: BTreeMap::new(),
428    };
429    let warm = server.resolver(root.path());
430    block_on(warm.prepare(std::slice::from_ref(&app))).unwrap();
431    warm.store().save().unwrap();
432    let pristine = fixture::read_lock(root.path());
433
434    let mut missing_instance = pristine.clone();
435    let instances = missing_instance["npm"]["instances"]
436        .as_object_mut()
437        .unwrap();
438    let dependency_instance = instances
439        .iter()
440        .find(|(_, pin)| pin["package"].as_str().unwrap().ends_with("|util|1.0.0"))
441        .map(|(id, _)| id.clone())
442        .unwrap();
443    instances.remove(&dependency_instance);
444
445    let mut missing_edge = pristine.clone();
446    let app_pin = missing_edge["npm"]["instances"]
447        .as_object_mut()
448        .unwrap()
449        .values_mut()
450        .find(|pin| pin["package"].as_str().unwrap().ends_with("|app|1.0.0"))
451        .unwrap();
452    app_pin["deps"]["util"] = serde_json::json!("p-0000000000000000");
453
454    let mut missing_package = pristine.clone();
455    let package = missing_package["npm"]["instances"]
456        .as_object()
457        .unwrap()
458        .values()
459        .find(|pin| pin["package"].as_str().unwrap().ends_with("|util|1.0.0"))
460        .unwrap()["package"]
461        .as_str()
462        .unwrap()
463        .to_owned();
464    missing_package["npm"]["packages"]
465        .as_object_mut()
466        .unwrap()
467        .remove(&package);
468
469    for damaged in [missing_instance, missing_edge, missing_package] {
470        std::fs::write(root.path().join("avril.lock"), damaged.to_string()).unwrap();
471        let mut options = server.options(root.path());
472        options.frozen = true;
473        options.offline = true;
474        let error = match PackageStore::new(fixture::runtime_handle(), options) {
475            Ok(store) => {
476                let cold = NpmResolver::new(Arc::new(store));
477                block_on(cold.prepare(std::slice::from_ref(&app)))
478                    .unwrap_err()
479                    .to_string()
480            }
481            Err(error) => error.to_string(),
482        };
483        assert!(error.contains("Lockfile is missing npm"), "{error}");
484    }
485
486    let alias = package.replacen("http://", "HTTP://", 1);
487    let noncanonical = pristine.to_string().replace(&package, &alias);
488    std::fs::write(root.path().join("avril.lock"), noncanonical).unwrap();
489    let error = PackageStore::new(fixture::runtime_handle(), server.options(root.path()))
490        .err()
491        .expect("distinct package identities must not alias the same storage");
492    assert!(
493        matches!(
494            error,
495            avril_runtime::registry::RegistryError::Npm(
496                avril_runtime::registry::npm::NpmError::InvalidNpmLockIdentity
497            )
498        ),
499        "{error}"
500    );
501}