1use anyhow::{Context, Result};
2#[cfg(feature = "openssl-tls")]
3use base64::{Engine as _, engine::general_purpose};
4#[cfg(feature = "openssl-tls")]
5use ed25519_dalek::Signer as Ed25519Signer;
6#[cfg(feature = "openssl-tls")]
7use ed25519_dalek::SigningKey;
8#[cfg(feature = "openssl-tls")]
9use ed25519_dalek::pkcs8::DecodePrivateKey;
10use flate2::read::GzDecoder;
11use hex;
12use hmac::{Hmac, Mac};
13use http::HeaderMap;
14use http::HeaderValue;
15use http::header::ACCEPT_ENCODING;
16use once_cell::sync::OnceCell;
17#[cfg(feature = "openssl-tls")]
18use openssl::{hash::MessageDigest, pkey::PKey, sign::Signer as OpenSslSigner};
19use rand::{RngCore, rngs::OsRng};
20use regex::Captures;
21use regex::Regex;
22use reqwest::Client;
23use reqwest::Proxy;
24use reqwest::{Method, Request};
25use serde::de::DeserializeOwned;
26use serde_json::Number;
27use serde_json::{Value, json};
28use sha2::Sha256;
29use std::fmt;
30use std::fmt::Display;
31use std::hash::BuildHasher;
32use std::sync::LazyLock;
33use std::{
34 collections::BTreeMap,
35 collections::HashMap,
36 io::Read,
37 time::Duration,
38 time::{SystemTime, UNIX_EPOCH},
39};
40#[cfg(feature = "openssl-tls")]
41use std::{fs, path::Path};
42use tokio::time::sleep;
43use url::form_urlencoded;
44use url::{Url, form_urlencoded::Serializer};
45
46use super::config::{
47 ConfigurationRestApi, ConfigurationWebsocketApi, HttpAgent, PrivateKey, ProxyConfig,
48};
49use super::errors::ConnectorError;
50use super::models::{
51 Interval, RateLimitType, RestApiRateLimit, RestApiResponse, StreamId, TimeUnit,
52};
53use super::websocket::WebsocketMessageSendOptions;
54
55pub(crate) static ID_REGEX: LazyLock<Regex> =
56 LazyLock::new(|| Regex::new(r"^[0-9a-f]{32}$").unwrap());
57static PLACEHOLDER_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(@)?<([^>]+)>").unwrap());
58static CLI_SKILL_UA_RE: LazyLock<Regex> = LazyLock::new(|| {
59 Regex::new(r"^binance-(?:cli|skill)(?:/[A-Za-z0-9._-]+){0,2} \([^;()]+; [^;()]+; [^;()]+\)$")
60 .unwrap()
61});
62
63#[derive(Default, Clone)]
78#[allow(dead_code)]
79pub struct SignatureGenerator {
80 api_secret: Option<String>,
81 private_key: Option<PrivateKey>,
82 private_key_passphrase: Option<String>,
83 raw_key_data: OnceCell<String>,
84 #[cfg(feature = "openssl-tls")]
85 key_object: OnceCell<PKey<openssl::pkey::Private>>,
86 #[cfg(feature = "openssl-tls")]
87 ed25519_signing_key: OnceCell<SigningKey>,
88}
89
90impl fmt::Debug for SignatureGenerator {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 f.debug_struct("SignatureGenerator")
93 .field(
94 "api_secret",
95 &self.api_secret.as_ref().map(|_| "[REDACTED]"),
96 )
97 .field(
98 "private_key",
99 &self.private_key.as_ref().map(|_| "[REDACTED]"),
100 )
101 .field(
102 "private_key_passphrase",
103 &self.private_key_passphrase.as_ref().map(|_| "[REDACTED]"),
104 )
105 .field(
106 "raw_key_data",
107 &self.raw_key_data.get().map(|_| "[REDACTED]"),
108 )
109 .field("key_object", &"[REDACTED]")
110 .field("ed25519_signing_key", &"[REDACTED]")
111 .finish()
112 }
113}
114
115impl SignatureGenerator {
116 #[must_use]
117 pub fn new(
118 api_secret: Option<String>,
119 private_key: Option<PrivateKey>,
120 private_key_passphrase: Option<String>,
121 ) -> Self {
122 SignatureGenerator {
123 api_secret,
124 private_key,
125 private_key_passphrase,
126 raw_key_data: OnceCell::new(),
127 #[cfg(feature = "openssl-tls")]
128 key_object: OnceCell::new(),
129 #[cfg(feature = "openssl-tls")]
130 ed25519_signing_key: OnceCell::new(),
131 }
132 }
133
134 #[cfg(feature = "openssl-tls")]
149 fn get_raw_key_data(&self) -> Result<&String> {
150 self.raw_key_data.get_or_try_init(|| {
151 let pk = self
152 .private_key
153 .as_ref()
154 .ok_or_else(|| anyhow::anyhow!("No private_key provided"))?;
155 match pk {
156 PrivateKey::File(path) => {
157 if Path::new(path).exists() {
158 fs::read_to_string(path)
159 .with_context(|| format!("Failed to read private key file: {path}"))
160 } else {
161 Err(anyhow::anyhow!("Private key file does not exist: {}", path))
162 }
163 }
164 PrivateKey::Raw(bytes) => Ok(String::from_utf8_lossy(bytes).to_string()),
165 }
166 })
167 }
168
169 #[cfg(feature = "openssl-tls")]
184 fn get_key_object(&self) -> Result<&PKey<openssl::pkey::Private>> {
185 self.key_object.get_or_try_init(|| {
186 let key_data = self.get_raw_key_data()?;
187 if let Some(pass) = self.private_key_passphrase.as_ref() {
188 PKey::private_key_from_pem_passphrase(key_data.as_bytes(), pass.as_bytes())
189 .context("Failed to parse private key with passphrase")
190 } else {
191 PKey::private_key_from_pem(key_data.as_bytes())
192 .context("Failed to parse private key")
193 }
194 })
195 }
196
197 #[cfg(feature = "openssl-tls")]
210 fn get_ed25519_signing_key(
211 &self,
212 key_obj: &PKey<openssl::pkey::Private>,
213 ) -> Result<&SigningKey> {
214 self.ed25519_signing_key.get_or_try_init(|| {
215 let der = key_obj
216 .private_key_to_der()
217 .context("Failed to export Ed25519 key to DER")?;
218 SigningKey::from_pkcs8_der(&der)
219 .map_err(|e| anyhow::anyhow!("Failed to parse Ed25519 key: {}", e))
220 })
221 }
222
223 pub fn get_signature(
246 &self,
247 query_params: &BTreeMap<String, Value>,
248 body_params: Option<&BTreeMap<String, Value>>,
249 ) -> Result<String> {
250 let query_str = build_query_string(query_params)?;
251 let params = if let Some(body) = body_params {
252 if body.is_empty() {
253 query_str
254 } else {
255 let body_str = build_query_string(body)?;
256 format!("{query_str}{body_str}")
257 }
258 } else {
259 query_str
260 };
261
262 self.sign_payload(¶ms)
263 }
264
265 pub fn get_signature_unencoded(&self, params: &BTreeMap<String, Value>) -> Result<String> {
283 let payload = build_plain_query_string(params)?;
284 self.sign_payload(&payload)
285 }
286
287 fn sign_payload(&self, params: &str) -> Result<String> {
304 if self.private_key.is_none() {
305 if let Some(secret) = self.api_secret.as_ref() {
306 let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
307 .context("HMAC key initialization failed")?;
308 mac.update(params.as_bytes());
309 let result = mac.finalize().into_bytes();
310 return Ok(hex::encode(result));
311 }
312 }
313
314 if self.private_key.is_some() {
315 #[cfg(feature = "openssl-tls")]
316 {
317 let key_obj = self.get_key_object()?;
318 match key_obj.id() {
319 openssl::pkey::Id::RSA => {
320 let mut signer = OpenSslSigner::new(MessageDigest::sha256(), key_obj)
321 .context("Failed to create RSA signer")?;
322 signer
323 .update(params.as_bytes())
324 .context("Failed to update RSA signer")?;
325 let sig = signer.sign_to_vec().context("RSA signing failed")?;
326 return Ok(general_purpose::STANDARD.encode(sig));
327 }
328 openssl::pkey::Id::ED25519 => {
329 let signing_key = self.get_ed25519_signing_key(key_obj)?;
330 let signature = signing_key.sign(params.as_bytes());
331 return Ok(general_purpose::STANDARD.encode(signature.to_bytes()));
332 }
333 other => {
334 return Err(anyhow::anyhow!(
335 "Unsupported private key type: {:?}. Must be RSA or ED25519.",
336 other
337 ));
338 }
339 }
340 }
341
342 #[cfg(not(feature = "openssl-tls"))]
343 {
344 return Err(anyhow::anyhow!(
345 "Private key signing requires the 'openssl-tls' feature to be enabled."
346 ));
347 }
348 }
349
350 Err(anyhow::anyhow!(
351 "Either 'api_secret' or 'private_key' must be provided for signed requests."
352 ))
353 }
354}
355
356#[must_use]
379pub fn build_client(
380 timeout: u64,
381 keep_alive: bool,
382 proxy: Option<&ProxyConfig>,
383 agent: Option<HttpAgent>,
384) -> Client {
385 let builder = Client::builder().timeout(Duration::from_millis(timeout));
386
387 let mut builder = if keep_alive {
388 builder
389 } else {
390 builder.pool_idle_timeout(Some(Duration::from_secs(0)))
391 };
392
393 if let Some(proxy_conf) = proxy {
394 let protocol = proxy_conf
395 .protocol
396 .clone()
397 .unwrap_or_else(|| "http".to_string());
398 let proxy_url = format!("{}://{}:{}", protocol, proxy_conf.host, proxy_conf.port);
399 let mut proxy_builder = Proxy::all(&proxy_url).expect("Failed to create proxy from URL");
400 if let Some(auth) = &proxy_conf.auth {
401 proxy_builder = proxy_builder.basic_auth(&auth.username, &auth.password);
402 }
403 builder = builder.proxy(proxy_builder);
404 }
405
406 if let Some(HttpAgent(agent_fn)) = agent {
407 builder = (agent_fn)(builder);
408 }
409
410 builder.build().expect("Failed to build reqwest client")
411}
412
413#[must_use]
436pub fn build_user_agent(product: &str) -> String {
437 if let Ok(override_ua) = std::env::var("BINANCE_CONNECTOR_RUST_USER_AGENT") {
438 let trimmed = override_ua.trim().to_string();
439 if CLI_SKILL_UA_RE.is_match(&trimmed) {
440 return trimmed;
441 }
442 }
443 format!(
444 "{}/{}/{} (Rust/{}; {}; {})",
445 env!("CARGO_PKG_NAME"),
446 product,
447 env!("CARGO_PKG_VERSION"),
448 env!("RUSTC_VERSION"),
449 std::env::consts::OS,
450 std::env::consts::ARCH,
451 )
452}
453
454pub fn validate_time_unit(time_unit: &str) -> Result<Option<&str>, anyhow::Error> {
482 match time_unit {
483 "" => Ok(None),
484 "MILLISECOND" | "MICROSECOND" | "millisecond" | "microsecond" => Ok(Some(time_unit)),
485 _ => Err(anyhow::anyhow!(
486 "time_unit must be either 'MILLISECOND' or 'MICROSECOND'"
487 )),
488 }
489}
490
491#[must_use]
508pub fn get_timestamp() -> u128 {
509 SystemTime::now()
510 .duration_since(UNIX_EPOCH)
511 .expect("Time went backwards")
512 .as_millis()
513}
514
515pub async fn delay(ms: u64) {
527 sleep(Duration::from_millis(ms)).await;
528}
529
530pub fn build_query_string(params: &BTreeMap<String, Value>) -> Result<String, anyhow::Error> {
549 let mut segments = Vec::with_capacity(params.len());
550
551 for (key, value) in params {
552 if value.is_null() {
553 continue;
554 }
555
556 let value_str = match value {
557 Value::String(s) => s.clone(),
558 Value::Bool(b) => b.to_string(),
559 Value::Number(n) => n.to_string(),
560 Value::Array(_) | Value::Object(_) => serde_json::to_string(value)
561 .with_context(|| format!("failed to JSON-serialize `{}`", key))?,
562 Value::Null => unreachable!(),
563 };
564
565 let mut ser = Serializer::new(String::new());
566 ser.append_pair(key, &value_str);
567 segments.push(ser.finish());
568 }
569
570 Ok(segments.join("&"))
571}
572
573pub fn build_plain_query_string(params: &BTreeMap<String, Value>) -> Result<String, anyhow::Error> {
592 let mut segments = Vec::with_capacity(params.len());
593
594 for (key, value) in params {
595 if value.is_null() {
596 continue;
597 }
598
599 let value_str = match value {
600 Value::String(s) => s.clone(),
601 Value::Bool(b) => b.to_string(),
602 Value::Number(n) => n.to_string(),
603 Value::Array(_) | Value::Object(_) => serde_json::to_string(value)
604 .with_context(|| format!("failed to JSON-serialize `{}`", key))?,
605 Value::Null => unreachable!(),
606 };
607
608 ensure_safe_plain_query_component(key)
609 .with_context(|| format!("unsafe character in parameter name `{key}`"))?;
610 ensure_safe_plain_query_component(&value_str)
611 .with_context(|| format!("unsafe character in value of parameter `{key}`"))?;
612
613 segments.push(format!("{key}={value_str}"));
614 }
615
616 Ok(segments.join("&"))
617}
618
619fn ensure_safe_plain_query_component(s: &str) -> Result<(), anyhow::Error> {
626 if let Some(c) = s.chars().find(|&c| c == '&' || c == '=' || c.is_control()) {
627 return Err(anyhow::anyhow!(
628 "value contains disallowed character {c:?}; '&', '=', and control characters are not permitted in WebSocket API signed parameters"
629 ));
630 }
631 Ok(())
632}
633
634#[must_use]
642pub fn should_retry_request(
643 error: &reqwest::Error,
644 method: Option<&str>,
645 retries_left: Option<usize>,
646) -> bool {
647 let method = method.unwrap_or("");
648 let is_retriable_method =
649 method.eq_ignore_ascii_case("GET") || method.eq_ignore_ascii_case("DELETE");
650
651 let status = error.status().map_or(0, |s| s.as_u16());
652 let is_retriable_status = [500, 502, 503, 504].contains(&status);
653
654 let retries_left = retries_left.unwrap_or(0);
655 retries_left > 0 && is_retriable_method && (is_retriable_status || error.status().is_none())
656}
657
658#[must_use]
683pub fn parse_rate_limit_headers<S>(headers: &HashMap<String, String, S>) -> Vec<RestApiRateLimit>
684where
685 S: BuildHasher,
686{
687 let mut rate_limits = Vec::new();
688 let re = Regex::new(r"x-mbx-(used-weight|order-count)-(\d+)([smhd])").unwrap();
689 for (key, value) in headers {
690 let normalized_key = key.to_lowercase();
691 if normalized_key.starts_with("x-mbx-used-weight-")
692 || normalized_key.starts_with("x-mbx-order-count-")
693 {
694 if let Some(caps) = re.captures(&normalized_key) {
695 let interval_num: u32 = caps.get(2).unwrap().as_str().parse().unwrap_or(0);
696 let interval_letter = caps.get(3).unwrap().as_str().to_uppercase();
697 let interval = match interval_letter.as_str() {
698 "S" => Interval::Second,
699 "M" => Interval::Minute,
700 "H" => Interval::Hour,
701 "D" => Interval::Day,
702 _ => continue,
703 };
704 let count: u32 = value.parse().unwrap_or(0);
705 let rate_limit_type = if normalized_key.starts_with("x-mbx-used-weight-") {
706 RateLimitType::RequestWeight
707 } else {
708 RateLimitType::Orders
709 };
710
711 rate_limits.push(RestApiRateLimit {
712 rate_limit_type,
713 interval,
714 interval_num,
715 count,
716 retry_after: headers.get("retry-after").and_then(|v| v.parse().ok()),
717 });
718 }
719 }
720 }
721 rate_limits
722}
723
724pub async fn http_request<T: DeserializeOwned + Send + 'static>(
754 req: Request,
755 configuration: &ConfigurationRestApi,
756) -> Result<RestApiResponse<T>, ConnectorError> {
757 let client = &configuration.client;
758 let retries = configuration.retries as usize;
759 let backoff = configuration.backoff;
760 let mut attempt = 0;
761
762 loop {
763 let req_clone = req
764 .try_clone()
765 .context("Failed to clone request")
766 .map_err(|e| ConnectorError::ConnectorClientError {
767 msg: e.to_string(),
768 code: None,
769 })?;
770 match client.execute(req_clone).await {
771 Ok(response) => {
772 let status = response.status();
773 let headers_map: HashMap<String, String> = response
774 .headers()
775 .iter()
776 .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
777 .collect();
778
779 let raw_bytes = match response.bytes().await {
780 Ok(b) => b,
781 Err(e) => {
782 attempt += 1;
783 if attempt <= retries {
784 continue;
785 }
786 return Err(ConnectorError::ConnectorClientError {
787 msg: format!(
788 "Failed to get response bytes: {:#}",
789 anyhow::Error::new(e)
790 ),
791 code: None,
792 });
793 }
794 };
795
796 let content = if headers_map
797 .get("content-encoding")
798 .is_some_and(|enc| enc.to_lowercase().contains("gzip"))
799 {
800 let mut decoder = GzDecoder::new(&raw_bytes[..]);
801 let mut decompressed = String::new();
802 decoder
803 .read_to_string(&mut decompressed)
804 .context("Failed to decompress gzip response")
805 .map_err(|e: anyhow::Error| ConnectorError::ConnectorClientError {
806 msg: e.to_string(),
807 code: None,
808 })?;
809 decompressed
810 } else {
811 String::from_utf8(raw_bytes.to_vec())
812 .context("Failed to convert response to UTF-8")
813 .map_err(|e| ConnectorError::ConnectorClientError {
814 msg: e.to_string(),
815 code: None,
816 })?
817 };
818
819 let rate_limits = parse_rate_limit_headers(&headers_map);
820
821 if status.is_client_error() || status.is_server_error() {
822 let mut err_msg = content.clone();
823 let mut err_code: Option<i64> = None;
824
825 if let Ok(v) = serde_json::from_str::<serde_json::Value>(&content) {
826 if let Some(m) = v.get("msg").and_then(|m| m.as_str()) {
827 err_msg = m.to_string();
828 }
829 err_code = v.get("code").and_then(serde_json::Value::as_i64);
830 }
831
832 match status.as_u16() {
833 400 => {
834 return Err(ConnectorError::BadRequestError {
835 msg: err_msg,
836 code: err_code,
837 });
838 }
839 401 => {
840 return Err(ConnectorError::UnauthorizedError {
841 msg: err_msg,
842 code: err_code,
843 });
844 }
845 403 => {
846 return Err(ConnectorError::ForbiddenError {
847 msg: err_msg,
848 code: err_code,
849 });
850 }
851 404 => {
852 return Err(ConnectorError::NotFoundError {
853 msg: err_msg,
854 code: err_code,
855 });
856 }
857 418 => {
858 return Err(ConnectorError::RateLimitBanError {
859 msg: err_msg,
860 code: err_code,
861 });
862 }
863 429 => {
864 return Err(ConnectorError::TooManyRequestsError {
865 msg: err_msg,
866 code: err_code,
867 });
868 }
869 s if (500..600).contains(&s) => {
870 return Err(ConnectorError::ServerError {
871 msg: format!("Server error: {s}"),
872 status_code: Some(s),
873 });
874 }
875 _ => {
876 return Err(ConnectorError::ConnectorClientError {
877 msg: err_msg,
878 code: err_code,
879 });
880 }
881 }
882 }
883
884 let raw = content.clone();
885 return Ok(RestApiResponse {
886 data_fn: Box::new(move || {
887 Box::pin(async move {
888 let parsed: T = serde_json::from_str(&raw).map_err(|e| {
889 ConnectorError::ConnectorClientError {
890 msg: e.to_string(),
891 code: None,
892 }
893 })?;
894 Ok(parsed)
895 })
896 }),
897 status: status.as_u16(),
898 headers: headers_map,
899 rate_limits: if rate_limits.is_empty() {
900 None
901 } else {
902 Some(rate_limits)
903 },
904 });
905 }
906 Err(e) => {
907 attempt += 1;
908 if should_retry_request(
909 &e,
910 Some(req.method().as_str()),
911 Some(retries.saturating_sub(attempt)),
912 ) {
913 delay(backoff * attempt as u64).await;
914 continue;
915 }
916 return Err(ConnectorError::ConnectorClientError {
917 msg: format!("HTTP request failed: {:#}", anyhow::Error::new(e)),
918 code: None,
919 });
920 }
921 }
922 }
923}
924
925pub async fn send_request<T: DeserializeOwned + Send + 'static>(
952 configuration: &ConfigurationRestApi,
953 endpoint: &str,
954 method: Method,
955 mut query_params: BTreeMap<String, Value>,
956 body_params: BTreeMap<String, Value>,
957 time_unit: Option<TimeUnit>,
958 is_signed: bool,
959) -> anyhow::Result<RestApiResponse<T>> {
960 let base = configuration.base_path.as_deref().unwrap_or("");
961 let full_url = reqwest::Url::parse(base)
962 .and_then(|u| u.join(endpoint))
963 .context("Failed to join base URL and endpoint")?
964 .to_string();
965
966 if is_signed {
967 let timestamp = get_timestamp();
968 query_params.insert("timestamp".to_string(), json!(timestamp));
969 }
970
971 let signature = if is_signed {
972 let body_ref = if body_params.is_empty() {
973 None
974 } else {
975 Some(&body_params)
976 };
977 Some(
978 configuration
979 .signature_gen
980 .get_signature(&query_params, body_ref)?,
981 )
982 } else {
983 None
984 };
985
986 let mut url = Url::parse(&full_url)?;
987 {
988 let mut pairs = url.query_pairs_mut();
989 for (key, value) in &query_params {
990 let val_str = match value {
991 Value::String(s) => s.clone(),
992 _ => value.to_string(),
993 };
994 pairs.append_pair(key, &val_str);
995 }
996 if let Some(signature) = &signature {
997 pairs.append_pair("signature", signature);
998 }
999 }
1000
1001 let mut headers = HeaderMap::new();
1002
1003 let forbidden = ["host", "authorization", "cookie", ":method", ":path"]
1004 .into_iter()
1005 .map(str::to_ascii_lowercase)
1006 .collect::<std::collections::HashSet<_>>();
1007
1008 if let Some(custom) = &configuration.custom_headers {
1009 for (raw_name, raw_val) in custom {
1010 let name = raw_name.trim();
1011 if forbidden.contains(&name.to_ascii_lowercase()) {
1012 continue;
1013 }
1014 if let (Ok(header_name), Ok(header_val)) = (
1015 name.parse::<reqwest::header::HeaderName>(),
1016 HeaderValue::from_str(raw_val),
1017 ) {
1018 headers.append(header_name, header_val);
1019 }
1020 }
1021 }
1022
1023 if body_params.is_empty() {
1024 headers.insert("Content-Type", HeaderValue::from_static("application/json"));
1025 } else {
1026 headers.insert(
1027 "Content-Type",
1028 HeaderValue::from_static("application/x-www-form-urlencoded"),
1029 );
1030 }
1031
1032 headers.insert("User-Agent", configuration.user_agent.parse().unwrap());
1033 if let Some(api_key) = &configuration.api_key {
1034 headers.insert("X-MBX-APIKEY", HeaderValue::from_str(api_key)?);
1035 }
1036
1037 if configuration.compression {
1038 headers.insert(ACCEPT_ENCODING, "gzip, deflate, br".parse().unwrap());
1039 }
1040
1041 let time_unit_to_apply = time_unit.or(configuration.time_unit);
1042 if let Some(time_unit) = time_unit_to_apply {
1043 headers.insert("X-MBX-TIME-UNIT", time_unit.as_upper_str().parse()?);
1044 }
1045
1046 let mut req_builder = configuration.client.request(method, url).headers(headers);
1047
1048 if !body_params.is_empty() {
1049 let mut serializer = form_urlencoded::Serializer::new(String::new());
1050 for (key, value) in body_params {
1051 let val_str = match value {
1052 Value::String(s) => s,
1053 _ => value.to_string(),
1054 };
1055 serializer.append_pair(&key, &val_str);
1056 }
1057 let body_str = serializer.finish();
1058 req_builder = req_builder.body(body_str);
1059 }
1060
1061 let req = req_builder.build()?;
1062
1063 Ok(http_request::<T>(req, configuration).await?)
1064}
1065
1066#[must_use]
1075pub fn random_string() -> String {
1076 let mut buf = [0u8; 16];
1077 rand::thread_rng().fill_bytes(&mut buf);
1078 hex::encode(buf)
1079}
1080
1081#[must_use]
1090pub fn random_integer() -> u32 {
1091 let mut buf = [0u8; 4];
1092 OsRng.fill_bytes(&mut buf);
1093 u32::from_ne_bytes(buf)
1094}
1095
1096#[must_use]
1107pub fn normalize_stream_id(id: Option<StreamId>, stream_id_is_strictly_number: bool) -> Value {
1108 if stream_id_is_strictly_number {
1109 let n = match id {
1110 Some(StreamId::Number(n)) => n,
1111 _ => random_integer(),
1112 };
1113 return Value::Number(Number::from(n));
1114 }
1115
1116 match id {
1117 Some(StreamId::Number(n)) => Value::Number(Number::from(n)),
1118 Some(StreamId::Str(s)) => {
1119 let out = if ID_REGEX.is_match(&s) {
1120 s
1121 } else {
1122 random_string()
1123 };
1124 Value::String(out)
1125 }
1126 None => Value::String(random_string()),
1127 }
1128}
1129
1130pub fn remove_empty_value<I>(entries: I) -> BTreeMap<String, Value>
1152where
1153 I: IntoIterator<Item = (String, Value)>,
1154{
1155 entries
1156 .into_iter()
1157 .filter(|(_, value)| match value {
1158 Value::Null => false,
1159 Value::String(s) if s.is_empty() => false,
1160 _ => true,
1161 })
1162 .collect()
1163}
1164
1165#[must_use]
1186pub fn sort_object_params(params: &BTreeMap<String, Value>) -> BTreeMap<String, Value> {
1187 let mut sorted = BTreeMap::new();
1188 for (k, v) in params {
1189 sorted.insert(k.clone(), v.clone());
1190 }
1191 sorted
1192}
1193
1194fn normalize_ws_streams_key(key: &str) -> String {
1204 key.to_lowercase().replace(&['_', '-'][..], "")
1205}
1206
1207pub fn replace_websocket_streams_placeholders<V, S>(
1232 input: &str,
1233 variables: &HashMap<&str, V, S>,
1234) -> String
1235where
1236 V: Display,
1237 S: BuildHasher,
1238{
1239 let original = input;
1240
1241 let body = original.strip_prefix('/').unwrap_or(original);
1243
1244 let normalized: HashMap<String, String> = variables
1246 .iter()
1247 .map(|(k, v)| (normalize_ws_streams_key(k), v.to_string()))
1248 .collect();
1249
1250 let replaced = PLACEHOLDER_RE
1252 .replace_all(body, |caps: &Captures| {
1253 let prefix = caps.get(1).map_or("", |m| m.as_str());
1254 let key = normalize_ws_streams_key(caps.get(2).unwrap().as_str());
1255 let val = normalized.get(&key).cloned().unwrap_or_default();
1256 format!("{prefix}{val}")
1257 })
1258 .into_owned();
1259
1260 let stripped = replaced.trim_end_matches('@').to_string();
1262
1263 let should_lower_head =
1266 original.starts_with('/') && PLACEHOLDER_RE.find(body).is_some_and(|m| m.start() == 0);
1267
1268 if should_lower_head {
1270 if let Some(caps) = PLACEHOLDER_RE.captures(body) {
1271 let key = normalize_ws_streams_key(caps.get(2).unwrap().as_str());
1272 let first_val = normalized.get(&key).cloned().unwrap_or_default();
1273 if stripped.starts_with(&first_val) {
1274 let tail = &stripped[first_val.len()..];
1275 format!("{}{}", first_val.to_lowercase(), tail)
1276 } else {
1277 stripped.clone()
1278 }
1279 } else {
1280 stripped.clone()
1281 }
1282 } else {
1283 stripped.clone()
1284 }
1285}
1286
1287pub fn build_websocket_api_message(
1305 configuration: &ConfigurationWebsocketApi,
1306 method: &str,
1307 mut payload: BTreeMap<String, Value>,
1308 options: &WebsocketMessageSendOptions,
1309 skip_auth: bool,
1310) -> (String, serde_json::Value) {
1311 let id = payload
1312 .get("id")
1313 .and_then(Value::as_str)
1314 .filter(|s| ID_REGEX.is_match(s))
1315 .map_or_else(random_string, String::from);
1316
1317 payload.remove("id");
1318
1319 let mut params = remove_empty_value(payload);
1320
1321 if (options.with_api_key || options.is_signed) && !skip_auth {
1322 params.insert(
1323 "apiKey".into(),
1324 Value::String(configuration.api_key.clone().expect("API key must be set")),
1325 );
1326 }
1327
1328 if options.is_signed {
1329 let ts = get_timestamp();
1330 let ts_i64 = i64::try_from(ts).expect("timestamp fits in i64");
1331 params.insert("timestamp".into(), Value::Number(ts_i64.into()));
1332
1333 let mut sorted = sort_object_params(¶ms);
1334 if !skip_auth {
1335 let sig = configuration
1336 .signature_gen
1337 .get_signature_unencoded(&sorted)
1338 .expect("signature generation");
1339 sorted.insert("signature".into(), Value::String(sig));
1340 }
1341 params = sorted.into_iter().collect();
1342 }
1343
1344 let request = json!({
1345 "id": id,
1346 "method": method,
1347 "params": params,
1348 });
1349
1350 (id, request)
1351}
1352
1353#[cfg(test)]
1354mod tests {
1355 use crate::TOKIO_SHARED_RT;
1356
1357 mod build_client {
1358 use std::{
1359 sync::{Arc, Mutex},
1360 time::{Duration, Instant},
1361 };
1362
1363 use reqwest::ClientBuilder;
1364
1365 use crate::{
1366 common::utils::build_client,
1367 config::{HttpAgent, ProxyAuth, ProxyConfig},
1368 };
1369
1370 use super::TOKIO_SHARED_RT;
1371
1372 #[test]
1373 fn enforces_timeout() {
1374 TOKIO_SHARED_RT.block_on(async {
1375 let client = build_client(100, true, None, None);
1376 let start = Instant::now();
1377 let res = client.get("http://10.255.255.1").send().await;
1378 assert!(
1379 res.is_err(),
1380 "expected an error (timeout or connect) but got {res:?}"
1381 );
1382 let elapsed = start.elapsed();
1383 assert!(
1384 elapsed < Duration::from_millis(500),
1385 "timed out too slowly: {elapsed:?}"
1386 );
1387 });
1388 }
1389
1390 #[test]
1391 fn builds_with_keep_alive_disabled() {
1392 let client = build_client(200, false, None, None);
1393 let _: reqwest::Client = client;
1394 }
1395
1396 #[test]
1397 #[should_panic(expected = "Failed to create proxy from URL")]
1398 fn invalid_proxy_url_panics() {
1399 let bad_proxy = ProxyConfig {
1400 protocol: Some("http".to_string()),
1401 host: String::new(),
1402 port: 8080,
1403 auth: None,
1404 };
1405 let _ = build_client(1_000, true, Some(&bad_proxy), None);
1406 }
1407
1408 #[test]
1409 fn builds_with_proxy_and_auth() {
1410 let proxy = ProxyConfig {
1411 protocol: Some("https".to_string()),
1412 host: "127.0.0.1".to_string(),
1413 port: 3128,
1414 auth: Some(ProxyAuth {
1415 username: "alice".to_string(),
1416 password: "secret".to_string(),
1417 }),
1418 };
1419 let client = build_client(2_000, true, Some(&proxy), None);
1420 let _: reqwest::Client = client;
1421 }
1422
1423 #[test]
1424 fn custom_agent_invoked() {
1425 let called = Arc::new(Mutex::new(false));
1426 let called_clone = Arc::clone(&called);
1427
1428 let agent = HttpAgent(Arc::new(move |builder: ClientBuilder| {
1429 *called_clone.lock().unwrap() = true;
1430 builder
1431 }));
1432
1433 let client = build_client(1_000, true, None, Some(agent));
1434 assert!(*called.lock().unwrap(), "agent closure wasn’t invoked");
1435 let _: reqwest::Client = client;
1436 }
1437 }
1438
1439 mod build_user_agent {
1440 use crate::common::utils::build_user_agent;
1441 use std::sync::Mutex;
1442
1443 static ENV_LOCK: Mutex<()> = Mutex::new(());
1444
1445 #[test]
1446 fn build_user_agent_contains_crate_product_and_rust_info() {
1447 let product = "product";
1448 let user_agent = build_user_agent(product);
1449
1450 let name = env!("CARGO_PKG_NAME");
1451 let version = env!("CARGO_PKG_VERSION");
1452 let rustc = env!("RUSTC_VERSION");
1453 let os = std::env::consts::OS;
1454 let arch = std::env::consts::ARCH;
1455
1456 let expected_prefix = format!("{name}/{product}/{version} (Rust/");
1457 assert!(
1458 user_agent.starts_with(&expected_prefix),
1459 "prefix mismatch: {user_agent}"
1460 );
1461
1462 assert!(
1463 user_agent.contains(rustc),
1464 "user agent missing RUSTC_VERSION: {user_agent}"
1465 );
1466
1467 assert!(
1468 user_agent.contains(&format!("; {os}")),
1469 "user agent missing OS: {user_agent}"
1470 );
1471 assert!(
1472 user_agent.contains(&format!("; {arch}")),
1473 "user agent missing ARCH: {user_agent}"
1474 );
1475 }
1476
1477 #[test]
1478 fn build_user_agent_is_deterministic() {
1479 let product = "product";
1480 let user_agent1 = build_user_agent(product);
1481 let user_agent2 = build_user_agent(product);
1482 assert_eq!(
1483 user_agent1, user_agent2,
1484 "user agent should be the same on repeated calls"
1485 );
1486 }
1487
1488 #[test]
1489 fn env_override_cli_is_used_when_valid() {
1490 let _guard = ENV_LOCK.lock().unwrap();
1491 let valid = "binance-cli/1.2.3 (linux; x86_64; extra)";
1492 unsafe { std::env::set_var("BINANCE_CONNECTOR_RUST_USER_AGENT", valid) };
1493 let ua = build_user_agent("spot");
1494 unsafe { std::env::remove_var("BINANCE_CONNECTOR_RUST_USER_AGENT") };
1495 assert_eq!(ua, valid);
1496 }
1497
1498 #[test]
1499 fn env_override_skill_is_used_when_valid() {
1500 let _guard = ENV_LOCK.lock().unwrap();
1501 let valid = "binance-skill/2.0 (darwin; aarch64; v2)";
1502 unsafe { std::env::set_var("BINANCE_CONNECTOR_RUST_USER_AGENT", valid) };
1503 let ua = build_user_agent("spot");
1504 unsafe { std::env::remove_var("BINANCE_CONNECTOR_RUST_USER_AGENT") };
1505 assert_eq!(ua, valid);
1506 }
1507
1508 #[test]
1509 fn env_override_with_leading_whitespace_is_trimmed_and_used() {
1510 let _guard = ENV_LOCK.lock().unwrap();
1511 let valid = "binance-cli/1.0 (linux; x86_64; v1)";
1512 unsafe {
1513 std::env::set_var("BINANCE_CONNECTOR_RUST_USER_AGENT", format!(" {valid} "));
1514 };
1515 let ua = build_user_agent("spot");
1516 unsafe { std::env::remove_var("BINANCE_CONNECTOR_RUST_USER_AGENT") };
1517 assert_eq!(ua, valid);
1518 }
1519
1520 #[test]
1521 fn invalid_env_override_falls_back_to_default() {
1522 let _guard = ENV_LOCK.lock().unwrap();
1523 unsafe {
1524 std::env::set_var(
1525 "BINANCE_CONNECTOR_RUST_USER_AGENT",
1526 "not-binance-cli/1.0 (linux; x86_64; v1)",
1527 );
1528 };
1529 let ua = build_user_agent("spot");
1530 unsafe { std::env::remove_var("BINANCE_CONNECTOR_RUST_USER_AGENT") };
1531 assert!(
1532 ua.starts_with(env!("CARGO_PKG_NAME")),
1533 "should fall back to default: {ua}"
1534 );
1535 }
1536
1537 #[test]
1538 fn empty_env_override_falls_back_to_default() {
1539 let _guard = ENV_LOCK.lock().unwrap();
1540 unsafe { std::env::set_var("BINANCE_CONNECTOR_RUST_USER_AGENT", "") };
1541 let ua = build_user_agent("spot");
1542 unsafe { std::env::remove_var("BINANCE_CONNECTOR_RUST_USER_AGENT") };
1543 assert!(
1544 ua.starts_with(env!("CARGO_PKG_NAME")),
1545 "should fall back to default: {ua}"
1546 );
1547 }
1548 }
1549
1550 mod validate_time_unit {
1551 use crate::common::utils::validate_time_unit;
1552
1553 #[test]
1554 fn empty_string_returns_none() {
1555 let res = validate_time_unit("").expect("Should not error on empty string");
1556 assert_eq!(res, None);
1557 }
1558
1559 #[test]
1560 fn uppercase_millisecond() {
1561 let res = validate_time_unit("MILLISECOND").expect("Should accept MILLISECOND");
1562 assert_eq!(res, Some("MILLISECOND"));
1563 }
1564
1565 #[test]
1566 fn uppercase_microsecond() {
1567 let res = validate_time_unit("MICROSECOND").expect("Should accept MICROSECOND");
1568 assert_eq!(res, Some("MICROSECOND"));
1569 }
1570
1571 #[test]
1572 fn lowercase_millisecond() {
1573 let res = validate_time_unit("millisecond").expect("Should accept millisecond");
1574 assert_eq!(res, Some("millisecond"));
1575 }
1576
1577 #[test]
1578 fn lowercase_microsecond() {
1579 let res = validate_time_unit("microsecond").expect("Should accept microsecond");
1580 assert_eq!(res, Some("microsecond"));
1581 }
1582
1583 #[test]
1584 fn invalid_value_returns_err() {
1585 let err = validate_time_unit("SECOND").unwrap_err();
1586 let msg = format!("{err}");
1587 assert!(msg.contains("time_unit must be either 'MILLISECOND' or 'MICROSECOND'"));
1588 }
1589
1590 #[test]
1591 fn partial_match_returns_err() {
1592 let err = validate_time_unit("MILLI").unwrap_err();
1593 let msg = format!("{err}");
1594 assert!(msg.contains("time_unit must be either 'MILLISECOND' or 'MICROSECOND'"));
1595 }
1596 }
1597
1598 mod get_timestamp {
1599 use crate::common::utils::get_timestamp;
1600 use std::{
1601 thread::sleep,
1602 time::{Duration, SystemTime, UNIX_EPOCH},
1603 };
1604
1605 #[test]
1606 fn timestamp_is_within_system_time_bounds() {
1607 let before = SystemTime::now()
1608 .duration_since(UNIX_EPOCH)
1609 .expect("SystemTime before UNIX_EPOCH")
1610 .as_millis();
1611 let ts = get_timestamp();
1612 let after = SystemTime::now()
1613 .duration_since(UNIX_EPOCH)
1614 .expect("SystemTime before UNIX_EPOCH")
1615 .as_millis();
1616
1617 assert!(
1618 ts >= before,
1619 "timestamp {ts} is before captured before time {before}"
1620 );
1621 assert!(
1622 ts <= after,
1623 "timestamp {ts} is after captured after time {after}"
1624 );
1625 }
1626
1627 #[test]
1628 fn timestamps_are_monotonic() {
1629 let t1 = get_timestamp();
1630 sleep(Duration::from_millis(1));
1631 let t2 = get_timestamp();
1632 assert!(
1633 t2 >= t1,
1634 "second timestamp {t2} is not >= first timestamp {t1}"
1635 );
1636 }
1637 }
1638
1639 mod build_query_string {
1640 use std::collections::BTreeMap;
1641
1642 use anyhow::Result;
1643 use serde_json::{Value, json};
1644 use url::form_urlencoded::Serializer;
1645
1646 use crate::common::utils::build_query_string;
1647
1648 fn mk_map(pairs: Vec<(&str, Value)>) -> BTreeMap<String, Value> {
1649 let mut m = BTreeMap::new();
1650 for (k, v) in pairs {
1651 m.insert(k.to_string(), v);
1652 }
1653 m
1654 }
1655
1656 #[test]
1657 fn empty_map_returns_empty_string() -> Result<()> {
1658 let params = BTreeMap::new();
1659 let qs = build_query_string(¶ms)?;
1660 assert_eq!(qs, "");
1661 Ok(())
1662 }
1663
1664 #[test]
1665 fn string_and_number_and_bool() -> Result<()> {
1666 let params = mk_map(vec![
1667 ("foo", json!("bar")),
1668 ("num", json!(42)),
1669 ("flag", json!(true)),
1670 ]);
1671 let qs = build_query_string(¶ms)?;
1672 assert_eq!(qs, "flag=true&foo=bar&num=42");
1673 Ok(())
1674 }
1675
1676 #[test]
1677 fn null_is_skipped() -> Result<()> {
1678 let params = mk_map(vec![("a", json!(true)), ("b", Value::Null)]);
1679 let qs = build_query_string(¶ms)?;
1680 assert_eq!(qs, "a=true");
1681 Ok(())
1682 }
1683
1684 #[test]
1685 fn percent_encode_special_chars() -> Result<()> {
1686 let params = mk_map(vec![
1687 ("space", json!("hello world")),
1688 ("symbols", json!("a/b?c")),
1689 ]);
1690 let qs = build_query_string(¶ms)?;
1691 let mut parts = vec![];
1692 let mut ser = Serializer::new(String::new());
1693 ser.append_pair("space", "hello world");
1694 parts.push(ser.finish());
1695 let mut ser = Serializer::new(String::new());
1696 ser.append_pair("symbols", "a/b?c");
1697 parts.push(ser.finish());
1698 let expected = parts.join("&");
1699 assert_eq!(qs, expected);
1700 Ok(())
1701 }
1702
1703 #[test]
1704 fn primitive_array_json_encoded() -> Result<()> {
1705 let params = mk_map(vec![
1706 ("strs", json!(["a", "b", "c"])),
1707 ("nums", json!([1, 2, 3])),
1708 ("bools", json!([true, false])),
1709 ]);
1710 let qs = build_query_string(¶ms)?;
1711
1712 let mut parts = Vec::new();
1713 for (k, v) in ¶ms {
1714 let json = serde_json::to_string(v)?;
1715 let mut ser = Serializer::new(String::new());
1716 ser.append_pair(k, &json);
1717 parts.push(ser.finish());
1718 }
1719 let expected = parts.join("&");
1720 assert_eq!(qs, expected);
1721 Ok(())
1722 }
1723
1724 #[test]
1725 fn nested_array_json_encoded() -> Result<()> {
1726 let params = mk_map(vec![("nested", json!([[1, 2], [3, 4]]))]);
1727 let qs = build_query_string(¶ms)?;
1728
1729 let nested_json = serde_json::to_string(&json!([[1, 2], [3, 4]]))?;
1730 let mut ser = Serializer::new(String::new());
1731 ser.append_pair("nested", &nested_json);
1732 let expected = ser.finish();
1733
1734 assert_eq!(qs, expected);
1735 Ok(())
1736 }
1737
1738 #[test]
1739 fn object_json_encoded() -> Result<()> {
1740 let params = mk_map(vec![("obj", json!({"k":1, "v":"two"}))]);
1741 let qs = build_query_string(¶ms)?;
1742
1743 let obj_json = serde_json::to_string(&json!({"k":1, "v":"two"}))?;
1744 let mut ser = Serializer::new(String::new());
1745 ser.append_pair("obj", &obj_json);
1746 let expected = ser.finish();
1747
1748 assert_eq!(qs, expected);
1749 Ok(())
1750 }
1751
1752 #[test]
1753 fn empty_array() {
1754 let params = mk_map(vec![("foo", json!([]))]);
1755 let qs = build_query_string(¶ms).unwrap();
1756
1757 let json = serde_json::to_string(&json!([])).unwrap();
1758 let expected = Serializer::new(String::new())
1759 .append_pair("foo", &json)
1760 .finish();
1761 assert_eq!(qs, expected);
1762 }
1763
1764 #[test]
1765 fn mixed_array() {
1766 let params = mk_map(vec![("mix", json!([1, "x", false]))]);
1767 let qs = build_query_string(¶ms).unwrap();
1768
1769 let json = serde_json::to_string(&json!([1, "x", false])).unwrap();
1770 let expected = Serializer::new(String::new())
1771 .append_pair("mix", &json)
1772 .finish();
1773 assert_eq!(qs, expected);
1774 }
1775
1776 #[test]
1777 fn array_of_objects() {
1778 let params = mk_map(vec![("objs", json!([{"a":1}, {"b":2}]))]);
1779 let qs = build_query_string(¶ms).unwrap();
1780
1781 let json = serde_json::to_string(&json!([{"a":1}, {"b":2}])).unwrap();
1782 let expected = Serializer::new(String::new())
1783 .append_pair("objs", &json)
1784 .finish();
1785 assert_eq!(qs, expected);
1786 }
1787
1788 #[test]
1789 fn empty_object() {
1790 let params = mk_map(vec![("emp", json!({}))]);
1791 let qs = build_query_string(¶ms).unwrap();
1792
1793 let json = serde_json::to_string(&json!({})).unwrap();
1794 let expected = Serializer::new(String::new())
1795 .append_pair("emp", &json)
1796 .finish();
1797 assert_eq!(qs, expected);
1798 }
1799
1800 #[test]
1801 fn floats_and_negatives() {
1802 let params = mk_map(vec![("fl", json!(1.23456)), ("neg", json!(-0.001))]);
1803 let qs = build_query_string(¶ms).unwrap();
1804 assert_eq!(qs, "fl=1.23456&neg=-0.001");
1805 }
1806
1807 #[test]
1808 fn unicode_and_special_key() {
1809 let params = mk_map(vec![
1810 ("こんにちは", json!("世界")),
1811 ("weird key/?=", json!("val")),
1812 ]);
1813 let qs = build_query_string(¶ms).unwrap();
1814
1815 let mut parts = Vec::new();
1816 for (k, v) in ¶ms {
1817 let mut ser = Serializer::new(String::new());
1818 ser.append_pair(k, v.as_str().unwrap());
1819 parts.push(ser.finish());
1820 }
1821 let expected = parts.join("&");
1822 assert_eq!(qs, expected);
1823 }
1824
1825 #[test]
1826 fn empty_string_value() {
1827 let params = mk_map(vec![("empty", json!(""))]);
1828 let qs = build_query_string(¶ms).unwrap();
1829 assert_eq!(qs, "empty=");
1830 }
1831
1832 #[test]
1833 fn nulls_in_array() {
1834 let params = mk_map(vec![("a", json!([null, 1, "x"]))]);
1835 let qs = build_query_string(¶ms).unwrap();
1836
1837 let json = serde_json::to_string(&json!([null, 1, "x"])).unwrap();
1838 let expected = Serializer::new(String::new())
1839 .append_pair("a", &json)
1840 .finish();
1841 assert_eq!(qs, expected);
1842 }
1843
1844 #[test]
1845 fn special_chars_in_key() {
1846 let params = mk_map(vec![("a=b&c%", json!("val"))]);
1847 let qs = build_query_string(¶ms).unwrap();
1848
1849 let expected = Serializer::new(String::new())
1850 .append_pair("a=b&c%", "val")
1851 .finish();
1852 assert_eq!(qs, expected);
1853 }
1854
1855 #[test]
1856 fn empty_key() {
1857 let params = mk_map(vec![("", json!("v"))]);
1858 let qs = build_query_string(¶ms).unwrap();
1859 assert_eq!(qs, "=v");
1860 }
1861 }
1862
1863 mod build_plain_query_string {
1864 use std::collections::BTreeMap;
1865
1866 use serde_json::{Value, json};
1867
1868 use crate::common::utils::build_plain_query_string;
1869
1870 fn mk_map(pairs: Vec<(&str, Value)>) -> BTreeMap<String, Value> {
1871 let mut m = BTreeMap::new();
1872 for (k, v) in pairs {
1873 m.insert(k.to_string(), v);
1874 }
1875 m
1876 }
1877
1878 #[test]
1879 fn empty_map_returns_empty_string() {
1880 let params = BTreeMap::new();
1881 let qs = build_plain_query_string(¶ms).unwrap();
1882 assert_eq!(qs, "");
1883 }
1884
1885 #[test]
1886 fn string_and_number_and_bool_are_not_percent_encoded() {
1887 let params = mk_map(vec![
1888 ("foo", json!("bar")),
1889 ("num", json!(42)),
1890 ("flag", json!(true)),
1891 ]);
1892 let qs = build_plain_query_string(¶ms).unwrap();
1893 assert_eq!(qs, "flag=true&foo=bar&num=42");
1894 }
1895
1896 #[test]
1897 fn null_is_skipped() {
1898 let params = mk_map(vec![("a", json!(true)), ("b", Value::Null)]);
1899 let qs = build_plain_query_string(¶ms).unwrap();
1900 assert_eq!(qs, "a=true");
1901 }
1902
1903 #[test]
1904 fn non_ascii_values_are_kept_raw() {
1905 let params = mk_map(vec![("symbol", json!("我踏马来了USDT"))]);
1906 let qs = build_plain_query_string(¶ms).unwrap();
1907 assert_eq!(qs, "symbol=我踏马来了USDT");
1908 }
1909
1910 #[test]
1911 fn value_containing_ampersand_is_rejected() {
1912 let params = mk_map(vec![("a", json!("1&b=2"))]);
1913 let err = build_plain_query_string(¶ms).unwrap_err().to_string();
1914 assert!(err.contains("unsafe character"), "unexpected error: {err}");
1915 }
1916
1917 #[test]
1918 fn value_containing_equals_is_rejected() {
1919 let params = mk_map(vec![("a", json!("1=2"))]);
1920 let err = build_plain_query_string(¶ms).unwrap_err().to_string();
1921 assert!(err.contains("unsafe character"), "unexpected error: {err}");
1922 }
1923
1924 #[test]
1925 fn value_containing_control_character_is_rejected() {
1926 let params = mk_map(vec![("a", json!("line1\nline2"))]);
1927 let err = build_plain_query_string(¶ms).unwrap_err().to_string();
1928 assert!(err.contains("unsafe character"), "unexpected error: {err}");
1929 }
1930
1931 #[test]
1932 fn key_containing_ampersand_is_rejected() {
1933 let params = mk_map(vec![("a&b", json!("v"))]);
1934 let err = build_plain_query_string(¶ms).unwrap_err().to_string();
1935 assert!(err.contains("unsafe character"), "unexpected error: {err}");
1936 }
1937
1938 #[test]
1939 fn ambiguous_parameter_sets_no_longer_collide() {
1940 let colliding = mk_map(vec![("a", json!("1&b=2"))]);
1941 let distinct = mk_map(vec![("a", json!("1")), ("b", json!("2"))]);
1942
1943 assert!(build_plain_query_string(&colliding).is_err());
1944 assert_eq!(build_plain_query_string(&distinct).unwrap(), "a=1&b=2");
1945 }
1946 }
1947
1948 #[cfg(feature = "openssl-tls")]
1949 mod signature_generator {
1950 use base64::{Engine, engine::general_purpose};
1951 use ed25519_dalek::{SigningKey, ed25519::signature::SignerMut, pkcs8::DecodePrivateKey};
1952 use hex;
1953 use hmac::{Hmac, Mac};
1954 #[cfg(feature = "openssl-tls")]
1955 use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa, sign::Verifier};
1956 use serde_json::Value;
1957 use sha2::Sha256;
1958 use std::collections::BTreeMap;
1959 use std::io::Write;
1960 use tempfile::NamedTempFile;
1961
1962 use crate::{common::utils::SignatureGenerator, config::PrivateKey};
1963
1964 #[test]
1965 fn hmac_sha256_signature() {
1966 let mut params = BTreeMap::new();
1967 params.insert("b".into(), Value::Number(2.into()));
1968 params.insert("a".into(), Value::Number(1.into()));
1969
1970 let signature_gen = SignatureGenerator::new(Some("test-secret".into()), None, None);
1971 let sig = signature_gen
1972 .get_signature(¶ms, None)
1973 .expect("HMAC signing failed");
1974
1975 let mut mac = Hmac::<Sha256>::new_from_slice(b"test-secret").unwrap();
1976 let qs = "a=1&b=2";
1977 mac.update(qs.as_bytes());
1978 let expected = hex::encode(mac.finalize().into_bytes());
1979
1980 assert_eq!(sig, expected);
1981 }
1982
1983 #[test]
1984 fn hmac_sha256_signature_with_body() {
1985 let mut query_params = BTreeMap::new();
1986 query_params.insert("b".into(), Value::Number(2.into()));
1987 query_params.insert("a".into(), Value::Number(1.into()));
1988
1989 let mut body_params = BTreeMap::new();
1990 body_params.insert("d".into(), Value::Number(4.into()));
1991 body_params.insert("c".into(), Value::Number(3.into()));
1992
1993 let signature_gen = SignatureGenerator::new(Some("test-secret".into()), None, None);
1994 let sig = signature_gen
1995 .get_signature(&query_params, Some(&body_params))
1996 .expect("HMAC signing with body failed");
1997
1998 let query_str = "a=1&b=2";
1999 let body_str = "c=3&d=4";
2000
2001 let payload = format!("{query_str}{body_str}");
2002
2003 let mut mac = Hmac::<Sha256>::new_from_slice(b"test-secret").unwrap();
2004 mac.update(payload.as_bytes());
2005 let expected = hex::encode(mac.finalize().into_bytes());
2006
2007 assert_eq!(sig, expected);
2008 }
2009
2010 #[test]
2011 fn repeated_hmac_signature() {
2012 let mut params = BTreeMap::new();
2013 params.insert("x".into(), Value::String("y".into()));
2014 let signature_gen = SignatureGenerator::new(Some("abc".into()), None, None);
2015 let s1 = signature_gen.get_signature(¶ms, None).unwrap();
2016 let s2 = signature_gen.get_signature(¶ms, None).unwrap();
2017 assert_eq!(s1, s2);
2018 }
2019
2020 #[test]
2021 fn rsa_signature_verification() {
2022 let mut params = BTreeMap::new();
2023 params.insert("a".into(), Value::Number(1.into()));
2024 params.insert("b".into(), Value::Number(2.into()));
2025
2026 let rsa = Rsa::generate(2048).unwrap();
2027 let priv_pem = rsa.private_key_to_pem().unwrap();
2028 let pub_pem = rsa.public_key_to_pem_pkcs1().unwrap();
2029
2030 let signature_gen =
2031 SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem.clone())), None);
2032 let sig = signature_gen
2033 .get_signature(¶ms, None)
2034 .expect("RSA signing failed");
2035
2036 let sig_bytes = general_purpose::STANDARD.decode(&sig).unwrap();
2037 let pubkey = PKey::public_key_from_pem(&pub_pem).unwrap();
2038 let mut verifier = Verifier::new(MessageDigest::sha256(), &pubkey).unwrap();
2039 verifier.update(b"a=1&b=2").unwrap();
2040 assert!(verifier.verify(&sig_bytes).unwrap());
2041 }
2042
2043 #[test]
2044 fn rsa_signature_verification_with_body() {
2045 let mut query_params = BTreeMap::new();
2046 query_params.insert("a".into(), Value::Number(1.into()));
2047 query_params.insert("b".into(), Value::Number(2.into()));
2048
2049 let mut body_params = BTreeMap::new();
2050 body_params.insert("c".into(), Value::Number(3.into()));
2051 body_params.insert("d".into(), Value::Number(4.into()));
2052
2053 let rsa = Rsa::generate(2048).unwrap();
2054 let priv_pem = rsa.private_key_to_pem().unwrap();
2055 let pub_pem = rsa.public_key_to_pem_pkcs1().unwrap();
2056
2057 let signature_gen =
2058 SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem.clone())), None);
2059 let sig = signature_gen
2060 .get_signature(&query_params, Some(&body_params))
2061 .expect("RSA signing with body failed");
2062
2063 let sig_bytes = general_purpose::STANDARD.decode(&sig).unwrap();
2064 let pubkey = PKey::public_key_from_pem(&pub_pem).unwrap();
2065 let mut verifier = Verifier::new(MessageDigest::sha256(), &pubkey).unwrap();
2066 verifier.update(b"a=1&b=2c=3&d=4").unwrap();
2067 assert!(verifier.verify(&sig_bytes).unwrap());
2068 }
2069
2070 #[test]
2071 fn repeated_rsa_signature() {
2072 let mut params = BTreeMap::new();
2073 params.insert("k".into(), Value::Number(5.into()));
2074 let rsa = Rsa::generate(2048).unwrap();
2075 let priv_pem = rsa.private_key_to_pem().unwrap();
2076 let signature_gen =
2077 SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem)), None);
2078 let s1 = signature_gen.get_signature(¶ms, None).unwrap();
2079 let s2 = signature_gen.get_signature(¶ms, None).unwrap();
2080 assert_eq!(s1, s2);
2081 }
2082
2083 #[test]
2084 fn ed25519_signature_verification() {
2085 let mut params = BTreeMap::new();
2086 params.insert("a".into(), Value::Number(1.into()));
2087 params.insert("b".into(), Value::Number(2.into()));
2088 let qs = "a=1&b=2";
2089
2090 let ed = PKey::generate_ed25519().unwrap();
2091 let priv_pem = ed.private_key_to_pem_pkcs8().unwrap();
2092
2093 let signature_gen =
2094 SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem.clone())), None);
2095 let sig = signature_gen
2096 .get_signature(¶ms, None)
2097 .expect("Ed25519 signing failed");
2098
2099 let pem_str = String::from_utf8(priv_pem).unwrap();
2100 let b64 = pem_str
2101 .lines()
2102 .filter(|l| !l.starts_with("-----"))
2103 .collect::<String>();
2104 let der = general_purpose::STANDARD.decode(b64).unwrap();
2105 let mut sk = SigningKey::from_pkcs8_der(&der).unwrap();
2106 let expected_bytes = sk.sign(qs.as_bytes()).to_bytes();
2107 let expected_sig = general_purpose::STANDARD.encode(expected_bytes);
2108 assert_eq!(sig, expected_sig);
2109 }
2110
2111 #[test]
2112 fn ed25519_signature_verification_with_body() {
2113 let mut query_params = BTreeMap::new();
2114 query_params.insert("a".into(), Value::Number(1.into()));
2115 query_params.insert("b".into(), Value::Number(2.into()));
2116 let qs = "a=1&b=2";
2117
2118 let mut body_params = BTreeMap::new();
2119 body_params.insert("c".into(), Value::Number(3.into()));
2120 body_params.insert("d".into(), Value::Number(4.into()));
2121 let body_qs = "c=3&d=4";
2122
2123 let ed = PKey::generate_ed25519().unwrap();
2124 let priv_pem = ed.private_key_to_pem_pkcs8().unwrap();
2125
2126 let signature_gen =
2127 SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem.clone())), None);
2128 let sig = signature_gen
2129 .get_signature(&query_params, Some(&body_params))
2130 .expect("Ed25519 signing with body failed");
2131
2132 let pem_str = String::from_utf8(priv_pem).unwrap();
2133 let b64 = pem_str
2134 .lines()
2135 .filter(|l| !l.starts_with("-----"))
2136 .collect::<String>();
2137 let der = general_purpose::STANDARD.decode(b64).unwrap();
2138 let mut sk = SigningKey::from_pkcs8_der(&der).unwrap();
2139 let payload = format!("{qs}{body_qs}");
2140 let expected_bytes = sk.sign(payload.as_bytes()).to_bytes();
2141 let expected_sig = general_purpose::STANDARD.encode(expected_bytes);
2142 assert_eq!(sig, expected_sig);
2143 }
2144
2145 #[test]
2146 fn repeated_ed25519_signature() {
2147 let mut params = BTreeMap::new();
2148 params.insert("m".into(), Value::String("n".into()));
2149 let ed = PKey::generate_ed25519().unwrap();
2150 let priv_pem = ed.private_key_to_pem_pkcs8().unwrap();
2151 let signature_gen =
2152 SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem.clone())), None);
2153 let s1 = signature_gen.get_signature(¶ms, None).unwrap();
2154 let s2 = signature_gen.get_signature(¶ms, None).unwrap();
2155 assert_eq!(s1, s2);
2156 }
2157
2158 #[test]
2159 fn file_based_key() {
2160 let rsa = Rsa::generate(1024).unwrap();
2161 let priv_pem = rsa.private_key_to_pem().unwrap();
2162 let pub_pem = rsa.public_key_to_pem_pkcs1().unwrap();
2163
2164 let mut file = NamedTempFile::new().unwrap();
2165 file.write_all(&priv_pem).unwrap();
2166 let path = file.path().to_str().unwrap().to_string();
2167
2168 let mut params = BTreeMap::new();
2169 params.insert("z".into(), Value::Number(9.into()));
2170
2171 let signature_gen = SignatureGenerator::new(None, Some(PrivateKey::File(path)), None);
2172 let sig = signature_gen.get_signature(¶ms, None).unwrap();
2173
2174 let sig_bytes = general_purpose::STANDARD.decode(&sig).unwrap();
2175 let pubkey = PKey::public_key_from_pem(&pub_pem).unwrap();
2176 let mut verifier = Verifier::new(MessageDigest::sha256(), &pubkey).unwrap();
2177 verifier.update(b"z=9").unwrap();
2178 assert!(verifier.verify(&sig_bytes).unwrap());
2179 }
2180
2181 #[cfg(feature = "openssl-tls")]
2182 #[test]
2183 fn unsupported_key_type_error() {
2184 let mut params = BTreeMap::new();
2185 params.insert("x".into(), Value::String("y".into()));
2186
2187 let group =
2188 openssl::ec::EcGroup::from_curve_name(openssl::nid::Nid::X9_62_PRIME256V1).unwrap();
2189 let ec_key = openssl::ec::EcKey::generate(&group).unwrap();
2190 let pkey_ec = PKey::from_ec_key(ec_key).unwrap();
2191 let raw = pkey_ec.private_key_to_pem_pkcs8().unwrap();
2192
2193 let signature_gen = SignatureGenerator::new(None, Some(PrivateKey::Raw(raw)), None);
2194 let err = signature_gen
2195 .get_signature(¶ms, None)
2196 .unwrap_err()
2197 .to_string();
2198 assert!(err.contains("Unsupported private key type"));
2199 }
2200
2201 #[test]
2202 fn invalid_private_key_error() {
2203 let mut params = BTreeMap::new();
2204 params.insert("foo".into(), Value::String("bar".into()));
2205
2206 let signature_gen =
2207 SignatureGenerator::new(None, Some(PrivateKey::Raw(b"not a key".to_vec())), None);
2208 let err = signature_gen
2209 .get_signature(¶ms, None)
2210 .unwrap_err()
2211 .to_string();
2212 assert!(err.contains("Failed to parse private key"));
2213 }
2214
2215 #[test]
2216 fn missing_credentials_error() {
2217 let mut params = BTreeMap::new();
2218 params.insert("a".into(), Value::Number(1.into()));
2219
2220 let signature_gen = SignatureGenerator::new(None, None, None);
2221 let err = signature_gen
2222 .get_signature(¶ms, None)
2223 .unwrap_err()
2224 .to_string();
2225 assert!(err.contains("Either 'api_secret' or 'private_key' must be provided"));
2226 }
2227
2228 #[test]
2229 fn unencoded_hmac_signature_does_not_percent_encode_non_ascii() {
2230 let mut params = BTreeMap::new();
2231 params.insert("apiKey".into(), Value::String("key".into()));
2232 params.insert("symbol".into(), Value::String("我踏马来了USDT".into()));
2233 params.insert("timestamp".into(), Value::Number(1_i64.into()));
2234
2235 let signature_gen = SignatureGenerator::new(Some("test-secret".into()), None, None);
2236 let sig = signature_gen
2237 .get_signature_unencoded(¶ms)
2238 .expect("HMAC unencoded signing failed");
2239
2240 let expected_payload = "apiKey=key&symbol=我踏马来了USDT×tamp=1";
2241 let mut mac = Hmac::<Sha256>::new_from_slice(b"test-secret").unwrap();
2242 mac.update(expected_payload.as_bytes());
2243 let expected = hex::encode(mac.finalize().into_bytes());
2244
2245 assert_eq!(sig, expected);
2246
2247 let encoded_sig = signature_gen
2248 .get_signature(¶ms, None)
2249 .expect("HMAC encoded signing failed");
2250 assert_ne!(sig, encoded_sig);
2251 }
2252
2253 #[test]
2254 fn unencoded_ed25519_signature_does_not_percent_encode_non_ascii() {
2255 let mut params = BTreeMap::new();
2256 params.insert("apiKey".into(), Value::String("key".into()));
2257 params.insert("symbol".into(), Value::String("我踏马来了USDT".into()));
2258 params.insert("timestamp".into(), Value::Number(1_i64.into()));
2259
2260 let ed = PKey::generate_ed25519().unwrap();
2261 let priv_pem = ed.private_key_to_pem_pkcs8().unwrap();
2262
2263 let signature_gen =
2264 SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem.clone())), None);
2265 let sig = signature_gen
2266 .get_signature_unencoded(¶ms)
2267 .expect("Ed25519 unencoded signing failed");
2268
2269 let expected_payload = "apiKey=key&symbol=我踏马来了USDT×tamp=1";
2270 let pem_str = String::from_utf8(priv_pem).unwrap();
2271 let b64 = pem_str
2272 .lines()
2273 .filter(|l| !l.starts_with("-----"))
2274 .collect::<String>();
2275 let der = general_purpose::STANDARD.decode(b64).unwrap();
2276 let mut sk = SigningKey::from_pkcs8_der(&der).unwrap();
2277 let expected_bytes = sk.sign(expected_payload.as_bytes()).to_bytes();
2278 let expected_sig = general_purpose::STANDARD.encode(expected_bytes);
2279
2280 assert_eq!(sig, expected_sig);
2281 }
2282 }
2283
2284 mod should_retry_request {
2285 use crate::common::utils::should_retry_request;
2286
2287 use reqwest::{Error, Response};
2288
2289 fn mk_http_error(code: u16) -> Error {
2290 let resp = Response::from(
2291 http::response::Response::builder()
2292 .status(code)
2293 .body("")
2294 .unwrap(),
2295 );
2296 resp.error_for_status().unwrap_err()
2297 }
2298
2299 fn mk_network_error() -> Error {
2300 reqwest::blocking::get("http://256.256.256.256").unwrap_err()
2301 }
2302
2303 #[test]
2304 fn retry_on_retriable_status_and_method() {
2305 let err = mk_http_error(500);
2306 assert!(should_retry_request(&err, Some("GET"), Some(1)));
2307 assert!(should_retry_request(&err, Some("delete"), Some(2)));
2308 }
2309
2310 #[test]
2311 fn retry_when_status_none_and_retriable_method() {
2312 let retriable_methods = ["GET", "DELETE"];
2313
2314 for &method in &retriable_methods {
2315 let err = mk_network_error();
2316 assert!(
2317 should_retry_request(&err, Some(method), Some(1)),
2318 "Should retry when no status and method {method}"
2319 );
2320 }
2321 }
2322
2323 #[test]
2324 fn no_retry_when_no_retries_left() {
2325 let err = mk_http_error(503);
2326 assert!(!should_retry_request(&err, Some("GET"), Some(0)));
2327 }
2328
2329 #[test]
2330 fn no_retry_on_non_retriable_status() {
2331 let non_retriable_statuses = [400, 401, 404, 422];
2332
2333 for &status in &non_retriable_statuses {
2334 let err = mk_http_error(status);
2335 assert!(
2336 !should_retry_request(&err, Some("GET"), Some(2)),
2337 "Should not retry for non-retriable status {status}"
2338 );
2339 }
2340 }
2341
2342 #[test]
2343 fn no_retry_on_non_retriable_method() {
2344 let non_retriable_methods = ["POST", "PUT", "PATCH"];
2345
2346 for &method in &non_retriable_methods {
2347 let err = mk_http_error(500);
2348 assert!(
2349 !should_retry_request(&err, Some(method), Some(2)),
2350 "Should not retry for non-retriable method {method}"
2351 );
2352 }
2353 }
2354
2355 #[test]
2356 fn no_retry_when_status_none_and_non_retriable_method() {
2357 let non_retriable_methods = ["POST", "PUT"];
2358
2359 for &method in &non_retriable_methods {
2360 let err = mk_network_error();
2361 assert!(
2362 !should_retry_request(&err, Some(method), Some(1)),
2363 "Should not retry when no status and method {method}"
2364 );
2365 }
2366 }
2367 }
2368
2369 mod parse_rate_limit_headers_tests {
2370 use crate::common::{
2371 models::{Interval, RateLimitType},
2372 utils::parse_rate_limit_headers,
2373 };
2374 use std::collections::HashMap;
2375
2376 fn mk_headers(pairs: Vec<(&str, &str)>) -> HashMap<String, String> {
2377 let mut m = HashMap::new();
2378 for (k, v) in pairs {
2379 m.insert(k.to_string(), v.to_string());
2380 }
2381 m
2382 }
2383
2384 #[test]
2385 fn single_weight_header() {
2386 let headers = mk_headers(vec![("x-mbx-used-weight-1s", "123")]);
2387 let limits = parse_rate_limit_headers(&headers);
2388 assert_eq!(limits.len(), 1);
2389 let rl = &limits[0];
2390 assert_eq!(rl.rate_limit_type, RateLimitType::RequestWeight);
2391 assert_eq!(rl.interval, Interval::Second);
2392 assert_eq!(rl.interval_num, 1);
2393 assert_eq!(rl.count, 123);
2394 assert_eq!(rl.retry_after, None);
2395 }
2396
2397 #[test]
2398 fn single_order_count_with_retry_after() {
2399 let headers = mk_headers(vec![("x-mbx-order-count-5m", "42"), ("retry-after", "7")]);
2400 let limits = parse_rate_limit_headers(&headers);
2401 assert_eq!(limits.len(), 1);
2402 let rl = &limits[0];
2403 assert_eq!(rl.rate_limit_type, RateLimitType::Orders);
2404 assert_eq!(rl.interval, Interval::Minute);
2405 assert_eq!(rl.interval_num, 5);
2406 assert_eq!(rl.count, 42);
2407 assert_eq!(rl.retry_after, Some(7));
2408 }
2409
2410 #[test]
2411 fn multiple_headers() {
2412 let headers = mk_headers(vec![
2413 ("X-MBX-USED-WEIGHT-1h", "10"),
2414 ("x-mbx-order-count-2d", "20"),
2415 ]);
2416 let mut limits = parse_rate_limit_headers(&headers);
2417 limits.sort_by_key(|r| (r.interval_num, format!("{:?}", r.rate_limit_type)));
2418 assert_eq!(limits.len(), 2);
2419 let w = &limits[0];
2420 assert_eq!(w.rate_limit_type, RateLimitType::RequestWeight);
2421 assert_eq!(w.interval, Interval::Hour);
2422 assert_eq!(w.interval_num, 1);
2423 assert_eq!(w.count, 10);
2424 let o = &limits[1];
2425 assert_eq!(o.rate_limit_type, RateLimitType::Orders);
2426 assert_eq!(o.interval, Interval::Day);
2427 assert_eq!(o.interval_num, 2);
2428 assert_eq!(o.count, 20);
2429 }
2430
2431 #[test]
2432 fn ignores_unknown_and_malformed() {
2433 let headers = mk_headers(vec![
2434 ("x-mbx-used-weight-3x", "5"),
2435 ("random-header", "100"),
2436 ]);
2437 let limits = parse_rate_limit_headers(&headers);
2438 assert!(limits.is_empty());
2439 }
2440 }
2441
2442 mod http_request {
2443 use std::io::Write;
2444
2445 use flate2::{Compression, write::GzEncoder};
2446 use httpmock::MockServer;
2447 use reqwest::{Client, Method, Request};
2448 use serde::Deserialize;
2449
2450 use crate::{
2451 common::utils::http_request, config::ConfigurationRestApi, errors::ConnectorError,
2452 models::RestApiResponse,
2453 };
2454
2455 use super::TOKIO_SHARED_RT;
2456
2457 #[derive(Deserialize, Debug, PartialEq)]
2458 struct Dummy {
2459 foo: String,
2460 }
2461
2462 fn make_config(server_url: &str) -> ConfigurationRestApi {
2463 ConfigurationRestApi::builder()
2464 .api_key("key")
2465 .api_secret("secret")
2466 .base_path(server_url)
2467 .build()
2468 .expect("Failed to build configuration")
2469 }
2470
2471 #[test]
2472 fn http_request_success_plain_text() {
2473 TOKIO_SHARED_RT.block_on(async {
2474 let server = MockServer::start();
2475 let mock = server.mock(|when, then| {
2476 when.method(httpmock::Method::GET).path("/test");
2477 then.status(200)
2478 .header("Content-Type", "application/json")
2479 .body(r#"{"foo":"bar"}"#);
2480 });
2481
2482 let client = Client::new();
2483 let req: Request = client
2484 .request(Method::GET, format!("{}{}", server.url(""), "/test"))
2485 .build()
2486 .unwrap();
2487
2488 let cfg = make_config(&server.url(""));
2489 let resp: RestApiResponse<Dummy> = http_request(req, &cfg).await.unwrap();
2490 assert_eq!(resp.status, 200);
2491 let data = resp.data().await.unwrap();
2492 assert_eq!(data, Dummy { foo: "bar".into() });
2493 mock.assert();
2494 });
2495 }
2496
2497 #[test]
2498 fn http_request_success_gzip() {
2499 TOKIO_SHARED_RT.block_on(async {
2500 let server = MockServer::start();
2501 let body = r#"{"foo":"baz"}"#;
2502 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
2503 encoder.write_all(body.as_bytes()).unwrap();
2504 let gz = encoder.finish().unwrap();
2505
2506 let mock = server.mock(|when, then| {
2507 when.method(httpmock::Method::GET).path("/gz");
2508 then.status(200)
2509 .header("Content-Type", "application/json")
2510 .header("Content-Encoding", "gzip")
2511 .body(gz);
2512 });
2513
2514 let client = Client::new();
2515 let req: Request = client
2516 .request(Method::GET, format!("{}{}", server.url(""), "/gz"))
2517 .build()
2518 .unwrap();
2519 let mut cfg = make_config(&server.url(""));
2520 cfg.compression = true;
2521
2522 let resp: RestApiResponse<Dummy> = http_request(req, &cfg).await.unwrap();
2523 assert_eq!(resp.status, 200);
2524 let data = resp.data().await.unwrap();
2525 assert_eq!(data, Dummy { foo: "baz".into() });
2526 mock.assert();
2527 });
2528 }
2529
2530 #[test]
2531 fn http_request_with_zero_retries_returns_transport_error() {
2532 TOKIO_SHARED_RT.block_on(async {
2533 let client = Client::builder()
2534 .proxy(reqwest::Proxy::all("http://127.0.0.1:9").unwrap())
2535 .build()
2536 .unwrap();
2537 let req = client
2538 .request(Method::GET, "http://example.test/unreachable")
2539 .build()
2540 .unwrap();
2541 let mut cfg = make_config("http://example.test");
2542 cfg.client = client;
2543 cfg.retries = 0;
2544
2545 let Err(err) = http_request::<Dummy>(req, &cfg).await else {
2546 panic!("request unexpectedly succeeded")
2547 };
2548
2549 let msg = err.to_string();
2550 assert!(
2551 msg.contains("HTTP request failed"),
2552 "missing top-level marker in: {msg}"
2553 );
2554 assert!(
2555 msg.contains("tcp connect error") || msg.contains("Connection refused"),
2556 "underlying transport cause was not surfaced in: {msg}"
2557 );
2558 });
2559 }
2560
2561 #[test]
2562 fn http_request_client_error_bad_request() {
2563 TOKIO_SHARED_RT.block_on(async {
2564 let server = MockServer::start();
2565 let mock = server.mock(|when, then| {
2566 when.method(httpmock::Method::GET).path("/400");
2567 then.status(400)
2568 .header("Content-Type", "application/json")
2569 .body(r#"{"code":-1121,"msg":"bad request"}"#);
2570 });
2571
2572 let client = Client::new();
2573 let req: Request = client
2574 .request(Method::GET, format!("{}{}", server.url(""), "/400"))
2575 .build()
2576 .unwrap();
2577 let cfg = make_config(&server.url(""));
2578
2579 let result = http_request::<Dummy>(req, &cfg).await;
2580
2581 assert!(matches!(
2582 result,
2583 Err(ConnectorError::BadRequestError { .. })
2584 ));
2585
2586 if let Err(ConnectorError::BadRequestError { msg, code }) = result {
2587 assert_eq!(msg, "bad request");
2588 assert_eq!(code, Some(-1121));
2589 }
2590
2591 mock.assert();
2592 });
2593 }
2594
2595 #[test]
2596 fn http_request_client_error_unauthorized() {
2597 TOKIO_SHARED_RT.block_on(async {
2598 let server = MockServer::start();
2599 let mock = server.mock(|when, then| {
2600 when.method(httpmock::Method::GET).path("/401");
2601 then.status(401)
2602 .header("Content-Type", "application/json")
2603 .body(r#"{"code":-2015,"msg":"unauthorized"}"#);
2604 });
2605
2606 let client = Client::new();
2607 let req: Request = client
2608 .request(Method::GET, format!("{}{}", server.url(""), "/401"))
2609 .build()
2610 .unwrap();
2611 let cfg = make_config(&server.url(""));
2612
2613 let result = http_request::<Dummy>(req, &cfg).await;
2614
2615 assert!(matches!(
2616 result,
2617 Err(ConnectorError::UnauthorizedError { .. })
2618 ));
2619
2620 if let Err(ConnectorError::UnauthorizedError { msg, code }) = result {
2621 assert_eq!(msg, "unauthorized");
2622 assert_eq!(code, Some(-2015));
2623 }
2624
2625 mock.assert();
2626 });
2627 }
2628
2629 #[test]
2630 fn http_request_client_error_forbidden() {
2631 TOKIO_SHARED_RT.block_on(async {
2632 let server = MockServer::start();
2633 let mock = server.mock(|when, then| {
2634 when.method(httpmock::Method::GET).path("/403");
2635 then.status(403)
2636 .header("Content-Type", "application/json")
2637 .body(r#"{"code":-2010,"msg":"forbidden"}"#);
2638 });
2639
2640 let client = Client::new();
2641 let req: Request = client
2642 .request(Method::GET, format!("{}{}", server.url(""), "/403"))
2643 .build()
2644 .unwrap();
2645 let cfg = make_config(&server.url(""));
2646
2647 let result = http_request::<Dummy>(req, &cfg).await;
2648
2649 assert!(matches!(result, Err(ConnectorError::ForbiddenError { .. })));
2650
2651 if let Err(ConnectorError::ForbiddenError { msg, code }) = result {
2652 assert_eq!(msg, "forbidden");
2653 assert_eq!(code, Some(-2010));
2654 }
2655
2656 mock.assert();
2657 });
2658 }
2659
2660 #[test]
2661 fn http_request_client_error_not_found() {
2662 TOKIO_SHARED_RT.block_on(async {
2663 let server = MockServer::start();
2664 let mock = server.mock(|when, then| {
2665 when.method(httpmock::Method::GET).path("/404");
2666 then.status(404)
2667 .header("Content-Type", "application/json")
2668 .body(r#"{"code":-1003,"msg":"not found"}"#);
2669 });
2670
2671 let client = Client::new();
2672 let req: Request = client
2673 .request(Method::GET, format!("{}{}", server.url(""), "/404"))
2674 .build()
2675 .unwrap();
2676 let cfg = make_config(&server.url(""));
2677
2678 let result = http_request::<Dummy>(req, &cfg).await;
2679
2680 assert!(matches!(result, Err(ConnectorError::NotFoundError { .. })));
2681
2682 if let Err(ConnectorError::NotFoundError { msg, code }) = result {
2683 assert_eq!(msg, "not found");
2684 assert_eq!(code, Some(-1003));
2685 }
2686
2687 mock.assert();
2688 });
2689 }
2690
2691 #[test]
2692 fn http_request_client_error_rate_limit_exceeded() {
2693 TOKIO_SHARED_RT.block_on(async {
2694 let server = MockServer::start();
2695 let mock = server.mock(|when, then| {
2696 when.method(httpmock::Method::GET).path("/418");
2697 then.status(418)
2698 .header("Content-Type", "application/json")
2699 .body(r#"{"code":-1003,"msg":"rate limit exceeded"}"#);
2700 });
2701
2702 let client = Client::new();
2703 let req: Request = client
2704 .request(Method::GET, format!("{}{}", server.url(""), "/418"))
2705 .build()
2706 .unwrap();
2707 let cfg = make_config(&server.url(""));
2708
2709 let result = http_request::<Dummy>(req, &cfg).await;
2710
2711 assert!(matches!(
2712 result,
2713 Err(ConnectorError::RateLimitBanError { .. })
2714 ));
2715
2716 if let Err(ConnectorError::RateLimitBanError { msg, code }) = result {
2717 assert_eq!(msg, "rate limit exceeded");
2718 assert_eq!(code, Some(-1003));
2719 }
2720
2721 mock.assert();
2722 });
2723 }
2724
2725 #[test]
2726 fn http_request_client_error_too_many_requests() {
2727 TOKIO_SHARED_RT.block_on(async {
2728 let server = MockServer::start();
2729 let mock = server.mock(|when, then| {
2730 when.method(httpmock::Method::GET).path("/429");
2731 then.status(429)
2732 .header("Content-Type", "application/json")
2733 .body(r#"{"code":-1003,"msg":"too many requests"}"#);
2734 });
2735
2736 let client = Client::new();
2737 let req: Request = client
2738 .request(Method::GET, format!("{}{}", server.url(""), "/429"))
2739 .build()
2740 .unwrap();
2741 let cfg = make_config(&server.url(""));
2742
2743 let result = http_request::<Dummy>(req, &cfg).await;
2744
2745 assert!(matches!(
2746 result,
2747 Err(ConnectorError::TooManyRequestsError { .. })
2748 ));
2749
2750 if let Err(ConnectorError::TooManyRequestsError { msg, code }) = result {
2751 assert_eq!(msg, "too many requests");
2752 assert_eq!(code, Some(-1003));
2753 }
2754
2755 mock.assert();
2756 });
2757 }
2758
2759 #[test]
2760 fn http_request_client_error_server_error() {
2761 TOKIO_SHARED_RT.block_on(async {
2762 let server = MockServer::start();
2763 let mock = server.mock(|when, then| {
2764 when.method(httpmock::Method::GET).path("/500");
2765 then.status(500)
2766 .header("Content-Type", "application/json")
2767 .body(r#"{"code":-1000,"msg":"internal server error"}"#);
2768 });
2769
2770 let client = Client::new();
2771 let req: Request = client
2772 .request(Method::GET, format!("{}{}", server.url(""), "/500"))
2773 .build()
2774 .unwrap();
2775 let cfg = make_config(&server.url(""));
2776
2777 let result = http_request::<Dummy>(req, &cfg).await;
2778
2779 assert!(matches!(result, Err(ConnectorError::ServerError { .. })));
2780
2781 if let Err(ConnectorError::ServerError {
2782 msg,
2783 status_code: Some(500),
2784 }) = result
2785 {
2786 assert_eq!(msg, "Server error: 500".to_string());
2787 }
2788
2789 mock.assert();
2790 });
2791 }
2792
2793 #[test]
2794 fn http_request_unexpected_status_maps_generic() {
2795 TOKIO_SHARED_RT.block_on(async {
2796 let server = MockServer::start();
2797 let code_http = 402;
2798 let mock = server.mock(|when, then| {
2799 when.method(httpmock::Method::GET).path("/402");
2800 then.status(code_http)
2801 .header("Content-Type", "application/json")
2802 .body(r#"{"code":-12345,"msg":"payment required"}"#);
2803 });
2804
2805 let client = Client::new();
2806 let req: Request = client
2807 .request(Method::GET, format!("{}{}", server.url(""), "/402"))
2808 .build()
2809 .unwrap();
2810 let cfg = make_config(&server.url(""));
2811
2812 let result = http_request::<Dummy>(req, &cfg).await;
2813
2814 assert!(matches!(
2815 result,
2816 Err(ConnectorError::ConnectorClientError { .. })
2817 ));
2818
2819 if let Err(ConnectorError::ConnectorClientError { msg, code }) = result {
2820 assert_eq!(msg, "payment required");
2821 assert_eq!(code, Some(-12345));
2822 }
2823
2824 mock.assert();
2825 });
2826 }
2827
2828 #[test]
2829 fn http_request_malformed_json_maps_generic() {
2830 TOKIO_SHARED_RT.block_on(async {
2831 let server = MockServer::start();
2832 let mock = server.mock(|when, then| {
2833 when.method(httpmock::Method::GET).path("/malformed");
2834 then.status(200)
2835 .header("Content-Type", "application/json")
2836 .body("not json");
2837 });
2838
2839 let client = Client::new();
2840 let req: Request = client
2841 .request(Method::GET, format!("{}{}", server.url(""), "/malformed"))
2842 .build()
2843 .unwrap();
2844 let cfg = make_config(&server.url(""));
2845
2846 let resp = http_request::<Dummy>(req, &cfg)
2847 .await
2848 .expect("http_request should succeed even if JSON is bad");
2849
2850 let err = resp
2851 .data()
2852 .await
2853 .expect_err("malformed JSON should turn into ConnectorClientError");
2854
2855 assert!(matches!(err, ConnectorError::ConnectorClientError { .. }));
2856
2857 if let ConnectorError::ConnectorClientError { msg: _, code } = err {
2858 assert_eq!(code, None);
2859 }
2860
2861 mock.assert();
2862 });
2863 }
2864 }
2865
2866 mod send_request {
2867 use anyhow::Result;
2868 use httpmock::prelude::*;
2869 use reqwest::Method;
2870 use serde::Deserialize;
2871 use serde_json::json;
2872 use std::collections::{BTreeMap, HashMap};
2873
2874 use crate::{
2875 common::{models::TimeUnit, utils::send_request},
2876 config::ConfigurationRestApi,
2877 };
2878
2879 use super::TOKIO_SHARED_RT;
2880
2881 #[derive(Deserialize, Debug, PartialEq)]
2882 struct TestResponse {
2883 message: String,
2884 }
2885
2886 #[test]
2887 fn basic_get_request() -> Result<()> {
2888 TOKIO_SHARED_RT.block_on(async {
2889 let server = MockServer::start();
2890
2891 server.mock(|when, then| {
2892 when.method(GET).path("/api/v1/test");
2893 then.status(200)
2894 .header("content-type", "application/json")
2895 .body(r#"{"message": "success"}"#);
2896 });
2897
2898 let configuration = ConfigurationRestApi::builder()
2899 .api_key("key")
2900 .api_secret("secret")
2901 .base_path(server.base_url())
2902 .compression(false)
2903 .build()
2904 .expect("Failed to build configuration");
2905
2906 let params = BTreeMap::new();
2907
2908 let result = send_request::<TestResponse>(
2909 &configuration,
2910 "/api/v1/test",
2911 Method::GET,
2912 params,
2913 BTreeMap::new(),
2914 None,
2915 false,
2916 )
2917 .await?;
2918
2919 let data = result.data().await.unwrap();
2920 assert_eq!(data.message, "success");
2921
2922 Ok(())
2923 })
2924 }
2925
2926 #[test]
2927 fn signed_post_request() -> Result<()> {
2928 TOKIO_SHARED_RT.block_on(async {
2929 let server = MockServer::start();
2930
2931 server.mock(|when, then| {
2932 when.method(POST).path("/api/v3/order");
2933 then.status(200)
2934 .header("content-type", "application/json")
2935 .body(r#"{"message": "order placed"}"#);
2936 });
2937
2938 let configuration = ConfigurationRestApi::builder()
2939 .api_key("key")
2940 .api_secret("secret")
2941 .base_path(server.base_url())
2942 .compression(false)
2943 .build()
2944 .expect("Failed to build configuration");
2945
2946 let mut params = BTreeMap::new();
2947 params.insert("symbol".to_string(), json!("ETHUSDT"));
2948 params.insert("side".to_string(), json!("BUY"));
2949 params.insert("type".to_string(), json!("MARKET"));
2950 params.insert("quantity".to_string(), json!("1"));
2951
2952 let result = send_request::<TestResponse>(
2953 &configuration,
2954 "/api/v3/order",
2955 Method::POST,
2956 params,
2957 BTreeMap::new(),
2958 None,
2959 true,
2960 )
2961 .await?;
2962
2963 let data = result.data().await.unwrap();
2964 assert_eq!(data.message, "order placed");
2965
2966 Ok(())
2967 })
2968 }
2969
2970 #[test]
2971 fn signed_post_request_with_body() -> Result<()> {
2972 TOKIO_SHARED_RT.block_on(async {
2973 let server = MockServer::start();
2974
2975 server.mock(|when, then| {
2976 when.method(POST).path("/api/v3/order");
2977 then.status(200)
2978 .header("content-type", "application/json")
2979 .body(r#"{"message": "order placed"}"#);
2980 });
2981
2982 let configuration = ConfigurationRestApi::builder()
2983 .api_key("key")
2984 .api_secret("secret")
2985 .base_path(server.base_url())
2986 .compression(false)
2987 .build()
2988 .expect("Failed to build configuration");
2989
2990 let mut query_params = BTreeMap::new();
2991 query_params.insert("symbol".to_string(), json!("ETHUSDT"));
2992
2993 let mut body_params = BTreeMap::new();
2994 body_params.insert("side".to_string(), json!("BUY"));
2995 body_params.insert("type".to_string(), json!("MARKET"));
2996 body_params.insert("quantity".to_string(), json!("1"));
2997
2998 let result = send_request::<TestResponse>(
2999 &configuration,
3000 "/api/v3/order",
3001 Method::POST,
3002 query_params,
3003 body_params,
3004 None,
3005 true,
3006 )
3007 .await?;
3008
3009 let data = result.data().await.unwrap();
3010 assert_eq!(data.message, "order placed");
3011
3012 Ok(())
3013 })
3014 }
3015
3016 #[test]
3017 fn get_request_with_params() -> Result<()> {
3018 TOKIO_SHARED_RT.block_on(async {
3019 let server = MockServer::start();
3020
3021 server.mock(|when, then| {
3022 when.method(GET)
3023 .path("/api/v1/data")
3024 .query_param("symbol", "BTCUSDT")
3025 .query_param("limit", "10");
3026 then.status(200)
3027 .header("content-type", "application/json")
3028 .body(r#"{"message": "data retrieved"}"#);
3029 });
3030
3031 let configuration = ConfigurationRestApi::builder()
3032 .api_key("key")
3033 .api_secret("secret")
3034 .base_path(server.base_url())
3035 .compression(false)
3036 .build()
3037 .expect("Failed to build configuration");
3038
3039 let mut params = BTreeMap::new();
3040 params.insert("symbol".to_string(), json!("BTCUSDT"));
3041 params.insert("limit".to_string(), json!(10));
3042
3043 let result = send_request::<TestResponse>(
3044 &configuration,
3045 "/api/v1/data",
3046 Method::GET,
3047 params,
3048 BTreeMap::new(),
3049 None,
3050 false,
3051 )
3052 .await?;
3053
3054 let data = result.data().await.unwrap();
3055 assert_eq!(data.message, "data retrieved");
3056
3057 Ok(())
3058 })
3059 }
3060
3061 #[test]
3062 fn invalid_endpoint() {
3063 TOKIO_SHARED_RT.block_on(async {
3064 let server = MockServer::start();
3065
3066 let configuration = ConfigurationRestApi::builder()
3067 .api_key("key")
3068 .api_secret("secret")
3069 .base_path(server.base_url())
3070 .compression(false)
3071 .build()
3072 .expect("Failed to build configuration");
3073
3074 let params = BTreeMap::new();
3075
3076 let result = send_request::<TestResponse>(
3077 &configuration,
3078 "http://invalid",
3079 Method::GET,
3080 params,
3081 BTreeMap::new(),
3082 None,
3083 false,
3084 )
3085 .await;
3086
3087 assert!(result.is_err());
3088 });
3089 }
3090
3091 #[test]
3092 fn missing_signature_on_signed_request() {
3093 TOKIO_SHARED_RT.block_on(async {
3094 let server = MockServer::start();
3095
3096 let configuration = ConfigurationRestApi::builder()
3097 .api_key("key")
3098 .api_secret("secret")
3099 .base_path(server.base_url())
3100 .compression(false)
3101 .build()
3102 .expect("Failed to build configuration");
3103
3104 let mut params = BTreeMap::new();
3105 params.insert("symbol".to_string(), json!("BTCUSDT"));
3106 params.insert("side".to_string(), json!("BUY"));
3107
3108 let result = send_request::<TestResponse>(
3109 &configuration,
3110 "/api/v3/order",
3111 Method::POST,
3112 params,
3113 BTreeMap::new(),
3114 None,
3115 true,
3116 )
3117 .await;
3118
3119 assert!(result.is_err());
3120 });
3121 }
3122
3123 #[test]
3124 fn compression_enabled() -> Result<()> {
3125 TOKIO_SHARED_RT.block_on(async {
3126 let server = MockServer::start();
3127
3128 server.mock(|when, then| {
3129 when.method(GET).path("/api/v1/test");
3130 then.status(200)
3131 .header("content-type", "application/json")
3132 .header("accept-encoding", "gzip, deflate, br")
3133 .body(r#"{"message": "compression enabled"}"#);
3134 });
3135
3136 let configuration = ConfigurationRestApi::builder()
3137 .api_key("key")
3138 .api_secret("secret")
3139 .base_path(server.base_url())
3140 .compression(true)
3141 .build()
3142 .expect("Failed to build configuration");
3143
3144 let params = BTreeMap::new();
3145
3146 let result = send_request::<TestResponse>(
3147 &configuration,
3148 "/api/v1/test",
3149 Method::GET,
3150 params,
3151 BTreeMap::new(),
3152 None,
3153 false,
3154 )
3155 .await?;
3156
3157 let data = result.data().await.unwrap();
3158 assert_eq!(data.message, "compression enabled");
3159
3160 Ok(())
3161 })
3162 }
3163
3164 #[test]
3165 fn get_request_with_time_unit_header() -> Result<()> {
3166 TOKIO_SHARED_RT.block_on(async {
3167 let server = MockServer::start();
3168
3169 server.mock(|when, then| {
3170 when.method(GET)
3171 .path("/api/v1/test")
3172 .header("X-MBX-TIME-UNIT", "MILLISECOND");
3173 then.status(200)
3174 .header("content-type", "application/json")
3175 .body(r#"{"message": "time unit applied"}"#);
3176 });
3177
3178 let configuration = ConfigurationRestApi::builder()
3179 .api_key("key")
3180 .api_secret("secret")
3181 .base_path(server.base_url())
3182 .compression(false)
3183 .time_unit(TimeUnit::Millisecond)
3184 .build()
3185 .expect("Failed to build configuration");
3186
3187 let params = BTreeMap::new();
3188
3189 let result = send_request::<TestResponse>(
3190 &configuration,
3191 "/api/v1/test",
3192 Method::GET,
3193 params,
3194 BTreeMap::new(),
3195 Some(TimeUnit::Millisecond),
3196 false,
3197 )
3198 .await?;
3199
3200 let data = result.data().await.unwrap();
3201 assert_eq!(data.message, "time unit applied");
3202
3203 Ok(())
3204 })
3205 }
3206
3207 #[test]
3208 fn custom_headers_are_sent() -> Result<()> {
3209 TOKIO_SHARED_RT.block_on(async {
3210 let server = MockServer::start();
3211
3212 server.mock(|when, then| {
3213 when.method(GET)
3214 .path("/api/v1/test")
3215 .header("X-My-Test", "all-clear");
3216 then.status(200)
3217 .header("content-type", "application/json")
3218 .body(r#"{"message":"ok"}"#);
3219 });
3220
3221 let mut custom = HashMap::new();
3222 custom.insert("X-My-Test".to_string(), "all-clear".to_string());
3223
3224 let configuration = ConfigurationRestApi::builder()
3225 .api_key("key")
3226 .api_secret("secret")
3227 .base_path(server.base_url())
3228 .compression(false)
3229 .custom_headers(custom)
3230 .build()
3231 .expect("Failed to build configuration");
3232
3233 let params = BTreeMap::new();
3234 let res = send_request::<TestResponse>(
3235 &configuration,
3236 "/api/v1/test",
3237 Method::GET,
3238 params,
3239 BTreeMap::new(),
3240 None,
3241 false,
3242 )
3243 .await?;
3244
3245 let data = res.data().await.unwrap();
3246 assert_eq!(data.message, "ok");
3247
3248 Ok(())
3249 })
3250 }
3251
3252 #[test]
3253 fn custom_header_override_prevention() -> Result<()> {
3254 TOKIO_SHARED_RT.block_on(async {
3255 let server = MockServer::start();
3256
3257 server.mock(|when, then| {
3258 when.method(GET)
3259 .path("/api/v1/test")
3260 .header("content-type", "application/json")
3261 .header("x-mbx-apikey", "key")
3262 .header("X-My-Test", "ok");
3263 then.status(200)
3264 .header("content-type", "application/json")
3265 .body(r#"{"message":"defaults intact"}"#);
3266 });
3267
3268 let mut custom = HashMap::new();
3269 custom.insert("Content-Type".to_string(), "text/plain".to_string());
3270 custom.insert("X-MBX-APIKEY".to_string(), "BAD".to_string());
3271 custom.insert("X-My-Test".to_string(), "ok".to_string());
3272
3273 let configuration = ConfigurationRestApi::builder()
3274 .api_key("key")
3275 .api_secret("secret")
3276 .base_path(server.base_url())
3277 .compression(false)
3278 .custom_headers(custom)
3279 .build()
3280 .expect("Failed to build configuration");
3281
3282 let params = BTreeMap::new();
3283 let res = send_request::<TestResponse>(
3284 &configuration,
3285 "/api/v1/test",
3286 Method::GET,
3287 params,
3288 BTreeMap::new(),
3289 None,
3290 false,
3291 )
3292 .await?;
3293
3294 let data = res.data().await.unwrap();
3295 assert_eq!(data.message, "defaults intact");
3296
3297 Ok(())
3298 })
3299 }
3300
3301 #[test]
3302 fn crlf_in_header_values_are_dropped() -> Result<()> {
3303 TOKIO_SHARED_RT.block_on(async {
3304 let server = MockServer::start();
3305
3306 server.mock(|when, then| {
3307 when.method(GET)
3308 .path("/api/v1/test")
3309 .header("X-Good", "safe");
3310 then.status(200)
3311 .header("content-type", "application/json")
3312 .body(r#"{"message":"clean only"}"#);
3313 });
3314
3315 let mut custom = HashMap::new();
3316 custom.insert("X-Bad".to_string(), "evil\r\ninject".to_string());
3317 custom.insert("X-Good".to_string(), "safe".to_string());
3318
3319 let configuration = ConfigurationRestApi::builder()
3320 .api_key("key")
3321 .api_secret("secret")
3322 .base_path(server.base_url())
3323 .compression(false)
3324 .custom_headers(custom)
3325 .build()
3326 .expect("Failed to build configuration");
3327
3328 let params = BTreeMap::new();
3329 let res = send_request::<TestResponse>(
3330 &configuration,
3331 "/api/v1/test",
3332 Method::GET,
3333 params,
3334 BTreeMap::new(),
3335 None,
3336 false,
3337 )
3338 .await?;
3339
3340 let data = res.data().await.unwrap();
3341 assert_eq!(data.message, "clean only");
3342
3343 Ok(())
3344 })
3345 }
3346 }
3347
3348 mod random_string {
3349 use crate::common::utils::random_string;
3350 use hex;
3351
3352 #[test]
3353 fn length_is_32() {
3354 let s = random_string();
3355 assert_eq!(
3356 s.len(),
3357 32,
3358 "random_string() should be 32 chars, got {}",
3359 s.len()
3360 );
3361 }
3362
3363 #[test]
3364 fn is_valid_lowercase_hex() {
3365 let s = random_string();
3366 assert!(
3367 s.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f')),
3368 "random_string() contains invalid hex characters: {s}"
3369 );
3370 }
3371
3372 #[test]
3373 fn decodes_to_16_bytes() {
3374 let s = random_string();
3375 let bytes = hex::decode(&s).expect("random_string() output must be valid hex");
3376 assert_eq!(
3377 bytes.len(),
3378 16,
3379 "hex::decode returned {} bytes",
3380 bytes.len()
3381 );
3382 }
3383
3384 #[test]
3385 fn two_calls_are_different() {
3386 let a = random_string();
3387 let b = random_string();
3388 assert_ne!(
3389 a, b,
3390 "Two calls to random_string() returned the same value: {a}"
3391 );
3392 }
3393 }
3394
3395 mod random_integer {
3396 use crate::common::utils::random_integer;
3397
3398 #[test]
3399 fn is_within_u32_range() {
3400 let n = random_integer();
3401 assert!(
3402 n <= u32::MAX,
3403 "random_integer() should be <= u32::MAX, got {n}"
3404 );
3405 }
3406
3407 #[test]
3408 fn two_calls_can_differ() {
3409 let a = random_integer();
3410 let b = random_integer();
3411 assert_ne!(
3412 a, b,
3413 "Two calls to random_integer() returned the same value: {a}"
3414 );
3415 }
3416 }
3417
3418 mod normalize_stream_id {
3419 use crate::common::utils::{StreamId, normalize_stream_id};
3420 use serde_json::Value;
3421
3422 fn is_lower_hex32(s: &str) -> bool {
3423 s.len() == 32 && s.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f'))
3424 }
3425
3426 #[test]
3427 fn valid_hex_string_is_kept() {
3428 let id = "0123456789abcdef0123456789abcdef".to_string();
3429 let out = normalize_stream_id(Some(StreamId::Str(id.clone())), false);
3430
3431 match out {
3432 Value::String(s) => assert_eq!(s, id, "Expected to keep the valid hex id"),
3433 other => panic!("Expected Value::String, got {other:?}"),
3434 }
3435 }
3436
3437 #[test]
3438 fn invalid_hex_string_generates_random_hex() {
3439 let id = "not-hex".to_string();
3440 let out = normalize_stream_id(Some(StreamId::Str(id.clone())), false);
3441
3442 match out {
3443 Value::String(s) => {
3444 assert_eq!(s.len(), 32, "Expected 32-char hex, got {}", s.len());
3445 assert_ne!(s, id, "Expected generated id to differ from input");
3446 assert!(
3447 is_lower_hex32(&s),
3448 "Generated id contains invalid hex characters: {s}"
3449 );
3450 }
3451 other => panic!("Expected Value::String, got {other:?}"),
3452 }
3453 }
3454
3455 #[test]
3456 fn none_generates_random_hex() {
3457 let out = normalize_stream_id(None, false);
3458
3459 match out {
3460 Value::String(s) => {
3461 assert_eq!(s.len(), 32, "Expected 32-char hex, got {}", s.len());
3462 assert!(
3463 is_lower_hex32(&s),
3464 "Generated id contains invalid hex characters: {s}"
3465 );
3466 }
3467 other => panic!("Expected Value::String, got {other:?}"),
3468 }
3469 }
3470
3471 #[test]
3472 fn number_is_kept_when_not_strict() {
3473 let out = normalize_stream_id(Some(StreamId::Number(42)), false);
3474
3475 match out {
3476 Value::Number(n) => {
3477 assert_eq!(n.as_u64(), Some(42), "Expected to keep the numeric id");
3478 }
3479 other => panic!("Expected Value::Number, got {other:?}"),
3480 }
3481 }
3482
3483 #[test]
3484 fn strict_number_forces_number_even_for_valid_hex_string() {
3485 let id = "0123456789abcdef0123456789abcdef".to_string();
3486 let out = normalize_stream_id(Some(StreamId::Str(id)), true);
3487
3488 match out {
3489 Value::Number(n) => {
3490 assert!(
3491 n.as_u64().is_some(),
3492 "Expected unsigned integer JSON number, got {n}"
3493 );
3494 }
3495 other => panic!("Expected Value::Number, got {other:?}"),
3496 }
3497 }
3498
3499 #[test]
3500 fn strict_number_keeps_number_if_provided() {
3501 let out = normalize_stream_id(Some(StreamId::Number(7)), true);
3502
3503 match out {
3504 Value::Number(n) => {
3505 assert_eq!(n.as_u64(), Some(7), "Expected to keep the numeric id");
3506 }
3507 other => panic!("Expected Value::Number, got {other:?}"),
3508 }
3509 }
3510
3511 #[test]
3512 fn strict_number_generates_number_when_none() {
3513 let out = normalize_stream_id(None, true);
3514
3515 match out {
3516 Value::Number(n) => {
3517 assert!(
3518 n.as_u64().is_some(),
3519 "Expected unsigned integer JSON number, got {n}"
3520 );
3521 }
3522 other => panic!("Expected Value::Number, got {other:?}"),
3523 }
3524 }
3525
3526 #[test]
3527 fn strict_number_generates_number_for_invalid_hex_string() {
3528 let out = normalize_stream_id(Some(StreamId::Str("nope".to_string())), true);
3529
3530 match out {
3531 Value::Number(n) => {
3532 assert!(
3533 n.as_u64().is_some(),
3534 "Expected unsigned integer JSON number, got {n}"
3535 );
3536 }
3537 other => panic!("Expected Value::Number, got {other:?}"),
3538 }
3539 }
3540 }
3541 mod remove_empty_value {
3542 use crate::common::utils::remove_empty_value;
3543 use serde_json::{Map, Value};
3544
3545 #[test]
3546 fn filters_out_null_and_empty_strings() {
3547 let entries = vec![
3548 ("key1".to_string(), Value::String("value1".to_string())),
3549 ("key2".to_string(), Value::Null),
3550 ("key3".to_string(), Value::String(String::new())),
3551 ];
3552 let result = remove_empty_value(entries);
3553 assert_eq!(
3554 result.len(),
3555 1,
3556 "expected only one entry, got {}",
3557 result.len()
3558 );
3559 assert_eq!(
3560 result.get("key1"),
3561 Some(&Value::String("value1".to_string()))
3562 );
3563 assert!(!result.contains_key("key2"));
3564 assert!(!result.contains_key("key3"));
3565 }
3566
3567 #[test]
3568 fn retains_other_value_types() {
3569 let entries = vec![
3570 ("bool".to_string(), Value::Bool(true)),
3571 ("num".to_string(), Value::Number(42.into())),
3572 ("arr".to_string(), Value::Array(vec![])),
3573 ("obj".to_string(), Value::Object(Map::default())),
3574 ("nil".to_string(), Value::Null),
3575 ("empty_str".to_string(), Value::String(String::new())),
3576 ];
3577 let result = remove_empty_value(entries);
3578 let keys: Vec<&String> = result.keys().collect();
3579 assert_eq!(keys.len(), 4, "expected 4 entries, got {}", keys.len());
3580 assert!(result.get("bool") == Some(&Value::Bool(true)));
3581 assert!(result.get("num") == Some(&Value::Number(42.into())));
3582 assert!(result.get("arr") == Some(&Value::Array(vec![])));
3583 assert!(result.get("obj") == Some(&Value::Object(Map::default())));
3584 assert!(!result.contains_key("nil"));
3585 assert!(!result.contains_key("empty_str"));
3586 }
3587
3588 #[test]
3589 fn empty_iterator_returns_empty_map() {
3590 let entries: Vec<(String, Value)> = vec![];
3591 let result = remove_empty_value(entries);
3592 assert!(result.is_empty(), "expected an empty map");
3593 }
3594
3595 #[test]
3596 fn keys_are_sorted() {
3597 let entries = vec![
3598 ("c".to_string(), Value::String("foo".to_string())),
3599 ("a".to_string(), Value::String("bar".to_string())),
3600 ("b".to_string(), Value::String("baz".to_string())),
3601 ];
3602 let result = remove_empty_value(entries);
3603 let sorted_keys: Vec<&String> = result.keys().collect();
3604 assert_eq!(
3605 sorted_keys,
3606 [&"a".to_string(), &"b".to_string(), &"c".to_string()]
3607 );
3608 }
3609 }
3610
3611 mod sort_object_params {
3612 use crate::common::utils::sort_object_params;
3613 use serde_json::Value;
3614 use std::collections::BTreeMap;
3615
3616 #[test]
3617 fn sorts_keys() {
3618 let mut params = BTreeMap::new();
3619 params.insert("z".to_string(), Value::String("last".to_string()));
3620 params.insert("a".to_string(), Value::String("first".to_string()));
3621 params.insert("m".to_string(), Value::String("middle".to_string()));
3622
3623 let sorted = sort_object_params(¶ms);
3624 let keys: Vec<&String> = sorted.keys().collect();
3625 assert_eq!(
3626 keys,
3627 [&"a".to_string(), &"m".to_string(), &"z".to_string()],
3628 "Keys should be sorted alphabetically"
3629 );
3630 }
3631
3632 #[test]
3633 fn preserves_values() {
3634 let mut params = BTreeMap::new();
3635 params.insert("one".to_string(), Value::Number(1.into()));
3636 params.insert("two".to_string(), Value::Bool(true));
3637
3638 let sorted = sort_object_params(¶ms);
3639 assert_eq!(sorted.get("one"), Some(&Value::Number(1.into())));
3640 assert_eq!(sorted.get("two"), Some(&Value::Bool(true)));
3641 }
3642
3643 #[test]
3644 fn empty_map_returns_empty() {
3645 let params: BTreeMap<String, Value> = BTreeMap::new();
3646 let sorted = sort_object_params(¶ms);
3647 assert!(sorted.is_empty(), "Expected empty map");
3648 }
3649
3650 #[test]
3651 fn independent_clone() {
3652 let mut params = BTreeMap::new();
3653 params.insert("key".to_string(), Value::String("val".to_string()));
3654
3655 let mut sorted = sort_object_params(¶ms);
3656 sorted.insert("new".to_string(), Value::String("x".to_string()));
3657
3658 assert!(
3659 !params.contains_key("new"),
3660 "Original should not be modified when changing sorted"
3661 );
3662 assert!(
3663 sorted.contains_key("new"),
3664 "Sorted map should reflect its own insertions"
3665 );
3666 }
3667 }
3668
3669 mod normalize_ws_streams_key {
3670 use crate::common::utils::normalize_ws_streams_key;
3671
3672 #[test]
3673 fn returns_empty_for_empty() {
3674 assert_eq!(normalize_ws_streams_key(""), "");
3675 }
3676
3677 #[test]
3678 fn already_normalized_stays_same() {
3679 assert_eq!(normalize_ws_streams_key("streamname"), "streamname");
3680 }
3681
3682 #[test]
3683 fn uppercases_are_lowercased() {
3684 assert_eq!(normalize_ws_streams_key("MyStream"), "mystream");
3685 }
3686
3687 #[test]
3688 fn underscores_are_removed() {
3689 assert_eq!(normalize_ws_streams_key("my_stream_name"), "mystreamname");
3690 }
3691
3692 #[test]
3693 fn hyphens_are_removed() {
3694 assert_eq!(normalize_ws_streams_key("my-stream-name"), "mystreamname");
3695 }
3696
3697 #[test]
3698 fn mixed_underscores_and_hyphens_and_case() {
3699 let input = "Mixed_Case-Stream_Name";
3700 let expected = "mixedcasestreamname";
3701 assert_eq!(normalize_ws_streams_key(input), expected);
3702 }
3703
3704 #[test]
3705 fn retains_other_punctuation() {
3706 assert_eq!(normalize_ws_streams_key("stream.name!"), "stream.name!");
3707 }
3708 }
3709
3710 mod replace_websocket_streams_placeholders {
3711 use crate::common::utils::replace_websocket_streams_placeholders;
3712 use std::collections::HashMap;
3713
3714 #[test]
3715 fn empty_string_unchanged() {
3716 let vars: HashMap<&str, &str> = HashMap::new();
3717 assert_eq!(replace_websocket_streams_placeholders("", &vars), "");
3718 }
3719
3720 #[test]
3721 fn unknown_placeholder_becomes_empty() {
3722 let vars: HashMap<&str, &str> = HashMap::new();
3723 assert_eq!(replace_websocket_streams_placeholders("<foo>", &vars), "");
3724 }
3725
3726 #[test]
3727 fn leading_slash_symbol_lowercases_head() {
3728 let mut vars = HashMap::new();
3729 vars.insert("symbol", "BTC");
3730 assert_eq!(
3731 replace_websocket_streams_placeholders("/<symbol>", &vars),
3732 "btc"
3733 );
3734 }
3735
3736 #[test]
3737 fn no_lowercase_without_slash() {
3738 let mut vars = HashMap::new();
3739 vars.insert("symbol", "BTC");
3740 assert_eq!(
3741 replace_websocket_streams_placeholders("<symbol>", &vars),
3742 "BTC"
3743 );
3744 }
3745
3746 #[test]
3747 fn multiple_placeholders_mid_preserve_ats() {
3748 let mut vars = HashMap::new();
3749 vars.insert("symbol", "BNBUSDT");
3750 vars.insert("levels", "10");
3751 vars.insert("updateSpeed", "1000ms");
3752 let out = replace_websocket_streams_placeholders(
3753 "/<symbol>@depth<levels>@<updateSpeed>",
3754 &vars,
3755 );
3756 assert_eq!(out, "bnbusdt@depth10@1000ms");
3757 }
3758
3759 #[test]
3760 fn trailing_at_removed_when_missing_var() {
3761 let mut vars = HashMap::new();
3762 vars.insert("symbol", "BNBUSDT");
3763 vars.insert("levels", "10");
3764 let out = replace_websocket_streams_placeholders(
3765 "/<symbol>@depth<levels>@<updateSpeed>",
3766 &vars,
3767 );
3768 assert_eq!(out, "bnbusdt@depth10");
3769 }
3770
3771 #[test]
3772 fn custom_key_normalization_and_value() {
3773 let mut vars = HashMap::new();
3774 vars.insert("my-stream_key", "Value");
3775 assert_eq!(
3776 replace_websocket_streams_placeholders("<My_Stream-Key>", &vars),
3777 "Value"
3778 );
3779 }
3780
3781 #[test]
3782 fn text_surrounding_placeholders_intact() {
3783 let mut vars = HashMap::new();
3784 vars.insert("symbol", "ABC");
3785 let input = "pre-<symbol>-post";
3786 assert_eq!(
3787 replace_websocket_streams_placeholders(input, &vars),
3788 "pre-ABC-post"
3789 );
3790 }
3791 }
3792
3793 mod build_websocket_api_message {
3794 use serde_json::{Value, json};
3795 use std::collections::BTreeMap;
3796
3797 use crate::{
3798 common::{
3799 utils::{ID_REGEX, build_websocket_api_message, remove_empty_value},
3800 websocket::WebsocketMessageSendOptions,
3801 },
3802 config::ConfigurationWebsocketApi,
3803 };
3804
3805 fn make_config() -> ConfigurationWebsocketApi {
3806 ConfigurationWebsocketApi::builder()
3807 .api_key("api-key".to_string())
3808 .api_secret("api-secret".to_string())
3809 .build()
3810 .unwrap()
3811 }
3812
3813 #[test]
3814 fn no_auth_or_sign_with_skip_auth() {
3815 let mut payload = BTreeMap::new();
3816 payload.insert("foo".into(), Value::String("bar".into()));
3817 let cfg = make_config();
3818
3819 let (id, req) = build_websocket_api_message(
3820 &cfg,
3821 "method",
3822 payload.clone(),
3823 &WebsocketMessageSendOptions {
3824 with_api_key: true,
3825 is_signed: true,
3826 ..Default::default()
3827 },
3828 true,
3829 );
3830
3831 assert!(ID_REGEX.is_match(&id));
3832 assert_eq!(req["method"], "method");
3833 assert_eq!(req["params"]["foo"], "bar");
3834 assert!(req["params"].get("apiKey").is_none());
3835 assert!(req["params"].get("signature").is_none());
3836 assert!(req["params"]["timestamp"].is_number());
3837 }
3838
3839 #[test]
3840 fn only_api_key_when_not_signed() {
3841 let cfg = make_config();
3842
3843 let (id, req) = build_websocket_api_message(
3844 &cfg,
3845 "method",
3846 BTreeMap::new(),
3847 &WebsocketMessageSendOptions {
3848 with_api_key: true,
3849 is_signed: false,
3850 ..Default::default()
3851 },
3852 false,
3853 );
3854
3855 assert!(ID_REGEX.is_match(&id));
3856 assert_eq!(req["method"], "method");
3857 assert_eq!(req["params"]["apiKey"], "api-key");
3858 assert!(req["params"].get("timestamp").is_none());
3859 assert!(req["params"].get("signature").is_none());
3860 }
3861
3862 #[test]
3863 fn signed_includes_timestamp_and_signature() {
3864 let mut payload = BTreeMap::new();
3865 payload.insert("foo".into(), Value::String("bar".into()));
3866 let cfg = make_config();
3867
3868 let (id, req) = build_websocket_api_message(
3869 &cfg,
3870 "method",
3871 payload.clone(),
3872 &WebsocketMessageSendOptions {
3873 with_api_key: true,
3874 is_signed: true,
3875 ..Default::default()
3876 },
3877 false,
3878 );
3879
3880 assert!(ID_REGEX.is_match(&id));
3881 assert_eq!(req["method"], "method");
3882
3883 let params = &req["params"];
3884 assert_eq!(params["apiKey"], "api-key");
3885
3886 let timestamp = params["timestamp"].as_i64().unwrap();
3887 assert!(timestamp > 0, "timestamp should not be empty");
3888
3889 let sig = params["signature"].as_str().unwrap();
3890 assert!(!sig.is_empty(), "signature should not be empty");
3891 }
3892
3893 #[test]
3894 fn signed_non_ascii_symbol_is_not_percent_encoded_in_signature() {
3895 use hmac::{Hmac, Mac};
3896 use sha2::Sha256;
3897
3898 let mut payload = BTreeMap::new();
3899 payload.insert("symbol".into(), Value::String("我踏马来了USDT".into()));
3900 let cfg = make_config();
3901
3902 let (_id, req) = build_websocket_api_message(
3903 &cfg,
3904 "method",
3905 payload.clone(),
3906 &WebsocketMessageSendOptions {
3907 with_api_key: true,
3908 is_signed: true,
3909 ..Default::default()
3910 },
3911 false,
3912 );
3913
3914 let params = &req["params"];
3915 let timestamp = params["timestamp"].as_i64().unwrap();
3916 let sig = params["signature"].as_str().unwrap();
3917
3918 let expected_payload =
3919 format!("apiKey=api-key&symbol=我踏马来了USDT×tamp={timestamp}");
3920 let mut mac = Hmac::<Sha256>::new_from_slice(b"api-secret").unwrap();
3921 mac.update(expected_payload.as_bytes());
3922 let expected_sig = hex::encode(mac.finalize().into_bytes());
3923
3924 assert_eq!(sig, expected_sig);
3925 }
3926
3927 #[test]
3928 fn respects_provided_valid_id_and_removes_from_params() {
3929 let mut payload = BTreeMap::new();
3930 let custom = "0123456789abcdef0123456789abcdef".to_string();
3931 payload.insert("id".into(), Value::String(custom.clone()));
3932 payload.insert("foo".into(), Value::Number(123.into()));
3933
3934 let cfg = make_config();
3935 let (id, req) = build_websocket_api_message(
3936 &cfg,
3937 "method",
3938 payload.clone(),
3939 &WebsocketMessageSendOptions::default(),
3940 true,
3941 );
3942
3943 assert_eq!(id, custom);
3944 assert!(req["params"].get("id").is_none());
3945 assert_eq!(req["params"]["foo"], 123);
3946 }
3947
3948 #[test]
3949 fn skip_auth_blocks_api_and_signature_but_keeps_timestamp() {
3950 let mut payload = BTreeMap::new();
3951 payload.insert("foo".into(), Value::String("bar".into()));
3952 let cfg = make_config();
3953
3954 let (_id, req) = build_websocket_api_message(
3955 &cfg,
3956 "method",
3957 payload.clone(),
3958 &WebsocketMessageSendOptions {
3959 with_api_key: true,
3960 is_signed: true,
3961 ..Default::default()
3962 },
3963 true,
3964 );
3965
3966 let p = &req["params"];
3967 assert_eq!(p["foo"], "bar");
3968 assert!(p.get("apiKey").is_none());
3969 assert!(p.get("signature").is_none());
3970 assert!(p["timestamp"].is_number());
3971 }
3972
3973 #[test]
3974 fn random_id_changes_each_call() {
3975 let cfg = make_config();
3976 let (id1, _) = build_websocket_api_message(
3977 &cfg,
3978 "method",
3979 BTreeMap::new(),
3980 &WebsocketMessageSendOptions::default(),
3981 true,
3982 );
3983 let (id2, _) = build_websocket_api_message(
3984 &cfg,
3985 "method",
3986 BTreeMap::new(),
3987 &WebsocketMessageSendOptions::default(),
3988 true,
3989 );
3990 assert!(ID_REGEX.is_match(&id1));
3991 assert!(ID_REGEX.is_match(&id2));
3992 assert_ne!(id1, id2, "IDs should be random and not equal");
3993 }
3994
3995 #[test]
3996 fn null_and_empty_values_are_stripped() {
3997 let mut payload = BTreeMap::new();
3998 payload.insert("a".into(), Value::Null);
3999 payload.insert("b".into(), Value::String(String::new()));
4000 payload.insert("c".into(), Value::String("ok".into()));
4001
4002 let cleaned = remove_empty_value(payload.clone());
4003 assert!(!cleaned.contains_key("a"), "Null should be stripped");
4004 assert!(
4005 !cleaned.contains_key("b"),
4006 "Empty string should be stripped"
4007 );
4008 assert!(cleaned.contains_key("c"), "Non-empty string should be kept");
4009
4010 let cfg = make_config();
4011 let (_id, req) = build_websocket_api_message(
4012 &cfg,
4013 "method",
4014 payload,
4015 &WebsocketMessageSendOptions::default(),
4016 true,
4017 );
4018 let params = &req["params"];
4019 assert!(params.get("a").is_none(), "`a` should not appear");
4020 assert!(params.get("b").is_none(), "`b` should not appear");
4021 assert_eq!(params["c"], "ok", "`c` should be present with value \"ok\"");
4022 }
4023
4024 #[test]
4025 fn provided_invalid_id_gets_replaced() {
4026 let mut payload = BTreeMap::new();
4027 payload.insert("id".into(), Value::String("not-hex-32-chars".into()));
4028 let cfg = make_config();
4029 let (id, _req) = build_websocket_api_message(
4030 &cfg,
4031 "method",
4032 payload,
4033 &WebsocketMessageSendOptions::default(),
4034 true,
4035 );
4036
4037 assert!(ID_REGEX.is_match(&id));
4038 assert_ne!(id, "not-hex-32-chars");
4039 }
4040
4041 #[test]
4042 fn sign_only_includes_api_key_even_when_with_api_key_false() {
4043 let mut payload = BTreeMap::new();
4044 payload.insert("x".into(), json!(1));
4045
4046 let cfg = make_config();
4047 let (_id, req) = build_websocket_api_message(
4048 &cfg,
4049 "method",
4050 payload,
4051 &WebsocketMessageSendOptions {
4052 with_api_key: false,
4053 is_signed: true,
4054 ..Default::default()
4055 },
4056 false,
4057 );
4058 let params = &req["params"];
4059
4060 assert_eq!(params["apiKey"], "api-key");
4061 assert!(params["timestamp"].is_number());
4062 assert!(params["signature"].is_string());
4063 }
4064
4065 #[test]
4066 fn skip_auth_false_without_any_auth_flags() {
4067 let cfg = make_config();
4068 let (_id, req) = build_websocket_api_message(
4069 &cfg,
4070 "method",
4071 BTreeMap::new(),
4072 &WebsocketMessageSendOptions {
4073 with_api_key: false,
4074 is_signed: false,
4075 ..Default::default()
4076 },
4077 false,
4078 );
4079 let params = &req["params"];
4080 assert!(params.as_object().unwrap().is_empty());
4081 }
4082 }
4083}