198 lines
10 KiB
Rust
198 lines
10 KiB
Rust
//! The `registry.*` commands through the registry: list, check, install, update, remove,
|
|
//! rollback, enable, the read-only refusal and the assistant gate classes.
|
|
|
|
mod addons_support;
|
|
mod common;
|
|
|
|
use std::sync::Arc;
|
|
|
|
use addons_support::{Shared, TINY, entry, index_json, manifest, serve, wasm};
|
|
use common::{TempVault, temp_config, temp_vault};
|
|
use notescraft_engine::addons::{FakeTransport, FakeVerifier, INDEX_URL, Response};
|
|
use notescraft_engine::{AssistantGate, AssistantSettings, Decision, Error, GateSource, Registry, Session, register_core};
|
|
use serde_json::{Value, json};
|
|
|
|
const GRAMMAR: &str = "twig.sublime-syntax";
|
|
|
|
struct Fixture {
|
|
reg: Registry<Session>,
|
|
session: Session,
|
|
net: Arc<FakeTransport>,
|
|
vault_path: std::path::PathBuf,
|
|
_guards: (TempVault, TempVault),
|
|
}
|
|
|
|
fn script(version: &str, bytes: &[u8]) -> FakeTransport {
|
|
let doc = index_json(&[entry("grammar", "twig", version, &[(GRAMMAR, bytes)], &[])], &[]);
|
|
let net = FakeTransport::new().with_response(INDEX_URL, Ok(Response { status: 200, etag: Some("\"e\"".into()), bytes: doc }));
|
|
serve(net, "grammar", "twig", version, &[(GRAMMAR, bytes)])
|
|
}
|
|
|
|
fn fixture(name: &str, net: FakeTransport) -> Fixture {
|
|
let (vault, config) = (temp_vault(name), temp_config(name));
|
|
let mut session = Session::open(vault.path()).unwrap();
|
|
session.use_settings_dir(config.path());
|
|
let net = Arc::new(net);
|
|
session.set_addons_transport(Box::new(Shared(net.clone())));
|
|
session.set_addons_verifier(Box::new(FakeVerifier { accept: true }));
|
|
let mut reg = Registry::new();
|
|
register_core(&mut reg).unwrap();
|
|
Fixture { reg, session, net, vault_path: vault.path().to_path_buf(), _guards: (vault, config) }
|
|
}
|
|
|
|
impl Fixture {
|
|
fn run(&mut self, id: &str, params: impl Into<Value>) -> Result<Value, Error> {
|
|
self.reg.run(id, &mut self.session, ¶ms.into())
|
|
}
|
|
|
|
fn ok(&mut self, id: &str, params: impl Into<Value>) -> Value {
|
|
self.run(id, params).unwrap_or_else(|e| panic!("{id}: {e}"))
|
|
}
|
|
|
|
fn code(&mut self, id: &str, params: impl Into<Value>) -> String {
|
|
match self.run(id, params) {
|
|
Err(Error::Command { source, .. }) => source.code,
|
|
other => panic!("{id}: expected a command error, got {other:?}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn list_is_empty_until_a_check_then_joins_the_state() {
|
|
let mut f = fixture("list", script("1.0.0", TINY.as_bytes()));
|
|
assert_eq!(f.ok("registry.list", json!({}))["cached"], false);
|
|
assert!(f.net.asked().is_empty(), "list never touches the network");
|
|
let checked = f.ok("registry.check", json!({}));
|
|
assert_eq!((checked["entries"].clone(), checked["updates"].clone()), (json!(1), json!(0)));
|
|
let rows = f.ok("registry.list", json!({}));
|
|
assert_eq!(rows["entries"][0]["state"], "available");
|
|
f.ok("registry.install", json!({ "kind": "grammar", "id": "twig" }));
|
|
assert_eq!(f.ok("registry.list", json!({ "state": "installed" }))["entries"][0]["installed_version"], "1.0.0");
|
|
assert_eq!(f.ok("registry.list", json!({ "state": "available" }))["entries"], json!([]));
|
|
assert_eq!(f.ok("registry.list", json!({ "query": "TW" }))["entries"].as_array().map(Vec::len), Some(1));
|
|
assert_eq!(f.ok("registry.info", json!({ "kind": "grammar", "id": "twig" }))["files"][0]["path"], GRAMMAR);
|
|
assert_eq!(f.ok("registry.suggest", json!({ "label": "Twig" }))["entries"][0]["id"], "twig");
|
|
assert_eq!(f.ok("registry.suggest", json!({ "label": "nope" }))["entries"], json!([]));
|
|
}
|
|
|
|
#[test]
|
|
fn install_update_rollback_remove_run_through_the_registry() {
|
|
let mut f = fixture("flow", script("1.0.0", TINY.as_bytes()));
|
|
f.ok("registry.check", json!({}));
|
|
let done = f.ok("registry.install", json!({ "kind": "grammar", "id": "twig" }));
|
|
assert_eq!(done["version"], "1.0.0");
|
|
let newer = format!("{TINY}# 1.1\n");
|
|
// A new index with 1.1.0 appears after another check.
|
|
let net = script("1.1.0", newer.as_bytes());
|
|
f.session.set_addons_transport(Box::new(Shared(Arc::new(net))));
|
|
assert_eq!(f.ok("registry.check", json!({}))["updates"], 1);
|
|
assert_eq!(f.ok("registry.list", json!({ "state": "updates" }))["entries"][0]["state"], "update");
|
|
let all = f.ok("registry.update", json!({}));
|
|
assert_eq!(all["updated"][0]["previous"], "1.0.0");
|
|
assert_eq!(f.ok("registry.rollback", json!({ "kind": "grammar", "id": "twig" }))["version"], "1.0.0");
|
|
f.ok("registry.remove", json!({ "kind": "grammar", "id": "twig" }));
|
|
assert_eq!(f.code("registry.remove", json!({ "kind": "grammar", "id": "twig" })), "not_found");
|
|
assert_eq!(f.code("registry.install", json!({ "kind": "grammar", "id": "nope" })), "not_found");
|
|
assert_eq!(f.code("registry.install", json!({ "kind": "exe", "id": "twig" })), "invalid_params");
|
|
}
|
|
|
|
#[test]
|
|
fn install_without_a_trusted_key_is_a_clear_addon_error() {
|
|
let mut f = fixture("nokey", script("1.0.0", TINY.as_bytes()));
|
|
f.ok("registry.check", json!({}));
|
|
let config = temp_config("nokey-default");
|
|
let vault = temp_vault("nokey-default");
|
|
let mut plain = Session::open(vault.path()).unwrap();
|
|
plain.use_settings_dir(config.path());
|
|
plain.set_addons_transport(Box::new(Shared(f.net.clone())));
|
|
f.session = plain;
|
|
f.ok("registry.check", json!({}));
|
|
let error = f.run("registry.install", json!({ "kind": "grammar", "id": "twig" })).unwrap_err().to_string();
|
|
assert!(error.contains("no trusted registry key"), "{error}");
|
|
assert_eq!(f.code("registry.install", json!({ "kind": "grammar", "id": "twig" })), "addon");
|
|
f.session.set_addons_verifier(Box::new(notescraft_engine::addons::Ed25519::new(vec![])));
|
|
assert_eq!(f.code("registry.install", json!({ "kind": "grammar", "id": "twig" })), "registry_refused");
|
|
}
|
|
|
|
#[test]
|
|
fn offline_check_leaves_the_cache() {
|
|
let mut f = fixture("offline", script("1.0.0", TINY.as_bytes()));
|
|
f.ok("registry.check", json!({}));
|
|
f.session.set_addons_transport(Box::new(Shared(Arc::new(FakeTransport::new()))));
|
|
assert_eq!(f.code("registry.check", json!({})), "offline");
|
|
assert_eq!(f.ok("registry.list", json!({}))["entries"].as_array().map(Vec::len), Some(1));
|
|
}
|
|
|
|
#[test]
|
|
fn install_before_any_check_says_so() {
|
|
let mut f = fixture("nocache", script("1.0.0", TINY.as_bytes()));
|
|
assert_eq!(f.code("registry.install", json!({ "kind": "grammar", "id": "twig" })), "not_found");
|
|
}
|
|
|
|
#[test]
|
|
fn enable_and_disable_write_the_vault_file() {
|
|
let text = manifest("dice", "1.0.0", &[]);
|
|
let files: &[(&str, &[u8])] = &[("manifest.json", text.as_bytes()), ("plugin.wasm", wasm())];
|
|
let doc = index_json(&[entry("plugin", "dice", "1.0.0", files, &[])], &[]);
|
|
let net = serve(FakeTransport::new().with(INDEX_URL, &doc), "plugin", "dice", "1.0.0", files);
|
|
let mut f = fixture("enable", net);
|
|
assert_eq!(f.code("registry.enable", json!({ "id": "dice" })), "not_found", "not installed yet");
|
|
f.ok("registry.check", json!({}));
|
|
f.ok("registry.install", json!({ "kind": "plugin", "id": "dice" }));
|
|
f.ok("registry.enable", json!({ "id": "dice" }));
|
|
let file = std::fs::read_to_string(f.vault_path.join(".notescraft/registry.json")).unwrap();
|
|
assert!(file.contains("plugin:dice"), "{file}");
|
|
f.ok("registry.disable", json!({ "id": "dice" }));
|
|
let file = std::fs::read_to_string(f.vault_path.join(".notescraft/registry.json")).unwrap();
|
|
assert!(!file.contains("plugin:dice"), "{file}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_read_only_session_refuses_the_mutating_ids() {
|
|
let vault = temp_vault("ro");
|
|
let mut session = Session::open_read_only(vault.path()).unwrap();
|
|
let mut reg = Registry::new();
|
|
register_core(&mut reg).unwrap();
|
|
for id in ["registry.check", "registry.install", "registry.update", "registry.remove", "registry.rollback", "registry.enable", "registry.disable"] {
|
|
let params = json!({ "kind": "grammar", "id": "twig" });
|
|
assert!(matches!(reg.run(id, &mut session, ¶ms), Err(Error::Disabled(_))), "{id}");
|
|
}
|
|
for id in ["registry.list", "registry.suggest"] {
|
|
assert!(!matches!(reg.run(id, &mut session, &json!({ "label": "x" })), Err(Error::Disabled(_))), "{id}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_gate_classes_hold() {
|
|
let f = fixture("gate", FakeTransport::new());
|
|
let mut session = f.session;
|
|
let decide = |session: &Session, id: &str| AssistantGate::check(GateSource::Assistant, session, id, &Value::Null);
|
|
for id in ["registry.list", "registry.info", "registry.suggest"] {
|
|
assert!(matches!(decide(&session, id), Decision::Allow), "{id}");
|
|
}
|
|
for id in ["registry.enable", "registry.disable", "registry.check", "registry.install", "registry.update", "registry.remove", "registry.rollback"] {
|
|
assert!(matches!(decide(&session, id), Decision::Deny(_)), "{id} needs allow_write");
|
|
}
|
|
session.set_assistant_settings(AssistantSettings { allow_write: true, allow_delete: false, review_writes: false, ..AssistantSettings::default() });
|
|
for id in ["registry.enable", "registry.disable"] {
|
|
assert!(matches!(decide(&session, id), Decision::Allow), "{id}");
|
|
}
|
|
for id in ["registry.check", "registry.install", "registry.update", "registry.remove", "registry.rollback"] {
|
|
assert!(matches!(decide(&session, id), Decision::Deny(_)), "{id} needs allow_delete");
|
|
}
|
|
session.set_assistant_settings(AssistantSettings { allow_write: true, allow_delete: true, review_writes: false, ..AssistantSettings::default() });
|
|
for id in ["registry.check", "registry.install", "registry.update", "registry.remove", "registry.rollback"] {
|
|
assert!(matches!(decide(&session, id), Decision::Allow), "{id}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn list_reports_a_legacy_grammar_as_installed() {
|
|
let mut f = fixture("legacy-list", script("1.0.0", TINY.as_bytes()));
|
|
f.ok("registry.check", json!({}));
|
|
f.ok("grammar.install_text", json!({ "id": "twig", "text": TINY }));
|
|
let row = f.ok("registry.list", json!({ "state": "installed" }))["entries"][0].clone();
|
|
assert_eq!((row["id"].clone(), row["state"].clone(), row["source"].clone()), (json!("twig"), json!("installed"), json!("legacy")));
|
|
assert_eq!(f.ok("registry.list", json!({ "state": "available" }))["entries"], json!([]));
|
|
}
|