v0.2.7-pre.004
This commit is contained in:
151
crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs
Normal file
151
crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs
|
||||
// version: 1
|
||||
|
||||
use futures_util::SinkExt; // rust-rules: trait-import
|
||||
use futures_util::StreamExt; // rust-rules: trait-import
|
||||
|
||||
fn local_endpoint(url: &str) -> crate::WsEndpointSettings {
|
||||
return crate::WsEndpointSettings::new(
|
||||
"local_ws",
|
||||
true,
|
||||
crate::WsProviderName::new("local-fixture"),
|
||||
crate::WsClusterName::new("local"),
|
||||
crate::WsProtocolKind::SolanaStandard,
|
||||
crate::WsEndpointUrl::parse(url).expect("local test WebSocket URL must parse"),
|
||||
crate::WsSessionSettings::default(),
|
||||
);
|
||||
}
|
||||
|
||||
async fn bind_local_listener() -> (tokio::net::TcpListener, std::string::String) {
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("local listener must bind");
|
||||
let address = listener.local_addr().expect("local listener must expose address");
|
||||
return (listener, format!("ws://{address}"));
|
||||
}
|
||||
|
||||
async fn read_request(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>) -> serde_json::Value {
|
||||
let message = websocket.next().await.expect("request message must exist").expect("request message must decode");
|
||||
let text = message.to_text().expect("request must be text");
|
||||
return serde_json::from_str(text).expect("request must contain JSON");
|
||||
}
|
||||
|
||||
async fn send_result(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>, request: &serde_json::Value, result: serde_json::Value) {
|
||||
let id = request.get("id").and_then(serde_json::Value::as_u64).expect("request id must be numeric");
|
||||
let response = serde_json::json!({"jsonrpc":"2.0","id":id,"result":result});
|
||||
websocket.send(tokio_tungstenite::tungstenite::Message::Text(response.to_string().into())).await.expect("local response must send");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn websocket_session_connects_and_round_trips_internal_json_rpc() {
|
||||
let (listener, url) = bind_local_listener().await;
|
||||
let server = tokio::spawn(async move {
|
||||
let (stream, _) = listener.accept().await.expect("local server must accept client");
|
||||
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local WebSocket handshake must succeed");
|
||||
let request = read_request(&mut websocket).await;
|
||||
assert_eq!(request.get("method").and_then(serde_json::Value::as_str), std::option::Option::Some("getVersion"));
|
||||
send_result(&mut websocket, &request, serde_json::json!({"solana-core":"fixture"})).await;
|
||||
});
|
||||
let session = crate::WsSession::connect(local_endpoint(url.as_str())).await.expect("client handshake must succeed");
|
||||
assert_eq!(session.state(), crate::WsSessionState::Active);
|
||||
assert_eq!(session.snapshot().pending_request_count(), 0);
|
||||
let result = session.execute_json_rpc("getVersion", std::vec::Vec::new()).await.expect("fixture JSON-RPC call must succeed");
|
||||
assert_eq!(result.get("solana-core").and_then(serde_json::Value::as_str), std::option::Option::Some("fixture"));
|
||||
tokio::task::yield_now().await;
|
||||
assert_eq!(session.snapshot().pending_request_count(), 0);
|
||||
server.await.expect("local server task must complete");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn websocket_sessions_are_physical_and_distinct_even_for_the_same_url() {
|
||||
let (listener, url) = bind_local_listener().await;
|
||||
let server = tokio::spawn(async move {
|
||||
let first = listener.accept().await.expect("first local client must connect");
|
||||
let first_ws = tokio_tungstenite::accept_async(first.0).await.expect("first handshake must succeed");
|
||||
let second = listener.accept().await.expect("second local client must connect");
|
||||
let second_ws = tokio_tungstenite::accept_async(second.0).await.expect("second handshake must succeed");
|
||||
return (first_ws, second_ws);
|
||||
});
|
||||
let first = crate::WsSession::connect(local_endpoint(url.as_str())).await.expect("first client must connect");
|
||||
let second = crate::WsSession::connect(local_endpoint(url.as_str())).await.expect("second client must connect");
|
||||
assert_ne!(first.id(), second.id());
|
||||
assert_eq!(first.snapshot().endpoint_name(), second.snapshot().endpoint_name());
|
||||
let _ = server.await.expect("local server task must complete");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn websocket_pending_map_dispatches_out_of_order_responses_by_request_id() {
|
||||
let (listener, url) = bind_local_listener().await;
|
||||
let server = tokio::spawn(async move {
|
||||
let (stream, _) = listener.accept().await.expect("local server must accept client");
|
||||
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local WebSocket handshake must succeed");
|
||||
let first = read_request(&mut websocket).await;
|
||||
let second = read_request(&mut websocket).await;
|
||||
send_result(&mut websocket, &second, serde_json::json!("second")).await;
|
||||
send_result(&mut websocket, &first, serde_json::json!("first")).await;
|
||||
});
|
||||
let session = crate::WsSession::connect(local_endpoint(url.as_str())).await.expect("client handshake must succeed");
|
||||
let first_session = session.clone();
|
||||
let second_session = session.clone();
|
||||
let (first, second) = tokio::join!(
|
||||
first_session.execute_json_rpc("firstMethod", std::vec::Vec::new()),
|
||||
second_session.execute_json_rpc("secondMethod", std::vec::Vec::new())
|
||||
);
|
||||
assert_eq!(first.expect("first response must dispatch"), serde_json::json!("first"));
|
||||
assert_eq!(second.expect("second response must dispatch"), serde_json::json!("second"));
|
||||
tokio::task::yield_now().await;
|
||||
assert_eq!(session.snapshot().pending_request_count(), 0);
|
||||
server.await.expect("local server task must complete");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn websocket_rpc_application_error_does_not_fail_the_physical_session() {
|
||||
let (listener, url) = bind_local_listener().await;
|
||||
let server = tokio::spawn(async move {
|
||||
let (stream, _) = listener.accept().await.expect("local server must accept client");
|
||||
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local WebSocket handshake must succeed");
|
||||
let request = read_request(&mut websocket).await;
|
||||
let id = request.get("id").and_then(serde_json::Value::as_u64).expect("request id must be numeric");
|
||||
let response = serde_json::json!({"jsonrpc":"2.0","id":id,"error":{"code":-32602,"message":"fixture invalid params"}});
|
||||
websocket.send(tokio_tungstenite::tungstenite::Message::Text(response.to_string().into())).await.expect("local error response must send");
|
||||
let request = read_request(&mut websocket).await;
|
||||
send_result(&mut websocket, &request, serde_json::json!(true)).await;
|
||||
});
|
||||
let session = crate::WsSession::connect(local_endpoint(url.as_str())).await.expect("client handshake must succeed");
|
||||
let error = session.execute_json_rpc("badMethod", std::vec::Vec::new()).await.expect_err("RPC application error must surface");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR);
|
||||
assert_eq!(session.state(), crate::WsSessionState::Active);
|
||||
let result = session.execute_json_rpc("goodMethod", std::vec::Vec::new()).await.expect("session must remain usable");
|
||||
assert_eq!(result, serde_json::json!(true));
|
||||
server.await.expect("local server task must complete");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn websocket_connection_errors_do_not_echo_url_credentials() {
|
||||
let defaults = crate::WsSessionSettings::default();
|
||||
let short_session = crate::WsSessionSettings::new(
|
||||
std::time::Duration::from_millis(150),
|
||||
defaults.close_timeout(),
|
||||
defaults.reconnect().clone(),
|
||||
defaults.resubscribe(),
|
||||
defaults.command_queue_capacity(),
|
||||
defaults.notification_queue_capacity(),
|
||||
defaults.max_active_subscriptions(),
|
||||
defaults.max_pending_requests(),
|
||||
defaults.max_message_size_bytes(),
|
||||
defaults.max_frame_size_bytes(),
|
||||
defaults.max_write_buffer_size_bytes(),
|
||||
);
|
||||
let endpoint = crate::WsEndpointSettings::new(
|
||||
"redaction_fixture",
|
||||
true,
|
||||
crate::WsProviderName::new("fixture-provider"),
|
||||
crate::WsClusterName::new("local"),
|
||||
crate::WsProtocolKind::SolanaStandard,
|
||||
crate::WsEndpointUrl::parse("ws://user:password@127.0.0.1:1/private?api-key=SECRET-CANARY").expect("test URL must parse"),
|
||||
short_session,
|
||||
);
|
||||
let error = crate::WsSession::connect(endpoint).await.expect_err("unreachable local endpoint must fail");
|
||||
let rendered = format!("{error:?}");
|
||||
assert!(!rendered.contains("SECRET-CANARY"));
|
||||
assert!(!rendered.contains("password"));
|
||||
assert!(!rendered.contains("private?"));
|
||||
}
|
||||
Reference in New Issue
Block a user