tsafe-cli 1.0.23

Local-first developer secret vault CLI — encrypted storage, process injection via exec, cloud sync, audit trail
Documentation
//! Integration tests for the plugin registry-file feature (ADR-031, task E5.3).
//!
//! These tests exercise the full CLI process for the adversarial proof scenarios
//! required by E5.3:
//!
//!   1. Registry file forgery attempt — command stored not executed at parse time.
//!   2. Missing required field — entry skipped, other entries still load.
//!   3. Missing registry file — error to stderr, non-zero exit.
//!   4. Static entry wins on name conflict — warning emitted.
//!
//! All tests require the `plugins` feature. Each uses `TSAFE_PLUGIN_REGISTRY`
//! to point at a temp file.

#[cfg(feature = "plugins")]
mod registry_integration {
    use assert_cmd::Command;
    use predicates::prelude::PredicateBooleanExt;
    use predicates::str::contains;
    use std::io::Write as _;
    use tempfile::tempdir;

    fn tsafe() -> Command {
        Command::cargo_bin("tsafe").unwrap()
    }

    fn init_vault(dir: &std::path::Path) {
        tsafe()
            .args(["init"])
            .env("TSAFE_VAULT_DIR", dir)
            .env("TSAFE_PASSWORD", "pw")
            .assert()
            .success();
    }

    fn write_registry(content: &str) -> tempfile::NamedTempFile {
        let mut f = tempfile::NamedTempFile::new().unwrap();
        f.write_all(content.as_bytes()).unwrap();
        f
    }

    // ── E5.3-1: Forgery attempt — stored, not executed ─────────────────────

    /// A registry entry containing a shell injection in the `command` field is
    /// stored literally and never executed at parse/list time. The `tsafe plugin
    /// list` command must succeed with exit 0 and display the entry without any
    /// side effects from executing the command string.
    ///
    /// This is the adversarial proof: if any code-execution occurred at load
    /// time (e.g., the command string was eval'd), this test would hang or fail.
    #[test]
    fn registry_forgery_command_listed_not_executed_at_load_time() {
        let dir = tempdir().unwrap();
        init_vault(dir.path());

        // A command string that would cause visible side effects if eval'd.
        // On any platform, this is a syntax error if run literally — which is
        // correct behavior: tsafe must not run it at all during loading.
        let registry = r#"
[[plugins]]
name = "evil-tool"
command = "/bin/sh -c 'rm -rf /'"
description = "Adversarial entry"
"#;
        let f = write_registry(registry);

        // `tsafe plugin list` must succeed and display the entry — no execution.
        tsafe()
            .args(["plugin", "list"])
            .env("TSAFE_VAULT_DIR", dir.path())
            .env("TSAFE_PASSWORD", "pw")
            .env("TSAFE_PLUGIN_REGISTRY", f.path())
            .assert()
            .success()
            // The entry name appears in the list.
            .stdout(contains("evil-tool"))
            // It is tagged [registry], not [built-in].
            .stdout(contains("[registry]"))
            // Static entries still appear.
            .stdout(contains("[built-in]"));
    }

    // ── E5.3-2: Missing required field — entry skipped ─────────────────────

    /// An entry missing the `command` field is skipped with a warning to stderr.
    /// Other valid entries in the same file continue to load normally.
    #[test]
    fn registry_entry_missing_command_skipped_others_load() {
        let dir = tempdir().unwrap();
        init_vault(dir.path());

        let registry = r#"
[[plugins]]
name = "bad-no-command"
# command is missing — this entry must be skipped

[[plugins]]
name = "good-tool"
command = "/usr/local/bin/good-tool"
description = "A valid entry"
"#;
        let f = write_registry(registry);

        let assert = tsafe()
            .args(["plugin", "list"])
            .env("TSAFE_VAULT_DIR", dir.path())
            .env("TSAFE_PASSWORD", "pw")
            .env("TSAFE_PLUGIN_REGISTRY", f.path())
            .assert()
            .success();

        // The good entry loads.
        assert.stdout(contains("good-tool"));

        // The bad entry is NOT listed (it was skipped).
        // We can check stderr for the warning.
        // Note: `assert` has been consumed; re-run to check stderr separately.
        tsafe()
            .args(["plugin", "list"])
            .env("TSAFE_VAULT_DIR", dir.path())
            .env("TSAFE_PASSWORD", "pw")
            .env("TSAFE_PLUGIN_REGISTRY", f.path())
            .assert()
            .success()
            .stderr(contains("skipping"))
            .stderr(contains("command"));
    }

    /// An entry missing the `name` field is also skipped with a warning.
    #[test]
    fn registry_entry_missing_name_skipped() {
        let dir = tempdir().unwrap();
        init_vault(dir.path());

        let registry = r#"
[[plugins]]
command = "/usr/local/bin/nameless"
# name is missing

[[plugins]]
name = "named-tool"
command = "/usr/local/bin/named-tool"
"#;
        let f = write_registry(registry);

        tsafe()
            .args(["plugin", "list"])
            .env("TSAFE_VAULT_DIR", dir.path())
            .env("TSAFE_PASSWORD", "pw")
            .env("TSAFE_PLUGIN_REGISTRY", f.path())
            .assert()
            .success()
            .stdout(contains("named-tool"))
            .stderr(contains("skipping"))
            .stderr(contains("name"));
    }

    // ── E5.3-3: Missing registry file → error, non-zero exit ───────────────

    /// When `TSAFE_PLUGIN_REGISTRY` is set to a non-existent path, tsafe must
    /// exit non-zero and print an error mentioning the registry problem.
    /// The error path must not silently succeed.
    #[test]
    fn missing_registry_file_exits_nonzero_with_error() {
        let dir = tempdir().unwrap();
        init_vault(dir.path());

        tsafe()
            .args(["plugin", "list"])
            .env("TSAFE_VAULT_DIR", dir.path())
            .env("TSAFE_PASSWORD", "pw")
            .env("TSAFE_PLUGIN_REGISTRY", "/nonexistent/path/to/plugins.toml")
            .assert()
            .failure()
            .stderr(contains("plugin registry"));
    }

    // ── E5.3-4: Static entry wins on name conflict ──────────────────────────

    /// When a registry entry has the same `name` as a static built-in entry,
    /// the static entry wins. The registry entry does NOT shadow the built-in.
    /// A warning must be emitted to inform the operator.
    #[test]
    fn static_entry_wins_on_name_conflict_warning_emitted() {
        let dir = tempdir().unwrap();
        init_vault(dir.path());

        // "gh" is a static built-in. A registry entry with the same name must
        // not override it.
        let registry = r#"
[[plugins]]
name = "gh"
command = "/attacker/fake-gh"
description = "Shadowing attempt"
"#;
        let f = write_registry(registry);

        // The list command must succeed, show gh as [built-in], and emit a
        // warning about the conflict.
        tsafe()
            .args(["plugin", "list"])
            .env("TSAFE_VAULT_DIR", dir.path())
            .env("TSAFE_PASSWORD", "pw")
            .env("TSAFE_PLUGIN_REGISTRY", f.path())
            .assert()
            .success()
            // gh appears (from static)
            .stdout(contains("gh"))
            // Warning about the conflict — must mention the tool name and conflict
            .stderr(contains("gh"))
            .stderr(
                contains("built-in")
                    .or(contains("conflict"))
                    .or(contains("static")),
            );
    }

    // ── Registry entry with optional fields ────────────────────────────────

    /// Optional fields (description, args, url) are shown correctly in list.
    #[test]
    fn registry_entry_with_optional_fields_shown_in_list() {
        let dir = tempdir().unwrap();
        init_vault(dir.path());

        let registry = r#"
[[plugins]]
name = "my-tool"
command = "/usr/local/bin/my-tool"
description = "My custom operator tool"
url = "https://example.com/my-tool"
args = ["--tsafe-mode"]
"#;
        let f = write_registry(registry);

        tsafe()
            .args(["plugin", "list"])
            .env("TSAFE_VAULT_DIR", dir.path())
            .env("TSAFE_PASSWORD", "pw")
            .env("TSAFE_PLUGIN_REGISTRY", f.path())
            .assert()
            .success()
            .stdout(contains("my-tool"))
            .stdout(contains("My custom operator tool"))
            .stdout(contains("[registry]"));
    }

    // ── No registry env var → opt-in only ──────────────────────────────────

    /// When `TSAFE_PLUGIN_REGISTRY` is not set, the plugin list shows only
    /// static built-ins. No implicit path is searched.
    #[test]
    fn no_registry_env_var_shows_only_builtins() {
        let dir = tempdir().unwrap();
        init_vault(dir.path());

        tsafe()
            .args(["plugin", "list"])
            .env("TSAFE_VAULT_DIR", dir.path())
            .env("TSAFE_PASSWORD", "pw")
            .env_remove("TSAFE_PLUGIN_REGISTRY")
            .assert()
            .success()
            .stdout(contains("[built-in]"))
            // No [registry] tag when no registry is configured.
            .stdout(contains("[registry]").not());
    }
}