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 if self.private_key.is_none() {
263 if let Some(secret) = self.api_secret.as_ref() {
264 let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
265 .context("HMAC key initialization failed")?;
266 mac.update(params.as_bytes());
267 let result = mac.finalize().into_bytes();
268 return Ok(hex::encode(result));
269 }
270 }
271
272 if self.private_key.is_some() {
273 #[cfg(feature = "openssl-tls")]
274 {
275 let key_obj = self.get_key_object()?;
276 match key_obj.id() {
277 openssl::pkey::Id::RSA => {
278 let mut signer = OpenSslSigner::new(MessageDigest::sha256(), key_obj)
279 .context("Failed to create RSA signer")?;
280 signer
281 .update(params.as_bytes())
282 .context("Failed to update RSA signer")?;
283 let sig = signer.sign_to_vec().context("RSA signing failed")?;
284 return Ok(general_purpose::STANDARD.encode(sig));
285 }
286 openssl::pkey::Id::ED25519 => {
287 let signing_key = self.get_ed25519_signing_key(key_obj)?;
288 let signature = signing_key.sign(params.as_bytes());
289 return Ok(general_purpose::STANDARD.encode(signature.to_bytes()));
290 }
291 other => {
292 return Err(anyhow::anyhow!(
293 "Unsupported private key type: {:?}. Must be RSA or ED25519.",
294 other
295 ));
296 }
297 }
298 }
299
300 #[cfg(not(feature = "openssl-tls"))]
301 {
302 return Err(anyhow::anyhow!(
303 "Private key signing requires the 'openssl-tls' feature to be enabled."
304 ));
305 }
306 }
307
308 Err(anyhow::anyhow!(
309 "Either 'api_secret' or 'private_key' must be provided for signed requests."
310 ))
311 }
312}
313
314#[must_use]
337pub fn build_client(
338 timeout: u64,
339 keep_alive: bool,
340 proxy: Option<&ProxyConfig>,
341 agent: Option<HttpAgent>,
342) -> Client {
343 let builder = Client::builder().timeout(Duration::from_millis(timeout));
344
345 let mut builder = if keep_alive {
346 builder
347 } else {
348 builder.pool_idle_timeout(Some(Duration::from_secs(0)))
349 };
350
351 if let Some(proxy_conf) = proxy {
352 let protocol = proxy_conf
353 .protocol
354 .clone()
355 .unwrap_or_else(|| "http".to_string());
356 let proxy_url = format!("{}://{}:{}", protocol, proxy_conf.host, proxy_conf.port);
357 let mut proxy_builder = Proxy::all(&proxy_url).expect("Failed to create proxy from URL");
358 if let Some(auth) = &proxy_conf.auth {
359 proxy_builder = proxy_builder.basic_auth(&auth.username, &auth.password);
360 }
361 builder = builder.proxy(proxy_builder);
362 }
363
364 if let Some(HttpAgent(agent_fn)) = agent {
365 builder = (agent_fn)(builder);
366 }
367
368 builder.build().expect("Failed to build reqwest client")
369}
370
371#[must_use]
394pub fn build_user_agent(product: &str) -> String {
395 if let Ok(override_ua) = std::env::var("BINANCE_CONNECTOR_RUST_USER_AGENT") {
396 let trimmed = override_ua.trim().to_string();
397 if CLI_SKILL_UA_RE.is_match(&trimmed) {
398 return trimmed;
399 }
400 }
401 format!(
402 "{}/{}/{} (Rust/{}; {}; {})",
403 env!("CARGO_PKG_NAME"),
404 product,
405 env!("CARGO_PKG_VERSION"),
406 env!("RUSTC_VERSION"),
407 std::env::consts::OS,
408 std::env::consts::ARCH,
409 )
410}
411
412pub fn validate_time_unit(time_unit: &str) -> Result<Option<&str>, anyhow::Error> {
440 match time_unit {
441 "" => Ok(None),
442 "MILLISECOND" | "MICROSECOND" | "millisecond" | "microsecond" => Ok(Some(time_unit)),
443 _ => Err(anyhow::anyhow!(
444 "time_unit must be either 'MILLISECOND' or 'MICROSECOND'"
445 )),
446 }
447}
448
449#[must_use]
466pub fn get_timestamp() -> u128 {
467 SystemTime::now()
468 .duration_since(UNIX_EPOCH)
469 .expect("Time went backwards")
470 .as_millis()
471}
472
473pub async fn delay(ms: u64) {
485 sleep(Duration::from_millis(ms)).await;
486}
487
488pub fn build_query_string(params: &BTreeMap<String, Value>) -> Result<String, anyhow::Error> {
507 let mut segments = Vec::with_capacity(params.len());
508
509 for (key, value) in params {
510 if value.is_null() {
511 continue;
512 }
513
514 let value_str = match value {
515 Value::String(s) => s.clone(),
516 Value::Bool(b) => b.to_string(),
517 Value::Number(n) => n.to_string(),
518 Value::Array(_) | Value::Object(_) => serde_json::to_string(value)
519 .with_context(|| format!("failed to JSON-serialize `{}`", key))?,
520 Value::Null => unreachable!(),
521 };
522
523 let mut ser = Serializer::new(String::new());
524 ser.append_pair(key, &value_str);
525 segments.push(ser.finish());
526 }
527
528 Ok(segments.join("&"))
529}
530
531#[must_use]
539pub fn should_retry_request(
540 error: &reqwest::Error,
541 method: Option<&str>,
542 retries_left: Option<usize>,
543) -> bool {
544 let method = method.unwrap_or("");
545 let is_retriable_method =
546 method.eq_ignore_ascii_case("GET") || method.eq_ignore_ascii_case("DELETE");
547
548 let status = error.status().map_or(0, |s| s.as_u16());
549 let is_retriable_status = [500, 502, 503, 504].contains(&status);
550
551 let retries_left = retries_left.unwrap_or(0);
552 retries_left > 0 && is_retriable_method && (is_retriable_status || error.status().is_none())
553}
554
555#[must_use]
580pub fn parse_rate_limit_headers<S>(headers: &HashMap<String, String, S>) -> Vec<RestApiRateLimit>
581where
582 S: BuildHasher,
583{
584 let mut rate_limits = Vec::new();
585 let re = Regex::new(r"x-mbx-(used-weight|order-count)-(\d+)([smhd])").unwrap();
586 for (key, value) in headers {
587 let normalized_key = key.to_lowercase();
588 if normalized_key.starts_with("x-mbx-used-weight-")
589 || normalized_key.starts_with("x-mbx-order-count-")
590 {
591 if let Some(caps) = re.captures(&normalized_key) {
592 let interval_num: u32 = caps.get(2).unwrap().as_str().parse().unwrap_or(0);
593 let interval_letter = caps.get(3).unwrap().as_str().to_uppercase();
594 let interval = match interval_letter.as_str() {
595 "S" => Interval::Second,
596 "M" => Interval::Minute,
597 "H" => Interval::Hour,
598 "D" => Interval::Day,
599 _ => continue,
600 };
601 let count: u32 = value.parse().unwrap_or(0);
602 let rate_limit_type = if normalized_key.starts_with("x-mbx-used-weight-") {
603 RateLimitType::RequestWeight
604 } else {
605 RateLimitType::Orders
606 };
607
608 rate_limits.push(RestApiRateLimit {
609 rate_limit_type,
610 interval,
611 interval_num,
612 count,
613 retry_after: headers.get("retry-after").and_then(|v| v.parse().ok()),
614 });
615 }
616 }
617 }
618 rate_limits
619}
620
621pub async fn http_request<T: DeserializeOwned + Send + 'static>(
651 req: Request,
652 configuration: &ConfigurationRestApi,
653) -> Result<RestApiResponse<T>, ConnectorError> {
654 let client = &configuration.client;
655 let retries = configuration.retries as usize;
656 let backoff = configuration.backoff;
657 let mut attempt = 0;
658
659 loop {
660 let req_clone = req
661 .try_clone()
662 .context("Failed to clone request")
663 .map_err(|e| ConnectorError::ConnectorClientError {
664 msg: e.to_string(),
665 code: None,
666 })?;
667 match client.execute(req_clone).await {
668 Ok(response) => {
669 let status = response.status();
670 let headers_map: HashMap<String, String> = response
671 .headers()
672 .iter()
673 .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
674 .collect();
675
676 let raw_bytes = match response.bytes().await {
677 Ok(b) => b,
678 Err(e) => {
679 attempt += 1;
680 if attempt <= retries {
681 continue;
682 }
683 return Err(ConnectorError::ConnectorClientError {
684 msg: format!(
685 "Failed to get response bytes: {:#}",
686 anyhow::Error::new(e)
687 ),
688 code: None,
689 });
690 }
691 };
692
693 let content = if headers_map
694 .get("content-encoding")
695 .is_some_and(|enc| enc.to_lowercase().contains("gzip"))
696 {
697 let mut decoder = GzDecoder::new(&raw_bytes[..]);
698 let mut decompressed = String::new();
699 decoder
700 .read_to_string(&mut decompressed)
701 .context("Failed to decompress gzip response")
702 .map_err(|e: anyhow::Error| ConnectorError::ConnectorClientError {
703 msg: e.to_string(),
704 code: None,
705 })?;
706 decompressed
707 } else {
708 String::from_utf8(raw_bytes.to_vec())
709 .context("Failed to convert response to UTF-8")
710 .map_err(|e| ConnectorError::ConnectorClientError {
711 msg: e.to_string(),
712 code: None,
713 })?
714 };
715
716 let rate_limits = parse_rate_limit_headers(&headers_map);
717
718 if status.is_client_error() || status.is_server_error() {
719 let mut err_msg = content.clone();
720 let mut err_code: Option<i64> = None;
721
722 if let Ok(v) = serde_json::from_str::<serde_json::Value>(&content) {
723 if let Some(m) = v.get("msg").and_then(|m| m.as_str()) {
724 err_msg = m.to_string();
725 }
726 err_code = v.get("code").and_then(serde_json::Value::as_i64);
727 }
728
729 match status.as_u16() {
730 400 => {
731 return Err(ConnectorError::BadRequestError {
732 msg: err_msg,
733 code: err_code,
734 });
735 }
736 401 => {
737 return Err(ConnectorError::UnauthorizedError {
738 msg: err_msg,
739 code: err_code,
740 });
741 }
742 403 => {
743 return Err(ConnectorError::ForbiddenError {
744 msg: err_msg,
745 code: err_code,
746 });
747 }
748 404 => {
749 return Err(ConnectorError::NotFoundError {
750 msg: err_msg,
751 code: err_code,
752 });
753 }
754 418 => {
755 return Err(ConnectorError::RateLimitBanError {
756 msg: err_msg,
757 code: err_code,
758 });
759 }
760 429 => {
761 return Err(ConnectorError::TooManyRequestsError {
762 msg: err_msg,
763 code: err_code,
764 });
765 }
766 s if (500..600).contains(&s) => {
767 return Err(ConnectorError::ServerError {
768 msg: format!("Server error: {s}"),
769 status_code: Some(s),
770 });
771 }
772 _ => {
773 return Err(ConnectorError::ConnectorClientError {
774 msg: err_msg,
775 code: err_code,
776 });
777 }
778 }
779 }
780
781 let raw = content.clone();
782 return Ok(RestApiResponse {
783 data_fn: Box::new(move || {
784 Box::pin(async move {
785 let parsed: T = serde_json::from_str(&raw).map_err(|e| {
786 ConnectorError::ConnectorClientError {
787 msg: e.to_string(),
788 code: None,
789 }
790 })?;
791 Ok(parsed)
792 })
793 }),
794 status: status.as_u16(),
795 headers: headers_map,
796 rate_limits: if rate_limits.is_empty() {
797 None
798 } else {
799 Some(rate_limits)
800 },
801 });
802 }
803 Err(e) => {
804 attempt += 1;
805 if should_retry_request(
806 &e,
807 Some(req.method().as_str()),
808 Some(retries.saturating_sub(attempt)),
809 ) {
810 delay(backoff * attempt as u64).await;
811 continue;
812 }
813 return Err(ConnectorError::ConnectorClientError {
814 msg: format!("HTTP request failed: {:#}", anyhow::Error::new(e)),
815 code: None,
816 });
817 }
818 }
819 }
820}
821
822pub async fn send_request<T: DeserializeOwned + Send + 'static>(
849 configuration: &ConfigurationRestApi,
850 endpoint: &str,
851 method: Method,
852 mut query_params: BTreeMap<String, Value>,
853 body_params: BTreeMap<String, Value>,
854 time_unit: Option<TimeUnit>,
855 is_signed: bool,
856) -> anyhow::Result<RestApiResponse<T>> {
857 let base = configuration.base_path.as_deref().unwrap_or("");
858 let full_url = reqwest::Url::parse(base)
859 .and_then(|u| u.join(endpoint))
860 .context("Failed to join base URL and endpoint")?
861 .to_string();
862
863 if is_signed {
864 let timestamp = get_timestamp();
865 query_params.insert("timestamp".to_string(), json!(timestamp));
866 }
867
868 let signature = if is_signed {
869 let body_ref = if body_params.is_empty() {
870 None
871 } else {
872 Some(&body_params)
873 };
874 Some(
875 configuration
876 .signature_gen
877 .get_signature(&query_params, body_ref)?,
878 )
879 } else {
880 None
881 };
882
883 let mut url = Url::parse(&full_url)?;
884 {
885 let mut pairs = url.query_pairs_mut();
886 for (key, value) in &query_params {
887 let val_str = match value {
888 Value::String(s) => s.clone(),
889 _ => value.to_string(),
890 };
891 pairs.append_pair(key, &val_str);
892 }
893 if let Some(signature) = &signature {
894 pairs.append_pair("signature", signature);
895 }
896 }
897
898 let mut headers = HeaderMap::new();
899
900 let forbidden = ["host", "authorization", "cookie", ":method", ":path"]
901 .into_iter()
902 .map(str::to_ascii_lowercase)
903 .collect::<std::collections::HashSet<_>>();
904
905 if let Some(custom) = &configuration.custom_headers {
906 for (raw_name, raw_val) in custom {
907 let name = raw_name.trim();
908 if forbidden.contains(&name.to_ascii_lowercase()) {
909 continue;
910 }
911 if let (Ok(header_name), Ok(header_val)) = (
912 name.parse::<reqwest::header::HeaderName>(),
913 HeaderValue::from_str(raw_val),
914 ) {
915 headers.append(header_name, header_val);
916 }
917 }
918 }
919
920 if body_params.is_empty() {
921 headers.insert("Content-Type", HeaderValue::from_static("application/json"));
922 } else {
923 headers.insert(
924 "Content-Type",
925 HeaderValue::from_static("application/x-www-form-urlencoded"),
926 );
927 }
928
929 headers.insert("User-Agent", configuration.user_agent.parse().unwrap());
930 if let Some(api_key) = &configuration.api_key {
931 headers.insert("X-MBX-APIKEY", HeaderValue::from_str(api_key)?);
932 }
933
934 if configuration.compression {
935 headers.insert(ACCEPT_ENCODING, "gzip, deflate, br".parse().unwrap());
936 }
937
938 let time_unit_to_apply = time_unit.or(configuration.time_unit);
939 if let Some(time_unit) = time_unit_to_apply {
940 headers.insert("X-MBX-TIME-UNIT", time_unit.as_upper_str().parse()?);
941 }
942
943 let mut req_builder = configuration.client.request(method, url).headers(headers);
944
945 if !body_params.is_empty() {
946 let mut serializer = form_urlencoded::Serializer::new(String::new());
947 for (key, value) in body_params {
948 let val_str = match value {
949 Value::String(s) => s,
950 _ => value.to_string(),
951 };
952 serializer.append_pair(&key, &val_str);
953 }
954 let body_str = serializer.finish();
955 req_builder = req_builder.body(body_str);
956 }
957
958 let req = req_builder.build()?;
959
960 Ok(http_request::<T>(req, configuration).await?)
961}
962
963#[must_use]
972pub fn random_string() -> String {
973 let mut buf = [0u8; 16];
974 rand::thread_rng().fill_bytes(&mut buf);
975 hex::encode(buf)
976}
977
978#[must_use]
987pub fn random_integer() -> u32 {
988 let mut buf = [0u8; 4];
989 OsRng.fill_bytes(&mut buf);
990 u32::from_ne_bytes(buf)
991}
992
993#[must_use]
1004pub fn normalize_stream_id(id: Option<StreamId>, stream_id_is_strictly_number: bool) -> Value {
1005 if stream_id_is_strictly_number {
1006 let n = match id {
1007 Some(StreamId::Number(n)) => n,
1008 _ => random_integer(),
1009 };
1010 return Value::Number(Number::from(n));
1011 }
1012
1013 match id {
1014 Some(StreamId::Number(n)) => Value::Number(Number::from(n)),
1015 Some(StreamId::Str(s)) => {
1016 let out = if ID_REGEX.is_match(&s) {
1017 s
1018 } else {
1019 random_string()
1020 };
1021 Value::String(out)
1022 }
1023 None => Value::String(random_string()),
1024 }
1025}
1026
1027pub fn remove_empty_value<I>(entries: I) -> BTreeMap<String, Value>
1049where
1050 I: IntoIterator<Item = (String, Value)>,
1051{
1052 entries
1053 .into_iter()
1054 .filter(|(_, value)| match value {
1055 Value::Null => false,
1056 Value::String(s) if s.is_empty() => false,
1057 _ => true,
1058 })
1059 .collect()
1060}
1061
1062#[must_use]
1083pub fn sort_object_params(params: &BTreeMap<String, Value>) -> BTreeMap<String, Value> {
1084 let mut sorted = BTreeMap::new();
1085 for (k, v) in params {
1086 sorted.insert(k.clone(), v.clone());
1087 }
1088 sorted
1089}
1090
1091fn normalize_ws_streams_key(key: &str) -> String {
1101 key.to_lowercase().replace(&['_', '-'][..], "")
1102}
1103
1104pub fn replace_websocket_streams_placeholders<V, S>(
1129 input: &str,
1130 variables: &HashMap<&str, V, S>,
1131) -> String
1132where
1133 V: Display,
1134 S: BuildHasher,
1135{
1136 let original = input;
1137
1138 let body = original.strip_prefix('/').unwrap_or(original);
1140
1141 let normalized: HashMap<String, String> = variables
1143 .iter()
1144 .map(|(k, v)| (normalize_ws_streams_key(k), v.to_string()))
1145 .collect();
1146
1147 let replaced = PLACEHOLDER_RE
1149 .replace_all(body, |caps: &Captures| {
1150 let prefix = caps.get(1).map_or("", |m| m.as_str());
1151 let key = normalize_ws_streams_key(caps.get(2).unwrap().as_str());
1152 let val = normalized.get(&key).cloned().unwrap_or_default();
1153 format!("{prefix}{val}")
1154 })
1155 .into_owned();
1156
1157 let stripped = replaced.trim_end_matches('@').to_string();
1159
1160 let should_lower_head =
1163 original.starts_with('/') && PLACEHOLDER_RE.find(body).is_some_and(|m| m.start() == 0);
1164
1165 if should_lower_head {
1167 if let Some(caps) = PLACEHOLDER_RE.captures(body) {
1168 let key = normalize_ws_streams_key(caps.get(2).unwrap().as_str());
1169 let first_val = normalized.get(&key).cloned().unwrap_or_default();
1170 if stripped.starts_with(&first_val) {
1171 let tail = &stripped[first_val.len()..];
1172 format!("{}{}", first_val.to_lowercase(), tail)
1173 } else {
1174 stripped.clone()
1175 }
1176 } else {
1177 stripped.clone()
1178 }
1179 } else {
1180 stripped.clone()
1181 }
1182}
1183
1184pub fn build_websocket_api_message(
1202 configuration: &ConfigurationWebsocketApi,
1203 method: &str,
1204 mut payload: BTreeMap<String, Value>,
1205 options: &WebsocketMessageSendOptions,
1206 skip_auth: bool,
1207) -> (String, serde_json::Value) {
1208 let id = payload
1209 .get("id")
1210 .and_then(Value::as_str)
1211 .filter(|s| ID_REGEX.is_match(s))
1212 .map_or_else(random_string, String::from);
1213
1214 payload.remove("id");
1215
1216 let mut params = remove_empty_value(payload);
1217
1218 if (options.with_api_key || options.is_signed) && !skip_auth {
1219 params.insert(
1220 "apiKey".into(),
1221 Value::String(configuration.api_key.clone().expect("API key must be set")),
1222 );
1223 }
1224
1225 if options.is_signed {
1226 let ts = get_timestamp();
1227 let ts_i64 = i64::try_from(ts).expect("timestamp fits in i64");
1228 params.insert("timestamp".into(), Value::Number(ts_i64.into()));
1229
1230 let mut sorted = sort_object_params(¶ms);
1231 if !skip_auth {
1232 let sig = configuration
1233 .signature_gen
1234 .get_signature(&sorted, None)
1235 .expect("signature generation");
1236 sorted.insert("signature".into(), Value::String(sig));
1237 }
1238 params = sorted.into_iter().collect();
1239 }
1240
1241 let request = json!({
1242 "id": id,
1243 "method": method,
1244 "params": params,
1245 });
1246
1247 (id, request)
1248}
1249
1250#[cfg(test)]
1251mod tests {
1252 use crate::TOKIO_SHARED_RT;
1253
1254 mod build_client {
1255 use std::{
1256 sync::{Arc, Mutex},
1257 time::{Duration, Instant},
1258 };
1259
1260 use reqwest::ClientBuilder;
1261
1262 use crate::{
1263 common::utils::build_client,
1264 config::{HttpAgent, ProxyAuth, ProxyConfig},
1265 };
1266
1267 use super::TOKIO_SHARED_RT;
1268
1269 #[test]
1270 fn enforces_timeout() {
1271 TOKIO_SHARED_RT.block_on(async {
1272 let client = build_client(100, true, None, None);
1273 let start = Instant::now();
1274 let res = client.get("http://10.255.255.1").send().await;
1275 assert!(
1276 res.is_err(),
1277 "expected an error (timeout or connect) but got {res:?}"
1278 );
1279 let elapsed = start.elapsed();
1280 assert!(
1281 elapsed < Duration::from_millis(500),
1282 "timed out too slowly: {elapsed:?}"
1283 );
1284 });
1285 }
1286
1287 #[test]
1288 fn builds_with_keep_alive_disabled() {
1289 let client = build_client(200, false, None, None);
1290 let _: reqwest::Client = client;
1291 }
1292
1293 #[test]
1294 #[should_panic(expected = "Failed to create proxy from URL")]
1295 fn invalid_proxy_url_panics() {
1296 let bad_proxy = ProxyConfig {
1297 protocol: Some("http".to_string()),
1298 host: String::new(),
1299 port: 8080,
1300 auth: None,
1301 };
1302 let _ = build_client(1_000, true, Some(&bad_proxy), None);
1303 }
1304
1305 #[test]
1306 fn builds_with_proxy_and_auth() {
1307 let proxy = ProxyConfig {
1308 protocol: Some("https".to_string()),
1309 host: "127.0.0.1".to_string(),
1310 port: 3128,
1311 auth: Some(ProxyAuth {
1312 username: "alice".to_string(),
1313 password: "secret".to_string(),
1314 }),
1315 };
1316 let client = build_client(2_000, true, Some(&proxy), None);
1317 let _: reqwest::Client = client;
1318 }
1319
1320 #[test]
1321 fn custom_agent_invoked() {
1322 let called = Arc::new(Mutex::new(false));
1323 let called_clone = Arc::clone(&called);
1324
1325 let agent = HttpAgent(Arc::new(move |builder: ClientBuilder| {
1326 *called_clone.lock().unwrap() = true;
1327 builder
1328 }));
1329
1330 let client = build_client(1_000, true, None, Some(agent));
1331 assert!(*called.lock().unwrap(), "agent closure wasn’t invoked");
1332 let _: reqwest::Client = client;
1333 }
1334 }
1335
1336 mod build_user_agent {
1337 use crate::common::utils::build_user_agent;
1338 use std::sync::Mutex;
1339
1340 static ENV_LOCK: Mutex<()> = Mutex::new(());
1341
1342 #[test]
1343 fn build_user_agent_contains_crate_product_and_rust_info() {
1344 let product = "product";
1345 let user_agent = build_user_agent(product);
1346
1347 let name = env!("CARGO_PKG_NAME");
1348 let version = env!("CARGO_PKG_VERSION");
1349 let rustc = env!("RUSTC_VERSION");
1350 let os = std::env::consts::OS;
1351 let arch = std::env::consts::ARCH;
1352
1353 let expected_prefix = format!("{name}/{product}/{version} (Rust/");
1354 assert!(
1355 user_agent.starts_with(&expected_prefix),
1356 "prefix mismatch: {user_agent}"
1357 );
1358
1359 assert!(
1360 user_agent.contains(rustc),
1361 "user agent missing RUSTC_VERSION: {user_agent}"
1362 );
1363
1364 assert!(
1365 user_agent.contains(&format!("; {os}")),
1366 "user agent missing OS: {user_agent}"
1367 );
1368 assert!(
1369 user_agent.contains(&format!("; {arch}")),
1370 "user agent missing ARCH: {user_agent}"
1371 );
1372 }
1373
1374 #[test]
1375 fn build_user_agent_is_deterministic() {
1376 let product = "product";
1377 let user_agent1 = build_user_agent(product);
1378 let user_agent2 = build_user_agent(product);
1379 assert_eq!(
1380 user_agent1, user_agent2,
1381 "user agent should be the same on repeated calls"
1382 );
1383 }
1384
1385 #[test]
1386 fn env_override_cli_is_used_when_valid() {
1387 let _guard = ENV_LOCK.lock().unwrap();
1388 let valid = "binance-cli/1.2.3 (linux; x86_64; extra)";
1389 unsafe { std::env::set_var("BINANCE_CONNECTOR_RUST_USER_AGENT", valid) };
1390 let ua = build_user_agent("spot");
1391 unsafe { std::env::remove_var("BINANCE_CONNECTOR_RUST_USER_AGENT") };
1392 assert_eq!(ua, valid);
1393 }
1394
1395 #[test]
1396 fn env_override_skill_is_used_when_valid() {
1397 let _guard = ENV_LOCK.lock().unwrap();
1398 let valid = "binance-skill/2.0 (darwin; aarch64; v2)";
1399 unsafe { std::env::set_var("BINANCE_CONNECTOR_RUST_USER_AGENT", valid) };
1400 let ua = build_user_agent("spot");
1401 unsafe { std::env::remove_var("BINANCE_CONNECTOR_RUST_USER_AGENT") };
1402 assert_eq!(ua, valid);
1403 }
1404
1405 #[test]
1406 fn env_override_with_leading_whitespace_is_trimmed_and_used() {
1407 let _guard = ENV_LOCK.lock().unwrap();
1408 let valid = "binance-cli/1.0 (linux; x86_64; v1)";
1409 unsafe {
1410 std::env::set_var("BINANCE_CONNECTOR_RUST_USER_AGENT", format!(" {valid} "));
1411 };
1412 let ua = build_user_agent("spot");
1413 unsafe { std::env::remove_var("BINANCE_CONNECTOR_RUST_USER_AGENT") };
1414 assert_eq!(ua, valid);
1415 }
1416
1417 #[test]
1418 fn invalid_env_override_falls_back_to_default() {
1419 let _guard = ENV_LOCK.lock().unwrap();
1420 unsafe {
1421 std::env::set_var(
1422 "BINANCE_CONNECTOR_RUST_USER_AGENT",
1423 "not-binance-cli/1.0 (linux; x86_64; v1)",
1424 );
1425 };
1426 let ua = build_user_agent("spot");
1427 unsafe { std::env::remove_var("BINANCE_CONNECTOR_RUST_USER_AGENT") };
1428 assert!(
1429 ua.starts_with(env!("CARGO_PKG_NAME")),
1430 "should fall back to default: {ua}"
1431 );
1432 }
1433
1434 #[test]
1435 fn empty_env_override_falls_back_to_default() {
1436 let _guard = ENV_LOCK.lock().unwrap();
1437 unsafe { std::env::set_var("BINANCE_CONNECTOR_RUST_USER_AGENT", "") };
1438 let ua = build_user_agent("spot");
1439 unsafe { std::env::remove_var("BINANCE_CONNECTOR_RUST_USER_AGENT") };
1440 assert!(
1441 ua.starts_with(env!("CARGO_PKG_NAME")),
1442 "should fall back to default: {ua}"
1443 );
1444 }
1445 }
1446
1447 mod validate_time_unit {
1448 use crate::common::utils::validate_time_unit;
1449
1450 #[test]
1451 fn empty_string_returns_none() {
1452 let res = validate_time_unit("").expect("Should not error on empty string");
1453 assert_eq!(res, None);
1454 }
1455
1456 #[test]
1457 fn uppercase_millisecond() {
1458 let res = validate_time_unit("MILLISECOND").expect("Should accept MILLISECOND");
1459 assert_eq!(res, Some("MILLISECOND"));
1460 }
1461
1462 #[test]
1463 fn uppercase_microsecond() {
1464 let res = validate_time_unit("MICROSECOND").expect("Should accept MICROSECOND");
1465 assert_eq!(res, Some("MICROSECOND"));
1466 }
1467
1468 #[test]
1469 fn lowercase_millisecond() {
1470 let res = validate_time_unit("millisecond").expect("Should accept millisecond");
1471 assert_eq!(res, Some("millisecond"));
1472 }
1473
1474 #[test]
1475 fn lowercase_microsecond() {
1476 let res = validate_time_unit("microsecond").expect("Should accept microsecond");
1477 assert_eq!(res, Some("microsecond"));
1478 }
1479
1480 #[test]
1481 fn invalid_value_returns_err() {
1482 let err = validate_time_unit("SECOND").unwrap_err();
1483 let msg = format!("{err}");
1484 assert!(msg.contains("time_unit must be either 'MILLISECOND' or 'MICROSECOND'"));
1485 }
1486
1487 #[test]
1488 fn partial_match_returns_err() {
1489 let err = validate_time_unit("MILLI").unwrap_err();
1490 let msg = format!("{err}");
1491 assert!(msg.contains("time_unit must be either 'MILLISECOND' or 'MICROSECOND'"));
1492 }
1493 }
1494
1495 mod get_timestamp {
1496 use crate::common::utils::get_timestamp;
1497 use std::{
1498 thread::sleep,
1499 time::{Duration, SystemTime, UNIX_EPOCH},
1500 };
1501
1502 #[test]
1503 fn timestamp_is_within_system_time_bounds() {
1504 let before = SystemTime::now()
1505 .duration_since(UNIX_EPOCH)
1506 .expect("SystemTime before UNIX_EPOCH")
1507 .as_millis();
1508 let ts = get_timestamp();
1509 let after = SystemTime::now()
1510 .duration_since(UNIX_EPOCH)
1511 .expect("SystemTime before UNIX_EPOCH")
1512 .as_millis();
1513
1514 assert!(
1515 ts >= before,
1516 "timestamp {ts} is before captured before time {before}"
1517 );
1518 assert!(
1519 ts <= after,
1520 "timestamp {ts} is after captured after time {after}"
1521 );
1522 }
1523
1524 #[test]
1525 fn timestamps_are_monotonic() {
1526 let t1 = get_timestamp();
1527 sleep(Duration::from_millis(1));
1528 let t2 = get_timestamp();
1529 assert!(
1530 t2 >= t1,
1531 "second timestamp {t2} is not >= first timestamp {t1}"
1532 );
1533 }
1534 }
1535
1536 mod build_query_string {
1537 use std::collections::BTreeMap;
1538
1539 use anyhow::Result;
1540 use serde_json::{Value, json};
1541 use url::form_urlencoded::Serializer;
1542
1543 use crate::common::utils::build_query_string;
1544
1545 fn mk_map(pairs: Vec<(&str, Value)>) -> BTreeMap<String, Value> {
1546 let mut m = BTreeMap::new();
1547 for (k, v) in pairs {
1548 m.insert(k.to_string(), v);
1549 }
1550 m
1551 }
1552
1553 #[test]
1554 fn empty_map_returns_empty_string() -> Result<()> {
1555 let params = BTreeMap::new();
1556 let qs = build_query_string(¶ms)?;
1557 assert_eq!(qs, "");
1558 Ok(())
1559 }
1560
1561 #[test]
1562 fn string_and_number_and_bool() -> Result<()> {
1563 let params = mk_map(vec![
1564 ("foo", json!("bar")),
1565 ("num", json!(42)),
1566 ("flag", json!(true)),
1567 ]);
1568 let qs = build_query_string(¶ms)?;
1569 assert_eq!(qs, "flag=true&foo=bar&num=42");
1570 Ok(())
1571 }
1572
1573 #[test]
1574 fn null_is_skipped() -> Result<()> {
1575 let params = mk_map(vec![("a", json!(true)), ("b", Value::Null)]);
1576 let qs = build_query_string(¶ms)?;
1577 assert_eq!(qs, "a=true");
1578 Ok(())
1579 }
1580
1581 #[test]
1582 fn percent_encode_special_chars() -> Result<()> {
1583 let params = mk_map(vec![
1584 ("space", json!("hello world")),
1585 ("symbols", json!("a/b?c")),
1586 ]);
1587 let qs = build_query_string(¶ms)?;
1588 let mut parts = vec![];
1589 let mut ser = Serializer::new(String::new());
1590 ser.append_pair("space", "hello world");
1591 parts.push(ser.finish());
1592 let mut ser = Serializer::new(String::new());
1593 ser.append_pair("symbols", "a/b?c");
1594 parts.push(ser.finish());
1595 let expected = parts.join("&");
1596 assert_eq!(qs, expected);
1597 Ok(())
1598 }
1599
1600 #[test]
1601 fn primitive_array_json_encoded() -> Result<()> {
1602 let params = mk_map(vec![
1603 ("strs", json!(["a", "b", "c"])),
1604 ("nums", json!([1, 2, 3])),
1605 ("bools", json!([true, false])),
1606 ]);
1607 let qs = build_query_string(¶ms)?;
1608
1609 let mut parts = Vec::new();
1610 for (k, v) in ¶ms {
1611 let json = serde_json::to_string(v)?;
1612 let mut ser = Serializer::new(String::new());
1613 ser.append_pair(k, &json);
1614 parts.push(ser.finish());
1615 }
1616 let expected = parts.join("&");
1617 assert_eq!(qs, expected);
1618 Ok(())
1619 }
1620
1621 #[test]
1622 fn nested_array_json_encoded() -> Result<()> {
1623 let params = mk_map(vec![("nested", json!([[1, 2], [3, 4]]))]);
1624 let qs = build_query_string(¶ms)?;
1625
1626 let nested_json = serde_json::to_string(&json!([[1, 2], [3, 4]]))?;
1627 let mut ser = Serializer::new(String::new());
1628 ser.append_pair("nested", &nested_json);
1629 let expected = ser.finish();
1630
1631 assert_eq!(qs, expected);
1632 Ok(())
1633 }
1634
1635 #[test]
1636 fn object_json_encoded() -> Result<()> {
1637 let params = mk_map(vec![("obj", json!({"k":1, "v":"two"}))]);
1638 let qs = build_query_string(¶ms)?;
1639
1640 let obj_json = serde_json::to_string(&json!({"k":1, "v":"two"}))?;
1641 let mut ser = Serializer::new(String::new());
1642 ser.append_pair("obj", &obj_json);
1643 let expected = ser.finish();
1644
1645 assert_eq!(qs, expected);
1646 Ok(())
1647 }
1648
1649 #[test]
1650 fn empty_array() {
1651 let params = mk_map(vec![("foo", json!([]))]);
1652 let qs = build_query_string(¶ms).unwrap();
1653
1654 let json = serde_json::to_string(&json!([])).unwrap();
1655 let expected = Serializer::new(String::new())
1656 .append_pair("foo", &json)
1657 .finish();
1658 assert_eq!(qs, expected);
1659 }
1660
1661 #[test]
1662 fn mixed_array() {
1663 let params = mk_map(vec![("mix", json!([1, "x", false]))]);
1664 let qs = build_query_string(¶ms).unwrap();
1665
1666 let json = serde_json::to_string(&json!([1, "x", false])).unwrap();
1667 let expected = Serializer::new(String::new())
1668 .append_pair("mix", &json)
1669 .finish();
1670 assert_eq!(qs, expected);
1671 }
1672
1673 #[test]
1674 fn array_of_objects() {
1675 let params = mk_map(vec![("objs", json!([{"a":1}, {"b":2}]))]);
1676 let qs = build_query_string(¶ms).unwrap();
1677
1678 let json = serde_json::to_string(&json!([{"a":1}, {"b":2}])).unwrap();
1679 let expected = Serializer::new(String::new())
1680 .append_pair("objs", &json)
1681 .finish();
1682 assert_eq!(qs, expected);
1683 }
1684
1685 #[test]
1686 fn empty_object() {
1687 let params = mk_map(vec![("emp", json!({}))]);
1688 let qs = build_query_string(¶ms).unwrap();
1689
1690 let json = serde_json::to_string(&json!({})).unwrap();
1691 let expected = Serializer::new(String::new())
1692 .append_pair("emp", &json)
1693 .finish();
1694 assert_eq!(qs, expected);
1695 }
1696
1697 #[test]
1698 fn floats_and_negatives() {
1699 let params = mk_map(vec![("fl", json!(1.23456)), ("neg", json!(-0.001))]);
1700 let qs = build_query_string(¶ms).unwrap();
1701 assert_eq!(qs, "fl=1.23456&neg=-0.001");
1702 }
1703
1704 #[test]
1705 fn unicode_and_special_key() {
1706 let params = mk_map(vec![
1707 ("こんにちは", json!("世界")),
1708 ("weird key/?=", json!("val")),
1709 ]);
1710 let qs = build_query_string(¶ms).unwrap();
1711
1712 let mut parts = Vec::new();
1713 for (k, v) in ¶ms {
1714 let mut ser = Serializer::new(String::new());
1715 ser.append_pair(k, v.as_str().unwrap());
1716 parts.push(ser.finish());
1717 }
1718 let expected = parts.join("&");
1719 assert_eq!(qs, expected);
1720 }
1721
1722 #[test]
1723 fn empty_string_value() {
1724 let params = mk_map(vec![("empty", json!(""))]);
1725 let qs = build_query_string(¶ms).unwrap();
1726 assert_eq!(qs, "empty=");
1727 }
1728
1729 #[test]
1730 fn nulls_in_array() {
1731 let params = mk_map(vec![("a", json!([null, 1, "x"]))]);
1732 let qs = build_query_string(¶ms).unwrap();
1733
1734 let json = serde_json::to_string(&json!([null, 1, "x"])).unwrap();
1735 let expected = Serializer::new(String::new())
1736 .append_pair("a", &json)
1737 .finish();
1738 assert_eq!(qs, expected);
1739 }
1740
1741 #[test]
1742 fn special_chars_in_key() {
1743 let params = mk_map(vec![("a=b&c%", json!("val"))]);
1744 let qs = build_query_string(¶ms).unwrap();
1745
1746 let expected = Serializer::new(String::new())
1747 .append_pair("a=b&c%", "val")
1748 .finish();
1749 assert_eq!(qs, expected);
1750 }
1751
1752 #[test]
1753 fn empty_key() {
1754 let params = mk_map(vec![("", json!("v"))]);
1755 let qs = build_query_string(¶ms).unwrap();
1756 assert_eq!(qs, "=v");
1757 }
1758 }
1759
1760 #[cfg(feature = "openssl-tls")]
1761 mod signature_generator {
1762 use base64::{Engine, engine::general_purpose};
1763 use ed25519_dalek::{SigningKey, ed25519::signature::SignerMut, pkcs8::DecodePrivateKey};
1764 use hex;
1765 use hmac::{Hmac, Mac};
1766 #[cfg(feature = "openssl-tls")]
1767 use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa, sign::Verifier};
1768 use serde_json::Value;
1769 use sha2::Sha256;
1770 use std::collections::BTreeMap;
1771 use std::io::Write;
1772 use tempfile::NamedTempFile;
1773
1774 use crate::{common::utils::SignatureGenerator, config::PrivateKey};
1775
1776 #[test]
1777 fn hmac_sha256_signature() {
1778 let mut params = BTreeMap::new();
1779 params.insert("b".into(), Value::Number(2.into()));
1780 params.insert("a".into(), Value::Number(1.into()));
1781
1782 let signature_gen = SignatureGenerator::new(Some("test-secret".into()), None, None);
1783 let sig = signature_gen
1784 .get_signature(¶ms, None)
1785 .expect("HMAC signing failed");
1786
1787 let mut mac = Hmac::<Sha256>::new_from_slice(b"test-secret").unwrap();
1788 let qs = "a=1&b=2";
1789 mac.update(qs.as_bytes());
1790 let expected = hex::encode(mac.finalize().into_bytes());
1791
1792 assert_eq!(sig, expected);
1793 }
1794
1795 #[test]
1796 fn hmac_sha256_signature_with_body() {
1797 let mut query_params = BTreeMap::new();
1798 query_params.insert("b".into(), Value::Number(2.into()));
1799 query_params.insert("a".into(), Value::Number(1.into()));
1800
1801 let mut body_params = BTreeMap::new();
1802 body_params.insert("d".into(), Value::Number(4.into()));
1803 body_params.insert("c".into(), Value::Number(3.into()));
1804
1805 let signature_gen = SignatureGenerator::new(Some("test-secret".into()), None, None);
1806 let sig = signature_gen
1807 .get_signature(&query_params, Some(&body_params))
1808 .expect("HMAC signing with body failed");
1809
1810 let query_str = "a=1&b=2";
1811 let body_str = "c=3&d=4";
1812
1813 let payload = format!("{query_str}{body_str}");
1814
1815 let mut mac = Hmac::<Sha256>::new_from_slice(b"test-secret").unwrap();
1816 mac.update(payload.as_bytes());
1817 let expected = hex::encode(mac.finalize().into_bytes());
1818
1819 assert_eq!(sig, expected);
1820 }
1821
1822 #[test]
1823 fn repeated_hmac_signature() {
1824 let mut params = BTreeMap::new();
1825 params.insert("x".into(), Value::String("y".into()));
1826 let signature_gen = SignatureGenerator::new(Some("abc".into()), None, None);
1827 let s1 = signature_gen.get_signature(¶ms, None).unwrap();
1828 let s2 = signature_gen.get_signature(¶ms, None).unwrap();
1829 assert_eq!(s1, s2);
1830 }
1831
1832 #[test]
1833 fn rsa_signature_verification() {
1834 let mut params = BTreeMap::new();
1835 params.insert("a".into(), Value::Number(1.into()));
1836 params.insert("b".into(), Value::Number(2.into()));
1837
1838 let rsa = Rsa::generate(2048).unwrap();
1839 let priv_pem = rsa.private_key_to_pem().unwrap();
1840 let pub_pem = rsa.public_key_to_pem_pkcs1().unwrap();
1841
1842 let signature_gen =
1843 SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem.clone())), None);
1844 let sig = signature_gen
1845 .get_signature(¶ms, None)
1846 .expect("RSA signing failed");
1847
1848 let sig_bytes = general_purpose::STANDARD.decode(&sig).unwrap();
1849 let pubkey = PKey::public_key_from_pem(&pub_pem).unwrap();
1850 let mut verifier = Verifier::new(MessageDigest::sha256(), &pubkey).unwrap();
1851 verifier.update(b"a=1&b=2").unwrap();
1852 assert!(verifier.verify(&sig_bytes).unwrap());
1853 }
1854
1855 #[test]
1856 fn rsa_signature_verification_with_body() {
1857 let mut query_params = BTreeMap::new();
1858 query_params.insert("a".into(), Value::Number(1.into()));
1859 query_params.insert("b".into(), Value::Number(2.into()));
1860
1861 let mut body_params = BTreeMap::new();
1862 body_params.insert("c".into(), Value::Number(3.into()));
1863 body_params.insert("d".into(), Value::Number(4.into()));
1864
1865 let rsa = Rsa::generate(2048).unwrap();
1866 let priv_pem = rsa.private_key_to_pem().unwrap();
1867 let pub_pem = rsa.public_key_to_pem_pkcs1().unwrap();
1868
1869 let signature_gen =
1870 SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem.clone())), None);
1871 let sig = signature_gen
1872 .get_signature(&query_params, Some(&body_params))
1873 .expect("RSA signing with body failed");
1874
1875 let sig_bytes = general_purpose::STANDARD.decode(&sig).unwrap();
1876 let pubkey = PKey::public_key_from_pem(&pub_pem).unwrap();
1877 let mut verifier = Verifier::new(MessageDigest::sha256(), &pubkey).unwrap();
1878 verifier.update(b"a=1&b=2c=3&d=4").unwrap();
1879 assert!(verifier.verify(&sig_bytes).unwrap());
1880 }
1881
1882 #[test]
1883 fn repeated_rsa_signature() {
1884 let mut params = BTreeMap::new();
1885 params.insert("k".into(), Value::Number(5.into()));
1886 let rsa = Rsa::generate(2048).unwrap();
1887 let priv_pem = rsa.private_key_to_pem().unwrap();
1888 let signature_gen =
1889 SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem)), None);
1890 let s1 = signature_gen.get_signature(¶ms, None).unwrap();
1891 let s2 = signature_gen.get_signature(¶ms, None).unwrap();
1892 assert_eq!(s1, s2);
1893 }
1894
1895 #[test]
1896 fn ed25519_signature_verification() {
1897 let mut params = BTreeMap::new();
1898 params.insert("a".into(), Value::Number(1.into()));
1899 params.insert("b".into(), Value::Number(2.into()));
1900 let qs = "a=1&b=2";
1901
1902 let ed = PKey::generate_ed25519().unwrap();
1903 let priv_pem = ed.private_key_to_pem_pkcs8().unwrap();
1904
1905 let signature_gen =
1906 SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem.clone())), None);
1907 let sig = signature_gen
1908 .get_signature(¶ms, None)
1909 .expect("Ed25519 signing failed");
1910
1911 let pem_str = String::from_utf8(priv_pem).unwrap();
1912 let b64 = pem_str
1913 .lines()
1914 .filter(|l| !l.starts_with("-----"))
1915 .collect::<String>();
1916 let der = general_purpose::STANDARD.decode(b64).unwrap();
1917 let mut sk = SigningKey::from_pkcs8_der(&der).unwrap();
1918 let expected_bytes = sk.sign(qs.as_bytes()).to_bytes();
1919 let expected_sig = general_purpose::STANDARD.encode(expected_bytes);
1920 assert_eq!(sig, expected_sig);
1921 }
1922
1923 #[test]
1924 fn ed25519_signature_verification_with_body() {
1925 let mut query_params = BTreeMap::new();
1926 query_params.insert("a".into(), Value::Number(1.into()));
1927 query_params.insert("b".into(), Value::Number(2.into()));
1928 let qs = "a=1&b=2";
1929
1930 let mut body_params = BTreeMap::new();
1931 body_params.insert("c".into(), Value::Number(3.into()));
1932 body_params.insert("d".into(), Value::Number(4.into()));
1933 let body_qs = "c=3&d=4";
1934
1935 let ed = PKey::generate_ed25519().unwrap();
1936 let priv_pem = ed.private_key_to_pem_pkcs8().unwrap();
1937
1938 let signature_gen =
1939 SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem.clone())), None);
1940 let sig = signature_gen
1941 .get_signature(&query_params, Some(&body_params))
1942 .expect("Ed25519 signing with body failed");
1943
1944 let pem_str = String::from_utf8(priv_pem).unwrap();
1945 let b64 = pem_str
1946 .lines()
1947 .filter(|l| !l.starts_with("-----"))
1948 .collect::<String>();
1949 let der = general_purpose::STANDARD.decode(b64).unwrap();
1950 let mut sk = SigningKey::from_pkcs8_der(&der).unwrap();
1951 let payload = format!("{qs}{body_qs}");
1952 let expected_bytes = sk.sign(payload.as_bytes()).to_bytes();
1953 let expected_sig = general_purpose::STANDARD.encode(expected_bytes);
1954 assert_eq!(sig, expected_sig);
1955 }
1956
1957 #[test]
1958 fn repeated_ed25519_signature() {
1959 let mut params = BTreeMap::new();
1960 params.insert("m".into(), Value::String("n".into()));
1961 let ed = PKey::generate_ed25519().unwrap();
1962 let priv_pem = ed.private_key_to_pem_pkcs8().unwrap();
1963 let signature_gen =
1964 SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem.clone())), None);
1965 let s1 = signature_gen.get_signature(¶ms, None).unwrap();
1966 let s2 = signature_gen.get_signature(¶ms, None).unwrap();
1967 assert_eq!(s1, s2);
1968 }
1969
1970 #[test]
1971 fn file_based_key() {
1972 let rsa = Rsa::generate(1024).unwrap();
1973 let priv_pem = rsa.private_key_to_pem().unwrap();
1974 let pub_pem = rsa.public_key_to_pem_pkcs1().unwrap();
1975
1976 let mut file = NamedTempFile::new().unwrap();
1977 file.write_all(&priv_pem).unwrap();
1978 let path = file.path().to_str().unwrap().to_string();
1979
1980 let mut params = BTreeMap::new();
1981 params.insert("z".into(), Value::Number(9.into()));
1982
1983 let signature_gen = SignatureGenerator::new(None, Some(PrivateKey::File(path)), None);
1984 let sig = signature_gen.get_signature(¶ms, None).unwrap();
1985
1986 let sig_bytes = general_purpose::STANDARD.decode(&sig).unwrap();
1987 let pubkey = PKey::public_key_from_pem(&pub_pem).unwrap();
1988 let mut verifier = Verifier::new(MessageDigest::sha256(), &pubkey).unwrap();
1989 verifier.update(b"z=9").unwrap();
1990 assert!(verifier.verify(&sig_bytes).unwrap());
1991 }
1992
1993 #[cfg(feature = "openssl-tls")]
1994 #[test]
1995 fn unsupported_key_type_error() {
1996 let mut params = BTreeMap::new();
1997 params.insert("x".into(), Value::String("y".into()));
1998
1999 let group =
2000 openssl::ec::EcGroup::from_curve_name(openssl::nid::Nid::X9_62_PRIME256V1).unwrap();
2001 let ec_key = openssl::ec::EcKey::generate(&group).unwrap();
2002 let pkey_ec = PKey::from_ec_key(ec_key).unwrap();
2003 let raw = pkey_ec.private_key_to_pem_pkcs8().unwrap();
2004
2005 let signature_gen = SignatureGenerator::new(None, Some(PrivateKey::Raw(raw)), None);
2006 let err = signature_gen
2007 .get_signature(¶ms, None)
2008 .unwrap_err()
2009 .to_string();
2010 assert!(err.contains("Unsupported private key type"));
2011 }
2012
2013 #[test]
2014 fn invalid_private_key_error() {
2015 let mut params = BTreeMap::new();
2016 params.insert("foo".into(), Value::String("bar".into()));
2017
2018 let signature_gen =
2019 SignatureGenerator::new(None, Some(PrivateKey::Raw(b"not a key".to_vec())), None);
2020 let err = signature_gen
2021 .get_signature(¶ms, None)
2022 .unwrap_err()
2023 .to_string();
2024 assert!(err.contains("Failed to parse private key"));
2025 }
2026
2027 #[test]
2028 fn missing_credentials_error() {
2029 let mut params = BTreeMap::new();
2030 params.insert("a".into(), Value::Number(1.into()));
2031
2032 let signature_gen = SignatureGenerator::new(None, None, None);
2033 let err = signature_gen
2034 .get_signature(¶ms, None)
2035 .unwrap_err()
2036 .to_string();
2037 assert!(err.contains("Either 'api_secret' or 'private_key' must be provided"));
2038 }
2039 }
2040
2041 mod should_retry_request {
2042 use crate::common::utils::should_retry_request;
2043
2044 use reqwest::{Error, Response};
2045
2046 fn mk_http_error(code: u16) -> Error {
2047 let resp = Response::from(
2048 http::response::Response::builder()
2049 .status(code)
2050 .body("")
2051 .unwrap(),
2052 );
2053 resp.error_for_status().unwrap_err()
2054 }
2055
2056 fn mk_network_error() -> Error {
2057 reqwest::blocking::get("http://256.256.256.256").unwrap_err()
2058 }
2059
2060 #[test]
2061 fn retry_on_retriable_status_and_method() {
2062 let err = mk_http_error(500);
2063 assert!(should_retry_request(&err, Some("GET"), Some(1)));
2064 assert!(should_retry_request(&err, Some("delete"), Some(2)));
2065 }
2066
2067 #[test]
2068 fn retry_when_status_none_and_retriable_method() {
2069 let retriable_methods = ["GET", "DELETE"];
2070
2071 for &method in &retriable_methods {
2072 let err = mk_network_error();
2073 assert!(
2074 should_retry_request(&err, Some(method), Some(1)),
2075 "Should retry when no status and method {method}"
2076 );
2077 }
2078 }
2079
2080 #[test]
2081 fn no_retry_when_no_retries_left() {
2082 let err = mk_http_error(503);
2083 assert!(!should_retry_request(&err, Some("GET"), Some(0)));
2084 }
2085
2086 #[test]
2087 fn no_retry_on_non_retriable_status() {
2088 let non_retriable_statuses = [400, 401, 404, 422];
2089
2090 for &status in &non_retriable_statuses {
2091 let err = mk_http_error(status);
2092 assert!(
2093 !should_retry_request(&err, Some("GET"), Some(2)),
2094 "Should not retry for non-retriable status {status}"
2095 );
2096 }
2097 }
2098
2099 #[test]
2100 fn no_retry_on_non_retriable_method() {
2101 let non_retriable_methods = ["POST", "PUT", "PATCH"];
2102
2103 for &method in &non_retriable_methods {
2104 let err = mk_http_error(500);
2105 assert!(
2106 !should_retry_request(&err, Some(method), Some(2)),
2107 "Should not retry for non-retriable method {method}"
2108 );
2109 }
2110 }
2111
2112 #[test]
2113 fn no_retry_when_status_none_and_non_retriable_method() {
2114 let non_retriable_methods = ["POST", "PUT"];
2115
2116 for &method in &non_retriable_methods {
2117 let err = mk_network_error();
2118 assert!(
2119 !should_retry_request(&err, Some(method), Some(1)),
2120 "Should not retry when no status and method {method}"
2121 );
2122 }
2123 }
2124 }
2125
2126 mod parse_rate_limit_headers_tests {
2127 use crate::common::{
2128 models::{Interval, RateLimitType},
2129 utils::parse_rate_limit_headers,
2130 };
2131 use std::collections::HashMap;
2132
2133 fn mk_headers(pairs: Vec<(&str, &str)>) -> HashMap<String, String> {
2134 let mut m = HashMap::new();
2135 for (k, v) in pairs {
2136 m.insert(k.to_string(), v.to_string());
2137 }
2138 m
2139 }
2140
2141 #[test]
2142 fn single_weight_header() {
2143 let headers = mk_headers(vec![("x-mbx-used-weight-1s", "123")]);
2144 let limits = parse_rate_limit_headers(&headers);
2145 assert_eq!(limits.len(), 1);
2146 let rl = &limits[0];
2147 assert_eq!(rl.rate_limit_type, RateLimitType::RequestWeight);
2148 assert_eq!(rl.interval, Interval::Second);
2149 assert_eq!(rl.interval_num, 1);
2150 assert_eq!(rl.count, 123);
2151 assert_eq!(rl.retry_after, None);
2152 }
2153
2154 #[test]
2155 fn single_order_count_with_retry_after() {
2156 let headers = mk_headers(vec![("x-mbx-order-count-5m", "42"), ("retry-after", "7")]);
2157 let limits = parse_rate_limit_headers(&headers);
2158 assert_eq!(limits.len(), 1);
2159 let rl = &limits[0];
2160 assert_eq!(rl.rate_limit_type, RateLimitType::Orders);
2161 assert_eq!(rl.interval, Interval::Minute);
2162 assert_eq!(rl.interval_num, 5);
2163 assert_eq!(rl.count, 42);
2164 assert_eq!(rl.retry_after, Some(7));
2165 }
2166
2167 #[test]
2168 fn multiple_headers() {
2169 let headers = mk_headers(vec![
2170 ("X-MBX-USED-WEIGHT-1h", "10"),
2171 ("x-mbx-order-count-2d", "20"),
2172 ]);
2173 let mut limits = parse_rate_limit_headers(&headers);
2174 limits.sort_by_key(|r| (r.interval_num, format!("{:?}", r.rate_limit_type)));
2175 assert_eq!(limits.len(), 2);
2176 let w = &limits[0];
2177 assert_eq!(w.rate_limit_type, RateLimitType::RequestWeight);
2178 assert_eq!(w.interval, Interval::Hour);
2179 assert_eq!(w.interval_num, 1);
2180 assert_eq!(w.count, 10);
2181 let o = &limits[1];
2182 assert_eq!(o.rate_limit_type, RateLimitType::Orders);
2183 assert_eq!(o.interval, Interval::Day);
2184 assert_eq!(o.interval_num, 2);
2185 assert_eq!(o.count, 20);
2186 }
2187
2188 #[test]
2189 fn ignores_unknown_and_malformed() {
2190 let headers = mk_headers(vec![
2191 ("x-mbx-used-weight-3x", "5"),
2192 ("random-header", "100"),
2193 ]);
2194 let limits = parse_rate_limit_headers(&headers);
2195 assert!(limits.is_empty());
2196 }
2197 }
2198
2199 mod http_request {
2200 use std::io::Write;
2201
2202 use flate2::{Compression, write::GzEncoder};
2203 use httpmock::MockServer;
2204 use reqwest::{Client, Method, Request};
2205 use serde::Deserialize;
2206
2207 use crate::{
2208 common::utils::http_request, config::ConfigurationRestApi, errors::ConnectorError,
2209 models::RestApiResponse,
2210 };
2211
2212 use super::TOKIO_SHARED_RT;
2213
2214 #[derive(Deserialize, Debug, PartialEq)]
2215 struct Dummy {
2216 foo: String,
2217 }
2218
2219 fn make_config(server_url: &str) -> ConfigurationRestApi {
2220 ConfigurationRestApi::builder()
2221 .api_key("key")
2222 .api_secret("secret")
2223 .base_path(server_url)
2224 .build()
2225 .expect("Failed to build configuration")
2226 }
2227
2228 #[test]
2229 fn http_request_success_plain_text() {
2230 TOKIO_SHARED_RT.block_on(async {
2231 let server = MockServer::start();
2232 let mock = server.mock(|when, then| {
2233 when.method(httpmock::Method::GET).path("/test");
2234 then.status(200)
2235 .header("Content-Type", "application/json")
2236 .body(r#"{"foo":"bar"}"#);
2237 });
2238
2239 let client = Client::new();
2240 let req: Request = client
2241 .request(Method::GET, format!("{}{}", server.url(""), "/test"))
2242 .build()
2243 .unwrap();
2244
2245 let cfg = make_config(&server.url(""));
2246 let resp: RestApiResponse<Dummy> = http_request(req, &cfg).await.unwrap();
2247 assert_eq!(resp.status, 200);
2248 let data = resp.data().await.unwrap();
2249 assert_eq!(data, Dummy { foo: "bar".into() });
2250 mock.assert();
2251 });
2252 }
2253
2254 #[test]
2255 fn http_request_success_gzip() {
2256 TOKIO_SHARED_RT.block_on(async {
2257 let server = MockServer::start();
2258 let body = r#"{"foo":"baz"}"#;
2259 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
2260 encoder.write_all(body.as_bytes()).unwrap();
2261 let gz = encoder.finish().unwrap();
2262
2263 let mock = server.mock(|when, then| {
2264 when.method(httpmock::Method::GET).path("/gz");
2265 then.status(200)
2266 .header("Content-Type", "application/json")
2267 .header("Content-Encoding", "gzip")
2268 .body(gz);
2269 });
2270
2271 let client = Client::new();
2272 let req: Request = client
2273 .request(Method::GET, format!("{}{}", server.url(""), "/gz"))
2274 .build()
2275 .unwrap();
2276 let mut cfg = make_config(&server.url(""));
2277 cfg.compression = true;
2278
2279 let resp: RestApiResponse<Dummy> = http_request(req, &cfg).await.unwrap();
2280 assert_eq!(resp.status, 200);
2281 let data = resp.data().await.unwrap();
2282 assert_eq!(data, Dummy { foo: "baz".into() });
2283 mock.assert();
2284 });
2285 }
2286
2287 #[test]
2288 fn http_request_with_zero_retries_returns_transport_error() {
2289 TOKIO_SHARED_RT.block_on(async {
2290 let client = Client::builder()
2291 .proxy(reqwest::Proxy::all("http://127.0.0.1:9").unwrap())
2292 .build()
2293 .unwrap();
2294 let req = client
2295 .request(Method::GET, "http://example.test/unreachable")
2296 .build()
2297 .unwrap();
2298 let mut cfg = make_config("http://example.test");
2299 cfg.client = client;
2300 cfg.retries = 0;
2301
2302 let Err(err) = http_request::<Dummy>(req, &cfg).await else {
2303 panic!("request unexpectedly succeeded")
2304 };
2305
2306 let msg = err.to_string();
2307 assert!(
2308 msg.contains("HTTP request failed"),
2309 "missing top-level marker in: {msg}"
2310 );
2311 assert!(
2312 msg.contains("tcp connect error") || msg.contains("Connection refused"),
2313 "underlying transport cause was not surfaced in: {msg}"
2314 );
2315 });
2316 }
2317
2318 #[test]
2319 fn http_request_client_error_bad_request() {
2320 TOKIO_SHARED_RT.block_on(async {
2321 let server = MockServer::start();
2322 let mock = server.mock(|when, then| {
2323 when.method(httpmock::Method::GET).path("/400");
2324 then.status(400)
2325 .header("Content-Type", "application/json")
2326 .body(r#"{"code":-1121,"msg":"bad request"}"#);
2327 });
2328
2329 let client = Client::new();
2330 let req: Request = client
2331 .request(Method::GET, format!("{}{}", server.url(""), "/400"))
2332 .build()
2333 .unwrap();
2334 let cfg = make_config(&server.url(""));
2335
2336 let result = http_request::<Dummy>(req, &cfg).await;
2337
2338 assert!(matches!(
2339 result,
2340 Err(ConnectorError::BadRequestError { .. })
2341 ));
2342
2343 if let Err(ConnectorError::BadRequestError { msg, code }) = result {
2344 assert_eq!(msg, "bad request");
2345 assert_eq!(code, Some(-1121));
2346 }
2347
2348 mock.assert();
2349 });
2350 }
2351
2352 #[test]
2353 fn http_request_client_error_unauthorized() {
2354 TOKIO_SHARED_RT.block_on(async {
2355 let server = MockServer::start();
2356 let mock = server.mock(|when, then| {
2357 when.method(httpmock::Method::GET).path("/401");
2358 then.status(401)
2359 .header("Content-Type", "application/json")
2360 .body(r#"{"code":-2015,"msg":"unauthorized"}"#);
2361 });
2362
2363 let client = Client::new();
2364 let req: Request = client
2365 .request(Method::GET, format!("{}{}", server.url(""), "/401"))
2366 .build()
2367 .unwrap();
2368 let cfg = make_config(&server.url(""));
2369
2370 let result = http_request::<Dummy>(req, &cfg).await;
2371
2372 assert!(matches!(
2373 result,
2374 Err(ConnectorError::UnauthorizedError { .. })
2375 ));
2376
2377 if let Err(ConnectorError::UnauthorizedError { msg, code }) = result {
2378 assert_eq!(msg, "unauthorized");
2379 assert_eq!(code, Some(-2015));
2380 }
2381
2382 mock.assert();
2383 });
2384 }
2385
2386 #[test]
2387 fn http_request_client_error_forbidden() {
2388 TOKIO_SHARED_RT.block_on(async {
2389 let server = MockServer::start();
2390 let mock = server.mock(|when, then| {
2391 when.method(httpmock::Method::GET).path("/403");
2392 then.status(403)
2393 .header("Content-Type", "application/json")
2394 .body(r#"{"code":-2010,"msg":"forbidden"}"#);
2395 });
2396
2397 let client = Client::new();
2398 let req: Request = client
2399 .request(Method::GET, format!("{}{}", server.url(""), "/403"))
2400 .build()
2401 .unwrap();
2402 let cfg = make_config(&server.url(""));
2403
2404 let result = http_request::<Dummy>(req, &cfg).await;
2405
2406 assert!(matches!(result, Err(ConnectorError::ForbiddenError { .. })));
2407
2408 if let Err(ConnectorError::ForbiddenError { msg, code }) = result {
2409 assert_eq!(msg, "forbidden");
2410 assert_eq!(code, Some(-2010));
2411 }
2412
2413 mock.assert();
2414 });
2415 }
2416
2417 #[test]
2418 fn http_request_client_error_not_found() {
2419 TOKIO_SHARED_RT.block_on(async {
2420 let server = MockServer::start();
2421 let mock = server.mock(|when, then| {
2422 when.method(httpmock::Method::GET).path("/404");
2423 then.status(404)
2424 .header("Content-Type", "application/json")
2425 .body(r#"{"code":-1003,"msg":"not found"}"#);
2426 });
2427
2428 let client = Client::new();
2429 let req: Request = client
2430 .request(Method::GET, format!("{}{}", server.url(""), "/404"))
2431 .build()
2432 .unwrap();
2433 let cfg = make_config(&server.url(""));
2434
2435 let result = http_request::<Dummy>(req, &cfg).await;
2436
2437 assert!(matches!(result, Err(ConnectorError::NotFoundError { .. })));
2438
2439 if let Err(ConnectorError::NotFoundError { msg, code }) = result {
2440 assert_eq!(msg, "not found");
2441 assert_eq!(code, Some(-1003));
2442 }
2443
2444 mock.assert();
2445 });
2446 }
2447
2448 #[test]
2449 fn http_request_client_error_rate_limit_exceeded() {
2450 TOKIO_SHARED_RT.block_on(async {
2451 let server = MockServer::start();
2452 let mock = server.mock(|when, then| {
2453 when.method(httpmock::Method::GET).path("/418");
2454 then.status(418)
2455 .header("Content-Type", "application/json")
2456 .body(r#"{"code":-1003,"msg":"rate limit exceeded"}"#);
2457 });
2458
2459 let client = Client::new();
2460 let req: Request = client
2461 .request(Method::GET, format!("{}{}", server.url(""), "/418"))
2462 .build()
2463 .unwrap();
2464 let cfg = make_config(&server.url(""));
2465
2466 let result = http_request::<Dummy>(req, &cfg).await;
2467
2468 assert!(matches!(
2469 result,
2470 Err(ConnectorError::RateLimitBanError { .. })
2471 ));
2472
2473 if let Err(ConnectorError::RateLimitBanError { msg, code }) = result {
2474 assert_eq!(msg, "rate limit exceeded");
2475 assert_eq!(code, Some(-1003));
2476 }
2477
2478 mock.assert();
2479 });
2480 }
2481
2482 #[test]
2483 fn http_request_client_error_too_many_requests() {
2484 TOKIO_SHARED_RT.block_on(async {
2485 let server = MockServer::start();
2486 let mock = server.mock(|when, then| {
2487 when.method(httpmock::Method::GET).path("/429");
2488 then.status(429)
2489 .header("Content-Type", "application/json")
2490 .body(r#"{"code":-1003,"msg":"too many requests"}"#);
2491 });
2492
2493 let client = Client::new();
2494 let req: Request = client
2495 .request(Method::GET, format!("{}{}", server.url(""), "/429"))
2496 .build()
2497 .unwrap();
2498 let cfg = make_config(&server.url(""));
2499
2500 let result = http_request::<Dummy>(req, &cfg).await;
2501
2502 assert!(matches!(
2503 result,
2504 Err(ConnectorError::TooManyRequestsError { .. })
2505 ));
2506
2507 if let Err(ConnectorError::TooManyRequestsError { msg, code }) = result {
2508 assert_eq!(msg, "too many requests");
2509 assert_eq!(code, Some(-1003));
2510 }
2511
2512 mock.assert();
2513 });
2514 }
2515
2516 #[test]
2517 fn http_request_client_error_server_error() {
2518 TOKIO_SHARED_RT.block_on(async {
2519 let server = MockServer::start();
2520 let mock = server.mock(|when, then| {
2521 when.method(httpmock::Method::GET).path("/500");
2522 then.status(500)
2523 .header("Content-Type", "application/json")
2524 .body(r#"{"code":-1000,"msg":"internal server error"}"#);
2525 });
2526
2527 let client = Client::new();
2528 let req: Request = client
2529 .request(Method::GET, format!("{}{}", server.url(""), "/500"))
2530 .build()
2531 .unwrap();
2532 let cfg = make_config(&server.url(""));
2533
2534 let result = http_request::<Dummy>(req, &cfg).await;
2535
2536 assert!(matches!(result, Err(ConnectorError::ServerError { .. })));
2537
2538 if let Err(ConnectorError::ServerError {
2539 msg,
2540 status_code: Some(500),
2541 }) = result
2542 {
2543 assert_eq!(msg, "Server error: 500".to_string());
2544 }
2545
2546 mock.assert();
2547 });
2548 }
2549
2550 #[test]
2551 fn http_request_unexpected_status_maps_generic() {
2552 TOKIO_SHARED_RT.block_on(async {
2553 let server = MockServer::start();
2554 let code_http = 402;
2555 let mock = server.mock(|when, then| {
2556 when.method(httpmock::Method::GET).path("/402");
2557 then.status(code_http)
2558 .header("Content-Type", "application/json")
2559 .body(r#"{"code":-12345,"msg":"payment required"}"#);
2560 });
2561
2562 let client = Client::new();
2563 let req: Request = client
2564 .request(Method::GET, format!("{}{}", server.url(""), "/402"))
2565 .build()
2566 .unwrap();
2567 let cfg = make_config(&server.url(""));
2568
2569 let result = http_request::<Dummy>(req, &cfg).await;
2570
2571 assert!(matches!(
2572 result,
2573 Err(ConnectorError::ConnectorClientError { .. })
2574 ));
2575
2576 if let Err(ConnectorError::ConnectorClientError { msg, code }) = result {
2577 assert_eq!(msg, "payment required");
2578 assert_eq!(code, Some(-12345));
2579 }
2580
2581 mock.assert();
2582 });
2583 }
2584
2585 #[test]
2586 fn http_request_malformed_json_maps_generic() {
2587 TOKIO_SHARED_RT.block_on(async {
2588 let server = MockServer::start();
2589 let mock = server.mock(|when, then| {
2590 when.method(httpmock::Method::GET).path("/malformed");
2591 then.status(200)
2592 .header("Content-Type", "application/json")
2593 .body("not json");
2594 });
2595
2596 let client = Client::new();
2597 let req: Request = client
2598 .request(Method::GET, format!("{}{}", server.url(""), "/malformed"))
2599 .build()
2600 .unwrap();
2601 let cfg = make_config(&server.url(""));
2602
2603 let resp = http_request::<Dummy>(req, &cfg)
2604 .await
2605 .expect("http_request should succeed even if JSON is bad");
2606
2607 let err = resp
2608 .data()
2609 .await
2610 .expect_err("malformed JSON should turn into ConnectorClientError");
2611
2612 assert!(matches!(err, ConnectorError::ConnectorClientError { .. }));
2613
2614 if let ConnectorError::ConnectorClientError { msg: _, code } = err {
2615 assert_eq!(code, None);
2616 }
2617
2618 mock.assert();
2619 });
2620 }
2621 }
2622
2623 mod send_request {
2624 use anyhow::Result;
2625 use httpmock::prelude::*;
2626 use reqwest::Method;
2627 use serde::Deserialize;
2628 use serde_json::json;
2629 use std::collections::{BTreeMap, HashMap};
2630
2631 use crate::{
2632 common::{models::TimeUnit, utils::send_request},
2633 config::ConfigurationRestApi,
2634 };
2635
2636 use super::TOKIO_SHARED_RT;
2637
2638 #[derive(Deserialize, Debug, PartialEq)]
2639 struct TestResponse {
2640 message: String,
2641 }
2642
2643 #[test]
2644 fn basic_get_request() -> Result<()> {
2645 TOKIO_SHARED_RT.block_on(async {
2646 let server = MockServer::start();
2647
2648 server.mock(|when, then| {
2649 when.method(GET).path("/api/v1/test");
2650 then.status(200)
2651 .header("content-type", "application/json")
2652 .body(r#"{"message": "success"}"#);
2653 });
2654
2655 let configuration = ConfigurationRestApi::builder()
2656 .api_key("key")
2657 .api_secret("secret")
2658 .base_path(server.base_url())
2659 .compression(false)
2660 .build()
2661 .expect("Failed to build configuration");
2662
2663 let params = BTreeMap::new();
2664
2665 let result = send_request::<TestResponse>(
2666 &configuration,
2667 "/api/v1/test",
2668 Method::GET,
2669 params,
2670 BTreeMap::new(),
2671 None,
2672 false,
2673 )
2674 .await?;
2675
2676 let data = result.data().await.unwrap();
2677 assert_eq!(data.message, "success");
2678
2679 Ok(())
2680 })
2681 }
2682
2683 #[test]
2684 fn signed_post_request() -> Result<()> {
2685 TOKIO_SHARED_RT.block_on(async {
2686 let server = MockServer::start();
2687
2688 server.mock(|when, then| {
2689 when.method(POST).path("/api/v3/order");
2690 then.status(200)
2691 .header("content-type", "application/json")
2692 .body(r#"{"message": "order placed"}"#);
2693 });
2694
2695 let configuration = ConfigurationRestApi::builder()
2696 .api_key("key")
2697 .api_secret("secret")
2698 .base_path(server.base_url())
2699 .compression(false)
2700 .build()
2701 .expect("Failed to build configuration");
2702
2703 let mut params = BTreeMap::new();
2704 params.insert("symbol".to_string(), json!("ETHUSDT"));
2705 params.insert("side".to_string(), json!("BUY"));
2706 params.insert("type".to_string(), json!("MARKET"));
2707 params.insert("quantity".to_string(), json!("1"));
2708
2709 let result = send_request::<TestResponse>(
2710 &configuration,
2711 "/api/v3/order",
2712 Method::POST,
2713 params,
2714 BTreeMap::new(),
2715 None,
2716 true,
2717 )
2718 .await?;
2719
2720 let data = result.data().await.unwrap();
2721 assert_eq!(data.message, "order placed");
2722
2723 Ok(())
2724 })
2725 }
2726
2727 #[test]
2728 fn signed_post_request_with_body() -> Result<()> {
2729 TOKIO_SHARED_RT.block_on(async {
2730 let server = MockServer::start();
2731
2732 server.mock(|when, then| {
2733 when.method(POST).path("/api/v3/order");
2734 then.status(200)
2735 .header("content-type", "application/json")
2736 .body(r#"{"message": "order placed"}"#);
2737 });
2738
2739 let configuration = ConfigurationRestApi::builder()
2740 .api_key("key")
2741 .api_secret("secret")
2742 .base_path(server.base_url())
2743 .compression(false)
2744 .build()
2745 .expect("Failed to build configuration");
2746
2747 let mut query_params = BTreeMap::new();
2748 query_params.insert("symbol".to_string(), json!("ETHUSDT"));
2749
2750 let mut body_params = BTreeMap::new();
2751 body_params.insert("side".to_string(), json!("BUY"));
2752 body_params.insert("type".to_string(), json!("MARKET"));
2753 body_params.insert("quantity".to_string(), json!("1"));
2754
2755 let result = send_request::<TestResponse>(
2756 &configuration,
2757 "/api/v3/order",
2758 Method::POST,
2759 query_params,
2760 body_params,
2761 None,
2762 true,
2763 )
2764 .await?;
2765
2766 let data = result.data().await.unwrap();
2767 assert_eq!(data.message, "order placed");
2768
2769 Ok(())
2770 })
2771 }
2772
2773 #[test]
2774 fn get_request_with_params() -> Result<()> {
2775 TOKIO_SHARED_RT.block_on(async {
2776 let server = MockServer::start();
2777
2778 server.mock(|when, then| {
2779 when.method(GET)
2780 .path("/api/v1/data")
2781 .query_param("symbol", "BTCUSDT")
2782 .query_param("limit", "10");
2783 then.status(200)
2784 .header("content-type", "application/json")
2785 .body(r#"{"message": "data retrieved"}"#);
2786 });
2787
2788 let configuration = ConfigurationRestApi::builder()
2789 .api_key("key")
2790 .api_secret("secret")
2791 .base_path(server.base_url())
2792 .compression(false)
2793 .build()
2794 .expect("Failed to build configuration");
2795
2796 let mut params = BTreeMap::new();
2797 params.insert("symbol".to_string(), json!("BTCUSDT"));
2798 params.insert("limit".to_string(), json!(10));
2799
2800 let result = send_request::<TestResponse>(
2801 &configuration,
2802 "/api/v1/data",
2803 Method::GET,
2804 params,
2805 BTreeMap::new(),
2806 None,
2807 false,
2808 )
2809 .await?;
2810
2811 let data = result.data().await.unwrap();
2812 assert_eq!(data.message, "data retrieved");
2813
2814 Ok(())
2815 })
2816 }
2817
2818 #[test]
2819 fn invalid_endpoint() {
2820 TOKIO_SHARED_RT.block_on(async {
2821 let server = MockServer::start();
2822
2823 let configuration = ConfigurationRestApi::builder()
2824 .api_key("key")
2825 .api_secret("secret")
2826 .base_path(server.base_url())
2827 .compression(false)
2828 .build()
2829 .expect("Failed to build configuration");
2830
2831 let params = BTreeMap::new();
2832
2833 let result = send_request::<TestResponse>(
2834 &configuration,
2835 "http://invalid",
2836 Method::GET,
2837 params,
2838 BTreeMap::new(),
2839 None,
2840 false,
2841 )
2842 .await;
2843
2844 assert!(result.is_err());
2845 });
2846 }
2847
2848 #[test]
2849 fn missing_signature_on_signed_request() {
2850 TOKIO_SHARED_RT.block_on(async {
2851 let server = MockServer::start();
2852
2853 let configuration = ConfigurationRestApi::builder()
2854 .api_key("key")
2855 .api_secret("secret")
2856 .base_path(server.base_url())
2857 .compression(false)
2858 .build()
2859 .expect("Failed to build configuration");
2860
2861 let mut params = BTreeMap::new();
2862 params.insert("symbol".to_string(), json!("BTCUSDT"));
2863 params.insert("side".to_string(), json!("BUY"));
2864
2865 let result = send_request::<TestResponse>(
2866 &configuration,
2867 "/api/v3/order",
2868 Method::POST,
2869 params,
2870 BTreeMap::new(),
2871 None,
2872 true,
2873 )
2874 .await;
2875
2876 assert!(result.is_err());
2877 });
2878 }
2879
2880 #[test]
2881 fn compression_enabled() -> Result<()> {
2882 TOKIO_SHARED_RT.block_on(async {
2883 let server = MockServer::start();
2884
2885 server.mock(|when, then| {
2886 when.method(GET).path("/api/v1/test");
2887 then.status(200)
2888 .header("content-type", "application/json")
2889 .header("accept-encoding", "gzip, deflate, br")
2890 .body(r#"{"message": "compression enabled"}"#);
2891 });
2892
2893 let configuration = ConfigurationRestApi::builder()
2894 .api_key("key")
2895 .api_secret("secret")
2896 .base_path(server.base_url())
2897 .compression(true)
2898 .build()
2899 .expect("Failed to build configuration");
2900
2901 let params = BTreeMap::new();
2902
2903 let result = send_request::<TestResponse>(
2904 &configuration,
2905 "/api/v1/test",
2906 Method::GET,
2907 params,
2908 BTreeMap::new(),
2909 None,
2910 false,
2911 )
2912 .await?;
2913
2914 let data = result.data().await.unwrap();
2915 assert_eq!(data.message, "compression enabled");
2916
2917 Ok(())
2918 })
2919 }
2920
2921 #[test]
2922 fn get_request_with_time_unit_header() -> Result<()> {
2923 TOKIO_SHARED_RT.block_on(async {
2924 let server = MockServer::start();
2925
2926 server.mock(|when, then| {
2927 when.method(GET)
2928 .path("/api/v1/test")
2929 .header("X-MBX-TIME-UNIT", "MILLISECOND");
2930 then.status(200)
2931 .header("content-type", "application/json")
2932 .body(r#"{"message": "time unit applied"}"#);
2933 });
2934
2935 let configuration = ConfigurationRestApi::builder()
2936 .api_key("key")
2937 .api_secret("secret")
2938 .base_path(server.base_url())
2939 .compression(false)
2940 .time_unit(TimeUnit::Millisecond)
2941 .build()
2942 .expect("Failed to build configuration");
2943
2944 let params = BTreeMap::new();
2945
2946 let result = send_request::<TestResponse>(
2947 &configuration,
2948 "/api/v1/test",
2949 Method::GET,
2950 params,
2951 BTreeMap::new(),
2952 Some(TimeUnit::Millisecond),
2953 false,
2954 )
2955 .await?;
2956
2957 let data = result.data().await.unwrap();
2958 assert_eq!(data.message, "time unit applied");
2959
2960 Ok(())
2961 })
2962 }
2963
2964 #[test]
2965 fn custom_headers_are_sent() -> Result<()> {
2966 TOKIO_SHARED_RT.block_on(async {
2967 let server = MockServer::start();
2968
2969 server.mock(|when, then| {
2970 when.method(GET)
2971 .path("/api/v1/test")
2972 .header("X-My-Test", "all-clear");
2973 then.status(200)
2974 .header("content-type", "application/json")
2975 .body(r#"{"message":"ok"}"#);
2976 });
2977
2978 let mut custom = HashMap::new();
2979 custom.insert("X-My-Test".to_string(), "all-clear".to_string());
2980
2981 let configuration = ConfigurationRestApi::builder()
2982 .api_key("key")
2983 .api_secret("secret")
2984 .base_path(server.base_url())
2985 .compression(false)
2986 .custom_headers(custom)
2987 .build()
2988 .expect("Failed to build configuration");
2989
2990 let params = BTreeMap::new();
2991 let res = send_request::<TestResponse>(
2992 &configuration,
2993 "/api/v1/test",
2994 Method::GET,
2995 params,
2996 BTreeMap::new(),
2997 None,
2998 false,
2999 )
3000 .await?;
3001
3002 let data = res.data().await.unwrap();
3003 assert_eq!(data.message, "ok");
3004
3005 Ok(())
3006 })
3007 }
3008
3009 #[test]
3010 fn custom_header_override_prevention() -> Result<()> {
3011 TOKIO_SHARED_RT.block_on(async {
3012 let server = MockServer::start();
3013
3014 server.mock(|when, then| {
3015 when.method(GET)
3016 .path("/api/v1/test")
3017 .header("content-type", "application/json")
3018 .header("x-mbx-apikey", "key")
3019 .header("X-My-Test", "ok");
3020 then.status(200)
3021 .header("content-type", "application/json")
3022 .body(r#"{"message":"defaults intact"}"#);
3023 });
3024
3025 let mut custom = HashMap::new();
3026 custom.insert("Content-Type".to_string(), "text/plain".to_string());
3027 custom.insert("X-MBX-APIKEY".to_string(), "BAD".to_string());
3028 custom.insert("X-My-Test".to_string(), "ok".to_string());
3029
3030 let configuration = ConfigurationRestApi::builder()
3031 .api_key("key")
3032 .api_secret("secret")
3033 .base_path(server.base_url())
3034 .compression(false)
3035 .custom_headers(custom)
3036 .build()
3037 .expect("Failed to build configuration");
3038
3039 let params = BTreeMap::new();
3040 let res = send_request::<TestResponse>(
3041 &configuration,
3042 "/api/v1/test",
3043 Method::GET,
3044 params,
3045 BTreeMap::new(),
3046 None,
3047 false,
3048 )
3049 .await?;
3050
3051 let data = res.data().await.unwrap();
3052 assert_eq!(data.message, "defaults intact");
3053
3054 Ok(())
3055 })
3056 }
3057
3058 #[test]
3059 fn crlf_in_header_values_are_dropped() -> Result<()> {
3060 TOKIO_SHARED_RT.block_on(async {
3061 let server = MockServer::start();
3062
3063 server.mock(|when, then| {
3064 when.method(GET)
3065 .path("/api/v1/test")
3066 .header("X-Good", "safe");
3067 then.status(200)
3068 .header("content-type", "application/json")
3069 .body(r#"{"message":"clean only"}"#);
3070 });
3071
3072 let mut custom = HashMap::new();
3073 custom.insert("X-Bad".to_string(), "evil\r\ninject".to_string());
3074 custom.insert("X-Good".to_string(), "safe".to_string());
3075
3076 let configuration = ConfigurationRestApi::builder()
3077 .api_key("key")
3078 .api_secret("secret")
3079 .base_path(server.base_url())
3080 .compression(false)
3081 .custom_headers(custom)
3082 .build()
3083 .expect("Failed to build configuration");
3084
3085 let params = BTreeMap::new();
3086 let res = send_request::<TestResponse>(
3087 &configuration,
3088 "/api/v1/test",
3089 Method::GET,
3090 params,
3091 BTreeMap::new(),
3092 None,
3093 false,
3094 )
3095 .await?;
3096
3097 let data = res.data().await.unwrap();
3098 assert_eq!(data.message, "clean only");
3099
3100 Ok(())
3101 })
3102 }
3103 }
3104
3105 mod random_string {
3106 use crate::common::utils::random_string;
3107 use hex;
3108
3109 #[test]
3110 fn length_is_32() {
3111 let s = random_string();
3112 assert_eq!(
3113 s.len(),
3114 32,
3115 "random_string() should be 32 chars, got {}",
3116 s.len()
3117 );
3118 }
3119
3120 #[test]
3121 fn is_valid_lowercase_hex() {
3122 let s = random_string();
3123 assert!(
3124 s.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f')),
3125 "random_string() contains invalid hex characters: {s}"
3126 );
3127 }
3128
3129 #[test]
3130 fn decodes_to_16_bytes() {
3131 let s = random_string();
3132 let bytes = hex::decode(&s).expect("random_string() output must be valid hex");
3133 assert_eq!(
3134 bytes.len(),
3135 16,
3136 "hex::decode returned {} bytes",
3137 bytes.len()
3138 );
3139 }
3140
3141 #[test]
3142 fn two_calls_are_different() {
3143 let a = random_string();
3144 let b = random_string();
3145 assert_ne!(
3146 a, b,
3147 "Two calls to random_string() returned the same value: {a}"
3148 );
3149 }
3150 }
3151
3152 mod random_integer {
3153 use crate::common::utils::random_integer;
3154
3155 #[test]
3156 fn is_within_u32_range() {
3157 let n = random_integer();
3158 assert!(
3159 n <= u32::MAX,
3160 "random_integer() should be <= u32::MAX, got {n}"
3161 );
3162 }
3163
3164 #[test]
3165 fn two_calls_can_differ() {
3166 let a = random_integer();
3167 let b = random_integer();
3168 assert_ne!(
3169 a, b,
3170 "Two calls to random_integer() returned the same value: {a}"
3171 );
3172 }
3173 }
3174
3175 mod normalize_stream_id {
3176 use crate::common::utils::{StreamId, normalize_stream_id};
3177 use serde_json::Value;
3178
3179 fn is_lower_hex32(s: &str) -> bool {
3180 s.len() == 32 && s.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f'))
3181 }
3182
3183 #[test]
3184 fn valid_hex_string_is_kept() {
3185 let id = "0123456789abcdef0123456789abcdef".to_string();
3186 let out = normalize_stream_id(Some(StreamId::Str(id.clone())), false);
3187
3188 match out {
3189 Value::String(s) => assert_eq!(s, id, "Expected to keep the valid hex id"),
3190 other => panic!("Expected Value::String, got {other:?}"),
3191 }
3192 }
3193
3194 #[test]
3195 fn invalid_hex_string_generates_random_hex() {
3196 let id = "not-hex".to_string();
3197 let out = normalize_stream_id(Some(StreamId::Str(id.clone())), false);
3198
3199 match out {
3200 Value::String(s) => {
3201 assert_eq!(s.len(), 32, "Expected 32-char hex, got {}", s.len());
3202 assert_ne!(s, id, "Expected generated id to differ from input");
3203 assert!(
3204 is_lower_hex32(&s),
3205 "Generated id contains invalid hex characters: {s}"
3206 );
3207 }
3208 other => panic!("Expected Value::String, got {other:?}"),
3209 }
3210 }
3211
3212 #[test]
3213 fn none_generates_random_hex() {
3214 let out = normalize_stream_id(None, false);
3215
3216 match out {
3217 Value::String(s) => {
3218 assert_eq!(s.len(), 32, "Expected 32-char hex, got {}", s.len());
3219 assert!(
3220 is_lower_hex32(&s),
3221 "Generated id contains invalid hex characters: {s}"
3222 );
3223 }
3224 other => panic!("Expected Value::String, got {other:?}"),
3225 }
3226 }
3227
3228 #[test]
3229 fn number_is_kept_when_not_strict() {
3230 let out = normalize_stream_id(Some(StreamId::Number(42)), false);
3231
3232 match out {
3233 Value::Number(n) => {
3234 assert_eq!(n.as_u64(), Some(42), "Expected to keep the numeric id");
3235 }
3236 other => panic!("Expected Value::Number, got {other:?}"),
3237 }
3238 }
3239
3240 #[test]
3241 fn strict_number_forces_number_even_for_valid_hex_string() {
3242 let id = "0123456789abcdef0123456789abcdef".to_string();
3243 let out = normalize_stream_id(Some(StreamId::Str(id)), true);
3244
3245 match out {
3246 Value::Number(n) => {
3247 assert!(
3248 n.as_u64().is_some(),
3249 "Expected unsigned integer JSON number, got {n}"
3250 );
3251 }
3252 other => panic!("Expected Value::Number, got {other:?}"),
3253 }
3254 }
3255
3256 #[test]
3257 fn strict_number_keeps_number_if_provided() {
3258 let out = normalize_stream_id(Some(StreamId::Number(7)), true);
3259
3260 match out {
3261 Value::Number(n) => {
3262 assert_eq!(n.as_u64(), Some(7), "Expected to keep the numeric id");
3263 }
3264 other => panic!("Expected Value::Number, got {other:?}"),
3265 }
3266 }
3267
3268 #[test]
3269 fn strict_number_generates_number_when_none() {
3270 let out = normalize_stream_id(None, true);
3271
3272 match out {
3273 Value::Number(n) => {
3274 assert!(
3275 n.as_u64().is_some(),
3276 "Expected unsigned integer JSON number, got {n}"
3277 );
3278 }
3279 other => panic!("Expected Value::Number, got {other:?}"),
3280 }
3281 }
3282
3283 #[test]
3284 fn strict_number_generates_number_for_invalid_hex_string() {
3285 let out = normalize_stream_id(Some(StreamId::Str("nope".to_string())), true);
3286
3287 match out {
3288 Value::Number(n) => {
3289 assert!(
3290 n.as_u64().is_some(),
3291 "Expected unsigned integer JSON number, got {n}"
3292 );
3293 }
3294 other => panic!("Expected Value::Number, got {other:?}"),
3295 }
3296 }
3297 }
3298 mod remove_empty_value {
3299 use crate::common::utils::remove_empty_value;
3300 use serde_json::{Map, Value};
3301
3302 #[test]
3303 fn filters_out_null_and_empty_strings() {
3304 let entries = vec![
3305 ("key1".to_string(), Value::String("value1".to_string())),
3306 ("key2".to_string(), Value::Null),
3307 ("key3".to_string(), Value::String(String::new())),
3308 ];
3309 let result = remove_empty_value(entries);
3310 assert_eq!(
3311 result.len(),
3312 1,
3313 "expected only one entry, got {}",
3314 result.len()
3315 );
3316 assert_eq!(
3317 result.get("key1"),
3318 Some(&Value::String("value1".to_string()))
3319 );
3320 assert!(!result.contains_key("key2"));
3321 assert!(!result.contains_key("key3"));
3322 }
3323
3324 #[test]
3325 fn retains_other_value_types() {
3326 let entries = vec![
3327 ("bool".to_string(), Value::Bool(true)),
3328 ("num".to_string(), Value::Number(42.into())),
3329 ("arr".to_string(), Value::Array(vec![])),
3330 ("obj".to_string(), Value::Object(Map::default())),
3331 ("nil".to_string(), Value::Null),
3332 ("empty_str".to_string(), Value::String(String::new())),
3333 ];
3334 let result = remove_empty_value(entries);
3335 let keys: Vec<&String> = result.keys().collect();
3336 assert_eq!(keys.len(), 4, "expected 4 entries, got {}", keys.len());
3337 assert!(result.get("bool") == Some(&Value::Bool(true)));
3338 assert!(result.get("num") == Some(&Value::Number(42.into())));
3339 assert!(result.get("arr") == Some(&Value::Array(vec![])));
3340 assert!(result.get("obj") == Some(&Value::Object(Map::default())));
3341 assert!(!result.contains_key("nil"));
3342 assert!(!result.contains_key("empty_str"));
3343 }
3344
3345 #[test]
3346 fn empty_iterator_returns_empty_map() {
3347 let entries: Vec<(String, Value)> = vec![];
3348 let result = remove_empty_value(entries);
3349 assert!(result.is_empty(), "expected an empty map");
3350 }
3351
3352 #[test]
3353 fn keys_are_sorted() {
3354 let entries = vec![
3355 ("c".to_string(), Value::String("foo".to_string())),
3356 ("a".to_string(), Value::String("bar".to_string())),
3357 ("b".to_string(), Value::String("baz".to_string())),
3358 ];
3359 let result = remove_empty_value(entries);
3360 let sorted_keys: Vec<&String> = result.keys().collect();
3361 assert_eq!(
3362 sorted_keys,
3363 [&"a".to_string(), &"b".to_string(), &"c".to_string()]
3364 );
3365 }
3366 }
3367
3368 mod sort_object_params {
3369 use crate::common::utils::sort_object_params;
3370 use serde_json::Value;
3371 use std::collections::BTreeMap;
3372
3373 #[test]
3374 fn sorts_keys() {
3375 let mut params = BTreeMap::new();
3376 params.insert("z".to_string(), Value::String("last".to_string()));
3377 params.insert("a".to_string(), Value::String("first".to_string()));
3378 params.insert("m".to_string(), Value::String("middle".to_string()));
3379
3380 let sorted = sort_object_params(¶ms);
3381 let keys: Vec<&String> = sorted.keys().collect();
3382 assert_eq!(
3383 keys,
3384 [&"a".to_string(), &"m".to_string(), &"z".to_string()],
3385 "Keys should be sorted alphabetically"
3386 );
3387 }
3388
3389 #[test]
3390 fn preserves_values() {
3391 let mut params = BTreeMap::new();
3392 params.insert("one".to_string(), Value::Number(1.into()));
3393 params.insert("two".to_string(), Value::Bool(true));
3394
3395 let sorted = sort_object_params(¶ms);
3396 assert_eq!(sorted.get("one"), Some(&Value::Number(1.into())));
3397 assert_eq!(sorted.get("two"), Some(&Value::Bool(true)));
3398 }
3399
3400 #[test]
3401 fn empty_map_returns_empty() {
3402 let params: BTreeMap<String, Value> = BTreeMap::new();
3403 let sorted = sort_object_params(¶ms);
3404 assert!(sorted.is_empty(), "Expected empty map");
3405 }
3406
3407 #[test]
3408 fn independent_clone() {
3409 let mut params = BTreeMap::new();
3410 params.insert("key".to_string(), Value::String("val".to_string()));
3411
3412 let mut sorted = sort_object_params(¶ms);
3413 sorted.insert("new".to_string(), Value::String("x".to_string()));
3414
3415 assert!(
3416 !params.contains_key("new"),
3417 "Original should not be modified when changing sorted"
3418 );
3419 assert!(
3420 sorted.contains_key("new"),
3421 "Sorted map should reflect its own insertions"
3422 );
3423 }
3424 }
3425
3426 mod normalize_ws_streams_key {
3427 use crate::common::utils::normalize_ws_streams_key;
3428
3429 #[test]
3430 fn returns_empty_for_empty() {
3431 assert_eq!(normalize_ws_streams_key(""), "");
3432 }
3433
3434 #[test]
3435 fn already_normalized_stays_same() {
3436 assert_eq!(normalize_ws_streams_key("streamname"), "streamname");
3437 }
3438
3439 #[test]
3440 fn uppercases_are_lowercased() {
3441 assert_eq!(normalize_ws_streams_key("MyStream"), "mystream");
3442 }
3443
3444 #[test]
3445 fn underscores_are_removed() {
3446 assert_eq!(normalize_ws_streams_key("my_stream_name"), "mystreamname");
3447 }
3448
3449 #[test]
3450 fn hyphens_are_removed() {
3451 assert_eq!(normalize_ws_streams_key("my-stream-name"), "mystreamname");
3452 }
3453
3454 #[test]
3455 fn mixed_underscores_and_hyphens_and_case() {
3456 let input = "Mixed_Case-Stream_Name";
3457 let expected = "mixedcasestreamname";
3458 assert_eq!(normalize_ws_streams_key(input), expected);
3459 }
3460
3461 #[test]
3462 fn retains_other_punctuation() {
3463 assert_eq!(normalize_ws_streams_key("stream.name!"), "stream.name!");
3464 }
3465 }
3466
3467 mod replace_websocket_streams_placeholders {
3468 use crate::common::utils::replace_websocket_streams_placeholders;
3469 use std::collections::HashMap;
3470
3471 #[test]
3472 fn empty_string_unchanged() {
3473 let vars: HashMap<&str, &str> = HashMap::new();
3474 assert_eq!(replace_websocket_streams_placeholders("", &vars), "");
3475 }
3476
3477 #[test]
3478 fn unknown_placeholder_becomes_empty() {
3479 let vars: HashMap<&str, &str> = HashMap::new();
3480 assert_eq!(replace_websocket_streams_placeholders("<foo>", &vars), "");
3481 }
3482
3483 #[test]
3484 fn leading_slash_symbol_lowercases_head() {
3485 let mut vars = HashMap::new();
3486 vars.insert("symbol", "BTC");
3487 assert_eq!(
3488 replace_websocket_streams_placeholders("/<symbol>", &vars),
3489 "btc"
3490 );
3491 }
3492
3493 #[test]
3494 fn no_lowercase_without_slash() {
3495 let mut vars = HashMap::new();
3496 vars.insert("symbol", "BTC");
3497 assert_eq!(
3498 replace_websocket_streams_placeholders("<symbol>", &vars),
3499 "BTC"
3500 );
3501 }
3502
3503 #[test]
3504 fn multiple_placeholders_mid_preserve_ats() {
3505 let mut vars = HashMap::new();
3506 vars.insert("symbol", "BNBUSDT");
3507 vars.insert("levels", "10");
3508 vars.insert("updateSpeed", "1000ms");
3509 let out = replace_websocket_streams_placeholders(
3510 "/<symbol>@depth<levels>@<updateSpeed>",
3511 &vars,
3512 );
3513 assert_eq!(out, "bnbusdt@depth10@1000ms");
3514 }
3515
3516 #[test]
3517 fn trailing_at_removed_when_missing_var() {
3518 let mut vars = HashMap::new();
3519 vars.insert("symbol", "BNBUSDT");
3520 vars.insert("levels", "10");
3521 let out = replace_websocket_streams_placeholders(
3522 "/<symbol>@depth<levels>@<updateSpeed>",
3523 &vars,
3524 );
3525 assert_eq!(out, "bnbusdt@depth10");
3526 }
3527
3528 #[test]
3529 fn custom_key_normalization_and_value() {
3530 let mut vars = HashMap::new();
3531 vars.insert("my-stream_key", "Value");
3532 assert_eq!(
3533 replace_websocket_streams_placeholders("<My_Stream-Key>", &vars),
3534 "Value"
3535 );
3536 }
3537
3538 #[test]
3539 fn text_surrounding_placeholders_intact() {
3540 let mut vars = HashMap::new();
3541 vars.insert("symbol", "ABC");
3542 let input = "pre-<symbol>-post";
3543 assert_eq!(
3544 replace_websocket_streams_placeholders(input, &vars),
3545 "pre-ABC-post"
3546 );
3547 }
3548 }
3549
3550 mod build_websocket_api_message {
3551 use serde_json::{Value, json};
3552 use std::collections::BTreeMap;
3553
3554 use crate::{
3555 common::{
3556 utils::{ID_REGEX, build_websocket_api_message, remove_empty_value},
3557 websocket::WebsocketMessageSendOptions,
3558 },
3559 config::ConfigurationWebsocketApi,
3560 };
3561
3562 fn make_config() -> ConfigurationWebsocketApi {
3563 ConfigurationWebsocketApi::builder()
3564 .api_key("api-key".to_string())
3565 .api_secret("api-secret".to_string())
3566 .build()
3567 .unwrap()
3568 }
3569
3570 #[test]
3571 fn no_auth_or_sign_with_skip_auth() {
3572 let mut payload = BTreeMap::new();
3573 payload.insert("foo".into(), Value::String("bar".into()));
3574 let cfg = make_config();
3575
3576 let (id, req) = build_websocket_api_message(
3577 &cfg,
3578 "method",
3579 payload.clone(),
3580 &WebsocketMessageSendOptions {
3581 with_api_key: true,
3582 is_signed: true,
3583 ..Default::default()
3584 },
3585 true,
3586 );
3587
3588 assert!(ID_REGEX.is_match(&id));
3589 assert_eq!(req["method"], "method");
3590 assert_eq!(req["params"]["foo"], "bar");
3591 assert!(req["params"].get("apiKey").is_none());
3592 assert!(req["params"].get("signature").is_none());
3593 assert!(req["params"]["timestamp"].is_number());
3594 }
3595
3596 #[test]
3597 fn only_api_key_when_not_signed() {
3598 let cfg = make_config();
3599
3600 let (id, req) = build_websocket_api_message(
3601 &cfg,
3602 "method",
3603 BTreeMap::new(),
3604 &WebsocketMessageSendOptions {
3605 with_api_key: true,
3606 is_signed: false,
3607 ..Default::default()
3608 },
3609 false,
3610 );
3611
3612 assert!(ID_REGEX.is_match(&id));
3613 assert_eq!(req["method"], "method");
3614 assert_eq!(req["params"]["apiKey"], "api-key");
3615 assert!(req["params"].get("timestamp").is_none());
3616 assert!(req["params"].get("signature").is_none());
3617 }
3618
3619 #[test]
3620 fn signed_includes_timestamp_and_signature() {
3621 let mut payload = BTreeMap::new();
3622 payload.insert("foo".into(), Value::String("bar".into()));
3623 let cfg = make_config();
3624
3625 let (id, req) = build_websocket_api_message(
3626 &cfg,
3627 "method",
3628 payload.clone(),
3629 &WebsocketMessageSendOptions {
3630 with_api_key: true,
3631 is_signed: true,
3632 ..Default::default()
3633 },
3634 false,
3635 );
3636
3637 assert!(ID_REGEX.is_match(&id));
3638 assert_eq!(req["method"], "method");
3639
3640 let params = &req["params"];
3641 assert_eq!(params["apiKey"], "api-key");
3642
3643 let timestamp = params["timestamp"].as_i64().unwrap();
3644 assert!(timestamp > 0, "timestamp should not be empty");
3645
3646 let sig = params["signature"].as_str().unwrap();
3647 assert!(!sig.is_empty(), "signature should not be empty");
3648 }
3649
3650 #[test]
3651 fn respects_provided_valid_id_and_removes_from_params() {
3652 let mut payload = BTreeMap::new();
3653 let custom = "0123456789abcdef0123456789abcdef".to_string();
3654 payload.insert("id".into(), Value::String(custom.clone()));
3655 payload.insert("foo".into(), Value::Number(123.into()));
3656
3657 let cfg = make_config();
3658 let (id, req) = build_websocket_api_message(
3659 &cfg,
3660 "method",
3661 payload.clone(),
3662 &WebsocketMessageSendOptions::default(),
3663 true,
3664 );
3665
3666 assert_eq!(id, custom);
3667 assert!(req["params"].get("id").is_none());
3668 assert_eq!(req["params"]["foo"], 123);
3669 }
3670
3671 #[test]
3672 fn skip_auth_blocks_api_and_signature_but_keeps_timestamp() {
3673 let mut payload = BTreeMap::new();
3674 payload.insert("foo".into(), Value::String("bar".into()));
3675 let cfg = make_config();
3676
3677 let (_id, req) = build_websocket_api_message(
3678 &cfg,
3679 "method",
3680 payload.clone(),
3681 &WebsocketMessageSendOptions {
3682 with_api_key: true,
3683 is_signed: true,
3684 ..Default::default()
3685 },
3686 true,
3687 );
3688
3689 let p = &req["params"];
3690 assert_eq!(p["foo"], "bar");
3691 assert!(p.get("apiKey").is_none());
3692 assert!(p.get("signature").is_none());
3693 assert!(p["timestamp"].is_number());
3694 }
3695
3696 #[test]
3697 fn random_id_changes_each_call() {
3698 let cfg = make_config();
3699 let (id1, _) = build_websocket_api_message(
3700 &cfg,
3701 "method",
3702 BTreeMap::new(),
3703 &WebsocketMessageSendOptions::default(),
3704 true,
3705 );
3706 let (id2, _) = build_websocket_api_message(
3707 &cfg,
3708 "method",
3709 BTreeMap::new(),
3710 &WebsocketMessageSendOptions::default(),
3711 true,
3712 );
3713 assert!(ID_REGEX.is_match(&id1));
3714 assert!(ID_REGEX.is_match(&id2));
3715 assert_ne!(id1, id2, "IDs should be random and not equal");
3716 }
3717
3718 #[test]
3719 fn null_and_empty_values_are_stripped() {
3720 let mut payload = BTreeMap::new();
3721 payload.insert("a".into(), Value::Null);
3722 payload.insert("b".into(), Value::String(String::new()));
3723 payload.insert("c".into(), Value::String("ok".into()));
3724
3725 let cleaned = remove_empty_value(payload.clone());
3726 assert!(!cleaned.contains_key("a"), "Null should be stripped");
3727 assert!(
3728 !cleaned.contains_key("b"),
3729 "Empty string should be stripped"
3730 );
3731 assert!(cleaned.contains_key("c"), "Non-empty string should be kept");
3732
3733 let cfg = make_config();
3734 let (_id, req) = build_websocket_api_message(
3735 &cfg,
3736 "method",
3737 payload,
3738 &WebsocketMessageSendOptions::default(),
3739 true,
3740 );
3741 let params = &req["params"];
3742 assert!(params.get("a").is_none(), "`a` should not appear");
3743 assert!(params.get("b").is_none(), "`b` should not appear");
3744 assert_eq!(params["c"], "ok", "`c` should be present with value \"ok\"");
3745 }
3746
3747 #[test]
3748 fn provided_invalid_id_gets_replaced() {
3749 let mut payload = BTreeMap::new();
3750 payload.insert("id".into(), Value::String("not-hex-32-chars".into()));
3751 let cfg = make_config();
3752 let (id, _req) = build_websocket_api_message(
3753 &cfg,
3754 "method",
3755 payload,
3756 &WebsocketMessageSendOptions::default(),
3757 true,
3758 );
3759
3760 assert!(ID_REGEX.is_match(&id));
3761 assert_ne!(id, "not-hex-32-chars");
3762 }
3763
3764 #[test]
3765 fn sign_only_includes_api_key_even_when_with_api_key_false() {
3766 let mut payload = BTreeMap::new();
3767 payload.insert("x".into(), json!(1));
3768
3769 let cfg = make_config();
3770 let (_id, req) = build_websocket_api_message(
3771 &cfg,
3772 "method",
3773 payload,
3774 &WebsocketMessageSendOptions {
3775 with_api_key: false,
3776 is_signed: true,
3777 ..Default::default()
3778 },
3779 false,
3780 );
3781 let params = &req["params"];
3782
3783 assert_eq!(params["apiKey"], "api-key");
3784 assert!(params["timestamp"].is_number());
3785 assert!(params["signature"].is_string());
3786 }
3787
3788 #[test]
3789 fn skip_auth_false_without_any_auth_flags() {
3790 let cfg = make_config();
3791 let (_id, req) = build_websocket_api_message(
3792 &cfg,
3793 "method",
3794 BTreeMap::new(),
3795 &WebsocketMessageSendOptions {
3796 with_api_key: false,
3797 is_signed: false,
3798 ..Default::default()
3799 },
3800 false,
3801 );
3802 let params = &req["params"];
3803 assert!(params.as_object().unwrap().is_empty());
3804 }
3805 }
3806}