diff --git a/crates/rpc-server/src/auth.rs b/crates/rpc-server/src/auth.rs index 189e5b10..d1ac04bc 100644 --- a/crates/rpc-server/src/auth.rs +++ b/crates/rpc-server/src/auth.rs @@ -96,4 +96,80 @@ mod tests { assert!(result.is_ok()); assert!(result.unwrap()); } + + #[test] + fn test_signature_verification_invalid_hotkey() { + let result = verify_validator_signature("invalid_hotkey", "message", "signature"); + assert!(result.is_err()); + } + + #[test] + fn test_signature_verification_invalid_signature_hex() { + let kp = Keypair::generate(); + let result = verify_validator_signature(&kp.hotkey().to_hex(), "message", "not_hex"); + assert!(result.is_err()); + } + + #[test] + fn test_signature_verification_wrong_signature() { + let kp1 = Keypair::generate(); + let kp2 = Keypair::generate(); + let message = "test:1234567890:nonce"; + let signed = kp1.sign(message.as_bytes()); + + // Use kp2's hotkey but kp1's signature - should fail + let hotkey_hex = kp2.hotkey().to_hex(); + let sig_hex = hex::encode(&signed.signature); + + let result = verify_validator_signature(&hotkey_hex, message, &sig_hex); + assert!(result.is_ok()); + assert!(!result.unwrap()); // Signature doesn't match + } + + #[test] + fn test_signature_verification_wrong_message() { + let kp = Keypair::generate(); + let message1 = "test:1234567890:nonce1"; + let message2 = "test:1234567890:nonce2"; + let signed = kp.sign(message1.as_bytes()); + + let hotkey_hex = kp.hotkey().to_hex(); + let sig_hex = hex::encode(&signed.signature); + + // Try to verify with different message - should fail + let result = verify_validator_signature(&hotkey_hex, message2, &sig_hex); + assert!(result.is_ok()); + assert!(!result.unwrap()); + } + + #[test] + fn test_verify_timestamp_edge_case() { + let now = chrono::Utc::now().timestamp(); + // Test exactly at 5 minute boundary + assert!(!verify_timestamp(now - 301)); // 5 minutes 1 second ago + assert!(verify_timestamp(now - 299)); // 4 minutes 59 seconds ago + } + + #[test] + fn test_verify_timestamp_future() { + let now = chrono::Utc::now().timestamp(); + assert!(verify_timestamp(now + 10)); // Future timestamp within 5 min should be valid + assert!(verify_timestamp(now + 299)); // Just under 5 minutes in future + } + + #[test] + fn test_signature_verification_invalid_length() { + let kp = Keypair::generate(); + let message = "test:1234567890:nonce"; + + // Test with signature that's too short (not 64 bytes) + let short_sig = hex::encode(&[0u8; 32]); // Only 32 bytes + let result = verify_validator_signature(&kp.hotkey().to_hex(), message, &short_sig); + assert!(result.is_err()); + + // Test with signature that's too long + let long_sig = hex::encode(&[0u8; 128]); // 128 bytes + let result = verify_validator_signature(&kp.hotkey().to_hex(), message, &long_sig); + assert!(result.is_err()); + } } diff --git a/crates/rpc-server/src/handlers.rs b/crates/rpc-server/src/handlers.rs index 91fe56e9..770e1401 100644 --- a/crates/rpc-server/src/handlers.rs +++ b/crates/rpc-server/src/handlers.rs @@ -480,3 +480,673 @@ pub async fn weight_reveal_handler( // Commitment verification and weight storage Json(RpcResponse::ok(true)) } +#[cfg(test)] +mod tests { + use super::*; + use platform_core::{Challenge, ChallengeConfig, ChallengeId, Keypair, NetworkConfig, Stake, ValidatorInfo}; + use platform_subnet_manager::BanList; + + fn create_test_state() -> Arc { + let kp = Keypair::generate(); + let chain_state = Arc::new(RwLock::new(ChainState::new( + kp.hotkey(), + NetworkConfig::default(), + ))); + let bans = Arc::new(RwLock::new(BanList::new())); + Arc::new(RpcState::new( + chain_state, + bans, + 1, + "Test-Chain".to_string(), + 1_000_000_000_000, + )) + } + + #[tokio::test] + async fn test_health_handler() { + let state = create_test_state(); + let response = health_handler(State(state)).await; + assert_eq!(response.0.data.as_ref().unwrap().status, "healthy"); + } + + #[tokio::test] + async fn test_status_handler() { + let state = create_test_state(); + let response = status_handler(State(state)).await; + let data = response.0.data.unwrap(); + assert_eq!(data.netuid, 1); + assert_eq!(data.name, "Test-Chain"); + } + + #[tokio::test] + async fn test_validators_handler_empty() { + let state = create_test_state(); + let params = PaginationParams::default(); + let response = validators_handler(State(state), Query(params)).await; + let validators = response.0.data.unwrap(); + assert!(validators.is_empty()); + } + + #[tokio::test] + async fn test_validators_handler_with_validators() { + let state = create_test_state(); + let kp = Keypair::generate(); + let info = ValidatorInfo::new(kp.hotkey(), Stake::new(5_000_000_000_000)); + + state.chain_state.write().validators.insert(kp.hotkey(), info); + + let params = PaginationParams::default(); + let response = validators_handler(State(state), Query(params)).await; + let validators = response.0.data.unwrap(); + assert_eq!(validators.len(), 1); + assert_eq!(validators[0].hotkey, kp.hotkey().to_hex()); + } + + #[tokio::test] + async fn test_challenges_handler_empty() { + let state = create_test_state(); + let response = challenges_handler(State(state)).await; + let challenges = response.0.data.unwrap(); + assert!(challenges.is_empty()); + } + + #[tokio::test] + async fn test_challenges_handler_with_challenges() { + let state = create_test_state(); + let kp = Keypair::generate(); + let challenge_id = ChallengeId::new(); + let config = ChallengeConfig { + mechanism_id: 1, + emission_weight: 1.0, + timeout_secs: 300, + max_memory_mb: 2048, + max_cpu_secs: 60, + min_validators: 1, + params_json: "{}".to_string(), + }; + let challenge = Challenge { + id: challenge_id, + name: "Test Challenge".to_string(), + description: "Test description".to_string(), + code_hash: "abc123".to_string(), + wasm_code: vec![], + is_active: true, + owner: kp.hotkey(), + config, + created_at: chrono::Utc::now(), + updated_at: chrono::Utc::now(), + }; + + state.chain_state.write().challenges.insert(challenge_id, challenge); + + let response = challenges_handler(State(state)).await; + let challenges = response.0.data.unwrap(); + assert_eq!(challenges.len(), 1); + assert_eq!(challenges[0].name, "Test Challenge"); + } + + #[tokio::test] + async fn test_challenge_handler_found() { + let state = create_test_state(); + let kp = Keypair::generate(); + let challenge_id = ChallengeId::new(); + let config = ChallengeConfig { + mechanism_id: 1, + emission_weight: 1.0, + timeout_secs: 300, + max_memory_mb: 2048, + max_cpu_secs: 60, + min_validators: 1, + params_json: "{}".to_string(), + }; + let challenge = Challenge { + id: challenge_id, + name: "Test Challenge".to_string(), + description: "Test description".to_string(), + code_hash: "abc123".to_string(), + wasm_code: vec![], + is_active: true, + owner: kp.hotkey(), + config, + created_at: chrono::Utc::now(), + updated_at: chrono::Utc::now(), + }; + + state.chain_state.write().challenges.insert(challenge_id, challenge); + + let response = challenge_handler(State(state), Path(challenge_id.to_string())).await; + assert!(response.is_ok()); + let json_response = response.unwrap(); + let challenge_data = json_response.0.data.unwrap(); + assert_eq!(challenge_data.name, "Test Challenge"); + } + + #[tokio::test] + async fn test_challenge_handler_not_found() { + let state = create_test_state(); + let response = challenge_handler(State(state), Path("nonexistent".to_string())).await; + assert!(response.is_err()); + assert_eq!(response.unwrap_err(), StatusCode::NOT_FOUND); + } + + #[tokio::test] + async fn test_challenge_handler_find_by_name() { + let state = create_test_state(); + let kp = Keypair::generate(); + let challenge_id = ChallengeId::new(); + let config = ChallengeConfig { + mechanism_id: 1, + emission_weight: 1.0, + timeout_secs: 300, + max_memory_mb: 2048, + max_cpu_secs: 60, + min_validators: 1, + params_json: "{}".to_string(), + }; + let challenge = Challenge { + id: challenge_id, + name: "test-challenge".to_string(), + description: "Test description".to_string(), + code_hash: "abc123".to_string(), + wasm_code: vec![], + is_active: true, + owner: kp.hotkey(), + config, + created_at: chrono::Utc::now(), + updated_at: chrono::Utc::now(), + }; + + state.chain_state.write().challenges.insert(challenge_id, challenge); + + let response = challenge_handler(State(state), Path("test-challenge".to_string())).await; + assert!(response.is_ok()); + } + + #[tokio::test] + async fn test_register_handler_invalid_signature() { + let state = create_test_state(); + let req = RegisterRequest { + hotkey: "0000000000000000000000000000000000000000000000000000000000000000".to_string(), + signature: "invalid".to_string(), + message: "register:1234567890:nonce".to_string(), + peer_id: None, + }; + + let response = register_handler(State(state), Json(req)).await; + assert!(!response.0.data.unwrap().accepted); + } + + #[tokio::test] + async fn test_register_handler_banned_validator() { + let state = create_test_state(); + let kp = Keypair::generate(); + + // Ban the validator + state.bans.write().ban_validator(&kp.hotkey(), "Test ban", "test"); + + let message = "register:1234567890:nonce"; + let signed = kp.sign(message.as_bytes()); + let req = RegisterRequest { + hotkey: kp.hotkey().to_hex(), + signature: hex::encode(&signed.signature), + message: message.to_string(), + peer_id: None, + }; + + let response = register_handler(State(state), Json(req)).await; + let register_resp = response.0.data.unwrap(); + assert!(!register_resp.accepted); + assert!(register_resp.reason.unwrap().contains("banned")); + } + + #[tokio::test] + async fn test_register_handler_already_registered() { + let state = create_test_state(); + let kp = Keypair::generate(); + + // Pre-register the validator + let info = ValidatorInfo::new(kp.hotkey(), Stake::new(5_000_000_000_000)); + state.chain_state.write().validators.insert(kp.hotkey(), info); + + let message = "register:1234567890:nonce"; + let signed = kp.sign(message.as_bytes()); + let req = RegisterRequest { + hotkey: kp.hotkey().to_hex(), + signature: hex::encode(&signed.signature), + message: message.to_string(), + peer_id: None, + }; + + let response = register_handler(State(state), Json(req)).await; + let register_resp = response.0.data.unwrap(); + assert!(register_resp.accepted); + assert!(register_resp.reason.unwrap().contains("Already registered")); + } + + #[tokio::test] + async fn test_register_handler_success() { + let state = create_test_state(); + let kp = Keypair::generate(); + + let message = "register:1234567890:nonce"; + let signed = kp.sign(message.as_bytes()); + let req = RegisterRequest { + hotkey: kp.hotkey().to_hex(), + signature: hex::encode(&signed.signature), + message: message.to_string(), + peer_id: None, + }; + + let response = register_handler(State(state.clone()), Json(req)).await; + let register_resp = response.0.data.unwrap(); + assert!(register_resp.accepted); + + // Verify validator was added + let chain = state.chain_state.read(); + assert!(chain.validators.contains_key(&kp.hotkey())); + } + + #[tokio::test] + async fn test_heartbeat_handler_invalid_hotkey() { + let state = create_test_state(); + let req = HeartbeatRequest { + hotkey: "invalid".to_string(), + signature: "sig".to_string(), + block_height: 100, + peer_id: None, + }; + + let response = heartbeat_handler(State(state), Json(req)).await; + assert!(response.0.error.is_some()); + } + + #[tokio::test] + async fn test_heartbeat_handler_not_registered() { + let state = create_test_state(); + let kp = Keypair::generate(); + let req = HeartbeatRequest { + hotkey: kp.hotkey().to_hex(), + signature: "sig".to_string(), + block_height: 100, + peer_id: Some("peer1".to_string()), + }; + + let response = heartbeat_handler(State(state), Json(req)).await; + let heartbeat_resp = response.0.data.unwrap(); + assert!(!heartbeat_resp.accepted); + } + + #[tokio::test] + async fn test_heartbeat_handler_success() { + let state = create_test_state(); + let kp = Keypair::generate(); + + // Register the validator + let info = ValidatorInfo::new(kp.hotkey(), Stake::new(5_000_000_000_000)); + state.chain_state.write().validators.insert(kp.hotkey(), info); + + let req = HeartbeatRequest { + hotkey: kp.hotkey().to_hex(), + signature: "sig".to_string(), + block_height: 100, + peer_id: Some("peer1".to_string()), + }; + + let response = heartbeat_handler(State(state.clone()), Json(req)).await; + let heartbeat_resp = response.0.data.unwrap(); + assert!(heartbeat_resp.accepted); + assert_eq!(heartbeat_resp.current_block, 0); + + // Verify peer_id was updated + let chain = state.chain_state.read(); + let validator = chain.validators.get(&kp.hotkey()).unwrap(); + assert_eq!(validator.peer_id, Some("peer1".to_string())); + } + + #[tokio::test] + async fn test_jobs_handler_empty() { + let state = create_test_state(); + let params = PaginationParams::default(); + let response = jobs_handler(State(state), Query(params)).await; + let jobs = response.0.data.unwrap(); + assert!(jobs.is_empty()); + } + + #[tokio::test] + async fn test_jobs_handler_with_pagination() { + let state = create_test_state(); + + // Add some jobs + let mut chain = state.chain_state.write(); + for i in 0..5 { + let job = platform_core::Job { + id: uuid::Uuid::new_v4(), + challenge_id: ChallengeId::new(), + agent_hash: format!("hash{}", i), + status: platform_core::JobStatus::Pending, + created_at: chrono::Utc::now(), + assigned_validator: None, + result: None, + }; + chain.pending_jobs.push(job); + } + drop(chain); + + let params = PaginationParams { + offset: Some(1), + limit: Some(2), + }; + let response = jobs_handler(State(state), Query(params)).await; + let jobs = response.0.data.unwrap(); + assert_eq!(jobs.len(), 2); + } + + #[tokio::test] + async fn test_job_result_handler_invalid_job_id() { + let state = create_test_state(); + let kp = Keypair::generate(); + + // Sign the message correctly first + let job_id = "not-a-uuid"; + let message = format!("result:{}:0.9", job_id); + let signed = kp.sign(message.as_bytes()); + + let req = JobResultRequest { + job_id: job_id.to_string(), + hotkey: kp.hotkey().to_hex(), + signature: hex::encode(&signed.signature), + score: 0.9, + metadata: None, + }; + + let response = job_result_handler(State(state), Path(job_id.to_string()), Json(req)).await; + // Invalid job ID (not a UUID) returns error through RpcResponse::error + assert!(!response.0.success); + assert!(response.0.error.is_some()); + } + + #[tokio::test] + async fn test_job_result_handler_invalid_signature() { + let state = create_test_state(); + let job_id = uuid::Uuid::new_v4(); + let req = JobResultRequest { + job_id: job_id.to_string(), + hotkey: "0000000000000000000000000000000000000000000000000000000000000000".to_string(), + signature: "invalid".to_string(), + score: 0.9, + metadata: None, + }; + + let response = job_result_handler(State(state), Path(job_id.to_string()), Json(req)).await; + let result_resp = response.0.data.unwrap(); + assert!(!result_resp.accepted); + } + + #[tokio::test] + async fn test_epoch_handler() { + let state = create_test_state(); + let response = epoch_handler(State(state)).await; + let epoch = response.0.data.unwrap(); + assert_eq!(epoch.current_epoch, 0); + assert_eq!(epoch.blocks_per_epoch, 100); + assert_eq!(epoch.phase, "evaluation"); + } + + #[tokio::test] + async fn test_epoch_handler_commit_phase() { + let state = create_test_state(); + + // Set block height to commit phase (75-87) + state.chain_state.write().block_height = 80; + + let response = epoch_handler(State(state)).await; + let epoch = response.0.data.unwrap(); + assert_eq!(epoch.phase, "commit"); + } + + #[tokio::test] + async fn test_epoch_handler_reveal_phase() { + let state = create_test_state(); + + // Set block height to reveal phase (88-99) + state.chain_state.write().block_height = 90; + + let response = epoch_handler(State(state)).await; + let epoch = response.0.data.unwrap(); + assert_eq!(epoch.phase, "reveal"); + } + + #[tokio::test] + async fn test_sync_handler() { + let state = create_test_state(); + let response = sync_handler(State(state)).await; + let sync = response.0.data.unwrap(); + assert_eq!(sync.block_height, 0); + assert_eq!(sync.epoch, 0); + assert!(sync.validators.is_empty()); + assert!(sync.challenges.is_empty()); + } + + #[tokio::test] + async fn test_weight_commit_handler_invalid_signature() { + let state = create_test_state(); + let req = WeightCommitRequest { + hotkey: "0000000000000000000000000000000000000000000000000000000000000000".to_string(), + signature: "invalid".to_string(), + challenge_id: "challenge1".to_string(), + commitment_hash: "hash123".to_string(), + epoch: 1, + }; + + let response = weight_commit_handler(State(state), Json(req)).await; + assert!(response.0.error.is_some()); + } + + #[tokio::test] + async fn test_weight_reveal_handler_invalid_signature() { + let state = create_test_state(); + let req = WeightRevealRequest { + hotkey: "0000000000000000000000000000000000000000000000000000000000000000".to_string(), + signature: "invalid".to_string(), + challenge_id: "challenge1".to_string(), + weights: vec![], + salt: "salt123".to_string(), + epoch: 1, + }; + + let response = weight_reveal_handler(State(state), Json(req)).await; + assert!(response.0.error.is_some()); + } + + #[tokio::test] + async fn test_register_handler_invalid_hotkey_format() { + let state = create_test_state(); + let kp = Keypair::generate(); + let message = "register:1234567890:nonce"; + let signed = kp.sign(message.as_bytes()); + + let req = RegisterRequest { + hotkey: "invalid-hotkey-format".to_string(), + signature: hex::encode(&signed.signature), + message: message.to_string(), + peer_id: None, + }; + + let response = register_handler(State(state), Json(req)).await; + let register_resp = response.0.data.unwrap(); + assert!(!register_resp.accepted); + assert!(register_resp.reason.unwrap().contains("Invalid hotkey format")); + } + + #[tokio::test] + async fn test_register_handler_signature_error() { + let state = create_test_state(); + let req = RegisterRequest { + hotkey: "invalid".to_string(), + signature: "sig".to_string(), + message: "register:1234567890:nonce".to_string(), + peer_id: None, + }; + + let response = register_handler(State(state), Json(req)).await; + let register_resp = response.0.data.unwrap(); + assert!(!register_resp.accepted); + assert!(register_resp.reason.unwrap().contains("Auth error")); + } + + #[tokio::test] + async fn test_register_handler_add_validator_error() { + let state = create_test_state(); + let kp = Keypair::generate(); + + // Fill validators to max capacity to trigger error + let mut chain = state.chain_state.write(); + for i in 0..chain.config.max_validators { + let temp_kp = Keypair::generate(); + let info = ValidatorInfo::new(temp_kp.hotkey(), Stake::new(5_000_000_000_000)); + chain.validators.insert(temp_kp.hotkey(), info); + } + drop(chain); + + let message = "register:1234567890:nonce"; + let signed = kp.sign(message.as_bytes()); + let req = RegisterRequest { + hotkey: kp.hotkey().to_hex(), + signature: hex::encode(&signed.signature), + message: message.to_string(), + peer_id: None, + }; + + let response = register_handler(State(state), Json(req)).await; + let register_resp = response.0.data.unwrap(); + assert!(!register_resp.accepted); + assert!(register_resp.reason.unwrap().contains("Registration failed")); + } + + #[tokio::test] + async fn test_job_result_handler_job_not_found() { + let state = create_test_state(); + let kp = Keypair::generate(); + let job_id = uuid::Uuid::new_v4(); + + let message = format!("result:{}:0.9", job_id); + let signed = kp.sign(message.as_bytes()); + + let req = JobResultRequest { + job_id: job_id.to_string(), + hotkey: kp.hotkey().to_hex(), + signature: hex::encode(&signed.signature), + score: 0.9, + metadata: None, + }; + + let response = job_result_handler(State(state), Path(job_id.to_string()), Json(req)).await; + let result_resp = response.0.data.unwrap(); + assert!(!result_resp.accepted); + } + + #[tokio::test] + async fn test_job_result_handler_success() { + let state = create_test_state(); + let kp = Keypair::generate(); + + // Add a job first + let job_id = uuid::Uuid::new_v4(); + let job = platform_core::Job { + id: job_id, + challenge_id: ChallengeId::new(), + agent_hash: "hash123".to_string(), + status: platform_core::JobStatus::Pending, + created_at: chrono::Utc::now(), + assigned_validator: None, + result: None, + }; + state.chain_state.write().pending_jobs.push(job); + + let message = format!("result:{}:0.95", job_id); + let signed = kp.sign(message.as_bytes()); + + let req = JobResultRequest { + job_id: job_id.to_string(), + hotkey: kp.hotkey().to_hex(), + signature: hex::encode(&signed.signature), + score: 0.95, + metadata: Some(serde_json::json!({"test": "data"})), + }; + + let response = job_result_handler(State(state.clone()), Path(job_id.to_string()), Json(req)).await; + let result_resp = response.0.data.unwrap(); + assert!(result_resp.accepted); + + // Verify job was updated + let chain = state.chain_state.read(); + let updated_job = chain.pending_jobs.iter().find(|j| j.id == job_id).unwrap(); + assert!(matches!(updated_job.status, platform_core::JobStatus::Completed)); + assert!(updated_job.result.is_some()); + } + + #[tokio::test] + async fn test_weight_commit_handler_success() { + let state = create_test_state(); + let kp = Keypair::generate(); + + let challenge_id = "challenge1"; + let epoch = 5; + let commitment_hash = "abc123"; + let message = format!("commit:{}:{}:{}", challenge_id, epoch, commitment_hash); + let signed = kp.sign(message.as_bytes()); + + let req = WeightCommitRequest { + hotkey: kp.hotkey().to_hex(), + signature: hex::encode(&signed.signature), + challenge_id: challenge_id.to_string(), + commitment_hash: commitment_hash.to_string(), + epoch, + }; + + let response = weight_commit_handler(State(state), Json(req)).await; + assert!(response.0.success); + assert_eq!(response.0.data, Some(true)); + } + + #[tokio::test] + async fn test_weight_reveal_handler_success() { + let state = create_test_state(); + let kp = Keypair::generate(); + + let challenge_id = "challenge1"; + let epoch = 5; + let salt = "salt123"; + let weights = vec![ + WeightEntry { + hotkey: "hk1".to_string(), + weight: 0.6, + }, + WeightEntry { + hotkey: "hk2".to_string(), + weight: 0.4, + }, + ]; + + let weights_str: String = weights + .iter() + .map(|w| format!("{}:{}", w.hotkey, w.weight)) + .collect::>() + .join(","); + let message = format!("reveal:{}:{}:{}:{}", challenge_id, epoch, weights_str, salt); + let signed = kp.sign(message.as_bytes()); + + let req = WeightRevealRequest { + hotkey: kp.hotkey().to_hex(), + signature: hex::encode(&signed.signature), + challenge_id: challenge_id.to_string(), + weights, + salt: salt.to_string(), + epoch, + }; + + let response = weight_reveal_handler(State(state), Json(req)).await; + assert!(response.0.success); + assert_eq!(response.0.data, Some(true)); + } +} \ No newline at end of file diff --git a/crates/rpc-server/src/jsonrpc.rs b/crates/rpc-server/src/jsonrpc.rs index 7dc60d4f..2114eaf9 100644 --- a/crates/rpc-server/src/jsonrpc.rs +++ b/crates/rpc-server/src/jsonrpc.rs @@ -1938,4 +1938,740 @@ mod tests { let resp = handler.handle(req); assert!(resp.result.is_some()); } + + #[test] + fn test_validator_list() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "validator_list".to_string(), + params: Value::Null, + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + let result = resp.result.unwrap(); + assert!(result.get("validators").is_some()); + } + + #[test] + fn test_validator_list_with_pagination() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "validator_list".to_string(), + params: json!([10, 50]), // offset, limit + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + let result = resp.result.unwrap(); + assert_eq!(result["offset"], 10); + assert_eq!(result["limit"], 50); + } + + #[test] + fn test_validator_count() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "validator_count".to_string(), + params: Value::Null, + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + } + + #[test] + fn test_metagraph_hotkeys() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "metagraph_hotkeys".to_string(), + params: Value::Null, + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + let result = resp.result.unwrap(); + assert!(result.get("hotkeys").is_some()); + assert!(result.get("count").is_some()); + } + + #[test] + fn test_metagraph_is_registered_invalid_hotkey() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "metagraph_isRegistered".to_string(), + params: json!(["invalid_hotkey"]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.error.is_some()); + } + + #[test] + fn test_metagraph_is_registered_missing_param() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "metagraph_isRegistered".to_string(), + params: json!([]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.error.is_some()); + assert_eq!(resp.error.unwrap().code, INVALID_PARAMS); + } + + #[test] + fn test_challenge_list() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "challenge_list".to_string(), + params: Value::Null, + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + let result = resp.result.unwrap(); + assert!(result.get("challenges").is_some()); + } + + #[test] + fn test_challenge_list_only_active() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "challenge_list".to_string(), + params: json!({"onlyActive": true}), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + } + + #[test] + fn test_challenge_get_not_found() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "challenge_get".to_string(), + params: json!(["nonexistent"]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.error.is_some()); + assert_eq!(resp.error.unwrap().code, CHALLENGE_NOT_FOUND); + } + + #[test] + fn test_challenge_get_missing_param() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "challenge_get".to_string(), + params: json!([]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.error.is_some()); + assert_eq!(resp.error.unwrap().code, INVALID_PARAMS); + } + + #[test] + fn test_challenge_get_routes_no_routes() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "challenge_getRoutes".to_string(), + params: json!(["test-challenge"]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + let result = resp.result.unwrap(); + assert_eq!(result["routesCount"], 0); + } + + #[test] + fn test_challenge_list_all_routes() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "challenge_listAllRoutes".to_string(), + params: Value::Null, + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + let result = resp.result.unwrap(); + assert!(result.get("routes").is_some()); + } + + #[test] + fn test_job_list() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "job_list".to_string(), + params: Value::Null, + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + let result = resp.result.unwrap(); + assert!(result.get("jobs").is_some()); + } + + #[test] + fn test_job_list_with_filter() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "job_list".to_string(), + params: json!([0, 100, "pending"]), // offset, limit, status + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + } + + #[test] + fn test_job_get_invalid_id() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "job_get".to_string(), + params: json!(["not-a-uuid"]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.error.is_some()); + assert_eq!(resp.error.unwrap().code, INVALID_PARAMS); + } + + #[test] + fn test_job_get_not_found() { + let handler = create_handler(); + let job_id = uuid::Uuid::new_v4().to_string(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "job_get".to_string(), + params: json!([job_id]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.error.is_some()); + assert_eq!(resp.error.unwrap().code, JOB_NOT_FOUND); + } + + #[test] + fn test_job_get_missing_param() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "job_get".to_string(), + params: json!([]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.error.is_some()); + assert_eq!(resp.error.unwrap().code, INVALID_PARAMS); + } + + #[test] + fn test_epoch_current() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "epoch_current".to_string(), + params: Value::Null, + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + let result = resp.result.unwrap(); + assert!(result.get("epochNumber").is_some()); + assert!(result.get("phase").is_some()); + assert!(result.get("blocksPerEpoch").is_some()); + } + + #[test] + fn test_epoch_get_phase() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "epoch_getPhase".to_string(), + params: Value::Null, + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + let phase = resp.result.unwrap(); + assert!(phase.is_string()); + } + + #[test] + fn test_state_get_storage_invalid_key() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "state_getStorage".to_string(), + params: json!(["unknownKey"]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.error.is_some()); + assert_eq!(resp.error.unwrap().code, INVALID_PARAMS); + } + + #[test] + fn test_state_get_storage_block_height() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "state_getStorage".to_string(), + params: json!(["blockHeight"]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + } + + #[test] + fn test_state_get_storage_missing_key() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "state_getStorage".to_string(), + params: json!([]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.error.is_some()); + assert_eq!(resp.error.unwrap().code, INVALID_PARAMS); + } + + #[test] + fn test_state_get_storage_validator_key() { + let handler = create_handler(); + let kp = Keypair::generate(); + let key = format!("validator:{}", kp.hotkey().to_hex()); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "state_getStorage".to_string(), + params: json!([key]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + } + + #[test] + fn test_state_get_storage_challenge_key() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "state_getStorage".to_string(), + params: json!(["challenge:test"]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + } + + #[test] + fn test_state_get_metadata() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "state_getMetadata".to_string(), + params: Value::Null, + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + let result = resp.result.unwrap(); + assert!(result.get("pallets").is_some()); + } + + #[test] + fn test_monitor_get_challenge_health() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "monitor_getChallengeHealth".to_string(), + params: Value::Null, + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + let result = resp.result.unwrap(); + assert!(result.get("challenges").is_some()); + } + + #[test] + fn test_monitor_get_challenge_logs() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "monitor_getChallengeLogs".to_string(), + params: json!(["test-challenge", 100]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + } + + #[test] + fn test_monitor_get_challenge_logs_missing_param() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "monitor_getChallengeLogs".to_string(), + params: json!([]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.error.is_some()); + assert_eq!(resp.error.unwrap().code, INVALID_PARAMS); + } + + #[test] + fn test_sudo_submit_missing_param() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "sudo_submit".to_string(), + params: json!([]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.error.is_some()); + assert_eq!(resp.error.unwrap().code, INVALID_PARAMS); + } + + #[test] + fn test_sudo_submit_invalid_hex() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "sudo_submit".to_string(), + params: json!(["not-hex"]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.error.is_some()); + assert_eq!(resp.error.unwrap().code, INVALID_PARAMS); + } + + #[test] + fn test_json_rpc_error_with_data() { + let resp = JsonRpcResponse::error_with_data( + json!(1), + -32000, + "Custom error", + json!({"detail": "extra info"}), + ); + assert!(resp.error.is_some()); + let error = resp.error.unwrap(); + assert_eq!(error.code, -32000); + assert!(error.data.is_some()); + } + + #[test] + fn test_normalize_challenge_name() { + assert_eq!( + RpcHandler::normalize_challenge_name("Test Challenge"), + "test-challenge" + ); + assert_eq!( + RpcHandler::normalize_challenge_name("My_Cool_Challenge"), + "my-cool-challenge" + ); + assert_eq!( + RpcHandler::normalize_challenge_name(" Spaces "), + "spaces" + ); + assert_eq!( + RpcHandler::normalize_challenge_name("Special!@#$%Chars"), + "specialchars" + ); + } + + #[test] + fn test_register_challenge_routes() { + let handler = create_handler(); + use platform_challenge_sdk::ChallengeRoute; + + let routes = vec![ + ChallengeRoute::get("/test", "Test route"), + ChallengeRoute::post("/submit", "Submit route"), + ]; + + handler.register_challenge_routes("test-challenge", routes); + + let registered = handler.challenge_routes.read(); + assert!(registered.contains_key("test-challenge")); + assert_eq!(registered.get("test-challenge").unwrap().len(), 2); + } + + #[test] + fn test_unregister_challenge_routes() { + let handler = create_handler(); + use platform_challenge_sdk::ChallengeRoute; + + let routes = vec![ChallengeRoute::get("/test", "Test route")]; + handler.register_challenge_routes("test-challenge", routes); + + handler.unregister_challenge_routes("test-challenge"); + + let registered = handler.challenge_routes.read(); + assert!(!registered.contains_key("test-challenge")); + } + + #[test] + fn test_get_all_challenge_routes() { + let handler = create_handler(); + use platform_challenge_sdk::ChallengeRoute; + + let routes = vec![ChallengeRoute::get("/test", "Test route")]; + handler.register_challenge_routes("test-challenge", routes); + + let all_routes = handler.get_all_challenge_routes(); + assert_eq!(all_routes.len(), 1); + } + + #[test] + fn test_set_keypair() { + let handler = create_handler(); + let kp = Keypair::generate(); + handler.set_keypair(kp.clone()); + + let stored = handler.keypair.read(); + assert!(stored.is_some()); + } + + #[test] + fn test_json_rpc_request_default_params() { + let json_str = r#"{"jsonrpc":"2.0","method":"test","id":1}"#; + let req: JsonRpcRequest = serde_json::from_str(json_str).unwrap(); + assert_eq!(req.params, Value::Null); + } + + #[test] + fn test_get_param_helpers() { + let handler = create_handler(); + let params = json!([10, "test", true]); + + assert_eq!(handler.get_param_u64(¶ms, 0, "x"), Some(10)); + assert_eq!(handler.get_param_str(¶ms, 1, "y"), Some("test".to_string())); + } + + #[test] + fn test_set_broadcast_tx() { + let handler = create_handler(); + let (tx, _rx) = tokio::sync::mpsc::unbounded_channel(); + handler.set_broadcast_tx(tx); + assert!(handler.broadcast_tx.read().is_some()); + } + + #[test] + fn test_set_orchestrator_tx() { + let handler = create_handler(); + let (tx, _rx) = tokio::sync::mpsc::unbounded_channel(); + handler.set_orchestrator_tx(tx); + assert!(handler.orchestrator_tx.read().is_some()); + } + + #[test] + fn test_set_route_handler() { + let handler = create_handler(); + let route_handler: ChallengeRouteHandler = Arc::new(|_challenge_id, _req| { + Box::pin(async move { + ChallengeRouteResponse { + status: 200, + body: json!({"success": true}), + headers: std::collections::HashMap::new(), + } + }) + }); + handler.set_route_handler(route_handler); + assert!(handler.route_handler.read().is_some()); + } + + #[test] + fn test_chain_get_block_invalid_number() { + let handler = create_handler(); + // Request a block that doesn't exist + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "chain_getBlock".to_string(), + params: json!([999]), // Block that doesn't exist + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.error.is_some()); + } + + #[test] + fn test_chain_get_block_hash_invalid() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "chain_getBlockHash".to_string(), + params: json!([999]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + assert_eq!(resp.result.unwrap(), serde_json::Value::Null); + } + + #[test] + fn test_state_get_storage_validator_key_invalid_hotkey() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "state_getStorage".to_string(), + params: json!(["validator:invalid_hex"]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.error.is_some()); + } + + #[test] + fn test_validator_get_invalid_hotkey_format() { + let handler = create_handler(); + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "validator_get".to_string(), + params: json!(["not_a_valid_hotkey"]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.error.is_some()); + assert_eq!(resp.error.unwrap().code, INVALID_PARAMS); + } + + #[test] + fn test_metagraph_is_registered_with_valid_hotkey() { + let handler = create_handler(); + let kp = Keypair::generate(); + + // Add hotkey to registered_hotkeys + { + let mut chain = handler.chain_state.write(); + chain.registered_hotkeys.insert(kp.hotkey()); + } + + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "metagraph_isRegistered".to_string(), + params: json!([kp.hotkey().to_hex()]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + let result = resp.result.unwrap(); + assert_eq!(result["isRegistered"], true); + } + + #[test] + fn test_challenge_get_with_routes() { + let handler = create_handler(); + use platform_challenge_sdk::ChallengeRoute; + + // Add a challenge + let kp = Keypair::generate(); + let challenge_id = platform_core::ChallengeId::new(); + let config = platform_core::ChallengeConfig::default(); + let challenge = platform_core::Challenge { + id: challenge_id, + name: "test-challenge".to_string(), + description: "Test description".to_string(), + wasm_code: vec![1, 2, 3], + code_hash: "abc123".to_string(), + owner: kp.hotkey(), + config, + created_at: chrono::Utc::now(), + updated_at: chrono::Utc::now(), + is_active: true, + }; + + handler.chain_state.write().challenges.insert(challenge_id, challenge); + + // Register routes for the challenge + let routes = vec![ + ChallengeRoute::get("/test", "Test route"), + ChallengeRoute::post("/submit", "Submit route"), + ]; + handler.register_challenge_routes(&challenge_id.to_string(), routes); + + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "challenge_get".to_string(), + params: json!([challenge_id.to_string()]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + let result = resp.result.unwrap(); + assert_eq!(result["routes"].as_array().unwrap().len(), 2); + } + + #[test] + fn test_challenge_get_routes_by_name() { + let handler = create_handler(); + use platform_challenge_sdk::ChallengeRoute; + + // Add a challenge + let kp = Keypair::generate(); + let challenge_id = platform_core::ChallengeId::new(); + let config = platform_core::ChallengeConfig::default(); + let challenge = platform_core::Challenge { + id: challenge_id, + name: "my-challenge".to_string(), + description: "Test description".to_string(), + wasm_code: vec![], + code_hash: "abc123".to_string(), + owner: kp.hotkey(), + config, + created_at: chrono::Utc::now(), + updated_at: chrono::Utc::now(), + is_active: true, + }; + + handler.chain_state.write().challenges.insert(challenge_id, challenge); + + // Register routes + let routes = vec![ChallengeRoute::get("/status", "Status route")]; + handler.register_challenge_routes(&challenge_id.to_string(), routes); + + // Query by name instead of ID + let req = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: "challenge_getRoutes".to_string(), + params: json!(["my-challenge"]), + id: json!(1), + }; + let resp = handler.handle(req); + assert!(resp.result.is_some()); + let result = resp.result.unwrap(); + assert_eq!(result["routesCount"], 1); + } + + #[test] + fn test_register_empty_routes() { + let handler = create_handler(); + // Registering empty routes should be a no-op + handler.register_challenge_routes("test-challenge", vec![]); + let routes = handler.challenge_routes.read(); + assert!(!routes.contains_key("test-challenge")); + } } diff --git a/crates/rpc-server/src/server.rs b/crates/rpc-server/src/server.rs index bcfc25f7..9dedad4a 100644 --- a/crates/rpc-server/src/server.rs +++ b/crates/rpc-server/src/server.rs @@ -556,6 +556,7 @@ pub async fn start_rpc_server( mod tests { use super::*; use platform_core::{Keypair, NetworkConfig}; + use serde_json::json; #[test] fn test_rpc_config_default() { @@ -579,4 +580,115 @@ mod tests { let router = server.router(); // Router created successfully } + + #[test] + fn test_rpc_server_rpc_handler() { + 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(); + assert_eq!(handler.netuid, 1); + } + + #[test] + fn test_rpc_server_addr() { + 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:9999".parse().unwrap(), + netuid: 42, + name: "Test".to_string(), + min_stake: 1000, + cors_enabled: false, + }; + let server = RpcServer::new(config, state, bans); + + assert_eq!(server.addr().port(), 9999); + } + + #[tokio::test] + async fn test_handle_single_request_invalid_json() { + let kp = Keypair::generate(); + let state = Arc::new(RwLock::new(ChainState::new( + kp.hotkey(), + NetworkConfig::default(), + ))); + let handler = Arc::new(RpcHandler::new(state, 1)); + + let invalid_body = json!({"method": "test"}); // Missing required fields + let (status, resp) = handle_single_request(invalid_body, &handler); + + assert_eq!(status, StatusCode::BAD_REQUEST); + assert!(resp.0.error.is_some()); + assert_eq!(resp.0.error.unwrap().code, PARSE_ERROR); + } + + #[tokio::test] + async fn test_handle_single_request_invalid_jsonrpc_version() { + let kp = Keypair::generate(); + let state = Arc::new(RwLock::new(ChainState::new( + kp.hotkey(), + NetworkConfig::default(), + ))); + let handler = Arc::new(RpcHandler::new(state, 1)); + + let body = json!({ + "jsonrpc": "1.0", // Wrong version + "method": "test", + "params": null, + "id": 1 + }); + let (status, resp) = handle_single_request(body, &handler); + + assert_eq!(status, StatusCode::BAD_REQUEST); + assert!(resp.0.error.is_some()); + } + + #[tokio::test] + async fn test_handle_single_request_success() { + let kp = Keypair::generate(); + let state = Arc::new(RwLock::new(ChainState::new( + kp.hotkey(), + NetworkConfig::default(), + ))); + let handler = Arc::new(RpcHandler::new(state, 1)); + + let body = json!({ + "jsonrpc": "2.0", + "method": "system_version", + "params": null, + "id": 1 + }); + let (status, resp) = handle_single_request(body, &handler); + + assert_eq!(status, StatusCode::OK); + assert!(resp.0.result.is_some()); + } + + #[test] + fn test_rpc_config_custom() { + let config = RpcConfig { + addr: "0.0.0.0:3000".parse().unwrap(), + netuid: 99, + name: "CustomChain".to_string(), + min_stake: 5_000_000_000_000, + cors_enabled: false, + }; + + assert_eq!(config.netuid, 99); + assert_eq!(config.name, "CustomChain"); + assert!(!config.cors_enabled); + } } diff --git a/crates/rpc-server/src/types.rs b/crates/rpc-server/src/types.rs index 99dbfdb7..311275f3 100644 --- a/crates/rpc-server/src/types.rs +++ b/crates/rpc-server/src/types.rs @@ -375,4 +375,143 @@ mod tests { assert_eq!(resp.block_height, 1000); assert!(resp.validators.is_empty()); } + + #[test] + fn test_rpc_response_ok() { + let resp: RpcResponse = RpcResponse::ok("test data".to_string()); + assert!(resp.data.is_some()); + assert!(resp.error.is_none()); + assert_eq!(resp.data.unwrap(), "test data"); + } + + #[test] + fn test_rpc_response_error() { + let resp: RpcResponse = RpcResponse::error("error message"); + assert!(resp.data.is_none()); + assert!(resp.error.is_some()); + assert_eq!(resp.error.unwrap(), "error message"); + } + + #[test] + fn test_health_response_serde() { + let health = HealthResponse { + status: "healthy".to_string(), + version: "1.0".to_string(), + uptime_secs: 100, + }; + let json = serde_json::to_string(&health).unwrap(); + let parsed: HealthResponse = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.status, "healthy"); + } + + #[test] + fn test_register_request_serde() { + let req = RegisterRequest { + hotkey: "hotkey123".to_string(), + signature: "sig".to_string(), + message: "msg".to_string(), + peer_id: Some("peer123".to_string()), + }; + let json = serde_json::to_string(&req).unwrap(); + let parsed: RegisterRequest = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.hotkey, "hotkey123"); + assert!(parsed.peer_id.is_some()); + } + + #[test] + fn test_register_response_accepted() { + let resp = RegisterResponse { + accepted: true, + uid: Some(5), + reason: None, + }; + assert!(resp.accepted); + assert_eq!(resp.uid, Some(5)); + } + + #[test] + fn test_register_response_rejected() { + let resp = RegisterResponse { + accepted: false, + uid: None, + reason: Some("Insufficient stake".to_string()), + }; + assert!(!resp.accepted); + assert!(resp.reason.is_some()); + } + + #[test] + fn test_job_result_request_serde() { + let req = JobResultRequest { + job_id: "job-1".to_string(), + hotkey: "hk".to_string(), + signature: "sig".to_string(), + score: 0.85, + metadata: Some(serde_json::json!({"key": "value"})), + }; + let json = serde_json::to_string(&req).unwrap(); + let parsed: JobResultRequest = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.score, 0.85); + assert!(parsed.metadata.is_some()); + } + + #[test] + fn test_job_result_response() { + let resp = JobResultResponse { + accepted: true, + job_id: "job-123".to_string(), + }; + assert!(resp.accepted); + assert_eq!(resp.job_id, "job-123"); + } + + #[test] + fn test_weight_commit_request_serde() { + let req = WeightCommitRequest { + hotkey: "hk".to_string(), + signature: "sig".to_string(), + challenge_id: "ch1".to_string(), + commitment_hash: "hash".to_string(), + epoch: 10, + }; + let json = serde_json::to_string(&req).unwrap(); + let parsed: WeightCommitRequest = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.epoch, 10); + assert_eq!(parsed.challenge_id, "ch1"); + } + + #[test] + fn test_weight_reveal_request_serde() { + let req = WeightRevealRequest { + hotkey: "hk".to_string(), + signature: "sig".to_string(), + challenge_id: "ch1".to_string(), + weights: vec![ + WeightEntry { + hotkey: "h1".to_string(), + weight: 0.6, + }, + WeightEntry { + hotkey: "h2".to_string(), + weight: 0.4, + }, + ], + salt: "salt123".to_string(), + epoch: 10, + }; + let json = serde_json::to_string(&req).unwrap(); + let parsed: WeightRevealRequest = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.weights.len(), 2); + assert_eq!(parsed.salt, "salt123"); + } + + #[test] + fn test_pagination_params_custom() { + let params = PaginationParams { + offset: Some(50), + limit: Some(200), + }; + assert_eq!(params.offset, Some(50)); + assert_eq!(params.limit, Some(200)); + } }