Files
foxhunt/testing/vault-integration/main.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

552 lines
19 KiB
Rust

//! Main test runner for Vault E2E integration tests
//!
//! This binary provides a comprehensive test runner for all Vault integration scenarios:
//! - Command-line interface for selective test execution
//! - Test result reporting and analysis
//! - Performance metrics collection
//! - HTML and JSON report generation
//! - CI/CD integration support
use anyhow::{Context, Result};
use clap::{Arg, Command};
use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use tracing::{info, warn, Level};
use tracing_subscriber;
// Type definitions for Vault E2E testing
/// 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, Serialize, Deserialize)]
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>,
}
/// Results from Vault E2E test execution
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct VaultTestResults {
/// Test execution metadata
pub test_start_time: DateTime<Utc>,
pub test_end_time: Option<DateTime<Utc>>,
pub total_duration: Option<Duration>,
/// Test execution results
pub tests_passed: u32,
pub tests_failed: u32,
pub tests_skipped: u32,
/// Performance metrics collected
pub performance_metrics: PerformanceMetrics,
/// Test execution details
pub test_details: HashMap<String, TestCategoryResults>,
/// Errors encountered during testing
pub errors: Vec<String>,
/// Warnings generated during testing
pub warnings: Vec<String>,
}
/// Results from a specific test category
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TestCategoryResults {
pub passed: u32,
pub failed: u32,
pub skipped: u32,
pub duration: Option<Duration>,
pub details: Vec<String>,
}
mod docker_compose;
mod vault_connectivity_tests;
mod certificate_lifecycle_tests;
mod service_integration_tests;
mod failure_scenario_tests;
mod performance_impact_tests;
use docker_compose::DockerEnvironment;
/// Configuration for test execution
#[derive(Debug, Clone)]
pub struct TestExecutionConfig {
/// Which test categories to run
pub test_categories: Vec<String>,
/// Whether to skip Docker cleanup
pub skip_cleanup: bool,
/// Output directory for reports
pub output_dir: String,
/// Test timeout
pub timeout: std::time::Duration,
/// Verbose logging
pub verbose: bool,
/// Run in CI mode (stricter validation)
pub ci_mode: bool,
}
impl Default for TestExecutionConfig {
fn default() -> Self {
Self {
test_categories: vec!["all".to_string()],
skip_cleanup: false,
output_dir: "test-results".to_string(),
timeout: std::time::Duration::from_secs(600), // 10 minutes
verbose: false,
ci_mode: false,
}
}
}
/// Main entry point for Vault E2E tests
#[tokio::main]
async fn main() -> Result<()> {
let matches = Command::new("Vault E2E Integration Tests")
.version("1.0")
.about("Comprehensive end-to-end tests for HashiCorp Vault integration")
.arg(
Arg::new("categories")
.short('c')
.long("categories")
.value_name("CATEGORIES")
.help("Test categories to run (comma-separated)")
.default_value("all")
)
.arg(
Arg::new("skip-cleanup")
.long("skip-cleanup")
.help("Skip Docker cleanup after tests")
.action(clap::ArgAction::SetTrue)
)
.arg(
Arg::new("output")
.short('o')
.long("output")
.value_name("DIR")
.help("Output directory for test reports")
.default_value("test-results")
)
.arg(
Arg::new("timeout")
.short('t')
.long("timeout")
.value_name("SECONDS")
.help("Test timeout in seconds")
.default_value("600")
)
.arg(
Arg::new("verbose")
.short('v')
.long("verbose")
.help("Enable verbose logging")
.action(clap::ArgAction::SetTrue)
)
.arg(
Arg::new("ci")
.long("ci")
.help("Run in CI mode with stricter validation")
.action(clap::ArgAction::SetTrue)
)
.get_matches();
// Initialize logging
let log_level = if matches.get_flag("verbose") {
Level::DEBUG
} else {
Level::INFO
};
tracing_subscriber::fmt()
.with_max_level(log_level)
.with_target(false)
.init();
// Parse configuration
let categories: Vec<String> = matches.get_one::<String>("categories")
.unwrap()
.split(',')
.map(|s| s.trim().to_string())
.collect();
let timeout_seconds: u64 = matches.get_one::<String>("timeout")
.unwrap()
.parse()
.context("Invalid timeout value")?;
let config = TestExecutionConfig {
test_categories: categories,
skip_cleanup: matches.get_flag("skip-cleanup"),
output_dir: matches.get_one::<String>("output").unwrap().to_string(),
timeout: std::time::Duration::from_secs(timeout_seconds),
verbose: matches.get_flag("verbose"),
ci_mode: matches.get_flag("ci"),
};
info!("Starting Vault E2E integration tests");
info!("Test categories: {:?}", config.test_categories);
info!("Output directory: {}", config.output_dir);
info!("Timeout: {:?}", config.timeout);
// Create output directory
std::fs::create_dir_all(&config.output_dir)
.with_context(|| format!("Failed to create output directory: {}", config.output_dir))?;
// Run tests
let test_start = Instant::now();
let result = run_vault_e2e_tests(config).await;
let total_duration = test_start.elapsed();
match result {
Ok(test_results) => {
info!("Vault E2E tests completed successfully in {:?}", total_duration);
// Print final report
println!("{}", test_results.generate_report());
// Generate detailed reports
generate_test_reports(&test_results, &config.output_dir).await?;
// Determine exit code based on test results
if test_results.failure_count() > 0 {
if config.ci_mode {
std::process::exit(1);
} else {
warn!("Some tests failed, but exiting with success in non-CI mode");
}
}
}
Err(e) => {
eprintln!("Vault E2E tests failed: {}", e);
std::process::exit(1);
}
}
Ok(())
}
/// Run comprehensive Vault E2E tests
async fn run_vault_e2e_tests(config: TestExecutionConfig) -> Result<VaultTestResults> {
let vault_config = VaultTestConfig {
cleanup_after_tests: !config.skip_cleanup,
test_timeout: config.timeout,
..Default::default()
};
let test_suite = VaultE2ETestSuite::new(vault_config);
// Run tests with timeout
let test_result = tokio::time::timeout(
config.timeout,
test_suite.run_selected_tests(&config.test_categories)
).await;
match test_result {
Ok(results) => results,
Err(_) => Err(anyhow::anyhow!("Tests timed out after {:?}", config.timeout)),
}
}
/// Enhanced test suite with selective test execution
pub struct VaultE2ETestSuite {
config: VaultTestConfig,
}
impl VaultE2ETestSuite {
pub fn new(config: VaultTestConfig) -> Self {
Self { config }
}
/// Run selected test categories
pub async fn run_selected_tests(&self, categories: &[String]) -> Result<VaultTestResults> {
use std::sync::Arc;
use tokio::sync::RwLock;
let results = Arc::new(RwLock::new(VaultTestResults::new()));
info!("Running selected test categories: {:?}", categories);
// Add test metadata
{
let mut test_results = results.write().await;
test_results.add_metadata("test_started".to_string(), chrono::Utc::now().to_rfc3339());
test_results.add_metadata("vault_addr".to_string(), self.config.vault_addr.clone());
test_results.add_metadata("categories".to_string(), categories.join(","));
}
let should_run_all = categories.contains(&"all".to_string());
// Test 1: Docker environment setup (always required)
if should_run_all || categories.contains(&"docker".to_string()) || categories.contains(&"setup".to_string()) {
self.run_docker_setup_tests(&results).await?;
}
// Test 2: Vault connectivity
if should_run_all || categories.contains(&"connectivity".to_string()) || categories.contains(&"basic".to_string()) {
self.run_connectivity_tests(&results).await?;
}
// Test 3: Certificate lifecycle
if should_run_all || categories.contains(&"certificates".to_string()) || categories.contains(&"lifecycle".to_string()) {
self.run_certificate_tests(&results).await?;
}
// Test 4: Service integration
if should_run_all || categories.contains(&"services".to_string()) || categories.contains(&"integration".to_string()) {
self.run_service_integration_tests(&results).await?;
}
// Test 5: Failure scenarios
if should_run_all || categories.contains(&"failures".to_string()) || categories.contains(&"resilience".to_string()) {
self.run_failure_scenario_tests(&results).await?;
}
// Test 6: Performance validation
if should_run_all || categories.contains(&"performance".to_string()) || categories.contains(&"hft".to_string()) {
self.run_performance_tests(&results).await?;
}
// Test 7: Cleanup
if self.config.cleanup_after_tests && (should_run_all || categories.contains(&"cleanup".to_string())) {
self.run_cleanup_tests(&results).await?;
}
let final_results = results.read().await;
info!("Test execution completed: {}/{} tests passed",
final_results.success_count(), final_results.total_count());
Ok(final_results.clone())
}
// Individual test category methods (similar to the main implementation)
async fn run_docker_setup_tests(&self, results: &Arc<RwLock<VaultTestResults>>) -> Result<()> {
info!("Running Docker environment setup tests");
let test_start = Instant::now();
match DockerEnvironment::new(&self.config).await {
Ok(mut env) => {
if let Err(e) = env.start_all_services().await {
let mut test_results = results.write().await;
test_results.add_failure("docker_environment_startup", e.to_string());
return Err(e);
}
let mut test_results = results.write().await;
test_results.add_success("docker_environment_startup", test_start.elapsed());
Ok(())
}
Err(e) => {
let mut test_results = results.write().await;
test_results.add_failure("docker_environment_setup", e.to_string());
Err(e)
}
}
}
async fn run_connectivity_tests(&self, results: &Arc<RwLock<VaultTestResults>>) -> Result<()> {
info!("Running Vault connectivity tests");
match vault_connectivity_tests::run_connectivity_tests(&self.config).await {
Ok(duration) => {
let mut test_results = results.write().await;
test_results.add_success("vault_connectivity", duration);
Ok(())
}
Err(e) => {
let mut test_results = results.write().await;
test_results.add_failure("vault_connectivity", e.to_string());
Err(e)
}
}
}
async fn run_certificate_tests(&self, results: &Arc<RwLock<VaultTestResults>>) -> Result<()> {
certificate_lifecycle_tests::run_certificate_tests(&self.config, results).await
}
async fn run_service_integration_tests(&self, results: &Arc<RwLock<VaultTestResults>>) -> Result<()> {
service_integration_tests::run_service_tests(&self.config, results).await
}
async fn run_failure_scenario_tests(&self, results: &Arc<RwLock<VaultTestResults>>) -> Result<()> {
failure_scenario_tests::run_failure_tests(&self.config, results).await
}
async fn run_performance_tests(&self, results: &Arc<RwLock<VaultTestResults>>) -> Result<()> {
performance_impact_tests::run_performance_tests(&self.config, results).await
}
async fn run_cleanup_tests(&self, results: &Arc<RwLock<VaultTestResults>>) -> Result<()> {
let test_start = Instant::now();
if let Err(e) = DockerEnvironment::cleanup(&self.config).await {
warn!("Docker cleanup failed: {}", e);
}
let mut test_results = results.write().await;
test_results.add_success("cleanup", test_start.elapsed());
Ok(())
}
}
/// Generate comprehensive test reports
async fn generate_test_reports(results: &VaultTestResults, output_dir: &str) -> Result<()> {
info!("Generating test reports in: {}", output_dir);
// Generate JSON report
let json_report = serde_json::to_string_pretty(results)
.context("Failed to serialize test results to JSON")?;
let json_path = format!("{}/vault_e2e_results.json", output_dir);
tokio::fs::write(&json_path, json_report).await
.with_context(|| format!("Failed to write JSON report: {}", json_path))?;
// Generate text report
let text_report = results.generate_report();
let text_path = format!("{}/vault_e2e_results.txt", output_dir);
tokio::fs::write(&text_path, text_report).await
.with_context(|| format!("Failed to write text report: {}", text_path))?;
// Generate HTML report (simplified)
let html_report = generate_html_report(results);
let html_path = format!("{}/vault_e2e_results.html", output_dir);
tokio::fs::write(&html_path, html_report).await
.with_context(|| format!("Failed to write HTML report: {}", html_path))?;
info!("Test reports generated:");
info!(" JSON: {}", json_path);
info!(" Text: {}", text_path);
info!(" HTML: {}", html_path);
Ok(())
}
/// Generate HTML test report
fn generate_html_report(results: &VaultTestResults) -> String {
let success_rate = results.success_rate() * 100.0;
let status_color = if success_rate >= 95.0 { "green" } else if success_rate >= 80.0 { "orange" } else { "red" };
format!(r#"<!DOCTYPE html>
<html>
<head>
<title>Vault E2E Test Results</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 40px; }}
.header {{ background: #f5f5f5; padding: 20px; border-radius: 5px; }}
.success {{ color: green; }}
.failure {{ color: red; }}
.metric {{ margin: 10px 0; }}
.performance {{ background: #f0f8ff; padding: 15px; border-radius: 5px; margin: 20px 0; }}
table {{ border-collapse: collapse; width: 100%; margin: 20px 0; }}
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
th {{ background-color: #f2f2f2; }}
</style>
</head>
<body>
<div class="header">
<h1>Vault E2E Integration Test Results</h1>
<p><strong>Success Rate:</strong> <span style="color: {status_color}">{success_rate:.1}%</span></p>
<p><strong>Total Tests:</strong> {total}</p>
<p><strong>Passed:</strong> <span class="success">{passed}</span></p>
<p><strong>Failed:</strong> <span class="failure">{failed}</span></p>
</div>
<div class="performance">
<h2>Performance Metrics</h2>
<div class="metric"><strong>Certificate Generation:</strong> {cert_time}</div>
<div class="metric"><strong>Cache Lookup:</strong> {cache_time}</div>
<div class="metric"><strong>TLS Handshake:</strong> {tls_time}</div>
<div class="metric"><strong>Memory Usage:</strong> {memory}</div>
<div class="metric"><strong>Cache Hit Rate:</strong> {hit_rate}</div>
</div>
<h2>Test Results Details</h2>
<table>
<tr><th>Test Name</th><th>Status</th><th>Duration</th><th>Details</th></tr>
{test_rows}
</table>
</body>
</html>"#,
status_color = status_color,
success_rate = success_rate,
total = results.total_count(),
passed = results.success_count(),
failed = results.failure_count(),
cert_time = results.performance.cert_generation_time.map(|d| format!("{:?}", d)).unwrap_or("N/A".to_string()),
cache_time = results.performance.cache_lookup_time.map(|d| format!("{:?}", d)).unwrap_or("N/A".to_string()),
tls_time = results.performance.tls_handshake_time.map(|d| format!("{:?}", d)).unwrap_or("N/A".to_string()),
memory = results.performance.memory_usage_mb.map(|m| format!("{:.1} MB", m)).unwrap_or("N/A".to_string()),
hit_rate = results.performance.cache_hit_rate.map(|r| format!("{:.1}%", r)).unwrap_or("N/A".to_string()),
test_rows = generate_test_table_rows(results),
)
}
fn generate_test_table_rows(results: &VaultTestResults) -> String {
let mut rows = String::new();
for (name, duration) in &results.successes {
rows.push_str(&format!(
"<tr><td>{}</td><td class=\"success\">PASSED</td><td>{:?}</td><td>-</td></tr>",
name, duration
));
}
for (name, error) in &results.failures {
rows.push_str(&format!(
"<tr><td>{}</td><td class=\"failure\">FAILED</td><td>-</td><td>{}</td></tr>",
name, error
));
}
rows
}
// Re-export the types and functions from the main module
// Types are now defined directly in this file