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