1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use crate::VeracodeError;
10use crate::client::VeracodeClient;
11
12#[derive(Debug, Serialize, Deserialize, Clone)]
17pub struct Application {
18 pub guid: String,
20 pub id: u64,
22 pub oid: Option<u64>,
24 pub alt_org_id: Option<u64>,
26 pub organization_id: Option<u64>,
28 pub created: String,
30 pub modified: Option<String>,
32 pub last_completed_scan_date: Option<String>,
34 pub last_policy_compliance_check_date: Option<String>,
36 pub app_profile_url: Option<String>,
38 pub profile: Option<Profile>,
40 pub scans: Option<Vec<Scan>>,
42 pub results_url: Option<String>,
44}
45
46#[derive(Debug, Serialize, Deserialize, Clone)]
48pub struct Profile {
49 pub name: String,
51 pub description: Option<String>,
53 pub tags: Option<String>,
55 pub business_unit: Option<BusinessUnit>,
57 pub business_owners: Option<Vec<BusinessOwner>>,
59 pub policies: Option<Vec<Policy>>,
61 pub teams: Option<Vec<Team>>,
63 pub archer_app_name: Option<String>,
65 pub custom_fields: Option<Vec<CustomField>>,
67 #[serde(serialize_with = "serialize_business_criticality")]
69 pub business_criticality: BusinessCriticality,
70 pub settings: Option<Settings>,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub custom_kms_alias: Option<String>,
75}
76
77#[derive(Debug, Serialize, Deserialize, Clone)]
78pub struct Settings {
79 pub nextday_consultation_allowed: bool,
81 pub static_scan_xpa_or_dpa: bool,
83 pub dynamic_scan_approval_not_required: bool,
85 pub sca_enabled: bool,
87 pub static_scan_xpp_enabled: bool,
89}
90
91#[derive(Debug, Serialize, Deserialize, Clone)]
93pub struct BusinessUnit {
94 pub id: Option<u64>,
96 pub name: Option<String>,
98 pub guid: Option<String>,
100}
101
102#[derive(Debug, Serialize, Deserialize, Clone)]
104pub struct BusinessOwner {
105 pub email: Option<String>,
107 pub name: Option<String>,
109}
110
111#[derive(Debug, Serialize, Deserialize, Clone)]
113pub struct Policy {
114 pub guid: String,
116 pub name: String,
118 pub is_default: bool,
120 pub policy_compliance_status: Option<String>,
122}
123
124#[derive(Debug, Serialize, Deserialize, Clone)]
126pub struct Team {
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub guid: Option<String>,
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub team_id: Option<u64>,
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub team_name: Option<String>,
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub team_legacy_id: Option<u64>,
139}
140
141#[derive(Debug, Serialize, Deserialize, Clone)]
143pub struct CustomField {
144 pub name: Option<String>,
146 pub value: Option<String>,
148}
149
150#[derive(Debug, Serialize, Deserialize, Clone)]
152pub struct Scan {
153 pub scan_id: Option<u64>,
155 pub scan_type: Option<String>,
157 pub status: Option<String>,
159 pub scan_url: Option<String>,
161 pub modified_date: Option<String>,
163 pub internal_status: Option<String>,
165 pub links: Option<Vec<Link>>,
167 pub fallback_type: Option<String>,
169 pub full_type: Option<String>,
171}
172
173#[derive(Debug, Serialize, Deserialize, Clone)]
175pub struct Link {
176 pub rel: Option<String>,
178 pub href: Option<String>,
180}
181
182#[derive(Debug, Serialize, Deserialize, Clone)]
184pub struct ApplicationsResponse {
185 #[serde(rename = "_embedded")]
187 pub embedded: Option<EmbeddedApplications>,
188 pub page: Option<PageInfo>,
190 #[serde(rename = "_links")]
192 pub links: Option<HashMap<String, Link>>,
193}
194
195#[derive(Debug, Serialize, Deserialize, Clone)]
197pub struct EmbeddedApplications {
198 pub applications: Vec<Application>,
200}
201
202#[derive(Debug, Serialize, Deserialize, Clone)]
204pub struct PageInfo {
205 pub size: Option<u32>,
207 pub number: Option<u32>,
209 pub total_elements: Option<u64>,
211 pub total_pages: Option<u32>,
213}
214
215#[derive(Debug, Serialize, Deserialize, Clone)]
217pub struct CreateApplicationRequest {
218 pub profile: CreateApplicationProfile,
220}
221
222#[derive(Debug, Serialize, Deserialize, Clone)]
224pub struct CreateApplicationProfile {
225 pub name: String,
227 #[serde(serialize_with = "serialize_business_criticality")]
229 pub business_criticality: BusinessCriticality,
230 #[serde(skip_serializing_if = "Option::is_none")]
232 pub description: Option<String>,
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub business_unit: Option<BusinessUnit>,
236 #[serde(skip_serializing_if = "Option::is_none")]
238 pub business_owners: Option<Vec<BusinessOwner>>,
239 #[serde(skip_serializing_if = "Option::is_none")]
241 pub policies: Option<Vec<Policy>>,
242 #[serde(skip_serializing_if = "Option::is_none")]
244 pub teams: Option<Vec<Team>>,
245 #[serde(skip_serializing_if = "Option::is_none")]
247 pub tags: Option<String>,
248 #[serde(skip_serializing_if = "Option::is_none")]
250 pub custom_fields: Option<Vec<CustomField>>,
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub custom_kms_alias: Option<String>,
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258pub enum BusinessCriticality {
259 VeryHigh,
260 High,
261 Medium,
262 Low,
263 VeryLow,
264}
265
266impl BusinessCriticality {
267 #[must_use]
269 pub fn as_str(&self) -> &'static str {
270 match self {
271 BusinessCriticality::VeryHigh => "VERY_HIGH",
272 BusinessCriticality::High => "HIGH",
273 BusinessCriticality::Medium => "MEDIUM",
274 BusinessCriticality::Low => "LOW",
275 BusinessCriticality::VeryLow => "VERY_LOW",
276 }
277 }
278}
279
280impl From<BusinessCriticality> for String {
281 fn from(criticality: BusinessCriticality) -> Self {
282 criticality.as_str().to_string()
283 }
284}
285
286impl std::fmt::Display for BusinessCriticality {
287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 write!(f, "{}", self.as_str())
289 }
290}
291
292fn serialize_business_criticality<S>(
294 criticality: &BusinessCriticality,
295 serializer: S,
296) -> Result<S::Ok, S::Error>
297where
298 S: serde::Serializer,
299{
300 serializer.serialize_str(criticality.as_str())
301}
302
303impl std::str::FromStr for BusinessCriticality {
305 type Err = String;
306
307 fn from_str(s: &str) -> Result<Self, Self::Err> {
308 match s {
309 "VERY_HIGH" => Ok(BusinessCriticality::VeryHigh),
310 "HIGH" => Ok(BusinessCriticality::High),
311 "MEDIUM" => Ok(BusinessCriticality::Medium),
312 "LOW" => Ok(BusinessCriticality::Low),
313 "VERY_LOW" => Ok(BusinessCriticality::VeryLow),
314 _ => Err(format!(
315 "Invalid business criticality: '{s}'. Must be one of: VERY_HIGH, HIGH, MEDIUM, LOW, VERY_LOW"
316 )),
317 }
318 }
319}
320
321impl<'de> serde::Deserialize<'de> for BusinessCriticality {
323 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
324 where
325 D: serde::Deserializer<'de>,
326 {
327 let s = String::deserialize(deserializer)?;
328 s.parse().map_err(serde::de::Error::custom)
329 }
330}
331
332#[derive(Debug, Serialize, Deserialize, Clone)]
334pub struct UpdateApplicationRequest {
335 pub profile: UpdateApplicationProfile,
337}
338
339#[derive(Debug, Serialize, Deserialize, Clone)]
341pub struct UpdateApplicationProfile {
342 pub name: Option<String>,
344 pub description: Option<String>,
346 pub business_unit: Option<BusinessUnit>,
348 pub business_owners: Option<Vec<BusinessOwner>>,
350 #[serde(serialize_with = "serialize_business_criticality")]
352 pub business_criticality: BusinessCriticality,
353 pub policies: Option<Vec<Policy>>,
355 pub teams: Option<Vec<Team>>,
357 pub tags: Option<String>,
359 pub custom_fields: Option<Vec<CustomField>>,
361 #[serde(skip_serializing_if = "Option::is_none")]
363 pub custom_kms_alias: Option<String>,
364}
365
366#[derive(Debug, Clone, Default)]
368pub struct ApplicationQuery {
369 pub name: Option<String>,
371 pub policy_compliance: Option<String>,
373 pub modified_after: Option<String>,
375 pub modified_before: Option<String>,
377 pub created_after: Option<String>,
379 pub created_before: Option<String>,
381 pub scan_type: Option<String>,
383 pub tags: Option<String>,
385 pub business_unit: Option<String>,
387 pub page: Option<u32>,
389 pub size: Option<u32>,
391}
392
393impl ApplicationQuery {
394 #[must_use]
396 pub fn new() -> Self {
397 ApplicationQuery::default()
398 }
399
400 #[must_use]
402 pub fn with_name(mut self, name: &str) -> Self {
403 self.name = Some(name.to_string());
404 self
405 }
406
407 #[must_use]
409 pub fn with_policy_compliance(mut self, compliance: &str) -> Self {
410 self.policy_compliance = Some(compliance.to_string());
411 self
412 }
413
414 #[must_use]
416 pub fn with_modified_after(mut self, date: &str) -> Self {
417 self.modified_after = Some(date.to_string());
418 self
419 }
420
421 #[must_use]
423 pub fn with_modified_before(mut self, date: &str) -> Self {
424 self.modified_before = Some(date.to_string());
425 self
426 }
427
428 #[must_use]
430 pub fn with_page(mut self, page: u32) -> Self {
431 self.page = Some(page);
432 self
433 }
434
435 #[must_use]
437 pub fn with_size(mut self, size: u32) -> Self {
438 self.size = Some(size);
439 self
440 }
441
442 #[must_use]
444 pub fn to_query_params(&self) -> Vec<(String, String)> {
445 Vec::from(self)
446 }
447}
448
449impl From<&ApplicationQuery> for Vec<(String, String)> {
451 fn from(query: &ApplicationQuery) -> Self {
452 let mut params = Vec::new();
453
454 if let Some(ref name) = query.name {
455 params.push(("name".to_string(), name.clone()));
456 }
457 if let Some(ref compliance) = query.policy_compliance {
458 params.push(("policy_compliance".to_string(), compliance.clone()));
459 }
460 if let Some(ref date) = query.modified_after {
461 params.push(("modified_after".to_string(), date.clone()));
462 }
463 if let Some(ref date) = query.modified_before {
464 params.push(("modified_before".to_string(), date.clone()));
465 }
466 if let Some(ref date) = query.created_after {
467 params.push(("created_after".to_string(), date.clone()));
468 }
469 if let Some(ref date) = query.created_before {
470 params.push(("created_before".to_string(), date.clone()));
471 }
472 if let Some(ref scan_type) = query.scan_type {
473 params.push(("scan_type".to_string(), scan_type.clone()));
474 }
475 if let Some(ref tags) = query.tags {
476 params.push(("tags".to_string(), tags.clone()));
477 }
478 if let Some(ref business_unit) = query.business_unit {
479 params.push(("business_unit".to_string(), business_unit.clone()));
480 }
481 if let Some(page) = query.page {
482 params.push(("page".to_string(), page.to_string()));
483 }
484 if let Some(size) = query.size {
485 params.push(("size".to_string(), size.to_string()));
486 }
487
488 params
489 }
490}
491
492impl From<ApplicationQuery> for Vec<(String, String)> {
494 fn from(query: ApplicationQuery) -> Self {
495 let mut params = Vec::new();
496
497 if let Some(name) = query.name {
498 params.push(("name".to_string(), name));
499 }
500 if let Some(compliance) = query.policy_compliance {
501 params.push(("policy_compliance".to_string(), compliance));
502 }
503 if let Some(date) = query.modified_after {
504 params.push(("modified_after".to_string(), date));
505 }
506 if let Some(date) = query.modified_before {
507 params.push(("modified_before".to_string(), date));
508 }
509 if let Some(date) = query.created_after {
510 params.push(("created_after".to_string(), date));
511 }
512 if let Some(date) = query.created_before {
513 params.push(("created_before".to_string(), date));
514 }
515 if let Some(scan_type) = query.scan_type {
516 params.push(("scan_type".to_string(), scan_type));
517 }
518 if let Some(tags) = query.tags {
519 params.push(("tags".to_string(), tags));
520 }
521 if let Some(business_unit) = query.business_unit {
522 params.push(("business_unit".to_string(), business_unit));
523 }
524 if let Some(page) = query.page {
525 params.push(("page".to_string(), page.to_string()));
526 }
527 if let Some(size) = query.size {
528 params.push(("size".to_string(), size.to_string()));
529 }
530
531 params
532 }
533}
534
535impl VeracodeClient {
537 pub async fn get_applications(
547 &self,
548 query: Option<ApplicationQuery>,
549 ) -> Result<ApplicationsResponse, VeracodeError> {
550 let endpoint = "/appsec/v1/applications";
551 let query_params = query.as_ref().map(Vec::from);
552
553 let response = self.get(endpoint, query_params.as_deref()).await?;
554 let response = Self::handle_response(response).await?;
555
556 let apps_response: ApplicationsResponse = response.json().await?;
557 Ok(apps_response)
558 }
559
560 pub async fn get_application(&self, guid: &str) -> Result<Application, VeracodeError> {
570 let endpoint = format!("/appsec/v1/applications/{guid}");
571
572 let response = self.get(&endpoint, None).await?;
573 let response = Self::handle_response(response).await?;
574
575 let app: Application = response.json().await?;
576 Ok(app)
577 }
578
579 pub async fn create_application(
589 &self,
590 request: &CreateApplicationRequest,
591 ) -> Result<Application, VeracodeError> {
592 let endpoint = "/appsec/v1/applications";
593
594 if let Ok(json_payload) = serde_json::to_string_pretty(&request) {
596 log::debug!(
597 "🔍 Creating application with JSON payload: {}",
598 json_payload
599 );
600 }
601
602 let response = self.post(endpoint, Some(&request)).await?;
603 let response = Self::handle_response(response).await?;
604
605 let app: Application = response.json().await?;
606 Ok(app)
607 }
608
609 pub async fn update_application(
620 &self,
621 guid: &str,
622 request: &UpdateApplicationRequest,
623 ) -> Result<Application, VeracodeError> {
624 let endpoint = format!("/appsec/v1/applications/{guid}");
625
626 let response = self.put(&endpoint, Some(&request)).await?;
627 let response = Self::handle_response(response).await?;
628
629 let app: Application = response.json().await?;
630 Ok(app)
631 }
632
633 pub async fn delete_application(&self, guid: &str) -> Result<(), VeracodeError> {
643 let endpoint = format!("/appsec/v1/applications/{guid}");
644
645 let response = self.delete(&endpoint).await?;
646 let _response = Self::handle_response(response).await?;
647
648 Ok(())
649 }
650
651 pub async fn get_non_compliant_applications(&self) -> Result<Vec<Application>, VeracodeError> {
657 let query = ApplicationQuery::new().with_policy_compliance("DID_NOT_PASS");
658
659 let response = self.get_applications(Some(query)).await?;
660
661 if let Some(embedded) = response.embedded {
662 Ok(embedded.applications)
663 } else {
664 Ok(Vec::new())
665 }
666 }
667
668 pub async fn get_applications_modified_after(
678 &self,
679 date: &str,
680 ) -> Result<Vec<Application>, VeracodeError> {
681 let query = ApplicationQuery::new().with_modified_after(date);
682
683 let response = self.get_applications(Some(query)).await?;
684
685 if let Some(embedded) = response.embedded {
686 Ok(embedded.applications)
687 } else {
688 Ok(Vec::new())
689 }
690 }
691
692 pub async fn search_applications_by_name(
702 &self,
703 name: &str,
704 ) -> Result<Vec<Application>, VeracodeError> {
705 let query = ApplicationQuery::new().with_name(name);
706
707 let response = self.get_applications(Some(query)).await?;
708
709 if let Some(embedded) = response.embedded {
710 Ok(embedded.applications)
711 } else {
712 Ok(Vec::new())
713 }
714 }
715
716 pub async fn get_all_applications(&self) -> Result<Vec<Application>, VeracodeError> {
722 let mut all_applications = Vec::new();
723 let mut page = 0;
724
725 loop {
726 let query = ApplicationQuery::new().with_page(page).with_size(100);
727
728 let response = self.get_applications(Some(query)).await?;
729
730 if let Some(embedded) = response.embedded {
731 if embedded.applications.is_empty() {
732 break;
733 }
734 all_applications.extend(embedded.applications);
735 page += 1;
736 } else {
737 break;
738 }
739 }
740
741 Ok(all_applications)
742 }
743
744 pub async fn get_application_by_name(
754 &self,
755 name: &str,
756 ) -> Result<Option<Application>, VeracodeError> {
757 let applications = self.search_applications_by_name(name).await?;
758
759 Ok(applications.into_iter().find(|app| {
761 if let Some(profile) = &app.profile {
762 profile.name == name
763 } else {
764 false
765 }
766 }))
767 }
768
769 pub async fn application_exists_by_name(&self, name: &str) -> Result<bool, VeracodeError> {
779 match self.get_application_by_name(name).await? {
780 Some(_) => Ok(true),
781 None => Ok(false),
782 }
783 }
784
785 pub async fn get_app_id_from_guid(&self, guid: &str) -> Result<String, VeracodeError> {
797 let app = self.get_application(guid).await?;
798 Ok(app.id.to_string())
799 }
800
801 pub async fn create_application_if_not_exists(
817 &self,
818 name: &str,
819 business_criticality: BusinessCriticality,
820 description: Option<String>,
821 team_names: Option<Vec<String>>,
822 ) -> Result<Application, VeracodeError> {
823 if let Some(existing_app) = self.get_application_by_name(name).await? {
825 return Ok(existing_app);
826 }
827
828 let teams = if let Some(names) = team_names {
832 let identity_api = self.identity_api();
833 let mut resolved_teams = Vec::new();
834
835 for team_name in names {
836 match identity_api.get_team_guid_by_name(&team_name).await {
837 Ok(Some(team_guid)) => {
838 resolved_teams.push(Team {
839 guid: Some(team_guid),
840 team_id: None,
841 team_name: None, team_legacy_id: None,
843 });
844 }
845 Ok(None) => {
846 return Err(VeracodeError::NotFound(format!(
847 "Team '{}' not found",
848 team_name
849 )));
850 }
851 Err(identity_err) => {
852 return Err(VeracodeError::InvalidResponse(format!(
853 "Failed to lookup team '{}': {}",
854 team_name, identity_err
855 )));
856 }
857 }
858 }
859
860 Some(resolved_teams)
861 } else {
862 None
863 };
864
865 let create_request = CreateApplicationRequest {
866 profile: CreateApplicationProfile {
867 name: name.to_string(),
868 business_criticality,
869 description,
870 business_unit: None,
871 business_owners: None,
872 policies: None,
873 teams,
874 tags: None,
875 custom_fields: None,
876 custom_kms_alias: None,
877 },
878 };
879
880 self.create_application(&create_request).await
881 }
882
883 pub async fn create_application_if_not_exists_with_team_guids(
899 &self,
900 name: &str,
901 business_criticality: BusinessCriticality,
902 description: Option<String>,
903 team_guids: Option<Vec<String>>,
904 ) -> Result<Application, VeracodeError> {
905 if let Some(existing_app) = self.get_application_by_name(name).await? {
907 return Ok(existing_app);
908 }
909
910 let teams = team_guids.map(|guids| {
914 guids
915 .into_iter()
916 .map(|team_guid| Team {
917 guid: Some(team_guid),
918 team_id: None, team_name: None, team_legacy_id: None, })
922 .collect()
923 });
924
925 let create_request = CreateApplicationRequest {
926 profile: CreateApplicationProfile {
927 name: name.to_string(),
928 business_criticality,
929 description,
930 business_unit: None,
931 business_owners: None,
932 policies: None,
933 teams,
934 tags: None,
935 custom_fields: None,
936 custom_kms_alias: None,
937 },
938 };
939
940 self.create_application(&create_request).await
941 }
942
943 pub async fn create_application_if_not_exists_simple(
958 &self,
959 name: &str,
960 business_criticality: BusinessCriticality,
961 description: Option<String>,
962 ) -> Result<Application, VeracodeError> {
963 self.create_application_if_not_exists(name, business_criticality, description, None)
964 .await
965 }
966
967 pub async fn enable_application_encryption(
1003 &self,
1004 app_guid: &str,
1005 kms_alias: &str,
1006 ) -> Result<Application, VeracodeError> {
1007 validate_kms_alias(kms_alias).map_err(VeracodeError::InvalidConfig)?;
1009
1010 let current_app = self.get_application(app_guid).await?;
1012
1013 let profile = current_app
1014 .profile
1015 .ok_or_else(|| VeracodeError::NotFound("Application profile not found".to_string()))?;
1016
1017 let update_request = UpdateApplicationRequest {
1019 profile: UpdateApplicationProfile {
1020 name: Some(profile.name),
1021 description: profile.description,
1022 business_unit: profile.business_unit,
1023 business_owners: profile.business_owners,
1024 business_criticality: profile.business_criticality,
1025 policies: profile.policies,
1026 teams: profile.teams,
1027 tags: profile.tags,
1028 custom_fields: profile.custom_fields,
1029 custom_kms_alias: Some(kms_alias.to_string()),
1030 },
1031 };
1032
1033 self.update_application(app_guid, &update_request).await
1034 }
1035
1036 pub async fn change_encryption_key(
1050 &self,
1051 app_guid: &str,
1052 new_kms_alias: &str,
1053 ) -> Result<Application, VeracodeError> {
1054 validate_kms_alias(new_kms_alias).map_err(VeracodeError::InvalidConfig)?;
1056
1057 let current_app = self.get_application(app_guid).await?;
1059
1060 let profile = current_app
1061 .profile
1062 .ok_or_else(|| VeracodeError::NotFound("Application profile not found".to_string()))?;
1063
1064 let update_request = UpdateApplicationRequest {
1066 profile: UpdateApplicationProfile {
1067 name: Some(profile.name),
1068 description: profile.description,
1069 business_unit: profile.business_unit,
1070 business_owners: profile.business_owners,
1071 business_criticality: profile.business_criticality,
1072 policies: profile.policies,
1073 teams: profile.teams,
1074 tags: profile.tags,
1075 custom_fields: profile.custom_fields,
1076 custom_kms_alias: Some(new_kms_alias.to_string()),
1077 },
1078 };
1079
1080 self.update_application(app_guid, &update_request).await
1081 }
1082
1083 pub async fn get_application_encryption_status(
1095 &self,
1096 app_guid: &str,
1097 ) -> Result<Option<String>, VeracodeError> {
1098 let app = self.get_application(app_guid).await?;
1099
1100 Ok(app.profile.and_then(|profile| profile.custom_kms_alias))
1102 }
1103}
1104
1105pub fn validate_kms_alias(alias: &str) -> Result<(), String> {
1124 if !alias.starts_with("alias/") {
1126 return Err("KMS alias must start with 'alias/'".to_string());
1127 }
1128
1129 if alias.len() < 8 || alias.len() > 256 {
1131 return Err("KMS alias must be between 8 and 256 characters long".to_string());
1132 }
1133
1134 let alias_name = &alias[6..];
1136
1137 if alias_name.starts_with("aws") || alias_name.ends_with("aws") {
1139 return Err("KMS alias cannot begin or end with 'aws' (reserved by AWS)".to_string());
1140 }
1141
1142 if !alias_name
1144 .chars()
1145 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '/')
1146 {
1147 return Err("KMS alias can only contain alphanumeric characters, hyphens, underscores, and forward slashes".to_string());
1148 }
1149
1150 if alias_name.is_empty() {
1152 return Err("KMS alias name cannot be empty after 'alias/' prefix".to_string());
1153 }
1154
1155 Ok(())
1156}
1157
1158#[cfg(test)]
1159mod tests {
1160 use super::*;
1161
1162 #[test]
1163 fn test_query_params() {
1164 let query = ApplicationQuery::new()
1165 .with_name("test_app")
1166 .with_policy_compliance("PASSED")
1167 .with_page(1)
1168 .with_size(50);
1169
1170 let params = query.to_query_params();
1171 assert!(params.contains(&("name".to_string(), "test_app".to_string())));
1172 assert!(params.contains(&("policy_compliance".to_string(), "PASSED".to_string())));
1173 assert!(params.contains(&("page".to_string(), "1".to_string())));
1174 assert!(params.contains(&("size".to_string(), "50".to_string())));
1175 }
1176
1177 #[test]
1178 fn test_application_query_builder() {
1179 let query = ApplicationQuery::new()
1180 .with_name("MyApp")
1181 .with_policy_compliance("DID_NOT_PASS")
1182 .with_modified_after("2023-01-01T00:00:00.000Z")
1183 .with_page(2)
1184 .with_size(25);
1185
1186 assert_eq!(query.name, Some("MyApp".to_string()));
1187 assert_eq!(query.policy_compliance, Some("DID_NOT_PASS".to_string()));
1188 assert_eq!(
1189 query.modified_after,
1190 Some("2023-01-01T00:00:00.000Z".to_string())
1191 );
1192 assert_eq!(query.page, Some(2));
1193 assert_eq!(query.size, Some(25));
1194 }
1195
1196 #[test]
1197 fn test_create_application_request_with_teams() {
1198 let team_names = vec!["Security Team".to_string(), "Development Team".to_string()];
1199 let teams: Vec<Team> = team_names
1200 .into_iter()
1201 .map(|team_name| Team {
1202 guid: None,
1203 team_id: None,
1204 team_name: Some(team_name),
1205 team_legacy_id: None,
1206 })
1207 .collect();
1208
1209 let request = CreateApplicationRequest {
1210 profile: CreateApplicationProfile {
1211 name: "Test Application".to_string(),
1212 business_criticality: BusinessCriticality::Medium,
1213 description: Some("Test description".to_string()),
1214 business_unit: None,
1215 business_owners: None,
1216 policies: None,
1217 teams: Some(teams.clone()),
1218 tags: None,
1219 custom_fields: None,
1220 custom_kms_alias: None,
1221 },
1222 };
1223
1224 assert_eq!(request.profile.name, "Test Application");
1225 assert_eq!(
1226 request.profile.business_criticality,
1227 BusinessCriticality::Medium
1228 );
1229 assert!(request.profile.teams.is_some());
1230
1231 let request_teams = request.profile.teams.unwrap();
1232 assert_eq!(request_teams.len(), 2);
1233 assert_eq!(
1234 request_teams[0].team_name,
1235 Some("Security Team".to_string())
1236 );
1237 assert_eq!(
1238 request_teams[1].team_name,
1239 Some("Development Team".to_string())
1240 );
1241 }
1242
1243 #[test]
1244 fn test_create_application_request_with_team_guids() {
1245 let team_guids = vec!["team-guid-1".to_string(), "team-guid-2".to_string()];
1246 let teams: Vec<Team> = team_guids
1247 .into_iter()
1248 .map(|team_guid| Team {
1249 guid: Some(team_guid),
1250 team_id: None,
1251 team_name: None,
1252 team_legacy_id: None,
1253 })
1254 .collect();
1255
1256 let request = CreateApplicationRequest {
1257 profile: CreateApplicationProfile {
1258 name: "Test Application".to_string(),
1259 business_criticality: BusinessCriticality::High,
1260 description: Some("Test description".to_string()),
1261 business_unit: None,
1262 business_owners: None,
1263 policies: None,
1264 teams: Some(teams.clone()),
1265 tags: None,
1266 custom_fields: None,
1267 custom_kms_alias: None,
1268 },
1269 };
1270
1271 assert_eq!(request.profile.name, "Test Application");
1272 assert_eq!(
1273 request.profile.business_criticality,
1274 BusinessCriticality::High
1275 );
1276 assert!(request.profile.teams.is_some());
1277
1278 let request_teams = request.profile.teams.unwrap();
1279 assert_eq!(request_teams.len(), 2);
1280 assert_eq!(request_teams[0].guid, Some("team-guid-1".to_string()));
1281 assert_eq!(request_teams[1].guid, Some("team-guid-2".to_string()));
1282 assert!(request_teams[0].team_name.is_none());
1283 assert!(request_teams[1].team_name.is_none());
1284 }
1285
1286 #[test]
1287 fn test_create_application_profile_cmek_serialization() {
1288 let profile_with_cmek = CreateApplicationProfile {
1290 name: "Test Application".to_string(),
1291 business_criticality: BusinessCriticality::High,
1292 description: None,
1293 business_unit: None,
1294 business_owners: None,
1295 policies: None,
1296 teams: None,
1297 tags: None,
1298 custom_fields: None,
1299 custom_kms_alias: Some("alias/my-app-key".to_string()),
1300 };
1301
1302 let json = serde_json::to_string(&profile_with_cmek).unwrap();
1303 assert!(json.contains("custom_kms_alias"));
1304 assert!(json.contains("alias/my-app-key"));
1305
1306 let profile_without_cmek = CreateApplicationProfile {
1308 name: "Test Application".to_string(),
1309 business_criticality: BusinessCriticality::High,
1310 description: None,
1311 business_unit: None,
1312 business_owners: None,
1313 policies: None,
1314 teams: None,
1315 tags: None,
1316 custom_fields: None,
1317 custom_kms_alias: None,
1318 };
1319
1320 let json = serde_json::to_string(&profile_without_cmek).unwrap();
1321 assert!(!json.contains("custom_kms_alias"));
1322 }
1323
1324 #[test]
1325 fn test_update_application_profile_cmek_serialization() {
1326 let profile_with_cmek = UpdateApplicationProfile {
1328 name: Some("Updated Application".to_string()),
1329 description: None,
1330 business_unit: None,
1331 business_owners: None,
1332 business_criticality: BusinessCriticality::Medium,
1333 policies: None,
1334 teams: None,
1335 tags: None,
1336 custom_fields: None,
1337 custom_kms_alias: Some("alias/updated-key".to_string()),
1338 };
1339
1340 let json = serde_json::to_string(&profile_with_cmek).unwrap();
1341 assert!(json.contains("custom_kms_alias"));
1342 assert!(json.contains("alias/updated-key"));
1343
1344 let profile_without_cmek = UpdateApplicationProfile {
1346 name: Some("Updated Application".to_string()),
1347 description: None,
1348 business_unit: None,
1349 business_owners: None,
1350 business_criticality: BusinessCriticality::Medium,
1351 policies: None,
1352 teams: None,
1353 tags: None,
1354 custom_fields: None,
1355 custom_kms_alias: None,
1356 };
1357
1358 let json = serde_json::to_string(&profile_without_cmek).unwrap();
1359 assert!(!json.contains("custom_kms_alias"));
1360 }
1361
1362 #[test]
1363 fn test_validate_kms_alias_valid_cases() {
1364 assert!(validate_kms_alias("alias/my-app-key").is_ok());
1366 assert!(validate_kms_alias("alias/my_app_key_2024").is_ok());
1367 assert!(validate_kms_alias("alias/app/environment/key").is_ok());
1368 assert!(validate_kms_alias("alias/123-test-key").is_ok());
1369 }
1370
1371 #[test]
1372 fn test_validate_kms_alias_invalid_cases() {
1373 assert!(validate_kms_alias("my-app-key").is_err());
1375 assert!(validate_kms_alias("invalid-alias").is_err());
1376
1377 assert!(validate_kms_alias("arn:aws:kms:us-east-1:123456789:alias/my-key").is_err());
1379
1380 assert!(validate_kms_alias("alias/aws-managed").is_err());
1382 assert!(validate_kms_alias("alias/my-key-aws").is_err());
1383
1384 assert!(validate_kms_alias("alias/").is_err());
1386
1387 assert!(validate_kms_alias("alias/a").is_err());
1389
1390 assert!(validate_kms_alias("alias/my@key").is_err());
1392 assert!(validate_kms_alias("alias/my key").is_err());
1393 assert!(validate_kms_alias("alias/my.key").is_err());
1394
1395 let long_alias = format!("alias/{}", "a".repeat(251));
1397 assert!(validate_kms_alias(&long_alias).is_err());
1398 }
1399
1400 #[test]
1401 fn test_cmek_backward_compatibility() {
1402 let legacy_profile = CreateApplicationProfile {
1404 name: "Legacy Application".to_string(),
1405 business_criticality: BusinessCriticality::High,
1406 description: Some("Legacy app without CMEK".to_string()),
1407 business_unit: None,
1408 business_owners: None,
1409 policies: None,
1410 teams: None,
1411 tags: None,
1412 custom_fields: None,
1413 custom_kms_alias: None,
1414 };
1415
1416 let request = CreateApplicationRequest {
1417 profile: legacy_profile,
1418 };
1419
1420 let json = serde_json::to_string(&request).unwrap();
1422
1423 assert!(!json.contains("custom_kms_alias"));
1425
1426 assert!(json.contains("name"));
1428 assert!(json.contains("business_criticality"));
1429 assert!(json.contains("Legacy Application"));
1430
1431 let _deserialized: CreateApplicationRequest = serde_json::from_str(&json).unwrap();
1433 }
1434
1435 #[test]
1436 fn test_cmek_field_deserialization() {
1437 let json_with_cmek = r#"{
1439 "profile": {
1440 "name": "Test App",
1441 "business_criticality": "HIGH",
1442 "custom_kms_alias": "alias/test-key"
1443 }
1444 }"#;
1445
1446 let request: CreateApplicationRequest = serde_json::from_str(json_with_cmek).unwrap();
1447 assert_eq!(
1448 request.profile.custom_kms_alias,
1449 Some("alias/test-key".to_string())
1450 );
1451
1452 let json_without_cmek = r#"{
1454 "profile": {
1455 "name": "Test App",
1456 "business_criticality": "HIGH"
1457 }
1458 }"#;
1459
1460 let request: CreateApplicationRequest = serde_json::from_str(json_without_cmek).unwrap();
1461 assert_eq!(request.profile.custom_kms_alias, None);
1462 }
1463}