tsafe-cli 1.0.21

tsafe CLI — local secret and credential manager (replaces .env files)
Documentation
//! Focused integration coverage for namespace copy/move conflict and alias edges.

use assert_cmd::assert::Assert;
use predicates::prelude::*;
use tempfile::tempdir;

use super::tsafe;

const PASSWORD: &str = "test-pw";

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

fn set_secret(vault_dir: &std::path::Path, key: &str, value: &str) {
    tsafe()
        .args(["set", key, value])
        .env("TSAFE_VAULT_DIR", vault_dir)
        .env("TSAFE_PASSWORD", PASSWORD)
        .assert()
        .success();
}

fn overwrite_secret(vault_dir: &std::path::Path, key: &str, value: &str) {
    tsafe()
        .args(["set", key, value, "--overwrite"])
        .env("TSAFE_VAULT_DIR", vault_dir)
        .env("TSAFE_PASSWORD", PASSWORD)
        .assert()
        .success();
}

fn create_alias(vault_dir: &std::path::Path, target: &str, alias: &str) {
    tsafe()
        .args(["alias", target, alias])
        .env("TSAFE_VAULT_DIR", vault_dir)
        .env("TSAFE_PASSWORD", PASSWORD)
        .assert()
        .success();
}

fn assert_secret_value(vault_dir: &std::path::Path, key: &str, expected: &str) {
    tsafe()
        .args(["get", key])
        .env("TSAFE_VAULT_DIR", vault_dir)
        .env("TSAFE_PASSWORD", PASSWORD)
        .assert()
        .success()
        .stdout(predicate::str::contains(expected));
}

fn assert_missing_secret(vault_dir: &std::path::Path, key: &str) {
    tsafe()
        .args(["get", key])
        .env("TSAFE_VAULT_DIR", vault_dir)
        .env("TSAFE_PASSWORD", PASSWORD)
        .assert()
        .failure()
        .stderr(predicate::str::contains(format!(
            "secret '{key}' not found"
        )));
}

fn assert_alias_list(vault_dir: &std::path::Path) -> Assert {
    tsafe()
        .args(["alias", "--list"])
        .env("TSAFE_VAULT_DIR", vault_dir)
        .env("TSAFE_PASSWORD", PASSWORD)
        .assert()
        .success()
}

#[test]
fn ns_copy_without_force_aborts_before_partial_writes() {
    let dir = tempdir().unwrap();
    let vault_dir = dir.path();
    init_vault(vault_dir);

    set_secret(vault_dir, "prod/API_KEY", "source-api");
    set_secret(vault_dir, "prod/DB_URL", "postgres://source");
    set_secret(vault_dir, "staging/API_KEY", "existing-api");

    tsafe()
        .args(["ns", "copy", "prod", "staging"])
        .env("TSAFE_VAULT_DIR", vault_dir)
        .env("TSAFE_PASSWORD", PASSWORD)
        .assert()
        .failure()
        .stderr(predicate::str::contains(
            "destination key 'staging/API_KEY' already exists",
        ));

    assert_secret_value(vault_dir, "staging/API_KEY", "existing-api");
    assert_missing_secret(vault_dir, "staging/DB_URL");
    assert_secret_value(vault_dir, "prod/API_KEY", "source-api");
    assert_secret_value(vault_dir, "prod/DB_URL", "postgres://source");
}

#[test]
fn ns_move_without_force_aborts_cleanly_on_conflicts() {
    let dir = tempdir().unwrap();
    let vault_dir = dir.path();
    init_vault(vault_dir);

    set_secret(vault_dir, "old/API_KEY", "source-api");
    set_secret(vault_dir, "old/DB_URL", "postgres://source");
    set_secret(vault_dir, "new/API_KEY", "existing-api");

    tsafe()
        .args(["ns", "move", "old", "new"])
        .env("TSAFE_VAULT_DIR", vault_dir)
        .env("TSAFE_PASSWORD", PASSWORD)
        .assert()
        .failure()
        .stderr(predicate::str::contains(
            "destination key 'new/API_KEY' already exists",
        ));

    assert_secret_value(vault_dir, "new/API_KEY", "existing-api");
    assert_missing_secret(vault_dir, "new/DB_URL");
    assert_secret_value(vault_dir, "old/API_KEY", "source-api");
    assert_secret_value(vault_dir, "old/DB_URL", "postgres://source");
}

#[test]
fn ns_copy_force_overwrites_destination_and_keeps_source() {
    let dir = tempdir().unwrap();
    let vault_dir = dir.path();
    init_vault(vault_dir);

    set_secret(vault_dir, "prod/API_KEY", "fresh-api");
    set_secret(vault_dir, "prod/DB_URL", "postgres://fresh");
    set_secret(vault_dir, "staging/API_KEY", "stale-api");

    tsafe()
        .args(["ns", "copy", "prod", "staging", "--force"])
        .env("TSAFE_VAULT_DIR", vault_dir)
        .env("TSAFE_PASSWORD", PASSWORD)
        .assert()
        .success()
        .stdout(predicate::str::contains("Copied 2 key"));

    assert_secret_value(vault_dir, "staging/API_KEY", "fresh-api");
    assert_secret_value(vault_dir, "staging/DB_URL", "postgres://fresh");
    assert_secret_value(vault_dir, "prod/API_KEY", "fresh-api");
    assert_secret_value(vault_dir, "prod/DB_URL", "postgres://fresh");
}

#[test]
fn ns_move_force_overwrites_destination_and_removes_source() {
    let dir = tempdir().unwrap();
    let vault_dir = dir.path();
    init_vault(vault_dir);

    set_secret(vault_dir, "old/API_KEY", "fresh-api");
    set_secret(vault_dir, "old/DB_URL", "postgres://fresh");
    set_secret(vault_dir, "new/API_KEY", "stale-api");

    tsafe()
        .args(["ns", "move", "old", "new", "--force"])
        .env("TSAFE_VAULT_DIR", vault_dir)
        .env("TSAFE_PASSWORD", PASSWORD)
        .assert()
        .success()
        .stdout(predicate::str::contains("Moved 2 key"));

    assert_secret_value(vault_dir, "new/API_KEY", "fresh-api");
    assert_secret_value(vault_dir, "new/DB_URL", "postgres://fresh");
    assert_missing_secret(vault_dir, "old/API_KEY");
    assert_missing_secret(vault_dir, "old/DB_URL");
}

#[test]
fn ns_copy_retargets_intra_namespace_aliases_and_lists_new_target() {
    let dir = tempdir().unwrap();
    let vault_dir = dir.path();
    init_vault(vault_dir);

    set_secret(vault_dir, "prod/REAL", "alpha");
    create_alias(vault_dir, "prod/REAL", "prod/ALIAS");

    tsafe()
        .args(["ns", "copy", "prod", "staging"])
        .env("TSAFE_VAULT_DIR", vault_dir)
        .env("TSAFE_PASSWORD", PASSWORD)
        .assert()
        .success();

    assert_alias_list(vault_dir)
        .stdout(predicate::str::contains("prod/ALIAS  →  prod/REAL"))
        .stdout(predicate::str::contains("staging/ALIAS  →  staging/REAL"));

    assert_secret_value(vault_dir, "staging/ALIAS", "alpha");
    overwrite_secret(vault_dir, "staging/REAL", "beta");
    assert_secret_value(vault_dir, "staging/ALIAS", "beta");
    assert_secret_value(vault_dir, "prod/ALIAS", "alpha");
}

#[test]
fn ns_move_retargets_intra_namespace_aliases_and_keeps_them_resolvable() {
    let dir = tempdir().unwrap();
    let vault_dir = dir.path();
    init_vault(vault_dir);

    set_secret(vault_dir, "prod/REAL", "alpha");
    create_alias(vault_dir, "prod/REAL", "prod/ALIAS");

    tsafe()
        .args(["ns", "move", "prod", "staging"])
        .env("TSAFE_VAULT_DIR", vault_dir)
        .env("TSAFE_PASSWORD", PASSWORD)
        .assert()
        .success();

    assert_alias_list(vault_dir)
        .stdout(predicate::str::contains("staging/ALIAS  →  staging/REAL"))
        .stdout(predicate::str::contains("prod/ALIAS").not());

    assert_secret_value(vault_dir, "staging/ALIAS", "alpha");
    assert_missing_secret(vault_dir, "prod/REAL");
    assert_missing_secret(vault_dir, "prod/ALIAS");
}

#[test]
fn ns_copy_preserves_external_alias_targets_when_alias_key_is_copied() {
    let dir = tempdir().unwrap();
    let vault_dir = dir.path();
    init_vault(vault_dir);

    set_secret(vault_dir, "shared/REAL", "omega");
    set_secret(vault_dir, "prod/LOCAL", "alpha");
    create_alias(vault_dir, "shared/REAL", "prod/EXTERNAL_ALIAS");

    tsafe()
        .args(["ns", "copy", "prod", "staging"])
        .env("TSAFE_VAULT_DIR", vault_dir)
        .env("TSAFE_PASSWORD", PASSWORD)
        .assert()
        .success();

    assert_alias_list(vault_dir)
        .stdout(predicate::str::contains(
            "prod/EXTERNAL_ALIAS  →  shared/REAL",
        ))
        .stdout(predicate::str::contains(
            "staging/EXTERNAL_ALIAS  →  shared/REAL",
        ))
        .stdout(predicate::str::contains("staging/EXTERNAL_ALIAS  →  staging/REAL").not());

    assert_secret_value(vault_dir, "staging/EXTERNAL_ALIAS", "omega");
    overwrite_secret(vault_dir, "shared/REAL", "delta");
    assert_secret_value(vault_dir, "prod/EXTERNAL_ALIAS", "delta");
    assert_secret_value(vault_dir, "staging/EXTERNAL_ALIAS", "delta");
    assert_secret_value(vault_dir, "staging/LOCAL", "alpha");
}

#[test]
fn ns_move_preserves_external_alias_targets_when_alias_key_moves_namespace() {
    let dir = tempdir().unwrap();
    let vault_dir = dir.path();
    init_vault(vault_dir);

    set_secret(vault_dir, "shared/REAL", "omega");
    set_secret(vault_dir, "prod/LOCAL", "alpha");
    create_alias(vault_dir, "shared/REAL", "prod/EXTERNAL_ALIAS");

    tsafe()
        .args(["ns", "move", "prod", "staging"])
        .env("TSAFE_VAULT_DIR", vault_dir)
        .env("TSAFE_PASSWORD", PASSWORD)
        .assert()
        .success();

    assert_alias_list(vault_dir)
        .stdout(predicate::str::contains(
            "staging/EXTERNAL_ALIAS  →  shared/REAL",
        ))
        .stdout(predicate::str::contains("prod/EXTERNAL_ALIAS").not())
        .stdout(predicate::str::contains("staging/EXTERNAL_ALIAS  →  staging/REAL").not());

    assert_secret_value(vault_dir, "staging/EXTERNAL_ALIAS", "omega");
    overwrite_secret(vault_dir, "shared/REAL", "delta");
    assert_secret_value(vault_dir, "staging/EXTERNAL_ALIAS", "delta");
    assert_missing_secret(vault_dir, "prod/LOCAL");
    assert_missing_secret(vault_dir, "prod/EXTERNAL_ALIAS");
}