#[cfg(test)]
use crate::cli::args::{parse_key_val, validate_params};
#[cfg(test)]
use crate::models::template::{ParameterSpec, ParameterType};
#[cfg(test)]
use proptest::prelude::*;
#[cfg(test)]
use serde_json::json;
proptest! {
#[test]
fn prop_parameter_parsing_preserves_types(
key in "[a-z_]+",
str_val in "[^0-9].*", bool_val in any::<bool>(),
num_val in any::<f64>().prop_filter("finite", |x| x.is_finite()),
) {
let input = format!("{key}={str_val}");
if let Ok((k, v)) = parse_key_val(&input) {
assert_eq!(k, key);
if str_val == "true" || str_val == "false" {
assert!(v.is_boolean());
assert_eq!(v.as_bool().unwrap(), str_val == "true");
} else if str_val.parse::<f64>().is_ok() {
assert!(v.is_number());
} else {
assert!(v.is_string());
assert_eq!(v.as_str().unwrap(), str_val);
}
}
let bool_str = if bool_val { "true" } else { "false" };
let input = format!("{key}={bool_str}");
if let Ok((k, v)) = parse_key_val(&input) {
assert_eq!(k, key);
assert!(v.is_boolean());
assert_eq!(v.as_bool().unwrap(), bool_val);
}
let input = format!("{key}={num_val}");
if let Ok((k, v)) = parse_key_val(&input) {
assert_eq!(k, key);
assert!(v.is_number());
assert_eq!(v.as_f64().unwrap(), num_val);
}
}
}
proptest! {
#[test]
fn prop_template_uri_format(
category in "(makefile|readme|gitignore)",
toolchain in "(rust|deno|python-uv)",
variant in "cli",
) {
let uri = format!("template://{category}/{toolchain}/{variant}");
assert!(uri.starts_with("template://"));
let parts: Vec<&str> = uri.strip_prefix("template://").unwrap().split('/').collect();
assert_eq!(parts.len(), 3);
assert_eq!(parts[0], category);
assert_eq!(parts[1], toolchain);
assert_eq!(parts[2], variant);
}
}
proptest! {
#[test]
fn prop_key_value_parsing_edge_cases(
key in "[a-zA-Z_][a-zA-Z0-9_]*",
value in ".*",
) {
let input = format!("{key}={value}");
match parse_key_val(&input) {
Ok((parsed_key, parsed_value)) => {
assert_eq!(parsed_key, key);
match &parsed_value {
serde_json::Value::Bool(b) => {
if value.is_empty() {
assert!(*b);
} else {
assert!(value == "true" || value == "false");
assert_eq!(value, b.to_string());
}
},
serde_json::Value::Number(n) => {
assert_eq!(value.parse::<f64>().unwrap(), n.as_f64().unwrap());
},
serde_json::Value::String(s) => {
assert_eq!(s, &value);
},
_ => panic!("Unexpected value type"),
}
}
Err(_) => {
panic!("Valid key=value should parse successfully");
}
}
}
}
proptest! {
#[test]
fn prop_empty_value_handling(
key in "[a-zA-Z_]+",
) {
let input = format!("{key}=");
let (parsed_key, parsed_value) = parse_key_val(&input).unwrap();
assert_eq!(parsed_key, key);
assert_eq!(parsed_value, json!(true)); }
}
proptest! {
#[test]
fn prop_parameter_type_validation(
param_name in "[a-z_]+",
str_value in ".*",
bool_str in "(true|false|yes|no|1|0)",
) {
let string_spec = vec![ParameterSpec {
name: param_name.clone(),
param_type: ParameterType::String,
required: true,
default_value: None,
description: "Test".to_string(),
validation_pattern: None,
}];
let mut params = serde_json::Map::new();
params.insert(param_name.clone(), json!(str_value));
assert!(validate_params(&string_spec, ¶ms).is_ok());
let bool_spec = vec![ParameterSpec {
name: param_name.clone(),
param_type: ParameterType::Boolean,
required: true,
default_value: None,
description: "Test".to_string(),
validation_pattern: None,
}];
let mut params = serde_json::Map::new();
params.insert(param_name.clone(), json!(bool_str));
assert!(validate_params(&bool_spec, ¶ms).is_ok());
}
}
proptest! {
#[test]
fn prop_validate_params_behavior(
param_name in "[a-z_]+",
valid_value in "[a-z][a-z0-9-]*",
invalid_pattern_value in "[A-Z][A-Z0-9]*",
) {
let spec = vec![ParameterSpec {
name: param_name.clone(),
param_type: ParameterType::String,
required: true,
default_value: None,
description: "Test".to_string(),
validation_pattern: Some(r"^[a-z][a-z0-9-]*$".to_string()),
}];
let mut params = serde_json::Map::new();
params.insert(param_name.clone(), json!(valid_value));
assert!(validate_params(&spec, ¶ms).is_ok());
let mut params = serde_json::Map::new();
params.insert(param_name.clone(), json!(invalid_pattern_value));
assert!(validate_params(&spec, ¶ms).is_ok());
}
}
proptest! {
#[test]
fn prop_multiple_parameter_validation(
params_count in 1..10usize,
keys in prop::collection::vec("[a-z_]+", 1..10),
values in prop::collection::vec(".*", 1..10),
) {
let specs: Vec<ParameterSpec> = keys
.iter()
.take(params_count)
.map(|key| ParameterSpec {
name: key.clone(),
param_type: ParameterType::String,
required: false,
default_value: Some("default".to_string()),
description: "Test".to_string(),
validation_pattern: None,
})
.collect();
let mut params = serde_json::Map::new();
for (i, key) in keys.iter().take(params_count).enumerate() {
if i < values.len() {
params.insert(key.clone(), json!(values[i]));
}
}
assert!(validate_params(&specs, ¶ms).is_ok());
}
}
proptest! {
#[test]
fn prop_default_values_respected(
param_name in "[a-z_]+",
default_value in ".*",
) {
let spec = vec![ParameterSpec {
name: param_name.clone(),
param_type: ParameterType::String,
required: false,
default_value: Some(default_value.clone()),
description: "Test".to_string(),
validation_pattern: None,
}];
let params = serde_json::Map::new();
assert!(validate_params(&spec, ¶ms).is_ok());
}
}
proptest! {
#[test]
fn prop_required_parameters_enforced(
param_name in "[a-z_]+",
other_param in "[a-z_]+",
value in ".*",
) {
prop_assume!(param_name != other_param);
let spec = vec![ParameterSpec {
name: param_name.clone(),
param_type: ParameterType::String,
required: true,
default_value: None,
description: "Test".to_string(),
validation_pattern: None,
}];
let params = serde_json::Map::new();
assert!(validate_params(&spec, ¶ms).is_err());
let mut params = serde_json::Map::new();
params.insert(other_param, json!(value));
assert!(validate_params(&spec, ¶ms).is_err());
let mut params = serde_json::Map::new();
params.insert(param_name, json!(value));
assert!(validate_params(&spec, ¶ms).is_ok());
}
}
proptest! {
#[test]
fn prop_unknown_parameters_rejected(
known_param in "[a-z_]+",
unknown_param in "[a-z_]+",
value in ".*",
) {
prop_assume!(known_param != unknown_param);
let spec = vec![ParameterSpec {
name: known_param.clone(),
param_type: ParameterType::String,
required: false,
default_value: Some("default".to_string()),
description: "Test".to_string(),
validation_pattern: None,
}];
let mut params = serde_json::Map::new();
params.insert(unknown_param, json!(value));
let result = validate_params(&spec, ¶ms);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.contains("Unknown parameter")));
}
}
proptest! {
#[test]
fn prop_parameter_escaping_preserved(
key in "[a-z_]+",
value_with_special_chars in r#".*[\n\r\t"'\\].*"#,
) {
let input = format!("{key}={value_with_special_chars}");
if let Ok((parsed_key, parsed_value)) = parse_key_val(&input) {
assert_eq!(parsed_key, key);
assert_eq!(parsed_value, json!(value_with_special_chars));
}
}
}
proptest! {
#[test]
fn prop_number_strings_parsed(
key in "[a-z_]+",
int_val in any::<i64>(),
float_val in any::<f64>().prop_filter("finite", |x| x.is_finite()),
) {
let input = format!("{key}={int_val}");
let (_, parsed_value) = parse_key_val(&input).unwrap();
assert_eq!(parsed_value, json!(int_val));
let input = format!("{key}={float_val}");
let (_, parsed_value) = parse_key_val(&input).unwrap();
assert!(parsed_value.is_number());
let parsed_float = parsed_value.as_f64().unwrap();
if float_val == 0.0 || float_val == -0.0 {
assert!(parsed_float == 0.0 || parsed_float == -0.0);
} else {
assert!((parsed_float - float_val).abs() < f64::EPSILON * float_val.abs().max(1.0));
}
}
}
proptest! {
#[test]
fn prop_complex_values_with_equals(
key in "[a-z_]+",
prefix in ".*",
suffix in ".*",
) {
let complex_value = format!("{prefix}=middle={suffix}");
let input = format!("{key}={complex_value}");
let (parsed_key, parsed_value) = parse_key_val(&input).unwrap();
assert_eq!(parsed_key, key);
assert_eq!(parsed_value, json!(complex_value));
}
}