// Copyright 2019-2025 Parity Technologies (UK) Ltd. // This file is dual-licensed as Apache-2.0 or GPL-3.0. // see LICENSE for license details. //! This module exposes a [`MockRpcClient`], which is useful for testing. //! //! # Example //! //! ```rust //! use subxt_rpcs::client::{ RpcClient, MockRpcClient }; //! use subxt_rpcs::client::mock_rpc_client::Json; //! //! let mut state = vec![ //! Json(1u8), //! Json(2u8), //! Json(3u8), //! ]; //! //! // Define a mock client by providing some functions which intercept //! // method and subscription calls and return some response. //! let mock_client = MockRpcClient::builder() //! .method_handler_once("foo", async move |params| { //! // Return each item from our state, and then null afterwards. //! state.pop() //! }) //! .subscription_handler("bar", async move |params, unsub| { //! // Arrays, vecs or an RpcSubscription can be returned here to //! // signal the set of values to be handed back on a subscription. //! vec![Json(1), Json(2), Json(3)] //! }) //! .build(); //! //! // Build an RPC Client that can be used in Subxt or in conjunction with //! // the RPC methods provided in this crate. //! let rpc_client = RpcClient::new(mock_client); //! ``` use super::{RpcClientT, RawRpcFuture, RawRpcSubscription}; use crate::{Error, UserError}; use core::future::Future; use futures::StreamExt; use serde_json::value::RawValue; use std::sync::{Arc, Mutex}; use std::collections::{HashMap, VecDeque}; type MethodHandlerFnOnce = Box>) -> RawRpcFuture<'static, Box> + Send + Sync + 'static>; type SubscriptionHandlerFnOnce = Box>, &str) -> RawRpcFuture<'static, RawRpcSubscription> + Send + Sync + 'static>; type MethodHandlerFn = Box>) -> RawRpcFuture<'static, Box> + Send + Sync + 'static>; type SubscriptionHandlerFn = Box>, &str) -> RawRpcFuture<'static, RawRpcSubscription> + Send + Sync + 'static>; /// A builder to configure and build a new [`MockRpcClient`]. #[derive(Default)] pub struct MockRpcClientBuilder { method_handlers_once: HashMap>, method_handlers: HashMap, method_fallback: Option, subscription_handlers_once: HashMap>, subscription_handlers: HashMap, subscription_fallback: Option } impl MockRpcClientBuilder { /// Add a handler for a specific RPC method. This is called exactly once, and multiple such calls for the same method can be /// added. Only when any calls registered with this have been used up is the method set by [`Self::method_handler`] called. pub fn method_handler_once(mut self, name: impl Into, f: MethodHandler) -> Self where MethodHandler: FnOnce(Option>) -> MFut + Send + Sync + 'static, MFut: Future + Send + 'static, MRes: IntoHandlerResponse, { let handler: MethodHandlerFnOnce = Box::new(move |_method: &str, params: Option>| { let fut = f(params); Box::pin(async move { fut.await.into_handler_response() }) }); self.method_handlers_once.entry(name.into()).or_default().push_back(handler); self } /// Add a handler for a specific RPC method. pub fn method_handler(mut self, name: impl Into, mut f: MethodHandler) -> Self where MethodHandler: FnMut(Option>) -> MFut + Send + Sync + 'static, MFut: Future + Send + 'static, MRes: IntoHandlerResponse, { let handler: MethodHandlerFn = Box::new(move |_method: &str, params: Option>| { let fut = f(params); Box::pin(async move { fut.await.into_handler_response() }) }); self.method_handlers.insert(name.into(), handler); self } /// Add a fallback handler to handle any methods not handled by a specific handler. pub fn method_fallback(mut self, mut f: MethodHandler) -> Self where MethodHandler: FnMut(String, Option>) -> MFut + Send + Sync + 'static, MFut: Future + Send + 'static, MRes: IntoHandlerResponse, { let handler: MethodHandlerFn = Box::new(move |method: &str, params: Option>| { let fut = f(method.to_owned(), params); Box::pin(async move { fut.await.into_handler_response() }) }); self.method_fallback = Some(handler); self } /// Add a handler for a specific RPC subscription. pub fn subscription_handler_once(mut self, name: impl Into, f: SubscriptionHandler) -> Self where SubscriptionHandler: FnOnce(Option>, String) -> SFut + Send + Sync + 'static, SFut: Future + Send + 'static, SRes: IntoSubscriptionResponse, { let handler: SubscriptionHandlerFnOnce = Box::new(move |_sub: &str, params: Option>, unsub: &str| { let fut = f(params, unsub.to_owned()); Box::pin(async move { fut.await.into_subscription_response() }) }); self.subscription_handlers_once.entry(name.into()).or_default().push_back(handler); self } /// Add a handler for a specific RPC subscription. pub fn subscription_handler(mut self, name: impl Into, mut f: SubscriptionHandler) -> Self where SubscriptionHandler: FnMut(Option>, String) -> SFut + Send + Sync + 'static, SFut: Future + Send + 'static, SRes: IntoSubscriptionResponse, { let handler: SubscriptionHandlerFn = Box::new(move |_sub: &str, params: Option>, unsub: &str| { let fut = f(params, unsub.to_owned()); Box::pin(async move { fut.await.into_subscription_response() }) }); self.subscription_handlers.insert(name.into(), handler); self } /// Add a fallback handler to handle any subscriptions not handled by a specific handler. pub fn subscription_fallback(mut self, mut f: SubscriptionHandler) -> Self where SubscriptionHandler: FnMut(String, Option>, String) -> SFut + Send + Sync + 'static, SFut: Future + Send + 'static, SRes: IntoSubscriptionResponse, { let handler: SubscriptionHandlerFn = Box::new(move |sub: &str, params: Option>, unsub: &str| { let fut = f(sub.to_owned(), params, unsub.to_owned()); Box::pin(async move { fut.await.into_subscription_response() }) }); self.subscription_fallback = Some(handler); self } /// Construct a [`MockRpcClient`] given some state which will be mutably available to each of the handlers. pub fn build(self) -> MockRpcClient { MockRpcClient { method_handlers_once: Arc::new(Mutex::new(self.method_handlers_once)), method_handlers: Arc::new(Mutex::new(self.method_handlers)), method_fallback: self.method_fallback.map(|f| Arc::new(Mutex::new(f))), subscription_handlers_once: Arc::new(Mutex::new(self.subscription_handlers_once)), subscription_handlers: Arc::new(Mutex::new(self.subscription_handlers)), subscription_fallback: self.subscription_fallback.map(|f| Arc::new(Mutex::new(f))), } } } /// A mock RPC client that responds programmatically to requests. /// Useful for testing. #[derive(Clone)] pub struct MockRpcClient { // These are all accessed for just long enough to call the method. The method // returns a future, but the method call itself isn't held for long. method_handlers_once: Arc>>>, method_handlers: Arc>>, method_fallback: Option>>, subscription_handlers_once: Arc>>>, subscription_handlers: Arc>>, subscription_fallback: Option>>, } impl MockRpcClient { /// Construct a new [`MockRpcClient`] pub fn builder() -> MockRpcClientBuilder { MockRpcClientBuilder::default() } } impl RpcClientT for MockRpcClient { fn request_raw<'a>( &'a self, method: &'a str, params: Option>, ) -> RawRpcFuture<'a, Box> { // Remove and call a one-time handler if any exist. let mut handlers_once = self.method_handlers_once.lock().unwrap(); if let Some(handlers) = handlers_once.get_mut(method) { if let Some(handler) = handlers.pop_front() { return handler(method, params) } } drop(handlers_once); // Call a specific handler for the method if one is found. let mut handlers = self.method_handlers.lock().unwrap(); if let Some(handler) = handlers.get_mut(method) { return handler(method, params) } drop(handlers); // Call a fallback handler if one exists if let Some(handler) = &self.method_fallback { let mut handler = handler.lock().unwrap(); return handler(method, params) } // Else, method not found. Box::pin(async move { Err(UserError::method_not_found().into()) }) } fn subscribe_raw<'a>( &'a self, sub: &'a str, params: Option>, unsub: &'a str, ) -> RawRpcFuture<'a, RawRpcSubscription> { // Remove and call a one-time handler if any exist. let mut handlers_once = self.subscription_handlers_once.lock().unwrap(); if let Some(handlers) = handlers_once.get_mut(sub) { if let Some(handler) = handlers.pop_front() { return handler(sub, params, unsub) } } drop(handlers_once); // Call a specific handler for the subscriptions if one is found. let mut handlers = self.subscription_handlers.lock().unwrap(); if let Some(handler) = handlers.get_mut(sub) { return handler(sub, params, unsub) } drop(handlers); // Call a fallback handler if one exists if let Some(handler) = &self.subscription_fallback { let mut handler = handler.lock().unwrap(); return handler(sub, params, unsub) } // Else, method not found. Box::pin(async move { Err(UserError::method_not_found().into()) }) } } /// Return responses wrapped in this to have them serialized to JSON. pub struct Json(pub T); impl Json { /// Create a [`Json`] from some serializable value. /// Useful when value types are heterogeneous. pub fn value_of(item: T) -> Self { Json(serde_json::to_value(item).expect("item cannot be converted to a serde_json::Value")) } } /// Anything that can be converted into a valid handler response implements this. pub trait IntoHandlerResponse { /// Convert self into a handler response. fn into_handler_response(self) -> Result, Error>; } impl IntoHandlerResponse for Result { fn into_handler_response(self) -> Result, Error> { self.and_then(|val| val.into_handler_response()) } } impl IntoHandlerResponse for Option { fn into_handler_response(self) -> Result, Error> { self.ok_or_else(|| UserError::method_not_found().into()) .and_then(|val| val.into_handler_response()) } } impl IntoHandlerResponse for Box { fn into_handler_response(self) -> Result, Error> { Ok(self) } } impl IntoHandlerResponse for serde_json::Value { fn into_handler_response(self) -> Result, Error> { serialize_to_raw_value(&self) } } impl IntoHandlerResponse for Json { fn into_handler_response(self) -> Result, Error> { serialize_to_raw_value(&self.0) } } impl IntoHandlerResponse for core::convert::Infallible { fn into_handler_response(self) -> Result, Error> { match self {} } } fn serialize_to_raw_value(val: &T) -> Result, Error> { let res = serde_json::to_string(val).map_err(Error::Deserialization)?; let raw_value = RawValue::from_string(res).map_err(Error::Deserialization)?; Ok(raw_value) } /// Anything that can be a response to a subscription handler implements this. pub trait IntoSubscriptionResponse { /// Convert self into a handler response. fn into_subscription_response(self) -> Result; } // A tuple of a subscription plus some string is treated as a subscription with that string ID. impl > IntoSubscriptionResponse for (T, S) { fn into_subscription_response(self) -> Result { self.0 .into_subscription_response() .map(|mut r| { r.id = Some(self.1.into()); r }) } } impl IntoSubscriptionResponse for tokio::sync::mpsc::Receiver { fn into_subscription_response(self) -> Result { struct IntoStream(tokio::sync::mpsc::Receiver); impl futures::Stream for IntoStream { type Item = T; fn poll_next(mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll> { self.0.poll_recv(cx) } } Ok(RawRpcSubscription { stream: Box::pin(IntoStream(self).map(|item| item.into_handler_response())), id: None, }) } } impl IntoSubscriptionResponse for tokio::sync::mpsc::UnboundedReceiver { fn into_subscription_response(self) -> Result { struct IntoStream(tokio::sync::mpsc::UnboundedReceiver); impl futures::Stream for IntoStream { type Item = T; fn poll_next(mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll> { self.0.poll_recv(cx) } } Ok(RawRpcSubscription { stream: Box::pin(IntoStream(self).map(|item| item.into_handler_response())), id: None, }) } } impl IntoSubscriptionResponse for RawRpcSubscription { fn into_subscription_response(self) -> Result { Ok(self) } } impl IntoSubscriptionResponse for Result { fn into_subscription_response(self) -> Result { self.and_then(|res| res.into_subscription_response()) } } impl IntoSubscriptionResponse for Vec { fn into_subscription_response(self) -> Result { let iter = self.into_iter().map(|item| item.into_handler_response()); Ok(RawRpcSubscription { stream: Box::pin(futures::stream::iter(iter)), id: None, }) } } impl IntoSubscriptionResponse for Option { fn into_subscription_response(self) -> Result { match self { Some(sub) => { sub.into_subscription_response() }, None => { Ok(RawRpcSubscription { stream: Box::pin(futures::stream::empty()), id: None, }) } } } } impl IntoSubscriptionResponse for [T; N] { fn into_subscription_response(self) -> Result { let iter = self.into_iter().map(|item| item.into_handler_response()); Ok(RawRpcSubscription { stream: Box::pin(futures::stream::iter(iter)), id: None, }) } } impl IntoSubscriptionResponse for core::convert::Infallible { fn into_subscription_response(self) -> Result { match self {} } } /// Send the first items and then the second items back on a subscription; /// If any one of the responses is an error, we'll return the error. /// If one response has an ID and the other doesn't, we'll use that ID. pub struct AndThen(pub A, pub B); impl IntoSubscriptionResponse for AndThen { fn into_subscription_response(self) -> Result { let a_responses = self.0.into_subscription_response(); let b_responses = self.1.into_subscription_response(); match (a_responses, b_responses) { (Err(a), _) => { Err(a) }, (_, Err(b)) => { Err(b) }, (Ok(mut a), Ok(b)) => { a.stream = Box::pin(a.stream.chain(b.stream)); a.id = a.id.or(b.id); Ok(a) } } } } /// Send back either one response or the other. pub enum Either { /// The first possibility. A(A), /// The second possibility. B(B) } impl IntoHandlerResponse for Either { fn into_handler_response(self) -> Result, Error> { match self { Either::A(a) => a.into_handler_response(), Either::B(b) => b.into_handler_response(), } } } impl IntoSubscriptionResponse for Either { fn into_subscription_response(self) -> Result { match self { Either::A(a) => a.into_subscription_response(), Either::B(b) => b.into_subscription_response(), } } } #[cfg(test)] mod test { use crate::{RpcClient, rpc_params}; use super::*; #[tokio::test] async fn test_method_params() { let rpc_client = MockRpcClient::builder() .method_handler("foo", async |params| { Json(params) }) .build(); let rpc_client = RpcClient::new(rpc_client); // We get back whatever params we give let res: (i32,i32,i32) = rpc_client.request("foo", rpc_params![1, 2, 3]).await.unwrap(); assert_eq!(res, (1,2,3)); let res: (String,) = rpc_client.request("foo", rpc_params!["hello"]).await.unwrap(); assert_eq!(res, ("hello".to_owned(),)); } #[tokio::test] async fn test_method_handler_then_fallback() { let rpc_client = MockRpcClient::builder() .method_handler("foo", async |_params| { Json(1) }) .method_fallback(async |name, _params| { Json(name) }) .build(); let rpc_client = RpcClient::new(rpc_client); // Whenever we call "foo", we get 1 back. for i in [1,1,1,1] { let res: i32 = rpc_client.request("foo", rpc_params![]).await.unwrap(); assert_eq!(res, i); } // Whenever we call anything else, we get the name of the method back for name in ["bar", "wibble", "steve"] { let res: String = rpc_client.request(name, rpc_params![]).await.unwrap(); assert_eq!(res, name); } } #[tokio::test] async fn test_method_once_then_handler() { let rpc_client = MockRpcClient::builder() .method_handler_once("foo", async |_params| { Json(1) }) .method_handler("foo", async |_params| { Json(2) }) .build(); let rpc_client = RpcClient::new(rpc_client); // Check that we call the "once" one time and then the second after that. for i in [1,2,2,2,2] { let res: i32 = rpc_client.request("foo", rpc_params![]).await.unwrap(); assert_eq!(res, i); } } #[tokio::test] async fn test_method_once() { let rpc_client = MockRpcClient::builder() .method_handler_once("foo", async |_params| { Json(1) }) .method_handler_once("foo", async |_params| { Json(2) }) .method_handler_once("foo", async |_params| { Json(3) }) .build(); let rpc_client = RpcClient::new(rpc_client); // Check that each method is only called once, in the right order. for i in [1,2,3] { let res: i32 = rpc_client.request("foo", rpc_params![]).await.unwrap(); assert_eq!(res, i); } // Check that we get a "method not found" error afterwards. let err = rpc_client.request::("foo", rpc_params![]).await.unwrap_err(); let not_found_code = UserError::method_not_found().code; assert!(matches!(err, Error::User(u) if u.code == not_found_code)); } #[tokio::test] async fn test_subscription_once_then_handler_then_fallback() { let rpc_client = MockRpcClient::builder() .subscription_handler_once("foo", async |_params, _unsub| { vec![Json(0), Json(0)] }) .subscription_handler("foo", async |_params, _unsub| { vec![Json(1), Json(2), Json(3)] }) .subscription_fallback(async |_name, _params, _unsub| { vec![Json(4)] }) .build(); let rpc_client = RpcClient::new(rpc_client); // "foo" returns 0,0 the first time it's subscribed to let sub = rpc_client.subscribe::("foo", rpc_params![], "unsub").await.unwrap(); let res: Vec = sub.map(|i| i.unwrap()).collect().await; assert_eq!(res, vec![0,0]); // then, "foo" returns 1,2,3 in subscription every other time for _ in 1..5 { let sub = rpc_client.subscribe::("foo", rpc_params![], "unsub").await.unwrap(); let res: Vec = sub.map(|i| i.unwrap()).collect().await; assert_eq!(res, vec![1,2,3]); } // anything else returns 4 let sub = rpc_client.subscribe::("bar", rpc_params![], "unsub").await.unwrap(); let res: Vec = sub.map(|i| i.unwrap()).collect().await; assert_eq!(res, vec![4]); } #[tokio::test] async fn test_subscription_and_then_with_channel() { let (tx, rx) = tokio::sync::mpsc::channel(10); let rpc_client = MockRpcClient::builder() .subscription_handler_once("foo", async move |_params, _unsub| { AndThen( // These should be sent first.. vec![Json(1), Json(2), Json(3)], // .. and then anything the channel is handing back. rx ) }) .build(); let rpc_client = RpcClient::new(rpc_client); // Send a few values down the channel to be handed back in "foo" subscription: tokio::spawn(async move { for i in 4..=6 { tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; tx.send(Json(i)).await.unwrap(); } }); // Expect all values back: let sub = rpc_client.subscribe::("foo", rpc_params![], "unsub").await.unwrap(); let res: Vec = sub.map(|i| i.unwrap()).collect().await; assert_eq!(res, vec![1,2,3,4,5,6]); } }