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