veracode_platform/
lib.rs

1//! # Veracode API Client Library
2//!
3//! A comprehensive Rust client library for interacting with Veracode APIs including
4//! Applications, Identity, Pipeline Scan, and Sandbox APIs.
5//!
6//! This library provides a safe and ergonomic interface to the Veracode platform,
7//! handling HMAC authentication, request/response serialization, and error handling.
8//!
9//! ## Features
10//!
11//! - ๐Ÿ” **HMAC Authentication** - Built-in support for Veracode API credentials
12//! - ๐ŸŒ **Multi-Regional Support** - Automatic endpoint routing for Commercial, European, and Federal regions
13//! - ๐Ÿ”„ **Smart API Routing** - Automatically uses REST or XML APIs based on the operation
14//! - ๐Ÿ“ฑ **Applications API** - Manage applications, builds, and scans (REST)
15//! - ๐Ÿ‘ค **Identity API** - User and team management (REST)
16//! - ๐Ÿ” **Pipeline Scan API** - Automated security scanning in CI/CD pipelines (REST)
17//! - ๐Ÿงช **Sandbox API** - Development sandbox management (REST)
18//! - ๐Ÿ“ค **Sandbox Scan API** - File upload and scan operations (XML)
19//! - ๐Ÿš€ **Async/Await** - Built on tokio for high-performance async operations
20//! - โšก **Type-Safe** - Full Rust type safety with serde serialization
21//! - ๐Ÿ“Š **Rich Data Types** - Comprehensive data structures for all API responses
22//!
23//! ## Quick Start
24//!
25//! ```no_run
26//! use veracode_platform::{VeracodeConfig, VeracodeClient, VeracodeRegion};
27//!
28//! #[tokio::main]
29//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
30//!     // Create configuration - automatically supports both API types
31//!     let config = VeracodeConfig::new(
32//!         "your_api_id",
33//!         "your_api_key",
34//!     ).with_region(VeracodeRegion::Commercial); // Optional: defaults to Commercial
35//!
36//!     let client = VeracodeClient::new(config)?;
37//!     
38//!     // REST API modules (use api.veracode.*)
39//!     let apps = client.get_all_applications().await?;
40//!     let pipeline = client.pipeline_api();
41//!     let identity = client.identity_api();
42//!     let sandbox = client.sandbox_api();  // REST API for sandbox management
43//!     let policy = client.policy_api();
44//!     
45//!     // XML API modules (automatically use analysiscenter.veracode.*)
46//!     let scan = client.scan_api(); // XML API for scanning
47//!     
48//!     Ok(())
49//! }
50//! ```
51//!
52//! ## Regional Support
53//!
54//! The library automatically handles regional endpoints for both API types:
55//!
56//! ```no_run
57//! use veracode_platform::{VeracodeConfig, VeracodeRegion};
58//!
59//! // European region
60//! let config = VeracodeConfig::new("api_id", "api_key")
61//!     .with_region(VeracodeRegion::European);
62//! // REST APIs will use: api.veracode.eu
63//! // XML APIs will use: analysiscenter.veracode.eu
64//!
65//! // US Federal region  
66//! let config = VeracodeConfig::new("api_id", "api_key")
67//!     .with_region(VeracodeRegion::Federal);
68//! // REST APIs will use: api.veracode.us
69//! // XML APIs will use: analysiscenter.veracode.us
70//! ```
71//!
72//! ## API Types
73//!
74//! Different Veracode modules use different API endpoints:
75//!
76//! - **REST API (api.veracode.*)**: Applications, Identity, Pipeline, Policy, Sandbox management
77//! - **XML API (analysiscenter.veracode.*)**: Sandbox scanning operations
78//!
79//! The client automatically routes each module to the correct API type based on the operation.
80//!
81//! ## Sandbox Operations
82//!
83//! Note that sandbox functionality is split across two modules:
84//!
85//! - **`sandbox_api()`** - Sandbox management (create, delete, list sandboxes) via REST API
86//! - **`scan_api()`** - File upload and scan operations via XML API
87//!
88//! This separation reflects the underlying Veracode API architecture where sandbox management
89//! uses the newer REST endpoints while scan operations use the legacy XML endpoints.
90
91pub mod app;
92pub mod build;
93pub mod client;
94pub mod findings;
95pub mod identity;
96pub mod pipeline;
97pub mod policy;
98pub mod sandbox;
99pub mod scan;
100pub mod workflow;
101
102use reqwest::Error as ReqwestError;
103use secrecy::{ExposeSecret, SecretString};
104use std::fmt;
105use std::sync::Arc;
106use std::time::Duration;
107
108// Re-export common types for convenience
109pub use app::{
110    Application, ApplicationQuery, ApplicationsResponse, CreateApplicationRequest,
111    UpdateApplicationRequest,
112};
113pub use build::{
114    Build, BuildApi, BuildError, BuildList, CreateBuildRequest, DeleteBuildRequest,
115    DeleteBuildResult, GetBuildInfoRequest, GetBuildListRequest, UpdateBuildRequest,
116};
117pub use client::VeracodeClient;
118pub use findings::{
119    CweInfo, FindingCategory, FindingDetails, FindingStatus, FindingsApi, FindingsError,
120    FindingsQuery, FindingsResponse, RestFinding,
121};
122pub use identity::{
123    ApiCredential, BusinessUnit, CreateApiCredentialRequest, CreateTeamRequest, CreateUserRequest,
124    IdentityApi, IdentityError, Role, Team, UpdateTeamRequest, UpdateUserRequest, User, UserQuery,
125    UserType,
126};
127pub use pipeline::{
128    CreateScanRequest, DevStage, Finding, FindingsSummary, PipelineApi, PipelineError, Scan,
129    ScanConfig, ScanResults, ScanStage, ScanStatus, SecurityStandards, Severity,
130};
131pub use policy::{
132    ApiSource, PolicyApi, PolicyComplianceResult, PolicyComplianceStatus, PolicyError, PolicyRule,
133    PolicyScanRequest, PolicyScanResult, PolicyThresholds, ScanType, SecurityPolicy, SummaryReport,
134};
135pub use sandbox::{
136    ApiError, ApiErrorResponse, CreateSandboxRequest, Sandbox, SandboxApi, SandboxError,
137    SandboxListParams, SandboxScan, UpdateSandboxRequest,
138};
139pub use scan::{
140    BeginPreScanRequest, BeginScanRequest, PreScanMessage, PreScanResults, ScanApi, ScanError,
141    ScanInfo, ScanModule, UploadFileRequest, UploadLargeFileRequest, UploadProgress,
142    UploadProgressCallback, UploadedFile,
143};
144pub use workflow::{VeracodeWorkflow, WorkflowConfig, WorkflowError, WorkflowResultData};
145/// Retry configuration for HTTP requests
146#[derive(Debug, Clone)]
147pub struct RetryConfig {
148    /// Maximum number of retry attempts (default: 5)
149    pub max_attempts: u32,
150    /// Initial delay between retries in milliseconds (default: 1000ms)
151    pub initial_delay_ms: u64,
152    /// Maximum delay between retries in milliseconds (default: 30000ms)
153    pub max_delay_ms: u64,
154    /// Exponential backoff multiplier (default: 2.0)
155    pub backoff_multiplier: f64,
156    /// Maximum total time to spend on retries in milliseconds (default: 300000ms = 5 minutes)
157    pub max_total_delay_ms: u64,
158    /// Buffer time in seconds to add when waiting for rate limit window reset (default: 5s)
159    pub rate_limit_buffer_seconds: u64,
160    /// Maximum number of retry attempts specifically for rate limit errors (default: 1)
161    pub rate_limit_max_attempts: u32,
162    /// Whether to enable jitter in retry delays (default: true)
163    pub jitter_enabled: bool,
164}
165
166impl Default for RetryConfig {
167    fn default() -> Self {
168        Self {
169            max_attempts: 5,
170            initial_delay_ms: 1000,
171            max_delay_ms: 30000,
172            backoff_multiplier: 2.0,
173            max_total_delay_ms: 300_000,  // 5 minutes
174            rate_limit_buffer_seconds: 5, // 5 second buffer for rate limit windows
175            rate_limit_max_attempts: 1,   // Only retry once for rate limits
176            jitter_enabled: true,         // Enable jitter by default
177        }
178    }
179}
180
181impl RetryConfig {
182    /// Create a new retry configuration with conservative defaults
183    #[must_use]
184    pub fn new() -> Self {
185        Self::default()
186    }
187
188    /// Set the maximum number of retry attempts
189    #[must_use]
190    pub fn with_max_attempts(mut self, max_attempts: u32) -> Self {
191        self.max_attempts = max_attempts;
192        self
193    }
194
195    /// Set the initial delay between retries
196    #[must_use]
197    pub fn with_initial_delay(mut self, delay_ms: u64) -> Self {
198        self.initial_delay_ms = delay_ms;
199        self
200    }
201
202    /// Set the initial delay between retries (alias for compatibility)
203    #[must_use]
204    pub fn with_initial_delay_millis(mut self, delay_ms: u64) -> Self {
205        self.initial_delay_ms = delay_ms;
206        self
207    }
208
209    /// Set the maximum delay between retries
210    #[must_use]
211    pub fn with_max_delay(mut self, delay_ms: u64) -> Self {
212        self.max_delay_ms = delay_ms;
213        self
214    }
215
216    /// Set the maximum delay between retries (alias for compatibility)
217    #[must_use]
218    pub fn with_max_delay_millis(mut self, delay_ms: u64) -> Self {
219        self.max_delay_ms = delay_ms;
220        self
221    }
222
223    /// Set the exponential backoff multiplier
224    #[must_use]
225    pub fn with_backoff_multiplier(mut self, multiplier: f64) -> Self {
226        self.backoff_multiplier = multiplier;
227        self
228    }
229
230    /// Set the exponential backoff multiplier (alias for compatibility)
231    #[must_use]
232    pub fn with_exponential_backoff(mut self, multiplier: f64) -> Self {
233        self.backoff_multiplier = multiplier;
234        self
235    }
236
237    /// Set the maximum total time to spend on retries
238    #[must_use]
239    pub fn with_max_total_delay(mut self, delay_ms: u64) -> Self {
240        self.max_total_delay_ms = delay_ms;
241        self
242    }
243
244    /// Set the buffer time to add when waiting for rate limit window reset
245    #[must_use]
246    pub fn with_rate_limit_buffer(mut self, buffer_seconds: u64) -> Self {
247        self.rate_limit_buffer_seconds = buffer_seconds;
248        self
249    }
250
251    /// Set the maximum number of retry attempts for rate limit errors
252    #[must_use]
253    pub fn with_rate_limit_max_attempts(mut self, max_attempts: u32) -> Self {
254        self.rate_limit_max_attempts = max_attempts;
255        self
256    }
257
258    /// Disable jitter in retry delays
259    ///
260    /// Jitter adds randomness to retry delays to prevent thundering herd problems.
261    /// Disabling jitter makes retry timing more predictable but may cause synchronized
262    /// retries from multiple clients.
263    #[must_use]
264    pub fn with_jitter_disabled(mut self) -> Self {
265        self.jitter_enabled = false;
266        self
267    }
268
269    /// Calculate the delay for a given attempt number using exponential backoff
270    #[must_use]
271    pub fn calculate_delay(&self, attempt: u32) -> Duration {
272        if attempt == 0 {
273            return Duration::from_millis(0);
274        }
275
276        let delay_ms = (self.initial_delay_ms as f64
277            * self.backoff_multiplier.powi((attempt - 1) as i32)) as u64;
278
279        let mut capped_delay = delay_ms.min(self.max_delay_ms);
280
281        // Apply jitter if enabled (ยฑ25% randomization)
282        if self.jitter_enabled {
283            use rand::Rng;
284            let jitter_range = (capped_delay as f64 * 0.25) as u64;
285            let min_delay = capped_delay.saturating_sub(jitter_range);
286            let max_delay = capped_delay + jitter_range;
287            capped_delay = rand::rng().random_range(min_delay..=max_delay);
288        }
289
290        Duration::from_millis(capped_delay)
291    }
292
293    /// Calculate the delay for rate limit (429) errors
294    ///
295    /// For Veracode's 500 requests/minute rate limiting, this calculates the optimal
296    /// wait time based on the current time within the minute window or uses the
297    /// server's Retry-After header if provided.
298    #[must_use]
299    pub fn calculate_rate_limit_delay(&self, retry_after_seconds: Option<u64>) -> Duration {
300        if let Some(seconds) = retry_after_seconds {
301            // Use the server's suggested delay
302            Duration::from_secs(seconds)
303        } else {
304            // Fall back to minute window calculation for Veracode's 500/minute limit
305            let now = std::time::SystemTime::now()
306                .duration_since(std::time::UNIX_EPOCH)
307                .unwrap_or_default();
308
309            let current_second = now.as_secs() % 60;
310
311            // Wait until the next minute window + configurable buffer to ensure window has reset
312            let seconds_until_next_minute = 60 - current_second;
313
314            Duration::from_secs(seconds_until_next_minute + self.rate_limit_buffer_seconds)
315        }
316    }
317
318    /// Check if an error is retryable based on its type
319    #[must_use]
320    pub fn is_retryable_error(&self, error: &VeracodeError) -> bool {
321        match error {
322            VeracodeError::Http(reqwest_error) => {
323                // Retry on network errors, timeouts, and temporary server errors
324                if reqwest_error.is_timeout()
325                    || reqwest_error.is_connect()
326                    || reqwest_error.is_request()
327                {
328                    return true;
329                }
330
331                // Check for retryable HTTP status codes
332                if let Some(status) = reqwest_error.status() {
333                    match status.as_u16() {
334                        // 429 Too Many Requests
335                        429 => true,
336                        // 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout
337                        502..=504 => true,
338                        // Other server errors (5xx) - retry conservatively
339                        500..=599 => true,
340                        // Don't retry client errors (4xx) except 429
341                        _ => false,
342                    }
343                } else {
344                    // Network-level errors without status codes are typically retryable
345                    true
346                }
347            }
348            // Don't retry authentication, serialization, or configuration errors
349            VeracodeError::Authentication(_)
350            | VeracodeError::Serialization(_)
351            | VeracodeError::InvalidConfig(_) => false,
352            // InvalidResponse could be temporary (like malformed JSON due to network issues)
353            VeracodeError::InvalidResponse(_) => true,
354            // NotFound is typically not retryable
355            VeracodeError::NotFound(_) => false,
356            // New retry-specific error is not retryable (avoid infinite loops)
357            VeracodeError::RetryExhausted(_) => false,
358            // Rate limited errors are retryable with special handling
359            VeracodeError::RateLimited { .. } => true,
360        }
361    }
362}
363
364/// Custom error type for Veracode API operations.
365///
366/// This enum represents all possible errors that can occur when interacting
367/// with the Veracode Applications API.
368#[derive(Debug)]
369pub enum VeracodeError {
370    /// HTTP request failed
371    Http(ReqwestError),
372    /// JSON serialization/deserialization failed
373    Serialization(serde_json::Error),
374    /// Authentication error (invalid credentials, signature generation failure, etc.)
375    Authentication(String),
376    /// API returned an error response
377    InvalidResponse(String),
378    /// Configuration is invalid
379    InvalidConfig(String),
380    /// When an item is not found
381    NotFound(String),
382    /// When all retry attempts have been exhausted
383    RetryExhausted(String),
384    /// Rate limit exceeded (HTTP 429) - includes server's suggested retry delay
385    RateLimited {
386        /// Number of seconds to wait before retrying (from Retry-After header)
387        retry_after_seconds: Option<u64>,
388        /// The original HTTP error response
389        message: String,
390    },
391}
392
393impl VeracodeClient {
394    /// Create a specialized client for XML API operations.
395    ///
396    /// This internal method creates a client configured for the XML API
397    /// (analysiscenter.veracode.*) based on the current region settings.
398    /// Used exclusively for sandbox scan operations that require the XML API.
399    fn new_xml_client(config: VeracodeConfig) -> Result<Self, VeracodeError> {
400        let mut xml_config = config;
401        xml_config.base_url = xml_config.xml_base_url.clone();
402        Self::new(xml_config)
403    }
404
405    /// Get an applications API instance.
406    /// Uses REST API (api.veracode.*).
407    #[must_use]
408    pub fn applications_api(&self) -> &Self {
409        self
410    }
411
412    /// Get a sandbox API instance.
413    /// Uses REST API (api.veracode.*).
414    #[must_use]
415    pub fn sandbox_api(&self) -> SandboxApi<'_> {
416        SandboxApi::new(self)
417    }
418
419    /// Get an identity API instance.
420    /// Uses REST API (api.veracode.*).
421    #[must_use]
422    pub fn identity_api(&self) -> IdentityApi<'_> {
423        IdentityApi::new(self)
424    }
425
426    /// Get a pipeline scan API instance.
427    /// Uses REST API (api.veracode.*).
428    #[must_use]
429    pub fn pipeline_api(&self) -> PipelineApi {
430        PipelineApi::new(self.clone())
431    }
432
433    /// Get a policy API instance.
434    /// Uses REST API (api.veracode.*).
435    #[must_use]
436    pub fn policy_api(&self) -> PolicyApi<'_> {
437        PolicyApi::new(self)
438    }
439
440    /// Get a findings API instance.
441    /// Uses REST API (api.veracode.*).
442    #[must_use]
443    pub fn findings_api(&self) -> FindingsApi {
444        FindingsApi::new(self.clone())
445    }
446
447    /// Get a scan API instance.
448    /// Uses XML API (analysiscenter.veracode.*) for both sandbox and application scans.
449    #[must_use]
450    pub fn scan_api(&self) -> ScanApi {
451        // Create a specialized XML client for scan operations
452        let xml_client = Self::new_xml_client(self.config().clone())
453            .expect("XML client creation should not fail if main client was created successfully");
454        ScanApi::new(xml_client)
455    }
456
457    /// Get a build API instance.
458    /// Uses XML API (analysiscenter.veracode.*) for build management operations.
459    #[must_use]
460    pub fn build_api(&self) -> build::BuildApi {
461        // Create a specialized XML client for build operations
462        let xml_client = Self::new_xml_client(self.config().clone())
463            .expect("XML client creation should not fail if main client was created successfully");
464        build::BuildApi::new(xml_client)
465    }
466
467    /// Get a workflow helper instance.
468    /// Provides high-level operations that combine multiple API calls.
469    #[must_use]
470    pub fn workflow(&self) -> workflow::VeracodeWorkflow {
471        workflow::VeracodeWorkflow::new(self.clone())
472    }
473}
474
475impl fmt::Display for VeracodeError {
476    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
477        match self {
478            VeracodeError::Http(e) => write!(f, "HTTP error: {e}"),
479            VeracodeError::Serialization(e) => write!(f, "Serialization error: {e}"),
480            VeracodeError::Authentication(e) => write!(f, "Authentication error: {e}"),
481            VeracodeError::InvalidResponse(e) => write!(f, "Invalid response: {e}"),
482            VeracodeError::InvalidConfig(e) => write!(f, "Invalid configuration: {e}"),
483            VeracodeError::NotFound(e) => write!(f, "Item not found: {e}"),
484            VeracodeError::RetryExhausted(e) => write!(f, "Retry attempts exhausted: {e}"),
485            VeracodeError::RateLimited {
486                retry_after_seconds,
487                message,
488            } => match retry_after_seconds {
489                Some(seconds) => {
490                    write!(f, "Rate limit exceeded: {message} (retry after {seconds}s)")
491                }
492                None => write!(f, "Rate limit exceeded: {message}"),
493            },
494        }
495    }
496}
497
498impl std::error::Error for VeracodeError {}
499
500impl From<ReqwestError> for VeracodeError {
501    fn from(error: ReqwestError) -> Self {
502        VeracodeError::Http(error)
503    }
504}
505
506impl From<serde_json::Error> for VeracodeError {
507    fn from(error: serde_json::Error) -> Self {
508        VeracodeError::Serialization(error)
509    }
510}
511
512/// ARC-based credential storage for thread-safe access via memory pointers
513///
514/// This struct provides secure credential storage with the following protections:
515/// - Fields are private to prevent direct access
516/// - SecretString provides memory protection and debug redaction
517/// - ARC allows safe sharing across threads
518/// - Access is only possible through controlled expose_* methods
519#[derive(Clone)]
520pub struct VeracodeCredentials {
521    /// API ID stored in ARC for shared access - PRIVATE for security
522    api_id: Arc<SecretString>,
523    /// API key stored in ARC for shared access - PRIVATE for security  
524    api_key: Arc<SecretString>,
525}
526
527impl VeracodeCredentials {
528    /// Create new ARC-based credentials
529    #[must_use]
530    pub fn new(api_id: String, api_key: String) -> Self {
531        Self {
532            api_id: Arc::new(SecretString::new(api_id.into())),
533            api_key: Arc::new(SecretString::new(api_key.into())),
534        }
535    }
536
537    /// Get API ID via memory pointer (ARC) - USE WITH CAUTION
538    ///
539    /// # Security Warning
540    /// This returns an `Arc<SecretString>` which allows the caller to call expose_secret().
541    /// Only use this method when you need to share credentials across thread boundaries.
542    /// For authentication, prefer using expose_api_id() directly.
543    #[must_use]
544    pub fn api_id_ptr(&self) -> Arc<SecretString> {
545        Arc::clone(&self.api_id)
546    }
547
548    /// Get API key via memory pointer (ARC) - USE WITH CAUTION
549    ///
550    /// # Security Warning  
551    /// This returns an `Arc<SecretString>` which allows the caller to call expose_secret().
552    /// Only use this method when you need to share credentials across thread boundaries.
553    /// For authentication, prefer using expose_api_key() directly.
554    #[must_use]
555    pub fn api_key_ptr(&self) -> Arc<SecretString> {
556        Arc::clone(&self.api_key)
557    }
558
559    /// Access API ID securely (temporary access for authentication)
560    ///
561    /// This is the preferred method for accessing the API ID during authentication.
562    /// The returned reference is only valid for the lifetime of this call.
563    #[must_use]
564    pub fn expose_api_id(&self) -> &str {
565        self.api_id.expose_secret()
566    }
567
568    /// Access API key securely (temporary access for authentication)
569    ///
570    /// This is the preferred method for accessing the API key during authentication.
571    /// The returned reference is only valid for the lifetime of this call.
572    #[must_use]
573    pub fn expose_api_key(&self) -> &str {
574        self.api_key.expose_secret()
575    }
576}
577
578impl std::fmt::Debug for VeracodeCredentials {
579    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
580        f.debug_struct("VeracodeCredentials")
581            .field("api_id", &"[REDACTED]")
582            .field("api_key", &"[REDACTED]")
583            .finish()
584    }
585}
586
587/// Configuration for the Veracode API client.
588///
589/// This struct contains all the necessary configuration for connecting to
590/// the Veracode APIs, including authentication credentials and regional settings.
591/// It automatically manages both REST API (api.veracode.*) and XML API
592/// (analysiscenter.veracode.*) endpoints based on the selected region.
593#[derive(Clone)]
594pub struct VeracodeConfig {
595    /// ARC-based credentials for thread-safe access
596    pub credentials: VeracodeCredentials,
597    /// Base URL for the current client instance
598    pub base_url: String,
599    /// REST API base URL (api.veracode.*)
600    pub rest_base_url: String,
601    /// XML API base URL (analysiscenter.veracode.*)
602    pub xml_base_url: String,
603    /// Veracode region for your account
604    pub region: VeracodeRegion,
605    /// Whether to validate TLS certificates (default: true)
606    pub validate_certificates: bool,
607    /// Retry configuration for HTTP requests
608    pub retry_config: RetryConfig,
609    /// HTTP connection timeout in seconds (default: 30)
610    pub connect_timeout: u64,
611    /// HTTP request timeout in seconds (default: 300)
612    pub request_timeout: u64,
613    /// HTTP/HTTPS proxy URL (optional)
614    pub proxy_url: Option<String>,
615    /// Proxy authentication username (optional)
616    pub proxy_username: Option<SecretString>,
617    /// Proxy authentication password (optional)
618    pub proxy_password: Option<SecretString>,
619}
620
621/// Custom Debug implementation for VeracodeConfig that redacts sensitive information
622impl std::fmt::Debug for VeracodeConfig {
623    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
624        // Redact proxy URL if it contains credentials
625        let proxy_url_redacted = self.proxy_url.as_ref().map(|url| {
626            if url.contains('@') {
627                // URL contains credentials, redact them
628                if let Some(at_pos) = url.rfind('@') {
629                    if let Some(proto_end) = url.find("://") {
630                        format!("{}://[REDACTED]@{}", &url[..proto_end], &url[at_pos + 1..])
631                    } else {
632                        "[REDACTED]".to_string()
633                    }
634                } else {
635                    "[REDACTED]".to_string()
636                }
637            } else {
638                url.clone()
639            }
640        });
641
642        f.debug_struct("VeracodeConfig")
643            .field("credentials", &self.credentials)
644            .field("base_url", &self.base_url)
645            .field("rest_base_url", &self.rest_base_url)
646            .field("xml_base_url", &self.xml_base_url)
647            .field("region", &self.region)
648            .field("validate_certificates", &self.validate_certificates)
649            .field("retry_config", &self.retry_config)
650            .field("connect_timeout", &self.connect_timeout)
651            .field("request_timeout", &self.request_timeout)
652            .field("proxy_url", &proxy_url_redacted)
653            .field(
654                "proxy_username",
655                &self.proxy_username.as_ref().map(|_| "[REDACTED]"),
656            )
657            .field(
658                "proxy_password",
659                &self.proxy_password.as_ref().map(|_| "[REDACTED]"),
660            )
661            .finish()
662    }
663}
664
665// URL constants for different regions
666const COMMERCIAL_REST_URL: &str = "https://api.veracode.com";
667const COMMERCIAL_XML_URL: &str = "https://analysiscenter.veracode.com";
668const EUROPEAN_REST_URL: &str = "https://api.veracode.eu";
669const EUROPEAN_XML_URL: &str = "https://analysiscenter.veracode.eu";
670const FEDERAL_REST_URL: &str = "https://api.veracode.us";
671const FEDERAL_XML_URL: &str = "https://analysiscenter.veracode.us";
672
673/// Veracode regions for API access.
674///
675/// Different regions use different API endpoints. Choose the region
676/// that matches your Veracode account configuration.
677#[derive(Debug, Clone, Copy, PartialEq)]
678pub enum VeracodeRegion {
679    /// Commercial region (default) - api.veracode.com
680    Commercial,
681    /// European region - api.veracode.eu
682    European,
683    /// US Federal region - api.veracode.us
684    Federal,
685}
686
687impl VeracodeConfig {
688    /// Create a new configuration for the Commercial region.
689    ///
690    /// This creates a configuration that supports both REST API (api.veracode.*)
691    /// and XML API (analysiscenter.veracode.*) endpoints. The base_url defaults
692    /// to REST API for most modules, while sandbox scan operations automatically
693    /// use the XML API endpoint.
694    ///
695    /// # Arguments
696    ///
697    /// * `api_id` - Your Veracode API ID
698    /// * `api_key` - Your Veracode API key
699    ///
700    /// # Returns
701    ///
702    /// A new `VeracodeConfig` instance configured for the Commercial region.
703    #[must_use]
704    pub fn new(api_id: &str, api_key: &str) -> Self {
705        let credentials = VeracodeCredentials::new(api_id.to_string(), api_key.to_string());
706        Self {
707            credentials,
708            base_url: COMMERCIAL_REST_URL.to_string(),
709            rest_base_url: COMMERCIAL_REST_URL.to_string(),
710            xml_base_url: COMMERCIAL_XML_URL.to_string(),
711            region: VeracodeRegion::Commercial,
712            validate_certificates: true, // Default to secure
713            retry_config: RetryConfig::default(),
714            connect_timeout: 30,  // Default: 30 seconds
715            request_timeout: 300, // Default: 5 minutes
716            proxy_url: None,
717            proxy_username: None,
718            proxy_password: None,
719        }
720    }
721
722    /// Create a new configuration with ARC-based credentials
723    ///
724    /// This method allows direct use of ARC pointers for credential sharing
725    /// across threads and components.
726    #[must_use]
727    pub fn from_arc_credentials(api_id: Arc<SecretString>, api_key: Arc<SecretString>) -> Self {
728        let credentials = VeracodeCredentials { api_id, api_key };
729
730        Self {
731            credentials,
732            base_url: COMMERCIAL_REST_URL.to_string(),
733            rest_base_url: COMMERCIAL_REST_URL.to_string(),
734            xml_base_url: COMMERCIAL_XML_URL.to_string(),
735            region: VeracodeRegion::Commercial,
736            validate_certificates: true,
737            retry_config: RetryConfig::default(),
738            connect_timeout: 30,
739            request_timeout: 300,
740            proxy_url: None,
741            proxy_username: None,
742            proxy_password: None,
743        }
744    }
745
746    /// Set the region for this configuration.
747    ///
748    /// This will automatically update both REST and XML API URLs to match the region.
749    /// All modules will use the appropriate regional endpoint for their API type.
750    ///
751    /// # Arguments
752    ///
753    /// * `region` - The Veracode region to use
754    ///
755    /// # Returns
756    ///
757    /// The updated configuration instance (for method chaining).
758    #[must_use]
759    pub fn with_region(mut self, region: VeracodeRegion) -> Self {
760        let (rest_url, xml_url) = match region {
761            VeracodeRegion::Commercial => (COMMERCIAL_REST_URL, COMMERCIAL_XML_URL),
762            VeracodeRegion::European => (EUROPEAN_REST_URL, EUROPEAN_XML_URL),
763            VeracodeRegion::Federal => (FEDERAL_REST_URL, FEDERAL_XML_URL),
764        };
765
766        self.region = region;
767        self.rest_base_url = rest_url.to_string();
768        self.xml_base_url = xml_url.to_string();
769        self.base_url = self.rest_base_url.clone(); // Default to REST
770        self
771    }
772
773    /// Disable certificate validation for development environments.
774    ///
775    /// WARNING: This should only be used in development environments with
776    /// self-signed certificates. Never use this in production.
777    ///
778    /// # Returns
779    ///
780    /// The updated configuration instance (for method chaining).
781    #[must_use]
782    pub fn with_certificate_validation_disabled(mut self) -> Self {
783        self.validate_certificates = false;
784        self
785    }
786
787    /// Set a custom retry configuration.
788    ///
789    /// This allows you to customize the retry behavior for HTTP requests,
790    /// including the number of attempts, delays, and backoff strategy.
791    ///
792    /// # Arguments
793    ///
794    /// * `retry_config` - The retry configuration to use
795    ///
796    /// # Returns
797    ///
798    /// The updated configuration instance (for method chaining).
799    #[must_use]
800    pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
801        self.retry_config = retry_config;
802        self
803    }
804
805    /// Disable retries for HTTP requests.
806    ///
807    /// This will set the retry configuration to perform no retries on failed requests.
808    /// Useful for scenarios where you want to handle errors immediately without any delays.
809    ///
810    /// # Returns
811    ///
812    /// The updated configuration instance (for method chaining).
813    #[must_use]
814    pub fn with_retries_disabled(mut self) -> Self {
815        self.retry_config = RetryConfig::new().with_max_attempts(0);
816        self
817    }
818
819    /// Set the HTTP connection timeout.
820    ///
821    /// This controls how long to wait for a connection to be established.
822    ///
823    /// # Arguments
824    ///
825    /// * `timeout_seconds` - Connection timeout in seconds
826    ///
827    /// # Returns
828    ///
829    /// The updated configuration instance (for method chaining).
830    #[must_use]
831    pub fn with_connect_timeout(mut self, timeout_seconds: u64) -> Self {
832        self.connect_timeout = timeout_seconds;
833        self
834    }
835
836    /// Set the HTTP request timeout.
837    ///
838    /// This controls the total time allowed for a request to complete,
839    /// including connection establishment, request transmission, and response reception.
840    ///
841    /// # Arguments
842    ///
843    /// * `timeout_seconds` - Request timeout in seconds
844    ///
845    /// # Returns
846    ///
847    /// The updated configuration instance (for method chaining).
848    #[must_use]
849    pub fn with_request_timeout(mut self, timeout_seconds: u64) -> Self {
850        self.request_timeout = timeout_seconds;
851        self
852    }
853
854    /// Set both connection and request timeouts.
855    ///
856    /// This is a convenience method to set both timeout values at once.
857    ///
858    /// # Arguments
859    ///
860    /// * `connect_timeout_seconds` - Connection timeout in seconds
861    /// * `request_timeout_seconds` - Request timeout in seconds
862    ///
863    /// # Returns
864    ///
865    /// The updated configuration instance (for method chaining).
866    #[must_use]
867    pub fn with_timeouts(
868        mut self,
869        connect_timeout_seconds: u64,
870        request_timeout_seconds: u64,
871    ) -> Self {
872        self.connect_timeout = connect_timeout_seconds;
873        self.request_timeout = request_timeout_seconds;
874        self
875    }
876
877    /// Get ARC pointer to API ID for sharing across threads
878    #[must_use]
879    pub fn api_id_arc(&self) -> Arc<SecretString> {
880        self.credentials.api_id_ptr()
881    }
882
883    /// Get ARC pointer to API key for sharing across threads
884    #[must_use]
885    pub fn api_key_arc(&self) -> Arc<SecretString> {
886        self.credentials.api_key_ptr()
887    }
888
889    /// Set the HTTP/HTTPS proxy URL.
890    ///
891    /// Configures an HTTP or HTTPS proxy for all requests. The proxy URL should include
892    /// the protocol (http:// or https://). Credentials can be embedded in the URL or
893    /// set separately using `with_proxy_auth()`.
894    ///
895    /// # Arguments
896    ///
897    /// * `proxy_url` - The proxy URL (e.g., "<http://proxy.example.com:8080>")
898    ///
899    /// # Returns
900    ///
901    /// The updated configuration instance (for method chaining).
902    ///
903    /// # Examples
904    ///
905    /// ```no_run
906    /// use veracode_platform::VeracodeConfig;
907    ///
908    /// // Without authentication
909    /// let config = VeracodeConfig::new("api_id", "api_key")
910    ///     .with_proxy("http://proxy.example.com:8080");
911    ///
912    /// // With embedded credentials (less secure)
913    /// let config = VeracodeConfig::new("api_id", "api_key")
914    ///     .with_proxy("http://user:pass@proxy.example.com:8080");
915    /// ```
916    #[must_use]
917    pub fn with_proxy(mut self, proxy_url: impl Into<String>) -> Self {
918        self.proxy_url = Some(proxy_url.into());
919        self
920    }
921
922    /// Set proxy authentication credentials.
923    ///
924    /// Configures username and password for proxy authentication using HTTP Basic Auth.
925    /// This is more secure than embedding credentials in the proxy URL as the credentials
926    /// are stored using SecretString and properly redacted in debug output.
927    ///
928    /// # Arguments
929    ///
930    /// * `username` - The proxy username
931    /// * `password` - The proxy password
932    ///
933    /// # Returns
934    ///
935    /// The updated configuration instance (for method chaining).
936    ///
937    /// # Examples
938    ///
939    /// ```no_run
940    /// use veracode_platform::VeracodeConfig;
941    ///
942    /// let config = VeracodeConfig::new("api_id", "api_key")
943    ///     .with_proxy("http://proxy.example.com:8080")
944    ///     .with_proxy_auth("username", "password");
945    /// ```
946    #[must_use]
947    pub fn with_proxy_auth(
948        mut self,
949        username: impl Into<String>,
950        password: impl Into<String>,
951    ) -> Self {
952        self.proxy_username = Some(SecretString::new(username.into().into()));
953        self.proxy_password = Some(SecretString::new(password.into().into()));
954        self
955    }
956}
957
958#[cfg(test)]
959mod tests {
960    use super::*;
961
962    #[test]
963    fn test_config_creation() {
964        let config = VeracodeConfig::new("test_api_id", "test_api_key");
965
966        assert_eq!(config.credentials.expose_api_id(), "test_api_id");
967        assert_eq!(config.credentials.expose_api_key(), "test_api_key");
968        assert_eq!(config.base_url, "https://api.veracode.com");
969        assert_eq!(config.rest_base_url, "https://api.veracode.com");
970        assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.com");
971        assert_eq!(config.region, VeracodeRegion::Commercial);
972        assert!(config.validate_certificates); // Default is secure
973        assert_eq!(config.retry_config.max_attempts, 5); // Default retry config
974    }
975
976    #[test]
977    fn test_european_region_config() {
978        let config = VeracodeConfig::new("test_api_id", "test_api_key")
979            .with_region(VeracodeRegion::European);
980
981        assert_eq!(config.base_url, "https://api.veracode.eu");
982        assert_eq!(config.rest_base_url, "https://api.veracode.eu");
983        assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.eu");
984        assert_eq!(config.region, VeracodeRegion::European);
985    }
986
987    #[test]
988    fn test_federal_region_config() {
989        let config =
990            VeracodeConfig::new("test_api_id", "test_api_key").with_region(VeracodeRegion::Federal);
991
992        assert_eq!(config.base_url, "https://api.veracode.us");
993        assert_eq!(config.rest_base_url, "https://api.veracode.us");
994        assert_eq!(config.xml_base_url, "https://analysiscenter.veracode.us");
995        assert_eq!(config.region, VeracodeRegion::Federal);
996    }
997
998    #[test]
999    fn test_certificate_validation_disabled() {
1000        let config = VeracodeConfig::new("test_api_id", "test_api_key")
1001            .with_certificate_validation_disabled();
1002
1003        assert!(!config.validate_certificates);
1004    }
1005
1006    #[test]
1007    fn test_veracode_credentials_debug_redaction() {
1008        let credentials = VeracodeCredentials::new(
1009            "test_api_id_123".to_string(),
1010            "test_api_key_456".to_string(),
1011        );
1012        let debug_output = format!("{credentials:?}");
1013
1014        // Should show structure but redact actual values
1015        assert!(debug_output.contains("VeracodeCredentials"));
1016        assert!(debug_output.contains("[REDACTED]"));
1017
1018        // Should not contain actual credential values
1019        assert!(!debug_output.contains("test_api_id_123"));
1020        assert!(!debug_output.contains("test_api_key_456"));
1021    }
1022
1023    #[test]
1024    fn test_veracode_config_debug_redaction() {
1025        let config = VeracodeConfig::new("test_api_id_123", "test_api_key_456");
1026        let debug_output = format!("{config:?}");
1027
1028        // Should show structure but redact actual values
1029        assert!(debug_output.contains("VeracodeConfig"));
1030        assert!(debug_output.contains("credentials"));
1031        assert!(debug_output.contains("[REDACTED]"));
1032
1033        // Should not contain actual credential values
1034        assert!(!debug_output.contains("test_api_id_123"));
1035        assert!(!debug_output.contains("test_api_key_456"));
1036    }
1037
1038    #[test]
1039    fn test_veracode_credentials_access_methods() {
1040        let credentials = VeracodeCredentials::new(
1041            "test_api_id_123".to_string(),
1042            "test_api_key_456".to_string(),
1043        );
1044
1045        // Test expose methods
1046        assert_eq!(credentials.expose_api_id(), "test_api_id_123");
1047        assert_eq!(credentials.expose_api_key(), "test_api_key_456");
1048    }
1049
1050    #[test]
1051    fn test_veracode_credentials_arc_pointers() {
1052        let credentials = VeracodeCredentials::new(
1053            "test_api_id_123".to_string(),
1054            "test_api_key_456".to_string(),
1055        );
1056
1057        // Test ARC pointer methods
1058        let api_id_arc = credentials.api_id_ptr();
1059        let api_key_arc = credentials.api_key_ptr();
1060
1061        // Should be able to access through ARC
1062        assert_eq!(api_id_arc.expose_secret(), "test_api_id_123");
1063        assert_eq!(api_key_arc.expose_secret(), "test_api_key_456");
1064    }
1065
1066    #[test]
1067    fn test_veracode_credentials_clone() {
1068        let credentials = VeracodeCredentials::new(
1069            "test_api_id_123".to_string(),
1070            "test_api_key_456".to_string(),
1071        );
1072        let cloned_credentials = credentials.clone();
1073
1074        // Both should have the same values
1075        assert_eq!(
1076            credentials.expose_api_id(),
1077            cloned_credentials.expose_api_id()
1078        );
1079        assert_eq!(
1080            credentials.expose_api_key(),
1081            cloned_credentials.expose_api_key()
1082        );
1083    }
1084
1085    #[test]
1086    fn test_config_with_arc_credentials() {
1087        use secrecy::SecretString;
1088        use std::sync::Arc;
1089
1090        let api_id_arc = Arc::new(SecretString::new("test_api_id".into()));
1091        let api_key_arc = Arc::new(SecretString::new("test_api_key".into()));
1092
1093        let config = VeracodeConfig::from_arc_credentials(api_id_arc, api_key_arc);
1094
1095        assert_eq!(config.credentials.expose_api_id(), "test_api_id");
1096        assert_eq!(config.credentials.expose_api_key(), "test_api_key");
1097        assert_eq!(config.region, VeracodeRegion::Commercial);
1098    }
1099
1100    #[test]
1101    fn test_error_display() {
1102        let error = VeracodeError::Authentication("Invalid API key".to_string());
1103        assert_eq!(format!("{error}"), "Authentication error: Invalid API key");
1104    }
1105
1106    #[test]
1107    fn test_error_from_reqwest() {
1108        // Test that we can convert from reqwest errors
1109        // Note: We can't easily create a reqwest::Error for testing,
1110        // so we'll just verify the From trait implementation exists
1111        // by checking that it compiles
1112        fn _test_conversion(error: reqwest::Error) -> VeracodeError {
1113            VeracodeError::from(error)
1114        }
1115
1116        // If this compiles, the From trait is implemented correctly
1117        // Test passes if no panic occurs
1118    }
1119
1120    #[test]
1121    fn test_retry_config_default() {
1122        let config = RetryConfig::default();
1123        assert_eq!(config.max_attempts, 5);
1124        assert_eq!(config.initial_delay_ms, 1000);
1125        assert_eq!(config.max_delay_ms, 30000);
1126        assert_eq!(config.backoff_multiplier, 2.0);
1127        assert_eq!(config.max_total_delay_ms, 300000);
1128        assert!(config.jitter_enabled); // Jitter should be enabled by default
1129    }
1130
1131    #[test]
1132    fn test_retry_config_builder() {
1133        let config = RetryConfig::new()
1134            .with_max_attempts(5)
1135            .with_initial_delay(500)
1136            .with_max_delay(60000)
1137            .with_backoff_multiplier(1.5)
1138            .with_max_total_delay(600000);
1139
1140        assert_eq!(config.max_attempts, 5);
1141        assert_eq!(config.initial_delay_ms, 500);
1142        assert_eq!(config.max_delay_ms, 60000);
1143        assert_eq!(config.backoff_multiplier, 1.5);
1144        assert_eq!(config.max_total_delay_ms, 600000);
1145    }
1146
1147    #[test]
1148    fn test_retry_config_calculate_delay() {
1149        let config = RetryConfig::new()
1150            .with_initial_delay(1000)
1151            .with_backoff_multiplier(2.0)
1152            .with_max_delay(10000)
1153            .with_jitter_disabled(); // Disable jitter for predictable testing
1154
1155        // Test exponential backoff calculation
1156        assert_eq!(config.calculate_delay(0).as_millis(), 0); // No delay for attempt 0
1157        assert_eq!(config.calculate_delay(1).as_millis(), 1000); // First retry: 1000ms
1158        assert_eq!(config.calculate_delay(2).as_millis(), 2000); // Second retry: 2000ms
1159        assert_eq!(config.calculate_delay(3).as_millis(), 4000); // Third retry: 4000ms
1160        assert_eq!(config.calculate_delay(4).as_millis(), 8000); // Fourth retry: 8000ms
1161        assert_eq!(config.calculate_delay(5).as_millis(), 10000); // Fifth retry: capped at max_delay
1162    }
1163
1164    #[test]
1165    fn test_retry_config_is_retryable_error() {
1166        let config = RetryConfig::new();
1167
1168        // Test retryable errors
1169        assert!(
1170            config.is_retryable_error(&VeracodeError::InvalidResponse("temp error".to_string()))
1171        );
1172
1173        // Test non-retryable errors
1174        assert!(!config.is_retryable_error(&VeracodeError::Authentication("bad auth".to_string())));
1175        assert!(!config.is_retryable_error(&VeracodeError::Serialization(
1176            serde_json::from_str::<i32>("invalid").unwrap_err()
1177        )));
1178        assert!(
1179            !config.is_retryable_error(&VeracodeError::InvalidConfig("bad config".to_string()))
1180        );
1181        assert!(!config.is_retryable_error(&VeracodeError::NotFound("not found".to_string())));
1182        assert!(
1183            !config.is_retryable_error(&VeracodeError::RetryExhausted("exhausted".to_string()))
1184        );
1185    }
1186
1187    #[test]
1188    fn test_veracode_config_with_retry_config() {
1189        let retry_config = RetryConfig::new().with_max_attempts(5);
1190        let config =
1191            VeracodeConfig::new("test_api_id", "test_api_key").with_retry_config(retry_config);
1192
1193        assert_eq!(config.retry_config.max_attempts, 5);
1194    }
1195
1196    #[test]
1197    fn test_veracode_config_with_retries_disabled() {
1198        let config = VeracodeConfig::new("test_api_id", "test_api_key").with_retries_disabled();
1199
1200        assert_eq!(config.retry_config.max_attempts, 0);
1201    }
1202
1203    #[test]
1204    fn test_timeout_configuration() {
1205        let config = VeracodeConfig::new("test_api_id", "test_api_key");
1206
1207        // Test default values
1208        assert_eq!(config.connect_timeout, 30);
1209        assert_eq!(config.request_timeout, 300);
1210    }
1211
1212    #[test]
1213    fn test_with_connect_timeout() {
1214        let config = VeracodeConfig::new("test_api_id", "test_api_key").with_connect_timeout(60);
1215
1216        assert_eq!(config.connect_timeout, 60);
1217        assert_eq!(config.request_timeout, 300); // Should remain default
1218    }
1219
1220    #[test]
1221    fn test_with_request_timeout() {
1222        let config = VeracodeConfig::new("test_api_id", "test_api_key").with_request_timeout(600);
1223
1224        assert_eq!(config.connect_timeout, 30); // Should remain default
1225        assert_eq!(config.request_timeout, 600);
1226    }
1227
1228    #[test]
1229    fn test_with_timeouts() {
1230        let config = VeracodeConfig::new("test_api_id", "test_api_key").with_timeouts(120, 1800);
1231
1232        assert_eq!(config.connect_timeout, 120);
1233        assert_eq!(config.request_timeout, 1800);
1234    }
1235
1236    #[test]
1237    fn test_timeout_configuration_chaining() {
1238        let config = VeracodeConfig::new("test_api_id", "test_api_key")
1239            .with_region(VeracodeRegion::European)
1240            .with_connect_timeout(45)
1241            .with_request_timeout(900)
1242            .with_retries_disabled();
1243
1244        assert_eq!(config.region, VeracodeRegion::European);
1245        assert_eq!(config.connect_timeout, 45);
1246        assert_eq!(config.request_timeout, 900);
1247        assert_eq!(config.retry_config.max_attempts, 0);
1248    }
1249
1250    #[test]
1251    fn test_retry_exhausted_error_display() {
1252        let error = VeracodeError::RetryExhausted("Failed after 3 attempts".to_string());
1253        assert_eq!(
1254            format!("{error}"),
1255            "Retry attempts exhausted: Failed after 3 attempts"
1256        );
1257    }
1258
1259    #[test]
1260    fn test_rate_limited_error_display_with_retry_after() {
1261        let error = VeracodeError::RateLimited {
1262            retry_after_seconds: Some(60),
1263            message: "Too Many Requests".to_string(),
1264        };
1265        assert_eq!(
1266            format!("{error}"),
1267            "Rate limit exceeded: Too Many Requests (retry after 60s)"
1268        );
1269    }
1270
1271    #[test]
1272    fn test_rate_limited_error_display_without_retry_after() {
1273        let error = VeracodeError::RateLimited {
1274            retry_after_seconds: None,
1275            message: "Too Many Requests".to_string(),
1276        };
1277        assert_eq!(format!("{error}"), "Rate limit exceeded: Too Many Requests");
1278    }
1279
1280    #[test]
1281    fn test_rate_limited_error_is_retryable() {
1282        let config = RetryConfig::new();
1283        let error = VeracodeError::RateLimited {
1284            retry_after_seconds: Some(60),
1285            message: "Rate limit exceeded".to_string(),
1286        };
1287        assert!(config.is_retryable_error(&error));
1288    }
1289
1290    #[test]
1291    fn test_calculate_rate_limit_delay_with_retry_after() {
1292        let config = RetryConfig::new();
1293        let delay = config.calculate_rate_limit_delay(Some(30));
1294        assert_eq!(delay.as_secs(), 30);
1295    }
1296
1297    #[test]
1298    fn test_calculate_rate_limit_delay_without_retry_after() {
1299        let config = RetryConfig::new();
1300        let delay = config.calculate_rate_limit_delay(None);
1301
1302        // Should be somewhere between buffer (5s) and 60 + buffer (65s)
1303        // depending on current second within the minute
1304        assert!(delay.as_secs() >= 5);
1305        assert!(delay.as_secs() <= 65);
1306    }
1307
1308    #[test]
1309    fn test_rate_limit_config_defaults() {
1310        let config = RetryConfig::default();
1311        assert_eq!(config.rate_limit_buffer_seconds, 5);
1312        assert_eq!(config.rate_limit_max_attempts, 1);
1313    }
1314
1315    #[test]
1316    fn test_rate_limit_config_builders() {
1317        let config = RetryConfig::new()
1318            .with_rate_limit_buffer(10)
1319            .with_rate_limit_max_attempts(2);
1320
1321        assert_eq!(config.rate_limit_buffer_seconds, 10);
1322        assert_eq!(config.rate_limit_max_attempts, 2);
1323    }
1324
1325    #[test]
1326    fn test_rate_limit_delay_uses_buffer() {
1327        let config = RetryConfig::new().with_rate_limit_buffer(15);
1328        let delay = config.calculate_rate_limit_delay(None);
1329
1330        // The delay should include our custom 15s buffer
1331        assert!(delay.as_secs() >= 15);
1332        assert!(delay.as_secs() <= 75); // 60 + 15
1333    }
1334
1335    #[test]
1336    fn test_jitter_disabled() {
1337        let config = RetryConfig::new().with_jitter_disabled();
1338        assert!(!config.jitter_enabled);
1339
1340        // With jitter disabled, delays should be consistent
1341        let delay1 = config.calculate_delay(2);
1342        let delay2 = config.calculate_delay(2);
1343        assert_eq!(delay1, delay2);
1344    }
1345
1346    #[test]
1347    fn test_jitter_enabled() {
1348        let config = RetryConfig::new(); // Jitter enabled by default
1349        assert!(config.jitter_enabled);
1350
1351        // With jitter enabled, delays may vary (though they might occasionally be the same)
1352        let base_delay = config.initial_delay_ms;
1353        let delay = config.calculate_delay(1);
1354
1355        // The delay should be within the expected range (ยฑ25% jitter)
1356        let min_expected = (base_delay as f64 * 0.75) as u64;
1357        let max_expected = (base_delay as f64 * 1.25) as u64;
1358
1359        assert!(delay.as_millis() >= min_expected as u128);
1360        assert!(delay.as_millis() <= max_expected as u128);
1361    }
1362}