Skip to content

Commit

Permalink
feat: mock transport helpers
Browse files Browse the repository at this point in the history
feat: mock transport for tests
  • Loading branch information
shramee committed Dec 2, 2023
1 parent 72cd272 commit 010f65c
Show file tree
Hide file tree
Showing 5 changed files with 283 additions and 13 deletions.
2 changes: 1 addition & 1 deletion starknet-providers/src/jsonrpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
Expand Down
33 changes: 21 additions & 12 deletions starknet-providers/src/jsonrpc/transports/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@ impl HttpTransport {
url: url.into(),
}
}

pub async fn send_request_raw(
&self,
request_body: String,
) -> Result<String, HttpTransportError> {
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)]
Expand All @@ -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)?;
Expand Down
138 changes: 138 additions & 0 deletions starknet-providers/src/jsonrpc/transports/mock.rs
Original file line number Diff line number Diff line change
@@ -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<String, String>,
// Mock method lookup if request lookup is None
mocked_methods: HashMap<String, String>,
// Requests made
pub requests_log: Arc<Mutex<Vec<(String, String)>>>,
// HTTP fallback to help build mock requests
http_transport: Option<HttpTransport>,
}

#[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<T> {
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<HttpTransport>,
requests_log: Arc<Mutex<Vec<(String, String)>>>,
) -> 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<P: Sync + Send, R>(
&self,
method: JsonRpcMethod,
params: P,
) -> Result<JsonRpcResponse<R>, 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 {request_json:?}.into(),\n {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)
}
}
3 changes: 3 additions & 0 deletions starknet-providers/src/jsonrpc/transports/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down
120 changes: 120 additions & 0 deletions starknet-providers/tests/mock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
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<Mutex<Vec<(String, String)>>>, 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(
"{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"starknet_getBlockWithTxHashes\",\"params\":[\"latest\"]}".into(),
"{\"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,
"{\"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(
"{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"starknet_getBlockWithTxHashes\",\"params\":[\"latest\"]}".into(),
"{\"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);
}

0 comments on commit 010f65c

Please sign in to comment.