use forjar::core::plugin_dispatch::{
available_plugin_types, dispatch_apply, dispatch_check, dispatch_destroy, is_plugin_type,
parse_plugin_type,
};
use forjar::core::plugin_hot_reload::{PluginCache, ReloadCheck};
use forjar::core::plugin_loader::{
list_plugins, resolve_and_verify, resolve_manifest, verify_plugin,
};
use forjar::core::types::PluginStatus;
use tempfile::TempDir;
fn create_plugin(dir: &std::path::Path, name: &str, wasm_content: &[u8]) -> String {
let plugin_dir = dir.join(name);
std::fs::create_dir_all(&plugin_dir).unwrap();
let hash = blake3::hash(wasm_content).to_hex().to_string();
std::fs::write(plugin_dir.join("plugin.wasm"), wasm_content).unwrap();
std::fs::write(
plugin_dir.join("plugin.yaml"),
format!(
r#"
name: {name}
version: "1.0.0"
abi_version: 1
description: "Test plugin {name}"
wasm: plugin.wasm
blake3: {hash}
permissions:
fs: {{}}
net: {{}}
env: {{}}
exec: {{}}
schema:
properties:
port:
type: integer
host:
type: string
required:
- host
"#
),
)
.unwrap();
hash
}
#[test]
fn plugin_lifecycle_check_apply_destroy() {
let dir = TempDir::new().unwrap();
create_plugin(dir.path(), "nginx-config", b"nginx wasm module v1");
let config = serde_json::json!({"host": "web-01", "port": 8080});
let check = dispatch_check(dir.path(), "nginx-config", &config);
assert!(check.success, "check failed: {}", check.message);
assert_eq!(check.operation, "check");
assert_eq!(check.status, PluginStatus::Converged);
let apply = dispatch_apply(dir.path(), "nginx-config", &config);
assert!(apply.success, "apply failed: {}", apply.message);
assert_eq!(apply.operation, "apply");
assert_eq!(apply.status, PluginStatus::Converged);
let destroy = dispatch_destroy(dir.path(), "nginx-config", &config);
assert!(destroy.success, "destroy failed: {}", destroy.message);
assert_eq!(destroy.operation, "destroy");
}
#[test]
fn manifest_resolution_and_verification() {
let dir = TempDir::new().unwrap();
let expected_hash = create_plugin(dir.path(), "verified-plugin", b"wasm content");
let manifest = resolve_manifest(dir.path(), "verified-plugin").unwrap();
assert_eq!(manifest.name, "verified-plugin");
assert_eq!(manifest.version, "1.0.0");
assert_eq!(manifest.blake3, expected_hash);
verify_plugin(dir.path(), &manifest).unwrap();
}
#[test]
fn blake3_hash_mismatch_detected() {
let dir = TempDir::new().unwrap();
create_plugin(dir.path(), "tampered", b"original content");
let wasm_path = dir.path().join("tampered").join("plugin.wasm");
std::fs::write(&wasm_path, b"tampered content").unwrap();
let manifest = resolve_manifest(dir.path(), "tampered").unwrap();
let result = verify_plugin(dir.path(), &manifest);
assert!(result.is_err(), "should detect hash mismatch");
assert!(result.unwrap_err().contains("hash mismatch"));
}
#[test]
fn schema_validation_valid_config() {
let dir = TempDir::new().unwrap();
create_plugin(dir.path(), "schema-test", b"wasm bytes");
let manifest = resolve_manifest(dir.path(), "schema-test").unwrap();
let mut props = indexmap::IndexMap::new();
props.insert(
"host".to_string(),
serde_yaml_ng::Value::String("web-01".into()),
);
props.insert(
"port".to_string(),
serde_yaml_ng::Value::Number(serde_yaml_ng::Number::from(8080)),
);
let errors = forjar::core::plugin_loader::validate_resource_schema(&manifest, &props);
assert!(errors.is_empty(), "unexpected errors: {:?}", errors);
}
#[test]
fn schema_validation_missing_required() {
let dir = TempDir::new().unwrap();
create_plugin(dir.path(), "schema-fail", b"wasm bytes");
let manifest = resolve_manifest(dir.path(), "schema-fail").unwrap();
let mut props = indexmap::IndexMap::new();
props.insert(
"port".to_string(),
serde_yaml_ng::Value::Number(serde_yaml_ng::Number::from(8080)),
);
let errors = forjar::core::plugin_loader::validate_resource_schema(&manifest, &props);
assert!(!errors.is_empty(), "should have validation errors");
}
#[test]
fn dispatch_nonexistent_plugin() {
let dir = TempDir::new().unwrap();
let config = serde_json::json!({});
let result = dispatch_check(dir.path(), "does-not-exist", &config);
assert!(!result.success);
assert_eq!(result.status, PluginStatus::Error);
}
#[test]
fn list_plugins_in_dir() {
let dir = TempDir::new().unwrap();
create_plugin(dir.path(), "alpha", b"alpha wasm");
create_plugin(dir.path(), "beta", b"beta wasm");
let plugins = list_plugins(dir.path());
assert!(plugins.contains(&"alpha".to_string()));
assert!(plugins.contains(&"beta".to_string()));
}
#[test]
fn plugin_types_prefixed() {
let dir = TempDir::new().unwrap();
create_plugin(dir.path(), "custom", b"custom wasm");
let types = available_plugin_types(dir.path());
assert!(types.contains(&"plugin:custom".to_string()));
}
#[test]
fn plugin_type_parsing() {
assert_eq!(parse_plugin_type("plugin:nginx"), Some("nginx"));
assert_eq!(parse_plugin_type("file"), None);
assert!(is_plugin_type("plugin:custom"));
assert!(!is_plugin_type("package"));
}
#[test]
fn hot_reload_detects_change() {
let dir = TempDir::new().unwrap();
let hash = create_plugin(dir.path(), "hot-plugin", b"original wasm v1");
let wasm_path = dir.path().join("hot-plugin").join("plugin.wasm");
let manifest = resolve_manifest(dir.path(), "hot-plugin").unwrap();
let mut cache = PluginCache::new();
cache.insert("hot-plugin", manifest, wasm_path.clone());
assert!(matches!(
cache.needs_reload("hot-plugin"),
ReloadCheck::UpToDate
));
std::fs::write(&wasm_path, b"updated wasm v2").unwrap();
match cache.needs_reload("hot-plugin") {
ReloadCheck::Changed { old_hash, new_hash } => {
assert_eq!(old_hash, hash);
assert_ne!(old_hash, new_hash);
}
other => panic!("expected Changed, got {:?}", other),
}
}
#[test]
fn hot_reload_detects_deletion() {
let dir = TempDir::new().unwrap();
create_plugin(dir.path(), "del-plugin", b"wasm data");
let wasm_path = dir.path().join("del-plugin").join("plugin.wasm");
let manifest = resolve_manifest(dir.path(), "del-plugin").unwrap();
let mut cache = PluginCache::new();
cache.insert("del-plugin", manifest, wasm_path.clone());
std::fs::remove_file(&wasm_path).unwrap();
assert!(matches!(
cache.needs_reload("del-plugin"),
ReloadCheck::FileGone
));
}
#[test]
fn resolve_and_verify_pipeline() {
let dir = TempDir::new().unwrap();
create_plugin(dir.path(), "pipeline", b"pipeline wasm");
let resolved = resolve_and_verify(dir.path(), "pipeline").unwrap();
assert_eq!(resolved.manifest.name, "pipeline");
assert_eq!(resolved.status, PluginStatus::Converged);
}
#[test]
fn multiple_operations_same_plugin() {
let dir = TempDir::new().unwrap();
create_plugin(dir.path(), "multi-op", b"multi wasm");
let config = serde_json::json!({"host": "app-01"});
let r1 = dispatch_check(dir.path(), "multi-op", &config);
let r2 = dispatch_check(dir.path(), "multi-op", &config);
assert!(r1.success);
assert!(r2.success);
let apply = dispatch_apply(dir.path(), "multi-op", &config);
assert!(apply.success);
let destroy = dispatch_destroy(dir.path(), "multi-op", &config);
assert!(destroy.success);
}