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}