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))
295 .round() as u64;
296
297 let mut capped_delay = delay_ms.min(self.max_delay_ms);
298
299 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 #[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 Duration::from_secs(seconds)
326 } else {
327 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 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 #[must_use]
345 pub fn is_retryable_error(&self, error: &VeracodeError) -> bool {
346 match error {
347 VeracodeError::Http(reqwest_error) => {
348 if reqwest_error.is_timeout()
350 || reqwest_error.is_connect()
351 || reqwest_error.is_request()
352 {
353 return true;
354 }
355
356 if let Some(status) = reqwest_error.status() {
358 match status.as_u16() {
359 429 => true,
361 502..=504 => true,
363 500..=599 => true,
365 _ => false,
367 }
368 } else {
369 true
371 }
372 }
373 VeracodeError::Authentication(_)
375 | VeracodeError::Serialization(_)
376 | VeracodeError::Validation(_)
377 | VeracodeError::InvalidConfig(_) => false,
378 VeracodeError::InvalidResponse(_) => true,
380 VeracodeError::NotFound(_) => false,
382 VeracodeError::RetryExhausted(_) => false,
384 VeracodeError::RateLimited { .. } => true,
386 }
387 }
388}
389
390#[derive(Debug)]
395#[must_use = "Need to handle all error enum types."]
396pub enum VeracodeError {
397 Http(ReqwestError),
399 Serialization(serde_json::Error),
401 Authentication(String),
403 InvalidResponse(String),
405 InvalidConfig(String),
407 NotFound(String),
409 RetryExhausted(String),
411 RateLimited {
413 retry_after_seconds: Option<u64>,
415 message: String,
417 },
418 Validation(validation::ValidationError),
420}
421
422impl VeracodeClient {
423 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 #[must_use]
437 pub fn applications_api(&self) -> &Self {
438 self
439 }
440
441 #[must_use]
444 pub fn sandbox_api(&self) -> SandboxApi<'_> {
445 SandboxApi::new(self)
446 }
447
448 #[must_use]
451 pub fn identity_api(&self) -> IdentityApi<'_> {
452 IdentityApi::new(self)
453 }
454
455 #[must_use]
458 pub fn pipeline_api(&self) -> PipelineApi {
459 PipelineApi::new(self.clone())
460 }
461
462 #[must_use]
465 pub fn policy_api(&self) -> PolicyApi<'_> {
466 PolicyApi::new(self)
467 }
468
469 #[must_use]
472 pub fn findings_api(&self) -> FindingsApi {
473 FindingsApi::new(self.clone())
474 }
475
476 #[must_use]
479 pub fn reporting_api(&self) -> reporting::ReportingApi {
480 reporting::ReportingApi::new(self.clone())
481 }
482
483 pub fn scan_api(&self) -> Result<ScanApi, VeracodeError> {
490 let xml_client = Self::new_xml_client(self.config().clone())?;
492 Ok(ScanApi::new(xml_client))
493 }
494
495 pub fn build_api(&self) -> Result<build::BuildApi, VeracodeError> {
502 let xml_client = Self::new_xml_client(self.config().clone())?;
504 Ok(build::BuildApi::new(xml_client))
505 }
506
507 #[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#[derive(Clone)]
567pub struct VeracodeCredentials {
568 api_id: Arc<SecretString>,
570 api_key: Arc<SecretString>,
572}
573
574impl VeracodeCredentials {
575 #[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 #[must_use]
591 pub fn api_id_ptr(&self) -> Arc<SecretString> {
592 Arc::clone(&self.api_id)
593 }
594
595 #[must_use]
602 pub fn api_key_ptr(&self) -> Arc<SecretString> {
603 Arc::clone(&self.api_key)
604 }
605
606 #[must_use]
611 pub fn expose_api_id(&self) -> &str {
612 self.api_id.expose_secret()
613 }
614
615 #[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#[derive(Clone)]
641pub struct VeracodeConfig {
642 pub credentials: VeracodeCredentials,
644 pub base_url: String,
646 pub rest_base_url: String,
648 pub xml_base_url: String,
650 pub region: VeracodeRegion,
652 pub validate_certificates: bool,
654 pub retry_config: RetryConfig,
656 pub connect_timeout: u64,
658 pub request_timeout: u64,
660 pub proxy_url: Option<String>,
662 pub proxy_username: Option<SecretString>,
664 pub proxy_password: Option<SecretString>,
666}
667
668impl std::fmt::Debug for VeracodeConfig {
670 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
671 let proxy_url_redacted = self.proxy_url.as_ref().map(|url| {
673 if url.contains('@') {
674 if let Some(at_pos) = url.rfind('@') {
676 if let Some(proto_end) = url.find("://") {
677 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
715const 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#[derive(Debug, Clone, Copy, PartialEq)]
728pub enum VeracodeRegion {
729 Commercial,
731 European,
733 Federal,
735}
736
737impl VeracodeConfig {
738 #[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, retry_config: RetryConfig::default(),
764 connect_timeout: 30, request_timeout: 300, proxy_url: None,
767 proxy_username: None,
768 proxy_password: None,
769 }
770 }
771
772 #[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 #[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(); self
821 }
822
823 #[must_use]
832 pub fn with_certificate_validation_disabled(mut self) -> Self {
833 self.validate_certificates = false;
834 self
835 }
836
837 #[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 #[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 #[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 #[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 #[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 #[must_use]
929 pub fn api_id_arc(&self) -> Arc<SecretString> {
930 self.credentials.api_id_ptr()
931 }
932
933 #[must_use]
935 pub fn api_key_arc(&self) -> Arc<SecretString> {
936 self.credentials.api_key_ptr()
937 }
938
939 #[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 #[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); assert_eq!(config.retry_config.max_attempts, 5); }
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 assert!(debug_output.contains("VeracodeCredentials"));
1067 assert!(debug_output.contains("[REDACTED]"));
1068
1069 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 assert!(debug_output.contains("VeracodeConfig"));
1081 assert!(debug_output.contains("credentials"));
1082 assert!(debug_output.contains("[REDACTED]"));
1083
1084 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 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 let api_id_arc = credentials.api_id_ptr();
1110 let api_key_arc = credentials.api_key_ptr();
1111
1112 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 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 fn _test_conversion(error: reqwest::Error) -> VeracodeError {
1164 VeracodeError::from(error)
1165 }
1166
1167 }
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); }
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(); 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); }
1214
1215 #[test]
1216 fn test_retry_config_is_retryable_error() {
1217 let config = RetryConfig::new();
1218
1219 assert!(
1221 config.is_retryable_error(&VeracodeError::InvalidResponse("temp error".to_string()))
1222 );
1223
1224 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 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); }
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); 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)] 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 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)] 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 assert!(delay.as_secs() >= 15);
1385 assert!(delay.as_secs() <= 75); }
1387
1388 #[test]
1389 fn test_jitter_disabled() {
1390 let config = RetryConfig::new().with_jitter_disabled();
1391 assert!(!config.jitter_enabled);
1392
1393 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(); assert!(config.jitter_enabled);
1403
1404 let base_delay = config.initial_delay_ms;
1406 let delay = config.calculate_delay(1);
1407
1408 #[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}