mod test_helpers;
use proptest::prelude::*;
use test_helpers::{
AdGroupBuilder, AdGroupCriterionBuilder, CampaignBuilder, CustomerBuilder, GoogleAdsRowBuilder,
MetricsBuilder, SegmentsBuilder,
};
use googleads_rs::google::ads::googleads::v23::enums::{
ad_group_status_enum::AdGroupStatus, advertising_channel_type_enum::AdvertisingChannelType,
campaign_status_enum::CampaignStatus,
};
fn to_screaming_snake_case(s: &str) -> String {
let mut result = String::new();
for (i, ch) in s.chars().enumerate() {
if ch.is_uppercase() && i > 0 {
result.push('_');
}
result.push(ch.to_ascii_uppercase());
}
result
}
proptest! {
#[test]
fn test_get_never_panics_with_random_field_paths(field_path in ".*") {
let row = GoogleAdsRowBuilder::new().build();
let _result = row.get(&field_path);
}
}
proptest! {
#[test]
fn test_get_never_panics_with_random_ascii_paths(
field_path in "[a-zA-Z0-9._-]{0,100}"
) {
let row = GoogleAdsRowBuilder::new().build();
let _result = row.get(&field_path);
}
}
proptest! {
#[test]
fn test_get_never_panics_with_sql_injection_like_inputs(
field_path in r#"[a-zA-Z0-9._;'"<>()]*"#
) {
let row = GoogleAdsRowBuilder::new().build();
let _result = row.get(&field_path);
}
}
proptest! {
#[test]
fn test_get_never_panics_with_unicode_paths(
field_path in r"[\p{L}\p{N}._-]{0,50}"
) {
let row = GoogleAdsRowBuilder::new().build();
let _result = row.get(&field_path);
}
}
fn campaign_field_path_strategy() -> impl Strategy<Value = String> {
prop::sample::select(vec![
"campaign.id".to_string(),
"campaign.name".to_string(),
"campaign.status".to_string(),
"campaign.advertising_channel_type".to_string(),
"campaign.bidding_strategy_type".to_string(),
"campaign.end_date_time".to_string(),
"campaign.campaign_budget".to_string(),
"campaign.optimization_score".to_string(),
"campaign.labels".to_string(),
])
}
proptest! {
#[test]
fn test_valid_campaign_paths_return_values(
field_path in campaign_field_path_strategy(),
campaign_id in 1i64..1_000_000_000i64,
campaign_name in "[a-zA-Z0-9 ]{1,50}",
) {
let campaign = CampaignBuilder::new()
.id(campaign_id)
.name(&campaign_name)
.status(CampaignStatus::Enabled)
.build();
let row = GoogleAdsRowBuilder::new()
.with_campaign(campaign)
.build();
let result = row.get(&field_path);
if field_path == "campaign.id" {
assert_eq!(result, campaign_id.to_string());
} else if field_path == "campaign.name" {
assert_eq!(result, campaign_name);
} else if field_path == "campaign.status" {
assert!(!result.is_empty());
}
}
}
fn metrics_field_path_strategy() -> impl Strategy<Value = String> {
prop::sample::select(vec![
"metrics.impressions".to_string(),
"metrics.clicks".to_string(),
"metrics.ctr".to_string(),
"metrics.cost_micros".to_string(),
"metrics.conversions".to_string(),
"metrics.conversions_value".to_string(),
"metrics.average_cpc".to_string(),
"metrics.average_cpm".to_string(),
])
}
proptest! {
#[test]
fn test_valid_metrics_paths_return_numeric_strings(
field_path in metrics_field_path_strategy(),
impressions in 0i64..1_000_000i64,
clicks in 0i64..100_000i64,
) {
let metrics = MetricsBuilder::new()
.impressions(impressions)
.clicks(clicks)
.ctr(if impressions > 0 { clicks as f64 / impressions as f64 } else { 0.0 })
.cost_micros(clicks * 1_000_000)
.build();
let row = GoogleAdsRowBuilder::new()
.with_metrics(metrics)
.build();
let result = row.get(&field_path);
match field_path.as_str() {
"metrics.impressions" => assert_eq!(result, impressions.to_string()),
"metrics.clicks" => assert_eq!(result, clicks.to_string()),
"metrics.ctr" => {
result.parse::<f64>().expect("Should be valid f64");
},
"metrics.cost_micros" => {
result.parse::<i64>().expect("Should be valid i64");
},
"metrics.average_cpc" | "metrics.average_cpm" | "metrics.conversions" | "metrics.conversions_value" => {
if !result.is_empty() {
result.parse::<f64>().expect("Should be valid f64 if set");
}
},
_ => {},
}
}
}
proptest! {
#[test]
fn test_campaign_id_with_random_values(campaign_id in any::<i64>()) {
let campaign = CampaignBuilder::new()
.id(campaign_id)
.build();
let row = GoogleAdsRowBuilder::new()
.with_campaign(campaign)
.build();
assert_eq!(row.get("campaign.id"), campaign_id.to_string());
}
}
proptest! {
#[test]
fn test_ad_group_id_with_random_values(ad_group_id in any::<i64>()) {
let ad_group = AdGroupBuilder::new()
.id(ad_group_id)
.build();
let row = GoogleAdsRowBuilder::new()
.with_ad_group(ad_group)
.build();
assert_eq!(row.get("ad_group.id"), ad_group_id.to_string());
}
}
proptest! {
#[test]
fn test_customer_id_with_random_values(customer_id in any::<i64>()) {
let customer = CustomerBuilder::new()
.id(customer_id)
.build();
let row = GoogleAdsRowBuilder::new()
.with_customer(customer)
.build();
assert_eq!(row.get("customer.id"), customer_id.to_string());
}
}
proptest! {
#[test]
fn test_campaign_name_with_random_strings(
campaign_name in ".*"
) {
let campaign = CampaignBuilder::new()
.name(&campaign_name)
.build();
let row = GoogleAdsRowBuilder::new()
.with_campaign(campaign)
.build();
assert_eq!(row.get("campaign.name"), campaign_name);
}
}
proptest! {
#[test]
fn test_ad_group_name_with_random_strings(
ad_group_name in "[a-zA-Z0-9 _-]{0,100}"
) {
let ad_group = AdGroupBuilder::new()
.name(&ad_group_name)
.build();
let row = GoogleAdsRowBuilder::new()
.with_ad_group(ad_group)
.build();
assert_eq!(row.get("ad_group.name"), ad_group_name);
}
}
proptest! {
#[test]
fn test_customer_descriptive_name_with_unicode(
descriptive_name in r"[\p{L}\p{N} ]{0,50}"
) {
let customer = CustomerBuilder::new()
.descriptive_name(&descriptive_name)
.build();
let row = GoogleAdsRowBuilder::new()
.with_customer(customer)
.build();
assert_eq!(row.get("customer.descriptive_name"), descriptive_name);
}
}
fn campaign_status_strategy() -> impl Strategy<Value = CampaignStatus> {
prop::sample::select(vec![
CampaignStatus::Unspecified,
CampaignStatus::Unknown,
CampaignStatus::Enabled,
CampaignStatus::Paused,
CampaignStatus::Removed,
])
}
proptest! {
#[test]
fn test_campaign_status_all_enum_values(
status in campaign_status_strategy()
) {
let campaign = CampaignBuilder::new()
.status(status)
.build();
let row = GoogleAdsRowBuilder::new()
.with_campaign(campaign)
.build();
let result = row.get("campaign.status");
assert!(!result.is_empty());
let debug_format = format!("{:?}", status);
let expected = to_screaming_snake_case(&debug_format);
assert_eq!(result, expected);
}
}
fn ad_group_status_strategy() -> impl Strategy<Value = AdGroupStatus> {
prop::sample::select(vec![
AdGroupStatus::Unspecified,
AdGroupStatus::Unknown,
AdGroupStatus::Enabled,
AdGroupStatus::Paused,
AdGroupStatus::Removed,
])
}
proptest! {
#[test]
fn test_ad_group_status_all_enum_values(
status in ad_group_status_strategy()
) {
let ad_group = AdGroupBuilder::new()
.status(status)
.build();
let row = GoogleAdsRowBuilder::new()
.with_ad_group(ad_group)
.build();
let result = row.get("ad_group.status");
assert!(!result.is_empty());
let debug_format = format!("{:?}", status);
let expected = to_screaming_snake_case(&debug_format);
assert_eq!(result, expected);
}
}
fn advertising_channel_type_strategy() -> impl Strategy<Value = AdvertisingChannelType> {
prop::sample::select(vec![
AdvertisingChannelType::Unspecified,
AdvertisingChannelType::Unknown,
AdvertisingChannelType::Search,
AdvertisingChannelType::Display,
AdvertisingChannelType::Shopping,
AdvertisingChannelType::Hotel,
AdvertisingChannelType::Video,
AdvertisingChannelType::MultiChannel,
AdvertisingChannelType::Local,
AdvertisingChannelType::Smart,
AdvertisingChannelType::PerformanceMax,
AdvertisingChannelType::LocalServices,
AdvertisingChannelType::Travel,
])
}
proptest! {
#[test]
fn test_advertising_channel_type_all_enum_values(
channel_type in advertising_channel_type_strategy()
) {
let campaign = CampaignBuilder::new()
.advertising_channel_type(channel_type)
.build();
let row = GoogleAdsRowBuilder::new()
.with_campaign(campaign)
.build();
let result = row.get("campaign.advertising_channel_type");
assert!(!result.is_empty());
let debug_format = format!("{:?}", channel_type);
let expected = to_screaming_snake_case(&debug_format);
assert_eq!(result, expected);
}
}
proptest! {
#[test]
fn test_keyword_with_random_text_and_match_types(
match_type in 0i32..5i32, keyword_text in prop::string::string_regex("[a-zA-Z0-9 ]{1,50}").unwrap(),
) {
let criterion = AdGroupCriterionBuilder::new()
.with_keyword(&keyword_text, match_type)
.build();
let row = GoogleAdsRowBuilder::new()
.with_ad_group_criterion(criterion)
.build();
assert_eq!(row.get("ad_group_criterion.keyword.text"), keyword_text);
let result = row.get("ad_group_criterion.keyword.match_type");
let expected_match_type = match match_type {
0 => "UNSPECIFIED",
1 => "UNKNOWN",
2 => "EXACT",
3 => "PHRASE",
4 => "BROAD",
_ => "UNSPECIFIED", };
assert_eq!(result, expected_match_type);
}
}
proptest! {
#[test]
fn test_metrics_impressions_full_i64_range(
impressions in any::<i64>()
) {
let metrics = MetricsBuilder::new()
.impressions(impressions)
.build();
let row = GoogleAdsRowBuilder::new()
.with_metrics(metrics)
.build();
assert_eq!(row.get("metrics.impressions"), impressions.to_string());
}
}
proptest! {
#[test]
fn test_metrics_ctr_full_f64_range(
ctr in -1000.0f64..1000.0f64
) {
let metrics = MetricsBuilder::new()
.ctr(ctr)
.build();
let row = GoogleAdsRowBuilder::new()
.with_metrics(metrics)
.build();
let result = row.get("metrics.ctr");
let parsed: f64 = result.parse().expect("Should parse as f64");
assert!((parsed - ctr).abs() < 0.000001 || ctr.is_nan() == parsed.is_nan());
}
}
proptest! {
#[test]
fn test_ad_group_bid_micros_realistic_range(
bid_micros in 0i64..1_000_000_000i64 ) {
let ad_group = AdGroupBuilder::new()
.cpc_bid_micros(bid_micros)
.build();
let row = GoogleAdsRowBuilder::new()
.with_ad_group(ad_group)
.build();
assert_eq!(row.get("ad_group.cpc_bid_micros"), bid_micros.to_string());
}
}
proptest! {
#[test]
fn test_segments_date_format(
year in 2020i32..2030i32,
month in 1u8..13u8,
day in 1u8..29u8, ) {
let date_str = format!("{:04}-{:02}-{:02}", year, month, day);
let segments = SegmentsBuilder::new()
.date(&date_str)
.build();
let row = GoogleAdsRowBuilder::new()
.with_segments(segments)
.build();
assert_eq!(row.get("segments.date"), date_str);
}
}
proptest! {
#[test]
fn test_segments_hour_valid_range(
hour in 0i32..24i32
) {
let segments = SegmentsBuilder::new()
.hour(hour)
.build();
let row = GoogleAdsRowBuilder::new()
.with_segments(segments)
.build();
assert_eq!(row.get("segments.hour"), hour.to_string());
}
}
proptest! {
#[test]
fn test_segments_year_range(
year in 2000i32..2100i32
) {
let segments = SegmentsBuilder::new()
.year(year)
.build();
let row = GoogleAdsRowBuilder::new()
.with_segments(segments)
.build();
assert_eq!(row.get("segments.year"), year.to_string());
}
}
proptest! {
#[test]
fn test_invalid_paths_return_not_implemented(
prefix in "[a-z]{3,10}",
suffix in "[a-z]{3,10}",
) {
let field_path = format!("{}.{}", prefix, suffix);
let row = GoogleAdsRowBuilder::new().build();
let result = row.get(&field_path);
assert!(
result.is_empty() || result == "not implemented by googleads-rs",
"Expected empty or 'not implemented', got: '{}'",
result
);
}
}
proptest! {
#[test]
fn test_multiple_resources_random_data(
campaign_id in 1i64..1_000_000i64,
ad_group_id in 1i64..1_000_000i64,
impressions in 0i64..1_000_000i64,
clicks in 0i64..100_000i64,
) {
let campaign = CampaignBuilder::new()
.id(campaign_id)
.status(CampaignStatus::Enabled)
.build();
let ad_group = AdGroupBuilder::new()
.id(ad_group_id)
.status(AdGroupStatus::Enabled)
.build();
let metrics = MetricsBuilder::new()
.impressions(impressions)
.clicks(clicks)
.build();
let row = GoogleAdsRowBuilder::new()
.with_campaign(campaign)
.with_ad_group(ad_group)
.with_metrics(metrics)
.build();
assert_eq!(row.get("campaign.id"), campaign_id.to_string());
assert_eq!(row.get("ad_group.id"), ad_group_id.to_string());
assert_eq!(row.get("metrics.impressions"), impressions.to_string());
assert_eq!(row.get("metrics.clicks"), clicks.to_string());
assert!(!row.get("campaign.status").is_empty());
assert!(!row.get("ad_group.status").is_empty());
}
}