1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5use crate::json_validator::{MAX_JSON_DEPTH, validate_json_depth};
6use crate::validation::validate_url_segment;
7use crate::{VeracodeClient, VeracodeError};
8
9const MAX_PAGE_SIZE: u64 = 500;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ApiErrorResponse {
15 #[serde(rename = "_embedded")]
16 pub embedded: Option<ApiErrorEmbedded>,
17 pub fallback_type: Option<String>,
18 pub full_type: Option<String>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ApiErrorEmbedded {
24 pub api_errors: Vec<ApiError>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ApiError {
30 pub id: String,
31 pub code: String,
32 pub title: String,
33 pub status: String,
34 pub source: Option<ApiErrorSource>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ApiErrorSource {
40 pub pointer: String,
41 pub parameter: String,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct Sandbox {
47 pub id: Option<u64>,
48 pub guid: String,
49 pub name: String,
50 pub description: Option<String>,
51 pub created: DateTime<Utc>,
52 pub modified: DateTime<Utc>,
53 pub auto_recreate: bool,
54 pub custom_fields: Option<HashMap<String, String>>,
55 pub owner: Option<String>,
56 pub owner_username: Option<String>,
57 pub organization_id: Option<u64>,
58 pub application_guid: Option<String>,
59 pub team_identifiers: Option<Vec<String>>,
60 pub scan_url: Option<String>,
61 pub last_scan_date: Option<DateTime<Utc>>,
62 pub status: Option<String>,
63 #[serde(rename = "_links")]
64 pub links: Option<serde_json::Value>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct CreateSandboxRequest {
70 pub name: String,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub description: Option<String>,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub auto_recreate: Option<bool>,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub custom_fields: Option<HashMap<String, String>>,
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub team_identifiers: Option<Vec<String>>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct UpdateSandboxRequest {
84 pub name: Option<String>,
85 pub description: Option<String>,
86 pub auto_recreate: Option<bool>,
87 pub custom_fields: Option<HashMap<String, String>>,
88 pub team_identifiers: Option<Vec<String>>,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct SandboxListResponse {
94 #[serde(rename = "_embedded")]
95 pub embedded: Option<SandboxEmbedded>,
96 pub page: Option<PageInfo>,
97 pub total: Option<u64>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct SandboxEmbedded {
103 pub sandboxes: Vec<Sandbox>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct PageInfo {
109 pub size: u64,
110 pub number: u64,
111 pub total_elements: u64,
112 pub total_pages: u64,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct SandboxScan {
118 pub scan_id: u64,
119 pub scan_type: String,
120 pub status: String,
121 pub created: DateTime<Utc>,
122 pub modified: DateTime<Utc>,
123 pub scan_url: Option<String>,
124 pub results_ready: bool,
125 pub engine_version: Option<String>,
126}
127
128#[derive(Debug, Clone, Default)]
130pub struct SandboxListParams {
131 pub name: Option<String>,
132 pub owner: Option<String>,
133 pub team: Option<String>,
134 pub page: Option<u64>,
135 pub size: Option<u64>,
136 pub modified_after: Option<DateTime<Utc>>,
137 pub modified_before: Option<DateTime<Utc>>,
138}
139
140impl SandboxListParams {
141 #[must_use]
143 pub fn to_query_params(&self) -> Vec<(String, String)> {
144 Vec::from(self) }
146}
147
148impl From<&SandboxListParams> for Vec<(String, String)> {
150 fn from(query: &SandboxListParams) -> Self {
151 let mut params = Vec::new();
152
153 if let Some(ref name) = query.name {
154 params.push(("name".to_string(), name.clone())); }
156 if let Some(ref owner) = query.owner {
157 params.push(("owner".to_string(), owner.clone()));
158 }
159 if let Some(ref team) = query.team {
160 params.push(("team".to_string(), team.clone()));
161 }
162 if let Some(page) = query.page {
163 params.push(("page".to_string(), page.to_string()));
164 }
165 if let Some(size) = query.size {
166 let safe_size = size.min(MAX_PAGE_SIZE);
168 params.push(("size".to_string(), safe_size.to_string()));
169 }
170 if let Some(modified_after) = query.modified_after {
171 params.push(("modified_after".to_string(), modified_after.to_rfc3339()));
172 }
173 if let Some(modified_before) = query.modified_before {
174 params.push(("modified_before".to_string(), modified_before.to_rfc3339()));
175 }
176
177 params
178 }
179}
180
181impl From<SandboxListParams> for Vec<(String, String)> {
182 fn from(query: SandboxListParams) -> Self {
183 let mut params = Vec::new();
184
185 if let Some(name) = query.name {
186 params.push(("name".to_string(), name)); }
188 if let Some(owner) = query.owner {
189 params.push(("owner".to_string(), owner)); }
191 if let Some(team) = query.team {
192 params.push(("team".to_string(), team)); }
194 if let Some(page) = query.page {
195 params.push(("page".to_string(), page.to_string()));
196 }
197 if let Some(size) = query.size {
198 let safe_size = size.min(MAX_PAGE_SIZE);
200 params.push(("size".to_string(), safe_size.to_string()));
201 }
202 if let Some(modified_after) = query.modified_after {
203 params.push(("modified_after".to_string(), modified_after.to_rfc3339()));
204 }
205 if let Some(modified_before) = query.modified_before {
206 params.push(("modified_before".to_string(), modified_before.to_rfc3339()));
207 }
208
209 params
210 }
211}
212
213#[derive(Debug)]
220#[must_use = "Need to handle all error enum types."]
221pub enum SandboxError {
222 Api(VeracodeError),
224 NotFound,
226 InvalidInput(String),
228 LimitExceeded,
230 OperationNotAllowed(String),
232 AlreadyExists(String),
234}
235
236impl std::fmt::Display for SandboxError {
237 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238 match self {
239 SandboxError::Api(err) => write!(f, "API error: {err}"),
240 SandboxError::NotFound => write!(f, "Sandbox not found"),
241 SandboxError::InvalidInput(msg) => write!(f, "Invalid input: {msg}"),
242 SandboxError::LimitExceeded => write!(f, "Maximum number of sandboxes reached"),
243 SandboxError::OperationNotAllowed(msg) => write!(f, "Operation not allowed: {msg}"),
244 SandboxError::AlreadyExists(msg) => write!(f, "Sandbox already exists: {msg}"),
245 }
246 }
247}
248
249impl std::error::Error for SandboxError {}
250
251impl From<VeracodeError> for SandboxError {
252 fn from(err: VeracodeError) -> Self {
253 SandboxError::Api(err)
254 }
255}
256
257impl From<reqwest::Error> for SandboxError {
258 fn from(err: reqwest::Error) -> Self {
259 SandboxError::Api(VeracodeError::Http(err))
260 }
261}
262
263impl From<serde_json::Error> for SandboxError {
264 fn from(err: serde_json::Error) -> Self {
265 SandboxError::Api(VeracodeError::Serialization(err))
266 }
267}
268
269pub struct SandboxApi<'a> {
271 client: &'a VeracodeClient,
272}
273
274impl<'a> SandboxApi<'a> {
275 #[must_use]
282 pub fn new(client: &'a VeracodeClient) -> Self {
283 Self { client }
284 }
285
286 pub async fn list_sandboxes(
302 &self,
303 application_guid: &str,
304 params: Option<SandboxListParams>,
305 ) -> Result<Vec<Sandbox>, SandboxError> {
306 let endpoint = format!("/appsec/v1/applications/{application_guid}/sandboxes");
307
308 let query_params = params.as_ref().map(Vec::from);
309
310 let response = self.client.get(&endpoint, query_params.as_deref()).await?;
311
312 let status = response.status().as_u16();
313 match status {
314 200 => {
315 let response_text = response.text().await?;
316
317 if validate_json_depth(&response_text, MAX_JSON_DEPTH).is_err() {
319 return Err(SandboxError::Api(VeracodeError::InvalidResponse(
320 "JSON validation failed on response".to_string(),
321 )));
322 }
323
324 let sandbox_response: SandboxListResponse = serde_json::from_str(&response_text)?;
325 Ok(sandbox_response
326 .embedded
327 .map(|e| e.sandboxes)
328 .unwrap_or_default())
329 }
330 404 => Err(SandboxError::NotFound),
331 _ => {
332 let error_text = response.text().await.unwrap_or_default();
333 Err(SandboxError::Api(VeracodeError::InvalidResponse(format!(
334 "HTTP {status}: {error_text}"
335 ))))
336 }
337 }
338 }
339
340 pub async fn get_sandbox(
356 &self,
357 application_guid: &str,
358 sandbox_guid: &str,
359 ) -> Result<Sandbox, SandboxError> {
360 let endpoint =
361 format!("/appsec/v1/applications/{application_guid}/sandboxes/{sandbox_guid}");
362
363 let response = self.client.get(&endpoint, None).await?;
364
365 let status = response.status().as_u16();
366 match status {
367 200 => {
368 let response_text = response.text().await?;
369
370 if validate_json_depth(&response_text, MAX_JSON_DEPTH).is_err() {
372 return Err(SandboxError::Api(VeracodeError::InvalidResponse(
373 "JSON validation failed on response".to_string(),
374 )));
375 }
376
377 let sandbox: Sandbox = serde_json::from_str(&response_text)?;
378 Ok(sandbox)
379 }
380 404 => Err(SandboxError::NotFound),
381 _ => {
382 let error_text = response.text().await.unwrap_or_default();
383 Err(SandboxError::Api(VeracodeError::InvalidResponse(format!(
384 "HTTP {status}: {error_text}"
385 ))))
386 }
387 }
388 }
389
390 pub async fn create_sandbox(
406 &self,
407 application_guid: &str,
408 request: CreateSandboxRequest,
409 ) -> Result<Sandbox, SandboxError> {
410 Self::validate_create_request(&request)?;
412
413 let endpoint = format!("/appsec/v1/applications/{application_guid}/sandboxes");
414
415 let response = self.client.post(&endpoint, Some(&request)).await?;
416
417 let status = response.status().as_u16();
418 match status {
419 200 | 201 => {
420 let sandbox: Sandbox = response.json().await?;
421 Ok(sandbox)
422 }
423 400 => {
424 let error_text = response.text().await.unwrap_or_default();
425
426 if validate_json_depth(&error_text, MAX_JSON_DEPTH).is_err() {
428 return Err(SandboxError::Api(VeracodeError::InvalidResponse(
429 "JSON validation failed on error response".to_string(),
430 )));
431 }
432
433 if let Ok(error_response) = serde_json::from_str::<ApiErrorResponse>(&error_text)
435 && let Some(embedded) = error_response.embedded
436 {
437 for api_error in embedded.api_errors {
438 if api_error.title.contains("already exists") {
439 return Err(SandboxError::AlreadyExists(api_error.title));
440 }
441 if api_error.title.contains("limit") || api_error.title.contains("maximum")
442 {
443 return Err(SandboxError::LimitExceeded);
444 }
445 if api_error.title.contains("Json Parse Error")
446 || api_error.title.contains("Cannot deserialize")
447 {
448 return Err(SandboxError::InvalidInput(format!(
449 "JSON parsing error: {}",
450 api_error.title
451 )));
452 }
453 }
454 }
455
456 if error_text.contains("limit") || error_text.contains("maximum") {
458 Err(SandboxError::LimitExceeded)
459 } else if error_text.contains("already exists") {
460 Err(SandboxError::AlreadyExists(error_text))
461 } else {
462 Err(SandboxError::InvalidInput(error_text))
463 }
464 }
465 404 => Err(SandboxError::NotFound),
466 _ => {
467 let error_text = response.text().await.unwrap_or_default();
468 Err(SandboxError::Api(VeracodeError::InvalidResponse(format!(
469 "HTTP {status}: {error_text}"
470 ))))
471 }
472 }
473 }
474
475 pub async fn update_sandbox(
492 &self,
493 application_guid: &str,
494 sandbox_guid: &str,
495 request: UpdateSandboxRequest,
496 ) -> Result<Sandbox, SandboxError> {
497 Self::validate_update_request(&request)?;
499
500 let endpoint =
501 format!("/appsec/v1/applications/{application_guid}/sandboxes/{sandbox_guid}");
502
503 let response = self.client.put(&endpoint, Some(&request)).await?;
504
505 let status = response.status().as_u16();
506 match status {
507 200 => {
508 let response_text = response.text().await?;
509
510 if validate_json_depth(&response_text, MAX_JSON_DEPTH).is_err() {
512 return Err(SandboxError::Api(VeracodeError::InvalidResponse(
513 "JSON validation failed on response".to_string(),
514 )));
515 }
516
517 let sandbox: Sandbox = serde_json::from_str(&response_text)?;
518 Ok(sandbox)
519 }
520 400 => {
521 let error_text = response.text().await.unwrap_or_default();
522 Err(SandboxError::InvalidInput(error_text))
523 }
524 404 => Err(SandboxError::NotFound),
525 _ => {
526 let error_text = response.text().await.unwrap_or_default();
527 Err(SandboxError::Api(VeracodeError::InvalidResponse(format!(
528 "HTTP {status}: {error_text}"
529 ))))
530 }
531 }
532 }
533
534 pub async fn delete_sandbox(
550 &self,
551 application_guid: &str,
552 sandbox_guid: &str,
553 ) -> Result<(), SandboxError> {
554 let endpoint =
555 format!("/appsec/v1/applications/{application_guid}/sandboxes/{sandbox_guid}");
556
557 let response = self.client.delete(&endpoint).await?;
558
559 let status = response.status().as_u16();
560 match status {
561 204 => Ok(()),
562 404 => Err(SandboxError::NotFound),
563 409 => {
564 let error_text = response.text().await.unwrap_or_default();
565 Err(SandboxError::OperationNotAllowed(error_text))
566 }
567 _ => {
568 let error_text = response.text().await.unwrap_or_default();
569 Err(SandboxError::Api(VeracodeError::InvalidResponse(format!(
570 "HTTP {status}: {error_text}"
571 ))))
572 }
573 }
574 }
575
576 pub async fn promote_sandbox_scan(
593 &self,
594 application_guid: &str,
595 sandbox_guid: &str,
596 delete_on_promote: bool,
597 ) -> Result<(), SandboxError> {
598 let endpoint = if delete_on_promote {
599 format!(
600 "/appsec/v1/applications/{application_guid}/sandboxes/{sandbox_guid}/promote?delete_on_promote=true"
601 )
602 } else {
603 format!("/appsec/v1/applications/{application_guid}/sandboxes/{sandbox_guid}/promote")
604 };
605
606 let response = self.client.post(&endpoint, None::<&()>).await?;
607
608 let status = response.status().as_u16();
609 match status {
610 200 | 204 => Ok(()),
611 404 => Err(SandboxError::NotFound),
612 409 => {
613 let error_text = response.text().await.unwrap_or_default();
614 Err(SandboxError::OperationNotAllowed(error_text))
615 }
616 _ => {
617 let error_text = response.text().await.unwrap_or_default();
618 Err(SandboxError::Api(VeracodeError::InvalidResponse(format!(
619 "HTTP {status}: {error_text}"
620 ))))
621 }
622 }
623 }
624
625 pub async fn get_sandbox_scans(
641 &self,
642 application_guid: &str,
643 sandbox_guid: &str,
644 ) -> Result<Vec<SandboxScan>, SandboxError> {
645 let endpoint =
646 format!("/appsec/v1/applications/{application_guid}/sandboxes/{sandbox_guid}/scans");
647
648 let response = self.client.get(&endpoint, None).await?;
649
650 let status = response.status().as_u16();
651 match status {
652 200 => {
653 let response_text = response.text().await?;
654
655 if validate_json_depth(&response_text, MAX_JSON_DEPTH).is_err() {
657 return Err(SandboxError::Api(VeracodeError::InvalidResponse(
658 "JSON validation failed on response".to_string(),
659 )));
660 }
661
662 let scans: Vec<SandboxScan> = serde_json::from_str(&response_text)?;
663 Ok(scans)
664 }
665 404 => Err(SandboxError::NotFound),
666 _ => {
667 let error_text = response.text().await.unwrap_or_default();
668 Err(SandboxError::Api(VeracodeError::InvalidResponse(format!(
669 "HTTP {status}: {error_text}"
670 ))))
671 }
672 }
673 }
674
675 pub async fn sandbox_exists(
691 &self,
692 application_guid: &str,
693 sandbox_guid: &str,
694 ) -> Result<bool, SandboxError> {
695 match self.get_sandbox(application_guid, sandbox_guid).await {
696 Ok(_) => Ok(true),
697 Err(SandboxError::NotFound) => Ok(false),
698 Err(e) => Err(e),
699 }
700 }
701
702 pub async fn get_sandbox_by_name(
718 &self,
719 application_guid: &str,
720 name: &str,
721 ) -> Result<Option<Sandbox>, SandboxError> {
722 let params = SandboxListParams {
723 name: Some(name.to_string()),
724 ..Default::default()
725 };
726
727 let sandboxes = self.list_sandboxes(application_guid, Some(params)).await?;
728 Ok(sandboxes.into_iter().find(|s| s.name == name))
729 }
730
731 fn validate_name(name: &str) -> Result<(), SandboxError> {
733 if name.is_empty() {
734 return Err(SandboxError::InvalidInput(
735 "Sandbox name cannot be empty".to_string(),
736 ));
737 }
738 if name.len() > 256 {
739 return Err(SandboxError::InvalidInput(
740 "Sandbox name too long (max 256 characters)".to_string(),
741 ));
742 }
743
744 if name.contains(['<', '>', '"', '&', '\'']) {
746 return Err(SandboxError::InvalidInput(
747 "Sandbox name contains invalid characters".to_string(),
748 ));
749 }
750
751 validate_url_segment(name, 256)
753 .map_err(|e| SandboxError::InvalidInput(format!("Invalid sandbox name: {}", e)))?;
754
755 Ok(())
756 }
757
758 fn validate_create_request(request: &CreateSandboxRequest) -> Result<(), SandboxError> {
760 Self::validate_name(&request.name)?;
761
762 if let Some(ref custom_fields) = request.custom_fields {
764 Self::validate_custom_fields(custom_fields)?;
765 }
766
767 if let Some(ref team_ids) = request.team_identifiers {
769 Self::validate_team_identifiers(team_ids)?;
770 }
771
772 Ok(())
773 }
774
775 fn validate_update_request(request: &UpdateSandboxRequest) -> Result<(), SandboxError> {
777 if let Some(name) = &request.name {
778 Self::validate_name(name)?;
779 }
780
781 if let Some(ref custom_fields) = request.custom_fields {
783 Self::validate_custom_fields(custom_fields)?;
784 }
785
786 if let Some(ref team_ids) = request.team_identifiers {
788 Self::validate_team_identifiers(team_ids)?;
789 }
790
791 Ok(())
792 }
793
794 fn validate_custom_fields(custom_fields: &HashMap<String, String>) -> Result<(), SandboxError> {
796 const MAX_CUSTOM_FIELDS: usize = 50;
797 const MAX_KEY_LENGTH: usize = 128;
798 const MAX_VALUE_LENGTH: usize = 1024;
799
800 if custom_fields.len() > MAX_CUSTOM_FIELDS {
801 return Err(SandboxError::InvalidInput(format!(
802 "Too many custom fields (max {MAX_CUSTOM_FIELDS})"
803 )));
804 }
805
806 for (key, value) in custom_fields {
807 if key.is_empty() {
809 return Err(SandboxError::InvalidInput(
810 "Custom field key cannot be empty".to_string(),
811 ));
812 }
813 if key.len() > MAX_KEY_LENGTH {
814 return Err(SandboxError::InvalidInput(format!(
815 "Custom field key too long (max {MAX_KEY_LENGTH} characters)"
816 )));
817 }
818 if key.chars().any(|c| c.is_control()) {
819 return Err(SandboxError::InvalidInput(
820 "Custom field key contains control characters".to_string(),
821 ));
822 }
823 if key.contains(['<', '>', '"', '&', '\'', '/', '\\']) {
824 return Err(SandboxError::InvalidInput(
825 "Custom field key contains invalid characters".to_string(),
826 ));
827 }
828
829 if value.len() > MAX_VALUE_LENGTH {
831 return Err(SandboxError::InvalidInput(format!(
832 "Custom field value too long (max {MAX_VALUE_LENGTH} characters)"
833 )));
834 }
835 if value
836 .chars()
837 .any(|c| c.is_control() && c != '\n' && c != '\r' && c != '\t')
838 {
839 return Err(SandboxError::InvalidInput(
840 "Custom field value contains invalid control characters".to_string(),
841 ));
842 }
843 }
844
845 Ok(())
846 }
847
848 fn validate_team_identifiers(team_ids: &[String]) -> Result<(), SandboxError> {
850 const MAX_TEAM_IDS: usize = 100;
851 const MAX_TEAM_ID_LENGTH: usize = 128;
852
853 if team_ids.len() > MAX_TEAM_IDS {
854 return Err(SandboxError::InvalidInput(format!(
855 "Too many team identifiers (max {MAX_TEAM_IDS})"
856 )));
857 }
858
859 for team_id in team_ids {
860 if team_id.is_empty() {
861 return Err(SandboxError::InvalidInput(
862 "Team identifier cannot be empty".to_string(),
863 ));
864 }
865 if team_id.len() > MAX_TEAM_ID_LENGTH {
866 return Err(SandboxError::InvalidInput(format!(
867 "Team identifier too long (max {MAX_TEAM_ID_LENGTH} characters)"
868 )));
869 }
870 if team_id.chars().any(|c| c.is_control()) {
871 return Err(SandboxError::InvalidInput(
872 "Team identifier contains control characters".to_string(),
873 ));
874 }
875 if team_id.contains(['<', '>', '"', '&', '\'', '/', '\\']) {
876 return Err(SandboxError::InvalidInput(
877 "Team identifier contains invalid characters".to_string(),
878 ));
879 }
880 }
881
882 Ok(())
883 }
884}
885
886impl<'a> SandboxApi<'a> {
888 pub async fn create_simple_sandbox(
904 &self,
905 application_guid: &str,
906 name: &str,
907 ) -> Result<Sandbox, SandboxError> {
908 let request = CreateSandboxRequest {
909 name: name.to_string(),
910 description: None,
911 auto_recreate: None,
912 custom_fields: None,
913 team_identifiers: None,
914 };
915
916 self.create_sandbox(application_guid, request).await
917 }
918
919 pub async fn create_auto_recreate_sandbox(
936 &self,
937 application_guid: &str,
938 name: &str,
939 description: Option<String>,
940 ) -> Result<Sandbox, SandboxError> {
941 let request = CreateSandboxRequest {
942 name: name.to_string(),
943 description,
944 auto_recreate: Some(true),
945 custom_fields: None,
946 team_identifiers: None,
947 };
948
949 self.create_sandbox(application_guid, request).await
950 }
951
952 pub async fn update_sandbox_name(
969 &self,
970 application_guid: &str,
971 sandbox_guid: &str,
972 new_name: &str,
973 ) -> Result<Sandbox, SandboxError> {
974 let request = UpdateSandboxRequest {
975 name: Some(new_name.to_string()),
976 description: None,
977 auto_recreate: None,
978 custom_fields: None,
979 team_identifiers: None,
980 };
981
982 self.update_sandbox(application_guid, sandbox_guid, request)
983 .await
984 }
985
986 pub async fn count_sandboxes(&self, application_guid: &str) -> Result<u64, SandboxError> {
1001 let endpoint = format!("/appsec/v1/applications/{application_guid}/sandboxes");
1002
1003 let query_params = vec![("size".to_string(), "1".to_string())];
1005
1006 let response = self.client.get(&endpoint, Some(&query_params)).await?;
1007
1008 let status = response.status().as_u16();
1009 match status {
1010 200 => {
1011 let response_text = response.text().await?;
1012
1013 if validate_json_depth(&response_text, MAX_JSON_DEPTH).is_err() {
1015 return Err(SandboxError::Api(VeracodeError::InvalidResponse(
1016 "JSON validation failed on response".to_string(),
1017 )));
1018 }
1019
1020 let sandbox_response: SandboxListResponse = serde_json::from_str(&response_text)?;
1021 Ok(sandbox_response
1023 .page
1024 .map(|p| p.total_elements)
1025 .or(sandbox_response.total)
1026 .unwrap_or(0))
1027 }
1028 404 => Ok(0), _ => {
1030 let error_text = response.text().await.unwrap_or_default();
1031 Err(SandboxError::Api(VeracodeError::InvalidResponse(format!(
1032 "HTTP {status}: {error_text}"
1033 ))))
1034 }
1035 }
1036 }
1037
1038 pub async fn get_sandbox_id_from_guid(
1066 &self,
1067 application_guid: &str,
1068 sandbox_guid: &str,
1069 ) -> Result<String, SandboxError> {
1070 let sandbox = self.get_sandbox(application_guid, sandbox_guid).await?;
1071 match sandbox.id {
1072 Some(id) => Ok(id.to_string()),
1073 None => Err(SandboxError::InvalidInput(
1074 "Sandbox has no numeric ID".to_string(),
1075 )),
1076 }
1077 }
1078
1079 pub async fn create_sandbox_if_not_exists(
1099 &self,
1100 application_guid: &str,
1101 name: &str,
1102 description: Option<String>,
1103 ) -> Result<Sandbox, SandboxError> {
1104 let create_request = CreateSandboxRequest {
1106 name: name.to_string(),
1107 description: description.clone(),
1108 auto_recreate: Some(true), custom_fields: None,
1110 team_identifiers: None,
1111 };
1112
1113 match self.create_sandbox(application_guid, create_request).await {
1114 Ok(sandbox) => Ok(sandbox),
1115 Err(SandboxError::AlreadyExists(_)) => {
1116 self.get_sandbox_by_name(application_guid, name)
1118 .await?
1119 .ok_or_else(|| {
1120 SandboxError::Api(VeracodeError::InvalidResponse(
1121 "Sandbox exists but cannot be retrieved".to_string(),
1122 ))
1123 })
1124 }
1125 Err(e) => Err(e),
1126 }
1127 }
1128}
1129
1130#[cfg(test)]
1131mod tests {
1132 use super::*;
1133 use proptest::prelude::*;
1134
1135 #[test]
1136 fn test_validate_create_request() {
1137 let valid_request = CreateSandboxRequest {
1139 name: "valid-sandbox".to_string(),
1140 description: None,
1141 auto_recreate: None,
1142 custom_fields: None,
1143 team_identifiers: None,
1144 };
1145 assert!(SandboxApi::validate_create_request(&valid_request).is_ok());
1146
1147 let empty_name_request = CreateSandboxRequest {
1149 name: String::new(),
1150 description: None,
1151 auto_recreate: None,
1152 custom_fields: None,
1153 team_identifiers: None,
1154 };
1155 assert!(SandboxApi::validate_create_request(&empty_name_request).is_err());
1156
1157 let long_name_request = CreateSandboxRequest {
1159 name: "x".repeat(300),
1160 description: None,
1161 auto_recreate: None,
1162 custom_fields: None,
1163 team_identifiers: None,
1164 };
1165 assert!(SandboxApi::validate_create_request(&long_name_request).is_err());
1166
1167 let invalid_char_request = CreateSandboxRequest {
1169 name: "invalid<name>".to_string(),
1170 description: None,
1171 auto_recreate: None,
1172 custom_fields: None,
1173 team_identifiers: None,
1174 };
1175 assert!(SandboxApi::validate_create_request(&invalid_char_request).is_err());
1176 }
1177
1178 #[test]
1179 fn test_sandbox_list_params_to_query() {
1180 let params = SandboxListParams {
1181 name: Some("test".to_string()),
1182 page: Some(1),
1183 size: Some(10),
1184 ..Default::default()
1185 };
1186
1187 let query_params: Vec<_> = params.into();
1188 assert_eq!(query_params.len(), 3);
1189 assert!(query_params.contains(&("name".to_string(), "test".to_string())));
1190 assert!(query_params.contains(&("page".to_string(), "1".to_string())));
1191 assert!(query_params.contains(&("size".to_string(), "10".to_string())));
1192 }
1193
1194 #[test]
1195 fn test_sandbox_list_params_page_size_limit() {
1196 let params = SandboxListParams {
1198 size: Some(999999999),
1199 ..Default::default()
1200 };
1201
1202 let query_params: Vec<_> = params.into();
1203 assert_eq!(query_params.len(), 1);
1204 assert!(query_params.contains(&("size".to_string(), MAX_PAGE_SIZE.to_string())));
1205
1206 let params = SandboxListParams {
1208 size: Some(u64::MAX),
1209 ..Default::default()
1210 };
1211
1212 let query_params: Vec<_> = params.into();
1213 assert_eq!(query_params.len(), 1);
1214 assert!(query_params.contains(&("size".to_string(), MAX_PAGE_SIZE.to_string())));
1215
1216 let params = SandboxListParams {
1218 size: Some(100),
1219 ..Default::default()
1220 };
1221
1222 let query_params: Vec<_> = params.into();
1223 assert_eq!(query_params.len(), 1);
1224 assert!(query_params.contains(&("size".to_string(), "100".to_string())));
1225 }
1226
1227 #[test]
1228 fn test_sandbox_error_display() {
1229 let error = SandboxError::NotFound;
1230 assert_eq!(error.to_string(), "Sandbox not found");
1231
1232 let error = SandboxError::InvalidInput("test".to_string());
1233 assert_eq!(error.to_string(), "Invalid input: test");
1234
1235 let error = SandboxError::LimitExceeded;
1236 assert_eq!(error.to_string(), "Maximum number of sandboxes reached");
1237 }
1238
1239 #[test]
1240 fn test_validate_name_control_characters() {
1241 assert!(SandboxApi::validate_name("test\x00name").is_err());
1243 assert!(SandboxApi::validate_name("test\nname").is_err());
1244 assert!(SandboxApi::validate_name("test\rname").is_err());
1245 assert!(SandboxApi::validate_name("test\x1Fname").is_err());
1246 }
1247
1248 #[test]
1249 fn test_validate_name_path_traversal() {
1250 assert!(SandboxApi::validate_name("../etc/passwd").is_err());
1252 assert!(SandboxApi::validate_name("test/../name").is_err());
1253 assert!(SandboxApi::validate_name("test/name").is_err());
1254 assert!(SandboxApi::validate_name("test\\name").is_err());
1255 }
1256
1257 #[test]
1258 fn test_validate_custom_fields() {
1259 use std::collections::HashMap;
1260
1261 let mut valid_fields = HashMap::new();
1263 valid_fields.insert("key1".to_string(), "value1".to_string());
1264 assert!(SandboxApi::validate_custom_fields(&valid_fields).is_ok());
1265
1266 let mut empty_key = HashMap::new();
1268 empty_key.insert("".to_string(), "value".to_string());
1269 assert!(SandboxApi::validate_custom_fields(&empty_key).is_err());
1270
1271 let mut long_key = HashMap::new();
1273 long_key.insert("x".repeat(200), "value".to_string());
1274 assert!(SandboxApi::validate_custom_fields(&long_key).is_err());
1275
1276 let mut long_value = HashMap::new();
1278 long_value.insert("key".to_string(), "x".repeat(2000));
1279 assert!(SandboxApi::validate_custom_fields(&long_value).is_err());
1280
1281 let mut control_key = HashMap::new();
1283 control_key.insert("test\x00key".to_string(), "value".to_string());
1284 assert!(SandboxApi::validate_custom_fields(&control_key).is_err());
1285
1286 let mut invalid_key = HashMap::new();
1288 invalid_key.insert("test<key>".to_string(), "value".to_string());
1289 assert!(SandboxApi::validate_custom_fields(&invalid_key).is_err());
1290
1291 let mut too_many = HashMap::new();
1293 for i in 0..100 {
1294 too_many.insert(format!("key{i}"), format!("value{i}"));
1295 }
1296 assert!(SandboxApi::validate_custom_fields(&too_many).is_err());
1297 }
1298
1299 #[test]
1300 fn test_validate_team_identifiers() {
1301 assert!(
1303 SandboxApi::validate_team_identifiers(&["team1".to_string(), "team2".to_string()])
1304 .is_ok()
1305 );
1306
1307 assert!(SandboxApi::validate_team_identifiers(&["".to_string()]).is_err());
1309
1310 assert!(SandboxApi::validate_team_identifiers(&["x".repeat(200)]).is_err());
1312
1313 assert!(SandboxApi::validate_team_identifiers(&["team\x00id".to_string()]).is_err());
1315
1316 assert!(SandboxApi::validate_team_identifiers(&["team<id>".to_string()]).is_err());
1318
1319 let too_many: Vec<String> = (0..150).map(|i| format!("team{i}")).collect();
1321 assert!(SandboxApi::validate_team_identifiers(&too_many).is_err());
1322 }
1323
1324 mod proptest_security_tests {
1354 use super::*;
1355
1356 proptest! {
1358 #![proptest_config(ProptestConfig {
1359 cases: if cfg!(miri) { 5 } else { 1000 },
1360 failure_persistence: None,
1361 .. ProptestConfig::default()
1362 })]
1363
1364 #[test]
1365 fn prop_validate_name_rejects_control_chars(
1366 prefix in "[a-zA-Z0-9_-]{0,10}",
1367 control_char in prop::char::range('\x00', '\x1F'),
1368 suffix in "[a-zA-Z0-9_-]{0,10}",
1369 ) {
1370 let name = format!("{}{}{}", prefix, control_char, suffix);
1371 let result = SandboxApi::validate_name(&name);
1372 prop_assert!(result.is_err(), "Should reject control character: {:?}", control_char);
1373 }
1374 }
1375
1376 proptest! {
1378 #![proptest_config(ProptestConfig {
1379 cases: if cfg!(miri) { 5 } else { 1000 },
1380 failure_persistence: None,
1381 .. ProptestConfig::default()
1382 })]
1383
1384 #[test]
1385 fn prop_validate_name_rejects_path_traversal(
1386 prefix in "[a-zA-Z0-9_-]{0,20}",
1387 suffix in "[a-zA-Z0-9_-]{0,20}",
1388 ) {
1389 let test_cases = vec![
1390 format!("{}/../{}", prefix, suffix),
1391 format!("{}//{}", prefix, suffix),
1392 format!("{}\\{}", prefix, suffix),
1393 format!("{}..", suffix),
1394 ];
1395
1396 for name in test_cases {
1397 let result = SandboxApi::validate_name(&name);
1398 prop_assert!(result.is_err(), "Should reject path traversal in: {}", name);
1399 }
1400 }
1401 }
1402
1403 proptest! {
1405 #![proptest_config(ProptestConfig {
1406 cases: if cfg!(miri) { 5 } else { 1000 },
1407 failure_persistence: None,
1408 .. ProptestConfig::default()
1409 })]
1410
1411 #[test]
1412 #[allow(clippy::arithmetic_side_effects)]
1413 fn prop_validate_name_rejects_too_long(
1414 extra_len in 1usize..=100usize,
1415 ) {
1416 let name = "a".repeat(256 + extra_len);
1417 let result = SandboxApi::validate_name(&name);
1418 prop_assert!(result.is_err(), "Should reject name of length {}", name.len());
1419 }
1420 }
1421
1422 proptest! {
1424 #![proptest_config(ProptestConfig {
1425 cases: if cfg!(miri) { 5 } else { 1000 },
1426 failure_persistence: None,
1427 .. ProptestConfig::default()
1428 })]
1429
1430 #[test]
1431 fn prop_validate_name_accepts_valid_names(
1432 name in "[a-zA-Z0-9_-]{1,256}",
1433 ) {
1434 prop_assume!(!name.contains(".."));
1436 prop_assume!(!name.contains('/'));
1437 prop_assume!(!name.contains('\\'));
1438
1439 let result = SandboxApi::validate_name(&name);
1440 prop_assert!(result.is_ok(), "Should accept valid name: {}", name);
1441 }
1442 }
1443
1444 proptest! {
1446 #![proptest_config(ProptestConfig {
1447 cases: if cfg!(miri) { 5 } else { 500 },
1448 failure_persistence: None,
1449 .. ProptestConfig::default()
1450 })]
1451
1452 #[test]
1453 fn prop_validate_name_rejects_dangerous_chars(
1454 prefix in "[a-zA-Z0-9]{0,20}",
1455 dangerous in prop::sample::select(vec!['<', '>', '"', '&', '\'']),
1456 suffix in "[a-zA-Z0-9]{0,20}",
1457 ) {
1458 let name = format!("{}{}{}", prefix, dangerous, suffix);
1459 let result = SandboxApi::validate_name(&name);
1460 prop_assert!(result.is_err(), "Should reject dangerous character: {}", dangerous);
1461 }
1462 }
1463
1464 proptest! {
1466 #![proptest_config(ProptestConfig {
1467 cases: if cfg!(miri) { 5 } else { 1000 },
1468 failure_persistence: None,
1469 .. ProptestConfig::default()
1470 })]
1471
1472 #[test]
1473 fn prop_validate_custom_fields_rejects_empty_key(
1474 value in "[a-zA-Z0-9]{1,100}",
1475 ) {
1476 let mut fields = HashMap::new();
1477 fields.insert("".to_string(), value);
1478 let result = SandboxApi::validate_custom_fields(&fields);
1479 prop_assert!(result.is_err(), "Should reject empty key");
1480 }
1481 }
1482
1483 proptest! {
1485 #![proptest_config(ProptestConfig {
1486 cases: if cfg!(miri) { 5 } else { 1000 },
1487 failure_persistence: None,
1488 .. ProptestConfig::default()
1489 })]
1490
1491 #[test]
1492 fn prop_validate_custom_fields_rejects_long_key(
1493 extra_len in 1usize..=50usize,
1494 ) {
1495 let mut fields = HashMap::new();
1496 let long_key = "a".repeat(128_usize.saturating_add(extra_len));
1497 fields.insert(long_key.clone(), "value".to_string());
1498 let result = SandboxApi::validate_custom_fields(&fields);
1499 prop_assert!(result.is_err(), "Should reject key of length {}", long_key.len());
1500 }
1501 }
1502
1503 proptest! {
1505 #![proptest_config(ProptestConfig {
1506 cases: if cfg!(miri) { 5 } else { 1000 },
1507 failure_persistence: None,
1508 .. ProptestConfig::default()
1509 })]
1510
1511 #[test]
1512 fn prop_validate_custom_fields_rejects_long_value(
1513 extra_len in 1usize..=100usize,
1514 ) {
1515 let mut fields = HashMap::new();
1516 let long_value = "a".repeat(1024_usize.saturating_add(extra_len));
1517 fields.insert("key".to_string(), long_value.clone());
1518 let result = SandboxApi::validate_custom_fields(&fields);
1519 prop_assert!(result.is_err(), "Should reject value of length {}", long_value.len());
1520 }
1521 }
1522
1523 proptest! {
1525 #![proptest_config(ProptestConfig {
1526 cases: if cfg!(miri) { 5 } else { 100 },
1527 failure_persistence: None,
1528 .. ProptestConfig::default()
1529 })]
1530
1531 #[test]
1532 fn prop_validate_custom_fields_rejects_too_many(
1533 extra_count in 1usize..=20usize,
1534 ) {
1535 let mut fields = HashMap::new();
1536 let total = 50_usize.saturating_add(extra_count);
1537 for i in 0..total {
1538 fields.insert(format!("key{}", i), format!("value{}", i));
1539 }
1540 let result = SandboxApi::validate_custom_fields(&fields);
1541 prop_assert!(result.is_err(), "Should reject {} fields", total);
1542 }
1543 }
1544
1545 proptest! {
1547 #![proptest_config(ProptestConfig {
1548 cases: if cfg!(miri) { 5 } else { 1000 },
1549 failure_persistence: None,
1550 .. ProptestConfig::default()
1551 })]
1552
1553 #[test]
1554 fn prop_validate_custom_fields_rejects_control_in_key(
1555 prefix in "[a-zA-Z]{1,10}",
1556 control_char in prop::char::range('\x00', '\x1F'),
1557 suffix in "[a-zA-Z]{1,10}",
1558 ) {
1559 let mut fields = HashMap::new();
1560 let key = format!("{}{}{}", prefix, control_char, suffix);
1561 fields.insert(key.clone(), "value".to_string());
1562 let result = SandboxApi::validate_custom_fields(&fields);
1563 prop_assert!(result.is_err(), "Should reject key with control character: {:?}", control_char);
1564 }
1565 }
1566
1567 proptest! {
1569 #![proptest_config(ProptestConfig {
1570 cases: if cfg!(miri) { 5 } else { 500 },
1571 failure_persistence: None,
1572 .. ProptestConfig::default()
1573 })]
1574
1575 #[test]
1576 fn prop_validate_custom_fields_rejects_dangerous_in_key(
1577 prefix in "[a-zA-Z]{1,10}",
1578 dangerous in prop::sample::select(vec!['<', '>', '"', '&', '\'', '/', '\\']),
1579 suffix in "[a-zA-Z]{1,10}",
1580 ) {
1581 let mut fields = HashMap::new();
1582 let key = format!("{}{}{}", prefix, dangerous, suffix);
1583 fields.insert(key.clone(), "value".to_string());
1584 let result = SandboxApi::validate_custom_fields(&fields);
1585 prop_assert!(result.is_err(), "Should reject key with dangerous character: {}", dangerous);
1586 }
1587 }
1588
1589 proptest! {
1591 #![proptest_config(ProptestConfig {
1592 cases: if cfg!(miri) { 5 } else { 1000 },
1593 failure_persistence: None,
1594 .. ProptestConfig::default()
1595 })]
1596
1597 #[test]
1598 fn prop_validate_team_identifiers_rejects_empty(
1599 prefix_count in 0usize..=5usize,
1600 suffix_count in 0usize..=5usize,
1601 ) {
1602 let mut team_ids = Vec::new();
1603 for i in 0..prefix_count {
1604 team_ids.push(format!("team{}", i));
1605 }
1606 team_ids.push("".to_string());
1607 for i in 0..suffix_count {
1608 team_ids.push(format!("team{}", i.saturating_add(prefix_count)));
1609 }
1610 let result = SandboxApi::validate_team_identifiers(&team_ids);
1611 prop_assert!(result.is_err(), "Should reject empty team identifier");
1612 }
1613 }
1614
1615 proptest! {
1617 #![proptest_config(ProptestConfig {
1618 cases: if cfg!(miri) { 5 } else { 1000 },
1619 failure_persistence: None,
1620 .. ProptestConfig::default()
1621 })]
1622
1623 #[test]
1624 fn prop_validate_team_identifiers_rejects_too_long(
1625 extra_len in 1usize..=50usize,
1626 ) {
1627 let long_id = "a".repeat(128_usize.saturating_add(extra_len));
1628 let result = SandboxApi::validate_team_identifiers(std::slice::from_ref(&long_id));
1629 prop_assert!(result.is_err(), "Should reject identifier of length {}", long_id.len());
1630 }
1631 }
1632
1633 proptest! {
1635 #![proptest_config(ProptestConfig {
1636 cases: if cfg!(miri) { 5 } else { 100 },
1637 failure_persistence: None,
1638 .. ProptestConfig::default()
1639 })]
1640
1641 #[test]
1642 fn prop_validate_team_identifiers_rejects_too_many(
1643 extra_count in 1usize..=20usize,
1644 ) {
1645 let total = 100_usize.saturating_add(extra_count);
1646 let team_ids: Vec<String> = (0..total).map(|i| format!("team{}", i)).collect();
1647 let result = SandboxApi::validate_team_identifiers(&team_ids);
1648 prop_assert!(result.is_err(), "Should reject {} identifiers", total);
1649 }
1650 }
1651
1652 proptest! {
1654 #![proptest_config(ProptestConfig {
1655 cases: if cfg!(miri) { 5 } else { 1000 },
1656 failure_persistence: None,
1657 .. ProptestConfig::default()
1658 })]
1659
1660 #[test]
1661 fn prop_validate_team_identifiers_rejects_control(
1662 prefix in "[a-zA-Z]{1,10}",
1663 control_char in prop::char::range('\x00', '\x1F'),
1664 suffix in "[a-zA-Z]{1,10}",
1665 ) {
1666 let team_id = format!("{}{}{}", prefix, control_char, suffix);
1667 let result = SandboxApi::validate_team_identifiers(std::slice::from_ref(&team_id));
1668 prop_assert!(result.is_err(), "Should reject identifier with control character: {:?}", control_char);
1669 }
1670 }
1671
1672 proptest! {
1674 #![proptest_config(ProptestConfig {
1675 cases: if cfg!(miri) { 5 } else { 500 },
1676 failure_persistence: None,
1677 .. ProptestConfig::default()
1678 })]
1679
1680 #[test]
1681 fn prop_validate_team_identifiers_rejects_dangerous(
1682 prefix in "[a-zA-Z]{1,10}",
1683 dangerous in prop::sample::select(vec!['<', '>', '"', '&', '\'', '/', '\\']),
1684 suffix in "[a-zA-Z]{1,10}",
1685 ) {
1686 let team_id = format!("{}{}{}", prefix, dangerous, suffix);
1687 let result = SandboxApi::validate_team_identifiers(std::slice::from_ref(&team_id));
1688 prop_assert!(result.is_err(), "Should reject identifier with dangerous character: {}", dangerous);
1689 }
1690 }
1691
1692 proptest! {
1694 #![proptest_config(ProptestConfig {
1695 cases: if cfg!(miri) { 5 } else { 1000 },
1696 failure_persistence: None,
1697 .. ProptestConfig::default()
1698 })]
1699
1700 #[test]
1701 fn prop_sandbox_list_params_caps_page_size(
1702 excessive_size in (MAX_PAGE_SIZE + 1)..=u64::MAX,
1703 ) {
1704 let params = SandboxListParams {
1705 size: Some(excessive_size),
1706 ..Default::default()
1707 };
1708 let query_params: Vec<_> = params.into();
1709
1710 let size_param = query_params.iter().find(|(k, _)| k == "size");
1712 prop_assert!(size_param.is_some(), "Should have size parameter");
1713
1714 let (_, size_value) = size_param.expect("Size parameter should exist");
1715 let parsed_size: u64 = size_value.parse().expect("Size value should be parseable");
1716 prop_assert_eq!(parsed_size, MAX_PAGE_SIZE, "Should cap size to MAX_PAGE_SIZE");
1717 }
1718 }
1719
1720 proptest! {
1722 #![proptest_config(ProptestConfig {
1723 cases: if cfg!(miri) { 5 } else { 1000 },
1724 failure_persistence: None,
1725 .. ProptestConfig::default()
1726 })]
1727
1728 #[test]
1729 fn prop_sandbox_list_params_preserves_reasonable_size(
1730 reasonable_size in 1u64..=MAX_PAGE_SIZE,
1731 ) {
1732 let params = SandboxListParams {
1733 size: Some(reasonable_size),
1734 ..Default::default()
1735 };
1736 let query_params: Vec<_> = params.into();
1737
1738 let size_param = query_params.iter().find(|(k, _)| k == "size");
1739 prop_assert!(size_param.is_some(), "Should have size parameter");
1740
1741 let (_, size_value) = size_param.expect("Size parameter should exist");
1742 let parsed_size: u64 = size_value.parse().expect("Size value should be parseable");
1743 prop_assert_eq!(parsed_size, reasonable_size, "Should preserve reasonable size");
1744 }
1745 }
1746
1747 proptest! {
1749 #![proptest_config(ProptestConfig {
1750 cases: if cfg!(miri) { 5 } else { 500 },
1751 failure_persistence: None,
1752 .. ProptestConfig::default()
1753 })]
1754
1755 #[test]
1756 fn prop_validate_create_request_accepts_valid(
1757 name in "[a-zA-Z0-9_-]{1,256}",
1758 ) {
1759 prop_assume!(!name.contains(".."));
1760 prop_assume!(!name.contains('/'));
1761 prop_assume!(!name.contains('\\'));
1762
1763 let request = CreateSandboxRequest {
1764 name,
1765 description: None,
1766 auto_recreate: None,
1767 custom_fields: None,
1768 team_identifiers: None,
1769 };
1770 let result = SandboxApi::validate_create_request(&request);
1771 prop_assert!(result.is_ok(), "Should accept valid create request");
1772 }
1773 }
1774
1775 proptest! {
1777 #![proptest_config(ProptestConfig {
1778 cases: if cfg!(miri) { 5 } else { 500 },
1779 failure_persistence: None,
1780 .. ProptestConfig::default()
1781 })]
1782
1783 #[test]
1784 fn prop_validate_update_request_accepts_valid(
1785 name in "[a-zA-Z0-9_-]{1,256}",
1786 ) {
1787 prop_assume!(!name.contains(".."));
1788 prop_assume!(!name.contains('/'));
1789 prop_assume!(!name.contains('\\'));
1790
1791 let request = UpdateSandboxRequest {
1792 name: Some(name),
1793 description: None,
1794 auto_recreate: None,
1795 custom_fields: None,
1796 team_identifiers: None,
1797 };
1798 let result = SandboxApi::validate_update_request(&request);
1799 prop_assert!(result.is_ok(), "Should accept valid update request");
1800 }
1801 }
1802 }
1803}