diff --git a/ethportal-api/src/types/jsonrpc/json_rpc_mock.rs b/ethportal-api/src/types/jsonrpc/json_rpc_mock.rs new file mode 100644 index 000000000..3a5afcc65 --- /dev/null +++ b/ethportal-api/src/types/jsonrpc/json_rpc_mock.rs @@ -0,0 +1,97 @@ +use serde::Serialize; +use serde_json::Value; +use tokio::sync::mpsc; + +use super::request::JsonRpcRequest; + +type FilterFn = Box bool + Send>; + +/// Defines whether and how to respond to the request. +struct Interaction { + request_selector_fn: FilterFn, + response: Result, +} + +impl Interaction { + /// Returns `Some(response)` if selector returns `true`, `None` otherwise. + fn maybe_respond(&self, request: &T) -> Option> { + if (self.request_selector_fn)(request) { + Some(self.response.clone()) + } else { + None + } + } +} + +/// Builder for mocking the JSPN-RPC handler. +pub struct MockJsonRpcBuilder { + interactions: Vec>, +} + +impl MockJsonRpcBuilder { + pub fn new() -> Self { + Self { + interactions: vec![], + } + } + + pub fn with_response(mut self, request: T, response: impl Serialize) -> Self { + self.interactions.push(Interaction { + request_selector_fn: Box::new(move |r| r == &request), + response: serde_json::to_value(response).map_err(|err| err.to_string()), + }); + self + } + + pub fn with_error(mut self, request: T, error: impl ToString) -> Self { + self.interactions.push(Interaction { + request_selector_fn: Box::new(move |r| r == &request), + response: Err(error.to_string()), + }); + self + } + + pub fn with_custom_trigger( + mut self, + trigger_fn: FilterFn, + response: Result, + ) -> Self { + self.interactions.push(Interaction { + request_selector_fn: trigger_fn, + response, + }); + self + } + + pub fn or_else(mut self, response: impl Serialize) -> mpsc::UnboundedSender> { + self.interactions.push(Interaction { + request_selector_fn: Box::new(|_| true), + response: serde_json::to_value(response).map_err(|err| err.to_string()), + }); + self.or_fail() + } + + pub fn or_fail(self) -> mpsc::UnboundedSender> { + let (tx, mut rx) = mpsc::unbounded_channel::>(); + tokio::spawn(async move { + while let Some(request) = rx.recv().await { + let response = self + .interactions + .iter() + .find_map(|interaction| interaction.maybe_respond(&request.endpoint)) + .unwrap_or_else(|| Err("No expected response found".to_string())); + request + .resp + .send(response) + .expect("Something should receive response"); + } + }); + tx + } +} + +impl Default for MockJsonRpcBuilder { + fn default() -> Self { + Self::new() + } +} diff --git a/ethportal-api/src/types/jsonrpc/mod.rs b/ethportal-api/src/types/jsonrpc/mod.rs index 30e1681b7..9b113aef0 100644 --- a/ethportal-api/src/types/jsonrpc/mod.rs +++ b/ethportal-api/src/types/jsonrpc/mod.rs @@ -1,3 +1,4 @@ pub mod endpoints; +pub mod json_rpc_mock; pub mod params; pub mod request;