|
|
@@ -13,7 +13,7 @@ pub use crate::mock_sender::Mocks;
|
|
|
use {
|
|
|
crate::{
|
|
|
http_sender::HttpSender,
|
|
|
- mock_sender::{mock_encoded_account, MockSender},
|
|
|
+ mock_sender::{mock_encoded_account, MockSender, MocksMap},
|
|
|
nonblocking::{self, rpc_client::get_rpc_request_str},
|
|
|
rpc_sender::*,
|
|
|
},
|
|
|
@@ -503,6 +503,102 @@ impl RpcClient {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
+ /// Create a mock `RpcClient`.
|
|
|
+ ///
|
|
|
+ /// A mock `RpcClient` contains an implementation of [`RpcSender`] that does
|
|
|
+ /// not use the network, and instead returns synthetic responses, for use in
|
|
|
+ /// tests.
|
|
|
+ ///
|
|
|
+ /// It is primarily for internal use, with limited customizability, and
|
|
|
+ /// behaviors determined by internal Solana test cases. New users should
|
|
|
+ /// consider implementing `RpcSender` themselves and constructing
|
|
|
+ /// `RpcClient` with [`RpcClient::new_sender`] to get mock behavior.
|
|
|
+ ///
|
|
|
+ /// Unless directed otherwise, a mock `RpcClient` will generally return a
|
|
|
+ /// reasonable default response to any request, at least for [`RpcRequest`]
|
|
|
+ /// values for which responses have been implemented.
|
|
|
+ ///
|
|
|
+ /// This mock can be customized in two ways:
|
|
|
+ ///
|
|
|
+ /// 1) By changing the `url` argument, which is not actually a URL, but a
|
|
|
+ /// simple string directive that changes the mock behavior in specific
|
|
|
+ /// scenarios.
|
|
|
+ ///
|
|
|
+ /// It is customary to set the `url` to "succeeds" for mocks that should
|
|
|
+ /// return successfully, though this value is not actually interpreted.
|
|
|
+ ///
|
|
|
+ /// If `url` is "fails" then any call to `send` will return `Ok(Value::Null)`.
|
|
|
+ ///
|
|
|
+ /// Other possible values of `url` are specific to different `RpcRequest`
|
|
|
+ /// values. Read the implementation of `MockSender` (which is non-public)
|
|
|
+ /// for details.
|
|
|
+ ///
|
|
|
+ /// 2) Custom responses can be configured by providing [`MocksMap`]. This type
|
|
|
+ /// is a [`HashMap`] from [`RpcRequest`] to a [`Vec`] of JSON [`Value`] responses,
|
|
|
+ /// Any entries in this map override the default behavior for the given
|
|
|
+ /// request.
|
|
|
+ ///
|
|
|
+ /// The [`RpcClient::new_mock_with_mocks_map`] function offers further
|
|
|
+ /// customization options.
|
|
|
+ ///
|
|
|
+ ///
|
|
|
+ /// # Examples
|
|
|
+ ///
|
|
|
+ /// ```
|
|
|
+ /// # use solana_rpc_client_api::{
|
|
|
+ /// # request::RpcRequest,
|
|
|
+ /// # response::{Response, RpcResponseContext},
|
|
|
+ /// # };
|
|
|
+ /// # use solana_rpc_client::{rpc_client::RpcClient, mock_sender::MocksMap};
|
|
|
+ /// # use serde_json::json;
|
|
|
+ /// // Create a mock with a custom response to the `GetBalance` request
|
|
|
+ /// let account_balance_x = 50;
|
|
|
+ /// let account_balance_y = 100;
|
|
|
+ /// let account_balance_z = 150;
|
|
|
+ /// let account_balance_req_responses = vec![
|
|
|
+ /// (
|
|
|
+ /// RpcRequest::GetBalance,
|
|
|
+ /// json!(Response {
|
|
|
+ /// context: RpcResponseContext {
|
|
|
+ /// slot: 1,
|
|
|
+ /// api_version: None,
|
|
|
+ /// },
|
|
|
+ /// value: json!(account_balance_x),
|
|
|
+ /// })
|
|
|
+ /// ),
|
|
|
+ /// (
|
|
|
+ /// RpcRequest::GetBalance,
|
|
|
+ /// json!(Response {
|
|
|
+ /// context: RpcResponseContext {
|
|
|
+ /// slot: 1,
|
|
|
+ /// api_version: None,
|
|
|
+ /// },
|
|
|
+ /// value: json!(account_balance_y),
|
|
|
+ /// })
|
|
|
+ /// ),
|
|
|
+ /// ];
|
|
|
+ ///
|
|
|
+ /// let mut mocks = MocksMap::from_iter(account_balance_req_responses);
|
|
|
+ /// mocks.insert(
|
|
|
+ /// RpcRequest::GetBalance,
|
|
|
+ /// json!(Response {
|
|
|
+ /// context: RpcResponseContext {
|
|
|
+ /// slot: 1,
|
|
|
+ /// api_version: None,
|
|
|
+ /// },
|
|
|
+ /// value: json!(account_balance_z),
|
|
|
+ /// }),
|
|
|
+ /// );
|
|
|
+ /// let url = "succeeds".to_string();
|
|
|
+ /// let client = RpcClient::new_mock_with_mocks_map(url, mocks);
|
|
|
+ /// ```
|
|
|
+ pub fn new_mock_with_mocks_map<U: ToString>(url: U, mocks: MocksMap) -> Self {
|
|
|
+ Self::new_sender(
|
|
|
+ MockSender::new_with_mocks_map(url, mocks),
|
|
|
+ RpcClientConfig::with_commitment(CommitmentConfig::default()),
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
/// Create an HTTP `RpcClient` from a [`SocketAddr`].
|
|
|
///
|
|
|
/// The client has a default timeout of 30 seconds, and a default [commitment
|
|
|
@@ -3691,9 +3787,7 @@ pub fn create_rpc_client_mocks() -> crate::mock_sender::Mocks {
|
|
|
},
|
|
|
})
|
|
|
.unwrap();
|
|
|
-
|
|
|
mocks.insert(get_account_request, get_account_response);
|
|
|
-
|
|
|
mocks
|
|
|
}
|
|
|
|
|
|
@@ -3948,7 +4042,7 @@ mod tests {
|
|
|
pubkey: pubkey.to_string(),
|
|
|
account: encode_ui_account(&pubkey, &account, UiAccountEncoding::Base64, None, None),
|
|
|
};
|
|
|
- let expected_result = vec![(pubkey, account)];
|
|
|
+ let expected_result = vec![(pubkey, account.clone())];
|
|
|
// Test: without context
|
|
|
{
|
|
|
let mocks: Mocks = [(
|
|
|
@@ -3987,7 +4081,7 @@ mod tests {
|
|
|
slot: 1,
|
|
|
api_version: None,
|
|
|
},
|
|
|
- value: vec![keyed_account],
|
|
|
+ value: vec![keyed_account.clone()],
|
|
|
}))
|
|
|
.unwrap(),
|
|
|
)]
|
|
|
@@ -4012,5 +4106,152 @@ mod tests {
|
|
|
.unwrap();
|
|
|
assert_eq!(expected_result, result);
|
|
|
}
|
|
|
+
|
|
|
+ // Test: Mock with duplicate requests
|
|
|
+ {
|
|
|
+ let expected_result = vec![
|
|
|
+ (pubkey, account.clone()),
|
|
|
+ (pubkey, account.clone()),
|
|
|
+ (pubkey, account.clone()),
|
|
|
+ (pubkey, account.clone()),
|
|
|
+ (pubkey, account.clone()),
|
|
|
+ ];
|
|
|
+
|
|
|
+ let mut mocks: MocksMap = [
|
|
|
+ (
|
|
|
+ RpcRequest::GetProgramAccounts,
|
|
|
+ serde_json::to_value(OptionalContext::Context(Response {
|
|
|
+ context: RpcResponseContext {
|
|
|
+ slot: 1,
|
|
|
+ api_version: None,
|
|
|
+ },
|
|
|
+ value: vec![keyed_account.clone()],
|
|
|
+ }))
|
|
|
+ .unwrap(),
|
|
|
+ ),
|
|
|
+ (
|
|
|
+ RpcRequest::GetProgramAccounts,
|
|
|
+ serde_json::to_value(OptionalContext::Context(Response {
|
|
|
+ context: RpcResponseContext {
|
|
|
+ slot: 1,
|
|
|
+ api_version: None,
|
|
|
+ },
|
|
|
+ value: vec![keyed_account.clone()],
|
|
|
+ }))
|
|
|
+ .unwrap(),
|
|
|
+ ),
|
|
|
+ ]
|
|
|
+ .into_iter()
|
|
|
+ .collect();
|
|
|
+
|
|
|
+ mocks.insert(
|
|
|
+ RpcRequest::GetProgramAccounts,
|
|
|
+ serde_json::to_value(OptionalContext::Context(Response {
|
|
|
+ context: RpcResponseContext {
|
|
|
+ slot: 1,
|
|
|
+ api_version: None,
|
|
|
+ },
|
|
|
+ value: vec![
|
|
|
+ keyed_account.clone(),
|
|
|
+ keyed_account.clone(),
|
|
|
+ keyed_account.clone(),
|
|
|
+ ],
|
|
|
+ }))
|
|
|
+ .unwrap(),
|
|
|
+ );
|
|
|
+
|
|
|
+ mocks.insert(
|
|
|
+ RpcRequest::GetProgramAccounts,
|
|
|
+ serde_json::to_value(OptionalContext::Context(Response {
|
|
|
+ context: RpcResponseContext {
|
|
|
+ slot: 1,
|
|
|
+ api_version: None,
|
|
|
+ },
|
|
|
+ value: Vec::<RpcKeyedAccount>::new(),
|
|
|
+ }))
|
|
|
+ .unwrap(),
|
|
|
+ );
|
|
|
+
|
|
|
+ let rpc_client = RpcClient::new_mock_with_mocks_map("mock_client".to_string(), mocks);
|
|
|
+ let mut result1 = rpc_client
|
|
|
+ .get_program_accounts_with_config(
|
|
|
+ &program_id,
|
|
|
+ RpcProgramAccountsConfig {
|
|
|
+ filters: None,
|
|
|
+ account_config: RpcAccountInfoConfig {
|
|
|
+ encoding: Some(UiAccountEncoding::Base64),
|
|
|
+ data_slice: None,
|
|
|
+ commitment: None,
|
|
|
+ min_context_slot: None,
|
|
|
+ },
|
|
|
+ with_context: Some(true),
|
|
|
+ sort_results: None,
|
|
|
+ },
|
|
|
+ )
|
|
|
+ .unwrap();
|
|
|
+
|
|
|
+ assert_eq!(result1.len(), 1);
|
|
|
+
|
|
|
+ let result2 = rpc_client
|
|
|
+ .get_program_accounts_with_config(
|
|
|
+ &program_id,
|
|
|
+ RpcProgramAccountsConfig {
|
|
|
+ filters: None,
|
|
|
+ account_config: RpcAccountInfoConfig {
|
|
|
+ encoding: Some(UiAccountEncoding::Base64),
|
|
|
+ data_slice: None,
|
|
|
+ commitment: None,
|
|
|
+ min_context_slot: None,
|
|
|
+ },
|
|
|
+ with_context: Some(true),
|
|
|
+ sort_results: None,
|
|
|
+ },
|
|
|
+ )
|
|
|
+ .unwrap();
|
|
|
+
|
|
|
+ assert_eq!(result2.len(), 1);
|
|
|
+
|
|
|
+ let result_3 = rpc_client
|
|
|
+ .get_program_accounts_with_config(
|
|
|
+ &program_id,
|
|
|
+ RpcProgramAccountsConfig {
|
|
|
+ filters: None,
|
|
|
+ account_config: RpcAccountInfoConfig {
|
|
|
+ encoding: Some(UiAccountEncoding::Base64),
|
|
|
+ data_slice: None,
|
|
|
+ commitment: None,
|
|
|
+ min_context_slot: None,
|
|
|
+ },
|
|
|
+ with_context: Some(true),
|
|
|
+ sort_results: None,
|
|
|
+ },
|
|
|
+ )
|
|
|
+ .unwrap();
|
|
|
+
|
|
|
+ assert_eq!(result_3.len(), 3);
|
|
|
+
|
|
|
+ let result_4 = rpc_client
|
|
|
+ .get_program_accounts_with_config(
|
|
|
+ &program_id,
|
|
|
+ RpcProgramAccountsConfig {
|
|
|
+ filters: None,
|
|
|
+ account_config: RpcAccountInfoConfig {
|
|
|
+ encoding: Some(UiAccountEncoding::Base64),
|
|
|
+ data_slice: None,
|
|
|
+ commitment: None,
|
|
|
+ min_context_slot: None,
|
|
|
+ },
|
|
|
+ with_context: Some(true),
|
|
|
+ sort_results: None,
|
|
|
+ },
|
|
|
+ )
|
|
|
+ .unwrap();
|
|
|
+
|
|
|
+ assert_eq!(result_4.len(), 0);
|
|
|
+
|
|
|
+ result1.extend(result2);
|
|
|
+ result1.extend(result_3);
|
|
|
+ assert_eq!(expected_result, result1);
|
|
|
+ }
|
|
|
}
|
|
|
}
|