use proptest::prelude::*;
use ricecoder_tools::search::{SearchInput, SearchOutput, SearchResult, SearchTool};
proptest! {
#[test]
fn prop_search_result_ranking(
results in prop::collection::vec(
(
".*", // title
"https://example.com/.*", // url
".*", // snippet
0usize..100usize, // rank
),
1..10
)
) {
let search_results: Vec<SearchResult> = results
.into_iter()
.enumerate()
.map(|(i, (title, url, snippet, _))| {
SearchResult::new(title, url, snippet, i)
})
.collect();
let output = SearchOutput::new(search_results.clone(), 100);
for i in 0..output.results.len() - 1 {
prop_assert!(
output.results[i].rank <= output.results[i + 1].rank,
"Results should be ranked by relevance (ascending)"
);
}
}
}
proptest! {
#[test]
fn prop_search_pagination(
limit in 1usize..100usize,
offset in 0usize..50usize,
total_count in 100usize..1000usize
) {
let input = SearchInput::new("test query")
.with_limit(limit)
.with_offset(offset);
let effective_limit = input.get_limit();
prop_assert!(effective_limit <= 100, "Limit should not exceed 100");
prop_assert_eq!(input.get_offset(), offset);
if offset + effective_limit > total_count {
let expected_results = total_count.saturating_sub(offset);
prop_assert!(expected_results <= effective_limit);
}
}
}
proptest! {
#[test]
fn prop_search_timeout_enforcement(query in ".*") {
let input = SearchInput::new(query);
prop_assert!(!input.query.is_empty() || input.query.is_empty());
}
}
proptest! {
#[test]
fn test_search_input_validation_property(query in "[a-zA-Z0-9 ]{1,100}") {
let input = SearchInput::new(query.clone());
prop_assert_eq!(input.query.clone(), query);
prop_assert_eq!(input.get_limit(), 10); prop_assert_eq!(input.get_offset(), 0); }
}
proptest! {
#[test]
fn test_search_input_limit_constraints_property(limit in 1usize..200usize) {
let input = SearchInput::new("test").with_limit(limit);
let effective_limit = input.get_limit();
prop_assert!(effective_limit <= 100);
if limit <= 100 {
prop_assert_eq!(effective_limit, limit);
} else {
prop_assert_eq!(effective_limit, 100);
}
}
}
proptest! {
#[test]
fn test_search_output_consistency_property(
result_count in 0usize..100usize,
total_count in 0usize..1000usize
) {
let results: Vec<SearchResult> = (0..result_count)
.map(|i| SearchResult::new(
format!("Title {}", i),
format!("https://example.com/{}", i),
format!("Snippet {}", i),
i
))
.collect();
let output = SearchOutput::new(results.clone(), total_count);
prop_assert_eq!(output.results.len(), result_count);
prop_assert_eq!(output.total_count, total_count);
}
}
proptest! {
#[test]
fn test_search_result_serialization_property(
title in "[a-zA-Z0-9 ]{1,50}",
url in "https: snippet in "[a-zA-Z0-9 ]{1,100}",
rank in 0usize..100usize
) {
let result = SearchResult::new(title.clone(), url.clone(), snippet.clone(), rank);
let json = serde_json::to_string(&result).unwrap();
let deserialized: SearchResult = serde_json::from_str(&json).unwrap();
prop_assert_eq!(deserialized.title, title);
prop_assert_eq!(deserialized.url, url);
prop_assert_eq!(deserialized.snippet, snippet);
prop_assert_eq!(deserialized.rank, rank);
}
}
proptest! {
#[test]
fn test_search_tool_empty_query_rejection_property(query in "[ \t\n]*") {
let result = SearchTool::validate_query(&query);
if query.trim().is_empty() {
prop_assert!(result.is_err());
}
}
}
proptest! {
#[test]
fn test_search_tool_long_query_rejection_property(query in "[a-zA-Z0-9 ]{1001,2000}") {
let result = SearchTool::validate_query(&query);
if query.len() > 1000 {
prop_assert!(result.is_err());
}
}
}
proptest! {
#[test]
fn test_search_tool_valid_query_acceptance_property(query in "[a-zA-Z0-9 ]{1,100}") {
let result = SearchTool::validate_query(&query);
if !query.trim().is_empty() && query.len() <= 1000 {
prop_assert!(result.is_ok());
}
}
}
proptest! {
#[test]
fn test_search_tool_sql_injection_rejection_property(
query in prop_oneof![
Just("test' UNION SELECT * FROM users"),
Just("test; DROP TABLE users"),
Just("test' OR '1'='1"),
Just("test\" OR \"1\"=\"1"),
]
) {
let result = SearchTool::validate_query(query);
prop_assert!(result.is_err());
}
}
proptest! {
#[test]
fn test_search_input_pagination_property(
limit in 1usize..100usize,
offset in 0usize..100usize
) {
let input = SearchInput::new("test")
.with_limit(limit)
.with_offset(offset);
prop_assert_eq!(input.get_limit(), limit.min(100));
prop_assert_eq!(input.get_offset(), offset);
}
}
proptest! {
#[test]
fn test_search_output_serialization_property(
result_count in 0usize..10usize,
total_count in 0usize..100usize
) {
let results: Vec<SearchResult> = (0..result_count)
.map(|i| SearchResult::new(
format!("Title {}", i),
format!("https://example.com/{}", i),
format!("Snippet {}", i),
i
))
.collect();
let output = SearchOutput::new(results, total_count);
let json = serde_json::to_string(&output).unwrap();
let deserialized: SearchOutput = serde_json::from_str(&json).unwrap();
prop_assert_eq!(deserialized.results.len(), result_count);
prop_assert_eq!(deserialized.total_count, total_count);
}
}