use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Mutex;
use yosh::env::ShellEnv;
use yosh::plugin::PluginManager;
static TEST_LOCK: Mutex<()> = Mutex::new(());
fn build_test_plugin() -> PathBuf {
let manifest = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/plugins/test_plugin/Cargo.toml");
let target_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/plugins/test_plugin/target");
let status = Command::new("cargo")
.args(["build", "--manifest-path", manifest.to_str().unwrap()])
.env("CARGO_TARGET_DIR", &target_dir)
.status()
.expect("failed to run cargo build for test plugin");
assert!(status.success(), "test plugin build failed");
let target_dir = target_dir.join("debug");
if cfg!(target_os = "macos") {
target_dir.join("libtest_plugin.dylib")
} else {
target_dir.join("libtest_plugin.so")
}
}
#[test]
fn load_plugin_successfully() {
let _guard = TEST_LOCK.lock().unwrap();
let dylib = build_test_plugin();
let mut manager = PluginManager::new();
let mut env = ShellEnv::new("yosh", vec![]);
manager.load_plugin(&dylib, &mut env).unwrap();
assert!(manager.has_command("test-hello"));
assert!(manager.has_command("test-set-var"));
assert!(!manager.has_command("nonexistent"));
}
#[test]
fn exec_plugin_command() {
let _guard = TEST_LOCK.lock().unwrap();
let dylib = build_test_plugin();
let mut manager = PluginManager::new();
let mut env = ShellEnv::new("yosh", vec![]);
manager.load_plugin(&dylib, &mut env).unwrap();
let status = manager.exec_command(&mut env, "test-hello", &[]);
assert_eq!(status, Some(0));
assert_eq!(env.vars.get("TEST_EXEC_CALLED"), Some("1"));
}
#[test]
fn exec_plugin_command_with_args() {
let _guard = TEST_LOCK.lock().unwrap();
let dylib = build_test_plugin();
let mut manager = PluginManager::new();
let mut env = ShellEnv::new("yosh", vec![]);
manager.load_plugin(&dylib, &mut env).unwrap();
let status = manager.exec_command(
&mut env,
"test-set-var",
&["MY_VAR".to_string(), "my_value".to_string()],
);
assert_eq!(status, Some(0));
assert_eq!(env.vars.get("MY_VAR"), Some("my_value"));
}
#[test]
fn exec_unknown_command_returns_none() {
let _guard = TEST_LOCK.lock().unwrap();
let dylib = build_test_plugin();
let mut manager = PluginManager::new();
let mut env = ShellEnv::new("yosh", vec![]);
manager.load_plugin(&dylib, &mut env).unwrap();
let status = manager.exec_command(&mut env, "nonexistent", &[]);
assert_eq!(status, None);
}
#[test]
fn hook_pre_exec() {
let _guard = TEST_LOCK.lock().unwrap();
let dylib = build_test_plugin();
let mut manager = PluginManager::new();
let mut env = ShellEnv::new("yosh", vec![]);
manager.load_plugin(&dylib, &mut env).unwrap();
manager.call_pre_exec(&mut env, "echo hello");
assert_eq!(env.vars.get("TEST_PRE_EXEC"), Some("echo hello"));
}
#[test]
fn hook_post_exec() {
let _guard = TEST_LOCK.lock().unwrap();
let dylib = build_test_plugin();
let mut manager = PluginManager::new();
let mut env = ShellEnv::new("yosh", vec![]);
manager.load_plugin(&dylib, &mut env).unwrap();
manager.call_post_exec(&mut env, "ls -la", 0);
assert_eq!(env.vars.get("TEST_POST_EXEC"), Some("ls -la:0"));
}
#[test]
fn hook_on_cd() {
let _guard = TEST_LOCK.lock().unwrap();
let dylib = build_test_plugin();
let mut manager = PluginManager::new();
let mut env = ShellEnv::new("yosh", vec![]);
manager.load_plugin(&dylib, &mut env).unwrap();
manager.call_on_cd(&mut env, "/old/dir", "/new/dir");
assert_eq!(env.vars.get("TEST_ON_CD"), Some("/old/dir->/new/dir"));
}
#[test]
fn hook_pre_prompt() {
let _guard = TEST_LOCK.lock().unwrap();
let dylib = build_test_plugin();
let mut manager = PluginManager::new();
let mut env = ShellEnv::new("yosh", vec![]);
manager.load_plugin(&dylib, &mut env).unwrap();
manager.call_pre_prompt(&mut env);
assert_eq!(env.vars.get("TEST_PRE_PROMPT"), Some("1"));
}
#[test]
fn load_nonexistent_plugin_fails() {
let _guard = TEST_LOCK.lock().unwrap();
let mut manager = PluginManager::new();
let mut env = ShellEnv::new("yosh", vec![]);
let result = manager.load_plugin(Path::new("/nonexistent/libfoo.dylib"), &mut env);
assert!(result.is_err());
}
#[test]
fn readonly_var_rejected_by_plugin() {
let _guard = TEST_LOCK.lock().unwrap();
let dylib = build_test_plugin();
let mut manager = PluginManager::new();
let mut env = ShellEnv::new("yosh", vec![]);
manager.load_plugin(&dylib, &mut env).unwrap();
let _ = env.vars.set("RO_VAR", "immutable");
env.vars.set_readonly("RO_VAR");
let status = manager.exec_command(
&mut env,
"test-set-var",
&["RO_VAR".to_string(), "changed".to_string()],
);
assert_eq!(env.vars.get("RO_VAR"), Some("immutable"));
assert!(status.is_some());
}
#[test]
fn sandbox_deny_set_var_without_capability() {
let _guard = TEST_LOCK.lock().unwrap();
let dylib = build_test_plugin();
let mut manager = PluginManager::new();
let mut env = ShellEnv::new("yosh", vec![]);
let caps = yosh_plugin_api::CAP_VARIABLES_READ | yosh_plugin_api::CAP_IO;
manager
.load_plugin_with_capabilities(&dylib, &mut env, Some(caps))
.unwrap();
let status = manager.exec_command(
&mut env,
"test-set-var",
&["MY_VAR".to_string(), "my_value".to_string()],
);
assert!(status.is_some());
assert_eq!(env.vars.get("MY_VAR"), None);
}
#[test]
fn sandbox_hook_not_fired_without_capability() {
let _guard = TEST_LOCK.lock().unwrap();
let dylib = build_test_plugin();
let mut manager = PluginManager::new();
let mut env = ShellEnv::new("yosh", vec![]);
let caps = yosh_plugin_api::CAP_VARIABLES_READ
| yosh_plugin_api::CAP_VARIABLES_WRITE
| yosh_plugin_api::CAP_IO;
manager
.load_plugin_with_capabilities(&dylib, &mut env, Some(caps))
.unwrap();
manager.call_pre_exec(&mut env, "echo hello");
assert_eq!(env.vars.get("TEST_PRE_EXEC"), None);
manager.call_post_exec(&mut env, "ls -la", 0);
assert_eq!(env.vars.get("TEST_POST_EXEC"), None);
manager.call_on_cd(&mut env, "/old", "/new");
assert_eq!(env.vars.get("TEST_ON_CD"), None);
}
#[test]
fn sandbox_selective_hook_capability() {
let _guard = TEST_LOCK.lock().unwrap();
let dylib = build_test_plugin();
let mut manager = PluginManager::new();
let mut env = ShellEnv::new("yosh", vec![]);
let caps = yosh_plugin_api::CAP_VARIABLES_READ
| yosh_plugin_api::CAP_VARIABLES_WRITE
| yosh_plugin_api::CAP_IO
| yosh_plugin_api::CAP_HOOK_PRE_EXEC;
manager
.load_plugin_with_capabilities(&dylib, &mut env, Some(caps))
.unwrap();
manager.call_pre_exec(&mut env, "echo hello");
assert_eq!(env.vars.get("TEST_PRE_EXEC"), Some("echo hello"));
manager.call_post_exec(&mut env, "ls", 0);
assert_eq!(env.vars.get("TEST_POST_EXEC"), None);
manager.call_on_cd(&mut env, "/old", "/new");
assert_eq!(env.vars.get("TEST_ON_CD"), None);
}
#[test]
fn sandbox_full_capabilities_works_normally() {
let _guard = TEST_LOCK.lock().unwrap();
let dylib = build_test_plugin();
let mut manager = PluginManager::new();
let mut env = ShellEnv::new("yosh", vec![]);
manager.load_plugin(&dylib, &mut env).unwrap();
let status = manager.exec_command(&mut env, "test-hello", &[]);
assert_eq!(status, Some(0));
assert_eq!(env.vars.get("TEST_EXEC_CALLED"), Some("1"));
manager.call_pre_exec(&mut env, "echo");
assert_eq!(env.vars.get("TEST_PRE_EXEC"), Some("echo"));
manager.call_post_exec(&mut env, "ls", 42);
assert_eq!(env.vars.get("TEST_POST_EXEC"), Some("ls:42"));
manager.call_on_cd(&mut env, "/a", "/b");
assert_eq!(env.vars.get("TEST_ON_CD"), Some("/a->/b"));
manager.call_pre_prompt(&mut env);
assert_eq!(env.vars.get("TEST_PRE_PROMPT"), Some("1"));
}
#[test]
fn sandbox_pre_prompt_not_fired_without_capability() {
let _guard = TEST_LOCK.lock().unwrap();
let dylib = build_test_plugin();
let mut manager = PluginManager::new();
let mut env = ShellEnv::new("yosh", vec![]);
let caps = yosh_plugin_api::CAP_VARIABLES_READ
| yosh_plugin_api::CAP_VARIABLES_WRITE
| yosh_plugin_api::CAP_IO;
manager
.load_plugin_with_capabilities(&dylib, &mut env, Some(caps))
.unwrap();
manager.call_pre_prompt(&mut env);
assert_eq!(env.vars.get("TEST_PRE_PROMPT"), None);
}
#[test]
fn sandbox_selective_pre_prompt_capability() {
let _guard = TEST_LOCK.lock().unwrap();
let dylib = build_test_plugin();
let mut manager = PluginManager::new();
let mut env = ShellEnv::new("yosh", vec![]);
let caps = yosh_plugin_api::CAP_VARIABLES_READ
| yosh_plugin_api::CAP_VARIABLES_WRITE
| yosh_plugin_api::CAP_IO
| yosh_plugin_api::CAP_HOOK_PRE_PROMPT;
manager
.load_plugin_with_capabilities(&dylib, &mut env, Some(caps))
.unwrap();
manager.call_pre_prompt(&mut env);
assert_eq!(env.vars.get("TEST_PRE_PROMPT"), Some("1"));
manager.call_pre_exec(&mut env, "echo hello");
assert_eq!(env.vars.get("TEST_PRE_EXEC"), None);
}
#[test]
fn sandbox_config_restricts_capabilities() {
let _guard = TEST_LOCK.lock().unwrap();
let dylib = build_test_plugin();
let mut manager = PluginManager::new();
let mut env = ShellEnv::new("yosh", vec![]);
let caps = yosh_plugin_api::CAP_IO;
manager
.load_plugin_with_capabilities(&dylib, &mut env, Some(caps))
.unwrap();
let status = manager.exec_command(&mut env, "test-hello", &[]);
assert_eq!(status, Some(0));
assert_eq!(env.vars.get("TEST_EXEC_CALLED"), None);
}