use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use crate::VeracodeError;
use crate::client::VeracodeClient;
use crate::validation::{
AppGuid, AppName, Description, ValidationError, build_query_param, validate_page_number,
validate_page_size,
};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Application {
pub guid: String,
pub id: u64,
pub oid: Option<u64>,
pub alt_org_id: Option<u64>,
pub organization_id: Option<u64>,
pub created: String,
pub modified: Option<String>,
pub last_completed_scan_date: Option<String>,
pub last_policy_compliance_check_date: Option<String>,
pub app_profile_url: Option<String>,
pub profile: Option<Profile>,
pub scans: Option<Vec<Scan>>,
pub results_url: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Profile {
pub name: AppName,
pub description: Option<Description>,
pub tags: Option<String>,
pub business_unit: Option<BusinessUnit>,
pub business_owners: Option<Vec<BusinessOwner>>,
pub policies: Option<Vec<Policy>>,
pub teams: Option<Vec<Team>>,
pub archer_app_name: Option<String>,
pub custom_fields: Option<Vec<CustomField>>,
#[serde(serialize_with = "serialize_business_criticality")]
pub business_criticality: BusinessCriticality,
pub settings: Option<Settings>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_kms_alias: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub repo_url: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Settings {
pub nextday_consultation_allowed: bool,
pub static_scan_xpa_or_dpa: bool,
pub dynamic_scan_approval_not_required: bool,
pub sca_enabled: bool,
pub static_scan_xpp_enabled: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BusinessUnit {
pub id: Option<u64>,
pub name: Option<String>,
pub guid: Option<String>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct BusinessOwner {
pub email: Option<String>,
pub name: Option<String>,
}
impl fmt::Debug for BusinessOwner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BusinessOwner")
.field("email", &"[REDACTED]")
.field("name", &"[REDACTED]")
.finish()
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Policy {
pub guid: String,
pub name: String,
pub is_default: bool,
pub policy_compliance_status: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Team {
#[serde(skip_serializing_if = "Option::is_none")]
pub guid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub team_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub team_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub team_legacy_id: Option<u64>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct CustomField {
pub name: Option<String>,
pub value: Option<String>,
}
impl fmt::Debug for CustomField {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CustomField")
.field("name", &self.name)
.field("value", &"[REDACTED]")
.finish()
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Scan {
pub scan_id: Option<u64>,
pub scan_type: Option<String>,
pub status: Option<String>,
pub scan_url: Option<String>,
pub modified_date: Option<String>,
pub internal_status: Option<String>,
pub links: Option<Vec<Link>>,
pub fallback_type: Option<String>,
pub full_type: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Link {
pub rel: Option<String>,
pub href: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ApplicationsResponse {
#[serde(rename = "_embedded")]
pub embedded: Option<EmbeddedApplications>,
pub page: Option<PageInfo>,
#[serde(rename = "_links")]
pub links: Option<HashMap<String, Link>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct EmbeddedApplications {
pub applications: Vec<Application>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PageInfo {
pub size: Option<u32>,
pub number: Option<u32>,
pub total_elements: Option<u64>,
pub total_pages: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CreateApplicationRequest {
pub profile: CreateApplicationProfile,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CreateApplicationProfile {
pub name: AppName,
#[serde(serialize_with = "serialize_business_criticality")]
pub business_criticality: BusinessCriticality,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<Description>,
#[serde(skip_serializing_if = "Option::is_none")]
pub business_unit: Option<BusinessUnit>,
#[serde(skip_serializing_if = "Option::is_none")]
pub business_owners: Option<Vec<BusinessOwner>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub policies: Option<Vec<Policy>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub teams: Option<Vec<Team>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_fields: Option<Vec<CustomField>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_kms_alias: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub repo_url: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BusinessCriticality {
VeryHigh,
High,
Medium,
Low,
VeryLow,
}
impl BusinessCriticality {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
BusinessCriticality::VeryHigh => "VERY_HIGH",
BusinessCriticality::High => "HIGH",
BusinessCriticality::Medium => "MEDIUM",
BusinessCriticality::Low => "LOW",
BusinessCriticality::VeryLow => "VERY_LOW",
}
}
}
impl From<BusinessCriticality> for String {
fn from(criticality: BusinessCriticality) -> Self {
criticality.as_str().to_string()
}
}
impl std::fmt::Display for BusinessCriticality {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
fn serialize_business_criticality<S>(
criticality: &BusinessCriticality,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(criticality.as_str())
}
impl std::str::FromStr for BusinessCriticality {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"VERY_HIGH" => Ok(BusinessCriticality::VeryHigh),
"HIGH" => Ok(BusinessCriticality::High),
"MEDIUM" => Ok(BusinessCriticality::Medium),
"LOW" => Ok(BusinessCriticality::Low),
"VERY_LOW" => Ok(BusinessCriticality::VeryLow),
_ => Err(format!(
"Invalid business criticality: '{s}'. Must be one of: VERY_HIGH, HIGH, MEDIUM, LOW, VERY_LOW"
)),
}
}
}
impl<'de> serde::Deserialize<'de> for BusinessCriticality {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UpdateApplicationRequest {
pub profile: UpdateApplicationProfile,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UpdateApplicationProfile {
pub name: Option<AppName>,
pub description: Option<Description>,
pub business_unit: Option<BusinessUnit>,
pub business_owners: Option<Vec<BusinessOwner>>,
#[serde(serialize_with = "serialize_business_criticality")]
pub business_criticality: BusinessCriticality,
pub policies: Option<Vec<Policy>>,
pub teams: Option<Vec<Team>>,
pub tags: Option<String>,
pub custom_fields: Option<Vec<CustomField>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_kms_alias: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub repo_url: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ApplicationQuery {
pub name: Option<String>,
pub policy_compliance: Option<String>,
pub modified_after: Option<String>,
pub modified_before: Option<String>,
pub created_after: Option<String>,
pub created_before: Option<String>,
pub scan_type: Option<String>,
pub tags: Option<String>,
pub business_unit: Option<String>,
pub page: Option<u32>,
pub size: Option<u32>,
}
impl ApplicationQuery {
#[must_use = "builder methods consume self and return modified Self"]
pub fn new() -> Self {
ApplicationQuery::default()
}
#[must_use = "builder methods consume self and return modified Self"]
pub fn with_name(mut self, name: &str) -> Self {
self.name = Some(name.to_string());
self
}
#[must_use = "builder methods consume self and return modified Self"]
pub fn with_policy_compliance(mut self, compliance: &str) -> Self {
self.policy_compliance = Some(compliance.to_string());
self
}
#[must_use = "builder methods consume self and return modified Self"]
pub fn with_modified_after(mut self, date: &str) -> Self {
self.modified_after = Some(date.to_string());
self
}
#[must_use = "builder methods consume self and return modified Self"]
pub fn with_modified_before(mut self, date: &str) -> Self {
self.modified_before = Some(date.to_string());
self
}
#[must_use]
pub fn with_page(mut self, page: u32) -> Self {
self.page = Some(page);
self
}
#[must_use]
pub fn with_size(mut self, size: u32) -> Self {
self.size = Some(size);
self
}
pub fn normalize(mut self) -> Result<Self, ValidationError> {
self.size = Some(validate_page_size(self.size)?);
self.page = validate_page_number(self.page)?;
Ok(self)
}
#[must_use]
pub fn to_query_params(&self) -> Vec<(String, String)> {
Vec::from(self)
}
}
impl From<&ApplicationQuery> for Vec<(String, String)> {
fn from(query: &ApplicationQuery) -> Self {
let mut params = Vec::new();
if let Some(ref name) = query.name {
params.push(build_query_param("name", name));
}
if let Some(ref compliance) = query.policy_compliance {
params.push(build_query_param("policy_compliance", compliance));
}
if let Some(ref date) = query.modified_after {
params.push(build_query_param("modified_after", date));
}
if let Some(ref date) = query.modified_before {
params.push(build_query_param("modified_before", date));
}
if let Some(ref date) = query.created_after {
params.push(build_query_param("created_after", date));
}
if let Some(ref date) = query.created_before {
params.push(build_query_param("created_before", date));
}
if let Some(ref scan_type) = query.scan_type {
params.push(build_query_param("scan_type", scan_type));
}
if let Some(ref tags) = query.tags {
params.push(build_query_param("tags", tags));
}
if let Some(ref business_unit) = query.business_unit {
params.push(build_query_param("business_unit", business_unit));
}
if let Some(page) = query.page {
params.push(("page".to_string(), page.to_string()));
}
if let Some(size) = query.size {
params.push(("size".to_string(), size.to_string()));
}
params
}
}
impl From<ApplicationQuery> for Vec<(String, String)> {
fn from(query: ApplicationQuery) -> Self {
let mut params = Vec::new();
if let Some(name) = query.name {
params.push(build_query_param("name", &name));
}
if let Some(compliance) = query.policy_compliance {
params.push(build_query_param("policy_compliance", &compliance));
}
if let Some(date) = query.modified_after {
params.push(build_query_param("modified_after", &date));
}
if let Some(date) = query.modified_before {
params.push(build_query_param("modified_before", &date));
}
if let Some(date) = query.created_after {
params.push(build_query_param("created_after", &date));
}
if let Some(date) = query.created_before {
params.push(build_query_param("created_before", &date));
}
if let Some(scan_type) = query.scan_type {
params.push(build_query_param("scan_type", &scan_type));
}
if let Some(tags) = query.tags {
params.push(build_query_param("tags", &tags));
}
if let Some(business_unit) = query.business_unit {
params.push(build_query_param("business_unit", &business_unit));
}
if let Some(page) = query.page {
params.push(("page".to_string(), page.to_string()));
}
if let Some(size) = query.size {
params.push(("size".to_string(), size.to_string()));
}
params
}
}
impl VeracodeClient {
pub async fn get_applications(
&self,
query: Option<ApplicationQuery>,
) -> Result<ApplicationsResponse, VeracodeError> {
let endpoint = "/appsec/v1/applications";
let normalized_query = if let Some(q) = query {
Some(q.normalize()?)
} else {
None
};
let query_params = normalized_query.as_ref().map(Vec::from);
let response = self.get(endpoint, query_params.as_deref()).await?;
let response = Self::handle_response(response, "list applications").await?;
let apps_response: ApplicationsResponse = response.json().await?;
Ok(apps_response)
}
pub async fn get_application(&self, guid: &AppGuid) -> Result<Application, VeracodeError> {
let endpoint = format!("/appsec/v1/applications/{}", guid.as_url_safe());
let response = self.get(&endpoint, None).await?;
let response = Self::handle_response(response, "get application details").await?;
let app: Application = response.json().await?;
Ok(app)
}
pub async fn create_application(
&self,
request: &CreateApplicationRequest,
) -> Result<Application, VeracodeError> {
let endpoint = "/appsec/v1/applications";
if let Ok(json_payload) = serde_json::to_string_pretty(&request) {
log::debug!(
"🔍 Creating application with JSON payload: {}",
json_payload
);
}
let response = self.post(endpoint, Some(&request)).await?;
let response = Self::handle_response(response, "create application").await?;
let app: Application = response.json().await?;
Ok(app)
}
pub async fn update_application(
&self,
guid: &AppGuid,
request: &UpdateApplicationRequest,
) -> Result<Application, VeracodeError> {
let endpoint = format!("/appsec/v1/applications/{}", guid.as_url_safe());
let response = self.put(&endpoint, Some(&request)).await?;
let response = Self::handle_response(response, "update application").await?;
let app: Application = response.json().await?;
Ok(app)
}
pub async fn delete_application(&self, guid: &AppGuid) -> Result<(), VeracodeError> {
let endpoint = format!("/appsec/v1/applications/{}", guid.as_url_safe());
let response = self.delete(&endpoint).await?;
let _response = Self::handle_response(response, "delete application").await?;
Ok(())
}
pub async fn get_non_compliant_applications(&self) -> Result<Vec<Application>, VeracodeError> {
let query = ApplicationQuery::new().with_policy_compliance("DID_NOT_PASS");
let response = self.get_applications(Some(query)).await?;
if let Some(embedded) = response.embedded {
Ok(embedded.applications)
} else {
Ok(Vec::new())
}
}
pub async fn get_applications_modified_after(
&self,
date: &str,
) -> Result<Vec<Application>, VeracodeError> {
let query = ApplicationQuery::new().with_modified_after(date);
let response = self.get_applications(Some(query)).await?;
if let Some(embedded) = response.embedded {
Ok(embedded.applications)
} else {
Ok(Vec::new())
}
}
pub async fn search_applications_by_name(
&self,
name: &str,
) -> Result<Vec<Application>, VeracodeError> {
let query = ApplicationQuery::new().with_name(name);
let response = self.get_applications(Some(query)).await?;
if let Some(embedded) = response.embedded {
Ok(embedded.applications)
} else {
Ok(Vec::new())
}
}
pub async fn get_all_applications(&self) -> Result<Vec<Application>, VeracodeError> {
let mut all_applications = Vec::new();
let mut page = 0;
loop {
let query = ApplicationQuery::new().with_page(page).with_size(100);
let response = self.get_applications(Some(query)).await?;
if let Some(embedded) = response.embedded {
if embedded.applications.is_empty() {
break;
}
all_applications.extend(embedded.applications);
page = page.saturating_add(1);
} else {
break;
}
}
Ok(all_applications)
}
pub async fn get_application_by_name(
&self,
name: &str,
) -> Result<Option<Application>, VeracodeError> {
let applications = self.search_applications_by_name(name).await?;
Ok(applications.into_iter().find(|app| {
if let Some(profile) = &app.profile {
profile.name.as_str() == name
} else {
false
}
}))
}
pub async fn application_exists_by_name(&self, name: &str) -> Result<bool, VeracodeError> {
match self.get_application_by_name(name).await? {
Some(_) => Ok(true),
None => Ok(false),
}
}
pub async fn get_app_id_from_guid(&self, guid: &AppGuid) -> Result<String, VeracodeError> {
let app = self.get_application(guid).await?;
Ok(app.id.to_string())
}
pub async fn create_application_if_not_exists(
&self,
name: &str,
business_criticality: BusinessCriticality,
description: Option<String>,
team_names: Option<Vec<String>>,
repo_url: Option<String>,
custom_kms_alias: Option<String>,
) -> Result<Application, VeracodeError> {
if let Some(existing_app) = self.get_application_by_name(name).await? {
let mut needs_update = false;
let mut update_repo_url = false;
let mut update_description = false;
if let Some(ref profile) = existing_app.profile {
if repo_url.is_some()
&& (profile.repo_url.is_none()
|| profile
.repo_url
.as_ref()
.is_some_and(|u| u.trim().is_empty()))
{
update_repo_url = true;
needs_update = true;
}
if description.is_some()
&& (profile.description.is_none()
|| profile
.description
.as_ref()
.is_some_and(|d| d.as_str().trim().is_empty()))
{
update_description = true;
needs_update = true;
}
}
if needs_update {
log::debug!("🔄 Updating fields for existing application '{}'", name);
if update_repo_url {
log::debug!(
" Setting repo_url: {}",
repo_url.as_deref().unwrap_or("None")
);
}
if update_description {
log::debug!(
" Setting description: {}",
description.as_deref().unwrap_or("None")
);
}
let profile = existing_app.profile.as_ref().ok_or_else(|| {
VeracodeError::InvalidResponse(format!("Application '{}' has no profile", name))
})?;
let update_request = UpdateApplicationRequest {
profile: UpdateApplicationProfile {
name: Some(profile.name.clone()),
description: if update_description {
description.map(Description::new).transpose()?
} else {
profile.description.clone()
},
business_unit: profile.business_unit.clone(),
business_owners: profile.business_owners.clone(),
business_criticality: profile.business_criticality, policies: profile.policies.clone(),
teams: profile.teams.clone(), tags: profile.tags.clone(),
custom_fields: profile.custom_fields.clone(),
custom_kms_alias: profile.custom_kms_alias.clone(), repo_url: if update_repo_url {
repo_url
} else {
profile.repo_url.clone()
},
},
};
let guid = AppGuid::new(&existing_app.guid)?;
return self.update_application(&guid, &update_request).await;
}
return Ok(existing_app);
}
let teams = if let Some(names) = team_names {
let identity_api = self.identity_api();
let mut resolved_teams = Vec::new();
for team_name in names {
match identity_api.get_team_guid_by_name(&team_name).await {
Ok(Some(team_guid)) => {
resolved_teams.push(Team {
guid: Some(team_guid),
team_id: None,
team_name: None, team_legacy_id: None,
});
}
Ok(None) => {
return Err(VeracodeError::NotFound(format!(
"Team '{}' not found",
team_name
)));
}
Err(identity_err) => {
return Err(VeracodeError::InvalidResponse(format!(
"Failed to lookup team '{}': {}",
team_name, identity_err
)));
}
}
}
Some(resolved_teams)
} else {
None
};
let create_request = CreateApplicationRequest {
profile: CreateApplicationProfile {
name: AppName::new(name)?,
business_criticality,
description: description.map(Description::new).transpose()?,
business_unit: None,
business_owners: None,
policies: None,
teams,
tags: None,
custom_fields: None,
custom_kms_alias,
repo_url,
},
};
self.create_application(&create_request).await
}
pub async fn create_application_if_not_exists_with_team_guids(
&self,
name: &str,
business_criticality: BusinessCriticality,
description: Option<String>,
team_guids: Option<Vec<String>>,
) -> Result<Application, VeracodeError> {
if let Some(existing_app) = self.get_application_by_name(name).await? {
return Ok(existing_app);
}
let teams = team_guids.map(|guids| {
guids
.into_iter()
.map(|team_guid| Team {
guid: Some(team_guid),
team_id: None, team_name: None, team_legacy_id: None, })
.collect()
});
let create_request = CreateApplicationRequest {
profile: CreateApplicationProfile {
name: AppName::new(name)?,
business_criticality,
description: description.map(Description::new).transpose()?,
business_unit: None,
business_owners: None,
policies: None,
teams,
tags: None,
custom_fields: None,
custom_kms_alias: None,
repo_url: None,
},
};
self.create_application(&create_request).await
}
pub async fn create_application_if_not_exists_simple(
&self,
name: &str,
business_criticality: BusinessCriticality,
description: Option<String>,
) -> Result<Application, VeracodeError> {
self.create_application_if_not_exists(
name,
business_criticality,
description,
None,
None,
None,
)
.await
}
pub async fn enable_application_encryption(
&self,
app_guid: &AppGuid,
kms_alias: &str,
) -> Result<Application, VeracodeError> {
validate_kms_alias(kms_alias).map_err(VeracodeError::InvalidConfig)?;
let current_app = self.get_application(app_guid).await?;
let profile = current_app
.profile
.ok_or_else(|| VeracodeError::NotFound("Application profile not found".to_string()))?;
let update_request = UpdateApplicationRequest {
profile: UpdateApplicationProfile {
name: Some(profile.name),
description: profile.description,
business_unit: profile.business_unit,
business_owners: profile.business_owners,
business_criticality: profile.business_criticality,
policies: profile.policies,
teams: profile.teams,
tags: profile.tags,
custom_fields: profile.custom_fields,
custom_kms_alias: Some(kms_alias.to_string()),
repo_url: profile.repo_url,
},
};
self.update_application(app_guid, &update_request).await
}
pub async fn change_encryption_key(
&self,
app_guid: &AppGuid,
new_kms_alias: &str,
) -> Result<Application, VeracodeError> {
validate_kms_alias(new_kms_alias).map_err(VeracodeError::InvalidConfig)?;
let current_app = self.get_application(app_guid).await?;
let profile = current_app
.profile
.ok_or_else(|| VeracodeError::NotFound("Application profile not found".to_string()))?;
let update_request = UpdateApplicationRequest {
profile: UpdateApplicationProfile {
name: Some(profile.name),
description: profile.description,
business_unit: profile.business_unit,
business_owners: profile.business_owners,
business_criticality: profile.business_criticality,
policies: profile.policies,
teams: profile.teams,
tags: profile.tags,
custom_fields: profile.custom_fields,
custom_kms_alias: Some(new_kms_alias.to_string()),
repo_url: profile.repo_url,
},
};
self.update_application(app_guid, &update_request).await
}
pub async fn get_application_encryption_status(
&self,
app_guid: &AppGuid,
) -> Result<Option<String>, VeracodeError> {
let app = self.get_application(app_guid).await?;
Ok(app.profile.and_then(|profile| profile.custom_kms_alias))
}
}
pub fn validate_kms_alias(alias: &str) -> Result<(), String> {
if !alias.starts_with("alias/") {
return Err("KMS alias must start with 'alias/'".to_string());
}
if alias.len() < 8 || alias.len() > 256 {
return Err("KMS alias must be between 8 and 256 characters long".to_string());
}
let alias_name = alias
.strip_prefix("alias/")
.ok_or_else(|| "KMS alias must start with 'alias/'".to_string())?;
if alias_name.starts_with("aws") || alias_name.ends_with("aws") {
return Err("KMS alias cannot begin or end with 'aws' (reserved by AWS)".to_string());
}
if !alias_name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '/')
{
return Err("KMS alias can only contain alphanumeric characters, hyphens, underscores, and forward slashes".to_string());
}
if alias_name.is_empty() {
return Err("KMS alias name cannot be empty after 'alias/' prefix".to_string());
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_query_params() {
let query = ApplicationQuery::new()
.with_name("test_app")
.with_policy_compliance("PASSED")
.with_page(1)
.with_size(50);
let params = query.to_query_params();
assert!(params.contains(&("name".to_string(), "test_app".to_string())));
assert!(params.contains(&("policy_compliance".to_string(), "PASSED".to_string())));
assert!(params.contains(&("page".to_string(), "1".to_string())));
assert!(params.contains(&("size".to_string(), "50".to_string())));
}
#[test]
fn test_application_query_builder() {
let query = ApplicationQuery::new()
.with_name("MyApp")
.with_policy_compliance("DID_NOT_PASS")
.with_modified_after("2023-01-01T00:00:00.000Z")
.with_page(2)
.with_size(25);
assert_eq!(query.name, Some("MyApp".to_string()));
assert_eq!(query.policy_compliance, Some("DID_NOT_PASS".to_string()));
assert_eq!(
query.modified_after,
Some("2023-01-01T00:00:00.000Z".to_string())
);
assert_eq!(query.page, Some(2));
assert_eq!(query.size, Some(25));
}
#[test]
fn test_application_query_normalize_defaults() {
let query = ApplicationQuery::new();
let normalized = query.normalize().expect("should normalize");
assert_eq!(normalized.size, Some(50)); assert_eq!(normalized.page, None);
}
#[test]
fn test_application_query_normalize_valid_values() {
let query = ApplicationQuery::new().with_page(10).with_size(100);
let normalized = query.normalize().expect("should normalize");
assert_eq!(normalized.page, Some(10));
assert_eq!(normalized.size, Some(100));
}
#[test]
fn test_application_query_normalize_zero_size() {
let query = ApplicationQuery::new().with_size(0);
let result = query.normalize();
assert!(result.is_err());
}
#[test]
fn test_application_query_normalize_caps_large_size() {
let query = ApplicationQuery::new().with_size(10000);
let normalized = query.normalize().expect("should cap to max");
assert_eq!(normalized.size, Some(500));
}
#[test]
fn test_application_query_normalize_caps_large_page() {
let query = ApplicationQuery::new().with_page(50000);
let normalized = query.normalize().expect("should cap to max");
assert_eq!(normalized.page, Some(10000));
}
#[test]
fn test_query_params_url_encoding_normal() {
let query = ApplicationQuery::new()
.with_name("MyApp")
.with_policy_compliance("PASSED");
let params = query.to_query_params();
assert!(params.contains(&("name".to_string(), "MyApp".to_string())));
assert!(params.contains(&("policy_compliance".to_string(), "PASSED".to_string())));
}
#[test]
fn test_query_params_url_encoding_special_chars() {
let query = ApplicationQuery::new()
.with_name("My App & Co")
.with_policy_compliance("DID_NOT_PASS");
let params = query.to_query_params();
assert!(params.contains(&("name".to_string(), "My%20App%20%26%20Co".to_string())));
}
#[test]
fn test_query_params_injection_attempt() {
let query = ApplicationQuery::new().with_name("foo&admin=true");
let params = query.to_query_params();
assert!(params.contains(&("name".to_string(), "foo%26admin%3Dtrue".to_string())));
assert!(!params.iter().any(|(key, _)| key == "admin"));
}
#[test]
fn test_query_params_equals_injection() {
let query = ApplicationQuery::new().with_name("test=malicious");
let params = query.to_query_params();
assert!(params.contains(&("name".to_string(), "test%3Dmalicious".to_string())));
}
#[test]
fn test_query_params_semicolon_injection() {
let query = ApplicationQuery::new().with_name("test;rm -rf /");
let params = query.to_query_params();
assert!(params.contains(&("name".to_string(), "test%3Brm%20-rf%20%2F".to_string())));
}
#[test]
fn test_query_params_multiple_fields_with_encoding() {
let mut query = ApplicationQuery::new()
.with_name("App & Test")
.with_policy_compliance("PASSED")
.with_modified_after("2023-01-01T00:00:00.000Z");
query.business_unit = Some("Test & Development".to_string());
let params = query.to_query_params();
assert!(params.contains(&("name".to_string(), "App%20%26%20Test".to_string())));
assert!(params.contains(&("policy_compliance".to_string(), "PASSED".to_string())));
assert!(params.contains(&(
"modified_after".to_string(),
"2023-01-01T00%3A00%3A00.000Z".to_string()
)));
assert!(params.contains(&(
"business_unit".to_string(),
"Test%20%26%20Development".to_string()
)));
}
#[test]
fn test_create_application_request_with_teams() {
let team_names = vec!["Security Team".to_string(), "Development Team".to_string()];
let teams: Vec<Team> = team_names
.into_iter()
.map(|team_name| Team {
guid: None,
team_id: None,
team_name: Some(team_name),
team_legacy_id: None,
})
.collect();
let request = CreateApplicationRequest {
profile: CreateApplicationProfile {
name: AppName::new("Test Application").expect("valid name"),
business_criticality: BusinessCriticality::Medium,
description: Some(Description::new("Test description").expect("valid description")),
business_unit: None,
business_owners: None,
policies: None,
teams: Some(teams.clone()),
tags: None,
custom_fields: None,
custom_kms_alias: None,
repo_url: None,
},
};
assert_eq!(request.profile.name.as_str(), "Test Application");
assert_eq!(
request.profile.business_criticality,
BusinessCriticality::Medium
);
assert!(request.profile.teams.is_some());
let request_teams = request.profile.teams.expect("teams should be present");
assert_eq!(request_teams.len(), 2);
assert_eq!(
request_teams
.first()
.expect("should have first team")
.team_name,
Some("Security Team".to_string())
);
assert_eq!(
request_teams
.get(1)
.expect("should have second team")
.team_name,
Some("Development Team".to_string())
);
}
#[test]
fn test_create_application_request_with_team_guids() {
let team_guids = vec!["team-guid-1".to_string(), "team-guid-2".to_string()];
let teams: Vec<Team> = team_guids
.into_iter()
.map(|team_guid| Team {
guid: Some(team_guid),
team_id: None,
team_name: None,
team_legacy_id: None,
})
.collect();
let request = CreateApplicationRequest {
profile: CreateApplicationProfile {
name: AppName::new("Test Application").expect("valid name"),
business_criticality: BusinessCriticality::High,
description: Some(Description::new("Test description").expect("valid description")),
business_unit: None,
business_owners: None,
policies: None,
teams: Some(teams.clone()),
tags: None,
custom_fields: None,
custom_kms_alias: None,
repo_url: None,
},
};
assert_eq!(request.profile.name.as_str(), "Test Application");
assert_eq!(
request.profile.business_criticality,
BusinessCriticality::High
);
assert!(request.profile.teams.is_some());
let request_teams = request.profile.teams.expect("teams should be present");
assert_eq!(request_teams.len(), 2);
assert_eq!(
request_teams.first().expect("should have first team").guid,
Some("team-guid-1".to_string())
);
assert_eq!(
request_teams.get(1).expect("should have second team").guid,
Some("team-guid-2".to_string())
);
assert!(
request_teams
.first()
.expect("should have first team")
.team_name
.is_none()
);
assert!(
request_teams
.get(1)
.expect("should have second team")
.team_name
.is_none()
);
}
#[test]
fn test_create_application_profile_cmek_serialization() {
let profile_with_cmek = CreateApplicationProfile {
name: AppName::new("Test Application").expect("valid name"),
business_criticality: BusinessCriticality::High,
description: None,
business_unit: None,
business_owners: None,
policies: None,
teams: None,
tags: None,
custom_fields: None,
custom_kms_alias: Some("alias/my-app-key".to_string()),
repo_url: None,
};
let json = serde_json::to_string(&profile_with_cmek).expect("should serialize to json");
assert!(json.contains("custom_kms_alias"));
assert!(json.contains("alias/my-app-key"));
let profile_without_cmek = CreateApplicationProfile {
name: AppName::new("Test Application").expect("valid name"),
business_criticality: BusinessCriticality::High,
description: None,
business_unit: None,
business_owners: None,
policies: None,
teams: None,
tags: None,
custom_fields: None,
custom_kms_alias: None,
repo_url: None,
};
let json = serde_json::to_string(&profile_without_cmek).expect("should serialize to json");
assert!(!json.contains("custom_kms_alias"));
}
#[test]
fn test_update_application_profile_cmek_serialization() {
let profile_with_cmek = UpdateApplicationProfile {
name: Some(AppName::new("Updated Application").expect("valid name")),
description: None,
business_unit: None,
business_owners: None,
business_criticality: BusinessCriticality::Medium,
policies: None,
teams: None,
tags: None,
custom_fields: None,
custom_kms_alias: Some("alias/updated-key".to_string()),
repo_url: None,
};
let json = serde_json::to_string(&profile_with_cmek).expect("should serialize to json");
assert!(json.contains("custom_kms_alias"));
assert!(json.contains("alias/updated-key"));
let profile_without_cmek = UpdateApplicationProfile {
name: Some(AppName::new("Updated Application").expect("valid name")),
description: None,
business_unit: None,
business_owners: None,
business_criticality: BusinessCriticality::Medium,
policies: None,
teams: None,
tags: None,
custom_fields: None,
custom_kms_alias: None,
repo_url: None,
};
let json = serde_json::to_string(&profile_without_cmek).expect("should serialize to json");
assert!(!json.contains("custom_kms_alias"));
}
#[test]
fn test_validate_kms_alias_valid_cases() {
assert!(validate_kms_alias("alias/my-app-key").is_ok());
assert!(validate_kms_alias("alias/my_app_key_2024").is_ok());
assert!(validate_kms_alias("alias/app/environment/key").is_ok());
assert!(validate_kms_alias("alias/123-test-key").is_ok());
}
#[test]
fn test_validate_kms_alias_invalid_cases() {
assert!(validate_kms_alias("my-app-key").is_err());
assert!(validate_kms_alias("invalid-alias").is_err());
assert!(validate_kms_alias("arn:aws:kms:us-east-1:123456789:alias/my-key").is_err());
assert!(validate_kms_alias("alias/aws-managed").is_err());
assert!(validate_kms_alias("alias/my-key-aws").is_err());
assert!(validate_kms_alias("alias/").is_err());
assert!(validate_kms_alias("alias/a").is_err());
assert!(validate_kms_alias("alias/my@key").is_err());
assert!(validate_kms_alias("alias/my key").is_err());
assert!(validate_kms_alias("alias/my.key").is_err());
let long_alias = format!("alias/{}", "a".repeat(251));
assert!(validate_kms_alias(&long_alias).is_err());
}
#[test]
fn test_cmek_backward_compatibility() {
let legacy_profile = CreateApplicationProfile {
name: AppName::new("Legacy Application").expect("valid name"),
business_criticality: BusinessCriticality::High,
description: Some(
Description::new("Legacy app without CMEK").expect("valid description"),
),
business_unit: None,
business_owners: None,
policies: None,
teams: None,
tags: None,
custom_fields: None,
custom_kms_alias: None,
repo_url: None,
};
let request = CreateApplicationRequest {
profile: legacy_profile,
};
let json = serde_json::to_string(&request).expect("should serialize to json");
assert!(!json.contains("custom_kms_alias"));
assert!(json.contains("name"));
assert!(json.contains("business_criticality"));
assert!(json.contains("Legacy Application"));
let _deserialized: CreateApplicationRequest =
serde_json::from_str(&json).expect("should deserialize json");
}
#[test]
fn test_cmek_field_deserialization() {
let json_with_cmek = r#"{
"profile": {
"name": "Test App",
"business_criticality": "HIGH",
"custom_kms_alias": "alias/test-key"
}
}"#;
let request: CreateApplicationRequest =
serde_json::from_str(json_with_cmek).expect("should deserialize json");
assert_eq!(
request.profile.custom_kms_alias,
Some("alias/test-key".to_string())
);
let json_without_cmek = r#"{
"profile": {
"name": "Test App",
"business_criticality": "HIGH"
}
}"#;
let request: CreateApplicationRequest =
serde_json::from_str(json_without_cmek).expect("should deserialize json");
assert_eq!(request.profile.custom_kms_alias, None);
}
#[test]
fn test_create_application_profile_with_cmek() {
let profile_with_cmek = CreateApplicationProfile {
name: AppName::new("MyApplication").expect("valid name"),
business_criticality: BusinessCriticality::High,
description: Some(
Description::new("Application created for assessment scanning")
.expect("valid description"),
),
business_unit: None,
business_owners: None,
policies: None,
teams: None,
tags: None,
custom_fields: None,
custom_kms_alias: Some("alias/my-encryption-key".to_string()),
repo_url: Some("https://github.com/user/repo".to_string()),
};
let request = CreateApplicationRequest {
profile: profile_with_cmek,
};
let json = serde_json::to_string_pretty(&request).expect("should serialize to json");
println!("\n📦 Example JSON Payload sent to Veracode API:");
println!("{}", json);
println!();
assert!(json.contains("custom_kms_alias"));
assert!(json.contains("alias/my-encryption-key"));
let deserialized: CreateApplicationRequest =
serde_json::from_str(&json).expect("should deserialize json");
assert_eq!(
deserialized.profile.custom_kms_alias,
Some("alias/my-encryption-key".to_string())
);
}
#[test]
fn test_create_application_profile_without_cmek() {
let profile_without_cmek = CreateApplicationProfile {
name: AppName::new("MyApplication").expect("valid name"),
business_criticality: BusinessCriticality::High,
description: Some(
Description::new("Application created for assessment scanning")
.expect("valid description"),
),
business_unit: None,
business_owners: None,
policies: None,
teams: None,
tags: None,
custom_fields: None,
custom_kms_alias: None,
repo_url: Some("https://github.com/user/repo".to_string()),
};
let request = CreateApplicationRequest {
profile: profile_without_cmek,
};
let json = serde_json::to_string_pretty(&request).expect("should serialize to json");
println!("\n📦 Example JSON Payload sent to Veracode API (without --cmek):");
println!("{}", json);
println!("⚠️ Notice: 'custom_kms_alias' field is NOT included in the payload");
println!();
assert!(!json.contains("custom_kms_alias"));
let deserialized: CreateApplicationRequest =
serde_json::from_str(&json).expect("should deserialize json");
assert_eq!(deserialized.profile.custom_kms_alias, None);
}
#[test]
fn test_update_application_profile_with_cmek() {
let profile_with_cmek = UpdateApplicationProfile {
name: Some(AppName::new("Updated Application").expect("valid name")),
description: Some(Description::new("Updated description").expect("valid description")),
business_unit: None,
business_owners: None,
business_criticality: BusinessCriticality::Medium,
policies: None,
teams: None,
tags: None,
custom_fields: None,
custom_kms_alias: Some("alias/updated-key".to_string()),
repo_url: None,
};
let json = serde_json::to_string(&profile_with_cmek).expect("should serialize to json");
assert!(json.contains("custom_kms_alias"));
assert!(json.contains("alias/updated-key"));
}
#[test]
fn test_cmek_enabled_payload_structure() {
let request = CreateApplicationRequest {
profile: CreateApplicationProfile {
name: AppName::new("MyApplication").expect("valid name"),
business_criticality: BusinessCriticality::High,
description: Some(
Description::new("Application created for assessment scanning")
.expect("valid description"),
),
business_unit: None,
business_owners: None,
policies: None,
teams: None,
tags: None,
custom_fields: None,
custom_kms_alias: Some("alias/my-encryption-key".to_string()),
repo_url: Some("https://github.com/user/repo".to_string()),
},
};
let json = serde_json::to_string_pretty(&request).expect("should serialize to json");
let expected_keys = vec![
"profile",
"name",
"business_criticality",
"description",
"custom_kms_alias",
"repo_url",
];
for key in expected_keys {
assert!(
json.contains(&format!("\"{key}\"")),
"Expected key '{}' not found in payload",
key
);
}
assert!(json.contains("\"custom_kms_alias\": \"alias/my-encryption-key\""));
assert!(json.contains("\"business_criticality\": \"HIGH\""));
assert!(json.contains("\"name\": \"MyApplication\""));
let parsed: serde_json::Value =
serde_json::from_str(&json).expect("should deserialize json");
assert_eq!(
parsed
.get("profile")
.and_then(|p| p.get("custom_kms_alias"))
.and_then(|v| v.as_str())
.expect("should have custom_kms_alias"),
"alias/my-encryption-key"
);
assert_eq!(
parsed
.get("profile")
.and_then(|p| p.get("business_criticality"))
.and_then(|v| v.as_str())
.expect("should have business_criticality"),
"HIGH"
);
}
#[test]
fn test_cmek_disabled_payload_structure() {
let request = CreateApplicationRequest {
profile: CreateApplicationProfile {
name: AppName::new("MyApplication").expect("valid name"),
business_criticality: BusinessCriticality::High,
description: Some(
Description::new("Application created for assessment scanning")
.expect("valid description"),
),
business_unit: None,
business_owners: None,
policies: None,
teams: None,
tags: None,
custom_fields: None,
custom_kms_alias: None, repo_url: Some("https://github.com/user/repo".to_string()),
},
};
let json = serde_json::to_string_pretty(&request).expect("should serialize to json");
assert!(
!json.contains("custom_kms_alias"),
"custom_kms_alias should not be present when None"
);
assert!(json.contains("\"name\": \"MyApplication\""));
assert!(json.contains("\"business_criticality\": \"HIGH\""));
assert!(json.contains("\"repo_url\""));
let parsed: serde_json::Value =
serde_json::from_str(&json).expect("should deserialize json");
assert_eq!(
parsed
.get("profile")
.and_then(|p| p.get("name"))
.and_then(|v| v.as_str())
.expect("should have name"),
"MyApplication"
);
assert_eq!(
parsed
.get("profile")
.and_then(|p| p.get("business_criticality"))
.and_then(|v| v.as_str())
.expect("should have business_criticality"),
"HIGH"
);
assert!(
!parsed
.get("profile")
.and_then(|p| p.as_object())
.expect("should have profile object")
.contains_key("custom_kms_alias"),
"custom_kms_alias key should not exist in JSON object"
);
}
#[test]
fn test_cmek_alias_format_variations() {
let test_cases = vec![
"alias/production-key",
"alias/dev_environment_key",
"alias/app/prod/2024",
"alias/KEY123",
"alias/my-app-key-2024",
];
for alias in test_cases {
let request = CreateApplicationRequest {
profile: CreateApplicationProfile {
name: AppName::new("TestApp").expect("valid name"),
business_criticality: BusinessCriticality::Medium,
description: None,
business_unit: None,
business_owners: None,
policies: None,
teams: None,
tags: None,
custom_fields: None,
custom_kms_alias: Some(alias.to_string()),
repo_url: None,
},
};
let json = serde_json::to_string(&request).expect("should serialize to json");
assert!(
json.contains(alias),
"Alias '{}' should be present in payload",
alias
);
let parsed: CreateApplicationRequest =
serde_json::from_str(&json).expect("should deserialize json");
assert_eq!(parsed.profile.custom_kms_alias, Some(alias.to_string()));
}
}
#[test]
fn test_complete_application_profile_with_cmek() {
let request = CreateApplicationRequest {
profile: CreateApplicationProfile {
name: AppName::new("CompleteApplication").expect("valid name"),
business_criticality: BusinessCriticality::VeryHigh,
description: Some(
Description::new("Full featured application with CMEK")
.expect("valid description"),
),
business_unit: Some(BusinessUnit {
id: Some(123),
name: Some("Engineering".to_string()),
guid: Some("bu-guid-123".to_string()),
}),
business_owners: Some(vec![BusinessOwner {
email: Some("owner@example.com".to_string()),
name: Some("App Owner".to_string()),
}]),
policies: None,
teams: Some(vec![Team {
guid: Some("team-guid-456".to_string()),
team_id: None,
team_name: None,
team_legacy_id: None,
}]),
tags: Some("production,encrypted".to_string()),
custom_fields: Some(vec![CustomField {
name: Some("Environment".to_string()),
value: Some("Production".to_string()),
}]),
custom_kms_alias: Some("alias/production-cmek-key".to_string()),
repo_url: Some("https://github.com/company/secure-app".to_string()),
},
};
let json = serde_json::to_string_pretty(&request).expect("should serialize to json");
assert!(json.contains("\"custom_kms_alias\": \"alias/production-cmek-key\""));
assert!(json.contains("\"business_unit\""));
assert!(json.contains("\"business_owners\""));
assert!(json.contains("\"teams\""));
assert!(json.contains("\"tags\""));
assert!(json.contains("\"custom_fields\""));
let parsed: CreateApplicationRequest =
serde_json::from_str(&json).expect("should deserialize json");
assert_eq!(
parsed.profile.custom_kms_alias,
Some("alias/production-cmek-key".to_string())
);
assert!(parsed.profile.business_unit.is_some());
assert!(parsed.profile.business_owners.is_some());
}
}
#[cfg(test)]
#[allow(clippy::expect_used)] mod proptests {
use super::*;
use proptest::prelude::*;
fn valid_kms_alias_strategy() -> impl Strategy<Value = String> {
prop::string::string_regex("[a-zA-Z0-9_/-]{2,250}")
.expect("valid regex pattern for KMS alias")
.prop_map(|s| format!("alias/{}", s))
.prop_filter("Cannot start with aws", |s| {
!s.strip_prefix("alias/").unwrap_or("").starts_with("aws")
})
.prop_filter("Cannot end with aws", |s| {
!s.strip_prefix("alias/").unwrap_or("").ends_with("aws")
})
}
fn invalid_kms_alias_strategy() -> impl Strategy<Value = String> {
prop_oneof![
prop::string::string_regex("[a-zA-Z0-9_/-]{5,20}")
.expect("valid regex for missing prefix test"),
Just("arn:aws:kms:us-east-1:123456789:alias/test".to_string()),
Just("alias/aws-managed".to_string()),
Just("alias/test-aws".to_string()),
Just("alias/".to_string()),
Just("alias/a".to_string()),
Just("alias/test@key".to_string()),
Just("alias/test key".to_string()),
Just("alias/test.key".to_string()),
prop::string::string_regex("[a-z]{252}")
.expect("valid regex for too long test")
.prop_map(|s| format!("alias/{}", s)),
]
}
proptest! {
#![proptest_config(ProptestConfig {
cases: if cfg!(miri) { 5 } else { 1000 },
failure_persistence: None, // Required for Miri compatibility
.. ProptestConfig::default()
})]
#[test]
fn proptest_valid_kms_aliases_accepted(alias in valid_kms_alias_strategy()) {
prop_assert!(validate_kms_alias(&alias).is_ok(),
"Valid alias rejected: {}", alias);
}
#[test]
fn proptest_invalid_kms_aliases_rejected(alias in invalid_kms_alias_strategy()) {
prop_assert!(validate_kms_alias(&alias).is_err(),
"Invalid alias accepted: {}", alias);
}
#[test]
fn proptest_kms_alias_length_bounds(
prefix in prop::string::string_regex("[a-zA-Z0-9_/-]{1,7}").expect("valid regex for prefix"),
suffix in prop::string::string_regex("[a-zA-Z0-9_/-]{251,300}").expect("valid regex for suffix")
) {
let too_short = format!("alias/{}", prefix);
let too_long = format!("alias/{}", suffix);
prop_assert!(validate_kms_alias(&too_short).is_err() || too_short.len() >= 8,
"Too short alias not rejected");
prop_assert!(validate_kms_alias(&too_long).is_err(),
"Too long alias not rejected");
}
}
}
#[cfg(test)]
#[allow(clippy::expect_used)] mod query_proptests {
use super::*;
use crate::validation::encode_query_param;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig {
cases: if cfg!(miri) { 5 } else { 1000 },
failure_persistence: None, // Required for Miri compatibility
.. ProptestConfig::default()
})]
#[test]
fn proptest_query_param_no_injection(
value in prop::string::string_regex(".{1,100}").expect("valid regex for query param")
) {
let encoded = encode_query_param(&value);
prop_assert!(!encoded.contains('&'), "Ampersand not encoded");
prop_assert!(!encoded.contains('=') || value.contains('=') && encoded.contains("%3D"),
"Equals not encoded");
prop_assert!(!encoded.contains(';'), "Semicolon not encoded");
}
#[test]
fn proptest_query_param_path_traversal_encoded(
segments in prop::collection::vec(
prop::string::string_regex("[a-zA-Z0-9]{1,10}").expect("valid regex for path segments"),
1..5
)
) {
let path_traversal = segments.join("../");
let encoded = encode_query_param(&path_traversal);
prop_assert!(!encoded.contains("../"), "Path traversal sequence '../' not broken by encoding");
prop_assert!(!encoded.contains("..\\"), "Path traversal sequence '..\\' not broken by encoding");
prop_assert!(encoded.contains("%2F") || !path_traversal.contains('/'),
"Forward slash not encoded");
}
#[test]
fn proptest_application_query_to_params_no_key_pollution(
name in prop::option::of(prop::string::string_regex("[a-zA-Z0-9 &=;]{1,50}").expect("valid regex for app name")),
compliance in prop::option::of(Just("PASSED".to_string())),
page in prop::option::of(0u32..1000u32),
size in prop::option::of(1u32..1000u32)
) {
let mut query = ApplicationQuery::new();
if let Some(n) = name {
query = query.with_name(&n);
}
if let Some(c) = compliance {
query = query.with_policy_compliance(&c);
}
query.page = page;
query.size = size;
let params = query.to_query_params();
let mut seen_keys = std::collections::HashSet::new();
for (key, _) in params.iter() {
prop_assert!(!seen_keys.contains(key),
"Duplicate parameter key: {}", key);
seen_keys.insert(key.clone());
}
}
}
}
#[cfg(test)]
#[allow(clippy::expect_used)] mod pagination_proptests {
use super::*;
use crate::validation::{MAX_PAGE_NUMBER, MAX_PAGE_SIZE};
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig {
cases: if cfg!(miri) { 5 } else { 1000 },
failure_persistence: None, // Required for Miri compatibility
.. ProptestConfig::default()
})]
#[test]
fn proptest_page_size_bounds_enforced(size in 0u32..u32::MAX) {
match validate_page_size(Some(size)) {
Ok(validated) => {
prop_assert!(validated >= 1, "Zero page size accepted");
prop_assert!(validated <= MAX_PAGE_SIZE,
"Page size {} exceeds maximum {}", validated, MAX_PAGE_SIZE);
}
Err(_) => {
prop_assert_eq!(size, 0, "Non-zero size rejected");
}
}
}
#[test]
fn proptest_page_number_bounds_enforced(page in 0u32..u32::MAX) {
let validated = validate_page_number(Some(page)).expect("page number validation should not fail");
if let Some(p) = validated {
prop_assert!(p <= MAX_PAGE_NUMBER,
"Page number {} exceeds maximum {}", p, MAX_PAGE_NUMBER);
}
}
#[test]
fn proptest_application_query_normalize_safety(
page in prop::option::of(0u32..u32::MAX),
size in prop::option::of(0u32..u32::MAX)
) {
let mut query = ApplicationQuery::new();
query.page = page;
query.size = size;
match query.normalize() {
Ok(normalized) => {
if let Some(s) = normalized.size {
prop_assert!((1..=MAX_PAGE_SIZE).contains(&s),
"Normalized size {} out of bounds", s);
}
if let Some(p) = normalized.page {
prop_assert!(p <= MAX_PAGE_NUMBER,
"Normalized page {} exceeds maximum", p);
}
}
Err(_) => {
prop_assert_eq!(size, Some(0), "Unexpected normalization error");
}
}
}
}
}
#[cfg(test)]
mod miri_tests {
use super::*;
#[test]
fn miri_business_owner_debug_redaction() {
let owner = BusinessOwner {
email: Some("sensitive@example.com".to_string()),
name: Some("Sensitive Name".to_string()),
};
let debug_str = format!("{:?}", owner);
assert!(debug_str.contains("[REDACTED]"));
assert!(!debug_str.contains("sensitive@example.com"));
assert!(!debug_str.contains("Sensitive Name"));
}
#[test]
fn miri_business_owner_none_fields() {
let owner = BusinessOwner {
email: None,
name: None,
};
let debug_str = format!("{:?}", owner);
assert!(debug_str.contains("[REDACTED]"));
}
#[test]
fn miri_custom_field_debug_redaction() {
let field = CustomField {
name: Some("API_KEY".to_string()),
value: Some("super-secret-key".to_string()),
};
let debug_str = format!("{:?}", field);
assert!(debug_str.contains("API_KEY"));
assert!(debug_str.contains("[REDACTED]"));
assert!(!debug_str.contains("super-secret-key"));
}
#[test]
fn miri_custom_field_none_value() {
let field = CustomField {
name: Some("EMPTY_FIELD".to_string()),
value: None,
};
let debug_str = format!("{:?}", field);
assert!(debug_str.contains("EMPTY_FIELD"));
assert!(debug_str.contains("[REDACTED]"));
}
}
#[cfg(test)]
mod miri_validation_tests {
use super::*;
#[test]
fn miri_app_name_utf8_boundaries() {
let emoji_name = "MyApp 🚀 Test";
let result = AppName::new(emoji_name);
assert!(result.is_ok());
let combining = "Café"; let result = AppName::new(combining);
assert!(result.is_ok());
}
#[test]
fn miri_description_null_byte_handling() {
let with_null = "test\0value";
let result = Description::new(with_null);
assert!(result.is_err());
if let Err(err) = result {
assert!(matches!(err, ValidationError::NullByteInDescription));
}
}
#[test]
fn miri_kms_alias_character_iteration() {
let test_cases = vec![
"alias/test-key",
"alias/test_key_2024",
"alias/app/prod/key",
"alias/UPPERCASE_KEY",
];
for alias in test_cases {
let _ = validate_kms_alias(alias);
}
}
}
#[cfg(test)]
#[allow(clippy::expect_used)] mod miri_proptest {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig {
cases: if cfg!(miri) { 5 } else { 1000 },
failure_persistence: None, // Required for Miri compatibility
.. ProptestConfig::default()
})]
#[test]
fn miri_proptest_app_name_utf8_safety(
s in prop::string::string_regex("[\\p{L}\\p{N} ]{1,50}").expect("valid regex")
) {
let _ = AppName::new(&s);
}
}
}