Skip to main content

binance_sdk/common/
utils.rs

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