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();
}
#[test]
fn import_dotenv_file_stores_secrets() {
let dir = tempdir().unwrap();
init_vault(dir.path());
let env_file = dir.path().join("app.env");
std::fs::write(
&env_file,
"DB_HOST=localhost\nDB_PORT=5432\nAPI_KEY=super-secret\n",
)
.unwrap();
tsafe()
.args([
"import",
"--from",
env_file.to_str().unwrap(),
"--ns",
"app",
])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(contains("3"));
for (key, expected) in [
("app/DB_HOST", "localhost"),
("app/DB_PORT", "5432"),
("app/API_KEY", "super-secret"),
] {
let out = tsafe()
.args(["get", key])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.get_output()
.stdout
.clone();
assert_eq!(
String::from_utf8(out).unwrap().trim(),
expected,
"key {key}"
);
}
}
#[test]
fn import_dotenv_with_namespace_prefixes_keys() {
let dir = tempdir().unwrap();
init_vault(dir.path());
let env_file = dir.path().join("svc.env");
std::fs::write(&env_file, "HOST=db.internal\nPORT=3306\n").unwrap();
tsafe()
.args([
"import",
"--from",
env_file.to_str().unwrap(),
"--ns",
"mysql",
])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success();
tsafe()
.args(["get", "mysql/HOST"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(contains("db.internal"));
}
#[test]
fn import_dotenv_skips_existing_without_overwrite() {
let dir = tempdir().unwrap();
init_vault(dir.path());
tsafe()
.args(["set", "data/EXISTING_KEY", "original"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success();
let env_file = dir.path().join("test.env");
std::fs::write(&env_file, "EXISTING_KEY=overwritten\nNEW_KEY=newval\n").unwrap();
tsafe()
.args([
"import",
"--from",
env_file.to_str().unwrap(),
"--ns",
"data",
])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success();
tsafe()
.args(["get", "data/EXISTING_KEY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(contains("original"));
tsafe()
.args(["get", "data/NEW_KEY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(contains("newval"));
}
#[test]
fn import_dotenv_overwrite_flag_replaces_existing() {
let dir = tempdir().unwrap();
init_vault(dir.path());
tsafe()
.args(["set", "svc/MY_KEY", "old"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success();
let env_file = dir.path().join("test.env");
std::fs::write(&env_file, "MY_KEY=new\n").unwrap();
tsafe()
.args([
"import",
"--from",
env_file.to_str().unwrap(),
"--ns",
"svc",
"--overwrite",
])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success();
tsafe()
.args(["get", "svc/MY_KEY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(contains("new"));
}
#[cfg_attr(
not(feature = "pm-import-extended"),
ignore = "requires pm-import-extended feature"
)]
#[test]
fn import_bitwarden_csv_duplicate_entries_error_by_default() {
let dir = tempdir().unwrap();
init_vault(dir.path());
let csv = "folder,favorite,type,name,notes,fields,reprompt,login_uri,login_username,login_password,login_totp\n\
,,login,GitHub,,,,https://github.com,alice@example.com,gh-secret-123,\n\
,,login,GitHub,,,,https://github.com,alice@example.com,gh-duplicate-456,\n";
let csv_file = dir.path().join("bitwarden_dup.csv");
std::fs::write(&csv_file, csv).unwrap();
tsafe()
.args([
"import",
"--from",
"bitwarden",
"--file",
csv_file.to_str().unwrap(),
])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure()
.stderr(contains("duplicate"))
.stderr(contains("GITHUB_PASSWORD"));
}
#[cfg_attr(
not(feature = "pm-import-extended"),
ignore = "requires pm-import-extended feature"
)]
#[test]
fn import_bitwarden_csv_duplicate_entries_skipped_with_flag() {
let dir = tempdir().unwrap();
init_vault(dir.path());
let csv = "folder,favorite,type,name,notes,fields,reprompt,login_uri,login_username,login_password,login_totp\n\
,,login,GitHub,,,,https://github.com,alice@example.com,gh-first,\n\
,,login,GitHub,,,,https://github.com,alice@example.com,gh-second,\n";
let csv_file = dir.path().join("bitwarden_dup_skip.csv");
std::fs::write(&csv_file, csv).unwrap();
tsafe()
.args([
"import",
"--from",
"bitwarden",
"--file",
csv_file.to_str().unwrap(),
"--skip-duplicates",
])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(contains("Imported"));
tsafe()
.args(["get", "GITHUB_PASSWORD"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(contains("gh-first"));
}
#[cfg_attr(
not(feature = "pm-import-extended"),
ignore = "requires pm-import-extended feature"
)]
#[test]
fn import_bitwarden_csv_duplicate_entries_overwrite_uses_last_row() {
let dir = tempdir().unwrap();
init_vault(dir.path());
let csv = "folder,favorite,type,name,notes,fields,reprompt,login_uri,login_username,login_password,login_totp\n\
,,login,GitHub,,,,https://github.com,alice@example.com,gh-first,\n\
,,login,GitHub,,,,https://github.com,alice@example.com,gh-second,\n";
let csv_file = dir.path().join("bitwarden_dup_overwrite.csv");
std::fs::write(&csv_file, csv).unwrap();
tsafe()
.args([
"import",
"--from",
"bitwarden",
"--file",
csv_file.to_str().unwrap(),
"--overwrite",
])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(contains("Imported"));
tsafe()
.args(["get", "GITHUB_PASSWORD"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(contains("gh-second"));
}
#[test]
fn import_dry_run_shows_plan_without_writing() {
let dir = tempdir().unwrap();
init_vault(dir.path());
tsafe()
.args(["set", "EXISTING_KEY", "original"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success();
let env_file = dir.path().join("dry.env");
std::fs::write(
&env_file,
"EXISTING_KEY=would-overwrite\nNEW_KEY=would-add\n",
)
.unwrap();
tsafe()
.args(["import", "--from", env_file.to_str().unwrap(), "--dry-run"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(contains("Dry-run"))
.stdout(contains("NEW_KEY"))
.stdout(contains("import"))
.stdout(contains("skip"));
tsafe()
.args(["get", "EXISTING_KEY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(contains("original"));
tsafe()
.args(["get", "NEW_KEY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure();
}
#[test]
fn import_dry_run_with_namespace_shows_prefixed_keys() {
let dir = tempdir().unwrap();
init_vault(dir.path());
let env_file = dir.path().join("ns.env");
std::fs::write(&env_file, "DB_HOST=localhost\nDB_PORT=5432\n").unwrap();
tsafe()
.args([
"import",
"--from",
env_file.to_str().unwrap(),
"--ns",
"myapp",
"--dry-run",
])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(contains("myapp/DB_HOST"))
.stdout(contains("myapp/DB_PORT"))
.stdout(contains("Dry-run"));
}
#[cfg_attr(
not(feature = "pm-import-extended"),
ignore = "requires pm-import-extended feature"
)]
#[test]
fn import_bitwarden_csv_stores_username_password_url() {
let dir = tempdir().unwrap();
init_vault(dir.path());
let csv = "folder,favorite,type,name,notes,fields,reprompt,login_uri,login_username,login_password,login_totp\n\
,,login,GitHub,,,,https://github.com,alice@example.com,gh-secret-123,\n";
let csv_file = dir.path().join("bitwarden_export.csv");
std::fs::write(&csv_file, csv).unwrap();
tsafe()
.args([
"import",
"--from",
"bitwarden",
"--file",
csv_file.to_str().unwrap(),
])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(contains("Imported"));
tsafe()
.args(["get", "GITHUB_USERNAME"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(contains("alice@example.com"));
tsafe()
.args(["get", "GITHUB_PASSWORD"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(contains("gh-secret-123"));
}
#[cfg_attr(
not(feature = "pm-import-extended"),
ignore = "requires pm-import-extended feature"
)]
#[test]
fn import_bitwarden_requires_file_flag() {
let dir = tempdir().unwrap();
init_vault(dir.path());
tsafe()
.args(["import", "--from", "bitwarden"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure()
.stderr(contains("--file"));
}
#[test]
fn import_nonexistent_file_errors() {
let dir = tempdir().unwrap();
init_vault(dir.path());
tsafe()
.args(["import", "--from", "/tmp/does-not-exist-xyzzy.env"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure()
.stderr(contains("import source not found"))
.stderr(contains("absolute path"));
}
#[test]
fn import_missing_dotenv_in_cwd_suggests_nested_env_files() {
let dir = tempdir().unwrap();
init_vault(dir.path());
let nested = dir.path().join("services").join("api");
std::fs::create_dir_all(&nested).unwrap();
std::fs::write(nested.join(".env"), "FROM_NESTED=1\n").unwrap();
tsafe()
.current_dir(dir.path())
.args(["import", "--from", ".env"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure()
.stderr(contains("import source not found"))
.stderr(contains("services"))
.stderr(contains("Try: tsafe import --from"));
}