Files
foxhunt/testing/vault-integration/vault_connectivity_tests.rs
jgrusewski 9c3d741a08 refactor: restructure repo — crates/, bin/, testing/ layout
Move 17 library crates into crates/, CLI binary into bin/fxt,
consolidate 10 test crates into testing/, split config crate
from deployment config files.

Root directory reduced from 38+ to ~17 directories.
All Cargo.toml paths and build.rs proto refs updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:56:00 +01:00

411 lines
13 KiB
Rust

//! 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<Duration> {
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<Duration> {
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<Duration> {
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<Duration> {
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<Duration> {
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<Duration> {
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<HashMap<String, Duration>> {
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<Duration> {
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);
}
}