pwgen2 0.8.2

password generator
Documentation
use anyhow::{Result, anyhow};
use assert_cmd::Command;
use bip39::{Language, Mnemonic};
use predicates::prelude::*;
use serde_json::Value;

fn cargo_cmd() -> Result<Command> {
    Command::cargo_bin(env!("CARGO_PKG_NAME")).map_err(Into::into)
}

fn first_array_entry(value: &Value) -> Result<&Value> {
    value
        .as_array()
        .and_then(|entries| entries.first())
        .ok_or_else(|| anyhow!("expected a non-empty JSON array"))
}

#[test]
fn test_help() -> Result<()> {
    let mut cmd = cargo_cmd()?;
    cmd.arg("--help")
        .assert()
        .stdout(predicate::str::contains("password generator"))
        .stdout(predicate::str::contains("-m, --mnemonic [<words>]"));
    Ok(())
}

#[test]
fn test_create_password() -> Result<()> {
    for _ in 0..100 {
        let mut cmd = cargo_cmd()?;
        cmd.assert()
            .stdout(predicate::function(|s: &str| s.trim().len() == 18));
    }
    Ok(())
}

#[test]
fn test_create_password_json_output() -> Result<()> {
    let mut cmd = cargo_cmd()?;
    let output = cmd
        .arg("-j")
        .arg("18")
        .arg("3")
        .assert()
        .get_output()
        .stdout
        .clone();
    let parsed: Value = serde_json::from_slice(&output)?;
    let first = first_array_entry(&parsed)?;

    assert_eq!(parsed.as_array().map(Vec::len), Some(3));
    assert!(first.get("password").is_some_and(Value::is_string));
    assert!(first.get("hash").is_some_and(Value::is_null));
    Ok(())
}

#[test]
fn test_create_pin() -> Result<()> {
    let mut cmd = cargo_cmd()?;
    let pin_pattern = predicate::str::is_match(r"\d{4}\n")?;
    cmd.arg("-p").assert().stdout(pin_pattern);
    Ok(())
}

#[test]
fn test_create_alphanumeric() -> Result<()> {
    let pattern = predicate::str::is_match(r"^[a-zA-Z0-9]{18}\n$")?;

    for _ in 0..100 {
        let mut cmd = cargo_cmd()?;
        cmd.arg("-a").assert().stdout(pattern.clone());
    }
    Ok(())
}

#[test]
fn test_multibyte_custom_symbol_does_not_panic() -> Result<()> {
    let mut cmd = cargo_cmd()?;
    cmd.arg("-c")
        .arg("")
        .arg("12")
        .arg("3")
        .assert()
        .success()
        .stdout(predicate::str::contains(""));
    Ok(())
}

#[test]
fn test_create_mnemonic_defaults_to_twelve_words() -> Result<()> {
    let mut cmd = cargo_cmd()?;
    let output = cmd.arg("-m").assert().get_output().stdout.clone();
    let phrase = String::from_utf8(output)?;
    let phrase = phrase.trim();

    assert_eq!(phrase.split_whitespace().count(), 12);
    assert!(Mnemonic::parse_in_normalized(Language::English, phrase).is_ok());
    Ok(())
}

#[test]
fn test_create_mnemonic_with_24_words() -> Result<()> {
    let mut cmd = cargo_cmd()?;
    let output = cmd.arg("-m").arg("24").assert().get_output().stdout.clone();
    let phrase = String::from_utf8(output)?;
    let phrase = phrase.trim();

    assert_eq!(phrase.split_whitespace().count(), 24);
    assert!(Mnemonic::parse_in_normalized(Language::English, phrase).is_ok());
    Ok(())
}

#[test]
fn test_create_mnemonic_json_output() -> Result<()> {
    let mut cmd = cargo_cmd()?;
    let output = cmd.arg("-m").arg("-j").assert().get_output().stdout.clone();
    let parsed: Value = serde_json::from_slice(&output)?;
    let first = first_array_entry(&parsed)?;
    let phrase = first
        .get("mnemonic")
        .and_then(Value::as_str)
        .ok_or_else(|| anyhow!("expected mnemonic string"))?;

    assert_eq!(first.get("word_count"), Some(&Value::from(12)));
    assert_eq!(phrase.split_whitespace().count(), 12);
    assert!(Mnemonic::parse_in_normalized(Language::English, phrase).is_ok());
    Ok(())
}

#[test]
fn test_mnemonic_conflicts_with_password_length() -> Result<()> {
    let mut cmd = cargo_cmd()?;
    cmd.arg("-m")
        .arg("24")
        .arg("18")
        .assert()
        .failure()
        .stderr(predicate::str::contains("cannot be used with"));
    Ok(())
}

#[test]
fn test_mnemonic_rejects_invalid_word_count() -> Result<()> {
    let mut cmd = cargo_cmd()?;
    cmd.arg("-m")
        .arg("14")
        .assert()
        .failure()
        .stderr(predicate::str::contains(
            "Mnemonic word count must be 12, 15, 18, 21, or 24",
        ));
    Ok(())
}