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>
485 lines
17 KiB
Rust
485 lines
17 KiB
Rust
//! Comprehensive End-to-End Tests for HashiCorp Vault Integration
|
|
//!
|
|
//! This module provides comprehensive E2E testing for Vault integration in the Foxhunt HFT system:
|
|
//! - Real Vault server testing with PKI secrets engine
|
|
//! - Service startup dependency validation
|
|
//! - Certificate lifecycle management (generation, caching, rotation)
|
|
//! - Failure scenario testing (Vault down, network partitions, timeouts)
|
|
//! - Performance impact measurement for HFT requirements
|
|
//! - Circuit breaker behavior validation
|
|
//! - Multi-service integration testing
|
|
|
|
use anyhow::{Context, Result};
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use std::time::{Duration, Instant};
|
|
use tokio::sync::RwLock;
|
|
use tracing::{debug, info, warn, error};
|
|
|
|
// Re-export test modules
|
|
pub mod docker_compose;
|
|
pub mod vault_connectivity_tests;
|
|
pub mod certificate_lifecycle_tests;
|
|
pub mod service_integration_tests;
|
|
pub mod failure_scenario_tests;
|
|
pub mod performance_impact_tests;
|
|
|
|
/// Configuration for Vault E2E tests
|
|
#[derive(Debug, Clone)]
|
|
pub struct VaultTestConfig {
|
|
/// Vault server address for testing
|
|
pub vault_addr: String,
|
|
/// Vault root token for test setup
|
|
pub vault_token: String,
|
|
/// Test timeout for individual tests
|
|
pub test_timeout: Duration,
|
|
/// Performance test iterations
|
|
pub perf_iterations: u32,
|
|
/// Docker Compose project name
|
|
pub compose_project: String,
|
|
/// Certificate cache directory for tests
|
|
pub cert_cache_dir: String,
|
|
/// Whether to cleanup resources after tests
|
|
pub cleanup_after_tests: bool,
|
|
}
|
|
|
|
impl Default for VaultTestConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
vault_addr: "http://localhost:8200".to_string(),
|
|
vault_token: "vault-root-token".to_string(),
|
|
test_timeout: Duration::from_secs(30),
|
|
perf_iterations: 1000,
|
|
compose_project: "foxhunt-vault-e2e".to_string(),
|
|
cert_cache_dir: "/tmp/foxhunt-vault-test-certs".to_string(),
|
|
cleanup_after_tests: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Performance metrics for test validation
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct PerformanceMetrics {
|
|
/// Certificate generation time from Vault
|
|
pub cert_generation_time: Option<Duration>,
|
|
/// Certificate cache lookup time
|
|
pub cache_lookup_time: Option<Duration>,
|
|
/// TLS handshake time with Vault certificates
|
|
pub tls_handshake_time: Option<Duration>,
|
|
/// Memory usage during certificate operations
|
|
pub memory_usage_mb: Option<f64>,
|
|
/// CPU usage during background rotation
|
|
pub cpu_usage_percent: Option<f64>,
|
|
/// Vault API call count
|
|
pub vault_api_calls: u64,
|
|
/// Cache hit rate percentage
|
|
pub cache_hit_rate: Option<f64>,
|
|
}
|
|
|
|
impl PerformanceMetrics {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Validate metrics against HFT requirements
|
|
pub fn validate_hft_requirements(&self) -> Result<()> {
|
|
// Certificate generation should be under 100ms
|
|
if let Some(cert_time) = self.cert_generation_time {
|
|
if cert_time > Duration::from_millis(100) {
|
|
return Err(anyhow::anyhow!(
|
|
"Certificate generation time {}ms exceeds 100ms HFT requirement",
|
|
cert_time.as_millis()
|
|
));
|
|
}
|
|
}
|
|
|
|
// Cache lookup should be under 1μs
|
|
if let Some(cache_time) = self.cache_lookup_time {
|
|
if cache_time > Duration::from_micros(1) {
|
|
return Err(anyhow::anyhow!(
|
|
"Cache lookup time {}μs exceeds 1μs HFT requirement",
|
|
cache_time.as_micros()
|
|
));
|
|
}
|
|
}
|
|
|
|
// TLS handshake should be under 1ms for cached certificates
|
|
if let Some(tls_time) = self.tls_handshake_time {
|
|
if tls_time > Duration::from_millis(1) {
|
|
return Err(anyhow::anyhow!(
|
|
"TLS handshake time {}ms exceeds 1ms HFT requirement",
|
|
tls_time.as_millis()
|
|
));
|
|
}
|
|
}
|
|
|
|
// Memory usage should be under 10MB per service
|
|
if let Some(memory) = self.memory_usage_mb {
|
|
if memory > 10.0 {
|
|
return Err(anyhow::anyhow!(
|
|
"Memory usage {:.1}MB exceeds 10MB HFT requirement",
|
|
memory
|
|
));
|
|
}
|
|
}
|
|
|
|
// CPU usage should be under 5% for background tasks
|
|
if let Some(cpu) = self.cpu_usage_percent {
|
|
if cpu > 5.0 {
|
|
return Err(anyhow::anyhow!(
|
|
"CPU usage {:.1}% exceeds 5% HFT requirement",
|
|
cpu
|
|
));
|
|
}
|
|
}
|
|
|
|
// Cache hit rate should be over 80%
|
|
if let Some(hit_rate) = self.cache_hit_rate {
|
|
if hit_rate < 80.0 {
|
|
return Err(anyhow::anyhow!(
|
|
"Cache hit rate {:.1}% is below 80% efficiency requirement",
|
|
hit_rate
|
|
));
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Test results aggregation for comprehensive reporting
|
|
#[derive(Debug, Default)]
|
|
pub struct VaultTestResults {
|
|
/// Successful tests with timing
|
|
pub successes: Vec<(String, Duration)>,
|
|
/// Failed tests with error messages
|
|
pub failures: Vec<(String, String)>,
|
|
/// Performance metrics collected
|
|
pub performance: PerformanceMetrics,
|
|
/// Additional metadata
|
|
pub metadata: HashMap<String, String>,
|
|
}
|
|
|
|
impl VaultTestResults {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
pub fn add_success(&mut self, test_name: &str, duration: Duration) {
|
|
self.successes.push((test_name.to_string(), duration));
|
|
info!("✓ Test passed: {} ({}ms)", test_name, duration.as_millis());
|
|
}
|
|
|
|
pub fn add_failure(&mut self, test_name: &str, error: String) {
|
|
self.failures.push((test_name.to_string(), error.clone()));
|
|
error!("✗ Test failed: {} - {}", test_name, error);
|
|
}
|
|
|
|
pub fn add_metadata(&mut self, key: &str, value: String) {
|
|
self.metadata.insert(key.to_string(), value);
|
|
}
|
|
|
|
pub fn success_count(&self) -> usize {
|
|
self.successes.len()
|
|
}
|
|
|
|
pub fn failure_count(&self) -> usize {
|
|
self.failures.len()
|
|
}
|
|
|
|
pub fn total_count(&self) -> usize {
|
|
self.successes.len() + self.failures.len()
|
|
}
|
|
|
|
pub fn success_rate(&self) -> f64 {
|
|
if self.total_count() == 0 {
|
|
return 0.0;
|
|
}
|
|
self.success_count() as f64 / self.total_count() as f64
|
|
}
|
|
|
|
/// Generate comprehensive test report
|
|
pub fn generate_report(&self) -> String {
|
|
let mut report = String::new();
|
|
|
|
report.push_str("\n======= VAULT E2E TEST RESULTS =======\n");
|
|
report.push_str(&format!("Total Tests: {}\n", self.total_count()));
|
|
report.push_str(&format!("Successes: {}\n", self.success_count()));
|
|
report.push_str(&format!("Failures: {}\n", self.failure_count()));
|
|
report.push_str(&format!("Success Rate: {:.1}%\n", self.success_rate() * 100.0));
|
|
|
|
// Performance metrics
|
|
report.push_str("\n--- Performance Metrics ---\n");
|
|
if let Some(cert_time) = self.performance.cert_generation_time {
|
|
report.push_str(&format!("Certificate Generation: {}ms\n", cert_time.as_millis()));
|
|
}
|
|
if let Some(cache_time) = self.performance.cache_lookup_time {
|
|
report.push_str(&format!("Cache Lookup: {}μs\n", cache_time.as_micros()));
|
|
}
|
|
if let Some(tls_time) = self.performance.tls_handshake_time {
|
|
report.push_str(&format!("TLS Handshake: {}ms\n", tls_time.as_millis()));
|
|
}
|
|
if let Some(memory) = self.performance.memory_usage_mb {
|
|
report.push_str(&format!("Memory Usage: {:.1}MB\n", memory));
|
|
}
|
|
if let Some(cpu) = self.performance.cpu_usage_percent {
|
|
report.push_str(&format!("CPU Usage: {:.1}%\n", cpu));
|
|
}
|
|
if let Some(hit_rate) = self.performance.cache_hit_rate {
|
|
report.push_str(&format!("Cache Hit Rate: {:.1}%\n", hit_rate));
|
|
}
|
|
report.push_str(&format!("Vault API Calls: {}\n", self.performance.vault_api_calls));
|
|
|
|
// HFT requirements validation
|
|
report.push_str("\n--- HFT Requirements Validation ---\n");
|
|
match self.performance.validate_hft_requirements() {
|
|
Ok(()) => report.push_str("✓ All HFT performance requirements met\n"),
|
|
Err(e) => report.push_str(&format!("✗ HFT requirement violation: {}\n", e)),
|
|
}
|
|
|
|
// Successful tests
|
|
if !self.successes.is_empty() {
|
|
report.push_str("\n--- Successful Tests ---\n");
|
|
for (name, duration) in &self.successes {
|
|
report.push_str(&format!("✓ {} - {}ms\n", name, duration.as_millis()));
|
|
}
|
|
}
|
|
|
|
// Failed tests
|
|
if !self.failures.is_empty() {
|
|
report.push_str("\n--- Failed Tests ---\n");
|
|
for (name, error) in &self.failures {
|
|
report.push_str(&format!("✗ {} - {}\n", name, error));
|
|
}
|
|
}
|
|
|
|
// Metadata
|
|
if !self.metadata.is_empty() {
|
|
report.push_str("\n--- Test Metadata ---\n");
|
|
for (key, value) in &self.metadata {
|
|
report.push_str(&format!("{}: {}\n", key, value));
|
|
}
|
|
}
|
|
|
|
report.push_str("======================================\n");
|
|
report
|
|
}
|
|
}
|
|
|
|
/// Main Vault E2E test suite coordinator
|
|
pub struct VaultE2ETestSuite {
|
|
config: VaultTestConfig,
|
|
results: Arc<RwLock<VaultTestResults>>,
|
|
}
|
|
|
|
impl VaultE2ETestSuite {
|
|
/// Create new test suite with configuration
|
|
pub fn new(config: VaultTestConfig) -> Self {
|
|
Self {
|
|
config,
|
|
results: Arc::new(RwLock::new(VaultTestResults::new())),
|
|
}
|
|
}
|
|
|
|
/// Run all Vault E2E tests in sequence
|
|
pub async fn run_all_tests(&self) -> Result<VaultTestResults> {
|
|
info!("Starting comprehensive Vault E2E test suite");
|
|
|
|
// Add test metadata
|
|
{
|
|
let mut results = self.results.write().await;
|
|
results.add_metadata("vault_addr".to_string(), self.config.vault_addr.clone());
|
|
results.add_metadata("test_started".to_string(), chrono::Utc::now().to_rfc3339());
|
|
}
|
|
|
|
// Test 1: Docker environment setup
|
|
self.run_docker_setup_tests().await?;
|
|
|
|
// Test 2: Basic Vault connectivity
|
|
self.run_connectivity_tests().await?;
|
|
|
|
// Test 3: Certificate lifecycle management
|
|
self.run_certificate_tests().await?;
|
|
|
|
// Test 4: Service integration testing
|
|
self.run_service_integration_tests().await?;
|
|
|
|
// Test 5: Failure scenario testing
|
|
self.run_failure_scenario_tests().await?;
|
|
|
|
// Test 6: Performance impact validation
|
|
self.run_performance_tests().await?;
|
|
|
|
// Test 7: Cleanup and validation
|
|
if self.config.cleanup_after_tests {
|
|
self.run_cleanup_tests().await?;
|
|
}
|
|
|
|
let results = self.results.read().await;
|
|
info!("Vault E2E test suite completed: {}/{} tests passed",
|
|
results.success_count(), results.total_count());
|
|
|
|
Ok(results.clone())
|
|
}
|
|
|
|
/// Run Docker environment setup tests
|
|
async fn run_docker_setup_tests(&self) -> Result<()> {
|
|
info!("Running Docker environment setup tests");
|
|
|
|
let test_start = Instant::now();
|
|
match docker_compose::DockerEnvironment::new(&self.config).await {
|
|
Ok(mut env) => {
|
|
if let Err(e) = env.start_all_services().await {
|
|
let mut results = self.results.write().await;
|
|
results.add_failure("docker_environment_startup", e.to_string());
|
|
return Err(e);
|
|
}
|
|
|
|
let mut results = self.results.write().await;
|
|
results.add_success("docker_environment_startup", test_start.elapsed());
|
|
Ok(())
|
|
}
|
|
Err(e) => {
|
|
let mut results = self.results.write().await;
|
|
results.add_failure("docker_environment_setup", e.to_string());
|
|
Err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Run Vault connectivity tests
|
|
async fn run_connectivity_tests(&self) -> Result<()> {
|
|
info!("Running Vault connectivity tests");
|
|
|
|
match vault_connectivity_tests::run_connectivity_tests(&self.config).await {
|
|
Ok(duration) => {
|
|
let mut results = self.results.write().await;
|
|
results.add_success("vault_connectivity", duration);
|
|
Ok(())
|
|
}
|
|
Err(e) => {
|
|
let mut results = self.results.write().await;
|
|
results.add_failure("vault_connectivity", e.to_string());
|
|
Err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Run certificate lifecycle tests
|
|
async fn run_certificate_tests(&self) -> Result<()> {
|
|
info!("Running certificate lifecycle tests");
|
|
|
|
match certificate_lifecycle_tests::run_certificate_tests(&self.config, &self.results).await {
|
|
Ok(()) => Ok(()),
|
|
Err(e) => {
|
|
let mut results = self.results.write().await;
|
|
results.add_failure("certificate_lifecycle", e.to_string());
|
|
Err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Run service integration tests
|
|
async fn run_service_integration_tests(&self) -> Result<()> {
|
|
info!("Running service integration tests");
|
|
|
|
match service_integration_tests::run_service_tests(&self.config, &self.results).await {
|
|
Ok(()) => Ok(()),
|
|
Err(e) => {
|
|
let mut results = self.results.write().await;
|
|
results.add_failure("service_integration", e.to_string());
|
|
Err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Run failure scenario tests
|
|
async fn run_failure_scenario_tests(&self) -> Result<()> {
|
|
info!("Running failure scenario tests");
|
|
|
|
match failure_scenario_tests::run_failure_tests(&self.config, &self.results).await {
|
|
Ok(()) => Ok(()),
|
|
Err(e) => {
|
|
let mut results = self.results.write().await;
|
|
results.add_failure("failure_scenarios", e.to_string());
|
|
Err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Run performance impact tests
|
|
async fn run_performance_tests(&self) -> Result<()> {
|
|
info!("Running performance impact tests");
|
|
|
|
match performance_impact_tests::run_performance_tests(&self.config, &self.results).await {
|
|
Ok(()) => Ok(()),
|
|
Err(e) => {
|
|
let mut results = self.results.write().await;
|
|
results.add_failure("performance_validation", e.to_string());
|
|
Err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Run cleanup and validation tests
|
|
async fn run_cleanup_tests(&self) -> Result<()> {
|
|
info!("Running cleanup tests");
|
|
|
|
let test_start = Instant::now();
|
|
// Cleanup Docker resources
|
|
if let Err(e) = docker_compose::DockerEnvironment::cleanup(&self.config).await {
|
|
warn!("Docker cleanup failed: {}", e);
|
|
}
|
|
|
|
let mut results = self.results.write().await;
|
|
results.add_success("cleanup", test_start.elapsed());
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_vault_test_config() {
|
|
let config = VaultTestConfig::default();
|
|
assert_eq!(config.vault_addr, "http://localhost:8200");
|
|
assert_eq!(config.vault_token, "vault-root-token");
|
|
assert!(config.cleanup_after_tests);
|
|
}
|
|
|
|
#[test]
|
|
fn test_performance_metrics_validation() {
|
|
let mut metrics = PerformanceMetrics::new();
|
|
|
|
// Should pass with good metrics
|
|
metrics.cert_generation_time = Some(Duration::from_millis(50));
|
|
metrics.cache_lookup_time = Some(Duration::from_nanos(500));
|
|
metrics.tls_handshake_time = Some(Duration::from_millis(1));
|
|
metrics.memory_usage_mb = Some(5.0);
|
|
metrics.cpu_usage_percent = Some(2.0);
|
|
metrics.cache_hit_rate = Some(90.0);
|
|
|
|
assert!(metrics.validate_hft_requirements().is_ok());
|
|
|
|
// Should fail with bad metrics
|
|
metrics.cert_generation_time = Some(Duration::from_millis(150));
|
|
assert!(metrics.validate_hft_requirements().is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_results_aggregation() {
|
|
let mut results = VaultTestResults::new();
|
|
results.add_success("test1", Duration::from_millis(10));
|
|
results.add_failure("test2", "Test error".to_string());
|
|
|
|
assert_eq!(results.success_count(), 1);
|
|
assert_eq!(results.failure_count(), 1);
|
|
assert_eq!(results.success_rate(), 0.5);
|
|
|
|
let report = results.generate_report();
|
|
assert!(report.contains("Total Tests: 2"));
|
|
assert!(report.contains("Success Rate: 50.0%"));
|
|
}
|
|
} |