veracode_platform/
app.rs

1//! Application-specific functionality built on top of the core client.
2//!
3//! This module contains application-specific methods and convenience functions
4//! that use the core VeracodeClient to perform application-related operations.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use crate::VeracodeError;
10use crate::client::VeracodeClient;
11
12/// Represents a Veracode application.
13///
14/// This struct contains all the information about a Veracode application,
15/// including its profile, scans, and metadata.
16#[derive(Debug, Serialize, Deserialize, Clone)]
17pub struct Application {
18    /// Globally unique identifier (GUID) for the application
19    pub guid: String,
20    /// Unique numeric identifier for id the application
21    pub id: u64,
22    /// Organization ID
23    pub oid: Option<u64>,
24    /// Organization ID
25    pub alt_org_id: Option<u64>,
26    /// Unique numeric identifier for organization_id the application
27    pub organization_id: Option<u64>,
28    /// ISO 8601 timestamp of the last completed scan
29    pub created: String,
30    /// ISO 8601 timestamp when the application was last modified
31    pub modified: Option<String>,
32    /// ISO 8601 timestamp of the last completed scan
33    pub last_completed_scan_date: Option<String>,
34    /// ISO 8601 timestamp of the last policy compliance check
35    pub last_policy_compliance_check_date: Option<String>,
36    /// URL to the application profile in the Veracode platform
37    pub app_profile_url: Option<String>,
38    /// Detailed application profile information
39    pub profile: Option<Profile>,
40    /// List of scans associated with this application
41    pub scans: Option<Vec<Scan>>,
42    /// URL to the application profile in the Veracode platform
43    pub results_url: Option<String>,
44}
45
46/// Application profile information.
47#[derive(Debug, Serialize, Deserialize, Clone)]
48pub struct Profile {
49    /// Profile name
50    pub name: String,
51    /// Profile description
52    pub description: Option<String>,
53    /// Profile tags
54    pub tags: Option<String>,
55    /// Business unit associated with the application
56    pub business_unit: Option<BusinessUnit>,
57    /// List of business owners
58    pub business_owners: Option<Vec<BusinessOwner>>,
59    /// List of policies applied to the application
60    pub policies: Option<Vec<Policy>>,
61    /// List of teams associated with the application
62    pub teams: Option<Vec<Team>>,
63    /// Archer application name
64    pub archer_app_name: Option<String>,
65    /// Custom fields
66    pub custom_fields: Option<Vec<CustomField>>,
67    /// Business criticality level (required)
68    #[serde(serialize_with = "serialize_business_criticality")]
69    pub business_criticality: BusinessCriticality,
70    /// Application Profile Settings
71    pub settings: Option<Settings>,
72}
73
74#[derive(Debug, Serialize, Deserialize, Clone)]
75pub struct Settings {
76    /// Profile name
77    pub nextday_consultation_allowed: bool,
78    /// Profile description
79    pub static_scan_xpa_or_dpa: bool,
80    /// Profile tags
81    pub dynamic_scan_approval_not_required: bool,
82    /// Business unit associated with the application
83    pub sca_enabled: bool,
84    /// List of business owners
85    pub static_scan_xpp_enabled: bool,
86}
87
88/// Business unit information.
89#[derive(Debug, Serialize, Deserialize, Clone)]
90pub struct BusinessUnit {
91    /// Business unit ID
92    pub id: Option<u64>,
93    /// Business unit name
94    pub name: Option<String>,
95    /// Business unit GUID
96    pub guid: Option<String>,
97}
98
99/// Business owner information.
100#[derive(Debug, Serialize, Deserialize, Clone)]
101pub struct BusinessOwner {
102    /// Owner's email address
103    pub email: Option<String>,
104    /// Owner's name
105    pub name: Option<String>,
106}
107
108/// Policy information.
109#[derive(Debug, Serialize, Deserialize, Clone)]
110pub struct Policy {
111    /// Policy GUID
112    pub guid: String,
113    /// Policy name
114    pub name: String,
115    /// Whether this is the default policy
116    pub is_default: bool,
117    /// Policy compliance status
118    pub policy_compliance_status: Option<String>,
119}
120
121/// Team information.
122#[derive(Debug, Serialize, Deserialize, Clone)]
123pub struct Team {
124    /// Team ID
125    pub team_id: Option<u64>,
126    /// Team name
127    pub team_name: Option<String>,
128    /// Legacy team ID
129    pub team_legacy_id: Option<u64>,
130}
131
132/// Custom field information.
133#[derive(Debug, Serialize, Deserialize, Clone)]
134pub struct CustomField {
135    /// Field name
136    pub name: Option<String>,
137    /// Field value
138    pub value: Option<String>,
139}
140
141/// Scan information.
142#[derive(Debug, Serialize, Deserialize, Clone)]
143pub struct Scan {
144    /// Scan ID
145    pub scan_id: Option<u64>,
146    /// Type of scan (STATIC, DYNAMIC, etc.)
147    pub scan_type: Option<String>,
148    /// Scan status
149    pub status: Option<String>,
150    /// URL to the scan results
151    pub scan_url: Option<String>,
152    /// When the scan was last modified
153    pub modified_date: Option<String>,
154    /// Internal scan status
155    pub internal_status: Option<String>,
156    /// Related links
157    pub links: Option<Vec<Link>>,
158    /// Fallback scan type
159    pub fallback_type: Option<String>,
160    /// Full scan type
161    pub full_type: Option<String>,
162}
163
164/// Link information.
165#[derive(Debug, Serialize, Deserialize, Clone)]
166pub struct Link {
167    /// Link relationship
168    pub rel: Option<String>,
169    /// Link URL
170    pub href: Option<String>,
171}
172
173/// Response from the Applications API.
174#[derive(Debug, Serialize, Deserialize, Clone)]
175pub struct ApplicationsResponse {
176    /// Embedded applications data
177    #[serde(rename = "_embedded")]
178    pub embedded: Option<EmbeddedApplications>,
179    /// Pagination information
180    pub page: Option<PageInfo>,
181    /// Response links
182    #[serde(rename = "_links")]
183    pub links: Option<HashMap<String, Link>>,
184}
185
186/// Embedded applications in the response.
187#[derive(Debug, Serialize, Deserialize, Clone)]
188pub struct EmbeddedApplications {
189    /// List of applications
190    pub applications: Vec<Application>,
191}
192
193/// Pagination information.
194#[derive(Debug, Serialize, Deserialize, Clone)]
195pub struct PageInfo {
196    /// Number of items per page
197    pub size: Option<u32>,
198    /// Current page number
199    pub number: Option<u32>,
200    /// Total number of elements
201    pub total_elements: Option<u64>,
202    /// Total number of pages
203    pub total_pages: Option<u32>,
204}
205
206/// Request for creating a new application.
207#[derive(Debug, Serialize, Deserialize, Clone)]
208pub struct CreateApplicationRequest {
209    /// Application profile information
210    pub profile: CreateApplicationProfile,
211}
212
213/// Profile information for creating an application.
214#[derive(Debug, Serialize, Deserialize, Clone)]
215pub struct CreateApplicationProfile {
216    /// Application name
217    pub name: String,
218    /// Business criticality level (required)
219    #[serde(serialize_with = "serialize_business_criticality")]
220    pub business_criticality: BusinessCriticality,
221    /// Application description
222    pub description: Option<String>,
223    /// Business unit
224    pub business_unit: Option<BusinessUnit>,
225    /// Business owners
226    pub business_owners: Option<Vec<BusinessOwner>>,
227    /// Policies
228    pub policies: Option<Vec<Policy>>,
229    /// Teams
230    pub teams: Option<Vec<Team>>,
231    /// Tags
232    pub tags: Option<String>,
233    /// Custom fields
234    pub custom_fields: Option<Vec<CustomField>>,
235}
236
237/// Business criticality levels for applications
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub enum BusinessCriticality {
240    VeryHigh,
241    High,
242    Medium,
243    Low,
244    VeryLow,
245}
246
247impl BusinessCriticality {
248    /// Convert to the string value expected by the API
249    pub fn as_str(&self) -> &'static str {
250        match self {
251            BusinessCriticality::VeryHigh => "VERY_HIGH",
252            BusinessCriticality::High => "HIGH",
253            BusinessCriticality::Medium => "MEDIUM",
254            BusinessCriticality::Low => "LOW",
255            BusinessCriticality::VeryLow => "VERY_LOW",
256        }
257    }
258}
259
260impl From<BusinessCriticality> for String {
261    fn from(criticality: BusinessCriticality) -> Self {
262        criticality.as_str().to_string()
263    }
264}
265
266impl std::fmt::Display for BusinessCriticality {
267    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268        write!(f, "{}", self.as_str())
269    }
270}
271
272/// Custom serializer for BusinessCriticality
273fn serialize_business_criticality<S>(
274    criticality: &BusinessCriticality,
275    serializer: S,
276) -> Result<S::Ok, S::Error>
277where
278    S: serde::Serializer,
279{
280    serializer.serialize_str(criticality.as_str())
281}
282
283/// Parse BusinessCriticality from string
284impl std::str::FromStr for BusinessCriticality {
285    type Err = String;
286
287    fn from_str(s: &str) -> Result<Self, Self::Err> {
288        match s {
289            "VERY_HIGH" => Ok(BusinessCriticality::VeryHigh),
290            "HIGH" => Ok(BusinessCriticality::High),
291            "MEDIUM" => Ok(BusinessCriticality::Medium),
292            "LOW" => Ok(BusinessCriticality::Low),
293            "VERY_LOW" => Ok(BusinessCriticality::VeryLow),
294            _ => Err(format!(
295                "Invalid business criticality: '{s}'. Must be one of: VERY_HIGH, HIGH, MEDIUM, LOW, VERY_LOW"
296            )),
297        }
298    }
299}
300
301/// Deserialize BusinessCriticality from string
302impl<'de> serde::Deserialize<'de> for BusinessCriticality {
303    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
304    where
305        D: serde::Deserializer<'de>,
306    {
307        let s = String::deserialize(deserializer)?;
308        s.parse().map_err(serde::de::Error::custom)
309    }
310}
311
312/// Request for updating an application.
313#[derive(Debug, Serialize, Deserialize, Clone)]
314pub struct UpdateApplicationRequest {
315    /// Application profile information
316    pub profile: UpdateApplicationProfile,
317}
318
319/// Profile information for updating an application.
320#[derive(Debug, Serialize, Deserialize, Clone)]
321pub struct UpdateApplicationProfile {
322    /// Application name
323    pub name: Option<String>,
324    /// Application description
325    pub description: Option<String>,
326    /// Business unit
327    pub business_unit: Option<BusinessUnit>,
328    /// Business owners
329    pub business_owners: Option<Vec<BusinessOwner>>,
330    /// Business criticality level (required)
331    #[serde(serialize_with = "serialize_business_criticality")]
332    pub business_criticality: BusinessCriticality,
333    /// Policies
334    pub policies: Option<Vec<Policy>>,
335    /// Teams
336    pub teams: Option<Vec<Team>>,
337    /// Tags
338    pub tags: Option<String>,
339    /// Custom fields
340    pub custom_fields: Option<Vec<CustomField>>,
341}
342
343/// Query parameters for filtering applications.
344#[derive(Debug, Clone, Default)]
345pub struct ApplicationQuery {
346    /// Filter by application name (partial match)
347    pub name: Option<String>,
348    /// Filter by policy compliance status (PASSED, DID_NOT_PASS, etc.)
349    pub policy_compliance: Option<String>,
350    /// Filter applications modified after this date (ISO 8601 format)
351    pub modified_after: Option<String>,
352    /// Filter applications modified before this date (ISO 8601 format)
353    pub modified_before: Option<String>,
354    /// Filter applications created after this date (ISO 8601 format)
355    pub created_after: Option<String>,
356    /// Filter applications created before this date (ISO 8601 format)
357    pub created_before: Option<String>,
358    /// Filter by scan type (STATIC, DYNAMIC, MANUAL, SCA)
359    pub scan_type: Option<String>,
360    /// Filter by tags (comma-separated)
361    pub tags: Option<String>,
362    /// Filter by business unit name
363    pub business_unit: Option<String>,
364    /// Page number for pagination (0-based)
365    pub page: Option<u32>,
366    /// Number of items per page (default: 20, max: 500)
367    pub size: Option<u32>,
368}
369
370impl ApplicationQuery {
371    /// Create a new empty query.
372    pub fn new() -> Self {
373        Default::default()
374    }
375
376    /// Filter applications by name (partial match).
377    pub fn with_name(mut self, name: String) -> Self {
378        self.name = Some(name);
379        self
380    }
381
382    /// Filter applications by policy compliance status.
383    pub fn with_policy_compliance(mut self, compliance: String) -> Self {
384        self.policy_compliance = Some(compliance);
385        self
386    }
387
388    /// Filter applications modified after the specified date.
389    pub fn with_modified_after(mut self, date: String) -> Self {
390        self.modified_after = Some(date);
391        self
392    }
393
394    /// Filter applications modified before the specified date.
395    pub fn with_modified_before(mut self, date: String) -> Self {
396        self.modified_before = Some(date);
397        self
398    }
399
400    /// Set the page number for pagination.
401    pub fn with_page(mut self, page: u32) -> Self {
402        self.page = Some(page);
403        self
404    }
405
406    /// Set the number of items per page.
407    pub fn with_size(mut self, size: u32) -> Self {
408        self.size = Some(size);
409        self
410    }
411
412    /// Convert the query to URL query parameters.
413    pub fn to_query_params(&self) -> Vec<(String, String)> {
414        let mut params = Vec::new();
415
416        if let Some(ref name) = self.name {
417            params.push(("name".to_string(), name.clone()));
418        }
419        if let Some(ref compliance) = self.policy_compliance {
420            params.push(("policy_compliance".to_string(), compliance.clone()));
421        }
422        if let Some(ref date) = self.modified_after {
423            params.push(("modified_after".to_string(), date.clone()));
424        }
425        if let Some(ref date) = self.modified_before {
426            params.push(("modified_before".to_string(), date.clone()));
427        }
428        if let Some(ref date) = self.created_after {
429            params.push(("created_after".to_string(), date.clone()));
430        }
431        if let Some(ref date) = self.created_before {
432            params.push(("created_before".to_string(), date.clone()));
433        }
434        if let Some(ref scan_type) = self.scan_type {
435            params.push(("scan_type".to_string(), scan_type.clone()));
436        }
437        if let Some(ref tags) = self.tags {
438            params.push(("tags".to_string(), tags.clone()));
439        }
440        if let Some(ref business_unit) = self.business_unit {
441            params.push(("business_unit".to_string(), business_unit.clone()));
442        }
443        if let Some(page) = self.page {
444            params.push(("page".to_string(), page.to_string()));
445        }
446        if let Some(size) = self.size {
447            params.push(("size".to_string(), size.to_string()));
448        }
449
450        params
451    }
452}
453
454/// Application-specific methods that build on the core client.
455impl VeracodeClient {
456    /// Get all applications with optional filtering.
457    ///
458    /// # Arguments
459    ///
460    /// * `query` - Optional query parameters to filter the results
461    ///
462    /// # Returns
463    ///
464    /// A `Result` containing an `ApplicationsResponse` with the list of applications.
465    pub async fn get_applications(
466        &self,
467        query: Option<ApplicationQuery>,
468    ) -> Result<ApplicationsResponse, VeracodeError> {
469        let endpoint = "/appsec/v1/applications";
470        let query_params = query.as_ref().map(|q| q.to_query_params());
471
472        let response = self.get(endpoint, query_params.as_deref()).await?;
473        let response = Self::handle_response(response).await?;
474
475        let apps_response: ApplicationsResponse = response.json().await?;
476        Ok(apps_response)
477    }
478
479    /// Get a specific application by its GUID.
480    ///
481    /// # Arguments
482    ///
483    /// * `guid` - The GUID of the application to retrieve
484    ///
485    /// # Returns
486    ///
487    /// A `Result` containing the `Application` details.
488    pub async fn get_application(&self, guid: &str) -> Result<Application, VeracodeError> {
489        let endpoint = format!("/appsec/v1/applications/{guid}");
490
491        let response = self.get(&endpoint, None).await?;
492        let response = Self::handle_response(response).await?;
493
494        let app: Application = response.json().await?;
495        Ok(app)
496    }
497
498    /// Create a new application.
499    ///
500    /// # Arguments
501    ///
502    /// * `request` - The application creation request containing profile information
503    ///
504    /// # Returns
505    ///
506    /// A `Result` containing the created `Application`.
507    pub async fn create_application(
508        &self,
509        request: CreateApplicationRequest,
510    ) -> Result<Application, VeracodeError> {
511        let endpoint = "/appsec/v1/applications";
512
513        let response = self.post(endpoint, Some(&request)).await?;
514        let response = Self::handle_response(response).await?;
515
516        let app: Application = response.json().await?;
517        Ok(app)
518    }
519
520    /// Update an existing application.
521    ///
522    /// # Arguments
523    ///
524    /// * `guid` - The GUID of the application to update
525    /// * `request` - The update request containing the new profile information
526    ///
527    /// # Returns
528    ///
529    /// A `Result` containing the updated `Application`.
530    pub async fn update_application(
531        &self,
532        guid: &str,
533        request: UpdateApplicationRequest,
534    ) -> Result<Application, VeracodeError> {
535        let endpoint = format!("/appsec/v1/applications/{guid}");
536
537        let response = self.put(&endpoint, Some(&request)).await?;
538        let response = Self::handle_response(response).await?;
539
540        let app: Application = response.json().await?;
541        Ok(app)
542    }
543
544    /// Delete an application.
545    ///
546    /// # Arguments
547    ///
548    /// * `guid` - The GUID of the application to delete
549    ///
550    /// # Returns
551    ///
552    /// A `Result` indicating success or failure.
553    pub async fn delete_application(&self, guid: &str) -> Result<(), VeracodeError> {
554        let endpoint = format!("/appsec/v1/applications/{guid}");
555
556        let response = self.delete(&endpoint).await?;
557        let _response = Self::handle_response(response).await?;
558
559        Ok(())
560    }
561
562    /// Get applications that failed policy compliance.
563    ///
564    /// # Returns
565    ///
566    /// A `Result` containing a `Vec<Application>` of non-compliant applications.
567    pub async fn get_non_compliant_applications(&self) -> Result<Vec<Application>, VeracodeError> {
568        let query = ApplicationQuery::new().with_policy_compliance("DID_NOT_PASS".to_string());
569
570        let response = self.get_applications(Some(query)).await?;
571
572        if let Some(embedded) = response.embedded {
573            Ok(embedded.applications)
574        } else {
575            Ok(Vec::new())
576        }
577    }
578
579    /// Get applications modified after a specific date.
580    ///
581    /// # Arguments
582    ///
583    /// * `date` - ISO 8601 formatted date string
584    ///
585    /// # Returns
586    ///
587    /// A `Result` containing a `Vec<Application>` of applications modified after the date.
588    pub async fn get_applications_modified_after(
589        &self,
590        date: &str,
591    ) -> Result<Vec<Application>, VeracodeError> {
592        let query = ApplicationQuery::new().with_modified_after(date.to_string());
593
594        let response = self.get_applications(Some(query)).await?;
595
596        if let Some(embedded) = response.embedded {
597            Ok(embedded.applications)
598        } else {
599            Ok(Vec::new())
600        }
601    }
602
603    /// Search applications by name.
604    ///
605    /// # Arguments
606    ///
607    /// * `name` - The name to search for (partial matches are supported)
608    ///
609    /// # Returns
610    ///
611    /// A `Result` containing a `Vec<Application>` of applications matching the name.
612    pub async fn search_applications_by_name(
613        &self,
614        name: &str,
615    ) -> Result<Vec<Application>, VeracodeError> {
616        let query = ApplicationQuery::new().with_name(name.to_string());
617
618        let response = self.get_applications(Some(query)).await?;
619
620        if let Some(embedded) = response.embedded {
621            Ok(embedded.applications)
622        } else {
623            Ok(Vec::new())
624        }
625    }
626
627    /// Get all applications with automatic pagination.
628    ///
629    /// # Returns
630    ///
631    /// A `Result` containing a `Vec<Application>` of all applications.
632    pub async fn get_all_applications(&self) -> Result<Vec<Application>, VeracodeError> {
633        let mut all_applications = Vec::new();
634        let mut page = 0;
635
636        loop {
637            let query = ApplicationQuery::new().with_page(page).with_size(100);
638
639            let response = self.get_applications(Some(query)).await?;
640
641            if let Some(embedded) = response.embedded {
642                if embedded.applications.is_empty() {
643                    break;
644                }
645                all_applications.extend(embedded.applications);
646                page += 1;
647            } else {
648                break;
649            }
650        }
651
652        Ok(all_applications)
653    }
654
655    /// Get application by name (exact match).
656    ///
657    /// # Arguments
658    ///
659    /// * `name` - The exact name of the application to find
660    ///
661    /// # Returns
662    ///
663    /// A `Result` containing an `Option<Application>` if found.
664    pub async fn get_application_by_name(
665        &self,
666        name: &str,
667    ) -> Result<Option<Application>, VeracodeError> {
668        let applications = self.search_applications_by_name(name).await?;
669
670        // Find exact match (search_applications_by_name does partial matching)
671        Ok(applications.into_iter().find(|app| {
672            if let Some(profile) = &app.profile {
673                profile.name == name
674            } else {
675                false
676            }
677        }))
678    }
679
680    /// Check if application exists by name.
681    ///
682    /// # Arguments
683    ///
684    /// * `name` - The name of the application to check
685    ///
686    /// # Returns
687    ///
688    /// A `Result` containing a boolean indicating if the application exists.
689    pub async fn application_exists_by_name(&self, name: &str) -> Result<bool, VeracodeError> {
690        match self.get_application_by_name(name).await? {
691            Some(_) => Ok(true),
692            None => Ok(false),
693        }
694    }
695
696    /// Get numeric app_id from application GUID.
697    ///
698    /// This is needed for XML API operations that require numeric IDs.
699    ///
700    /// # Arguments
701    ///
702    /// * `guid` - The application GUID
703    ///
704    /// # Returns
705    ///
706    /// A `Result` containing the numeric app_id as a string.
707    pub async fn get_app_id_from_guid(&self, guid: &str) -> Result<String, VeracodeError> {
708        let app = self.get_application(guid).await?;
709        Ok(app.id.to_string())
710    }
711
712    /// Create application if it doesn't exist, or return existing application.
713    ///
714    /// This method implements the "check and create" pattern commonly needed
715    /// for automated workflows.
716    ///
717    /// # Arguments
718    ///
719    /// * `name` - The name of the application
720    /// * `business_criticality` - Business criticality level (required for creation)
721    /// * `description` - Optional description for new applications
722    /// * `team_names` - Optional list of team names to assign to the application
723    ///
724    /// # Returns
725    ///
726    /// A `Result` containing the application (existing or newly created).
727    pub async fn create_application_if_not_exists(
728        &self,
729        name: &str,
730        business_criticality: BusinessCriticality,
731        description: Option<String>,
732        team_names: Option<Vec<String>>,
733    ) -> Result<Application, VeracodeError> {
734        // First, check if application already exists
735        if let Some(existing_app) = self.get_application_by_name(name).await? {
736            return Ok(existing_app);
737        }
738
739        // Application doesn't exist, create it
740
741        // Convert team names to Team objects if provided
742        let teams = team_names.map(|names| {
743            names
744                .into_iter()
745                .map(|team_name| Team {
746                    team_id: None, // Will be assigned by Veracode
747                    team_name: Some(team_name),
748                    team_legacy_id: None, // Will be assigned by Veracode
749                })
750                .collect()
751        });
752
753        let create_request = CreateApplicationRequest {
754            profile: CreateApplicationProfile {
755                name: name.to_string(),
756                business_criticality,
757                description,
758                business_unit: None,
759                business_owners: None,
760                policies: None,
761                teams,
762                tags: None,
763                custom_fields: None,
764            },
765        };
766
767        self.create_application(create_request).await
768    }
769
770    /// Create application if it doesn't exist, or return existing application (without teams).
771    ///
772    /// This is a convenience method that maintains backward compatibility
773    /// for callers that don't need to specify teams.
774    ///
775    /// # Arguments
776    ///
777    /// * `name` - The name of the application
778    /// * `business_criticality` - Business criticality level (required for creation)
779    /// * `description` - Optional description for new applications
780    ///
781    /// # Returns
782    ///
783    /// A `Result` containing the application (existing or newly created).
784    pub async fn create_application_if_not_exists_simple(
785        &self,
786        name: &str,
787        business_criticality: BusinessCriticality,
788        description: Option<String>,
789    ) -> Result<Application, VeracodeError> {
790        self.create_application_if_not_exists(name, business_criticality, description, None)
791            .await
792    }
793}
794
795#[cfg(test)]
796mod tests {
797    use super::*;
798
799    #[test]
800    fn test_query_params() {
801        let query = ApplicationQuery::new()
802            .with_name("test_app".to_string())
803            .with_policy_compliance("PASSED".to_string())
804            .with_page(1)
805            .with_size(50);
806
807        let params = query.to_query_params();
808        assert!(params.contains(&("name".to_string(), "test_app".to_string())));
809        assert!(params.contains(&("policy_compliance".to_string(), "PASSED".to_string())));
810        assert!(params.contains(&("page".to_string(), "1".to_string())));
811        assert!(params.contains(&("size".to_string(), "50".to_string())));
812    }
813
814    #[test]
815    fn test_application_query_builder() {
816        let query = ApplicationQuery::new()
817            .with_name("MyApp".to_string())
818            .with_policy_compliance("DID_NOT_PASS".to_string())
819            .with_modified_after("2023-01-01T00:00:00.000Z".to_string())
820            .with_page(2)
821            .with_size(25);
822
823        assert_eq!(query.name, Some("MyApp".to_string()));
824        assert_eq!(query.policy_compliance, Some("DID_NOT_PASS".to_string()));
825        assert_eq!(
826            query.modified_after,
827            Some("2023-01-01T00:00:00.000Z".to_string())
828        );
829        assert_eq!(query.page, Some(2));
830        assert_eq!(query.size, Some(25));
831    }
832
833    #[test]
834    fn test_create_application_request_with_teams() {
835        let team_names = vec!["Security Team".to_string(), "Development Team".to_string()];
836        let teams: Vec<Team> = team_names
837            .into_iter()
838            .map(|team_name| Team {
839                team_id: None,
840                team_name: Some(team_name),
841                team_legacy_id: None,
842            })
843            .collect();
844
845        let request = CreateApplicationRequest {
846            profile: CreateApplicationProfile {
847                name: "Test Application".to_string(),
848                business_criticality: BusinessCriticality::Medium,
849                description: Some("Test description".to_string()),
850                business_unit: None,
851                business_owners: None,
852                policies: None,
853                teams: Some(teams.clone()),
854                tags: None,
855                custom_fields: None,
856            },
857        };
858
859        assert_eq!(request.profile.name, "Test Application");
860        assert_eq!(
861            request.profile.business_criticality,
862            BusinessCriticality::Medium
863        );
864        assert!(request.profile.teams.is_some());
865
866        let request_teams = request.profile.teams.unwrap();
867        assert_eq!(request_teams.len(), 2);
868        assert_eq!(
869            request_teams[0].team_name,
870            Some("Security Team".to_string())
871        );
872        assert_eq!(
873            request_teams[1].team_name,
874            Some("Development Team".to_string())
875        );
876    }
877}