diff --git a/crates/challenge-registry/src/error.rs b/crates/challenge-registry/src/error.rs index 369db73..8bc5c70 100644 --- a/crates/challenge-registry/src/error.rs +++ b/crates/challenge-registry/src/error.rs @@ -59,3 +59,188 @@ impl From for RegistryError { RegistryError::Serialization(err.to_string()) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::io::{Error as IoError, ErrorKind}; + + #[test] + fn test_registry_error_display_challenge_not_found() { + let err = RegistryError::ChallengeNotFound("test-challenge".to_string()); + assert_eq!(err.to_string(), "Challenge not found: test-challenge"); + } + + #[test] + fn test_registry_error_display_already_registered() { + let err = RegistryError::AlreadyRegistered("my-challenge".to_string()); + assert_eq!(err.to_string(), "Challenge already registered: my-challenge"); + } + + #[test] + fn test_registry_error_display_version_conflict() { + let err = RegistryError::VersionConflict("v1.0.0 vs v2.0.0".to_string()); + assert_eq!(err.to_string(), "Version conflict: v1.0.0 vs v2.0.0"); + } + + #[test] + fn test_registry_error_display_all_variants() { + let test_cases = vec![ + ( + RegistryError::ChallengeNotFound("challenge-id".to_string()), + "Challenge not found: challenge-id", + ), + ( + RegistryError::AlreadyRegistered("existing".to_string()), + "Challenge already registered: existing", + ), + ( + RegistryError::VersionConflict("mismatch".to_string()), + "Version conflict: mismatch", + ), + ( + RegistryError::MigrationFailed("migration error".to_string()), + "Migration failed: migration error", + ), + ( + RegistryError::HealthCheckFailed("health issue".to_string()), + "Health check failed: health issue", + ), + ( + RegistryError::StatePersistence("persist error".to_string()), + "State persistence error: persist error", + ), + ( + RegistryError::StateRestoration("restore error".to_string()), + "State restoration error: restore error", + ), + ( + RegistryError::InvalidConfig("bad config".to_string()), + "Invalid challenge configuration: bad config", + ), + ( + RegistryError::Serialization("serde error".to_string()), + "Serialization error: serde error", + ), + ( + RegistryError::Network("connection refused".to_string()), + "Network error: connection refused", + ), + ( + RegistryError::Internal("unexpected".to_string()), + "Internal error: unexpected", + ), + ]; + + for (error, expected_message) in test_cases { + assert_eq!( + error.to_string(), + expected_message, + "Display mismatch for {:?}", + error + ); + } + } + + #[test] + fn test_from_io_error() { + let io_err = IoError::new(ErrorKind::NotFound, "file not found"); + let registry_err: RegistryError = io_err.into(); + + match registry_err { + RegistryError::Internal(msg) => { + assert!( + msg.contains("file not found"), + "Expected message to contain 'file not found', got: {}", + msg + ); + } + other => panic!("Expected Internal variant, got: {:?}", other), + } + } + + #[test] + fn test_from_serde_json_error() { + // Create an invalid JSON to trigger a parse error + let invalid_json = "{ invalid json }"; + let serde_err = serde_json::from_str::(invalid_json).unwrap_err(); + let registry_err: RegistryError = serde_err.into(); + + match registry_err { + RegistryError::Serialization(msg) => { + assert!( + !msg.is_empty(), + "Serialization error message should not be empty" + ); + } + other => panic!("Expected Serialization variant, got: {:?}", other), + } + } + + #[test] + fn test_from_bincode_error() { + // Create invalid bincode data to trigger an error + let invalid_data: &[u8] = &[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]; + let bincode_err: bincode::Error = + bincode::deserialize::(invalid_data).unwrap_err(); + let registry_err: RegistryError = bincode_err.into(); + + match registry_err { + RegistryError::Serialization(msg) => { + assert!( + !msg.is_empty(), + "Serialization error message should not be empty" + ); + } + other => panic!("Expected Serialization variant, got: {:?}", other), + } + } + + #[test] + fn test_registry_result_type() { + // Test that RegistryResult works as expected with Ok + fn returns_ok() -> RegistryResult { + Ok(42) + } + assert_eq!(returns_ok().unwrap(), 42); + + // Test that RegistryResult works as expected with Err + fn returns_err() -> RegistryResult { + Err(RegistryError::Internal("test error".to_string())) + } + assert!(returns_err().is_err()); + + // Test with different types + fn returns_string() -> RegistryResult { + Ok("success".to_string()) + } + assert_eq!(returns_string().unwrap(), "success"); + } + + #[test] + fn test_error_debug_impl() { + let err = RegistryError::ChallengeNotFound("debug-test".to_string()); + let debug_str = format!("{:?}", err); + + // Debug format should contain the variant name and the inner value + assert!( + debug_str.contains("ChallengeNotFound"), + "Debug should contain variant name, got: {}", + debug_str + ); + assert!( + debug_str.contains("debug-test"), + "Debug should contain inner value, got: {}", + debug_str + ); + + // Test debug for another variant + let err2 = RegistryError::Network("connection timeout".to_string()); + let debug_str2 = format!("{:?}", err2); + assert!( + debug_str2.contains("Network"), + "Debug should contain variant name, got: {}", + debug_str2 + ); + } +} diff --git a/crates/challenge-registry/src/health.rs b/crates/challenge-registry/src/health.rs index 5973271..9de50cf 100644 --- a/crates/challenge-registry/src/health.rs +++ b/crates/challenge-registry/src/health.rs @@ -259,4 +259,182 @@ mod tests { // 100 * 0.8 + 200 * 0.2 = 80 + 40 = 120 assert!((health.avg_response_time_ms - 120.0).abs() < 0.01); } + + #[test] + fn test_health_status_default() { + let status = HealthStatus::default(); + assert_eq!(status, HealthStatus::Unknown); + } + + #[test] + fn test_challenge_health_new() { + let challenge_id = ChallengeId::new(); + let health = ChallengeHealth::new(challenge_id); + + assert_eq!(health.challenge_id, challenge_id); + assert_eq!(health.status, HealthStatus::Unknown); + assert_eq!(health.last_check_at, 0); + assert_eq!(health.consecutive_failures, 0); + assert_eq!(health.avg_response_time_ms, 0.0); + assert!(health.metrics.is_empty()); + } + + #[test] + fn test_challenge_health_metrics() { + let mut health = ChallengeHealth::new(ChallengeId::new()); + + health.metrics.insert("cpu_usage".to_string(), 45.5); + health.metrics.insert("memory_mb".to_string(), 512.0); + health.metrics.insert("requests_per_sec".to_string(), 1000.0); + + assert_eq!(health.metrics.len(), 3); + assert_eq!(health.metrics.get("cpu_usage"), Some(&45.5)); + assert_eq!(health.metrics.get("memory_mb"), Some(&512.0)); + assert_eq!(health.metrics.get("requests_per_sec"), Some(&1000.0)); + assert_eq!(health.metrics.get("nonexistent"), None); + } + + #[test] + fn test_health_config_default() { + let config = HealthConfig::default(); + + assert_eq!(config.check_interval, Duration::from_secs(30)); + assert_eq!(config.check_timeout, Duration::from_secs(5)); + assert_eq!(config.failure_threshold, 3); + assert_eq!(config.recovery_threshold, 2); + } + + #[test] + fn test_health_config_custom() { + let config = HealthConfig { + check_interval: Duration::from_secs(60), + check_timeout: Duration::from_secs(10), + failure_threshold: 5, + recovery_threshold: 3, + }; + + assert_eq!(config.check_interval, Duration::from_secs(60)); + assert_eq!(config.check_timeout, Duration::from_secs(10)); + assert_eq!(config.failure_threshold, 5); + assert_eq!(config.recovery_threshold, 3); + } + + #[test] + fn test_health_monitor_with_config() { + let config = HealthConfig { + check_interval: Duration::from_secs(120), + check_timeout: Duration::from_secs(15), + failure_threshold: 10, + recovery_threshold: 5, + }; + + let monitor = HealthMonitor::with_config(config); + let monitor_config = monitor.config(); + + assert_eq!(monitor_config.check_interval, Duration::from_secs(120)); + assert_eq!(monitor_config.check_timeout, Duration::from_secs(15)); + assert_eq!(monitor_config.failure_threshold, 10); + assert_eq!(monitor_config.recovery_threshold, 5); + } + + #[test] + fn test_health_monitor_get_all_health() { + let monitor = HealthMonitor::new(); + let id1 = ChallengeId::new(); + let id2 = ChallengeId::new(); + let id3 = ChallengeId::new(); + + assert!(monitor.get_all_health().is_empty()); + + monitor.register(id1); + monitor.register(id2); + monitor.register(id3); + + let all_health = monitor.get_all_health(); + assert_eq!(all_health.len(), 3); + + let ids: Vec = all_health.iter().map(|h| h.challenge_id).collect(); + assert!(ids.contains(&id1)); + assert!(ids.contains(&id2)); + assert!(ids.contains(&id3)); + } + + #[test] + fn test_health_monitor_update_health() { + let monitor = HealthMonitor::new(); + let id = ChallengeId::new(); + + monitor.register(id); + let health = monitor.get_health(&id).expect("should be registered"); + assert_eq!(health.status, HealthStatus::Unknown); + + monitor.update_health(&id, HealthStatus::Healthy); + let health = monitor.get_health(&id).expect("should be registered"); + assert_eq!(health.status, HealthStatus::Healthy); + assert!(health.last_check_at > 0); + + monitor.update_health(&id, HealthStatus::Degraded("high latency".to_string())); + let health = monitor.get_health(&id).expect("should be registered"); + assert_eq!( + health.status, + HealthStatus::Degraded("high latency".to_string()) + ); + + monitor.update_health(&id, HealthStatus::Unhealthy("connection lost".to_string())); + let health = monitor.get_health(&id).expect("should be registered"); + assert_eq!( + health.status, + HealthStatus::Unhealthy("connection lost".to_string()) + ); + } + + #[test] + fn test_health_status_variants() { + let unknown = HealthStatus::Unknown; + let healthy = HealthStatus::Healthy; + let degraded = HealthStatus::Degraded("slow response".to_string()); + let unhealthy = HealthStatus::Unhealthy("service down".to_string()); + + assert_eq!(unknown, HealthStatus::Unknown); + assert_eq!(healthy, HealthStatus::Healthy); + assert_eq!( + degraded, + HealthStatus::Degraded("slow response".to_string()) + ); + assert_eq!( + unhealthy, + HealthStatus::Unhealthy("service down".to_string()) + ); + + assert_ne!(unknown, healthy); + assert_ne!(healthy, degraded); + assert_ne!(degraded, unhealthy); + + let degraded_clone = degraded.clone(); + assert_eq!(degraded, degraded_clone); + } + + #[test] + fn test_challenge_health_consecutive_successes() { + let mut health = ChallengeHealth::new(ChallengeId::new()); + + health.record_failure("error 1".to_string()); + health.record_failure("error 2".to_string()); + assert_eq!(health.consecutive_failures, 2); + assert!(matches!(health.status, HealthStatus::Degraded(_))); + + health.record_success(50.0); + assert_eq!(health.consecutive_failures, 0); + assert_eq!(health.status, HealthStatus::Healthy); + + health.record_failure("error 3".to_string()); + health.record_failure("error 4".to_string()); + health.record_failure("error 5".to_string()); + assert_eq!(health.consecutive_failures, 3); + assert!(matches!(health.status, HealthStatus::Unhealthy(_))); + + health.record_success(75.0); + assert_eq!(health.consecutive_failures, 0); + assert_eq!(health.status, HealthStatus::Healthy); + } } diff --git a/crates/challenge-registry/src/lifecycle.rs b/crates/challenge-registry/src/lifecycle.rs index 8e99e44..a90d964 100644 --- a/crates/challenge-registry/src/lifecycle.rs +++ b/crates/challenge-registry/src/lifecycle.rs @@ -162,4 +162,225 @@ mod tests { assert!(!lifecycle.auto_restart_enabled()); assert_eq!(lifecycle.max_restart_attempts(), 5); } + + #[test] + fn test_lifecycle_state_default() { + let state = LifecycleState::default(); + assert_eq!(state, LifecycleState::Registered); + } + + #[test] + fn test_lifecycle_default() { + let default_lifecycle = ChallengeLifecycle::default(); + let new_lifecycle = ChallengeLifecycle::new(); + + assert_eq!( + default_lifecycle.auto_restart_enabled(), + new_lifecycle.auto_restart_enabled() + ); + assert_eq!( + default_lifecycle.max_restart_attempts(), + new_lifecycle.max_restart_attempts() + ); + } + + #[test] + fn test_all_valid_transition_paths() { + let lifecycle = ChallengeLifecycle::new(); + + // From Registered + assert!( + lifecycle.is_valid_transition(&LifecycleState::Registered, &LifecycleState::Starting) + ); + assert!( + lifecycle.is_valid_transition(&LifecycleState::Registered, &LifecycleState::Stopped) + ); + + // From Starting + assert!(lifecycle.is_valid_transition(&LifecycleState::Starting, &LifecycleState::Running)); + assert!(lifecycle.is_valid_transition( + &LifecycleState::Starting, + &LifecycleState::Failed("error".to_string()) + )); + + // From Running + assert!(lifecycle.is_valid_transition(&LifecycleState::Running, &LifecycleState::Stopping)); + assert!(lifecycle.is_valid_transition( + &LifecycleState::Running, + &LifecycleState::Failed("crash".to_string()) + )); + assert!( + lifecycle.is_valid_transition(&LifecycleState::Running, &LifecycleState::Migrating) + ); + + // From Stopping + assert!(lifecycle.is_valid_transition(&LifecycleState::Stopping, &LifecycleState::Stopped)); + + // From Stopped + assert!(lifecycle.is_valid_transition(&LifecycleState::Stopped, &LifecycleState::Starting)); + assert!( + lifecycle.is_valid_transition(&LifecycleState::Stopped, &LifecycleState::Registered) + ); + + // From Failed + assert!(lifecycle.is_valid_transition( + &LifecycleState::Failed("any error".to_string()), + &LifecycleState::Starting + )); + assert!(lifecycle.is_valid_transition( + &LifecycleState::Failed("any error".to_string()), + &LifecycleState::Stopped + )); + + // From Migrating + assert!( + lifecycle.is_valid_transition(&LifecycleState::Migrating, &LifecycleState::Running) + ); + assert!(lifecycle.is_valid_transition( + &LifecycleState::Migrating, + &LifecycleState::Failed("migration failed".to_string()) + )); + } + + #[test] + fn test_failed_state_with_message() { + let error_message = "Connection timeout after 30s".to_string(); + let failed_state = LifecycleState::Failed(error_message.clone()); + + match failed_state { + LifecycleState::Failed(msg) => { + assert_eq!(msg, error_message); + } + _ => panic!("Expected Failed state"), + } + } + + #[test] + fn test_lifecycle_event_variants() { + let challenge_id = ChallengeId::new(); + + // Test Registered event + let registered_event = LifecycleEvent::Registered { + challenge_id: challenge_id.clone(), + }; + match registered_event { + LifecycleEvent::Registered { challenge_id: id } => { + assert_eq!(id, challenge_id); + } + _ => panic!("Expected Registered event"), + } + + // Test Unregistered event + let unregistered_event = LifecycleEvent::Unregistered { + challenge_id: challenge_id.clone(), + }; + match unregistered_event { + LifecycleEvent::Unregistered { challenge_id: id } => { + assert_eq!(id, challenge_id); + } + _ => panic!("Expected Unregistered event"), + } + + // Test StateChanged event + let state_changed_event = LifecycleEvent::StateChanged { + challenge_id: challenge_id.clone(), + old_state: LifecycleState::Registered, + new_state: LifecycleState::Starting, + }; + match state_changed_event { + LifecycleEvent::StateChanged { + challenge_id: id, + old_state, + new_state, + } => { + assert_eq!(id, challenge_id); + assert_eq!(old_state, LifecycleState::Registered); + assert_eq!(new_state, LifecycleState::Starting); + } + _ => panic!("Expected StateChanged event"), + } + + // Test VersionChanged event + let old_version = ChallengeVersion::new(1, 0, 0); + let new_version = ChallengeVersion::new(1, 1, 0); + let version_changed_event = LifecycleEvent::VersionChanged { + challenge_id: challenge_id.clone(), + old_version: old_version.clone(), + new_version: new_version.clone(), + }; + match version_changed_event { + LifecycleEvent::VersionChanged { + challenge_id: id, + old_version: old_v, + new_version: new_v, + } => { + assert_eq!(id, challenge_id); + assert_eq!(old_v, old_version); + assert_eq!(new_v, new_version); + } + _ => panic!("Expected VersionChanged event"), + } + } + + #[test] + fn test_auto_restart_default_values() { + let lifecycle = ChallengeLifecycle::new(); + + assert!(lifecycle.auto_restart_enabled()); + assert_eq!(lifecycle.max_restart_attempts(), 3); + } + + #[test] + fn test_with_auto_restart_builder() { + // Test disabling auto-restart + let lifecycle_disabled = ChallengeLifecycle::new().with_auto_restart(false, 0); + assert!(!lifecycle_disabled.auto_restart_enabled()); + assert_eq!(lifecycle_disabled.max_restart_attempts(), 0); + + // Test custom max attempts + let lifecycle_custom = ChallengeLifecycle::new().with_auto_restart(true, 10); + assert!(lifecycle_custom.auto_restart_enabled()); + assert_eq!(lifecycle_custom.max_restart_attempts(), 10); + + // Test chaining after default + let lifecycle_chained = ChallengeLifecycle::default().with_auto_restart(false, 1); + assert!(!lifecycle_chained.auto_restart_enabled()); + assert_eq!(lifecycle_chained.max_restart_attempts(), 1); + } + + #[test] + fn test_migrating_transitions() { + let lifecycle = ChallengeLifecycle::new(); + + // Valid: Running -> Migrating + assert!( + lifecycle.is_valid_transition(&LifecycleState::Running, &LifecycleState::Migrating) + ); + + // Valid: Migrating -> Running (successful migration) + assert!( + lifecycle.is_valid_transition(&LifecycleState::Migrating, &LifecycleState::Running) + ); + + // Valid: Migrating -> Failed (migration failed) + assert!(lifecycle.is_valid_transition( + &LifecycleState::Migrating, + &LifecycleState::Failed("migration error".to_string()) + )); + + // Invalid: Migrating -> Stopped (must go through Failed first) + assert!( + !lifecycle.is_valid_transition(&LifecycleState::Migrating, &LifecycleState::Stopped) + ); + + // Invalid: Migrating -> Starting + assert!( + !lifecycle.is_valid_transition(&LifecycleState::Migrating, &LifecycleState::Starting) + ); + + // Invalid: Registered -> Migrating (can't migrate without running first) + assert!( + !lifecycle.is_valid_transition(&LifecycleState::Registered, &LifecycleState::Migrating) + ); + } } diff --git a/crates/challenge-registry/src/version.rs b/crates/challenge-registry/src/version.rs index d325c56..9c17d30 100644 --- a/crates/challenge-registry/src/version.rs +++ b/crates/challenge-registry/src/version.rs @@ -127,6 +127,7 @@ pub struct VersionedChallenge { #[cfg(test)] mod tests { use super::*; + use std::collections::HashMap; #[test] fn test_version_parsing() { @@ -161,4 +162,235 @@ mod tests { assert!(!VersionConstraint::Exact(ChallengeVersion::new(1, 0, 0)).satisfies(&v)); assert!(VersionConstraint::Compatible(ChallengeVersion::new(1, 0, 0)).satisfies(&v)); } + + #[test] + fn test_version_default() { + let v = ChallengeVersion::default(); + assert_eq!(v.major, 0); + assert_eq!(v.minor, 1); + assert_eq!(v.patch, 0); + assert_eq!(v.prerelease, None); + } + + #[test] + fn test_version_display() { + let v1 = ChallengeVersion::new(1, 2, 3); + assert_eq!(format!("{}", v1), "1.2.3"); + + let v2 = ChallengeVersion { + major: 2, + minor: 0, + patch: 0, + prerelease: Some("alpha".to_string()), + }; + assert_eq!(format!("{}", v2), "2.0.0-alpha"); + + let v3 = ChallengeVersion { + major: 0, + minor: 0, + patch: 1, + prerelease: Some("rc1".to_string()), + }; + assert_eq!(format!("{}", v3), "0.0.1-rc1"); + + let v4 = ChallengeVersion::new(10, 20, 30); + assert_eq!(v4.to_string(), "10.20.30"); + } + + #[test] + fn test_version_parsing_invalid() { + assert!(ChallengeVersion::parse("").is_none()); + assert!(ChallengeVersion::parse("1").is_none()); + assert!(ChallengeVersion::parse("1.2").is_none()); + assert!(ChallengeVersion::parse("a.b.c").is_none()); + assert!(ChallengeVersion::parse("1.2.x").is_none()); + assert!(ChallengeVersion::parse("-1.2.3").is_none()); + assert!(ChallengeVersion::parse("1.2.3.4").is_some()); // Extra parts are ignored + } + + #[test] + fn test_version_parsing_edge_cases() { + let v1 = ChallengeVersion::parse("0.0.0").unwrap(); + assert_eq!(v1.major, 0); + assert_eq!(v1.minor, 0); + assert_eq!(v1.patch, 0); + + let v2 = ChallengeVersion::parse("99.99.99").unwrap(); + assert_eq!(v2.major, 99); + assert_eq!(v2.minor, 99); + assert_eq!(v2.patch, 99); + + let v3 = ChallengeVersion::parse("v0.0.1").unwrap(); + assert_eq!(v3.major, 0); + assert_eq!(v3.minor, 0); + assert_eq!(v3.patch, 1); + + let v4 = ChallengeVersion::parse("1.0.0-beta.1").unwrap(); + assert_eq!(v4.prerelease, Some("beta.1".to_string())); + } + + #[test] + fn test_version_ordering() { + let v1 = ChallengeVersion::new(1, 0, 0); + let v2 = ChallengeVersion::new(1, 0, 1); + let v3 = ChallengeVersion::new(1, 1, 0); + let v4 = ChallengeVersion::new(2, 0, 0); + let v5 = ChallengeVersion::new(0, 9, 9); + + assert!(v1 < v2); + assert!(v2 < v3); + assert!(v3 < v4); + assert!(v5 < v1); + + let mut versions = vec![v4.clone(), v2.clone(), v5.clone(), v1.clone(), v3.clone()]; + versions.sort(); + assert_eq!(versions, vec![v5, v1, v2, v3, v4]); + } + + #[test] + fn test_version_partial_ord() { + let v1 = ChallengeVersion::new(1, 0, 0); + let v2 = ChallengeVersion::new(1, 0, 1); + let v3 = ChallengeVersion::new(1, 0, 0); + + assert_eq!(v1.partial_cmp(&v2), Some(Ordering::Less)); + assert_eq!(v2.partial_cmp(&v1), Some(Ordering::Greater)); + assert_eq!(v1.partial_cmp(&v3), Some(Ordering::Equal)); + } + + #[test] + fn test_version_equality() { + let v1 = ChallengeVersion::new(1, 2, 3); + let v2 = ChallengeVersion::new(1, 2, 3); + let v3 = ChallengeVersion::new(1, 2, 4); + + assert_eq!(v1, v2); + assert_ne!(v1, v3); + + let v4 = ChallengeVersion { + major: 1, + minor: 2, + patch: 3, + prerelease: Some("alpha".to_string()), + }; + let v5 = ChallengeVersion { + major: 1, + minor: 2, + patch: 3, + prerelease: Some("alpha".to_string()), + }; + let v6 = ChallengeVersion { + major: 1, + minor: 2, + patch: 3, + prerelease: Some("beta".to_string()), + }; + + assert_eq!(v4, v5); + assert_ne!(v4, v6); + assert_ne!(v1, v4); + } + + #[test] + fn test_version_hash() { + let mut map: HashMap = HashMap::new(); + + let v1 = ChallengeVersion::new(1, 0, 0); + let v2 = ChallengeVersion::new(2, 0, 0); + let v3 = ChallengeVersion::new(1, 0, 0); + + map.insert(v1.clone(), "version_one"); + map.insert(v2.clone(), "version_two"); + + assert_eq!(map.get(&v1), Some(&"version_one")); + assert_eq!(map.get(&v2), Some(&"version_two")); + assert_eq!(map.get(&v3), Some(&"version_one")); + + map.insert(v3, "version_one_updated"); + assert_eq!(map.len(), 2); + assert_eq!(map.get(&ChallengeVersion::new(1, 0, 0)), Some(&"version_one_updated")); + } + + #[test] + fn test_version_constraint_range() { + let min = ChallengeVersion::new(1, 0, 0); + let max = ChallengeVersion::new(2, 0, 0); + let range = VersionConstraint::Range { min: min.clone(), max: max.clone() }; + + assert!(range.satisfies(&ChallengeVersion::new(1, 0, 0))); + assert!(range.satisfies(&ChallengeVersion::new(1, 5, 0))); + assert!(range.satisfies(&ChallengeVersion::new(1, 99, 99))); + assert!(!range.satisfies(&ChallengeVersion::new(2, 0, 0))); + assert!(!range.satisfies(&ChallengeVersion::new(0, 9, 9))); + assert!(!range.satisfies(&ChallengeVersion::new(3, 0, 0))); + + let tight_range = VersionConstraint::Range { + min: ChallengeVersion::new(1, 2, 3), + max: ChallengeVersion::new(1, 2, 5), + }; + assert!(!tight_range.satisfies(&ChallengeVersion::new(1, 2, 2))); + assert!(tight_range.satisfies(&ChallengeVersion::new(1, 2, 3))); + assert!(tight_range.satisfies(&ChallengeVersion::new(1, 2, 4))); + assert!(!tight_range.satisfies(&ChallengeVersion::new(1, 2, 5))); + } + + #[test] + fn test_versioned_challenge_creation() { + let challenge = VersionedChallenge { + challenge_id: "test-challenge".to_string(), + version: ChallengeVersion::new(1, 0, 0), + min_platform_version: Some(ChallengeVersion::new(0, 5, 0)), + deprecated: false, + deprecation_message: None, + }; + + assert_eq!(challenge.challenge_id, "test-challenge"); + assert_eq!(challenge.version, ChallengeVersion::new(1, 0, 0)); + assert_eq!(challenge.min_platform_version, Some(ChallengeVersion::new(0, 5, 0))); + assert!(!challenge.deprecated); + assert!(challenge.deprecation_message.is_none()); + + let deprecated_challenge = VersionedChallenge { + challenge_id: "old-challenge".to_string(), + version: ChallengeVersion::new(0, 1, 0), + min_platform_version: None, + deprecated: true, + deprecation_message: Some("Use new-challenge instead".to_string()), + }; + + assert!(deprecated_challenge.deprecated); + assert_eq!(deprecated_challenge.deprecation_message, Some("Use new-challenge instead".to_string())); + } + + #[test] + fn test_version_compatible_same_major() { + let v1 = ChallengeVersion::new(1, 0, 0); + let v2 = ChallengeVersion::new(1, 1, 0); + let v3 = ChallengeVersion::new(1, 99, 99); + + assert!(v1.is_compatible_with(&v2)); + assert!(v2.is_compatible_with(&v1)); + assert!(v1.is_compatible_with(&v3)); + assert!(v3.is_compatible_with(&v1)); + assert!(v2.is_compatible_with(&v3)); + + let v0 = ChallengeVersion::new(0, 1, 0); + let v0_2 = ChallengeVersion::new(0, 2, 0); + assert!(v0.is_compatible_with(&v0_2)); + } + + #[test] + fn test_version_compatible_different_major() { + let v1 = ChallengeVersion::new(1, 0, 0); + let v2 = ChallengeVersion::new(2, 0, 0); + let v3 = ChallengeVersion::new(3, 5, 10); + let v0 = ChallengeVersion::new(0, 9, 9); + + assert!(!v1.is_compatible_with(&v2)); + assert!(!v2.is_compatible_with(&v1)); + assert!(!v1.is_compatible_with(&v3)); + assert!(!v2.is_compatible_with(&v3)); + assert!(!v0.is_compatible_with(&v1)); + assert!(!v1.is_compatible_with(&v0)); + } } diff --git a/crates/core/src/challenge.rs b/crates/core/src/challenge.rs index b73a4ff..626fa29 100644 --- a/crates/core/src/challenge.rs +++ b/crates/core/src/challenge.rs @@ -239,4 +239,202 @@ mod tests { assert_eq!(config.max_memory_mb, 512); assert_eq!(config.emission_weight, 1.0); } + + #[test] + fn test_challenge_config_default() { + let config = ChallengeConfig::default(); + assert_eq!(config.mechanism_id, 1); + assert_eq!(config.timeout_secs, 300); + assert_eq!(config.max_memory_mb, 512); + assert_eq!(config.max_cpu_secs, 60); + assert_eq!(config.emission_weight, 1.0); + assert_eq!(config.min_validators, 1); + assert_eq!(config.params_json, "{}"); + } + + #[test] + fn test_challenge_verify_code_tampered() { + let owner = Keypair::generate(); + let wasm = vec![0u8; 100]; + + let mut challenge = Challenge::new( + "Test".into(), + "Test".into(), + wasm, + owner.hotkey(), + ChallengeConfig::default(), + ); + + // Tamper with the code without updating the hash + challenge.wasm_code[0] = 255; + + // verify_code should return false since hash doesn't match + assert!(!challenge.verify_code()); + } + + #[test] + fn test_challenge_is_active_default() { + let owner = Keypair::generate(); + let wasm = vec![0u8; 50]; + + let challenge = Challenge::new( + "Test".into(), + "Test".into(), + wasm, + owner.hotkey(), + ChallengeConfig::default(), + ); + + assert!(challenge.is_active); + } + + #[test] + fn test_challenge_id_uniqueness() { + let owner = Keypair::generate(); + let wasm = vec![0u8; 50]; + + let challenge1 = Challenge::new( + "Test 1".into(), + "Test".into(), + wasm.clone(), + owner.hotkey(), + ChallengeConfig::default(), + ); + + let challenge2 = Challenge::new( + "Test 2".into(), + "Test".into(), + wasm, + owner.hotkey(), + ChallengeConfig::default(), + ); + + assert_ne!(challenge1.id, challenge2.id); + } + + #[test] + fn test_challenge_timestamps() { + let owner = Keypair::generate(); + let wasm = vec![0u8; 50]; + + let before = chrono::Utc::now(); + let challenge = Challenge::new( + "Test".into(), + "Test".into(), + wasm, + owner.hotkey(), + ChallengeConfig::default(), + ); + let after = chrono::Utc::now(); + + // created_at and updated_at should be within the time bounds + assert!(challenge.created_at >= before); + assert!(challenge.created_at <= after); + assert!(challenge.updated_at >= before); + assert!(challenge.updated_at <= after); + // For a new challenge, created_at equals updated_at + assert_eq!(challenge.created_at, challenge.updated_at); + } + + #[test] + fn test_challenge_update_code_changes_timestamp() { + let owner = Keypair::generate(); + let wasm1 = vec![1u8; 50]; + let wasm2 = vec![2u8; 50]; + + let mut challenge = Challenge::new( + "Test".into(), + "Test".into(), + wasm1, + owner.hotkey(), + ChallengeConfig::default(), + ); + + let original_updated_at = challenge.updated_at; + let original_created_at = challenge.created_at; + + // Small sleep to ensure timestamp changes + std::thread::sleep(std::time::Duration::from_millis(10)); + + challenge.update_code(wasm2); + + // created_at should not change + assert_eq!(challenge.created_at, original_created_at); + // updated_at should change + assert!(challenge.updated_at > original_updated_at); + } + + #[test] + fn test_challenge_meta_preserves_fields() { + let owner = Keypair::generate(); + let wasm = vec![42u8; 75]; + let config = ChallengeConfig::with_mechanism(3); + + let challenge = Challenge::new( + "Meta Test".into(), + "Description for meta".into(), + wasm, + owner.hotkey(), + config, + ); + + let meta: ChallengeMeta = (&challenge).into(); + + assert_eq!(meta.id, challenge.id); + assert_eq!(meta.name, challenge.name); + assert_eq!(meta.description, challenge.description); + assert_eq!(meta.code_hash, challenge.code_hash); + assert_eq!(meta.owner, challenge.owner); + assert_eq!(meta.config.mechanism_id, challenge.config.mechanism_id); + assert_eq!(meta.config.timeout_secs, challenge.config.timeout_secs); + assert_eq!(meta.created_at, challenge.created_at); + assert_eq!(meta.updated_at, challenge.updated_at); + assert_eq!(meta.is_active, challenge.is_active); + } + + #[test] + fn test_challenge_config_params_json() { + let config = ChallengeConfig::default(); + assert_eq!(config.params_json, "{}"); + + let config_mechanism = ChallengeConfig::with_mechanism(2); + assert_eq!(config_mechanism.params_json, "{}"); + } + + #[test] + fn test_challenge_empty_wasm() { + let owner = Keypair::generate(); + let empty_wasm: Vec = vec![]; + + let challenge = Challenge::new( + "Empty WASM".into(), + "Challenge with empty wasm".into(), + empty_wasm, + owner.hotkey(), + ChallengeConfig::default(), + ); + + // Should still create successfully and verify + assert!(challenge.verify_code()); + assert!(challenge.wasm_code.is_empty()); + assert!(!challenge.code_hash.is_empty()); // Hash should still be computed + } + + #[test] + fn test_challenge_large_wasm() { + let owner = Keypair::generate(); + let large_wasm = vec![0xABu8; 10 * 1024]; // 10KB + + let challenge = Challenge::new( + "Large WASM".into(), + "Challenge with 10KB wasm".into(), + large_wasm.clone(), + owner.hotkey(), + ChallengeConfig::default(), + ); + + assert!(challenge.verify_code()); + assert_eq!(challenge.wasm_code.len(), 10 * 1024); + assert_eq!(challenge.wasm_code, large_wasm); + } } diff --git a/crates/distributed-storage/src/error.rs b/crates/distributed-storage/src/error.rs index 3bf90b0..399c520 100644 --- a/crates/distributed-storage/src/error.rs +++ b/crates/distributed-storage/src/error.rs @@ -74,3 +74,209 @@ impl From for StorageError { /// Result type for storage operations pub type StorageResult = Result; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_storage_error_display_database() { + let err = StorageError::Database("connection failed".to_string()); + assert_eq!(err.to_string(), "Database error: connection failed"); + } + + #[test] + fn test_storage_error_display_not_found() { + let err = StorageError::NotFound { + namespace: "users".to_string(), + key: "user_123".to_string(), + }; + assert_eq!(err.to_string(), "Key not found: users:user_123"); + } + + #[test] + fn test_storage_error_display_quorum() { + let err = StorageError::QuorumNotReached { + required: 3, + received: 1, + }; + assert_eq!( + err.to_string(), + "Quorum not reached: got 1 of 3 confirmations" + ); + } + + #[test] + fn test_storage_error_display_all_variants() { + // Test Database variant + let database_err = StorageError::Database("db failure".to_string()); + assert_eq!(database_err.to_string(), "Database error: db failure"); + + // Test Serialization variant + let serialization_err = StorageError::Serialization("invalid format".to_string()); + assert_eq!( + serialization_err.to_string(), + "Serialization error: invalid format" + ); + + // Test NotFound variant + let not_found_err = StorageError::NotFound { + namespace: "config".to_string(), + key: "setting_1".to_string(), + }; + assert_eq!(not_found_err.to_string(), "Key not found: config:setting_1"); + + // Test NamespaceNotFound variant + let namespace_err = StorageError::NamespaceNotFound("missing_ns".to_string()); + assert_eq!( + namespace_err.to_string(), + "Namespace not found: missing_ns" + ); + + // Test Dht variant + let dht_err = StorageError::Dht("peer unreachable".to_string()); + assert_eq!(dht_err.to_string(), "DHT error: peer unreachable"); + + // Test Replication variant + let replication_err = StorageError::Replication("sync failed".to_string()); + assert_eq!(replication_err.to_string(), "Replication error: sync failed"); + + // Test QuorumNotReached variant + let quorum_err = StorageError::QuorumNotReached { + required: 5, + received: 2, + }; + assert_eq!( + quorum_err.to_string(), + "Quorum not reached: got 2 of 5 confirmations" + ); + + // Test Conflict variant + let conflict_err = StorageError::Conflict("concurrent write detected".to_string()); + assert_eq!( + conflict_err.to_string(), + "Write conflict: concurrent write detected" + ); + + // Test InvalidData variant + let invalid_data_err = StorageError::InvalidData("corrupted checksum".to_string()); + assert_eq!( + invalid_data_err.to_string(), + "Invalid data: corrupted checksum" + ); + + // Test Timeout variant + let timeout_err = StorageError::Timeout("operation exceeded 30s".to_string()); + assert_eq!( + timeout_err.to_string(), + "Operation timed out: operation exceeded 30s" + ); + + // Test NotInitialized variant + let not_initialized_err = StorageError::NotInitialized; + assert_eq!(not_initialized_err.to_string(), "Storage not initialized"); + + // Test Internal variant + let internal_err = StorageError::Internal("unexpected state".to_string()); + assert_eq!(internal_err.to_string(), "Internal error: unexpected state"); + } + + #[test] + fn test_from_sled_error() { + // Create a sled error by opening an invalid path scenario + // sled::Error doesn't have public constructors, so we trigger a real error + let sled_result = sled::open("/\0invalid"); + if let Err(sled_err) = sled_result { + let storage_err: StorageError = sled_err.into(); + let display = storage_err.to_string(); + assert!( + display.starts_with("Database error:"), + "Expected 'Database error:' prefix, got: {}", + display + ); + } + } + + #[test] + fn test_from_bincode_error() { + // Create a bincode error by attempting to deserialize invalid data + let invalid_data: &[u8] = &[0xff, 0xff, 0xff, 0xff]; + let bincode_result: Result = + bincode::deserialize(invalid_data); + if let Err(bincode_err) = bincode_result { + let storage_err: StorageError = bincode_err.into(); + let display = storage_err.to_string(); + assert!( + display.starts_with("Serialization error:"), + "Expected 'Serialization error:' prefix, got: {}", + display + ); + } + } + + #[test] + fn test_from_serde_json_error() { + // Create a serde_json error by parsing invalid JSON + let invalid_json = "{invalid json}"; + let json_result: Result = + serde_json::from_str(invalid_json); + if let Err(json_err) = json_result { + let storage_err: StorageError = json_err.into(); + let display = storage_err.to_string(); + assert!( + display.starts_with("Serialization error:"), + "Expected 'Serialization error:' prefix, got: {}", + display + ); + } + } + + #[test] + fn test_storage_result_type() { + // Test that StorageResult works as expected + fn returns_ok() -> StorageResult { + Ok(42) + } + + fn returns_err() -> StorageResult { + Err(StorageError::NotInitialized) + } + + // Test Ok case + let ok_result = returns_ok(); + assert!(ok_result.is_ok()); + assert_eq!(ok_result.unwrap(), 42); + + // Test Err case + let err_result = returns_err(); + assert!(err_result.is_err()); + assert_eq!( + err_result.unwrap_err().to_string(), + "Storage not initialized" + ); + } + + #[test] + fn test_storage_error_is_send_sync() { + // Verify StorageError can be sent across threads + fn assert_send() {} + fn assert_sync() {} + assert_send::(); + assert_sync::(); + } + + #[test] + fn test_storage_error_debug_format() { + // Verify Debug trait is implemented correctly + let err = StorageError::Database("test".to_string()); + let debug_str = format!("{:?}", err); + assert!( + debug_str.contains("Database"), + "Debug format should contain variant name" + ); + assert!( + debug_str.contains("test"), + "Debug format should contain error message" + ); + } +} diff --git a/crates/p2p-consensus/src/network.rs b/crates/p2p-consensus/src/network.rs index 54be27a..6f6bdf7 100644 --- a/crates/p2p-consensus/src/network.rs +++ b/crates/p2p-consensus/src/network.rs @@ -924,4 +924,423 @@ mod tests { let peer_id = PeerId::random(); assert!(mapping.get_hotkey(&peer_id).is_none()); } + + #[test] + fn test_peer_mapping_len_and_is_empty() { + let mapping = PeerMapping::new(); + + // Initially empty + assert!(mapping.is_empty()); + assert_eq!(mapping.len(), 0); + + // Add first peer + let peer_id1 = PeerId::random(); + let hotkey1 = Hotkey([1u8; 32]); + mapping.insert(peer_id1, hotkey1); + + assert!(!mapping.is_empty()); + assert_eq!(mapping.len(), 1); + + // Add second peer + let peer_id2 = PeerId::random(); + let hotkey2 = Hotkey([2u8; 32]); + mapping.insert(peer_id2, hotkey2); + + assert!(!mapping.is_empty()); + assert_eq!(mapping.len(), 2); + + // Remove one peer + mapping.remove_peer(&peer_id1); + assert_eq!(mapping.len(), 1); + + // Remove the other peer + mapping.remove_peer(&peer_id2); + assert!(mapping.is_empty()); + assert_eq!(mapping.len(), 0); + } + + #[test] + fn test_peer_mapping_overwrite() { + let mapping = PeerMapping::new(); + let peer_id = PeerId::random(); + let hotkey1 = Hotkey([1u8; 32]); + let hotkey2 = Hotkey([2u8; 32]); + + // Insert with first hotkey + mapping.insert(peer_id, hotkey1.clone()); + assert_eq!(mapping.get_hotkey(&peer_id), Some(hotkey1.clone())); + assert_eq!(mapping.get_peer(&hotkey1), Some(peer_id)); + + // Overwrite with second hotkey + mapping.insert(peer_id, hotkey2.clone()); + assert_eq!(mapping.get_hotkey(&peer_id), Some(hotkey2.clone())); + assert_eq!(mapping.get_peer(&hotkey2), Some(peer_id)); + + // Old hotkey should still point to the peer (due to current impl not cleaning old entry) + // This tests the actual behavior - hotkey_to_peer is not cleaned on overwrite + assert_eq!(mapping.get_peer(&hotkey1), Some(peer_id)); + } + + #[test] + fn test_peer_mapping_multiple_peers() { + let mapping = PeerMapping::new(); + + // Create multiple peers with unique hotkeys + let peers: Vec<(PeerId, Hotkey)> = (0..5) + .map(|i| { + let peer_id = PeerId::random(); + let mut hotkey_bytes = [0u8; 32]; + hotkey_bytes[0] = i as u8; + (peer_id, Hotkey(hotkey_bytes)) + }) + .collect(); + + // Insert all peers + for (peer_id, hotkey) in &peers { + mapping.insert(*peer_id, hotkey.clone()); + } + + assert_eq!(mapping.len(), 5); + + // Verify all mappings are correct + for (peer_id, hotkey) in &peers { + assert_eq!(mapping.get_hotkey(peer_id), Some(hotkey.clone())); + assert_eq!(mapping.get_peer(hotkey), Some(*peer_id)); + } + + // Remove a middle peer and verify others still work + let (removed_peer, removed_hotkey) = &peers[2]; + mapping.remove_peer(removed_peer); + + assert_eq!(mapping.len(), 4); + assert!(mapping.get_hotkey(removed_peer).is_none()); + assert!(mapping.get_peer(removed_hotkey).is_none()); + + // Other peers should still be intact + assert_eq!(mapping.get_hotkey(&peers[0].0), Some(peers[0].1.clone())); + assert_eq!(mapping.get_hotkey(&peers[4].0), Some(peers[4].1.clone())); + } + + #[test] + fn test_network_error_display() { + // Test Transport error display + let transport_err = NetworkError::Transport("connection refused".to_string()); + assert_eq!( + format!("{}", transport_err), + "Transport error: connection refused" + ); + + // Test Gossipsub error display + let gossipsub_err = NetworkError::Gossipsub("subscription failed".to_string()); + assert_eq!( + format!("{}", gossipsub_err), + "Gossipsub error: subscription failed" + ); + + // Test DHT error display + let dht_err = NetworkError::Dht("bootstrap failed".to_string()); + assert_eq!(format!("{}", dht_err), "DHT error: bootstrap failed"); + + // Test Serialization error display + let serial_err = NetworkError::Serialization("invalid data".to_string()); + assert_eq!( + format!("{}", serial_err), + "Serialization error: invalid data" + ); + + // Test NoPeers error display + let no_peers_err = NetworkError::NoPeers; + assert_eq!( + format!("{}", no_peers_err), + "Not connected to any peers" + ); + + // Test Channel error display + let channel_err = NetworkError::Channel("channel closed".to_string()); + assert_eq!(format!("{}", channel_err), "Channel error: channel closed"); + + // Test ReplayAttack error display + let replay_err = NetworkError::ReplayAttack { + signer: "abc123".to_string(), + nonce: 42, + }; + assert_eq!( + format!("{}", replay_err), + "Replay attack detected: nonce 42 already seen for abc123" + ); + + // Test RateLimitExceeded error display + let rate_limit_err = NetworkError::RateLimitExceeded { + signer: "def456".to_string(), + count: 150, + }; + assert_eq!( + format!("{}", rate_limit_err), + "Rate limit exceeded for def456: 150 messages in current window" + ); + } + + #[tokio::test] + async fn test_replay_attack_detection() { + let keypair = Keypair::generate(); + let config = P2PConfig::development(); + let validator_set = Arc::new(ValidatorSet::new(keypair.clone(), 0)); + let (tx, _rx) = mpsc::channel(100); + + let network = P2PNetwork::new(keypair, config, validator_set, tx) + .expect("Failed to create network"); + + let signer = Hotkey([5u8; 32]); + let nonce = 12345u64; + + // First use of nonce should succeed + let result1 = network.check_replay(&signer, nonce); + assert!(result1.is_ok(), "First nonce use should succeed"); + + // Second use of same nonce from same signer should fail + let result2 = network.check_replay(&signer, nonce); + assert!(result2.is_err(), "Replay should be detected"); + + match result2 { + Err(NetworkError::ReplayAttack { + signer: err_signer, + nonce: err_nonce, + }) => { + assert_eq!(err_signer, signer.to_hex()); + assert_eq!(err_nonce, nonce); + } + _ => panic!("Expected ReplayAttack error"), + } + + // Different nonce from same signer should succeed + let result3 = network.check_replay(&signer, nonce + 1); + assert!(result3.is_ok(), "Different nonce should succeed"); + + // Same nonce from different signer should succeed + let signer2 = Hotkey([6u8; 32]); + let result4 = network.check_replay(&signer2, nonce); + assert!( + result4.is_ok(), + "Same nonce from different signer should succeed" + ); + } + + #[tokio::test] + async fn test_rate_limit_enforcement() { + let keypair = Keypair::generate(); + let config = P2PConfig::development(); + let validator_set = Arc::new(ValidatorSet::new(keypair.clone(), 0)); + let (tx, _rx) = mpsc::channel(100); + + let network = P2PNetwork::new(keypair, config, validator_set, tx) + .expect("Failed to create network"); + + let signer = Hotkey([7u8; 32]); + + // Send DEFAULT_RATE_LIMIT (100) messages - should all succeed + for i in 0..DEFAULT_RATE_LIMIT { + let result = network.check_rate_limit(&signer); + assert!( + result.is_ok(), + "Message {} should be within rate limit", + i + 1 + ); + } + + // The next message should exceed the limit + let result = network.check_rate_limit(&signer); + assert!(result.is_err(), "Should exceed rate limit after 100 messages"); + + match result { + Err(NetworkError::RateLimitExceeded { signer: err_signer, count }) => { + assert_eq!(err_signer, signer.to_hex()); + assert_eq!(count, DEFAULT_RATE_LIMIT); + } + _ => panic!("Expected RateLimitExceeded error"), + } + + // Different signer should have separate rate limit + let signer2 = Hotkey([8u8; 32]); + let result2 = network.check_rate_limit(&signer2); + assert!( + result2.is_ok(), + "Different signer should have separate rate limit" + ); + } + + #[tokio::test] + async fn test_clean_old_nonces() { + let keypair = Keypair::generate(); + let config = P2PConfig::development(); + let validator_set = Arc::new(ValidatorSet::new(keypair.clone(), 0)); + let (tx, _rx) = mpsc::channel(100); + + let network = P2PNetwork::new(keypair, config, validator_set, tx) + .expect("Failed to create network"); + + let signer = Hotkey([9u8; 32]); + + // Add some nonces + network.check_replay(&signer, 1).expect("Nonce 1 should succeed"); + network.check_replay(&signer, 2).expect("Nonce 2 should succeed"); + network.check_replay(&signer, 3).expect("Nonce 3 should succeed"); + + // Verify nonces are tracked + { + let seen_nonces = network.seen_nonces.read(); + let signer_nonces = seen_nonces.get(&signer); + assert!(signer_nonces.is_some()); + assert_eq!(signer_nonces.unwrap().len(), 3); + } + + // Clean with 0 max_age_secs - all nonces should be considered old and removed + network.clean_old_nonces(0); + + // After cleaning with 0 age, all nonces should be gone + { + let seen_nonces = network.seen_nonces.read(); + // Signer entry should be removed since all its nonces expired + assert!( + seen_nonces.get(&signer).is_none() || seen_nonces.get(&signer).unwrap().is_empty(), + "Nonces should be cleaned" + ); + } + + // Now the same nonces should be usable again + let result = network.check_replay(&signer, 1); + assert!(result.is_ok(), "Nonce 1 should be usable after cleaning"); + } + + #[tokio::test] + async fn test_clean_rate_limit_entries() { + let keypair = Keypair::generate(); + let config = P2PConfig::development(); + let validator_set = Arc::new(ValidatorSet::new(keypair.clone(), 0)); + let (tx, _rx) = mpsc::channel(100); + + let network = P2PNetwork::new(keypair, config, validator_set, tx) + .expect("Failed to create network"); + + let signer1 = Hotkey([10u8; 32]); + let signer2 = Hotkey([11u8; 32]); + + // Add rate limit entries for both signers + network.check_rate_limit(&signer1).expect("Rate limit check should succeed"); + network.check_rate_limit(&signer2).expect("Rate limit check should succeed"); + + // Verify entries exist + { + let timestamps = network.message_timestamps.read(); + assert!(timestamps.contains_key(&signer1)); + assert!(timestamps.contains_key(&signer2)); + } + + // Clean entries (this removes entries older than RATE_LIMIT_WINDOW_MS) + // Since entries were just added, they shouldn't be removed yet + network.clean_rate_limit_entries(); + + { + let timestamps = network.message_timestamps.read(); + // Entries should still exist since they're recent + assert!(timestamps.contains_key(&signer1)); + assert!(timestamps.contains_key(&signer2)); + } + + // Manually manipulate timestamps to simulate old entries for testing + // by replacing with empty queues (simulating all old entries removed) + { + let mut timestamps = network.message_timestamps.write(); + timestamps.clear(); + } + + // After clearing, clean should not find anything + network.clean_rate_limit_entries(); + + { + let timestamps = network.message_timestamps.read(); + assert!(timestamps.is_empty()); + } + } + + #[tokio::test] + async fn test_network_connected_peer_count() { + let keypair = Keypair::generate(); + let config = P2PConfig::development(); + let validator_set = Arc::new(ValidatorSet::new(keypair.clone(), 0)); + let (tx, _rx) = mpsc::channel(100); + + let network = P2PNetwork::new(keypair, config, validator_set, tx) + .expect("Failed to create network"); + + // Initially no connected peers + assert_eq!(network.connected_peer_count(), 0); + + // Add peers to the peer mapping + let peer_id1 = PeerId::random(); + let hotkey1 = Hotkey([20u8; 32]); + network.peer_mapping.insert(peer_id1, hotkey1); + + assert_eq!(network.connected_peer_count(), 1); + + let peer_id2 = PeerId::random(); + let hotkey2 = Hotkey([21u8; 32]); + network.peer_mapping.insert(peer_id2, hotkey2); + + assert_eq!(network.connected_peer_count(), 2); + + let peer_id3 = PeerId::random(); + let hotkey3 = Hotkey([22u8; 32]); + network.peer_mapping.insert(peer_id3, hotkey3); + + assert_eq!(network.connected_peer_count(), 3); + + // Remove a peer + network.peer_mapping.remove_peer(&peer_id2); + assert_eq!(network.connected_peer_count(), 2); + } + + #[tokio::test] + async fn test_network_has_min_peers() { + let keypair = Keypair::generate(); + let config = P2PConfig::development(); + let validator_set = Arc::new(ValidatorSet::new(keypair.clone(), 0)); + let (tx, _rx) = mpsc::channel(100); + + let network = P2PNetwork::new(keypair, config, validator_set, tx) + .expect("Failed to create network"); + + // Initially no peers + assert!(!network.has_min_peers(1)); + assert!(!network.has_min_peers(3)); + assert!(network.has_min_peers(0)); // 0 is always satisfied + + // Add one peer + let peer_id1 = PeerId::random(); + let hotkey1 = Hotkey([30u8; 32]); + network.peer_mapping.insert(peer_id1, hotkey1); + + assert!(network.has_min_peers(0)); + assert!(network.has_min_peers(1)); + assert!(!network.has_min_peers(2)); + + // Add two more peers + let peer_id2 = PeerId::random(); + let hotkey2 = Hotkey([31u8; 32]); + network.peer_mapping.insert(peer_id2, hotkey2); + + let peer_id3 = PeerId::random(); + let hotkey3 = Hotkey([32u8; 32]); + network.peer_mapping.insert(peer_id3, hotkey3); + + assert!(network.has_min_peers(0)); + assert!(network.has_min_peers(1)); + assert!(network.has_min_peers(2)); + assert!(network.has_min_peers(3)); + assert!(!network.has_min_peers(4)); + + // Remove one peer + network.peer_mapping.remove_peer(&peer_id2); + assert!(network.has_min_peers(2)); + assert!(!network.has_min_peers(3)); + } } diff --git a/crates/rpc-server/src/server.rs b/crates/rpc-server/src/server.rs index 7e24968..4933a77 100644 --- a/crates/rpc-server/src/server.rs +++ b/crates/rpc-server/src/server.rs @@ -691,4 +691,118 @@ mod tests { assert_eq!(config.name, "CustomChain"); assert!(!config.cors_enabled); } + + #[test] + fn test_rpc_config_clone() { + let config = RpcConfig { + addr: "127.0.0.1:8080".parse().unwrap(), + netuid: 42, + name: "CloneTest".to_string(), + min_stake: 1_000_000, + cors_enabled: true, + }; + + let cloned = config.clone(); + + assert_eq!(cloned.addr, config.addr); + assert_eq!(cloned.netuid, config.netuid); + assert_eq!(cloned.name, config.name); + assert_eq!(cloned.min_stake, config.min_stake); + assert_eq!(cloned.cors_enabled, config.cors_enabled); + } + + #[test] + fn test_rpc_config_debug() { + let config = RpcConfig { + addr: "0.0.0.0:9000".parse().unwrap(), + netuid: 7, + name: "DebugTest".to_string(), + min_stake: 500_000, + cors_enabled: false, + }; + + let debug_str = format!("{:?}", config); + + assert!(debug_str.contains("RpcConfig")); + assert!(debug_str.contains("9000")); + assert!(debug_str.contains("7")); + assert!(debug_str.contains("DebugTest")); + assert!(debug_str.contains("500000")); + assert!(debug_str.contains("false")); + } + + #[tokio::test] + async fn test_rpc_server_router_has_routes() { + let kp = Keypair::generate(); + let state = Arc::new(RwLock::new(ChainState::new( + kp.hotkey(), + NetworkConfig::default(), + ))); + let bans = Arc::new(RwLock::new(BanList::new())); + + let config = RpcConfig::default(); + let server = RpcServer::new(config, state, bans); + + // The router() method should create a router with routes defined + // We verify this by checking that the router can be created without panicking + let router = server.router(); + + // The router is created successfully, which means routes for /, /rpc, /health are configured + // We can't directly inspect routes, but creation success proves they're registered + assert!(std::mem::size_of_val(&router) > 0); + } + + #[tokio::test] + async fn test_rpc_server_cors_disabled() { + let kp = Keypair::generate(); + let state = Arc::new(RwLock::new(ChainState::new( + kp.hotkey(), + NetworkConfig::default(), + ))); + let bans = Arc::new(RwLock::new(BanList::new())); + + let config = RpcConfig { + addr: "127.0.0.1:8081".parse().unwrap(), + netuid: 1, + name: "NoCorsTest".to_string(), + min_stake: 1_000_000_000_000, + cors_enabled: false, + }; + + // Verify config is set correctly + assert!(!config.cors_enabled); + + let server = RpcServer::new(config, state, bans); + + // Server creation should succeed with cors disabled + let router = server.router(); + assert!(std::mem::size_of_val(&router) > 0); + + // Verify the addr is set correctly + assert_eq!(server.addr().port(), 8081); + } + + #[tokio::test] + async fn test_handle_batch_request_empty() { + let kp = Keypair::generate(); + let state = Arc::new(RwLock::new(ChainState::new( + kp.hotkey(), + NetworkConfig::default(), + ))); + let bans = Arc::new(RwLock::new(BanList::new())); + + let config = RpcConfig::default(); + let server = RpcServer::new(config, state, bans); + let handler = server.rpc_handler(); + + // Empty batch array should return error + let empty_batch = json!([]); + let (status, response) = jsonrpc_handler(Json(empty_batch), handler).await; + + assert_eq!(status, StatusCode::BAD_REQUEST); + assert!(response.0.error.is_some()); + let error = response.0.error.unwrap(); + assert_eq!(error.code, PARSE_ERROR); + assert!(error.message.contains("Empty batch")); + } }