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");
}