From 6c8a1adcabd5ac0565f38341bac22ed1e8ae35c0 Mon Sep 17 00:00:00 2001 From: Shramee Srivastav Date: Mon, 27 Nov 2023 06:35:29 +0800 Subject: [PATCH] feat: mock transport for tests mock: use raw strings --- starknet-providers/src/jsonrpc/mod.rs | 2 +- .../src/jsonrpc/transports/http.rs | 33 +++-- .../src/jsonrpc/transports/mock.rs | 138 ++++++++++++++++++ .../src/jsonrpc/transports/mod.rs | 3 + starknet-providers/tests/mock.rs | 121 +++++++++++++++ 5 files changed, 284 insertions(+), 13 deletions(-) create mode 100644 starknet-providers/src/jsonrpc/transports/mock.rs create mode 100644 starknet-providers/tests/mock.rs diff --git a/starknet-providers/src/jsonrpc/mod.rs b/starknet-providers/src/jsonrpc/mod.rs index 17b9d82b..414bc6cc 100644 --- a/starknet-providers/src/jsonrpc/mod.rs +++ b/starknet-providers/src/jsonrpc/mod.rs @@ -23,7 +23,7 @@ use crate::{ }; mod transports; -pub use transports::{HttpTransport, HttpTransportError, JsonRpcTransport}; +pub use transports::{HttpTransport, HttpTransportError, JsonRpcTransport, MockTransport}; #[derive(Debug)] pub struct JsonRpcClient { diff --git a/starknet-providers/src/jsonrpc/transports/http.rs b/starknet-providers/src/jsonrpc/transports/http.rs index 565c6c18..f5766a08 100644 --- a/starknet-providers/src/jsonrpc/transports/http.rs +++ b/starknet-providers/src/jsonrpc/transports/http.rs @@ -37,6 +37,24 @@ impl HttpTransport { url: url.into(), } } + + pub async fn send_request_raw( + &self, + request_body: String, + ) -> Result { + trace!("Sending request via JSON-RPC: {}", request_body); + + let response = self + .client + .post(self.url.clone()) + .body(request_body) + .header("Content-Type", "application/json") + .send() + .await + .map_err(HttpTransportError::Reqwest)?; + + response.text().await.map_err(HttpTransportError::Reqwest) + } } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -60,19 +78,10 @@ impl JsonRpcTransport for HttpTransport { params, }; - let request_body = serde_json::to_string(&request_body).map_err(Self::Error::Json)?; - trace!("Sending request via JSON-RPC: {}", request_body); - - let response = self - .client - .post(self.url.clone()) - .body(request_body) - .header("Content-Type", "application/json") - .send() - .await - .map_err(Self::Error::Reqwest)?; + let request_body = + serde_json::to_string(&request_body).map_err(HttpTransportError::Json)?; + let response_body = self.send_request_raw(request_body).await?; - let response_body = response.text().await.map_err(Self::Error::Reqwest)?; trace!("Response from JSON-RPC: {}", response_body); let parsed_response = serde_json::from_str(&response_body).map_err(Self::Error::Json)?; diff --git a/starknet-providers/src/jsonrpc/transports/mock.rs b/starknet-providers/src/jsonrpc/transports/mock.rs new file mode 100644 index 00000000..74d76e57 --- /dev/null +++ b/starknet-providers/src/jsonrpc/transports/mock.rs @@ -0,0 +1,138 @@ +use core::fmt; +use std::{ + collections::HashMap, + error::Error, + sync::{Arc, Mutex}, +}; + +use async_trait::async_trait; + +use serde::{de::DeserializeOwned, Serialize}; + +use crate::jsonrpc::{transports::JsonRpcTransport, JsonRpcMethod, JsonRpcResponse}; + +use super::{HttpTransport, HttpTransportError}; + +#[derive(Debug)] +pub struct MockTransport { + // Mock requests lookup + mocked_requests: HashMap, + // Mock method lookup if request lookup is None + mocked_methods: HashMap, + // Requests made + pub requests_log: Arc>>, + // HTTP fallback to help build mock requests + http_transport: Option, +} + +#[derive(Debug)] +pub struct MissingRequestMock(String); + +impl fmt::Display for MissingRequestMock { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Error for MissingRequestMock {} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub enum MockTransportError { + Missing(MissingRequestMock), + Http(HttpTransportError), + Json(serde_json::Error), +} + +#[derive(Debug, Serialize)] +struct JsonRpcRequest { + id: u64, + jsonrpc: &'static str, + method: JsonRpcMethod, + params: T, +} + +impl MockTransport { + /// Creates a mock transport to use for tests + /// ``` + /// + /// ``` + pub fn new( + http_transport: Option, + requests_log: Arc>>, + ) -> Self { + Self { + mocked_requests: HashMap::new(), + mocked_methods: HashMap::new(), + requests_log, + http_transport, + } + } + + pub fn mock_request(&mut self, request_json: String, response_json: String) { + self.mocked_requests.insert(request_json, response_json); + } + + pub fn mock_method(&mut self, method: JsonRpcMethod, response_json: String) { + let method_str = serde_json::to_string(&method) + .map_err(MockTransportError::Json) + .unwrap(); + self.mocked_methods.insert(method_str, response_json); + } +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl JsonRpcTransport for MockTransport { + type Error = MockTransportError; + + async fn send_request( + &self, + method: JsonRpcMethod, + params: P, + ) -> Result, MockTransportError> + where + P: Serialize + Send, + R: DeserializeOwned, + { + let request_body = JsonRpcRequest { + id: 1, + jsonrpc: "2.0", + method, + params, + }; + + let method_str = serde_json::to_string(&method).map_err(MockTransportError::Json)?; + + let request_json = + serde_json::to_string(&request_body).map_err(MockTransportError::Json)?; + + let response_body; + + if let Some(request_mock) = self.mocked_requests.get(&request_json) { + response_body = request_mock.clone(); + } else if let Some(method_mock) = self.mocked_methods.get(&method_str) { + response_body = method_mock.clone(); + } else if let Some(http_transport) = &self.http_transport { + response_body = http_transport + .send_request_raw(request_json.clone()) + .await + .map_err(MockTransportError::Http)?; + println!("\nUse this code to mock this request\n\n```rs"); + println!("mock_transport.mock_request(\n r#\"{request_json}\"#.into(),\n r#\"{response_body}\"#.into()\n);"); + // serde_json::to_string(&resp)?; + println!("```\n"); + } else { + return Err(MockTransportError::Missing(MissingRequestMock("".into()))); + } + self.requests_log + .lock() + .unwrap() + .push((request_json.clone(), response_body.clone())); + + let parsed_response = + serde_json::from_str(&response_body).map_err(MockTransportError::Json)?; + + Ok(parsed_response) + } +} diff --git a/starknet-providers/src/jsonrpc/transports/mod.rs b/starknet-providers/src/jsonrpc/transports/mod.rs index 7b119f74..a5091e65 100644 --- a/starknet-providers/src/jsonrpc/transports/mod.rs +++ b/starknet-providers/src/jsonrpc/transports/mod.rs @@ -6,7 +6,10 @@ use std::error::Error; use crate::jsonrpc::{JsonRpcMethod, JsonRpcResponse}; mod http; +mod mock; + pub use http::{HttpTransport, HttpTransportError}; +pub use mock::{MockTransport, MockTransportError}; #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] diff --git a/starknet-providers/tests/mock.rs b/starknet-providers/tests/mock.rs new file mode 100644 index 00000000..ef2a17b2 --- /dev/null +++ b/starknet-providers/tests/mock.rs @@ -0,0 +1,121 @@ +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; + +use starknet_core::types::{BlockId, BlockTag, MaybePendingBlockWithTxHashes}; +use starknet_providers::{ + jsonrpc::{HttpTransport, JsonRpcClient, JsonRpcMethod, MockTransport}, + Provider, +}; +use url::Url; + +fn mock_transport_with_http() -> (Arc>>, MockTransport) { + let rpc_url = + std::env::var("STARKNET_RPC").unwrap_or("https://rpc-goerli-1.starknet.rs/rpc/v0.4".into()); + let http_transport = HttpTransport::new(Url::parse(&rpc_url).unwrap()); + let req_log = Arc::new(Mutex::new(vec![])); + ( + req_log.clone(), + MockTransport::new(Some(http_transport), req_log), + ) +} + +#[tokio::test] +async fn mock_transport_fallback() { + let (_, mock_transport) = mock_transport_with_http(); + + let rpc_client = JsonRpcClient::new(mock_transport); + + let block = rpc_client + .get_block_with_tx_hashes(BlockId::Tag(BlockTag::Latest)) + .await + .unwrap(); + + let block = match block { + MaybePendingBlockWithTxHashes::Block(block) => block, + _ => panic!("unexpected block response type"), + }; + + assert!(block.block_number > 0); +} + +#[tokio::test] +async fn mock_transport() { + let (_, mut mock_transport) = mock_transport_with_http(); + // Block number 100000 + mock_transport.mock_request( + r#"{"id":1,"jsonrpc":"2.0","method":"starknet_getBlockWithTxHashes","params":["latest"]}"#.into(), + r#"{"jsonrpc":"2.0","result":{"block_hash":"0x127edd99c58b5e7405c3fa24920abbf4c3fcfcd532a1c9f496afb917363c386","block_number":100000,"new_root":"0x562df6c11a47b6711242d00318fec36c9f0f2613f7b711cd732857675b4f7f5","parent_hash":"0x294f21cc482c8329b7e1f745cff69071685aec7955de7f5f9dae2be3cc27446","sequencer_address":"0x1176a1bd84444c89232ec27754698e5d2e7e1a7f1539f12027f28b23ec9f3d8","status":"ACCEPTED_ON_L2","timestamp":1701037710,"transactions":["0x1"]},"id":1}"#.into() + ); + + let rpc_client = JsonRpcClient::new(mock_transport); + + let block = rpc_client + .get_block_with_tx_hashes(BlockId::Tag(BlockTag::Latest)) + .await + .unwrap(); + + let block = match block { + MaybePendingBlockWithTxHashes::Block(block) => block, + _ => panic!("unexpected block response type"), + }; + + assert!(block.block_number == 100000); +} + +#[tokio::test] +async fn mock_transport_method() { + let (_, mut mock_transport) = mock_transport_with_http(); + // Block number 100000 + mock_transport.mock_method( + JsonRpcMethod::GetBlockWithTxHashes, + r#"{"jsonrpc":"2.0","result":{"block_hash":"0x127edd99c58b5e7405c3fa24920abbf4c3fcfcd532a1c9f496afb917363c386","block_number":100000,"new_root":"0x562df6c11a47b6711242d00318fec36c9f0f2613f7b711cd732857675b4f7f5","parent_hash":"0x294f21cc482c8329b7e1f745cff69071685aec7955de7f5f9dae2be3cc27446","sequencer_address":"0x1176a1bd84444c89232ec27754698e5d2e7e1a7f1539f12027f28b23ec9f3d8","status":"ACCEPTED_ON_L2","timestamp":1701037710,"transactions":["0x1"]},"id":1}"#.into() + ); + + let rpc_client = JsonRpcClient::new(mock_transport); + + let block = rpc_client + .get_block_with_tx_hashes(BlockId::Tag(BlockTag::Latest)) + .await + .unwrap(); + + let block = match block { + MaybePendingBlockWithTxHashes::Block(block) => block, + _ => panic!("unexpected block response type"), + }; + + assert!(block.block_number == 100000); +} + +#[tokio::test] +async fn mock_transport_log() { + let (logs, mut mock_transport) = mock_transport_with_http(); + + mock_transport.mock_request( + r#"{"id":1,"jsonrpc":"2.0","method":"starknet_getBlockWithTxHashes","params":["latest"]}"#.into(), + r#"{"jsonrpc":"2.0","result":{"block_hash":"0x42fd8152ab51f0d5937ca83225035865c0dcdaea85ab84d38243ec5df23edac","block_number":100000,"new_root":"0x372c133dace5d2842e3791741b6c05af840f249b52febb18f483d1eb38aaf8a","parent_hash":"0x7f6df65f94584de3ff9807c67822197692cc8895aa1de5340af0072ac2ccfb5","sequencer_address":"0x1176a1bd84444c89232ec27754698e5d2e7e1a7f1539f12027f28b23ec9f3d8","status":"ACCEPTED_ON_L2","timestamp":1701033987,"transactions":["0x1"]},"id":1}"#.into() + ); + + let rpc_client = JsonRpcClient::new(mock_transport); + + let block = rpc_client + .get_block_with_tx_hashes(BlockId::Tag(BlockTag::Latest)) + .await + .unwrap(); + + let block = match block { + MaybePendingBlockWithTxHashes::Block(block) => block, + _ => panic!("unexpected block response type"), + }; + + let logs = logs.lock().unwrap(); + + assert!(block.block_number > 0); + + assert!(logs.len() == 1); + // Check request contains getBlockWithTxHashes + assert!(logs[0].0.contains("starknet_getBlockWithTxHashes") == true); + // Check response result has block_hash + assert!(logs[0].1.contains("block_hash") == true); +}