//! Vault connectivity and basic functionality tests //! //! This module tests fundamental Vault operations: //! - Server connectivity and authentication //! - PKI secrets engine functionality //! - AppRole authentication flow //! - Basic certificate generation //! - API response times and reliability use anyhow::{Context, Result}; use serde_json::Value; use std::collections::HashMap; use std::time::{Duration, Instant}; use tokio::process::Command as AsyncCommand; use tracing::{debug, info, warn}; use crate::VaultTestConfig; /// Test basic Vault server connectivity pub async fn test_vault_status(config: &VaultTestConfig) -> Result { let start = Instant::now(); info!("Testing Vault server status"); let mut cmd = AsyncCommand::new("curl"); cmd.args([ "-s", "-f", &format!("{}/v1/sys/health", config.vault_addr) ]); let output = cmd.output().await .context("Failed to check Vault status")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Vault health check failed: {}", stderr)); } let health_json = String::from_utf8_lossy(&output.stdout); let health: Value = serde_json::from_str(&health_json) .context("Failed to parse Vault health response")?; // Verify Vault is initialized and unsealed let initialized = health.get("initialized").and_then(|v| v.as_bool()).unwrap_or(false); let sealed = health.get("sealed").and_then(|v| v.as_bool()).unwrap_or(true); if !initialized { return Err(anyhow::anyhow!("Vault is not initialized")); } if sealed { return Err(anyhow::anyhow!("Vault is sealed")); } info!("Vault server is healthy and operational"); Ok(start.elapsed()) } /// Test Vault authentication with root token pub async fn test_vault_authentication(config: &VaultTestConfig) -> Result { let start = Instant::now(); info!("Testing Vault authentication"); let mut cmd = AsyncCommand::new("curl"); cmd.args([ "-s", "-f", "-H", &format!("X-Vault-Token: {}", config.vault_token), &format!("{}/v1/auth/token/lookup-self", config.vault_addr) ]); let output = cmd.output().await .context("Failed to authenticate with Vault")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Vault authentication failed: {}", stderr)); } let token_info = String::from_utf8_lossy(&output.stdout); let token_data: Value = serde_json::from_str(&token_info) .context("Failed to parse token info")?; // Verify token has necessary permissions if let Some(policies) = token_data.get("data").and_then(|d| d.get("policies")) { if !policies.as_array().unwrap_or(&Vec::new()).iter() .any(|p| p.as_str() == Some("root")) { return Err(anyhow::anyhow!("Token does not have root policy")); } } info!("Vault authentication successful"); Ok(start.elapsed()) } /// Test PKI secrets engine configuration pub async fn test_pki_secrets_engine(config: &VaultTestConfig) -> Result { let start = Instant::now(); info!("Testing PKI secrets engine"); // List secrets engines let mut cmd = AsyncCommand::new("curl"); cmd.args([ "-s", "-f", "-H", &format!("X-Vault-Token: {}", config.vault_token), &format!("{}/v1/sys/mounts", config.vault_addr) ]); let output = cmd.output().await .context("Failed to list secrets engines")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to list secrets engines: {}", stderr)); } let mounts_json = String::from_utf8_lossy(&output.stdout); let mounts: Value = serde_json::from_str(&mounts_json) .context("Failed to parse mounts response")?; // Verify PKI engines are mounted let data = mounts.get("data").or_else(|| mounts.as_object().map(|_| &mounts)) .context("No data in mounts response")?; let has_pki = data.get("pki/").is_some(); let has_pki_int = data.get("pki_int/").is_some(); if !has_pki || !has_pki_int { return Err(anyhow::anyhow!("PKI secrets engines not properly configured")); } info!("PKI secrets engines are properly configured"); Ok(start.elapsed()) } /// Test AppRole authentication method pub async fn test_approle_authentication(config: &VaultTestConfig) -> Result { let start = Instant::now(); info!("Testing AppRole authentication method"); // List auth methods let mut cmd = AsyncCommand::new("curl"); cmd.args([ "-s", "-f", "-H", &format!("X-Vault-Token: {}", config.vault_token), &format!("{}/v1/sys/auth", config.vault_addr) ]); let output = cmd.output().await .context("Failed to list auth methods")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to list auth methods: {}", stderr)); } let auth_json = String::from_utf8_lossy(&output.stdout); let auth_methods: Value = serde_json::from_str(&auth_json) .context("Failed to parse auth methods response")?; // Verify AppRole is enabled let data = auth_methods.get("data").or_else(|| auth_methods.as_object().map(|_| &auth_methods)) .context("No data in auth methods response")?; let has_approle = data.get("approle/").is_some(); if !has_approle { return Err(anyhow::anyhow!("AppRole authentication method not enabled")); } // Test AppRole role exists let mut role_cmd = AsyncCommand::new("curl"); role_cmd.args([ "-s", "-f", "-H", &format!("X-Vault-Token: {}", config.vault_token), &format!("{}/v1/auth/approle/role/trading-services", config.vault_addr) ]); let role_output = role_cmd.output().await .context("Failed to check AppRole role")?; if !role_output.status.success() { return Err(anyhow::anyhow!("AppRole trading-services role not found")); } info!("AppRole authentication method is properly configured"); Ok(start.elapsed()) } /// Test basic certificate generation pub async fn test_certificate_generation(config: &VaultTestConfig) -> Result { let start = Instant::now(); info!("Testing certificate generation"); let cert_request = serde_json::json!({ "common_name": "test.foxhunt.internal", "ttl": "1h", "format": "pem" }); let mut cmd = AsyncCommand::new("curl"); cmd.args([ "-s", "-f", "-X", "POST", "-H", &format!("X-Vault-Token: {}", config.vault_token), "-H", "Content-Type: application/json", "-d", &cert_request.to_string(), &format!("{}/v1/pki_int/issue/hft-trading", config.vault_addr) ]); let output = cmd.output().await .context("Failed to generate certificate")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Certificate generation failed: {}", stderr)); } let cert_json = String::from_utf8_lossy(&output.stdout); let cert_response: Value = serde_json::from_str(&cert_json) .context("Failed to parse certificate response")?; // Verify certificate data is present if let Some(data) = cert_response.get("data") { let has_certificate = data.get("certificate").and_then(|v| v.as_str()).is_some(); let has_private_key = data.get("private_key").and_then(|v| v.as_str()).is_some(); let has_ca_chain = data.get("ca_chain").is_some() || data.get("issuing_ca").is_some(); if !has_certificate || !has_private_key || !has_ca_chain { return Err(anyhow::anyhow!("Certificate response missing required fields")); } // Verify certificate common name if let Some(cert_pem) = data.get("certificate").and_then(|v| v.as_str()) { if !cert_pem.contains("BEGIN CERTIFICATE") { return Err(anyhow::anyhow!("Invalid certificate format")); } } info!("Certificate generation successful"); } else { return Err(anyhow::anyhow!("No data in certificate response")); } Ok(start.elapsed()) } /// Test certificate with different parameters pub async fn test_certificate_variations(config: &VaultTestConfig) -> Result { let start = Instant::now(); info!("Testing certificate generation with different parameters"); let test_cases = vec![ ("trading.foxhunt.internal", "30m"), ("backtesting.foxhunt.internal", "1h"), ("tli.foxhunt.internal", "2h"), ]; for (common_name, ttl) in test_cases { let cert_request = serde_json::json!({ "common_name": common_name, "ttl": ttl, "format": "pem", "alt_names": format!("*.{}", common_name) }); let mut cmd = AsyncCommand::new("curl"); cmd.args([ "-s", "-f", "-X", "POST", "-H", &format!("X-Vault-Token: {}", config.vault_token), "-H", "Content-Type: application/json", "-d", &cert_request.to_string(), &format!("{}/v1/pki_int/issue/hft-trading", config.vault_addr) ]); let output = cmd.output().await .with_context(|| format!("Failed to generate certificate for {}", common_name))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!( "Certificate generation failed for {}: {}", common_name, stderr )); } debug!("Certificate generated successfully for {}", common_name); } info!("Certificate variation testing completed"); Ok(start.elapsed()) } /// Test Vault API response times pub async fn test_api_response_times(config: &VaultTestConfig) -> Result> { info!("Testing Vault API response times"); let mut response_times = HashMap::new(); // Test various API endpoints let endpoints = vec![ ("health", format!("{}/v1/sys/health", config.vault_addr)), ("auth", format!("{}/v1/auth/token/lookup-self", config.vault_addr)), ("pki_ca", format!("{}/v1/pki_int/ca/pem", config.vault_addr)), ]; for (name, url) in endpoints { let start = Instant::now(); let mut cmd = AsyncCommand::new("curl"); cmd.args(["-s", "-f"]); if name != "health" && name != "pki_ca" { cmd.args(["-H", &format!("X-Vault-Token: {}", config.vault_token)]); } cmd.arg(&url); let output = cmd.output().await .with_context(|| format!("Failed to test endpoint: {}", name))?; let duration = start.elapsed(); if !output.status.success() { warn!("Endpoint {} failed", name); } else { response_times.insert(name.to_string(), duration); debug!("Endpoint {} response time: {:?}", name, duration); } } info!("API response time testing completed"); Ok(response_times) } /// Run all connectivity tests pub async fn run_connectivity_tests(config: &VaultTestConfig) -> Result { let overall_start = Instant::now(); info!("Running Vault connectivity tests"); // Test 1: Vault status test_vault_status(config).await .context("Vault status test failed")?; // Test 2: Authentication test_vault_authentication(config).await .context("Vault authentication test failed")?; // Test 3: PKI secrets engine test_pki_secrets_engine(config).await .context("PKI secrets engine test failed")?; // Test 4: AppRole authentication test_approle_authentication(config).await .context("AppRole authentication test failed")?; // Test 5: Certificate generation test_certificate_generation(config).await .context("Certificate generation test failed")?; // Test 6: Certificate variations test_certificate_variations(config).await .context("Certificate variations test failed")?; // Test 7: API response times let response_times = test_api_response_times(config).await .context("API response times test failed")?; // Validate response times are acceptable for (endpoint, duration) in &response_times { if *duration > Duration::from_millis(500) { warn!("Endpoint {} response time {}ms exceeds 500ms", endpoint, duration.as_millis()); } } info!("All Vault connectivity tests passed"); Ok(overall_start.elapsed()) } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_vault_connectivity_integration() { let config = VaultTestConfig::default(); // This test requires a running Vault server match run_connectivity_tests(&config).await { Ok(duration) => { println!("Connectivity tests completed in {:?}", duration); } Err(e) => { println!("Connectivity tests failed (expected without Vault): {}", e); } } } #[test] fn test_config_values() { let config = VaultTestConfig::default(); assert!(!config.vault_addr.is_empty()); assert!(!config.vault_token.is_empty()); assert!(config.test_timeout > Duration::ZERO); } }