181 lines
7.8 KiB
Rust
181 lines
7.8 KiB
Rust
//! Plugins installed from the registry: loaded for the vaults that enabled them, commands
|
|
//! `plugin.<id>.<command>` registered, run only with the permissions the user approved.
|
|
|
|
mod addons_support;
|
|
mod common;
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::Arc;
|
|
|
|
use addons_support::{Shared, entry, index_json, manifest, serve, wasm};
|
|
use common::{TempVault, temp_config, temp_vault};
|
|
use notescraft_engine::addons::{FakeTransport, FakeVerifier, INDEX_URL};
|
|
use notescraft_engine::{Error, Registry, Session, register_core};
|
|
use serde_json::{Value, json};
|
|
|
|
struct Fixture {
|
|
reg: Registry<Session>,
|
|
session: Session,
|
|
_guards: (TempVault, TempVault),
|
|
}
|
|
|
|
/// A session whose transport serves the plugin `id` made of `manifest_text` and `module`.
|
|
fn fixture(name: &str, id: &str, manifest_text: &str, module: &[u8], permissions: &[&str]) -> Fixture {
|
|
let files: &[(&str, &[u8])] = &[("manifest.json", manifest_text.as_bytes()), ("plugin.wasm", module)];
|
|
let doc = index_json(&[entry("plugin", id, "1.0.0", files, permissions)], &[]);
|
|
let net = serve(FakeTransport::new().with(INDEX_URL, &doc), "plugin", id, "1.0.0", files);
|
|
let (vault, config) = (temp_vault(name), temp_config(name));
|
|
let mut session = Session::open(vault.path()).unwrap();
|
|
session.use_settings_dir(config.path());
|
|
session.set_addons_transport(Box::new(Shared(Arc::new(net))));
|
|
session.set_addons_verifier(Box::new(FakeVerifier { accept: true }));
|
|
let mut reg = Registry::new();
|
|
register_core(&mut reg).unwrap();
|
|
Fixture { reg, session, _guards: (vault, config) }
|
|
}
|
|
|
|
impl Fixture {
|
|
fn run(&mut self, id: &str, params: &Value) -> Result<Value, Error> {
|
|
self.reg.run(id, &mut self.session, params)
|
|
}
|
|
|
|
fn ok(&mut self, id: &str, params: &Value) -> Value {
|
|
self.run(id, params).unwrap_or_else(|e| panic!("{id}: {e}"))
|
|
}
|
|
|
|
fn install(&mut self, id: &str) {
|
|
self.ok("registry.check", &json!({}));
|
|
self.ok("registry.install", &json!({ "kind": "plugin", "id": id }));
|
|
}
|
|
|
|
fn commands(&mut self) -> usize {
|
|
self.session.reload_plugin_commands(&mut self.reg).unwrap()
|
|
}
|
|
}
|
|
|
|
fn dice_dir() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../plugins/dice")
|
|
}
|
|
|
|
#[test]
|
|
fn an_enabled_installed_plugin_registers_and_runs() {
|
|
let mut f = fixture("on", "demo", &manifest("demo", "1.0.0", &[]), wasm(), &[]);
|
|
f.install("demo");
|
|
assert_eq!(f.commands(), 0, "installed but not enabled in this vault");
|
|
assert!(f.reg.get("plugin.demo.roll").is_none());
|
|
f.ok("registry.enable", &json!({ "id": "demo" }));
|
|
let n = f.commands();
|
|
assert_eq!(n, 1, "{:?}", f.session.diagnostics());
|
|
let answer = f.ok("plugin.demo.roll", &json!({}));
|
|
assert_eq!(answer["command"], "plugin.demo.roll");
|
|
assert_eq!(f.session.plugin_infos().first().map(|p| p.id.as_str()), Some("demo"));
|
|
}
|
|
|
|
#[test]
|
|
fn a_disabled_plugin_is_not_registered() {
|
|
let mut f = fixture("off", "demo", &manifest("demo", "1.0.0", &[]), wasm(), &[]);
|
|
f.install("demo");
|
|
f.ok("registry.enable", &json!({ "id": "demo" }));
|
|
assert_eq!(f.commands(), 1);
|
|
f.ok("registry.disable", &json!({ "id": "demo" }));
|
|
assert_eq!(f.commands(), 0);
|
|
assert!(f.reg.get("plugin.demo.roll").is_none());
|
|
assert_eq!(f.session.plugin_infos().len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn a_plugin_removed_while_enabled_unloads() {
|
|
let mut f = fixture("gone", "demo", &manifest("demo", "1.0.0", &[]), wasm(), &[]);
|
|
f.install("demo");
|
|
f.ok("registry.enable", &json!({ "id": "demo" }));
|
|
assert_eq!(f.commands(), 1);
|
|
f.ok("registry.remove", &json!({ "kind": "plugin", "id": "demo" }));
|
|
assert_eq!(f.commands(), 0);
|
|
assert_eq!(f.session.plugin_infos().len(), 0);
|
|
assert_eq!(f.session.diagnostics().len(), 0, "{:?}", f.session.diagnostics());
|
|
}
|
|
|
|
#[test]
|
|
fn the_registry_owns_the_switch_of_its_plugins() {
|
|
let mut f = fixture("switch", "demo", &manifest("demo", "1.0.0", &[]), wasm(), &[]);
|
|
f.install("demo");
|
|
f.ok("registry.enable", &json!({ "id": "demo" }));
|
|
assert!(f.run("plugins.disable", &json!({ "id": "demo" })).is_err());
|
|
assert!(f.run("plugins.enable", &json!({ "id": "demo" })).is_err());
|
|
assert!(f.session.is_plugin_enabled("demo"));
|
|
}
|
|
|
|
#[test]
|
|
fn approval_never_goes_beyond_the_manifest() {
|
|
let mut f = fixture("approve", "demo", &manifest("demo", "1.0.0", &["editor"]), wasm(), &["editor"]);
|
|
f.install("demo");
|
|
assert!(f.run("plugins.approve", &json!({ "id": "demo", "permissions": ["vault.write"] })).is_err());
|
|
assert!(f.run("plugins.approve", &json!({ "id": "demo", "permissions": "editor" })).is_err());
|
|
assert!(f.run("plugins.approve", &json!({ "id": "ghost" })).is_err());
|
|
assert!(f.session.plugin_settings().approved.is_empty());
|
|
let answer = f.ok("plugins.approve", &json!({ "id": "demo" }));
|
|
assert_eq!(answer["approved"], json!(["editor"]));
|
|
let answer = f.ok("plugins.approve", &json!({ "id": "demo", "permissions": [] }));
|
|
assert_eq!(answer["approved"], json!([]));
|
|
}
|
|
|
|
#[test]
|
|
fn dice_installs_from_a_fake_registry_and_needs_approval_to_roll() {
|
|
let (Ok(module), Ok(text)) = (std::fs::read(dice_dir().join("dice.wasm")), std::fs::read_to_string(dice_dir().join("manifest.json"))) else {
|
|
println!("skipped: build plugins/dice/build.sh first");
|
|
return;
|
|
};
|
|
let mut f = fixture("dice", "dice", &text, &module, &["editor"]);
|
|
f.install("dice");
|
|
f.ok("registry.enable", &json!({ "id": "dice" }));
|
|
assert_eq!(f.commands(), 1);
|
|
let refused = f.run("plugin.dice.roll", &json!({ "args": { "expr": "1d1" } }));
|
|
assert!(refused.is_err(), "the editor permission was not approved: {refused:?}");
|
|
f.ok("plugins.approve", &json!({ "id": "dice" }));
|
|
let answer = f.ok("plugin.dice.roll", &json!({ "args": { "expr": "2d6+3" } }));
|
|
let total = answer["result"]["total"].as_i64().unwrap();
|
|
assert!((5..=15).contains(&total), "{answer}");
|
|
}
|
|
|
|
/// Like `wasm()`, but `ns_run_command` traps.
|
|
fn trapping_wasm() -> Vec<u8> {
|
|
let text = r#"(module
|
|
(memory (export "memory") 2)
|
|
(global $bump (mut i32) (i32.const 1024))
|
|
(func (export "ns_alloc") (param $len i32) (result i32)
|
|
(local $ptr i32)
|
|
(local.set $ptr (global.get $bump))
|
|
(global.set $bump (i32.add (local.get $ptr) (local.get $len)))
|
|
(local.get $ptr))
|
|
(func (export "ns_free") (param i32 i32))
|
|
(func (export "ns_abi_version") (result i32) (i32.const 1))
|
|
(func (export "ns_on_load") (result i32) (i32.const 0))
|
|
(func (export "ns_on_unload") (result i32) (i32.const 0))
|
|
(func (export "ns_on_event") (param i32 i32) (result i32) (i32.const 0))
|
|
(func (export "ns_run_command") (param i32 i32 i32 i32) (result i64) (unreachable)))"#;
|
|
wat::parse_str(text).unwrap_or_default()
|
|
}
|
|
|
|
#[test]
|
|
fn a_registry_plugin_disabled_after_failures_stays_off_after_a_restart() {
|
|
let mut f = fixture("autooff", "demo", &manifest("demo", "1.0.0", &[]), &trapping_wasm(), &[]);
|
|
f.install("demo");
|
|
f.ok("registry.enable", &json!({ "id": "demo" }));
|
|
assert_eq!(f.commands(), 1);
|
|
for _ in 0..3 {
|
|
assert!(f.run("plugin.demo.roll", &json!({})).is_err());
|
|
}
|
|
assert!(!f.session.is_plugin_enabled("demo"));
|
|
let file = f.session.vault().root().join(".notescraft/registry.json");
|
|
let saved: Value = serde_json::from_slice(&std::fs::read(&file).unwrap()).unwrap();
|
|
assert_eq!(saved["disabled_after_failures"], json!(["plugin:demo"]));
|
|
assert_eq!(saved["enabled"], json!(["plugin:demo"]), "the user's choice is kept");
|
|
// A restart: load again from the files.
|
|
assert_eq!(f.commands(), 0, "{:?}", f.session.diagnostics());
|
|
assert!(!f.session.is_plugin_enabled("demo"));
|
|
f.ok("registry.enable", &json!({ "id": "demo" }));
|
|
assert_eq!(f.commands(), 1);
|
|
let saved: Value = serde_json::from_slice(&std::fs::read(&file).unwrap()).unwrap();
|
|
assert_eq!(saved["disabled_after_failures"], json!([]));
|
|
}
|