use proptest::prelude::*;
use rss_gen::{
data::{parse_date, validate_url, RssData, RssItem, RssVersion},
error::RssError,
generate_rss, MAX_DESCRIPTION_LENGTH, MAX_GENERAL_LENGTH,
MAX_TITLE_LENGTH,
};
fn url_strategy() -> impl Strategy<Value = String> {
prop_oneof![
Just("https://example.com".to_string()),
Just("http://test.org".to_string()),
Just("https://www.rust-lang.org".to_string()),
prop::collection::vec(prop::char::range('a', 'z'), 1..20)
.prop_map(|chars| format!(
"https://{}.com",
chars.into_iter().collect::<String>()
))
]
}
fn rss_version_strategy() -> impl Strategy<Value = RssVersion> {
prop_oneof![
Just(RssVersion::RSS0_90),
Just(RssVersion::RSS0_91),
Just(RssVersion::RSS0_92),
Just(RssVersion::RSS1_0),
Just(RssVersion::RSS2_0),
]
}
fn safe_string_strategy(
max_len: usize,
) -> impl Strategy<Value = String> {
prop::collection::vec(
prop::char::range('\u{0020}', '\u{007E}'), 0..=max_len,
)
.prop_map(|chars| chars.into_iter().collect())
}
fn safe_nonempty_string_strategy(
max_len: usize,
) -> impl Strategy<Value = String> {
prop::collection::vec(
prop::char::range('\u{0020}', '\u{007E}'),
1..=max_len,
)
.prop_map(|chars| chars.into_iter().collect())
}
fn rss_data_strategy() -> impl Strategy<Value = RssData> {
(
rss_version_strategy(),
safe_nonempty_string_strategy(MAX_TITLE_LENGTH),
url_strategy(),
safe_nonempty_string_strategy(MAX_DESCRIPTION_LENGTH),
safe_string_strategy(MAX_GENERAL_LENGTH), safe_string_strategy(MAX_GENERAL_LENGTH), )
.prop_map(
|(version, title, link, description, author, category)| {
RssData::new(Some(version))
.title(title)
.link(link)
.description(description)
.author(author)
.category(category)
},
)
}
fn rss_item_strategy() -> impl Strategy<Value = RssItem> {
(
safe_nonempty_string_strategy(MAX_TITLE_LENGTH),
url_strategy(),
safe_nonempty_string_strategy(MAX_DESCRIPTION_LENGTH),
safe_nonempty_string_strategy(100), safe_string_strategy(MAX_GENERAL_LENGTH), )
.prop_map(|(title, link, description, guid, author)| {
RssItem::new()
.title(title)
.link(link)
.description(description)
.guid(guid)
.author(author)
})
}
#[cfg(test)]
mod property_tests {
use super::*;
proptest! {
#[test]
fn test_rss_generation_deterministic(data in rss_data_strategy()) {
let result1 = generate_rss(&data);
let result2 = generate_rss(&data);
match (result1, result2) {
(Ok(rss1), Ok(rss2)) => prop_assert_eq!(rss1, rss2),
(Err(_), Err(_)) => {}, _ => prop_assert!(false, "Non-deterministic RSS generation"),
}
}
#[test]
fn test_rss_generation_no_panic(data in rss_data_strategy()) {
let _result = generate_rss(&data);
}
#[test]
fn test_url_validation_consistency(url in url_strategy()) {
let result1 = validate_url(&url);
let result2 = validate_url(&url);
prop_assert_eq!(result1.is_ok(), result2.is_ok());
}
#[test]
fn test_rss_version_string_roundtrip(version in rss_version_strategy()) {
let version_str = version.as_str();
let parsed_version = version_str.parse::<RssVersion>();
prop_assert!(parsed_version.is_ok());
if let Ok(parsed) = parsed_version {
prop_assert_eq!(parsed, version);
}
}
#[test]
fn test_item_operations_consistency(
mut data in rss_data_strategy(),
item in rss_item_strategy(),
) {
let initial_count = data.item_count();
let guid = item.guid.clone();
data.add_item(item);
prop_assert_eq!(data.item_count(), initial_count + 1);
let removed = data.remove_item(&guid);
prop_assert!(removed);
prop_assert_eq!(data.item_count(), initial_count);
}
#[test]
fn test_validation_consistency(data in rss_data_strategy()) {
let result1 = data.validate();
let result2 = data.validate();
prop_assert_eq!(result1.is_ok(), result2.is_ok());
}
#[test]
fn test_clear_items_mathematical_property(mut data in rss_data_strategy()) {
data.clear_items();
prop_assert_eq!(data.item_count(), 0);
prop_assert!(data.items.is_empty());
}
#[test]
fn test_sanitize_content_idempotent(content in ".*") {
use rss_gen::generator::sanitize_content;
let sanitized_once = sanitize_content(&content);
let sanitized_twice = sanitize_content(&sanitized_once);
prop_assert_eq!(sanitized_once, sanitized_twice);
}
#[test]
fn test_validation_no_panic(data in rss_data_strategy()) {
let _result = data.validate();
}
#[test]
fn test_url_validation_no_panic(url in ".*") {
let _result = validate_url(&url);
}
#[test]
fn test_date_parsing_no_panic(date in ".*") {
let _result = parse_date(&date);
}
}
}
#[cfg(test)]
mod regression_tests {
use super::*;
#[test]
fn test_empty_string_validation_regression() {
let data =
RssData::new(None).title("").link("").description("");
let result = data.validate();
assert!(result.is_err());
if let Err(RssError::ValidationErrors(errors)) = result {
assert!(errors.len() >= 3);
assert!(errors.iter().any(|e| e.field == "channel.title"
&& e.message == "channel.title is missing"));
assert!(errors.iter().any(|e| e.field == "channel.link"
&& e.message == "channel.link is missing"));
assert!(errors
.iter()
.any(|e| e.field == "channel.description"
&& e.message == "channel.description is missing"));
} else {
panic!("Expected ValidationErrors");
}
}
#[test]
fn test_url_protocol_validation_regression() {
assert!(validate_url("https://example.com").is_ok());
assert!(validate_url("http://example.com").is_ok());
assert!(validate_url("ftp://example.com").is_err());
assert!(validate_url("file:///path/to/file").is_err());
assert!(validate_url("example.com").is_err());
}
#[test]
fn test_max_length_validation_regression() {
let long_title = "a".repeat(MAX_TITLE_LENGTH + 1);
let data = RssData::new(None)
.title(long_title)
.link("https://example.com")
.description("Valid description");
let _result = data.validate(); }
#[test]
fn test_special_character_sanitization_regression() {
use rss_gen::generator::sanitize_content;
let dangerous_content = "<script>alert('xss')</script>";
let sanitized = sanitize_content(dangerous_content);
assert!(!sanitized.contains("<script>"));
assert!(!sanitized.contains("</script>"));
assert!(sanitized.contains("<"));
assert!(sanitized.contains(">"));
}
#[test]
fn test_item_removal_edge_cases_regression() {
let mut data = RssData::new(None);
assert!(!data.remove_item("nonexistent"));
let item = RssItem::new().guid("test-guid");
data.add_item(item);
assert_eq!(data.item_count(), 1);
assert!(data.remove_item("test-guid"));
assert_eq!(data.item_count(), 0);
assert!(!data.remove_item("test-guid"));
}
}