use rusty_pwgen::{CompatibilityMode, Error, Pwgen, PwgenBuilder};
#[test]
fn builder_default_yields_8char_pronounceable() {
let mut pwgen = PwgenBuilder::new().build().expect("default builder ok");
let pw = pwgen.generate_one();
assert_eq!(pw.len(), 8);
assert!(pw.is_ascii());
}
#[test]
fn builder_length_count_secure_symbols() {
let mut pwgen = PwgenBuilder::new()
.length(16)
.secure(true)
.symbols(true)
.build()
.expect("builder ok");
let pw = pwgen.generate_one();
assert_eq!(pw.len(), 16);
assert!(pw.is_ascii());
}
#[test]
fn generate_n_returns_correct_count() {
let mut pwgen = PwgenBuilder::new()
.length(12)
.secure(true)
.build()
.expect("builder ok");
let batch = pwgen.generate_n(5);
assert_eq!(batch.len(), 5);
for pw in &batch {
assert_eq!(pw.len(), 12);
}
}
#[test]
fn iter_streams_passwords_without_panic() {
let mut pwgen = PwgenBuilder::new()
.length(8)
.secure(true)
.build()
.expect("builder ok");
let count = pwgen.iter().take(1000).count();
assert_eq!(count, 1000);
}
#[test]
fn substitutability_of_three_methods_under_seeded_rng() {
let seed_bytes = b"shared-seed".to_vec();
let mut p1 = PwgenBuilder::new()
.length(12)
.secure(true)
.reproducible_seed(seed_bytes.clone())
.build()
.unwrap();
let one_shot: Vec<String> = (0..5).map(|_| p1.generate_one()).collect();
let mut p2 = PwgenBuilder::new()
.length(12)
.secure(true)
.reproducible_seed(seed_bytes.clone())
.build()
.unwrap();
let batch = p2.generate_n(5);
let mut p3 = PwgenBuilder::new()
.length(12)
.secure(true)
.reproducible_seed(seed_bytes.clone())
.build()
.unwrap();
let streamed: Vec<String> = p3.iter().take(5).collect();
assert_eq!(one_shot, batch, "generate_one × N must equal generate_n(N)");
assert_eq!(
batch, streamed,
"generate_n(N) must equal iter().take(N).collect()"
);
}
#[test]
fn no_vowels_silently_implies_secure() {
let mut pwgen = PwgenBuilder::new()
.length(16)
.no_vowels(true)
.build()
.expect("builder ok");
let pw = pwgen.generate_one();
for byte in pw.bytes() {
assert!(
!b"aeiouAEIOU".contains(&byte),
"vowel '{}' in no-vowels output",
byte as char
);
}
}
#[test]
fn length_below_5_silently_engages_secure() {
let mut pwgen = PwgenBuilder::new().length(3).build().expect("builder ok");
let pw = pwgen.generate_one();
assert_eq!(pw.len(), 3);
}
#[test]
fn length_zero_returns_empty_strings() {
let mut pwgen = PwgenBuilder::new()
.length(0)
.secure(true)
.build()
.expect("zero length is valid");
let pw = pwgen.generate_one();
assert_eq!(pw, "");
}
#[test]
fn remove_chars_implies_secure() {
let mut pwgen = PwgenBuilder::new()
.length(12)
.remove_chars("abcdef")
.build()
.expect("builder ok");
let pw = pwgen.generate_one();
for byte in pw.bytes() {
assert!(
!b"abcdef".contains(&byte),
"removed char '{}' should be absent",
byte as char
);
}
}
#[test]
fn ambiguous_filter_works_in_library() {
let mut pwgen = PwgenBuilder::new()
.length(16)
.secure(true)
.ambiguous_filter(true)
.build()
.expect("builder ok");
let batch = pwgen.generate_n(20);
for pw in &batch {
for byte in pw.bytes() {
assert!(!b"l1O0I".contains(&byte));
}
}
}
#[test]
fn reproducible_seed_yields_same_passwords_across_builds() {
let seed_a = b"fixture-seed-42".to_vec();
let mut a = PwgenBuilder::new()
.length(16)
.secure(true)
.reproducible_seed(seed_a.clone())
.build()
.unwrap();
let mut b = PwgenBuilder::new()
.length(16)
.secure(true)
.reproducible_seed(seed_a)
.build()
.unwrap();
let a_pw = a.generate_n(10);
let b_pw = b.generate_n(10);
assert_eq!(a_pw, b_pw);
}
#[test]
fn reproducible_seed_different_inputs_differ() {
let mut a = PwgenBuilder::new()
.length(16)
.secure(true)
.reproducible_seed(b"alpha".to_vec())
.build()
.unwrap();
let mut b = PwgenBuilder::new()
.length(16)
.secure(true)
.reproducible_seed(b"beta".to_vec())
.build()
.unwrap();
let a_pw = a.generate_n(5);
let b_pw = b.generate_n(5);
assert_ne!(a_pw, b_pw, "different seeds → different output");
}
#[test]
fn empty_charset_after_filters_yields_error() {
let all_alphanumeric: Vec<u8> = (b'a'..=b'z')
.chain(b'A'..=b'Z')
.chain(b'0'..=b'9')
.collect();
let result = PwgenBuilder::new()
.length(8)
.remove_chars(String::from_utf8(all_alphanumeric).unwrap())
.build();
assert!(
matches!(result, Err(Error::InvalidBuilderConfiguration(_))),
"empty charset should error; got {result:?}"
);
}
#[test]
fn send_sync_compile_time_assertions() {
use static_assertions::{assert_impl_all, assert_not_impl_any};
assert_impl_all!(Pwgen: Send);
assert_not_impl_any!(Pwgen: Sync);
assert_impl_all!(PwgenBuilder: Send, Sync);
assert_impl_all!(CompatibilityMode: Send, Sync, Copy);
assert_impl_all!(Error: Send, Sync);
}
#[test]
fn default_features_off_excludes_cli_deps() {
let output = std::process::Command::new("cargo")
.args(["tree", "--no-default-features", "--prefix", "none"])
.current_dir(env!("CARGO_MANIFEST_DIR"))
.output()
.expect("cargo tree should run");
assert!(output.status.success());
let tree = String::from_utf8_lossy(&output.stdout);
for forbidden in ["clap ", "clap_complete", "anyhow ", "terminal_size"] {
assert!(
!tree.contains(forbidden),
"no-default-features tree must not contain {forbidden:?}\nfull tree:\n{tree}"
);
}
}