#[cfg(test)]
mod property_tests {
use icookforms::parser::parse_set_cookie;
use icookforms::storage::Storage; use icookforms::types::*;
use icookforms::ReportFormat;
use proptest::prelude::*;
proptest! {
#[test]
fn parse_serialize_roundtrip(
name in "[a-zA-Z0-9_-]{1,50}",
value in "[a-zA-Z0-9_-]{1,100}"
) {
let original = format!("{}={}", name, value);
let parsed = parse_set_cookie(&original, false);
if let Ok(cookie) = parsed {
assert_eq!(cookie.name, name);
assert_eq!(cookie.value, value);
let serialized = cookie.to_string();
assert!(serialized.contains(&name));
assert!(serialized.contains(&value));
}
}
}
proptest! {
#[test]
fn parse_with_attributes_preserves_data(
name in "[a-zA-Z0-9_-]{1,50}",
value in "[a-zA-Z0-9_-]{1,100}",
secure in any::<bool>(),
http_only in any::<bool>()
) {
let cookie_str = format!(
"{}={}{}{}",
name,
value,
if secure { "; Secure" } else { "" },
if http_only { "; HttpOnly" } else { "" }
);
let parsed = parse_set_cookie(&cookie_str, false);
if let Ok(cookie) = parsed {
assert_eq!(cookie.name, name);
assert_eq!(cookie.value, value);
assert_eq!(cookie.secure, secure);
assert_eq!(cookie.http_only, http_only);
}
}
}
proptest! {
#[test]
fn parser_handles_any_string_gracefully(
input in ".*"
) {
let result = std::panic::catch_unwind(|| {
parse_set_cookie(&input, false)
});
assert!(result.is_ok(), "Parser should never panic on any input");
}
}
proptest! {
#[test]
fn parser_lenient_mode_accepts_more(
name in "[a-zA-Z0-9_-]{1,50}",
value in ".*"
) {
let cookie_str = format!("{}={}", name, value);
let strict_result = parse_set_cookie(&cookie_str, true);
let lenient_result = parse_set_cookie(&cookie_str, false);
if strict_result.is_ok() {
assert!(lenient_result.is_ok(), "Lenient mode should succeed when strict does");
}
}
}
proptest! {
#[test]
fn cookie_attributes_preserved(
name in "[a-zA-Z0-9_-]{1,50}",
value in "[a-zA-Z0-9_-]{1,100}",
domain in prop::option::of("[a-z]{3,10}\\.[a-z]{2,5}"),
path in prop::option::of("/[a-z/]{0,20}"),
max_age in prop::option::of(0i64..31536000),
secure in any::<bool>(),
http_only in any::<bool>()
) {
let mut cookie = Cookie::new(name.clone(), value.clone());
cookie.domain = domain.clone();
cookie.path = path.clone();
cookie.max_age = max_age;
cookie.secure = secure;
cookie.http_only = http_only;
assert_eq!(cookie.name, name);
assert_eq!(cookie.value, value);
assert_eq!(cookie.domain, domain);
assert_eq!(cookie.path, path);
assert_eq!(cookie.max_age, max_age);
assert_eq!(cookie.secure, secure);
assert_eq!(cookie.http_only, http_only);
}
}
proptest! {
#[test]
fn base64_roundtrip(
data in prop::collection::vec(any::<u8>(), 0..1000)
) {
use icookforms::utils::crypto::{base64_encode, base64_decode};
let encoded = base64_encode(&data);
let decoded = base64_decode(&encoded);
assert!(decoded.is_ok());
assert_eq!(decoded.unwrap(), data);
}
}
proptest! {
#[test]
fn hmac_consistent(
key in prop::collection::vec(any::<u8>(), 16..64),
message in prop::collection::vec(any::<u8>(), 0..1000)
) {
use icookforms::utils::crypto::hmac_sha256;
let result1 = hmac_sha256(&key, &message);
let result2 = hmac_sha256(&key, &message);
assert!(result1.is_ok());
assert!(result2.is_ok());
assert_eq!(result1.unwrap(), result2.unwrap());
}
}
proptest! {
#[test]
fn analyzer_never_crashes(
name in "[a-zA-Z0-9_-]{1,50}",
value in ".*",
secure in any::<bool>(),
http_only in any::<bool>()
) {
use icookforms::analyzer::Analyzer;
let mut cookie = Cookie::new(name, value);
cookie.secure = secure;
cookie.http_only = http_only;
let analyzer = Analyzer::new();
let result = std::panic::catch_unwind(|| {
analyzer.analyze(&cookie)
});
assert!(result.is_ok(), "Analyzer should never panic");
}
}
proptest! {
#[test]
fn compliance_check_deterministic(
name in "[a-zA-Z0-9_-]{1,50}",
value in "[a-zA-Z0-9_-]{1,100}",
secure in any::<bool>(),
http_only in any::<bool>()
) {
use icookforms::compliance::ComplianceChecker;
let mut cookie = Cookie::new(name, value);
cookie.secure = secure;
cookie.http_only = http_only;
let checker = ComplianceChecker::new();
let result1 = checker.check(&cookie, Regulation::GDPR);
let result2 = checker.check(&cookie, Regulation::GDPR);
assert_eq!(result1.compliant, result2.compliant);
assert_eq!(result1.issues.len(), result2.issues.len());
}
}
proptest! {
#[test]
fn cache_operations_consistent(
entries in prop::collection::vec(
("[a-zA-Z0-9_-]{1,20}", "[a-zA-Z0-9_-]{1,50}"),
1..100
)
) {
use icookforms::storage::cache::Cache;
use std::collections::HashMap;
let cache = Cache::new(3600, 1000);
let mut expected: HashMap<String, String> = HashMap::new();
for (key, value) in &entries {
expected.insert(key.clone(), value.clone());
let _ = cache.set(key.clone(), value.clone());
}
for (key, expected_value) in &expected {
let retrieved = cache.get(key);
assert_eq!(retrieved, Some(expected_value.clone()), "Cache should preserve inserted values");
}
}
}
proptest! {
#[test]
fn json_report_always_valid(
scan_id in "[a-z0-9-]{10,50}"
) {
use icookforms::reporter::Reporter;
let analysis = AnalysisResult::new(&scan_id);
let reporter = Reporter::new(ReportFormat::Json);
let result = reporter.generate_string(&analysis);
assert!(result.is_ok(), "JSON generation should always succeed");
let json_str = result.unwrap();
let parsed: serde_json::Result<serde_json::Value> = serde_json::from_str(&json_str);
assert!(parsed.is_ok(), "Generated JSON should be valid");
}
}
proptest! {
#[test]
fn csv_report_has_valid_structure(
scan_id in "[a-z0-9-]{10,50}"
) {
use icookforms::reporter::Reporter;
let analysis = AnalysisResult::new(&scan_id);
let reporter = Reporter::new(ReportFormat::Csv);
let result = reporter.generate_string(&analysis);
assert!(result.is_ok());
let csv_str = result.unwrap();
let lines: Vec<_> = csv_str.lines().collect();
assert!(lines.len() >= 1, "CSV should have at least header row");
}
}
proptest! {
#[test]
fn error_messages_non_empty(
error_msg in ".{1,200}"
) {
use icookforms::types::Error;
let errors = vec![
Error::config(error_msg.clone()),
Error::scanner(error_msg.clone()),
Error::analysis(error_msg.clone()),
];
for error in errors {
let displayed = error.to_string();
assert!(!displayed.is_empty(), "Error message should not be empty");
}
}
}
proptest! {
#[test]
fn database_save_load_consistent(
url in "https?: ) {
use icookforms::storage::database::DatabaseStorage;
let db = DatabaseStorage::in_memory();
if db.is_err() {
return Ok(());
}
let db = db.unwrap();
let scan = ScanResult::new(&url);
let save_result = db.save_scan(&scan);
assert!(save_result.is_ok());
let load_result = db.load_scan(&scan.id);
assert!(load_result.is_ok());
let loaded = load_result.unwrap();
assert_eq!(loaded.url, url);
}
}
}