#![allow(dead_code)]
#![allow(clippy::similar_names)]
#![allow(clippy::trivially_copy_pass_by_ref)]
#![allow(clippy::ref_option_ref)]
#![allow(clippy::ref_option)]
#![allow(clippy::modulo_one)]
use fastapi_core::error::LocItem;
use fastapi_macros::Validate;
#[derive(Validate)]
struct LengthMinTest {
#[validate(length(min = 3))]
value: String,
}
#[test]
fn test_length_min_valid() {
let valid = LengthMinTest {
value: "abc".to_string(),
};
assert!(valid.validate().is_ok());
}
#[test]
fn test_length_min_invalid() {
let invalid = LengthMinTest {
value: "ab".to_string(),
};
let result = invalid.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
}
#[derive(Validate)]
struct LengthMaxTest {
#[validate(length(max = 5))]
value: String,
}
#[test]
fn test_length_max_valid() {
let valid = LengthMaxTest {
value: "hello".to_string(),
};
assert!(valid.validate().is_ok());
}
#[test]
fn test_length_max_invalid() {
let invalid = LengthMaxTest {
value: "toolong".to_string(),
};
let result = invalid.validate();
assert!(result.is_err());
}
#[derive(Validate)]
struct LengthRangeTest {
#[validate(length(min = 2, max = 5))]
value: String,
}
#[test]
fn test_length_range_valid() {
let valid = LengthRangeTest {
value: "abc".to_string(),
};
assert!(valid.validate().is_ok());
}
#[test]
fn test_length_range_too_short() {
let invalid = LengthRangeTest {
value: "a".to_string(),
};
assert!(invalid.validate().is_err());
}
#[test]
fn test_length_range_too_long() {
let invalid = LengthRangeTest {
value: "toolong".to_string(),
};
assert!(invalid.validate().is_err());
}
#[derive(Validate)]
struct RangeGeTest {
#[validate(range(ge = 0))]
value: i32,
}
#[test]
fn test_range_ge_valid() {
let valid = RangeGeTest { value: 0 };
assert!(valid.validate().is_ok());
let also_valid = RangeGeTest { value: 100 };
assert!(also_valid.validate().is_ok());
}
#[test]
fn test_range_ge_invalid() {
let invalid = RangeGeTest { value: -1 };
assert!(invalid.validate().is_err());
}
#[derive(Validate)]
struct RangeLeTest {
#[validate(range(le = 100))]
value: i32,
}
#[test]
fn test_range_le_valid() {
let valid = RangeLeTest { value: 100 };
assert!(valid.validate().is_ok());
let also_valid = RangeLeTest { value: 0 };
assert!(also_valid.validate().is_ok());
}
#[test]
fn test_range_le_invalid() {
let invalid = RangeLeTest { value: 101 };
assert!(invalid.validate().is_err());
}
#[derive(Validate)]
struct RangeGtTest {
#[validate(range(gt = 0))]
value: i32,
}
#[test]
fn test_range_gt_valid() {
let valid = RangeGtTest { value: 1 };
assert!(valid.validate().is_ok());
}
#[test]
fn test_range_gt_boundary_invalid() {
let invalid = RangeGtTest { value: 0 };
assert!(invalid.validate().is_err());
}
#[derive(Validate)]
struct RangeLtTest {
#[validate(range(lt = 100))]
value: i32,
}
#[test]
fn test_range_lt_valid() {
let valid = RangeLtTest { value: 99 };
assert!(valid.validate().is_ok());
}
#[test]
fn test_range_lt_boundary_invalid() {
let invalid = RangeLtTest { value: 100 };
assert!(invalid.validate().is_err());
}
#[derive(Validate)]
struct RangeFullTest {
#[validate(range(ge = 0, le = 100))]
value: i32,
}
#[test]
fn test_range_full_valid() {
let valid_min = RangeFullTest { value: 0 };
assert!(valid_min.validate().is_ok());
let valid_max = RangeFullTest { value: 100 };
assert!(valid_max.validate().is_ok());
let valid_mid = RangeFullTest { value: 50 };
assert!(valid_mid.validate().is_ok());
}
#[test]
fn test_range_full_invalid() {
let below = RangeFullTest { value: -1 };
assert!(below.validate().is_err());
let above = RangeFullTest { value: 101 };
assert!(above.validate().is_err());
}
#[derive(Validate)]
struct RangeFloatTest {
#[validate(range(ge = 0.0, le = 1.0))]
value: f64,
}
#[test]
fn test_range_float_valid() {
let valid = RangeFloatTest { value: 0.5 };
assert!(valid.validate().is_ok());
let valid_min = RangeFloatTest { value: 0.0 };
assert!(valid_min.validate().is_ok());
let valid_max = RangeFloatTest { value: 1.0 };
assert!(valid_max.validate().is_ok());
}
#[test]
fn test_range_float_invalid() {
let invalid = RangeFloatTest { value: 1.1 };
assert!(invalid.validate().is_err());
}
#[derive(Validate)]
struct EmailTest {
#[validate(email)]
value: String,
}
#[test]
fn test_email_valid() {
let valid = EmailTest {
value: "user@example.com".to_string(),
};
assert!(valid.validate().is_ok());
}
#[test]
fn test_email_valid_subdomain() {
let valid = EmailTest {
value: "user@mail.example.com".to_string(),
};
assert!(valid.validate().is_ok());
}
#[test]
fn test_email_invalid_no_at() {
let invalid = EmailTest {
value: "userexample.com".to_string(),
};
assert!(invalid.validate().is_err());
}
#[test]
fn test_email_invalid_no_domain() {
let invalid = EmailTest {
value: "user@".to_string(),
};
assert!(invalid.validate().is_err());
}
#[test]
fn test_email_invalid_no_user() {
let invalid = EmailTest {
value: "@example.com".to_string(),
};
assert!(invalid.validate().is_err());
}
#[test]
fn test_email_invalid_no_dot() {
let invalid = EmailTest {
value: "user@examplecom".to_string(),
};
assert!(invalid.validate().is_err());
}
#[derive(Validate)]
struct UrlTest {
#[validate(url)]
value: String,
}
#[test]
fn test_url_valid_https() {
let valid = UrlTest {
value: "https://example.com".to_string(),
};
assert!(valid.validate().is_ok());
}
#[test]
fn test_url_valid_http() {
let valid = UrlTest {
value: "http://example.com".to_string(),
};
assert!(valid.validate().is_ok());
}
#[test]
fn test_url_invalid_no_protocol() {
let invalid = UrlTest {
value: "example.com".to_string(),
};
assert!(invalid.validate().is_err());
}
#[test]
fn test_url_invalid_wrong_protocol() {
let invalid = UrlTest {
value: "ftp://example.com".to_string(),
};
assert!(invalid.validate().is_err());
}
#[derive(Validate)]
struct RegexTest {
#[validate(regex = "^[a-z]+$")]
value: String,
}
#[test]
fn test_regex_valid() {
let valid = RegexTest {
value: "abc".to_string(),
};
assert!(valid.validate().is_ok());
}
#[test]
fn test_regex_invalid_uppercase() {
let invalid = RegexTest {
value: "ABC".to_string(),
};
assert!(invalid.validate().is_err());
}
#[test]
fn test_regex_invalid_numbers() {
let invalid = RegexTest {
value: "abc123".to_string(),
};
assert!(invalid.validate().is_err());
}
#[derive(Validate)]
struct RegexPhoneTest {
#[validate(regex = r"^\d{3}-\d{3}-\d{4}$")]
phone: String,
}
#[test]
fn test_regex_phone_valid() {
let valid = RegexPhoneTest {
phone: "123-456-7890".to_string(),
};
assert!(valid.validate().is_ok());
}
#[test]
fn test_regex_phone_invalid() {
let invalid = RegexPhoneTest {
phone: "1234567890".to_string(),
};
assert!(invalid.validate().is_err());
}
#[derive(Validate)]
struct MultipleOfTest {
#[validate(multiple_of = 5)]
value: i32,
}
#[test]
fn test_multiple_of_valid() {
let valid = MultipleOfTest { value: 10 };
assert!(valid.validate().is_ok());
let valid_zero = MultipleOfTest { value: 0 };
assert!(valid_zero.validate().is_ok());
let valid_negative = MultipleOfTest { value: -15 };
assert!(valid_negative.validate().is_ok());
}
#[test]
fn test_multiple_of_invalid() {
let invalid = MultipleOfTest { value: 7 };
assert!(invalid.validate().is_err());
}
#[derive(Validate)]
struct Inner {
#[validate(length(min = 1))]
name: String,
}
#[derive(Validate)]
struct Outer {
#[validate(nested)]
inner: Inner,
}
#[test]
fn test_nested_valid() {
let valid = Outer {
inner: Inner {
name: "valid".to_string(),
},
};
assert!(valid.validate().is_ok());
}
#[test]
fn test_nested_invalid() {
let invalid = Outer {
inner: Inner {
name: String::new(),
},
};
let result = invalid.validate();
assert!(result.is_err());
}
fn validate_even(value: &i32) -> Result<(), String> {
if value % 2 == 0 {
Ok(())
} else {
Err("Value must be even".to_string())
}
}
#[derive(Validate)]
struct CustomValidatorTest {
#[validate(custom = validate_even)]
value: i32,
}
#[test]
fn test_custom_validator_valid() {
let valid = CustomValidatorTest { value: 4 };
assert!(valid.validate().is_ok());
}
#[test]
fn test_custom_validator_invalid() {
let invalid = CustomValidatorTest { value: 3 };
assert!(invalid.validate().is_err());
}
#[derive(Validate)]
struct MultipleValidators {
#[validate(length(min = 5, max = 10))]
username: String,
#[validate(range(ge = 18, le = 150))]
age: i32,
}
#[test]
fn test_multiple_validators_all_valid() {
let valid = MultipleValidators {
username: "hello".to_string(),
age: 25,
};
assert!(valid.validate().is_ok());
}
#[test]
fn test_multiple_validators_one_invalid() {
let invalid = MultipleValidators {
username: "hi".to_string(), age: 25,
};
assert!(invalid.validate().is_err());
}
#[test]
fn test_multiple_validators_both_invalid() {
let invalid = MultipleValidators {
username: "hi".to_string(), age: 10, };
let result = invalid.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 2);
}
#[derive(Validate)]
struct VecLengthTest {
#[validate(length(min = 1, max = 5))]
items: Vec<String>,
}
#[test]
fn test_vec_length_valid() {
let valid = VecLengthTest {
items: vec!["one".to_string(), "two".to_string()],
};
assert!(valid.validate().is_ok());
}
#[test]
fn test_vec_length_empty_invalid() {
let invalid = VecLengthTest { items: vec![] };
assert!(invalid.validate().is_err());
}
#[test]
fn test_vec_length_too_many_invalid() {
let invalid = VecLengthTest {
items: vec![
"1".to_string(),
"2".to_string(),
"3".to_string(),
"4".to_string(),
"5".to_string(),
"6".to_string(),
],
};
assert!(invalid.validate().is_err());
}
#[derive(Validate)]
#[allow(dead_code)]
struct NoValidation {
name: String,
age: i32,
}
#[test]
fn test_no_validation_always_valid() {
let valid = NoValidation {
name: String::new(), age: -1, };
assert!(valid.validate().is_ok());
}
#[test]
fn test_error_contains_field_name() {
let invalid = LengthMinTest {
value: "ab".to_string(),
};
let result = invalid.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
let error = &errors.errors[0];
let loc_str = format!("{:?}", error.loc);
assert!(
loc_str.contains("value"),
"Error should reference 'value' field"
);
}
#[derive(Validate)]
struct LengthZeroMinTest {
#[validate(length(min = 0))]
value: String,
}
#[test]
fn test_length_min_zero_allows_empty() {
let valid = LengthZeroMinTest {
value: String::new(),
};
assert!(valid.validate().is_ok());
}
#[derive(Validate)]
struct LengthOneMinTest {
#[validate(length(min = 1))]
value: String,
}
#[test]
fn test_length_min_one_boundary() {
let valid = LengthOneMinTest {
value: "x".to_string(),
};
assert!(valid.validate().is_ok());
let invalid = LengthOneMinTest {
value: String::new(),
};
assert!(invalid.validate().is_err());
}
#[derive(Validate)]
struct LengthExactBoundary {
#[validate(length(min = 3, max = 3))]
value: String,
}
#[test]
fn test_length_exact_match() {
let valid = LengthExactBoundary {
value: "abc".to_string(),
};
assert!(valid.validate().is_ok());
let too_short = LengthExactBoundary {
value: "ab".to_string(),
};
assert!(too_short.validate().is_err());
let too_long = LengthExactBoundary {
value: "abcd".to_string(),
};
assert!(too_long.validate().is_err());
}
#[derive(Validate)]
struct NumericBoundaryTest {
#[validate(range(ge = -128, le = 127))]
value: i8,
}
#[test]
fn test_numeric_i8_full_range() {
let min_val = NumericBoundaryTest { value: -128 };
assert!(min_val.validate().is_ok());
let max_val = NumericBoundaryTest { value: 127 };
assert!(max_val.validate().is_ok());
let mid = NumericBoundaryTest { value: 0 };
assert!(mid.validate().is_ok());
}
#[derive(Validate)]
struct ExclusiveRangeTest {
#[validate(range(gt = 0, lt = 100))]
value: i32,
}
#[test]
fn test_exclusive_range_boundaries() {
let at_lower = ExclusiveRangeTest { value: 0 };
assert!(at_lower.validate().is_err());
let at_upper = ExclusiveRangeTest { value: 100 };
assert!(at_upper.validate().is_err());
let just_above_lower = ExclusiveRangeTest { value: 1 };
assert!(just_above_lower.validate().is_ok());
let just_below_upper = ExclusiveRangeTest { value: 99 };
assert!(just_below_upper.validate().is_ok());
}
#[derive(Validate)]
struct MixedInclusiveExclusiveTest {
#[validate(range(ge = 0, lt = 100))]
value: i32,
}
#[test]
fn test_mixed_inclusive_exclusive_range() {
let at_lower = MixedInclusiveExclusiveTest { value: 0 };
assert!(at_lower.validate().is_ok());
let at_upper = MixedInclusiveExclusiveTest { value: 100 };
assert!(at_upper.validate().is_err());
let just_below = MixedInclusiveExclusiveTest { value: 99 };
assert!(just_below.validate().is_ok());
}
#[derive(Validate)]
struct FloatPrecisionTest {
#[validate(range(ge = 0.0, le = 1.0))]
value: f64,
}
#[test]
fn test_float_precision_boundaries() {
let at_zero = FloatPrecisionTest { value: 0.0 };
assert!(at_zero.validate().is_ok());
let at_one = FloatPrecisionTest { value: 1.0 };
assert!(at_one.validate().is_ok());
let epsilon = FloatPrecisionTest {
value: f64::EPSILON,
};
assert!(epsilon.validate().is_ok());
let over_one = FloatPrecisionTest {
value: 1.0 + f64::EPSILON,
};
assert!(over_one.validate().is_err());
}
#[derive(Validate)]
struct FloatNegativeTest {
#[validate(range(ge = -1.0, le = 1.0))]
value: f64,
}
#[test]
fn test_float_negative_range() {
let neg_one = FloatNegativeTest { value: -1.0 };
assert!(neg_one.validate().is_ok());
let below_neg = FloatNegativeTest { value: -1.01 };
assert!(below_neg.validate().is_err());
let zero = FloatNegativeTest { value: 0.0 };
assert!(zero.validate().is_ok());
}
#[derive(Validate)]
struct Level3 {
#[validate(length(min = 1))]
data: String,
}
#[derive(Validate)]
struct Level2 {
#[validate(nested)]
level3: Level3,
}
#[derive(Validate)]
struct Level1 {
#[validate(nested)]
level2: Level2,
}
#[test]
fn test_deeply_nested_valid() {
let valid = Level1 {
level2: Level2 {
level3: Level3 {
data: "valid".to_string(),
},
},
};
assert!(valid.validate().is_ok());
}
#[test]
fn test_deeply_nested_invalid() {
let invalid = Level1 {
level2: Level2 {
level3: Level3 {
data: String::new(),
},
},
};
let result = invalid.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
let loc_str = format!("{:?}", errors.errors[0].loc);
assert!(loc_str.contains("level2"), "Should contain level2 in path");
assert!(loc_str.contains("level3"), "Should contain level3 in path");
assert!(loc_str.contains("data"), "Should contain data in path");
}
#[derive(Validate)]
struct AllFieldsInvalid {
#[validate(length(min = 5))]
name: String,
#[validate(email)]
email: String,
#[validate(range(ge = 18))]
age: i32,
#[validate(url)]
website: String,
}
#[test]
fn test_all_fields_invalid_aggregation() {
let invalid = AllFieldsInvalid {
name: "ab".to_string(), email: "not-an-email".to_string(), age: 10, website: "not-a-url".to_string(), };
let result = invalid.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 4, "Should have 4 validation errors");
let all_locs: Vec<String> = errors
.errors
.iter()
.map(|e| format!("{:?}", e.loc))
.collect();
let combined = all_locs.join(" ");
assert!(combined.contains("name"), "Should have name error");
assert!(combined.contains("email"), "Should have email error");
assert!(combined.contains("age"), "Should have age error");
assert!(combined.contains("website"), "Should have website error");
}
#[test]
fn test_error_type_is_correct() {
let invalid = LengthMinTest {
value: "ab".to_string(),
};
let result = invalid.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
let error = &errors.errors[0];
assert_eq!(
error.error_type, "string_too_short",
"Error type should be string_too_short"
);
}
#[test]
fn test_range_error_has_context() {
let invalid = RangeGeTest { value: -5 };
let result = invalid.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
let error = &errors.errors[0];
assert!(
error.ctx.is_some() && !error.ctx.as_ref().unwrap().is_empty(),
"Range error should have context with constraint"
);
}
#[test]
fn test_validation_errors_json_format() {
let invalid = LengthMinTest {
value: "ab".to_string(),
};
let result = invalid.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
let json = errors.to_json();
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Should be valid JSON");
assert!(parsed.get("detail").is_some(), "Should have 'detail' key");
let detail = parsed.get("detail").unwrap();
assert!(detail.is_array(), "detail should be an array");
let errors_array = detail.as_array().unwrap();
assert!(!errors_array.is_empty(), "Should have at least one error");
let first_error = &errors_array[0];
assert!(
first_error.get("type").is_some(),
"Should have 'type' field"
);
assert!(first_error.get("loc").is_some(), "Should have 'loc' field");
assert!(first_error.get("msg").is_some(), "Should have 'msg' field");
}
#[test]
fn test_multiple_errors_json_format() {
let invalid = MultipleValidators {
username: "hi".to_string(), age: 10, };
let result = invalid.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
let json = errors.to_json();
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Should be valid JSON");
let detail = parsed.get("detail").unwrap().as_array().unwrap();
assert_eq!(detail.len(), 2, "Should have 2 errors in JSON");
}
fn validate_optional_email(value: &Option<String>) -> Result<(), String> {
match value {
Some(email) => {
if email.contains('@') && email.contains('.') {
Ok(())
} else {
Err("Invalid email format".to_string())
}
}
None => Ok(()), }
}
#[derive(Validate)]
struct OptionalFieldTest {
#[validate(custom = validate_optional_email)]
email: Option<String>,
}
#[test]
fn test_optional_field_none_valid() {
let valid = OptionalFieldTest { email: None };
assert!(valid.validate().is_ok());
}
#[test]
fn test_optional_field_some_valid() {
let valid = OptionalFieldTest {
email: Some("user@example.com".to_string()),
};
assert!(valid.validate().is_ok());
}
#[test]
fn test_optional_field_some_invalid() {
let invalid = OptionalFieldTest {
email: Some("not-an-email".to_string()),
};
assert!(invalid.validate().is_err());
}
fn validate_username(value: &str) -> Result<(), String> {
if value.chars().next().is_some_and(char::is_alphabetic) {
Ok(())
} else {
Err("Username must start with a letter".to_string())
}
}
#[derive(Validate)]
struct CreateUserRequest {
#[validate(length(min = 3, max = 50))]
#[validate(custom = validate_username)]
username: String,
#[validate(email)]
email: String,
#[validate(length(min = 8, max = 128))]
password: String,
#[validate(range(ge = 13, le = 120))]
age: i32,
#[validate(url)]
website: String,
}
#[test]
fn test_real_world_valid_user() {
let valid = CreateUserRequest {
username: "johndoe".to_string(),
email: "john@example.com".to_string(),
password: "securepassword123".to_string(),
age: 25,
website: "https://johndoe.com".to_string(),
};
assert!(valid.validate().is_ok());
}
#[test]
fn test_real_world_invalid_username_start() {
let invalid = CreateUserRequest {
username: "123john".to_string(), email: "john@example.com".to_string(),
password: "securepassword123".to_string(),
age: 25,
website: "https://johndoe.com".to_string(),
};
assert!(invalid.validate().is_err());
}
#[test]
fn test_real_world_multiple_issues() {
let invalid = CreateUserRequest {
username: "ab".to_string(), email: "invalid".to_string(), password: "short".to_string(), age: 10, website: "not-a-url".to_string(), };
let result = invalid.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(
errors.len() >= 5,
"Should have at least 5 validation errors"
);
}
fn validate_tags(tags: &[String]) -> Result<(), String> {
for (i, tag) in tags.iter().enumerate() {
if tag.is_empty() {
return Err(format!("Tag at index {i} cannot be empty"));
}
if tag.len() > 20 {
return Err(format!("Tag at index {i} is too long (max 20 chars)"));
}
}
Ok(())
}
#[derive(Validate)]
struct TaggedItem {
#[validate(length(min = 1))]
name: String,
#[validate(length(min = 1, max = 10))]
#[validate(custom = validate_tags)]
tags: Vec<String>,
}
#[test]
fn test_vec_items_custom_validation_valid() {
let valid = TaggedItem {
name: "My Item".to_string(),
tags: vec!["rust".to_string(), "web".to_string()],
};
assert!(valid.validate().is_ok());
}
#[test]
fn test_vec_items_custom_validation_empty_tag() {
let invalid = TaggedItem {
name: "My Item".to_string(),
tags: vec!["rust".to_string(), String::new()], };
assert!(invalid.validate().is_err());
}
#[test]
fn test_vec_items_custom_validation_tag_too_long() {
let invalid = TaggedItem {
name: "My Item".to_string(),
tags: vec!["this-tag-is-way-too-long".to_string()],
};
assert!(invalid.validate().is_err());
}
#[derive(Validate)]
struct UnicodeTest {
#[validate(length(min = 2, max = 10))]
text: String,
}
#[test]
fn test_unicode_length_by_chars() {
let valid = UnicodeTest {
text: "日本".to_string(), };
assert!(valid.validate().is_ok());
}
#[test]
fn test_emoji_length() {
let emoji = UnicodeTest {
text: "👋🌍".to_string(), };
assert!(emoji.validate().is_ok());
}
#[test]
fn test_emoji_exceeds_max_bytes() {
let emoji = UnicodeTest {
text: "👋🌍🚀".to_string(), };
assert!(emoji.validate().is_err());
}
#[derive(Validate)]
#[allow(clippy::modulo_one)]
struct ZeroMultipleTest {
#[validate(multiple_of = 1)]
value: i32,
}
#[test]
fn test_multiple_of_one_always_passes() {
let valid = ZeroMultipleTest { value: 0 };
assert!(valid.validate().is_ok());
let also_valid = ZeroMultipleTest { value: -999 };
assert!(also_valid.validate().is_ok());
let still_valid = ZeroMultipleTest { value: 12345 };
assert!(still_valid.validate().is_ok());
}
#[derive(Validate)]
struct SlugTest {
#[validate(length(min = 3, max = 50))]
#[validate(regex = "^[a-z0-9-]+$")]
slug: String,
}
#[test]
fn test_slug_valid() {
let valid = SlugTest {
slug: "my-article-slug".to_string(),
};
assert!(valid.validate().is_ok());
}
#[test]
fn test_slug_too_short() {
let invalid = SlugTest {
slug: "ab".to_string(),
};
assert!(invalid.validate().is_err());
}
#[test]
fn test_slug_invalid_chars() {
let invalid = SlugTest {
slug: "My Article".to_string(), };
assert!(invalid.validate().is_err());
}
#[test]
fn test_slug_both_length_and_pattern_fail() {
let invalid = SlugTest {
slug: "AB".to_string(), };
let result = invalid.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 2, "Should have 2 errors");
}
#[derive(fastapi_macros::Validate)]
struct PhoneTest {
#[validate(phone)]
phone: String,
}
#[test]
fn test_phone_valid_us() {
let v = PhoneTest {
phone: "+1 (555) 123-4567".to_string(),
};
assert!(v.validate().is_ok());
}
#[test]
fn test_phone_valid_international() {
let v = PhoneTest {
phone: "+44 20 7946 0958".to_string(),
};
assert!(v.validate().is_ok());
}
#[test]
fn test_phone_valid_digits_only() {
let v = PhoneTest {
phone: "5551234567".to_string(),
};
assert!(v.validate().is_ok());
}
#[test]
fn test_phone_invalid_too_short() {
let v = PhoneTest {
phone: "12345".to_string(),
};
assert!(v.validate().is_err());
}
#[test]
fn test_phone_invalid_letters() {
let v = PhoneTest {
phone: "555-CALL-ME".to_string(),
};
assert!(v.validate().is_err());
}
#[test]
fn test_phone_invalid_empty() {
let v = PhoneTest {
phone: String::new(),
};
assert!(v.validate().is_err());
}
#[test]
fn test_phone_invalid_plus_in_middle() {
let v = PhoneTest {
phone: "555+1234567".to_string(),
};
assert!(v.validate().is_err());
}
#[test]
fn test_phone_invalid_consecutive_separators() {
let v = PhoneTest {
phone: "555--1234567".to_string(),
};
assert!(v.validate().is_err());
let v = PhoneTest {
phone: "555..1234567".to_string(),
};
assert!(v.validate().is_err());
}
#[test]
fn test_phone_invalid_unbalanced_parens() {
let v = PhoneTest {
phone: "(555 1234567".to_string(),
};
assert!(v.validate().is_err());
let v = PhoneTest {
phone: "555) 1234567".to_string(),
};
assert!(v.validate().is_err());
}
#[test]
fn test_phone_invalid_empty_parens() {
let v = PhoneTest {
phone: "() 5551234567".to_string(),
};
assert!(v.validate().is_err());
}
#[test]
fn test_phone_invalid_leading_separator() {
let v = PhoneTest {
phone: "-5551234567".to_string(),
};
assert!(v.validate().is_err());
}
#[test]
fn test_phone_invalid_trailing_separator() {
let v = PhoneTest {
phone: "5551234567-".to_string(),
};
assert!(v.validate().is_err());
}
#[test]
fn test_phone_valid_with_dots() {
let v = PhoneTest {
phone: "555.123.4567".to_string(),
};
assert!(v.validate().is_ok());
}
#[test]
fn test_phone_valid_with_multiple_spaces() {
let v = PhoneTest {
phone: "+1 555 1234567".to_string(),
};
assert!(v.validate().is_ok());
}
#[derive(fastapi_macros::Validate)]
struct ContainsTest {
#[validate(contains = "@")]
value: String,
}
#[derive(fastapi_macros::Validate)]
struct StartsWithTest {
#[validate(starts_with = "https://")]
url: String,
}
#[derive(fastapi_macros::Validate)]
struct EndsWithTest {
#[validate(ends_with = ".com")]
domain: String,
}
#[test]
fn test_contains_valid() {
let v = ContainsTest {
value: "user@example.com".to_string(),
};
assert!(v.validate().is_ok());
}
#[test]
fn test_contains_invalid() {
let v = ContainsTest {
value: "no-at-sign".to_string(),
};
let err = v.validate().unwrap_err();
assert_eq!(err.len(), 1);
}
#[test]
fn test_starts_with_valid() {
let v = StartsWithTest {
url: "https://example.com".to_string(),
};
assert!(v.validate().is_ok());
}
#[test]
fn test_starts_with_invalid() {
let v = StartsWithTest {
url: "http://example.com".to_string(),
};
assert!(v.validate().is_err());
}
#[test]
fn test_ends_with_valid() {
let v = EndsWithTest {
domain: "example.com".to_string(),
};
assert!(v.validate().is_ok());
}
#[test]
fn test_ends_with_invalid() {
let v = EndsWithTest {
domain: "example.org".to_string(),
};
assert!(v.validate().is_err());
}
#[derive(Validate)]
struct TupleStructTest(
#[validate(length(min = 3))] String,
#[validate(range(ge = 0))] i32,
);
#[derive(Validate)]
struct UnitStructTest;
#[test]
fn test_tuple_struct_validation_uses_index_loc() {
let v = TupleStructTest("ab".to_string(), -1);
let err = v.validate().unwrap_err();
assert_eq!(err.len(), 2);
let expected0 = vec![LocItem::field("body"), LocItem::index(0)];
let expected1 = vec![LocItem::field("body"), LocItem::index(1)];
let locs: Vec<_> = err.errors.iter().map(|e| e.loc.clone()).collect();
assert!(locs.iter().any(|l| l == &expected0));
assert!(locs.iter().any(|l| l == &expected1));
}
#[test]
fn test_unit_struct_validation_always_ok() {
let v = UnitStructTest;
assert!(v.validate().is_ok());
}