//! Property-based tests for deterministic behavior. //! //! Uses proptest to verify that hashing, JSON serialization, and spec construction //! produce deterministic results regardless of input data. use proptest::prelude::*; use serde_json::json; use sha2::{Digest, Sha256}; // The SHA-256 hash function is deterministic: same bytes always produce the same hash. proptest! { #[test] fn hash_deterministic(data in proptest::collection::vec(any::(), 0..10000)) { let mut h1 = Sha256::new(); h1.update(&data); let r1 = format!("{:x}", h1.finalize()); let mut h2 = Sha256::new(); h2.update(&data); let r2 = format!("{:x}", h2.finalize()); prop_assert_eq!(r1, r2); } } // Two specs with the same paths (inserted in different order) should produce // identical canonical JSON after a parse-serialize roundtrip through serde_json::Value. // // serde_json::Value uses BTreeMap for objects, so key ordering is deterministic // regardless of insertion order. proptest! { #![proptest_config(ProptestConfig::with_cases(20))] #[test] fn index_ordering_deterministic( // Use proptest to pick a permutation index perm_seed in 0..40320u32, // 8! = 40320 ) { let paths: Vec<(&str, &str, &str)> = vec![ ("/pets", "get", "List pets"), ("/pets", "post", "Create pet"), ("/pets/{petId}", "get", "Get pet"), ("/pets/{petId}", "delete", "Delete pet"), ("/users", "get", "List users"), ("/users/{userId}", "get", "Get user"), ("/orders", "get", "List orders"), ("/orders", "post", "Create order"), ]; // Build the spec with original order let spec1 = build_spec_from_paths(&paths); // Generate a deterministic permutation from the seed let shuffled = permute(&paths, perm_seed); let spec2 = build_spec_from_paths(&shuffled); // After roundtripping through serde_json::Value (BTreeMap ordering), // both should produce identical canonical JSON. let val1: serde_json::Value = serde_json::from_str(&serde_json::to_string(&spec1).unwrap()).unwrap(); let val2: serde_json::Value = serde_json::from_str(&serde_json::to_string(&spec2).unwrap()).unwrap(); let canonical1 = serde_json::to_string(&val1).unwrap(); let canonical2 = serde_json::to_string(&val2).unwrap(); prop_assert_eq!(canonical1, canonical2); } } /// Deterministic permutation using factorial number system (no rand dependency). fn permute(items: &[T], mut seed: u32) -> Vec { let mut remaining: Vec = items.to_vec(); let mut result = Vec::with_capacity(items.len()); for i in (1..=items.len()).rev() { let idx = (seed % i as u32) as usize; seed /= i as u32; result.push(remaining.remove(idx)); } result } /// Build a minimal OpenAPI spec from a list of (path, method, summary) tuples. fn build_spec_from_paths(paths: &[(&str, &str, &str)]) -> serde_json::Value { let mut path_map = serde_json::Map::new(); for (path, method, summary) in paths { let path_entry = path_map .entry(path.to_string()) .or_insert_with(|| json!({})); path_entry[method] = json!({ "summary": summary, "responses": { "200": { "description": "OK" } } }); } json!({ "openapi": "3.0.3", "info": { "title": "Test", "version": "1.0.0" }, "paths": path_map }) } // Verify that JSON canonical form is stable across parse-serialize cycles. proptest! { #[test] fn json_roundtrip_stable( key1 in "[a-z]{1,10}", key2 in "[a-z]{1,10}", val1 in any::(), val2 in any::(), ) { let obj = json!({ key1.clone(): val1, key2.clone(): val2 }); let s1 = serde_json::to_string(&obj).unwrap(); let reparsed: serde_json::Value = serde_json::from_str(&s1).unwrap(); let s2 = serde_json::to_string(&reparsed).unwrap(); // Parse -> serialize should be idempotent prop_assert_eq!(s1, s2); } } // Hash output format is always "sha256:" followed by 64 hex chars. proptest! { #[test] fn hash_format_consistent(data in proptest::collection::vec(any::(), 0..1000)) { let mut hasher = Sha256::new(); hasher.update(&data); let hash = format!("sha256:{:x}", hasher.finalize()); prop_assert!(hash.starts_with("sha256:")); // SHA-256 hex is 64 chars prop_assert_eq!(hash.len(), 7 + 64); // "sha256:" + 64 hex digits } }