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>
411 lines
13 KiB
Rust
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);
|
|
}
|
|
} |