diff --git a/crates/platform-server/src/data_api.rs b/crates/platform-server/src/data_api.rs index e631bac8e..564894574 100644 --- a/crates/platform-server/src/data_api.rs +++ b/crates/platform-server/src/data_api.rs @@ -266,3 +266,78 @@ pub async fn get_snapshot( total_stake, })) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_now_returns_timestamp() { + let timestamp = now(); + // Should be a reasonable Unix timestamp (after 2020) + assert!(timestamp > 1577836800); // Jan 1, 2020 + // Should be less than far future (year 2100) + assert!(timestamp < 4102444800); // Jan 1, 2100 + } + + #[test] + fn test_now_increases() { + let t1 = now(); + std::thread::sleep(std::time::Duration::from_millis(10)); + let t2 = now(); + assert!(t2 >= t1); + } + + #[test] + fn test_list_tasks_query_default_limit() { + let query = ListTasksQuery { limit: None }; + assert_eq!(query.limit, None); + } + + #[test] + fn test_list_tasks_query_with_limit() { + let query = ListTasksQuery { limit: Some(50) }; + assert_eq!(query.limit, Some(50)); + } + + #[test] + fn test_snapshot_query_default_epoch() { + let query = SnapshotQuery { epoch: None }; + assert_eq!(query.epoch, None); + } + + #[test] + fn test_snapshot_query_with_epoch() { + let query = SnapshotQuery { epoch: Some(100) }; + assert_eq!(query.epoch, Some(100)); + } + + #[test] + fn test_snapshot_response_serialization() { + let response = SnapshotResponse { + epoch: 10, + snapshot_time: 1234567890, + leaderboard: vec![], + validators: vec![], + total_stake: 1000, + }; + + let json = serde_json::to_string(&response).unwrap(); + assert!(json.contains("\"epoch\":10")); + assert!(json.contains("\"total_stake\":1000")); + } + + #[test] + fn test_renew_request_deserialization() { + let json = r#"{ + "validator_hotkey": "test_validator", + "signature": "test_sig", + "ttl_seconds": 300 + }"#; + + let request: RenewRequest = serde_json::from_str(json).unwrap(); + assert_eq!(request.validator_hotkey, "test_validator"); + assert_eq!(request.signature, "test_sig"); + assert_eq!(request.ttl_seconds, 300); + } +} diff --git a/crates/platform-server/src/db/mod.rs b/crates/platform-server/src/db/mod.rs index b5912383b..7481c6794 100644 --- a/crates/platform-server/src/db/mod.rs +++ b/crates/platform-server/src/db/mod.rs @@ -54,3 +54,21 @@ async fn create_pool(database_url: &str) -> Result { let pool = cfg.create_pool(Some(Runtime::Tokio1), NoTls)?; Ok(pool) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_db_pool_type() { + // Test that DbPool is correctly aliased to Pool + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + let pool_result = create_pool("postgresql://localhost/test").await; + if let Ok(_pool) = pool_result { + // Pool type should match DbPool + let _typed: DbPool = _pool; + } + }); + } +} diff --git a/crates/platform-server/src/db/schema.rs b/crates/platform-server/src/db/schema.rs index 5b6304fcc..bd0c082f2 100644 --- a/crates/platform-server/src/db/schema.rs +++ b/crates/platform-server/src/db/schema.rs @@ -218,3 +218,91 @@ INSERT INTO network_state (key, value) VALUES ('challenge_id', '') ON CONFLICT (key) DO NOTHING; "#; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_schema_sql_contains_tables() { + // Verify that the schema SQL contains all expected table definitions + assert!(SCHEMA_SQL.contains("CREATE TABLE IF NOT EXISTS validators")); + assert!(SCHEMA_SQL.contains("CREATE TABLE IF NOT EXISTS challenge_config")); + assert!(SCHEMA_SQL.contains("CREATE TABLE IF NOT EXISTS submissions")); + assert!(SCHEMA_SQL.contains("CREATE TABLE IF NOT EXISTS evaluations")); + assert!(SCHEMA_SQL.contains("CREATE TABLE IF NOT EXISTS leaderboard")); + assert!(SCHEMA_SQL.contains("CREATE TABLE IF NOT EXISTS task_leases")); + assert!(SCHEMA_SQL.contains("CREATE TABLE IF NOT EXISTS events")); + assert!(SCHEMA_SQL.contains("CREATE TABLE IF NOT EXISTS network_state")); + assert!(SCHEMA_SQL.contains("CREATE TABLE IF NOT EXISTS challenges")); + assert!(SCHEMA_SQL.contains("CREATE TABLE IF NOT EXISTS evaluation_jobs")); + } + + #[test] + fn test_schema_sql_contains_indexes() { + // Verify that the schema SQL contains index definitions + assert!(SCHEMA_SQL.contains("CREATE INDEX IF NOT EXISTS idx_submissions_miner")); + assert!(SCHEMA_SQL.contains("CREATE INDEX IF NOT EXISTS idx_submissions_epoch")); + assert!(SCHEMA_SQL.contains("CREATE INDEX IF NOT EXISTS idx_evaluations_agent")); + assert!(SCHEMA_SQL.contains("CREATE INDEX IF NOT EXISTS idx_leaderboard_rank")); + assert!(SCHEMA_SQL.contains("CREATE INDEX IF NOT EXISTS idx_task_leases_validator")); + assert!(SCHEMA_SQL.contains("CREATE INDEX IF NOT EXISTS idx_events_type")); + assert!(SCHEMA_SQL.contains("CREATE INDEX IF NOT EXISTS idx_challenges_status")); + assert!(SCHEMA_SQL.contains("CREATE INDEX IF NOT EXISTS idx_jobs_status")); + } + + #[test] + fn test_schema_sql_contains_initial_data() { + // Verify initial network state data + assert!(SCHEMA_SQL.contains("INSERT INTO network_state")); + assert!(SCHEMA_SQL.contains("current_epoch")); + assert!(SCHEMA_SQL.contains("current_block")); + assert!(SCHEMA_SQL.contains("total_stake")); + assert!(SCHEMA_SQL.contains("challenge_id")); + } + + #[test] + fn test_schema_sql_contains_foreign_keys() { + // Verify foreign key relationships + assert!(SCHEMA_SQL.contains("REFERENCES submissions(id)")); + } + + #[test] + fn test_schema_sql_contains_unique_constraints() { + // Verify unique constraints + assert!(SCHEMA_SQL.contains("UNIQUE(submission_id, validator_hotkey)")); + assert!(SCHEMA_SQL.contains("agent_hash VARCHAR(128) NOT NULL UNIQUE")); + } + + #[test] + fn test_schema_sql_default_values() { + // Verify default values are set correctly with specific column checks + assert!(SCHEMA_SQL.contains("status VARCHAR(32) DEFAULT 'pending'")); + assert!(SCHEMA_SQL.contains("status VARCHAR(32) DEFAULT 'active'")); + assert!(SCHEMA_SQL.contains("is_active BOOLEAN DEFAULT TRUE")); + assert!(SCHEMA_SQL.contains("stake BIGINT NOT NULL DEFAULT 0")); + assert!(SCHEMA_SQL.contains("gpu_required BOOLEAN DEFAULT FALSE")); + assert!(SCHEMA_SQL.contains("is_healthy BOOLEAN DEFAULT FALSE")); + assert!(SCHEMA_SQL.contains("DEFAULT NOW()")); + } + + #[test] + fn test_schema_sql_jsonb_fields() { + // Verify JSONB columns exist for flexible data + assert!(SCHEMA_SQL.contains("module_whitelist JSONB")); + assert!(SCHEMA_SQL.contains("model_whitelist JSONB")); + assert!(SCHEMA_SQL.contains("pricing_config JSONB")); + assert!(SCHEMA_SQL.contains("evaluation_config JSONB")); + assert!(SCHEMA_SQL.contains("task_results JSONB")); + assert!(SCHEMA_SQL.contains("payload JSONB")); + } + + #[test] + fn test_schema_sql_timestamp_fields() { + // Verify timestamp fields are properly defined + assert!(SCHEMA_SQL.contains("created_at TIMESTAMPTZ")); + assert!(SCHEMA_SQL.contains("updated_at TIMESTAMPTZ")); + assert!(SCHEMA_SQL.contains("expires_at TIMESTAMPTZ")); + assert!(SCHEMA_SQL.contains("last_seen TIMESTAMPTZ")); + } +} diff --git a/crates/platform-server/src/models/mod.rs b/crates/platform-server/src/models/mod.rs index 6eb828b3c..0c70207d8 100644 --- a/crates/platform-server/src/models/mod.rs +++ b/crates/platform-server/src/models/mod.rs @@ -650,3 +650,337 @@ pub struct ChallengeActionRequest { pub signature: String, pub timestamp: i64, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_submission_status_to_string() { + assert_eq!(SubmissionStatus::Pending.to_string(), "pending"); + assert_eq!(SubmissionStatus::Evaluating.to_string(), "evaluating"); + assert_eq!(SubmissionStatus::Completed.to_string(), "completed"); + assert_eq!(SubmissionStatus::Failed.to_string(), "failed"); + assert_eq!(SubmissionStatus::Rejected.to_string(), "rejected"); + } + + #[test] + fn test_submission_status_from_str() { + assert_eq!(SubmissionStatus::from("pending"), SubmissionStatus::Pending); + assert_eq!( + SubmissionStatus::from("evaluating"), + SubmissionStatus::Evaluating + ); + assert_eq!( + SubmissionStatus::from("completed"), + SubmissionStatus::Completed + ); + assert_eq!(SubmissionStatus::from("failed"), SubmissionStatus::Failed); + assert_eq!( + SubmissionStatus::from("rejected"), + SubmissionStatus::Rejected + ); + // Default case + assert_eq!(SubmissionStatus::from("unknown"), SubmissionStatus::Pending); + } + + #[test] + fn test_submission_status_equality() { + assert_eq!(SubmissionStatus::Pending, SubmissionStatus::Pending); + assert_ne!(SubmissionStatus::Pending, SubmissionStatus::Completed); + } + + #[test] + fn test_challenge_status_equality() { + assert_eq!(ChallengeStatus::Active, ChallengeStatus::Active); + assert_ne!(ChallengeStatus::Active, ChallengeStatus::Paused); + assert_ne!(ChallengeStatus::Paused, ChallengeStatus::Deprecated); + } + + #[test] + fn test_task_lease_status_equality() { + assert_eq!(TaskLeaseStatus::Active, TaskLeaseStatus::Active); + assert_ne!(TaskLeaseStatus::Active, TaskLeaseStatus::Completed); + assert_ne!(TaskLeaseStatus::Completed, TaskLeaseStatus::Failed); + assert_ne!(TaskLeaseStatus::Failed, TaskLeaseStatus::Expired); + } + + #[test] + fn test_auth_role_equality() { + assert_eq!(AuthRole::Validator, AuthRole::Validator); + assert_ne!(AuthRole::Validator, AuthRole::Miner); + assert_ne!(AuthRole::Miner, AuthRole::Owner); + assert_ne!(AuthRole::Owner, AuthRole::Challenge); + } + + #[test] + fn test_job_status_equality() { + assert_eq!(JobStatus::Pending, JobStatus::Pending); + assert_ne!(JobStatus::Pending, JobStatus::Assigned); + assert_ne!(JobStatus::Assigned, JobStatus::Running); + assert_ne!(JobStatus::Running, JobStatus::Completed); + assert_ne!(JobStatus::Completed, JobStatus::Failed); + } + + #[test] + fn test_task_progress_status_equality() { + assert_eq!(TaskProgressStatus::Started, TaskProgressStatus::Started); + assert_ne!(TaskProgressStatus::Started, TaskProgressStatus::Running); + assert_ne!(TaskProgressStatus::Running, TaskProgressStatus::Passed); + assert_ne!(TaskProgressStatus::Passed, TaskProgressStatus::Failed); + assert_ne!(TaskProgressStatus::Failed, TaskProgressStatus::Skipped); + } + + #[test] + fn test_registered_challenge_new() { + let challenge = RegisteredChallenge::new( + "test-challenge", + "Test Challenge", + "test-image:latest", + ); + + assert_eq!(challenge.id, "test-challenge"); + assert_eq!(challenge.name, "Test Challenge"); + assert_eq!(challenge.docker_image, "test-image:latest"); + assert_eq!(challenge.mechanism_id, 1); + assert_eq!(challenge.emission_weight, 0.1); + assert_eq!(challenge.timeout_secs, 3600); + assert_eq!(challenge.cpu_cores, 2.0); + assert_eq!(challenge.memory_mb, 4096); + assert_eq!(challenge.gpu_required, false); + assert_eq!(challenge.status, "active"); + assert_eq!(challenge.endpoint, None); + assert_eq!(challenge.container_id, None); + assert_eq!(challenge.last_health_check, None); + assert_eq!(challenge.is_healthy, false); + } + + #[test] + fn test_default_functions() { + assert_eq!(default_mechanism_id(), 1); + assert_eq!(default_emission_weight(), 0.1); + assert_eq!(default_timeout(), 3600); + assert_eq!(default_cpu(), 2.0); + assert_eq!(default_memory(), 4096); + } + + #[test] + fn test_validator_serialization() { + let validator = Validator { + hotkey: "test_hotkey".to_string(), + stake: 1000, + last_seen: Some(1234567890), + is_active: true, + created_at: 1234567800, + }; + + let json = serde_json::to_string(&validator).unwrap(); + assert!(json.contains("test_hotkey")); + assert!(json.contains("1000")); + + let deserialized: Validator = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.hotkey, "test_hotkey"); + assert_eq!(deserialized.stake, 1000); + } + + #[test] + fn test_submission_serialization() { + let submission = Submission { + id: "sub-123".to_string(), + agent_hash: "hash123".to_string(), + miner_hotkey: "miner1".to_string(), + source_code: Some("code".to_string()), + source_hash: "src_hash".to_string(), + name: Some("Agent 1".to_string()), + version: "1.0.0".to_string(), + epoch: 10, + status: SubmissionStatus::Pending, + api_key: Some("key123".to_string()), + api_provider: Some("openrouter".to_string()), + total_cost_usd: Some(1.5), + api_keys_encrypted: None, + created_at: 1234567890, + }; + + let json = serde_json::to_string(&submission).unwrap(); + let deserialized: Submission = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.id, "sub-123"); + assert_eq!(deserialized.agent_hash, "hash123"); + assert_eq!(deserialized.status, SubmissionStatus::Pending); + } + + #[test] + fn test_evaluation_serialization() { + let evaluation = Evaluation { + id: "eval-123".to_string(), + submission_id: "sub-123".to_string(), + agent_hash: "hash123".to_string(), + validator_hotkey: "validator1".to_string(), + score: 0.95, + tasks_passed: 19, + tasks_total: 20, + tasks_failed: 1, + total_cost_usd: 2.5, + execution_time_ms: Some(5000), + task_results: Some("results".to_string()), + execution_log: Some("log".to_string()), + created_at: 1234567890, + }; + + let json = serde_json::to_string(&evaluation).unwrap(); + let deserialized: Evaluation = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.score, 0.95); + assert_eq!(deserialized.tasks_passed, 19); + } + + #[test] + fn test_task_lease_serialization() { + let lease = TaskLease { + task_id: "task-123".to_string(), + validator_hotkey: "validator1".to_string(), + claimed_at: 1234567890, + expires_at: 1234568190, + status: TaskLeaseStatus::Active, + }; + + let json = serde_json::to_string(&lease).unwrap(); + let deserialized: TaskLease = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.task_id, "task-123"); + assert_eq!(deserialized.status, TaskLeaseStatus::Active); + } + + #[test] + fn test_ws_event_serialization() { + let event = WsEvent::Ping; + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("ping")); + + let event = WsEvent::Pong; + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("pong")); + } + + #[test] + fn test_ws_event_submission_received() { + let event = WsEvent::SubmissionReceived(SubmissionEvent { + submission_id: "sub-123".to_string(), + agent_hash: "hash123".to_string(), + miner_hotkey: "miner1".to_string(), + name: Some("Agent 1".to_string()), + epoch: 10, + }); + + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("submission_received")); + assert!(json.contains("sub-123")); + } + + #[test] + fn test_ws_event_task_claimed() { + let event = WsEvent::TaskClaimed(TaskClaimedEvent { + task_id: "task-123".to_string(), + validator_hotkey: "validator1".to_string(), + expires_at: 1234568190, + }); + + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("task_claimed")); + assert!(json.contains("task-123")); + } + + #[test] + fn test_challenge_custom_event_serialization() { + let event = WsEvent::ChallengeEvent(ChallengeCustomEvent { + challenge_id: "test-challenge".to_string(), + event_name: "custom_event".to_string(), + payload: serde_json::json!({"key": "value"}), + timestamp: 1234567890, + }); + + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("challenge_event")); + assert!(json.contains("test-challenge")); + assert!(json.contains("custom_event")); + } + + #[test] + fn test_auth_request_serialization() { + let request = AuthRequest { + hotkey: "validator1".to_string(), + timestamp: 1234567890, + signature: "sig123".to_string(), + role: AuthRole::Validator, + }; + + let json = serde_json::to_string(&request).unwrap(); + let deserialized: AuthRequest = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.hotkey, "validator1"); + assert_eq!(deserialized.role, AuthRole::Validator); + } + + #[test] + fn test_job_assigned_event_serialization() { + let event = WsEvent::JobAssigned(JobAssignedEvent { + job_id: "job-123".to_string(), + submission_id: "sub-123".to_string(), + agent_hash: "hash123".to_string(), + validator_hotkey: "validator1".to_string(), + challenge_id: "challenge-1".to_string(), + }); + + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("job_assigned")); + assert!(json.contains("job-123")); + } + + #[test] + fn test_job_progress_event_serialization() { + let event = WsEvent::JobProgress(JobProgressEvent { + job_id: "job-123".to_string(), + validator_hotkey: "validator1".to_string(), + task_index: 5, + task_total: 10, + task_id: "task-5".to_string(), + status: TaskProgressStatus::Running, + message: Some("Running test".to_string()), + }); + + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("job_progress")); + assert!(json.contains("task-5")); + } + + #[test] + fn test_challenge_registered_event_serialization() { + let event = WsEvent::ChallengeRegistered(ChallengeRegisteredEvent { + id: "challenge-1".to_string(), + name: "Test Challenge".to_string(), + docker_image: "test:latest".to_string(), + mechanism_id: 1, + emission_weight: 0.2, + }); + + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("challenge_registered")); + assert!(json.contains("Test Challenge")); + } + + #[test] + fn test_challenge_started_event_serialization() { + let event = WsEvent::ChallengeStarted(ChallengeStartedEvent { + id: "challenge-1".to_string(), + endpoint: "http://localhost:8080".to_string(), + docker_image: "test:latest".to_string(), + mechanism_id: 1, + emission_weight: 0.2, + timeout_secs: 3600, + cpu_cores: 2.0, + memory_mb: 4096, + gpu_required: false, + }); + + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("challenge_started")); + assert!(json.contains("localhost:8080")); + } +} diff --git a/crates/platform-server/src/observability.rs b/crates/platform-server/src/observability.rs index ba21e5528..e82d517f8 100644 --- a/crates/platform-server/src/observability.rs +++ b/crates/platform-server/src/observability.rs @@ -26,3 +26,25 @@ pub fn init_sentry() -> Option { info!("Sentry initialized for error tracking"); Some(guard) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_init_sentry_no_dsn() { + // Without SENTRY_DSN environment variable + std::env::remove_var("SENTRY_DSN"); + let guard = init_sentry(); + assert!(guard.is_none()); + } + + #[test] + fn test_init_sentry_empty_dsn() { + // With empty SENTRY_DSN + std::env::set_var("SENTRY_DSN", ""); + let _guard = init_sentry(); + assert!(_guard.is_none()); + std::env::remove_var("SENTRY_DSN"); + } +} diff --git a/crates/platform-server/src/orchestration.rs b/crates/platform-server/src/orchestration.rs index 6e268343a..a5ed25fff 100644 --- a/crates/platform-server/src/orchestration.rs +++ b/crates/platform-server/src/orchestration.rs @@ -146,3 +146,111 @@ impl ChallengeManager { self.endpoints.read().keys().cloned().collect() } } + +#[cfg(test)] +mod tests { + use super::*; + + // These tests validate the underlying HashMap storage semantics + // that ChallengeManager uses for endpoint and ID tracking. + // Full ChallengeManager integration tests require async setup with Docker. + + #[test] + fn test_endpoints_hashmap_empty() { + // Validate empty endpoint storage + let endpoints: Arc>> = + Arc::new(RwLock::new(HashMap::new())); + + let ids: Vec = endpoints.read().keys().cloned().collect(); + assert_eq!(ids.len(), 0); + } + + #[test] + fn test_endpoints_hashmap_insert_and_retrieve() { + // Validate endpoint insertion and retrieval + let endpoints: Arc>> = + Arc::new(RwLock::new(HashMap::new())); + + endpoints + .write() + .insert("challenge1".to_string(), "http://localhost:8080".to_string()); + + let endpoint = endpoints.read().get("challenge1").cloned(); + assert_eq!(endpoint, Some("http://localhost:8080".to_string())); + + let ids: Vec = endpoints.read().keys().cloned().collect(); + assert_eq!(ids.len(), 1); + assert!(ids.contains(&"challenge1".to_string())); + } + + #[test] + fn test_id_map_hashmap_storage() { + // Validate ChallengeId mapping storage + let id_map: Arc>> = + Arc::new(RwLock::new(HashMap::new())); + + let challenge_id = ChallengeId::from_string("test-challenge"); + id_map + .write() + .insert("test-challenge".to_string(), challenge_id); + + let retrieved = id_map.read().get("test-challenge").cloned(); + assert!(retrieved.is_some()); + } + + #[test] + fn test_endpoints_hashmap_remove() { + // Validate endpoint removal + let endpoints: Arc>> = + Arc::new(RwLock::new(HashMap::new())); + + endpoints + .write() + .insert("challenge1".to_string(), "http://localhost:8080".to_string()); + assert_eq!(endpoints.read().len(), 1); + + endpoints.write().remove("challenge1"); + assert_eq!(endpoints.read().len(), 0); + } + + #[test] + fn test_endpoints_hashmap_multiple() { + // Validate multiple endpoint storage + let endpoints: Arc>> = + Arc::new(RwLock::new(HashMap::new())); + + endpoints + .write() + .insert("challenge1".to_string(), "http://localhost:8080".to_string()); + endpoints + .write() + .insert("challenge2".to_string(), "http://localhost:8081".to_string()); + endpoints + .write() + .insert("challenge3".to_string(), "http://localhost:8082".to_string()); + + assert_eq!(endpoints.read().len(), 3); + assert_eq!( + endpoints.read().get("challenge1").cloned(), + Some("http://localhost:8080".to_string()) + ); + assert_eq!( + endpoints.read().get("challenge2").cloned(), + Some("http://localhost:8081".to_string()) + ); + assert_eq!( + endpoints.read().get("challenge3").cloned(), + Some("http://localhost:8082".to_string()) + ); + } + + #[test] + fn test_endpoints_hashmap_nonexistent_key() { + // Validate nonexistent key returns None + let endpoints: Arc>> = + Arc::new(RwLock::new(HashMap::new())); + + let endpoint = endpoints.read().get("nonexistent").cloned(); + assert_eq!(endpoint, None); + } +} diff --git a/crates/platform-server/src/state.rs b/crates/platform-server/src/state.rs index 0966e5608..e3fc9d841 100644 --- a/crates/platform-server/src/state.rs +++ b/crates/platform-server/src/state.rs @@ -183,3 +183,209 @@ impl AppState { .unwrap_or(false) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::{AuthRole, AuthSession}; + + #[test] + fn test_validator_metrics_cache_new() { + let cache = MetricsCache::new(); + let metrics = cache.get_all(); + assert_eq!(metrics.len(), 0); + } + + #[test] + fn test_validator_metrics_cache_update_and_get() { + let cache = MetricsCache::new(); + let metrics = ValidatorMetrics { + cpu_percent: 50.0, + memory_used_mb: 1024, + memory_total_mb: 4096, + timestamp: 1234567890, + }; + + cache.update("hotkey1", metrics.clone()); + let all_metrics = cache.get_all(); + + assert_eq!(all_metrics.len(), 1); + assert_eq!(all_metrics[0].0, "hotkey1"); + assert_eq!(all_metrics[0].1.cpu_percent, 50.0); + assert_eq!(all_metrics[0].1.memory_used_mb, 1024); + } + + #[test] + fn test_validator_metrics_cache_multiple_validators() { + let cache = MetricsCache::new(); + + let metrics1 = ValidatorMetrics { + cpu_percent: 50.0, + memory_used_mb: 1024, + memory_total_mb: 4096, + timestamp: 1234567890, + }; + + let metrics2 = ValidatorMetrics { + cpu_percent: 75.0, + memory_used_mb: 2048, + memory_total_mb: 8192, + timestamp: 1234567891, + }; + + cache.update("hotkey1", metrics1); + cache.update("hotkey2", metrics2); + + let all_metrics = cache.get_all(); + assert_eq!(all_metrics.len(), 2); + } + + #[test] + fn test_validator_metrics_cache_update_existing() { + let cache = MetricsCache::new(); + + let metrics1 = ValidatorMetrics { + cpu_percent: 50.0, + memory_used_mb: 1024, + memory_total_mb: 4096, + timestamp: 1234567890, + }; + + let metrics2 = ValidatorMetrics { + cpu_percent: 60.0, + memory_used_mb: 2048, + memory_total_mb: 4096, + timestamp: 1234567891, + }; + + cache.update("hotkey1", metrics1); + cache.update("hotkey1", metrics2); + + let all_metrics = cache.get_all(); + assert_eq!(all_metrics.len(), 1); + assert_eq!(all_metrics[0].1.cpu_percent, 60.0); + assert_eq!(all_metrics[0].1.memory_used_mb, 2048); + } + + #[test] + fn test_metrics_cache_default() { + let cache = MetricsCache::default(); + let metrics = cache.get_all(); + assert_eq!(metrics.len(), 0); + } + + #[test] + fn test_default_tempo() { + assert_eq!(DEFAULT_TEMPO, 360); + } + + #[test] + fn test_app_state_tempo_operations() { + let state = create_test_state(); + + // Default tempo + assert_eq!(state.get_tempo(), DEFAULT_TEMPO); + + // Set and get tempo + state.set_tempo(500); + assert_eq!(state.get_tempo(), 500); + + state.set_tempo(1000); + assert_eq!(state.get_tempo(), 1000); + } + + #[test] + fn test_app_state_current_block_operations() { + let state = create_test_state(); + + // Default block + assert_eq!(state.get_current_block(), 0); + + // Set and get block + state.set_current_block(100); + assert_eq!(state.get_current_block(), 100); + + state.set_current_block(500); + assert_eq!(state.get_current_block(), 500); + } + + #[test] + fn test_app_state_validator_stake_whitelist() { + let state = create_test_state_with_whitelist(vec!["validator1".to_string()]); + + // Whitelisted validator should have high stake + let stake = state.get_validator_stake("validator1"); + assert_eq!(stake, 100_000_000_000_000); + + // Non-whitelisted validator should have 0 stake + let stake = state.get_validator_stake("validator2"); + assert_eq!(stake, 0); + } + + #[test] + fn test_app_state_validator_stake_no_metagraph() { + let state = create_test_state(); + + // Without metagraph or whitelist, stake should be 0 + let stake = state.get_validator_stake("any_validator"); + assert_eq!(stake, 0); + } + + #[test] + fn test_app_state_is_owner() { + let state = create_test_state_with_owner("owner_hotkey".to_string()); + + assert!(state.is_owner("owner_hotkey")); + assert!(!state.is_owner("other_hotkey")); + } + + #[test] + fn test_app_state_is_owner_none() { + let state = create_test_state(); + + assert!(!state.is_owner("any_hotkey")); + } + + #[test] + fn test_app_state_sessions() { + let state = create_test_state(); + + // Sessions should be empty initially + assert_eq!(state.sessions.len(), 0); + + // Insert a session + state.sessions.insert( + "token1".to_string(), + AuthSession { + hotkey: "validator1".to_string(), + role: AuthRole::Validator, + expires_at: 9999999999, + }, + ); + + assert_eq!(state.sessions.len(), 1); + assert!(state.sessions.contains_key("token1")); + } + + // Helper functions for tests + fn create_test_pool() -> crate::db::DbPool { + use deadpool_postgres::{Config, Runtime}; + use tokio_postgres::NoTls; + + let mut cfg = Config::new(); + cfg.url = Some("postgresql://localhost/test".to_string()); + cfg.create_pool(Some(Runtime::Tokio1), NoTls).unwrap() + } + + fn create_test_state() -> AppState { + AppState::new_dynamic(create_test_pool(), None, None, None) + } + + fn create_test_state_with_whitelist(whitelist: Vec) -> AppState { + AppState::new_dynamic_with_whitelist(create_test_pool(), None, None, None, whitelist) + } + + fn create_test_state_with_owner(owner: String) -> AppState { + AppState::new_dynamic(create_test_pool(), Some(owner), None, None) + } +}