Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: mock transport for tests #507

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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)
}
}
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 crate::jsonrpc::{JsonRpcMethod, JsonRpcResponse};

mod http;
mod mock;

pub use http::{HttpTransport, HttpTransportError};
pub use mock::{MockTransport, MockTransportError};

Check warning on line 12 in starknet-providers/src/jsonrpc/transports/mod.rs

View workflow job for this annotation

GitHub Actions / udeps

unused import: `MockTransportError`

#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
Expand Down
121 changes: 121 additions & 0 deletions starknet-providers/tests/mock.rs
Original file line number Diff line number Diff line change
@@ -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<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(
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);
}
Loading