tsafe-cli 1.0.21

tsafe CLI — local secret and credential manager (replaces .env files)
Documentation
//! Integration tests for `tsafe ssh` subcommands (list, public-key, generate).

use assert_cmd::Command;
use predicates::str::contains;
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", "test-pw")
        .assert()
        .success();
}

/// A key value that looks like an SSH private key (contains the header).
const FAKE_SSH_PRIVATE_KEY: &str =
    "-----BEGIN OPENSSH PRIVATE KEY-----\nfakebase64content\n-----END OPENSSH PRIVATE KEY-----\n";

// ── tsafe ssh list ────────────────────────────────────────────────────────────

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

    tsafe()
        .args(["ssh", "list"])
        .env("TSAFE_VAULT_DIR", dir.path())
        .env("TSAFE_PASSWORD", "test-pw")
        .assert()
        .success()
        .stdout(contains("No SSH keys found"));
}

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

    // Write a fake key file.
    let key_file = dir.path().join("id_test");
    std::fs::write(&key_file, FAKE_SSH_PRIVATE_KEY).unwrap();

    // Import it.
    tsafe()
        .args([
            "ssh-import",
            key_file.to_str().unwrap(),
            "--name",
            "TEST_SSH_KEY",
        ])
        .env("TSAFE_VAULT_DIR", dir.path())
        .env("TSAFE_PASSWORD", "test-pw")
        .assert()
        .success();

    // List should now show the key.
    tsafe()
        .args(["ssh", "list"])
        .env("TSAFE_VAULT_DIR", dir.path())
        .env("TSAFE_PASSWORD", "test-pw")
        .assert()
        .success()
        .stdout(contains("TEST_SSH_KEY"));
}

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

    // Store a key manually with type=ssh tag using the set command.
    // (Simulates a key that was imported with the tag but perhaps as raw PEM.)
    let key_file = dir.path().join("id_tagged");
    std::fs::write(&key_file, FAKE_SSH_PRIVATE_KEY).unwrap();

    tsafe()
        .args([
            "ssh-import",
            key_file.to_str().unwrap(),
            "--name",
            "TAGGED_KEY",
            "--tag",
            "env=ci",
        ])
        .env("TSAFE_VAULT_DIR", dir.path())
        .env("TSAFE_PASSWORD", "test-pw")
        .assert()
        .success();

    tsafe()
        .args(["ssh", "list"])
        .env("TSAFE_VAULT_DIR", dir.path())
        .env("TSAFE_PASSWORD", "test-pw")
        .assert()
        .success()
        .stdout(contains("TAGGED_KEY"));
}

// ── tsafe ssh public-key ──────────────────────────────────────────────────────

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

    tsafe()
        .args(["ssh", "public-key", "DOES_NOT_EXIST"])
        .env("TSAFE_VAULT_DIR", dir.path())
        .env("TSAFE_PASSWORD", "test-pw")
        .assert()
        .failure()
        .stderr(contains("DOES_NOT_EXIST"))
        .stderr(contains("not found"));
}

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

    tsafe()
        .args(["set", "PLAIN_SECRET", "notaprivatekey"])
        .env("TSAFE_VAULT_DIR", dir.path())
        .env("TSAFE_PASSWORD", "test-pw")
        .assert()
        .success();

    tsafe()
        .args(["ssh", "public-key", "PLAIN_SECRET"])
        .env("TSAFE_VAULT_DIR", dir.path())
        .env("TSAFE_PASSWORD", "test-pw")
        .assert()
        .failure()
        .stderr(contains("does not appear to be an SSH private key"));
}

// ── tsafe ssh generate ────────────────────────────────────────────────────────

/// Test that `tsafe ssh generate` runs ssh-keygen and stores the private key.
/// This test is skipped if ssh-keygen is not available.
#[test]
fn ssh_generate_ed25519_stores_private_key_in_vault() {
    // Skip if ssh-keygen is not available.
    if std::process::Command::new("ssh-keygen")
        .args(["-V"])
        .output()
        .is_err()
    {
        eprintln!(
            "skipping ssh_generate_ed25519_stores_private_key_in_vault: ssh-keygen not found"
        );
        return;
    }

    let dir = tempdir().unwrap();
    init_vault(dir.path());

    tsafe()
        .args(["ssh", "generate", "MY_DEPLOY_KEY"])
        .env("TSAFE_VAULT_DIR", dir.path())
        .env("TSAFE_PASSWORD", "test-pw")
        .assert()
        .success()
        .stdout(contains("Generated ed25519 SSH key pair"))
        .stdout(contains("MY_DEPLOY_KEY"))
        .stdout(contains("ssh-ed25519"));

    // The private key should now be in the vault.
    tsafe()
        .args(["get", "MY_DEPLOY_KEY"])
        .env("TSAFE_VAULT_DIR", dir.path())
        .env("TSAFE_PASSWORD", "test-pw")
        .assert()
        .success()
        .stdout(contains("PRIVATE KEY"));

    // And should appear in the SSH list.
    tsafe()
        .args(["ssh", "list"])
        .env("TSAFE_VAULT_DIR", dir.path())
        .env("TSAFE_PASSWORD", "test-pw")
        .assert()
        .success()
        .stdout(contains("MY_DEPLOY_KEY"));
}

/// Test that public-key extraction works after generate (requires ssh-keygen).
#[test]
fn ssh_public_key_extracted_after_generate() {
    if std::process::Command::new("ssh-keygen")
        .args(["-V"])
        .output()
        .is_err()
    {
        eprintln!("skipping ssh_public_key_extracted_after_generate: ssh-keygen not found");
        return;
    }

    let dir = tempdir().unwrap();
    init_vault(dir.path());

    // Generate a key.
    tsafe()
        .args(["ssh", "generate", "EXTRACT_KEY"])
        .env("TSAFE_VAULT_DIR", dir.path())
        .env("TSAFE_PASSWORD", "test-pw")
        .assert()
        .success();

    // Extract its public key.
    tsafe()
        .args(["ssh", "public-key", "EXTRACT_KEY"])
        .env("TSAFE_VAULT_DIR", dir.path())
        .env("TSAFE_PASSWORD", "test-pw")
        .assert()
        .success()
        .stdout(contains("ssh-ed25519"));
}