use assert_cmd::Command;
use mockito::Server;
use predicates::prelude::*;
use tempfile::tempdir;
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();
}
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 mock_empty_remote(server: &mut Server) -> mockito::Mock {
server
.mock("GET", mockito::Matcher::Regex(r"^/secrets\?".to_string()))
.match_header("Authorization", "Bearer tok")
.with_status(200)
.with_header("Content-Type", "application/json")
.with_body(r#"{"value":[]}"#)
.create()
}
fn mock_put_secret(server: &mut Server, name: &str) -> mockito::Mock {
server
.mock(
"GET",
mockito::Matcher::Regex(format!(r"^/secrets/{name}\?").to_string()),
)
.match_header("Authorization", "Bearer tok")
.with_status(404)
.with_header("Content-Type", "application/json")
.with_body(r#"{"error":{"code":"SecretNotFound"}}"#)
.create();
server
.mock(
"PUT",
mockito::Matcher::Regex(format!(r"^/secrets/{name}\?").to_string()),
)
.match_header("Authorization", "Bearer tok")
.with_status(200)
.with_header("Content-Type", "application/json")
.with_body(format!(
r#"{{"id":"https://vault/secrets/{name}/v1","value":"x"}}"#
))
.create()
}
fn write_two_source_push_config(
dir: &std::path::Path,
server1_url: &str,
source1_name: &str,
server2_url: &str,
source2_name: &str,
) {
let content = format!(
r#"pushes:
- source: akv
name: {source1_name}
vault_url: {server1_url}
- source: akv
name: {source2_name}
vault_url: {server2_url}
"#
);
std::fs::write(dir.join(".tsafe.yml"), content).unwrap();
}
#[allow(dead_code)]
fn write_single_source_push_config(dir: &std::path::Path, server_url: &str, source_name: &str) {
let content = format!(
r#"pushes:
- source: akv
name: {source_name}
vault_url: {server_url}
"#
);
std::fs::write(dir.join(".tsafe.yml"), content).unwrap();
}
fn push_cmd(dir: &std::path::Path) -> Command {
let mut cmd = tsafe();
cmd.args(["--profile", "default"])
.env("TSAFE_VAULT_DIR", dir)
.env("TSAFE_PASSWORD", "test-pw")
.env_remove("AZURE_TENANT_ID")
.env_remove("AZURE_CLIENT_ID")
.env_remove("AZURE_CLIENT_SECRET");
cmd
}
#[cfg(feature = "akv-pull")]
#[test]
fn push_dry_run_orchestrates_all_sources() {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "MY_SECRET", "hello");
write_two_source_push_config(
dir.path(),
"https://prod.vault.azure.net",
"prod-akv",
"https://staging.vault.azure.net",
"staging-akv",
);
push_cmd(dir.path())
.args(["push", "--dry-run"])
.current_dir(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("prod-akv"))
.stdout(predicate::str::contains("staging-akv"))
.stdout(predicate::str::contains("Dry run"))
.stdout(predicate::str::contains("Pushed").not());
}
#[cfg(feature = "akv-pull")]
#[test]
fn push_source_filter_selects_named_only() {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "DB_PASS", "secret-val");
let mut prod_server = Server::new();
mock_empty_remote(&mut prod_server);
mock_put_secret(&mut prod_server, "db-pass");
let mut staging_server = Server::new();
let no_staging_call = staging_server
.mock("GET", mockito::Matcher::Any)
.expect(0)
.create();
write_two_source_push_config(
dir.path(),
&prod_server.url(),
"prod-akv",
&staging_server.url(),
"staging-akv",
);
push_cmd(dir.path())
.args(["push", "--source", "prod-akv", "--yes"])
.env("TSAFE_AKV_TEST_LOCAL_URL", prod_server.url())
.env("TSAFE_AKV_TEST_TOKEN", "tok")
.current_dir(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("prod-akv"))
.stdout(predicate::str::contains("staging-akv").not());
no_staging_call.assert(); }
#[cfg(feature = "akv-pull")]
#[test]
fn push_fail_all_aborts_on_first_error() {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "API_KEY", "val123");
let mut failing_server = Server::new();
failing_server
.mock("GET", mockito::Matcher::Regex(r"^/secrets\?".to_string()))
.match_header("Authorization", "Bearer tok")
.with_status(500)
.with_header("Content-Type", "application/json")
.with_body(r#"{"error":{"code":"InternalServerError"}}"#)
.create();
let mut ok_server = Server::new();
let no_ok_call = ok_server
.mock("GET", mockito::Matcher::Any)
.expect(0)
.create();
write_two_source_push_config(
dir.path(),
&failing_server.url(),
"source-a",
&ok_server.url(),
"source-b",
);
push_cmd(dir.path())
.args(["push", "--yes", "--on-error", "fail-all"])
.env("TSAFE_AKV_TEST_LOCAL_URL", failing_server.url())
.env("TSAFE_AKV_TEST_TOKEN", "tok")
.current_dir(dir.path())
.assert()
.failure() .stderr(
predicate::str::contains("source-a")
.or(predicate::str::contains("failed").or(predicate::str::contains("error"))),
);
no_ok_call.assert(); }
#[cfg(feature = "akv-pull")]
#[test]
fn push_skip_failed_continues_on_error() {
let dir = tempdir().unwrap();
init_vault(dir.path());
set_secret(dir.path(), "DB_KEY", "my-db-val");
let mut failing_server = Server::new();
failing_server
.mock("GET", mockito::Matcher::Regex(r"^/secrets\?".to_string()))
.match_header("Authorization", "Bearer tok")
.with_status(500)
.with_header("Content-Type", "application/json")
.with_body(r#"{"error":{"code":"InternalServerError"}}"#)
.create();
let mut ok_server = Server::new();
mock_empty_remote(&mut ok_server);
mock_put_secret(&mut ok_server, "db-key");
let mut server_b = Server::new();
server_b
.mock("GET", mockito::Matcher::Regex(r"^/secrets\?".to_string()))
.match_header("Authorization", "Bearer tok")
.with_status(500)
.with_header("Content-Type", "application/json")
.with_body(r#"{"error":{"code":"InternalServerError2"}}"#)
.expect(1) .create();
write_two_source_push_config(
dir.path(),
&failing_server.url(),
"source-a",
&server_b.url(),
"source-b",
);
push_cmd(dir.path())
.args(["push", "--yes", "--on-error", "skip-failed"])
.env("TSAFE_AKV_TEST_LOCAL_URL", failing_server.url())
.env("TSAFE_AKV_TEST_TOKEN", "tok")
.current_dir(dir.path())
.assert()
.success()
.stderr(predicate::str::contains("failed").or(predicate::str::contains("source(s) failed")))
.stdout(predicate::str::contains("source-a"))
.stdout(predicate::str::contains("source-b"));
}