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