Files
foxhunt/crates/common/tests/traits_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

620 lines
17 KiB
Rust

//! Comprehensive tests for traits module
//!
//! Tests cover:
//! - HealthStatus and DetailedHealth structures
//! - RateLimitStatus structure
//! - Trait implementations with mock types
//! - Serialization/deserialization
use chrono::Utc;
use common::traits::{DetailedHealth, HealthStatus, RateLimitStatus};
use common::types::ServiceStatus;
use std::collections::HashMap;
// ============================================================================
// HealthStatus Tests
// ============================================================================
#[test]
fn test_health_status_creation() {
let timestamp = Utc::now();
let status = HealthStatus {
status: ServiceStatus::Running,
timestamp,
message: Some("All systems operational".to_string()),
};
assert_eq!(status.status, ServiceStatus::Running);
assert_eq!(status.timestamp, timestamp);
assert_eq!(status.message, Some("All systems operational".to_string()));
}
#[test]
fn test_health_status_without_message() {
let timestamp = Utc::now();
let status = HealthStatus {
status: ServiceStatus::Running,
timestamp,
message: None,
};
assert_eq!(status.status, ServiceStatus::Running);
assert!(status.message.is_none());
}
#[test]
fn test_health_status_degraded() {
let status = HealthStatus {
status: ServiceStatus::Degraded,
timestamp: Utc::now(),
message: Some("Performance degraded".to_string()),
};
assert_eq!(status.status, ServiceStatus::Degraded);
assert!(status.message.is_some());
}
#[test]
fn test_health_status_unhealthy() {
let status = HealthStatus {
status: ServiceStatus::Stopped,
timestamp: Utc::now(),
message: Some("Service unavailable".to_string()),
};
assert_eq!(status.status, ServiceStatus::Stopped);
}
#[test]
fn test_health_status_serialization() {
let status = HealthStatus {
status: ServiceStatus::Running,
timestamp: Utc::now(),
message: Some("OK".to_string()),
};
let json = serde_json::to_string(&status).expect("Failed to serialize");
let deserialized: HealthStatus = serde_json::from_str(&json).expect("Failed to deserialize");
assert_eq!(deserialized.status, status.status);
assert_eq!(deserialized.message, status.message);
}
#[test]
fn test_health_status_clone() {
let status = HealthStatus {
status: ServiceStatus::Running,
timestamp: Utc::now(),
message: Some("Test".to_string()),
};
let cloned = status.clone();
assert_eq!(cloned.status, status.status);
assert_eq!(cloned.message, status.message);
}
// ============================================================================
// DetailedHealth Tests
// ============================================================================
#[test]
fn test_detailed_health_creation() {
let timestamp = Utc::now();
let base_status = HealthStatus {
status: ServiceStatus::Running,
timestamp,
message: None,
};
let mut metrics = HashMap::new();
metrics.insert("cpu_usage".to_string(), 45.5);
metrics.insert("memory_usage".to_string(), 60.2);
let mut components = HashMap::new();
components.insert(
"database".to_string(),
HealthStatus {
status: ServiceStatus::Running,
timestamp,
message: Some("DB OK".to_string()),
},
);
let detailed = DetailedHealth {
status: base_status.clone(),
metrics: metrics.clone(),
components: components.clone(),
};
assert_eq!(detailed.status.status, ServiceStatus::Running);
assert_eq!(detailed.metrics.len(), 2);
assert_eq!(detailed.components.len(), 1);
assert_eq!(detailed.metrics.get("cpu_usage"), Some(&45.5));
}
#[test]
fn test_detailed_health_empty_metrics() {
let detailed = DetailedHealth {
status: HealthStatus {
status: ServiceStatus::Running,
timestamp: Utc::now(),
message: None,
},
metrics: HashMap::new(),
components: HashMap::new(),
};
assert!(detailed.metrics.is_empty());
assert!(detailed.components.is_empty());
}
#[test]
fn test_detailed_health_multiple_components() {
let timestamp = Utc::now();
let mut components = HashMap::new();
components.insert(
"database".to_string(),
HealthStatus {
status: ServiceStatus::Running,
timestamp,
message: Some("DB OK".to_string()),
},
);
components.insert(
"cache".to_string(),
HealthStatus {
status: ServiceStatus::Degraded,
timestamp,
message: Some("Cache slow".to_string()),
},
);
components.insert(
"queue".to_string(),
HealthStatus {
status: ServiceStatus::Running,
timestamp,
message: Some("Queue OK".to_string()),
},
);
let detailed = DetailedHealth {
status: HealthStatus {
status: ServiceStatus::Degraded, // Overall status reflects degraded component
timestamp,
message: Some("Some components degraded".to_string()),
},
metrics: HashMap::new(),
components,
};
assert_eq!(detailed.components.len(), 3);
assert_eq!(detailed.status.status, ServiceStatus::Degraded);
let cache_status = detailed.components.get("cache").unwrap();
assert_eq!(cache_status.status, ServiceStatus::Degraded);
}
#[test]
fn test_detailed_health_serialization() {
let mut metrics = HashMap::new();
metrics.insert("latency_ms".to_string(), 12.5);
let detailed = DetailedHealth {
status: HealthStatus {
status: ServiceStatus::Running,
timestamp: Utc::now(),
message: None,
},
metrics,
components: HashMap::new(),
};
let json = serde_json::to_string(&detailed).expect("Failed to serialize");
let deserialized: DetailedHealth = serde_json::from_str(&json).expect("Failed to deserialize");
assert_eq!(deserialized.status.status, ServiceStatus::Running);
assert_eq!(deserialized.metrics.len(), 1);
assert_eq!(deserialized.metrics.get("latency_ms"), Some(&12.5));
}
#[test]
fn test_detailed_health_clone() {
let mut metrics = HashMap::new();
metrics.insert("test_metric".to_string(), 100.0);
let detailed = DetailedHealth {
status: HealthStatus {
status: ServiceStatus::Running,
timestamp: Utc::now(),
message: None,
},
metrics,
components: HashMap::new(),
};
let cloned = detailed.clone();
assert_eq!(cloned.metrics.len(), detailed.metrics.len());
assert_eq!(cloned.status.status, detailed.status.status);
}
// ============================================================================
// RateLimitStatus Tests
// ============================================================================
#[test]
fn test_rate_limit_status_creation() {
let status = RateLimitStatus {
current_count: 50,
max_requests: 100,
window_seconds: 60,
reset_in_seconds: 45,
};
assert_eq!(status.current_count, 50);
assert_eq!(status.max_requests, 100);
assert_eq!(status.window_seconds, 60);
assert_eq!(status.reset_in_seconds, 45);
}
#[test]
fn test_rate_limit_status_at_limit() {
let status = RateLimitStatus {
current_count: 100,
max_requests: 100,
window_seconds: 60,
reset_in_seconds: 30,
};
assert_eq!(status.current_count, status.max_requests);
}
#[test]
fn test_rate_limit_status_under_limit() {
let status = RateLimitStatus {
current_count: 25,
max_requests: 100,
window_seconds: 60,
reset_in_seconds: 55,
};
assert!(status.current_count < status.max_requests);
}
#[test]
fn test_rate_limit_status_zero_count() {
let status = RateLimitStatus {
current_count: 0,
max_requests: 100,
window_seconds: 60,
reset_in_seconds: 60,
};
assert_eq!(status.current_count, 0);
}
#[test]
fn test_rate_limit_status_about_to_reset() {
let status = RateLimitStatus {
current_count: 90,
max_requests: 100,
window_seconds: 60,
reset_in_seconds: 1, // About to reset
};
assert_eq!(status.reset_in_seconds, 1);
}
#[test]
fn test_rate_limit_status_serialization() {
let status = RateLimitStatus {
current_count: 75,
max_requests: 100,
window_seconds: 60,
reset_in_seconds: 30,
};
let json = serde_json::to_string(&status).expect("Failed to serialize");
let deserialized: RateLimitStatus = serde_json::from_str(&json).expect("Failed to deserialize");
assert_eq!(deserialized.current_count, status.current_count);
assert_eq!(deserialized.max_requests, status.max_requests);
assert_eq!(deserialized.window_seconds, status.window_seconds);
assert_eq!(deserialized.reset_in_seconds, status.reset_in_seconds);
}
#[test]
fn test_rate_limit_status_clone() {
let status = RateLimitStatus {
current_count: 50,
max_requests: 100,
window_seconds: 60,
reset_in_seconds: 45,
};
let cloned = status.clone();
assert_eq!(cloned.current_count, status.current_count);
assert_eq!(cloned.max_requests, status.max_requests);
}
#[test]
fn test_rate_limit_status_utilization_calculation() {
let status = RateLimitStatus {
current_count: 75,
max_requests: 100,
window_seconds: 60,
reset_in_seconds: 30,
};
// Calculate utilization percentage
let utilization = (status.current_count as f64 / status.max_requests as f64) * 100.0;
assert_eq!(utilization, 75.0);
}
#[test]
fn test_rate_limit_status_remaining_requests() {
let status = RateLimitStatus {
current_count: 40,
max_requests: 100,
window_seconds: 60,
reset_in_seconds: 20,
};
let remaining = status.max_requests - status.current_count;
assert_eq!(remaining, 60);
}
// ============================================================================
// Mock Trait Implementations for Testing
// ============================================================================
// ============================================================================
// Trait Default Implementation Tests
// ============================================================================
#[test]
fn test_reloadable_default_supports_reload() {
use async_trait::async_trait;
use common::traits::Reloadable;
struct TestReloadable;
#[async_trait]
impl Reloadable for TestReloadable {
async fn reload(&mut self) -> common::error::CommonResult<()> {
Ok(())
}
// uses default supports_reload implementation
}
let component = TestReloadable;
assert!(component.supports_reload());
}
#[test]
fn test_graceful_shutdown_default_timeout() {
use async_trait::async_trait;
use common::traits::GracefulShutdown;
struct TestShutdown;
#[async_trait]
impl GracefulShutdown for TestShutdown {
async fn shutdown(&mut self) -> common::error::CommonResult<()> {
Ok(())
}
async fn force_shutdown(&mut self) -> common::error::CommonResult<()> {
Ok(())
}
// uses default shutdown_timeout_seconds implementation
}
let component = TestShutdown;
assert_eq!(component.shutdown_timeout_seconds(), 30);
}
#[cfg(test)]
mod mock_implementations {
use super::*;
use async_trait::async_trait;
use common::error::CommonResult;
use common::traits::{CircuitBreaker, Configurable, HealthCheck, RateLimited};
#[derive(Clone)]
struct MockConfig {
value: String,
}
struct MockComponent {
config: MockConfig,
is_healthy: bool,
circuit_open: bool,
failure_count: u64,
request_count: u64,
}
#[async_trait]
impl Configurable for MockComponent {
type Config = MockConfig;
async fn configure(&mut self, config: Self::Config) -> CommonResult<()> {
self.config = config;
Ok(())
}
fn get_config(&self) -> &Self::Config {
&self.config
}
fn validate_config(_config: &Self::Config) -> CommonResult<()> {
Ok(())
}
}
#[async_trait]
impl HealthCheck for MockComponent {
async fn health_check(&self) -> CommonResult<HealthStatus> {
Ok(HealthStatus {
status: if self.is_healthy {
ServiceStatus::Running
} else {
ServiceStatus::Stopped
},
timestamp: Utc::now(),
message: None,
})
}
async fn detailed_health(&self) -> CommonResult<DetailedHealth> {
let mut metrics = HashMap::new();
metrics.insert("request_count".to_string(), self.request_count as f64);
Ok(DetailedHealth {
status: self.health_check().await?,
metrics,
components: HashMap::new(),
})
}
}
impl CircuitBreaker for MockComponent {
fn is_circuit_open(&self) -> bool {
self.circuit_open
}
fn failure_count(&self) -> u64 {
self.failure_count
}
fn reset_circuit(&mut self) {
self.circuit_open = false;
self.failure_count = 0;
}
}
impl RateLimited for MockComponent {
fn is_allowed(&self) -> bool {
self.request_count < 100
}
fn rate_limit_status(&self) -> RateLimitStatus {
RateLimitStatus {
current_count: self.request_count,
max_requests: 100,
window_seconds: 60,
reset_in_seconds: 30,
}
}
}
#[tokio::test]
async fn test_mock_configurable() {
let mut component = MockComponent {
config: MockConfig {
value: "initial".to_string(),
},
is_healthy: true,
circuit_open: false,
failure_count: 0,
request_count: 0,
};
let new_config = MockConfig {
value: "updated".to_string(),
};
component.configure(new_config.clone()).await.unwrap();
assert_eq!(component.get_config().value, "updated");
}
#[tokio::test]
async fn test_mock_health_check() {
let component = MockComponent {
config: MockConfig {
value: "test".to_string(),
},
is_healthy: true,
circuit_open: false,
failure_count: 0,
request_count: 0,
};
let health = component.health_check().await.unwrap();
assert_eq!(health.status, ServiceStatus::Running);
}
#[tokio::test]
async fn test_mock_detailed_health() {
let component = MockComponent {
config: MockConfig {
value: "test".to_string(),
},
is_healthy: true,
circuit_open: false,
failure_count: 0,
request_count: 42,
};
let detailed = component.detailed_health().await.unwrap();
assert_eq!(detailed.status.status, ServiceStatus::Running);
assert_eq!(detailed.metrics.get("request_count"), Some(&42.0));
}
#[test]
fn test_mock_circuit_breaker() {
let mut component = MockComponent {
config: MockConfig {
value: "test".to_string(),
},
is_healthy: true,
circuit_open: true,
failure_count: 5,
request_count: 0,
};
assert!(component.is_circuit_open());
assert_eq!(component.failure_count(), 5);
component.reset_circuit();
assert!(!component.is_circuit_open());
assert_eq!(component.failure_count(), 0);
}
#[test]
fn test_mock_rate_limited() {
let component = MockComponent {
config: MockConfig {
value: "test".to_string(),
},
is_healthy: true,
circuit_open: false,
failure_count: 0,
request_count: 50,
};
assert!(component.is_allowed());
let status = component.rate_limit_status();
assert_eq!(status.current_count, 50);
assert_eq!(status.max_requests, 100);
}
#[test]
fn test_mock_rate_limited_at_limit() {
let component = MockComponent {
config: MockConfig {
value: "test".to_string(),
},
is_healthy: true,
circuit_open: false,
failure_count: 0,
request_count: 100,
};
assert!(!component.is_allowed());
}
}