use super::*;
use serial_test::serial;
use std::env;
struct EnvGuard {
vars: Vec<(String, Option<String>)>,
}
impl EnvGuard {
fn new() -> Self {
Self { vars: Vec::new() }
}
fn set(&mut self, key: &str, value: &str) -> &mut Self {
let original = env::var(key).ok();
self.vars.push((key.to_owned(), original));
unsafe { env::set_var(key, value) };
self
}
fn remove(&mut self, key: &str) -> &mut Self {
let original = env::var(key).ok();
self.vars.push((key.to_owned(), original));
unsafe { env::remove_var(key) };
self
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
for (key, original) in self.vars.iter().rev() {
unsafe {
match original {
Some(val) => env::set_var(key, val),
None => env::remove_var(key),
}
}
}
}
}
mod credential_config_tests {
use super::*;
#[test]
fn test_all_credentials_present_resolves_to_client_secret() {
let config = CredentialConfig {
tenant_id: Some("tenant-123".to_owned()),
client_id: Some("client-456".to_owned()),
client_secret: Some("secret-789".to_owned()),
disable_managed_identity: false,
};
let result = config.resolve_credential_type();
assert!(result.is_ok());
assert_eq!(result.unwrap(), CredentialType::ClientSecret);
}
#[test]
fn test_no_credentials_resolves_to_developer_tools() {
let config = CredentialConfig {
tenant_id: None,
client_id: None,
client_secret: None,
disable_managed_identity: false,
};
let result = config.resolve_credential_type();
assert!(result.is_ok());
assert_eq!(result.unwrap(), CredentialType::DeveloperTools);
}
#[test]
fn test_no_credentials_with_mi_disabled_fails() {
let config = CredentialConfig {
tenant_id: None,
client_id: None,
client_secret: None,
disable_managed_identity: true,
};
let result = config.resolve_credential_type();
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("no env credentials provided"));
assert!(err.contains("managed identity disabled"));
}
#[test]
fn test_partial_credentials_with_mi_disabled_fails() {
let config = CredentialConfig {
tenant_id: Some("tenant-123".to_owned()),
client_id: None,
client_secret: None,
disable_managed_identity: true,
};
let result = config.resolve_credential_type();
assert!(result.is_err());
}
#[test]
fn test_partial_credentials_falls_back_to_developer_tools() {
let config = CredentialConfig {
tenant_id: Some("tenant-123".to_owned()),
client_id: Some("client-456".to_owned()),
client_secret: None,
disable_managed_identity: false,
};
let result = config.resolve_credential_type();
assert!(result.is_ok());
assert_eq!(result.unwrap(), CredentialType::DeveloperTools);
}
#[test]
fn test_has_client_secret_credentials_all_present() {
let config = CredentialConfig {
tenant_id: Some("t".to_owned()),
client_id: Some("c".to_owned()),
client_secret: Some("s".to_owned()),
disable_managed_identity: false,
};
assert!(config.has_client_secret_credentials());
}
#[test]
fn test_has_client_secret_credentials_missing_one() {
let config = CredentialConfig {
tenant_id: Some("t".to_owned()),
client_id: Some("c".to_owned()),
client_secret: None,
disable_managed_identity: false,
};
assert!(!config.has_client_secret_credentials());
}
#[test]
fn test_has_client_secret_credentials_all_missing() {
let config = CredentialConfig {
tenant_id: None,
client_id: None,
client_secret: None,
disable_managed_identity: false,
};
assert!(!config.has_client_secret_credentials());
}
#[test]
fn test_has_partial_credentials_none() {
let config = CredentialConfig {
tenant_id: None,
client_id: None,
client_secret: None,
disable_managed_identity: false,
};
assert!(!config.has_partial_credentials());
}
#[test]
fn test_has_partial_credentials_one() {
let config = CredentialConfig {
tenant_id: Some("t".to_owned()),
client_id: None,
client_secret: None,
disable_managed_identity: false,
};
assert!(config.has_partial_credentials());
}
#[test]
fn test_has_partial_credentials_two() {
let config = CredentialConfig {
tenant_id: Some("t".to_owned()),
client_id: Some("c".to_owned()),
client_secret: None,
disable_managed_identity: false,
};
assert!(config.has_partial_credentials());
}
#[test]
fn test_has_partial_credentials_all() {
let config = CredentialConfig {
tenant_id: Some("t".to_owned()),
client_id: Some("c".to_owned()),
client_secret: Some("s".to_owned()),
disable_managed_identity: false,
};
assert!(!config.has_partial_credentials());
}
}
mod env_tests {
use super::*;
#[test]
#[serial]
fn test_from_env_with_all_credentials() {
let mut guard = EnvGuard::new();
guard
.set("AZURE_TENANT_ID", "test-tenant")
.set("AZURE_CLIENT_ID", "test-client")
.set("AZURE_CLIENT_SECRET", "test-secret")
.remove("AZURE_IDENTITY_DISABLE_MANAGED_IDENTITY_CREDENTIAL");
let config = CredentialConfig::from_env();
assert_eq!(config.tenant_id.as_deref(), Some("test-tenant"));
assert_eq!(config.client_id.as_deref(), Some("test-client"));
assert_eq!(config.client_secret.as_deref(), Some("test-secret"));
assert!(!config.disable_managed_identity);
}
#[test]
#[serial]
fn test_from_env_with_no_credentials() {
let mut guard = EnvGuard::new();
guard
.remove("AZURE_TENANT_ID")
.remove("AZURE_CLIENT_ID")
.remove("AZURE_CLIENT_SECRET")
.remove("AZURE_IDENTITY_DISABLE_MANAGED_IDENTITY_CREDENTIAL");
let config = CredentialConfig::from_env();
assert!(config.tenant_id.is_none());
assert!(config.client_id.is_none());
assert!(config.client_secret.is_none());
assert!(!config.disable_managed_identity);
}
#[test]
#[serial]
fn test_from_env_with_mi_disabled_true() {
let mut guard = EnvGuard::new();
guard
.remove("AZURE_TENANT_ID")
.remove("AZURE_CLIENT_ID")
.remove("AZURE_CLIENT_SECRET")
.set("AZURE_IDENTITY_DISABLE_MANAGED_IDENTITY_CREDENTIAL", "true");
let config = CredentialConfig::from_env();
assert!(config.disable_managed_identity);
}
#[test]
#[serial]
fn test_from_env_with_mi_disabled_one() {
let mut guard = EnvGuard::new();
guard
.remove("AZURE_TENANT_ID")
.remove("AZURE_CLIENT_ID")
.remove("AZURE_CLIENT_SECRET")
.set("AZURE_IDENTITY_DISABLE_MANAGED_IDENTITY_CREDENTIAL", "1");
let config = CredentialConfig::from_env();
assert!(config.disable_managed_identity);
}
#[test]
#[serial]
fn test_from_env_with_mi_disabled_case_insensitive() {
let mut guard = EnvGuard::new();
guard
.remove("AZURE_TENANT_ID")
.remove("AZURE_CLIENT_ID")
.remove("AZURE_CLIENT_SECRET")
.set("AZURE_IDENTITY_DISABLE_MANAGED_IDENTITY_CREDENTIAL", "TRUE");
let config = CredentialConfig::from_env();
assert!(config.disable_managed_identity);
}
#[test]
#[serial]
fn test_from_env_with_mi_disabled_false() {
let mut guard = EnvGuard::new();
guard
.remove("AZURE_TENANT_ID")
.remove("AZURE_CLIENT_ID")
.remove("AZURE_CLIENT_SECRET")
.set(
"AZURE_IDENTITY_DISABLE_MANAGED_IDENTITY_CREDENTIAL",
"false",
);
let config = CredentialConfig::from_env();
assert!(!config.disable_managed_identity);
}
#[test]
#[serial]
fn test_from_env_with_mi_disabled_invalid_value() {
let mut guard = EnvGuard::new();
guard
.remove("AZURE_TENANT_ID")
.remove("AZURE_CLIENT_ID")
.remove("AZURE_CLIENT_SECRET")
.set("AZURE_IDENTITY_DISABLE_MANAGED_IDENTITY_CREDENTIAL", "yes");
let config = CredentialConfig::from_env();
assert!(!config.disable_managed_identity);
}
}
mod client_tests {
use super::*;
#[test]
fn test_new_with_empty_vault_url() {
let config = CredentialConfig {
tenant_id: Some("t".to_owned()),
client_id: Some("c".to_owned()),
client_secret: Some("s".to_owned()),
disable_managed_identity: false,
};
let result = KeyVaultClient::with_config("", config);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("cannot be empty"));
}
#[test]
fn test_new_with_whitespace_only_vault_url() {
let config = CredentialConfig {
tenant_id: Some("t".to_owned()),
client_id: Some("c".to_owned()),
client_secret: Some("s".to_owned()),
disable_managed_identity: false,
};
let result = KeyVaultClient::with_config(" ", config);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("cannot be empty"));
}
#[test]
fn test_new_with_tabs_only_vault_url() {
let config = CredentialConfig {
tenant_id: Some("t".to_owned()),
client_id: Some("c".to_owned()),
client_secret: Some("s".to_owned()),
disable_managed_identity: false,
};
let result = KeyVaultClient::with_config("\t\t", config);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("cannot be empty"));
}
#[test]
fn test_new_fails_when_mi_disabled_and_no_creds() {
let config = CredentialConfig {
tenant_id: None,
client_id: None,
client_secret: None,
disable_managed_identity: true,
};
let result = KeyVaultClient::with_config("https://my-vault.vault.azure.net", config);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("no env credentials provided"));
}
#[test]
fn test_new_with_valid_client_secret_credentials() {
let config = CredentialConfig {
tenant_id: Some("12345678-1234-1234-1234-123456789012".to_owned()),
client_id: Some("87654321-4321-4321-4321-210987654321".to_owned()),
client_secret: Some("super-secret-value".to_owned()),
disable_managed_identity: false,
};
let result = KeyVaultClient::with_config("https://my-vault.vault.azure.net", config);
let _ = result;
}
}
mod vault_url_tests {
use super::*;
#[test]
fn test_empty_url_rejected() {
let config = CredentialConfig {
tenant_id: None,
client_id: None,
client_secret: None,
disable_managed_identity: false,
};
let result = KeyVaultClient::with_config("", config);
assert!(result.is_err());
}
#[test]
fn test_valid_url_format() {
let config = CredentialConfig {
tenant_id: Some("t".to_owned()),
client_id: Some("c".to_owned()),
client_secret: Some("s".to_owned()),
disable_managed_identity: false,
};
let urls = [
"https://vault.vault.azure.net",
"https://my-vault.vault.azure.net/",
"https://test-keyvault-001.vault.azure.net",
];
for url in urls {
let _ = KeyVaultClient::with_config(url, config.clone());
}
}
}
mod precedence_tests {
use super::*;
#[test]
fn test_client_secret_takes_precedence_over_developer_tools() {
let config = CredentialConfig {
tenant_id: Some("tenant".to_owned()),
client_id: Some("client".to_owned()),
client_secret: Some("secret".to_owned()),
disable_managed_identity: false, };
let cred_type = config.resolve_credential_type().unwrap();
assert_eq!(cred_type, CredentialType::ClientSecret);
}
#[test]
fn test_developer_tools_used_when_any_credential_missing() {
let config1 = CredentialConfig {
tenant_id: None,
client_id: Some("client".to_owned()),
client_secret: Some("secret".to_owned()),
disable_managed_identity: false,
};
assert_eq!(
config1.resolve_credential_type().unwrap(),
CredentialType::DeveloperTools
);
let config2 = CredentialConfig {
tenant_id: Some("tenant".to_owned()),
client_id: None,
client_secret: Some("secret".to_owned()),
disable_managed_identity: false,
};
assert_eq!(
config2.resolve_credential_type().unwrap(),
CredentialType::DeveloperTools
);
let config3 = CredentialConfig {
tenant_id: Some("tenant".to_owned()),
client_id: Some("client".to_owned()),
client_secret: None,
disable_managed_identity: false,
};
assert_eq!(
config3.resolve_credential_type().unwrap(),
CredentialType::DeveloperTools
);
}
}
mod error_tests {
use super::*;
#[test]
fn test_empty_vault_url_error_message() {
let config = CredentialConfig {
tenant_id: Some("t".to_owned()),
client_id: Some("c".to_owned()),
client_secret: Some("s".to_owned()),
disable_managed_identity: false,
};
let err = KeyVaultClient::with_config("", config).unwrap_err();
assert_eq!(err.to_string(), "KeyVault URL cannot be empty");
}
#[test]
fn test_no_credentials_with_mi_disabled_error_message() {
let config = CredentialConfig {
tenant_id: None,
client_id: None,
client_secret: None,
disable_managed_identity: true,
};
let err = config.resolve_credential_type().unwrap_err();
assert!(
err.to_string()
.contains("KeyVault enabled but no env credentials provided")
);
assert!(err.to_string().contains("managed identity disabled"));
}
}
mod credential_type_tests {
use super::*;
#[test]
fn test_credential_type_debug() {
assert_eq!(
format!("{:?}", CredentialType::ClientSecret),
"ClientSecret"
);
assert_eq!(
format!("{:?}", CredentialType::DeveloperTools),
"DeveloperTools"
);
}
#[test]
fn test_credential_type_clone() {
let original = CredentialType::ClientSecret;
let cloned = original.clone();
assert_eq!(original, cloned);
}
#[test]
fn test_credential_type_equality() {
assert_eq!(CredentialType::ClientSecret, CredentialType::ClientSecret);
assert_eq!(
CredentialType::DeveloperTools,
CredentialType::DeveloperTools
);
assert_ne!(CredentialType::ClientSecret, CredentialType::DeveloperTools);
}
}
mod config_traits_tests {
use super::*;
#[test]
fn test_credential_config_debug() {
let config = CredentialConfig {
tenant_id: Some("tenant".to_owned()),
client_id: Some("client".to_owned()),
client_secret: Some("secret".to_owned()),
disable_managed_identity: true,
};
let debug_str = format!("{config:?}");
assert!(debug_str.contains("tenant_id"));
assert!(debug_str.contains("client_id"));
assert!(debug_str.contains("client_secret"));
assert!(debug_str.contains("disable_managed_identity"));
}
#[test]
fn test_credential_config_clone() {
let config = CredentialConfig {
tenant_id: Some("tenant".to_owned()),
client_id: Some("client".to_owned()),
client_secret: Some("secret".to_owned()),
disable_managed_identity: true,
};
let cloned = config.clone();
assert_eq!(cloned.tenant_id, config.tenant_id);
assert_eq!(cloned.client_id, config.client_id);
assert_eq!(cloned.client_secret, config.client_secret);
assert_eq!(
cloned.disable_managed_identity,
config.disable_managed_identity
);
}
}
#[cfg(not(feature = "keyvault"))]
mod feature_disabled_tests {
use super::*;
#[test]
fn test_keyvault_disabled_returns_error() {
let result = KeyVaultClient::new("https://vault.vault.azure.net");
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("KeyVault feature not enabled")
);
}
}
mod secret_fetching_tests {
use super::*;
#[tokio::test]
async fn test_fetch_secret_success_path_setup() {
let config = CredentialConfig {
tenant_id: Some("12345678-1234-1234-1234-123456789012".to_owned()),
client_id: Some("87654321-4321-4321-4321-210987654321".to_owned()),
client_secret: Some("test-secret-value".to_owned()),
disable_managed_identity: false,
};
let result = KeyVaultClient::with_config("https://test-vault.vault.azure.net", config);
assert!(
result.is_ok(),
"Client construction failed: {:?}",
result.err()
);
let client = result.unwrap();
let empty_result = client.fetch_secret("").await;
assert!(empty_result.is_ok());
assert!(empty_result.unwrap().is_none());
}
#[test]
fn test_fetch_secret_not_found_error_context() {
let secret_name = "non-existent-secret";
let expected_context = format!("failed to fetch secret '{secret_name}' from KeyVault");
assert!(expected_context.contains(secret_name));
assert!(expected_context.contains("failed to fetch"));
assert!(expected_context.contains("KeyVault"));
}
#[test]
fn test_fetch_secret_auth_failure_credential_path() {
let config = CredentialConfig {
tenant_id: None,
client_id: None,
client_secret: None,
disable_managed_identity: true,
};
let result = KeyVaultClient::with_config("https://test-vault.vault.azure.net", config);
assert!(result.is_err());
let err = result.unwrap_err();
let err_string = err.to_string();
assert!(
err_string.contains("no env credentials provided"),
"Expected auth error, got: {err_string}"
);
}
#[tokio::test]
async fn test_fetch_secret_timeout_pattern() {
use std::time::Duration;
use tokio::time::timeout;
let config = CredentialConfig {
tenant_id: Some("12345678-1234-1234-1234-123456789012".to_owned()),
client_id: Some("87654321-4321-4321-4321-210987654321".to_owned()),
client_secret: Some("test-secret".to_owned()),
disable_managed_identity: false,
};
let client =
KeyVaultClient::with_config("https://test-vault.vault.azure.net", config).unwrap();
let result = timeout(Duration::from_millis(100), client.fetch_secret("")).await;
assert!(result.is_ok(), "Timeout should not trigger for empty name");
let inner_result = result.unwrap();
assert!(inner_result.is_ok());
assert!(inner_result.unwrap().is_none());
}
#[tokio::test]
async fn test_fetch_secrets_parallel_empty_names() {
let config = CredentialConfig {
tenant_id: Some("12345678-1234-1234-1234-123456789012".to_owned()),
client_id: Some("87654321-4321-4321-4321-210987654321".to_owned()),
client_secret: Some("test-secret".to_owned()),
disable_managed_identity: false,
};
let client =
KeyVaultClient::with_config("https://test-vault.vault.azure.net", config).unwrap();
let names: &[&str] = &["", " ", "\t", ""];
let results = client.fetch_secrets(names).await;
assert!(results.is_ok());
let values = results.unwrap();
assert_eq!(values.len(), 4);
for value in values {
assert!(value.is_none(), "Empty/whitespace names should return None");
}
}
#[tokio::test]
async fn test_fetch_secrets_partial_failure_behavior() {
let config = CredentialConfig {
tenant_id: Some("12345678-1234-1234-1234-123456789012".to_owned()),
client_id: Some("87654321-4321-4321-4321-210987654321".to_owned()),
client_secret: Some("test-secret".to_owned()),
disable_managed_identity: false,
};
let client =
KeyVaultClient::with_config("https://test-vault.vault.azure.net", config).unwrap();
let names: &[&str] = &["", ""];
let results = client.fetch_secrets(names).await;
assert!(results.is_ok());
let values = results.unwrap();
assert_eq!(values.len(), 2);
}
#[tokio::test]
async fn test_secret_name_special_chars_handling() {
let config = CredentialConfig {
tenant_id: Some("12345678-1234-1234-1234-123456789012".to_owned()),
client_id: Some("87654321-4321-4321-4321-210987654321".to_owned()),
client_secret: Some("test-secret".to_owned()),
disable_managed_identity: false,
};
let _client =
KeyVaultClient::with_config("https://test-vault.vault.azure.net", config).unwrap();
let special_names = [
"my-secret-name",
"my_secret_name",
"my-secret_name-123",
"SECRET-NAME",
"secret--double-dash",
"secret__double_underscore",
];
for name in special_names {
assert!(!name.trim().is_empty(), "Name '{name}' should not be empty");
}
}
#[tokio::test]
async fn test_secret_value_empty_name_variants() {
let config = CredentialConfig {
tenant_id: Some("12345678-1234-1234-1234-123456789012".to_owned()),
client_id: Some("87654321-4321-4321-4321-210987654321".to_owned()),
client_secret: Some("test-secret".to_owned()),
disable_managed_identity: false,
};
let client =
KeyVaultClient::with_config("https://test-vault.vault.azure.net", config).unwrap();
let empty_variants = ["", " ", " ", "\t", "\n", " \t\n "];
for name in empty_variants {
let result = client.fetch_secret(name).await;
assert!(
result.is_ok(),
"Empty name '{}' should succeed",
name.escape_debug()
);
assert!(
result.unwrap().is_none(),
"Empty name '{}' should return None",
name.escape_debug()
);
}
}
#[test]
fn test_secret_value_large_credentials() {
let large_secret = "x".repeat(1024 * 1024);
let config = CredentialConfig {
tenant_id: Some("12345678-1234-1234-1234-123456789012".to_owned()),
client_id: Some("87654321-4321-4321-4321-210987654321".to_owned()),
client_secret: Some(large_secret),
disable_managed_identity: false,
};
assert!(config.has_client_secret_credentials());
assert!(!config.has_partial_credentials());
assert_eq!(
config.resolve_credential_type().unwrap(),
CredentialType::ClientSecret
);
assert_eq!(config.client_secret.unwrap().len(), 1024 * 1024);
}
#[test]
fn test_credential_client_secret_creation_path() {
let config = CredentialConfig {
tenant_id: Some("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee".to_owned()),
client_id: Some("11111111-2222-3333-4444-555555555555".to_owned()),
client_secret: Some("super-secret-client-credential".to_owned()),
disable_managed_identity: false,
};
let cred_type = config.resolve_credential_type().unwrap();
assert_eq!(cred_type, CredentialType::ClientSecret);
let result = KeyVaultClient::with_config("https://my-vault.vault.azure.net", config);
assert!(
result.is_ok(),
"ClientSecretCredential creation failed: {:?}",
result.err()
);
}
#[test]
fn test_credential_developer_tools_creation_path() {
let config = CredentialConfig {
tenant_id: None,
client_id: None,
client_secret: None,
disable_managed_identity: false,
};
let cred_type = config.resolve_credential_type().unwrap();
assert_eq!(cred_type, CredentialType::DeveloperTools);
let result = KeyVaultClient::with_config("https://my-vault.vault.azure.net", config);
assert!(
result.is_ok(),
"DeveloperToolsCredential creation failed: {:?}",
result.err()
);
}
#[tokio::test]
async fn test_concurrent_fetch_thread_safety() {
use std::sync::Arc;
use tokio::task::JoinSet;
let config = CredentialConfig {
tenant_id: Some("12345678-1234-1234-1234-123456789012".to_owned()),
client_id: Some("87654321-4321-4321-4321-210987654321".to_owned()),
client_secret: Some("test-secret".to_owned()),
disable_managed_identity: false,
};
let client = Arc::new(
KeyVaultClient::with_config("https://test-vault.vault.azure.net", config).unwrap(),
);
let mut join_set = JoinSet::new();
for i in 0..100 {
let client_clone = Arc::clone(&client);
let name = if i % 2 == 0 { "" } else { " " };
join_set.spawn(async move {
let result = client_clone.fetch_secret(name).await;
assert!(result.is_ok(), "Task {i} failed");
assert!(result.unwrap().is_none(), "Task {i} returned value");
});
}
while let Some(result) = join_set.join_next().await {
assert!(result.is_ok(), "Task panicked: {:?}", result.err());
}
}
}
mod debug_impl_tests {
use super::*;
#[test]
fn test_keyvault_client_debug_format() {
let config = CredentialConfig {
tenant_id: Some("12345678-1234-1234-1234-123456789012".to_owned()),
client_id: Some("87654321-4321-4321-4321-210987654321".to_owned()),
client_secret: Some("test-secret".to_owned()),
disable_managed_identity: false,
};
let client =
KeyVaultClient::with_config("https://test-vault.vault.azure.net", config).unwrap();
let debug_str = format!("{client:?}");
assert!(debug_str.contains("KeyVaultClient"));
assert!(debug_str.contains("<SecretClient>"));
assert!(!debug_str.contains("test-secret"));
}
}
#[cfg(feature = "keyvault-integration")]
mod integration_tests {
use super::*;
#[tokio::test]
#[ignore = "Requires real Azure KeyVault credentials"]
async fn test_real_fetch_secret() {
let client = KeyVaultClient::new("https://your-vault.vault.azure.net").unwrap();
let result = client.fetch_secret("test-secret").await;
assert!(result.is_ok());
}
#[tokio::test]
#[ignore = "Requires real Azure KeyVault credentials"]
async fn test_real_fetch_secret_not_found() {
let client = KeyVaultClient::new("https://your-vault.vault.azure.net").unwrap();
let result = client
.fetch_secret("definitely-not-existing-secret-12345")
.await;
assert!(result.is_err());
}
}