ic_test/icp/
caller.rs

1//! Defines the call mechanism for interacting with canisters in tests.
2//!
3//! This includes the [`CallBuilder`] for chaining test call setup, the [`CallError`] enum for
4//! error handling, and the [`Caller`] trait for types that can initiate calls.
5
6use std::marker::PhantomData;
7
8use candid::{decode_one, CandidType, Principal};
9use serde::Deserialize;
10use thiserror::Error;
11
12use super::provider::{Provider, RejectResponse};
13
14/// Errors that can occur during a canister method call.
15#[derive(Debug, Error)]
16pub enum CallError {
17    /// Error during argument encoding (Candid serialization).
18    #[error("failed to candid encode call arguments: {}", .0)]
19    ArgumentEncoding(candid::error::Error),
20
21    /// The canister rejected the call, providing a rejection message and error code.
22    #[error("canister rejected: {}, error_code: {}", .0.reject_message, .0.error_code)]
23    Reject(RejectResponse),
24
25    /// Error during decoding the response (Candid deserialization).
26    #[error("failed to candid decode call result: {}", .0)]
27    ResultDecoding(candid::error::Error),
28}
29
30/// Call mode.
31pub enum CallMode {
32    Query,
33    Update,
34}
35
36/// Trait for objects that can initiate canister calls.
37pub trait Caller {
38    type Provider: Provider;
39
40    /// Initiate a call to a canister method.
41    ///
42    /// # Parameters
43    /// - `canister_id`: The target canister's principal.
44    /// - `call_mode`: Whether this is a query or update.
45    /// - `method`: Method name to call.
46    /// - `args`: Encoded Candid arguments or error.
47    ///
48    /// # Returns
49    /// A configured [`CallBuilder`] to execute the call.
50    fn call<ResultType>(
51        &self,
52        canister_id: Principal,
53        call_mode: CallMode,
54        method: &str,
55        args: Result<Vec<u8>, candid::error::Error>,
56    ) -> CallBuilder<ResultType, Self::Provider>
57    where
58        ResultType: for<'a> Deserialize<'a> + CandidType;
59}
60
61/// A builder for creating and executing canister method calls in tests.
62///
63/// Generic over:
64/// - `R`: The expected result type of the method.
65/// - `P`: The provider type (e.g., `IcpUser` or PocketIC).
66pub struct CallBuilder<R: for<'a> Deserialize<'a> + CandidType, P: Provider> {
67    /// The test environment provider.
68    pub provider: P,
69
70    /// The principal of the canister to call.
71    pub canister_id: Principal,
72
73    /// The mode of the call (query or update).
74    pub call_mode: CallMode,
75
76    /// The name of the method being called.
77    pub method: String,
78
79    /// Candid-encoded call arguments, or an encoding error.
80    pub args: Result<Vec<u8>, candid::error::Error>,
81
82    /// Phantom type to carry the result type without storing it.
83    pub _result: PhantomData<R>,
84}
85
86impl<R: for<'a> Deserialize<'a> + CandidType, P: Provider> CallBuilder<R, P> {
87    /// Setup caller of the bulider
88    pub fn with_caller<C: Caller>(self, caller: C) -> CallBuilder<R, C::Provider> {
89        caller.call::<R>(self.canister_id, self.call_mode, &self.method, self.args)
90    }
91
92    /// Switch caller mode to update
93    pub fn with_update(self) -> Self {
94        Self {
95            call_mode: CallMode::Update,
96            ..self
97        }
98    }
99
100    /// Execute the call and returns a `Result` with decoded output or [`CallError`].
101    ///
102    /// # Errors
103    /// Returns a [`CallError`] if encoding, calling, or decoding fails.
104    pub async fn maybe_call(self) -> Result<R, CallError> {
105        let args = self.args.map_err(CallError::ArgumentEncoding)?;
106
107        let result = match self.call_mode {
108            CallMode::Query => {
109                self.provider
110                    .query_call(self.canister_id, &self.method, args)
111                    .await
112            }
113            CallMode::Update => {
114                self.provider
115                    .update_call(self.canister_id, &self.method, args)
116                    .await
117            }
118        };
119
120        let reply = result.map_err(CallError::Reject)?;
121
122        decode_one(&reply).map_err(CallError::ResultDecoding)
123    }
124
125    /// Execute the call assuming there is no error ().
126    ///
127    pub async fn call(self) -> R {
128        self.maybe_call().await.unwrap()
129    }
130}