ccxt_exchanges/bitget/signed_request.rs
1//! Signed request builder for Bitget API.
2//!
3//! This module provides a builder pattern for creating authenticated Bitget API requests,
4//! encapsulating the common signing workflow used across all authenticated endpoints.
5//!
6//! # Overview
7//!
8//! The `BitgetSignedRequestBuilder` eliminates code duplication by centralizing:
9//! - Credential validation
10//! - Millisecond timestamp generation
11//! - Parameter signing with HMAC-SHA256
12//! - Authentication header injection
13//! - HTTP request execution
14//!
15//! # Example
16//!
17//! ```no_run
18//! # use ccxt_exchanges::bitget::Bitget;
19//! # use ccxt_exchanges::bitget::signed_request::HttpMethod;
20//! # use ccxt_core::ExchangeConfig;
21//! # async fn example() -> ccxt_core::Result<()> {
22//! let bitget = Bitget::new(ExchangeConfig::default())?;
23//!
24//! // Simple GET request
25//! let data = bitget.signed_request("/api/v2/spot/account/assets")
26//! .execute()
27//! .await?;
28//!
29//! // POST request with parameters
30//! let data = bitget.signed_request("/api/v2/spot/trade/place-order")
31//! .method(HttpMethod::Post)
32//! .param("symbol", "BTCUSDT")
33//! .param("side", "buy")
34//! .execute()
35//! .await?;
36//! # Ok(())
37//! # }
38//! ```
39
40use super::Bitget;
41use ccxt_core::{Error, ParseError, Result};
42use reqwest::header::HeaderMap;
43use serde_json::Value;
44use std::collections::BTreeMap;
45
46/// HTTP request methods supported by the signed request builder.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum HttpMethod {
49 /// GET request - parameters in query string
50 #[default]
51 Get,
52 /// POST request - parameters in JSON body
53 Post,
54 /// DELETE request - parameters in JSON body
55 Delete,
56}
57
58/// Builder for creating authenticated Bitget API requests.
59///
60/// This builder encapsulates the common signing workflow:
61/// 1. Credential validation
62/// 2. Millisecond timestamp generation
63/// 3. Parameter signing with HMAC-SHA256
64/// 4. Authentication header injection
65/// 5. HTTP request execution
66///
67/// # Example
68///
69/// ```no_run
70/// # use ccxt_exchanges::bitget::Bitget;
71/// # use ccxt_exchanges::bitget::signed_request::HttpMethod;
72/// # use ccxt_core::ExchangeConfig;
73/// # async fn example() -> ccxt_core::Result<()> {
74/// let bitget = Bitget::new(ExchangeConfig::default())?;
75///
76/// let data = bitget.signed_request("/api/v2/spot/account/assets")
77/// .param("symbol", "BTCUSDT")
78/// .optional_param("limit", Some(100))
79/// .execute()
80/// .await?;
81/// # Ok(())
82/// # }
83/// ```
84pub struct BitgetSignedRequestBuilder<'a> {
85 /// Reference to the Bitget exchange instance
86 bitget: &'a Bitget,
87 /// Request parameters to be signed
88 params: BTreeMap<String, String>,
89 /// Request body for POST/DELETE requests
90 body: Option<Value>,
91 /// API endpoint path
92 endpoint: String,
93 /// HTTP method for the request
94 method: HttpMethod,
95}
96
97impl<'a> BitgetSignedRequestBuilder<'a> {
98 /// Creates a new signed request builder.
99 ///
100 /// # Arguments
101 ///
102 /// * `bitget` - Reference to the Bitget exchange instance
103 /// * `endpoint` - API endpoint path (e.g., "/api/v2/spot/account/assets")
104 ///
105 /// # Example
106 ///
107 /// ```no_run
108 /// # use ccxt_exchanges::bitget::Bitget;
109 /// # use ccxt_exchanges::bitget::signed_request::BitgetSignedRequestBuilder;
110 /// # use ccxt_core::ExchangeConfig;
111 /// let bitget = Bitget::new(ExchangeConfig::default()).unwrap();
112 /// let builder = BitgetSignedRequestBuilder::new(&bitget, "/api/v2/spot/account/assets");
113 /// ```
114 pub fn new(bitget: &'a Bitget, endpoint: impl Into<String>) -> Self {
115 Self {
116 bitget,
117 params: BTreeMap::new(),
118 body: None,
119 endpoint: endpoint.into(),
120 method: HttpMethod::default(),
121 }
122 }
123
124 /// Sets the HTTP method for the request.
125 ///
126 /// Default is GET.
127 ///
128 /// # Arguments
129 ///
130 /// * `method` - The HTTP method to use
131 ///
132 /// # Example
133 ///
134 /// ```no_run
135 /// # use ccxt_exchanges::bitget::Bitget;
136 /// # use ccxt_exchanges::bitget::signed_request::HttpMethod;
137 /// # use ccxt_core::ExchangeConfig;
138 /// # async fn example() -> ccxt_core::Result<()> {
139 /// let bitget = Bitget::new(ExchangeConfig::default())?;
140 /// let data = bitget.signed_request("/api/v2/spot/trade/place-order")
141 /// .method(HttpMethod::Post)
142 /// .param("symbol", "BTCUSDT")
143 /// .execute()
144 /// .await?;
145 /// # Ok(())
146 /// # }
147 /// ```
148 pub fn method(mut self, method: HttpMethod) -> Self {
149 self.method = method;
150 self
151 }
152
153 /// Adds a required parameter.
154 ///
155 /// # Arguments
156 ///
157 /// * `key` - Parameter name
158 /// * `value` - Parameter value (will be converted to string)
159 ///
160 /// # Example
161 ///
162 /// ```no_run
163 /// # use ccxt_exchanges::bitget::Bitget;
164 /// # use ccxt_core::ExchangeConfig;
165 /// # async fn example() -> ccxt_core::Result<()> {
166 /// let bitget = Bitget::new(ExchangeConfig::default())?;
167 /// let data = bitget.signed_request("/api/v2/spot/trade/orders-history")
168 /// .param("symbol", "BTCUSDT")
169 /// .param("limit", 100)
170 /// .execute()
171 /// .await?;
172 /// # Ok(())
173 /// # }
174 /// ```
175 pub fn param(mut self, key: impl Into<String>, value: impl ToString) -> Self {
176 self.params.insert(key.into(), value.to_string());
177 self
178 }
179
180 /// Adds an optional parameter (only if value is Some).
181 ///
182 /// # Arguments
183 ///
184 /// * `key` - Parameter name
185 /// * `value` - Optional parameter value
186 ///
187 /// # Example
188 ///
189 /// ```no_run
190 /// # use ccxt_exchanges::bitget::Bitget;
191 /// # use ccxt_core::ExchangeConfig;
192 /// # async fn example() -> ccxt_core::Result<()> {
193 /// let bitget = Bitget::new(ExchangeConfig::default())?;
194 /// let since: Option<i64> = Some(1234567890000);
195 /// let limit: Option<u32> = None;
196 ///
197 /// let data = bitget.signed_request("/api/v2/spot/trade/orders-history")
198 /// .param("symbol", "BTCUSDT")
199 /// .optional_param("after", since)
200 /// .optional_param("limit", limit) // Won't be added since it's None
201 /// .execute()
202 /// .await?;
203 /// # Ok(())
204 /// # }
205 /// ```
206 pub fn optional_param<T: ToString>(mut self, key: impl Into<String>, value: Option<T>) -> Self {
207 if let Some(v) = value {
208 self.params.insert(key.into(), v.to_string());
209 }
210 self
211 }
212
213 /// Adds multiple parameters from a BTreeMap.
214 ///
215 /// # Arguments
216 ///
217 /// * `params` - Map of parameter key-value pairs
218 ///
219 /// # Example
220 ///
221 /// ```no_run
222 /// # use ccxt_exchanges::bitget::Bitget;
223 /// # use ccxt_core::ExchangeConfig;
224 /// # use std::collections::BTreeMap;
225 /// # async fn example() -> ccxt_core::Result<()> {
226 /// let bitget = Bitget::new(ExchangeConfig::default())?;
227 /// let mut params = BTreeMap::new();
228 /// params.insert("symbol".to_string(), "BTCUSDT".to_string());
229 /// params.insert("side".to_string(), "buy".to_string());
230 ///
231 /// let data = bitget.signed_request("/api/v2/spot/trade/place-order")
232 /// .params(params)
233 /// .execute()
234 /// .await?;
235 /// # Ok(())
236 /// # }
237 /// ```
238 pub fn params(mut self, params: BTreeMap<String, String>) -> Self {
239 self.params.extend(params);
240 self
241 }
242
243 /// Sets the request body for POST/DELETE requests.
244 ///
245 /// # Arguments
246 ///
247 /// * `body` - JSON value to use as request body
248 ///
249 /// # Example
250 ///
251 /// ```no_run
252 /// # use ccxt_exchanges::bitget::Bitget;
253 /// # use ccxt_core::ExchangeConfig;
254 /// # use serde_json::json;
255 /// # async fn example() -> ccxt_core::Result<()> {
256 /// let bitget = Bitget::new(ExchangeConfig::default())?;
257 /// let body = json!({
258 /// "symbol": "BTCUSDT",
259 /// "side": "buy",
260 /// "orderType": "limit"
261 /// });
262 ///
263 /// let data = bitget.signed_request("/api/v2/spot/trade/place-order")
264 /// .method(ccxt_exchanges::bitget::signed_request::HttpMethod::Post)
265 /// .body(body)
266 /// .execute()
267 /// .await?;
268 /// # Ok(())
269 /// # }
270 /// ```
271 pub fn body(mut self, body: Value) -> Self {
272 self.body = Some(body);
273 self
274 }
275
276 /// Executes the signed request and returns the response.
277 ///
278 /// This method:
279 /// 1. Validates that credentials are configured
280 /// 2. Gets the current millisecond timestamp
281 /// 3. Signs the request with HMAC-SHA256
282 /// 4. Adds authentication headers
283 /// 5. Executes the HTTP request
284 ///
285 /// # Returns
286 ///
287 /// Returns the raw `serde_json::Value` response for further parsing.
288 ///
289 /// # Errors
290 ///
291 /// - Returns authentication error if credentials are missing
292 /// - Returns network error if the request fails
293 /// - Returns parse error if response parsing fails
294 ///
295 /// # Example
296 ///
297 /// ```no_run
298 /// # use ccxt_exchanges::bitget::Bitget;
299 /// # use ccxt_core::ExchangeConfig;
300 /// # async fn example() -> ccxt_core::Result<()> {
301 /// let bitget = Bitget::new(ExchangeConfig::default())?;
302 /// let data = bitget.signed_request("/api/v2/spot/account/assets")
303 /// .execute()
304 /// .await?;
305 /// println!("Response: {:?}", data);
306 /// # Ok(())
307 /// # }
308 /// ```
309 pub async fn execute(self) -> Result<Value> {
310 // Step 1: Validate credentials
311 self.bitget.check_required_credentials()?;
312
313 // Step 2: Get current millisecond timestamp
314 let timestamp = (chrono::Utc::now().timestamp_millis()).to_string();
315
316 // Step 3: Get auth and sign request
317 let auth = self.bitget.get_auth()?;
318
319 // Build the body string for signing
320 let body_str = if let Some(ref body) = self.body {
321 body.to_string()
322 } else if self.method == HttpMethod::Get {
323 String::new()
324 } else {
325 // For POST/DELETE without explicit body, use empty string
326 String::new()
327 };
328
329 // Build the path with query string for GET requests
330 let path = if self.method == HttpMethod::Get && !self.params.is_empty() {
331 format!("{}?{}", self.endpoint, build_query_string(&self.params))
332 } else {
333 self.endpoint.clone()
334 };
335
336 // Sign the request
337 let signature = auth.sign(×tamp, &method_to_string(self.method), &path, &body_str);
338
339 // Step 4: Add authentication headers
340 let mut headers = HeaderMap::new();
341 auth.add_auth_headers(&mut headers, ×tamp, &signature);
342
343 // Step 5: Execute HTTP request based on method
344 let urls = self.bitget.urls();
345 let full_url = format!("{}{}", urls.rest, self.endpoint);
346
347 match self.method {
348 HttpMethod::Get => {
349 // Build query string for GET requests
350 let query_string = build_query_string(&self.params);
351 let url = if query_string.is_empty() {
352 full_url
353 } else {
354 format!("{}?{}", full_url, query_string)
355 };
356 self.bitget
357 .base()
358 .http_client
359 .get(&url, Some(headers))
360 .await
361 }
362 HttpMethod::Post => {
363 // Use explicit body if provided, otherwise use params as JSON
364 let body = if let Some(b) = self.body {
365 b
366 } else {
367 serde_json::to_value(&self.params).map_err(|e| {
368 Error::from(ParseError::invalid_format(
369 "data",
370 format!("Failed to serialize request body: {}", e),
371 ))
372 })?
373 };
374 self.bitget
375 .base()
376 .http_client
377 .post(&full_url, Some(headers), Some(body))
378 .await
379 }
380 HttpMethod::Delete => {
381 // Use explicit body if provided, otherwise use params as JSON
382 let body = if let Some(b) = self.body {
383 b
384 } else {
385 serde_json::to_value(&self.params).map_err(|e| {
386 Error::from(ParseError::invalid_format(
387 "data",
388 format!("Failed to serialize request body: {}", e),
389 ))
390 })?
391 };
392 self.bitget
393 .base()
394 .http_client
395 .delete(&full_url, Some(headers), Some(body))
396 .await
397 }
398 }
399 }
400}
401
402/// Converts HttpMethod to string representation.
403fn method_to_string(method: HttpMethod) -> String {
404 match method {
405 HttpMethod::Get => "GET".to_string(),
406 HttpMethod::Post => "POST".to_string(),
407 HttpMethod::Delete => "DELETE".to_string(),
408 }
409}
410
411/// Builds a URL-encoded query string from parameters.
412///
413/// Parameters are sorted alphabetically by key (due to BTreeMap ordering).
414fn build_query_string(params: &BTreeMap<String, String>) -> String {
415 params
416 .iter()
417 .map(|(k, v)| format!("{}={}", k, v))
418 .collect::<Vec<_>>()
419 .join("&")
420}
421
422#[cfg(test)]
423mod tests {
424 #![allow(clippy::disallowed_methods)]
425 use super::*;
426 use ccxt_core::ExchangeConfig;
427
428 #[test]
429 fn test_http_method_default() {
430 assert_eq!(HttpMethod::default(), HttpMethod::Get);
431 }
432
433 #[test]
434 fn test_builder_construction() {
435 let config = ExchangeConfig::default();
436 let bitget = Bitget::new(config).unwrap();
437
438 let builder = BitgetSignedRequestBuilder::new(&bitget, "/api/v2/spot/account/assets");
439
440 assert_eq!(builder.endpoint, "/api/v2/spot/account/assets");
441 assert_eq!(builder.method, HttpMethod::Get);
442 assert!(builder.params.is_empty());
443 assert!(builder.body.is_none());
444 }
445
446 #[test]
447 fn test_builder_method_chaining() {
448 let config = ExchangeConfig::default();
449 let bitget = Bitget::new(config).unwrap();
450
451 let builder = BitgetSignedRequestBuilder::new(&bitget, "/api/v2/spot/trade/place-order")
452 .method(HttpMethod::Post)
453 .param("symbol", "BTCUSDT")
454 .param("side", "buy");
455
456 assert_eq!(builder.method, HttpMethod::Post);
457 assert_eq!(builder.params.get("symbol"), Some(&"BTCUSDT".to_string()));
458 assert_eq!(builder.params.get("side"), Some(&"buy".to_string()));
459 }
460
461 #[test]
462 fn test_builder_param() {
463 let config = ExchangeConfig::default();
464 let bitget = Bitget::new(config).unwrap();
465
466 let builder = BitgetSignedRequestBuilder::new(&bitget, "/test")
467 .param("string_param", "value")
468 .param("int_param", 123)
469 .param("float_param", 45.67);
470
471 assert_eq!(
472 builder.params.get("string_param"),
473 Some(&"value".to_string())
474 );
475 assert_eq!(builder.params.get("int_param"), Some(&"123".to_string()));
476 assert_eq!(
477 builder.params.get("float_param"),
478 Some(&"45.67".to_string())
479 );
480 }
481
482 #[test]
483 fn test_builder_optional_param_some() {
484 let config = ExchangeConfig::default();
485 let bitget = Bitget::new(config).unwrap();
486
487 let builder = BitgetSignedRequestBuilder::new(&bitget, "/test")
488 .optional_param("limit", Some(100u32))
489 .optional_param("after", Some(1234567890i64));
490
491 assert_eq!(builder.params.get("limit"), Some(&"100".to_string()));
492 assert_eq!(builder.params.get("after"), Some(&"1234567890".to_string()));
493 }
494
495 #[test]
496 fn test_builder_optional_param_none() {
497 let config = ExchangeConfig::default();
498 let bitget = Bitget::new(config).unwrap();
499
500 let none_value: Option<u32> = None;
501 let builder =
502 BitgetSignedRequestBuilder::new(&bitget, "/test").optional_param("limit", none_value);
503
504 assert!(builder.params.get("limit").is_none());
505 }
506
507 #[test]
508 fn test_builder_params_bulk() {
509 let config = ExchangeConfig::default();
510 let bitget = Bitget::new(config).unwrap();
511
512 let mut params = BTreeMap::new();
513 params.insert("symbol".to_string(), "BTCUSDT".to_string());
514 params.insert("side".to_string(), "buy".to_string());
515
516 let builder = BitgetSignedRequestBuilder::new(&bitget, "/test").params(params);
517
518 assert_eq!(builder.params.get("symbol"), Some(&"BTCUSDT".to_string()));
519 assert_eq!(builder.params.get("side"), Some(&"buy".to_string()));
520 }
521
522 #[test]
523 fn test_builder_body() {
524 let config = ExchangeConfig::default();
525 let bitget = Bitget::new(config).unwrap();
526
527 let body = serde_json::json!({
528 "symbol": "BTCUSDT",
529 "side": "buy"
530 });
531
532 let builder = BitgetSignedRequestBuilder::new(&bitget, "/test").body(body.clone());
533
534 assert_eq!(builder.body, Some(body));
535 }
536
537 #[test]
538 fn test_builder_all_http_methods() {
539 let config = ExchangeConfig::default();
540 let bitget = Bitget::new(config).unwrap();
541
542 // Test all HTTP methods can be set
543 let get_builder = BitgetSignedRequestBuilder::new(&bitget, "/test").method(HttpMethod::Get);
544 assert_eq!(get_builder.method, HttpMethod::Get);
545
546 let post_builder =
547 BitgetSignedRequestBuilder::new(&bitget, "/test").method(HttpMethod::Post);
548 assert_eq!(post_builder.method, HttpMethod::Post);
549
550 let delete_builder =
551 BitgetSignedRequestBuilder::new(&bitget, "/test").method(HttpMethod::Delete);
552 assert_eq!(delete_builder.method, HttpMethod::Delete);
553 }
554
555 #[test]
556 fn test_builder_parameter_ordering() {
557 let config = ExchangeConfig::default();
558 let bitget = Bitget::new(config).unwrap();
559
560 // Add parameters in non-alphabetical order
561 let builder = BitgetSignedRequestBuilder::new(&bitget, "/test")
562 .param("zebra", "z")
563 .param("apple", "a")
564 .param("mango", "m");
565
566 // BTreeMap should maintain alphabetical order
567 let keys: Vec<_> = builder.params.keys().collect();
568 assert_eq!(keys, vec!["apple", "mango", "zebra"]);
569 }
570
571 #[test]
572 fn test_method_to_string() {
573 assert_eq!(method_to_string(HttpMethod::Get), "GET");
574 assert_eq!(method_to_string(HttpMethod::Post), "POST");
575 assert_eq!(method_to_string(HttpMethod::Delete), "DELETE");
576 }
577
578 #[test]
579 fn test_build_query_string() {
580 let mut params = BTreeMap::new();
581 params.insert("symbol".to_string(), "BTCUSDT".to_string());
582 params.insert("side".to_string(), "buy".to_string());
583 params.insert("amount".to_string(), "1.5".to_string());
584
585 let query = build_query_string(¶ms);
586
587 // BTreeMap maintains alphabetical order
588 assert_eq!(query, "amount=1.5&side=buy&symbol=BTCUSDT");
589 }
590
591 #[test]
592 fn test_build_query_string_empty() {
593 let params = BTreeMap::new();
594 let query = build_query_string(¶ms);
595 assert!(query.is_empty());
596 }
597}
598
599#[cfg(test)]
600mod property_tests {
601 use super::*;
602 use ccxt_core::ExchangeConfig;
603 use proptest::prelude::*;
604
605 // Strategy to generate valid parameter keys (alphanumeric, non-empty)
606 fn param_key_strategy() -> impl Strategy<Value = String> {
607 "[a-zA-Z][a-zA-Z0-9]{0,19}".prop_map(|s| s)
608 }
609
610 // Strategy to generate valid parameter values
611 fn param_value_strategy() -> impl Strategy<Value = String> {
612 "[a-zA-Z0-9._-]{1,50}".prop_map(|s| s)
613 }
614
615 // Strategy to generate a BTreeMap of parameters
616 fn params_strategy() -> impl Strategy<Value = BTreeMap<String, String>> {
617 proptest::collection::btree_map(param_key_strategy(), param_value_strategy(), 0..10)
618 }
619
620 proptest! {
621 #![proptest_config(ProptestConfig::with_cases(100))]
622
623 /// **Feature: exchange-signed-request-refactor, Property 1: Fluent API Method Chaining**
624 ///
625 /// *For any* sequence of builder method calls (param, optional_param, params, method),
626 /// each method SHALL return Self allowing further method chaining, and the final execute()
627 /// call SHALL consume the builder.
628 ///
629 /// **Validates: Requirements 2.1**
630 #[test]
631 fn prop_fluent_api_method_chaining(
632 params in params_strategy(),
633 other_key in param_key_strategy(),
634 other_value in param_value_strategy()
635 ) {
636 let config = ExchangeConfig::default();
637 let bitget = Bitget::new(config).unwrap();
638
639 // Build with method chaining
640 let builder = BitgetSignedRequestBuilder::new(&bitget, "/test")
641 .method(HttpMethod::Post)
642 .params(params.clone())
643 .param(&other_key, &other_value)
644 .optional_param("optional", Some("value"));
645
646 // Verify all parameters are present
647 for (key, value) in ¶ms {
648 prop_assert_eq!(builder.params.get(key), Some(value));
649 }
650 prop_assert_eq!(builder.params.get(&other_key), Some(&other_value));
651 prop_assert_eq!(builder.params.get("optional"), Some(&"value".to_string()));
652 prop_assert_eq!(builder.method, HttpMethod::Post);
653 }
654
655 /// **Feature: exchange-signed-request-refactor, Property 5: Parameter Methods Correctly Populate Params**
656 ///
657 /// *For any* combination of param(), optional_param(), and params() calls:
658 /// - param(key, value) SHALL always add the key-value pair
659 /// - optional_param(key, Some(value)) SHALL add the key-value pair
660 /// - optional_param(key, None) SHALL NOT add any key-value pair
661 /// - params(map) SHALL add all key-value pairs from the map
662 ///
663 /// **Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7**
664 #[test]
665 fn prop_optional_param_conditional_addition(
666 key in param_key_strategy(),
667 value in proptest::option::of(param_value_strategy()),
668 other_key in param_key_strategy(),
669 other_value in param_value_strategy()
670 ) {
671 let config = ExchangeConfig::default();
672 let bitget = Bitget::new(config).unwrap();
673
674 // Build with optional parameter
675 let builder = BitgetSignedRequestBuilder::new(&bitget, "/test")
676 .param(&other_key, &other_value)
677 .optional_param(&key, value.clone());
678
679 // Property: If value is Some, parameter should be present with correct value
680 // If value is None, parameter should NOT be present
681 match value {
682 Some(v) => {
683 prop_assert!(
684 builder.params.contains_key(&key),
685 "Parameter {} should be present when value is Some",
686 key
687 );
688 prop_assert_eq!(
689 builder.params.get(&key).unwrap(),
690 &v,
691 "Parameter {} should have correct value",
692 key
693 );
694 }
695 None => {
696 // Only check if key is different from other_key
697 if key != other_key {
698 prop_assert!(
699 !builder.params.contains_key(&key),
700 "Parameter {} should NOT be present when value is None",
701 key
702 );
703 }
704 }
705 }
706
707 // Property: Other parameters should always be present
708 prop_assert!(
709 builder.params.contains_key(&other_key),
710 "Other parameter {} should always be present",
711 other_key
712 );
713 prop_assert_eq!(
714 builder.params.get(&other_key).unwrap(),
715 &other_value,
716 "Other parameter {} should have correct value",
717 other_key
718 );
719 }
720 }
721}