diff --git a/Cargo.lock b/Cargo.lock index 0b1adb5..c673947 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1305,7 +1305,7 @@ dependencies = [ ] [[package]] -name = "ic-mock-http-canister-runtime" +name = "ic-pocket-canister-runtime" version = "0.1.0" dependencies = [ "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 0cc5527..636a214 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ "examples/multi_canister", "ic-agent-canister-runtime", "ic-canister-runtime", - "ic-mock-http-canister-runtime", + "ic-pocket-canister-runtime", "test_fixtures", ] resolver = "2" diff --git a/ic-canister-runtime/README.md b/ic-canister-runtime/README.md index 0e7ea5c..e98694e 100644 --- a/ic-canister-runtime/README.md +++ b/ic-canister-runtime/README.md @@ -8,7 +8,7 @@ Library to abstract the canister runtime so that code making requests to canisters can be reused, e.g.: * in production using [`ic_cdk`](https://crates.io/crates/ic-cdk), * in unit tests by mocking this crate's `Runtime` trait, -* in integration tests by implementing this trait for [PocketIC](https://internetcomputer.org/docs/building-apps/test/pocket-ic) or using the `MockHttpRuntime` implementation from the [`ic-mock-http-canister-runtime`](https://crates.io/crates/ic-mock-http-canister-runtime) crate. +* in integration tests by implementing this trait for [PocketIC](https://internetcomputer.org/docs/building-apps/test/pocket-ic) yourself or using the `PokcetIcRuntime` implementation from the [`ic-pocket-canister-runtime`](https://crates.io/crates/ic-pocket-canister-runtime) crate. ## Usage @@ -55,7 +55,7 @@ assert!(http_request_result.contains("Hello, World!")); assert!(http_request_result.contains("\"X-Id\": \"42\"")); ``` -See the [Rust documentation](https://docs.rs/ic-canister-runtime) for more details as well as the [`ic-mock-http-canister-runtime`](https://docs.rs/ic-mock-http-canister-runtime) and [`ic-agent-canister-runtime`](https://docs.rs/ic-agent-canister-runtime) crates for some further implementations of the `Runtime` trait. +See the [Rust documentation](https://docs.rs/ic-canister-runtime) for more details as well as the [`ic-pocket-canister-runtime`](https://docs.rs/ic-pocket-canister-runtime) and [`ic-agent-canister-runtime`](https://docs.rs/ic-agent-canister-runtime) crates for some further implementations of the `Runtime` trait. ## Cargo Features diff --git a/ic-mock-http-canister-runtime/src/lib.rs b/ic-mock-http-canister-runtime/src/lib.rs deleted file mode 100644 index 518e7e0..0000000 --- a/ic-mock-http-canister-runtime/src/lib.rs +++ /dev/null @@ -1,232 +0,0 @@ -//! Library to mock HTTP outcalls on the Internet Computer leveraging the [`ic_canister_runtime`] -//! crate's [`Runtime`] trait as well as [`PocketIc`]. - -#![forbid(unsafe_code)] -#![forbid(missing_docs)] - -mod mock; - -use async_trait::async_trait; -use candid::{decode_one, encode_args, utils::ArgumentEncoder, CandidType, Principal}; -use ic_canister_runtime::{IcError, Runtime}; -use ic_cdk::call::{CallFailed, CallRejected}; -use ic_error_types::RejectCode; -pub use mock::{ - json::{JsonRpcRequestMatcher, JsonRpcResponse}, - AnyCanisterHttpRequestMatcher, CanisterHttpReject, CanisterHttpReply, - CanisterHttpRequestMatcher, MockHttpOutcall, MockHttpOutcallBuilder, MockHttpOutcalls, - MockHttpOutcallsBuilder, -}; -use pocket_ic::{ - common::rest::{CanisterHttpRequest, CanisterHttpResponse, MockCanisterHttpResponse}, - nonblocking::PocketIc, - RejectResponse, -}; -use serde::de::DeserializeOwned; -use std::{ - sync::{Arc, Mutex}, - time::Duration, -}; - -const DEFAULT_MAX_RESPONSE_BYTES: u64 = 2_000_000; -const MAX_TICKS: usize = 10; - -/// [`Runtime`] using [`PocketIc`] to mock HTTP outcalls. -/// -/// This runtime allows making calls to canisters through Pocket IC while verifying the HTTP -/// outcalls made and mocking their responses. -/// -/// # Examples -/// Call the `make_http_post_request` endpoint on the example [`http_canister`] deployed with -/// Pocket IC and mock the resulting HTTP outcall. -/// ```rust, no_run -/// # #[tokio::main] -/// # async fn main() -> Result<(), Box> { -/// use ic_canister_runtime::Runtime; -/// use ic_mock_http_canister_runtime::{ -/// AnyCanisterHttpRequestMatcher, CanisterHttpReply, MockHttpOutcallsBuilder, -/// MockHttpRuntime -/// }; -/// use pocket_ic::nonblocking::PocketIc; -/// use std::sync::Arc; -/// # use candid::Principal; -/// -/// let mocks = MockHttpOutcallsBuilder::new() -/// .given(AnyCanisterHttpRequestMatcher) -/// .respond_with( -/// CanisterHttpReply::with_status(200) -/// .with_body(r#"{"data": "Hello, World!", "headers": {"X-Id": "42"}}"#) -/// ); -/// -/// let pocket_ic = Arc::new(PocketIc::new().await); -/// let runtime = MockHttpRuntime::new(pocket_ic, Principal::anonymous(), mocks); -/// # let canister_id = Principal::anonymous(); -/// -/// let http_request_result: String = runtime -/// .update_call(canister_id, "make_http_post_request", (), 0) -/// .await -/// .expect("Call to `http_canister` failed"); -/// -/// assert!(http_request_result.contains("Hello, World!")); -/// assert!(http_request_result.contains("\"X-Id\": \"42\"")); -/// # Ok(()) -/// # } -/// ``` -/// -/// [`http_canister`]: https://github.com/dfinity/canhttp/tree/main/examples/http_canister/ -pub struct MockHttpRuntime { - env: Arc, - caller: Principal, - mocks: Mutex, -} - -impl MockHttpRuntime { - /// Create a new [`MockHttpRuntime`] with the given [`PocketIc`] and [`MockHttpOutcalls`]. - /// All calls to canisters are made using the given caller identity. - pub fn new(env: Arc, caller: Principal, mocks: impl Into) -> Self { - Self { - env, - caller, - mocks: Mutex::new(mocks.into()), - } - } -} - -#[async_trait] -impl Runtime for MockHttpRuntime { - async fn update_call( - &self, - id: Principal, - method: &str, - args: In, - _cycles: u128, - ) -> Result - where - In: ArgumentEncoder + Send, - Out: CandidType + DeserializeOwned, - { - let message_id = self - .env - .submit_call( - id, - self.caller, - method, - encode_args(args).unwrap_or_else(panic_when_encode_fails), - ) - .await - .unwrap(); - self.execute_mocks().await; - self.env - .await_call(message_id) - .await - .map(decode_call_response) - .map_err(parse_reject_response)? - } - - async fn query_call( - &self, - id: Principal, - method: &str, - args: In, - ) -> Result - where - In: ArgumentEncoder + Send, - Out: CandidType + DeserializeOwned, - { - self.env - .query_call( - id, - self.caller, - method, - encode_args(args).unwrap_or_else(panic_when_encode_fails), - ) - .await - .map(decode_call_response) - .map_err(parse_reject_response)? - } -} - -impl MockHttpRuntime { - async fn execute_mocks(&self) { - loop { - let pending_requests = tick_until_http_requests(self.env.as_ref()).await; - if let Some(request) = pending_requests.first() { - let maybe_mock = { - let mut mocks = self.mocks.lock().unwrap(); - mocks.pop_matching(request) - }; - match maybe_mock { - Some(mock) => { - let mock_response = MockCanisterHttpResponse { - subnet_id: request.subnet_id, - request_id: request.request_id, - response: check_response_size(request, mock.response), - additional_responses: vec![], - }; - self.env.mock_canister_http_response(mock_response).await; - } - None => { - panic!("No mocks matching the request: {:?}", request); - } - } - } else { - return; - } - } - } -} - -fn check_response_size( - request: &CanisterHttpRequest, - response: CanisterHttpResponse, -) -> CanisterHttpResponse { - if let CanisterHttpResponse::CanisterHttpReply(reply) = &response { - let max_response_bytes = request - .max_response_bytes - .unwrap_or(DEFAULT_MAX_RESPONSE_BYTES); - if reply.body.len() as u64 > max_response_bytes { - // Approximate replica behavior since headers are not accounted for. - return CanisterHttpResponse::CanisterHttpReject( - pocket_ic::common::rest::CanisterHttpReject { - reject_code: RejectCode::SysFatal as u64, - message: format!("Http body exceeds size limit of {max_response_bytes} bytes.",), - }, - ); - } - } - response -} - -fn parse_reject_response(response: RejectResponse) -> IcError { - CallFailed::CallRejected(CallRejected::with_rejection( - response.reject_code as u32, - response.reject_message, - )) - .into() -} - -fn decode_call_response(bytes: Vec) -> Result -where - Out: CandidType + DeserializeOwned, -{ - decode_one(&bytes).map_err(|e| IcError::CandidDecodeFailed { - message: e.to_string(), - }) -} - -fn panic_when_encode_fails(err: candid::error::Error) -> Vec { - panic!("failed to encode args: {err}") -} - -async fn tick_until_http_requests(env: &PocketIc) -> Vec { - let mut requests = Vec::new(); - for _ in 0..MAX_TICKS { - requests = env.get_canister_http().await; - if !requests.is_empty() { - break; - } - env.tick().await; - env.advance_time(Duration::from_nanos(1)).await; - } - requests -} diff --git a/ic-mock-http-canister-runtime/CHANGELOG.md b/ic-pocket-canister-runtime/CHANGELOG.md similarity index 100% rename from ic-mock-http-canister-runtime/CHANGELOG.md rename to ic-pocket-canister-runtime/CHANGELOG.md diff --git a/ic-mock-http-canister-runtime/Cargo.toml b/ic-pocket-canister-runtime/Cargo.toml similarity index 79% rename from ic-mock-http-canister-runtime/Cargo.toml rename to ic-pocket-canister-runtime/Cargo.toml index 5c56f3d..fefdc7e 100644 --- a/ic-mock-http-canister-runtime/Cargo.toml +++ b/ic-pocket-canister-runtime/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "ic-mock-http-canister-runtime" +name = "ic-pocket-canister-runtime" version = "0.1.0" -description = "Mock HTTP outcalls with Pocket IC" +description = "Canisters runtime on the Internet Computer using Pocket IC" license.workspace = true readme.workspace = true homepage.workspace = true @@ -9,7 +9,7 @@ authors.workspace = true edition.workspace = true include = ["src", "Cargo.toml", "CHANGELOG.md", "LICENSE", "README.md"] repository.workspace = true -documentation = "https://docs.rs/ic-mock-http-canister-runtime" +documentation = "https://docs.rs/ic-pocket-canister-runtime" [dependencies] async-trait = { workspace = true } @@ -21,7 +21,5 @@ ic-error-types = { workspace = true } pocket-ic = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -url = { workspace = true } - -[dev-dependencies] tokio = { workspace = true } +url = { workspace = true } diff --git a/ic-mock-http-canister-runtime/LICENSE b/ic-pocket-canister-runtime/LICENSE similarity index 100% rename from ic-mock-http-canister-runtime/LICENSE rename to ic-pocket-canister-runtime/LICENSE diff --git a/ic-mock-http-canister-runtime/NOTICE b/ic-pocket-canister-runtime/NOTICE similarity index 100% rename from ic-mock-http-canister-runtime/NOTICE rename to ic-pocket-canister-runtime/NOTICE diff --git a/ic-mock-http-canister-runtime/README.md b/ic-pocket-canister-runtime/README.md similarity index 66% rename from ic-mock-http-canister-runtime/README.md rename to ic-pocket-canister-runtime/README.md index a9ae4da..7eddec6 100644 --- a/ic-mock-http-canister-runtime/README.md +++ b/ic-pocket-canister-runtime/README.md @@ -2,27 +2,28 @@ [![DFinity Forum](https://img.shields.io/badge/help-post%20on%20forum.dfinity.org-blue?style=for-the-badge)](https://forum.dfinity.org/) [![GitHub license](https://img.shields.io/badge/license-Apache%202.0-blue.svg?logo=apache&style=for-the-badge)](LICENSE) +# `ic-pocket-canister-runtime` -# `ic-mock-http-canister-runtime` - -Library to mock [HTTPs outcalls](https://internetcomputer.org/https-outcalls) on the Internet Computer leveraging the [`ic_canister_runtime`](https://crates.io/crates/ic-canister-runtime) crate's `Runtime` trait as well as [PocketIC](https://internetcomputer.org/docs/building-apps/test/pocket-ic). +Implementation of the [`ic_canister_runtime`](https://crates.io/crates/ic-canister-runtime) crate's `Runtime` trait for [PocketIC](https://internetcomputer.org/docs/building-apps/test/pocket-ic) allowing to mock +[HTTPs outcalls](https://internetcomputer.org/https-outcalls). ## Usage -Add this to your `Cargo.toml` (see [crates.io](https://crates.io/crates/ic-mock-http-canister-runtime) for the latest version): +Add this to your `Cargo.toml` (see [crates.io](https://crates.io/crates/ic-pocket-canister-runtime) for the latest version): ```toml ic-canister-runtime = "0.1.0" -ic-mock-http-canister-runtime = "0.1.0" +ic-pocket-canister-runtime = "0.1.0" ``` Then, use the library to mock HTTP outcalls for canister deployed with PocketIC, as follows: ```rust use ic_canister_runtime::Runtime; -use ic_mock_http_canister_runtime::{ +use ic_pocket_canister_runtime::{ AnyCanisterHttpRequestMatcher, CanisterHttpReply, MockHttpOutcallsBuilder, MockHttpRuntime }; +use pocket_ic::nonblocking::PocketIc; let mocks = MockHttpOutcallsBuilder::new() .given(AnyCanisterHttpRequestMatcher) @@ -31,7 +32,9 @@ let mocks = MockHttpOutcallsBuilder::new() .with_body(r#"{"data": "Hello, World!", "headers": {"X-Id": "42"}}"#) ); -let runtime = MockHttpRuntime::new(pocket_ic, Principal::anonymous(), mocks); +let pocket_ic = PocketIc::new().await; +let runtime = MockHttpRuntime::new(&pocket_ic, Principal::anonymous()) + .with_http_mocks(mocks.build()); let http_request_result: String = runtime .update_call(canister_id, "make_http_post_request", (), 0) @@ -42,7 +45,7 @@ assert!(http_request_result.contains("Hello, World!")); assert!(http_request_result.contains("\"X-Id\": \"42\"")); ``` -See the [Rust documentation](https://docs.rs/ic-mock-http-canister-runtime) for more details. +See the [Rust documentation](https://docs.rs/ic-pocket-canister-runtime) for more details. ## License diff --git a/ic-pocket-canister-runtime/src/lib.rs b/ic-pocket-canister-runtime/src/lib.rs new file mode 100644 index 0000000..4ecd876 --- /dev/null +++ b/ic-pocket-canister-runtime/src/lib.rs @@ -0,0 +1,339 @@ +//! Library to mock HTTP outcalls on the Internet Computer leveraging the [`ic_canister_runtime`] +//! crate's [`Runtime`] trait as well as [`PocketIc`]. + +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +mod mock; + +use async_trait::async_trait; +use candid::{decode_one, encode_args, utils::ArgumentEncoder, CandidType, Principal}; +use ic_canister_runtime::{IcError, Runtime}; +use ic_cdk::call::{CallFailed, CallRejected}; +use ic_error_types::RejectCode; +pub use mock::{ + json::{JsonRpcRequestMatcher, JsonRpcResponse}, + AnyCanisterHttpRequestMatcher, CanisterHttpReject, CanisterHttpReply, + CanisterHttpRequestMatcher, MockHttpOutcall, MockHttpOutcallBuilder, MockHttpOutcalls, + MockHttpOutcallsBuilder, +}; +use pocket_ic::{ + common::rest::{CanisterHttpRequest, CanisterHttpResponse, MockCanisterHttpResponse}, + nonblocking::PocketIc, + RejectResponse, +}; +use serde::de::DeserializeOwned; +use std::time::Duration; +use tokio::sync::Mutex; + +const DEFAULT_MAX_RESPONSE_BYTES: u64 = 2_000_000; +const MAX_TICKS: usize = 10; + +#[derive(Default)] +enum PocketIcLiveMode { + Enabled, + #[default] + Disabled, +} + +/// [`Runtime`] using [`PocketIc`] to make calls to canisters. +/// +/// # Examples +/// Call the `make_http_post_request` endpoint on the example [`http_canister`] deployed with +/// Pocket IC and mock the resulting HTTP outcall. +/// ```rust, no_run +/// # #[tokio::main] +/// # async fn main() -> Result<(), Box> { +/// use ic_canister_runtime::Runtime; +/// use ic_pocket_canister_runtime::{ +/// AnyCanisterHttpRequestMatcher, CanisterHttpReply, MockHttpOutcallsBuilder, +/// PocketIcRuntime +/// }; +/// use pocket_ic::nonblocking::PocketIc; +/// # use candid::Principal; +/// +/// let mocks = MockHttpOutcallsBuilder::new() +/// .given(AnyCanisterHttpRequestMatcher) +/// .respond_with( +/// CanisterHttpReply::with_status(200) +/// .with_body(r#"{"data": "Hello, World!", "headers": {"X-Id": "42"}}"#) +/// ); +/// +/// let pocket_ic = PocketIc::new().await; +/// let runtime = PocketIcRuntime::new(&pocket_ic, Principal::anonymous()) +/// .await +/// .with_http_mocks(mocks.build()); +/// # let canister_id = Principal::anonymous(); +/// +/// let http_request_result: String = runtime +/// .update_call(canister_id, "make_http_post_request", (), 0) +/// .await +/// .expect("Call to `http_canister` failed"); +/// +/// assert!(http_request_result.contains("Hello, World!")); +/// assert!(http_request_result.contains("\"X-Id\": \"42\"")); +/// # Ok(()) +/// # } +/// ``` +/// +/// [`http_canister`]: https://github.com/dfinity/canhttp/tree/main/examples/http_canister/ +pub struct PocketIcRuntime<'a> { + env: &'a PocketIc, + caller: Principal, + // The mocks are stored in a Mutex> so they can be modified in the implementation of + // the `Runtime::update_call` method using interior mutability. + // This is necessary since `Runtime::update_call` takes an immutable reference to the runtime. + mocks: Option>>, + mode: PocketIcLiveMode, +} + +impl<'a> PocketIcRuntime<'a> { + /// Create a new [`PocketIcRuntime`] with the given [`PocketIc`]. + /// All calls to canisters are made using the given caller identity. + /// + /// The given [`PocketIc`] instance must not be configured to use live mode. + /// To build a [`PocketIcRuntime`] instance with [`PocketIc`] running in live mode, + /// see [`PocketIcRuntime::with_live_mode`]. + pub async fn new(env: &'a PocketIc, caller: Principal) -> Self { + assert!( + !env.auto_progress_enabled().await, + "Auto-progress is be enabled on `PocketIc`. To use `PocketIc` in live mode, use `PocketIcRuntime::with_live_mode`" + ); + Self { + env, + caller, + mocks: None, + mode: Default::default(), + } + } + + /// Create a new [`PocketIcRuntime`] with the given [`PocketIc`] configured to use live mode. + /// + /// In live mode, PocketIC automatically progresses time and executes outgoing HTTPS requests + /// as they occur. + /// This makes test execution **non-deterministic**. + /// + /// **Important:** + /// This method **does not** switch the underlying [`PocketIc`] instance to live mode. + /// You must first call [`PocketIc::make_live`] to do so. + /// + /// # Panics + /// Panics if the underlying [`PocketIc`] instance is not in live mode + /// (i.e., if [`PocketIc::make_live`] was not called beforehand). + /// + /// # See also + /// - [PocketIC live mode documentation](https://github.com/dfinity/ic/blob/f0c82237ae16745ac54dd3838b3f91ce32a6bc52/packages/pocket-ic/HOWTO.md?plain=1#L43) + pub async fn with_live_mode(env: &'a PocketIc, caller: Principal) -> Self { + assert!( + env.auto_progress_enabled().await, + "Auto-progress must be enabled on `PocketIc` instance with `PocketIc::make_live()`" + ); + Self { + env, + caller, + mocks: None, + mode: PocketIcLiveMode::Enabled, + } + } + + /// Mock HTTP outcalls and their responses. + /// + /// This allows making calls to canisters through Pocket IC while verifying the HTTP outcalls + /// made and mocking their responses. + /// + /// # Examples + /// Call the `make_http_post_request` endpoint on the example [`http_canister`] deployed with + /// Pocket IC and mock the resulting HTTP outcall. + /// ```rust, no_run + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// use ic_canister_runtime::Runtime; + /// use ic_pocket_canister_runtime::{ + /// AnyCanisterHttpRequestMatcher, CanisterHttpReply, MockHttpOutcallsBuilder, + /// PocketIcRuntime + /// }; + /// use pocket_ic::nonblocking::PocketIc; + /// # use candid::Principal; + /// + /// let mocks = MockHttpOutcallsBuilder::new() + /// // Matches any HTTP outcall request + /// .given(AnyCanisterHttpRequestMatcher) + /// // Assert that the HTTP outcall response has the given status code and body + /// .respond_with( + /// CanisterHttpReply::with_status(200) + /// .with_body(r#"{"data": "Hello, World!", "headers": {"X-Id": "42"}}"#) + /// ); + /// + /// let pocket_ic = PocketIc::new().await; + /// let runtime = PocketIcRuntime::new(&pocket_ic, Principal::anonymous()) + /// .await + /// .with_http_mocks(mocks.build()); + /// # let canister_id = Principal::anonymous(); + /// + /// let http_request_result: String = runtime + /// .update_call(canister_id, "make_http_post_request", (), 0) + /// .await + /// .expect("Call to `http_canister` failed"); + /// + /// assert!(http_request_result.contains("Hello, World!")); + /// assert!(http_request_result.contains("\"X-Id\": \"42\"")); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`http_canister`]: https://github.com/dfinity/canhttp/tree/main/examples/http_canister/ + pub fn with_http_mocks(mut self, mocks: impl ExecuteHttpOutcallMocks + 'static) -> Self { + self.mocks = Some(Mutex::new(Box::new(mocks))); + self + } +} + +#[async_trait] +impl Runtime for PocketIcRuntime<'_> { + async fn update_call( + &self, + id: Principal, + method: &str, + args: In, + _cycles: u128, + ) -> Result + where + In: ArgumentEncoder + Send, + Out: CandidType + DeserializeOwned, + { + let message_id = self + .env + .submit_call( + id, + self.caller, + method, + encode_args(args).unwrap_or_else(panic_when_encode_fails), + ) + .await + .map_err(parse_reject_response)?; + if let Some(mock) = &self.mocks { + mock.try_lock() + .unwrap() + .execute_http_outcall_mocks(self.env) + .await; + } + (match self.mode { + PocketIcLiveMode::Disabled => self.env.await_call(message_id).await, + PocketIcLiveMode::Enabled => self.env.await_call_no_ticks(message_id).await, + }) + .map(decode_call_response) + .map_err(parse_reject_response)? + } + + async fn query_call( + &self, + id: Principal, + method: &str, + args: In, + ) -> Result + where + In: ArgumentEncoder + Send, + Out: CandidType + DeserializeOwned, + { + self.env + .query_call( + id, + self.caller, + method, + encode_args(args).unwrap_or_else(panic_when_encode_fails), + ) + .await + .map(decode_call_response) + .map_err(parse_reject_response)? + } +} + +/// Execute HTTP outcall mocks. +#[async_trait] +pub trait ExecuteHttpOutcallMocks: Send + Sync { + /// Execute HTTP outcall mocks. + async fn execute_http_outcall_mocks(&mut self, runtime: &PocketIc) -> (); +} + +#[async_trait] +impl ExecuteHttpOutcallMocks for MockHttpOutcalls { + async fn execute_http_outcall_mocks(&mut self, env: &PocketIc) -> () { + loop { + let pending_requests = tick_until_http_requests(env).await; + if let Some(request) = pending_requests.first() { + let maybe_mock = { self.pop_matching(request) }; + match maybe_mock { + Some(mock) => { + let mock_response = MockCanisterHttpResponse { + subnet_id: request.subnet_id, + request_id: request.request_id, + response: check_response_size(request, mock.response), + additional_responses: vec![], + }; + env.mock_canister_http_response(mock_response).await; + } + None => { + panic!("No mocks matching the request: {:?}", request); + } + } + } else { + return; + } + } + } +} + +fn check_response_size( + request: &CanisterHttpRequest, + response: CanisterHttpResponse, +) -> CanisterHttpResponse { + if let CanisterHttpResponse::CanisterHttpReply(reply) = &response { + let max_response_bytes = request + .max_response_bytes + .unwrap_or(DEFAULT_MAX_RESPONSE_BYTES); + if reply.body.len() as u64 > max_response_bytes { + // Approximate replica behavior since headers are not accounted for. + return CanisterHttpResponse::CanisterHttpReject( + pocket_ic::common::rest::CanisterHttpReject { + reject_code: RejectCode::SysFatal as u64, + message: format!("Http body exceeds size limit of {max_response_bytes} bytes.",), + }, + ); + } + } + response +} + +fn parse_reject_response(response: RejectResponse) -> IcError { + CallFailed::CallRejected(CallRejected::with_rejection( + response.reject_code as u32, + response.reject_message, + )) + .into() +} + +fn decode_call_response(bytes: Vec) -> Result +where + Out: CandidType + DeserializeOwned, +{ + decode_one(&bytes).map_err(|e| IcError::CandidDecodeFailed { + message: e.to_string(), + }) +} + +fn panic_when_encode_fails(err: candid::error::Error) -> Vec { + panic!("failed to encode args: {err}") +} + +async fn tick_until_http_requests(env: &PocketIc) -> Vec { + let mut requests = Vec::new(); + for _ in 0..MAX_TICKS { + requests = env.get_canister_http().await; + if !requests.is_empty() { + break; + } + env.tick().await; + env.advance_time(Duration::from_nanos(1)).await; + } + requests +} diff --git a/ic-mock-http-canister-runtime/src/mock/json/mod.rs b/ic-pocket-canister-runtime/src/mock/json/mod.rs similarity index 100% rename from ic-mock-http-canister-runtime/src/mock/json/mod.rs rename to ic-pocket-canister-runtime/src/mock/json/mod.rs diff --git a/ic-mock-http-canister-runtime/src/mock/json/tests.rs b/ic-pocket-canister-runtime/src/mock/json/tests.rs similarity index 100% rename from ic-mock-http-canister-runtime/src/mock/json/tests.rs rename to ic-pocket-canister-runtime/src/mock/json/tests.rs diff --git a/ic-mock-http-canister-runtime/src/mock/mod.rs b/ic-pocket-canister-runtime/src/mock/mod.rs similarity index 96% rename from ic-mock-http-canister-runtime/src/mock/mod.rs rename to ic-pocket-canister-runtime/src/mock/mod.rs index 902412e..769965f 100644 --- a/ic-mock-http-canister-runtime/src/mock/mod.rs +++ b/ic-pocket-canister-runtime/src/mock/mod.rs @@ -13,7 +13,9 @@ pub struct MockHttpOutcalls(Vec); impl MockHttpOutcalls { /// Asserts that no HTTP outcalls are performed. - pub const NEVER: MockHttpOutcalls = Self(Vec::new()); + pub fn never() -> MockHttpOutcalls { + MockHttpOutcalls(Vec::new()) + } /// Add a new mocked HTTP outcall. pub fn push(&mut self, mock: MockHttpOutcall) { @@ -83,7 +85,7 @@ impl MockHttpOutcallsBuilder { /// # Examples /// /// ```rust - /// use ic_mock_http_canister_runtime::{ + /// use ic_pocket_canister_runtime::{ /// CanisterHttpReply, JsonRpcRequestMatcher, MockHttpOutcallsBuilder /// }; /// @@ -146,7 +148,7 @@ impl MockHttpOutcallBuilder { /// # Examples /// /// ```rust - /// use ic_mock_http_canister_runtime::{ + /// use ic_pocket_canister_runtime::{ /// CanisterHttpReply, JsonRpcRequestMatcher, MockHttpOutcallsBuilder /// }; /// @@ -183,7 +185,7 @@ impl MockHttpOutcallBuilder { } /// A trait that allows checking if a given [`CanisterHttpRequest`] matches an HTTP outcall mock. -pub trait CanisterHttpRequestMatcher: Send + Debug { +pub trait CanisterHttpRequestMatcher: Send + Sync + Debug { /// Returns whether the given [`CanisterHttpRequest`] matches. fn matches(&self, request: &CanisterHttpRequest) -> bool; } @@ -203,7 +205,7 @@ impl CanisterHttpRequestMatcher for AnyCanisterHttpRequestMatcher { /// # Examples /// /// ```rust -/// use ic_mock_http_canister_runtime::CanisterHttpReply; +/// use ic_pocket_canister_runtime::CanisterHttpReply; /// use pocket_ic::common::rest::{CanisterHttpHeader, CanisterHttpResponse}; /// use serde_json::json; /// @@ -281,7 +283,7 @@ impl From for CanisterHttpResponse { /// /// ```rust /// use ic_error_types::RejectCode; -/// use ic_mock_http_canister_runtime::CanisterHttpReject; +/// use ic_pocket_canister_runtime::CanisterHttpReject; /// use pocket_ic::common::rest::CanisterHttpResponse; /// /// let response: CanisterHttpResponse = CanisterHttpReject::with_reject_code(RejectCode::SysTransient) diff --git a/release-plz.toml b/release-plz.toml index 1cb88c3..c9fd70e 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -35,20 +35,28 @@ name = "canhttp" publish = true # enable `cargo publish` [[package]] -name = "ic-canister-runtime" +name = "ic-agent-canister-runtime" #git_release_enable = false # enable GitHub releases publish = true # enable `cargo publish` [[package]] -name = "ic-agent-canister-runtime" +name = "ic-canister-runtime" #git_release_enable = false # enable GitHub releases publish = true # enable `cargo publish` [[package]] -name = "ic-mock-http-canister-runtime" +name = "ic-pocket-canister-runtime" #git_release_enable = false # enable GitHub releases publish = true # enable `cargo publish` [[package]] name = "http_canister" +release = false # don't process this package + +[[package]] +name = "json_rpc_canister" +release = false # don't process this package + +[[package]] +name = "multi_canister" release = false # don't process this package \ No newline at end of file