1use bytes::Bytes;
7use hex;
8use hmac::{Hmac, Mac};
9use log::{info, warn};
10use reqwest::{Body, Client, multipart};
11use secrecy::ExposeSecret;
12use serde::Serialize;
13use sha2::Sha256;
14use std::borrow::Cow;
15use std::collections::HashMap;
16use std::fs::File;
17use std::io::{Read, Seek, SeekFrom};
18use std::sync::Arc;
19use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
20use tokio::fs::File as TokioFile;
21use url::Url;
22
23use crate::json_validator::{MAX_JSON_DEPTH, validate_json_depth};
24use crate::{VeracodeConfig, VeracodeError};
25
26type HmacSha256 = Hmac<Sha256>;
28
29const INVALID_URL_MSG: &str = "Invalid URL";
31const INVALID_API_KEY_MSG: &str = "Invalid API key format - must be hex string";
32const INVALID_NONCE_MSG: &str = "Invalid nonce format";
33const HMAC_CREATION_FAILED_MSG: &str = "Failed to create HMAC";
34
35#[derive(Clone)]
40pub struct VeracodeClient {
41 config: VeracodeConfig,
42 client: Client,
43}
44
45impl VeracodeClient {
46 fn build_url_with_params(&self, endpoint: &str, query_params: &[(&str, &str)]) -> String {
48 let estimated_capacity = self
50 .config
51 .base_url
52 .len()
53 .saturating_add(endpoint.len())
54 .saturating_add(query_params.len().saturating_mul(32)); let mut url = String::with_capacity(estimated_capacity);
57 url.push_str(&self.config.base_url);
58 url.push_str(endpoint);
59
60 if !query_params.is_empty() {
61 url.push('?');
62 for (i, (key, value)) in query_params.iter().enumerate() {
63 if i > 0 {
64 url.push('&');
65 }
66 url.push_str(&urlencoding::encode(key));
67 url.push('=');
68 url.push_str(&urlencoding::encode(value));
69 }
70 }
71
72 url
73 }
74
75 pub fn new(config: VeracodeConfig) -> Result<Self, VeracodeError> {
90 let mut client_builder = Client::builder();
91
92 if !config.validate_certificates {
94 client_builder = client_builder
95 .danger_accept_invalid_certs(true)
96 .danger_accept_invalid_hostnames(true);
97 }
98
99 client_builder = client_builder
101 .connect_timeout(Duration::from_secs(config.connect_timeout))
102 .timeout(Duration::from_secs(config.request_timeout));
103
104 if let Some(proxy_url) = &config.proxy_url {
106 let mut proxy = reqwest::Proxy::all(proxy_url)
107 .map_err(|e| VeracodeError::InvalidConfig(format!("Invalid proxy URL: {e}")))?;
108
109 if let (Some(username), Some(password)) =
111 (&config.proxy_username, &config.proxy_password)
112 {
113 proxy = proxy.basic_auth(username.expose_secret(), password.expose_secret());
114 }
115
116 client_builder = client_builder.proxy(proxy);
117 }
118
119 let client = client_builder.build().map_err(VeracodeError::Http)?;
120 Ok(Self { config, client })
121 }
122
123 #[must_use]
129 pub fn new_xml_variant(&self) -> Self {
130 let mut xml_config = self.config.clone();
131 xml_config.base_url = xml_config.xml_base_url.clone();
132 Self {
133 config: xml_config,
134 client: self.client.clone(),
135 }
136 }
137
138 #[must_use]
140 pub fn base_url(&self) -> &str {
141 &self.config.base_url
142 }
143
144 #[must_use]
146 pub fn config(&self) -> &VeracodeConfig {
147 &self.config
148 }
149
150 #[must_use]
152 pub fn client(&self) -> &Client {
153 &self.client
154 }
155
156 async fn execute_with_retry<F>(
182 &self,
183 request_builder: F,
184 operation_name: Cow<'_, str>,
185 ) -> Result<reqwest::Response, VeracodeError>
186 where
187 F: Fn() -> reqwest::RequestBuilder,
188 {
189 let retry_config = &self.config.retry_config;
190 let start_time = Instant::now();
191 let mut total_delay = std::time::Duration::from_millis(0);
192
193 if retry_config.max_attempts == 0 {
195 return match request_builder().send().await {
196 Ok(response) => Ok(response),
197 Err(e) => Err(VeracodeError::Http(e)),
198 };
199 }
200
201 let mut last_error = None;
202 let mut rate_limit_attempts: u32 = 0;
203
204 for attempt in 1..=retry_config.max_attempts.saturating_add(1) {
205 match request_builder().send().await {
207 Ok(response) => {
208 if response.status().as_u16() == 429 {
210 let retry_after_seconds = response
212 .headers()
213 .get("retry-after")
214 .and_then(|h| h.to_str().ok())
215 .and_then(|s| s.parse::<u64>().ok());
216
217 let message = "HTTP 429: Rate limit exceeded".to_string();
218 let veracode_error = VeracodeError::RateLimited {
219 retry_after_seconds,
220 message,
221 };
222
223 rate_limit_attempts = rate_limit_attempts.saturating_add(1);
225
226 if attempt > retry_config.max_attempts
228 || rate_limit_attempts > retry_config.rate_limit_max_attempts
229 {
230 last_error = Some(veracode_error);
231 break;
232 }
233
234 let delay = retry_config.calculate_rate_limit_delay(retry_after_seconds);
236 total_delay = total_delay.saturating_add(delay);
237
238 if total_delay.as_millis() > retry_config.max_total_delay_ms as u128 {
240 let msg = format!(
241 "{} exceeded maximum total retry time of {}ms after {} attempts",
242 operation_name, retry_config.max_total_delay_ms, attempt
243 );
244 last_error = Some(VeracodeError::RetryExhausted(msg));
245 break;
246 }
247
248 let wait_time = match retry_after_seconds {
250 Some(seconds) => format!("{seconds}s (from Retry-After header)"),
251 None => format!("{}s (until next minute window)", delay.as_secs()),
252 };
253 warn!(
254 "π¦ {operation_name} rate limited on attempt {attempt}, waiting {wait_time}"
255 );
256
257 tokio::time::sleep(delay).await;
259 last_error = Some(veracode_error);
260 continue;
261 }
262
263 if attempt > 1 {
264 info!("β
{operation_name} succeeded on attempt {attempt}");
266 }
267 return Ok(response);
268 }
269 Err(e) => {
270 let veracode_error = VeracodeError::Http(e);
272
273 if attempt > retry_config.max_attempts
275 || !retry_config.is_retryable_error(&veracode_error)
276 {
277 last_error = Some(veracode_error);
278 break;
279 }
280
281 let delay = retry_config.calculate_delay(attempt);
283 total_delay = total_delay.saturating_add(delay);
284
285 if total_delay.as_millis() > retry_config.max_total_delay_ms as u128 {
287 let msg = format!(
289 "{} exceeded maximum total retry time of {}ms after {} attempts",
290 operation_name, retry_config.max_total_delay_ms, attempt
291 );
292 last_error = Some(VeracodeError::RetryExhausted(msg));
293 break;
294 }
295
296 warn!(
298 "β οΈ {operation_name} failed on attempt {attempt}, retrying in {}ms: {veracode_error}",
299 delay.as_millis()
300 );
301
302 tokio::time::sleep(delay).await;
304 last_error = Some(veracode_error);
305 }
306 }
307 }
308
309 match last_error {
311 Some(error) => {
312 let elapsed = start_time.elapsed();
313 match error {
314 VeracodeError::RetryExhausted(_) => Err(error),
315 VeracodeError::Http(_)
316 | VeracodeError::Serialization(_)
317 | VeracodeError::Authentication(_)
318 | VeracodeError::InvalidResponse(_)
319 | VeracodeError::HttpStatus { .. }
320 | VeracodeError::InvalidConfig(_)
321 | VeracodeError::NotFound(_)
322 | VeracodeError::RateLimited { .. }
323 | VeracodeError::Validation(_) => {
324 let msg = format!(
325 "{} failed after {} attempts over {}ms: {}",
326 operation_name,
327 retry_config.max_attempts.saturating_add(1),
328 elapsed.as_millis(),
329 error
330 );
331 Err(VeracodeError::RetryExhausted(msg))
332 }
333 }
334 }
335 None => {
336 let msg = format!(
337 "{} failed after {} attempts with unknown error",
338 operation_name,
339 retry_config.max_attempts.saturating_add(1)
340 );
341 Err(VeracodeError::RetryExhausted(msg))
342 }
343 }
344 }
345
346 fn generate_hmac_signature(
348 &self,
349 method: &str,
350 url: &str,
351 timestamp: u64,
352 nonce: &str,
353 ) -> Result<String, VeracodeError> {
354 let url_parsed = Url::parse(url)
355 .map_err(|_| VeracodeError::Authentication(INVALID_URL_MSG.to_string()))?;
356
357 let path_and_query = match url_parsed.query() {
358 Some(query) => format!("{}?{}", url_parsed.path(), query),
359 None => url_parsed.path().to_string(),
360 };
361
362 let host = url_parsed.host_str().unwrap_or("");
363
364 let data = format!(
367 "id={}&host={}&url={}&method={}",
368 self.config.credentials.expose_api_id(),
369 host,
370 path_and_query,
371 method
372 );
373
374 let timestamp_str = timestamp.to_string();
375 let ver_str = "vcode_request_version_1";
376
377 let key_bytes = hex::decode(self.config.credentials.expose_api_key())
379 .map_err(|_| VeracodeError::Authentication(INVALID_API_KEY_MSG.to_string()))?;
380
381 let nonce_bytes = hex::decode(nonce)
382 .map_err(|_| VeracodeError::Authentication(INVALID_NONCE_MSG.to_string()))?;
383
384 let mut mac1 = HmacSha256::new_from_slice(&key_bytes)
386 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
387 mac1.update(&nonce_bytes);
388 let hashed_nonce = mac1.finalize().into_bytes();
389
390 let mut mac2 = HmacSha256::new_from_slice(&hashed_nonce)
392 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
393 mac2.update(timestamp_str.as_bytes());
394 let hashed_timestamp = mac2.finalize().into_bytes();
395
396 let mut mac3 = HmacSha256::new_from_slice(&hashed_timestamp)
398 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
399 mac3.update(ver_str.as_bytes());
400 let hashed_ver_str = mac3.finalize().into_bytes();
401
402 let mut mac4 = HmacSha256::new_from_slice(&hashed_ver_str)
404 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
405 mac4.update(data.as_bytes());
406 let signature = mac4.finalize().into_bytes();
407
408 Ok(hex::encode(signature).to_lowercase())
410 }
411
412 pub fn generate_auth_header(&self, method: &str, url: &str) -> Result<String, VeracodeError> {
419 #[allow(clippy::cast_possible_truncation)]
420 let timestamp = SystemTime::now()
421 .duration_since(UNIX_EPOCH)
422 .map_err(|e| VeracodeError::Authentication(format!("System time error: {e}")))?
423 .as_millis() as u64; let nonce_bytes: [u8; 16] = rand::random();
427 let nonce = hex::encode(nonce_bytes);
428
429 let signature = self.generate_hmac_signature(method, url, timestamp, &nonce)?;
430
431 Ok(format!(
432 "VERACODE-HMAC-SHA-256 id={},ts={},nonce={},sig={}",
433 self.config.credentials.expose_api_id(),
434 timestamp,
435 nonce,
436 signature
437 ))
438 }
439
440 pub async fn get(
456 &self,
457 endpoint: &str,
458 query_params: Option<&[(String, String)]>,
459 ) -> Result<reqwest::Response, VeracodeError> {
460 let param_count = query_params.map_or(0, |p| p.len());
462 let estimated_capacity = self
463 .config
464 .base_url
465 .len()
466 .saturating_add(endpoint.len())
467 .saturating_add(param_count.saturating_mul(32));
468 let mut url = String::with_capacity(estimated_capacity);
469 url.push_str(&self.config.base_url);
470 url.push_str(endpoint);
471
472 if let Some(params) = query_params
473 && !params.is_empty()
474 {
475 url.push('?');
476 for (i, (key, value)) in params.iter().enumerate() {
477 if i > 0 {
478 url.push('&');
479 }
480 url.push_str(key);
481 url.push('=');
482 url.push_str(value);
483 }
484 }
485
486 let request_builder = || {
488 let Ok(auth_header) = self.generate_auth_header("GET", &url) else {
490 return self.client.get("invalid://url");
491 };
492
493 self.client
494 .get(&url)
495 .header("Authorization", auth_header)
496 .header("Content-Type", "application/json")
497 };
498
499 let operation_name = if endpoint.len() < 50 {
501 Cow::Owned(format!("GET {endpoint}"))
502 } else {
503 Cow::Borrowed("GET [long endpoint]")
504 };
505 self.execute_with_retry(request_builder, operation_name)
506 .await
507 }
508
509 pub async fn post<T: Serialize>(
525 &self,
526 endpoint: &str,
527 body: Option<&T>,
528 ) -> Result<reqwest::Response, VeracodeError> {
529 let mut url =
530 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
531 url.push_str(&self.config.base_url);
532 url.push_str(endpoint);
533
534 let serialized_body = if let Some(body) = body {
536 Some(serde_json::to_string(body)?)
537 } else {
538 None
539 };
540
541 let request_builder = || {
543 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
545 return self.client.post("invalid://url");
546 };
547
548 let mut request = self
549 .client
550 .post(&url)
551 .header("Authorization", auth_header)
552 .header("Content-Type", "application/json");
553
554 if let Some(ref body_str) = serialized_body {
555 request = request.body(body_str.clone());
556 }
557
558 request
559 };
560
561 let operation_name = if endpoint.len() < 50 {
562 Cow::Owned(format!("POST {endpoint}"))
563 } else {
564 Cow::Borrowed("POST [long endpoint]")
565 };
566 self.execute_with_retry(request_builder, operation_name)
567 .await
568 }
569
570 pub async fn put<T: Serialize>(
586 &self,
587 endpoint: &str,
588 body: Option<&T>,
589 ) -> Result<reqwest::Response, VeracodeError> {
590 let mut url =
591 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
592 url.push_str(&self.config.base_url);
593 url.push_str(endpoint);
594
595 let serialized_body = if let Some(body) = body {
597 Some(serde_json::to_string(body)?)
598 } else {
599 None
600 };
601
602 let request_builder = || {
604 let Ok(auth_header) = self.generate_auth_header("PUT", &url) else {
606 return self.client.put("invalid://url");
607 };
608
609 let mut request = self
610 .client
611 .put(&url)
612 .header("Authorization", auth_header)
613 .header("Content-Type", "application/json");
614
615 if let Some(ref body_str) = serialized_body {
616 request = request.body(body_str.clone());
617 }
618
619 request
620 };
621
622 let operation_name = if endpoint.len() < 50 {
623 Cow::Owned(format!("PUT {endpoint}"))
624 } else {
625 Cow::Borrowed("PUT [long endpoint]")
626 };
627 self.execute_with_retry(request_builder, operation_name)
628 .await
629 }
630
631 pub async fn delete(&self, endpoint: &str) -> Result<reqwest::Response, VeracodeError> {
646 let mut url =
647 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
648 url.push_str(&self.config.base_url);
649 url.push_str(endpoint);
650
651 let request_builder = || {
653 let Ok(auth_header) = self.generate_auth_header("DELETE", &url) else {
655 return self.client.delete("invalid://url");
656 };
657
658 self.client
659 .delete(&url)
660 .header("Authorization", auth_header)
661 .header("Content-Type", "application/json")
662 };
663
664 let operation_name = if endpoint.len() < 50 {
665 Cow::Owned(format!("DELETE {endpoint}"))
666 } else {
667 Cow::Borrowed("DELETE [long endpoint]")
668 };
669 self.execute_with_retry(request_builder, operation_name)
670 .await
671 }
672
673 pub async fn handle_response(
696 response: reqwest::Response,
697 context: &str,
698 ) -> Result<reqwest::Response, VeracodeError> {
699 if !response.status().is_success() {
700 let status = response.status();
701 let status_code = status.as_u16();
702 let url = response.url().to_string();
703 let error_text = response.text().await?;
704
705 return Err(VeracodeError::HttpStatus {
707 status_code,
708 url,
709 message: format!("Failed to {context}: {error_text}"),
710 });
711 }
712 Ok(response)
713 }
714
715 pub async fn get_with_query(
733 &self,
734 endpoint: &str,
735 query_params: Option<Vec<(String, String)>>,
736 ) -> Result<reqwest::Response, VeracodeError> {
737 let query_slice = query_params.as_deref();
738 let response = self.get(endpoint, query_slice).await?;
739 Self::handle_response(response, &format!("GET {endpoint}")).await
740 }
741
742 pub async fn post_with_response<T: Serialize>(
758 &self,
759 endpoint: &str,
760 body: Option<&T>,
761 ) -> Result<reqwest::Response, VeracodeError> {
762 let response = self.post(endpoint, body).await?;
763 Self::handle_response(response, &format!("POST {endpoint}")).await
764 }
765
766 pub async fn put_with_response<T: Serialize>(
782 &self,
783 endpoint: &str,
784 body: Option<&T>,
785 ) -> Result<reqwest::Response, VeracodeError> {
786 let response = self.put(endpoint, body).await?;
787 Self::handle_response(response, &format!("PUT {endpoint}")).await
788 }
789
790 pub async fn delete_with_response(
805 &self,
806 endpoint: &str,
807 ) -> Result<reqwest::Response, VeracodeError> {
808 let response = self.delete(endpoint).await?;
809 Self::handle_response(response, &format!("DELETE {endpoint}")).await
810 }
811
812 pub async fn get_paginated(
832 &self,
833 endpoint: &str,
834 base_query_params: Option<Vec<(String, String)>>,
835 page_size: Option<u32>,
836 ) -> Result<String, VeracodeError> {
837 let size = page_size.unwrap_or(500);
838 let mut page: u32 = 0;
839 let mut all_items = Vec::new();
840 let mut page_info = None;
841
842 loop {
843 let mut query_params = base_query_params.clone().unwrap_or_default();
844 query_params.push(("page".to_string(), page.to_string()));
845 query_params.push(("size".to_string(), size.to_string()));
846
847 let response = self.get_with_query(endpoint, Some(query_params)).await?;
848 let response_text = response.text().await?;
849
850 validate_json_depth(&response_text, MAX_JSON_DEPTH).map_err(|e| {
852 VeracodeError::InvalidResponse(format!("JSON validation failed: {}", e))
853 })?;
854
855 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
857 if let Some(embedded) = json_value.get("_embedded") {
859 if let Some(items_array) =
860 embedded.as_object().and_then(|obj| obj.values().next())
861 && let Some(items) = items_array.as_array()
862 {
863 if items.is_empty() {
864 break; }
866 all_items.extend(items.clone());
867 }
868 } else if let Some(items) = json_value.as_array() {
869 if items.is_empty() {
871 break;
872 }
873 all_items.extend(items.clone());
874 } else {
875 return Ok(response_text);
877 }
878
879 if let Some(page_obj) = json_value.get("page") {
881 page_info = Some(page_obj.clone());
882 if let (Some(current), Some(total)) = (
883 page_obj.get("number").and_then(|n| n.as_u64()),
884 page_obj.get("totalPages").and_then(|n| n.as_u64()),
885 ) && current.saturating_add(1) >= total
886 {
887 break; }
889 }
890 } else {
891 return Ok(response_text);
893 }
894
895 page = page.saturating_add(1);
896
897 if page > 100 {
899 break;
900 }
901 }
902
903 let combined_response = if let Some(page_info) = page_info {
905 serde_json::json!({
907 "_embedded": {
908 "roles": all_items },
910 "page": page_info
911 })
912 } else {
913 serde_json::Value::Array(all_items)
915 };
916
917 Ok(combined_response.to_string())
918 }
919
920 pub async fn get_with_params(
936 &self,
937 endpoint: &str,
938 params: &[(&str, &str)],
939 ) -> Result<reqwest::Response, VeracodeError> {
940 let mut url =
941 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
942 url.push_str(&self.config.base_url);
943 url.push_str(endpoint);
944 let mut request_url =
945 Url::parse(&url).map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
946
947 if !params.is_empty() {
949 let mut query_pairs = request_url.query_pairs_mut();
950 for (key, value) in params {
951 query_pairs.append_pair(key, value);
952 }
953 }
954
955 let auth_header = self.generate_auth_header("GET", request_url.as_str())?;
956
957 let response = self
958 .client
959 .get(request_url)
960 .header("Authorization", auth_header)
961 .header("User-Agent", "Veracode Rust Client")
962 .send()
963 .await?;
964
965 Ok(response)
966 }
967
968 pub async fn post_form(
984 &self,
985 endpoint: &str,
986 params: &[(&str, &str)],
987 ) -> Result<reqwest::Response, VeracodeError> {
988 let mut url =
989 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
990 url.push_str(&self.config.base_url);
991 url.push_str(endpoint);
992
993 let form_data: Vec<(&str, &str)> = params.to_vec();
995
996 let auth_header = self.generate_auth_header("POST", &url)?;
997
998 let response = self
999 .client
1000 .post(&url)
1001 .header("Authorization", auth_header)
1002 .header("User-Agent", "Veracode Rust Client")
1003 .form(&form_data)
1004 .send()
1005 .await?;
1006
1007 Ok(response)
1008 }
1009
1010 pub async fn upload_file_multipart(
1029 &self,
1030 endpoint: &str,
1031 params: HashMap<&str, &str>,
1032 file_field_name: &str,
1033 filename: &str,
1034 file_data: Vec<u8>,
1035 ) -> Result<reqwest::Response, VeracodeError> {
1036 let mut url =
1037 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
1038 url.push_str(&self.config.base_url);
1039 url.push_str(endpoint);
1040
1041 let mut form = multipart::Form::new();
1043
1044 for (key, value) in params {
1046 form = form.text(key.to_string(), value.to_string());
1047 }
1048
1049 let part = multipart::Part::bytes(file_data)
1051 .file_name(filename.to_string())
1052 .mime_str("application/octet-stream")
1053 .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
1054
1055 form = form.part(file_field_name.to_string(), part);
1056
1057 let auth_header = self.generate_auth_header("POST", &url)?;
1058
1059 let response = self
1060 .client
1061 .post(&url)
1062 .header("Authorization", auth_header)
1063 .header("User-Agent", "Veracode Rust Client")
1064 .multipart(form)
1065 .send()
1066 .await?;
1067
1068 Ok(response)
1069 }
1070
1071 pub async fn upload_file_multipart_put(
1090 &self,
1091 url: &str,
1092 file_field_name: &str,
1093 filename: &str,
1094 file_data: Vec<u8>,
1095 additional_headers: Option<HashMap<&str, &str>>,
1096 ) -> Result<reqwest::Response, VeracodeError> {
1097 let part = multipart::Part::bytes(file_data)
1099 .file_name(filename.to_string())
1100 .mime_str("application/octet-stream")
1101 .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
1102
1103 let form = multipart::Form::new().part(file_field_name.to_string(), part);
1104
1105 let auth_header = self.generate_auth_header("PUT", url)?;
1106
1107 let mut request = self
1108 .client
1109 .put(url)
1110 .header("Authorization", auth_header)
1111 .header("User-Agent", "Veracode Rust Client")
1112 .multipart(form);
1113
1114 if let Some(headers) = additional_headers {
1116 for (key, value) in headers {
1117 request = request.header(key, value);
1118 }
1119 }
1120
1121 let response = request.send().await?;
1122 Ok(response)
1123 }
1124
1125 pub async fn upload_file_with_query_params(
1150 &self,
1151 endpoint: &str,
1152 query_params: &[(&str, &str)],
1153 file_field_name: &str,
1154 filename: &str,
1155 file_data: Vec<u8>,
1156 ) -> Result<reqwest::Response, VeracodeError> {
1157 let url = self.build_url_with_params(endpoint, query_params);
1159
1160 let file_data_arc = Arc::new(file_data);
1162
1163 let filename_cow: Cow<str> = if filename.len() < 128 {
1165 Cow::Borrowed(filename)
1166 } else {
1167 Cow::Owned(filename.to_string())
1168 };
1169
1170 let field_name_cow: Cow<str> = if file_field_name.len() < 32 {
1171 Cow::Borrowed(file_field_name)
1172 } else {
1173 Cow::Owned(file_field_name.to_string())
1174 };
1175
1176 let request_builder = || {
1178 let file_data_clone = Arc::clone(&file_data_arc);
1180
1181 let Ok(part) = multipart::Part::bytes((*file_data_clone).clone())
1183 .file_name(filename_cow.to_string())
1184 .mime_str("application/octet-stream")
1185 else {
1186 return self.client.post("invalid://url");
1187 };
1188
1189 let form = multipart::Form::new().part(field_name_cow.to_string(), part);
1190
1191 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1193 return self.client.post("invalid://url");
1194 };
1195
1196 self.client
1197 .post(&url)
1198 .header("Authorization", auth_header)
1199 .header("User-Agent", "Veracode Rust Client")
1200 .multipart(form)
1201 };
1202
1203 let operation_name: Cow<str> = if endpoint.len() < 50 {
1205 Cow::Owned(format!("File Upload POST {endpoint}"))
1206 } else {
1207 Cow::Borrowed("File Upload POST [long endpoint]")
1208 };
1209
1210 self.execute_with_retry(request_builder, operation_name)
1211 .await
1212 }
1213
1214 pub async fn post_with_query_params(
1233 &self,
1234 endpoint: &str,
1235 query_params: &[(&str, &str)],
1236 ) -> Result<reqwest::Response, VeracodeError> {
1237 let url = self.build_url_with_params(endpoint, query_params);
1239
1240 let auth_header = self.generate_auth_header("POST", &url)?;
1241
1242 let response = self
1243 .client
1244 .post(&url)
1245 .header("Authorization", auth_header)
1246 .header("User-Agent", "Veracode Rust Client")
1247 .send()
1248 .await?;
1249
1250 Ok(response)
1251 }
1252
1253 pub async fn get_with_query_params(
1272 &self,
1273 endpoint: &str,
1274 query_params: &[(&str, &str)],
1275 ) -> Result<reqwest::Response, VeracodeError> {
1276 let url = self.build_url_with_params(endpoint, query_params);
1278
1279 let auth_header = self.generate_auth_header("GET", &url)?;
1280
1281 let response = self
1282 .client
1283 .get(&url)
1284 .header("Authorization", auth_header)
1285 .header("User-Agent", "Veracode Rust Client")
1286 .send()
1287 .await?;
1288
1289 Ok(response)
1290 }
1291
1292 pub async fn upload_large_file_chunked<F>(
1314 &self,
1315 endpoint: &str,
1316 query_params: &[(&str, &str)],
1317 file_path: &str,
1318 content_type: Option<&str>,
1319 progress_callback: Option<F>,
1320 ) -> Result<reqwest::Response, VeracodeError>
1321 where
1322 F: Fn(u64, u64, f64) + Send + Sync,
1323 {
1324 let url = self.build_url_with_params(endpoint, query_params);
1326
1327 let mut file = File::open(file_path)
1329 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to open file: {e}")))?;
1330
1331 let file_size = file
1332 .metadata()
1333 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to get file size: {e}")))?
1334 .len();
1335
1336 #[allow(clippy::arithmetic_side_effects)]
1338 const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; if file_size > MAX_FILE_SIZE {
1340 return Err(VeracodeError::InvalidConfig(format!(
1341 "File size ({file_size} bytes) exceeds maximum limit of {MAX_FILE_SIZE} bytes"
1342 )));
1343 }
1344
1345 file.seek(SeekFrom::Start(0))
1347 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to seek file: {e}")))?;
1348
1349 #[allow(clippy::cast_possible_truncation)]
1350 let mut file_data_vec = Vec::with_capacity(file_size as usize);
1351 file.read_to_end(&mut file_data_vec)
1352 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to read file: {e}")))?;
1353
1354 let file_data = Bytes::from(file_data_vec);
1357 let content_type_cow: Cow<str> =
1358 content_type.map_or(Cow::Borrowed("binary/octet-stream"), |ct| {
1359 if ct.len() < 64 {
1360 Cow::Borrowed(ct)
1361 } else {
1362 Cow::Owned(ct.to_string())
1363 }
1364 });
1365
1366 let request_builder = || {
1368 let body_data = file_data.clone();
1370
1371 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1373 return self.client.post("invalid://url");
1374 };
1375
1376 self.client
1377 .post(&url)
1378 .header("Authorization", auth_header)
1379 .header("User-Agent", "Veracode Rust Client")
1380 .header("Content-Type", content_type_cow.as_ref())
1381 .header("Content-Length", file_size.to_string())
1382 .body(body_data)
1383 };
1384
1385 if let Some(ref callback) = progress_callback {
1387 callback(0, file_size, 0.0);
1388 }
1389
1390 let operation_name: Cow<str> = if endpoint.len() < 50 {
1392 Cow::Owned(format!("Large File Upload POST {endpoint}"))
1393 } else {
1394 Cow::Borrowed("Large File Upload POST [long endpoint]")
1395 };
1396
1397 let response = self
1398 .execute_with_retry(request_builder, operation_name)
1399 .await?;
1400
1401 if let Some(callback) = progress_callback {
1403 callback(file_size, file_size, 100.0);
1404 }
1405
1406 Ok(response)
1407 }
1408
1409 pub async fn upload_file_binary(
1433 &self,
1434 endpoint: &str,
1435 query_params: &[(&str, &str)],
1436 file_data: Vec<u8>,
1437 content_type: &str,
1438 ) -> Result<reqwest::Response, VeracodeError> {
1439 let url = self.build_url_with_params(endpoint, query_params);
1441
1442 let file_data = Bytes::from(file_data);
1445 let file_size = file_data.len();
1446
1447 let content_type_cow: Cow<str> = if content_type.len() < 64 {
1449 Cow::Borrowed(content_type)
1450 } else {
1451 Cow::Owned(content_type.to_string())
1452 };
1453
1454 let request_builder = || {
1456 let body_data = file_data.clone();
1458
1459 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1461 return self.client.post("invalid://url");
1462 };
1463
1464 self.client
1465 .post(&url)
1466 .header("Authorization", auth_header)
1467 .header("User-Agent", "Veracode Rust Client")
1468 .header("Content-Type", content_type_cow.as_ref())
1469 .header("Content-Length", file_size.to_string())
1470 .body(body_data)
1471 };
1472
1473 let operation_name: Cow<str> = if endpoint.len() < 50 {
1475 Cow::Owned(format!("Binary File Upload POST {endpoint}"))
1476 } else {
1477 Cow::Borrowed("Binary File Upload POST [long endpoint]")
1478 };
1479
1480 self.execute_with_retry(request_builder, operation_name)
1481 .await
1482 }
1483
1484 pub async fn upload_file_streaming(
1506 &self,
1507 endpoint: &str,
1508 query_params: &[(&str, &str)],
1509 file_path: &str,
1510 file_size: u64,
1511 content_type: &str,
1512 ) -> Result<reqwest::Response, VeracodeError> {
1513 let url = self.build_url_with_params(endpoint, query_params);
1515
1516 let file = TokioFile::open(file_path)
1518 .await
1519 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to open file: {e}")))?;
1520
1521 let stream = tokio_util::io::ReaderStream::new(file);
1523 let body = Body::wrap_stream(stream);
1524
1525 let auth_header = self.generate_auth_header("POST", &url)?;
1527
1528 let response = self
1531 .client
1532 .post(&url)
1533 .header("Authorization", auth_header)
1534 .header("User-Agent", "Veracode Rust Client")
1535 .header("Content-Type", content_type)
1536 .header("Content-Length", file_size.to_string())
1537 .body(body)
1538 .send()
1539 .await
1540 .map_err(VeracodeError::Http)?;
1541
1542 Ok(response)
1543 }
1544}
1545
1546#[cfg(test)]
1547#[allow(clippy::expect_used)] mod tests {
1549 use super::*;
1550 use proptest::prelude::*;
1551
1552 fn create_test_config() -> VeracodeConfig {
1558 use crate::{VeracodeCredentials, VeracodeRegion};
1559
1560 VeracodeConfig {
1561 credentials: VeracodeCredentials::new(
1562 "test_api_id".to_string(),
1563 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
1564 ),
1565 base_url: "https://api.veracode.com".to_string(),
1566 rest_base_url: "https://api.veracode.com".to_string(),
1567 xml_base_url: "https://analysiscenter.veracode.com".to_string(),
1568 region: VeracodeRegion::Commercial,
1569 validate_certificates: true,
1570 connect_timeout: 30,
1571 request_timeout: 300,
1572 proxy_url: None,
1573 proxy_username: None,
1574 proxy_password: None,
1575 retry_config: Default::default(),
1576 }
1577 }
1578
1579 proptest! {
1584 #![proptest_config(ProptestConfig {
1585 cases: if cfg!(miri) { 5 } else { 1000 },
1586 failure_persistence: None,
1587 .. ProptestConfig::default()
1588 })]
1589
1590 #[test]
1593 fn proptest_url_params_prevent_injection(
1594 key in "[a-zA-Z0-9_]{1,50}",
1595 value in ".*{0,100}",
1596 ) {
1597 let config = create_test_config();
1598 let client = VeracodeClient::new(config)
1599 .expect("valid test client configuration");
1600
1601 let params = vec![(key.as_str(), value.as_str())];
1602 let url = client.build_url_with_params("/api/test", ¶ms);
1603
1604 prop_assert!(!url.contains("<script>"));
1606 prop_assert!(!url.contains("javascript:"));
1607
1608 prop_assert!(url.starts_with("https://api.veracode.com/api/test"));
1610
1611 if !params.is_empty() && !key.is_empty() {
1613 prop_assert!(url.contains('?'));
1614 }
1615 }
1616
1617 #[test]
1620 fn proptest_url_params_capacity_safe(
1621 param_count in 0usize..=100,
1622 ) {
1623 let config = create_test_config();
1624 let client = VeracodeClient::new(config)
1625 .expect("valid test client configuration");
1626
1627 let params: Vec<(&str, &str)> = (0..param_count)
1629 .map(|_| ("key", "value"))
1630 .collect();
1631
1632 let url = client.build_url_with_params("/api/test", ¶ms);
1634
1635 prop_assert!(url.starts_with("https://"));
1637 prop_assert!(url.len() < 100000); }
1639
1640 #[test]
1642 fn proptest_url_params_empty_safe(
1643 key in "\\s*",
1644 value in "\\s*",
1645 ) {
1646 let config = create_test_config();
1647 let client = VeracodeClient::new(config)
1648 .expect("valid test client configuration");
1649
1650 let params = vec![(key.as_str(), value.as_str())];
1651 let url = client.build_url_with_params("/api/test", ¶ms);
1652
1653 prop_assert!(url.starts_with("https://"));
1655 }
1656 }
1657
1658 proptest! {
1663 #![proptest_config(ProptestConfig {
1664 cases: if cfg!(miri) { 5 } else { 1000 },
1665 failure_persistence: None,
1666 .. ProptestConfig::default()
1667 })]
1668
1669 #[test]
1672 fn proptest_hmac_invalid_urls_return_error(
1673 invalid_url in ".*{0,100}",
1674 ) {
1675 let config = create_test_config();
1676 let client = VeracodeClient::new(config)
1677 .expect("valid test client configuration");
1678
1679 let result = client.generate_hmac_signature(
1681 "GET",
1682 &invalid_url,
1683 1234567890000,
1684 "0123456789abcdef0123456789abcdef",
1685 );
1686
1687 match result {
1689 Ok(_) => {
1690 prop_assert!(Url::parse(&invalid_url).is_ok());
1692 },
1693 Err(e) => {
1694 prop_assert!(matches!(e, VeracodeError::Authentication(_)));
1696 }
1697 }
1698 }
1699
1700 #[test]
1703 fn proptest_hmac_deterministic(
1704 method in "[A-Z]{3,7}",
1705 timestamp in 1000000000000u64..2000000000000u64,
1706 ) {
1707 let config = create_test_config();
1708 let client = VeracodeClient::new(config)
1709 .expect("valid test client configuration");
1710
1711 let url = "https://api.veracode.com/api/test";
1712 let nonce = "0123456789abcdef0123456789abcdef";
1713
1714 let sig1 = client.generate_hmac_signature(&method, url, timestamp, nonce);
1715 let sig2 = client.generate_hmac_signature(&method, url, timestamp, nonce);
1716
1717 match (sig1, sig2) {
1719 (Ok(s1), Ok(s2)) => prop_assert_eq!(s1, s2),
1720 (Err(_), Err(_)) => {}, _ => prop_assert!(false, "Non-deterministic result"),
1722 }
1723 }
1724
1725 #[test]
1728 fn proptest_hmac_invalid_nonce_returns_error(
1729 invalid_nonce in "[^0-9a-fA-F]{1,32}",
1730 ) {
1731 let config = create_test_config();
1732 let client = VeracodeClient::new(config)
1733 .expect("valid test client configuration");
1734
1735 let result = client.generate_hmac_signature(
1736 "GET",
1737 "https://api.veracode.com/api/test",
1738 1234567890000,
1739 &invalid_nonce,
1740 );
1741
1742 prop_assert!(matches!(result, Err(VeracodeError::Authentication(_))));
1744 }
1745
1746 #[test]
1749 fn proptest_hmac_timestamp_safe(
1750 timestamp in any::<u64>(),
1751 ) {
1752 let config = create_test_config();
1753 let client = VeracodeClient::new(config)
1754 .expect("valid test client configuration");
1755
1756 let url = "https://api.veracode.com/api/test";
1757 let nonce = "0123456789abcdef0123456789abcdef";
1758
1759 let result = client.generate_hmac_signature("GET", url, timestamp, nonce);
1761
1762 prop_assert!(result.is_ok() || result.is_err());
1764 }
1765 }
1766
1767 proptest! {
1772 #![proptest_config(ProptestConfig {
1773 cases: if cfg!(miri) { 5 } else { 1000 },
1774 failure_persistence: None,
1775 .. ProptestConfig::default()
1776 })]
1777
1778 #[test]
1781 fn proptest_auth_header_format(
1782 method in "[A-Z]{3,7}",
1783 ) {
1784 let config = create_test_config();
1785 let client = VeracodeClient::new(config)
1786 .expect("valid test client configuration");
1787
1788 let url = "https://api.veracode.com/api/test";
1789 let result = client.generate_auth_header(&method, url);
1790
1791 if let Ok(header) = result {
1792 prop_assert!(header.starts_with("VERACODE-HMAC-SHA-256"));
1794
1795 prop_assert!(header.contains("id="));
1797 prop_assert!(header.contains("ts="));
1798 prop_assert!(header.contains("nonce="));
1799 prop_assert!(header.contains("sig="));
1800
1801 let parts: Vec<&str> = header.split(',').collect();
1803 prop_assert_eq!(parts.len(), 4);
1804 }
1805 }
1806
1807 #[test]
1810 fn proptest_auth_header_nonce_unique(
1811 _seed in any::<u8>(),
1812 ) {
1813 let config = create_test_config();
1814 let client = VeracodeClient::new(config)
1815 .expect("valid test client configuration");
1816
1817 let url = "https://api.veracode.com/api/test";
1818
1819 let header1 = client.generate_auth_header("GET", url)
1821 .expect("valid auth header generation");
1822 let header2 = client.generate_auth_header("GET", url)
1823 .expect("valid auth header generation");
1824
1825 fn extract_nonce(h: &str) -> Option<String> {
1827 Some(h.split("nonce=")
1828 .nth(1)?
1829 .split(',')
1830 .next()?
1831 .to_string())
1832 }
1833
1834 if let (Some(nonce1), Some(nonce2)) = (extract_nonce(&header1), extract_nonce(&header2)) {
1835 prop_assert_ne!(&nonce1, &nonce2);
1838
1839 prop_assert_eq!(nonce1.len(), 32);
1841 prop_assert_eq!(nonce2.len(), 32);
1842 prop_assert!(nonce1.chars().all(|c| c.is_ascii_hexdigit()));
1843 prop_assert!(nonce2.chars().all(|c| c.is_ascii_hexdigit()));
1844 }
1845 }
1846 }
1847
1848 proptest! {
1853 #![proptest_config(ProptestConfig {
1854 cases: if cfg!(miri) { 5 } else { 100 },
1855 failure_persistence: None,
1856 .. ProptestConfig::default()
1857 })]
1858
1859 #[test]
1862 fn proptest_client_creation_invalid_proxy(
1863 invalid_proxy in ".*{0,100}",
1864 ) {
1865 use crate::{VeracodeCredentials, VeracodeRegion};
1866
1867 let config = VeracodeConfig {
1868 credentials: VeracodeCredentials::new(
1869 "test_api_id".to_string(),
1870 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
1871 ),
1872 base_url: "https://api.veracode.com".to_string(),
1873 rest_base_url: "https://api.veracode.com".to_string(),
1874 xml_base_url: "https://analysiscenter.veracode.com".to_string(),
1875 region: VeracodeRegion::Commercial,
1876 validate_certificates: true,
1877 connect_timeout: 30,
1878 request_timeout: 300,
1879 proxy_url: Some(invalid_proxy.clone()),
1880 proxy_username: None,
1881 proxy_password: None,
1882 retry_config: Default::default(),
1883 };
1884
1885 let result = VeracodeClient::new(config);
1886
1887 match result {
1889 Ok(_) => {
1890 prop_assert!(reqwest::Proxy::all(&invalid_proxy).is_ok());
1892 },
1893 Err(e) => {
1894 prop_assert!(matches!(e, VeracodeError::InvalidConfig(_)));
1896 }
1897 }
1898 }
1899
1900 #[test]
1903 fn proptest_client_timeouts_safe(
1904 connect_timeout in 1u64..=3600,
1905 request_timeout in 1u64..=7200,
1906 ) {
1907 use crate::{VeracodeCredentials, VeracodeRegion};
1908
1909 let config = VeracodeConfig {
1910 credentials: VeracodeCredentials::new(
1911 "test_api_id".to_string(),
1912 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
1913 ),
1914 base_url: "https://api.veracode.com".to_string(),
1915 rest_base_url: "https://api.veracode.com".to_string(),
1916 xml_base_url: "https://analysiscenter.veracode.com".to_string(),
1917 region: VeracodeRegion::Commercial,
1918 validate_certificates: true,
1919 connect_timeout,
1920 request_timeout,
1921 proxy_url: None,
1922 proxy_username: None,
1923 proxy_password: None,
1924 retry_config: Default::default(),
1925 };
1926
1927 let result = VeracodeClient::new(config);
1929 prop_assert!(result.is_ok());
1930 }
1931 }
1932
1933 proptest! {
1938 #![proptest_config(ProptestConfig {
1939 cases: if cfg!(miri) { 5 } else { 100 },
1940 failure_persistence: None,
1941 .. ProptestConfig::default()
1942 })]
1943
1944 #[test]
1947 fn proptest_file_upload_capacity_safe(
1948 file_size in 0usize..=1000000,
1949 ) {
1950 let file_data = vec![0u8; file_size];
1952
1953 let file_data_arc = Arc::new(file_data);
1955
1956 prop_assert_eq!(file_data_arc.len(), file_size);
1958
1959 let clone1 = Arc::clone(&file_data_arc);
1961 let clone2 = Arc::clone(&file_data_arc);
1962 prop_assert_eq!(clone1.len(), file_size);
1963 prop_assert_eq!(clone2.len(), file_size);
1964 }
1965
1966 #[test]
1969 fn proptest_content_type_safe(
1970 content_type in ".*{0,200}",
1971 ) {
1972 let content_type_cow: Cow<str> = if content_type.len() < 64 {
1974 Cow::Borrowed(&content_type)
1975 } else {
1976 Cow::Owned(content_type.clone())
1977 };
1978
1979 let ct_lower = content_type_cow.to_lowercase();
1981 if ct_lower.contains("<script>") || ct_lower.contains("javascript:") {
1982 prop_assert!(content_type_cow.as_ref().contains("<script>") ||
1984 content_type_cow.as_ref().contains("javascript:"));
1985 }
1986
1987 prop_assert_eq!(content_type_cow.len(), content_type.len());
1989 }
1990 }
1991
1992 #[test]
1997 fn test_hmac_signature_with_query_params() {
1998 let config = create_test_config();
1999 let client = VeracodeClient::new(config).expect("valid test client configuration");
2000
2001 let url = "https://api.veracode.com/api/test?param1=value1¶m2=value2";
2003 let nonce = "0123456789abcdef0123456789abcdef";
2004 let timestamp = 1234567890000;
2005
2006 let result = client.generate_hmac_signature("GET", url, timestamp, nonce);
2007 assert!(result.is_ok());
2008
2009 let signature = result.expect("valid HMAC signature");
2010 assert_eq!(signature.len(), 64);
2012 assert!(signature.chars().all(|c| c.is_ascii_hexdigit()));
2013 }
2014
2015 #[test]
2016 fn test_hmac_signature_different_methods() {
2017 let config = create_test_config();
2018 let client = VeracodeClient::new(config).expect("valid test client configuration");
2019
2020 let url = "https://api.veracode.com/api/test";
2021 let nonce = "0123456789abcdef0123456789abcdef";
2022 let timestamp = 1234567890000;
2023
2024 let sig_get = client
2025 .generate_hmac_signature("GET", url, timestamp, nonce)
2026 .expect("valid HMAC signature for GET");
2027 let sig_post = client
2028 .generate_hmac_signature("POST", url, timestamp, nonce)
2029 .expect("valid HMAC signature for POST");
2030
2031 assert_ne!(sig_get, sig_post);
2033 }
2034
2035 #[test]
2036 fn test_url_encoding_special_characters() {
2037 let config = create_test_config();
2038 let client = VeracodeClient::new(config).expect("valid test client configuration");
2039
2040 let params = vec![
2042 ("key1", "value with spaces"),
2043 ("key2", "value&with&ersands"),
2044 ("key3", "value=with=equals"),
2045 ("key4", "value?with?questions"),
2046 ];
2047
2048 let url = client.build_url_with_params("/api/test", ¶ms);
2049
2050 assert!(url.contains("value%20with%20spaces") || url.contains("value+with+spaces"));
2052 assert!(url.contains("%26"));
2054 assert!(url.starts_with("https://api.veracode.com/api/test?"));
2056 }
2057
2058 #[test]
2059 fn test_url_encoding_unicode() {
2060 let config = create_test_config();
2061 let client = VeracodeClient::new(config).expect("valid test client configuration");
2062
2063 let params = vec![
2065 ("key", "δ½ ε₯½δΈη"), ("key2", "ππ‘οΈ"), ];
2068
2069 let url = client.build_url_with_params("/api/test", ¶ms);
2070
2071 assert!(url.starts_with("https://api.veracode.com/api/test?"));
2073 assert!(url.contains('%'));
2075 }
2076
2077 #[test]
2078 fn test_empty_query_params() {
2079 let config = create_test_config();
2080 let client = VeracodeClient::new(config).expect("valid test client configuration");
2081
2082 let url = client.build_url_with_params("/api/test", &[]);
2083
2084 assert_eq!(url, "https://api.veracode.com/api/test");
2086 }
2087
2088 #[test]
2089 fn test_invalid_api_key_format() {
2090 use crate::{VeracodeCredentials, VeracodeRegion};
2091
2092 let config = VeracodeConfig {
2094 credentials: VeracodeCredentials::new(
2095 "test_api_id".to_string(),
2096 "not_valid_hex_key".to_string(),
2097 ),
2098 base_url: "https://api.veracode.com".to_string(),
2099 rest_base_url: "https://api.veracode.com".to_string(),
2100 xml_base_url: "https://analysiscenter.veracode.com".to_string(),
2101 region: VeracodeRegion::Commercial,
2102 validate_certificates: true,
2103 connect_timeout: 30,
2104 request_timeout: 300,
2105 proxy_url: None,
2106 proxy_username: None,
2107 proxy_password: None,
2108 retry_config: Default::default(),
2109 };
2110
2111 let client = VeracodeClient::new(config).expect("valid test client configuration");
2112 let result = client.generate_auth_header("GET", "https://api.veracode.com/api/test");
2113
2114 assert!(matches!(result, Err(VeracodeError::Authentication(_))));
2116 }
2117
2118 #[test]
2119 fn test_auth_header_format() {
2120 let config = create_test_config();
2121 let client = VeracodeClient::new(config).expect("valid test client configuration");
2122
2123 let header = client
2124 .generate_auth_header("GET", "https://api.veracode.com/api/test")
2125 .expect("valid auth header generation");
2126
2127 assert!(header.starts_with("VERACODE-HMAC-SHA-256 "));
2129 assert!(header.contains("id=test_api_id"));
2130 assert!(header.contains("ts="));
2131 assert!(header.contains("nonce="));
2132 assert!(header.contains("sig="));
2133
2134 let parts: Vec<&str> = header.split(',').collect();
2136 assert_eq!(parts.len(), 4);
2137 }
2138
2139 #[cfg(not(miri))] #[test]
2141 fn test_auth_header_timestamp_monotonic() {
2142 let config = create_test_config();
2143 let client = VeracodeClient::new(config).expect("valid test client configuration");
2144
2145 let header1 = client
2146 .generate_auth_header("GET", "https://api.veracode.com/api/test")
2147 .expect("valid auth header generation");
2148 std::thread::sleep(std::time::Duration::from_millis(10));
2149 let header2 = client
2150 .generate_auth_header("GET", "https://api.veracode.com/api/test")
2151 .expect("valid auth header generation");
2152
2153 let extract_ts =
2155 |h: &str| -> Option<u64> { h.split("ts=").nth(1)?.split(',').next()?.parse().ok() };
2156
2157 let ts1 = extract_ts(&header1).expect("valid timestamp extraction");
2158 let ts2 = extract_ts(&header2).expect("valid timestamp extraction");
2159
2160 assert!(ts2 >= ts1);
2162 }
2163
2164 #[test]
2165 fn test_base_url_accessor() {
2166 let config = create_test_config();
2167 let client = VeracodeClient::new(config).expect("valid test client configuration");
2168
2169 assert_eq!(client.base_url(), "https://api.veracode.com");
2170 }
2171
2172 #[test]
2173 fn test_client_clone() {
2174 let config = create_test_config();
2175 let client1 = VeracodeClient::new(config).expect("valid test client configuration");
2176 let client2 = client1.clone();
2177
2178 assert_eq!(client1.base_url(), client2.base_url());
2180 }
2181
2182 #[test]
2183 fn test_url_capacity_estimation() {
2184 let config = create_test_config();
2185 let client = VeracodeClient::new(config).expect("valid test client configuration");
2186
2187 let params: Vec<(&str, &str)> = (0..100).map(|_| ("key", "value")).collect();
2189
2190 let url = client.build_url_with_params("/api/test", ¶ms);
2191
2192 assert!(url.starts_with("https://api.veracode.com/api/test?"));
2194 assert!(url.len() > 100); }
2196
2197 #[test]
2198 fn test_saturating_arithmetic() {
2199 let config = create_test_config();
2200 let client = VeracodeClient::new(config).expect("valid test client configuration");
2201
2202 let params: Vec<(&str, &str)> = vec![("k", "v"); 1000];
2204
2205 let url = client.build_url_with_params("/api/test", ¶ms);
2207 assert!(url.len() < usize::MAX);
2208 }
2209}