dash_sdk/mock/
sdk.rs

1//! Mocking mechanisms for Dash Platform SDK.
2//!
3//! See [MockDashPlatformSdk] for more details.
4use super::MockResponse;
5use crate::{
6    platform::{
7        types::{evonode::EvoNode, identity::IdentityRequest},
8        DocumentQuery, Fetch, FetchMany, Query,
9    },
10    sync::block_on,
11    Error, Sdk,
12};
13use arc_swap::ArcSwapOption;
14use dapi_grpc::platform::v0::{Proof, ResponseMetadata};
15use dapi_grpc::{
16    mock::Mockable,
17    platform::v0::{self as proto},
18};
19use dash_context_provider::{ContextProvider, ContextProviderError};
20use dpp::dashcore::Network;
21use dpp::version::PlatformVersion;
22use drive_proof_verifier::FromProof;
23use rs_dapi_client::mock::MockError;
24use rs_dapi_client::{
25    mock::{Key, MockDapiClient},
26    transport::TransportRequest,
27    DapiClient, DumpData, ExecutionResponse,
28};
29use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
30use tokio::sync::{Mutex, OwnedMutexGuard};
31
32/// Mechanisms to mock Dash Platform SDK.
33///
34/// This object is returned by [Sdk::mock()](crate::Sdk::mock()) and is used to define mock expectations.
35///
36/// Use [MockDashPlatformSdk::expect_fetch_many()] to define expectations for [FetchMany] requests
37/// and [MockDashPlatformSdk::expect_fetch()] for [Fetch] requests.
38///
39/// ## Panics
40///
41/// Can panic on errors.
42#[derive(Debug)]
43pub struct MockDashPlatformSdk {
44    from_proof_expectations: BTreeMap<Key, Vec<u8>>,
45    platform_version: &'static PlatformVersion,
46    dapi: Arc<Mutex<MockDapiClient>>,
47    sdk: ArcSwapOption<Sdk>,
48}
49
50impl MockDashPlatformSdk {
51    /// Returns true when requests should use proofs.
52    ///
53    /// ## Panics
54    ///
55    /// Panics when sdk is not set during initialization.
56    pub fn prove(&self) -> bool {
57        if let Some(sdk) = self.sdk.load().as_ref() {
58            sdk.prove()
59        } else {
60            panic!("sdk must be set when creating mock ")
61        }
62    }
63
64    /// Create new mock SDK.
65    ///
66    /// ## Note
67    ///
68    /// You have to call [MockDashPlatformSdk::set_sdk()] to set sdk, otherwise Mock SDK will panic.
69    pub(crate) fn new(version: &'static PlatformVersion, dapi: Arc<Mutex<MockDapiClient>>) -> Self {
70        Self {
71            from_proof_expectations: Default::default(),
72            platform_version: version,
73            dapi,
74            sdk: ArcSwapOption::new(None),
75        }
76    }
77
78    pub(crate) fn set_sdk(&mut self, sdk: Sdk) {
79        self.sdk.store(Some(Arc::new(sdk)));
80    }
81
82    pub(crate) fn version<'v>(&self) -> &'v PlatformVersion {
83        self.platform_version
84    }
85
86    /// Load all expectations from files in a directory asynchronously.
87    ///
88    /// See [MockDashPlatformSdk::load_expectations_sync()] for more details.
89    #[deprecated(since = "1.4.0", note = "use load_expectations_sync")]
90    pub async fn load_expectations<P: AsRef<std::path::Path> + Send + 'static>(
91        &mut self,
92        dir: P,
93    ) -> Result<&mut Self, Error> {
94        self.load_expectations_sync(dir)
95    }
96
97    /// Load all expectations from files in a directory.
98    ///
99    ///
100    /// By default, mock expectations are loaded when Sdk is built with [SdkBuilder::build()](crate::SdkBuilder::build()).
101    /// This function can be used to load expectations after the Sdk is created, or use alternative location.
102    /// Expectation files must be prefixed with [DapiClient::DUMP_FILE_PREFIX] and
103    /// have `.json` extension.
104    pub fn load_expectations_sync<P: AsRef<std::path::Path>>(
105        &mut self,
106        dir: P,
107    ) -> Result<&mut Self, Error> {
108        let prefix = DapiClient::DUMP_FILE_PREFIX;
109
110        let entries = dir.as_ref().read_dir().map_err(|e| {
111            Error::Config(format!(
112                "cannot load mock expectations from {}: {}",
113                dir.as_ref().display(),
114                e
115            ))
116        })?;
117
118        let files: Vec<PathBuf> = entries
119            .into_iter()
120            .filter_map(|x| x.ok())
121            .filter(|f| {
122                f.file_type().is_ok_and(|t| t.is_file())
123                    && f.file_name().to_string_lossy().starts_with(prefix)
124                    && f.file_name().to_string_lossy().ends_with(".json")
125            })
126            .map(|f| f.path())
127            .collect();
128
129        let mut dapi = block_on(self.dapi.clone().lock_owned())?;
130
131        for filename in &files {
132            let basename = filename.file_name().unwrap().to_str().unwrap();
133            let request_type = basename.split('_').nth(1).unwrap_or_default();
134
135            match request_type {
136                "DocumentQuery" => load_expectation::<DocumentQuery>(&mut dapi, filename)?,
137                "GetEpochsInfoRequest" => {
138                    load_expectation::<proto::GetEpochsInfoRequest>(&mut dapi, filename)?
139                }
140                "GetDataContractRequest" => {
141                    load_expectation::<proto::GetDataContractRequest>(&mut dapi, filename)?
142                }
143                "GetDataContractsRequest" => {
144                    load_expectation::<proto::GetDataContractsRequest>(&mut dapi, filename)?
145                }
146                "GetDataContractHistoryRequest" => {
147                    load_expectation::<proto::GetDataContractHistoryRequest>(&mut dapi, filename)?
148                }
149                "IdentityRequest" => load_expectation::<IdentityRequest>(&mut dapi, filename)?,
150                "GetIdentityRequest" => {
151                    load_expectation::<proto::GetIdentityRequest>(&mut dapi, filename)?
152                }
153
154                "GetIdentityBalanceRequest" => {
155                    load_expectation::<proto::GetIdentityBalanceRequest>(&mut dapi, filename)?
156                }
157                "GetIdentityContractNonceRequest" => {
158                    load_expectation::<proto::GetIdentityContractNonceRequest>(&mut dapi, filename)?
159                }
160                "GetIdentityBalanceAndRevisionRequest" => load_expectation::<
161                    proto::GetIdentityBalanceAndRevisionRequest,
162                >(&mut dapi, filename)?,
163                "GetAddressInfoRequest" => {
164                    load_expectation::<proto::GetAddressInfoRequest>(&mut dapi, filename)?
165                }
166                "GetAddressesInfosRequest" => {
167                    load_expectation::<proto::GetAddressesInfosRequest>(&mut dapi, filename)?
168                }
169                "GetIdentityKeysRequest" => {
170                    load_expectation::<proto::GetIdentityKeysRequest>(&mut dapi, filename)?
171                }
172                "GetProtocolVersionUpgradeStateRequest" => load_expectation::<
173                    proto::GetProtocolVersionUpgradeStateRequest,
174                >(&mut dapi, filename)?,
175                "GetProtocolVersionUpgradeVoteStatusRequest" => {
176                    load_expectation::<proto::GetProtocolVersionUpgradeVoteStatusRequest>(
177                        &mut dapi, filename,
178                    )?
179                }
180                "GetContestedResourcesRequest" => {
181                    load_expectation::<proto::GetContestedResourcesRequest>(&mut dapi, filename)?
182                }
183                "GetContestedResourceVoteStateRequest" => load_expectation::<
184                    proto::GetContestedResourceVoteStateRequest,
185                >(&mut dapi, filename)?,
186                "GetContestedResourceVotersForIdentityRequest" => {
187                    load_expectation::<proto::GetContestedResourceVotersForIdentityRequest>(
188                        &mut dapi, filename,
189                    )?
190                }
191                "GetContestedResourceIdentityVotesRequest" => {
192                    load_expectation::<proto::GetContestedResourceIdentityVotesRequest>(
193                        &mut dapi, filename,
194                    )?
195                }
196                "GetVotePollsByEndDateRequest" => {
197                    load_expectation::<proto::GetVotePollsByEndDateRequest>(&mut dapi, filename)?
198                }
199                "GetPrefundedSpecializedBalanceRequest" => load_expectation::<
200                    proto::GetPrefundedSpecializedBalanceRequest,
201                >(&mut dapi, filename)?,
202                "GetPathElementsRequest" => {
203                    load_expectation::<proto::GetPathElementsRequest>(&mut dapi, filename)?
204                }
205                "GetTotalCreditsInPlatformRequest" => load_expectation::<
206                    proto::GetTotalCreditsInPlatformRequest,
207                >(&mut dapi, filename)?,
208                "GetIdentityTokenBalancesRequest" => {
209                    load_expectation::<proto::GetIdentityTokenBalancesRequest>(&mut dapi, filename)?
210                }
211                "GetIdentitiesTokenBalancesRequest" => load_expectation::<
212                    proto::GetIdentitiesTokenBalancesRequest,
213                >(&mut dapi, filename)?,
214                "GetIdentityTokenInfosRequest" => {
215                    load_expectation::<proto::GetIdentityTokenInfosRequest>(&mut dapi, filename)?
216                }
217                "GetIdentitiesTokenInfosRequest" => {
218                    load_expectation::<proto::GetIdentitiesTokenInfosRequest>(&mut dapi, filename)?
219                }
220                "GetTokenStatusesRequest" => {
221                    load_expectation::<proto::GetTokenStatusesRequest>(&mut dapi, filename)?
222                }
223                "GetTokenTotalSupplyRequest" => {
224                    load_expectation::<proto::GetTokenTotalSupplyRequest>(&mut dapi, filename)?
225                }
226                "GetGroupInfoRequest" => {
227                    load_expectation::<proto::GetGroupInfoRequest>(&mut dapi, filename)?
228                }
229                "GetGroupInfosRequest" => {
230                    load_expectation::<proto::GetGroupInfosRequest>(&mut dapi, filename)?
231                }
232                "GetGroupActionsRequest" => {
233                    load_expectation::<proto::GetGroupActionsRequest>(&mut dapi, filename)?
234                }
235                "GetGroupActionSignersRequest" => {
236                    load_expectation::<proto::GetGroupActionSignersRequest>(&mut dapi, filename)?
237                }
238                "EvoNode" => load_expectation::<EvoNode>(&mut dapi, filename)?,
239                "GetTokenDirectPurchasePricesRequest" => load_expectation::<
240                    proto::GetTokenDirectPurchasePricesRequest,
241                >(&mut dapi, filename)?,
242                "GetTokenPerpetualDistributionLastClaimRequest" => {
243                    load_expectation::<proto::GetTokenPerpetualDistributionLastClaimRequest>(
244                        &mut dapi, filename,
245                    )?
246                }
247                "GetTokenPreProgrammedDistributionsRequest" => {
248                    load_expectation::<proto::GetTokenPreProgrammedDistributionsRequest>(
249                        &mut dapi, filename,
250                    )?
251                }
252                "GetAddressesTrunkStateRequest" => {
253                    load_expectation::<proto::GetAddressesTrunkStateRequest>(&mut dapi, filename)?
254                }
255                _ => {
256                    return Err(Error::Config(format!(
257                        "unknown request type {} in {}, missing match arm in load_expectations?",
258                        request_type,
259                        filename.display()
260                    )))
261                }
262            };
263        }
264
265        Ok(self)
266    }
267
268    /// Expect a [Fetch] request and return provided object.
269    ///
270    /// This method is used to define mock expectations for [Fetch] requests.
271    ///
272    /// ## Generic Parameters
273    ///
274    /// - `O`: Type of the object that will be returned in response to the query. Must implement [Fetch] and [MockResponse].
275    /// - `Q`: Type of the query that will be sent to Platform. Must implement [Query].
276    ///
277    /// ## Arguments
278    ///
279    /// - `query`: Query that will be sent to Platform.
280    /// - `object`: Object that will be returned in response to `query`, or None if the object is expected to not exist.
281    ///
282    /// ## Returns
283    ///
284    /// * Reference to self on success, to allow chaining
285    /// * Error when expectation cannot be set or is already defined for this request
286    ///
287    /// ## Panics
288    ///
289    /// Can panic on errors.
290    ///
291    /// ## Example
292    ///
293    /// ```no_run
294    /// # let r = tokio::runtime::Runtime::new().unwrap();
295    /// #
296    /// # r.block_on(async {
297    ///     use dash_sdk::{Sdk, platform::{Identity, Fetch, dpp::identity::accessors::IdentityGettersV0}};
298    ///
299    ///     let mut api = Sdk::new_mock();
300    ///     // Define expected response
301    ///     let expected: Identity = Identity::random_identity(1, None, api.version())
302    ///         .expect("create expected identity");
303    ///     // Define query that will be sent
304    ///     let query = expected.id();
305    ///     // Expect that in response to `query`, `expected` will be returned
306    ///     api.mock().expect_fetch(query, Some(expected.clone())).await.unwrap();
307    ///
308    ///     // Fetch the identity
309    ///     let retrieved = dpp::prelude::Identity::fetch(&api, query)
310    ///         .await
311    ///         .unwrap()
312    ///         .expect("object should exist");
313    ///
314    ///     // Check that the identity is the same as expected
315    ///     assert_eq!(retrieved, expected);
316    /// # });
317    /// ```
318    pub async fn expect_fetch<O: Fetch + MockResponse, Q: Query<<O as Fetch>::Request>>(
319        &mut self,
320        query: Q,
321        object: Option<O>,
322    ) -> Result<&mut Self, Error>
323    where
324        <<O as Fetch>::Request as TransportRequest>::Response: Default,
325    {
326        let grpc_request = query.query(self.prove()).expect("query must be correct");
327        self.expect(grpc_request, object).await?;
328
329        Ok(self)
330    }
331
332    /// Remove previously defined expectation for a [Fetch] request.
333    ///
334    /// Returns `true` if any expectation was removed.
335    pub async fn remove_fetch_expectation<O, Q>(&mut self, query: Q) -> bool
336    where
337        O: Fetch,
338        Q: Query<<O as Fetch>::Request>,
339        <O as Fetch>::Request: TransportRequest,
340    {
341        let grpc_request = query.query(self.prove()).expect("query must be correct");
342        self.remove(grpc_request).await
343    }
344
345    /// Expect a [FetchMany] request and return provided object.
346    ///
347    /// This method is used to define mock expectations for [FetchMany] requests.
348    ///
349    /// ## Generic Parameters
350    ///
351    /// - `O`: Type of the object that will be returned in response to the query.
352    ///   Must implement [FetchMany].
353    /// - `Q`: Type of the query that will be sent to Platform. Must implement [Query].
354    /// - `R`: Collection type for the results. Must implement [MockResponse].
355    ///
356    /// ## Arguments
357    ///
358    /// - `query`: Query that will be sent to Platform.
359    /// - `objects`: Collection of objects that will be returned in response to `query`, or None if no objects are expected.
360    ///
361    /// ## Returns
362    ///
363    /// * Reference to self on success, to allow chaining
364    /// * Error when expectation cannot be set or is already defined for this request
365    ///
366    /// ## Panics
367    ///
368    /// Can panic on errors.
369    ///
370    /// ## Example
371    ///
372    /// Usage example is similar to
373    /// [MockDashPlatformSdk::expect_fetch()], but the expected
374    /// object must be a vector of objects.
375    pub async fn expect_fetch_many<
376        K: Ord,
377        O: FetchMany<K, R>,
378        Q: Query<<O as FetchMany<K, R>>::Request>,
379        R,
380    >(
381        &mut self,
382        query: Q,
383        objects: Option<R>,
384    ) -> Result<&mut Self, Error>
385    where
386        R: FromIterator<(K, Option<O>)>
387            + MockResponse
388            + FromProof<
389                <O as FetchMany<K, R>>::Request,
390                Request = <O as FetchMany<K, R>>::Request,
391                Response = <<O as FetchMany<K, R>>::Request as TransportRequest>::Response,
392            > + Sync
393            + Send
394            + Default,
395        <<O as FetchMany<K, R>>::Request as TransportRequest>::Response: Default,
396    {
397        let grpc_request = query.query(self.prove()).expect("query must be correct");
398        self.expect(grpc_request, objects).await?;
399
400        Ok(self)
401    }
402
403    /// Save expectations for a request.
404    async fn expect<I: TransportRequest, O: MockResponse>(
405        &mut self,
406        grpc_request: I,
407        returned_object: Option<O>,
408    ) -> Result<(), Error>
409    where
410        I::Response: Default,
411    {
412        let key = Key::new(&grpc_request);
413
414        // detect duplicates
415        if self.from_proof_expectations.contains_key(&key) {
416            return Err(MockError::MockExpectationConflict(format!(
417                "proof expectation key {} already defined for {} request: {:?}",
418                key,
419                std::any::type_name::<I>(),
420                grpc_request
421            ))
422            .into());
423        }
424
425        // This expectation will work for from_proof
426        self.from_proof_expectations
427            .insert(key, returned_object.mock_serialize(self));
428
429        // This expectation will work for execute
430        let mut dapi_guard = self.dapi.lock().await;
431        // We don't really care about the response, as it will be mocked by from_proof, so we provide default()
432        dapi_guard.expect(
433            &grpc_request,
434            &Ok(ExecutionResponse {
435                inner: Default::default(),
436                retries: 0,
437                address: "http://127.0.0.1".parse().expect("failed to parse address"),
438            }),
439        )?;
440
441        Ok(())
442    }
443
444    /// Remove expectations for a request.
445    async fn remove<I: TransportRequest>(&mut self, grpc_request: I) -> bool {
446        let key = Key::new(&grpc_request);
447        let removed_from_proof = self.from_proof_expectations.remove(&key).is_some();
448
449        let mut dapi_guard = self.dapi.lock().await;
450        let removed_from_dapi = dapi_guard.remove(&grpc_request);
451
452        removed_from_proof || removed_from_dapi
453    }
454
455    /// Wrapper around [FromProof] that uses mock expectations, falling back to [FromProof] if no expectation is found.
456    pub(crate) fn parse_proof_with_metadata<I, O: FromProof<I>>(
457        &self,
458        request: O::Request,
459        response: O::Response,
460    ) -> Result<(Option<O>, ResponseMetadata, Proof), drive_proof_verifier::Error>
461    where
462        O::Request: Mockable,
463        Option<O>: MockResponse,
464        // O: FromProof<<O as FromProof<I>>::Request>,
465    {
466        let key = Key::new(&request);
467
468        let data = match self.from_proof_expectations.get(&key) {
469            Some(d) => (
470                Option::<O>::mock_deserialize(self, d),
471                ResponseMetadata::default(),
472                Proof::default(),
473            ),
474            None => {
475                let version = self.version();
476                let provider = self.context_provider()
477                    .ok_or(ContextProviderError::InvalidQuorum(
478                        "expectation not found and quorum info provider not initialized with sdk.mock().quorum_info_dir()".to_string()
479                    ))?;
480                O::maybe_from_proof_with_metadata(
481                    request,
482                    response,
483                    Network::Regtest,
484                    version,
485                    &provider,
486                )?
487            }
488        };
489
490        Ok(data)
491    }
492    /// Return context provider implementation defined for upstream Sdk object.
493    fn context_provider(&self) -> Option<impl ContextProvider> {
494        if let Some(sdk) = self.sdk.load_full() {
495            sdk.clone().context_provider()
496        } else {
497            None
498        }
499    }
500}
501
502/// Load expectation from file and save it to `dapi_guard` mock Dapi client.
503///
504/// This function is used to load expectations from files in a directory.
505/// It is implemented without reference to the `MockDashPlatformSdk` object
506/// to make it easier to use in async context.
507fn load_expectation<T: TransportRequest>(
508    dapi_guard: &mut OwnedMutexGuard<MockDapiClient>,
509    path: &PathBuf,
510) -> Result<(), Error> {
511    let data = DumpData::<T>::load(path)
512        .map_err(|e| {
513            Error::Config(format!(
514                "cannot load mock expectations from {}: {}",
515                path.display(),
516                e
517            ))
518        })?
519        .deserialize();
520    dapi_guard.expect(&data.0, &data.1)?;
521    Ok(())
522}