//! Install, update, remove and rollback against a fake transport and a temp store. mod addons_support; use addons_support::{TINY, entry, index, manifest, serve, wasm}; use notescraft_engine::addons::{ Ctx, Ed25519, FakeTransport, FakeVerifier, Index, Kind, Seed, Source, Store, Version, fetch_files, install, key, remove, rollback, update, write_tree, }; const GRAMMAR: &str = "twig.sublime-syntax"; struct Env { dir: std::path::PathBuf, store: Store, } impl Env { fn new(name: &str) -> Self { let dir = std::env::temp_dir().join(format!("notescraft-addons-install-{}-{name}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); let store = Store::open(&dir).unwrap(); Self { dir, store } } fn ctx<'a>(&'a self, transport: &'a FakeTransport, verifier: &'a FakeVerifier) -> Ctx<'a> { Ctx { store: &self.store, transport, verifier, app_version: v(0, 5, 0), seed: None } } } impl Drop for Env { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.dir); } } fn v(major: u16, minor: u16, patch: u16) -> Version { Version { major, minor, patch } } const OK: FakeVerifier = FakeVerifier { accept: true }; fn grammar_index(version: &str, bytes: &[u8]) -> Index { index(&[entry("grammar", "twig", version, &[(GRAMMAR, bytes)], &[])], &[]) } fn grammar_net(version: &str, bytes: &[u8]) -> FakeTransport { serve(FakeTransport::new(), "grammar", "twig", version, &[(GRAMMAR, bytes)]) } fn error_of(result: notescraft_engine::Result) -> String { result.expect_err("must fail").to_string() } #[test] fn a_clean_install_commits_state_and_files() { let env = Env::new("clean"); let report = install(&env.ctx(&grammar_net("1.0.0", TINY.as_bytes()), &OK), &grammar_index("1.0.0", TINY.as_bytes()), Kind::Grammar, "twig", None).unwrap(); assert_eq!((report.version, report.previous, report.source), (v(1, 0, 0), None, Source::Registry)); let state = env.store.load().unwrap(); assert_eq!(state.installed[&key(Kind::Grammar, "twig")].version, v(1, 0, 0)); let file = env.store.version_dir(Kind::Grammar, "twig", v(1, 0, 0)).unwrap().join(GRAMMAR); assert_eq!(std::fs::read(file).unwrap(), TINY.as_bytes()); } #[test] fn a_bad_download_changes_nothing() { let env = Env::new("bad"); let idx = grammar_index("1.0.0", TINY.as_bytes()); let short = grammar_net("1.0.0", &TINY.as_bytes()[..10]); assert!(install(&env.ctx(&short, &OK), &idx, Kind::Grammar, "twig", None).is_err()); let wrong = grammar_net("1.0.0", &TINY.to_uppercase().into_bytes()); assert!(error_of(install(&env.ctx(&wrong, &OK), &idx, Kind::Grammar, "twig", None)).contains("sha256")); let long = grammar_net("1.0.0", &[TINY.as_bytes(), b"x"].concat()); assert!(install(&env.ctx(&long, &OK), &idx, Kind::Grammar, "twig", None).is_err()); assert!(error_of(install(&env.ctx(&FakeTransport::new(), &OK), &idx, Kind::Grammar, "twig", None)).contains("offline")); assert!(env.store.load().unwrap().installed.is_empty()); assert_eq!(env.store.gc(&env.store.load().unwrap()).unwrap(), 0, "no folder was written"); } #[test] fn a_content_check_failure_writes_nothing() { let env = Env::new("content"); let junk = b"nope: [".as_slice(); let idx = grammar_index("1.0.0", junk); assert!(install(&env.ctx(&grammar_net("1.0.0", junk), &OK), &idx, Kind::Grammar, "twig", None).is_err()); assert!(!env.store.version_dir(Kind::Grammar, "twig", v(1, 0, 0)).unwrap().exists()); } #[test] fn the_gates_refuse_before_any_download() { let env = Env::new("gates"); let net = grammar_net("1.0.0", TINY.as_bytes()); let idx = grammar_index("1.0.0", TINY.as_bytes()); assert!(error_of(install(&env.ctx(&net, &FakeVerifier { accept: false }), &idx, Kind::Grammar, "twig", None)).contains("refuses")); let real = Ed25519::new(vec![]); let ctx = Ctx { store: &env.store, transport: &net, verifier: &real, app_version: v(0, 5, 0), seed: None }; assert!(error_of(install(&ctx, &idx, Kind::Grammar, "twig", None)).contains("unknown signing key")); let old_app = Ctx { app_version: v(0, 0, 9), ..env.ctx(&net, &OK) }; assert!(error_of(install(&old_app, &idx, Kind::Grammar, "twig", None)).contains("incompatible")); let revoked = index(&[entry("grammar", "twig", "1.0.0", &[(GRAMMAR, TINY.as_bytes())], &[])], &[("twig", "1.0.0")]); assert!(error_of(install(&env.ctx(&net, &OK), &revoked, Kind::Grammar, "twig", None)).contains("revoked")); assert!(error_of(install(&env.ctx(&net, &OK), &idx, Kind::Grammar, "missing", None)).contains("not_found")); assert!(error_of(install(&env.ctx(&net, &OK), &idx, Kind::Grammar, "twig", Some(v(2, 0, 0)))).contains("not_found")); let snippet = index(&[entry("snippet", "s", "1.0.0", &[("s.md", b"hi")], &[])], &[]); assert!(error_of(install(&env.ctx(&net, &OK), &snippet, Kind::Snippet, "s", None)).contains("no use")); assert!(net.asked().is_empty(), "nothing was fetched: {:?}", net.asked()); assert!(env.store.load().unwrap().installed.is_empty()); } #[test] fn a_crash_before_the_state_flip_leaves_the_old_state_and_gc_cleans_up() { let env = Env::new("crash"); let (idx, net) = (grammar_index("1.0.0", TINY.as_bytes()), grammar_net("1.0.0", TINY.as_bytes())); let ctx = env.ctx(&net, &OK); let entry = idx.find(Kind::Grammar, "twig").unwrap(); let staged = fetch_files(&ctx, entry).unwrap(); write_tree(&env.store, entry, &staged).unwrap(); let dir = env.store.version_dir(Kind::Grammar, "twig", v(1, 0, 0)).unwrap(); assert!(dir.is_dir()); assert!(env.store.load().unwrap().installed.is_empty(), "state was never flipped"); assert_eq!(env.store.gc(&env.store.load().unwrap()).unwrap(), 1); assert!(!dir.exists()); write_tree(&env.store, entry, &staged).unwrap(); write_tree(&env.store, entry, &staged).unwrap(); } #[test] fn update_keeps_previous_and_rollback_swaps() { let env = Env::new("update"); install(&env.ctx(&grammar_net("1.0.0", TINY.as_bytes()), &OK), &grammar_index("1.0.0", TINY.as_bytes()), Kind::Grammar, "twig", None).unwrap(); let newer = format!("{TINY}# 1.1\n"); let (idx, net) = (grammar_index("1.1.0", newer.as_bytes()), grammar_net("1.1.0", newer.as_bytes())); let report = update(&env.ctx(&net, &OK), &idx, Kind::Grammar, "twig").unwrap(); assert_eq!((report.version, report.previous), (v(1, 1, 0), Some(v(1, 0, 0)))); assert!(env.store.version_dir(Kind::Grammar, "twig", v(1, 0, 0)).unwrap().is_dir(), "the old version stays for rollback"); assert!(error_of(update(&env.ctx(&net, &OK), &idx, Kind::Grammar, "twig")).contains("already installed")); let back = rollback(&env.store, Kind::Grammar, "twig").unwrap(); assert_eq!((back.version, back.previous), (v(1, 0, 0), Some(v(1, 1, 0)))); let forward = rollback(&env.store, Kind::Grammar, "twig").unwrap(); assert_eq!(forward.version, v(1, 1, 0)); let old = (grammar_index("1.0.0", TINY.as_bytes()), grammar_net("1.0.0", TINY.as_bytes())); assert!(error_of(install(&env.ctx(&old.1, &OK), &old.0, Kind::Grammar, "twig", None)).contains("rollback"), "a downgrade is never an install"); } #[test] fn update_and_rollback_need_an_installed_add_on() { let env = Env::new("needs"); let (idx, net) = (grammar_index("1.0.0", TINY.as_bytes()), grammar_net("1.0.0", TINY.as_bytes())); assert!(error_of(update(&env.ctx(&net, &OK), &idx, Kind::Grammar, "twig")).contains("not installed")); assert!(error_of(rollback(&env.store, Kind::Grammar, "twig")).contains("not installed")); install(&env.ctx(&net, &OK), &idx, Kind::Grammar, "twig", None).unwrap(); assert!(error_of(rollback(&env.store, Kind::Grammar, "twig")).contains("earlier")); } #[test] fn remove_commits_first_then_deletes_the_files() { let env = Env::new("remove"); install(&env.ctx(&grammar_net("1.0.0", TINY.as_bytes()), &OK), &grammar_index("1.0.0", TINY.as_bytes()), Kind::Grammar, "twig", None).unwrap(); remove(&env.store, Kind::Grammar, "twig").unwrap(); assert!(env.store.load().unwrap().installed.is_empty()); assert!(!env.store.version_dir(Kind::Grammar, "twig", v(1, 0, 0)).unwrap().exists()); assert!(error_of(remove(&env.store, Kind::Grammar, "twig")).contains("not_found")); } #[derive(Debug)] struct Fixed(Vec); impl Seed for Fixed { fn read(&self, _: Kind, _: &str, _: Version, _: &str) -> Option> { Some(self.0.clone()) } } #[test] fn a_seed_installs_offline_and_a_tampered_seed_is_ignored() { let env = Env::new("seed"); let idx = grammar_index("1.0.0", TINY.as_bytes()); let seed = Fixed(TINY.as_bytes().to_vec()); let offline = FakeTransport::new(); let ctx = Ctx { seed: Some(&seed), ..env.ctx(&offline, &OK) }; let report = install(&ctx, &idx, Kind::Grammar, "twig", None).unwrap(); assert_eq!(report.source, Source::Seed); assert_eq!(offline.asked().len(), 0); let env = Env::new("seed-bad"); let tampered = Fixed(b"tampered".to_vec()); let ctx = Ctx { seed: Some(&tampered), ..env.ctx(&offline, &OK) }; assert!(error_of(install(&ctx, &idx, Kind::Grammar, "twig", None)).contains("offline"), "falls back to the network, which is down"); } #[test] fn a_plugin_installs_with_its_manifest_checked() { let env = Env::new("plugin"); let text = manifest("dice", "1.0.0", &["vault.read"]); let files: &[(&str, &[u8])] = &[("manifest.json", text.as_bytes()), ("plugin.wasm", wasm())]; let idx = index(&[entry("plugin", "dice", "1.0.0", files, &["vault.read"])], &[]); let net = serve(FakeTransport::new(), "plugin", "dice", "1.0.0", files); install(&env.ctx(&net, &OK), &idx, Kind::Plugin, "dice", None).unwrap(); let dir = env.store.version_dir(Kind::Plugin, "dice", v(1, 0, 0)).unwrap(); assert!(dir.join("manifest.json").is_file() && dir.join("plugin.wasm").is_file()); } #[test] fn the_installed_table_cap_is_enforced() { let env = Env::new("cap"); let mut state = env.store.load().unwrap(); for n in 0..512 { state.installed.insert( format!("grammar:g{n}"), notescraft_engine::addons::Installed { version: v(1, 0, 0), previous: None, source: Source::Registry, installed_at: "t".into(), sha256_of_files: "e".repeat(64), }, ); } env.store.commit(&state).unwrap(); let (idx, net) = (grammar_index("1.0.0", TINY.as_bytes()), grammar_net("1.0.0", TINY.as_bytes())); assert!(error_of(install(&env.ctx(&net, &OK), &idx, Kind::Grammar, "twig", None)).contains("limit")); } #[test] fn a_font_entry_installs() { let env = Env::new("font"); let font = index(&[entry("font", "stix-math", "1.0.0", &[("stix.ttf", &[0, 1, 0, 0])], &[])], &[]); let net = serve(FakeTransport::new(), "font", "stix-math", "1.0.0", &[("stix.ttf", &[0, 1, 0, 0])]); let report = install(&env.ctx(&net, &OK), &font, Kind::Font, "stix-math", None).unwrap(); assert_eq!(report.version, v(1, 0, 0)); assert!(env.store.load().unwrap().installed.contains_key(&key(Kind::Font, "stix-math"))); }