use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::tempdir;
use tsafe_azure::{pull_secrets, KvConfig};
use tsafe_core::{profile::vault_path, vault::Vault};
fn tsafe() -> Command {
Command::cargo_bin("tsafe").unwrap()
}
fn init_vault(dir: &std::path::Path) {
tsafe()
.args(["--profile", "default", "init"])
.env("TSAFE_VAULT_DIR", dir)
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success();
}
#[cfg(feature = "multi-pull")]
fn write_pull_config(dir: &std::path::Path, server_url: &str) {
std::fs::write(
dir.join(".tsafe.yml"),
format!(
r#"
pulls:
- source: hcp
addr: http://example.com:8200
mount: secret
prefix: app
- source: hcp
addr: {server_url}
mount: secret
prefix: app
"#
),
)
.unwrap();
}
fn kv_pull_cmd(vault_url: &str) -> Command {
let mut cmd = tsafe();
cmd.env("TSAFE_AKV_URL", vault_url)
.env_remove("AZURE_TENANT_ID")
.env_remove("AZURE_CLIENT_ID")
.env_remove("AZURE_CLIENT_SECRET");
cmd
}
fn kv_pull_local_test_cmd(vault_url: &str) -> Command {
let mut cmd = tsafe();
cmd.arg("--profile")
.arg("default")
.env("TSAFE_AKV_TEST_LOCAL_URL", vault_url)
.env("TSAFE_AKV_TEST_TOKEN", "tok")
.env_remove("TSAFE_AKV_URL")
.env_remove("AZURE_TENANT_ID")
.env_remove("AZURE_CLIENT_ID")
.env_remove("AZURE_CLIENT_SECRET");
cmd
}
fn kv_pull_without_url_cmd() -> Command {
let mut cmd = tsafe();
cmd.env_remove("TSAFE_AKV_URL")
.env_remove("AZURE_TENANT_ID")
.env_remove("AZURE_CLIENT_ID")
.env_remove("AZURE_CLIENT_SECRET");
cmd
}
fn kv_pull_service_principal_token_fail_cmd(vault_url: &str) -> Command {
let mut cmd = tsafe();
cmd.env("TSAFE_AKV_URL", vault_url)
.env("AZURE_TENANT_ID", "test-tenant")
.env("AZURE_CLIENT_ID", "test-client")
.env("AZURE_CLIENT_SECRET", "test-secret")
.env("HTTPS_PROXY", "http://127.0.0.1:9")
.env_remove("HTTP_PROXY");
cmd
}
fn set_secret(dir: &std::path::Path, key: &str, value: &str) {
tsafe()
.args(["--profile", "default", "set", key, value])
.env("TSAFE_VAULT_DIR", dir)
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success();
}
fn apply_akv_success_seam_to_real_vault(
dir: &std::path::Path,
secrets: &[(String, String)],
overwrite: bool,
) -> (usize, usize) {
temp_env::with_var("TSAFE_VAULT_DIR", Some(dir), || {
let mut vault = Vault::open(&vault_path("default"), b"test-pw").unwrap();
let mut imported = 0usize;
let mut skipped = 0usize;
for (key, value) in secrets {
let exists = vault.list().contains(&key.as_str());
if exists && !overwrite {
skipped += 1;
continue;
}
vault
.set(key, value, std::collections::HashMap::new())
.unwrap();
imported += 1;
}
(imported, skipped)
})
}
fn mock_akv_paginated_success_pages(server: &mut mockito::Server) {
let page2_url = format!("{}/secrets?api-version=7.4&skiptoken=p2", server.url());
let page1_body = format!(
r#"{{
"value":[
{{"id":"https://vault/secrets/secret-a","attributes":{{"enabled":true}}}},
{{"id":"https://vault/secrets/disabled-secret","attributes":{{"enabled":false}}}}
],
"nextLink":"{page2_url}"
}}"#
);
let page2_body = r#"{
"value":[
{"id":"https://vault/secrets/secret-b","attributes":{"enabled":true}}
]
}"#;
server
.mock(
"GET",
mockito::Matcher::Regex(r"^/secrets\?api-version=7\.4&maxresults=25$".to_string()),
)
.match_header("Authorization", "Bearer tok")
.with_status(200)
.with_header("Content-Type", "application/json")
.with_body(page1_body)
.create();
server
.mock("GET", mockito::Matcher::Regex(r"skiptoken=p2".to_string()))
.match_header("Authorization", "Bearer tok")
.with_status(200)
.with_header("Content-Type", "application/json")
.with_body(page2_body)
.create();
server
.mock(
"GET",
mockito::Matcher::Regex(r"^/secrets/secret-a\?api-version=7\.4$".to_string()),
)
.match_header("Authorization", "Bearer tok")
.with_status(200)
.with_header("Content-Type", "application/json")
.with_body(r#"{"value":"remote-a"}"#)
.create();
server
.mock(
"GET",
mockito::Matcher::Regex(r"^/secrets/secret-b\?api-version=7\.4$".to_string()),
)
.match_header("Authorization", "Bearer tok")
.with_status(200)
.with_header("Content-Type", "application/json")
.with_body(r#"{"value":"remote-b"}"#)
.create();
}
fn mock_akv_paginated_page2_failure(server: &mut mockito::Server) {
let page2_url = format!("{}/secrets?api-version=7.4&skiptoken=p2", server.url());
let page1_body = format!(
r#"{{
"value":[
{{"id":"https://vault/secrets/secret-a","attributes":{{"enabled":true}}}}
],
"nextLink":"{page2_url}"
}}"#
);
server
.mock(
"GET",
mockito::Matcher::Regex(r"^/secrets\?api-version=7\.4&maxresults=25$".to_string()),
)
.match_header("Authorization", "Bearer tok")
.with_status(200)
.with_header("Content-Type", "application/json")
.with_body(page1_body)
.create();
server
.mock("GET", mockito::Matcher::Regex(r"skiptoken=p2".to_string()))
.match_header("Authorization", "Bearer tok")
.with_status(500)
.with_header("Content-Type", "application/json")
.with_body(r#"{"error":{"code":"InternalServerError"}}"#)
.create();
}
#[test]
fn kv_pull_local_success_seam_imports_paginated_enabled_secrets_and_skips_existing_without_overwrite(
) {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "SECRET_B", "local-old");
let mut server = mockito::Server::new();
mock_akv_paginated_success_pages(&mut server);
let secrets = pull_secrets(
&KvConfig {
vault_url: server.url(),
},
&|| Ok("tok".to_string()),
None,
)
.unwrap();
assert_eq!(
secrets,
vec![
("SECRET_A".to_string(), "remote-a".to_string()),
("SECRET_B".to_string(), "remote-b".to_string()),
]
);
let (imported, skipped) = apply_akv_success_seam_to_real_vault(dir.path(), &secrets, false);
assert_eq!((imported, skipped), (1, 1));
tsafe()
.args(["--profile", "default", "get", "SECRET_A"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("remote-a"));
tsafe()
.args(["--profile", "default", "get", "SECRET_B"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("local-old"));
tsafe()
.args(["--profile", "default", "get", "DISABLED_SECRET"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure();
}
#[test]
fn kv_pull_local_success_seam_overwrite_replaces_existing_values_after_paginated_fetch() {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "SECRET_A", "local-a");
set_secret(dir.path(), "SECRET_B", "local-b");
let mut server = mockito::Server::new();
mock_akv_paginated_success_pages(&mut server);
let secrets = pull_secrets(
&KvConfig {
vault_url: server.url(),
},
&|| Ok("tok".to_string()),
None,
)
.unwrap();
let (imported, skipped) = apply_akv_success_seam_to_real_vault(dir.path(), &secrets, true);
assert_eq!((imported, skipped), (2, 0));
tsafe()
.args(["--profile", "default", "get", "SECRET_A"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("remote-a"));
tsafe()
.args(["--profile", "default", "get", "SECRET_B"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("remote-b"));
tsafe()
.args(["--profile", "default", "get", "DISABLED_SECRET"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure();
}
#[test]
fn kv_pull_cli_local_test_seam_imports_paginated_enabled_secrets_and_skips_existing_without_overwrite(
) {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "SECRET_B", "local-old");
let mut server = mockito::Server::new();
mock_akv_paginated_success_pages(&mut server);
kv_pull_local_test_cmd(&server.url())
.args(["kv-pull"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("Imported 1 secret(s)"))
.stdout(predicate::str::contains("1 skipped"))
.stdout(predicate::str::contains(server.url()));
tsafe()
.args(["--profile", "default", "get", "SECRET_A"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("remote-a"));
tsafe()
.args(["--profile", "default", "get", "SECRET_B"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("local-old"));
tsafe()
.args(["--profile", "default", "get", "DISABLED_SECRET"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure();
}
#[test]
fn kv_pull_cli_local_test_seam_overwrite_replaces_existing_values_after_paginated_fetch() {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "SECRET_A", "local-a");
set_secret(dir.path(), "SECRET_B", "local-b");
let mut server = mockito::Server::new();
mock_akv_paginated_success_pages(&mut server);
kv_pull_local_test_cmd(&server.url())
.args(["kv-pull", "--overwrite"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("Imported 2 secret(s)"))
.stdout(predicate::str::contains("skipped").not());
tsafe()
.args(["--profile", "default", "get", "SECRET_A"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("remote-a"));
tsafe()
.args(["--profile", "default", "get", "SECRET_B"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("remote-b"));
}
#[test]
fn kv_pull_cli_local_test_seam_page2_failure_fails_before_mutating_local_vault() {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "LOCAL_ONLY", "still-here");
let mut server = mockito::Server::new();
mock_akv_paginated_page2_failure(&mut server);
kv_pull_local_test_cmd(&server.url())
.args(["kv-pull"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure()
.stdout(predicate::str::contains("Imported").not())
.stderr(predicate::str::contains(
"failed to pull secrets from Azure Key Vault",
));
tsafe()
.args(["--profile", "default", "get", "LOCAL_ONLY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("still-here"));
tsafe()
.args(["--profile", "default", "get", "SECRET_A"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure();
}
#[cfg(feature = "multi-pull")]
#[test]
fn pull_skip_failed_continues_to_later_sources() {
let dir = tempdir().unwrap();
init_vault(dir.path());
let mut server = mockito::Server::new();
let _list = server
.mock("LIST", "/v1/secret/metadata/app")
.match_header("x-vault-token", "test-token")
.with_status(200)
.with_header("Content-Type", "application/json")
.with_body(r#"{"data":{"keys":["API_KEY"]}}"#)
.create();
let _get = server
.mock("GET", "/v1/secret/data/app/API_KEY")
.match_header("x-vault-token", "test-token")
.with_status(200)
.with_header("Content-Type", "application/json")
.with_body(r#"{"data":{"data":{"value":"abc123"}}}"#)
.create();
write_pull_config(dir.path(), &server.url());
tsafe()
.args(["--profile", "default", "pull", "--on-error", "skip-failed"])
.current_dir(dir.path())
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.env("VAULT_TOKEN", "test-token")
.assert()
.success()
.stdout(predicate::str::contains("Pull complete (2 sources)"))
.stdout(predicate::str::contains(
"source summary: 1 succeeded, 1 failed",
))
.stderr(predicate::str::contains(
"1 source(s) failed (mode: SkipFailed)",
));
tsafe()
.args(["--profile", "default", "get", "API_KEY_VALUE"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("abc123"));
}
#[cfg(feature = "multi-pull")]
#[test]
fn pull_warn_only_continues_to_later_sources() {
let dir = tempdir().unwrap();
init_vault(dir.path());
let mut server = mockito::Server::new();
let _list = server
.mock("LIST", "/v1/secret/metadata/app")
.match_header("x-vault-token", "test-token")
.with_status(200)
.with_header("Content-Type", "application/json")
.with_body(r#"{"data":{"keys":["API_KEY"]}}"#)
.create();
let _get = server
.mock("GET", "/v1/secret/data/app/API_KEY")
.match_header("x-vault-token", "test-token")
.with_status(200)
.with_header("Content-Type", "application/json")
.with_body(r#"{"data":{"data":{"value":"abc123"}}}"#)
.create();
write_pull_config(dir.path(), &server.url());
tsafe()
.args(["--profile", "default", "pull", "--on-error", "warn-only"])
.current_dir(dir.path())
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.env("VAULT_TOKEN", "test-token")
.assert()
.success()
.stdout(predicate::str::contains("Pull complete (2 sources)"))
.stdout(predicate::str::contains(
"source summary: 1 succeeded, 1 failed",
))
.stderr(predicate::str::contains(
"1 source(s) failed (mode: WarnOnly)",
));
tsafe()
.args(["--profile", "default", "get", "API_KEY_VALUE"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("abc123"));
}
#[cfg(feature = "multi-pull")]
#[test]
fn pull_fail_all_stops_before_later_sources() {
let dir = tempdir().unwrap();
init_vault(dir.path());
let server = mockito::Server::new();
write_pull_config(dir.path(), &server.url());
tsafe()
.args(["--profile", "default", "pull"])
.current_dir(dir.path())
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.env("VAULT_TOKEN", "test-token")
.assert()
.failure()
.stderr(predicate::str::contains(
"vault-pull address must use https:// for remote servers",
));
tsafe()
.args(["--profile", "default", "get", "API_KEY_VALUE"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure();
}
#[test]
fn kv_pull_rejects_plain_http_akv_url_before_network_use() {
kv_pull_cmd("http://example.vault.azure.net")
.args(["kv-pull"])
.assert()
.failure()
.stderr(predicate::str::contains(
"TSAFE_AKV_URL must start with https://",
))
.stderr(predicate::str::contains("plain HTTP is not allowed"));
}
#[test]
fn kv_pull_skip_failed_does_not_downgrade_local_config_validation_errors() {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "LOCAL_ONLY", "still-here");
kv_pull_cmd("http://example.vault.azure.net")
.args(["kv-pull", "--on-error", "skip-failed"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure()
.stdout(predicate::str::contains("Imported").not())
.stderr(predicate::str::contains(
"Key Vault is not configured — set TSAFE_AKV_URL",
))
.stderr(predicate::str::contains(
"TSAFE_AKV_URL must start with https://",
))
.stderr(predicate::str::contains("Azure Key Vault pull failed").not());
tsafe()
.args(["--profile", "default", "get", "LOCAL_ONLY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("still-here"));
}
#[test]
fn kv_pull_fail_all_surfaces_imds_auth_failure() {
kv_pull_cmd("https://example.vault.azure.net")
.args(["kv-pull"])
.assert()
.failure()
.stderr(predicate::str::contains(
"failed to pull secrets from Azure Key Vault",
))
.stderr(predicate::str::contains(
"IMDS unreachable and no service principal vars set",
));
}
#[test]
fn kv_pull_missing_akv_url_reports_actionable_configuration_error() {
kv_pull_without_url_cmd()
.args(["kv-pull"])
.assert()
.failure()
.stderr(predicate::str::contains(
"Key Vault is not configured — set TSAFE_AKV_URL",
))
.stderr(predicate::str::contains(
"TSAFE_AKV_URL is not set — e.g. https://myvault.vault.azure.net",
));
}
#[test]
fn kv_pull_fail_all_preserves_local_vault_on_missing_akv_url_config_error() {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "LOCAL_ONLY", "still-here");
kv_pull_without_url_cmd()
.args(["kv-pull"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure()
.stdout(predicate::str::contains("Imported").not())
.stderr(predicate::str::contains(
"Key Vault is not configured — set TSAFE_AKV_URL",
))
.stderr(predicate::str::contains(
"TSAFE_AKV_URL is not set — e.g. https://myvault.vault.azure.net",
));
tsafe()
.args(["--profile", "default", "get", "LOCAL_ONLY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("still-here"));
}
#[test]
fn kv_pull_warn_only_does_not_downgrade_missing_akv_url_local_config_error() {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "LOCAL_ONLY", "still-here");
kv_pull_without_url_cmd()
.args(["kv-pull", "--on-error", "warn-only"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure()
.stdout(predicate::str::contains("Imported").not())
.stderr(predicate::str::contains(
"Key Vault is not configured — set TSAFE_AKV_URL",
))
.stderr(predicate::str::contains(
"TSAFE_AKV_URL is not set — e.g. https://myvault.vault.azure.net",
))
.stderr(predicate::str::contains("Azure Key Vault pull failed").not());
tsafe()
.args(["--profile", "default", "get", "LOCAL_ONLY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("still-here"));
}
#[test]
fn kv_pull_fail_all_preserves_local_vault_on_plain_http_akv_url_config_error() {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "LOCAL_ONLY", "still-here");
kv_pull_cmd("http://example.vault.azure.net")
.args(["kv-pull"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure()
.stdout(predicate::str::contains("Imported").not())
.stderr(predicate::str::contains(
"Key Vault is not configured — set TSAFE_AKV_URL",
))
.stderr(predicate::str::contains(
"TSAFE_AKV_URL must start with https://",
));
tsafe()
.args(["--profile", "default", "get", "LOCAL_ONLY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("still-here"));
}
#[test]
fn kv_pull_skip_failed_does_not_downgrade_missing_akv_url_local_config_error() {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "LOCAL_ONLY", "still-here");
kv_pull_without_url_cmd()
.args(["kv-pull", "--on-error", "skip-failed"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure()
.stdout(predicate::str::contains("Imported").not())
.stderr(predicate::str::contains(
"Key Vault is not configured — set TSAFE_AKV_URL",
))
.stderr(predicate::str::contains(
"TSAFE_AKV_URL is not set — e.g. https://myvault.vault.azure.net",
))
.stderr(predicate::str::contains("Azure Key Vault pull failed").not());
tsafe()
.args(["--profile", "default", "get", "LOCAL_ONLY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("still-here"));
}
#[test]
fn kv_pull_fail_all_surfaces_service_principal_token_failure() {
kv_pull_service_principal_token_fail_cmd("https://example.vault.azure.net")
.args(["kv-pull"])
.assert()
.failure()
.stderr(predicate::str::contains(
"failed to pull secrets from Azure Key Vault",
))
.stderr(predicate::str::contains(
"https://login.microsoftonline.com/test-tenant/oauth2/v2.0/token: status code 400",
));
}
#[test]
fn kv_pull_warn_only_does_not_downgrade_plain_http_akv_url_local_config_error() {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "LOCAL_ONLY", "still-here");
kv_pull_cmd("http://example.vault.azure.net")
.args(["kv-pull", "--on-error", "warn-only"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure()
.stdout(predicate::str::contains("Imported").not())
.stderr(predicate::str::contains(
"Key Vault is not configured — set TSAFE_AKV_URL",
))
.stderr(predicate::str::contains(
"TSAFE_AKV_URL must start with https://",
))
.stderr(predicate::str::contains("Azure Key Vault pull failed").not());
tsafe()
.args(["--profile", "default", "get", "LOCAL_ONLY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("still-here"));
}
#[test]
fn kv_pull_fail_all_preserves_local_vault_on_service_principal_token_failure() {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "LOCAL_ONLY", "still-here");
kv_pull_service_principal_token_fail_cmd("https://example.vault.azure.net")
.args(["kv-pull"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure()
.stdout(predicate::str::contains("Imported").not())
.stderr(predicate::str::contains(
"failed to pull secrets from Azure Key Vault",
))
.stderr(predicate::str::contains(
"https://login.microsoftonline.com/test-tenant/oauth2/v2.0/token: status code 400",
));
tsafe()
.args(["--profile", "default", "get", "LOCAL_ONLY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("still-here"));
tsafe()
.args(["--profile", "default", "get", "REMOTE_ONLY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure();
}
#[test]
fn kv_pull_skip_failed_success_adjacent_preserves_local_vault_on_service_principal_token_failure() {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "LOCAL_ONLY", "still-here");
kv_pull_service_principal_token_fail_cmd("https://example.vault.azure.net")
.args(["kv-pull", "--on-error", "skip-failed"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("Imported").not())
.stderr(predicate::str::contains("Azure Key Vault pull failed"))
.stderr(predicate::str::contains(
"failed to pull secrets from Azure Key Vault",
));
tsafe()
.args(["--profile", "default", "get", "LOCAL_ONLY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("still-here"));
}
#[test]
fn kv_pull_fail_all_preserves_local_vault_on_imds_auth_failure() {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "LOCAL_ONLY", "still-here");
kv_pull_cmd("https://example.vault.azure.net")
.args(["kv-pull"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure()
.stdout(predicate::str::contains("Imported").not())
.stderr(predicate::str::contains(
"failed to pull secrets from Azure Key Vault",
))
.stderr(predicate::str::contains(
"IMDS unreachable and no service principal vars set",
));
tsafe()
.args(["--profile", "default", "get", "LOCAL_ONLY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("still-here"));
tsafe()
.args(["--profile", "default", "get", "REMOTE_ONLY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure();
}
#[test]
fn kv_pull_skip_failed_success_adjacent_preserves_local_vault_on_imds_auth_failure() {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "LOCAL_ONLY", "still-here");
kv_pull_cmd("https://example.vault.azure.net")
.args(["kv-pull", "--on-error", "skip-failed"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("Imported").not())
.stderr(predicate::str::contains("Azure Key Vault pull failed"))
.stderr(predicate::str::contains(
"failed to pull secrets from Azure Key Vault",
))
.stderr(
predicate::str::contains("IMDS unreachable and no service principal vars set").not(),
);
tsafe()
.args(["--profile", "default", "get", "LOCAL_ONLY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("still-here"));
tsafe()
.args(["--profile", "default", "get", "REMOTE_ONLY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure();
}
#[test]
fn kv_pull_warn_only_success_adjacent_preserves_local_vault_on_service_principal_token_failure() {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "LOCAL_ONLY", "still-here");
kv_pull_service_principal_token_fail_cmd("https://example.vault.azure.net")
.args(["kv-pull", "--on-error", "warn-only"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("Imported").not())
.stderr(predicate::str::contains("Azure Key Vault pull failed"))
.stderr(predicate::str::contains(
"failed to pull secrets from Azure Key Vault",
));
tsafe()
.args(["--profile", "default", "get", "LOCAL_ONLY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("still-here"));
}
#[test]
fn kv_pull_warn_only_success_adjacent_preserves_local_vault_on_imds_auth_failure() {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "LOCAL_ONLY", "still-here");
kv_pull_cmd("https://example.vault.azure.net")
.args(["kv-pull", "--on-error", "warn-only"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("Imported").not())
.stderr(predicate::str::contains("Azure Key Vault pull failed"))
.stderr(predicate::str::contains(
"failed to pull secrets from Azure Key Vault",
));
tsafe()
.args(["--profile", "default", "get", "LOCAL_ONLY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.success()
.stdout(predicate::str::contains("still-here"));
tsafe()
.args(["--profile", "default", "get", "REMOTE_ONLY"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "test-pw")
.assert()
.failure();
}