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]
125 pub fn base_url(&self) -> &str {
126 &self.config.base_url
127 }
128
129 #[must_use]
131 pub fn config(&self) -> &VeracodeConfig {
132 &self.config
133 }
134
135 #[must_use]
137 pub fn client(&self) -> &Client {
138 &self.client
139 }
140
141 async fn execute_with_retry<F>(
167 &self,
168 request_builder: F,
169 operation_name: Cow<'_, str>,
170 ) -> Result<reqwest::Response, VeracodeError>
171 where
172 F: Fn() -> reqwest::RequestBuilder,
173 {
174 let retry_config = &self.config.retry_config;
175 let start_time = Instant::now();
176 let mut total_delay = std::time::Duration::from_millis(0);
177
178 if retry_config.max_attempts == 0 {
180 return match request_builder().send().await {
181 Ok(response) => Ok(response),
182 Err(e) => Err(VeracodeError::Http(e)),
183 };
184 }
185
186 let mut last_error = None;
187 let mut rate_limit_attempts: u32 = 0;
188
189 for attempt in 1..=retry_config.max_attempts.saturating_add(1) {
190 match request_builder().send().await {
192 Ok(response) => {
193 if response.status().as_u16() == 429 {
195 let retry_after_seconds = response
197 .headers()
198 .get("retry-after")
199 .and_then(|h| h.to_str().ok())
200 .and_then(|s| s.parse::<u64>().ok());
201
202 let message = "HTTP 429: Rate limit exceeded".to_string();
203 let veracode_error = VeracodeError::RateLimited {
204 retry_after_seconds,
205 message,
206 };
207
208 rate_limit_attempts = rate_limit_attempts.saturating_add(1);
210
211 if attempt > retry_config.max_attempts
213 || rate_limit_attempts > retry_config.rate_limit_max_attempts
214 {
215 last_error = Some(veracode_error);
216 break;
217 }
218
219 let delay = retry_config.calculate_rate_limit_delay(retry_after_seconds);
221 total_delay = total_delay.saturating_add(delay);
222
223 if total_delay.as_millis() > retry_config.max_total_delay_ms as u128 {
225 let msg = format!(
226 "{} exceeded maximum total retry time of {}ms after {} attempts",
227 operation_name, retry_config.max_total_delay_ms, attempt
228 );
229 last_error = Some(VeracodeError::RetryExhausted(msg));
230 break;
231 }
232
233 let wait_time = match retry_after_seconds {
235 Some(seconds) => format!("{seconds}s (from Retry-After header)"),
236 None => format!("{}s (until next minute window)", delay.as_secs()),
237 };
238 warn!(
239 "π¦ {operation_name} rate limited on attempt {attempt}, waiting {wait_time}"
240 );
241
242 tokio::time::sleep(delay).await;
244 last_error = Some(veracode_error);
245 continue;
246 }
247
248 if attempt > 1 {
249 info!("β
{operation_name} succeeded on attempt {attempt}");
251 }
252 return Ok(response);
253 }
254 Err(e) => {
255 let veracode_error = VeracodeError::Http(e);
257
258 if attempt > retry_config.max_attempts
260 || !retry_config.is_retryable_error(&veracode_error)
261 {
262 last_error = Some(veracode_error);
263 break;
264 }
265
266 let delay = retry_config.calculate_delay(attempt);
268 total_delay = total_delay.saturating_add(delay);
269
270 if total_delay.as_millis() > retry_config.max_total_delay_ms as u128 {
272 let msg = format!(
274 "{} exceeded maximum total retry time of {}ms after {} attempts",
275 operation_name, retry_config.max_total_delay_ms, attempt
276 );
277 last_error = Some(VeracodeError::RetryExhausted(msg));
278 break;
279 }
280
281 warn!(
283 "β οΈ {operation_name} failed on attempt {attempt}, retrying in {}ms: {veracode_error}",
284 delay.as_millis()
285 );
286
287 tokio::time::sleep(delay).await;
289 last_error = Some(veracode_error);
290 }
291 }
292 }
293
294 match last_error {
296 Some(error) => {
297 let elapsed = start_time.elapsed();
298 match error {
299 VeracodeError::RetryExhausted(_) => Err(error),
300 VeracodeError::Http(_)
301 | VeracodeError::Serialization(_)
302 | VeracodeError::Authentication(_)
303 | VeracodeError::InvalidResponse(_)
304 | VeracodeError::HttpStatus { .. }
305 | VeracodeError::InvalidConfig(_)
306 | VeracodeError::NotFound(_)
307 | VeracodeError::RateLimited { .. }
308 | VeracodeError::Validation(_) => {
309 let msg = format!(
310 "{} failed after {} attempts over {}ms: {}",
311 operation_name,
312 retry_config.max_attempts.saturating_add(1),
313 elapsed.as_millis(),
314 error
315 );
316 Err(VeracodeError::RetryExhausted(msg))
317 }
318 }
319 }
320 None => {
321 let msg = format!(
322 "{} failed after {} attempts with unknown error",
323 operation_name,
324 retry_config.max_attempts.saturating_add(1)
325 );
326 Err(VeracodeError::RetryExhausted(msg))
327 }
328 }
329 }
330
331 fn generate_hmac_signature(
333 &self,
334 method: &str,
335 url: &str,
336 timestamp: u64,
337 nonce: &str,
338 ) -> Result<String, VeracodeError> {
339 let url_parsed = Url::parse(url)
340 .map_err(|_| VeracodeError::Authentication(INVALID_URL_MSG.to_string()))?;
341
342 let path_and_query = match url_parsed.query() {
343 Some(query) => format!("{}?{}", url_parsed.path(), query),
344 None => url_parsed.path().to_string(),
345 };
346
347 let host = url_parsed.host_str().unwrap_or("");
348
349 let data = format!(
352 "id={}&host={}&url={}&method={}",
353 self.config.credentials.expose_api_id(),
354 host,
355 path_and_query,
356 method
357 );
358
359 let timestamp_str = timestamp.to_string();
360 let ver_str = "vcode_request_version_1";
361
362 let key_bytes = hex::decode(self.config.credentials.expose_api_key())
364 .map_err(|_| VeracodeError::Authentication(INVALID_API_KEY_MSG.to_string()))?;
365
366 let nonce_bytes = hex::decode(nonce)
367 .map_err(|_| VeracodeError::Authentication(INVALID_NONCE_MSG.to_string()))?;
368
369 let mut mac1 = HmacSha256::new_from_slice(&key_bytes)
371 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
372 mac1.update(&nonce_bytes);
373 let hashed_nonce = mac1.finalize().into_bytes();
374
375 let mut mac2 = HmacSha256::new_from_slice(&hashed_nonce)
377 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
378 mac2.update(timestamp_str.as_bytes());
379 let hashed_timestamp = mac2.finalize().into_bytes();
380
381 let mut mac3 = HmacSha256::new_from_slice(&hashed_timestamp)
383 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
384 mac3.update(ver_str.as_bytes());
385 let hashed_ver_str = mac3.finalize().into_bytes();
386
387 let mut mac4 = HmacSha256::new_from_slice(&hashed_ver_str)
389 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
390 mac4.update(data.as_bytes());
391 let signature = mac4.finalize().into_bytes();
392
393 Ok(hex::encode(signature).to_lowercase())
395 }
396
397 pub fn generate_auth_header(&self, method: &str, url: &str) -> Result<String, VeracodeError> {
404 #[allow(clippy::cast_possible_truncation)]
405 let timestamp = SystemTime::now()
406 .duration_since(UNIX_EPOCH)
407 .map_err(|e| VeracodeError::Authentication(format!("System time error: {e}")))?
408 .as_millis() as u64; let nonce_bytes: [u8; 16] = rand::random();
412 let nonce = hex::encode(nonce_bytes);
413
414 let signature = self.generate_hmac_signature(method, url, timestamp, &nonce)?;
415
416 Ok(format!(
417 "VERACODE-HMAC-SHA-256 id={},ts={},nonce={},sig={}",
418 self.config.credentials.expose_api_id(),
419 timestamp,
420 nonce,
421 signature
422 ))
423 }
424
425 pub async fn get(
441 &self,
442 endpoint: &str,
443 query_params: Option<&[(String, String)]>,
444 ) -> Result<reqwest::Response, VeracodeError> {
445 let param_count = query_params.map_or(0, |p| p.len());
447 let estimated_capacity = self
448 .config
449 .base_url
450 .len()
451 .saturating_add(endpoint.len())
452 .saturating_add(param_count.saturating_mul(32));
453 let mut url = String::with_capacity(estimated_capacity);
454 url.push_str(&self.config.base_url);
455 url.push_str(endpoint);
456
457 if let Some(params) = query_params
458 && !params.is_empty()
459 {
460 url.push('?');
461 for (i, (key, value)) in params.iter().enumerate() {
462 if i > 0 {
463 url.push('&');
464 }
465 url.push_str(key);
466 url.push('=');
467 url.push_str(value);
468 }
469 }
470
471 let request_builder = || {
473 let Ok(auth_header) = self.generate_auth_header("GET", &url) else {
475 return self.client.get("invalid://url");
476 };
477
478 self.client
479 .get(&url)
480 .header("Authorization", auth_header)
481 .header("Content-Type", "application/json")
482 };
483
484 let operation_name = if endpoint.len() < 50 {
486 Cow::Owned(format!("GET {endpoint}"))
487 } else {
488 Cow::Borrowed("GET [long endpoint]")
489 };
490 self.execute_with_retry(request_builder, operation_name)
491 .await
492 }
493
494 pub async fn post<T: Serialize>(
510 &self,
511 endpoint: &str,
512 body: Option<&T>,
513 ) -> Result<reqwest::Response, VeracodeError> {
514 let mut url =
515 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
516 url.push_str(&self.config.base_url);
517 url.push_str(endpoint);
518
519 let serialized_body = if let Some(body) = body {
521 Some(serde_json::to_string(body)?)
522 } else {
523 None
524 };
525
526 let request_builder = || {
528 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
530 return self.client.post("invalid://url");
531 };
532
533 let mut request = self
534 .client
535 .post(&url)
536 .header("Authorization", auth_header)
537 .header("Content-Type", "application/json");
538
539 if let Some(ref body_str) = serialized_body {
540 request = request.body(body_str.clone());
541 }
542
543 request
544 };
545
546 let operation_name = if endpoint.len() < 50 {
547 Cow::Owned(format!("POST {endpoint}"))
548 } else {
549 Cow::Borrowed("POST [long endpoint]")
550 };
551 self.execute_with_retry(request_builder, operation_name)
552 .await
553 }
554
555 pub async fn put<T: Serialize>(
571 &self,
572 endpoint: &str,
573 body: Option<&T>,
574 ) -> Result<reqwest::Response, VeracodeError> {
575 let mut url =
576 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
577 url.push_str(&self.config.base_url);
578 url.push_str(endpoint);
579
580 let serialized_body = if let Some(body) = body {
582 Some(serde_json::to_string(body)?)
583 } else {
584 None
585 };
586
587 let request_builder = || {
589 let Ok(auth_header) = self.generate_auth_header("PUT", &url) else {
591 return self.client.put("invalid://url");
592 };
593
594 let mut request = self
595 .client
596 .put(&url)
597 .header("Authorization", auth_header)
598 .header("Content-Type", "application/json");
599
600 if let Some(ref body_str) = serialized_body {
601 request = request.body(body_str.clone());
602 }
603
604 request
605 };
606
607 let operation_name = if endpoint.len() < 50 {
608 Cow::Owned(format!("PUT {endpoint}"))
609 } else {
610 Cow::Borrowed("PUT [long endpoint]")
611 };
612 self.execute_with_retry(request_builder, operation_name)
613 .await
614 }
615
616 pub async fn delete(&self, endpoint: &str) -> Result<reqwest::Response, VeracodeError> {
631 let mut url =
632 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
633 url.push_str(&self.config.base_url);
634 url.push_str(endpoint);
635
636 let request_builder = || {
638 let Ok(auth_header) = self.generate_auth_header("DELETE", &url) else {
640 return self.client.delete("invalid://url");
641 };
642
643 self.client
644 .delete(&url)
645 .header("Authorization", auth_header)
646 .header("Content-Type", "application/json")
647 };
648
649 let operation_name = if endpoint.len() < 50 {
650 Cow::Owned(format!("DELETE {endpoint}"))
651 } else {
652 Cow::Borrowed("DELETE [long endpoint]")
653 };
654 self.execute_with_retry(request_builder, operation_name)
655 .await
656 }
657
658 pub async fn handle_response(
681 response: reqwest::Response,
682 context: &str,
683 ) -> Result<reqwest::Response, VeracodeError> {
684 if !response.status().is_success() {
685 let status = response.status();
686 let status_code = status.as_u16();
687 let url = response.url().to_string();
688 let error_text = response.text().await?;
689
690 return Err(VeracodeError::HttpStatus {
692 status_code,
693 url,
694 message: format!("Failed to {context}: {error_text}"),
695 });
696 }
697 Ok(response)
698 }
699
700 pub async fn get_with_query(
718 &self,
719 endpoint: &str,
720 query_params: Option<Vec<(String, String)>>,
721 ) -> Result<reqwest::Response, VeracodeError> {
722 let query_slice = query_params.as_deref();
723 let response = self.get(endpoint, query_slice).await?;
724 Self::handle_response(response, &format!("GET {endpoint}")).await
725 }
726
727 pub async fn post_with_response<T: Serialize>(
743 &self,
744 endpoint: &str,
745 body: Option<&T>,
746 ) -> Result<reqwest::Response, VeracodeError> {
747 let response = self.post(endpoint, body).await?;
748 Self::handle_response(response, &format!("POST {endpoint}")).await
749 }
750
751 pub async fn put_with_response<T: Serialize>(
767 &self,
768 endpoint: &str,
769 body: Option<&T>,
770 ) -> Result<reqwest::Response, VeracodeError> {
771 let response = self.put(endpoint, body).await?;
772 Self::handle_response(response, &format!("PUT {endpoint}")).await
773 }
774
775 pub async fn delete_with_response(
790 &self,
791 endpoint: &str,
792 ) -> Result<reqwest::Response, VeracodeError> {
793 let response = self.delete(endpoint).await?;
794 Self::handle_response(response, &format!("DELETE {endpoint}")).await
795 }
796
797 pub async fn get_paginated(
817 &self,
818 endpoint: &str,
819 base_query_params: Option<Vec<(String, String)>>,
820 page_size: Option<u32>,
821 ) -> Result<String, VeracodeError> {
822 let size = page_size.unwrap_or(500);
823 let mut page: u32 = 0;
824 let mut all_items = Vec::new();
825 let mut page_info = None;
826
827 loop {
828 let mut query_params = base_query_params.clone().unwrap_or_default();
829 query_params.push(("page".to_string(), page.to_string()));
830 query_params.push(("size".to_string(), size.to_string()));
831
832 let response = self.get_with_query(endpoint, Some(query_params)).await?;
833 let response_text = response.text().await?;
834
835 validate_json_depth(&response_text, MAX_JSON_DEPTH).map_err(|e| {
837 VeracodeError::InvalidResponse(format!("JSON validation failed: {}", e))
838 })?;
839
840 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
842 if let Some(embedded) = json_value.get("_embedded") {
844 if let Some(items_array) =
845 embedded.as_object().and_then(|obj| obj.values().next())
846 && let Some(items) = items_array.as_array()
847 {
848 if items.is_empty() {
849 break; }
851 all_items.extend(items.clone());
852 }
853 } else if let Some(items) = json_value.as_array() {
854 if items.is_empty() {
856 break;
857 }
858 all_items.extend(items.clone());
859 } else {
860 return Ok(response_text);
862 }
863
864 if let Some(page_obj) = json_value.get("page") {
866 page_info = Some(page_obj.clone());
867 if let (Some(current), Some(total)) = (
868 page_obj.get("number").and_then(|n| n.as_u64()),
869 page_obj.get("totalPages").and_then(|n| n.as_u64()),
870 ) && current.saturating_add(1) >= total
871 {
872 break; }
874 }
875 } else {
876 return Ok(response_text);
878 }
879
880 page = page.saturating_add(1);
881
882 if page > 100 {
884 break;
885 }
886 }
887
888 let combined_response = if let Some(page_info) = page_info {
890 serde_json::json!({
892 "_embedded": {
893 "roles": all_items },
895 "page": page_info
896 })
897 } else {
898 serde_json::Value::Array(all_items)
900 };
901
902 Ok(combined_response.to_string())
903 }
904
905 pub async fn get_with_params(
921 &self,
922 endpoint: &str,
923 params: &[(&str, &str)],
924 ) -> Result<reqwest::Response, VeracodeError> {
925 let mut url =
926 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
927 url.push_str(&self.config.base_url);
928 url.push_str(endpoint);
929 let mut request_url =
930 Url::parse(&url).map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
931
932 if !params.is_empty() {
934 let mut query_pairs = request_url.query_pairs_mut();
935 for (key, value) in params {
936 query_pairs.append_pair(key, value);
937 }
938 }
939
940 let auth_header = self.generate_auth_header("GET", request_url.as_str())?;
941
942 let response = self
943 .client
944 .get(request_url)
945 .header("Authorization", auth_header)
946 .header("User-Agent", "Veracode Rust Client")
947 .send()
948 .await?;
949
950 Ok(response)
951 }
952
953 pub async fn post_form(
969 &self,
970 endpoint: &str,
971 params: &[(&str, &str)],
972 ) -> Result<reqwest::Response, VeracodeError> {
973 let mut url =
974 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
975 url.push_str(&self.config.base_url);
976 url.push_str(endpoint);
977
978 let form_data: Vec<(&str, &str)> = params.to_vec();
980
981 let auth_header = self.generate_auth_header("POST", &url)?;
982
983 let response = self
984 .client
985 .post(&url)
986 .header("Authorization", auth_header)
987 .header("User-Agent", "Veracode Rust Client")
988 .form(&form_data)
989 .send()
990 .await?;
991
992 Ok(response)
993 }
994
995 pub async fn upload_file_multipart(
1014 &self,
1015 endpoint: &str,
1016 params: HashMap<&str, &str>,
1017 file_field_name: &str,
1018 filename: &str,
1019 file_data: Vec<u8>,
1020 ) -> Result<reqwest::Response, VeracodeError> {
1021 let mut url =
1022 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
1023 url.push_str(&self.config.base_url);
1024 url.push_str(endpoint);
1025
1026 let mut form = multipart::Form::new();
1028
1029 for (key, value) in params {
1031 form = form.text(key.to_string(), value.to_string());
1032 }
1033
1034 let part = multipart::Part::bytes(file_data)
1036 .file_name(filename.to_string())
1037 .mime_str("application/octet-stream")
1038 .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
1039
1040 form = form.part(file_field_name.to_string(), part);
1041
1042 let auth_header = self.generate_auth_header("POST", &url)?;
1043
1044 let response = self
1045 .client
1046 .post(&url)
1047 .header("Authorization", auth_header)
1048 .header("User-Agent", "Veracode Rust Client")
1049 .multipart(form)
1050 .send()
1051 .await?;
1052
1053 Ok(response)
1054 }
1055
1056 pub async fn upload_file_multipart_put(
1075 &self,
1076 url: &str,
1077 file_field_name: &str,
1078 filename: &str,
1079 file_data: Vec<u8>,
1080 additional_headers: Option<HashMap<&str, &str>>,
1081 ) -> Result<reqwest::Response, VeracodeError> {
1082 let part = multipart::Part::bytes(file_data)
1084 .file_name(filename.to_string())
1085 .mime_str("application/octet-stream")
1086 .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
1087
1088 let form = multipart::Form::new().part(file_field_name.to_string(), part);
1089
1090 let auth_header = self.generate_auth_header("PUT", url)?;
1091
1092 let mut request = self
1093 .client
1094 .put(url)
1095 .header("Authorization", auth_header)
1096 .header("User-Agent", "Veracode Rust Client")
1097 .multipart(form);
1098
1099 if let Some(headers) = additional_headers {
1101 for (key, value) in headers {
1102 request = request.header(key, value);
1103 }
1104 }
1105
1106 let response = request.send().await?;
1107 Ok(response)
1108 }
1109
1110 pub async fn upload_file_with_query_params(
1135 &self,
1136 endpoint: &str,
1137 query_params: &[(&str, &str)],
1138 file_field_name: &str,
1139 filename: &str,
1140 file_data: Vec<u8>,
1141 ) -> Result<reqwest::Response, VeracodeError> {
1142 let url = self.build_url_with_params(endpoint, query_params);
1144
1145 let file_data_arc = Arc::new(file_data);
1147
1148 let filename_cow: Cow<str> = if filename.len() < 128 {
1150 Cow::Borrowed(filename)
1151 } else {
1152 Cow::Owned(filename.to_string())
1153 };
1154
1155 let field_name_cow: Cow<str> = if file_field_name.len() < 32 {
1156 Cow::Borrowed(file_field_name)
1157 } else {
1158 Cow::Owned(file_field_name.to_string())
1159 };
1160
1161 let request_builder = || {
1163 let file_data_clone = Arc::clone(&file_data_arc);
1165
1166 let Ok(part) = multipart::Part::bytes((*file_data_clone).clone())
1168 .file_name(filename_cow.to_string())
1169 .mime_str("application/octet-stream")
1170 else {
1171 return self.client.post("invalid://url");
1172 };
1173
1174 let form = multipart::Form::new().part(field_name_cow.to_string(), part);
1175
1176 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1178 return self.client.post("invalid://url");
1179 };
1180
1181 self.client
1182 .post(&url)
1183 .header("Authorization", auth_header)
1184 .header("User-Agent", "Veracode Rust Client")
1185 .multipart(form)
1186 };
1187
1188 let operation_name: Cow<str> = if endpoint.len() < 50 {
1190 Cow::Owned(format!("File Upload POST {endpoint}"))
1191 } else {
1192 Cow::Borrowed("File Upload POST [long endpoint]")
1193 };
1194
1195 self.execute_with_retry(request_builder, operation_name)
1196 .await
1197 }
1198
1199 pub async fn post_with_query_params(
1218 &self,
1219 endpoint: &str,
1220 query_params: &[(&str, &str)],
1221 ) -> Result<reqwest::Response, VeracodeError> {
1222 let url = self.build_url_with_params(endpoint, query_params);
1224
1225 let auth_header = self.generate_auth_header("POST", &url)?;
1226
1227 let response = self
1228 .client
1229 .post(&url)
1230 .header("Authorization", auth_header)
1231 .header("User-Agent", "Veracode Rust Client")
1232 .send()
1233 .await?;
1234
1235 Ok(response)
1236 }
1237
1238 pub async fn get_with_query_params(
1257 &self,
1258 endpoint: &str,
1259 query_params: &[(&str, &str)],
1260 ) -> Result<reqwest::Response, VeracodeError> {
1261 let url = self.build_url_with_params(endpoint, query_params);
1263
1264 let auth_header = self.generate_auth_header("GET", &url)?;
1265
1266 let response = self
1267 .client
1268 .get(&url)
1269 .header("Authorization", auth_header)
1270 .header("User-Agent", "Veracode Rust Client")
1271 .send()
1272 .await?;
1273
1274 Ok(response)
1275 }
1276
1277 pub async fn upload_large_file_chunked<F>(
1299 &self,
1300 endpoint: &str,
1301 query_params: &[(&str, &str)],
1302 file_path: &str,
1303 content_type: Option<&str>,
1304 progress_callback: Option<F>,
1305 ) -> Result<reqwest::Response, VeracodeError>
1306 where
1307 F: Fn(u64, u64, f64) + Send + Sync,
1308 {
1309 let url = self.build_url_with_params(endpoint, query_params);
1311
1312 let mut file = File::open(file_path)
1314 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to open file: {e}")))?;
1315
1316 let file_size = file
1317 .metadata()
1318 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to get file size: {e}")))?
1319 .len();
1320
1321 #[allow(clippy::arithmetic_side_effects)]
1323 const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; if file_size > MAX_FILE_SIZE {
1325 return Err(VeracodeError::InvalidConfig(format!(
1326 "File size ({file_size} bytes) exceeds maximum limit of {MAX_FILE_SIZE} bytes"
1327 )));
1328 }
1329
1330 file.seek(SeekFrom::Start(0))
1332 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to seek file: {e}")))?;
1333
1334 #[allow(clippy::cast_possible_truncation)]
1335 let mut file_data_vec = Vec::with_capacity(file_size as usize);
1336 file.read_to_end(&mut file_data_vec)
1337 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to read file: {e}")))?;
1338
1339 let file_data = Bytes::from(file_data_vec);
1342 let content_type_cow: Cow<str> =
1343 content_type.map_or(Cow::Borrowed("binary/octet-stream"), |ct| {
1344 if ct.len() < 64 {
1345 Cow::Borrowed(ct)
1346 } else {
1347 Cow::Owned(ct.to_string())
1348 }
1349 });
1350
1351 let request_builder = || {
1353 let body_data = file_data.clone();
1355
1356 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1358 return self.client.post("invalid://url");
1359 };
1360
1361 self.client
1362 .post(&url)
1363 .header("Authorization", auth_header)
1364 .header("User-Agent", "Veracode Rust Client")
1365 .header("Content-Type", content_type_cow.as_ref())
1366 .header("Content-Length", file_size.to_string())
1367 .body(body_data)
1368 };
1369
1370 if let Some(ref callback) = progress_callback {
1372 callback(0, file_size, 0.0);
1373 }
1374
1375 let operation_name: Cow<str> = if endpoint.len() < 50 {
1377 Cow::Owned(format!("Large File Upload POST {endpoint}"))
1378 } else {
1379 Cow::Borrowed("Large File Upload POST [long endpoint]")
1380 };
1381
1382 let response = self
1383 .execute_with_retry(request_builder, operation_name)
1384 .await?;
1385
1386 if let Some(callback) = progress_callback {
1388 callback(file_size, file_size, 100.0);
1389 }
1390
1391 Ok(response)
1392 }
1393
1394 pub async fn upload_file_binary(
1418 &self,
1419 endpoint: &str,
1420 query_params: &[(&str, &str)],
1421 file_data: Vec<u8>,
1422 content_type: &str,
1423 ) -> Result<reqwest::Response, VeracodeError> {
1424 let url = self.build_url_with_params(endpoint, query_params);
1426
1427 let file_data = Bytes::from(file_data);
1430 let file_size = file_data.len();
1431
1432 let content_type_cow: Cow<str> = if content_type.len() < 64 {
1434 Cow::Borrowed(content_type)
1435 } else {
1436 Cow::Owned(content_type.to_string())
1437 };
1438
1439 let request_builder = || {
1441 let body_data = file_data.clone();
1443
1444 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1446 return self.client.post("invalid://url");
1447 };
1448
1449 self.client
1450 .post(&url)
1451 .header("Authorization", auth_header)
1452 .header("User-Agent", "Veracode Rust Client")
1453 .header("Content-Type", content_type_cow.as_ref())
1454 .header("Content-Length", file_size.to_string())
1455 .body(body_data)
1456 };
1457
1458 let operation_name: Cow<str> = if endpoint.len() < 50 {
1460 Cow::Owned(format!("Binary File Upload POST {endpoint}"))
1461 } else {
1462 Cow::Borrowed("Binary File Upload POST [long endpoint]")
1463 };
1464
1465 self.execute_with_retry(request_builder, operation_name)
1466 .await
1467 }
1468
1469 pub async fn upload_file_streaming(
1491 &self,
1492 endpoint: &str,
1493 query_params: &[(&str, &str)],
1494 file_path: &str,
1495 file_size: u64,
1496 content_type: &str,
1497 ) -> Result<reqwest::Response, VeracodeError> {
1498 let url = self.build_url_with_params(endpoint, query_params);
1500
1501 let file = TokioFile::open(file_path)
1503 .await
1504 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to open file: {e}")))?;
1505
1506 let stream = tokio_util::io::ReaderStream::new(file);
1508 let body = Body::wrap_stream(stream);
1509
1510 let auth_header = self.generate_auth_header("POST", &url)?;
1512
1513 let response = self
1516 .client
1517 .post(&url)
1518 .header("Authorization", auth_header)
1519 .header("User-Agent", "Veracode Rust Client")
1520 .header("Content-Type", content_type)
1521 .header("Content-Length", file_size.to_string())
1522 .body(body)
1523 .send()
1524 .await
1525 .map_err(VeracodeError::Http)?;
1526
1527 Ok(response)
1528 }
1529}
1530
1531#[cfg(test)]
1532#[allow(clippy::expect_used)] mod tests {
1534 use super::*;
1535 use proptest::prelude::*;
1536
1537 fn create_test_config() -> VeracodeConfig {
1543 use crate::{VeracodeCredentials, VeracodeRegion};
1544
1545 VeracodeConfig {
1546 credentials: VeracodeCredentials::new(
1547 "test_api_id".to_string(),
1548 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
1549 ),
1550 base_url: "https://api.veracode.com".to_string(),
1551 rest_base_url: "https://api.veracode.com".to_string(),
1552 xml_base_url: "https://analysiscenter.veracode.com".to_string(),
1553 region: VeracodeRegion::Commercial,
1554 validate_certificates: true,
1555 connect_timeout: 30,
1556 request_timeout: 300,
1557 proxy_url: None,
1558 proxy_username: None,
1559 proxy_password: None,
1560 retry_config: Default::default(),
1561 }
1562 }
1563
1564 proptest! {
1569 #![proptest_config(ProptestConfig {
1570 cases: if cfg!(miri) { 5 } else { 1000 },
1571 failure_persistence: None,
1572 .. ProptestConfig::default()
1573 })]
1574
1575 #[test]
1578 fn proptest_url_params_prevent_injection(
1579 key in "[a-zA-Z0-9_]{1,50}",
1580 value in ".*{0,100}",
1581 ) {
1582 let config = create_test_config();
1583 let client = VeracodeClient::new(config)
1584 .expect("valid test client configuration");
1585
1586 let params = vec![(key.as_str(), value.as_str())];
1587 let url = client.build_url_with_params("/api/test", ¶ms);
1588
1589 prop_assert!(!url.contains("<script>"));
1591 prop_assert!(!url.contains("javascript:"));
1592
1593 prop_assert!(url.starts_with("https://api.veracode.com/api/test"));
1595
1596 if !params.is_empty() && !key.is_empty() {
1598 prop_assert!(url.contains('?'));
1599 }
1600 }
1601
1602 #[test]
1605 fn proptest_url_params_capacity_safe(
1606 param_count in 0usize..=100,
1607 ) {
1608 let config = create_test_config();
1609 let client = VeracodeClient::new(config)
1610 .expect("valid test client configuration");
1611
1612 let params: Vec<(&str, &str)> = (0..param_count)
1614 .map(|_| ("key", "value"))
1615 .collect();
1616
1617 let url = client.build_url_with_params("/api/test", ¶ms);
1619
1620 prop_assert!(url.starts_with("https://"));
1622 prop_assert!(url.len() < 100000); }
1624
1625 #[test]
1627 fn proptest_url_params_empty_safe(
1628 key in "\\s*",
1629 value in "\\s*",
1630 ) {
1631 let config = create_test_config();
1632 let client = VeracodeClient::new(config)
1633 .expect("valid test client configuration");
1634
1635 let params = vec![(key.as_str(), value.as_str())];
1636 let url = client.build_url_with_params("/api/test", ¶ms);
1637
1638 prop_assert!(url.starts_with("https://"));
1640 }
1641 }
1642
1643 proptest! {
1648 #![proptest_config(ProptestConfig {
1649 cases: if cfg!(miri) { 5 } else { 1000 },
1650 failure_persistence: None,
1651 .. ProptestConfig::default()
1652 })]
1653
1654 #[test]
1657 fn proptest_hmac_invalid_urls_return_error(
1658 invalid_url in ".*{0,100}",
1659 ) {
1660 let config = create_test_config();
1661 let client = VeracodeClient::new(config)
1662 .expect("valid test client configuration");
1663
1664 let result = client.generate_hmac_signature(
1666 "GET",
1667 &invalid_url,
1668 1234567890000,
1669 "0123456789abcdef0123456789abcdef",
1670 );
1671
1672 match result {
1674 Ok(_) => {
1675 prop_assert!(Url::parse(&invalid_url).is_ok());
1677 },
1678 Err(e) => {
1679 prop_assert!(matches!(e, VeracodeError::Authentication(_)));
1681 }
1682 }
1683 }
1684
1685 #[test]
1688 fn proptest_hmac_deterministic(
1689 method in "[A-Z]{3,7}",
1690 timestamp in 1000000000000u64..2000000000000u64,
1691 ) {
1692 let config = create_test_config();
1693 let client = VeracodeClient::new(config)
1694 .expect("valid test client configuration");
1695
1696 let url = "https://api.veracode.com/api/test";
1697 let nonce = "0123456789abcdef0123456789abcdef";
1698
1699 let sig1 = client.generate_hmac_signature(&method, url, timestamp, nonce);
1700 let sig2 = client.generate_hmac_signature(&method, url, timestamp, nonce);
1701
1702 match (sig1, sig2) {
1704 (Ok(s1), Ok(s2)) => prop_assert_eq!(s1, s2),
1705 (Err(_), Err(_)) => {}, _ => prop_assert!(false, "Non-deterministic result"),
1707 }
1708 }
1709
1710 #[test]
1713 fn proptest_hmac_invalid_nonce_returns_error(
1714 invalid_nonce in "[^0-9a-fA-F]{1,32}",
1715 ) {
1716 let config = create_test_config();
1717 let client = VeracodeClient::new(config)
1718 .expect("valid test client configuration");
1719
1720 let result = client.generate_hmac_signature(
1721 "GET",
1722 "https://api.veracode.com/api/test",
1723 1234567890000,
1724 &invalid_nonce,
1725 );
1726
1727 prop_assert!(matches!(result, Err(VeracodeError::Authentication(_))));
1729 }
1730
1731 #[test]
1734 fn proptest_hmac_timestamp_safe(
1735 timestamp in any::<u64>(),
1736 ) {
1737 let config = create_test_config();
1738 let client = VeracodeClient::new(config)
1739 .expect("valid test client configuration");
1740
1741 let url = "https://api.veracode.com/api/test";
1742 let nonce = "0123456789abcdef0123456789abcdef";
1743
1744 let result = client.generate_hmac_signature("GET", url, timestamp, nonce);
1746
1747 prop_assert!(result.is_ok() || result.is_err());
1749 }
1750 }
1751
1752 proptest! {
1757 #![proptest_config(ProptestConfig {
1758 cases: if cfg!(miri) { 5 } else { 1000 },
1759 failure_persistence: None,
1760 .. ProptestConfig::default()
1761 })]
1762
1763 #[test]
1766 fn proptest_auth_header_format(
1767 method in "[A-Z]{3,7}",
1768 ) {
1769 let config = create_test_config();
1770 let client = VeracodeClient::new(config)
1771 .expect("valid test client configuration");
1772
1773 let url = "https://api.veracode.com/api/test";
1774 let result = client.generate_auth_header(&method, url);
1775
1776 if let Ok(header) = result {
1777 prop_assert!(header.starts_with("VERACODE-HMAC-SHA-256"));
1779
1780 prop_assert!(header.contains("id="));
1782 prop_assert!(header.contains("ts="));
1783 prop_assert!(header.contains("nonce="));
1784 prop_assert!(header.contains("sig="));
1785
1786 let parts: Vec<&str> = header.split(',').collect();
1788 prop_assert_eq!(parts.len(), 4);
1789 }
1790 }
1791
1792 #[test]
1795 fn proptest_auth_header_nonce_unique(
1796 _seed in any::<u8>(),
1797 ) {
1798 let config = create_test_config();
1799 let client = VeracodeClient::new(config)
1800 .expect("valid test client configuration");
1801
1802 let url = "https://api.veracode.com/api/test";
1803
1804 let header1 = client.generate_auth_header("GET", url)
1806 .expect("valid auth header generation");
1807 let header2 = client.generate_auth_header("GET", url)
1808 .expect("valid auth header generation");
1809
1810 fn extract_nonce(h: &str) -> Option<String> {
1812 Some(h.split("nonce=")
1813 .nth(1)?
1814 .split(',')
1815 .next()?
1816 .to_string())
1817 }
1818
1819 if let (Some(nonce1), Some(nonce2)) = (extract_nonce(&header1), extract_nonce(&header2)) {
1820 prop_assert_ne!(&nonce1, &nonce2);
1823
1824 prop_assert_eq!(nonce1.len(), 32);
1826 prop_assert_eq!(nonce2.len(), 32);
1827 prop_assert!(nonce1.chars().all(|c| c.is_ascii_hexdigit()));
1828 prop_assert!(nonce2.chars().all(|c| c.is_ascii_hexdigit()));
1829 }
1830 }
1831 }
1832
1833 proptest! {
1838 #![proptest_config(ProptestConfig {
1839 cases: if cfg!(miri) { 5 } else { 100 },
1840 failure_persistence: None,
1841 .. ProptestConfig::default()
1842 })]
1843
1844 #[test]
1847 fn proptest_client_creation_invalid_proxy(
1848 invalid_proxy in ".*{0,100}",
1849 ) {
1850 use crate::{VeracodeCredentials, VeracodeRegion};
1851
1852 let config = VeracodeConfig {
1853 credentials: VeracodeCredentials::new(
1854 "test_api_id".to_string(),
1855 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
1856 ),
1857 base_url: "https://api.veracode.com".to_string(),
1858 rest_base_url: "https://api.veracode.com".to_string(),
1859 xml_base_url: "https://analysiscenter.veracode.com".to_string(),
1860 region: VeracodeRegion::Commercial,
1861 validate_certificates: true,
1862 connect_timeout: 30,
1863 request_timeout: 300,
1864 proxy_url: Some(invalid_proxy.clone()),
1865 proxy_username: None,
1866 proxy_password: None,
1867 retry_config: Default::default(),
1868 };
1869
1870 let result = VeracodeClient::new(config);
1871
1872 match result {
1874 Ok(_) => {
1875 prop_assert!(reqwest::Proxy::all(&invalid_proxy).is_ok());
1877 },
1878 Err(e) => {
1879 prop_assert!(matches!(e, VeracodeError::InvalidConfig(_)));
1881 }
1882 }
1883 }
1884
1885 #[test]
1888 fn proptest_client_timeouts_safe(
1889 connect_timeout in 1u64..=3600,
1890 request_timeout in 1u64..=7200,
1891 ) {
1892 use crate::{VeracodeCredentials, VeracodeRegion};
1893
1894 let config = VeracodeConfig {
1895 credentials: VeracodeCredentials::new(
1896 "test_api_id".to_string(),
1897 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
1898 ),
1899 base_url: "https://api.veracode.com".to_string(),
1900 rest_base_url: "https://api.veracode.com".to_string(),
1901 xml_base_url: "https://analysiscenter.veracode.com".to_string(),
1902 region: VeracodeRegion::Commercial,
1903 validate_certificates: true,
1904 connect_timeout,
1905 request_timeout,
1906 proxy_url: None,
1907 proxy_username: None,
1908 proxy_password: None,
1909 retry_config: Default::default(),
1910 };
1911
1912 let result = VeracodeClient::new(config);
1914 prop_assert!(result.is_ok());
1915 }
1916 }
1917
1918 proptest! {
1923 #![proptest_config(ProptestConfig {
1924 cases: if cfg!(miri) { 5 } else { 100 },
1925 failure_persistence: None,
1926 .. ProptestConfig::default()
1927 })]
1928
1929 #[test]
1932 fn proptest_file_upload_capacity_safe(
1933 file_size in 0usize..=1000000,
1934 ) {
1935 let file_data = vec![0u8; file_size];
1937
1938 let file_data_arc = Arc::new(file_data);
1940
1941 prop_assert_eq!(file_data_arc.len(), file_size);
1943
1944 let clone1 = Arc::clone(&file_data_arc);
1946 let clone2 = Arc::clone(&file_data_arc);
1947 prop_assert_eq!(clone1.len(), file_size);
1948 prop_assert_eq!(clone2.len(), file_size);
1949 }
1950
1951 #[test]
1954 fn proptest_content_type_safe(
1955 content_type in ".*{0,200}",
1956 ) {
1957 let content_type_cow: Cow<str> = if content_type.len() < 64 {
1959 Cow::Borrowed(&content_type)
1960 } else {
1961 Cow::Owned(content_type.clone())
1962 };
1963
1964 let ct_lower = content_type_cow.to_lowercase();
1966 if ct_lower.contains("<script>") || ct_lower.contains("javascript:") {
1967 prop_assert!(content_type_cow.as_ref().contains("<script>") ||
1969 content_type_cow.as_ref().contains("javascript:"));
1970 }
1971
1972 prop_assert_eq!(content_type_cow.len(), content_type.len());
1974 }
1975 }
1976
1977 #[test]
1982 fn test_hmac_signature_with_query_params() {
1983 let config = create_test_config();
1984 let client = VeracodeClient::new(config).expect("valid test client configuration");
1985
1986 let url = "https://api.veracode.com/api/test?param1=value1¶m2=value2";
1988 let nonce = "0123456789abcdef0123456789abcdef";
1989 let timestamp = 1234567890000;
1990
1991 let result = client.generate_hmac_signature("GET", url, timestamp, nonce);
1992 assert!(result.is_ok());
1993
1994 let signature = result.expect("valid HMAC signature");
1995 assert_eq!(signature.len(), 64);
1997 assert!(signature.chars().all(|c| c.is_ascii_hexdigit()));
1998 }
1999
2000 #[test]
2001 fn test_hmac_signature_different_methods() {
2002 let config = create_test_config();
2003 let client = VeracodeClient::new(config).expect("valid test client configuration");
2004
2005 let url = "https://api.veracode.com/api/test";
2006 let nonce = "0123456789abcdef0123456789abcdef";
2007 let timestamp = 1234567890000;
2008
2009 let sig_get = client
2010 .generate_hmac_signature("GET", url, timestamp, nonce)
2011 .expect("valid HMAC signature for GET");
2012 let sig_post = client
2013 .generate_hmac_signature("POST", url, timestamp, nonce)
2014 .expect("valid HMAC signature for POST");
2015
2016 assert_ne!(sig_get, sig_post);
2018 }
2019
2020 #[test]
2021 fn test_url_encoding_special_characters() {
2022 let config = create_test_config();
2023 let client = VeracodeClient::new(config).expect("valid test client configuration");
2024
2025 let params = vec![
2027 ("key1", "value with spaces"),
2028 ("key2", "value&with&ersands"),
2029 ("key3", "value=with=equals"),
2030 ("key4", "value?with?questions"),
2031 ];
2032
2033 let url = client.build_url_with_params("/api/test", ¶ms);
2034
2035 assert!(url.contains("value%20with%20spaces") || url.contains("value+with+spaces"));
2037 assert!(url.contains("%26"));
2039 assert!(url.starts_with("https://api.veracode.com/api/test?"));
2041 }
2042
2043 #[test]
2044 fn test_url_encoding_unicode() {
2045 let config = create_test_config();
2046 let client = VeracodeClient::new(config).expect("valid test client configuration");
2047
2048 let params = vec![
2050 ("key", "δ½ ε₯½δΈη"), ("key2", "ππ‘οΈ"), ];
2053
2054 let url = client.build_url_with_params("/api/test", ¶ms);
2055
2056 assert!(url.starts_with("https://api.veracode.com/api/test?"));
2058 assert!(url.contains('%'));
2060 }
2061
2062 #[test]
2063 fn test_empty_query_params() {
2064 let config = create_test_config();
2065 let client = VeracodeClient::new(config).expect("valid test client configuration");
2066
2067 let url = client.build_url_with_params("/api/test", &[]);
2068
2069 assert_eq!(url, "https://api.veracode.com/api/test");
2071 }
2072
2073 #[test]
2074 fn test_invalid_api_key_format() {
2075 use crate::{VeracodeCredentials, VeracodeRegion};
2076
2077 let config = VeracodeConfig {
2079 credentials: VeracodeCredentials::new(
2080 "test_api_id".to_string(),
2081 "not_valid_hex_key".to_string(),
2082 ),
2083 base_url: "https://api.veracode.com".to_string(),
2084 rest_base_url: "https://api.veracode.com".to_string(),
2085 xml_base_url: "https://analysiscenter.veracode.com".to_string(),
2086 region: VeracodeRegion::Commercial,
2087 validate_certificates: true,
2088 connect_timeout: 30,
2089 request_timeout: 300,
2090 proxy_url: None,
2091 proxy_username: None,
2092 proxy_password: None,
2093 retry_config: Default::default(),
2094 };
2095
2096 let client = VeracodeClient::new(config).expect("valid test client configuration");
2097 let result = client.generate_auth_header("GET", "https://api.veracode.com/api/test");
2098
2099 assert!(matches!(result, Err(VeracodeError::Authentication(_))));
2101 }
2102
2103 #[test]
2104 fn test_auth_header_format() {
2105 let config = create_test_config();
2106 let client = VeracodeClient::new(config).expect("valid test client configuration");
2107
2108 let header = client
2109 .generate_auth_header("GET", "https://api.veracode.com/api/test")
2110 .expect("valid auth header generation");
2111
2112 assert!(header.starts_with("VERACODE-HMAC-SHA-256 "));
2114 assert!(header.contains("id=test_api_id"));
2115 assert!(header.contains("ts="));
2116 assert!(header.contains("nonce="));
2117 assert!(header.contains("sig="));
2118
2119 let parts: Vec<&str> = header.split(',').collect();
2121 assert_eq!(parts.len(), 4);
2122 }
2123
2124 #[cfg(not(miri))] #[test]
2126 fn test_auth_header_timestamp_monotonic() {
2127 let config = create_test_config();
2128 let client = VeracodeClient::new(config).expect("valid test client configuration");
2129
2130 let header1 = client
2131 .generate_auth_header("GET", "https://api.veracode.com/api/test")
2132 .expect("valid auth header generation");
2133 std::thread::sleep(std::time::Duration::from_millis(10));
2134 let header2 = client
2135 .generate_auth_header("GET", "https://api.veracode.com/api/test")
2136 .expect("valid auth header generation");
2137
2138 let extract_ts =
2140 |h: &str| -> Option<u64> { h.split("ts=").nth(1)?.split(',').next()?.parse().ok() };
2141
2142 let ts1 = extract_ts(&header1).expect("valid timestamp extraction");
2143 let ts2 = extract_ts(&header2).expect("valid timestamp extraction");
2144
2145 assert!(ts2 >= ts1);
2147 }
2148
2149 #[test]
2150 fn test_base_url_accessor() {
2151 let config = create_test_config();
2152 let client = VeracodeClient::new(config).expect("valid test client configuration");
2153
2154 assert_eq!(client.base_url(), "https://api.veracode.com");
2155 }
2156
2157 #[test]
2158 fn test_client_clone() {
2159 let config = create_test_config();
2160 let client1 = VeracodeClient::new(config).expect("valid test client configuration");
2161 let client2 = client1.clone();
2162
2163 assert_eq!(client1.base_url(), client2.base_url());
2165 }
2166
2167 #[test]
2168 fn test_url_capacity_estimation() {
2169 let config = create_test_config();
2170 let client = VeracodeClient::new(config).expect("valid test client configuration");
2171
2172 let params: Vec<(&str, &str)> = (0..100).map(|_| ("key", "value")).collect();
2174
2175 let url = client.build_url_with_params("/api/test", ¶ms);
2176
2177 assert!(url.starts_with("https://api.veracode.com/api/test?"));
2179 assert!(url.len() > 100); }
2181
2182 #[test]
2183 fn test_saturating_arithmetic() {
2184 let config = create_test_config();
2185 let client = VeracodeClient::new(config).expect("valid test client configuration");
2186
2187 let params: Vec<(&str, &str)> = vec![("k", "v"); 1000];
2189
2190 let url = client.build_url_with_params("/api/test", ¶ms);
2192 assert!(url.len() < usize::MAX);
2193 }
2194}