veracode_platform/
client.rs

1//! Core Veracode API client implementation.
2//!
3//! This module contains the foundational client for making authenticated requests
4//! to the Veracode API, including HMAC authentication and HTTP request handling.
5
6use hex;
7use hmac::{Hmac, Mac};
8use log::{info, warn};
9use reqwest::{Client, multipart};
10use secrecy::ExposeSecret;
11use serde::Serialize;
12use sha2::Sha256;
13use std::borrow::Cow;
14use std::collections::HashMap;
15use std::fs::File;
16use std::io::{Read, Seek, SeekFrom};
17use std::sync::Arc;
18use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
19use url::Url;
20
21use crate::json_validator::{MAX_JSON_DEPTH, validate_json_depth};
22use crate::{VeracodeConfig, VeracodeError};
23
24// Type aliases for HMAC
25type HmacSha256 = Hmac<Sha256>;
26
27// Constants for authentication error messages to avoid repeated allocations
28const INVALID_URL_MSG: &str = "Invalid URL";
29const INVALID_API_KEY_MSG: &str = "Invalid API key format - must be hex string";
30const INVALID_NONCE_MSG: &str = "Invalid nonce format";
31const HMAC_CREATION_FAILED_MSG: &str = "Failed to create HMAC";
32
33/// Core Veracode API client.
34///
35/// This struct provides the foundational HTTP client with HMAC authentication
36/// for making requests to any Veracode API endpoint.
37#[derive(Clone)]
38pub struct VeracodeClient {
39    config: VeracodeConfig,
40    client: Client,
41}
42
43impl VeracodeClient {
44    /// Build URL with query parameters - centralized helper
45    fn build_url_with_params(&self, endpoint: &str, query_params: &[(&str, &str)]) -> String {
46        // Pre-allocate string capacity for better performance
47        let estimated_capacity = self
48            .config
49            .base_url
50            .len()
51            .saturating_add(endpoint.len())
52            .saturating_add(query_params.len().saturating_mul(32)); // Rough estimate for query params
53
54        let mut url = String::with_capacity(estimated_capacity);
55        url.push_str(&self.config.base_url);
56        url.push_str(endpoint);
57
58        if !query_params.is_empty() {
59            url.push('?');
60            for (i, (key, value)) in query_params.iter().enumerate() {
61                if i > 0 {
62                    url.push('&');
63                }
64                url.push_str(&urlencoding::encode(key));
65                url.push('=');
66                url.push_str(&urlencoding::encode(value));
67            }
68        }
69
70        url
71    }
72
73    /// Create a new Veracode API client.
74    ///
75    /// # Arguments
76    ///
77    /// * `config` - Configuration containing API credentials and settings
78    ///
79    /// # Returns
80    ///
81    /// A new `VeracodeClient` instance ready to make API calls.
82    ///
83    /// # Errors
84    ///
85    /// Returns an error if the API request fails, the resource is not found,
86    /// or authentication/authorization fails.
87    pub fn new(config: VeracodeConfig) -> Result<Self, VeracodeError> {
88        let mut client_builder = Client::builder();
89
90        // Use the certificate validation setting from config
91        if !config.validate_certificates {
92            client_builder = client_builder
93                .danger_accept_invalid_certs(true)
94                .danger_accept_invalid_hostnames(true);
95        }
96
97        // Configure HTTP timeouts from config
98        client_builder = client_builder
99            .connect_timeout(Duration::from_secs(config.connect_timeout))
100            .timeout(Duration::from_secs(config.request_timeout));
101
102        // Configure proxy if specified
103        if let Some(proxy_url) = &config.proxy_url {
104            let mut proxy = reqwest::Proxy::all(proxy_url)
105                .map_err(|e| VeracodeError::InvalidConfig(format!("Invalid proxy URL: {e}")))?;
106
107            // Add basic authentication if credentials are provided
108            if let (Some(username), Some(password)) =
109                (&config.proxy_username, &config.proxy_password)
110            {
111                proxy = proxy.basic_auth(username.expose_secret(), password.expose_secret());
112            }
113
114            client_builder = client_builder.proxy(proxy);
115        }
116
117        let client = client_builder.build().map_err(VeracodeError::Http)?;
118        Ok(Self { config, client })
119    }
120
121    /// Get the base URL for API requests.
122    #[must_use]
123    pub fn base_url(&self) -> &str {
124        &self.config.base_url
125    }
126
127    /// Get access to the configuration
128    #[must_use]
129    pub fn config(&self) -> &VeracodeConfig {
130        &self.config
131    }
132
133    /// Get access to the underlying reqwest client
134    #[must_use]
135    pub fn client(&self) -> &Client {
136        &self.client
137    }
138
139    /// Execute an HTTP request with retry logic and exponential backoff.
140    ///
141    /// This method implements the retry strategy defined in the client's configuration.
142    /// It will retry requests that fail due to transient errors (network issues,
143    /// server errors, rate limiting) using exponential backoff. For rate limiting (429),
144    /// it uses intelligent delays based on Veracode's minute-window rate limits.
145    ///
146    /// # Arguments
147    ///
148    ///
149    /// # Errors
150    ///
151    /// Returns an error if the API request fails, the resource is not found,
152    /// or authentication/authorization fails.
153    /// * `request_builder` - A closure that creates the `reqwest::RequestBuilder`
154    /// * `operation_name` - A human-readable name for logging/error messages
155    ///
156    /// # Returns
157    ///
158    ///
159    /// # Errors
160    ///
161    /// Returns an error if the API request fails, the resource is not found,
162    /// or authentication/authorization fails.
163    /// A `Result` containing the HTTP response or a `VeracodeError`.
164    async fn execute_with_retry<F>(
165        &self,
166        request_builder: F,
167        operation_name: Cow<'_, str>,
168    ) -> Result<reqwest::Response, VeracodeError>
169    where
170        F: Fn() -> reqwest::RequestBuilder,
171    {
172        let retry_config = &self.config.retry_config;
173        let start_time = Instant::now();
174        let mut total_delay = std::time::Duration::from_millis(0);
175
176        // If retries are disabled, make a single attempt
177        if retry_config.max_attempts == 0 {
178            return match request_builder().send().await {
179                Ok(response) => Ok(response),
180                Err(e) => Err(VeracodeError::Http(e)),
181            };
182        }
183
184        let mut last_error = None;
185        let mut rate_limit_attempts: u32 = 0;
186
187        for attempt in 1..=retry_config.max_attempts.saturating_add(1) {
188            // Build and send the request
189            match request_builder().send().await {
190                Ok(response) => {
191                    // Check for rate limiting before treating as success
192                    if response.status().as_u16() == 429 {
193                        // Extract Retry-After header if present
194                        let retry_after_seconds = response
195                            .headers()
196                            .get("retry-after")
197                            .and_then(|h| h.to_str().ok())
198                            .and_then(|s| s.parse::<u64>().ok());
199
200                        let message = "HTTP 429: Rate limit exceeded".to_string();
201                        let veracode_error = VeracodeError::RateLimited {
202                            retry_after_seconds,
203                            message,
204                        };
205
206                        // Increment rate limit attempt counter
207                        rate_limit_attempts = rate_limit_attempts.saturating_add(1);
208
209                        // Check if we should retry based on rate limit specific limits
210                        if attempt > retry_config.max_attempts
211                            || rate_limit_attempts > retry_config.rate_limit_max_attempts
212                        {
213                            last_error = Some(veracode_error);
214                            break;
215                        }
216
217                        // Calculate rate limit specific delay
218                        let delay = retry_config.calculate_rate_limit_delay(retry_after_seconds);
219                        total_delay = total_delay.saturating_add(delay);
220
221                        // Check total delay limit
222                        if total_delay.as_millis() > retry_config.max_total_delay_ms as u128 {
223                            let msg = format!(
224                                "{} exceeded maximum total retry time of {}ms after {} attempts",
225                                operation_name, retry_config.max_total_delay_ms, attempt
226                            );
227                            last_error = Some(VeracodeError::RetryExhausted(msg));
228                            break;
229                        }
230
231                        // Log rate limit with specific formatting
232                        let wait_time = match retry_after_seconds {
233                            Some(seconds) => format!("{seconds}s (from Retry-After header)"),
234                            None => format!("{}s (until next minute window)", delay.as_secs()),
235                        };
236                        warn!(
237                            "🚦 {operation_name} rate limited on attempt {attempt}, waiting {wait_time}"
238                        );
239
240                        // Wait and continue to next attempt
241                        tokio::time::sleep(delay).await;
242                        last_error = Some(veracode_error);
243                        continue;
244                    }
245
246                    if attempt > 1 {
247                        // Log successful retry for debugging
248                        info!("βœ… {operation_name} succeeded on attempt {attempt}");
249                    }
250                    return Ok(response);
251                }
252                Err(e) => {
253                    // For connection errors, network issues, etc., use normal retry logic
254                    let veracode_error = VeracodeError::Http(e);
255
256                    // Check if this is the last attempt or if the error is not retryable
257                    if attempt > retry_config.max_attempts
258                        || !retry_config.is_retryable_error(&veracode_error)
259                    {
260                        last_error = Some(veracode_error);
261                        break;
262                    }
263
264                    // Use normal exponential backoff for non-429 errors
265                    let delay = retry_config.calculate_delay(attempt);
266                    total_delay = total_delay.saturating_add(delay);
267
268                    // Check if we've exceeded the maximum total delay
269                    if total_delay.as_millis() > retry_config.max_total_delay_ms as u128 {
270                        // Format error message once
271                        let msg = format!(
272                            "{} exceeded maximum total retry time of {}ms after {} attempts",
273                            operation_name, retry_config.max_total_delay_ms, attempt
274                        );
275                        last_error = Some(VeracodeError::RetryExhausted(msg));
276                        break;
277                    }
278
279                    // Log retry attempt for debugging
280                    warn!(
281                        "⚠️  {operation_name} failed on attempt {attempt}, retrying in {}ms: {veracode_error}",
282                        delay.as_millis()
283                    );
284
285                    // Wait before next attempt
286                    tokio::time::sleep(delay).await;
287                    last_error = Some(veracode_error);
288                }
289            }
290        }
291
292        // All attempts failed - create error message efficiently
293        match last_error {
294            Some(error) => {
295                let elapsed = start_time.elapsed();
296                match error {
297                    VeracodeError::RetryExhausted(_) => Err(error),
298                    VeracodeError::Http(_)
299                    | VeracodeError::Serialization(_)
300                    | VeracodeError::Authentication(_)
301                    | VeracodeError::InvalidResponse(_)
302                    | VeracodeError::InvalidConfig(_)
303                    | VeracodeError::NotFound(_)
304                    | VeracodeError::RateLimited { .. }
305                    | VeracodeError::Validation(_) => {
306                        let msg = format!(
307                            "{} failed after {} attempts over {}ms: {}",
308                            operation_name,
309                            retry_config.max_attempts.saturating_add(1),
310                            elapsed.as_millis(),
311                            error
312                        );
313                        Err(VeracodeError::RetryExhausted(msg))
314                    }
315                }
316            }
317            None => {
318                let msg = format!(
319                    "{} failed after {} attempts with unknown error",
320                    operation_name,
321                    retry_config.max_attempts.saturating_add(1)
322                );
323                Err(VeracodeError::RetryExhausted(msg))
324            }
325        }
326    }
327
328    /// Generate HMAC signature for authentication based on official Veracode JavaScript implementation
329    fn generate_hmac_signature(
330        &self,
331        method: &str,
332        url: &str,
333        timestamp: u64,
334        nonce: &str,
335    ) -> Result<String, VeracodeError> {
336        let url_parsed = Url::parse(url)
337            .map_err(|_| VeracodeError::Authentication(INVALID_URL_MSG.to_string()))?;
338
339        let path_and_query = match url_parsed.query() {
340            Some(query) => format!("{}?{}", url_parsed.path(), query),
341            None => url_parsed.path().to_string(),
342        };
343
344        let host = url_parsed.host_str().unwrap_or("");
345
346        // Based on the official Veracode JavaScript implementation:
347        // var data = `id=${id}&host=${host}&url=${url}&method=${method}`;
348        let data = format!(
349            "id={}&host={}&url={}&method={}",
350            self.config.credentials.expose_api_id(),
351            host,
352            path_and_query,
353            method
354        );
355
356        let timestamp_str = timestamp.to_string();
357        let ver_str = "vcode_request_version_1";
358
359        // Convert hex strings to bytes
360        let key_bytes = hex::decode(self.config.credentials.expose_api_key())
361            .map_err(|_| VeracodeError::Authentication(INVALID_API_KEY_MSG.to_string()))?;
362
363        let nonce_bytes = hex::decode(nonce)
364            .map_err(|_| VeracodeError::Authentication(INVALID_NONCE_MSG.to_string()))?;
365
366        // Step 1: HMAC(nonce, key)
367        let mut mac1 = HmacSha256::new_from_slice(&key_bytes)
368            .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
369        mac1.update(&nonce_bytes);
370        let hashed_nonce = mac1.finalize().into_bytes();
371
372        // Step 2: HMAC(timestamp, hashed_nonce)
373        let mut mac2 = HmacSha256::new_from_slice(&hashed_nonce)
374            .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
375        mac2.update(timestamp_str.as_bytes());
376        let hashed_timestamp = mac2.finalize().into_bytes();
377
378        // Step 3: HMAC(ver_str, hashed_timestamp)
379        let mut mac3 = HmacSha256::new_from_slice(&hashed_timestamp)
380            .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
381        mac3.update(ver_str.as_bytes());
382        let hashed_ver_str = mac3.finalize().into_bytes();
383
384        // Step 4: HMAC(data, hashed_ver_str)
385        let mut mac4 = HmacSha256::new_from_slice(&hashed_ver_str)
386            .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
387        mac4.update(data.as_bytes());
388        let signature = mac4.finalize().into_bytes();
389
390        // Return the hex-encoded signature (lowercase)
391        Ok(hex::encode(signature).to_lowercase())
392    }
393
394    /// Generate authorization header for HMAC authentication
395    ///
396    /// # Errors
397    ///
398    /// Returns an error if the API request fails, the resource is not found,
399    /// or authentication/authorization fails.
400    pub fn generate_auth_header(&self, method: &str, url: &str) -> Result<String, VeracodeError> {
401        #[allow(clippy::cast_possible_truncation)]
402        let timestamp = SystemTime::now()
403            .duration_since(UNIX_EPOCH)
404            .map_err(|e| VeracodeError::Authentication(format!("System time error: {e}")))?
405            .as_millis() as u64; // Use milliseconds like JavaScript
406
407        // Generate a 16-byte random nonce and convert to hex string
408        let nonce_bytes: [u8; 16] = rand::random();
409        let nonce = hex::encode(nonce_bytes);
410
411        let signature = self.generate_hmac_signature(method, url, timestamp, &nonce)?;
412
413        Ok(format!(
414            "VERACODE-HMAC-SHA-256 id={},ts={},nonce={},sig={}",
415            self.config.credentials.expose_api_id(),
416            timestamp,
417            nonce,
418            signature
419        ))
420    }
421
422    /// Make a GET request to the specified endpoint.
423    ///
424    /// # Arguments
425    ///
426    /// * `endpoint` - The API endpoint path (e.g., "/appsec/v1/applications")
427    /// * `query_params` - Optional query parameters as key-value pairs
428    ///
429    /// # Returns
430    ///
431    /// A `Result` containing the HTTP response.
432    ///
433    /// # Errors
434    ///
435    /// Returns an error if the API request fails, the resource is not found,
436    /// or authentication/authorization fails.
437    pub async fn get(
438        &self,
439        endpoint: &str,
440        query_params: Option<&[(String, String)]>,
441    ) -> Result<reqwest::Response, VeracodeError> {
442        // Pre-allocate URL capacity
443        let param_count = query_params.map_or(0, |p| p.len());
444        let estimated_capacity = self
445            .config
446            .base_url
447            .len()
448            .saturating_add(endpoint.len())
449            .saturating_add(param_count.saturating_mul(32));
450        let mut url = String::with_capacity(estimated_capacity);
451        url.push_str(&self.config.base_url);
452        url.push_str(endpoint);
453
454        if let Some(params) = query_params
455            && !params.is_empty()
456        {
457            url.push('?');
458            for (i, (key, value)) in params.iter().enumerate() {
459                if i > 0 {
460                    url.push('&');
461                }
462                url.push_str(key);
463                url.push('=');
464                url.push_str(value);
465            }
466        }
467
468        // Create request builder closure for retry logic
469        let request_builder = || {
470            // Re-generate auth header for each attempt to avoid signature expiry
471            let Ok(auth_header) = self.generate_auth_header("GET", &url) else {
472                return self.client.get("invalid://url");
473            };
474
475            self.client
476                .get(&url)
477                .header("Authorization", auth_header)
478                .header("Content-Type", "application/json")
479        };
480
481        // Use Cow::Borrowed for simple operations when possible
482        let operation_name = if endpoint.len() < 50 {
483            Cow::Owned(format!("GET {endpoint}"))
484        } else {
485            Cow::Borrowed("GET [long endpoint]")
486        };
487        self.execute_with_retry(request_builder, operation_name)
488            .await
489    }
490
491    /// Make a POST request to the specified endpoint.
492    ///
493    /// # Arguments
494    ///
495    /// * `endpoint` - The API endpoint path (e.g., "/appsec/v1/applications")
496    /// * `body` - Optional request body that implements Serialize
497    ///
498    /// # Returns
499    ///
500    /// A `Result` containing the HTTP response.
501    ///
502    /// # Errors
503    ///
504    /// Returns an error if the API request fails, the resource is not found,
505    /// or authentication/authorization fails.
506    pub async fn post<T: Serialize>(
507        &self,
508        endpoint: &str,
509        body: Option<&T>,
510    ) -> Result<reqwest::Response, VeracodeError> {
511        let mut url =
512            String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
513        url.push_str(&self.config.base_url);
514        url.push_str(endpoint);
515
516        // Serialize body once outside the retry loop for efficiency
517        let serialized_body = if let Some(body) = body {
518            Some(serde_json::to_string(body)?)
519        } else {
520            None
521        };
522
523        // Create request builder closure for retry logic
524        let request_builder = || {
525            // Re-generate auth header for each attempt to avoid signature expiry
526            let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
527                return self.client.post("invalid://url");
528            };
529
530            let mut request = self
531                .client
532                .post(&url)
533                .header("Authorization", auth_header)
534                .header("Content-Type", "application/json");
535
536            if let Some(ref body_str) = serialized_body {
537                request = request.body(body_str.clone());
538            }
539
540            request
541        };
542
543        let operation_name = if endpoint.len() < 50 {
544            Cow::Owned(format!("POST {endpoint}"))
545        } else {
546            Cow::Borrowed("POST [long endpoint]")
547        };
548        self.execute_with_retry(request_builder, operation_name)
549            .await
550    }
551
552    /// Make a PUT request to the specified endpoint.
553    ///
554    /// # Arguments
555    ///
556    /// * `endpoint` - The API endpoint path (e.g., "/appsec/v1/applications/guid")
557    /// * `body` - Optional request body that implements Serialize
558    ///
559    /// # Returns
560    ///
561    /// A `Result` containing the HTTP response.
562    ///
563    /// # Errors
564    ///
565    /// Returns an error if the API request fails, the resource is not found,
566    /// or authentication/authorization fails.
567    pub async fn put<T: Serialize>(
568        &self,
569        endpoint: &str,
570        body: Option<&T>,
571    ) -> Result<reqwest::Response, VeracodeError> {
572        let mut url =
573            String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
574        url.push_str(&self.config.base_url);
575        url.push_str(endpoint);
576
577        // Serialize body once outside the retry loop for efficiency
578        let serialized_body = if let Some(body) = body {
579            Some(serde_json::to_string(body)?)
580        } else {
581            None
582        };
583
584        // Create request builder closure for retry logic
585        let request_builder = || {
586            // Re-generate auth header for each attempt to avoid signature expiry
587            let Ok(auth_header) = self.generate_auth_header("PUT", &url) else {
588                return self.client.put("invalid://url");
589            };
590
591            let mut request = self
592                .client
593                .put(&url)
594                .header("Authorization", auth_header)
595                .header("Content-Type", "application/json");
596
597            if let Some(ref body_str) = serialized_body {
598                request = request.body(body_str.clone());
599            }
600
601            request
602        };
603
604        let operation_name = if endpoint.len() < 50 {
605            Cow::Owned(format!("PUT {endpoint}"))
606        } else {
607            Cow::Borrowed("PUT [long endpoint]")
608        };
609        self.execute_with_retry(request_builder, operation_name)
610            .await
611    }
612
613    /// Make a DELETE request to the specified endpoint.
614    ///
615    /// # Arguments
616    ///
617    /// * `endpoint` - The API endpoint path (e.g., "/appsec/v1/applications/guid")
618    ///
619    /// # Returns
620    ///
621    /// A `Result` containing the HTTP response.
622    ///
623    /// # Errors
624    ///
625    /// Returns an error if the API request fails, the resource is not found,
626    /// or authentication/authorization fails.
627    pub async fn delete(&self, endpoint: &str) -> Result<reqwest::Response, VeracodeError> {
628        let mut url =
629            String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
630        url.push_str(&self.config.base_url);
631        url.push_str(endpoint);
632
633        // Create request builder closure for retry logic
634        let request_builder = || {
635            // Re-generate auth header for each attempt to avoid signature expiry
636            let Ok(auth_header) = self.generate_auth_header("DELETE", &url) else {
637                return self.client.delete("invalid://url");
638            };
639
640            self.client
641                .delete(&url)
642                .header("Authorization", auth_header)
643                .header("Content-Type", "application/json")
644        };
645
646        let operation_name = if endpoint.len() < 50 {
647            Cow::Owned(format!("DELETE {endpoint}"))
648        } else {
649            Cow::Borrowed("DELETE [long endpoint]")
650        };
651        self.execute_with_retry(request_builder, operation_name)
652            .await
653    }
654
655    /// Helper method to handle common response processing.
656    ///
657    /// Checks if the response is successful and returns an error if not.
658    ///
659    /// # Arguments
660    ///
661    /// * `response` - The HTTP response to check
662    /// * `context` - A description of the operation being performed (e.g., "get application")
663    ///
664    /// # Returns
665    ///
666    /// A `Result` containing the response if successful, or an error if not.
667    ///
668    /// # Error Context
669    ///
670    /// This method enhances error messages with context about the failed operation
671    /// to improve debugging and user experience.
672    ///
673    /// # Errors
674    ///
675    /// Returns an error if the API request fails, the resource is not found,
676    /// or authentication/authorization fails.
677    pub async fn handle_response(
678        response: reqwest::Response,
679        context: &str,
680    ) -> Result<reqwest::Response, VeracodeError> {
681        if !response.status().is_success() {
682            let status = response.status();
683            let url = response.url().clone();
684            let error_text = response.text().await?;
685            return Err(VeracodeError::InvalidResponse(format!(
686                "Failed to {context}\n  URL: {url}\n  HTTP {status}: {error_text}"
687            )));
688        }
689        Ok(response)
690    }
691
692    /// Make a GET request with full URL construction and query parameter handling.
693    ///
694    /// This is a higher-level method that builds the full URL and handles query parameters.
695    ///
696    /// # Arguments
697    ///
698    /// * `endpoint` - The API endpoint path
699    /// * `query_params` - Optional query parameters
700    ///
701    /// # Returns
702    ///
703    /// A `Result` containing the HTTP response, pre-processed for success/failure.
704    ///
705    /// # Errors
706    ///
707    /// Returns an error if the API request fails, the resource is not found,
708    /// or authentication/authorization fails.
709    pub async fn get_with_query(
710        &self,
711        endpoint: &str,
712        query_params: Option<Vec<(String, String)>>,
713    ) -> Result<reqwest::Response, VeracodeError> {
714        let query_slice = query_params.as_deref();
715        let response = self.get(endpoint, query_slice).await?;
716        Self::handle_response(response, &format!("GET {endpoint}")).await
717    }
718
719    /// Make a POST request with automatic response handling.
720    ///
721    /// # Arguments
722    ///
723    /// * `endpoint` - The API endpoint path
724    /// * `body` - Optional request body
725    ///
726    /// # Returns
727    ///
728    /// A `Result` containing the HTTP response, pre-processed for success/failure.
729    ///
730    /// # Errors
731    ///
732    /// Returns an error if the API request fails, the resource is not found,
733    /// or authentication/authorization fails.
734    pub async fn post_with_response<T: Serialize>(
735        &self,
736        endpoint: &str,
737        body: Option<&T>,
738    ) -> Result<reqwest::Response, VeracodeError> {
739        let response = self.post(endpoint, body).await?;
740        Self::handle_response(response, &format!("POST {endpoint}")).await
741    }
742
743    /// Make a PUT request with automatic response handling.
744    ///
745    /// # Arguments
746    ///
747    /// * `endpoint` - The API endpoint path
748    /// * `body` - Optional request body
749    ///
750    /// # Returns
751    ///
752    /// A `Result` containing the HTTP response, pre-processed for success/failure.
753    ///
754    /// # Errors
755    ///
756    /// Returns an error if the API request fails, the resource is not found,
757    /// or authentication/authorization fails.
758    pub async fn put_with_response<T: Serialize>(
759        &self,
760        endpoint: &str,
761        body: Option<&T>,
762    ) -> Result<reqwest::Response, VeracodeError> {
763        let response = self.put(endpoint, body).await?;
764        Self::handle_response(response, &format!("PUT {endpoint}")).await
765    }
766
767    /// Make a DELETE request with automatic response handling.
768    ///
769    /// # Arguments
770    ///
771    /// * `endpoint` - The API endpoint path
772    ///
773    /// # Returns
774    ///
775    /// A `Result` containing the HTTP response, pre-processed for success/failure.
776    ///
777    /// # Errors
778    ///
779    /// Returns an error if the API request fails, the resource is not found,
780    /// or authentication/authorization fails.
781    pub async fn delete_with_response(
782        &self,
783        endpoint: &str,
784    ) -> Result<reqwest::Response, VeracodeError> {
785        let response = self.delete(endpoint).await?;
786        Self::handle_response(response, &format!("DELETE {endpoint}")).await
787    }
788
789    /// Make paginated GET requests to collect all results.
790    ///
791    /// This method automatically handles pagination by making multiple requests
792    /// and combining all results into a single response.
793    ///
794    /// # Arguments
795    ///
796    /// * `endpoint` - The API endpoint path
797    /// * `base_query_params` - Base query parameters (non-pagination)
798    /// * `page_size` - Number of items per page (default: 500)
799    ///
800    /// # Returns
801    ///
802    /// A `Result` containing all paginated results as a single response body string.
803    ///
804    /// # Errors
805    ///
806    /// Returns an error if the API request fails, the resource is not found,
807    /// or authentication/authorization fails.
808    pub async fn get_paginated(
809        &self,
810        endpoint: &str,
811        base_query_params: Option<Vec<(String, String)>>,
812        page_size: Option<u32>,
813    ) -> Result<String, VeracodeError> {
814        let size = page_size.unwrap_or(500);
815        let mut page: u32 = 0;
816        let mut all_items = Vec::new();
817        let mut page_info = None;
818
819        loop {
820            let mut query_params = base_query_params.clone().unwrap_or_default();
821            query_params.push(("page".to_string(), page.to_string()));
822            query_params.push(("size".to_string(), size.to_string()));
823
824            let response = self.get_with_query(endpoint, Some(query_params)).await?;
825            let response_text = response.text().await?;
826
827            // Validate JSON depth before parsing to prevent DoS attacks
828            validate_json_depth(&response_text, MAX_JSON_DEPTH).map_err(|e| {
829                VeracodeError::InvalidResponse(format!("JSON validation failed: {}", e))
830            })?;
831
832            // Try to parse as JSON to extract items and pagination info
833            if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
834                // Handle embedded response format
835                if let Some(embedded) = json_value.get("_embedded") {
836                    if let Some(items_array) =
837                        embedded.as_object().and_then(|obj| obj.values().next())
838                        && let Some(items) = items_array.as_array()
839                    {
840                        if items.is_empty() {
841                            break; // No more items
842                        }
843                        all_items.extend(items.clone());
844                    }
845                } else if let Some(items) = json_value.as_array() {
846                    // Handle direct array response
847                    if items.is_empty() {
848                        break;
849                    }
850                    all_items.extend(items.clone());
851                } else {
852                    // Single page response, return as-is
853                    return Ok(response_text);
854                }
855
856                // Check pagination info
857                if let Some(page_obj) = json_value.get("page") {
858                    page_info = Some(page_obj.clone());
859                    if let (Some(current), Some(total)) = (
860                        page_obj.get("number").and_then(|n| n.as_u64()),
861                        page_obj.get("totalPages").and_then(|n| n.as_u64()),
862                    ) && current.saturating_add(1) >= total
863                    {
864                        break; // Last page reached
865                    }
866                }
867            } else {
868                // Not JSON or parsing failed, return single response
869                return Ok(response_text);
870            }
871
872            page = page.saturating_add(1);
873
874            // Safety check to prevent infinite loops
875            if page > 100 {
876                break;
877            }
878        }
879
880        // Reconstruct response with all items
881        let combined_response = if let Some(page_info) = page_info {
882            // Use embedded format
883            serde_json::json!({
884                "_embedded": {
885                    "roles": all_items // This key might need to be dynamic
886                },
887                "page": page_info
888            })
889        } else {
890            // Use direct array format
891            serde_json::Value::Array(all_items)
892        };
893
894        Ok(combined_response.to_string())
895    }
896
897    /// Make a GET request with query parameters
898    ///
899    /// # Arguments
900    ///
901    /// * `endpoint` - The API endpoint to call
902    /// * `params` - Query parameters as a slice of tuples
903    ///
904    /// # Returns
905    ///
906    /// A `Result` containing the response or an error.
907    ///
908    /// # Errors
909    ///
910    /// Returns an error if the API request fails, the resource is not found,
911    /// or authentication/authorization fails.
912    pub async fn get_with_params(
913        &self,
914        endpoint: &str,
915        params: &[(&str, &str)],
916    ) -> Result<reqwest::Response, VeracodeError> {
917        let mut url =
918            String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
919        url.push_str(&self.config.base_url);
920        url.push_str(endpoint);
921        let mut request_url =
922            Url::parse(&url).map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
923
924        // Add query parameters
925        if !params.is_empty() {
926            let mut query_pairs = request_url.query_pairs_mut();
927            for (key, value) in params {
928                query_pairs.append_pair(key, value);
929            }
930        }
931
932        let auth_header = self.generate_auth_header("GET", request_url.as_str())?;
933
934        let response = self
935            .client
936            .get(request_url)
937            .header("Authorization", auth_header)
938            .header("User-Agent", "Veracode Rust Client")
939            .send()
940            .await?;
941
942        Ok(response)
943    }
944
945    /// Make a POST request with form data
946    ///
947    /// # Arguments
948    ///
949    /// * `endpoint` - The API endpoint to call
950    /// * `params` - Form parameters as a slice of tuples
951    ///
952    /// # Returns
953    ///
954    /// A `Result` containing the response or an error.
955    ///
956    /// # Errors
957    ///
958    /// Returns an error if the API request fails, the resource is not found,
959    /// or authentication/authorization fails.
960    pub async fn post_form(
961        &self,
962        endpoint: &str,
963        params: &[(&str, &str)],
964    ) -> Result<reqwest::Response, VeracodeError> {
965        let mut url =
966            String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
967        url.push_str(&self.config.base_url);
968        url.push_str(endpoint);
969
970        // Build form data - avoid unnecessary allocations
971        let form_data: Vec<(&str, &str)> = params.to_vec();
972
973        let auth_header = self.generate_auth_header("POST", &url)?;
974
975        let response = self
976            .client
977            .post(&url)
978            .header("Authorization", auth_header)
979            .header("User-Agent", "Veracode Rust Client")
980            .form(&form_data)
981            .send()
982            .await?;
983
984        Ok(response)
985    }
986
987    /// Upload a file using multipart form data
988    ///
989    /// # Arguments
990    ///
991    /// * `endpoint` - The API endpoint to call
992    /// * `params` - Additional form parameters
993    /// * `file_field_name` - Name of the file field
994    /// * `filename` - Name of the file
995    /// * `file_data` - File data as bytes
996    ///
997    /// # Returns
998    ///
999    /// A `Result` containing the response or an error.
1000    ///
1001    /// # Errors
1002    ///
1003    /// Returns an error if the API request fails, the resource is not found,
1004    /// or authentication/authorization fails.
1005    pub async fn upload_file_multipart(
1006        &self,
1007        endpoint: &str,
1008        params: HashMap<&str, &str>,
1009        file_field_name: &str,
1010        filename: &str,
1011        file_data: Vec<u8>,
1012    ) -> Result<reqwest::Response, VeracodeError> {
1013        let mut url =
1014            String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
1015        url.push_str(&self.config.base_url);
1016        url.push_str(endpoint);
1017
1018        // Build multipart form
1019        let mut form = multipart::Form::new();
1020
1021        // Add regular form fields
1022        for (key, value) in params {
1023            form = form.text(key.to_string(), value.to_string());
1024        }
1025
1026        // Add file
1027        let part = multipart::Part::bytes(file_data)
1028            .file_name(filename.to_string())
1029            .mime_str("application/octet-stream")
1030            .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
1031
1032        form = form.part(file_field_name.to_string(), part);
1033
1034        let auth_header = self.generate_auth_header("POST", &url)?;
1035
1036        let response = self
1037            .client
1038            .post(&url)
1039            .header("Authorization", auth_header)
1040            .header("User-Agent", "Veracode Rust Client")
1041            .multipart(form)
1042            .send()
1043            .await?;
1044
1045        Ok(response)
1046    }
1047
1048    /// Upload a file using multipart form data with PUT method (for pipeline scans)
1049    ///
1050    /// # Arguments
1051    ///
1052    /// * `url` - The full URL to upload to
1053    /// * `file_field_name` - Name of the file field
1054    /// * `filename` - Name of the file
1055    /// * `file_data` - File data as bytes
1056    /// * `additional_headers` - Additional headers to include
1057    ///
1058    /// # Returns
1059    ///
1060    /// A `Result` containing the response or an error.
1061    ///
1062    /// # Errors
1063    ///
1064    /// Returns an error if the API request fails, the resource is not found,
1065    /// or authentication/authorization fails.
1066    pub async fn upload_file_multipart_put(
1067        &self,
1068        url: &str,
1069        file_field_name: &str,
1070        filename: &str,
1071        file_data: Vec<u8>,
1072        additional_headers: Option<HashMap<&str, &str>>,
1073    ) -> Result<reqwest::Response, VeracodeError> {
1074        // Build multipart form
1075        let part = multipart::Part::bytes(file_data)
1076            .file_name(filename.to_string())
1077            .mime_str("application/octet-stream")
1078            .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
1079
1080        let form = multipart::Form::new().part(file_field_name.to_string(), part);
1081
1082        let auth_header = self.generate_auth_header("PUT", url)?;
1083
1084        let mut request = self
1085            .client
1086            .put(url)
1087            .header("Authorization", auth_header)
1088            .header("User-Agent", "Veracode Rust Client")
1089            .multipart(form);
1090
1091        // Add any additional headers
1092        if let Some(headers) = additional_headers {
1093            for (key, value) in headers {
1094                request = request.header(key, value);
1095            }
1096        }
1097
1098        let response = request.send().await?;
1099        Ok(response)
1100    }
1101
1102    /// Upload a file with query parameters (like Java implementation)
1103    ///
1104    /// This method mimics the Java API wrapper's approach where parameters
1105    /// are added to the query string and the file is uploaded separately.
1106    ///
1107    /// Memory optimization: Uses Cow for strings and Arc for file data to minimize cloning
1108    /// during retry attempts. Automatically retries on transient failures.
1109    ///
1110    /// # Arguments
1111    ///
1112    /// * `endpoint` - The API endpoint to call
1113    /// * `query_params` - Query parameters as key-value pairs
1114    /// * `file_field_name` - Name of the file field
1115    /// * `filename` - Name of the file
1116    /// * `file_data` - File data as bytes
1117    ///
1118    /// # Returns
1119    ///
1120    /// A `Result` containing the response or an error.
1121    ///
1122    /// # Errors
1123    ///
1124    /// Returns an error if the API request fails, the resource is not found,
1125    /// or authentication/authorization fails.
1126    pub async fn upload_file_with_query_params(
1127        &self,
1128        endpoint: &str,
1129        query_params: &[(&str, &str)],
1130        file_field_name: &str,
1131        filename: &str,
1132        file_data: Vec<u8>,
1133    ) -> Result<reqwest::Response, VeracodeError> {
1134        // Build URL with query parameters using centralized helper for consistency
1135        let url = self.build_url_with_params(endpoint, query_params);
1136
1137        // Wrap file data in Arc to avoid cloning during retries
1138        let file_data_arc = Arc::new(file_data);
1139
1140        // Use Cow for strings to minimize allocations - borrow for short strings, own for long ones
1141        let filename_cow: Cow<str> = if filename.len() < 128 {
1142            Cow::Borrowed(filename)
1143        } else {
1144            Cow::Owned(filename.to_string())
1145        };
1146
1147        let field_name_cow: Cow<str> = if file_field_name.len() < 32 {
1148            Cow::Borrowed(file_field_name)
1149        } else {
1150            Cow::Owned(file_field_name.to_string())
1151        };
1152
1153        // Create request builder closure for retry logic
1154        let request_builder = || {
1155            // Clone Arc (cheap - just increments reference count)
1156            let file_data_clone = Arc::clone(&file_data_arc);
1157
1158            // Re-create multipart form for each attempt
1159            let Ok(part) = multipart::Part::bytes((*file_data_clone).clone())
1160                .file_name(filename_cow.to_string())
1161                .mime_str("application/octet-stream")
1162            else {
1163                return self.client.post("invalid://url");
1164            };
1165
1166            let form = multipart::Form::new().part(field_name_cow.to_string(), part);
1167
1168            // Re-generate auth header for each attempt to avoid signature expiry
1169            let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1170                return self.client.post("invalid://url");
1171            };
1172
1173            self.client
1174                .post(&url)
1175                .header("Authorization", auth_header)
1176                .header("User-Agent", "Veracode Rust Client")
1177                .multipart(form)
1178        };
1179
1180        // Use Cow for operation name based on endpoint length to minimize allocations
1181        let operation_name: Cow<str> = if endpoint.len() < 50 {
1182            Cow::Owned(format!("File Upload POST {endpoint}"))
1183        } else {
1184            Cow::Borrowed("File Upload POST [long endpoint]")
1185        };
1186
1187        self.execute_with_retry(request_builder, operation_name)
1188            .await
1189    }
1190
1191    /// Make a POST request with query parameters (like Java implementation for XML API)
1192    ///
1193    /// This method mimics the Java API wrapper's approach for POST operations
1194    /// where parameters are added to the query string rather than form data.
1195    ///
1196    /// # Arguments
1197    ///
1198    /// * `endpoint` - The API endpoint to call
1199    /// * `query_params` - Query parameters as key-value pairs
1200    ///
1201    /// # Returns
1202    ///
1203    /// A `Result` containing the response or an error.
1204    ///
1205    /// # Errors
1206    ///
1207    /// Returns an error if the API request fails, the resource is not found,
1208    /// or authentication/authorization fails.
1209    pub async fn post_with_query_params(
1210        &self,
1211        endpoint: &str,
1212        query_params: &[(&str, &str)],
1213    ) -> Result<reqwest::Response, VeracodeError> {
1214        // Build URL with query parameters using centralized helper
1215        let url = self.build_url_with_params(endpoint, query_params);
1216
1217        let auth_header = self.generate_auth_header("POST", &url)?;
1218
1219        let response = self
1220            .client
1221            .post(&url)
1222            .header("Authorization", auth_header)
1223            .header("User-Agent", "Veracode Rust Client")
1224            .send()
1225            .await?;
1226
1227        Ok(response)
1228    }
1229
1230    /// Make a GET request with query parameters (like Java implementation for XML API)
1231    ///
1232    /// This method mimics the Java API wrapper's approach for GET operations
1233    /// where parameters are added to the query string.
1234    ///
1235    /// # Arguments
1236    ///
1237    /// * `endpoint` - The API endpoint to call
1238    /// * `query_params` - Query parameters as key-value pairs
1239    ///
1240    /// # Returns
1241    ///
1242    /// A `Result` containing the response or an error.
1243    ///
1244    /// # Errors
1245    ///
1246    /// Returns an error if the API request fails, the resource is not found,
1247    /// or authentication/authorization fails.
1248    pub async fn get_with_query_params(
1249        &self,
1250        endpoint: &str,
1251        query_params: &[(&str, &str)],
1252    ) -> Result<reqwest::Response, VeracodeError> {
1253        // Build URL with query parameters using centralized helper
1254        let url = self.build_url_with_params(endpoint, query_params);
1255
1256        let auth_header = self.generate_auth_header("GET", &url)?;
1257
1258        let response = self
1259            .client
1260            .get(&url)
1261            .header("Authorization", auth_header)
1262            .header("User-Agent", "Veracode Rust Client")
1263            .send()
1264            .await?;
1265
1266        Ok(response)
1267    }
1268
1269    /// Upload a large file using chunked streaming (for uploadlargefile.do)
1270    ///
1271    /// This method implements chunked upload functionality similar to the Java API wrapper.
1272    /// It uploads files in chunks and provides progress tracking capabilities.
1273    ///
1274    /// # Arguments
1275    ///
1276    /// * `endpoint` - The API endpoint to call  
1277    /// * `query_params` - Query parameters as key-value pairs
1278    /// * `file_path` - Path to the file to upload
1279    /// * `content_type` - Content type for the file (default: binary/octet-stream)
1280    /// * `progress_callback` - Optional callback for progress tracking
1281    ///
1282    /// # Returns
1283    ///
1284    /// A `Result` containing the response or an error.
1285    ///
1286    /// # Errors
1287    ///
1288    /// Returns an error if the API request fails, the resource is not found,
1289    /// or authentication/authorization fails.
1290    pub async fn upload_large_file_chunked<F>(
1291        &self,
1292        endpoint: &str,
1293        query_params: &[(&str, &str)],
1294        file_path: &str,
1295        content_type: Option<&str>,
1296        progress_callback: Option<F>,
1297    ) -> Result<reqwest::Response, VeracodeError>
1298    where
1299        F: Fn(u64, u64, f64) + Send + Sync,
1300    {
1301        // Build URL with query parameters using centralized helper
1302        let url = self.build_url_with_params(endpoint, query_params);
1303
1304        // Open file and get size
1305        let mut file = File::open(file_path)
1306            .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to open file: {e}")))?;
1307
1308        let file_size = file
1309            .metadata()
1310            .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to get file size: {e}")))?
1311            .len();
1312
1313        // Check file size limit (2GB for uploadlargefile.do)
1314        #[allow(clippy::arithmetic_side_effects)]
1315        const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; // 2GB
1316        if file_size > MAX_FILE_SIZE {
1317            return Err(VeracodeError::InvalidConfig(format!(
1318                "File size ({file_size} bytes) exceeds maximum limit of {MAX_FILE_SIZE} bytes"
1319            )));
1320        }
1321
1322        // Read entire file for now (can be optimized to streaming later)
1323        file.seek(SeekFrom::Start(0))
1324            .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to seek file: {e}")))?;
1325
1326        #[allow(clippy::cast_possible_truncation)]
1327        let mut file_data = Vec::with_capacity(file_size as usize);
1328        file.read_to_end(&mut file_data)
1329            .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to read file: {e}")))?;
1330
1331        // Memory optimization: Wrap file data in Arc to avoid cloning during retries
1332        let file_data_arc = Arc::new(file_data);
1333        let content_type_cow: Cow<str> =
1334            content_type.map_or(Cow::Borrowed("binary/octet-stream"), |ct| {
1335                if ct.len() < 64 {
1336                    Cow::Borrowed(ct)
1337                } else {
1338                    Cow::Owned(ct.to_string())
1339                }
1340            });
1341
1342        // Create request builder closure for retry logic
1343        let request_builder = || {
1344            // Clone Arc (cheap - just increments reference count)
1345            let file_data_clone = Arc::clone(&file_data_arc);
1346
1347            // Re-generate auth header for each attempt to avoid signature expiry
1348            let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1349                return self.client.post("invalid://url");
1350            };
1351
1352            self.client
1353                .post(&url)
1354                .header("Authorization", auth_header)
1355                .header("User-Agent", "Veracode Rust Client")
1356                .header("Content-Type", content_type_cow.as_ref())
1357                .header("Content-Length", file_size.to_string())
1358                .body((*file_data_clone).clone())
1359        };
1360
1361        // Track progress if callback provided (do this before retry loop)
1362        if let Some(callback) = progress_callback {
1363            callback(file_size, file_size, 100.0);
1364        }
1365
1366        // Use optimized operation name
1367        let operation_name: Cow<str> = if endpoint.len() < 50 {
1368            Cow::Owned(format!("Large File Upload POST {endpoint}"))
1369        } else {
1370            Cow::Borrowed("Large File Upload POST [long endpoint]")
1371        };
1372
1373        self.execute_with_retry(request_builder, operation_name)
1374            .await
1375    }
1376
1377    /// Upload a file with binary data (optimized for uploadlargefile.do)
1378    ///
1379    /// This method uploads a file as raw binary data without multipart encoding,
1380    /// which is the expected format for the uploadlargefile.do endpoint.
1381    ///
1382    /// Memory optimization: Uses Arc for file data and Cow for strings to minimize
1383    /// allocations during retry attempts. Automatically retries on transient failures.
1384    ///
1385    /// # Arguments
1386    ///
1387    /// * `endpoint` - The API endpoint to call
1388    /// * `query_params` - Query parameters as key-value pairs  
1389    /// * `file_data` - File data as bytes
1390    /// * `content_type` - Content type for the file
1391    ///
1392    /// # Returns
1393    ///
1394    /// A `Result` containing the response or an error.
1395    ///
1396    /// # Errors
1397    ///
1398    /// Returns an error if the API request fails, the resource is not found,
1399    /// or authentication/authorization fails.
1400    pub async fn upload_file_binary(
1401        &self,
1402        endpoint: &str,
1403        query_params: &[(&str, &str)],
1404        file_data: Vec<u8>,
1405        content_type: &str,
1406    ) -> Result<reqwest::Response, VeracodeError> {
1407        // Build URL with query parameters using centralized helper
1408        let url = self.build_url_with_params(endpoint, query_params);
1409
1410        // Memory optimization: Wrap file data in Arc to avoid cloning during retries
1411        let file_data_arc = Arc::new(file_data);
1412        let file_size = file_data_arc.len();
1413
1414        // Use Cow for content type to minimize allocations
1415        let content_type_cow: Cow<str> = if content_type.len() < 64 {
1416            Cow::Borrowed(content_type)
1417        } else {
1418            Cow::Owned(content_type.to_string())
1419        };
1420
1421        // Create request builder closure for retry logic
1422        let request_builder = || {
1423            // Clone Arc (cheap - just increments reference count)
1424            let file_data_clone = Arc::clone(&file_data_arc);
1425
1426            // Re-generate auth header for each attempt to avoid signature expiry
1427            let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1428                return self.client.post("invalid://url");
1429            };
1430
1431            self.client
1432                .post(&url)
1433                .header("Authorization", auth_header)
1434                .header("User-Agent", "Veracode Rust Client")
1435                .header("Content-Type", content_type_cow.as_ref())
1436                .header("Content-Length", file_size.to_string())
1437                .body((*file_data_clone).clone())
1438        };
1439
1440        // Use optimized operation name based on endpoint length
1441        let operation_name: Cow<str> = if endpoint.len() < 50 {
1442            Cow::Owned(format!("Binary File Upload POST {endpoint}"))
1443        } else {
1444            Cow::Borrowed("Binary File Upload POST [long endpoint]")
1445        };
1446
1447        self.execute_with_retry(request_builder, operation_name)
1448            .await
1449    }
1450}
1451
1452#[cfg(test)]
1453#[allow(clippy::expect_used)] // Test code: expect is acceptable for test setup
1454mod tests {
1455    use super::*;
1456    use proptest::prelude::*;
1457
1458    // ============================================================================
1459    // TIER 1: PROPERTY-BASED SECURITY TESTS (Fast, High ROI)
1460    // ============================================================================
1461
1462    /// Helper to create a test config with dummy credentials
1463    fn create_test_config() -> VeracodeConfig {
1464        use crate::{VeracodeCredentials, VeracodeRegion};
1465
1466        VeracodeConfig {
1467            credentials: VeracodeCredentials::new(
1468                "test_api_id".to_string(),
1469                "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
1470            ),
1471            base_url: "https://api.veracode.com".to_string(),
1472            rest_base_url: "https://api.veracode.com".to_string(),
1473            xml_base_url: "https://analysiscenter.veracode.com".to_string(),
1474            region: VeracodeRegion::Commercial,
1475            validate_certificates: true,
1476            connect_timeout: 30,
1477            request_timeout: 300,
1478            proxy_url: None,
1479            proxy_username: None,
1480            proxy_password: None,
1481            retry_config: Default::default(),
1482        }
1483    }
1484
1485    // ============================================================================
1486    // SECURITY TEST: URL Construction & Parameter Encoding
1487    // ============================================================================
1488
1489    proptest! {
1490        #![proptest_config(ProptestConfig {
1491            cases: if cfg!(miri) { 5 } else { 1000 },
1492            failure_persistence: None,
1493            .. ProptestConfig::default()
1494        })]
1495
1496        /// Property: URL parameter encoding must prevent injection attacks
1497        /// Tests that special characters are properly encoded and cannot break URL structure
1498        #[test]
1499        fn proptest_url_params_prevent_injection(
1500            key in "[a-zA-Z0-9_]{1,50}",
1501            value in ".*{0,100}",
1502        ) {
1503            let config = create_test_config();
1504            let client = VeracodeClient::new(config)
1505                .expect("valid test client configuration");
1506
1507            let params = vec![(key.as_str(), value.as_str())];
1508            let url = client.build_url_with_params("/api/test", &params);
1509
1510            // Property 1: URL must not contain unencoded dangerous characters
1511            prop_assert!(!url.contains("<script>"));
1512            prop_assert!(!url.contains("javascript:"));
1513
1514            // Property 2: URL must contain properly encoded parameters
1515            prop_assert!(url.starts_with("https://api.veracode.com/api/test"));
1516
1517            // Property 3: If params are present, URL must contain '?'
1518            if !params.is_empty() && !key.is_empty() {
1519                prop_assert!(url.contains('?'));
1520            }
1521        }
1522
1523        /// Property: URL construction must handle capacity overflow safely
1524        /// Tests that large numbers of parameters don't cause panics or overflows
1525        #[test]
1526        fn proptest_url_params_capacity_safe(
1527            param_count in 0usize..=100,
1528        ) {
1529            let config = create_test_config();
1530            let client = VeracodeClient::new(config)
1531                .expect("valid test client configuration");
1532
1533            // Create param_count parameters
1534            let params: Vec<(&str, &str)> = (0..param_count)
1535                .map(|_| ("key", "value"))
1536                .collect();
1537
1538            // Must not panic on capacity calculations
1539            let url = client.build_url_with_params("/api/test", &params);
1540
1541            // Property: URL should be valid and not panic
1542            prop_assert!(url.starts_with("https://"));
1543            prop_assert!(url.len() < 100000); // Reasonable upper bound
1544        }
1545
1546        /// Property: Empty and whitespace-only parameters are handled safely
1547        #[test]
1548        fn proptest_url_params_empty_safe(
1549            key in "\\s*",
1550            value in "\\s*",
1551        ) {
1552            let config = create_test_config();
1553            let client = VeracodeClient::new(config)
1554                .expect("valid test client configuration");
1555
1556            let params = vec![(key.as_str(), value.as_str())];
1557            let url = client.build_url_with_params("/api/test", &params);
1558
1559            // Must not panic and produce valid URL
1560            prop_assert!(url.starts_with("https://"));
1561        }
1562    }
1563
1564    // ============================================================================
1565    // SECURITY TEST: HMAC Signature Generation
1566    // ============================================================================
1567
1568    proptest! {
1569        #![proptest_config(ProptestConfig {
1570            cases: if cfg!(miri) { 5 } else { 1000 },
1571            failure_persistence: None,
1572            .. ProptestConfig::default()
1573        })]
1574
1575        /// Property: HMAC signature generation must handle invalid URLs gracefully
1576        /// Tests that malformed URLs return errors instead of panicking
1577        #[test]
1578        fn proptest_hmac_invalid_urls_return_error(
1579            invalid_url in ".*{0,100}",
1580        ) {
1581            let config = create_test_config();
1582            let client = VeracodeClient::new(config)
1583                .expect("valid test client configuration");
1584
1585            // Property: Invalid URLs must return Err, never panic
1586            let result = client.generate_hmac_signature(
1587                "GET",
1588                &invalid_url,
1589                1234567890000,
1590                "0123456789abcdef0123456789abcdef",
1591            );
1592
1593            // Either succeeds (if URL happens to be valid) or returns error
1594            match result {
1595                Ok(_) => {
1596                    // If it succeeded, the URL must have been parseable
1597                    prop_assert!(Url::parse(&invalid_url).is_ok());
1598                },
1599                Err(e) => {
1600                    // Error must be Authentication error
1601                    prop_assert!(matches!(e, VeracodeError::Authentication(_)));
1602                }
1603            }
1604        }
1605
1606        /// Property: HMAC signature must be deterministic
1607        /// Same inputs must always produce the same signature
1608        #[test]
1609        fn proptest_hmac_deterministic(
1610            method in "[A-Z]{3,7}",
1611            timestamp in 1000000000000u64..2000000000000u64,
1612        ) {
1613            let config = create_test_config();
1614            let client = VeracodeClient::new(config)
1615                .expect("valid test client configuration");
1616
1617            let url = "https://api.veracode.com/api/test";
1618            let nonce = "0123456789abcdef0123456789abcdef";
1619
1620            let sig1 = client.generate_hmac_signature(&method, url, timestamp, nonce);
1621            let sig2 = client.generate_hmac_signature(&method, url, timestamp, nonce);
1622
1623            // Property: Deterministic - same inputs produce same output
1624            match (sig1, sig2) {
1625                (Ok(s1), Ok(s2)) => prop_assert_eq!(s1, s2),
1626                (Err(_), Err(_)) => {}, // Both failed - also deterministic
1627                _ => prop_assert!(false, "Non-deterministic result"),
1628            }
1629        }
1630
1631        /// Property: Invalid hex nonce must return error
1632        /// Tests that non-hex nonces are rejected safely
1633        #[test]
1634        fn proptest_hmac_invalid_nonce_returns_error(
1635            invalid_nonce in "[^0-9a-fA-F]{1,32}",
1636        ) {
1637            let config = create_test_config();
1638            let client = VeracodeClient::new(config)
1639                .expect("valid test client configuration");
1640
1641            let result = client.generate_hmac_signature(
1642                "GET",
1643                "https://api.veracode.com/api/test",
1644                1234567890000,
1645                &invalid_nonce,
1646            );
1647
1648            // Property: Non-hex nonce must return Authentication error
1649            prop_assert!(matches!(result, Err(VeracodeError::Authentication(_))));
1650        }
1651
1652        /// Property: Timestamp overflow must be handled safely
1653        /// Tests edge cases in timestamp handling
1654        #[test]
1655        fn proptest_hmac_timestamp_safe(
1656            timestamp in any::<u64>(),
1657        ) {
1658            let config = create_test_config();
1659            let client = VeracodeClient::new(config)
1660                .expect("valid test client configuration");
1661
1662            let url = "https://api.veracode.com/api/test";
1663            let nonce = "0123456789abcdef0123456789abcdef";
1664
1665            // Must not panic on any timestamp value
1666            let result = client.generate_hmac_signature("GET", url, timestamp, nonce);
1667
1668            // Property: Either succeeds or returns error, never panics
1669            prop_assert!(result.is_ok() || result.is_err());
1670        }
1671    }
1672
1673    // ============================================================================
1674    // SECURITY TEST: Authentication Header Generation
1675    // ============================================================================
1676
1677    proptest! {
1678        #![proptest_config(ProptestConfig {
1679            cases: if cfg!(miri) { 5 } else { 1000 },
1680            failure_persistence: None,
1681            .. ProptestConfig::default()
1682        })]
1683
1684        /// Property: Auth header must contain all required components
1685        /// Tests that generated headers have proper VERACODE-HMAC-SHA-256 format
1686        #[test]
1687        fn proptest_auth_header_format(
1688            method in "[A-Z]{3,7}",
1689        ) {
1690            let config = create_test_config();
1691            let client = VeracodeClient::new(config)
1692                .expect("valid test client configuration");
1693
1694            let url = "https://api.veracode.com/api/test";
1695            let result = client.generate_auth_header(&method, url);
1696
1697            if let Ok(header) = result {
1698                // Property 1: Must start with correct prefix
1699                prop_assert!(header.starts_with("VERACODE-HMAC-SHA-256"));
1700
1701                // Property 2: Must contain all required fields
1702                prop_assert!(header.contains("id="));
1703                prop_assert!(header.contains("ts="));
1704                prop_assert!(header.contains("nonce="));
1705                prop_assert!(header.contains("sig="));
1706
1707                // Property 3: Fields must be comma-separated
1708                let parts: Vec<&str> = header.split(',').collect();
1709                prop_assert_eq!(parts.len(), 4);
1710            }
1711        }
1712
1713        /// Property: Auth header nonce must be unique and valid hex
1714        /// Tests that nonces are properly generated as 32-character hex strings
1715        #[test]
1716        fn proptest_auth_header_nonce_unique(
1717            _seed in any::<u8>(),
1718        ) {
1719            let config = create_test_config();
1720            let client = VeracodeClient::new(config)
1721                .expect("valid test client configuration");
1722
1723            let url = "https://api.veracode.com/api/test";
1724
1725            // Generate two headers
1726            let header1 = client.generate_auth_header("GET", url)
1727                .expect("valid auth header generation");
1728            let header2 = client.generate_auth_header("GET", url)
1729                .expect("valid auth header generation");
1730
1731            // Extract nonces using a helper function (avoids lifetime issues)
1732            fn extract_nonce(h: &str) -> Option<String> {
1733                Some(h.split("nonce=")
1734                    .nth(1)?
1735                    .split(',')
1736                    .next()?
1737                    .to_string())
1738            }
1739
1740            if let (Some(nonce1), Some(nonce2)) = (extract_nonce(&header1), extract_nonce(&header2)) {
1741                // Property 1: Nonces should be different (probabilistically)
1742                // With 128-bit random, collision is extremely unlikely
1743                prop_assert_ne!(&nonce1, &nonce2);
1744
1745                // Property 2: Nonces must be valid hex (32 chars for 16 bytes)
1746                prop_assert_eq!(nonce1.len(), 32);
1747                prop_assert_eq!(nonce2.len(), 32);
1748                prop_assert!(nonce1.chars().all(|c| c.is_ascii_hexdigit()));
1749                prop_assert!(nonce2.chars().all(|c| c.is_ascii_hexdigit()));
1750            }
1751        }
1752    }
1753
1754    // ============================================================================
1755    // SECURITY TEST: Configuration & Client Creation
1756    // ============================================================================
1757
1758    proptest! {
1759        #![proptest_config(ProptestConfig {
1760            cases: if cfg!(miri) { 5 } else { 100 },
1761            failure_persistence: None,
1762            .. ProptestConfig::default()
1763        })]
1764
1765        /// Property: Client creation with invalid config must fail gracefully
1766        /// Tests that invalid proxy URLs are caught during client creation
1767        #[test]
1768        fn proptest_client_creation_invalid_proxy(
1769            invalid_proxy in ".*{0,100}",
1770        ) {
1771            use crate::{VeracodeCredentials, VeracodeRegion};
1772
1773            let config = VeracodeConfig {
1774                credentials: VeracodeCredentials::new(
1775                    "test_api_id".to_string(),
1776                    "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
1777                ),
1778                base_url: "https://api.veracode.com".to_string(),
1779                rest_base_url: "https://api.veracode.com".to_string(),
1780                xml_base_url: "https://analysiscenter.veracode.com".to_string(),
1781                region: VeracodeRegion::Commercial,
1782                validate_certificates: true,
1783                connect_timeout: 30,
1784                request_timeout: 300,
1785                proxy_url: Some(invalid_proxy.clone()),
1786                proxy_username: None,
1787                proxy_password: None,
1788                retry_config: Default::default(),
1789            };
1790
1791            let result = VeracodeClient::new(config);
1792
1793            // Property: Either succeeds (if proxy URL is valid) or returns InvalidConfig error
1794            match result {
1795                Ok(_) => {
1796                    // If successful, proxy URL must be valid
1797                    prop_assert!(reqwest::Proxy::all(&invalid_proxy).is_ok());
1798                },
1799                Err(e) => {
1800                    // Must be InvalidConfig error
1801                    prop_assert!(matches!(e, VeracodeError::InvalidConfig(_)));
1802                }
1803            }
1804        }
1805
1806        /// Property: Timeout values must be handled safely
1807        /// Tests that extreme timeout values don't cause panics
1808        #[test]
1809        fn proptest_client_timeouts_safe(
1810            connect_timeout in 1u64..=3600,
1811            request_timeout in 1u64..=7200,
1812        ) {
1813            use crate::{VeracodeCredentials, VeracodeRegion};
1814
1815            let config = VeracodeConfig {
1816                credentials: VeracodeCredentials::new(
1817                    "test_api_id".to_string(),
1818                    "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
1819                ),
1820                base_url: "https://api.veracode.com".to_string(),
1821                rest_base_url: "https://api.veracode.com".to_string(),
1822                xml_base_url: "https://analysiscenter.veracode.com".to_string(),
1823                region: VeracodeRegion::Commercial,
1824                validate_certificates: true,
1825                connect_timeout,
1826                request_timeout,
1827                proxy_url: None,
1828                proxy_username: None,
1829                proxy_password: None,
1830                retry_config: Default::default(),
1831            };
1832
1833            // Must not panic on any valid timeout values
1834            let result = VeracodeClient::new(config);
1835            prop_assert!(result.is_ok());
1836        }
1837    }
1838
1839    // ============================================================================
1840    // SECURITY TEST: File Size Limits & Memory Safety
1841    // ============================================================================
1842
1843    proptest! {
1844        #![proptest_config(ProptestConfig {
1845            cases: if cfg!(miri) { 5 } else { 100 },
1846            failure_persistence: None,
1847            .. ProptestConfig::default()
1848        })]
1849
1850        /// Property: File size calculations must not overflow
1851        /// Tests that capacity calculations for file uploads are safe
1852        #[test]
1853        fn proptest_file_upload_capacity_safe(
1854            file_size in 0usize..=1000000,
1855        ) {
1856            // Create file data of specified size
1857            let file_data = vec![0u8; file_size];
1858
1859            // Wrap in Arc like the upload functions do
1860            let file_data_arc = Arc::new(file_data);
1861
1862            // Property 1: Length must match original size
1863            prop_assert_eq!(file_data_arc.len(), file_size);
1864
1865            // Property 2: Arc clone must be cheap (same allocation)
1866            let clone1 = Arc::clone(&file_data_arc);
1867            let clone2 = Arc::clone(&file_data_arc);
1868            prop_assert_eq!(clone1.len(), file_size);
1869            prop_assert_eq!(clone2.len(), file_size);
1870        }
1871
1872        /// Property: Content-Type handling must prevent injection
1873        /// Tests that content types are handled safely without code execution
1874        #[test]
1875        fn proptest_content_type_safe(
1876            content_type in ".*{0,200}",
1877        ) {
1878            // Test Cow allocation strategy
1879            let content_type_cow: Cow<str> = if content_type.len() < 64 {
1880                Cow::Borrowed(&content_type)
1881            } else {
1882                Cow::Owned(content_type.clone())
1883            };
1884
1885            // Property 1: Must not contain script injection attempts
1886            let ct_lower = content_type_cow.to_lowercase();
1887            if ct_lower.contains("<script>") || ct_lower.contains("javascript:") {
1888                // These should be treated as literal strings, not executed
1889                prop_assert!(content_type_cow.as_ref().contains("<script>") ||
1890                           content_type_cow.as_ref().contains("javascript:"));
1891            }
1892
1893            // Property 2: Length must be preserved
1894            prop_assert_eq!(content_type_cow.len(), content_type.len());
1895        }
1896    }
1897
1898    // ============================================================================
1899    // UNIT TESTS: Specific Security Scenarios
1900    // ============================================================================
1901
1902    #[test]
1903    fn test_hmac_signature_with_query_params() {
1904        let config = create_test_config();
1905        let client = VeracodeClient::new(config).expect("valid test client configuration");
1906
1907        // Test URL with query parameters
1908        let url = "https://api.veracode.com/api/test?param1=value1&param2=value2";
1909        let nonce = "0123456789abcdef0123456789abcdef";
1910        let timestamp = 1234567890000;
1911
1912        let result = client.generate_hmac_signature("GET", url, timestamp, nonce);
1913        assert!(result.is_ok());
1914
1915        let signature = result.expect("valid HMAC signature");
1916        // HMAC signature should be 64 hex characters (32 bytes * 2)
1917        assert_eq!(signature.len(), 64);
1918        assert!(signature.chars().all(|c| c.is_ascii_hexdigit()));
1919    }
1920
1921    #[test]
1922    fn test_hmac_signature_different_methods() {
1923        let config = create_test_config();
1924        let client = VeracodeClient::new(config).expect("valid test client configuration");
1925
1926        let url = "https://api.veracode.com/api/test";
1927        let nonce = "0123456789abcdef0123456789abcdef";
1928        let timestamp = 1234567890000;
1929
1930        let sig_get = client
1931            .generate_hmac_signature("GET", url, timestamp, nonce)
1932            .expect("valid HMAC signature for GET");
1933        let sig_post = client
1934            .generate_hmac_signature("POST", url, timestamp, nonce)
1935            .expect("valid HMAC signature for POST");
1936
1937        // Different methods should produce different signatures
1938        assert_ne!(sig_get, sig_post);
1939    }
1940
1941    #[test]
1942    fn test_url_encoding_special_characters() {
1943        let config = create_test_config();
1944        let client = VeracodeClient::new(config).expect("valid test client configuration");
1945
1946        // Test that special characters are properly encoded
1947        let params = vec![
1948            ("key1", "value with spaces"),
1949            ("key2", "value&with&ampersands"),
1950            ("key3", "value=with=equals"),
1951            ("key4", "value?with?questions"),
1952        ];
1953
1954        let url = client.build_url_with_params("/api/test", &params);
1955
1956        // URL should contain encoded spaces
1957        assert!(url.contains("value%20with%20spaces") || url.contains("value+with+spaces"));
1958        // URL should contain encoded ampersands
1959        assert!(url.contains("%26"));
1960        // URL should start with base URL
1961        assert!(url.starts_with("https://api.veracode.com/api/test?"));
1962    }
1963
1964    #[test]
1965    fn test_url_encoding_unicode() {
1966        let config = create_test_config();
1967        let client = VeracodeClient::new(config).expect("valid test client configuration");
1968
1969        // Test Unicode handling
1970        let params = vec![
1971            ("key", "δ½ ε₯½δΈ–η•Œ"), // Chinese characters
1972            ("key2", "πŸ”’πŸ›‘οΈ"),    // Emojis
1973        ];
1974
1975        let url = client.build_url_with_params("/api/test", &params);
1976
1977        // Must not panic and should produce valid URL
1978        assert!(url.starts_with("https://api.veracode.com/api/test?"));
1979        // URL should contain percent-encoded Unicode
1980        assert!(url.contains('%'));
1981    }
1982
1983    #[test]
1984    fn test_empty_query_params() {
1985        let config = create_test_config();
1986        let client = VeracodeClient::new(config).expect("valid test client configuration");
1987
1988        let url = client.build_url_with_params("/api/test", &[]);
1989
1990        // Empty params should not add '?'
1991        assert_eq!(url, "https://api.veracode.com/api/test");
1992    }
1993
1994    #[test]
1995    fn test_invalid_api_key_format() {
1996        use crate::{VeracodeCredentials, VeracodeRegion};
1997
1998        // Create config with non-hex API key
1999        let config = VeracodeConfig {
2000            credentials: VeracodeCredentials::new(
2001                "test_api_id".to_string(),
2002                "not_valid_hex_key".to_string(),
2003            ),
2004            base_url: "https://api.veracode.com".to_string(),
2005            rest_base_url: "https://api.veracode.com".to_string(),
2006            xml_base_url: "https://analysiscenter.veracode.com".to_string(),
2007            region: VeracodeRegion::Commercial,
2008            validate_certificates: true,
2009            connect_timeout: 30,
2010            request_timeout: 300,
2011            proxy_url: None,
2012            proxy_username: None,
2013            proxy_password: None,
2014            retry_config: Default::default(),
2015        };
2016
2017        let client = VeracodeClient::new(config).expect("valid test client configuration");
2018        let result = client.generate_auth_header("GET", "https://api.veracode.com/api/test");
2019
2020        // Should return Authentication error for invalid hex key
2021        assert!(matches!(result, Err(VeracodeError::Authentication(_))));
2022    }
2023
2024    #[test]
2025    fn test_auth_header_format() {
2026        let config = create_test_config();
2027        let client = VeracodeClient::new(config).expect("valid test client configuration");
2028
2029        let header = client
2030            .generate_auth_header("GET", "https://api.veracode.com/api/test")
2031            .expect("valid auth header generation");
2032
2033        // Verify format
2034        assert!(header.starts_with("VERACODE-HMAC-SHA-256 "));
2035        assert!(header.contains("id=test_api_id"));
2036        assert!(header.contains("ts="));
2037        assert!(header.contains("nonce="));
2038        assert!(header.contains("sig="));
2039
2040        // Verify structure (should have 4 comma-separated parts after prefix)
2041        let parts: Vec<&str> = header.split(',').collect();
2042        assert_eq!(parts.len(), 4);
2043    }
2044
2045    #[cfg(not(miri))] // Skip under Miri - uses SystemTime
2046    #[test]
2047    fn test_auth_header_timestamp_monotonic() {
2048        let config = create_test_config();
2049        let client = VeracodeClient::new(config).expect("valid test client configuration");
2050
2051        let header1 = client
2052            .generate_auth_header("GET", "https://api.veracode.com/api/test")
2053            .expect("valid auth header generation");
2054        std::thread::sleep(std::time::Duration::from_millis(10));
2055        let header2 = client
2056            .generate_auth_header("GET", "https://api.veracode.com/api/test")
2057            .expect("valid auth header generation");
2058
2059        // Extract timestamps
2060        let extract_ts =
2061            |h: &str| -> Option<u64> { h.split("ts=").nth(1)?.split(',').next()?.parse().ok() };
2062
2063        let ts1 = extract_ts(&header1).expect("valid timestamp extraction");
2064        let ts2 = extract_ts(&header2).expect("valid timestamp extraction");
2065
2066        // Second timestamp should be >= first (monotonic)
2067        assert!(ts2 >= ts1);
2068    }
2069
2070    #[test]
2071    fn test_base_url_accessor() {
2072        let config = create_test_config();
2073        let client = VeracodeClient::new(config).expect("valid test client configuration");
2074
2075        assert_eq!(client.base_url(), "https://api.veracode.com");
2076    }
2077
2078    #[test]
2079    fn test_client_clone() {
2080        let config = create_test_config();
2081        let client1 = VeracodeClient::new(config).expect("valid test client configuration");
2082        let client2 = client1.clone();
2083
2084        // Both clients should have same base URL
2085        assert_eq!(client1.base_url(), client2.base_url());
2086    }
2087
2088    #[test]
2089    fn test_url_capacity_estimation() {
2090        let config = create_test_config();
2091        let client = VeracodeClient::new(config).expect("valid test client configuration");
2092
2093        // Test with large number of parameters
2094        let params: Vec<(&str, &str)> = (0..100).map(|_| ("key", "value")).collect();
2095
2096        let url = client.build_url_with_params("/api/test", &params);
2097
2098        // Should handle large param counts without panic
2099        assert!(url.starts_with("https://api.veracode.com/api/test?"));
2100        assert!(url.len() > 100); // Should contain all params
2101    }
2102
2103    #[test]
2104    fn test_saturating_arithmetic() {
2105        let config = create_test_config();
2106        let client = VeracodeClient::new(config).expect("valid test client configuration");
2107
2108        // Test saturating_add in capacity calculation
2109        let params: Vec<(&str, &str)> = vec![("k", "v"); 1000];
2110
2111        // Should not panic even with large numbers
2112        let url = client.build_url_with_params("/api/test", &params);
2113        assert!(url.len() < usize::MAX);
2114    }
2115}