232 lines
11 KiB
Rust
232 lines
11 KiB
Rust
//! 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<T: std::fmt::Debug>(result: notescraft_engine::Result<T>) -> 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<u8>);
|
|
|
|
impl Seed for Fixed {
|
|
fn read(&self, _: Kind, _: &str, _: Version, _: &str) -> Option<Vec<u8>> {
|
|
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")));
|
|
}
|