// file: crates/ksp-onchain-transport-lib/unit_tests/pool.rs // version: 1 fn role(name: &str, priority: u32, request_kinds: std::vec::Vec) -> crate::HttpEndpointRoleSettings { return crate::HttpEndpointRoleSettings::new( crate::HttpRoleName::new(name), true, request_kinds, priority, crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None), ); } fn endpoint(name: &str, enabled: bool, priority: u32, request_kinds: std::vec::Vec) -> crate::HttpEndpointSettings { return crate::HttpEndpointSettings::new( name, enabled, crate::HttpProviderName::new("provider"), crate::HttpClusterName::new("devnet"), crate::HttpEndpointUrl::parse(format!("https://{name}.invalid/rpc?token=SECRET-CANARY")).expect("test URL must parse"), std::time::Duration::from_secs(1), std::time::Duration::from_secs(2), std::option::Option::Some(4), std::vec![role("default", priority, request_kinds)], ); } fn settings(endpoints: std::vec::Vec) -> crate::HttpTransportSettings { return crate::HttpTransportSettings::new( endpoints, crate::HttpRetrySettings::new(2, std::time::Duration::from_millis(10), std::time::Duration::from_millis(50)), ); } #[test] fn pool_prefers_lowest_priority_tier() { let pool = super::HttpTransportPool::new(settings(std::vec![ endpoint("secondary", true, 20, std::vec![crate::HttpRequestKind::wildcard()]), endpoint("primary", true, 10, std::vec![crate::HttpRequestKind::wildcard()]), ])) .expect("pool must build"); let selection = pool .select_for_request_kind(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance")) .expect("selection must succeed"); assert_eq!(selection.endpoint_name(), "primary"); assert_eq!(selection.priority(), 10); } #[test] fn pool_round_robins_fairly_inside_best_priority_tier() { let pool = super::HttpTransportPool::new(settings(std::vec![ endpoint("one", true, 10, std::vec![crate::HttpRequestKind::wildcard()]), endpoint("two", true, 10, std::vec![crate::HttpRequestKind::wildcard()]), endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]), ])) .expect("pool must build"); let role = crate::HttpRoleName::new("default"); let kind = crate::HttpRequestKind::new("get_balance"); let first = pool.select_for_request_kind(&role, &kind).expect("first selection must succeed"); let second = pool.select_for_request_kind(&role, &kind).expect("second selection must succeed"); let third = pool.select_for_request_kind(&role, &kind).expect("third selection must succeed"); assert_eq!(first.endpoint_name(), "one"); assert_eq!(second.endpoint_name(), "two"); assert_eq!(third.endpoint_name(), "one"); } #[test] fn disabled_best_priority_endpoint_falls_back_to_next_tier() { let pool = super::HttpTransportPool::new(settings(std::vec![ endpoint("disabled-primary", false, 1, std::vec![crate::HttpRequestKind::wildcard()]), endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]), ])) .expect("pool must build"); let selection = pool .select_for_request_kind(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance")) .expect("fallback must be selected"); assert_eq!(selection.endpoint_name(), "fallback"); } #[test] fn pool_filters_role_and_capability_before_priority() { let pool = super::HttpTransportPool::new(settings(std::vec![ endpoint("wrong-capability", true, 1, std::vec![crate::HttpRequestKind::new("send_transaction")]), endpoint("matching", true, 50, std::vec![crate::HttpRequestKind::new("get_balance")]), ])) .expect("pool must build"); let selection = pool .select_for_request_kind(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance")) .expect("matching capability must be selected"); assert_eq!(selection.endpoint_name(), "matching"); } #[test] fn pool_returns_structured_error_when_no_endpoint_matches() { let pool = super::HttpTransportPool::new(settings(std::vec![endpoint("read-only", true, 10, std::vec![crate::HttpRequestKind::new("get_balance")],)])) .expect("pool must build"); let error = pool .select_for_request_kind(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("send_transaction")) .expect_err("unsupported request kind must fail selection"); assert_eq!(error.code(), crate::ERROR_CODE_ENDPOINT_SELECTION_FAILED); assert!(!format!("{error:?}").contains("SECRET-CANARY")); } #[test] fn standard_method_selection_uses_registry_request_kind() { let pool = super::HttpTransportPool::new(settings(std::vec![endpoint("balance", true, 10, std::vec![crate::HttpRequestKind::new("get_balance")],)])) .expect("pool must build"); let method = crate::find_http_rpc_method("getBalance").expect("audited method must exist"); let selection = pool.select_for_method(&crate::HttpRoleName::new("default"), method).expect("standard method must route"); assert_eq!(selection.request_kind().as_str(), "get_balance"); } #[test] fn pool_snapshot_is_safe_and_preserves_disabled_endpoints() { let pool = super::HttpTransportPool::new(settings(std::vec![ endpoint("enabled", true, 10, std::vec![crate::HttpRequestKind::wildcard()]), endpoint("disabled", false, 10, std::vec![crate::HttpRequestKind::wildcard()]), ])) .expect("pool must build"); let snapshot = pool.snapshot(); let rendered = format!("{snapshot:?} {pool:?}"); assert_eq!(snapshot.endpoint_count(), 2); assert_eq!(snapshot.available_endpoint_count(), 1); assert!(!rendered.contains("SECRET-CANARY")); assert!(!rendered.contains(".invalid/rpc")); } #[test] fn disabled_role_is_excluded_before_priority_selection() { let base = endpoint("disabled-role", true, 1, std::vec![crate::HttpRequestKind::wildcard()]); let disabled_role = crate::HttpEndpointRoleSettings::new( crate::HttpRoleName::new("default"), false, std::vec![crate::HttpRequestKind::wildcard()], 1, crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None), ); let disabled_role_endpoint = crate::HttpEndpointSettings::new( base.name(), true, base.provider().clone(), base.cluster().clone(), base.url().clone(), base.connect_timeout(), base.request_timeout(), base.max_idle_connections_per_host(), std::vec![disabled_role], ); let pool = super::HttpTransportPool::new(settings(std::vec![ disabled_role_endpoint, endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]), ])) .expect("pool must build"); let selection = pool .select_for_request_kind(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance")) .expect("enabled fallback role must be selected"); assert_eq!(selection.endpoint_name(), "fallback"); } #[test] fn removed_standard_method_is_rejected_before_endpoint_routing() { let pool = super::HttpTransportPool::new(settings(std::vec![endpoint("wildcard", true, 10, std::vec![crate::HttpRequestKind::wildcard()],)])) .expect("pool must build"); let method = crate::find_http_rpc_method("confirmTransaction").expect("historical method must exist"); let error = pool.select_for_method(&crate::HttpRoleName::new("default"), method).expect_err("removed standard method must be rejected before routing"); assert_eq!(error.code(), crate::ERROR_CODE_METHOD_REMOVED); }