1#![allow(unused_imports)]
20use async_trait::async_trait;
21use derive_builder::Builder;
22use reqwest;
23use rust_decimal::prelude::*;
24use serde::{Deserialize, Serialize};
25use serde_json::{Value, json};
26use std::collections::BTreeMap;
27
28use crate::common::{
29 config::ConfigurationRestApi,
30 models::{ParamBuildError, RestApiResponse},
31 utils::send_request,
32};
33use crate::spot::rest_api::models;
34
35const HAS_TIME_UNIT: bool = true;
36
37#[async_trait]
38pub trait GeneralApi: Send + Sync {
39 async fn exchange_info(
40 &self,
41 params: ExchangeInfoParams,
42 ) -> anyhow::Result<RestApiResponse<models::ExchangeInfoResponse>>;
43 async fn execution_rules(
44 &self,
45 params: ExecutionRulesParams,
46 ) -> anyhow::Result<RestApiResponse<models::ExecutionRulesResponse>>;
47 async fn ping(&self) -> anyhow::Result<RestApiResponse<Value>>;
48 async fn time(&self) -> anyhow::Result<RestApiResponse<models::TimeResponse>>;
49}
50
51#[derive(Debug, Clone)]
52pub struct GeneralApiClient {
53 configuration: ConfigurationRestApi,
54}
55
56impl GeneralApiClient {
57 pub fn new(configuration: ConfigurationRestApi) -> Self {
58 Self { configuration }
59 }
60}
61
62#[allow(non_camel_case_types)]
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub enum ExchangeInfoSymbolStatusEnum {
65 #[serde(rename = "TRADING")]
66 Trading,
67 #[serde(rename = "END_OF_DAY")]
68 EndOfDay,
69 #[serde(rename = "HALT")]
70 Halt,
71 #[serde(rename = "BREAK")]
72 Break,
73 #[serde(rename = "NON_REPRESENTABLE")]
74 NonRepresentable,
75}
76
77impl ExchangeInfoSymbolStatusEnum {
78 #[must_use]
79 pub fn as_str(&self) -> &'static str {
80 match self {
81 Self::Trading => "TRADING",
82 Self::EndOfDay => "END_OF_DAY",
83 Self::Halt => "HALT",
84 Self::Break => "BREAK",
85 Self::NonRepresentable => "NON_REPRESENTABLE",
86 }
87 }
88}
89
90impl std::str::FromStr for ExchangeInfoSymbolStatusEnum {
91 type Err = Box<dyn std::error::Error + Send + Sync>;
92
93 fn from_str(s: &str) -> Result<Self, Self::Err> {
94 match s {
95 "TRADING" => Ok(Self::Trading),
96 "END_OF_DAY" => Ok(Self::EndOfDay),
97 "HALT" => Ok(Self::Halt),
98 "BREAK" => Ok(Self::Break),
99 "NON_REPRESENTABLE" => Ok(Self::NonRepresentable),
100 other => Err(format!("invalid ExchangeInfoSymbolStatusEnum: {}", other).into()),
101 }
102 }
103}
104
105#[allow(non_camel_case_types)]
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub enum ExecutionRulesSymbolStatusEnum {
108 #[serde(rename = "TRADING")]
109 Trading,
110 #[serde(rename = "END_OF_DAY")]
111 EndOfDay,
112 #[serde(rename = "HALT")]
113 Halt,
114 #[serde(rename = "BREAK")]
115 Break,
116 #[serde(rename = "NON_REPRESENTABLE")]
117 NonRepresentable,
118}
119
120impl ExecutionRulesSymbolStatusEnum {
121 #[must_use]
122 pub fn as_str(&self) -> &'static str {
123 match self {
124 Self::Trading => "TRADING",
125 Self::EndOfDay => "END_OF_DAY",
126 Self::Halt => "HALT",
127 Self::Break => "BREAK",
128 Self::NonRepresentable => "NON_REPRESENTABLE",
129 }
130 }
131}
132
133impl std::str::FromStr for ExecutionRulesSymbolStatusEnum {
134 type Err = Box<dyn std::error::Error + Send + Sync>;
135
136 fn from_str(s: &str) -> Result<Self, Self::Err> {
137 match s {
138 "TRADING" => Ok(Self::Trading),
139 "END_OF_DAY" => Ok(Self::EndOfDay),
140 "HALT" => Ok(Self::Halt),
141 "BREAK" => Ok(Self::Break),
142 "NON_REPRESENTABLE" => Ok(Self::NonRepresentable),
143 other => Err(format!("invalid ExecutionRulesSymbolStatusEnum: {}", other).into()),
144 }
145 }
146}
147
148#[derive(Clone, Debug, Builder, Default)]
153#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
154pub struct ExchangeInfoParams {
155 #[builder(setter(into), default)]
159 pub symbol: Option<String>,
160 #[builder(setter(into), default)]
164 pub symbols: Option<Vec<String>>,
165 #[builder(setter(into), default)]
169 pub permissions: Option<Vec<String>>,
170 #[builder(setter(into), default)]
174 pub show_permission_sets: Option<bool>,
175 #[builder(setter(into), default)]
180 pub symbol_status: Option<ExchangeInfoSymbolStatusEnum>,
181}
182
183impl ExchangeInfoParams {
184 #[must_use]
187 pub fn builder() -> ExchangeInfoParamsBuilder {
188 ExchangeInfoParamsBuilder::default()
189 }
190}
191#[derive(Clone, Debug, Builder, Default)]
196#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
197pub struct ExecutionRulesParams {
198 #[builder(setter(into), default)]
202 pub symbol: Option<String>,
203 #[builder(setter(into), default)]
207 pub symbols: Option<Vec<String>>,
208 #[builder(setter(into), default)]
213 pub symbol_status: Option<ExecutionRulesSymbolStatusEnum>,
214}
215
216impl ExecutionRulesParams {
217 #[must_use]
220 pub fn builder() -> ExecutionRulesParamsBuilder {
221 ExecutionRulesParamsBuilder::default()
222 }
223}
224
225#[async_trait]
226impl GeneralApi for GeneralApiClient {
227 async fn exchange_info(
228 &self,
229 params: ExchangeInfoParams,
230 ) -> anyhow::Result<RestApiResponse<models::ExchangeInfoResponse>> {
231 let ExchangeInfoParams {
232 symbol,
233 symbols,
234 permissions,
235 show_permission_sets,
236 symbol_status,
237 } = params;
238
239 let mut query_params = BTreeMap::new();
240 let body_params = BTreeMap::new();
241
242 if let Some(rw) = symbol {
243 query_params.insert("symbol".to_string(), json!(rw));
244 }
245
246 if let Some(rw) = symbols {
247 query_params.insert("symbols".to_string(), json!(rw));
248 }
249
250 if let Some(rw) = permissions {
251 query_params.insert("permissions".to_string(), json!(rw));
252 }
253
254 if let Some(rw) = show_permission_sets {
255 query_params.insert("showPermissionSets".to_string(), json!(rw));
256 }
257
258 if let Some(rw) = symbol_status {
259 query_params.insert("symbolStatus".to_string(), json!(rw));
260 }
261
262 send_request::<models::ExchangeInfoResponse>(
263 &self.configuration,
264 "/api/v3/exchangeInfo",
265 reqwest::Method::GET,
266 query_params,
267 body_params,
268 if HAS_TIME_UNIT {
269 self.configuration.time_unit
270 } else {
271 None
272 },
273 false,
274 )
275 .await
276 }
277
278 async fn execution_rules(
279 &self,
280 params: ExecutionRulesParams,
281 ) -> anyhow::Result<RestApiResponse<models::ExecutionRulesResponse>> {
282 let ExecutionRulesParams {
283 symbol,
284 symbols,
285 symbol_status,
286 } = params;
287
288 let mut query_params = BTreeMap::new();
289 let body_params = BTreeMap::new();
290
291 if let Some(rw) = symbol {
292 query_params.insert("symbol".to_string(), json!(rw));
293 }
294
295 if let Some(rw) = symbols {
296 query_params.insert("symbols".to_string(), json!(rw));
297 }
298
299 if let Some(rw) = symbol_status {
300 query_params.insert("symbolStatus".to_string(), json!(rw));
301 }
302
303 send_request::<models::ExecutionRulesResponse>(
304 &self.configuration,
305 "/api/v3/executionRules",
306 reqwest::Method::GET,
307 query_params,
308 body_params,
309 if HAS_TIME_UNIT {
310 self.configuration.time_unit
311 } else {
312 None
313 },
314 false,
315 )
316 .await
317 }
318
319 async fn ping(&self) -> anyhow::Result<RestApiResponse<Value>> {
320 let query_params = BTreeMap::new();
321 let body_params = BTreeMap::new();
322
323 send_request::<Value>(
324 &self.configuration,
325 "/api/v3/ping",
326 reqwest::Method::GET,
327 query_params,
328 body_params,
329 if HAS_TIME_UNIT {
330 self.configuration.time_unit
331 } else {
332 None
333 },
334 false,
335 )
336 .await
337 }
338
339 async fn time(&self) -> anyhow::Result<RestApiResponse<models::TimeResponse>> {
340 let query_params = BTreeMap::new();
341 let body_params = BTreeMap::new();
342
343 send_request::<models::TimeResponse>(
344 &self.configuration,
345 "/api/v3/time",
346 reqwest::Method::GET,
347 query_params,
348 body_params,
349 if HAS_TIME_UNIT {
350 self.configuration.time_unit
351 } else {
352 None
353 },
354 false,
355 )
356 .await
357 }
358}
359
360#[cfg(all(test, feature = "spot"))]
361mod tests {
362 use super::*;
363 use crate::TOKIO_SHARED_RT;
364 use crate::{errors::ConnectorError, models::DataFuture, models::RestApiRateLimit};
365 use async_trait::async_trait;
366 use std::collections::HashMap;
367
368 struct DummyRestApiResponse<T> {
369 inner: Box<dyn FnOnce() -> DataFuture<Result<T, ConnectorError>> + Send + Sync>,
370 status: u16,
371 headers: HashMap<String, String>,
372 rate_limits: Option<Vec<RestApiRateLimit>>,
373 }
374
375 impl<T> From<DummyRestApiResponse<T>> for RestApiResponse<T> {
376 fn from(dummy: DummyRestApiResponse<T>) -> Self {
377 Self {
378 data_fn: dummy.inner,
379 status: dummy.status,
380 headers: dummy.headers,
381 rate_limits: dummy.rate_limits,
382 }
383 }
384 }
385
386 struct MockGeneralApiClient {
387 force_error: bool,
388 }
389
390 #[async_trait]
391 impl GeneralApi for MockGeneralApiClient {
392 async fn exchange_info(
393 &self,
394 _params: ExchangeInfoParams,
395 ) -> anyhow::Result<RestApiResponse<models::ExchangeInfoResponse>> {
396 if self.force_error {
397 return Err(ConnectorError::ConnectorClientError {
398 msg: "ResponseError".to_string(),
399 code: None,
400 }
401 .into());
402 }
403
404 let resp_json: Value = serde_json::from_str(r#"{"timezone":"UTC","serverTime":1565246363776,"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000},{"rateLimitType":"ORDERS","interval":"DAY","intervalNum":1,"limit":160000},{"rateLimitType":"RAW_REQUESTS","interval":"MINUTE","intervalNum":5,"limit":61000}],"exchangeFilters":[],"symbols":[{"symbol":"ETHBTC","status":"TRADING","baseAsset":"ETH","baseAssetPrecision":8,"quoteAsset":"BTC","quotePrecision":8,"quoteAssetPrecision":8,"baseCommissionPrecision":8,"quoteCommissionPrecision":8,"orderTypes":["LIMIT LIMIT_MAKER MARKET STOP_LOSS STOP_LOSS_LIMIT TAKE_PROFIT TAKE_PROFIT_LIMIT"],"icebergAllowed":true,"ocoAllowed":true,"otoAllowed":true,"opoAllowed":true,"quoteOrderQtyMarketAllowed":true,"allowTrailingStop":false,"cancelReplaceAllowed":false,"amendAllowed":false,"pegInstructionsAllowed":true,"isSpotTradingAllowed":true,"isMarginTradingAllowed":true,"filters":[],"permissions":[],"permissionSets":[["SPOT","MARGIN"]],"defaultSelfTradePreventionMode":"NONE","allowedSelfTradePreventionModes":["NONE"]}]}"#).unwrap();
405 let dummy_response: models::ExchangeInfoResponse =
406 serde_json::from_value(resp_json.clone())
407 .expect("should parse into models::ExchangeInfoResponse");
408
409 let dummy = DummyRestApiResponse {
410 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
411 status: 200,
412 headers: HashMap::new(),
413 rate_limits: None,
414 };
415
416 Ok(dummy.into())
417 }
418
419 async fn execution_rules(
420 &self,
421 _params: ExecutionRulesParams,
422 ) -> anyhow::Result<RestApiResponse<models::ExecutionRulesResponse>> {
423 if self.force_error {
424 return Err(ConnectorError::ConnectorClientError {
425 msg: "ResponseError".to_string(),
426 code: None,
427 }
428 .into());
429 }
430
431 let resp_json: Value = serde_json::from_str(r#"{"symbolRules":[{"symbol":"BAZUSD","rules":[{"ruleType":"PRICE_RANGE","bidLimitMultUp":"1.0001","bidLimitMultDown":"0.9999","askLimitMultUp":"1.0001","askLimitMultDown":"0.9999"}]}]}"#).unwrap();
432 let dummy_response: models::ExecutionRulesResponse =
433 serde_json::from_value(resp_json.clone())
434 .expect("should parse into models::ExecutionRulesResponse");
435
436 let dummy = DummyRestApiResponse {
437 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
438 status: 200,
439 headers: HashMap::new(),
440 rate_limits: None,
441 };
442
443 Ok(dummy.into())
444 }
445
446 async fn ping(&self) -> anyhow::Result<RestApiResponse<Value>> {
447 if self.force_error {
448 return Err(ConnectorError::ConnectorClientError {
449 msg: "ResponseError".to_string(),
450 code: None,
451 }
452 .into());
453 }
454
455 let dummy_response = Value::Null;
456
457 let dummy = DummyRestApiResponse {
458 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
459 status: 200,
460 headers: HashMap::new(),
461 rate_limits: None,
462 };
463
464 Ok(dummy.into())
465 }
466
467 async fn time(&self) -> anyhow::Result<RestApiResponse<models::TimeResponse>> {
468 if self.force_error {
469 return Err(ConnectorError::ConnectorClientError {
470 msg: "ResponseError".to_string(),
471 code: None,
472 }
473 .into());
474 }
475
476 let resp_json: Value = serde_json::from_str(r#"{"serverTime":1499827319559}"#).unwrap();
477 let dummy_response: models::TimeResponse = serde_json::from_value(resp_json.clone())
478 .expect("should parse into models::TimeResponse");
479
480 let dummy = DummyRestApiResponse {
481 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
482 status: 200,
483 headers: HashMap::new(),
484 rate_limits: None,
485 };
486
487 Ok(dummy.into())
488 }
489 }
490
491 #[test]
492 fn exchange_info_required_params_success() {
493 TOKIO_SHARED_RT.block_on(async {
494 let client = MockGeneralApiClient { force_error: false };
495
496 let params = ExchangeInfoParams::builder().build().unwrap();
497
498 let resp_json: Value = serde_json::from_str(r#"{"timezone":"UTC","serverTime":1565246363776,"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000},{"rateLimitType":"ORDERS","interval":"DAY","intervalNum":1,"limit":160000},{"rateLimitType":"RAW_REQUESTS","interval":"MINUTE","intervalNum":5,"limit":61000}],"exchangeFilters":[],"symbols":[{"symbol":"ETHBTC","status":"TRADING","baseAsset":"ETH","baseAssetPrecision":8,"quoteAsset":"BTC","quotePrecision":8,"quoteAssetPrecision":8,"baseCommissionPrecision":8,"quoteCommissionPrecision":8,"orderTypes":["LIMIT LIMIT_MAKER MARKET STOP_LOSS STOP_LOSS_LIMIT TAKE_PROFIT TAKE_PROFIT_LIMIT"],"icebergAllowed":true,"ocoAllowed":true,"otoAllowed":true,"opoAllowed":true,"quoteOrderQtyMarketAllowed":true,"allowTrailingStop":false,"cancelReplaceAllowed":false,"amendAllowed":false,"pegInstructionsAllowed":true,"isSpotTradingAllowed":true,"isMarginTradingAllowed":true,"filters":[],"permissions":[],"permissionSets":[["SPOT","MARGIN"]],"defaultSelfTradePreventionMode":"NONE","allowedSelfTradePreventionModes":["NONE"]}]}"#).unwrap();
499 let expected_response : models::ExchangeInfoResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::ExchangeInfoResponse");
500
501 let resp = client.exchange_info(params).await.expect("Expected a response");
502 let data_future = resp.data();
503 let actual_response = data_future.await.unwrap();
504 assert_eq!(actual_response, expected_response);
505 });
506 }
507
508 #[test]
509 fn exchange_info_optional_params_success() {
510 TOKIO_SHARED_RT.block_on(async {
511 let client = MockGeneralApiClient { force_error: false };
512
513 let params = ExchangeInfoParams::builder().symbol("BNBUSDT".to_string()).symbols(["null".to_string(),].to_vec()).permissions(["null".to_string(),].to_vec()).show_permission_sets(true).symbol_status(ExchangeInfoSymbolStatusEnum::Trading).build().unwrap();
514
515 let resp_json: Value = serde_json::from_str(r#"{"timezone":"UTC","serverTime":1565246363776,"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000},{"rateLimitType":"ORDERS","interval":"DAY","intervalNum":1,"limit":160000},{"rateLimitType":"RAW_REQUESTS","interval":"MINUTE","intervalNum":5,"limit":61000}],"exchangeFilters":[],"symbols":[{"symbol":"ETHBTC","status":"TRADING","baseAsset":"ETH","baseAssetPrecision":8,"quoteAsset":"BTC","quotePrecision":8,"quoteAssetPrecision":8,"baseCommissionPrecision":8,"quoteCommissionPrecision":8,"orderTypes":["LIMIT LIMIT_MAKER MARKET STOP_LOSS STOP_LOSS_LIMIT TAKE_PROFIT TAKE_PROFIT_LIMIT"],"icebergAllowed":true,"ocoAllowed":true,"otoAllowed":true,"opoAllowed":true,"quoteOrderQtyMarketAllowed":true,"allowTrailingStop":false,"cancelReplaceAllowed":false,"amendAllowed":false,"pegInstructionsAllowed":true,"isSpotTradingAllowed":true,"isMarginTradingAllowed":true,"filters":[],"permissions":[],"permissionSets":[["SPOT","MARGIN"]],"defaultSelfTradePreventionMode":"NONE","allowedSelfTradePreventionModes":["NONE"]}]}"#).unwrap();
516 let expected_response : models::ExchangeInfoResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::ExchangeInfoResponse");
517
518 let resp = client.exchange_info(params).await.expect("Expected a response");
519 let data_future = resp.data();
520 let actual_response = data_future.await.unwrap();
521 assert_eq!(actual_response, expected_response);
522 });
523 }
524
525 #[test]
526 fn exchange_info_response_error() {
527 TOKIO_SHARED_RT.block_on(async {
528 let client = MockGeneralApiClient { force_error: true };
529
530 let params = ExchangeInfoParams::builder().build().unwrap();
531
532 match client.exchange_info(params).await {
533 Ok(_) => panic!("Expected an error"),
534 Err(err) => {
535 assert_eq!(err.to_string(), "Connector client error: ResponseError");
536 }
537 }
538 });
539 }
540
541 #[test]
542 fn execution_rules_required_params_success() {
543 TOKIO_SHARED_RT.block_on(async {
544 let client = MockGeneralApiClient { force_error: false };
545
546 let params = ExecutionRulesParams::builder().build().unwrap();
547
548 let resp_json: Value = serde_json::from_str(r#"{"symbolRules":[{"symbol":"BAZUSD","rules":[{"ruleType":"PRICE_RANGE","bidLimitMultUp":"1.0001","bidLimitMultDown":"0.9999","askLimitMultUp":"1.0001","askLimitMultDown":"0.9999"}]}]}"#).unwrap();
549 let expected_response : models::ExecutionRulesResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::ExecutionRulesResponse");
550
551 let resp = client.execution_rules(params).await.expect("Expected a response");
552 let data_future = resp.data();
553 let actual_response = data_future.await.unwrap();
554 assert_eq!(actual_response, expected_response);
555 });
556 }
557
558 #[test]
559 fn execution_rules_optional_params_success() {
560 TOKIO_SHARED_RT.block_on(async {
561 let client = MockGeneralApiClient { force_error: false };
562
563 let params = ExecutionRulesParams::builder().symbol("BNBUSDT".to_string()).symbols(["null".to_string(),].to_vec()).symbol_status(ExecutionRulesSymbolStatusEnum::Trading).build().unwrap();
564
565 let resp_json: Value = serde_json::from_str(r#"{"symbolRules":[{"symbol":"BAZUSD","rules":[{"ruleType":"PRICE_RANGE","bidLimitMultUp":"1.0001","bidLimitMultDown":"0.9999","askLimitMultUp":"1.0001","askLimitMultDown":"0.9999"}]}]}"#).unwrap();
566 let expected_response : models::ExecutionRulesResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::ExecutionRulesResponse");
567
568 let resp = client.execution_rules(params).await.expect("Expected a response");
569 let data_future = resp.data();
570 let actual_response = data_future.await.unwrap();
571 assert_eq!(actual_response, expected_response);
572 });
573 }
574
575 #[test]
576 fn execution_rules_response_error() {
577 TOKIO_SHARED_RT.block_on(async {
578 let client = MockGeneralApiClient { force_error: true };
579
580 let params = ExecutionRulesParams::builder().build().unwrap();
581
582 match client.execution_rules(params).await {
583 Ok(_) => panic!("Expected an error"),
584 Err(err) => {
585 assert_eq!(err.to_string(), "Connector client error: ResponseError");
586 }
587 }
588 });
589 }
590
591 #[test]
592 fn ping_required_params_success() {
593 TOKIO_SHARED_RT.block_on(async {
594 let client = MockGeneralApiClient { force_error: false };
595
596 let expected_response = Value::Null;
597
598 let resp = client.ping().await.expect("Expected a response");
599 let data_future = resp.data();
600 let actual_response = data_future.await.unwrap();
601 assert_eq!(actual_response, expected_response);
602 });
603 }
604
605 #[test]
606 fn ping_optional_params_success() {
607 TOKIO_SHARED_RT.block_on(async {
608 let client = MockGeneralApiClient { force_error: false };
609
610 let expected_response = Value::Null;
611
612 let resp = client.ping().await.expect("Expected a response");
613 let data_future = resp.data();
614 let actual_response = data_future.await.unwrap();
615 assert_eq!(actual_response, expected_response);
616 });
617 }
618
619 #[test]
620 fn ping_response_error() {
621 TOKIO_SHARED_RT.block_on(async {
622 let client = MockGeneralApiClient { force_error: true };
623
624 match client.ping().await {
625 Ok(_) => panic!("Expected an error"),
626 Err(err) => {
627 assert_eq!(err.to_string(), "Connector client error: ResponseError");
628 }
629 }
630 });
631 }
632
633 #[test]
634 fn time_required_params_success() {
635 TOKIO_SHARED_RT.block_on(async {
636 let client = MockGeneralApiClient { force_error: false };
637
638 let resp_json: Value = serde_json::from_str(r#"{"serverTime":1499827319559}"#).unwrap();
639 let expected_response: models::TimeResponse = serde_json::from_value(resp_json.clone())
640 .expect("should parse into models::TimeResponse");
641
642 let resp = client.time().await.expect("Expected a response");
643 let data_future = resp.data();
644 let actual_response = data_future.await.unwrap();
645 assert_eq!(actual_response, expected_response);
646 });
647 }
648
649 #[test]
650 fn time_optional_params_success() {
651 TOKIO_SHARED_RT.block_on(async {
652 let client = MockGeneralApiClient { force_error: false };
653
654 let resp_json: Value = serde_json::from_str(r#"{"serverTime":1499827319559}"#).unwrap();
655 let expected_response: models::TimeResponse = serde_json::from_value(resp_json.clone())
656 .expect("should parse into models::TimeResponse");
657
658 let resp = client.time().await.expect("Expected a response");
659 let data_future = resp.data();
660 let actual_response = data_future.await.unwrap();
661 assert_eq!(actual_response, expected_response);
662 });
663 }
664
665 #[test]
666 fn time_response_error() {
667 TOKIO_SHARED_RT.block_on(async {
668 let client = MockGeneralApiClient { force_error: true };
669
670 match client.time().await {
671 Ok(_) => panic!("Expected an error"),
672 Err(err) => {
673 assert_eq!(err.to_string(), "Connector client error: ResponseError");
674 }
675 }
676 });
677 }
678}