rusty-pwgen 0.1.0

Generate pronounceable or random passwords from the OS CSPRNG — a Rust port of Theodore Ts'o's `pwgen` with strict-compat mode, deterministic `-H` reproducible mode (SHA256 + ChaCha20), and a typed library API.
Documentation
//! US5 (Library API for Embedding, P2) integration tests.

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() {
    // FR-069: generate_one × N, generate_n(N), iter().take(N).collect() all yield same Vec<String>
    // when the Pwgen instance starts from the same state. Use reproducible_seed.
    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() {
    // FR-008: no_vowels(true) implicitly engages secure mode at build() time.
    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() {
    // FR-032: length < 5 forces secure mode at the library layer too.
    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() {
    // FR-035: length == 0 → empty passwords.
    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() {
    // FR-009: 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() {
    // FR-028 / SC-014: same seed → identical output.
    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() {
    // Building a Pwgen with `remove_chars` so aggressive that the active set
    // becomes empty must error out at build() time, not panic at run.
    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() {
    // FR-068: thread-safety contracts.
    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() {
    // FR-030 / SC-008: cargo tree --no-default-features must not contain
    // clap, clap_complete, anyhow, or terminal_size.
    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}"
        );
    }
}