//! Generator integration tests — unbiased sampling (smoke), BIP39 sanity, //! zxcvbn strength gate. //! //! # Note on length cap //! //! `generate_password` enforces `length <= 128`. The task originally specified //! `length: 10_000` in a single call, but that would error at runtime. //! //! We use **Option 1 (aggregation)**: call `generate_password` 80 times with //! `length: 128` to gather 10,240 characters total, then aggregate per-class //! counts before asserting proportions. The ±5pp tolerance is unchanged because //! sample size is the same (~10k chars). use relicario_core::{ Capitalization, CharClasses, GeneratorRequest, SymbolCharset, generate_passphrase, generate_password, validate_passphrase_strength, }; #[test] fn random_password_class_balance_is_reasonable() { // Aggregate 80 × 128 = 10,240 chars so we have enough for tight statistics. // (generate_password caps at length 128, so we cannot do a single 10,000-char call.) let req = GeneratorRequest::Random { length: 128, classes: CharClasses { lower: true, upper: true, digits: true, symbols: true }, symbol_charset: SymbolCharset::SafeOnly, }; let mut lower = 0usize; let mut upper = 0usize; let mut digits = 0usize; let mut total = 0usize; for _ in 0..80 { let pw = generate_password(&req).unwrap(); lower += pw.chars().filter(|c| c.is_ascii_lowercase()).count(); upper += pw.chars().filter(|c| c.is_ascii_uppercase()).count(); digits += pw.chars().filter(|c| c.is_ascii_digit()).count(); total += pw.len(); } let symbols = total - lower - upper - digits; // Charset sizes: lower 26 + upper 26 + digits 10 + safe_symbols 12 = 74 // Expected proportions: 26/74 ≈ 35.1%, 10/74 ≈ 13.5%, 12/74 ≈ 16.2% // Allow ±5pp slop. let t = total as f64; let assert_pct = |label: &str, actual: usize, expected_pct: f64| { let pct = (actual as f64) / t * 100.0; assert!( (pct - expected_pct).abs() < 5.0, "{label}: actual {pct:.1}% vs expected {expected_pct:.1}%" ); }; assert_pct("lower", lower, 26.0 / 74.0 * 100.0); assert_pct("upper", upper, 26.0 / 74.0 * 100.0); assert_pct("digits", digits, 10.0 / 74.0 * 100.0); assert_pct("symbols", symbols, 12.0 / 74.0 * 100.0); } #[test] fn bip39_5_word_passphrase_passes_zxcvbn_gate() { let req = GeneratorRequest::Bip39 { word_count: 5, separator: " ".into(), capitalization: Capitalization::Lower, }; let pw = generate_passphrase(&req).unwrap(); validate_passphrase_strength(&pw).expect("5-word bip39 should pass score >= 3"); } #[test] fn common_weak_passphrases_fail_gate() { for weak in &["password", "12345678", "letmein", "qwertyui", "hunter2"] { assert!( validate_passphrase_strength(weak).is_err(), "expected '{weak}' to fail gate" ); } } #[test] fn random_passwords_are_unique_across_calls() { let req = GeneratorRequest::default(); let mut seen = std::collections::HashSet::new(); for _ in 0..1000 { let pw = generate_password(&req).unwrap(); assert!(seen.insert(pw.as_str().to_owned())); } }