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