1use color_eyre::Result;
2use color_eyre::eyre::eyre;
3use rand::seq::{IndexedRandom, SliceRandom};
4use rand::{Rng, rng};
5#[allow(clippy::struct_excessive_bools)]
6#[derive(Debug, Clone)]
7pub struct PasswordConfig {
8 pub length: usize,
9 pub include_uppercase: bool,
10 pub include_lowercase: bool,
11 pub include_digits: bool,
12 pub include_symbols: bool,
13 pub exclude_ambiguous: bool,
14}
15
16impl Default for PasswordConfig {
17 fn default() -> Self {
18 Self {
19 length: 16,
20 include_uppercase: true,
21 include_lowercase: true,
22 include_digits: true,
23 include_symbols: true,
24 exclude_ambiguous: true,
25 }
26 }
27}
28
29impl PasswordConfig {
30 #[must_use]
31 pub fn new() -> Self {
32 Self::default()
33 }
34
35 #[must_use]
36 pub fn with_length(mut self, length: usize) -> Self {
37 self.length = length.clamp(4, 128); self
39 }
40
41 #[must_use]
42 pub const fn with_uppercase(mut self, include: bool) -> Self {
43 self.include_uppercase = include;
44 self
45 }
46
47 #[must_use]
48 pub const fn with_lowercase(mut self, include: bool) -> Self {
49 self.include_lowercase = include;
50 self
51 }
52
53 #[must_use]
54 pub const fn with_digits(mut self, include: bool) -> Self {
55 self.include_digits = include;
56 self
57 }
58
59 #[must_use]
60 pub const fn with_symbols(mut self, include: bool) -> Self {
61 self.include_symbols = include;
62 self
63 }
64
65 #[must_use]
66 pub const fn with_exclude_ambiguous(mut self, exclude: bool) -> Self {
67 self.exclude_ambiguous = exclude;
68 self
69 }
70
71 #[must_use]
73 pub const fn is_valid(&self) -> bool {
74 self.include_uppercase || self.include_lowercase || self.include_digits || self.include_symbols
75 }
76
77 fn get_character_sets(&self) -> Vec<&'static str> {
79 let mut sets = Vec::new();
80
81 if self.include_lowercase {
82 if self.exclude_ambiguous {
83 sets.push("abcdefghijkmnopqrstuvwxyz"); } else {
85 sets.push("abcdefghijklmnopqrstuvwxyz");
86 }
87 }
88
89 if self.include_uppercase {
90 if self.exclude_ambiguous {
91 sets.push("ABCDEFGHJKLMNPQRSTUVWXYZ"); } else {
93 sets.push("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
94 }
95 }
96
97 if self.include_digits {
98 if self.exclude_ambiguous {
99 sets.push("23456789"); } else {
101 sets.push("0123456789");
102 }
103 }
104
105 if self.include_symbols {
106 if self.exclude_ambiguous {
107 sets.push("!@#$%^&*+-=?"); } else {
109 sets.push("!@#$%^&*()_+-=[]{}|;:,.<>?");
110 }
111 }
112
113 sets
114 }
115
116 pub fn generate(&self) -> Result<String> {
144 if !self.is_valid() {
145 return Err(eyre!("At least one character set must be enabled"));
146 }
147
148 let character_sets = self.get_character_sets();
149 let all_chars: String = character_sets.join("");
150 let all_chars: Vec<char> = all_chars.chars().collect();
151
152 if all_chars.is_empty() {
153 return Err(eyre!("No valid characters available"));
154 }
155
156 let mut rng = rng();
157 let mut password = Vec::with_capacity(self.length);
158
159 for set in &character_sets {
161 let chars: Vec<char> = set.chars().collect();
162 if let Some(&ch) = chars.choose(&mut rng) {
163 password.push(ch);
164 }
165 }
166
167 while password.len() < self.length {
169 if let Some(&ch) = all_chars.choose(&mut rng) {
170 password.push(ch);
171 }
172 }
173
174 password.shuffle(&mut rng);
176
177 Ok(password.into_iter().collect())
178 }
179}
180
181pub fn generate_simple_password(length: usize) -> Result<String> {
206 PasswordConfig::new().with_length(length).with_symbols(false).generate()
207}
208
209pub fn generate_complex_password(length: usize) -> Result<String> {
237 PasswordConfig::new()
238 .with_length(length)
239 .with_exclude_ambiguous(false)
240 .generate()
241}
242
243#[must_use]
275pub fn generate_memorable_password() -> String {
276 let mut rng = rng();
277
278 let syllables = [
280 "ba", "be", "bi", "bo", "bu", "ca", "ce", "ci", "co", "cu", "da", "de", "di", "do", "du", "fa", "fe", "fi",
281 "fo", "fu", "ga", "ge", "gi", "go", "gu", "ha", "he", "hi", "ho", "hu", "ja", "je", "ji", "jo", "ju", "ka",
282 "ke", "ki", "ko", "ku", "la", "le", "li", "lo", "lu", "ma", "me", "mi", "mo", "mu", "na", "ne", "ni", "no",
283 "nu", "pa", "pe", "pi", "po", "pu", "ra", "re", "ri", "ro", "ru", "sa", "se", "si", "so", "su", "ta", "te",
284 "ti", "to", "tu", "va", "ve", "vi", "vo", "vu", "wa", "we", "wi", "wo", "wu", "ya", "ye", "yi", "yo", "yu",
285 "za", "ze", "zi", "zo", "zu",
286 ];
287
288 let mut password = String::new();
289
290 for i in 0..2 {
292 let word_length = rng.random_range(3..=4);
293 for j in 0..word_length {
294 if let Some(&syllable) = syllables.choose(&mut rng) {
295 if j == 0 && i == 0 {
296 password.push_str(&syllable.to_uppercase());
298 } else {
299 password.push_str(syllable);
300 }
301 }
302 }
303
304 if i == 0 {
306 #[allow(clippy::expect_used)]
307 password.push(
308 rng.random_range(0..=9)
309 .to_string()
310 .chars()
311 .next()
312 .expect("No ASCII digits"),
313 );
314 }
315 }
316
317 #[allow(clippy::expect_used)]
318 for _ in 0..2 {
320 password.push(
321 rng.random_range(0..=9)
322 .to_string()
323 .chars()
324 .next()
325 .expect("No ASCII digits"),
326 );
327 }
328
329 password
330}
331
332#[cfg(test)]
333mod tests {
334 #![allow(clippy::unwrap_used)]
335 use super::*;
336
337 #[test]
338 fn test_password_config_default() {
339 let config = PasswordConfig::default();
340 assert_eq!(config.length, 16);
341 assert!(config.include_uppercase);
342 assert!(config.include_lowercase);
343 assert!(config.include_digits);
344 assert!(config.include_symbols);
345 assert!(config.exclude_ambiguous);
346 }
347
348 #[test]
349 fn test_password_config_new() {
350 let config = PasswordConfig::new();
351 assert_eq!(config.length, 16);
352 assert!(config.include_uppercase);
353 assert!(config.include_lowercase);
354 assert!(config.include_digits);
355 assert!(config.include_symbols);
356 assert!(config.exclude_ambiguous);
357 }
358
359 #[test]
360 fn test_password_config_builder_methods() {
361 let config = PasswordConfig::new()
362 .with_length(24)
363 .with_uppercase(false)
364 .with_lowercase(true)
365 .with_digits(false)
366 .with_symbols(true)
367 .with_exclude_ambiguous(true);
368
369 assert_eq!(config.length, 24);
370 assert!(!config.include_uppercase);
371 assert!(config.include_lowercase);
372 assert!(!config.include_digits);
373 assert!(config.include_symbols);
374 assert!(config.exclude_ambiguous);
375 }
376
377 #[test]
378 fn test_generate_basic_password() {
379 let config = PasswordConfig::default();
380 let password = config.generate().unwrap();
381
382 assert_eq!(password.len(), 16);
383 assert!(!password.is_empty());
384 }
385
386 #[test]
387 fn test_generate_different_lengths() {
388 for length in [4, 8, 12, 16, 32, 64, 128] {
389 let config = PasswordConfig::new().with_length(length);
390 let password = config.generate().unwrap();
391 assert_eq!(password.len(), length);
392 }
393 }
394
395 #[test]
396 fn test_generate_minimum_length() {
397 let config = PasswordConfig::new().with_length(1);
398 let password = config.generate().unwrap();
399 assert_eq!(password.len(), 4);
400 }
401
402 #[test]
403 fn test_generate_maximum_length() {
404 let config = PasswordConfig::new().with_length(128);
405 let password = config.generate().unwrap();
406 assert_eq!(password.len(), 128);
407 }
408
409 #[test]
410 fn test_generate_uppercase_only() {
411 let config = PasswordConfig::new()
412 .with_length(20)
413 .with_uppercase(true)
414 .with_lowercase(false)
415 .with_digits(false)
416 .with_symbols(false);
417
418 let password = config.generate().unwrap();
419 assert_eq!(password.len(), 20);
420 assert!(password.chars().all(|c| c.is_ascii_uppercase()));
421 assert!(password.chars().any(|c| c.is_ascii_alphabetic()));
422 }
423
424 #[test]
425 fn test_generate_lowercase_only() {
426 let config = PasswordConfig::new()
427 .with_length(20)
428 .with_uppercase(false)
429 .with_lowercase(true)
430 .with_digits(false)
431 .with_symbols(false);
432
433 let password = config.generate().unwrap();
434 assert_eq!(password.len(), 20);
435 assert!(password.chars().all(|c| c.is_ascii_lowercase()));
436 assert!(password.chars().any(|c| c.is_ascii_alphabetic()));
437 }
438
439 #[test]
440 fn test_generate_digits_only() {
441 let config = PasswordConfig::new()
442 .with_length(20)
443 .with_uppercase(false)
444 .with_lowercase(false)
445 .with_digits(true)
446 .with_symbols(false);
447
448 let password = config.generate().unwrap();
449 assert_eq!(password.len(), 20);
450 assert!(password.chars().all(|c| c.is_ascii_digit()));
451 }
452
453 #[test]
454 fn test_generate_symbols_only() {
455 let config = PasswordConfig::new()
456 .with_length(20)
457 .with_uppercase(false)
458 .with_lowercase(false)
459 .with_digits(false)
460 .with_symbols(true);
461
462 let password = config.generate().unwrap();
463 assert_eq!(password.len(), 20);
464 assert!(password.chars().all(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c)));
465 }
466
467 #[test]
468 fn test_generate_mixed_character_sets() {
469 let config = PasswordConfig::new()
470 .with_length(100)
471 .with_uppercase(true)
472 .with_lowercase(true)
473 .with_digits(true)
474 .with_symbols(true);
475
476 let password = config.generate().unwrap();
477 assert_eq!(password.len(), 100);
478
479 assert!(password.chars().any(|c| c.is_ascii_uppercase()));
481 assert!(password.chars().any(|c| c.is_ascii_lowercase()));
482 assert!(password.chars().any(|c| c.is_ascii_digit()));
483 assert!(password.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c)));
484 }
485
486 #[test]
487 fn test_generate_exclude_ambiguous() {
488 let config = PasswordConfig::new().with_length(100).with_exclude_ambiguous(true);
489
490 let password = config.generate().unwrap();
491 assert_eq!(password.len(), 100);
492
493 let ambiguous = "0O1lI";
495 assert!(password.chars().all(|c| !ambiguous.contains(c)));
496 }
497
498 #[test]
499 fn test_generate_include_ambiguous() {
500 let config = PasswordConfig::new()
501 .with_length(128) .with_exclude_ambiguous(false);
503
504 let password = config.generate().unwrap();
505 assert_eq!(password.len(), 128);
506
507 let ambiguous = "0O1lI";
510 let has_ambiguous = password.chars().any(|c| ambiguous.contains(c));
511 assert!(has_ambiguous);
513 }
514
515 #[test]
516 fn test_generate_no_character_sets_enabled() {
517 let config = PasswordConfig::new()
518 .with_length(10)
519 .with_uppercase(false)
520 .with_lowercase(false)
521 .with_digits(false)
522 .with_symbols(false);
523
524 let result = config.generate();
525 assert!(result.is_err());
526 assert!(
527 result
528 .unwrap_err()
529 .to_string()
530 .contains("At least one character set must be enabled")
531 );
532 }
533
534 #[test]
535 fn test_generate_zero_length() {
536 let config = PasswordConfig::new().with_length(0);
537 let password = config.generate().unwrap();
538 assert_eq!(password.len(), 4);
539 assert!(!password.is_empty());
540 }
541
542 #[test]
543 fn test_generate_reproducible_with_different_configs() {
544 let config1 = PasswordConfig::new().with_length(16).with_symbols(false);
546 let config2 = PasswordConfig::new().with_length(16).with_symbols(true);
547
548 let password1 = config1.generate().unwrap();
549 let password2 = config2.generate().unwrap();
550
551 assert_ne!(password1, password2);
553 }
554
555 #[test]
556 fn test_generate_multiple_passwords_different() {
557 let config = PasswordConfig::new();
558
559 let password1 = config.generate().unwrap();
560 let password2 = config.generate().unwrap();
561 let password3 = config.generate().unwrap();
562
563 assert_ne!(password1, password2);
565 assert_ne!(password2, password3);
566 assert_ne!(password1, password3);
567 }
568
569 #[test]
570 fn test_simple_password() {
571 let password = generate_simple_password(12).unwrap();
572 assert_eq!(password.len(), 12);
573 assert!(!password.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c)));
574
575 assert!(password.chars().all(|c| c.is_ascii_alphanumeric()));
577 }
578
579 #[test]
580 fn test_simple_password_different_lengths() {
581 for length in [4, 8, 16, 32, 64] {
582 let password = generate_simple_password(length).unwrap();
583 assert_eq!(password.len(), length);
584 assert!(!password.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c)));
585 }
586 }
587
588 #[test]
589 fn test_simple_password_character_distribution() {
590 let password = generate_simple_password(100).unwrap();
592 assert_eq!(password.len(), 100);
593
594 let has_uppercase = password.chars().any(|c| c.is_ascii_uppercase());
595 let has_lowercase = password.chars().any(|c| c.is_ascii_lowercase());
596 let has_digits = password.chars().any(|c| c.is_ascii_digit());
597 let has_symbols = password.chars().any(|c| !c.is_ascii_alphanumeric());
598
599 assert!(has_uppercase || has_lowercase); assert!(has_digits); assert!(!has_symbols); }
604
605 #[test]
606 fn test_memorable_password_basic() {
607 let password = generate_memorable_password();
608
609 assert!(!password.is_empty());
610 assert!(password.len() >= 8); assert!(password.len() <= 20); assert!(password.chars().any(|c| c.is_ascii_alphabetic()));
615 assert!(password.chars().any(|c| c.is_ascii_digit()));
616 }
617
618 #[test]
619 fn test_memorable_password_format() {
620 let password = generate_memorable_password();
621
622 assert!(password.chars().next().unwrap().is_ascii_uppercase());
624
625 assert!(password.chars().any(|c| c.is_ascii_digit()));
627
628 assert!(password.chars().all(|c| c.is_ascii_alphanumeric()));
630 }
631
632 #[test]
633 fn test_memorable_password_uniqueness() {
634 let password1 = generate_memorable_password();
635 let password2 = generate_memorable_password();
636 let password3 = generate_memorable_password();
637
638 assert_ne!(password1, password2);
640 assert_ne!(password2, password3);
641 assert_ne!(password1, password3);
642 }
643
644 #[test]
645 fn test_memorable_password_syllable_structure() {
646 let password = generate_memorable_password();
647
648 let letters_only: String = password.chars().filter(char::is_ascii_alphabetic).collect();
650
651 assert!(letters_only.len() >= 6);
654 assert!(letters_only.len() <= 20); assert!(!letters_only.is_empty());
659 }
660
661 #[test]
662 fn test_password_consistency_over_multiple_generations() {
663 let config = PasswordConfig::new().with_length(20);
665
666 for _ in 0..100 {
667 let password = config.generate().unwrap();
668 assert_eq!(password.len(), 20);
669 assert!(password.is_ascii());
670 }
671 }
672
673 #[test]
674 fn test_extreme_configurations() {
675 let config = PasswordConfig::new()
679 .with_length(1)
680 .with_uppercase(true)
681 .with_lowercase(false)
682 .with_digits(false)
683 .with_symbols(false);
684
685 let password = config.generate().unwrap();
686 assert_eq!(password.len(), 4);
687 assert!(password.chars().next().unwrap().is_ascii_uppercase());
688 }
689
690 #[test]
691 fn test_character_set_boundaries() {
692 let config = PasswordConfig::new()
694 .with_length(10000) .with_exclude_ambiguous(false);
696
697 let password = config.generate().unwrap();
698
699 let has_upper = password.chars().any(|c| c.is_ascii_uppercase());
701 let has_lower = password.chars().any(|c| c.is_ascii_lowercase());
702 let has_digit = password.chars().any(|c| c.is_ascii_digit());
703 let has_symbol = password.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c));
704
705 assert!(has_upper);
706 assert!(has_lower);
707 assert!(has_digit);
708 assert!(has_symbol);
709 }
710
711 #[test]
712 fn test_complex_password() {
713 let password = generate_complex_password(16).unwrap();
714 assert_eq!(password.len(), 16);
715
716 let large_password = generate_complex_password(100).unwrap();
719 let ambiguous = "0O1lI";
720 let has_ambiguous = large_password.chars().any(|c| ambiguous.contains(c));
721 assert!(has_ambiguous, "Complex password should include ambiguous characters");
722 }
723
724 #[test]
725 fn test_complex_password_different_lengths() {
726 for length in [4, 8, 16, 32, 64] {
727 let password = generate_complex_password(length).unwrap();
728 assert_eq!(password.len(), length);
729 assert!(!password.is_empty());
730 }
731 }
732
733 #[test]
734 fn test_is_valid_method() {
735 let valid_configs = [
737 PasswordConfig::new(),
738 PasswordConfig::new()
739 .with_uppercase(true)
740 .with_lowercase(false)
741 .with_digits(false)
742 .with_symbols(false),
743 PasswordConfig::new()
744 .with_uppercase(false)
745 .with_lowercase(true)
746 .with_digits(false)
747 .with_symbols(false),
748 PasswordConfig::new()
749 .with_uppercase(false)
750 .with_lowercase(false)
751 .with_digits(true)
752 .with_symbols(false),
753 PasswordConfig::new()
754 .with_uppercase(false)
755 .with_lowercase(false)
756 .with_digits(false)
757 .with_symbols(true),
758 ];
759
760 for config in valid_configs {
761 assert!(config.is_valid(), "Configuration should be valid: {config:?}");
762 }
763
764 let invalid_config = PasswordConfig::new()
766 .with_uppercase(false)
767 .with_lowercase(false)
768 .with_digits(false)
769 .with_symbols(false);
770 assert!(
771 !invalid_config.is_valid(),
772 "Configuration with no character sets should be invalid"
773 );
774 }
775
776 #[test]
777 fn test_length_clamping_upper_bound() {
778 let config = PasswordConfig::new().with_length(200); let password = config.generate().unwrap();
780 assert_eq!(password.len(), 128); }
782
783 #[test]
784 fn test_length_clamping_comprehensive() {
785 let test_cases = [
786 (0, 4), (1, 4), (3, 4), (4, 4), (10, 10), (64, 64), (128, 128), (150, 128), (1000, 128), ];
796
797 for (input, expected) in test_cases {
798 let config = PasswordConfig::new().with_length(input);
799 assert_eq!(
800 config.length, expected,
801 "Length {input} should be clamped to {expected}"
802 );
803
804 let password = config.generate().unwrap();
805 assert_eq!(password.len(), expected);
806 }
807 }
808
809 #[test]
810 fn test_builder_method_chaining_comprehensive() {
811 let config = PasswordConfig::new()
812 .with_length(24)
813 .with_uppercase(false)
814 .with_lowercase(true)
815 .with_digits(true)
816 .with_symbols(false)
817 .with_exclude_ambiguous(false);
818
819 let password = config.generate().unwrap();
820 assert_eq!(password.len(), 24);
821
822 assert!(password.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()));
824 assert!(password.chars().any(|c| c.is_ascii_lowercase()));
825 assert!(password.chars().any(|c| c.is_ascii_digit()));
826 assert!(!password.chars().any(|c| c.is_ascii_uppercase()));
827 assert!(!password.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c)));
828 }
829
830 #[test]
831 fn test_builder_method_overriding() {
832 let config = PasswordConfig::new()
833 .with_length(10)
834 .with_length(20) .with_symbols(true)
836 .with_symbols(false); let password = config.generate().unwrap();
839 assert_eq!(password.len(), 20); assert!(!password.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c))); }
842
843 #[test]
844 fn test_specific_ambiguous_character_exclusion() {
845 let config = PasswordConfig::new()
846 .with_length(200) .with_exclude_ambiguous(true);
848
849 let password = config.generate().unwrap();
850
851 let ambiguous_chars = ['0', 'O', '1', 'l', 'I'];
853 for ch in ambiguous_chars {
854 assert!(
855 !password.contains(ch),
856 "Password should not contain ambiguous character '{ch}'"
857 );
858 }
859 }
860
861 #[test]
862 fn test_specific_ambiguous_character_inclusion() {
863 let config = PasswordConfig::new()
864 .with_length(500) .with_exclude_ambiguous(false);
866
867 let password = config.generate().unwrap();
868
869 let ambiguous_chars = ['0', 'O', '1', 'l', 'I'];
871 let has_any_ambiguous = ambiguous_chars.iter().any(|&ch| password.contains(ch));
872 assert!(
873 has_any_ambiguous,
874 "Large password should contain at least some ambiguous characters"
875 );
876 }
877
878 #[test]
879 fn test_character_set_exact_contents() {
880 let test_cases = [
882 (
884 PasswordConfig::new()
885 .with_uppercase(true)
886 .with_lowercase(false)
887 .with_digits(false)
888 .with_symbols(false)
889 .with_exclude_ambiguous(true),
890 "ABCDEFGHJKLMNPQRSTUVWXYZ", "IO0123456789!@#$%^&*()_+-=[]{}|;:,.<>?abcdefghijklmnopqrstuvwxyz",
892 ),
893 (
894 PasswordConfig::new()
895 .with_uppercase(false)
896 .with_lowercase(true)
897 .with_digits(false)
898 .with_symbols(false)
899 .with_exclude_ambiguous(true),
900 "abcdefghijkmnopqrstuvwxyz", "lIO0123456789!@#$%^&*()_+-=[]{}|;:,.<>?ABCDEFGHIJKLMNOPQRSTUVWXYZ",
902 ),
903 (
904 PasswordConfig::new()
905 .with_uppercase(false)
906 .with_lowercase(false)
907 .with_digits(true)
908 .with_symbols(false)
909 .with_exclude_ambiguous(true),
910 "23456789", "01lIOi!@#$%^&*()_+-=[]{}|;:,.<>?ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
912 ),
913 (
914 PasswordConfig::new()
915 .with_uppercase(false)
916 .with_lowercase(false)
917 .with_digits(false)
918 .with_symbols(true)
919 .with_exclude_ambiguous(true),
920 "!@#$%^&*+-=?", "()_[]{}|;:,.<>lIO0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
922 ),
923 ];
924
925 for (config, expected_chars, forbidden_chars) in test_cases {
926 let password = config.generate().unwrap();
927
928 for ch in password.chars() {
930 assert!(
931 expected_chars.contains(ch),
932 "Character '{ch}' should be in expected set '{expected_chars}'"
933 );
934 assert!(
935 !forbidden_chars.contains(ch),
936 "Character '{ch}' should not be in forbidden set"
937 );
938 }
939 }
940 }
941
942 #[test]
943 fn test_minimum_character_requirements() {
944 let config = PasswordConfig::new()
946 .with_length(20)
947 .with_uppercase(true)
948 .with_lowercase(true)
949 .with_digits(true)
950 .with_symbols(true);
951
952 for _ in 0..10 {
954 let password = config.generate().unwrap();
955
956 assert!(
957 password.chars().any(|c| c.is_ascii_uppercase()),
958 "Password should contain at least one uppercase letter"
959 );
960 assert!(
961 password.chars().any(|c| c.is_ascii_lowercase()),
962 "Password should contain at least one lowercase letter"
963 );
964 assert!(
965 password.chars().any(|c| c.is_ascii_digit()),
966 "Password should contain at least one digit"
967 );
968 assert!(
969 password.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c)),
970 "Password should contain at least one symbol"
971 );
972 }
973 }
974
975 #[test]
976 fn test_memorable_password_syllable_count() {
977 let password = generate_memorable_password();
978
979 let letters_only: String = password.chars().filter(char::is_ascii_alphabetic).collect();
981
982 assert!(
984 letters_only.len() >= 12,
985 "Should have at least 12 letters from syllables, got {}",
986 letters_only.len()
987 );
988 assert!(
989 letters_only.len() <= 16,
990 "Should have at most 16 letters from syllables, got {}",
991 letters_only.len()
992 );
993 }
994
995 #[test]
996 fn test_memorable_password_digit_count() {
997 let password = generate_memorable_password();
998 let digit_count = password.chars().filter(char::is_ascii_digit).count();
999
1000 assert_eq!(digit_count, 3, "Memorable password should have exactly 3 digits");
1002 }
1003
1004 #[test]
1005 fn test_memorable_password_capitalization_pattern() {
1006 for _ in 0..10 {
1007 let password = generate_memorable_password();
1008
1009 assert!(
1011 password.chars().next().unwrap().is_ascii_uppercase(),
1012 "First character should be uppercase"
1013 );
1014
1015 if let Some(separator_pos) = password.chars().position(|c| c.is_ascii_digit()) {
1017 let chars: Vec<char> = password.chars().collect();
1018
1019 let mut first_syllable_chars = 0;
1020 for (i, &ch) in chars.iter().enumerate() {
1021 if i >= separator_pos {
1022 break;
1023 }
1024 if ch.is_ascii_alphabetic() {
1025 first_syllable_chars += 1;
1026 if first_syllable_chars <= 2 {
1027 assert!(
1028 ch.is_ascii_uppercase(),
1029 "Character '{ch}' at position {i} in first syllable should be uppercase"
1030 );
1031 } else {
1032 assert!(
1033 ch.is_ascii_lowercase(),
1034 "Character '{ch}' at position {i} should be lowercase (after first syllable)"
1035 );
1036 }
1037 }
1038 }
1039 }
1040 }
1041 }
1042
1043 #[test]
1044 fn test_memorable_password_structure_consistency() {
1045 for _ in 0..20 {
1047 let password = generate_memorable_password();
1048 let chars: Vec<char> = password.chars().collect();
1049
1050 assert!(chars[0].is_ascii_uppercase());
1052
1053 let len = chars.len();
1055 assert!(chars[len - 1].is_ascii_digit(), "Should end with digit");
1056 assert!(chars[len - 2].is_ascii_digit(), "Second to last should be digit");
1057
1058 let digit_count = password.chars().filter(char::is_ascii_digit).count();
1060 assert_eq!(digit_count, 3, "Should have exactly 3 digits");
1061 }
1062 }
1063
1064 #[test]
1065 fn test_performance_large_passwords() {
1066 use std::time::Instant;
1067
1068 let start = Instant::now();
1069 let config = PasswordConfig::new().with_length(128);
1070
1071 for _ in 0..100 {
1072 let password = config.generate().unwrap();
1073 assert_eq!(password.len(), 128);
1074 }
1075
1076 let duration = start.elapsed();
1077 assert!(
1078 duration.as_secs() < 5,
1079 "Password generation should complete within 5 seconds"
1080 );
1081 }
1082
1083 #[test]
1084 fn test_character_distribution_balance() {
1085 let config = PasswordConfig::new().with_length(400); let password = config.generate().unwrap();
1087
1088 let uppercase_count = password.chars().filter(char::is_ascii_uppercase).count();
1089 let lowercase_count = password.chars().filter(char::is_ascii_lowercase).count();
1090 let digit_count = password.chars().filter(char::is_ascii_digit).count();
1091 let symbol_count = password
1092 .chars()
1093 .filter(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(*c))
1094 .count();
1095
1096 assert!(
1099 uppercase_count >= 20,
1100 "Should have at least 20 uppercase chars, got {uppercase_count}"
1101 );
1102 assert!(
1103 lowercase_count >= 20,
1104 "Should have at least 20 lowercase chars, got {lowercase_count}"
1105 );
1106 assert!(digit_count >= 5, "Should have at least 5 digits, got {digit_count}");
1107 assert!(symbol_count >= 5, "Should have at least 5 symbols, got {symbol_count}");
1108
1109 assert!(uppercase_count < 320, "Uppercase shouldn't dominate");
1111 assert!(lowercase_count < 320, "Lowercase shouldn't dominate");
1112 assert!(digit_count < 320, "Digits shouldn't dominate");
1113 assert!(symbol_count < 320, "Symbols shouldn't dominate");
1114 }
1115
1116 #[test]
1117 fn test_all_builder_methods_together() {
1118 let config = PasswordConfig::new()
1119 .with_length(32)
1120 .with_uppercase(true)
1121 .with_lowercase(true)
1122 .with_digits(true)
1123 .with_symbols(true)
1124 .with_exclude_ambiguous(true);
1125
1126 let password = config.generate().unwrap();
1127 assert_eq!(password.len(), 32);
1128 assert_eq!(config.length, 32);
1129 assert!(config.include_uppercase);
1130 assert!(config.include_lowercase);
1131 assert!(config.include_digits);
1132 assert!(config.include_symbols);
1133 assert!(config.exclude_ambiguous);
1134
1135 assert!(password.chars().any(|c| c.is_ascii_uppercase()));
1137 assert!(password.chars().any(|c| c.is_ascii_lowercase()));
1138 assert!(password.chars().any(|c| c.is_ascii_digit()));
1139 assert!(password.chars().any(|c| "!@#$%^&*+-=?".contains(c))); let ambiguous = "0O1lI";
1143 assert!(!password.chars().any(|c| ambiguous.contains(c)));
1144 }
1145
1146 #[test]
1147 fn test_password_entropy_distribution() {
1148 let config = PasswordConfig::new().with_length(16);
1150 let mut passwords = std::collections::HashSet::new();
1151
1152 for _ in 0..100 {
1154 let password = config.generate().unwrap();
1155 passwords.insert(password);
1156 }
1157
1158 assert!(
1160 passwords.len() >= 95,
1161 "Should generate mostly unique passwords, got {} unique out of 100",
1162 passwords.len()
1163 );
1164 }
1165
1166 #[test]
1167 fn test_edge_case_single_character_type_minimum_length() {
1168 let config = PasswordConfig::new()
1169 .with_length(4) .with_uppercase(true)
1171 .with_lowercase(false)
1172 .with_digits(false)
1173 .with_symbols(false);
1174
1175 let password = config.generate().unwrap();
1176 assert_eq!(password.len(), 4);
1177 assert!(password.chars().all(|c| c.is_ascii_uppercase()));
1178 assert!(password.chars().any(|c| c.is_ascii_uppercase())); }
1180}