ic_pocket_canister_runtime/mock/json/
mod.rs1#[cfg(test)]
2mod tests;
3
4use crate::mock::CanisterHttpRequestMatcher;
5use canhttp::http::json::{ConstantSizeId, Id, JsonRpcRequest};
6use pocket_ic::common::rest::{
7 CanisterHttpHeader, CanisterHttpMethod, CanisterHttpReply, CanisterHttpRequest,
8 CanisterHttpResponse,
9};
10use serde::Serialize;
11use serde_json::Value;
12use std::{collections::BTreeSet, str::FromStr};
13use url::{Host, Url};
14
15#[derive(Clone, Debug)]
17pub struct SingleJsonRpcMatcher {
18 method: String,
19 id: Option<Id>,
20 params: Option<Value>,
21}
22
23impl SingleJsonRpcMatcher {
24 pub fn with_method(method: impl Into<String>) -> Self {
26 Self {
27 method: method.into(),
28 id: None,
29 params: None,
30 }
31 }
32
33 pub fn with_id(self, id: u64) -> Self {
36 self.with_raw_id(Id::from(ConstantSizeId::from(id)))
37 }
38
39 pub fn with_raw_id(self, id: Id) -> Self {
42 Self {
43 id: Some(id),
44 ..self
45 }
46 }
47
48 pub fn with_params(self, params: impl Into<Value>) -> Self {
51 Self {
52 params: Some(params.into()),
53 ..self
54 }
55 }
56
57 fn matches_body(&self, request: &JsonRpcRequest<Value>) -> bool {
58 if self.method != request.method() {
59 return false;
60 }
61 if let Some(ref id) = self.id {
62 if id != request.id() {
63 return false;
64 }
65 }
66 if let Some(ref params) = self.params {
67 if Some(params) != request.params() {
68 return false;
69 }
70 }
71 true
72 }
73}
74
75#[derive(Clone, Debug)]
81pub struct HttpRequestMatcher<B> {
82 body: B,
83 url: Option<Url>,
84 host: Option<Host>,
85 request_headers: Option<Vec<CanisterHttpHeader>>,
86 max_response_bytes: Option<u64>,
87}
88
89pub type JsonRpcRequestMatcher = HttpRequestMatcher<SingleJsonRpcMatcher>;
91
92impl<B> HttpRequestMatcher<B> {
93 pub fn with_url(self, url: &str) -> Self {
97 Self {
98 url: Some(Url::parse(url).expect("BUG: invalid URL")),
99 ..self
100 }
101 }
102
103 pub fn with_host(self, host: &str) -> Self {
107 Self {
108 host: Some(Host::parse(host).expect("BUG: invalid host for a URL")),
109 ..self
110 }
111 }
112
113 pub fn with_request_headers(self, headers: Vec<(impl ToString, impl ToString)>) -> Self {
115 Self {
116 request_headers: Some(
117 headers
118 .into_iter()
119 .map(|(name, value)| CanisterHttpHeader {
120 name: name.to_string(),
121 value: value.to_string(),
122 })
123 .collect(),
124 ),
125 ..self
126 }
127 }
128
129 pub fn with_max_response_bytes(self, max_response_bytes: impl Into<u64>) -> Self {
133 Self {
134 max_response_bytes: Some(max_response_bytes.into()),
135 ..self
136 }
137 }
138
139 fn matches_http(&self, request: &CanisterHttpRequest) -> bool {
140 let req_url = Url::from_str(&request.url).expect("BUG: invalid URL");
141 if let Some(ref mock_url) = self.url {
142 if mock_url != &req_url {
143 return false;
144 }
145 }
146 if let Some(ref host) = self.host {
147 match req_url.host() {
148 Some(ref req_host) if req_host == host => {}
149 _ => return false,
150 }
151 }
152 if CanisterHttpMethod::POST != request.http_method {
153 return false;
154 }
155 if let Some(ref headers) = self.request_headers {
156 fn lower_case_header_name(
157 CanisterHttpHeader { name, value }: &CanisterHttpHeader,
158 ) -> CanisterHttpHeader {
159 CanisterHttpHeader {
160 name: name.to_lowercase(),
161 value: value.clone(),
162 }
163 }
164 let expected: BTreeSet<_> = headers.iter().map(lower_case_header_name).collect();
165 let actual: BTreeSet<_> = request.headers.iter().map(lower_case_header_name).collect();
166 if expected != actual {
167 return false;
168 }
169 }
170 if let Some(max_response_bytes) = self.max_response_bytes {
171 if Some(max_response_bytes) != request.max_response_bytes {
172 return false;
173 }
174 }
175 true
176 }
177}
178
179impl HttpRequestMatcher<SingleJsonRpcMatcher> {
180 pub fn with_method(method: impl Into<String>) -> Self {
182 Self {
183 body: SingleJsonRpcMatcher::with_method(method),
184 url: None,
185 host: None,
186 request_headers: None,
187 max_response_bytes: None,
188 }
189 }
190
191 pub fn with_id(self, id: u64) -> Self {
194 Self {
195 body: self.body.with_id(id),
196 ..self
197 }
198 }
199
200 pub fn with_raw_id(self, id: Id) -> Self {
203 Self {
204 body: self.body.with_raw_id(id),
205 ..self
206 }
207 }
208
209 pub fn with_params(self, params: impl Into<Value>) -> Self {
212 Self {
213 body: self.body.with_params(params),
214 ..self
215 }
216 }
217}
218
219impl CanisterHttpRequestMatcher for HttpRequestMatcher<SingleJsonRpcMatcher> {
220 fn matches(&self, request: &CanisterHttpRequest) -> bool {
221 if !self.matches_http(request) {
222 return false;
223 }
224 match serde_json::from_slice::<JsonRpcRequest<Value>>(&request.body) {
225 Ok(actual_body) => self.body.matches_body(&actual_body),
226 Err(_) => false,
227 }
228 }
229}
230
231pub type BatchJsonRpcRequestMatcher = HttpRequestMatcher<Vec<SingleJsonRpcMatcher>>;
233
234impl HttpRequestMatcher<Vec<SingleJsonRpcMatcher>> {
235 pub fn batch(matchers: Vec<SingleJsonRpcMatcher>) -> Self {
238 Self {
239 body: matchers,
240 url: None,
241 host: None,
242 request_headers: None,
243 max_response_bytes: None,
244 }
245 }
246}
247
248impl CanisterHttpRequestMatcher for HttpRequestMatcher<Vec<SingleJsonRpcMatcher>> {
249 fn matches(&self, request: &CanisterHttpRequest) -> bool {
250 if !self.matches_http(request) {
251 return false;
252 }
253 match serde_json::from_slice::<Vec<JsonRpcRequest<Value>>>(&request.body) {
254 Ok(actual_batch) => {
255 actual_batch.len() == self.body.len()
256 && self
257 .body
258 .iter()
259 .zip(actual_batch.iter())
260 .all(|(matcher, req)| matcher.matches_body(req))
261 }
262 Err(_) => false,
263 }
264 }
265}
266
267#[derive(Clone)]
273pub struct HttpResponse<B> {
274 status: u16,
275 headers: Vec<CanisterHttpHeader>,
276 body: B,
277}
278
279pub type JsonRpcResponse = HttpResponse<Value>;
281
282pub type BatchJsonRpcResponse = HttpResponse<Vec<Value>>;
284
285impl<B: Serialize> From<HttpResponse<B>> for CanisterHttpResponse {
286 fn from(response: HttpResponse<B>) -> Self {
287 CanisterHttpResponse::CanisterHttpReply(CanisterHttpReply {
288 status: response.status,
289 headers: response.headers,
290 body: serde_json::to_vec(&response.body).unwrap(),
291 })
292 }
293}
294
295impl From<Value> for HttpResponse<Value> {
296 fn from(body: Value) -> Self {
297 Self {
298 status: 200,
299 headers: vec![],
300 body,
301 }
302 }
303}
304
305impl From<&Value> for HttpResponse<Value> {
306 fn from(body: &Value) -> Self {
307 Self::from(body.clone())
308 }
309}
310
311impl From<String> for HttpResponse<Value> {
312 fn from(body: String) -> Self {
313 Self::from(Value::from_str(&body).expect("BUG: invalid JSON-RPC response"))
314 }
315}
316
317impl From<&str> for HttpResponse<Value> {
318 fn from(body: &str) -> Self {
319 Self::from(body.to_string())
320 }
321}
322
323impl HttpResponse<Value> {
324 pub fn with_id(self, id: u64) -> Self {
327 self.with_raw_id(Id::from(ConstantSizeId::from(id)))
328 }
329
330 pub fn with_raw_id(mut self, id: Id) -> Self {
332 self.body["id"] = serde_json::to_value(id).expect("BUG: cannot serialize ID");
333 self
334 }
335}
336
337impl From<Vec<Value>> for HttpResponse<Vec<Value>> {
338 fn from(body: Vec<Value>) -> Self {
339 Self {
340 status: 200,
341 headers: vec![],
342 body,
343 }
344 }
345}