// file: crates/ksp-onchain-transport-lib/unit_tests/json_rpc.rs // version: 3 #[test] fn request_serialization_matches_json_rpc_2_0_shape() { let request = crate::JsonRpcRequest::new(7, "getBalance", std::vec![serde_json::json!("Address111"), serde_json::json!({"commitment":"confirmed"})]) .expect("valid request must construct"); let encoded = request.to_json_string().expect("serializable request must encode"); let value: serde_json::Value = serde_json::from_str(encoded.as_str()).expect("encoded request must remain JSON"); assert_eq!(value["jsonrpc"], serde_json::json!("2.0")); assert_eq!(value["id"], serde_json::json!(7)); assert_eq!(value["method"], serde_json::json!("getBalance")); assert_eq!(value["params"].as_array().expect("params must be an array").len(), 2); } #[test] fn request_rejects_empty_or_untrimmed_method() { assert!(crate::JsonRpcRequest::new(1, "", std::vec![]).is_err()); assert!(crate::JsonRpcRequest::new(1, " getHealth", std::vec![]).is_err()); } #[test] fn response_parser_preserves_null_success_result() { let response = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":null,"id":9}"#, 9).expect("null result is a valid success payload"); assert!(matches!(&response, crate::JsonRpcResponse::Success(_)), "success response must not parse as error"); if let crate::JsonRpcResponse::Success(success) = response { assert!(success.result().is_null()); } } #[test] fn response_parser_preserves_rpc_error_payload() { let response = crate::parse_json_rpc_response_text( r#"{"jsonrpc":"2.0","error":{"code":-32005,"message":"Node is unhealthy","data":{"numSlotsBehind":12}},"id":4}"#, 4, ) .expect("valid JSON-RPC error envelope must parse"); assert!(matches!(&response, crate::JsonRpcResponse::Error(_)), "RPC error response must not parse as success"); if let crate::JsonRpcResponse::Error(error_response) = response { assert_eq!(error_response.error().code(), -32005); assert_eq!(error_response.error().message(), "Node is unhealthy"); assert_eq!(error_response.error().data(), std::option::Option::Some(&serde_json::json!({"numSlotsBehind":12}))); } } #[test] fn response_parser_rejects_id_mismatch() { let error = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":"ok","id":2}"#, 1).expect_err("mismatched id must fail"); assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID); } #[test] fn response_parser_rejects_wrong_protocol_version() { let error = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"1.0","result":"ok","id":1}"#, 1).expect_err("wrong version must fail"); assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID); } #[test] fn response_parser_rejects_both_result_and_error() { let error = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":"ok","error":{"code":-1,"message":"bad"},"id":1}"#, 1) .expect_err("mutually exclusive fields must fail"); assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID); } #[test] fn response_parser_rejects_missing_result_and_error() { let error = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","id":1}"#, 1).expect_err("missing outcome must fail"); assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID); } #[test] fn response_parser_distinguishes_invalid_json_from_protocol_error() { let error = crate::parse_json_rpc_response_text("not-json", 1).expect_err("invalid JSON must fail decoding"); assert_eq!(error.code(), crate::ERROR_CODE_JSON_DECODE_FAILED); } #[test] fn rpc_error_maps_to_shared_ksp_error_without_copying_remote_payload_into_context() { let response = crate::parse_json_rpc_response_text( r#"{"jsonrpc":"2.0","error":{"code":-32000,"message":"SECRET-CANARY","data":{"payload":"SECRET-DATA"}},"id":1}"#, 1, ) .expect("valid error envelope must parse"); let error = response.into_result().expect_err("RPC application error must map to KSP error"); assert_eq!(error.code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR); let rendered = format!("{error:?}"); assert!(!rendered.contains("SECRET-CANARY")); assert!(!rendered.contains("SECRET-DATA")); } #[test] fn request_debug_omits_parameter_payloads() { let request = crate::JsonRpcRequest::new(1, "sendTransaction", std::vec![serde_json::json!("SIGNED-TRANSACTION-SECRET-CANARY")]) .expect("test request must construct"); let rendered = format!("{request:?}"); assert!(rendered.contains("sendTransaction")); assert!(rendered.contains("param_count")); assert!(!rendered.contains("SIGNED-TRANSACTION-SECRET-CANARY")); } #[test] fn response_debug_omits_result_and_remote_error_payloads() { let success = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":{"secret":"RESULT-SECRET-CANARY"},"id":1}"#, 1) .expect("test success response must parse"); let success_rendered = format!("{success:?}"); assert!(!success_rendered.contains("RESULT-SECRET-CANARY")); let failure = crate::parse_json_rpc_response_text( r#"{"jsonrpc":"2.0","error":{"code":-32000,"message":"MESSAGE-SECRET-CANARY","data":{"secret":"DATA-SECRET-CANARY"}},"id":2}"#, 2, ) .expect("test error response must parse"); let failure_rendered = format!("{failure:?}"); assert!(!failure_rendered.contains("MESSAGE-SECRET-CANARY")); assert!(!failure_rendered.contains("DATA-SECRET-CANARY")); }