// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs // version: 5 #[test] fn cluster_node_fixture_preserves_v4_client_id_and_optional_fields() { let value: serde_json::Value = serde_json::from_str(include_str!("../fixtures/http/cluster_node.v4_2_1.json")).expect("fixture must decode"); let node = crate::SolanaClusterNode::decode_wire("getClusterNodes", value).expect("cluster node must decode"); assert_eq!(node.client_id(), std::option::Option::Some("Agave")); assert_eq!(node.rpc(), std::option::Option::Some("127.0.0.1:8899")); assert_eq!(node.pubsub(), std::option::Option::None); } #[test] fn vote_account_fixture_preserves_optional_basis_point_commission_and_epoch_credit_triples() { let value: serde_json::Value = serde_json::from_str(include_str!("../fixtures/http/vote_account.v4_2_1.json")).expect("fixture must decode"); let info = crate::SolanaVoteAccountInfo::decode_wire("getVoteAccounts", value).expect("vote account must decode"); assert_eq!(info.commission(), 8); assert_eq!(info.inflation_rewards_commission_bps(), std::option::Option::Some(750)); assert_eq!(info.epoch_credits().len(), 2); assert_eq!(info.epoch_credits()[0].epoch(), 700); assert_eq!(info.epoch_credits()[0].previous_credits(), 90); } #[test] fn vote_account_wire_accepts_nodes_predating_basis_point_commission_field() { let value = serde_json::json!({ "votePubkey":"11111111111111111111111111111111", "nodePubkey":"11111111111111111111111111111111", "activatedStake":1, "commission":5, "epochVoteAccount":true, "epochCredits":[], "lastVote":2, "rootSlot":1 }); let info = crate::SolanaVoteAccountInfo::decode_wire("getVoteAccounts", value).expect("legacy node shape must decode"); assert_eq!(info.inflation_rewards_commission_bps(), std::option::Option::None); } #[test] fn leader_schedule_request_encodes_current_epoch_and_slot_overloads_without_ambiguous_arrays() { let identity = "11111111111111111111111111111111".parse::().expect("fixture pubkey must parse"); let config = crate::SolanaLeaderScheduleConfig::new(std::option::Option::Some(identity), std::option::Option::Some(crate::SolanaCommitment::Finalized)); assert_eq!( crate::SolanaLeaderScheduleRequest::CurrentEpoch(std::option::Option::Some(config.clone())).to_json_params(), std::vec![serde_json::json!({"identity":"11111111111111111111111111111111","commitment":"finalized"})] ); assert_eq!( crate::SolanaLeaderScheduleRequest::Slot { slot: 123, config: std::option::Option::Some(config) }.to_json_params(), std::vec![serde_json::json!(123), serde_json::json!({"identity":"11111111111111111111111111111111","commitment":"finalized"})] ); } #[test] fn epoch_snapshot_and_leader_helpers_preserve_wire_shapes() { let epoch = crate::SolanaEpochInfo::decode_wire( "getEpochInfo", serde_json::json!({"absoluteSlot":10,"blockHeight":9,"epoch":2,"slotIndex":3,"slotsInEpoch":32,"transactionCount":null}), ) .expect("epoch info must decode"); assert_eq!(epoch.transaction_count(), std::option::Option::None); let schedule = crate::SolanaEpochSchedule::decode_wire( "getEpochSchedule", serde_json::json!({"firstNormalEpoch":1,"firstNormalSlot":32,"leaderScheduleSlotOffset":32,"slotsPerEpoch":64,"warmup":false}), ) .expect("epoch schedule must decode"); assert_eq!(schedule.slots_per_epoch(), 64); let snapshot = crate::SolanaSnapshotSlotInfo::decode_wire("getHighestSnapshotSlot", serde_json::json!({"full":100,"incremental":null})) .expect("snapshot info must decode"); assert_eq!(snapshot.full(), 100); assert_eq!(snapshot.incremental(), std::option::Option::None); let leader = crate::SolanaLeaderSchedule::decode_wire("getLeaderSchedule", serde_json::json!({"11111111111111111111111111111111":[0,2,4]})) .expect("leader schedule must decode"); assert_eq!(leader.entries().len(), 1); } #[test] fn vote_status_and_config_helpers_preserve_wire_shapes() { let vote_pubkey = "11111111111111111111111111111111".parse::().expect("fixture pubkey must parse"); let config = crate::SolanaVoteAccountsConfig::new( std::option::Option::Some(crate::SolanaCommitment::Finalized), std::option::Option::Some(vote_pubkey), std::option::Option::Some(true), std::option::Option::Some(128), ); assert_eq!( config.to_json_value(), serde_json::json!({"commitment":"finalized","votePubkey":"11111111111111111111111111111111","keepUnstakedDelinquents":true,"delinquentSlotDistance":128}) ); let status = crate::SolanaVoteAccountStatus::decode_wire( "getVoteAccounts", serde_json::json!({ "current":[{ "votePubkey":"11111111111111111111111111111111", "nodePubkey":"11111111111111111111111111111111", "activatedStake":1, "commission":5, "epochVoteAccount":true, "epochCredits":[], "lastVote":2, "rootSlot":1 }], "delinquent":[] }), ) .expect("vote account status must decode"); assert_eq!(status.current().len(), 1); assert!(status.delinquent().is_empty()); } fn pool_for_url(url: &str) -> crate::HttpTransportPool { let role = crate::HttpEndpointRoleSettings::new( crate::HttpRoleName::new("default"), true, std::vec![crate::HttpRequestKind::wildcard()], 10, crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None), ); let endpoint = crate::HttpEndpointSettings::new( "fixture", true, crate::HttpProviderName::new("fixture"), crate::HttpClusterName::new("local"), crate::HttpEndpointUrl::parse(url).expect("fixture URL must parse"), std::time::Duration::from_secs(1), std::time::Duration::from_secs(1), std::option::Option::Some(1), std::vec![role], ); let settings = crate::HttpTransportSettings::new( std::vec![endpoint], crate::HttpRetrySettings::new(0, std::time::Duration::from_millis(1), std::time::Duration::from_millis(1)), ); return crate::HttpTransportPool::new(settings).expect("fixture pool must build"); } fn serve_once(body: &'static str) -> (std::string::String, std::thread::JoinHandle) { let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("fixture listener must bind"); let address = listener.local_addr().expect("fixture listener address must resolve"); let handle = std::thread::spawn(move || { let (mut stream, _) = listener.accept().expect("fixture server must accept one request"); let request = read_request(&mut stream); let response = format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body); std::io::Write::write_all(&mut stream, response.as_bytes()).expect("fixture response must write"); return request; }); return (format!("http://{address}"), handle); } fn read_request(stream: &mut std::net::TcpStream) -> std::string::String { let mut bytes = std::vec::Vec::new(); let mut buffer = [0_u8; 1024]; loop { let count = std::io::Read::read(stream, &mut buffer).expect("fixture request must read"); if count == 0 { break; } bytes.extend_from_slice(&buffer[..count]); if request_complete(bytes.as_slice()) { break; } } return std::string::String::from_utf8(bytes).expect("fixture request must be UTF-8"); } fn request_complete(bytes: &[u8]) -> bool { let text = match std::str::from_utf8(bytes) { std::result::Result::Ok(text) => text, std::result::Result::Err(_) => return false, }; let header_end = match text.find("\r\n\r\n") { std::option::Option::Some(value) => value, std::option::Option::None => return false, }; let mut content_length = 0_usize; for line in text[..header_end].lines() { let (name, value) = match line.split_once(':') { std::option::Option::Some(parts) => parts, std::option::Option::None => continue, }; if name.eq_ignore_ascii_case("content-length") { content_length = value.trim().parse::().expect("content length must parse"); } } return bytes.len() >= header_end.saturating_add(4).saturating_add(content_length); } fn request_body(request: &str) -> serde_json::Value { let body = request.split("\r\n\r\n").nth(1).expect("fixture request body must exist"); return serde_json::from_str(body).expect("fixture request body must be JSON"); } #[tokio::test(flavor = "current_thread")] async fn typed_get_cluster_nodes_preserves_optional_v4_fields_and_omissions() { let (url, handle) = serve_once(include_str!("../fixtures/http/get_cluster_nodes.success.json")); let pool = pool_for_url(url.as_str()); let nodes = pool.get_cluster_nodes(&crate::HttpRoleName::new("default")).await.expect("cluster nodes fixture must succeed"); assert_eq!(nodes.len(), 2); assert_eq!(nodes[0].client_id(), std::option::Option::Some("Agave")); assert_eq!(nodes[0].serve_repair(), std::option::Option::Some("127.0.0.1:8004")); assert_eq!(nodes[1].rpc(), std::option::Option::None); assert_eq!(nodes[1].client_id(), std::option::Option::None); let request = handle.join().expect("fixture server must join"); let body = request_body(request.as_str()); assert_eq!(body["method"], serde_json::json!("getClusterNodes")); assert_eq!(body["params"], serde_json::json!([])); } #[tokio::test(flavor = "current_thread")] async fn typed_get_cluster_nodes_rejects_invalid_node_pubkey() { let (url, handle) = serve_once(include_str!("../fixtures/http/get_cluster_nodes.invalid_pubkey.json")); let pool = pool_for_url(url.as_str()); let result = pool.get_cluster_nodes(&crate::HttpRoleName::new("default")).await; let error = result.expect_err("invalid cluster node pubkey must reject typed response"); assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE); handle.join().expect("fixture server must join"); } #[tokio::test(flavor = "current_thread")] async fn typed_get_epoch_info_serializes_context_config_and_preserves_nullable_transaction_count() { let (url, handle) = serve_once(include_str!("../fixtures/http/get_epoch_info.success.json")); let pool = pool_for_url(url.as_str()); let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Finalized), std::option::Option::Some(429_000_000)); let info = pool .get_epoch_info(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config)) .await .expect("epoch info fixture must succeed"); assert_eq!(info.absolute_slot(), 430_000_001); assert_eq!(info.transaction_count(), std::option::Option::None); let request = handle.join().expect("fixture server must join"); let body = request_body(request.as_str()); assert_eq!(body["params"], serde_json::json!([{"commitment":"finalized","minContextSlot":429000000}])); } #[tokio::test(flavor = "current_thread")] async fn typed_get_epoch_info_omits_explicitly_empty_config() { let (url, handle) = serve_once(include_str!("../fixtures/http/get_epoch_info.success.json")); let pool = pool_for_url(url.as_str()); let config = crate::SolanaContextConfig::default(); let info = pool .get_epoch_info(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config)) .await .expect("epoch info fixture must succeed"); assert_eq!(info.epoch(), 995); let request = handle.join().expect("fixture server must join"); assert_eq!(request_body(request.as_str())["params"], serde_json::json!([])); } #[tokio::test(flavor = "current_thread")] async fn typed_get_epoch_schedule_preserves_fixed_wire_shape() { let (url, handle) = serve_once(include_str!("../fixtures/http/get_epoch_schedule.success.json")); let pool = pool_for_url(url.as_str()); let schedule = pool.get_epoch_schedule(&crate::HttpRoleName::new("default")).await.expect("epoch schedule fixture must succeed"); assert_eq!(schedule.slots_per_epoch(), 432_000); assert!(!schedule.warmup()); let request = handle.join().expect("fixture server must join"); assert_eq!(request_body(request.as_str())["method"], serde_json::json!("getEpochSchedule")); } #[tokio::test(flavor = "current_thread")] async fn typed_get_highest_snapshot_slot_preserves_nullable_incremental_slot() { let (url, handle) = serve_once(include_str!("../fixtures/http/get_highest_snapshot_slot.success.json")); let pool = pool_for_url(url.as_str()); let snapshot = pool.get_highest_snapshot_slot(&crate::HttpRoleName::new("default")).await.expect("snapshot fixture must succeed"); assert_eq!(snapshot.full(), 429_990_000); assert_eq!(snapshot.incremental(), std::option::Option::None); let request = handle.join().expect("fixture server must join"); assert_eq!(request_body(request.as_str())["params"], serde_json::json!([])); } #[tokio::test(flavor = "current_thread")] async fn typed_get_highest_snapshot_slot_preserves_no_snapshot_rpc_error() { let (url, handle) = serve_once(include_str!("../fixtures/http/get_highest_snapshot_slot.error.json")); let pool = pool_for_url(url.as_str()); let result = pool.get_highest_snapshot_slot(&crate::HttpRoleName::new("default")).await; let error = result.expect_err("no snapshot must remain an RPC application error"); assert_eq!(error.code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR); handle.join().expect("fixture server must join"); } #[tokio::test(flavor = "current_thread")] async fn typed_get_identity_decodes_pubkey_object() { let (url, handle) = serve_once(include_str!("../fixtures/http/get_identity.success.json")); let pool = pool_for_url(url.as_str()); let identity = pool.get_identity(&crate::HttpRoleName::new("default")).await.expect("identity fixture must succeed"); assert_eq!(identity.to_string(), "ComputeBudget111111111111111111111111111111"); let request = handle.join().expect("fixture server must join"); assert_eq!(request_body(request.as_str())["method"], serde_json::json!("getIdentity")); } #[tokio::test(flavor = "current_thread")] async fn typed_get_identity_rejects_invalid_wire_pubkey() { let (url, handle) = serve_once(include_str!("../fixtures/http/get_identity.invalid_pubkey.json")); let pool = pool_for_url(url.as_str()); let result = pool.get_identity(&crate::HttpRoleName::new("default")).await; let error = result.expect_err("invalid identity pubkey must reject typed response"); assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE); handle.join().expect("fixture server must join"); } #[tokio::test(flavor = "current_thread")] async fn typed_get_max_cluster_slots_decode_u64_without_params() { let (retransmit_url, retransmit_handle) = serve_once(include_str!("../fixtures/http/get_max_retransmit_slot.success.json")); let retransmit_pool = pool_for_url(retransmit_url.as_str()); let retransmit = retransmit_pool.get_max_retransmit_slot(&crate::HttpRoleName::new("default")).await.expect("max retransmit slot fixture must succeed"); assert_eq!(retransmit, 430_000_010); let retransmit_request = retransmit_handle.join().expect("fixture server must join"); let retransmit_body = request_body(retransmit_request.as_str()); assert_eq!(retransmit_body["method"], serde_json::json!("getMaxRetransmitSlot")); assert_eq!(retransmit_body["params"], serde_json::json!([])); let (shred_url, shred_handle) = serve_once(include_str!("../fixtures/http/get_max_shred_insert_slot.success.json")); let shred_pool = pool_for_url(shred_url.as_str()); let shred = shred_pool.get_max_shred_insert_slot(&crate::HttpRoleName::new("default")).await.expect("max shred insert slot fixture must succeed"); assert_eq!(shred, 430_000_011); let shred_request = shred_handle.join().expect("fixture server must join"); let shred_body = request_body(shred_request.as_str()); assert_eq!(shred_body["method"], serde_json::json!("getMaxShredInsertSlot")); assert_eq!(shred_body["params"], serde_json::json!([])); } #[tokio::test(flavor = "current_thread")] async fn typed_get_leader_schedule_serializes_slot_config_and_decodes_map() { let (url, handle) = serve_once(include_str!("../fixtures/http/get_leader_schedule.success.json")); let pool = pool_for_url(url.as_str()); let identity = "11111111111111111111111111111111".parse::().expect("fixture pubkey must parse"); let config = crate::SolanaLeaderScheduleConfig::new(std::option::Option::Some(identity), std::option::Option::Some(crate::SolanaCommitment::Finalized)); let request = crate::SolanaLeaderScheduleRequest::Slot { slot: 430_000_000, config: std::option::Option::Some(config) }; let schedule = pool .get_leader_schedule(&crate::HttpRoleName::new("default"), &request) .await .expect("leader schedule fixture must succeed") .expect("leader schedule fixture must not be null"); assert_eq!(schedule.entries().get(&identity), std::option::Option::Some(&std::vec![0, 2, 4])); let request = handle.join().expect("fixture server must join"); let body = request_body(request.as_str()); assert_eq!(body["method"], serde_json::json!("getLeaderSchedule")); assert_eq!(body["params"], serde_json::json!([430000000,{"identity":"11111111111111111111111111111111","commitment":"finalized"}])); } #[tokio::test(flavor = "current_thread")] async fn typed_get_leader_schedule_preserves_null_result() { let (url, handle) = serve_once(include_str!("../fixtures/http/get_leader_schedule.null.json")); let pool = pool_for_url(url.as_str()); let schedule = pool .get_leader_schedule(&crate::HttpRoleName::new("default"), &crate::SolanaLeaderScheduleRequest::default()) .await .expect("nullable leader schedule fixture must succeed"); assert_eq!(schedule, std::option::Option::None); let request = handle.join().expect("fixture server must join"); assert_eq!(request_body(request.as_str())["params"], serde_json::json!([])); } #[tokio::test(flavor = "current_thread")] async fn typed_get_slot_serializes_context_config_and_omits_empty_config() { let (configured_url, configured_handle) = serve_once(include_str!("../fixtures/http/get_slot.success.json")); let configured_pool = pool_for_url(configured_url.as_str()); let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Confirmed), std::option::Option::Some(429_999_999)); let slot = configured_pool.get_slot(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config)).await.expect("slot fixture must succeed"); assert_eq!(slot, 430_000_020); let request = configured_handle.join().expect("fixture server must join"); assert_eq!(request_body(request.as_str())["params"], serde_json::json!([{"commitment":"confirmed","minContextSlot":429999999}])); let (empty_url, empty_handle) = serve_once(include_str!("../fixtures/http/get_slot.success.json")); let empty_pool = pool_for_url(empty_url.as_str()); let empty = crate::SolanaContextConfig::default(); let slot = empty_pool .get_slot(&crate::HttpRoleName::new("default"), std::option::Option::Some(&empty)) .await .expect("slot fixture with empty config must succeed"); assert_eq!(slot, 430_000_020); let request = empty_handle.join().expect("fixture server must join"); assert_eq!(request_body(request.as_str())["params"], serde_json::json!([])); } #[tokio::test(flavor = "current_thread")] async fn typed_get_slot_leader_serializes_context_config_and_decodes_pubkey() { let (url, handle) = serve_once(include_str!("../fixtures/http/get_slot_leader.success.json")); let pool = pool_for_url(url.as_str()); let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Finalized), std::option::Option::Some(430_000_000)); let leader = pool .get_slot_leader(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config)) .await .expect("slot leader fixture must succeed"); assert_eq!(leader.to_string(), "ComputeBudget111111111111111111111111111111"); let request = handle.join().expect("fixture server must join"); assert_eq!(request_body(request.as_str())["params"], serde_json::json!([{"commitment":"finalized","minContextSlot":430000000}])); } #[tokio::test(flavor = "current_thread")] async fn typed_get_slot_leader_rejects_invalid_wire_pubkey() { let (url, handle) = serve_once(include_str!("../fixtures/http/get_slot_leader.invalid_pubkey.json")); let pool = pool_for_url(url.as_str()); let result = pool.get_slot_leader(&crate::HttpRoleName::new("default"), std::option::Option::None).await; let error = result.expect_err("invalid slot leader must reject typed response"); assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE); handle.join().expect("fixture server must join"); } #[tokio::test(flavor = "current_thread")] async fn typed_get_slot_leaders_preserves_order_and_serializes_range() { let (url, handle) = serve_once(include_str!("../fixtures/http/get_slot_leaders.success.json")); let pool = pool_for_url(url.as_str()); let leaders = pool.get_slot_leaders(&crate::HttpRoleName::new("default"), 430_000_020, 2).await.expect("slot leaders fixture must succeed"); assert_eq!(leaders.len(), 2); assert_eq!(leaders[0].to_string(), "11111111111111111111111111111111"); assert_eq!(leaders[1].to_string(), "ComputeBudget111111111111111111111111111111"); let request = handle.join().expect("fixture server must join"); let body = request_body(request.as_str()); assert_eq!(body["method"], serde_json::json!("getSlotLeaders")); assert_eq!(body["params"], serde_json::json!([430000020, 2])); } #[tokio::test(flavor = "current_thread")] async fn typed_get_slot_leaders_rejects_out_of_range_limit_before_io() { let pool = pool_for_url("http://127.0.0.1:9"); let zero = pool.get_slot_leaders(&crate::HttpRoleName::new("default"), 1, 0).await; let zero_error = zero.expect_err("zero slot-leader limit must reject before I/O"); assert_eq!(zero_error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS); let above = pool.get_slot_leaders(&crate::HttpRoleName::new("default"), 1, 5_001).await; let above_error = above.expect_err("slot-leader limit above 5000 must reject before I/O"); assert_eq!(above_error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS); } #[tokio::test(flavor = "current_thread")] async fn typed_get_slot_leaders_rejects_invalid_wire_pubkey() { let (url, handle) = serve_once(include_str!("../fixtures/http/get_slot_leaders.invalid_pubkey.json")); let pool = pool_for_url(url.as_str()); let result = pool.get_slot_leaders(&crate::HttpRoleName::new("default"), 1, 2).await; let error = result.expect_err("invalid slot leader list item must reject typed response"); assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE); handle.join().expect("fixture server must join"); } #[tokio::test(flavor = "current_thread")] async fn typed_get_vote_accounts_serializes_full_config_and_preserves_optional_bps() { let (url, handle) = serve_once(include_str!("../fixtures/http/get_vote_accounts.success.json")); let pool = pool_for_url(url.as_str()); let vote_pubkey = "11111111111111111111111111111111".parse::().expect("fixture pubkey must parse"); let config = crate::SolanaVoteAccountsConfig::new( std::option::Option::Some(crate::SolanaCommitment::Finalized), std::option::Option::Some(vote_pubkey), std::option::Option::Some(true), std::option::Option::Some(128), ); let status = pool .get_vote_accounts(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config)) .await .expect("vote accounts fixture must succeed"); assert_eq!(status.current().len(), 1); assert_eq!(status.delinquent().len(), 1); assert_eq!(status.current()[0].inflation_rewards_commission_bps(), std::option::Option::Some(750)); assert_eq!(status.current()[0].epoch_credits().len(), 2); assert_eq!(status.delinquent()[0].inflation_rewards_commission_bps(), std::option::Option::None); let request = handle.join().expect("fixture server must join"); assert_eq!( request_body(request.as_str())["params"], serde_json::json!([{"commitment":"finalized","votePubkey":"11111111111111111111111111111111","keepUnstakedDelinquents":true,"delinquentSlotDistance":128}]) ); } #[tokio::test(flavor = "current_thread")] async fn typed_get_vote_accounts_omits_explicitly_empty_config() { let (url, handle) = serve_once(include_str!("../fixtures/http/get_vote_accounts.success.json")); let pool = pool_for_url(url.as_str()); let config = crate::SolanaVoteAccountsConfig::default(); let status = pool .get_vote_accounts(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config)) .await .expect("vote accounts fixture with empty config must succeed"); assert_eq!(status.current().len(), 1); let request = handle.join().expect("fixture server must join"); assert_eq!(request_body(request.as_str())["params"], serde_json::json!([])); } #[tokio::test(flavor = "current_thread")] async fn typed_get_vote_accounts_rejects_invalid_wire_pubkey() { let (url, handle) = serve_once(include_str!("../fixtures/http/get_vote_accounts.invalid_pubkey.json")); let pool = pool_for_url(url.as_str()); let result = pool.get_vote_accounts(&crate::HttpRoleName::new("default"), std::option::Option::None).await; let error = result.expect_err("invalid vote-account pubkey must reject typed response"); assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE); handle.join().expect("fixture server must join"); }