249 lines
7.7 KiB
Rust
249 lines
7.7 KiB
Rust
|
|
//! Manager-level tests for the admin management surface (install / toggle /
|
||
|
|
//! remove) and disabled-state persistence. These drive a real Extism sandbox,
|
||
|
|
//! so they run only under `cargo test --features plugins`.
|
||
|
|
//!
|
||
|
|
//! The `.wasm` fixtures are the same ones the runtime tests use, built by
|
||
|
|
//! `scripts/build-plugin-hello.sh`.
|
||
|
|
|
||
|
|
use super::ExtismPluginManager;
|
||
|
|
use crate::application::ports::plugin_ports::{PluginDispatchPort, PluginManagementPort};
|
||
|
|
use crate::common::config::PluginConfig;
|
||
|
|
|
||
|
|
fn cfg() -> PluginConfig {
|
||
|
|
PluginConfig::default()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn fixture(name: &str) -> Vec<u8> {
|
||
|
|
let path = format!(
|
||
|
|
"{}/tests/fixtures/plugins/{}",
|
||
|
|
env!("CARGO_MANIFEST_DIR"),
|
||
|
|
name
|
||
|
|
);
|
||
|
|
std::fs::read(&path).unwrap_or_else(|e| {
|
||
|
|
panic!("missing fixture {path}: {e}\n run scripts/build-plugin-hello.sh to (re)build it")
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
/// A valid manifest for the `hello.wasm` fixture (subscribes to both events).
|
||
|
|
fn hello_manifest() -> String {
|
||
|
|
r#"
|
||
|
|
[plugin]
|
||
|
|
id = "com.example.hello"
|
||
|
|
name = "Hello"
|
||
|
|
version = "0.1.0"
|
||
|
|
abi = 0
|
||
|
|
entrypoint = "hello.wasm"
|
||
|
|
|
||
|
|
[events]
|
||
|
|
subscribe = ["file.uploaded", "user.login"]
|
||
|
|
"#
|
||
|
|
.to_string()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// A manifest that parses fine but points at the `wrong_abi.wasm` fixture,
|
||
|
|
/// which reports ABI 1 at runtime.
|
||
|
|
fn wrong_abi_manifest() -> String {
|
||
|
|
r#"
|
||
|
|
[plugin]
|
||
|
|
id = "com.example.wrongabi"
|
||
|
|
name = "Wrong ABI"
|
||
|
|
version = "0.1.0"
|
||
|
|
abi = 0
|
||
|
|
entrypoint = "wrong_abi.wasm"
|
||
|
|
|
||
|
|
[events]
|
||
|
|
subscribe = ["file.uploaded"]
|
||
|
|
"#
|
||
|
|
.to_string()
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn install_loads_plugin_and_writes_files() {
|
||
|
|
let tmp = tempfile::tempdir().unwrap();
|
||
|
|
let mgr = ExtismPluginManager::load_from_dir(cfg(), tmp.path());
|
||
|
|
assert_eq!(mgr.loaded_count(), 0);
|
||
|
|
|
||
|
|
let info = mgr
|
||
|
|
.install(&hello_manifest(), fixture("hello.wasm"))
|
||
|
|
.expect("install should succeed");
|
||
|
|
|
||
|
|
assert_eq!(info.id, "com.example.hello");
|
||
|
|
assert_eq!(info.name, "Hello");
|
||
|
|
assert!(info.enabled);
|
||
|
|
assert_eq!(info.subscriptions, vec!["file.uploaded", "user.login"]);
|
||
|
|
assert_eq!(mgr.loaded_count(), 1);
|
||
|
|
|
||
|
|
let plugin_dir = tmp.path().join("com.example.hello");
|
||
|
|
assert!(plugin_dir.join("plugin.toml").exists());
|
||
|
|
assert!(plugin_dir.join("hello.wasm").exists());
|
||
|
|
|
||
|
|
// The live dispatch path sees it immediately.
|
||
|
|
assert!(mgr.has_subscribers("file.uploaded"));
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Build an in-memory `.zip` with the given entries (name, bytes).
|
||
|
|
fn make_zip(entries: &[(&str, &[u8])]) -> Vec<u8> {
|
||
|
|
use std::io::Write;
|
||
|
|
use zip::write::SimpleFileOptions;
|
||
|
|
let mut writer = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||
|
|
for (name, bytes) in entries {
|
||
|
|
writer
|
||
|
|
.start_file(*name, SimpleFileOptions::default())
|
||
|
|
.unwrap();
|
||
|
|
writer.write_all(bytes).unwrap();
|
||
|
|
}
|
||
|
|
writer.finish().unwrap().into_inner()
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn install_bundle_from_zip_loads_plugin() {
|
||
|
|
let tmp = tempfile::tempdir().unwrap();
|
||
|
|
let mgr = ExtismPluginManager::load_from_dir(cfg(), tmp.path());
|
||
|
|
|
||
|
|
// Wrap everything under a top-level folder to exercise prefix resolution.
|
||
|
|
let zip = make_zip(&[
|
||
|
|
("hello/plugin.toml", hello_manifest().as_bytes()),
|
||
|
|
("hello/hello.wasm", &fixture("hello.wasm")),
|
||
|
|
]);
|
||
|
|
|
||
|
|
let info = mgr
|
||
|
|
.install_bundle(zip)
|
||
|
|
.expect("bundle install should succeed");
|
||
|
|
assert_eq!(info.id, "com.example.hello");
|
||
|
|
assert!(info.enabled);
|
||
|
|
assert_eq!(mgr.loaded_count(), 1);
|
||
|
|
assert!(
|
||
|
|
tmp.path()
|
||
|
|
.join("com.example.hello")
|
||
|
|
.join("hello.wasm")
|
||
|
|
.exists()
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn install_bundle_without_manifest_is_rejected() {
|
||
|
|
let tmp = tempfile::tempdir().unwrap();
|
||
|
|
let mgr = ExtismPluginManager::load_from_dir(cfg(), tmp.path());
|
||
|
|
|
||
|
|
let zip = make_zip(&[("hello.wasm", &fixture("hello.wasm"))]);
|
||
|
|
let err = mgr
|
||
|
|
.install_bundle(zip)
|
||
|
|
.expect_err("a zip without plugin.toml must be rejected");
|
||
|
|
assert_eq!(err.reason(), "no_manifest_in_zip");
|
||
|
|
assert_eq!(mgr.loaded_count(), 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn install_bundle_missing_entrypoint_is_rejected() {
|
||
|
|
let tmp = tempfile::tempdir().unwrap();
|
||
|
|
let mgr = ExtismPluginManager::load_from_dir(cfg(), tmp.path());
|
||
|
|
|
||
|
|
// Manifest declares entrypoint = "hello.wasm", but the zip omits it.
|
||
|
|
let zip = make_zip(&[("plugin.toml", hello_manifest().as_bytes())]);
|
||
|
|
let err = mgr
|
||
|
|
.install_bundle(zip)
|
||
|
|
.expect_err("a zip missing the entrypoint wasm must be rejected");
|
||
|
|
assert_eq!(err.reason(), "entrypoint_not_in_zip");
|
||
|
|
assert_eq!(mgr.loaded_count(), 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn install_bundle_with_garbage_is_rejected() {
|
||
|
|
let tmp = tempfile::tempdir().unwrap();
|
||
|
|
let mgr = ExtismPluginManager::load_from_dir(cfg(), tmp.path());
|
||
|
|
let err = mgr
|
||
|
|
.install_bundle(b"not a zip file".to_vec())
|
||
|
|
.expect_err("non-zip bytes must be rejected");
|
||
|
|
assert_eq!(err.reason(), "bad_zip");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn install_duplicate_id_is_rejected() {
|
||
|
|
let tmp = tempfile::tempdir().unwrap();
|
||
|
|
let mgr = ExtismPluginManager::load_from_dir(cfg(), tmp.path());
|
||
|
|
mgr.install(&hello_manifest(), fixture("hello.wasm"))
|
||
|
|
.unwrap();
|
||
|
|
|
||
|
|
let err = mgr
|
||
|
|
.install(&hello_manifest(), fixture("hello.wasm"))
|
||
|
|
.expect_err("second install of the same id must fail");
|
||
|
|
assert_eq!(err.reason(), "id_exists");
|
||
|
|
assert_eq!(mgr.loaded_count(), 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn install_wrong_abi_is_rejected() {
|
||
|
|
let tmp = tempfile::tempdir().unwrap();
|
||
|
|
let mgr = ExtismPluginManager::load_from_dir(cfg(), tmp.path());
|
||
|
|
|
||
|
|
let err = mgr
|
||
|
|
.install(&wrong_abi_manifest(), fixture("wrong_abi.wasm"))
|
||
|
|
.expect_err("a plugin reporting the wrong ABI must be rejected");
|
||
|
|
assert_eq!(err.reason(), "abi_mismatch");
|
||
|
|
assert_eq!(mgr.loaded_count(), 0);
|
||
|
|
// Nothing should have been written to disk.
|
||
|
|
assert!(!tmp.path().join("com.example.wrongabi").exists());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn disable_stops_dispatch_and_persists_across_reload() {
|
||
|
|
let tmp = tempfile::tempdir().unwrap();
|
||
|
|
let mgr = ExtismPluginManager::load_from_dir(cfg(), tmp.path());
|
||
|
|
mgr.install(&hello_manifest(), fixture("hello.wasm"))
|
||
|
|
.unwrap();
|
||
|
|
assert!(mgr.has_subscribers("file.uploaded"));
|
||
|
|
|
||
|
|
mgr.set_enabled("com.example.hello", false).unwrap();
|
||
|
|
assert!(!mgr.has_subscribers("file.uploaded"));
|
||
|
|
assert!(
|
||
|
|
tmp.path()
|
||
|
|
.join("com.example.hello")
|
||
|
|
.join(".disabled")
|
||
|
|
.exists()
|
||
|
|
);
|
||
|
|
|
||
|
|
// A fresh manager re-reads the marker and loads it disabled.
|
||
|
|
drop(mgr);
|
||
|
|
let reloaded = ExtismPluginManager::load_from_dir(cfg(), tmp.path());
|
||
|
|
assert_eq!(reloaded.loaded_count(), 1);
|
||
|
|
let info = reloaded.list();
|
||
|
|
assert_eq!(info.len(), 1);
|
||
|
|
assert!(!info[0].enabled);
|
||
|
|
assert!(!reloaded.has_subscribers("file.uploaded"));
|
||
|
|
|
||
|
|
// Re-enabling removes the marker.
|
||
|
|
reloaded.set_enabled("com.example.hello", true).unwrap();
|
||
|
|
assert!(reloaded.has_subscribers("file.uploaded"));
|
||
|
|
assert!(
|
||
|
|
!tmp.path()
|
||
|
|
.join("com.example.hello")
|
||
|
|
.join(".disabled")
|
||
|
|
.exists()
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn set_enabled_unknown_id_is_not_found() {
|
||
|
|
let tmp = tempfile::tempdir().unwrap();
|
||
|
|
let mgr = ExtismPluginManager::load_from_dir(cfg(), tmp.path());
|
||
|
|
let err = mgr.set_enabled("does.not.exist", false).unwrap_err();
|
||
|
|
assert_eq!(err.reason(), "not_found");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn remove_unloads_and_deletes_directory() {
|
||
|
|
let tmp = tempfile::tempdir().unwrap();
|
||
|
|
let mgr = ExtismPluginManager::load_from_dir(cfg(), tmp.path());
|
||
|
|
mgr.install(&hello_manifest(), fixture("hello.wasm"))
|
||
|
|
.unwrap();
|
||
|
|
let plugin_dir = tmp.path().join("com.example.hello");
|
||
|
|
assert!(plugin_dir.exists());
|
||
|
|
|
||
|
|
mgr.remove("com.example.hello").unwrap();
|
||
|
|
assert_eq!(mgr.loaded_count(), 0);
|
||
|
|
assert!(!plugin_dir.exists());
|
||
|
|
|
||
|
|
let err = mgr.remove("com.example.hello").unwrap_err();
|
||
|
|
assert_eq!(err.reason(), "not_found");
|
||
|
|
}
|