tsafe-cli 1.0.20

tsafe CLI — local secret and credential manager (replaces .env files)
Documentation
use predicates::prelude::PredicateBooleanExt;
use predicates::str::contains;
use tempfile::tempdir;
use tsafe_core::age_crypto;

use super::{tsafe, write_age_identity};

#[test]
fn team_vault_cli_supports_normal_crud_surface() {
    let dir = tempdir().unwrap();
    let vault_dir = dir.path().join("vaults");
    let alice_identity = dir.path().join("alice.txt");
    let bob_identity = dir.path().join("bob.txt");
    let (alice_secret, alice_recipient) = age_crypto::generate_identity();
    let (bob_secret, bob_recipient) = age_crypto::generate_identity();
    write_age_identity(&alice_identity, &alice_secret);
    write_age_identity(&bob_identity, &bob_secret);

    tsafe()
        .args([
            "--profile",
            "team",
            "team",
            "init",
            "--identity",
            alice_identity.to_str().unwrap(),
        ])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .assert()
        .success()
        .stdout(contains("Team vault created"));

    tsafe()
        .args(["--profile", "team", "team", "members"])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .assert()
        .success()
        .stdout(contains("Team members (1):").and(contains(&alice_recipient)));

    tsafe()
        .args(["--profile", "team", "set", "APP_SECRET", "alpha"])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .env("TSAFE_AGE_IDENTITY", &alice_identity)
        .assert()
        .success();

    tsafe()
        .args(["--profile", "team", "team", "add-member", &bob_recipient])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .env("TSAFE_AGE_IDENTITY", &alice_identity)
        .args(["--identity", alice_identity.to_str().unwrap()])
        .assert()
        .success()
        .stdout(contains("Added team member").and(contains(&bob_recipient)));

    tsafe()
        .args(["--profile", "team", "team", "members"])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .assert()
        .success()
        .stdout(
            contains("Team members (2):")
                .and(contains(&alice_recipient))
                .and(contains(&bob_recipient)),
        );

    tsafe()
        .args(["--profile", "team", "list"])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .env("TSAFE_AGE_IDENTITY", &bob_identity)
        .assert()
        .success()
        .stdout(contains("APP_SECRET"));

    tsafe()
        .args(["--profile", "team", "get", "APP_SECRET"])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .env("TSAFE_AGE_IDENTITY", &bob_identity)
        .assert()
        .success()
        .stdout("alpha");

    tsafe()
        .args([
            "--profile",
            "team",
            "set",
            "APP_SECRET",
            "beta",
            "--overwrite",
        ])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .env("TSAFE_AGE_IDENTITY", &bob_identity)
        .assert()
        .success();

    tsafe()
        .args(["--profile", "team", "export"])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .env("TSAFE_AGE_IDENTITY", &bob_identity)
        .assert()
        .success()
        .stdout(contains("APP_SECRET=beta"));

    tsafe()
        .args(["--profile", "team", "set", "BOB_ONLY_SECRET", "gamma"])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .env("TSAFE_AGE_IDENTITY", &bob_identity)
        .assert()
        .success();

    tsafe()
        .args(["--profile", "team", "get", "BOB_ONLY_SECRET"])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .env("TSAFE_AGE_IDENTITY", &alice_identity)
        .assert()
        .success()
        .stdout("gamma");

    tsafe()
        .args(["--profile", "team", "delete", "APP_SECRET"])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .env("TSAFE_AGE_IDENTITY", &bob_identity)
        .assert()
        .success()
        .stdout(contains("Deleted 'APP_SECRET'"));

    tsafe()
        .args(["--profile", "team", "list"])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .env("TSAFE_AGE_IDENTITY", &alice_identity)
        .assert()
        .success()
        .stdout(contains("BOB_ONLY_SECRET").and(contains("APP_SECRET").not()));

    tsafe()
        .args(["--profile", "team", "delete", "BOB_ONLY_SECRET"])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .env("TSAFE_AGE_IDENTITY", &alice_identity)
        .assert()
        .success()
        .stdout(contains("Deleted 'BOB_ONLY_SECRET'"));

    tsafe()
        .args(["--profile", "team", "list"])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .env("TSAFE_AGE_IDENTITY", &bob_identity)
        .assert()
        .success()
        .stdout(contains("No secrets in profile 'team'"));

    tsafe()
        .args(["--profile", "team", "get", "APP_SECRET"])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .env("TSAFE_AGE_IDENTITY", &alice_identity)
        .assert()
        .failure()
        .stderr(contains("secret 'APP_SECRET' not found"));
}

#[test]
fn team_vault_cli_rekeys_on_remove_member_and_blocks_removed_member_reads() {
    let dir = tempdir().unwrap();
    let vault_dir = dir.path().join("vaults");
    let alice_identity = dir.path().join("alice.txt");
    let bob_identity = dir.path().join("bob.txt");
    let (alice_secret, alice_recipient) = age_crypto::generate_identity();
    let (bob_secret, bob_recipient) = age_crypto::generate_identity();
    write_age_identity(&alice_identity, &alice_secret);
    write_age_identity(&bob_identity, &bob_secret);

    tsafe()
        .args([
            "--profile",
            "team",
            "team",
            "init",
            "--identity",
            alice_identity.to_str().unwrap(),
        ])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .assert()
        .success()
        .stdout(contains("Team vault created"));

    tsafe()
        .args(["--profile", "team", "set", "TEAM_SECRET", "alpha"])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .env("TSAFE_AGE_IDENTITY", &alice_identity)
        .assert()
        .success();

    tsafe()
        .args(["--profile", "team", "team", "add-member", &bob_recipient])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .env("TSAFE_AGE_IDENTITY", &alice_identity)
        .args(["--identity", alice_identity.to_str().unwrap()])
        .assert()
        .success()
        .stdout(contains("Added team member").and(contains(&bob_recipient)));

    tsafe()
        .args(["--profile", "team", "get", "TEAM_SECRET"])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .env("TSAFE_AGE_IDENTITY", &bob_identity)
        .assert()
        .success()
        .stdout("alpha");

    tsafe()
        .args([
            "--profile",
            "team",
            "team",
            "remove-member",
            &bob_recipient,
            "--identity",
            alice_identity.to_str().unwrap(),
        ])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .assert()
        .success()
        .stdout(contains("Removed team member").and(contains(&bob_recipient)));

    tsafe()
        .args(["--profile", "team", "team", "members"])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .assert()
        .success()
        .stdout(
            contains("Team members (1):")
                .and(contains(&alice_recipient))
                .and(contains(&bob_recipient).not()),
        );

    tsafe()
        .args(["--profile", "team", "list"])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .env("TSAFE_AGE_IDENTITY", &bob_identity)
        .assert()
        .failure()
        .stderr(contains("cannot decrypt team vault"));

    tsafe()
        .args(["--profile", "team", "get", "TEAM_SECRET"])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .env("TSAFE_AGE_IDENTITY", &bob_identity)
        .assert()
        .failure()
        .stderr(contains("cannot decrypt team vault"));

    tsafe()
        .args(["--profile", "team", "get", "TEAM_SECRET"])
        .env("TSAFE_VAULT_DIR", &vault_dir)
        .env("TSAFE_AGE_IDENTITY", &alice_identity)
        .assert()
        .success()
        .stdout("alpha");
}