Skip to main content

rs_dapi_client/
mock.rs

1//! Mock implementation of rs-dapi-client for testing
2//!
3//! rs-dapi-client provides `mocks` feature that makes it possible to mock the transport layer.
4//! Core concept of the mocks is a [MockDapiClient] that mimics [DapiClient](crate::DapiClient) behavior and allows
5//! to define expectations for requests and responses using [`MockDapiClient::expect`] function.
6//!
7//! In order to use the mocking feature, you need to:
8//!
9//! 1. Define your requests and responses.
10//! 2. Create a [MockDapiClient] and use it instead of [DapiClient](crate::DapiClient) in your tests.
11//!
12//! See `tests/mock_dapi_client.rs` for an example.
13
14use crate::{
15    transport::TransportRequest, DapiClientError, DapiRequestExecutor, ExecutionError,
16    ExecutionResponse, ExecutionResult, RequestSettings,
17};
18use dapi_grpc::mock::Mockable;
19use dapi_grpc::tonic::async_trait;
20use hex::ToHex;
21use sha2::Digest;
22use std::{
23    any::type_name,
24    collections::HashMap,
25    fmt::{Debug, Display},
26    sync::Mutex,
27};
28
29/// Mock DAPI client.
30///
31/// This is a mock implmeneation of [DapiRequestExecutor] that can be used for testing.
32///
33/// See `tests/mock_dapi_client.rs` for an example.
34#[derive(Default, Debug)]
35pub struct MockDapiClient {
36    expectations: Expectations,
37    request_counts: Mutex<HashMap<Key, usize>>,
38}
39/// Result of executing a mock request
40pub type MockResult<T> = ExecutionResult<<T as TransportRequest>::Response, DapiClientError>;
41
42impl MockDapiClient {
43    /// Create a new mock client
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Number of transport executions of this exact request, including failures.
49    pub fn request_count<R: Mockable>(&self, request: &R) -> usize {
50        self.request_counts
51            .lock()
52            .expect("request counter lock")
53            .get(&Key::new(request))
54            .copied()
55            .unwrap_or_default()
56    }
57
58    /// Add a new expectation for a request
59    pub fn expect<R>(&mut self, request: &R, result: &MockResult<R>) -> Result<&mut Self, MockError>
60    where
61        R: TransportRequest + Mockable,
62        R::Response: Mockable,
63    {
64        let key = self.expectations.add(request, result)?;
65
66        tracing::trace!(
67            %key,
68            request_type = std::any::type_name::<R>(),
69            response_type = std::any::type_name::<R::Response>(),
70            "mock added expectation"
71        );
72
73        Ok(self)
74    }
75
76    /// Remove an existing expectation for a request.
77    ///
78    /// Returns `true` if the expectation was present.
79    pub fn remove<R>(&mut self, request: &R) -> bool
80    where
81        R: TransportRequest + Mockable,
82        R::Response: Mockable,
83    {
84        let key = Key::new(request);
85        let removed = self.expectations.remove(&key);
86
87        if removed {
88            tracing::trace!(
89                %key,
90                request_type = std::any::type_name::<R>(),
91                "mock removed expectation"
92            );
93        }
94
95        removed
96    }
97
98    /// Load expectation from file.
99    ///
100    /// The file must contain JSON structure.
101    /// See [DumpData](crate::DumpData) and [DapiClient::dump_dir()](crate::DapiClient::dump_dir()) more for details.
102    ///
103    /// # Panics
104    ///
105    /// Panics if the file can't be read or the data can't be parsed.
106    #[cfg(feature = "dump")]
107    pub fn load<T, P: AsRef<std::path::Path>>(
108        &mut self,
109        file: P,
110    ) -> Result<(T, MockResult<T>), std::io::Error>
111    where
112        T: TransportRequest + Mockable,
113        T::Response: Mockable,
114    {
115        use crate::DumpData;
116
117        let buf = std::fs::read(file)?;
118        let data = DumpData::<T>::mock_deserialize(&buf).ok_or({
119            std::io::Error::new(std::io::ErrorKind::InvalidData, "unable to parse json")
120        })?;
121
122        let (request, response) = data.deserialize();
123        self.expect(&request, &response).map_err(|e| {
124            std::io::Error::new(
125                std::io::ErrorKind::InvalidInput,
126                format!("unable to add expectation: {}", e),
127            )
128        })?;
129        Ok((request, response))
130    }
131}
132
133#[async_trait]
134impl DapiRequestExecutor for MockDapiClient {
135    async fn execute<R: TransportRequest>(
136        &self,
137        request: R,
138        _settings: RequestSettings,
139    ) -> MockResult<R>
140    where
141        R: Mockable,
142        R::Response: Mockable,
143    {
144        let (key, response) = self.expectations.get(&request);
145        *self
146            .request_counts
147            .lock()
148            .expect("request counter lock")
149            .entry(key.clone())
150            .or_default() += 1;
151
152        tracing::trace!(
153            %key,
154            request_type = std::any::type_name::<R>(),
155            response_type = std::any::type_name::<R::Response>(),
156            response = ?response,
157            "mock execute"
158        );
159
160        if let Some(response) = response {
161            response
162        } else {
163            let error = MockError::MockExpectationNotFound(format!(
164                "unexpected mock request with key {}, use MockDapiClient::expect(): {:?}",
165                key, request
166            ));
167
168            Err(ExecutionError {
169                inner: DapiClientError::Mock(error),
170                retries: 0,
171                address: None,
172            })
173        }
174    }
175}
176
177#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Debug)]
178/// Unique identifier of some serializable object (e.g. request) that can be used as a key in a hashmap.
179pub struct Key([u8; 32]);
180
181impl Key {
182    /// Create a new expectation key from a serializable object (e.g. request).
183    ///
184    /// # Panics
185    ///
186    /// Panics if the object can't be serialized.
187    pub fn new<S: Mockable>(request: &S) -> Self {
188        Self::try_new(request).expect("unable to create a key")
189    }
190
191    /// Generate unique identifier of some serializable object (e.g. request).
192    pub fn try_new<S: Mockable>(request: &S) -> Result<Self, std::io::Error> {
193        // we append type name to the encoded value to make sure that different types
194        // will have different keys
195        let typ = type_name::<S>().replace('&', ""); //remove & from type name
196
197        let mut encoded = S::mock_serialize(request).ok_or(std::io::Error::new(
198            std::io::ErrorKind::InvalidData,
199            format!("mocking not supported for object of type {}", typ),
200        ))?;
201        encoded.append(&mut typ.as_bytes().to_vec());
202
203        let mut e = sha2::Sha256::new();
204        e.update(encoded);
205        let key = e.finalize().into();
206
207        Ok(Self(key))
208    }
209}
210
211impl ToHex for Key {
212    fn encode_hex<T: std::iter::FromIterator<char>>(&self) -> T {
213        self.0.encode_hex()
214    }
215
216    fn encode_hex_upper<T: std::iter::FromIterator<char>>(&self) -> T {
217        self.0.encode_hex_upper()
218    }
219}
220impl Display for Key {
221    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222        Display::fmt(&self.encode_hex::<String>(), f)
223    }
224}
225
226#[derive(Debug, thiserror::Error, Clone)]
227#[cfg_attr(feature = "mocks", derive(serde::Serialize, serde::Deserialize))]
228/// Mock errors
229pub enum MockError {
230    #[error("mock expectation not found for request: {0}")]
231    /// Expectation not found
232    MockExpectationNotFound(String),
233
234    #[error("expectation already defined for request: {0}")]
235    /// Expectation already defined for request
236    MockExpectationConflict(String),
237}
238
239#[derive(Debug)]
240/// Wrapper that encapsulated serialized form of expected response for a request
241struct ExpectedResult(Vec<u8>);
242
243impl ExpectedResult {
244    fn serialize<I: Mockable>(request: &I) -> Self {
245        // We use json because bincode sometimes fail to deserialize
246        Self(request.mock_serialize().expect("encode value"))
247    }
248
249    fn deserialize<O: Mockable>(&self) -> O {
250        // We use json because bincode sometimes fail to deserialize
251        O::mock_deserialize(&self.0).expect("deserialize value")
252    }
253}
254
255#[derive(Default, Debug)]
256/// Requests expected by a mock and their responses.
257struct Expectations {
258    expectations: HashMap<Key, ExpectedResult>,
259}
260
261impl Expectations {
262    /// Add expected request and provide given response.
263    ///
264    /// If the expectation already exists, error is returned.
265    pub fn add<I: Mockable + Debug, O: Mockable>(
266        &mut self,
267        request: &I,
268        result: &O,
269    ) -> Result<Key, MockError> {
270        let key = Key::new(request);
271        let value = ExpectedResult::serialize(result);
272
273        if self.expectations.contains_key(&key) {
274            return Err(MockError::MockExpectationConflict(format!(
275                "expectation with key {} already defined for {} request",
276                key,
277                std::any::type_name::<I>(),
278            )));
279        }
280
281        self.expectations.insert(key.clone(), value);
282
283        Ok(key)
284    }
285
286    /// Get the response for a given request.
287    ///
288    /// Returns `None` if the request has not been expected.
289    pub fn get<I: Mockable, O: Mockable>(&self, request: &I) -> (Key, Option<O>) {
290        let key = Key::new(request);
291
292        let response = self.expectations.get(&key).and_then(|v| v.deserialize());
293
294        (key, response)
295    }
296
297    /// Remove expectation associated with the given key.
298    pub fn remove(&mut self, key: &Key) -> bool {
299        self.expectations.remove(key).is_some()
300    }
301}
302
303impl<R: Mockable> Mockable for ExecutionResponse<R> {
304    fn mock_serialize(&self) -> Option<Vec<u8>> {
305        R::mock_serialize(&self.inner)
306    }
307
308    fn mock_deserialize(data: &[u8]) -> Option<Self> {
309        // TODO: We need serialize retries and address too
310        R::mock_deserialize(data).map(|inner| ExecutionResponse {
311            inner,
312            retries: 0,
313            address: "http://127.0.0.1:9000"
314                .parse()
315                .expect("failed to parse address"),
316        })
317    }
318}
319
320impl<E: Mockable> Mockable for ExecutionError<E> {
321    fn mock_serialize(&self) -> Option<Vec<u8>> {
322        E::mock_serialize(&self.inner)
323    }
324
325    fn mock_deserialize(data: &[u8]) -> Option<Self> {
326        // TODO: We need serialize retries and address too
327        E::mock_deserialize(data).map(|inner| ExecutionError {
328            inner,
329            retries: 0,
330            address: None,
331        })
332    }
333}
334
335/// Create full wrapping object from inner type, using defaults for
336/// fields that cannot be derived from the inner type.
337pub trait FromInner<R>
338where
339    Self: Default,
340{
341    /// Create full wrapping object from inner type, using defaults for
342    /// fields that cannot be derived from the inner type.
343    ///
344    /// Note this is imprecise conversion and should be avoided outside of tests.
345    fn from_inner(inner: R) -> Self;
346}
347
348impl<R> FromInner<R> for ExecutionResponse<R>
349where
350    Self: Default,
351{
352    fn from_inner(inner: R) -> Self {
353        Self {
354            inner,
355            ..Default::default()
356        }
357    }
358}