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::InvalidConfig(_)
305 | VeracodeError::NotFound(_)
306 | VeracodeError::RateLimited { .. }
307 | VeracodeError::Validation(_) => {
308 let msg = format!(
309 "{} failed after {} attempts over {}ms: {}",
310 operation_name,
311 retry_config.max_attempts.saturating_add(1),
312 elapsed.as_millis(),
313 error
314 );
315 Err(VeracodeError::RetryExhausted(msg))
316 }
317 }
318 }
319 None => {
320 let msg = format!(
321 "{} failed after {} attempts with unknown error",
322 operation_name,
323 retry_config.max_attempts.saturating_add(1)
324 );
325 Err(VeracodeError::RetryExhausted(msg))
326 }
327 }
328 }
329
330 fn generate_hmac_signature(
332 &self,
333 method: &str,
334 url: &str,
335 timestamp: u64,
336 nonce: &str,
337 ) -> Result<String, VeracodeError> {
338 let url_parsed = Url::parse(url)
339 .map_err(|_| VeracodeError::Authentication(INVALID_URL_MSG.to_string()))?;
340
341 let path_and_query = match url_parsed.query() {
342 Some(query) => format!("{}?{}", url_parsed.path(), query),
343 None => url_parsed.path().to_string(),
344 };
345
346 let host = url_parsed.host_str().unwrap_or("");
347
348 let data = format!(
351 "id={}&host={}&url={}&method={}",
352 self.config.credentials.expose_api_id(),
353 host,
354 path_and_query,
355 method
356 );
357
358 let timestamp_str = timestamp.to_string();
359 let ver_str = "vcode_request_version_1";
360
361 let key_bytes = hex::decode(self.config.credentials.expose_api_key())
363 .map_err(|_| VeracodeError::Authentication(INVALID_API_KEY_MSG.to_string()))?;
364
365 let nonce_bytes = hex::decode(nonce)
366 .map_err(|_| VeracodeError::Authentication(INVALID_NONCE_MSG.to_string()))?;
367
368 let mut mac1 = HmacSha256::new_from_slice(&key_bytes)
370 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
371 mac1.update(&nonce_bytes);
372 let hashed_nonce = mac1.finalize().into_bytes();
373
374 let mut mac2 = HmacSha256::new_from_slice(&hashed_nonce)
376 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
377 mac2.update(timestamp_str.as_bytes());
378 let hashed_timestamp = mac2.finalize().into_bytes();
379
380 let mut mac3 = HmacSha256::new_from_slice(&hashed_timestamp)
382 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
383 mac3.update(ver_str.as_bytes());
384 let hashed_ver_str = mac3.finalize().into_bytes();
385
386 let mut mac4 = HmacSha256::new_from_slice(&hashed_ver_str)
388 .map_err(|_| VeracodeError::Authentication(HMAC_CREATION_FAILED_MSG.to_string()))?;
389 mac4.update(data.as_bytes());
390 let signature = mac4.finalize().into_bytes();
391
392 Ok(hex::encode(signature).to_lowercase())
394 }
395
396 pub fn generate_auth_header(&self, method: &str, url: &str) -> Result<String, VeracodeError> {
403 #[allow(clippy::cast_possible_truncation)]
404 let timestamp = SystemTime::now()
405 .duration_since(UNIX_EPOCH)
406 .map_err(|e| VeracodeError::Authentication(format!("System time error: {e}")))?
407 .as_millis() as u64; let nonce_bytes: [u8; 16] = rand::random();
411 let nonce = hex::encode(nonce_bytes);
412
413 let signature = self.generate_hmac_signature(method, url, timestamp, &nonce)?;
414
415 Ok(format!(
416 "VERACODE-HMAC-SHA-256 id={},ts={},nonce={},sig={}",
417 self.config.credentials.expose_api_id(),
418 timestamp,
419 nonce,
420 signature
421 ))
422 }
423
424 pub async fn get(
440 &self,
441 endpoint: &str,
442 query_params: Option<&[(String, String)]>,
443 ) -> Result<reqwest::Response, VeracodeError> {
444 let param_count = query_params.map_or(0, |p| p.len());
446 let estimated_capacity = self
447 .config
448 .base_url
449 .len()
450 .saturating_add(endpoint.len())
451 .saturating_add(param_count.saturating_mul(32));
452 let mut url = String::with_capacity(estimated_capacity);
453 url.push_str(&self.config.base_url);
454 url.push_str(endpoint);
455
456 if let Some(params) = query_params
457 && !params.is_empty()
458 {
459 url.push('?');
460 for (i, (key, value)) in params.iter().enumerate() {
461 if i > 0 {
462 url.push('&');
463 }
464 url.push_str(key);
465 url.push('=');
466 url.push_str(value);
467 }
468 }
469
470 let request_builder = || {
472 let Ok(auth_header) = self.generate_auth_header("GET", &url) else {
474 return self.client.get("invalid://url");
475 };
476
477 self.client
478 .get(&url)
479 .header("Authorization", auth_header)
480 .header("Content-Type", "application/json")
481 };
482
483 let operation_name = if endpoint.len() < 50 {
485 Cow::Owned(format!("GET {endpoint}"))
486 } else {
487 Cow::Borrowed("GET [long endpoint]")
488 };
489 self.execute_with_retry(request_builder, operation_name)
490 .await
491 }
492
493 pub async fn post<T: Serialize>(
509 &self,
510 endpoint: &str,
511 body: Option<&T>,
512 ) -> Result<reqwest::Response, VeracodeError> {
513 let mut url =
514 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
515 url.push_str(&self.config.base_url);
516 url.push_str(endpoint);
517
518 let serialized_body = if let Some(body) = body {
520 Some(serde_json::to_string(body)?)
521 } else {
522 None
523 };
524
525 let request_builder = || {
527 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
529 return self.client.post("invalid://url");
530 };
531
532 let mut request = self
533 .client
534 .post(&url)
535 .header("Authorization", auth_header)
536 .header("Content-Type", "application/json");
537
538 if let Some(ref body_str) = serialized_body {
539 request = request.body(body_str.clone());
540 }
541
542 request
543 };
544
545 let operation_name = if endpoint.len() < 50 {
546 Cow::Owned(format!("POST {endpoint}"))
547 } else {
548 Cow::Borrowed("POST [long endpoint]")
549 };
550 self.execute_with_retry(request_builder, operation_name)
551 .await
552 }
553
554 pub async fn put<T: Serialize>(
570 &self,
571 endpoint: &str,
572 body: Option<&T>,
573 ) -> Result<reqwest::Response, VeracodeError> {
574 let mut url =
575 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
576 url.push_str(&self.config.base_url);
577 url.push_str(endpoint);
578
579 let serialized_body = if let Some(body) = body {
581 Some(serde_json::to_string(body)?)
582 } else {
583 None
584 };
585
586 let request_builder = || {
588 let Ok(auth_header) = self.generate_auth_header("PUT", &url) else {
590 return self.client.put("invalid://url");
591 };
592
593 let mut request = self
594 .client
595 .put(&url)
596 .header("Authorization", auth_header)
597 .header("Content-Type", "application/json");
598
599 if let Some(ref body_str) = serialized_body {
600 request = request.body(body_str.clone());
601 }
602
603 request
604 };
605
606 let operation_name = if endpoint.len() < 50 {
607 Cow::Owned(format!("PUT {endpoint}"))
608 } else {
609 Cow::Borrowed("PUT [long endpoint]")
610 };
611 self.execute_with_retry(request_builder, operation_name)
612 .await
613 }
614
615 pub async fn delete(&self, endpoint: &str) -> Result<reqwest::Response, VeracodeError> {
630 let mut url =
631 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
632 url.push_str(&self.config.base_url);
633 url.push_str(endpoint);
634
635 let request_builder = || {
637 let Ok(auth_header) = self.generate_auth_header("DELETE", &url) else {
639 return self.client.delete("invalid://url");
640 };
641
642 self.client
643 .delete(&url)
644 .header("Authorization", auth_header)
645 .header("Content-Type", "application/json")
646 };
647
648 let operation_name = if endpoint.len() < 50 {
649 Cow::Owned(format!("DELETE {endpoint}"))
650 } else {
651 Cow::Borrowed("DELETE [long endpoint]")
652 };
653 self.execute_with_retry(request_builder, operation_name)
654 .await
655 }
656
657 pub async fn handle_response(
680 response: reqwest::Response,
681 context: &str,
682 ) -> Result<reqwest::Response, VeracodeError> {
683 if !response.status().is_success() {
684 let status = response.status();
685 let url = response.url().clone();
686 let error_text = response.text().await?;
687 return Err(VeracodeError::InvalidResponse(format!(
688 "Failed to {context}\n URL: {url}\n HTTP {status}: {error_text}"
689 )));
690 }
691 Ok(response)
692 }
693
694 pub async fn get_with_query(
712 &self,
713 endpoint: &str,
714 query_params: Option<Vec<(String, String)>>,
715 ) -> Result<reqwest::Response, VeracodeError> {
716 let query_slice = query_params.as_deref();
717 let response = self.get(endpoint, query_slice).await?;
718 Self::handle_response(response, &format!("GET {endpoint}")).await
719 }
720
721 pub async fn post_with_response<T: Serialize>(
737 &self,
738 endpoint: &str,
739 body: Option<&T>,
740 ) -> Result<reqwest::Response, VeracodeError> {
741 let response = self.post(endpoint, body).await?;
742 Self::handle_response(response, &format!("POST {endpoint}")).await
743 }
744
745 pub async fn put_with_response<T: Serialize>(
761 &self,
762 endpoint: &str,
763 body: Option<&T>,
764 ) -> Result<reqwest::Response, VeracodeError> {
765 let response = self.put(endpoint, body).await?;
766 Self::handle_response(response, &format!("PUT {endpoint}")).await
767 }
768
769 pub async fn delete_with_response(
784 &self,
785 endpoint: &str,
786 ) -> Result<reqwest::Response, VeracodeError> {
787 let response = self.delete(endpoint).await?;
788 Self::handle_response(response, &format!("DELETE {endpoint}")).await
789 }
790
791 pub async fn get_paginated(
811 &self,
812 endpoint: &str,
813 base_query_params: Option<Vec<(String, String)>>,
814 page_size: Option<u32>,
815 ) -> Result<String, VeracodeError> {
816 let size = page_size.unwrap_or(500);
817 let mut page: u32 = 0;
818 let mut all_items = Vec::new();
819 let mut page_info = None;
820
821 loop {
822 let mut query_params = base_query_params.clone().unwrap_or_default();
823 query_params.push(("page".to_string(), page.to_string()));
824 query_params.push(("size".to_string(), size.to_string()));
825
826 let response = self.get_with_query(endpoint, Some(query_params)).await?;
827 let response_text = response.text().await?;
828
829 validate_json_depth(&response_text, MAX_JSON_DEPTH).map_err(|e| {
831 VeracodeError::InvalidResponse(format!("JSON validation failed: {}", e))
832 })?;
833
834 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
836 if let Some(embedded) = json_value.get("_embedded") {
838 if let Some(items_array) =
839 embedded.as_object().and_then(|obj| obj.values().next())
840 && let Some(items) = items_array.as_array()
841 {
842 if items.is_empty() {
843 break; }
845 all_items.extend(items.clone());
846 }
847 } else if let Some(items) = json_value.as_array() {
848 if items.is_empty() {
850 break;
851 }
852 all_items.extend(items.clone());
853 } else {
854 return Ok(response_text);
856 }
857
858 if let Some(page_obj) = json_value.get("page") {
860 page_info = Some(page_obj.clone());
861 if let (Some(current), Some(total)) = (
862 page_obj.get("number").and_then(|n| n.as_u64()),
863 page_obj.get("totalPages").and_then(|n| n.as_u64()),
864 ) && current.saturating_add(1) >= total
865 {
866 break; }
868 }
869 } else {
870 return Ok(response_text);
872 }
873
874 page = page.saturating_add(1);
875
876 if page > 100 {
878 break;
879 }
880 }
881
882 let combined_response = if let Some(page_info) = page_info {
884 serde_json::json!({
886 "_embedded": {
887 "roles": all_items },
889 "page": page_info
890 })
891 } else {
892 serde_json::Value::Array(all_items)
894 };
895
896 Ok(combined_response.to_string())
897 }
898
899 pub async fn get_with_params(
915 &self,
916 endpoint: &str,
917 params: &[(&str, &str)],
918 ) -> Result<reqwest::Response, VeracodeError> {
919 let mut url =
920 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
921 url.push_str(&self.config.base_url);
922 url.push_str(endpoint);
923 let mut request_url =
924 Url::parse(&url).map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
925
926 if !params.is_empty() {
928 let mut query_pairs = request_url.query_pairs_mut();
929 for (key, value) in params {
930 query_pairs.append_pair(key, value);
931 }
932 }
933
934 let auth_header = self.generate_auth_header("GET", request_url.as_str())?;
935
936 let response = self
937 .client
938 .get(request_url)
939 .header("Authorization", auth_header)
940 .header("User-Agent", "Veracode Rust Client")
941 .send()
942 .await?;
943
944 Ok(response)
945 }
946
947 pub async fn post_form(
963 &self,
964 endpoint: &str,
965 params: &[(&str, &str)],
966 ) -> Result<reqwest::Response, VeracodeError> {
967 let mut url =
968 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
969 url.push_str(&self.config.base_url);
970 url.push_str(endpoint);
971
972 let form_data: Vec<(&str, &str)> = params.to_vec();
974
975 let auth_header = self.generate_auth_header("POST", &url)?;
976
977 let response = self
978 .client
979 .post(&url)
980 .header("Authorization", auth_header)
981 .header("User-Agent", "Veracode Rust Client")
982 .form(&form_data)
983 .send()
984 .await?;
985
986 Ok(response)
987 }
988
989 pub async fn upload_file_multipart(
1008 &self,
1009 endpoint: &str,
1010 params: HashMap<&str, &str>,
1011 file_field_name: &str,
1012 filename: &str,
1013 file_data: Vec<u8>,
1014 ) -> Result<reqwest::Response, VeracodeError> {
1015 let mut url =
1016 String::with_capacity(self.config.base_url.len().saturating_add(endpoint.len()));
1017 url.push_str(&self.config.base_url);
1018 url.push_str(endpoint);
1019
1020 let mut form = multipart::Form::new();
1022
1023 for (key, value) in params {
1025 form = form.text(key.to_string(), value.to_string());
1026 }
1027
1028 let part = multipart::Part::bytes(file_data)
1030 .file_name(filename.to_string())
1031 .mime_str("application/octet-stream")
1032 .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
1033
1034 form = form.part(file_field_name.to_string(), part);
1035
1036 let auth_header = self.generate_auth_header("POST", &url)?;
1037
1038 let response = self
1039 .client
1040 .post(&url)
1041 .header("Authorization", auth_header)
1042 .header("User-Agent", "Veracode Rust Client")
1043 .multipart(form)
1044 .send()
1045 .await?;
1046
1047 Ok(response)
1048 }
1049
1050 pub async fn upload_file_multipart_put(
1069 &self,
1070 url: &str,
1071 file_field_name: &str,
1072 filename: &str,
1073 file_data: Vec<u8>,
1074 additional_headers: Option<HashMap<&str, &str>>,
1075 ) -> Result<reqwest::Response, VeracodeError> {
1076 let part = multipart::Part::bytes(file_data)
1078 .file_name(filename.to_string())
1079 .mime_str("application/octet-stream")
1080 .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
1081
1082 let form = multipart::Form::new().part(file_field_name.to_string(), part);
1083
1084 let auth_header = self.generate_auth_header("PUT", url)?;
1085
1086 let mut request = self
1087 .client
1088 .put(url)
1089 .header("Authorization", auth_header)
1090 .header("User-Agent", "Veracode Rust Client")
1091 .multipart(form);
1092
1093 if let Some(headers) = additional_headers {
1095 for (key, value) in headers {
1096 request = request.header(key, value);
1097 }
1098 }
1099
1100 let response = request.send().await?;
1101 Ok(response)
1102 }
1103
1104 pub async fn upload_file_with_query_params(
1129 &self,
1130 endpoint: &str,
1131 query_params: &[(&str, &str)],
1132 file_field_name: &str,
1133 filename: &str,
1134 file_data: Vec<u8>,
1135 ) -> Result<reqwest::Response, VeracodeError> {
1136 let url = self.build_url_with_params(endpoint, query_params);
1138
1139 let file_data_arc = Arc::new(file_data);
1141
1142 let filename_cow: Cow<str> = if filename.len() < 128 {
1144 Cow::Borrowed(filename)
1145 } else {
1146 Cow::Owned(filename.to_string())
1147 };
1148
1149 let field_name_cow: Cow<str> = if file_field_name.len() < 32 {
1150 Cow::Borrowed(file_field_name)
1151 } else {
1152 Cow::Owned(file_field_name.to_string())
1153 };
1154
1155 let request_builder = || {
1157 let file_data_clone = Arc::clone(&file_data_arc);
1159
1160 let Ok(part) = multipart::Part::bytes((*file_data_clone).clone())
1162 .file_name(filename_cow.to_string())
1163 .mime_str("application/octet-stream")
1164 else {
1165 return self.client.post("invalid://url");
1166 };
1167
1168 let form = multipart::Form::new().part(field_name_cow.to_string(), part);
1169
1170 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1172 return self.client.post("invalid://url");
1173 };
1174
1175 self.client
1176 .post(&url)
1177 .header("Authorization", auth_header)
1178 .header("User-Agent", "Veracode Rust Client")
1179 .multipart(form)
1180 };
1181
1182 let operation_name: Cow<str> = if endpoint.len() < 50 {
1184 Cow::Owned(format!("File Upload POST {endpoint}"))
1185 } else {
1186 Cow::Borrowed("File Upload POST [long endpoint]")
1187 };
1188
1189 self.execute_with_retry(request_builder, operation_name)
1190 .await
1191 }
1192
1193 pub async fn post_with_query_params(
1212 &self,
1213 endpoint: &str,
1214 query_params: &[(&str, &str)],
1215 ) -> Result<reqwest::Response, VeracodeError> {
1216 let url = self.build_url_with_params(endpoint, query_params);
1218
1219 let auth_header = self.generate_auth_header("POST", &url)?;
1220
1221 let response = self
1222 .client
1223 .post(&url)
1224 .header("Authorization", auth_header)
1225 .header("User-Agent", "Veracode Rust Client")
1226 .send()
1227 .await?;
1228
1229 Ok(response)
1230 }
1231
1232 pub async fn get_with_query_params(
1251 &self,
1252 endpoint: &str,
1253 query_params: &[(&str, &str)],
1254 ) -> Result<reqwest::Response, VeracodeError> {
1255 let url = self.build_url_with_params(endpoint, query_params);
1257
1258 let auth_header = self.generate_auth_header("GET", &url)?;
1259
1260 let response = self
1261 .client
1262 .get(&url)
1263 .header("Authorization", auth_header)
1264 .header("User-Agent", "Veracode Rust Client")
1265 .send()
1266 .await?;
1267
1268 Ok(response)
1269 }
1270
1271 pub async fn upload_large_file_chunked<F>(
1293 &self,
1294 endpoint: &str,
1295 query_params: &[(&str, &str)],
1296 file_path: &str,
1297 content_type: Option<&str>,
1298 progress_callback: Option<F>,
1299 ) -> Result<reqwest::Response, VeracodeError>
1300 where
1301 F: Fn(u64, u64, f64) + Send + Sync,
1302 {
1303 let url = self.build_url_with_params(endpoint, query_params);
1305
1306 let mut file = File::open(file_path)
1308 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to open file: {e}")))?;
1309
1310 let file_size = file
1311 .metadata()
1312 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to get file size: {e}")))?
1313 .len();
1314
1315 #[allow(clippy::arithmetic_side_effects)]
1317 const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; if file_size > MAX_FILE_SIZE {
1319 return Err(VeracodeError::InvalidConfig(format!(
1320 "File size ({file_size} bytes) exceeds maximum limit of {MAX_FILE_SIZE} bytes"
1321 )));
1322 }
1323
1324 file.seek(SeekFrom::Start(0))
1326 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to seek file: {e}")))?;
1327
1328 #[allow(clippy::cast_possible_truncation)]
1329 let mut file_data_vec = Vec::with_capacity(file_size as usize);
1330 file.read_to_end(&mut file_data_vec)
1331 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to read file: {e}")))?;
1332
1333 let file_data = Bytes::from(file_data_vec);
1336 let content_type_cow: Cow<str> =
1337 content_type.map_or(Cow::Borrowed("binary/octet-stream"), |ct| {
1338 if ct.len() < 64 {
1339 Cow::Borrowed(ct)
1340 } else {
1341 Cow::Owned(ct.to_string())
1342 }
1343 });
1344
1345 let request_builder = || {
1347 let body_data = file_data.clone();
1349
1350 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1352 return self.client.post("invalid://url");
1353 };
1354
1355 self.client
1356 .post(&url)
1357 .header("Authorization", auth_header)
1358 .header("User-Agent", "Veracode Rust Client")
1359 .header("Content-Type", content_type_cow.as_ref())
1360 .header("Content-Length", file_size.to_string())
1361 .body(body_data)
1362 };
1363
1364 if let Some(ref callback) = progress_callback {
1366 callback(0, file_size, 0.0);
1367 }
1368
1369 let operation_name: Cow<str> = if endpoint.len() < 50 {
1371 Cow::Owned(format!("Large File Upload POST {endpoint}"))
1372 } else {
1373 Cow::Borrowed("Large File Upload POST [long endpoint]")
1374 };
1375
1376 let response = self
1377 .execute_with_retry(request_builder, operation_name)
1378 .await?;
1379
1380 if let Some(callback) = progress_callback {
1382 callback(file_size, file_size, 100.0);
1383 }
1384
1385 Ok(response)
1386 }
1387
1388 pub async fn upload_file_binary(
1412 &self,
1413 endpoint: &str,
1414 query_params: &[(&str, &str)],
1415 file_data: Vec<u8>,
1416 content_type: &str,
1417 ) -> Result<reqwest::Response, VeracodeError> {
1418 let url = self.build_url_with_params(endpoint, query_params);
1420
1421 let file_data = Bytes::from(file_data);
1424 let file_size = file_data.len();
1425
1426 let content_type_cow: Cow<str> = if content_type.len() < 64 {
1428 Cow::Borrowed(content_type)
1429 } else {
1430 Cow::Owned(content_type.to_string())
1431 };
1432
1433 let request_builder = || {
1435 let body_data = file_data.clone();
1437
1438 let Ok(auth_header) = self.generate_auth_header("POST", &url) else {
1440 return self.client.post("invalid://url");
1441 };
1442
1443 self.client
1444 .post(&url)
1445 .header("Authorization", auth_header)
1446 .header("User-Agent", "Veracode Rust Client")
1447 .header("Content-Type", content_type_cow.as_ref())
1448 .header("Content-Length", file_size.to_string())
1449 .body(body_data)
1450 };
1451
1452 let operation_name: Cow<str> = if endpoint.len() < 50 {
1454 Cow::Owned(format!("Binary File Upload POST {endpoint}"))
1455 } else {
1456 Cow::Borrowed("Binary File Upload POST [long endpoint]")
1457 };
1458
1459 self.execute_with_retry(request_builder, operation_name)
1460 .await
1461 }
1462
1463 pub async fn upload_file_streaming(
1485 &self,
1486 endpoint: &str,
1487 query_params: &[(&str, &str)],
1488 file_path: &str,
1489 file_size: u64,
1490 content_type: &str,
1491 ) -> Result<reqwest::Response, VeracodeError> {
1492 let url = self.build_url_with_params(endpoint, query_params);
1494
1495 let file = TokioFile::open(file_path)
1497 .await
1498 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to open file: {e}")))?;
1499
1500 let stream = tokio_util::io::ReaderStream::new(file);
1502 let body = Body::wrap_stream(stream);
1503
1504 let auth_header = self.generate_auth_header("POST", &url)?;
1506
1507 let response = self
1510 .client
1511 .post(&url)
1512 .header("Authorization", auth_header)
1513 .header("User-Agent", "Veracode Rust Client")
1514 .header("Content-Type", content_type)
1515 .header("Content-Length", file_size.to_string())
1516 .body(body)
1517 .send()
1518 .await
1519 .map_err(VeracodeError::Http)?;
1520
1521 Ok(response)
1522 }
1523}
1524
1525#[cfg(test)]
1526#[allow(clippy::expect_used)] mod tests {
1528 use super::*;
1529 use proptest::prelude::*;
1530
1531 fn create_test_config() -> VeracodeConfig {
1537 use crate::{VeracodeCredentials, VeracodeRegion};
1538
1539 VeracodeConfig {
1540 credentials: VeracodeCredentials::new(
1541 "test_api_id".to_string(),
1542 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
1543 ),
1544 base_url: "https://api.veracode.com".to_string(),
1545 rest_base_url: "https://api.veracode.com".to_string(),
1546 xml_base_url: "https://analysiscenter.veracode.com".to_string(),
1547 region: VeracodeRegion::Commercial,
1548 validate_certificates: true,
1549 connect_timeout: 30,
1550 request_timeout: 300,
1551 proxy_url: None,
1552 proxy_username: None,
1553 proxy_password: None,
1554 retry_config: Default::default(),
1555 }
1556 }
1557
1558 proptest! {
1563 #![proptest_config(ProptestConfig {
1564 cases: if cfg!(miri) { 5 } else { 1000 },
1565 failure_persistence: None,
1566 .. ProptestConfig::default()
1567 })]
1568
1569 #[test]
1572 fn proptest_url_params_prevent_injection(
1573 key in "[a-zA-Z0-9_]{1,50}",
1574 value in ".*{0,100}",
1575 ) {
1576 let config = create_test_config();
1577 let client = VeracodeClient::new(config)
1578 .expect("valid test client configuration");
1579
1580 let params = vec![(key.as_str(), value.as_str())];
1581 let url = client.build_url_with_params("/api/test", ¶ms);
1582
1583 prop_assert!(!url.contains("<script>"));
1585 prop_assert!(!url.contains("javascript:"));
1586
1587 prop_assert!(url.starts_with("https://api.veracode.com/api/test"));
1589
1590 if !params.is_empty() && !key.is_empty() {
1592 prop_assert!(url.contains('?'));
1593 }
1594 }
1595
1596 #[test]
1599 fn proptest_url_params_capacity_safe(
1600 param_count in 0usize..=100,
1601 ) {
1602 let config = create_test_config();
1603 let client = VeracodeClient::new(config)
1604 .expect("valid test client configuration");
1605
1606 let params: Vec<(&str, &str)> = (0..param_count)
1608 .map(|_| ("key", "value"))
1609 .collect();
1610
1611 let url = client.build_url_with_params("/api/test", ¶ms);
1613
1614 prop_assert!(url.starts_with("https://"));
1616 prop_assert!(url.len() < 100000); }
1618
1619 #[test]
1621 fn proptest_url_params_empty_safe(
1622 key in "\\s*",
1623 value in "\\s*",
1624 ) {
1625 let config = create_test_config();
1626 let client = VeracodeClient::new(config)
1627 .expect("valid test client configuration");
1628
1629 let params = vec![(key.as_str(), value.as_str())];
1630 let url = client.build_url_with_params("/api/test", ¶ms);
1631
1632 prop_assert!(url.starts_with("https://"));
1634 }
1635 }
1636
1637 proptest! {
1642 #![proptest_config(ProptestConfig {
1643 cases: if cfg!(miri) { 5 } else { 1000 },
1644 failure_persistence: None,
1645 .. ProptestConfig::default()
1646 })]
1647
1648 #[test]
1651 fn proptest_hmac_invalid_urls_return_error(
1652 invalid_url in ".*{0,100}",
1653 ) {
1654 let config = create_test_config();
1655 let client = VeracodeClient::new(config)
1656 .expect("valid test client configuration");
1657
1658 let result = client.generate_hmac_signature(
1660 "GET",
1661 &invalid_url,
1662 1234567890000,
1663 "0123456789abcdef0123456789abcdef",
1664 );
1665
1666 match result {
1668 Ok(_) => {
1669 prop_assert!(Url::parse(&invalid_url).is_ok());
1671 },
1672 Err(e) => {
1673 prop_assert!(matches!(e, VeracodeError::Authentication(_)));
1675 }
1676 }
1677 }
1678
1679 #[test]
1682 fn proptest_hmac_deterministic(
1683 method in "[A-Z]{3,7}",
1684 timestamp in 1000000000000u64..2000000000000u64,
1685 ) {
1686 let config = create_test_config();
1687 let client = VeracodeClient::new(config)
1688 .expect("valid test client configuration");
1689
1690 let url = "https://api.veracode.com/api/test";
1691 let nonce = "0123456789abcdef0123456789abcdef";
1692
1693 let sig1 = client.generate_hmac_signature(&method, url, timestamp, nonce);
1694 let sig2 = client.generate_hmac_signature(&method, url, timestamp, nonce);
1695
1696 match (sig1, sig2) {
1698 (Ok(s1), Ok(s2)) => prop_assert_eq!(s1, s2),
1699 (Err(_), Err(_)) => {}, _ => prop_assert!(false, "Non-deterministic result"),
1701 }
1702 }
1703
1704 #[test]
1707 fn proptest_hmac_invalid_nonce_returns_error(
1708 invalid_nonce in "[^0-9a-fA-F]{1,32}",
1709 ) {
1710 let config = create_test_config();
1711 let client = VeracodeClient::new(config)
1712 .expect("valid test client configuration");
1713
1714 let result = client.generate_hmac_signature(
1715 "GET",
1716 "https://api.veracode.com/api/test",
1717 1234567890000,
1718 &invalid_nonce,
1719 );
1720
1721 prop_assert!(matches!(result, Err(VeracodeError::Authentication(_))));
1723 }
1724
1725 #[test]
1728 fn proptest_hmac_timestamp_safe(
1729 timestamp in any::<u64>(),
1730 ) {
1731 let config = create_test_config();
1732 let client = VeracodeClient::new(config)
1733 .expect("valid test client configuration");
1734
1735 let url = "https://api.veracode.com/api/test";
1736 let nonce = "0123456789abcdef0123456789abcdef";
1737
1738 let result = client.generate_hmac_signature("GET", url, timestamp, nonce);
1740
1741 prop_assert!(result.is_ok() || result.is_err());
1743 }
1744 }
1745
1746 proptest! {
1751 #![proptest_config(ProptestConfig {
1752 cases: if cfg!(miri) { 5 } else { 1000 },
1753 failure_persistence: None,
1754 .. ProptestConfig::default()
1755 })]
1756
1757 #[test]
1760 fn proptest_auth_header_format(
1761 method in "[A-Z]{3,7}",
1762 ) {
1763 let config = create_test_config();
1764 let client = VeracodeClient::new(config)
1765 .expect("valid test client configuration");
1766
1767 let url = "https://api.veracode.com/api/test";
1768 let result = client.generate_auth_header(&method, url);
1769
1770 if let Ok(header) = result {
1771 prop_assert!(header.starts_with("VERACODE-HMAC-SHA-256"));
1773
1774 prop_assert!(header.contains("id="));
1776 prop_assert!(header.contains("ts="));
1777 prop_assert!(header.contains("nonce="));
1778 prop_assert!(header.contains("sig="));
1779
1780 let parts: Vec<&str> = header.split(',').collect();
1782 prop_assert_eq!(parts.len(), 4);
1783 }
1784 }
1785
1786 #[test]
1789 fn proptest_auth_header_nonce_unique(
1790 _seed in any::<u8>(),
1791 ) {
1792 let config = create_test_config();
1793 let client = VeracodeClient::new(config)
1794 .expect("valid test client configuration");
1795
1796 let url = "https://api.veracode.com/api/test";
1797
1798 let header1 = client.generate_auth_header("GET", url)
1800 .expect("valid auth header generation");
1801 let header2 = client.generate_auth_header("GET", url)
1802 .expect("valid auth header generation");
1803
1804 fn extract_nonce(h: &str) -> Option<String> {
1806 Some(h.split("nonce=")
1807 .nth(1)?
1808 .split(',')
1809 .next()?
1810 .to_string())
1811 }
1812
1813 if let (Some(nonce1), Some(nonce2)) = (extract_nonce(&header1), extract_nonce(&header2)) {
1814 prop_assert_ne!(&nonce1, &nonce2);
1817
1818 prop_assert_eq!(nonce1.len(), 32);
1820 prop_assert_eq!(nonce2.len(), 32);
1821 prop_assert!(nonce1.chars().all(|c| c.is_ascii_hexdigit()));
1822 prop_assert!(nonce2.chars().all(|c| c.is_ascii_hexdigit()));
1823 }
1824 }
1825 }
1826
1827 proptest! {
1832 #![proptest_config(ProptestConfig {
1833 cases: if cfg!(miri) { 5 } else { 100 },
1834 failure_persistence: None,
1835 .. ProptestConfig::default()
1836 })]
1837
1838 #[test]
1841 fn proptest_client_creation_invalid_proxy(
1842 invalid_proxy in ".*{0,100}",
1843 ) {
1844 use crate::{VeracodeCredentials, VeracodeRegion};
1845
1846 let config = VeracodeConfig {
1847 credentials: VeracodeCredentials::new(
1848 "test_api_id".to_string(),
1849 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
1850 ),
1851 base_url: "https://api.veracode.com".to_string(),
1852 rest_base_url: "https://api.veracode.com".to_string(),
1853 xml_base_url: "https://analysiscenter.veracode.com".to_string(),
1854 region: VeracodeRegion::Commercial,
1855 validate_certificates: true,
1856 connect_timeout: 30,
1857 request_timeout: 300,
1858 proxy_url: Some(invalid_proxy.clone()),
1859 proxy_username: None,
1860 proxy_password: None,
1861 retry_config: Default::default(),
1862 };
1863
1864 let result = VeracodeClient::new(config);
1865
1866 match result {
1868 Ok(_) => {
1869 prop_assert!(reqwest::Proxy::all(&invalid_proxy).is_ok());
1871 },
1872 Err(e) => {
1873 prop_assert!(matches!(e, VeracodeError::InvalidConfig(_)));
1875 }
1876 }
1877 }
1878
1879 #[test]
1882 fn proptest_client_timeouts_safe(
1883 connect_timeout in 1u64..=3600,
1884 request_timeout in 1u64..=7200,
1885 ) {
1886 use crate::{VeracodeCredentials, VeracodeRegion};
1887
1888 let config = VeracodeConfig {
1889 credentials: VeracodeCredentials::new(
1890 "test_api_id".to_string(),
1891 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
1892 ),
1893 base_url: "https://api.veracode.com".to_string(),
1894 rest_base_url: "https://api.veracode.com".to_string(),
1895 xml_base_url: "https://analysiscenter.veracode.com".to_string(),
1896 region: VeracodeRegion::Commercial,
1897 validate_certificates: true,
1898 connect_timeout,
1899 request_timeout,
1900 proxy_url: None,
1901 proxy_username: None,
1902 proxy_password: None,
1903 retry_config: Default::default(),
1904 };
1905
1906 let result = VeracodeClient::new(config);
1908 prop_assert!(result.is_ok());
1909 }
1910 }
1911
1912 proptest! {
1917 #![proptest_config(ProptestConfig {
1918 cases: if cfg!(miri) { 5 } else { 100 },
1919 failure_persistence: None,
1920 .. ProptestConfig::default()
1921 })]
1922
1923 #[test]
1926 fn proptest_file_upload_capacity_safe(
1927 file_size in 0usize..=1000000,
1928 ) {
1929 let file_data = vec![0u8; file_size];
1931
1932 let file_data_arc = Arc::new(file_data);
1934
1935 prop_assert_eq!(file_data_arc.len(), file_size);
1937
1938 let clone1 = Arc::clone(&file_data_arc);
1940 let clone2 = Arc::clone(&file_data_arc);
1941 prop_assert_eq!(clone1.len(), file_size);
1942 prop_assert_eq!(clone2.len(), file_size);
1943 }
1944
1945 #[test]
1948 fn proptest_content_type_safe(
1949 content_type in ".*{0,200}",
1950 ) {
1951 let content_type_cow: Cow<str> = if content_type.len() < 64 {
1953 Cow::Borrowed(&content_type)
1954 } else {
1955 Cow::Owned(content_type.clone())
1956 };
1957
1958 let ct_lower = content_type_cow.to_lowercase();
1960 if ct_lower.contains("<script>") || ct_lower.contains("javascript:") {
1961 prop_assert!(content_type_cow.as_ref().contains("<script>") ||
1963 content_type_cow.as_ref().contains("javascript:"));
1964 }
1965
1966 prop_assert_eq!(content_type_cow.len(), content_type.len());
1968 }
1969 }
1970
1971 #[test]
1976 fn test_hmac_signature_with_query_params() {
1977 let config = create_test_config();
1978 let client = VeracodeClient::new(config).expect("valid test client configuration");
1979
1980 let url = "https://api.veracode.com/api/test?param1=value1¶m2=value2";
1982 let nonce = "0123456789abcdef0123456789abcdef";
1983 let timestamp = 1234567890000;
1984
1985 let result = client.generate_hmac_signature("GET", url, timestamp, nonce);
1986 assert!(result.is_ok());
1987
1988 let signature = result.expect("valid HMAC signature");
1989 assert_eq!(signature.len(), 64);
1991 assert!(signature.chars().all(|c| c.is_ascii_hexdigit()));
1992 }
1993
1994 #[test]
1995 fn test_hmac_signature_different_methods() {
1996 let config = create_test_config();
1997 let client = VeracodeClient::new(config).expect("valid test client configuration");
1998
1999 let url = "https://api.veracode.com/api/test";
2000 let nonce = "0123456789abcdef0123456789abcdef";
2001 let timestamp = 1234567890000;
2002
2003 let sig_get = client
2004 .generate_hmac_signature("GET", url, timestamp, nonce)
2005 .expect("valid HMAC signature for GET");
2006 let sig_post = client
2007 .generate_hmac_signature("POST", url, timestamp, nonce)
2008 .expect("valid HMAC signature for POST");
2009
2010 assert_ne!(sig_get, sig_post);
2012 }
2013
2014 #[test]
2015 fn test_url_encoding_special_characters() {
2016 let config = create_test_config();
2017 let client = VeracodeClient::new(config).expect("valid test client configuration");
2018
2019 let params = vec![
2021 ("key1", "value with spaces"),
2022 ("key2", "value&with&ersands"),
2023 ("key3", "value=with=equals"),
2024 ("key4", "value?with?questions"),
2025 ];
2026
2027 let url = client.build_url_with_params("/api/test", ¶ms);
2028
2029 assert!(url.contains("value%20with%20spaces") || url.contains("value+with+spaces"));
2031 assert!(url.contains("%26"));
2033 assert!(url.starts_with("https://api.veracode.com/api/test?"));
2035 }
2036
2037 #[test]
2038 fn test_url_encoding_unicode() {
2039 let config = create_test_config();
2040 let client = VeracodeClient::new(config).expect("valid test client configuration");
2041
2042 let params = vec![
2044 ("key", "δ½ ε₯½δΈη"), ("key2", "ππ‘οΈ"), ];
2047
2048 let url = client.build_url_with_params("/api/test", ¶ms);
2049
2050 assert!(url.starts_with("https://api.veracode.com/api/test?"));
2052 assert!(url.contains('%'));
2054 }
2055
2056 #[test]
2057 fn test_empty_query_params() {
2058 let config = create_test_config();
2059 let client = VeracodeClient::new(config).expect("valid test client configuration");
2060
2061 let url = client.build_url_with_params("/api/test", &[]);
2062
2063 assert_eq!(url, "https://api.veracode.com/api/test");
2065 }
2066
2067 #[test]
2068 fn test_invalid_api_key_format() {
2069 use crate::{VeracodeCredentials, VeracodeRegion};
2070
2071 let config = VeracodeConfig {
2073 credentials: VeracodeCredentials::new(
2074 "test_api_id".to_string(),
2075 "not_valid_hex_key".to_string(),
2076 ),
2077 base_url: "https://api.veracode.com".to_string(),
2078 rest_base_url: "https://api.veracode.com".to_string(),
2079 xml_base_url: "https://analysiscenter.veracode.com".to_string(),
2080 region: VeracodeRegion::Commercial,
2081 validate_certificates: true,
2082 connect_timeout: 30,
2083 request_timeout: 300,
2084 proxy_url: None,
2085 proxy_username: None,
2086 proxy_password: None,
2087 retry_config: Default::default(),
2088 };
2089
2090 let client = VeracodeClient::new(config).expect("valid test client configuration");
2091 let result = client.generate_auth_header("GET", "https://api.veracode.com/api/test");
2092
2093 assert!(matches!(result, Err(VeracodeError::Authentication(_))));
2095 }
2096
2097 #[test]
2098 fn test_auth_header_format() {
2099 let config = create_test_config();
2100 let client = VeracodeClient::new(config).expect("valid test client configuration");
2101
2102 let header = client
2103 .generate_auth_header("GET", "https://api.veracode.com/api/test")
2104 .expect("valid auth header generation");
2105
2106 assert!(header.starts_with("VERACODE-HMAC-SHA-256 "));
2108 assert!(header.contains("id=test_api_id"));
2109 assert!(header.contains("ts="));
2110 assert!(header.contains("nonce="));
2111 assert!(header.contains("sig="));
2112
2113 let parts: Vec<&str> = header.split(',').collect();
2115 assert_eq!(parts.len(), 4);
2116 }
2117
2118 #[cfg(not(miri))] #[test]
2120 fn test_auth_header_timestamp_monotonic() {
2121 let config = create_test_config();
2122 let client = VeracodeClient::new(config).expect("valid test client configuration");
2123
2124 let header1 = client
2125 .generate_auth_header("GET", "https://api.veracode.com/api/test")
2126 .expect("valid auth header generation");
2127 std::thread::sleep(std::time::Duration::from_millis(10));
2128 let header2 = client
2129 .generate_auth_header("GET", "https://api.veracode.com/api/test")
2130 .expect("valid auth header generation");
2131
2132 let extract_ts =
2134 |h: &str| -> Option<u64> { h.split("ts=").nth(1)?.split(',').next()?.parse().ok() };
2135
2136 let ts1 = extract_ts(&header1).expect("valid timestamp extraction");
2137 let ts2 = extract_ts(&header2).expect("valid timestamp extraction");
2138
2139 assert!(ts2 >= ts1);
2141 }
2142
2143 #[test]
2144 fn test_base_url_accessor() {
2145 let config = create_test_config();
2146 let client = VeracodeClient::new(config).expect("valid test client configuration");
2147
2148 assert_eq!(client.base_url(), "https://api.veracode.com");
2149 }
2150
2151 #[test]
2152 fn test_client_clone() {
2153 let config = create_test_config();
2154 let client1 = VeracodeClient::new(config).expect("valid test client configuration");
2155 let client2 = client1.clone();
2156
2157 assert_eq!(client1.base_url(), client2.base_url());
2159 }
2160
2161 #[test]
2162 fn test_url_capacity_estimation() {
2163 let config = create_test_config();
2164 let client = VeracodeClient::new(config).expect("valid test client configuration");
2165
2166 let params: Vec<(&str, &str)> = (0..100).map(|_| ("key", "value")).collect();
2168
2169 let url = client.build_url_with_params("/api/test", ¶ms);
2170
2171 assert!(url.starts_with("https://api.veracode.com/api/test?"));
2173 assert!(url.len() > 100); }
2175
2176 #[test]
2177 fn test_saturating_arithmetic() {
2178 let config = create_test_config();
2179 let client = VeracodeClient::new(config).expect("valid test client configuration");
2180
2181 let params: Vec<(&str, &str)> = vec![("k", "v"); 1000];
2183
2184 let url = client.build_url_with_params("/api/test", ¶ms);
2186 assert!(url.len() < usize::MAX);
2187 }
2188}