1use super::build_query_smartstring;
7use crate::Result;
8use crate::SmartString;
9use crate::collections::FxHashMap;
10use hex;
11use sha2::{Digest, Sha512};
12use smallvec::SmallVec;
13use std::fmt::Write;
14use std::time::{SystemTime, UNIX_EPOCH};
15use uuid::Uuid;
16
17const HASH_ALGORITHM: &str = "SHA512";
18
19thread_local! {
21 static JSON_BUFFER: std::cell::RefCell<Vec<u8>> = std::cell::RefCell::new(Vec::with_capacity(8192));
22}
23
24pub mod header_keys {
26 use crate::SmartString;
27
28 pub const AUTHORIZATION: &str = "Authorization";
30 pub const CONTENT_TYPE: &str = "Content-Type";
32 pub const CONTENT_TYPE_VALUE: &str = "application/json; charset=utf-8";
34
35 #[must_use]
37 pub fn authorization() -> SmartString {
38 AUTHORIZATION.into()
39 }
40 pub fn content_type() -> SmartString {
42 CONTENT_TYPE.into()
43 }
44 pub fn content_type_value() -> SmartString {
46 CONTENT_TYPE_VALUE.into()
47 }
48}
49
50#[derive(Debug, Clone)]
52pub struct BithumbAuth {
53 api_key: SmartString,
54 secret_key: SmartString,
55}
56
57impl BithumbAuth {
58 #[must_use]
60 pub const fn new(api_key: SmartString, api_secret: SmartString) -> Self {
61 Self {
62 api_key,
63 secret_key: api_secret,
64 }
65 }
66
67 pub fn build_param_string_optimized(params: &[(&str, &str)]) -> Result<SmartString> {
78 if params.is_empty() {
79 return Ok(SmartString::new());
80 }
81
82 let mut param_strings: SmallVec<[SmartString; 8]> = SmallVec::with_capacity(params.len());
84 let mut buffer = SmartString::new();
85
86 for (key, value) in params {
87 Self::encode_params_zero_copy(value, &mut buffer)?;
88 let param = crate::safe_format!("{}={}", key, buffer.as_str());
89 param_strings.push(param);
90 }
91
92 Ok(SmartString::from(
94 param_strings
95 .iter()
96 .map(|s| s.as_str())
97 .collect::<SmallVec<[&str; 8]>>()
98 .join("&"),
99 ))
100 }
101
102 #[must_use]
104 pub fn generate_nonce() -> SmartString {
105 Uuid::new_v4().to_string().into()
106 }
107
108 #[must_use]
110 pub fn generate_timestamp() -> u64 {
111 SystemTime::now()
112 .duration_since(UNIX_EPOCH)
113 .unwrap_or_default()
114 .as_millis() as u64
115 }
116
117 #[must_use]
119 pub fn generate_timestamp_nanos() -> u128 {
120 SystemTime::now()
121 .duration_since(UNIX_EPOCH)
122 .unwrap_or_default()
123 .as_nanos()
124 }
125
126 fn generate_query_hash(params: &str) -> SmartString {
128 if params.is_empty() {
129 return "".into();
130 }
131
132 let mut hasher = Sha512::new();
133 hasher.update(params.as_bytes());
134 let result = hasher.finalize();
135 hex::encode(result).into()
136 }
137
138 pub fn encode_params_zero_copy(params: &str, buffer: &mut SmartString) -> Result<()> {
143 super::url_encode_params(params, buffer)
144 }
145
146 fn generate_jwt(&self, query_hash: Option<SmartString>) -> Result<SmartString> {
148 use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
149 use serde::Serialize;
150
151 #[derive(Serialize)]
152 struct Claims {
153 access_key: String,
154 nonce: String,
155 timestamp: u64,
156 #[serde(skip_serializing_if = "Option::is_none")]
157 query_hash: Option<SmartString>,
158 #[serde(skip_serializing_if = "Option::is_none")]
159 query_hash_alg: Option<SmartString>,
160 }
161
162 let hash_alg: Option<SmartString> = if query_hash.is_some() {
163 Some(HASH_ALGORITHM.into())
164 } else {
165 None
166 };
167
168 let query_hash: SmartString = query_hash.unwrap_or_default();
169
170 let is_empty = query_hash.is_empty();
171
172 let claims = Claims {
173 access_key: self.api_key.to_string(),
174 nonce: Self::generate_nonce().to_string(),
175 timestamp: Self::generate_timestamp(),
176 query_hash: if is_empty { None } else { Some(query_hash) },
177 query_hash_alg: if is_empty { None } else { hash_alg },
178 };
179
180 let header = Header::new(Algorithm::HS256);
181 let encoding_key = EncodingKey::from_secret(self.secret_key.as_bytes());
182
183 encode(&header, &claims, &encoding_key)
184 .map(|s| s.into())
185 .map_err(|e| crate::CommonError::Auth(crate::safe_format!("JWT encoding error: {e}")))
186 }
187
188 pub fn generate_headers(
190 &self,
191 _method: &str,
192 _path: &str,
193 params: Option<&[(&str, &str)]>,
194 ) -> Result<FxHashMap<SmartString, SmartString>> {
195 let query_hash = if let Some(params) = params {
197 if params.is_empty() {
198 None
199 } else {
200 let joined_params = Self::build_param_string_optimized(params)?;
201 let mut encoded_params = SmartString::new();
202 Self::encode_params_zero_copy(&joined_params, &mut encoded_params)?;
203 Some(Self::generate_query_hash(&encoded_params))
204 }
205 } else {
206 None
207 };
208
209 let jwt_token = self.generate_jwt(query_hash)?;
211
212 let mut headers = FxHashMap::default();
214 headers.insert(
215 header_keys::authorization(),
216 crate::safe_format!("Bearer {jwt_token}"),
217 );
218 headers.insert(
219 header_keys::content_type(),
220 header_keys::content_type_value(),
221 );
222
223 Ok(headers)
224 }
225
226 pub fn generate_headers_for_json_body(
233 &self,
234 json_body: &str,
235 ) -> Result<FxHashMap<SmartString, SmartString>> {
236 let query_hash = if json_body.is_empty() || json_body == "{}" {
238 None
239 } else {
240 let encoded_params = Self::json_to_url_encoded(json_body)?;
242 Some(Self::generate_query_hash(&encoded_params))
243 };
244
245 let jwt_token = self.generate_jwt(query_hash)?;
247
248 let mut headers = FxHashMap::default();
250 headers.insert(
251 header_keys::authorization(),
252 crate::safe_format!("Bearer {jwt_token}"),
253 );
254 headers.insert(
255 header_keys::content_type(),
256 header_keys::content_type_value(),
257 );
258
259 Ok(headers)
260 }
261
262 fn json_to_url_encoded(json_body: &str) -> Result<SmartString> {
269 use simd_json::OwnedValue;
270 use simd_json::prelude::*;
271
272 let parsed: OwnedValue = JSON_BUFFER.with(|buffer_cell| {
274 let mut buffer = buffer_cell.borrow_mut();
275
276 let required_len = json_body.len();
278 if buffer.capacity() < required_len {
279 let additional = required_len - buffer.capacity();
280 buffer.reserve(additional);
281 }
282
283 buffer.clear();
285 buffer.extend_from_slice(json_body.as_bytes());
286
287 simd_json::from_slice(&mut buffer).map_err(|e| {
290 crate::CommonError::Json(crate::safe_format!("Failed to parse JSON body: {e}"))
291 })
292 })?;
293
294 let obj = parsed
296 .as_object()
297 .ok_or_else(|| crate::CommonError::Json("JSON body must be an object".into()))?;
298
299 if obj.is_empty() {
300 return Ok(SmartString::new());
301 }
302
303 let mut params: SmallVec<[(SmartString, SmartString); 8]> =
305 SmallVec::with_capacity(obj.len());
306
307 for (key, value) in obj.iter() {
308 if value.is_null() {
310 continue;
311 }
312
313 let key_str: SmartString = key.clone().into();
314 let value_str: SmartString = {
315 let mut s = SmartString::new();
317 if let Some(i) = value.as_i64() {
318 let _ = write!(s, "{i}");
319 s
320 } else if let Some(u) = value.as_u64() {
321 let _ = write!(s, "{u}");
322 s
323 } else if let Some(f) = value.as_f64() {
324 let _ = write!(s, "{f}");
325 s
326 } else if let Some(b) = value.as_bool() {
327 let _ = write!(s, "{b}");
328 s
329 } else if let OwnedValue::String(val) = value {
330 val.clone().into()
331 } else {
332 return Err(crate::CommonError::Json(crate::safe_format!(
333 "Unsupported JSON value type for key '{key}'"
334 )));
335 }
336 };
337 params.push((key_str, value_str));
338 }
339
340 params.sort_by(|a, b| a.0.cmp(&b.0));
342
343 let mut result = SmartString::new();
345 for (i, (key, value)) in params.iter().enumerate() {
346 if i > 0 {
347 result.push('&');
348 }
349
350 let mut encoded_key = SmartString::new();
352 Self::encode_params_zero_copy(key, &mut encoded_key)?;
353 result.push_str(&encoded_key);
354
355 result.push('=');
356
357 let mut encoded_value = SmartString::new();
359 Self::encode_params_zero_copy(value, &mut encoded_value)?;
360 result.push_str(&encoded_value);
361 }
362
363 Ok(result)
364 }
365
366 pub fn build_query_string(params: &[(&str, &str)]) -> Result<SmartString> {
375 Ok(build_query_smartstring(params))
378 }
379
380 #[inline]
382 #[must_use]
383 pub fn param_count(params: Option<&[(&str, &str)]>) -> usize {
384 params.map_or(0, |p| p.len())
385 }
386
387 pub fn generate_ws_auth(&self) -> Result<SmartString> {
390 Ok("PING".into())
394 }
395
396 #[must_use]
398 pub fn generate_joined_param(params: &[(&str, &str)]) -> Option<SmartString> {
399 if params.is_empty() {
400 None
401 } else {
402 Self::build_param_string_optimized(params).ok()
403 }
404 }
405
406 #[must_use]
408 pub fn api_key(&self) -> &str {
409 &self.api_key
410 }
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416
417 fn create_test_auth() -> BithumbAuth {
418 BithumbAuth::new("test_api_key".into(), "test_secret_key".into())
419 }
420
421 #[test]
422 fn test_new() {
423 let auth = create_test_auth();
424 assert_eq!(auth.api_key(), "test_api_key");
425 }
426
427 #[test]
428 fn test_generate_nonce_format() {
429 let nonce = BithumbAuth::generate_nonce();
430 assert_eq!(nonce.len(), 36);
432 assert_eq!(nonce.chars().nth(8), Some('-'));
433 assert_eq!(nonce.chars().nth(13), Some('-'));
434 assert_eq!(nonce.chars().nth(18), Some('-'));
435 assert_eq!(nonce.chars().nth(23), Some('-'));
436 }
437
438 #[test]
439 fn test_generate_nonce_uniqueness() {
440 let nonce1 = BithumbAuth::generate_nonce();
441 let nonce2 = BithumbAuth::generate_nonce();
442 assert_ne!(nonce1, nonce2);
443 }
444
445 #[test]
446 fn test_generate_timestamp() {
447 let timestamp1 = BithumbAuth::generate_timestamp();
448 std::thread::sleep(std::time::Duration::from_millis(1));
449 let timestamp2 = BithumbAuth::generate_timestamp();
450
451 assert!(timestamp2 > timestamp1);
452 assert!(timestamp1 > 1_577_836_800_000); }
455
456 #[test]
457 fn test_generate_query_hash_empty() {
458 let hash = BithumbAuth::generate_query_hash("");
459 assert_eq!(hash, SmartString::from(""));
460 }
461
462 #[test]
463 fn test_generate_query_hash_non_empty() {
464 let hash = BithumbAuth::generate_query_hash("test=value");
465 assert_eq!(hash.len(), 128);
467 assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
468 }
469
470 #[test]
471 fn test_generate_query_hash_consistency() {
472 let input = "key1=value1&key2=value2";
473 let hash1 = BithumbAuth::generate_query_hash(input);
474 let hash2 = BithumbAuth::generate_query_hash(input);
475 assert_eq!(hash1, hash2);
476 }
477
478 #[test]
479 fn test_encode_params_zero_copy() {
480 let input = "param=value with spaces";
481 let mut buffer = SmartString::new();
482 BithumbAuth::encode_params_zero_copy(input, &mut buffer).unwrap();
483 assert_eq!(buffer, "param%3Dvalue%20with%20spaces");
484
485 let safe_input = "ABC-_.~";
487 BithumbAuth::encode_params_zero_copy(safe_input, &mut buffer).unwrap();
488 assert_eq!(buffer, safe_input);
489 }
490
491 #[test]
492 fn test_generate_joined_param_empty() {
493 let params: &[(&str, &str)] = &[];
494 let result = BithumbAuth::generate_joined_param(params);
495
496 assert!(result.is_none());
497 }
498
499 #[test]
500 fn test_generate_joined_param_single() {
501 let params = &[("key", "value")];
502 let result = BithumbAuth::generate_joined_param(params);
503
504 assert!(result.is_some());
505
506 let result = result.unwrap();
507 assert_eq!(result, SmartString::from("key=value"));
508 }
509
510 #[test]
511 fn test_generate_joined_param_multiple() {
512 let params = &[("key1", "value1"), ("key2", "value2")];
513 let result = BithumbAuth::generate_joined_param(params);
514
515 assert!(result.is_some());
516
517 let result = result.unwrap();
518 assert_eq!(result, SmartString::from("key1=value1&key2=value2"));
519 }
520
521 #[test]
522 fn test_generate_jwt_without_query_hash() {
523 let auth = create_test_auth();
524 let jwt = auth.generate_jwt(None).unwrap();
525
526 let parts: Vec<&str> = jwt.split('.').collect();
528 assert_eq!(parts.len(), 3);
529
530 for part in parts {
532 assert!(part.chars().all(|c| c.is_alphanumeric()
533 || c == '+'
534 || c == '/'
535 || c == '='
536 || c == '-'
537 || c == '_'));
538 }
539 }
540
541 #[test]
542 fn test_generate_jwt_with_query_hash() {
543 let auth = create_test_auth();
544 let query_hash = Some("test_hash".into());
545 let jwt = auth.generate_jwt(query_hash).unwrap();
546
547 let parts: Vec<&str> = jwt.split('.').collect();
549 assert_eq!(parts.len(), 3);
550 assert!(!jwt.is_empty());
551 }
552
553 #[test]
554 fn test_generate_auth_headers_with_query_hash() {
555 let auth = create_test_auth();
556
557 let jwt = auth.generate_jwt(None);
558 let second_jwt = auth.generate_jwt(Some(SmartString::new()));
559
560 assert!(jwt.is_ok());
561 assert!(second_jwt.is_ok());
562
563 assert_ne!(jwt.unwrap(), second_jwt.unwrap());
564 }
565
566 #[test]
567 fn test_generate_headers_no_params() {
568 let auth = create_test_auth();
569 let headers = auth.generate_headers("GET", "/test", None).unwrap();
570
571 assert!(headers.contains_key(&SmartString::from("Authorization")));
572 assert!(headers.contains_key(&SmartString::from("Content-Type")));
573
574 let auth_header = headers.get(&SmartString::from("Authorization")).unwrap();
575 assert!(auth_header.starts_with("Bearer "));
576
577 let content_type = headers.get(&SmartString::from("Content-Type")).unwrap();
578 assert_eq!(
579 content_type,
580 &SmartString::from("application/json; charset=utf-8")
581 );
582 }
583
584 #[test]
585 fn test_generate_headers_with_params() {
586 let auth = create_test_auth();
587 let params = &[("key1", "value1"), ("key2", "value2")];
588 let headers = auth
589 .generate_headers("POST", "/test", Some(params))
590 .unwrap();
591
592 assert!(headers.contains_key(&SmartString::from("Authorization")));
593 assert!(headers.contains_key(&SmartString::from("Content-Type")));
594
595 let auth_header = headers.get(&SmartString::from("Authorization")).unwrap();
596 assert!(auth_header.starts_with("Bearer "));
597 assert!(auth_header.len() > 10); }
599
600 #[test]
601 fn test_generate_headers_different_params_different_tokens() {
602 let auth = create_test_auth();
603 let params1 = &[("key1", "value1")];
604 let params2 = &[("key2", "value2")];
605
606 let headers1 = auth
607 .generate_headers("POST", "/test", Some(params1))
608 .unwrap();
609 let headers2 = auth
610 .generate_headers("POST", "/test", Some(params2))
611 .unwrap();
612
613 let auth1 = headers1.get(&SmartString::from("Authorization")).unwrap();
614 let auth2 = headers2.get(&SmartString::from("Authorization")).unwrap();
615
616 assert_ne!(auth1, auth2);
618 }
619
620 #[test]
621 fn test_generate_ws_auth() {
622 let auth = create_test_auth();
623 let ws_auth = auth.generate_ws_auth().unwrap();
624 assert_eq!(ws_auth, SmartString::from("PING"));
625 }
626
627 #[test]
628 fn test_api_key() {
629 let auth = create_test_auth();
630 assert_eq!(auth.api_key(), "test_api_key");
631 }
632
633 #[test]
634 fn test_hash_algorithm_constant() {
635 assert_eq!(HASH_ALGORITHM, "SHA512");
636 }
637
638 #[test]
639 fn test_jwt_claims_structure() {
640 let auth = create_test_auth();
641 let jwt = auth.generate_jwt(None).unwrap();
642
643 use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode};
645 use serde::Deserialize;
646
647 #[derive(Deserialize)]
648 struct Claims {
649 access_key: String,
650 nonce: String,
651 timestamp: u64,
652 #[serde(skip_serializing_if = "Option::is_none")]
653 query_hash: Option<String>,
654 #[serde(skip_serializing_if = "Option::is_none")]
655 query_hash_alg: Option<String>,
656 }
657
658 let key = DecodingKey::from_secret("test_secret_key".as_bytes());
659 let mut validation = Validation::new(Algorithm::HS256);
660 validation.validate_exp = false; validation.validate_nbf = false; validation.required_spec_claims.clear(); let decoded = decode::<Claims>(&jwt, &key, &validation);
665 if let Err(ref e) = decoded {
666 panic!("JWT decode failed: {e:?}");
667 }
668
669 let claims = decoded.unwrap().claims;
670 assert_eq!(claims.access_key, "test_api_key");
671 assert!(!claims.nonce.is_empty());
672 assert!(claims.timestamp > 0);
673 assert!(claims.query_hash.is_none()); assert!(claims.query_hash_alg.is_none()); }
676
677 #[test]
678 fn test_jwt_claims_with_query_hash() {
679 let auth = create_test_auth();
680 let query_hash = Some("test_query_hash".into());
681 let jwt = auth.generate_jwt(query_hash).unwrap();
682
683 use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode};
685 use serde::Deserialize;
686
687 #[derive(Deserialize)]
688 struct Claims {
689 access_key: String,
690 nonce: String,
691 timestamp: u64,
692 query_hash: Option<String>,
693 query_hash_alg: Option<String>,
694 }
695
696 let key = DecodingKey::from_secret("test_secret_key".as_bytes());
697 let mut validation = Validation::new(Algorithm::HS256);
698 validation.validate_exp = false;
699 validation.validate_nbf = false;
700 validation.required_spec_claims.clear(); let decoded = decode::<Claims>(&jwt, &key, &validation);
703 if let Err(ref e) = decoded {
704 panic!("JWT decode failed: {e:?}");
705 }
706
707 let claims = decoded.unwrap().claims;
708 assert_eq!(claims.access_key, "test_api_key");
709 assert!(!claims.nonce.is_empty());
710 assert!(claims.timestamp > 0);
711 assert_eq!(claims.query_hash, Some("test_query_hash".to_string()));
712 assert_eq!(claims.query_hash_alg, Some("SHA512".to_string()));
713 }
714
715 #[test]
716 fn test_smallvec_performance_advantage() {
717 let params = &[
719 ("param1", "value1"),
720 ("param2", "value2"),
721 ("param3", "value3"),
722 ("param4", "value4"),
723 ("param5", "value5"),
724 ("param6", "value6"),
725 ("param7", "value7"),
726 ("param8", "value8"),
727 ];
728
729 let result = BithumbAuth::build_param_string_optimized(params).unwrap();
730 assert!(result.contains("param1=value1"));
731 assert!(result.contains("param8=value8"));
732 assert_eq!(result.matches('&').count(), 7); }
734
735 #[test]
736 fn test_optimized_methods() {
737 let auth = create_test_auth();
738
739 let headers = auth.generate_headers("POST", "/trade/place", None).unwrap();
741 assert!(headers.contains_key(&SmartString::from("Authorization")));
742 assert!(headers.contains_key(&SmartString::from("Content-Type")));
743
744 let params = &[("symbol", "BTC_KRW"), ("side", "buy")];
746 let query_string = BithumbAuth::build_query_string(params).unwrap();
747 assert!(!query_string.is_empty());
748 assert!(query_string.contains("symbol"));
749 assert!(query_string.contains("buy"));
750
751 assert_eq!(BithumbAuth::param_count(Some(params)), 2);
753 assert_eq!(BithumbAuth::param_count(None), 0);
754
755 let nanos1 = BithumbAuth::generate_timestamp_nanos();
757 let nanos2 = BithumbAuth::generate_timestamp_nanos();
758 assert!(nanos2 >= nanos1);
759 }
760
761 #[test]
762 fn test_header_keys_module() {
763 assert_eq!(header_keys::AUTHORIZATION, "Authorization");
765 assert_eq!(header_keys::CONTENT_TYPE, "Content-Type");
766 assert_eq!(
767 header_keys::CONTENT_TYPE_VALUE,
768 "application/json; charset=utf-8"
769 );
770
771 let auth_header = header_keys::authorization();
773 let content_type = header_keys::content_type();
774 let content_value = header_keys::content_type_value();
775
776 assert_eq!(auth_header, "Authorization");
777 assert_eq!(content_type, "Content-Type");
778 assert_eq!(content_value, "application/json; charset=utf-8");
779 }
780
781 #[test]
782 fn test_generate_headers_consistency() {
783 let auth = create_test_auth();
784 let params = &[("symbol", "BTC_KRW"), ("amount", "100")];
785
786 let headers1 = auth
788 .generate_headers("POST", "/trade/place", Some(params))
789 .unwrap();
790 let headers2 = auth
791 .generate_headers("POST", "/trade/place", Some(params))
792 .unwrap();
793
794 assert_eq!(headers1.len(), headers2.len());
796 assert!(headers1.contains_key(&header_keys::authorization()));
797 assert!(headers1.contains_key(&header_keys::content_type()));
798 assert!(headers2.contains_key(&header_keys::authorization()));
799 assert!(headers2.contains_key(&header_keys::content_type()));
800 }
801
802 #[test]
803 fn test_clone_and_debug() {
804 let auth = create_test_auth();
805 let auth_clone = auth.clone();
806
807 assert_eq!(auth.api_key(), auth_clone.api_key());
808
809 let debug_output = format!("{auth:?}");
811 assert!(debug_output.contains("BithumbAuth"));
812 }
813
814 #[test]
815 fn test_performance_optimizations() {
816 let auth = create_test_auth();
817
818 let headers = auth.generate_headers("GET", "/info/balance", None).unwrap();
820 assert_eq!(headers.len(), 2); let params = &[("order_id", "12345"), ("currency", "BTC")];
824 let headers_with_params = auth
825 .generate_headers("POST", "/trade/cancel", Some(params))
826 .unwrap();
827 assert_eq!(headers_with_params.len(), 2); let auth_value = headers.get(&header_keys::authorization()).unwrap();
831 let content_type_value = headers.get(&header_keys::content_type()).unwrap();
832
833 assert!(auth_value.starts_with("Bearer "));
834 assert_eq!(content_type_value, &header_keys::content_type_value());
835 }
836
837 #[test]
838 fn test_stack_allocation_performance() {
839 let small_params = &[("key", "value")];
843 let small_result = BithumbAuth::build_param_string_optimized(small_params).unwrap();
844 assert_eq!(small_result, "key=value");
845
846 let medium_params = &[
848 ("param1", "value1"),
849 ("param2", "value2"),
850 ("param3", "value3"),
851 ("param4", "value4"),
852 ("param5", "value5"),
853 ("param6", "value6"),
854 ("param7", "value7"),
855 ("param8", "value8"),
856 ];
857 let medium_result = BithumbAuth::build_param_string_optimized(medium_params).unwrap();
858 assert_eq!(medium_result.matches('&').count(), 7); let empty_params: &[(&str, &str)] = &[];
862 let empty_result = BithumbAuth::build_param_string_optimized(empty_params).unwrap();
863 assert!(empty_result.is_empty());
864 }
865
866 #[test]
867 fn test_generate_headers_for_json_body_empty() {
868 let auth = create_test_auth();
869 let headers = auth.generate_headers_for_json_body("{}").unwrap();
870
871 assert_eq!(headers.len(), 2);
872 assert!(headers.contains_key(&header_keys::authorization()));
873 assert!(headers.contains_key(&header_keys::content_type()));
874
875 let content_type = headers.get(&header_keys::content_type()).unwrap();
876 assert_eq!(content_type, &header_keys::content_type_value());
877 }
878
879 #[test]
880 fn test_generate_headers_for_json_body_with_data() {
881 let auth = create_test_auth();
882 let json_body = r#"{"symbol":"BTC_KRW","side":"buy","amount":"100"}"#;
883 let headers = auth.generate_headers_for_json_body(json_body).unwrap();
884
885 assert_eq!(headers.len(), 2);
886 assert!(headers.contains_key(&header_keys::authorization()));
887 assert!(headers.contains_key(&header_keys::content_type()));
888
889 let auth_header = headers.get(&header_keys::authorization()).unwrap();
890 assert!(auth_header.starts_with("Bearer "));
891 assert!(auth_header.len() > 10); }
893
894 #[test]
895 fn test_json_to_url_encoded_empty() {
896 let result = BithumbAuth::json_to_url_encoded("{}").unwrap();
897 assert!(result.is_empty());
898 }
899
900 #[test]
901 fn test_json_to_url_encoded_single_param() {
902 let json = r#"{"key":"value"}"#;
903 let result = BithumbAuth::json_to_url_encoded(json).unwrap();
904 assert_eq!(result, "key=value");
905 }
906
907 #[test]
908 fn test_json_to_url_encoded_multiple_params() {
909 let json = r#"{"symbol":"BTC_KRW","side":"buy","amount":"100"}"#;
910 let result = BithumbAuth::json_to_url_encoded(json).unwrap();
911
912 assert!(result.contains("amount=100"));
914 assert!(result.contains("side=buy"));
915 assert!(result.contains("symbol=BTC_KRW"));
916
917 assert!(result.starts_with("amount=100"));
919 assert!(result.contains("side=buy"));
920 assert!(result.ends_with("symbol=BTC_KRW"));
921 }
922
923 #[test]
924 fn test_json_to_url_encoded_numeric_types() {
925 let json = r#"{"price":84000000,"volume":0.001,"is_test":true}"#;
926 let result = BithumbAuth::json_to_url_encoded(json).unwrap();
927
928 assert!(result.contains("price=84000000"));
929 assert!(result.contains("volume=0.001"));
930 assert!(result.contains("is_test=true"));
931 }
932
933 #[test]
934 fn test_json_to_url_encoded_url_encoding() {
935 let json = r#"{"message":"hello world","special":"key=value&other"}"#;
936 let result = BithumbAuth::json_to_url_encoded(json).unwrap();
937
938 assert!(result.contains("hello%20world"));
940 assert!(result.contains("key%3Dvalue%26other"));
941 }
942
943 #[test]
944 fn test_json_to_url_encoded_invalid_json() {
945 let invalid_json = "{invalid json}";
946 let result = BithumbAuth::json_to_url_encoded(invalid_json);
947 assert!(result.is_err());
948 }
949
950 #[test]
951 fn test_json_to_url_encoded_non_object() {
952 let array_json = "[1, 2, 3]";
953 let result = BithumbAuth::json_to_url_encoded(array_json);
954 assert!(result.is_err());
955
956 let string_json = "\"hello\"";
957 let result = BithumbAuth::json_to_url_encoded(string_json);
958 assert!(result.is_err());
959 }
960
961 #[test]
962 fn test_json_to_url_encoded_null_values() {
963 let json = r#"{
965 "symbol": "BTC_KRW",
966 "amount": 100,
967 "comment": null,
968 "side": "buy",
969 "metadata": null
970 }"#;
971 let result = BithumbAuth::json_to_url_encoded(json).unwrap();
972
973 assert!(result.contains("symbol=BTC_KRW"));
975 assert!(result.contains("amount=100"));
976 assert!(result.contains("side=buy"));
977
978 assert!(!result.contains("comment"));
980 assert!(!result.contains("metadata"));
981 assert!(!result.contains("null"));
982
983 let null_only_json = r#"{"field1": null, "field2": null}"#;
985 let null_result = BithumbAuth::json_to_url_encoded(null_only_json).unwrap();
986 assert_eq!(null_result, ""); }
988
989 #[test]
990 fn test_json_body_vs_params_consistency() {
991 let auth = create_test_auth();
992
993 let params = &[("amount", "100"), ("side", "buy"), ("symbol", "BTC_KRW")];
995 let json_body = r#"{"symbol":"BTC_KRW","side":"buy","amount":"100"}"#;
996
997 let headers_params = auth
998 .generate_headers("POST", "/test", Some(params))
999 .unwrap();
1000 let headers_json = auth.generate_headers_for_json_body(json_body).unwrap();
1001
1002 assert_eq!(headers_params.len(), headers_json.len());
1004 assert!(headers_params.contains_key(&header_keys::authorization()));
1005 assert!(headers_json.contains_key(&header_keys::authorization()));
1006
1007 let content_type_params = headers_params.get(&header_keys::content_type()).unwrap();
1009 let content_type_json = headers_json.get(&header_keys::content_type()).unwrap();
1010 assert_eq!(content_type_params, content_type_json);
1011 assert_eq!(content_type_json, &header_keys::content_type_value());
1012 }
1013
1014 #[test]
1015 fn test_json_body_authentication_flow() {
1016 let auth = create_test_auth();
1017
1018 let order_data = r#"{
1020 "symbol": "BTC_KRW",
1021 "side": "buy",
1022 "order_type": "limit",
1023 "price": 84000000,
1024 "volume": 0.001
1025 }"#;
1026
1027 let headers = auth.generate_headers_for_json_body(order_data).unwrap();
1028
1029 assert!(headers.contains_key(&header_keys::authorization()));
1031 assert!(headers.contains_key(&header_keys::content_type()));
1032
1033 let auth_header = headers.get(&header_keys::authorization()).unwrap();
1035 assert!(auth_header.starts_with("Bearer "));
1036
1037 let jwt_token = auth_header.strip_prefix("Bearer ").unwrap();
1039 let parts: Vec<&str> = jwt_token.split('.').collect();
1040 assert_eq!(parts.len(), 3);
1041
1042 let content_type = headers.get(&header_keys::content_type()).unwrap();
1044 assert_eq!(content_type, "application/json; charset=utf-8");
1045 }
1046}