1use anyhow::{Context, Result, bail};
2use base64::{Engine as _, engine::general_purpose};
3use ed25519_dalek::Signer as Ed25519Signer;
4use ed25519_dalek::SigningKey;
5use ed25519_dalek::pkcs8::DecodePrivateKey;
6use flate2::read::GzDecoder;
7use hex;
8use hmac::{Hmac, Mac};
9use http::HeaderMap;
10use http::header::ACCEPT_ENCODING;
11use once_cell::sync::OnceCell;
12use openssl::{hash::MessageDigest, pkey::PKey, sign::Signer as OpenSslSigner};
13use rand::RngCore;
14use regex::Captures;
15use regex::Regex;
16use reqwest::Client;
17use reqwest::Proxy;
18use reqwest::{Method, Request};
19use serde::de::DeserializeOwned;
20use serde_json::{Value, json};
21use sha2::Sha256;
22use std::fmt::Display;
23use std::hash::BuildHasher;
24use std::sync::LazyLock;
25use std::{
26 collections::BTreeMap,
27 collections::HashMap,
28 fs,
29 io::Read,
30 path::Path,
31 time::Duration,
32 time::{SystemTime, UNIX_EPOCH},
33};
34use tokio::time::sleep;
35use tracing::info;
36use url::{Url, form_urlencoded::Serializer};
37
38use super::config::HttpAgent;
39use super::config::ProxyConfig;
40use super::config::{ConfigurationRestApi, PrivateKey};
41use super::errors::ConnectorError;
42use super::models::TimeUnit;
43use super::models::{Interval, RateLimitType, RestApiRateLimit, RestApiResponse};
44
45static PLACEHOLDER_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(@)?<([^>]+)>").unwrap());
46
47#[derive(Debug, Default, Clone)]
62pub struct SignatureGenerator {
63 api_secret: Option<String>,
64 private_key: Option<PrivateKey>,
65 private_key_passphrase: Option<String>,
66 raw_key_data: OnceCell<String>,
67 key_object: OnceCell<PKey<openssl::pkey::Private>>,
68 ed25519_signing_key: OnceCell<SigningKey>,
69}
70
71impl SignatureGenerator {
72 #[must_use]
73 pub fn new(
74 api_secret: Option<String>,
75 private_key: Option<PrivateKey>,
76 private_key_passphrase: Option<String>,
77 ) -> Self {
78 SignatureGenerator {
79 api_secret,
80 private_key,
81 private_key_passphrase,
82 raw_key_data: OnceCell::new(),
83 key_object: OnceCell::new(),
84 ed25519_signing_key: OnceCell::new(),
85 }
86 }
87
88 fn get_raw_key_data(&self) -> Result<&String> {
103 self.raw_key_data.get_or_try_init(|| {
104 let pk = self
105 .private_key
106 .as_ref()
107 .ok_or_else(|| anyhow::anyhow!("No private_key provided"))?;
108 match pk {
109 PrivateKey::File(path) => {
110 if Path::new(path).exists() {
111 fs::read_to_string(path)
112 .with_context(|| format!("Failed to read private key file: {path}"))
113 } else {
114 Err(anyhow::anyhow!("Private key file does not exist: {}", path))
115 }
116 }
117 PrivateKey::Raw(bytes) => Ok(String::from_utf8_lossy(bytes).to_string()),
118 }
119 })
120 }
121
122 fn get_key_object(&self) -> Result<&PKey<openssl::pkey::Private>> {
137 self.key_object.get_or_try_init(|| {
138 let key_data = self.get_raw_key_data()?;
139 if let Some(pass) = self.private_key_passphrase.as_ref() {
140 PKey::private_key_from_pem_passphrase(key_data.as_bytes(), pass.as_bytes())
141 .context("Failed to parse private key with passphrase")
142 } else {
143 PKey::private_key_from_pem(key_data.as_bytes())
144 .context("Failed to parse private key")
145 }
146 })
147 }
148
149 fn get_ed25519_signing_key(&self) -> Result<&SigningKey> {
162 self.ed25519_signing_key.get_or_try_init(|| {
163 let key_data = self.get_raw_key_data()?;
164 let b64 = key_data
165 .lines()
166 .filter(|l| !l.starts_with("-----"))
167 .collect::<String>();
168 let der = general_purpose::STANDARD
169 .decode(b64)
170 .context("Failed to base64 decode Ed25519 PEM")?;
171 SigningKey::from_pkcs8_der(&der)
172 .map_err(|e| anyhow::anyhow!("Failed to parse Ed25519 key: {}", e))
173 })
174 }
175
176 pub fn get_signature(&self, query_params: &BTreeMap<String, Value>) -> Result<String> {
199 let params = build_query_string(query_params)?;
200
201 if let Some(secret) = self.api_secret.as_ref() {
202 if self.private_key.is_none() {
203 let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
204 .context("HMAC key initialization failed")?;
205 mac.update(params.as_bytes());
206 let result = mac.finalize().into_bytes();
207 return Ok(hex::encode(result));
208 }
209 }
210
211 if self.private_key.is_some() {
212 let key_obj = self.get_key_object()?;
213 match key_obj.id() {
214 openssl::pkey::Id::RSA => {
215 let mut signer = OpenSslSigner::new(MessageDigest::sha256(), key_obj)
216 .context("Failed to create RSA signer")?;
217 signer
218 .update(params.as_bytes())
219 .context("Failed to update RSA signer")?;
220 let sig = signer.sign_to_vec().context("RSA signing failed")?;
221 return Ok(general_purpose::STANDARD.encode(sig));
222 }
223 openssl::pkey::Id::ED25519 => {
224 let signing_key = self.get_ed25519_signing_key()?;
225 let signature = signing_key.sign(params.as_bytes());
226 return Ok(general_purpose::STANDARD.encode(signature.to_bytes()));
227 }
228 other => {
229 return Err(anyhow::anyhow!(
230 "Unsupported private key type: {:?}. Must be RSA or ED25519.",
231 other
232 ));
233 }
234 }
235 }
236
237 Err(anyhow::anyhow!(
238 "Either 'api_secret' or 'private_key' must be provided for signed requests."
239 ))
240 }
241}
242
243#[must_use]
266pub fn build_client(
267 timeout: u64,
268 keep_alive: bool,
269 proxy: Option<&ProxyConfig>,
270 agent: Option<HttpAgent>,
271) -> Client {
272 let builder = Client::builder().timeout(Duration::from_millis(timeout));
273
274 let mut builder = if keep_alive {
275 builder
276 } else {
277 builder.pool_idle_timeout(Some(Duration::from_secs(0)))
278 };
279
280 if let Some(proxy_conf) = proxy {
281 let protocol = proxy_conf
282 .protocol
283 .clone()
284 .unwrap_or_else(|| "http".to_string());
285 let proxy_url = format!("{}://{}:{}", protocol, proxy_conf.host, proxy_conf.port);
286 let mut proxy_builder = Proxy::all(&proxy_url).expect("Failed to create proxy from URL");
287 if let Some(auth) = &proxy_conf.auth {
288 proxy_builder = proxy_builder.basic_auth(&auth.username, &auth.password);
289 }
290 builder = builder.proxy(proxy_builder);
291 }
292
293 if let Some(HttpAgent(agent_fn)) = agent {
294 builder = (agent_fn)(builder);
295 }
296
297 info!("Client builder {:?}", builder);
298
299 builder.build().expect("Failed to build reqwest client")
300}
301
302#[must_use]
316pub fn build_user_agent() -> String {
317 format!(
318 "{}/{} (Rust/{}; {}; {})",
319 env!("CARGO_PKG_NAME"),
320 env!("CARGO_PKG_VERSION"),
321 env!("RUSTC_VERSION"),
322 std::env::consts::OS,
323 std::env::consts::ARCH
324 )
325}
326
327pub fn validate_time_unit(time_unit: &str) -> Result<Option<&str>, anyhow::Error> {
355 match time_unit {
356 "" => Ok(None),
357 "MILLISECOND" | "MICROSECOND" | "millisecond" | "microsecond" => Ok(Some(time_unit)),
358 _ => Err(anyhow::anyhow!(
359 "time_unit must be either 'MILLISECOND' or 'MICROSECOND'"
360 )),
361 }
362}
363
364#[must_use]
381pub fn get_timestamp() -> u128 {
382 SystemTime::now()
383 .duration_since(UNIX_EPOCH)
384 .expect("Time went backwards")
385 .as_millis()
386}
387
388pub async fn delay(ms: u64) {
400 sleep(Duration::from_millis(ms)).await;
401}
402
403pub fn build_query_string(params: &BTreeMap<String, Value>) -> Result<String, anyhow::Error> {
422 let mut segments = Vec::with_capacity(params.len());
423
424 for (key, value) in params {
425 match value {
426 Value::Null => {}
427 Value::String(s) => {
428 let mut ser = Serializer::new(String::new());
429 ser.append_pair(key, s);
430 segments.push(ser.finish());
431 }
432 Value::Bool(b) => {
433 let val = b.to_string();
434 let mut ser = Serializer::new(String::new());
435 ser.append_pair(key, &val);
436 segments.push(ser.finish());
437 }
438 Value::Number(n) => {
439 let val = n.to_string();
440 let mut ser = Serializer::new(String::new());
441 ser.append_pair(key, &val);
442 segments.push(ser.finish());
443 }
444 Value::Array(arr)
445 if arr
446 .iter()
447 .all(|v| matches!(v, Value::String(_) | Value::Bool(_) | Value::Number(_))) =>
448 {
449 let mut parts = Vec::with_capacity(arr.len());
450 for v in arr {
451 match v {
452 Value::String(s) => parts.push(s.clone()),
453 Value::Bool(b) => parts.push(b.to_string()),
454 Value::Number(n) => parts.push(n.to_string()),
455 _ => unreachable!(),
456 }
457 }
458 segments.push(format!("{}={}", key, parts.join(",")));
459 }
460 Value::Array(arr) => {
461 let json =
462 serde_json::to_string(arr).context("Failed to JSON-serialize nested array")?;
463 let mut ser = Serializer::new(String::new());
464 ser.append_pair(key, &json);
465 segments.push(ser.finish());
466 }
467 Value::Object(_) => {
468 bail!("Cannot serialize object for key `{}` in query params", key);
469 }
470 }
471 }
472
473 Ok(segments.join("&"))
474}
475
476#[must_use]
484pub fn should_retry_request(
485 error: &reqwest::Error,
486 method: Option<&str>,
487 retries_left: Option<usize>,
488) -> bool {
489 let method = method.unwrap_or("");
490 let is_retriable_method =
491 method.eq_ignore_ascii_case("GET") || method.eq_ignore_ascii_case("DELETE");
492
493 let status = error.status().map_or(0, |s| s.as_u16());
494 let is_retriable_status = [500, 502, 503, 504].contains(&status);
495
496 let retries_left = retries_left.unwrap_or(0);
497 retries_left > 0 && is_retriable_method && (is_retriable_status || error.status().is_none())
498}
499
500#[must_use]
525pub fn parse_rate_limit_headers<S>(headers: &HashMap<String, String, S>) -> Vec<RestApiRateLimit>
526where
527 S: BuildHasher,
528{
529 let mut rate_limits = Vec::new();
530 let re = Regex::new(r"x-mbx-(used-weight|order-count)-(\d+)([smhd])").unwrap();
531 for (key, value) in headers {
532 let normalized_key = key.to_lowercase();
533 if normalized_key.starts_with("x-mbx-used-weight-")
534 || normalized_key.starts_with("x-mbx-order-count-")
535 {
536 if let Some(caps) = re.captures(&normalized_key) {
537 let interval_num: u32 = caps.get(2).unwrap().as_str().parse().unwrap_or(0);
538 let interval_letter = caps.get(3).unwrap().as_str().to_uppercase();
539 let interval = match interval_letter.as_str() {
540 "S" => Interval::Second,
541 "M" => Interval::Minute,
542 "H" => Interval::Hour,
543 "D" => Interval::Day,
544 _ => continue,
545 };
546 let count: u32 = value.parse().unwrap_or(0);
547 let rate_limit_type = if normalized_key.starts_with("x-mbx-used-weight-") {
548 RateLimitType::RequestWeight
549 } else {
550 RateLimitType::Orders
551 };
552 rate_limits.push(RestApiRateLimit {
553 rate_limit_type,
554 interval,
555 interval_num,
556 count,
557 retry_after: headers.get("retry-after").and_then(|v| v.parse().ok()),
558 });
559 }
560 }
561 }
562 rate_limits
563}
564
565pub async fn http_request<T: DeserializeOwned + Send + 'static>(
595 req: Request,
596 configuration: &ConfigurationRestApi,
597) -> Result<RestApiResponse<T>, ConnectorError> {
598 let client = &configuration.client;
599 let retries = configuration.retries as usize;
600 let backoff = configuration.backoff;
601 let mut attempt = 0;
602
603 loop {
604 let req_clone = req
605 .try_clone()
606 .context("Failed to clone request")
607 .map_err(|e| ConnectorError::ConnectorClientError(e.to_string()))?;
608 match client.execute(req_clone).await {
609 Ok(response) => {
610 let status = response.status();
611 let headers_map: HashMap<String, String> = response
612 .headers()
613 .iter()
614 .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
615 .collect();
616
617 let raw_bytes = match response.bytes().await {
618 Ok(b) => b,
619 Err(e) => {
620 attempt += 1;
621 if attempt <= retries {
622 continue;
623 }
624 return Err(ConnectorError::ConnectorClientError(format!(
625 "Failed to get response bytes: {e}"
626 )));
627 }
628 };
629
630 let content = if headers_map
631 .get("content-encoding")
632 .is_some_and(|enc| enc.to_lowercase().contains("gzip"))
633 {
634 let mut decoder = GzDecoder::new(&raw_bytes[..]);
635 let mut decompressed = String::new();
636 decoder
637 .read_to_string(&mut decompressed)
638 .context("Failed to decompress gzip response")
639 .map_err(|e| ConnectorError::ConnectorClientError(e.to_string()))?;
640 decompressed
641 } else {
642 String::from_utf8(raw_bytes.to_vec())
643 .context("Failed to convert response to UTF-8")
644 .map_err(|e| ConnectorError::ConnectorClientError(e.to_string()))?
645 };
646
647 let rate_limits = parse_rate_limit_headers(&headers_map);
648
649 if status.is_client_error() || status.is_server_error() {
650 let error_msg = serde_json::from_str::<serde_json::Value>(&content)
651 .ok()
652 .and_then(|v| {
653 v.get("msg")
654 .and_then(|m| m.as_str())
655 .map(std::string::ToString::to_string)
656 })
657 .unwrap_or_else(|| content.clone());
658
659 match status.as_u16() {
660 400 => return Err(ConnectorError::BadRequestError(error_msg)),
661 401 => return Err(ConnectorError::UnauthorizedError(error_msg)),
662 403 => return Err(ConnectorError::ForbiddenError(error_msg)),
663 404 => return Err(ConnectorError::NotFoundError(error_msg)),
664 418 => return Err(ConnectorError::RateLimitBanError(error_msg)),
665 429 => return Err(ConnectorError::TooManyRequestsError(error_msg)),
666 s if (500..600).contains(&s) => {
667 return Err(ConnectorError::ServerError {
668 msg: format!("Server error: {s}"),
669 status_code: Some(s),
670 });
671 }
672 _ => return Err(ConnectorError::ConnectorClientError(error_msg)),
673 }
674 }
675
676 let raw = content.clone();
677 return Ok(RestApiResponse {
678 data_fn: Box::new(move || {
679 Box::pin(async move {
680 let parsed: T = serde_json::from_str(&raw)
681 .map_err(|e| ConnectorError::ConnectorClientError(e.to_string()))?;
682 Ok(parsed)
683 })
684 }),
685 status: status.as_u16(),
686 headers: headers_map,
687 rate_limits: if rate_limits.is_empty() {
688 None
689 } else {
690 Some(rate_limits)
691 },
692 });
693 }
694 Err(e) => {
695 attempt += 1;
696 if should_retry_request(&e, Some(req.method().as_str()), Some(retries - attempt)) {
697 delay(backoff * attempt as u64).await;
698 continue;
699 }
700 return Err(ConnectorError::ConnectorClientError(format!(
701 "HTTP request failed: {e}"
702 )));
703 }
704 }
705 }
706}
707
708pub async fn send_request<T: DeserializeOwned + Send + 'static>(
734 configuration: &ConfigurationRestApi,
735 endpoint: &str,
736 method: Method,
737 mut params: BTreeMap<String, Value>,
738 time_unit: Option<TimeUnit>,
739 is_signed: bool,
740) -> anyhow::Result<RestApiResponse<T>> {
741 let base = configuration.base_path.as_deref().unwrap_or("");
742 let full_url = reqwest::Url::parse(base)
743 .and_then(|u| u.join(endpoint))
744 .context("Failed to join base URL and endpoint")?
745 .to_string();
746
747 if is_signed {
748 let timestamp = get_timestamp();
749 params.insert("timestamp".to_string(), json!(timestamp));
750 let signature = configuration.signature_gen.get_signature(¶ms)?;
751 params.insert("signature".to_string(), Value::String(signature));
752 }
753
754 let mut url = Url::parse(&full_url)?;
755 {
756 let mut pairs = url.query_pairs_mut();
757 for (key, value) in ¶ms {
758 let val_str = match value {
759 Value::String(s) => s.clone(),
760 _ => value.to_string(),
761 };
762 pairs.append_pair(key, &val_str);
763 }
764 }
765
766 let mut headers = HeaderMap::new();
767 headers.insert("Content-Type", "application/json".parse().unwrap());
768 headers.insert("User-Agent", configuration.user_agent.parse().unwrap());
769 if let Some(api_key) = &configuration.api_key {
770 headers.insert("X-MBX-APIKEY", api_key.parse().unwrap());
771 }
772
773 if configuration.compression {
774 headers.insert(ACCEPT_ENCODING, "gzip, deflate, br".parse().unwrap());
775 }
776
777 let time_unit_to_apply = time_unit.or(configuration.time_unit);
778 if let Some(time_unit) = time_unit_to_apply {
779 headers.insert("X-MBX-TIME-UNIT", time_unit.as_upper_str().parse()?);
780 }
781
782 let req_builder = configuration.client.request(method, url).headers(headers);
783 let req = req_builder.build()?;
784
785 Ok(http_request::<T>(req, configuration).await?)
786}
787
788#[must_use]
797pub fn random_string() -> String {
798 let mut buf = [0u8; 16];
799 rand::thread_rng().fill_bytes(&mut buf);
800 hex::encode(buf)
801}
802
803pub fn remove_empty_value<I>(entries: I) -> BTreeMap<String, Value>
825where
826 I: IntoIterator<Item = (String, Value)>,
827{
828 entries
829 .into_iter()
830 .filter(|(_, value)| match value {
831 Value::Null => false,
832 Value::String(s) if s.is_empty() => false,
833 _ => true,
834 })
835 .collect()
836}
837
838#[must_use]
859pub fn sort_object_params(params: &BTreeMap<String, Value>) -> BTreeMap<String, Value> {
860 let mut sorted = BTreeMap::new();
861 for (k, v) in params {
862 sorted.insert(k.clone(), v.clone());
863 }
864 sorted
865}
866
867fn normalize_ws_streams_key(key: &str) -> String {
877 key.to_lowercase().replace(&['_', '-'][..], "")
878}
879
880pub fn replace_websocket_streams_placeholders<V, S>(
905 input: &str,
906 variables: &HashMap<&str, V, S>,
907) -> String
908where
909 V: Display,
910 S: BuildHasher,
911{
912 let original = input;
913
914 let body = original.strip_prefix('/').unwrap_or(original);
916
917 let normalized: HashMap<String, String> = variables
919 .iter()
920 .map(|(k, v)| (normalize_ws_streams_key(k), v.to_string()))
921 .collect();
922
923 let replaced = PLACEHOLDER_RE
925 .replace_all(body, |caps: &Captures| {
926 let prefix = caps.get(1).map_or("", |m| m.as_str());
927 let key = normalize_ws_streams_key(caps.get(2).unwrap().as_str());
928 let val = normalized.get(&key).cloned().unwrap_or_default();
929 format!("{prefix}{val}")
930 })
931 .into_owned();
932
933 let stripped = replaced.trim_end_matches('@').to_string();
935
936 let should_lower_head =
939 original.starts_with('/') && PLACEHOLDER_RE.find(body).is_some_and(|m| m.start() == 0);
940
941 let result = if should_lower_head {
943 if let Some(caps) = PLACEHOLDER_RE.captures(body) {
944 let key = normalize_ws_streams_key(caps.get(2).unwrap().as_str());
945 let first_val = normalized.get(&key).cloned().unwrap_or_default();
946 if stripped.starts_with(&first_val) {
947 let tail = &stripped[first_val.len()..];
948 format!("{}{}", first_val.to_lowercase(), tail)
949 } else {
950 stripped.clone()
951 }
952 } else {
953 stripped.clone()
954 }
955 } else {
956 stripped.clone()
957 };
958
959 result
960}
961
962#[cfg(test)]
963mod tests {
964 use crate::TOKIO_SHARED_RT;
965
966 mod build_client {
967 use std::{
968 sync::{Arc, Mutex},
969 time::{Duration, Instant},
970 };
971
972 use reqwest::ClientBuilder;
973
974 use crate::{
975 common::utils::build_client,
976 config::{HttpAgent, ProxyAuth, ProxyConfig},
977 };
978
979 use super::TOKIO_SHARED_RT;
980
981 #[test]
982 fn enforces_timeout() {
983 TOKIO_SHARED_RT.block_on(async {
984 let client = build_client(100, true, None, None);
985 let start = Instant::now();
986 let res = client.get("http://10.255.255.1").send().await;
987 assert!(
988 res.is_err(),
989 "expected an error (timeout or connect) but got {res:?}"
990 );
991 let elapsed = start.elapsed();
992 assert!(
993 elapsed < Duration::from_millis(500),
994 "timed out too slowly: {elapsed:?}"
995 );
996 });
997 }
998
999 #[test]
1000 fn builds_with_keep_alive_disabled() {
1001 let client = build_client(200, false, None, None);
1002 let _: reqwest::Client = client;
1003 }
1004
1005 #[test]
1006 #[should_panic(expected = "Failed to create proxy from URL")]
1007 fn invalid_proxy_url_panics() {
1008 let bad_proxy = ProxyConfig {
1009 protocol: Some("http".to_string()),
1010 host: String::new(),
1011 port: 8080,
1012 auth: None,
1013 };
1014 let _ = build_client(1_000, true, Some(&bad_proxy), None);
1015 }
1016
1017 #[test]
1018 fn builds_with_proxy_and_auth() {
1019 let proxy = ProxyConfig {
1020 protocol: Some("https".to_string()),
1021 host: "127.0.0.1".to_string(),
1022 port: 3128,
1023 auth: Some(ProxyAuth {
1024 username: "alice".to_string(),
1025 password: "secret".to_string(),
1026 }),
1027 };
1028 let client = build_client(2_000, true, Some(&proxy), None);
1029 let _: reqwest::Client = client;
1030 }
1031
1032 #[test]
1033 fn custom_agent_invoked() {
1034 let called = Arc::new(Mutex::new(false));
1035 let called_clone = Arc::clone(&called);
1036
1037 let agent = HttpAgent(Arc::new(move |builder: ClientBuilder| {
1038 *called_clone.lock().unwrap() = true;
1039 builder
1040 }));
1041
1042 let client = build_client(1_000, true, None, Some(agent));
1043 assert!(*called.lock().unwrap(), "agent closure wasn’t invoked");
1044 let _: reqwest::Client = client;
1045 }
1046 }
1047
1048 mod build_user_agent {
1049 use crate::common::utils::build_user_agent;
1050
1051 #[test]
1052 fn build_user_agent_contains_crate_and_rust_info() {
1053 let ua = build_user_agent();
1054 let name = env!("CARGO_PKG_NAME");
1055 let version = env!("CARGO_PKG_VERSION");
1056 let rustc = env!("RUSTC_VERSION");
1057 let os = std::env::consts::OS;
1058 let arch = std::env::consts::ARCH;
1059
1060 let expected_prefix = format!("{name}/{version} (Rust/");
1061 assert!(ua.starts_with(&expected_prefix), "prefix mismatch: {ua}");
1062
1063 assert!(ua.contains(rustc), "user agent missing RUSTC_VERSION: {ua}");
1064
1065 let expected_os = format!("; {os}");
1066 let expected_arch = format!("; {arch}");
1067 assert!(ua.contains(&expected_os), "user agent missing OS: {ua}");
1068 assert!(ua.contains(&expected_arch), "user agent missing ARCH: {ua}");
1069
1070 assert!(ua.ends_with(')'), "missing trailing ')': {ua}");
1071 }
1072
1073 #[test]
1074 fn build_user_agent_is_deterministic() {
1075 let ua1 = build_user_agent();
1076 let ua2 = build_user_agent();
1077 assert_eq!(ua1, ua2, "user agent should be the same on repeated calls");
1078 }
1079 }
1080
1081 mod validate_time_unit {
1082 use crate::common::utils::validate_time_unit;
1083
1084 #[test]
1085 fn empty_string_returns_none() {
1086 let res = validate_time_unit("").expect("Should not error on empty string");
1087 assert_eq!(res, None);
1088 }
1089
1090 #[test]
1091 fn uppercase_millisecond() {
1092 let res = validate_time_unit("MILLISECOND").expect("Should accept MILLISECOND");
1093 assert_eq!(res, Some("MILLISECOND"));
1094 }
1095
1096 #[test]
1097 fn uppercase_microsecond() {
1098 let res = validate_time_unit("MICROSECOND").expect("Should accept MICROSECOND");
1099 assert_eq!(res, Some("MICROSECOND"));
1100 }
1101
1102 #[test]
1103 fn lowercase_millisecond() {
1104 let res = validate_time_unit("millisecond").expect("Should accept millisecond");
1105 assert_eq!(res, Some("millisecond"));
1106 }
1107
1108 #[test]
1109 fn lowercase_microsecond() {
1110 let res = validate_time_unit("microsecond").expect("Should accept microsecond");
1111 assert_eq!(res, Some("microsecond"));
1112 }
1113
1114 #[test]
1115 fn invalid_value_returns_err() {
1116 let err = validate_time_unit("SECOND").unwrap_err();
1117 let msg = format!("{err}");
1118 assert!(msg.contains("time_unit must be either 'MILLISECOND' or 'MICROSECOND'"));
1119 }
1120
1121 #[test]
1122 fn partial_match_returns_err() {
1123 let err = validate_time_unit("MILLI").unwrap_err();
1124 let msg = format!("{err}");
1125 assert!(msg.contains("time_unit must be either 'MILLISECOND' or 'MICROSECOND'"));
1126 }
1127 }
1128
1129 mod get_timestamp {
1130 use crate::common::utils::get_timestamp;
1131 use std::{
1132 thread::sleep,
1133 time::{Duration, SystemTime, UNIX_EPOCH},
1134 };
1135
1136 #[test]
1137 fn timestamp_is_within_system_time_bounds() {
1138 let before = SystemTime::now()
1139 .duration_since(UNIX_EPOCH)
1140 .expect("SystemTime before UNIX_EPOCH")
1141 .as_millis();
1142 let ts = get_timestamp();
1143 let after = SystemTime::now()
1144 .duration_since(UNIX_EPOCH)
1145 .expect("SystemTime before UNIX_EPOCH")
1146 .as_millis();
1147
1148 assert!(
1149 ts >= before,
1150 "timestamp {ts} is before captured before time {before}"
1151 );
1152 assert!(
1153 ts <= after,
1154 "timestamp {ts} is after captured after time {after}"
1155 );
1156 }
1157
1158 #[test]
1159 fn timestamps_are_monotonic() {
1160 let t1 = get_timestamp();
1161 sleep(Duration::from_millis(1));
1162 let t2 = get_timestamp();
1163 assert!(
1164 t2 >= t1,
1165 "second timestamp {t2} is not >= first timestamp {t1}"
1166 );
1167 }
1168 }
1169
1170 mod build_query_string {
1171 use std::collections::BTreeMap;
1172
1173 use anyhow::Result;
1174 use serde_json::{Value, json};
1175 use url::form_urlencoded::Serializer;
1176
1177 use crate::common::utils::build_query_string;
1178
1179 fn mk_map(pairs: Vec<(&str, Value)>) -> BTreeMap<String, Value> {
1180 let mut m = BTreeMap::new();
1181 for (k, v) in pairs {
1182 m.insert(k.to_string(), v);
1183 }
1184 m
1185 }
1186
1187 #[test]
1188 fn empty_map_returns_empty_string() -> Result<()> {
1189 let params = BTreeMap::new();
1190 let qs = build_query_string(¶ms)?;
1191 assert_eq!(qs, "");
1192 Ok(())
1193 }
1194
1195 #[test]
1196 fn string_and_number() -> Result<()> {
1197 let params = mk_map(vec![("foo", json!("bar")), ("num", json!(42))]);
1198 let qs = build_query_string(¶ms)?;
1199 assert_eq!(qs, "foo=bar&num=42");
1200 Ok(())
1201 }
1202
1203 #[test]
1204 fn bool_and_null_skipped() -> Result<()> {
1205 let params = mk_map(vec![("a", json!(true)), ("b", Value::Null)]);
1206 let qs = build_query_string(¶ms)?;
1207 assert_eq!(qs, "a=true");
1208 Ok(())
1209 }
1210
1211 #[test]
1212 fn flat_array() -> Result<()> {
1213 let params = mk_map(vec![("list", json!(vec!["x", "y", "z"]))]);
1214 let qs = build_query_string(¶ms)?;
1215 assert_eq!(qs, "list=x,y,z");
1216 Ok(())
1217 }
1218
1219 #[test]
1220 fn nested_array_json_encoded() -> Result<()> {
1221 let params = mk_map(vec![("nested", json!([[1, 2], [3, 4]]))]);
1222 let qs = build_query_string(¶ms)?;
1223
1224 let nested_json = serde_json::to_string(&json!([[1, 2], [3, 4]]))?;
1225 let mut ser = Serializer::new(String::new());
1226 ser.append_pair("nested", &nested_json);
1227 let expected = ser.finish();
1228
1229 assert_eq!(qs, expected);
1230 Ok(())
1231 }
1232
1233 #[test]
1234 fn object_not_supported() {
1235 let params = mk_map(vec![("obj", json!({"k":1}))]);
1236 let err = build_query_string(¶ms).unwrap_err();
1237 let msg = format!("{err}");
1238 assert!(msg.contains("Cannot serialize object for key `obj`"));
1239 }
1240 }
1241
1242 mod signature_generator {
1243 use base64::{Engine, engine::general_purpose};
1244 use ed25519_dalek::{SigningKey, ed25519::signature::SignerMut, pkcs8::DecodePrivateKey};
1245 use hex;
1246 use hmac::{Hmac, Mac};
1247 use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa, sign::Verifier};
1248 use serde_json::Value;
1249 use sha2::Sha256;
1250 use std::collections::BTreeMap;
1251 use std::io::Write;
1252 use tempfile::NamedTempFile;
1253
1254 use crate::{common::utils::SignatureGenerator, config::PrivateKey};
1255
1256 #[test]
1257 fn hmac_sha256_signature() {
1258 let mut params = BTreeMap::new();
1259 params.insert("b".into(), Value::Number(2.into()));
1260 params.insert("a".into(), Value::Number(1.into()));
1261
1262 let signature_gen = SignatureGenerator::new(Some("test-secret".into()), None, None);
1263 let sig = signature_gen
1264 .get_signature(¶ms)
1265 .expect("HMAC signing failed");
1266
1267 let mut mac = Hmac::<Sha256>::new_from_slice(b"test-secret").unwrap();
1268 let qs = "a=1&b=2";
1269 mac.update(qs.as_bytes());
1270 let expected = hex::encode(mac.finalize().into_bytes());
1271
1272 assert_eq!(sig, expected);
1273 }
1274
1275 #[test]
1276 fn repeated_hmac_signature() {
1277 let mut params = BTreeMap::new();
1278 params.insert("x".into(), Value::String("y".into()));
1279 let signature_gen = SignatureGenerator::new(Some("abc".into()), None, None);
1280 let s1 = signature_gen.get_signature(¶ms).unwrap();
1281 let s2 = signature_gen.get_signature(¶ms).unwrap();
1282 assert_eq!(s1, s2);
1283 }
1284
1285 #[test]
1286 fn rsa_signature_verification() {
1287 let mut params = BTreeMap::new();
1288 params.insert("a".into(), Value::Number(1.into()));
1289 params.insert("b".into(), Value::Number(2.into()));
1290
1291 let rsa = Rsa::generate(2048).unwrap();
1292 let priv_pem = rsa.private_key_to_pem().unwrap();
1293 let pub_pem = rsa.public_key_to_pem_pkcs1().unwrap();
1294
1295 let signature_gen =
1296 SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem.clone())), None);
1297 let sig = signature_gen
1298 .get_signature(¶ms)
1299 .expect("RSA signing failed");
1300
1301 let sig_bytes = general_purpose::STANDARD.decode(&sig).unwrap();
1302 let pubkey = PKey::public_key_from_pem(&pub_pem).unwrap();
1303 let mut verifier = Verifier::new(MessageDigest::sha256(), &pubkey).unwrap();
1304 verifier.update(b"a=1&b=2").unwrap();
1305 assert!(verifier.verify(&sig_bytes).unwrap());
1306 }
1307
1308 #[test]
1309 fn repeated_rsa_signature() {
1310 let mut params = BTreeMap::new();
1311 params.insert("k".into(), Value::Number(5.into()));
1312 let rsa = Rsa::generate(2048).unwrap();
1313 let priv_pem = rsa.private_key_to_pem().unwrap();
1314 let signature_gen =
1315 SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem)), None);
1316 let s1 = signature_gen.get_signature(¶ms).unwrap();
1317 let s2 = signature_gen.get_signature(¶ms).unwrap();
1318 assert_eq!(s1, s2);
1319 }
1320
1321 #[test]
1322 fn ed25519_signature_verification() {
1323 let mut params = BTreeMap::new();
1324 params.insert("a".into(), Value::Number(1.into()));
1325 params.insert("b".into(), Value::Number(2.into()));
1326 let qs = "a=1&b=2";
1327
1328 let ed = PKey::generate_ed25519().unwrap();
1329 let priv_pem = ed.private_key_to_pem_pkcs8().unwrap();
1330
1331 let signature_gen =
1332 SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem.clone())), None);
1333 let sig = signature_gen
1334 .get_signature(¶ms)
1335 .expect("Ed25519 signing failed");
1336
1337 let pem_str = String::from_utf8(priv_pem).unwrap();
1338 let b64 = pem_str
1339 .lines()
1340 .filter(|l| !l.starts_with("-----"))
1341 .collect::<String>();
1342 let der = general_purpose::STANDARD.decode(b64).unwrap();
1343 let mut sk = SigningKey::from_pkcs8_der(&der).unwrap();
1344 let expected_bytes = sk.sign(qs.as_bytes()).to_bytes();
1345 let expected_sig = general_purpose::STANDARD.encode(expected_bytes);
1346 assert_eq!(sig, expected_sig);
1347 }
1348
1349 #[test]
1350 fn repeated_ed25519_signature() {
1351 let mut params = BTreeMap::new();
1352 params.insert("m".into(), Value::String("n".into()));
1353 let ed = PKey::generate_ed25519().unwrap();
1354 let priv_pem = ed.private_key_to_pem_pkcs8().unwrap();
1355 let signature_gen =
1356 SignatureGenerator::new(None, Some(PrivateKey::Raw(priv_pem.clone())), None);
1357 let s1 = signature_gen.get_signature(¶ms).unwrap();
1358 let s2 = signature_gen.get_signature(¶ms).unwrap();
1359 assert_eq!(s1, s2);
1360 }
1361
1362 #[test]
1363 fn file_based_key() {
1364 let rsa = Rsa::generate(1024).unwrap();
1365 let priv_pem = rsa.private_key_to_pem().unwrap();
1366 let pub_pem = rsa.public_key_to_pem_pkcs1().unwrap();
1367
1368 let mut file = NamedTempFile::new().unwrap();
1369 file.write_all(&priv_pem).unwrap();
1370 let path = file.path().to_str().unwrap().to_string();
1371
1372 let mut params = BTreeMap::new();
1373 params.insert("z".into(), Value::Number(9.into()));
1374
1375 let signature_gen = SignatureGenerator::new(None, Some(PrivateKey::File(path)), None);
1376 let sig = signature_gen.get_signature(¶ms).unwrap();
1377
1378 let sig_bytes = general_purpose::STANDARD.decode(&sig).unwrap();
1379 let pubkey = PKey::public_key_from_pem(&pub_pem).unwrap();
1380 let mut verifier = Verifier::new(MessageDigest::sha256(), &pubkey).unwrap();
1381 verifier.update(b"z=9").unwrap();
1382 assert!(verifier.verify(&sig_bytes).unwrap());
1383 }
1384
1385 #[test]
1386 fn unsupported_key_type_error() {
1387 let mut params = BTreeMap::new();
1388 params.insert("x".into(), Value::String("y".into()));
1389
1390 let group =
1391 openssl::ec::EcGroup::from_curve_name(openssl::nid::Nid::X9_62_PRIME256V1).unwrap();
1392 let ec_key = openssl::ec::EcKey::generate(&group).unwrap();
1393 let pkey_ec = PKey::from_ec_key(ec_key).unwrap();
1394 let raw = pkey_ec.private_key_to_pem_pkcs8().unwrap();
1395
1396 let signature_gen = SignatureGenerator::new(None, Some(PrivateKey::Raw(raw)), None);
1397 let err = signature_gen
1398 .get_signature(¶ms)
1399 .unwrap_err()
1400 .to_string();
1401 assert!(err.contains("Unsupported private key type"));
1402 }
1403
1404 #[test]
1405 fn invalid_private_key_error() {
1406 let mut params = BTreeMap::new();
1407 params.insert("foo".into(), Value::String("bar".into()));
1408
1409 let signature_gen =
1410 SignatureGenerator::new(None, Some(PrivateKey::Raw(b"not a key".to_vec())), None);
1411 let err = signature_gen
1412 .get_signature(¶ms)
1413 .unwrap_err()
1414 .to_string();
1415 assert!(err.contains("Failed to parse private key"));
1416 }
1417
1418 #[test]
1419 fn missing_credentials_error() {
1420 let mut params = BTreeMap::new();
1421 params.insert("a".into(), Value::Number(1.into()));
1422
1423 let signature_gen = SignatureGenerator::new(None, None, None);
1424 let err = signature_gen
1425 .get_signature(¶ms)
1426 .unwrap_err()
1427 .to_string();
1428 assert!(err.contains("Either 'api_secret' or 'private_key' must be provided"));
1429 }
1430 }
1431
1432 mod should_retry_request {
1433 use crate::common::utils::should_retry_request;
1434
1435 use reqwest::{Error, Response};
1436
1437 fn mk_http_error(code: u16) -> Error {
1438 let resp = Response::from(
1439 http::response::Response::builder()
1440 .status(code)
1441 .body("")
1442 .unwrap(),
1443 );
1444 resp.error_for_status().unwrap_err()
1445 }
1446
1447 fn mk_network_error() -> Error {
1448 reqwest::blocking::get("http://256.256.256.256").unwrap_err()
1449 }
1450
1451 #[test]
1452 fn retry_on_retriable_status_and_method() {
1453 let err = mk_http_error(500);
1454 assert!(should_retry_request(&err, Some("GET"), Some(1)));
1455 assert!(should_retry_request(&err, Some("delete"), Some(2)));
1456 }
1457
1458 #[test]
1459 fn retry_when_status_none_and_retriable_method() {
1460 let retriable_methods = ["GET", "DELETE"];
1461
1462 for &method in &retriable_methods {
1463 let err = mk_network_error();
1464 assert!(
1465 should_retry_request(&err, Some(method), Some(1)),
1466 "Should retry when no status and method {method}"
1467 );
1468 }
1469 }
1470
1471 #[test]
1472 fn no_retry_when_no_retries_left() {
1473 let err = mk_http_error(503);
1474 assert!(!should_retry_request(&err, Some("GET"), Some(0)));
1475 }
1476
1477 #[test]
1478 fn no_retry_on_non_retriable_status() {
1479 let non_retriable_statuses = [400, 401, 404, 422];
1480
1481 for &status in &non_retriable_statuses {
1482 let err = mk_http_error(status);
1483 assert!(
1484 !should_retry_request(&err, Some("GET"), Some(2)),
1485 "Should not retry for non-retriable status {status}"
1486 );
1487 }
1488 }
1489
1490 #[test]
1491 fn no_retry_on_non_retriable_method() {
1492 let non_retriable_methods = ["POST", "PUT", "PATCH"];
1493
1494 for &method in &non_retriable_methods {
1495 let err = mk_http_error(500);
1496 assert!(
1497 !should_retry_request(&err, Some(method), Some(2)),
1498 "Should not retry for non-retriable method {method}"
1499 );
1500 }
1501 }
1502
1503 #[test]
1504 fn no_retry_when_status_none_and_non_retriable_method() {
1505 let non_retriable_methods = ["POST", "PUT"];
1506
1507 for &method in &non_retriable_methods {
1508 let err = mk_network_error();
1509 assert!(
1510 !should_retry_request(&err, Some(method), Some(1)),
1511 "Should not retry when no status and method {method}"
1512 );
1513 }
1514 }
1515 }
1516
1517 mod parse_rate_limit_headers_tests {
1518 use crate::common::{
1519 models::{Interval, RateLimitType},
1520 utils::parse_rate_limit_headers,
1521 };
1522 use std::collections::HashMap;
1523
1524 fn mk_headers(pairs: Vec<(&str, &str)>) -> HashMap<String, String> {
1525 let mut m = HashMap::new();
1526 for (k, v) in pairs {
1527 m.insert(k.to_string(), v.to_string());
1528 }
1529 m
1530 }
1531
1532 #[test]
1533 fn single_weight_header() {
1534 let headers = mk_headers(vec![("x-mbx-used-weight-1s", "123")]);
1535 let limits = parse_rate_limit_headers(&headers);
1536 assert_eq!(limits.len(), 1);
1537 let rl = &limits[0];
1538 assert_eq!(rl.rate_limit_type, RateLimitType::RequestWeight);
1539 assert_eq!(rl.interval, Interval::Second);
1540 assert_eq!(rl.interval_num, 1);
1541 assert_eq!(rl.count, 123);
1542 assert_eq!(rl.retry_after, None);
1543 }
1544
1545 #[test]
1546 fn single_order_count_with_retry_after() {
1547 let headers = mk_headers(vec![("x-mbx-order-count-5m", "42"), ("retry-after", "7")]);
1548 let limits = parse_rate_limit_headers(&headers);
1549 assert_eq!(limits.len(), 1);
1550 let rl = &limits[0];
1551 assert_eq!(rl.rate_limit_type, RateLimitType::Orders);
1552 assert_eq!(rl.interval, Interval::Minute);
1553 assert_eq!(rl.interval_num, 5);
1554 assert_eq!(rl.count, 42);
1555 assert_eq!(rl.retry_after, Some(7));
1556 }
1557
1558 #[test]
1559 fn multiple_headers() {
1560 let headers = mk_headers(vec![
1561 ("X-MBX-USED-WEIGHT-1h", "10"),
1562 ("x-mbx-order-count-2d", "20"),
1563 ]);
1564 let mut limits = parse_rate_limit_headers(&headers);
1565 limits.sort_by_key(|r| (r.interval_num, format!("{:?}", r.rate_limit_type)));
1566 assert_eq!(limits.len(), 2);
1567 let w = &limits[0];
1568 assert_eq!(w.rate_limit_type, RateLimitType::RequestWeight);
1569 assert_eq!(w.interval, Interval::Hour);
1570 assert_eq!(w.interval_num, 1);
1571 assert_eq!(w.count, 10);
1572 let o = &limits[1];
1573 assert_eq!(o.rate_limit_type, RateLimitType::Orders);
1574 assert_eq!(o.interval, Interval::Day);
1575 assert_eq!(o.interval_num, 2);
1576 assert_eq!(o.count, 20);
1577 }
1578
1579 #[test]
1580 fn ignores_unknown_and_malformed() {
1581 let headers = mk_headers(vec![
1582 ("x-mbx-used-weight-3x", "5"),
1583 ("random-header", "100"),
1584 ]);
1585 let limits = parse_rate_limit_headers(&headers);
1586 assert!(limits.is_empty());
1587 }
1588 }
1589
1590 mod http_request {
1591 use std::io::Write;
1592
1593 use flate2::{Compression, write::GzEncoder};
1594 use httpmock::MockServer;
1595 use reqwest::{Client, Method, Request};
1596 use serde::Deserialize;
1597
1598 use crate::{
1599 common::utils::http_request, config::ConfigurationRestApi, errors::ConnectorError,
1600 models::RestApiResponse,
1601 };
1602
1603 use super::TOKIO_SHARED_RT;
1604
1605 #[derive(Deserialize, Debug, PartialEq)]
1606 struct Dummy {
1607 foo: String,
1608 }
1609
1610 fn make_config(server_url: &str) -> ConfigurationRestApi {
1611 ConfigurationRestApi::builder()
1612 .api_key("key")
1613 .api_secret("secret")
1614 .base_path(server_url)
1615 .build()
1616 .expect("Failed to build configuration")
1617 }
1618
1619 #[test]
1620 fn http_request_success_plain_text() {
1621 TOKIO_SHARED_RT.block_on(async {
1622 let server = MockServer::start();
1623 let mock = server.mock(|when, then| {
1624 when.method(httpmock::Method::GET).path("/test");
1625 then.status(200)
1626 .header("Content-Type", "application/json")
1627 .body(r#"{"foo":"bar"}"#);
1628 });
1629
1630 let client = Client::new();
1631 let req: Request = client
1632 .request(Method::GET, format!("{}{}", server.url(""), "/test"))
1633 .build()
1634 .unwrap();
1635
1636 let cfg = make_config(&server.url(""));
1637 let resp: RestApiResponse<Dummy> = http_request(req, &cfg).await.unwrap();
1638 assert_eq!(resp.status, 200);
1639 let data = resp.data().await.unwrap();
1640 assert_eq!(data, Dummy { foo: "bar".into() });
1641 mock.assert();
1642 });
1643 }
1644
1645 #[test]
1646 fn http_request_success_gzip() {
1647 TOKIO_SHARED_RT.block_on(async {
1648 let server = MockServer::start();
1649 let body = r#"{"foo":"baz"}"#;
1650 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
1651 encoder.write_all(body.as_bytes()).unwrap();
1652 let gz = encoder.finish().unwrap();
1653
1654 let mock = server.mock(|when, then| {
1655 when.method(httpmock::Method::GET).path("/gz");
1656 then.status(200)
1657 .header("Content-Type", "application/json")
1658 .header("Content-Encoding", "gzip")
1659 .body(gz);
1660 });
1661
1662 let client = Client::new();
1663 let req: Request = client
1664 .request(Method::GET, format!("{}{}", server.url(""), "/gz"))
1665 .build()
1666 .unwrap();
1667 let mut cfg = make_config(&server.url(""));
1668 cfg.compression = true;
1669
1670 let resp: RestApiResponse<Dummy> = http_request(req, &cfg).await.unwrap();
1671 assert_eq!(resp.status, 200);
1672 let data = resp.data().await.unwrap();
1673 assert_eq!(data, Dummy { foo: "baz".into() });
1674 mock.assert();
1675 });
1676 }
1677
1678 #[test]
1679 fn http_request_client_error_bad_request() {
1680 TOKIO_SHARED_RT.block_on(async {
1681 let server = MockServer::start();
1682 let mock = server.mock(|when, then| {
1683 when.method(httpmock::Method::GET).path("/400");
1684 then.status(400)
1685 .header("Content-Type", "application/json")
1686 .body(r#"{"msg":"bad request"}"#);
1687 });
1688
1689 let client = Client::new();
1690 let req: Request = client
1691 .request(Method::GET, format!("{}{}", server.url(""), "/400"))
1692 .build()
1693 .unwrap();
1694 let cfg = make_config(&server.url(""));
1695
1696 let result = http_request::<Dummy>(req, &cfg).await;
1697 assert!(matches!(result, Err(ConnectorError::BadRequestError(_))));
1698 if let Err(ConnectorError::BadRequestError(msg)) = result {
1699 assert_eq!(msg, "bad request");
1700 }
1701 mock.assert();
1702 });
1703 }
1704
1705 #[test]
1706 fn http_request_client_error_unauthorized() {
1707 TOKIO_SHARED_RT.block_on(async {
1708 let server = MockServer::start();
1709 let mock = server.mock(|when, then| {
1710 when.method(httpmock::Method::GET).path("/401");
1711 then.status(401)
1712 .header("Content-Type", "application/json")
1713 .body(r#"{"msg":"unauthorized"}"#);
1714 });
1715
1716 let client = Client::new();
1717 let req: Request = client
1718 .request(Method::GET, format!("{}{}", server.url(""), "/401"))
1719 .build()
1720 .unwrap();
1721 let cfg = make_config(&server.url(""));
1722
1723 let result = http_request::<Dummy>(req, &cfg).await;
1724 assert!(matches!(result, Err(ConnectorError::UnauthorizedError(_))));
1725 if let Err(ConnectorError::UnauthorizedError(msg)) = result {
1726 assert_eq!(msg, "unauthorized");
1727 }
1728 mock.assert();
1729 });
1730 }
1731
1732 #[test]
1733 fn http_request_client_error_forbidden() {
1734 TOKIO_SHARED_RT.block_on(async {
1735 let server = MockServer::start();
1736 let mock = server.mock(|when, then| {
1737 when.method(httpmock::Method::GET).path("/403");
1738 then.status(403)
1739 .header("Content-Type", "application/json")
1740 .body(r#"{"msg":"forbidden"}"#);
1741 });
1742
1743 let client = Client::new();
1744 let req: Request = client
1745 .request(Method::GET, format!("{}{}", server.url(""), "/403"))
1746 .build()
1747 .unwrap();
1748 let cfg = make_config(&server.url(""));
1749
1750 let result = http_request::<Dummy>(req, &cfg).await;
1751 assert!(matches!(result, Err(ConnectorError::ForbiddenError(_))));
1752 if let Err(ConnectorError::ForbiddenError(msg)) = result {
1753 assert_eq!(msg, "forbidden");
1754 }
1755 mock.assert();
1756 });
1757 }
1758
1759 #[test]
1760 fn http_request_client_error_not_found() {
1761 TOKIO_SHARED_RT.block_on(async {
1762 let server = MockServer::start();
1763 let mock = server.mock(|when, then| {
1764 when.method(httpmock::Method::GET).path("/404");
1765 then.status(404)
1766 .header("Content-Type", "application/json")
1767 .body(r#"{"msg":"not found"}"#);
1768 });
1769
1770 let client = Client::new();
1771 let req: Request = client
1772 .request(Method::GET, format!("{}{}", server.url(""), "/404"))
1773 .build()
1774 .unwrap();
1775 let cfg = make_config(&server.url(""));
1776
1777 let result = http_request::<Dummy>(req, &cfg).await;
1778 assert!(matches!(result, Err(ConnectorError::NotFoundError(_))));
1779 if let Err(ConnectorError::NotFoundError(msg)) = result {
1780 assert_eq!(msg, "not found");
1781 }
1782 mock.assert();
1783 });
1784 }
1785
1786 #[test]
1787 fn http_request_client_error_rate_limit_exceeded() {
1788 TOKIO_SHARED_RT.block_on(async {
1789 let server = MockServer::start();
1790 let mock = server.mock(|when, then| {
1791 when.method(httpmock::Method::GET).path("/418");
1792 then.status(418)
1793 .header("Content-Type", "application/json")
1794 .body(r#"{"msg":"rate limit exceeded"}"#);
1795 });
1796
1797 let client = Client::new();
1798 let req: Request = client
1799 .request(Method::GET, format!("{}{}", server.url(""), "/418"))
1800 .build()
1801 .unwrap();
1802 let cfg = make_config(&server.url(""));
1803
1804 let result = http_request::<Dummy>(req, &cfg).await;
1805 assert!(matches!(result, Err(ConnectorError::RateLimitBanError(_))));
1806 if let Err(ConnectorError::RateLimitBanError(msg)) = result {
1807 assert_eq!(msg, "rate limit exceeded");
1808 }
1809 mock.assert();
1810 });
1811 }
1812
1813 #[test]
1814 fn http_request_client_error_too_many_requests() {
1815 TOKIO_SHARED_RT.block_on(async {
1816 let server = MockServer::start();
1817 let mock = server.mock(|when, then| {
1818 when.method(httpmock::Method::GET).path("/429");
1819 then.status(429)
1820 .header("Content-Type", "application/json")
1821 .body(r#"{"msg":"too many requests"}"#);
1822 });
1823
1824 let client = Client::new();
1825 let req: Request = client
1826 .request(Method::GET, format!("{}{}", server.url(""), "/429"))
1827 .build()
1828 .unwrap();
1829 let cfg = make_config(&server.url(""));
1830
1831 let result = http_request::<Dummy>(req, &cfg).await;
1832 assert!(matches!(
1833 result,
1834 Err(ConnectorError::TooManyRequestsError(_))
1835 ));
1836 if let Err(ConnectorError::TooManyRequestsError(msg)) = result {
1837 assert_eq!(msg, "too many requests");
1838 }
1839 mock.assert();
1840 });
1841 }
1842
1843 #[test]
1844 fn http_request_client_error_server_error() {
1845 TOKIO_SHARED_RT.block_on(async {
1846 let server = MockServer::start();
1847 let mock = server.mock(|when, then| {
1848 when.method(httpmock::Method::GET).path("/500");
1849 then.status(500)
1850 .header("Content-Type", "application/json")
1851 .body(r#"{"msg":"internal server error"}"#);
1852 });
1853
1854 let client = Client::new();
1855 let req: Request = client
1856 .request(Method::GET, format!("{}{}", server.url(""), "/500"))
1857 .build()
1858 .unwrap();
1859 let cfg = make_config(&server.url(""));
1860
1861 let result = http_request::<Dummy>(req, &cfg).await;
1862 assert!(matches!(result, Err(ConnectorError::ServerError { .. })));
1863 if let Err(ConnectorError::ServerError {
1864 msg,
1865 status_code: Some(500),
1866 }) = result
1867 {
1868 assert_eq!(msg, "Server error: 500".to_string());
1869 }
1870 mock.assert();
1871 });
1872 }
1873
1874 #[test]
1875 fn http_request_unexpected_status_maps_generic() {
1876 TOKIO_SHARED_RT.block_on(async {
1877 let server = MockServer::start();
1878 let code = 402;
1879 let mock = server.mock(|when, then| {
1880 when.method(httpmock::Method::GET).path("/402");
1881 then.status(code).body("error text");
1882 });
1883
1884 let client = Client::new();
1885 let req: Request = client
1886 .request(Method::GET, format!("{}{}", server.url(""), "/402"))
1887 .build()
1888 .unwrap();
1889 let cfg = make_config(&server.url(""));
1890
1891 let result = http_request::<Dummy>(req, &cfg).await;
1892 assert!(matches!(
1893 result,
1894 Err(ConnectorError::ConnectorClientError(_))
1895 ));
1896 mock.assert();
1897 });
1898 }
1899
1900 #[test]
1901 fn http_request_malformed_json_maps_generic() {
1902 TOKIO_SHARED_RT.block_on(async {
1903 let server = MockServer::start();
1904 let mock = server.mock(|when, then| {
1905 when.method(httpmock::Method::GET).path("/malformed");
1906 then.status(200)
1907 .header("Content-Type", "application/json")
1908 .body("not json");
1909 });
1910
1911 let client = Client::new();
1912 let req: Request = client
1913 .request(Method::GET, format!("{}{}", server.url(""), "/malformed"))
1914 .build()
1915 .unwrap();
1916 let cfg = make_config(&server.url(""));
1917
1918 let resp = http_request::<Dummy>(req, &cfg)
1920 .await
1921 .expect("http_request should succeed even if JSON is bad");
1922
1923 let err = resp
1925 .data() .await
1927 .expect_err("malformed JSON should turn into ConnectorClientError");
1928
1929 assert!(matches!(err, ConnectorError::ConnectorClientError(_)));
1930
1931 mock.assert();
1932 });
1933 }
1934 }
1935
1936 mod send_request {
1937 use anyhow::Result;
1938 use httpmock::prelude::*;
1939 use reqwest::Method;
1940 use serde::Deserialize;
1941 use serde_json::json;
1942 use std::collections::BTreeMap;
1943
1944 use crate::{
1945 common::{models::TimeUnit, utils::send_request},
1946 config::ConfigurationRestApi,
1947 };
1948
1949 use super::TOKIO_SHARED_RT;
1950
1951 #[derive(Deserialize, Debug, PartialEq)]
1952 struct TestResponse {
1953 message: String,
1954 }
1955
1956 #[test]
1957 fn basic_get_request() -> Result<()> {
1958 TOKIO_SHARED_RT.block_on(async {
1959 let server = MockServer::start();
1960
1961 server.mock(|when, then| {
1962 when.method(GET).path("/api/v1/test");
1963 then.status(200)
1964 .header("content-type", "application/json")
1965 .body(r#"{"message": "success"}"#);
1966 });
1967
1968 let configuration = ConfigurationRestApi::builder()
1969 .api_key("key")
1970 .api_secret("secret")
1971 .base_path(server.base_url())
1972 .compression(false)
1973 .build()
1974 .expect("Failed to build configuration");
1975
1976 let params = BTreeMap::new();
1977
1978 let result = send_request::<TestResponse>(
1979 &configuration,
1980 "/api/v1/test",
1981 Method::GET,
1982 params,
1983 None,
1984 false,
1985 )
1986 .await?;
1987
1988 let data = result.data().await.unwrap();
1989 assert_eq!(data.message, "success");
1990
1991 Ok(())
1992 })
1993 }
1994
1995 #[test]
1996 fn signed_post_request() -> Result<()> {
1997 TOKIO_SHARED_RT.block_on(async {
1998 let server = MockServer::start();
1999
2000 server.mock(|when, then| {
2001 when.method(POST).path("/api/v3/order");
2002 then.status(200)
2003 .header("content-type", "application/json")
2004 .body(r#"{"message": "order placed"}"#);
2005 });
2006
2007 let configuration = ConfigurationRestApi::builder()
2008 .api_key("key")
2009 .api_secret("secret")
2010 .base_path(server.base_url())
2011 .compression(false)
2012 .build()
2013 .expect("Failed to build configuration");
2014
2015 let mut params = BTreeMap::new();
2016 params.insert("symbol".to_string(), json!("ETHUSDT"));
2017 params.insert("side".to_string(), json!("BUY"));
2018 params.insert("type".to_string(), json!("MARKET"));
2019 params.insert("quantity".to_string(), json!("1"));
2020
2021 let result = send_request::<TestResponse>(
2022 &configuration,
2023 "/api/v3/order",
2024 Method::POST,
2025 params,
2026 None,
2027 true,
2028 )
2029 .await?;
2030
2031 let data = result.data().await.unwrap();
2032 assert_eq!(data.message, "order placed");
2033
2034 Ok(())
2035 })
2036 }
2037
2038 #[test]
2039 fn get_request_with_params() -> Result<()> {
2040 TOKIO_SHARED_RT.block_on(async {
2041 let server = MockServer::start();
2042
2043 server.mock(|when, then| {
2044 when.method(GET)
2045 .path("/api/v1/data")
2046 .query_param("symbol", "BTCUSDT")
2047 .query_param("limit", "10");
2048 then.status(200)
2049 .header("content-type", "application/json")
2050 .body(r#"{"message": "data retrieved"}"#);
2051 });
2052
2053 let configuration = ConfigurationRestApi::builder()
2054 .api_key("key")
2055 .api_secret("secret")
2056 .base_path(server.base_url())
2057 .compression(false)
2058 .build()
2059 .expect("Failed to build configuration");
2060
2061 let mut params = BTreeMap::new();
2062 params.insert("symbol".to_string(), json!("BTCUSDT"));
2063 params.insert("limit".to_string(), json!(10));
2064
2065 let result = send_request::<TestResponse>(
2066 &configuration,
2067 "/api/v1/data",
2068 Method::GET,
2069 params,
2070 None,
2071 false,
2072 )
2073 .await?;
2074
2075 let data = result.data().await.unwrap();
2076 assert_eq!(data.message, "data retrieved");
2077
2078 Ok(())
2079 })
2080 }
2081
2082 #[test]
2083 fn invalid_endpoint() {
2084 TOKIO_SHARED_RT.block_on(async {
2085 let server = MockServer::start();
2086
2087 let configuration = ConfigurationRestApi::builder()
2088 .api_key("key")
2089 .api_secret("secret")
2090 .base_path(server.base_url())
2091 .compression(false)
2092 .build()
2093 .expect("Failed to build configuration");
2094
2095 let params = BTreeMap::new();
2096
2097 let result = send_request::<TestResponse>(
2098 &configuration,
2099 "http://invalid",
2100 Method::GET,
2101 params,
2102 None,
2103 false,
2104 )
2105 .await;
2106
2107 assert!(result.is_err());
2108 });
2109 }
2110
2111 #[test]
2112 fn missing_signature_on_signed_request() {
2113 TOKIO_SHARED_RT.block_on(async {
2114 let server = MockServer::start();
2115
2116 let configuration = ConfigurationRestApi::builder()
2117 .api_key("key")
2118 .api_secret("secret")
2119 .base_path(server.base_url())
2120 .compression(false)
2121 .build()
2122 .expect("Failed to build configuration");
2123
2124 let mut params = BTreeMap::new();
2125 params.insert("symbol".to_string(), json!("BTCUSDT"));
2126 params.insert("side".to_string(), json!("BUY"));
2127
2128 let result = send_request::<TestResponse>(
2129 &configuration,
2130 "/api/v3/order",
2131 Method::POST,
2132 params,
2133 None,
2134 true,
2135 )
2136 .await;
2137
2138 assert!(result.is_err());
2139 });
2140 }
2141
2142 #[test]
2143 fn compression_enabled() -> Result<()> {
2144 TOKIO_SHARED_RT.block_on(async {
2145 let server = MockServer::start();
2146
2147 server.mock(|when, then| {
2148 when.method(GET).path("/api/v1/test");
2149 then.status(200)
2150 .header("content-type", "application/json")
2151 .header("accept-encoding", "gzip, deflate, br")
2152 .body(r#"{"message": "compression enabled"}"#);
2153 });
2154
2155 let configuration = ConfigurationRestApi::builder()
2156 .api_key("key")
2157 .api_secret("secret")
2158 .base_path(server.base_url())
2159 .compression(true)
2160 .build()
2161 .expect("Failed to build configuration");
2162
2163 let params = BTreeMap::new();
2164
2165 let result = send_request::<TestResponse>(
2166 &configuration,
2167 "/api/v1/test",
2168 Method::GET,
2169 params,
2170 None,
2171 false,
2172 )
2173 .await?;
2174
2175 let data = result.data().await.unwrap();
2176 assert_eq!(data.message, "compression enabled");
2177
2178 Ok(())
2179 })
2180 }
2181
2182 #[test]
2183 fn get_request_with_time_unit_header() -> Result<()> {
2184 TOKIO_SHARED_RT.block_on(async {
2185 let server = MockServer::start();
2186
2187 server.mock(|when, then| {
2188 when.method(GET)
2189 .path("/api/v1/test")
2190 .header("X-MBX-TIME-UNIT", "MILLISECOND");
2191 then.status(200)
2192 .header("content-type", "application/json")
2193 .body(r#"{"message": "time unit applied"}"#);
2194 });
2195
2196 let configuration = ConfigurationRestApi::builder()
2197 .api_key("key")
2198 .api_secret("secret")
2199 .base_path(server.base_url())
2200 .compression(false)
2201 .time_unit(TimeUnit::Millisecond)
2202 .build()
2203 .expect("Failed to build configuration");
2204
2205 let params = BTreeMap::new();
2206
2207 let result = send_request::<TestResponse>(
2208 &configuration,
2209 "/api/v1/test",
2210 Method::GET,
2211 params,
2212 Some(TimeUnit::Millisecond),
2213 false,
2214 )
2215 .await?;
2216
2217 let data = result.data().await.unwrap();
2218 assert_eq!(data.message, "time unit applied");
2219
2220 Ok(())
2221 })
2222 }
2223 }
2224
2225 mod random_string {
2226 use crate::common::utils::random_string;
2227 use hex;
2228
2229 #[test]
2230 fn length_is_32() {
2231 let s = random_string();
2232 assert_eq!(
2233 s.len(),
2234 32,
2235 "random_string() should be 32 chars, got {}",
2236 s.len()
2237 );
2238 }
2239
2240 #[test]
2241 fn is_valid_lowercase_hex() {
2242 let s = random_string();
2243 assert!(
2244 s.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f')),
2245 "random_string() contains invalid hex characters: {s}"
2246 );
2247 }
2248
2249 #[test]
2250 fn decodes_to_16_bytes() {
2251 let s = random_string();
2252 let bytes = hex::decode(&s).expect("random_string() output must be valid hex");
2253 assert_eq!(
2254 bytes.len(),
2255 16,
2256 "hex::decode returned {} bytes",
2257 bytes.len()
2258 );
2259 }
2260
2261 #[test]
2262 fn two_calls_are_different() {
2263 let a = random_string();
2264 let b = random_string();
2265 assert_ne!(
2266 a, b,
2267 "Two calls to random_string() returned the same value: {a}"
2268 );
2269 }
2270 }
2271
2272 mod remove_empty_value {
2273 use crate::common::utils::remove_empty_value;
2274 use serde_json::{Map, Value};
2275
2276 #[test]
2277 fn filters_out_null_and_empty_strings() {
2278 let entries = vec![
2279 ("key1".to_string(), Value::String("value1".to_string())),
2280 ("key2".to_string(), Value::Null),
2281 ("key3".to_string(), Value::String(String::new())),
2282 ];
2283 let result = remove_empty_value(entries);
2284 assert_eq!(
2285 result.len(),
2286 1,
2287 "expected only one entry, got {}",
2288 result.len()
2289 );
2290 assert_eq!(
2291 result.get("key1"),
2292 Some(&Value::String("value1".to_string()))
2293 );
2294 assert!(!result.contains_key("key2"));
2295 assert!(!result.contains_key("key3"));
2296 }
2297
2298 #[test]
2299 fn retains_other_value_types() {
2300 let entries = vec![
2301 ("bool".to_string(), Value::Bool(true)),
2302 ("num".to_string(), Value::Number(42.into())),
2303 ("arr".to_string(), Value::Array(vec![])),
2304 ("obj".to_string(), Value::Object(Map::default())),
2305 ("nil".to_string(), Value::Null),
2306 ("empty_str".to_string(), Value::String(String::new())),
2307 ];
2308 let result = remove_empty_value(entries);
2309 let keys: Vec<&String> = result.keys().collect();
2310 assert_eq!(keys.len(), 4, "expected 4 entries, got {}", keys.len());
2311 assert!(result.get("bool") == Some(&Value::Bool(true)));
2312 assert!(result.get("num") == Some(&Value::Number(42.into())));
2313 assert!(result.get("arr") == Some(&Value::Array(vec![])));
2314 assert!(result.get("obj") == Some(&Value::Object(Map::default())));
2315 assert!(!result.contains_key("nil"));
2316 assert!(!result.contains_key("empty_str"));
2317 }
2318
2319 #[test]
2320 fn empty_iterator_returns_empty_map() {
2321 let entries: Vec<(String, Value)> = vec![];
2322 let result = remove_empty_value(entries);
2323 assert!(result.is_empty(), "expected an empty map");
2324 }
2325
2326 #[test]
2327 fn keys_are_sorted() {
2328 let entries = vec![
2329 ("c".to_string(), Value::String("foo".to_string())),
2330 ("a".to_string(), Value::String("bar".to_string())),
2331 ("b".to_string(), Value::String("baz".to_string())),
2332 ];
2333 let result = remove_empty_value(entries);
2334 let sorted_keys: Vec<&String> = result.keys().collect();
2335 assert_eq!(
2336 sorted_keys,
2337 [&"a".to_string(), &"b".to_string(), &"c".to_string()]
2338 );
2339 }
2340 }
2341
2342 mod sort_object_params {
2343 use crate::common::utils::sort_object_params;
2344 use serde_json::Value;
2345 use std::collections::BTreeMap;
2346
2347 #[test]
2348 fn sorts_keys() {
2349 let mut params = BTreeMap::new();
2350 params.insert("z".to_string(), Value::String("last".to_string()));
2351 params.insert("a".to_string(), Value::String("first".to_string()));
2352 params.insert("m".to_string(), Value::String("middle".to_string()));
2353
2354 let sorted = sort_object_params(¶ms);
2355 let keys: Vec<&String> = sorted.keys().collect();
2356 assert_eq!(
2357 keys,
2358 [&"a".to_string(), &"m".to_string(), &"z".to_string()],
2359 "Keys should be sorted alphabetically"
2360 );
2361 }
2362
2363 #[test]
2364 fn preserves_values() {
2365 let mut params = BTreeMap::new();
2366 params.insert("one".to_string(), Value::Number(1.into()));
2367 params.insert("two".to_string(), Value::Bool(true));
2368
2369 let sorted = sort_object_params(¶ms);
2370 assert_eq!(sorted.get("one"), Some(&Value::Number(1.into())));
2371 assert_eq!(sorted.get("two"), Some(&Value::Bool(true)));
2372 }
2373
2374 #[test]
2375 fn empty_map_returns_empty() {
2376 let params: BTreeMap<String, Value> = BTreeMap::new();
2377 let sorted = sort_object_params(¶ms);
2378 assert!(sorted.is_empty(), "Expected empty map");
2379 }
2380
2381 #[test]
2382 fn independent_clone() {
2383 let mut params = BTreeMap::new();
2384 params.insert("key".to_string(), Value::String("val".to_string()));
2385
2386 let mut sorted = sort_object_params(¶ms);
2387 sorted.insert("new".to_string(), Value::String("x".to_string()));
2388
2389 assert!(
2390 !params.contains_key("new"),
2391 "Original should not be modified when changing sorted"
2392 );
2393 assert!(
2394 sorted.contains_key("new"),
2395 "Sorted map should reflect its own insertions"
2396 );
2397 }
2398 }
2399
2400 mod normalize_ws_streams_key {
2401 use crate::common::utils::normalize_ws_streams_key;
2402
2403 #[test]
2404 fn returns_empty_for_empty() {
2405 assert_eq!(normalize_ws_streams_key(""), "");
2406 }
2407
2408 #[test]
2409 fn already_normalized_stays_same() {
2410 assert_eq!(normalize_ws_streams_key("streamname"), "streamname");
2411 }
2412
2413 #[test]
2414 fn uppercases_are_lowercased() {
2415 assert_eq!(normalize_ws_streams_key("MyStream"), "mystream");
2416 }
2417
2418 #[test]
2419 fn underscores_are_removed() {
2420 assert_eq!(normalize_ws_streams_key("my_stream_name"), "mystreamname");
2421 }
2422
2423 #[test]
2424 fn hyphens_are_removed() {
2425 assert_eq!(normalize_ws_streams_key("my-stream-name"), "mystreamname");
2426 }
2427
2428 #[test]
2429 fn mixed_underscores_and_hyphens_and_case() {
2430 let input = "Mixed_Case-Stream_Name";
2431 let expected = "mixedcasestreamname";
2432 assert_eq!(normalize_ws_streams_key(input), expected);
2433 }
2434
2435 #[test]
2436 fn retains_other_punctuation() {
2437 assert_eq!(normalize_ws_streams_key("stream.name!"), "stream.name!");
2438 }
2439 }
2440
2441 mod replace_websocket_streams_placeholders {
2442 use crate::common::utils::replace_websocket_streams_placeholders;
2443 use std::collections::HashMap;
2444
2445 #[test]
2446 fn empty_string_unchanged() {
2447 let vars: HashMap<&str, &str> = HashMap::new();
2448 assert_eq!(replace_websocket_streams_placeholders("", &vars), "");
2449 }
2450
2451 #[test]
2452 fn unknown_placeholder_becomes_empty() {
2453 let vars: HashMap<&str, &str> = HashMap::new();
2454 assert_eq!(replace_websocket_streams_placeholders("<foo>", &vars), "");
2455 }
2456
2457 #[test]
2458 fn leading_slash_symbol_lowercases_head() {
2459 let mut vars = HashMap::new();
2460 vars.insert("symbol", "BTC");
2461 assert_eq!(
2462 replace_websocket_streams_placeholders("/<symbol>", &vars),
2463 "btc"
2464 );
2465 }
2466
2467 #[test]
2468 fn no_lowercase_without_slash() {
2469 let mut vars = HashMap::new();
2470 vars.insert("symbol", "BTC");
2471 assert_eq!(
2472 replace_websocket_streams_placeholders("<symbol>", &vars),
2473 "BTC"
2474 );
2475 }
2476
2477 #[test]
2478 fn multiple_placeholders_mid_preserve_ats() {
2479 let mut vars = HashMap::new();
2480 vars.insert("symbol", "BNBUSDT");
2481 vars.insert("levels", "10");
2482 vars.insert("updateSpeed", "1000ms");
2483 let out = replace_websocket_streams_placeholders(
2484 "/<symbol>@depth<levels>@<updateSpeed>",
2485 &vars,
2486 );
2487 assert_eq!(out, "bnbusdt@depth10@1000ms");
2488 }
2489
2490 #[test]
2491 fn trailing_at_removed_when_missing_var() {
2492 let mut vars = HashMap::new();
2493 vars.insert("symbol", "BNBUSDT");
2494 vars.insert("levels", "10");
2495 let out = replace_websocket_streams_placeholders(
2496 "/<symbol>@depth<levels>@<updateSpeed>",
2497 &vars,
2498 );
2499 assert_eq!(out, "bnbusdt@depth10");
2500 }
2501
2502 #[test]
2503 fn custom_key_normalization_and_value() {
2504 let mut vars = HashMap::new();
2505 vars.insert("my-stream_key", "Value");
2506 assert_eq!(
2507 replace_websocket_streams_placeholders("<My_Stream-Key>", &vars),
2508 "Value"
2509 );
2510 }
2511
2512 #[test]
2513 fn text_surrounding_placeholders_intact() {
2514 let mut vars = HashMap::new();
2515 vars.insert("symbol", "ABC");
2516 let input = "pre-<symbol>-post";
2517 assert_eq!(
2518 replace_websocket_streams_placeholders(input, &vars),
2519 "pre-ABC-post"
2520 );
2521 }
2522 }
2523}