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 serde::{Deserialize, Serialize};
8use chrono::{DateTime, Utc, NaiveDate};
9use std::collections::HashMap;
10use quick_xml::Reader;
11use quick_xml::events::Event;
12
13use crate::{VeracodeClient, VeracodeError};
14
15/// Represents a Veracode build
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Build {
18    /// Build ID
19    pub build_id: String,
20    /// Application ID
21    pub app_id: String,
22    /// Build version
23    pub version: Option<String>,
24    /// Application name
25    pub app_name: Option<String>,
26    /// Sandbox ID (if sandbox build)
27    pub sandbox_id: Option<String>,
28    /// Sandbox name (if sandbox build)
29    pub sandbox_name: Option<String>,
30    /// Lifecycle stage
31    pub lifecycle_stage: Option<String>,
32    /// Launch date
33    pub launch_date: Option<NaiveDate>,
34    /// Submitter
35    pub submitter: Option<String>,
36    /// Platform
37    pub platform: Option<String>,
38    /// Analysis unit
39    pub analysis_unit: Option<String>,
40    /// Policy name
41    pub policy_name: Option<String>,
42    /// Policy version
43    pub policy_version: Option<String>,
44    /// Policy compliance status
45    pub policy_compliance_status: Option<String>,
46    /// Rules status
47    pub rules_status: Option<String>,
48    /// Grace period expired
49    pub grace_period_expired: Option<bool>,
50    /// Scan overdue
51    pub scan_overdue: Option<bool>,
52    /// Policy updated date
53    pub policy_updated_date: Option<DateTime<Utc>>,
54    /// Legacy scan engine
55    pub legacy_scan_engine: Option<bool>,
56    /// Additional attributes
57    pub attributes: HashMap<String, String>,
58}
59
60/// List of builds
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct BuildList {
63    /// Account ID
64    pub account_id: Option<String>,
65    /// Application ID
66    pub app_id: String,
67    /// Application name
68    pub app_name: Option<String>,
69    /// List of builds
70    pub builds: Vec<Build>,
71}
72
73/// Request for creating a build
74#[derive(Debug, Clone)]
75pub struct CreateBuildRequest {
76    /// Application ID
77    pub app_id: String,
78    /// Build version (optional, system will generate if not provided)
79    pub version: Option<String>,
80    /// Lifecycle stage
81    pub lifecycle_stage: Option<String>,
82    /// Launch date in MM/DD/YYYY format
83    pub launch_date: Option<String>,
84    /// Sandbox ID (optional, for sandbox builds)
85    pub sandbox_id: Option<String>,
86}
87
88/// Request for updating a build
89#[derive(Debug, Clone)]
90pub struct UpdateBuildRequest {
91    /// Application ID
92    pub app_id: String,
93    /// Build ID (optional, defaults to most recent)
94    pub build_id: Option<String>,
95    /// New build version
96    pub version: Option<String>,
97    /// New lifecycle stage
98    pub lifecycle_stage: Option<String>,
99    /// New launch date in MM/DD/YYYY format
100    pub launch_date: Option<String>,
101    /// Sandbox ID (optional, for sandbox builds)
102    pub sandbox_id: Option<String>,
103}
104
105/// Request for deleting a build
106#[derive(Debug, Clone)]
107pub struct DeleteBuildRequest {
108    /// Application ID
109    pub app_id: String,
110    /// Sandbox ID (optional, for sandbox builds)
111    pub sandbox_id: Option<String>,
112}
113
114/// Request for getting build information
115#[derive(Debug, Clone)]
116pub struct GetBuildInfoRequest {
117    /// Application ID
118    pub app_id: String,
119    /// Build ID (optional, defaults to most recent)
120    pub build_id: Option<String>,
121    /// Sandbox ID (optional, for sandbox builds)
122    pub sandbox_id: Option<String>,
123}
124
125/// Request for getting build list
126#[derive(Debug, Clone)]
127pub struct GetBuildListRequest {
128    /// Application ID
129    pub app_id: String,
130    /// Sandbox ID (optional, for sandbox builds only)
131    pub sandbox_id: Option<String>,
132}
133
134/// Result of build deletion
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct DeleteBuildResult {
137    /// Result status (typically "success")
138    pub result: String,
139}
140
141/// Build specific error types
142#[derive(Debug)]
143pub enum BuildError {
144    /// Veracode API error
145    Api(VeracodeError),
146    /// Build not found
147    BuildNotFound,
148    /// Application not found
149    ApplicationNotFound,
150    /// Sandbox not found
151    SandboxNotFound,
152    /// Invalid parameter
153    InvalidParameter(String),
154    /// Build creation failed
155    CreationFailed(String),
156    /// Build update failed
157    UpdateFailed(String),
158    /// Build deletion failed
159    DeletionFailed(String),
160    /// XML parsing error
161    XmlParsingError(String),
162    /// Unauthorized access
163    Unauthorized,
164    /// Permission denied
165    PermissionDenied,
166    /// Build in progress (cannot modify)
167    BuildInProgress,
168}
169
170impl std::fmt::Display for BuildError {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        match self {
173            BuildError::Api(err) => write!(f, "API error: {err}"),
174            BuildError::BuildNotFound => write!(f, "Build not found"),
175            BuildError::ApplicationNotFound => write!(f, "Application not found"),
176            BuildError::SandboxNotFound => write!(f, "Sandbox not found"),
177            BuildError::InvalidParameter(msg) => write!(f, "Invalid parameter: {msg}"),
178            BuildError::CreationFailed(msg) => write!(f, "Build creation failed: {msg}"),
179            BuildError::UpdateFailed(msg) => write!(f, "Build update failed: {msg}"),
180            BuildError::DeletionFailed(msg) => write!(f, "Build deletion failed: {msg}"),
181            BuildError::XmlParsingError(msg) => write!(f, "XML parsing error: {msg}"),
182            BuildError::Unauthorized => write!(f, "Unauthorized access"),
183            BuildError::PermissionDenied => write!(f, "Permission denied"),
184            BuildError::BuildInProgress => write!(f, "Build in progress, cannot modify"),
185        }
186    }
187}
188
189impl std::error::Error for BuildError {}
190
191impl From<VeracodeError> for BuildError {
192    fn from(err: VeracodeError) -> Self {
193        BuildError::Api(err)
194    }
195}
196
197impl From<std::io::Error> for BuildError {
198    fn from(err: std::io::Error) -> Self {
199        BuildError::Api(VeracodeError::InvalidResponse(err.to_string()))
200    }
201}
202
203impl From<reqwest::Error> for BuildError {
204    fn from(err: reqwest::Error) -> Self {
205        BuildError::Api(VeracodeError::Http(err))
206    }
207}
208
209/// Build API operations for Veracode platform
210pub struct BuildApi {
211    client: VeracodeClient,
212}
213
214impl BuildApi {
215    /// Create a new BuildApi instance
216    pub fn new(client: VeracodeClient) -> Self {
217        Self { client }
218    }
219
220    /// Create a new build
221    ///
222    /// # Arguments
223    ///
224    /// * `request` - The create build request
225    ///
226    /// # Returns
227    ///
228    /// A `Result` containing the created build information or an error.
229    pub async fn create_build(&self, request: CreateBuildRequest) -> Result<Build, BuildError> {
230        let endpoint = "api/5.0/createbuild.do";
231        
232        // Build query parameters
233        let mut query_params = Vec::new();
234        query_params.push(("app_id", request.app_id.as_str()));
235        
236        if let Some(version) = &request.version {
237            query_params.push(("version", version.as_str()));
238        }
239        
240        if let Some(lifecycle_stage) = &request.lifecycle_stage {
241            query_params.push(("lifecycle_stage", lifecycle_stage.as_str()));
242        }
243        
244        if let Some(launch_date) = &request.launch_date {
245            query_params.push(("launch_date", launch_date.as_str()));
246        }
247        
248        if let Some(sandbox_id) = &request.sandbox_id {
249            query_params.push(("sandbox_id", sandbox_id.as_str()));
250        }
251
252        let response = self.client.post_with_query_params(endpoint, &query_params).await?;
253        
254        let status = response.status().as_u16();
255        match status {
256            200 => {
257                let response_text = response.text().await?;
258                self.parse_build_info(&response_text)
259            }
260            400 => {
261                let error_text = response.text().await.unwrap_or_default();
262                Err(BuildError::InvalidParameter(error_text))
263            }
264            401 => Err(BuildError::Unauthorized),
265            403 => Err(BuildError::PermissionDenied),
266            404 => Err(BuildError::ApplicationNotFound),
267            _ => {
268                let error_text = response.text().await.unwrap_or_default();
269                Err(BuildError::CreationFailed(format!("HTTP {status}: {error_text}")))
270            }
271        }
272    }
273
274    /// Update an existing build
275    ///
276    /// # Arguments
277    ///
278    /// * `request` - The update build request
279    ///
280    /// # Returns
281    ///
282    /// A `Result` containing the updated build information or an error.
283    pub async fn update_build(&self, request: UpdateBuildRequest) -> Result<Build, BuildError> {
284        let endpoint = "api/5.0/updatebuild.do";
285        
286        // Build query parameters
287        let mut query_params = Vec::new();
288        query_params.push(("app_id", request.app_id.as_str()));
289        
290        if let Some(build_id) = &request.build_id {
291            query_params.push(("build_id", build_id.as_str()));
292        }
293        
294        if let Some(version) = &request.version {
295            query_params.push(("version", version.as_str()));
296        }
297        
298        if let Some(lifecycle_stage) = &request.lifecycle_stage {
299            query_params.push(("lifecycle_stage", lifecycle_stage.as_str()));
300        }
301        
302        if let Some(launch_date) = &request.launch_date {
303            query_params.push(("launch_date", launch_date.as_str()));
304        }
305        
306        if let Some(sandbox_id) = &request.sandbox_id {
307            query_params.push(("sandbox_id", sandbox_id.as_str()));
308        }
309
310        let response = self.client.post_with_query_params(endpoint, &query_params).await?;
311        
312        let status = response.status().as_u16();
313        match status {
314            200 => {
315                let response_text = response.text().await?;
316                self.parse_build_info(&response_text)
317            }
318            400 => {
319                let error_text = response.text().await.unwrap_or_default();
320                Err(BuildError::InvalidParameter(error_text))
321            }
322            401 => Err(BuildError::Unauthorized),
323            403 => Err(BuildError::PermissionDenied),
324            404 => {
325                if request.sandbox_id.is_some() {
326                    Err(BuildError::SandboxNotFound)
327                } else {
328                    Err(BuildError::BuildNotFound)
329                }
330            }
331            _ => {
332                let error_text = response.text().await.unwrap_or_default();
333                Err(BuildError::UpdateFailed(format!("HTTP {status}: {error_text}")))
334            }
335        }
336    }
337
338    /// Delete a build
339    ///
340    /// # Arguments
341    ///
342    /// * `request` - The delete build request
343    ///
344    /// # Returns
345    ///
346    /// A `Result` containing the deletion result or an error.
347    pub async fn delete_build(&self, request: DeleteBuildRequest) -> Result<DeleteBuildResult, BuildError> {
348        let endpoint = "api/5.0/deletebuild.do";
349        
350        // Build query parameters
351        let mut query_params = Vec::new();
352        query_params.push(("app_id", request.app_id.as_str()));
353        
354        if let Some(sandbox_id) = &request.sandbox_id {
355            query_params.push(("sandbox_id", sandbox_id.as_str()));
356        }
357
358        let response = self.client.post_with_query_params(endpoint, &query_params).await?;
359        
360        let status = response.status().as_u16();
361        match status {
362            200 => {
363                let response_text = response.text().await?;
364                self.parse_delete_result(&response_text)
365            }
366            400 => {
367                let error_text = response.text().await.unwrap_or_default();
368                Err(BuildError::InvalidParameter(error_text))
369            }
370            401 => Err(BuildError::Unauthorized),
371            403 => Err(BuildError::PermissionDenied),
372            404 => {
373                if request.sandbox_id.is_some() {
374                    Err(BuildError::SandboxNotFound)
375                } else {
376                    Err(BuildError::BuildNotFound)
377                }
378            }
379            _ => {
380                let error_text = response.text().await.unwrap_or_default();
381                Err(BuildError::DeletionFailed(format!("HTTP {status}: {error_text}")))
382            }
383        }
384    }
385
386    /// Get build information
387    ///
388    /// # Arguments
389    ///
390    /// * `request` - The get build info request
391    ///
392    /// # Returns
393    ///
394    /// A `Result` containing the build information or an error.
395    pub async fn get_build_info(&self, request: GetBuildInfoRequest) -> Result<Build, BuildError> {
396        let endpoint = "api/5.0/getbuildinfo.do";
397        
398        // Build query parameters
399        let mut query_params = Vec::new();
400        query_params.push(("app_id", request.app_id.as_str()));
401        
402        if let Some(build_id) = &request.build_id {
403            query_params.push(("build_id", build_id.as_str()));
404        }
405        
406        if let Some(sandbox_id) = &request.sandbox_id {
407            query_params.push(("sandbox_id", sandbox_id.as_str()));
408        }
409
410        let response = self.client.get_with_query_params(endpoint, &query_params).await?;
411        
412        let status = response.status().as_u16();
413        match status {
414            200 => {
415                let response_text = response.text().await?;
416                self.parse_build_info(&response_text)
417            }
418            400 => {
419                let error_text = response.text().await.unwrap_or_default();
420                Err(BuildError::InvalidParameter(error_text))
421            }
422            401 => Err(BuildError::Unauthorized),
423            403 => Err(BuildError::PermissionDenied),
424            404 => {
425                if request.sandbox_id.is_some() {
426                    Err(BuildError::SandboxNotFound)
427                } else {
428                    Err(BuildError::BuildNotFound)
429                }
430            }
431            _ => {
432                let error_text = response.text().await.unwrap_or_default();
433                Err(BuildError::Api(VeracodeError::InvalidResponse(format!("HTTP {status}: {error_text}"))))
434            }
435        }
436    }
437
438    /// Get list of builds
439    ///
440    /// # Arguments
441    ///
442    /// * `request` - The get build list request
443    ///
444    /// # Returns
445    ///
446    /// A `Result` containing the build list or an error.
447    pub async fn get_build_list(&self, request: GetBuildListRequest) -> Result<BuildList, BuildError> {
448        let endpoint = "api/5.0/getbuildlist.do";
449        
450        // Build query parameters
451        let mut query_params = Vec::new();
452        query_params.push(("app_id", request.app_id.as_str()));
453        
454        if let Some(sandbox_id) = &request.sandbox_id {
455            query_params.push(("sandbox_id", sandbox_id.as_str()));
456        }
457
458        let response = self.client.get_with_query_params(endpoint, &query_params).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_list(&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::ApplicationNotFound)
477                }
478            }
479            _ => {
480                let error_text = response.text().await.unwrap_or_default();
481                Err(BuildError::Api(VeracodeError::InvalidResponse(format!("HTTP {status}: {error_text}"))))
482            }
483        }
484    }
485
486    /// Parse build info XML response
487    fn parse_build_info(&self, xml: &str) -> Result<Build, BuildError> {
488        let mut reader = Reader::from_str(xml);
489        reader.config_mut().trim_text(true);
490        
491        let mut buf = Vec::new();
492        let mut build = Build {
493            build_id: String::new(),
494            app_id: String::new(),
495            version: None,
496            app_name: None,
497            sandbox_id: None,
498            sandbox_name: None,
499            lifecycle_stage: None,
500            launch_date: None,
501            submitter: None,
502            platform: None,
503            analysis_unit: None,
504            policy_name: None,
505            policy_version: None,
506            policy_compliance_status: None,
507            rules_status: None,
508            grace_period_expired: None,
509            scan_overdue: None,
510            policy_updated_date: None,
511            legacy_scan_engine: None,
512            attributes: HashMap::new(),
513        };
514
515        loop {
516            match reader.read_event_into(&mut buf) {
517                Ok(Event::Start(ref e)) => {
518                    if e.name().as_ref() == b"build" {
519                        for attr in e.attributes() {
520                            if let Ok(attr) = attr {
521                                let key = String::from_utf8_lossy(attr.key.as_ref());
522                                let value = String::from_utf8_lossy(&attr.value);
523                                
524                                match key.as_ref() {
525                                    "build_id" => build.build_id = value.to_string(),
526                                    "app_id" => build.app_id = value.to_string(),
527                                    "version" => build.version = Some(value.to_string()),
528                                    "app_name" => build.app_name = Some(value.to_string()),
529                                    "sandbox_id" => build.sandbox_id = Some(value.to_string()),
530                                    "sandbox_name" => build.sandbox_name = Some(value.to_string()),
531                                    "lifecycle_stage" => build.lifecycle_stage = Some(value.to_string()),
532                                    "submitter" => build.submitter = Some(value.to_string()),
533                                    "platform" => build.platform = Some(value.to_string()),
534                                    "analysis_unit" => build.analysis_unit = Some(value.to_string()),
535                                    "policy_name" => build.policy_name = Some(value.to_string()),
536                                    "policy_version" => build.policy_version = Some(value.to_string()),
537                                    "policy_compliance_status" => build.policy_compliance_status = Some(value.to_string()),
538                                    "rules_status" => build.rules_status = Some(value.to_string()),
539                                    "grace_period_expired" => {
540                                        build.grace_period_expired = value.parse::<bool>().ok();
541                                    }
542                                    "scan_overdue" => {
543                                        build.scan_overdue = value.parse::<bool>().ok();
544                                    }
545                                    "legacy_scan_engine" => {
546                                        build.legacy_scan_engine = value.parse::<bool>().ok();
547                                    }
548                                    "launch_date" => {
549                                        if let Ok(date) = NaiveDate::parse_from_str(&value, "%m/%d/%Y") {
550                                            build.launch_date = Some(date);
551                                        }
552                                    }
553                                    "policy_updated_date" => {
554                                        if let Ok(datetime) = chrono::DateTime::parse_from_rfc3339(&value) {
555                                            build.policy_updated_date = Some(datetime.with_timezone(&Utc));
556                                        }
557                                    }
558                                    _ => {
559                                        build.attributes.insert(key.to_string(), value.to_string());
560                                    }
561                                }
562                            }
563                        }
564                    }
565                }
566                Ok(Event::Eof) => break,
567                Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
568                _ => {}
569            }
570            buf.clear();
571        }
572
573        if build.build_id.is_empty() {
574            return Err(BuildError::XmlParsingError("No build information found in response".to_string()));
575        }
576
577        Ok(build)
578    }
579
580    /// Parse build list XML response
581    fn parse_build_list(&self, xml: &str) -> Result<BuildList, BuildError> {
582        let mut reader = Reader::from_str(xml);
583        reader.config_mut().trim_text(true);
584        
585        let mut buf = Vec::new();
586        let mut build_list = BuildList {
587            account_id: None,
588            app_id: String::new(),
589            app_name: None,
590            builds: Vec::new(),
591        };
592
593        loop {
594            match reader.read_event_into(&mut buf) {
595                Ok(Event::Start(ref e)) => {
596                    match e.name().as_ref() {
597                        b"buildlist" => {
598                            for attr in e.attributes() {
599                                if let Ok(attr) = attr {
600                                    let key = String::from_utf8_lossy(attr.key.as_ref());
601                                    let value = String::from_utf8_lossy(&attr.value);
602                                    
603                                    match key.as_ref() {
604                                        "account_id" => build_list.account_id = Some(value.to_string()),
605                                        "app_id" => build_list.app_id = value.to_string(),
606                                        "app_name" => build_list.app_name = Some(value.to_string()),
607                                        _ => {}
608                                    }
609                                }
610                            }
611                        }
612                        b"build" => {
613                            let mut build = Build {
614                                build_id: String::new(),
615                                app_id: build_list.app_id.clone(),
616                                version: None,
617                                app_name: build_list.app_name.clone(),
618                                sandbox_id: None,
619                                sandbox_name: None,
620                                lifecycle_stage: None,
621                                launch_date: None,
622                                submitter: None,
623                                platform: None,
624                                analysis_unit: None,
625                                policy_name: None,
626                                policy_version: None,
627                                policy_compliance_status: None,
628                                rules_status: None,
629                                grace_period_expired: None,
630                                scan_overdue: None,
631                                policy_updated_date: None,
632                                legacy_scan_engine: None,
633                                attributes: HashMap::new(),
634                            };
635
636                            for attr in e.attributes() {
637                                if let Ok(attr) = attr {
638                                    let key = String::from_utf8_lossy(attr.key.as_ref());
639                                    let value = String::from_utf8_lossy(&attr.value);
640                                    
641                                    match key.as_ref() {
642                                        "build_id" => build.build_id = value.to_string(),
643                                        "version" => build.version = Some(value.to_string()),
644                                        "sandbox_id" => build.sandbox_id = Some(value.to_string()),
645                                        "sandbox_name" => build.sandbox_name = Some(value.to_string()),
646                                        "lifecycle_stage" => build.lifecycle_stage = Some(value.to_string()),
647                                        "submitter" => build.submitter = Some(value.to_string()),
648                                        "platform" => build.platform = Some(value.to_string()),
649                                        "analysis_unit" => build.analysis_unit = Some(value.to_string()),
650                                        "policy_name" => build.policy_name = Some(value.to_string()),
651                                        "policy_version" => build.policy_version = Some(value.to_string()),
652                                        "policy_compliance_status" => build.policy_compliance_status = Some(value.to_string()),
653                                        "rules_status" => build.rules_status = Some(value.to_string()),
654                                        "grace_period_expired" => {
655                                            build.grace_period_expired = value.parse::<bool>().ok();
656                                        }
657                                        "scan_overdue" => {
658                                            build.scan_overdue = value.parse::<bool>().ok();
659                                        }
660                                        "legacy_scan_engine" => {
661                                            build.legacy_scan_engine = value.parse::<bool>().ok();
662                                        }
663                                        "launch_date" => {
664                                            if let Ok(date) = NaiveDate::parse_from_str(&value, "%m/%d/%Y") {
665                                                build.launch_date = Some(date);
666                                            }
667                                        }
668                                        "policy_updated_date" => {
669                                            if let Ok(datetime) = chrono::DateTime::parse_from_rfc3339(&value) {
670                                                build.policy_updated_date = Some(datetime.with_timezone(&Utc));
671                                            }
672                                        }
673                                        _ => {
674                                            build.attributes.insert(key.to_string(), value.to_string());
675                                        }
676                                    }
677                                }
678                            }
679                            
680                            if !build.build_id.is_empty() {
681                                build_list.builds.push(build);
682                            }
683                        }
684                        _ => {}
685                    }
686                }
687                Ok(Event::Eof) => break,
688                Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
689                _ => {}
690            }
691            buf.clear();
692        }
693
694        Ok(build_list)
695    }
696
697    /// Parse delete build result XML response
698    fn parse_delete_result(&self, xml: &str) -> Result<DeleteBuildResult, BuildError> {
699        let mut reader = Reader::from_str(xml);
700        reader.config_mut().trim_text(true);
701        
702        let mut buf = Vec::new();
703        let mut result = String::new();
704
705        loop {
706            match reader.read_event_into(&mut buf) {
707                Ok(Event::Start(ref e)) => {
708                    if e.name().as_ref() == b"result" {
709                        // Read the text content of the result element
710                        match reader.read_event_into(&mut buf) {
711                            Ok(Event::Text(e)) => {
712                                result = String::from_utf8_lossy(&e).to_string();
713                            }
714                            _ => {}
715                        }
716                    }
717                }
718                Ok(Event::Eof) => break,
719                Err(e) => return Err(BuildError::XmlParsingError(e.to_string())),
720                _ => {}
721            }
722            buf.clear();
723        }
724
725        if result.is_empty() {
726            return Err(BuildError::XmlParsingError("No result found in delete response".to_string()));
727        }
728
729        Ok(DeleteBuildResult { result })
730    }
731}
732
733// Convenience methods implementation
734impl BuildApi {
735    /// Create a build with minimal parameters
736    ///
737    /// # Arguments
738    ///
739    /// * `app_id` - Application ID
740    /// * `version` - Optional build version
741    ///
742    /// # Returns
743    ///
744    /// A `Result` containing the created build information or an error.
745    pub async fn create_simple_build(&self, app_id: &str, version: Option<&str>) -> Result<Build, BuildError> {
746        let request = CreateBuildRequest {
747            app_id: app_id.to_string(),
748            version: version.map(|s| s.to_string()),
749            lifecycle_stage: None,
750            launch_date: None,
751            sandbox_id: None,
752        };
753        
754        self.create_build(request).await
755    }
756
757    /// Create a sandbox build
758    ///
759    /// # Arguments
760    ///
761    /// * `app_id` - Application ID
762    /// * `sandbox_id` - Sandbox ID
763    /// * `version` - Optional build version
764    ///
765    /// # Returns
766    ///
767    /// A `Result` containing the created build information or an error.
768    pub async fn create_sandbox_build(&self, app_id: &str, sandbox_id: &str, version: Option<&str>) -> Result<Build, BuildError> {
769        let request = CreateBuildRequest {
770            app_id: app_id.to_string(),
771            version: version.map(|s| s.to_string()),
772            lifecycle_stage: None,
773            launch_date: None,
774            sandbox_id: Some(sandbox_id.to_string()),
775        };
776        
777        self.create_build(request).await
778    }
779
780    /// Delete the most recent application build
781    ///
782    /// # Arguments
783    ///
784    /// * `app_id` - Application ID
785    ///
786    /// # Returns
787    ///
788    /// A `Result` containing the deletion result or an error.
789    pub async fn delete_app_build(&self, app_id: &str) -> Result<DeleteBuildResult, BuildError> {
790        let request = DeleteBuildRequest {
791            app_id: app_id.to_string(),
792            sandbox_id: None,
793        };
794        
795        self.delete_build(request).await
796    }
797
798    /// Delete the most recent sandbox build
799    ///
800    /// # Arguments
801    ///
802    /// * `app_id` - Application ID
803    /// * `sandbox_id` - Sandbox ID
804    ///
805    /// # Returns
806    ///
807    /// A `Result` containing the deletion result or an error.
808    pub async fn delete_sandbox_build(&self, app_id: &str, sandbox_id: &str) -> Result<DeleteBuildResult, BuildError> {
809        let request = DeleteBuildRequest {
810            app_id: app_id.to_string(),
811            sandbox_id: Some(sandbox_id.to_string()),
812        };
813        
814        self.delete_build(request).await
815    }
816
817    /// Get the most recent build info for an application
818    ///
819    /// # Arguments
820    ///
821    /// * `app_id` - Application ID
822    ///
823    /// # Returns
824    ///
825    /// A `Result` containing the build information or an error.
826    pub async fn get_app_build_info(&self, app_id: &str) -> Result<Build, BuildError> {
827        let request = GetBuildInfoRequest {
828            app_id: app_id.to_string(),
829            build_id: None,
830            sandbox_id: None,
831        };
832        
833        self.get_build_info(request).await
834    }
835
836    /// Get build info for a specific sandbox
837    ///
838    /// # Arguments
839    ///
840    /// * `app_id` - Application ID
841    /// * `sandbox_id` - Sandbox ID
842    ///
843    /// # Returns
844    ///
845    /// A `Result` containing the build information or an error.
846    pub async fn get_sandbox_build_info(&self, app_id: &str, sandbox_id: &str) -> Result<Build, BuildError> {
847        let request = GetBuildInfoRequest {
848            app_id: app_id.to_string(),
849            build_id: None,
850            sandbox_id: Some(sandbox_id.to_string()),
851        };
852        
853        self.get_build_info(request).await
854    }
855
856    /// Get list of all builds for an application
857    ///
858    /// # Arguments
859    ///
860    /// * `app_id` - Application ID
861    ///
862    /// # Returns
863    ///
864    /// A `Result` containing the build list or an error.
865    pub async fn get_app_builds(&self, app_id: &str) -> Result<BuildList, BuildError> {
866        let request = GetBuildListRequest {
867            app_id: app_id.to_string(),
868            sandbox_id: None,
869        };
870        
871        self.get_build_list(request).await
872    }
873
874    /// Get list of builds for a sandbox
875    ///
876    /// # Arguments
877    ///
878    /// * `app_id` - Application ID
879    /// * `sandbox_id` - Sandbox ID
880    ///
881    /// # Returns
882    ///
883    /// A `Result` containing the build list or an error.
884    pub async fn get_sandbox_builds(&self, app_id: &str, sandbox_id: &str) -> Result<BuildList, BuildError> {
885        let request = GetBuildListRequest {
886            app_id: app_id.to_string(),
887            sandbox_id: Some(sandbox_id.to_string()),
888        };
889        
890        self.get_build_list(request).await
891    }
892}
893
894#[cfg(test)]
895mod tests {
896    use super::*;
897    use crate::VeracodeConfig;
898
899    #[test]
900    fn test_create_build_request() {
901        let request = CreateBuildRequest {
902            app_id: "123".to_string(),
903            version: Some("1.0.0".to_string()),
904            lifecycle_stage: Some("Development".to_string()),
905            launch_date: Some("12/31/2024".to_string()),
906            sandbox_id: None,
907        };
908        
909        assert_eq!(request.app_id, "123");
910        assert_eq!(request.version, Some("1.0.0".to_string()));
911        assert_eq!(request.lifecycle_stage, Some("Development".to_string()));
912    }
913
914    #[test]
915    fn test_update_build_request() {
916        let request = UpdateBuildRequest {
917            app_id: "123".to_string(),
918            build_id: Some("456".to_string()),
919            version: Some("1.1.0".to_string()),
920            lifecycle_stage: Some("QA".to_string()),
921            launch_date: None,
922            sandbox_id: Some("789".to_string()),
923        };
924        
925        assert_eq!(request.app_id, "123");
926        assert_eq!(request.build_id, Some("456".to_string()));
927        assert_eq!(request.sandbox_id, Some("789".to_string()));
928    }
929
930    #[test]
931    fn test_build_error_display() {
932        let error = BuildError::BuildNotFound;
933        assert_eq!(error.to_string(), "Build not found");
934
935        let error = BuildError::InvalidParameter("Invalid app_id".to_string());
936        assert_eq!(error.to_string(), "Invalid parameter: Invalid app_id");
937
938        let error = BuildError::CreationFailed("Build creation failed".to_string());
939        assert_eq!(error.to_string(), "Build creation failed: Build creation failed");
940    }
941
942    #[tokio::test]
943    async fn test_build_api_method_signatures() {
944        async fn _test_build_methods() -> Result<(), Box<dyn std::error::Error>> {
945            let config = VeracodeConfig::new("test".to_string(), "test".to_string());
946            let client = VeracodeClient::new(config)?;
947            let api = client.build_api();
948            
949            // Test that the method signatures exist and compile
950            let create_request = CreateBuildRequest {
951                app_id: "123".to_string(),
952                version: None,
953                lifecycle_stage: None,
954                launch_date: None,
955                sandbox_id: None,
956            };
957            
958            // These calls won't actually execute due to test environment,
959            // but they validate the method signatures exist
960            let _: Result<Build, _> = api.create_build(create_request).await;
961            let _: Result<Build, _> = api.create_simple_build("123", None).await;
962            let _: Result<Build, _> = api.create_sandbox_build("123", "456", None).await;
963            let _: Result<DeleteBuildResult, _> = api.delete_app_build("123").await;
964            let _: Result<Build, _> = api.get_app_build_info("123").await;
965            let _: Result<BuildList, _> = api.get_app_builds("123").await;
966            
967            Ok(())
968        }
969        
970        // If this compiles, the methods have correct signatures
971        assert!(true);
972    }
973}