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(&timestamp, &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, &timestamp, &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    use super::*;
425    use ccxt_core::ExchangeConfig;
426
427    #[test]
428    fn test_http_method_default() {
429        assert_eq!(HttpMethod::default(), HttpMethod::Get);
430    }
431
432    #[test]
433    fn test_builder_construction() {
434        let config = ExchangeConfig::default();
435        let bitget = Bitget::new(config).unwrap();
436
437        let builder = BitgetSignedRequestBuilder::new(&bitget, "/api/v2/spot/account/assets");
438
439        assert_eq!(builder.endpoint, "/api/v2/spot/account/assets");
440        assert_eq!(builder.method, HttpMethod::Get);
441        assert!(builder.params.is_empty());
442        assert!(builder.body.is_none());
443    }
444
445    #[test]
446    fn test_builder_method_chaining() {
447        let config = ExchangeConfig::default();
448        let bitget = Bitget::new(config).unwrap();
449
450        let builder = BitgetSignedRequestBuilder::new(&bitget, "/api/v2/spot/trade/place-order")
451            .method(HttpMethod::Post)
452            .param("symbol", "BTCUSDT")
453            .param("side", "buy");
454
455        assert_eq!(builder.method, HttpMethod::Post);
456        assert_eq!(builder.params.get("symbol"), Some(&"BTCUSDT".to_string()));
457        assert_eq!(builder.params.get("side"), Some(&"buy".to_string()));
458    }
459
460    #[test]
461    fn test_builder_param() {
462        let config = ExchangeConfig::default();
463        let bitget = Bitget::new(config).unwrap();
464
465        let builder = BitgetSignedRequestBuilder::new(&bitget, "/test")
466            .param("string_param", "value")
467            .param("int_param", 123)
468            .param("float_param", 45.67);
469
470        assert_eq!(
471            builder.params.get("string_param"),
472            Some(&"value".to_string())
473        );
474        assert_eq!(builder.params.get("int_param"), Some(&"123".to_string()));
475        assert_eq!(
476            builder.params.get("float_param"),
477            Some(&"45.67".to_string())
478        );
479    }
480
481    #[test]
482    fn test_builder_optional_param_some() {
483        let config = ExchangeConfig::default();
484        let bitget = Bitget::new(config).unwrap();
485
486        let builder = BitgetSignedRequestBuilder::new(&bitget, "/test")
487            .optional_param("limit", Some(100u32))
488            .optional_param("after", Some(1234567890i64));
489
490        assert_eq!(builder.params.get("limit"), Some(&"100".to_string()));
491        assert_eq!(builder.params.get("after"), Some(&"1234567890".to_string()));
492    }
493
494    #[test]
495    fn test_builder_optional_param_none() {
496        let config = ExchangeConfig::default();
497        let bitget = Bitget::new(config).unwrap();
498
499        let none_value: Option<u32> = None;
500        let builder =
501            BitgetSignedRequestBuilder::new(&bitget, "/test").optional_param("limit", none_value);
502
503        assert!(builder.params.get("limit").is_none());
504    }
505
506    #[test]
507    fn test_builder_params_bulk() {
508        let config = ExchangeConfig::default();
509        let bitget = Bitget::new(config).unwrap();
510
511        let mut params = BTreeMap::new();
512        params.insert("symbol".to_string(), "BTCUSDT".to_string());
513        params.insert("side".to_string(), "buy".to_string());
514
515        let builder = BitgetSignedRequestBuilder::new(&bitget, "/test").params(params);
516
517        assert_eq!(builder.params.get("symbol"), Some(&"BTCUSDT".to_string()));
518        assert_eq!(builder.params.get("side"), Some(&"buy".to_string()));
519    }
520
521    #[test]
522    fn test_builder_body() {
523        let config = ExchangeConfig::default();
524        let bitget = Bitget::new(config).unwrap();
525
526        let body = serde_json::json!({
527            "symbol": "BTCUSDT",
528            "side": "buy"
529        });
530
531        let builder = BitgetSignedRequestBuilder::new(&bitget, "/test").body(body.clone());
532
533        assert_eq!(builder.body, Some(body));
534    }
535
536    #[test]
537    fn test_builder_all_http_methods() {
538        let config = ExchangeConfig::default();
539        let bitget = Bitget::new(config).unwrap();
540
541        // Test all HTTP methods can be set
542        let get_builder = BitgetSignedRequestBuilder::new(&bitget, "/test").method(HttpMethod::Get);
543        assert_eq!(get_builder.method, HttpMethod::Get);
544
545        let post_builder =
546            BitgetSignedRequestBuilder::new(&bitget, "/test").method(HttpMethod::Post);
547        assert_eq!(post_builder.method, HttpMethod::Post);
548
549        let delete_builder =
550            BitgetSignedRequestBuilder::new(&bitget, "/test").method(HttpMethod::Delete);
551        assert_eq!(delete_builder.method, HttpMethod::Delete);
552    }
553
554    #[test]
555    fn test_builder_parameter_ordering() {
556        let config = ExchangeConfig::default();
557        let bitget = Bitget::new(config).unwrap();
558
559        // Add parameters in non-alphabetical order
560        let builder = BitgetSignedRequestBuilder::new(&bitget, "/test")
561            .param("zebra", "z")
562            .param("apple", "a")
563            .param("mango", "m");
564
565        // BTreeMap should maintain alphabetical order
566        let keys: Vec<_> = builder.params.keys().collect();
567        assert_eq!(keys, vec!["apple", "mango", "zebra"]);
568    }
569
570    #[test]
571    fn test_method_to_string() {
572        assert_eq!(method_to_string(HttpMethod::Get), "GET");
573        assert_eq!(method_to_string(HttpMethod::Post), "POST");
574        assert_eq!(method_to_string(HttpMethod::Delete), "DELETE");
575    }
576
577    #[test]
578    fn test_build_query_string() {
579        let mut params = BTreeMap::new();
580        params.insert("symbol".to_string(), "BTCUSDT".to_string());
581        params.insert("side".to_string(), "buy".to_string());
582        params.insert("amount".to_string(), "1.5".to_string());
583
584        let query = build_query_string(&params);
585
586        // BTreeMap maintains alphabetical order
587        assert_eq!(query, "amount=1.5&side=buy&symbol=BTCUSDT");
588    }
589
590    #[test]
591    fn test_build_query_string_empty() {
592        let params = BTreeMap::new();
593        let query = build_query_string(&params);
594        assert!(query.is_empty());
595    }
596}
597
598#[cfg(test)]
599mod property_tests {
600    use super::*;
601    use ccxt_core::ExchangeConfig;
602    use proptest::prelude::*;
603
604    // Strategy to generate valid parameter keys (alphanumeric, non-empty)
605    fn param_key_strategy() -> impl Strategy<Value = String> {
606        "[a-zA-Z][a-zA-Z0-9]{0,19}".prop_map(|s| s)
607    }
608
609    // Strategy to generate valid parameter values
610    fn param_value_strategy() -> impl Strategy<Value = String> {
611        "[a-zA-Z0-9._-]{1,50}".prop_map(|s| s)
612    }
613
614    // Strategy to generate a BTreeMap of parameters
615    fn params_strategy() -> impl Strategy<Value = BTreeMap<String, String>> {
616        proptest::collection::btree_map(param_key_strategy(), param_value_strategy(), 0..10)
617    }
618
619    proptest! {
620        #![proptest_config(ProptestConfig::with_cases(100))]
621
622        /// **Feature: exchange-signed-request-refactor, Property 1: Fluent API Method Chaining**
623        ///
624        /// *For any* sequence of builder method calls (param, optional_param, params, method),
625        /// each method SHALL return Self allowing further method chaining, and the final execute()
626        /// call SHALL consume the builder.
627        ///
628        /// **Validates: Requirements 2.1**
629        #[test]
630        fn prop_fluent_api_method_chaining(
631            params in params_strategy(),
632            other_key in param_key_strategy(),
633            other_value in param_value_strategy()
634        ) {
635            let config = ExchangeConfig::default();
636            let bitget = Bitget::new(config).unwrap();
637
638            // Build with method chaining
639            let builder = BitgetSignedRequestBuilder::new(&bitget, "/test")
640                .method(HttpMethod::Post)
641                .params(params.clone())
642                .param(&other_key, &other_value)
643                .optional_param("optional", Some("value"));
644
645            // Verify all parameters are present
646            for (key, value) in &params {
647                prop_assert_eq!(builder.params.get(key), Some(value));
648            }
649            prop_assert_eq!(builder.params.get(&other_key), Some(&other_value));
650            prop_assert_eq!(builder.params.get("optional"), Some(&"value".to_string()));
651            prop_assert_eq!(builder.method, HttpMethod::Post);
652        }
653
654        /// **Feature: exchange-signed-request-refactor, Property 5: Parameter Methods Correctly Populate Params**
655        ///
656        /// *For any* combination of param(), optional_param(), and params() calls:
657        /// - param(key, value) SHALL always add the key-value pair
658        /// - optional_param(key, Some(value)) SHALL add the key-value pair
659        /// - optional_param(key, None) SHALL NOT add any key-value pair
660        /// - params(map) SHALL add all key-value pairs from the map
661        ///
662        /// **Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7**
663        #[test]
664        fn prop_optional_param_conditional_addition(
665            key in param_key_strategy(),
666            value in proptest::option::of(param_value_strategy()),
667            other_key in param_key_strategy(),
668            other_value in param_value_strategy()
669        ) {
670            let config = ExchangeConfig::default();
671            let bitget = Bitget::new(config).unwrap();
672
673            // Build with optional parameter
674            let builder = BitgetSignedRequestBuilder::new(&bitget, "/test")
675                .param(&other_key, &other_value)
676                .optional_param(&key, value.clone());
677
678            // Property: If value is Some, parameter should be present with correct value
679            // If value is None, parameter should NOT be present
680            match value {
681                Some(v) => {
682                    prop_assert!(
683                        builder.params.contains_key(&key),
684                        "Parameter {} should be present when value is Some",
685                        key
686                    );
687                    prop_assert_eq!(
688                        builder.params.get(&key).unwrap(),
689                        &v,
690                        "Parameter {} should have correct value",
691                        key
692                    );
693                }
694                None => {
695                    // Only check if key is different from other_key
696                    if key != other_key {
697                        prop_assert!(
698                            !builder.params.contains_key(&key),
699                            "Parameter {} should NOT be present when value is None",
700                            key
701                        );
702                    }
703                }
704            }
705
706            // Property: Other parameters should always be present
707            prop_assert!(
708                builder.params.contains_key(&other_key),
709                "Other parameter {} should always be present",
710                other_key
711            );
712            prop_assert_eq!(
713                builder.params.get(&other_key).unwrap(),
714                &other_value,
715                "Other parameter {} should have correct value",
716                other_key
717            );
718        }
719    }
720}