v0.2.7-pre.006
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_lifecycle.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
fn non_zero(value: u64) -> std::num::NonZeroU64 {
|
||||
return std::num::NonZeroU64::new(value).expect("test ID must be non-zero");
|
||||
@@ -68,3 +68,23 @@ fn websocket_snapshots_expose_safe_metadata_without_remote_ids_or_urls() {
|
||||
assert!(!rendered.contains("wss://"));
|
||||
assert!(!rendered.contains("remote_subscription_id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn websocket_subscription_kinds_map_exact_standard_method_triplets() {
|
||||
let cases = [
|
||||
(crate::WsSubscriptionKind::Account, "accountSubscribe", "accountUnsubscribe", "accountNotification"),
|
||||
(crate::WsSubscriptionKind::Block, "blockSubscribe", "blockUnsubscribe", "blockNotification"),
|
||||
(crate::WsSubscriptionKind::Logs, "logsSubscribe", "logsUnsubscribe", "logsNotification"),
|
||||
(crate::WsSubscriptionKind::Program, "programSubscribe", "programUnsubscribe", "programNotification"),
|
||||
(crate::WsSubscriptionKind::Root, "rootSubscribe", "rootUnsubscribe", "rootNotification"),
|
||||
(crate::WsSubscriptionKind::Signature, "signatureSubscribe", "signatureUnsubscribe", "signatureNotification"),
|
||||
(crate::WsSubscriptionKind::Slot, "slotSubscribe", "slotUnsubscribe", "slotNotification"),
|
||||
(crate::WsSubscriptionKind::SlotsUpdates, "slotsUpdatesSubscribe", "slotsUpdatesUnsubscribe", "slotsUpdatesNotification"),
|
||||
(crate::WsSubscriptionKind::Vote, "voteSubscribe", "voteUnsubscribe", "voteNotification"),
|
||||
];
|
||||
for (kind, subscribe, unsubscribe, notification) in cases {
|
||||
assert_eq!(kind.subscribe_method(), subscribe);
|
||||
assert_eq!(kind.unsubscribe_method(), unsubscribe);
|
||||
assert_eq!(kind.notification_method(), notification);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
use futures_util::SinkExt; // rust-rules: trait-import
|
||||
use futures_util::StreamExt; // rust-rules: trait-import
|
||||
@@ -380,3 +380,185 @@ async fn websocket_repeated_connect_close_cycles_are_bounded() {
|
||||
.expect("repeated close server must remain bounded")
|
||||
.expect("repeated close server task must join");
|
||||
}
|
||||
|
||||
async fn send_notification(
|
||||
websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>,
|
||||
method: &str,
|
||||
remote_subscription_id: u64,
|
||||
result: serde_json::Value,
|
||||
) {
|
||||
let notification = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": {
|
||||
"result": result,
|
||||
"subscription": remote_subscription_id
|
||||
}
|
||||
});
|
||||
websocket.send(tokio_tungstenite::tungstenite::Message::Text(notification.to_string().into())).await.expect("local notification must send");
|
||||
}
|
||||
|
||||
async fn wait_for_subscription_state<T>(subscription: &crate::WsSubscription<T>, expected: crate::WsSubscriptionState) {
|
||||
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(1);
|
||||
loop {
|
||||
if subscription.state() == expected {
|
||||
return;
|
||||
}
|
||||
assert!(tokio::time::Instant::now() < deadline, "subscription did not reach expected state: {expected:?}");
|
||||
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn websocket_generic_subscription_registers_remote_id_dispatches_typed_notification_and_unsubscribes() {
|
||||
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 subscribe = read_request(&mut websocket).await;
|
||||
assert_eq!(subscribe.get("method").and_then(serde_json::Value::as_str), std::option::Option::Some("slotSubscribe"));
|
||||
send_result(&mut websocket, &subscribe, serde_json::json!(41)).await;
|
||||
send_notification(&mut websocket, "slotNotification", 41, serde_json::json!({"slot": 9001})).await;
|
||||
let unsubscribe = read_request(&mut websocket).await;
|
||||
assert_eq!(unsubscribe.get("method").and_then(serde_json::Value::as_str), std::option::Option::Some("slotUnsubscribe"));
|
||||
assert_eq!(unsubscribe.get("params"), std::option::Option::Some(&serde_json::json!([41])));
|
||||
send_result(&mut websocket, &unsubscribe, serde_json::json!(true)).await;
|
||||
});
|
||||
let session = crate::WsSession::connect(local_endpoint(url.as_str())).await.expect("client handshake must succeed");
|
||||
let mut subscription = session
|
||||
.subscribe_typed(crate::WsSubscriptionKind::Slot, std::vec::Vec::new(), |value| {
|
||||
return std::result::Result::Ok(value);
|
||||
})
|
||||
.await
|
||||
.expect("generic slot subscribe must succeed");
|
||||
assert_eq!(subscription.id().get(), 1);
|
||||
assert_eq!(subscription.kind(), crate::WsSubscriptionKind::Slot);
|
||||
assert_eq!(subscription.state(), crate::WsSubscriptionState::Active);
|
||||
let snapshot = session.snapshot();
|
||||
assert_eq!(snapshot.subscription_count(), 1);
|
||||
assert_eq!(snapshot.subscriptions()[0].id(), subscription.id());
|
||||
assert!(snapshot.subscriptions()[0].remote_bound());
|
||||
let notification = subscription.recv().await.expect("typed notification channel must remain open").expect("notification must decode");
|
||||
assert_eq!(notification, serde_json::json!({"slot": 9001}));
|
||||
assert!(subscription.unsubscribe().await.expect("unsubscribe must succeed"));
|
||||
assert_eq!(subscription.state(), crate::WsSubscriptionState::Closed);
|
||||
tokio::task::yield_now().await;
|
||||
assert_eq!(session.snapshot().subscription_count(), 0);
|
||||
server.await.expect("local server task must complete");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn websocket_remote_subscription_mapping_routes_multiple_families_to_stable_local_ids() {
|
||||
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;
|
||||
send_result(&mut websocket, &first, serde_json::json!(77)).await;
|
||||
let second = read_request(&mut websocket).await;
|
||||
send_result(&mut websocket, &second, serde_json::json!(12)).await;
|
||||
send_notification(&mut websocket, "logsNotification", 12, serde_json::json!({"family": "logs"})).await;
|
||||
send_notification(&mut websocket, "accountNotification", 77, serde_json::json!({"family": "account"})).await;
|
||||
});
|
||||
let session = crate::WsSession::connect(local_endpoint(url.as_str())).await.expect("client handshake must succeed");
|
||||
let mut account = session
|
||||
.subscribe_typed(crate::WsSubscriptionKind::Account, std::vec![serde_json::json!("account")], |value| {
|
||||
return std::result::Result::Ok(value);
|
||||
})
|
||||
.await
|
||||
.expect("account subscribe must succeed");
|
||||
let mut logs = session
|
||||
.subscribe_typed(crate::WsSubscriptionKind::Logs, std::vec![serde_json::json!("all")], |value| {
|
||||
return std::result::Result::Ok(value);
|
||||
})
|
||||
.await
|
||||
.expect("logs subscribe must succeed");
|
||||
assert_eq!(account.id().get(), 1);
|
||||
assert_eq!(logs.id().get(), 2);
|
||||
assert_eq!(logs.recv().await.expect("logs notification must exist").expect("logs notification must decode"), serde_json::json!({"family": "logs"}));
|
||||
assert_eq!(
|
||||
account.recv().await.expect("account notification must exist").expect("account notification must decode"),
|
||||
serde_json::json!({"family": "account"})
|
||||
);
|
||||
server.await.expect("local server task must complete");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn websocket_unknown_remote_subscription_notification_is_ignored_without_affecting_registered_subscription() {
|
||||
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 subscribe = read_request(&mut websocket).await;
|
||||
send_result(&mut websocket, &subscribe, serde_json::json!(3)).await;
|
||||
send_notification(&mut websocket, "rootNotification", 999, serde_json::json!(100)).await;
|
||||
send_notification(&mut websocket, "rootNotification", 3, serde_json::json!(101)).await;
|
||||
});
|
||||
let session = crate::WsSession::connect(local_endpoint(url.as_str())).await.expect("client handshake must succeed");
|
||||
let mut subscription = session
|
||||
.subscribe_typed(crate::WsSubscriptionKind::Root, std::vec::Vec::new(), |value| {
|
||||
return std::result::Result::Ok(value);
|
||||
})
|
||||
.await
|
||||
.expect("root subscribe must succeed");
|
||||
let notification = subscription.recv().await.expect("valid notification must exist").expect("valid notification must decode");
|
||||
assert_eq!(notification, serde_json::json!(101));
|
||||
assert_eq!(subscription.state(), crate::WsSubscriptionState::Active);
|
||||
server.await.expect("local server task must complete");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn websocket_notification_method_mismatch_fails_only_the_logical_subscription() {
|
||||
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 subscribe = read_request(&mut websocket).await;
|
||||
send_result(&mut websocket, &subscribe, serde_json::json!(5)).await;
|
||||
send_notification(&mut websocket, "rootNotification", 5, serde_json::json!(1)).await;
|
||||
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 mut subscription = session
|
||||
.subscribe_typed(crate::WsSubscriptionKind::Slot, std::vec::Vec::new(), |value| {
|
||||
return std::result::Result::Ok(value);
|
||||
})
|
||||
.await
|
||||
.expect("slot subscribe must succeed");
|
||||
wait_for_subscription_state(&subscription, crate::WsSubscriptionState::Failed).await;
|
||||
assert!(subscription.recv().await.is_none());
|
||||
assert_eq!(session.state(), crate::WsSessionState::Active);
|
||||
let result = session.execute_json_rpc("afterMismatch", std::vec::Vec::new()).await.expect("physical 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_typed_notification_decode_failure_fails_only_one_subscription_and_surfaces_error() {
|
||||
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 subscribe = read_request(&mut websocket).await;
|
||||
send_result(&mut websocket, &subscribe, serde_json::json!(8)).await;
|
||||
send_notification(&mut websocket, "rootNotification", 8, serde_json::json!("not-a-slot")).await;
|
||||
});
|
||||
let session = crate::WsSession::connect(local_endpoint(url.as_str())).await.expect("client handshake must succeed");
|
||||
let mut subscription = session
|
||||
.subscribe_typed(crate::WsSubscriptionKind::Root, std::vec::Vec::new(), |value| {
|
||||
return match value.as_u64() {
|
||||
std::option::Option::Some(slot) => std::result::Result::Ok(slot),
|
||||
std::option::Option::None => {
|
||||
std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "fixture root notification must be numeric"))
|
||||
},
|
||||
};
|
||||
})
|
||||
.await
|
||||
.expect("root subscribe must succeed");
|
||||
let error = subscription.recv().await.expect("decode error must be delivered").expect_err("fixture payload must fail typed decoder");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||
wait_for_subscription_state(&subscription, crate::WsSubscriptionState::Failed).await;
|
||||
assert_eq!(session.state(), crate::WsSessionState::Active);
|
||||
server.await.expect("local server task must complete");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user