1use hex;
7use hmac::{Hmac, Mac};
8use log::{info, warn};
9use reqwest::{Client, multipart};
10use secrecy::ExposeSecret;
11use serde::Serialize;
12use sha2::Sha256;
13use std::borrow::Cow;
14use std::collections::HashMap;
15use std::fs::File;
16use std::io::{Read, Seek, SeekFrom};
17use std::sync::Arc;
18use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
19use url::Url;
20
21use crate::json_validator::{MAX_JSON_DEPTH, validate_json_depth};
22use crate::{VeracodeConfig, VeracodeError};
23
24type HmacSha256 = Hmac<Sha256>;
26
27const INVALID_URL_MSG: &str = "Invalid URL";
29const INVALID_API_KEY_MSG: &str = "Invalid API key format - must be hex string";
30const INVALID_NONCE_MSG: &str = "Invalid nonce format";
31const HMAC_CREATION_FAILED_MSG: &str = "Failed to create HMAC";
32
33#[derive(Clone)]
38pub struct VeracodeClient {
39 config: VeracodeConfig,
40 client: Client,
41}
42
43impl VeracodeClient {
44 fn build_url_with_params(&self, endpoint: &str, query_params: &[(&str, &str)]) -> String {
46 let estimated_capacity = self
48 .config
49 .base_url
50 .len()
51 .saturating_add(endpoint.len())
52 .saturating_add(query_params.len().saturating_mul(32)); let mut url = String::with_capacity(estimated_capacity);
55 url.push_str(&self.config.base_url);
56 url.push_str(endpoint);
57
58 if !query_params.is_empty() {
59 url.push('?');
60 for (i, (key, value)) in query_params.iter().enumerate() {
61 if i > 0 {
62 url.push('&');
63 }
64 url.push_str(&urlencoding::encode(key));
65 url.push('=');
66 url.push_str(&urlencoding::encode(value));
67 }
68 }
69
70 url
71 }
72
73 pub fn new(config: VeracodeConfig) -> Result<Self, VeracodeError> {
88 let mut client_builder = Client::builder();
89
90 if !config.validate_certificates {
92 client_builder = client_builder
93 .danger_accept_invalid_certs(true)
94 .danger_accept_invalid_hostnames(true);
95 }
96
97 client_builder = client_builder
99 .connect_timeout(Duration::from_secs(config.connect_timeout))
100 .timeout(Duration::from_secs(config.request_timeout));
101
102 if let Some(proxy_url) = &config.proxy_url {
104 let mut proxy = reqwest::Proxy::all(proxy_url)
105 .map_err(|e| VeracodeError::InvalidConfig(format!("Invalid proxy URL: {e}")))?;
106
107 if let (Some(username), Some(password)) =
109 (&config.proxy_username, &config.proxy_password)
110 {
111 proxy = proxy.basic_auth(username.expose_secret(), password.expose_secret());
112 }
113
114 client_builder = client_builder.proxy(proxy);
115 }
116
117 let client = client_builder.build().map_err(VeracodeError::Http)?;
118 Ok(Self { config, client })
119 }
120
121 #[must_use]
123 pub fn base_url(&self) -> &str {
124 &self.config.base_url
125 }
126
127 #[must_use]
129 pub fn config(&self) -> &VeracodeConfig {
130 &self.config
131 }
132
133 #[must_use]
135 pub fn client(&self) -> &Client {
136 &self.client
137 }
138
139 async fn execute_with_retry<F>(
165 &self,
166 request_builder: F,
167 operation_name: Cow<'_, str>,
168 ) -> Result<reqwest::Response, VeracodeError>
169 where
170 F: Fn() -> reqwest::RequestBuilder,
171 {
172 let retry_config = &self.config.retry_config;
173 let start_time = Instant::now();
174 let mut total_delay = std::time::Duration::from_millis(0);
175
176 if retry_config.max_attempts == 0 {
178 return match request_builder().send().await {
179 Ok(response) => Ok(response),
180 Err(e) => Err(VeracodeError::Http(e)),
181 };
182 }
183
184 let mut last_error = None;
185 let mut rate_limit_attempts: u32 = 0;
186
187 for attempt in 1..=retry_config.max_attempts.saturating_add(1) {
188 match request_builder().send().await {
190 Ok(response) => {
191 if response.status().as_u16() == 429 {
193 let retry_after_seconds = response
195 .headers()
196 .get("retry-after")
197 .and_then(|h| h.to_str().ok())
198 .and_then(|s| s.parse::<u64>().ok());
199
200 let message = "HTTP 429: Rate limit exceeded".to_string();
201 let veracode_error = VeracodeError::RateLimited {
202 retry_after_seconds,
203 message,
204 };
205
206 rate_limit_attempts = rate_limit_attempts.saturating_add(1);
208
209 if attempt > retry_config.max_attempts
211 || rate_limit_attempts > retry_config.rate_limit_max_attempts
212 {
213 last_error = Some(veracode_error);
214 break;
215 }
216
217 let delay = retry_config.calculate_rate_limit_delay(retry_after_seconds);
219 total_delay = total_delay.saturating_add(delay);
220
221 if total_delay.as_millis() > retry_config.max_total_delay_ms as u128 {
223 let msg = format!(
224 "{} exceeded maximum total retry time of {}ms after {} attempts",
225 operation_name, retry_config.max_total_delay_ms, attempt
226 );
227 last_error = Some(VeracodeError::RetryExhausted(msg));
228 break;
229 }
230
231 let wait_time = match retry_after_seconds {
233 Some(seconds) => format!("{seconds}s (from Retry-After header)"),
234 None => format!("{}s (until next minute window)", delay.as_secs()),
235 };
236 warn!(
237 "π¦ {operation_name} rate limited on attempt {attempt}, waiting {wait_time}"
238 );
239
240 tokio::time::sleep(delay).await;
242 last_error = Some(veracode_error);
243 continue;
244 }
245
246 if attempt > 1 {
247 info!("β
{operation_name} succeeded on attempt {attempt}");
249 }
250 return Ok(response);
251 }
252 Err(e) => {
253 let veracode_error = VeracodeError::Http(e);
255
256 if attempt > retry_config.max_attempts
258 || !retry_config.is_retryable_error(&veracode_error)
259 {
260 last_error = Some(veracode_error);
261 break;
262 }
263
264 let delay = retry_config.calculate_delay(attempt);
266 total_delay = total_delay.saturating_add(delay);
267
268 if total_delay.as_millis() > retry_config.max_total_delay_ms as u128 {
270 let msg = format!(
272 "{} exceeded maximum total retry time of {}ms after {} attempts",
273 operation_name, retry_config.max_total_delay_ms, attempt
274 );
275 last_error = Some(VeracodeError::RetryExhausted(msg));
276 break;
277 }
278
279 warn!(
281 "β οΈ {operation_name} failed on attempt {attempt}, retrying in {}ms: {veracode_error}",
282 delay.as_millis()
283 );
284
285 tokio::time::sleep(delay).await;
287 last_error = Some(veracode_error);
288 }
289 }
290 }
291
292 match last_error {
294 Some(error) => {
295 let elapsed = start_time.elapsed();
296 match error {
297 VeracodeError::RetryExhausted(_) => Err(error),
298 VeracodeError::Http(_)
299 | VeracodeError::Serialization(_)
300 | VeracodeError::Authentication(_)
301 | VeracodeError::InvalidResponse(_)
302 | VeracodeError::InvalidConfig(_)
303 | VeracodeError::NotFound(_)
304 | VeracodeError::RateLimited { .. }
305 | VeracodeError::Validation(_) => {
306 let msg = format!(
307 "{} failed after {} attempts over {}ms: {}",
308 operation_name,
309 retry_config.max_attempts.saturating_add(1),
310 elapsed.as_millis(),
311 error
312 );
313 Err(VeracodeError::RetryExhausted(msg))
314 }
315 }
316 }
317 None => {
318 let msg = format!(
319 "{} failed after {} attempts with unknown error",
320 operation_name,
321 retry_config.max_attempts.saturating_add(1)
322 );
323 Err(VeracodeError::RetryExhausted(msg))
324 }
325 }
326 }
327
328 fn generate_hmac_signature(
330 &self,
331 method: &str,
332 url: &str,
333 timestamp: u64,
334 nonce: &str,
335 ) -> Result<String, VeracodeError> {
336 let url_parsed = Url::parse(url)
337 .map_err(|_| VeracodeError::Authentication(INVALID_URL_MSG.to_string()))?;
338
339 let path_and_query = match url_parsed.query() {
340 Some(query) => format!("{}?{}", url_parsed.path(), query),
341 None => url_parsed.path().to_string(),
342 };
343
344 let host = url_parsed.host_str().unwrap_or("");
345
346 let data = format!(
349 "id={}&host={}&url={}&method={}",
350 self.config.credentials.expose_api_id(),
351 host,
352 path_and_query,
353 method
354 );
355
356 let timestamp_str = timestamp.to_string();
357 let ver_str = "vcode_request_version_1";
358
359 let key_bytes = hex::decode(self.config.credentials.expose_api_key())
361 .map_err(|_| VeracodeError::Authentication(INVALID_API_KEY_MSG.to_string()))?;
362
363 let nonce_bytes = hex::decode(nonce)
364 .map_err(|_| VeracodeError::Authentication(INVALID_NONCE_MSG.to_string()))?;
365
366 let mut mac1 = HmacSha256::new_from_slice(&key_bytes)
368 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
369 mac1.update(&nonce_bytes);
370 let hashed_nonce = mac1.finalize().into_bytes();
371
372 let mut mac2 = HmacSha256::new_from_slice(&hashed_nonce)
374 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
375 mac2.update(timestamp_str.as_bytes());
376 let hashed_timestamp = mac2.finalize().into_bytes();
377
378 let mut mac3 = HmacSha256::new_from_slice(&hashed_timestamp)
380 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
381 mac3.update(ver_str.as_bytes());
382 let hashed_ver_str = mac3.finalize().into_bytes();
383
384 let mut mac4 = HmacSha256::new_from_slice(&hashed_ver_str)
386 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
387 mac4.update(data.as_bytes());
388 let signature = mac4.finalize().into_bytes();
389
390 Ok(hex::encode(signature).to_lowercase())
392 }
393
394 pub fn generate_auth_header(&self, method: &str, url: &str) -> Result<String, VeracodeError> {
401 #[allow(clippy::cast_possible_truncation)]
402 let timestamp = SystemTime::now()
403 .duration_since(UNIX_EPOCH)
404 .map_err(|e| VeracodeError::Authentication(format!("System time error: {e}")))?
405 .as_millis() as u64; let nonce_bytes: [u8; 16] = rand::random();
409 let nonce = hex::encode(nonce_bytes);
410
411 let signature = self.generate_hmac_signature(method, url, timestamp, &nonce)?;
412
413 Ok(format!(
414 "VERACODE-HMAC-SHA-256 id={},ts={},nonce={},sig={}",
415 self.config.credentials.expose_api_id(),
416 timestamp,
417 nonce,
418 signature
419 ))
420 }
421
422 pub async fn get(
438 &self,
439 endpoint: &str,
440 query_params: Option<&[(String, String)]>,
441 ) -> Result<reqwest::Response, VeracodeError> {
442 let param_count = query_params.map_or(0, |p| p.len());
444 let estimated_capacity = self
445 .config
446 .base_url
447 .len()
448 .saturating_add(endpoint.len())
449 .saturating_add(param_count.saturating_mul(32));
450 let mut url = String::with_capacity(estimated_capacity);
451 url.push_str(&self.config.base_url);
452 url.push_str(endpoint);
453
454 if let Some(params) = query_params
455 && !params.is_empty()
456 {
457 url.push('?');
458 for (i, (key, value)) in params.iter().enumerate() {
459 if i > 0 {
460 url.push('&');
461 }
462 url.push_str(key);
463 url.push('=');
464 url.push_str(value);
465 }
466 }
467
468 let request_builder = || {
470 let Ok(auth_header) = self.generate_auth_header("GET", &url) else {
472 return self.client.get("invalid://url");
473 };
474
475 self.client
476 .get(&url)
477 .header("Authorization", auth_header)
478 .header("Content-Type", "application/json")
479 };
480
481 let operation_name = if endpoint.len() < 50 {
483 Cow::Owned(format!("GET {endpoint}"))
484 } else {
485 Cow::Borrowed("GET [long endpoint]")
486 };
487 self.execute_with_retry(request_builder, operation_name)
488 .await
489 }
490
491 pub async fn post<T: Serialize>(
507 &self,
508 endpoint: &str,
509 body: Option<&T>,
510 ) -> Result<reqwest::Response, VeracodeError> {
511 let mut url =
512 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
513 url.push_str(&self.config.base_url);
514 url.push_str(endpoint);
515
516 let serialized_body = if let Some(body) = body {
518 Some(serde_json::to_string(body)?)
519 } else {
520 None
521 };
522
523 let request_builder = || {
525 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
527 return self.client.post("invalid://url");
528 };
529
530 let mut request = self
531 .client
532 .post(&url)
533 .header("Authorization", auth_header)
534 .header("Content-Type", "application/json");
535
536 if let Some(ref body_str) = serialized_body {
537 request = request.body(body_str.clone());
538 }
539
540 request
541 };
542
543 let operation_name = if endpoint.len() < 50 {
544 Cow::Owned(format!("POST {endpoint}"))
545 } else {
546 Cow::Borrowed("POST [long endpoint]")
547 };
548 self.execute_with_retry(request_builder, operation_name)
549 .await
550 }
551
552 pub async fn put<T: Serialize>(
568 &self,
569 endpoint: &str,
570 body: Option<&T>,
571 ) -> Result<reqwest::Response, VeracodeError> {
572 let mut url =
573 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
574 url.push_str(&self.config.base_url);
575 url.push_str(endpoint);
576
577 let serialized_body = if let Some(body) = body {
579 Some(serde_json::to_string(body)?)
580 } else {
581 None
582 };
583
584 let request_builder = || {
586 let Ok(auth_header) = self.generate_auth_header("PUT", &url) else {
588 return self.client.put("invalid://url");
589 };
590
591 let mut request = self
592 .client
593 .put(&url)
594 .header("Authorization", auth_header)
595 .header("Content-Type", "application/json");
596
597 if let Some(ref body_str) = serialized_body {
598 request = request.body(body_str.clone());
599 }
600
601 request
602 };
603
604 let operation_name = if endpoint.len() < 50 {
605 Cow::Owned(format!("PUT {endpoint}"))
606 } else {
607 Cow::Borrowed("PUT [long endpoint]")
608 };
609 self.execute_with_retry(request_builder, operation_name)
610 .await
611 }
612
613 pub async fn delete(&self, endpoint: &str) -> Result<reqwest::Response, VeracodeError> {
628 let mut url =
629 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
630 url.push_str(&self.config.base_url);
631 url.push_str(endpoint);
632
633 let request_builder = || {
635 let Ok(auth_header) = self.generate_auth_header("DELETE", &url) else {
637 return self.client.delete("invalid://url");
638 };
639
640 self.client
641 .delete(&url)
642 .header("Authorization", auth_header)
643 .header("Content-Type", "application/json")
644 };
645
646 let operation_name = if endpoint.len() < 50 {
647 Cow::Owned(format!("DELETE {endpoint}"))
648 } else {
649 Cow::Borrowed("DELETE [long endpoint]")
650 };
651 self.execute_with_retry(request_builder, operation_name)
652 .await
653 }
654
655 pub async fn handle_response(
678 response: reqwest::Response,
679 context: &str,
680 ) -> Result<reqwest::Response, VeracodeError> {
681 if !response.status().is_success() {
682 let status = response.status();
683 let url = response.url().clone();
684 let error_text = response.text().await?;
685 return Err(VeracodeError::InvalidResponse(format!(
686 "Failed to {context}\n URL: {url}\n HTTP {status}: {error_text}"
687 )));
688 }
689 Ok(response)
690 }
691
692 pub async fn get_with_query(
710 &self,
711 endpoint: &str,
712 query_params: Option<Vec<(String, String)>>,
713 ) -> Result<reqwest::Response, VeracodeError> {
714 let query_slice = query_params.as_deref();
715 let response = self.get(endpoint, query_slice).await?;
716 Self::handle_response(response, &format!("GET {endpoint}")).await
717 }
718
719 pub async fn post_with_response<T: Serialize>(
735 &self,
736 endpoint: &str,
737 body: Option<&T>,
738 ) -> Result<reqwest::Response, VeracodeError> {
739 let response = self.post(endpoint, body).await?;
740 Self::handle_response(response, &format!("POST {endpoint}")).await
741 }
742
743 pub async fn put_with_response<T: Serialize>(
759 &self,
760 endpoint: &str,
761 body: Option<&T>,
762 ) -> Result<reqwest::Response, VeracodeError> {
763 let response = self.put(endpoint, body).await?;
764 Self::handle_response(response, &format!("PUT {endpoint}")).await
765 }
766
767 pub async fn delete_with_response(
782 &self,
783 endpoint: &str,
784 ) -> Result<reqwest::Response, VeracodeError> {
785 let response = self.delete(endpoint).await?;
786 Self::handle_response(response, &format!("DELETE {endpoint}")).await
787 }
788
789 pub async fn get_paginated(
809 &self,
810 endpoint: &str,
811 base_query_params: Option<Vec<(String, String)>>,
812 page_size: Option<u32>,
813 ) -> Result<String, VeracodeError> {
814 let size = page_size.unwrap_or(500);
815 let mut page: u32 = 0;
816 let mut all_items = Vec::new();
817 let mut page_info = None;
818
819 loop {
820 let mut query_params = base_query_params.clone().unwrap_or_default();
821 query_params.push(("page".to_string(), page.to_string()));
822 query_params.push(("size".to_string(), size.to_string()));
823
824 let response = self.get_with_query(endpoint, Some(query_params)).await?;
825 let response_text = response.text().await?;
826
827 validate_json_depth(&response_text, MAX_JSON_DEPTH).map_err(|e| {
829 VeracodeError::InvalidResponse(format!("JSON validation failed: {}", e))
830 })?;
831
832 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
834 if let Some(embedded) = json_value.get("_embedded") {
836 if let Some(items_array) =
837 embedded.as_object().and_then(|obj| obj.values().next())
838 && let Some(items) = items_array.as_array()
839 {
840 if items.is_empty() {
841 break; }
843 all_items.extend(items.clone());
844 }
845 } else if let Some(items) = json_value.as_array() {
846 if items.is_empty() {
848 break;
849 }
850 all_items.extend(items.clone());
851 } else {
852 return Ok(response_text);
854 }
855
856 if let Some(page_obj) = json_value.get("page") {
858 page_info = Some(page_obj.clone());
859 if let (Some(current), Some(total)) = (
860 page_obj.get("number").and_then(|n| n.as_u64()),
861 page_obj.get("totalPages").and_then(|n| n.as_u64()),
862 ) && current.saturating_add(1) >= total
863 {
864 break; }
866 }
867 } else {
868 return Ok(response_text);
870 }
871
872 page = page.saturating_add(1);
873
874 if page > 100 {
876 break;
877 }
878 }
879
880 let combined_response = if let Some(page_info) = page_info {
882 serde_json::json!({
884 "_embedded": {
885 "roles": all_items },
887 "page": page_info
888 })
889 } else {
890 serde_json::Value::Array(all_items)
892 };
893
894 Ok(combined_response.to_string())
895 }
896
897 pub async fn get_with_params(
913 &self,
914 endpoint: &str,
915 params: &[(&str, &str)],
916 ) -> Result<reqwest::Response, VeracodeError> {
917 let mut url =
918 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
919 url.push_str(&self.config.base_url);
920 url.push_str(endpoint);
921 let mut request_url =
922 Url::parse(&url).map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
923
924 if !params.is_empty() {
926 let mut query_pairs = request_url.query_pairs_mut();
927 for (key, value) in params {
928 query_pairs.append_pair(key, value);
929 }
930 }
931
932 let auth_header = self.generate_auth_header("GET", request_url.as_str())?;
933
934 let response = self
935 .client
936 .get(request_url)
937 .header("Authorization", auth_header)
938 .header("User-Agent", "Veracode Rust Client")
939 .send()
940 .await?;
941
942 Ok(response)
943 }
944
945 pub async fn post_form(
961 &self,
962 endpoint: &str,
963 params: &[(&str, &str)],
964 ) -> Result<reqwest::Response, VeracodeError> {
965 let mut url =
966 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
967 url.push_str(&self.config.base_url);
968 url.push_str(endpoint);
969
970 let form_data: Vec<(&str, &str)> = params.to_vec();
972
973 let auth_header = self.generate_auth_header("POST", &url)?;
974
975 let response = self
976 .client
977 .post(&url)
978 .header("Authorization", auth_header)
979 .header("User-Agent", "Veracode Rust Client")
980 .form(&form_data)
981 .send()
982 .await?;
983
984 Ok(response)
985 }
986
987 pub async fn upload_file_multipart(
1006 &self,
1007 endpoint: &str,
1008 params: HashMap<&str, &str>,
1009 file_field_name: &str,
1010 filename: &str,
1011 file_data: Vec<u8>,
1012 ) -> Result<reqwest::Response, VeracodeError> {
1013 let mut url =
1014 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
1015 url.push_str(&self.config.base_url);
1016 url.push_str(endpoint);
1017
1018 let mut form = multipart::Form::new();
1020
1021 for (key, value) in params {
1023 form = form.text(key.to_string(), value.to_string());
1024 }
1025
1026 let part = multipart::Part::bytes(file_data)
1028 .file_name(filename.to_string())
1029 .mime_str("application/octet-stream")
1030 .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
1031
1032 form = form.part(file_field_name.to_string(), part);
1033
1034 let auth_header = self.generate_auth_header("POST", &url)?;
1035
1036 let response = self
1037 .client
1038 .post(&url)
1039 .header("Authorization", auth_header)
1040 .header("User-Agent", "Veracode Rust Client")
1041 .multipart(form)
1042 .send()
1043 .await?;
1044
1045 Ok(response)
1046 }
1047
1048 pub async fn upload_file_multipart_put(
1067 &self,
1068 url: &str,
1069 file_field_name: &str,
1070 filename: &str,
1071 file_data: Vec<u8>,
1072 additional_headers: Option<HashMap<&str, &str>>,
1073 ) -> Result<reqwest::Response, VeracodeError> {
1074 let part = multipart::Part::bytes(file_data)
1076 .file_name(filename.to_string())
1077 .mime_str("application/octet-stream")
1078 .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
1079
1080 let form = multipart::Form::new().part(file_field_name.to_string(), part);
1081
1082 let auth_header = self.generate_auth_header("PUT", url)?;
1083
1084 let mut request = self
1085 .client
1086 .put(url)
1087 .header("Authorization", auth_header)
1088 .header("User-Agent", "Veracode Rust Client")
1089 .multipart(form);
1090
1091 if let Some(headers) = additional_headers {
1093 for (key, value) in headers {
1094 request = request.header(key, value);
1095 }
1096 }
1097
1098 let response = request.send().await?;
1099 Ok(response)
1100 }
1101
1102 pub async fn upload_file_with_query_params(
1127 &self,
1128 endpoint: &str,
1129 query_params: &[(&str, &str)],
1130 file_field_name: &str,
1131 filename: &str,
1132 file_data: Vec<u8>,
1133 ) -> Result<reqwest::Response, VeracodeError> {
1134 let url = self.build_url_with_params(endpoint, query_params);
1136
1137 let file_data_arc = Arc::new(file_data);
1139
1140 let filename_cow: Cow<str> = if filename.len() < 128 {
1142 Cow::Borrowed(filename)
1143 } else {
1144 Cow::Owned(filename.to_string())
1145 };
1146
1147 let field_name_cow: Cow<str> = if file_field_name.len() < 32 {
1148 Cow::Borrowed(file_field_name)
1149 } else {
1150 Cow::Owned(file_field_name.to_string())
1151 };
1152
1153 let request_builder = || {
1155 let file_data_clone = Arc::clone(&file_data_arc);
1157
1158 let Ok(part) = multipart::Part::bytes((*file_data_clone).clone())
1160 .file_name(filename_cow.to_string())
1161 .mime_str("application/octet-stream")
1162 else {
1163 return self.client.post("invalid://url");
1164 };
1165
1166 let form = multipart::Form::new().part(field_name_cow.to_string(), part);
1167
1168 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1170 return self.client.post("invalid://url");
1171 };
1172
1173 self.client
1174 .post(&url)
1175 .header("Authorization", auth_header)
1176 .header("User-Agent", "Veracode Rust Client")
1177 .multipart(form)
1178 };
1179
1180 let operation_name: Cow<str> = if endpoint.len() < 50 {
1182 Cow::Owned(format!("File Upload POST {endpoint}"))
1183 } else {
1184 Cow::Borrowed("File Upload POST [long endpoint]")
1185 };
1186
1187 self.execute_with_retry(request_builder, operation_name)
1188 .await
1189 }
1190
1191 pub async fn post_with_query_params(
1210 &self,
1211 endpoint: &str,
1212 query_params: &[(&str, &str)],
1213 ) -> Result<reqwest::Response, VeracodeError> {
1214 let url = self.build_url_with_params(endpoint, query_params);
1216
1217 let auth_header = self.generate_auth_header("POST", &url)?;
1218
1219 let response = self
1220 .client
1221 .post(&url)
1222 .header("Authorization", auth_header)
1223 .header("User-Agent", "Veracode Rust Client")
1224 .send()
1225 .await?;
1226
1227 Ok(response)
1228 }
1229
1230 pub async fn get_with_query_params(
1249 &self,
1250 endpoint: &str,
1251 query_params: &[(&str, &str)],
1252 ) -> Result<reqwest::Response, VeracodeError> {
1253 let url = self.build_url_with_params(endpoint, query_params);
1255
1256 let auth_header = self.generate_auth_header("GET", &url)?;
1257
1258 let response = self
1259 .client
1260 .get(&url)
1261 .header("Authorization", auth_header)
1262 .header("User-Agent", "Veracode Rust Client")
1263 .send()
1264 .await?;
1265
1266 Ok(response)
1267 }
1268
1269 pub async fn upload_large_file_chunked<F>(
1291 &self,
1292 endpoint: &str,
1293 query_params: &[(&str, &str)],
1294 file_path: &str,
1295 content_type: Option<&str>,
1296 progress_callback: Option<F>,
1297 ) -> Result<reqwest::Response, VeracodeError>
1298 where
1299 F: Fn(u64, u64, f64) + Send + Sync,
1300 {
1301 let url = self.build_url_with_params(endpoint, query_params);
1303
1304 let mut file = File::open(file_path)
1306 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to open file: {e}")))?;
1307
1308 let file_size = file
1309 .metadata()
1310 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to get file size: {e}")))?
1311 .len();
1312
1313 #[allow(clippy::arithmetic_side_effects)]
1315 const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; if file_size > MAX_FILE_SIZE {
1317 return Err(VeracodeError::InvalidConfig(format!(
1318 "File size ({file_size} bytes) exceeds maximum limit of {MAX_FILE_SIZE} bytes"
1319 )));
1320 }
1321
1322 file.seek(SeekFrom::Start(0))
1324 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to seek file: {e}")))?;
1325
1326 #[allow(clippy::cast_possible_truncation)]
1327 let mut file_data = Vec::with_capacity(file_size as usize);
1328 file.read_to_end(&mut file_data)
1329 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to read file: {e}")))?;
1330
1331 let file_data_arc = Arc::new(file_data);
1333 let content_type_cow: Cow<str> =
1334 content_type.map_or(Cow::Borrowed("binary/octet-stream"), |ct| {
1335 if ct.len() < 64 {
1336 Cow::Borrowed(ct)
1337 } else {
1338 Cow::Owned(ct.to_string())
1339 }
1340 });
1341
1342 let request_builder = || {
1344 let file_data_clone = Arc::clone(&file_data_arc);
1346
1347 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1349 return self.client.post("invalid://url");
1350 };
1351
1352 self.client
1353 .post(&url)
1354 .header("Authorization", auth_header)
1355 .header("User-Agent", "Veracode Rust Client")
1356 .header("Content-Type", content_type_cow.as_ref())
1357 .header("Content-Length", file_size.to_string())
1358 .body((*file_data_clone).clone())
1359 };
1360
1361 if let Some(callback) = progress_callback {
1363 callback(file_size, file_size, 100.0);
1364 }
1365
1366 let operation_name: Cow<str> = if endpoint.len() < 50 {
1368 Cow::Owned(format!("Large File Upload POST {endpoint}"))
1369 } else {
1370 Cow::Borrowed("Large File Upload POST [long endpoint]")
1371 };
1372
1373 self.execute_with_retry(request_builder, operation_name)
1374 .await
1375 }
1376
1377 pub async fn upload_file_binary(
1401 &self,
1402 endpoint: &str,
1403 query_params: &[(&str, &str)],
1404 file_data: Vec<u8>,
1405 content_type: &str,
1406 ) -> Result<reqwest::Response, VeracodeError> {
1407 let url = self.build_url_with_params(endpoint, query_params);
1409
1410 let file_data_arc = Arc::new(file_data);
1412 let file_size = file_data_arc.len();
1413
1414 let content_type_cow: Cow<str> = if content_type.len() < 64 {
1416 Cow::Borrowed(content_type)
1417 } else {
1418 Cow::Owned(content_type.to_string())
1419 };
1420
1421 let request_builder = || {
1423 let file_data_clone = Arc::clone(&file_data_arc);
1425
1426 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1428 return self.client.post("invalid://url");
1429 };
1430
1431 self.client
1432 .post(&url)
1433 .header("Authorization", auth_header)
1434 .header("User-Agent", "Veracode Rust Client")
1435 .header("Content-Type", content_type_cow.as_ref())
1436 .header("Content-Length", file_size.to_string())
1437 .body((*file_data_clone).clone())
1438 };
1439
1440 let operation_name: Cow<str> = if endpoint.len() < 50 {
1442 Cow::Owned(format!("Binary File Upload POST {endpoint}"))
1443 } else {
1444 Cow::Borrowed("Binary File Upload POST [long endpoint]")
1445 };
1446
1447 self.execute_with_retry(request_builder, operation_name)
1448 .await
1449 }
1450}
1451
1452#[cfg(test)]
1453#[allow(clippy::expect_used)] mod tests {
1455 use super::*;
1456 use proptest::prelude::*;
1457
1458 fn create_test_config() -> VeracodeConfig {
1464 use crate::{VeracodeCredentials, VeracodeRegion};
1465
1466 VeracodeConfig {
1467 credentials: VeracodeCredentials::new(
1468 "test_api_id".to_string(),
1469 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
1470 ),
1471 base_url: "https://api.veracode.com".to_string(),
1472 rest_base_url: "https://api.veracode.com".to_string(),
1473 xml_base_url: "https://analysiscenter.veracode.com".to_string(),
1474 region: VeracodeRegion::Commercial,
1475 validate_certificates: true,
1476 connect_timeout: 30,
1477 request_timeout: 300,
1478 proxy_url: None,
1479 proxy_username: None,
1480 proxy_password: None,
1481 retry_config: Default::default(),
1482 }
1483 }
1484
1485 proptest! {
1490 #![proptest_config(ProptestConfig {
1491 cases: if cfg!(miri) { 5 } else { 1000 },
1492 failure_persistence: None,
1493 .. ProptestConfig::default()
1494 })]
1495
1496 #[test]
1499 fn proptest_url_params_prevent_injection(
1500 key in "[a-zA-Z0-9_]{1,50}",
1501 value in ".*{0,100}",
1502 ) {
1503 let config = create_test_config();
1504 let client = VeracodeClient::new(config)
1505 .expect("valid test client configuration");
1506
1507 let params = vec![(key.as_str(), value.as_str())];
1508 let url = client.build_url_with_params("/api/test", ¶ms);
1509
1510 prop_assert!(!url.contains("<script>"));
1512 prop_assert!(!url.contains("javascript:"));
1513
1514 prop_assert!(url.starts_with("https://api.veracode.com/api/test"));
1516
1517 if !params.is_empty() && !key.is_empty() {
1519 prop_assert!(url.contains('?'));
1520 }
1521 }
1522
1523 #[test]
1526 fn proptest_url_params_capacity_safe(
1527 param_count in 0usize..=100,
1528 ) {
1529 let config = create_test_config();
1530 let client = VeracodeClient::new(config)
1531 .expect("valid test client configuration");
1532
1533 let params: Vec<(&str, &str)> = (0..param_count)
1535 .map(|_| ("key", "value"))
1536 .collect();
1537
1538 let url = client.build_url_with_params("/api/test", ¶ms);
1540
1541 prop_assert!(url.starts_with("https://"));
1543 prop_assert!(url.len() < 100000); }
1545
1546 #[test]
1548 fn proptest_url_params_empty_safe(
1549 key in "\\s*",
1550 value in "\\s*",
1551 ) {
1552 let config = create_test_config();
1553 let client = VeracodeClient::new(config)
1554 .expect("valid test client configuration");
1555
1556 let params = vec![(key.as_str(), value.as_str())];
1557 let url = client.build_url_with_params("/api/test", ¶ms);
1558
1559 prop_assert!(url.starts_with("https://"));
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_hmac_invalid_urls_return_error(
1579 invalid_url in ".*{0,100}",
1580 ) {
1581 let config = create_test_config();
1582 let client = VeracodeClient::new(config)
1583 .expect("valid test client configuration");
1584
1585 let result = client.generate_hmac_signature(
1587 "GET",
1588 &invalid_url,
1589 1234567890000,
1590 "0123456789abcdef0123456789abcdef",
1591 );
1592
1593 match result {
1595 Ok(_) => {
1596 prop_assert!(Url::parse(&invalid_url).is_ok());
1598 },
1599 Err(e) => {
1600 prop_assert!(matches!(e, VeracodeError::Authentication(_)));
1602 }
1603 }
1604 }
1605
1606 #[test]
1609 fn proptest_hmac_deterministic(
1610 method in "[A-Z]{3,7}",
1611 timestamp in 1000000000000u64..2000000000000u64,
1612 ) {
1613 let config = create_test_config();
1614 let client = VeracodeClient::new(config)
1615 .expect("valid test client configuration");
1616
1617 let url = "https://api.veracode.com/api/test";
1618 let nonce = "0123456789abcdef0123456789abcdef";
1619
1620 let sig1 = client.generate_hmac_signature(&method, url, timestamp, nonce);
1621 let sig2 = client.generate_hmac_signature(&method, url, timestamp, nonce);
1622
1623 match (sig1, sig2) {
1625 (Ok(s1), Ok(s2)) => prop_assert_eq!(s1, s2),
1626 (Err(_), Err(_)) => {}, _ => prop_assert!(false, "Non-deterministic result"),
1628 }
1629 }
1630
1631 #[test]
1634 fn proptest_hmac_invalid_nonce_returns_error(
1635 invalid_nonce in "[^0-9a-fA-F]{1,32}",
1636 ) {
1637 let config = create_test_config();
1638 let client = VeracodeClient::new(config)
1639 .expect("valid test client configuration");
1640
1641 let result = client.generate_hmac_signature(
1642 "GET",
1643 "https://api.veracode.com/api/test",
1644 1234567890000,
1645 &invalid_nonce,
1646 );
1647
1648 prop_assert!(matches!(result, Err(VeracodeError::Authentication(_))));
1650 }
1651
1652 #[test]
1655 fn proptest_hmac_timestamp_safe(
1656 timestamp in any::<u64>(),
1657 ) {
1658 let config = create_test_config();
1659 let client = VeracodeClient::new(config)
1660 .expect("valid test client configuration");
1661
1662 let url = "https://api.veracode.com/api/test";
1663 let nonce = "0123456789abcdef0123456789abcdef";
1664
1665 let result = client.generate_hmac_signature("GET", url, timestamp, nonce);
1667
1668 prop_assert!(result.is_ok() || result.is_err());
1670 }
1671 }
1672
1673 proptest! {
1678 #![proptest_config(ProptestConfig {
1679 cases: if cfg!(miri) { 5 } else { 1000 },
1680 failure_persistence: None,
1681 .. ProptestConfig::default()
1682 })]
1683
1684 #[test]
1687 fn proptest_auth_header_format(
1688 method in "[A-Z]{3,7}",
1689 ) {
1690 let config = create_test_config();
1691 let client = VeracodeClient::new(config)
1692 .expect("valid test client configuration");
1693
1694 let url = "https://api.veracode.com/api/test";
1695 let result = client.generate_auth_header(&method, url);
1696
1697 if let Ok(header) = result {
1698 prop_assert!(header.starts_with("VERACODE-HMAC-SHA-256"));
1700
1701 prop_assert!(header.contains("id="));
1703 prop_assert!(header.contains("ts="));
1704 prop_assert!(header.contains("nonce="));
1705 prop_assert!(header.contains("sig="));
1706
1707 let parts: Vec<&str> = header.split(',').collect();
1709 prop_assert_eq!(parts.len(), 4);
1710 }
1711 }
1712
1713 #[test]
1716 fn proptest_auth_header_nonce_unique(
1717 _seed in any::<u8>(),
1718 ) {
1719 let config = create_test_config();
1720 let client = VeracodeClient::new(config)
1721 .expect("valid test client configuration");
1722
1723 let url = "https://api.veracode.com/api/test";
1724
1725 let header1 = client.generate_auth_header("GET", url)
1727 .expect("valid auth header generation");
1728 let header2 = client.generate_auth_header("GET", url)
1729 .expect("valid auth header generation");
1730
1731 fn extract_nonce(h: &str) -> Option<String> {
1733 Some(h.split("nonce=")
1734 .nth(1)?
1735 .split(',')
1736 .next()?
1737 .to_string())
1738 }
1739
1740 if let (Some(nonce1), Some(nonce2)) = (extract_nonce(&header1), extract_nonce(&header2)) {
1741 prop_assert_ne!(&nonce1, &nonce2);
1744
1745 prop_assert_eq!(nonce1.len(), 32);
1747 prop_assert_eq!(nonce2.len(), 32);
1748 prop_assert!(nonce1.chars().all(|c| c.is_ascii_hexdigit()));
1749 prop_assert!(nonce2.chars().all(|c| c.is_ascii_hexdigit()));
1750 }
1751 }
1752 }
1753
1754 proptest! {
1759 #![proptest_config(ProptestConfig {
1760 cases: if cfg!(miri) { 5 } else { 100 },
1761 failure_persistence: None,
1762 .. ProptestConfig::default()
1763 })]
1764
1765 #[test]
1768 fn proptest_client_creation_invalid_proxy(
1769 invalid_proxy in ".*{0,100}",
1770 ) {
1771 use crate::{VeracodeCredentials, VeracodeRegion};
1772
1773 let config = VeracodeConfig {
1774 credentials: VeracodeCredentials::new(
1775 "test_api_id".to_string(),
1776 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
1777 ),
1778 base_url: "https://api.veracode.com".to_string(),
1779 rest_base_url: "https://api.veracode.com".to_string(),
1780 xml_base_url: "https://analysiscenter.veracode.com".to_string(),
1781 region: VeracodeRegion::Commercial,
1782 validate_certificates: true,
1783 connect_timeout: 30,
1784 request_timeout: 300,
1785 proxy_url: Some(invalid_proxy.clone()),
1786 proxy_username: None,
1787 proxy_password: None,
1788 retry_config: Default::default(),
1789 };
1790
1791 let result = VeracodeClient::new(config);
1792
1793 match result {
1795 Ok(_) => {
1796 prop_assert!(reqwest::Proxy::all(&invalid_proxy).is_ok());
1798 },
1799 Err(e) => {
1800 prop_assert!(matches!(e, VeracodeError::InvalidConfig(_)));
1802 }
1803 }
1804 }
1805
1806 #[test]
1809 fn proptest_client_timeouts_safe(
1810 connect_timeout in 1u64..=3600,
1811 request_timeout in 1u64..=7200,
1812 ) {
1813 use crate::{VeracodeCredentials, VeracodeRegion};
1814
1815 let config = VeracodeConfig {
1816 credentials: VeracodeCredentials::new(
1817 "test_api_id".to_string(),
1818 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
1819 ),
1820 base_url: "https://api.veracode.com".to_string(),
1821 rest_base_url: "https://api.veracode.com".to_string(),
1822 xml_base_url: "https://analysiscenter.veracode.com".to_string(),
1823 region: VeracodeRegion::Commercial,
1824 validate_certificates: true,
1825 connect_timeout,
1826 request_timeout,
1827 proxy_url: None,
1828 proxy_username: None,
1829 proxy_password: None,
1830 retry_config: Default::default(),
1831 };
1832
1833 let result = VeracodeClient::new(config);
1835 prop_assert!(result.is_ok());
1836 }
1837 }
1838
1839 proptest! {
1844 #![proptest_config(ProptestConfig {
1845 cases: if cfg!(miri) { 5 } else { 100 },
1846 failure_persistence: None,
1847 .. ProptestConfig::default()
1848 })]
1849
1850 #[test]
1853 fn proptest_file_upload_capacity_safe(
1854 file_size in 0usize..=1000000,
1855 ) {
1856 let file_data = vec![0u8; file_size];
1858
1859 let file_data_arc = Arc::new(file_data);
1861
1862 prop_assert_eq!(file_data_arc.len(), file_size);
1864
1865 let clone1 = Arc::clone(&file_data_arc);
1867 let clone2 = Arc::clone(&file_data_arc);
1868 prop_assert_eq!(clone1.len(), file_size);
1869 prop_assert_eq!(clone2.len(), file_size);
1870 }
1871
1872 #[test]
1875 fn proptest_content_type_safe(
1876 content_type in ".*{0,200}",
1877 ) {
1878 let content_type_cow: Cow<str> = if content_type.len() < 64 {
1880 Cow::Borrowed(&content_type)
1881 } else {
1882 Cow::Owned(content_type.clone())
1883 };
1884
1885 let ct_lower = content_type_cow.to_lowercase();
1887 if ct_lower.contains("<script>") || ct_lower.contains("javascript:") {
1888 prop_assert!(content_type_cow.as_ref().contains("<script>") ||
1890 content_type_cow.as_ref().contains("javascript:"));
1891 }
1892
1893 prop_assert_eq!(content_type_cow.len(), content_type.len());
1895 }
1896 }
1897
1898 #[test]
1903 fn test_hmac_signature_with_query_params() {
1904 let config = create_test_config();
1905 let client = VeracodeClient::new(config).expect("valid test client configuration");
1906
1907 let url = "https://api.veracode.com/api/test?param1=value1¶m2=value2";
1909 let nonce = "0123456789abcdef0123456789abcdef";
1910 let timestamp = 1234567890000;
1911
1912 let result = client.generate_hmac_signature("GET", url, timestamp, nonce);
1913 assert!(result.is_ok());
1914
1915 let signature = result.expect("valid HMAC signature");
1916 assert_eq!(signature.len(), 64);
1918 assert!(signature.chars().all(|c| c.is_ascii_hexdigit()));
1919 }
1920
1921 #[test]
1922 fn test_hmac_signature_different_methods() {
1923 let config = create_test_config();
1924 let client = VeracodeClient::new(config).expect("valid test client configuration");
1925
1926 let url = "https://api.veracode.com/api/test";
1927 let nonce = "0123456789abcdef0123456789abcdef";
1928 let timestamp = 1234567890000;
1929
1930 let sig_get = client
1931 .generate_hmac_signature("GET", url, timestamp, nonce)
1932 .expect("valid HMAC signature for GET");
1933 let sig_post = client
1934 .generate_hmac_signature("POST", url, timestamp, nonce)
1935 .expect("valid HMAC signature for POST");
1936
1937 assert_ne!(sig_get, sig_post);
1939 }
1940
1941 #[test]
1942 fn test_url_encoding_special_characters() {
1943 let config = create_test_config();
1944 let client = VeracodeClient::new(config).expect("valid test client configuration");
1945
1946 let params = vec![
1948 ("key1", "value with spaces"),
1949 ("key2", "value&with&ersands"),
1950 ("key3", "value=with=equals"),
1951 ("key4", "value?with?questions"),
1952 ];
1953
1954 let url = client.build_url_with_params("/api/test", ¶ms);
1955
1956 assert!(url.contains("value%20with%20spaces") || url.contains("value+with+spaces"));
1958 assert!(url.contains("%26"));
1960 assert!(url.starts_with("https://api.veracode.com/api/test?"));
1962 }
1963
1964 #[test]
1965 fn test_url_encoding_unicode() {
1966 let config = create_test_config();
1967 let client = VeracodeClient::new(config).expect("valid test client configuration");
1968
1969 let params = vec![
1971 ("key", "δ½ ε₯½δΈη"), ("key2", "ππ‘οΈ"), ];
1974
1975 let url = client.build_url_with_params("/api/test", ¶ms);
1976
1977 assert!(url.starts_with("https://api.veracode.com/api/test?"));
1979 assert!(url.contains('%'));
1981 }
1982
1983 #[test]
1984 fn test_empty_query_params() {
1985 let config = create_test_config();
1986 let client = VeracodeClient::new(config).expect("valid test client configuration");
1987
1988 let url = client.build_url_with_params("/api/test", &[]);
1989
1990 assert_eq!(url, "https://api.veracode.com/api/test");
1992 }
1993
1994 #[test]
1995 fn test_invalid_api_key_format() {
1996 use crate::{VeracodeCredentials, VeracodeRegion};
1997
1998 let config = VeracodeConfig {
2000 credentials: VeracodeCredentials::new(
2001 "test_api_id".to_string(),
2002 "not_valid_hex_key".to_string(),
2003 ),
2004 base_url: "https://api.veracode.com".to_string(),
2005 rest_base_url: "https://api.veracode.com".to_string(),
2006 xml_base_url: "https://analysiscenter.veracode.com".to_string(),
2007 region: VeracodeRegion::Commercial,
2008 validate_certificates: true,
2009 connect_timeout: 30,
2010 request_timeout: 300,
2011 proxy_url: None,
2012 proxy_username: None,
2013 proxy_password: None,
2014 retry_config: Default::default(),
2015 };
2016
2017 let client = VeracodeClient::new(config).expect("valid test client configuration");
2018 let result = client.generate_auth_header("GET", "https://api.veracode.com/api/test");
2019
2020 assert!(matches!(result, Err(VeracodeError::Authentication(_))));
2022 }
2023
2024 #[test]
2025 fn test_auth_header_format() {
2026 let config = create_test_config();
2027 let client = VeracodeClient::new(config).expect("valid test client configuration");
2028
2029 let header = client
2030 .generate_auth_header("GET", "https://api.veracode.com/api/test")
2031 .expect("valid auth header generation");
2032
2033 assert!(header.starts_with("VERACODE-HMAC-SHA-256 "));
2035 assert!(header.contains("id=test_api_id"));
2036 assert!(header.contains("ts="));
2037 assert!(header.contains("nonce="));
2038 assert!(header.contains("sig="));
2039
2040 let parts: Vec<&str> = header.split(',').collect();
2042 assert_eq!(parts.len(), 4);
2043 }
2044
2045 #[cfg(not(miri))] #[test]
2047 fn test_auth_header_timestamp_monotonic() {
2048 let config = create_test_config();
2049 let client = VeracodeClient::new(config).expect("valid test client configuration");
2050
2051 let header1 = client
2052 .generate_auth_header("GET", "https://api.veracode.com/api/test")
2053 .expect("valid auth header generation");
2054 std::thread::sleep(std::time::Duration::from_millis(10));
2055 let header2 = client
2056 .generate_auth_header("GET", "https://api.veracode.com/api/test")
2057 .expect("valid auth header generation");
2058
2059 let extract_ts =
2061 |h: &str| -> Option<u64> { h.split("ts=").nth(1)?.split(',').next()?.parse().ok() };
2062
2063 let ts1 = extract_ts(&header1).expect("valid timestamp extraction");
2064 let ts2 = extract_ts(&header2).expect("valid timestamp extraction");
2065
2066 assert!(ts2 >= ts1);
2068 }
2069
2070 #[test]
2071 fn test_base_url_accessor() {
2072 let config = create_test_config();
2073 let client = VeracodeClient::new(config).expect("valid test client configuration");
2074
2075 assert_eq!(client.base_url(), "https://api.veracode.com");
2076 }
2077
2078 #[test]
2079 fn test_client_clone() {
2080 let config = create_test_config();
2081 let client1 = VeracodeClient::new(config).expect("valid test client configuration");
2082 let client2 = client1.clone();
2083
2084 assert_eq!(client1.base_url(), client2.base_url());
2086 }
2087
2088 #[test]
2089 fn test_url_capacity_estimation() {
2090 let config = create_test_config();
2091 let client = VeracodeClient::new(config).expect("valid test client configuration");
2092
2093 let params: Vec<(&str, &str)> = (0..100).map(|_| ("key", "value")).collect();
2095
2096 let url = client.build_url_with_params("/api/test", ¶ms);
2097
2098 assert!(url.starts_with("https://api.veracode.com/api/test?"));
2100 assert!(url.len() > 100); }
2102
2103 #[test]
2104 fn test_saturating_arithmetic() {
2105 let config = create_test_config();
2106 let client = VeracodeClient::new(config).expect("valid test client configuration");
2107
2108 let params: Vec<(&str, &str)> = vec![("k", "v"); 1000];
2110
2111 let url = client.build_url_with_params("/api/test", ¶ms);
2113 assert!(url.len() < usize::MAX);
2114 }
2115}