v0.2.1-pre.002

This commit is contained in:
2026-08-17 18:02:18 +02:00
parent d98d152f08
commit 0cff0406ab
15 changed files with 2989 additions and 10 deletions

View File

@@ -0,0 +1,117 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/json_rpc.rs
// version: 1
#[test]
fn request_serialization_matches_json_rpc_2_0_shape() {
let request = super::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!(super::JsonRpcRequest::new(1, "", std::vec![]).is_err());
assert!(super::JsonRpcRequest::new(1, " getHealth", std::vec![]).is_err());
}
#[test]
fn response_parser_preserves_null_success_result() {
let response = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":null,"id":9}"#, 9).expect("null result is a valid success payload");
match response {
super::JsonRpcResponse::Success(success) => assert!(success.result().is_null()),
super::JsonRpcResponse::Error(_) => assert!(false, "success response must not parse as error"),
}
}
#[test]
fn response_parser_preserves_rpc_error_payload() {
let response = super::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");
match response {
super::JsonRpcResponse::Error(error_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})));
},
super::JsonRpcResponse::Success(_) => assert!(false, "RPC error response must not parse as success"),
}
}
#[test]
fn response_parser_rejects_id_mismatch() {
let error = super::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 = super::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 = super::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 = super::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 = super::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 = super::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 = super::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 = super::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 = super::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"));
}

View File

@@ -0,0 +1,121 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_method.rs
// version: 1
#[test]
fn audited_registry_has_expected_current_and_historical_counts() {
assert_eq!(super::current_http_rpc_methods().len(), 52);
assert_eq!(super::historical_http_rpc_methods().len(), 14);
}
#[test]
fn audited_registry_method_names_are_unique() {
let mut names = std::collections::BTreeSet::<&str>::new();
for descriptor in super::current_http_rpc_methods() {
assert!(names.insert(descriptor.method()), "duplicate current method {}", descriptor.method());
}
for descriptor in super::historical_http_rpc_methods() {
assert!(names.insert(descriptor.method()), "duplicate historical method {}", descriptor.method());
}
assert_eq!(names.len(), 66);
}
#[test]
fn coverage_release_counts_match_recalibrated_matrix() {
let mut foundation = 0_usize;
let mut accounts_tokens_cluster = 0_usize;
let mut transactions = 0_usize;
let mut blocks_economics = 0_usize;
for descriptor in super::current_http_rpc_methods() {
match descriptor.coverage_release() {
super::HttpRpcCoverageRelease::V0_2_1 => foundation += 1,
super::HttpRpcCoverageRelease::V0_2_2 => accounts_tokens_cluster += 1,
super::HttpRpcCoverageRelease::V0_2_3 => transactions += 1,
super::HttpRpcCoverageRelease::V0_2_4 => blocks_economics += 1,
super::HttpRpcCoverageRelease::Historical => assert!(false, "current method must not be historical"),
}
}
assert_eq!(foundation, 4);
assert_eq!(accounts_tokens_cluster, 22);
assert_eq!(transactions, 11);
assert_eq!(blocks_economics, 15);
}
#[test]
fn foundation_canary_assignment_is_exact() {
let mut names = std::vec::Vec::<&str>::new();
for descriptor in super::current_http_rpc_methods() {
if descriptor.coverage_release() == super::HttpRpcCoverageRelease::V0_2_1 {
names.push(descriptor.method());
}
}
names.sort_unstable();
assert_eq!(names, std::vec!["getBalance", "getGenesisHash", "getHealth", "getVersion"]);
}
#[test]
fn historical_methods_are_deprecated_removed_and_not_retryable() {
for descriptor in super::historical_http_rpc_methods() {
assert_eq!(descriptor.documentation_status(), super::RpcDocumentationStatus::Deprecated);
assert_eq!(descriptor.runtime_status(), super::RpcRuntimeStatus::Removed);
assert_eq!(descriptor.transport_retry_class(), super::TransportRetryClass::NotApplicable);
assert_eq!(descriptor.coverage_release(), super::HttpRpcCoverageRelease::Historical);
}
}
#[test]
fn get_transaction_and_get_block_track_deprecated_legacy_request_form() {
let get_transaction = super::find_http_rpc_method("getTransaction").expect("getTransaction descriptor must exist");
let get_block = super::find_http_rpc_method("getBlock").expect("getBlock descriptor must exist");
assert!(get_transaction.request_form_status().has_deprecated_legacy());
assert!(get_block.request_form_status().has_deprecated_legacy());
let get_balance = super::find_http_rpc_method("getBalance").expect("getBalance descriptor must exist");
assert!(!get_balance.request_form_status().has_deprecated_legacy());
}
#[test]
fn write_submission_methods_are_never_retry_after_ambiguous_dispatch() {
for method in ["sendTransaction", "requestAirdrop"] {
let descriptor = super::find_http_rpc_method(method).expect("write descriptor must exist");
assert_eq!(descriptor.operation_kind(), super::RpcOperationKind::WriteSubmission);
assert_eq!(descriptor.transport_retry_class(), super::TransportRetryClass::NeverAfterDispatch);
}
let simulation = super::find_http_rpc_method("simulateTransaction").expect("simulation descriptor must exist");
assert_eq!(simulation.operation_kind(), super::RpcOperationKind::Simulation);
assert_eq!(simulation.transport_retry_class(), super::TransportRetryClass::RetrySafe);
}
#[test]
fn removed_method_support_check_returns_method_removed_error() {
let descriptor = super::find_http_rpc_method("confirmTransaction").expect("historical descriptor must exist");
let error = descriptor.ensure_runtime_supported().expect_err("removed method must not be callable");
assert_eq!(error.code(), crate::ERROR_CODE_METHOD_REMOVED);
}
#[test]
fn stable_supported_method_passes_runtime_support_check() {
let descriptor = super::find_http_rpc_method("getHealth").expect("current descriptor must exist");
assert!(descriptor.ensure_runtime_supported().is_ok());
}
#[test]
fn supported_unstable_descriptor_executes_central_warning_path() {
let descriptor = super::HttpRpcMethodDescriptor::new(
"experimentalMethod",
super::HttpRpcCategory::Cluster,
"experimental_method",
super::RpcDocumentationStatus::Unstable,
super::RpcRuntimeStatus::Supported,
super::RpcRequestFormStatus::Stable,
super::RpcOperationKind::Read,
super::TransportRetryClass::RetrySafe,
std::option::Option::None,
super::HttpRpcCoverageRelease::V0_2_1,
);
assert!(descriptor.requires_method_usage_warning());
assert!(descriptor.ensure_runtime_supported().is_ok());
}
#[test]
fn lookup_rejects_unknown_method_without_affecting_raw_provider_extensions() {
assert!(super::find_http_rpc_method("providerCustomMethod").is_none());
}

View File

@@ -0,0 +1,207 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/settings.rs
// version: 1
fn non_zero(value: u32) -> std::num::NonZeroU32 {
return std::num::NonZeroU32::new(value).expect("test non-zero value must remain non-zero");
}
fn valid_settings(url_text: &str) -> super::HttpTransportSettings {
let url = super::HttpEndpointUrl::parse(url_text).expect("test URL must be valid");
let limits = super::HttpRoleLimits::new(
std::option::Option::Some(non_zero(10)),
std::option::Option::Some(non_zero(20)),
std::option::Option::Some(non_zero(4)),
std::option::Option::Some(std::time::Duration::from_millis(500)),
);
let role = super::HttpEndpointRoleSettings::new(super::HttpRoleName::new("default"), true, std::vec![super::HttpRequestKind::wildcard()], 100, limits);
let endpoint = super::HttpEndpointSettings::new(
"devnet_public",
true,
super::HttpProviderName::new("solana-public"),
super::HttpClusterName::new("devnet"),
url,
std::time::Duration::from_secs(5),
std::time::Duration::from_secs(15),
std::option::Option::Some(8),
std::vec![role],
);
return super::HttpTransportSettings::new(
std::vec![endpoint],
super::HttpRetrySettings::new(2, std::time::Duration::from_millis(100), std::time::Duration::from_secs(2)),
);
}
#[test]
fn endpoint_url_accepts_http_and_https() {
assert!(super::HttpEndpointUrl::parse("https://api.devnet.solana.com").is_ok());
assert!(super::HttpEndpointUrl::parse("http://127.0.0.1:8899").is_ok());
}
#[test]
fn endpoint_url_rejects_non_http_schemes() {
let result = super::HttpEndpointUrl::parse("ws://api.devnet.solana.com");
let error = result.expect_err("WebSocket URL must not be accepted by HTTP settings");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
}
#[test]
fn endpoint_url_debug_redacts_secret_material() {
let url = super::HttpEndpointUrl::parse("https://provider.invalid/rpc?api-key=SECRET-CANARY").expect("test URL must parse");
let rendered = format!("{url:?}");
assert!(rendered.contains("<redacted>"));
assert!(!rendered.contains("SECRET-CANARY"));
assert!(!rendered.contains("provider.invalid"));
}
#[test]
fn valid_transport_settings_pass_validation() {
let settings = valid_settings("https://api.devnet.solana.com");
assert!(settings.validate().is_ok());
}
#[test]
fn transport_settings_debug_does_not_leak_endpoint_url() {
let settings = valid_settings("https://provider.invalid/rpc?api-key=SECRET-CANARY");
let rendered = format!("{settings:?}");
assert!(!rendered.contains("SECRET-CANARY"));
assert!(!rendered.contains("provider.invalid"));
assert!(rendered.contains("HttpEndpointUrl(<redacted>)"));
}
#[test]
fn transport_settings_require_one_enabled_endpoint() {
let url = super::HttpEndpointUrl::parse("https://api.devnet.solana.com").expect("test URL must parse");
let role = super::HttpEndpointRoleSettings::new(
super::HttpRoleName::new("default"),
true,
std::vec![super::HttpRequestKind::wildcard()],
100,
super::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
);
let endpoint = super::HttpEndpointSettings::new(
"disabled",
false,
super::HttpProviderName::new("provider"),
super::HttpClusterName::new("devnet"),
url,
std::time::Duration::from_secs(1),
std::time::Duration::from_secs(1),
std::option::Option::None,
std::vec![role],
);
let settings = super::HttpTransportSettings::new(
std::vec![endpoint],
super::HttpRetrySettings::new(1, std::time::Duration::from_millis(1), std::time::Duration::from_millis(2)),
);
let error = settings.validate().expect_err("all-disabled settings must fail");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
}
#[test]
fn transport_settings_reject_duplicate_endpoint_names() {
let first = valid_settings("https://one.invalid");
let second = valid_settings("https://two.invalid");
let settings = super::HttpTransportSettings::new(std::vec![first.endpoints()[0].clone(), second.endpoints()[0].clone()], first.retry().clone());
let error = settings.validate().expect_err("duplicate endpoint names must fail");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
}
#[test]
fn transport_settings_reject_duplicate_roles() {
let base = valid_settings("https://api.devnet.solana.com");
let endpoint = &base.endpoints()[0];
let duplicated_endpoint = super::HttpEndpointSettings::new(
endpoint.name(),
true,
endpoint.provider().clone(),
endpoint.cluster().clone(),
endpoint.url().clone(),
endpoint.connect_timeout(),
endpoint.request_timeout(),
endpoint.max_idle_connections_per_host(),
std::vec![endpoint.roles()[0].clone(), endpoint.roles()[0].clone()],
);
let settings = super::HttpTransportSettings::new(std::vec![duplicated_endpoint], base.retry().clone());
assert!(settings.validate().is_err());
}
#[test]
fn transport_settings_reject_wildcard_mixed_with_specific_kind() {
let base = valid_settings("https://api.devnet.solana.com");
let endpoint = &base.endpoints()[0];
let role = super::HttpEndpointRoleSettings::new(
super::HttpRoleName::new("default"),
true,
std::vec![super::HttpRequestKind::wildcard(), super::HttpRequestKind::new("get_balance")],
100,
endpoint.roles()[0].limits().clone(),
);
let modified_endpoint = super::HttpEndpointSettings::new(
endpoint.name(),
true,
endpoint.provider().clone(),
endpoint.cluster().clone(),
endpoint.url().clone(),
endpoint.connect_timeout(),
endpoint.request_timeout(),
endpoint.max_idle_connections_per_host(),
std::vec![role],
);
let settings = super::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
assert!(settings.validate().is_err());
}
#[test]
fn transport_settings_reject_burst_without_rps() {
let base = valid_settings("https://api.devnet.solana.com");
let endpoint = &base.endpoints()[0];
let role = super::HttpEndpointRoleSettings::new(
super::HttpRoleName::new("default"),
true,
std::vec![super::HttpRequestKind::wildcard()],
100,
super::HttpRoleLimits::new(std::option::Option::None, std::option::Option::Some(non_zero(2)), std::option::Option::None, std::option::Option::None),
);
let modified_endpoint = super::HttpEndpointSettings::new(
endpoint.name(),
true,
endpoint.provider().clone(),
endpoint.cluster().clone(),
endpoint.url().clone(),
endpoint.connect_timeout(),
endpoint.request_timeout(),
endpoint.max_idle_connections_per_host(),
std::vec![role],
);
let settings = super::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
assert!(settings.validate().is_err());
}
#[test]
fn transport_settings_reject_reversed_retry_backoff() {
let base = valid_settings("https://api.devnet.solana.com");
let settings = super::HttpTransportSettings::new(
base.endpoints().to_vec(),
super::HttpRetrySettings::new(2, std::time::Duration::from_secs(2), std::time::Duration::from_secs(1)),
);
assert!(settings.validate().is_err());
}
#[test]
fn transport_settings_reject_zero_request_timeout() {
let base = valid_settings("https://api.devnet.solana.com");
let endpoint = &base.endpoints()[0];
let modified_endpoint = super::HttpEndpointSettings::new(
endpoint.name(),
true,
endpoint.provider().clone(),
endpoint.cluster().clone(),
endpoint.url().clone(),
endpoint.connect_timeout(),
std::time::Duration::ZERO,
endpoint.max_idle_connections_per_host(),
endpoint.roles().to_vec(),
);
let settings = super::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
assert!(settings.validate().is_err());
}