1pub 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
111pub 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#[derive(Debug, Clone)]
156pub struct RetryConfig {
157 pub max_attempts: u32,
159 pub initial_delay_ms: u64,
161 pub max_delay_ms: u64,
163 pub backoff_multiplier: f64,
165 pub max_total_delay_ms: u64,
167 pub rate_limit_buffer_seconds: u64,
169 pub rate_limit_max_attempts: u32,
171 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, rate_limit_buffer_seconds: 5, rate_limit_max_attempts: 1, jitter_enabled: true, }
187 }
188}
189
190impl RetryConfig {
191 #[must_use]
193 pub fn new() -> Self {
194 Self::default()
195 }
196
197 #[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 #[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 #[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 #[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 #[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 #[must_use]
234 pub fn with_backoff_multiplier(mut self, multiplier: f64) -> Self {
235 self.backoff_multiplier = multiplier;
236 self
237 }
238
239 #[must_use]
241 pub fn with_exponential_backoff(mut self, multiplier: f64) -> Self {
242 self.backoff_multiplier = multiplier;
243 self
244 }
245
246 #[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 #[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 #[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 #[must_use]
273 pub fn with_jitter_disabled(mut self) -> Self {
274 self.jitter_enabled = false;
275 self
276 }
277
278 #[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 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 #[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 Duration::from_secs(seconds)
325 } else {
326 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 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 #[must_use]
344 pub fn is_retryable_error(&self, error: &VeracodeError) -> bool {
345 match error {
346 VeracodeError::Http(reqwest_error) => {
347 if reqwest_error.is_timeout()
349 || reqwest_error.is_connect()
350 || reqwest_error.is_request()
351 {
352 return true;
353 }
354
355 if let Some(status) = reqwest_error.status() {
357 match status.as_u16() {
358 429 => true,
360 502..=504 => true,
362 500..=599 => true,
364 _ => false,
366 }
367 } else {
368 true
370 }
371 }
372 VeracodeError::Authentication(_)
374 | VeracodeError::Serialization(_)
375 | VeracodeError::Validation(_)
376 | VeracodeError::InvalidConfig(_) => false,
377 VeracodeError::InvalidResponse(_) => true,
379 VeracodeError::NotFound(_) => false,
381 VeracodeError::RetryExhausted(_) => false,
383 VeracodeError::RateLimited { .. } => true,
385 }
386 }
387}
388
389#[derive(Debug)]
394#[must_use = "Need to handle all error enum types."]
395pub enum VeracodeError {
396 Http(ReqwestError),
398 Serialization(serde_json::Error),
400 Authentication(String),
402 InvalidResponse(String),
404 InvalidConfig(String),
406 NotFound(String),
408 RetryExhausted(String),
410 RateLimited {
412 retry_after_seconds: Option<u64>,
414 message: String,
416 },
417 Validation(validation::ValidationError),
419}
420
421impl VeracodeClient {
422 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 #[must_use]
436 pub fn applications_api(&self) -> &Self {
437 self
438 }
439
440 #[must_use]
443 pub fn sandbox_api(&self) -> SandboxApi<'_> {
444 SandboxApi::new(self)
445 }
446
447 #[must_use]
450 pub fn identity_api(&self) -> IdentityApi<'_> {
451 IdentityApi::new(self)
452 }
453
454 #[must_use]
457 pub fn pipeline_api(&self) -> PipelineApi {
458 PipelineApi::new(self.clone())
459 }
460
461 #[must_use]
464 pub fn policy_api(&self) -> PolicyApi<'_> {
465 PolicyApi::new(self)
466 }
467
468 #[must_use]
471 pub fn findings_api(&self) -> FindingsApi {
472 FindingsApi::new(self.clone())
473 }
474
475 #[must_use]
478 pub fn reporting_api(&self) -> reporting::ReportingApi {
479 reporting::ReportingApi::new(self.clone())
480 }
481
482 pub fn scan_api(&self) -> Result<ScanApi, VeracodeError> {
489 let xml_client = Self::new_xml_client(self.config().clone())?;
491 Ok(ScanApi::new(xml_client))
492 }
493
494 pub fn build_api(&self) -> Result<build::BuildApi, VeracodeError> {
501 let xml_client = Self::new_xml_client(self.config().clone())?;
503 Ok(build::BuildApi::new(xml_client))
504 }
505
506 #[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#[derive(Clone)]
566pub struct VeracodeCredentials {
567 api_id: Arc<SecretString>,
569 api_key: Arc<SecretString>,
571}
572
573impl VeracodeCredentials {
574 #[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 #[must_use]
590 pub fn api_id_ptr(&self) -> Arc<SecretString> {
591 Arc::clone(&self.api_id)
592 }
593
594 #[must_use]
601 pub fn api_key_ptr(&self) -> Arc<SecretString> {
602 Arc::clone(&self.api_key)
603 }
604
605 #[must_use]
610 pub fn expose_api_id(&self) -> &str {
611 self.api_id.expose_secret()
612 }
613
614 #[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#[derive(Clone)]
640pub struct VeracodeConfig {
641 pub credentials: VeracodeCredentials,
643 pub base_url: String,
645 pub rest_base_url: String,
647 pub xml_base_url: String,
649 pub region: VeracodeRegion,
651 pub validate_certificates: bool,
653 pub retry_config: RetryConfig,
655 pub connect_timeout: u64,
657 pub request_timeout: u64,
659 pub proxy_url: Option<String>,
661 pub proxy_username: Option<SecretString>,
663 pub proxy_password: Option<SecretString>,
665}
666
667impl std::fmt::Debug for VeracodeConfig {
669 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
670 let proxy_url_redacted = self.proxy_url.as_ref().map(|url| {
672 if url.contains('@') {
673 if let Some(at_pos) = url.rfind('@') {
675 if let Some(proto_end) = url.find("://") {
676 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
714const 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#[derive(Debug, Clone, Copy, PartialEq)]
727pub enum VeracodeRegion {
728 Commercial,
730 European,
732 Federal,
734}
735
736impl VeracodeConfig {
737 #[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, retry_config: RetryConfig::default(),
763 connect_timeout: 30, request_timeout: 300, proxy_url: None,
766 proxy_username: None,
767 proxy_password: None,
768 }
769 }
770
771 #[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 #[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(); self
820 }
821
822 #[must_use]
831 pub fn with_certificate_validation_disabled(mut self) -> Self {
832 self.validate_certificates = false;
833 self
834 }
835
836 #[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 #[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 #[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 #[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 #[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 #[must_use]
928 pub fn api_id_arc(&self) -> Arc<SecretString> {
929 self.credentials.api_id_ptr()
930 }
931
932 #[must_use]
934 pub fn api_key_arc(&self) -> Arc<SecretString> {
935 self.credentials.api_key_ptr()
936 }
937
938 #[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 #[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); assert_eq!(config.retry_config.max_attempts, 5); }
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 assert!(debug_output.contains("VeracodeCredentials"));
1066 assert!(debug_output.contains("[REDACTED]"));
1067
1068 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 assert!(debug_output.contains("VeracodeConfig"));
1080 assert!(debug_output.contains("credentials"));
1081 assert!(debug_output.contains("[REDACTED]"));
1082
1083 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 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 let api_id_arc = credentials.api_id_ptr();
1109 let api_key_arc = credentials.api_key_ptr();
1110
1111 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 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 fn _test_conversion(error: reqwest::Error) -> VeracodeError {
1163 VeracodeError::from(error)
1164 }
1165
1166 }
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); }
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(); assert_eq!(config.calculate_delay(0).as_millis(), 0); assert_eq!(config.calculate_delay(1).as_millis(), 1000); assert_eq!(config.calculate_delay(2).as_millis(), 2000); assert_eq!(config.calculate_delay(3).as_millis(), 4000); assert_eq!(config.calculate_delay(4).as_millis(), 8000); assert_eq!(config.calculate_delay(5).as_millis(), 10000); }
1213
1214 #[test]
1215 fn test_retry_config_is_retryable_error() {
1216 let config = RetryConfig::new();
1217
1218 assert!(
1220 config.is_retryable_error(&VeracodeError::InvalidResponse("temp error".to_string()))
1221 );
1222
1223 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 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); }
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); 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 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 assert!(delay.as_secs() >= 15);
1382 assert!(delay.as_secs() <= 75); }
1384
1385 #[test]
1386 fn test_jitter_disabled() {
1387 let config = RetryConfig::new().with_jitter_disabled();
1388 assert!(!config.jitter_enabled);
1389
1390 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(); assert!(config.jitter_enabled);
1400
1401 let base_delay = config.initial_delay_ms;
1403 let delay = config.calculate_delay(1);
1404
1405 #[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}