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