//! The good plugin end to end: lifecycle, commands, permissions and the plugin set. mod common; use std::sync::Arc; use common::{StubHost, load, manifest_for}; use notescraft_plugins::{Error, Limits, Manifest, Plugin, PluginSet}; const READ: [&str; 1] = ["vault.read"]; #[test] fn loads_and_reports_its_manifest() { let plugin = load("test-plugin", "good", &READ, &["echo"], Arc::new(StubHost::new())).unwrap_or_else(|e| panic!("{e}")); assert_eq!(plugin.id(), "test-plugin"); assert_eq!(plugin.manifest().name, "Test plugin"); assert_eq!(plugin.limits(), &Limits::default()); assert!(!plugin.is_disabled()); } #[test] fn run_command_echoes_args_and_reaches_the_host() { let host = Arc::new(StubHost::new()); let mut plugin = load("test-plugin", "good", &READ, &["echo"], Arc::clone(&host) as Arc<_>).unwrap_or_else(|e| panic!("{e}")); let out = plugin.run_command("echo", r#"{"n":1}"#).unwrap_or_else(|e| panic!("{e}")); assert_eq!(out, r#"{"n":1}"#); assert_eq!(host.calls(), vec![String::from("log(info,running)"), String::from("vault_read(notes/a.md)")]); } #[test] fn lifecycle_runs_load_unload_and_events() { let host = Arc::new(StubHost::new()); let mut plugin = load("test-plugin", "good", &READ, &["echo"], Arc::clone(&host) as Arc<_>).unwrap_or_else(|e| panic!("{e}")); plugin.on_load().unwrap_or_else(|e| panic!("{e}")); plugin.send_event(r#"{"type":"note.opened"}"#).unwrap_or_else(|e| panic!("{e}")); plugin.on_unload().unwrap_or_else(|e| panic!("{e}")); assert_eq!(host.calls(), vec![String::from("log(info,running)"), String::from("notice(goodbye)")]); } #[test] fn a_call_without_its_permission_never_reaches_the_host() { let host = Arc::new(StubHost::new()); let mut plugin = load("test-plugin", "good", &[], &["echo"], Arc::clone(&host) as Arc<_>).unwrap_or_else(|e| panic!("{e}")); let err = plugin.run_command("echo", "{}").unwrap_err(); assert!(matches!(err, Error::PermissionDenied { call: "vault_read", .. }), "{err}"); // `ns.log` ran first and is always allowed; the read never did. assert_eq!(host.calls(), vec![String::from("log(info,running)")]); } #[test] fn empty_arguments_are_accepted() { let host = Arc::new(StubHost::new()); let mut plugin = load("test-plugin", "good", &READ, &["echo"], host as Arc<_>).unwrap_or_else(|e| panic!("{e}")); assert_eq!(plugin.run_command("echo", "").unwrap_or_else(|e| panic!("{e}")), ""); } #[test] fn a_declared_permission_lets_the_call_through() { let host = Arc::new(StubHost::new()); let mut plugin = load("test-plugin", "good", &["vault.read"], &["echo"], Arc::clone(&host) as Arc<_>).unwrap_or_else(|e| panic!("{e}")); plugin.run_command("echo", "{}").unwrap_or_else(|e| panic!("{e}")); assert!(host.calls().contains(&String::from("vault_read(notes/a.md)"))); } #[test] fn a_failing_host_call_is_reported_with_its_message() { let host = Arc::new(StubHost::failing()); let mut plugin = load("test-plugin", "good", &READ, &["echo"], host as Arc<_>).unwrap_or_else(|e| panic!("{e}")); let err = plugin.run_command("echo", "{}").unwrap_err(); assert!(matches!(&err, Error::HostCall { call: "log", message, .. } if message == "the vault is locked"), "{err}"); } #[test] fn an_oversize_host_answer_hits_the_result_cap() { let host = Arc::new(StubHost::with_payload(&"x".repeat(5 << 20))); let mut plugin = load("test-plugin", "huge_host_answer", &[], &["now"], host as Arc<_>).unwrap_or_else(|e| panic!("{e}")); let err = plugin.run_command("now", "{}").unwrap_err(); assert!(matches!(err, Error::PluginLimit(_)), "{err}"); } #[test] fn vault_write_needs_its_own_permission() { let host = Arc::new(StubHost::new()); let mut plugin = load("test-plugin", "write", &["vault.read"], &["write"], Arc::clone(&host) as Arc<_>).unwrap_or_else(|e| panic!("{e}")); let err = plugin.run_command("write", "body").unwrap_err(); assert!(matches!(&err, Error::PermissionDenied { call: "vault_write", .. }), "{err}"); assert_eq!(host.calls().len(), 0); let host = Arc::new(StubHost::new()); let mut plugin = load("test-plugin", "write", &["vault.write"], &["write"], Arc::clone(&host) as Arc<_>).unwrap_or_else(|e| panic!("{e}")); plugin.run_command("write", "body").unwrap_or_else(|e| panic!("{e}")); assert_eq!(host.calls(), vec![String::from("vault_write(notes/new.md,body)")]); } #[test] fn back_to_back_calls_stay_inside_the_per_call_host_budget() { // The budget is 8 host calls per millisecond *of one guest call*. Two calls in a row is // 4 host calls; a plugin that is busy must not be charged for its neighbour's calls. let host = Arc::new(StubHost::new()); let mut plugin = load("test-plugin", "good", &READ, &["echo"], host as Arc<_>).unwrap_or_else(|e| panic!("{e}")); for round in 0..50 { plugin.run_command("echo", "{}").unwrap_or_else(|e| panic!("round {round}: {e}")); } } #[test] fn an_unknown_command_is_not_found() { let host = Arc::new(StubHost::new()); let mut plugin = load("test-plugin", "good", &READ, &["echo"], host as Arc<_>).unwrap_or_else(|e| panic!("{e}")); assert!(matches!(plugin.run_command("nope", "{}"), Err(Error::NotFound(_)))); } #[test] fn an_invalid_manifest_is_rejected_before_the_module_runs() { let bad = Manifest { id: "Bad Id".into(), ..manifest_for("x", &[], &[]) }; let err = Plugin::load(bad, &common::wasm("good"), Arc::new(StubHost::new()), Limits::default()).unwrap_err(); assert!(matches!(err, Error::Manifest(_)), "{err}"); } #[test] fn the_plugin_set_lists_and_dispatches_full_command_ids() { let mut set = PluginSet::new(); set.insert(load("alpha", "good", &READ, &["echo", "shout"], Arc::new(StubHost::new())).unwrap_or_else(|e| panic!("{e}"))); set.insert(load("beta", "good", &READ, &["only"], Arc::new(StubHost::new())).unwrap_or_else(|e| panic!("{e}"))); assert_eq!(set.commands(), vec![String::from("plugin.alpha.echo"), String::from("plugin.alpha.shout"), String::from("plugin.beta.only")]); assert_eq!(set.ids(), vec!["alpha", "beta"]); assert!(set.get("alpha").is_some()); assert!(set.get_mut("alpha").is_some()); let out = set.dispatch("plugin.alpha.echo", r#"{"a":true}"#).unwrap_or_else(|e| panic!("{e}")); assert_eq!(out, r#"{"a":true}"#); assert!(matches!(set.dispatch("plugin.alpha.nope", "{}"), Err(Error::NotFound(_)))); assert!(matches!(set.dispatch("plugin.other.echo", "{}"), Err(Error::NotFound(_)))); assert!(matches!(set.dispatch("echo", "{}"), Err(Error::NotFound(_)))); assert!(set.remove("alpha")); assert!(!set.remove("alpha")); assert_eq!(set.len(), 1); assert!(set.remove("beta")); assert!(set.is_empty()); } #[test] fn broadcast_reports_one_error_per_failing_plugin() { let mut set = PluginSet::new(); set.insert(load("test-plugin", "trap", &[], &["echo"], Arc::new(StubHost::new())).unwrap_or_else(|e| panic!("{e}"))); let errors = set.broadcast_event(r#"{"type":"vault.indexed"}"#); assert_eq!(errors.len(), 1); assert_eq!(errors.first().map(|(id, _)| id.as_str()), Some("test-plugin")); }