entropy-auth 2026.7.31

Authentication and authorization for Entropy Softworks server and API projects
#![allow(missing_docs)]

//! End-to-end password hashing and verification through the public API.

use entropy_auth::{PasswordConfig, PasswordHash};

/// Test-only config with minimum parameters for speed.
fn fast_config() -> PasswordConfig {
    PasswordConfig::new()
        .with_memory_kib(entropy_auth::local::password::MIN_MEMORY_KIB)
        .with_iterations(1)
        .with_parallelism(1)
}

// --- Generate and Verify ---

#[test]
fn generate_hash_verify_correct_password_returns_true() {
    let config = fast_config();
    let hash = PasswordHash::generate(b"correct-horse-battery-staple", &config).unwrap();
    assert!(hash.verify(b"correct-horse-battery-staple"));
}

#[test]
fn verify_wrong_password_returns_false() {
    let config = fast_config();
    let hash = PasswordHash::generate(b"correct-password", &config).unwrap();
    assert!(!hash.verify(b"wrong-password"));
}

// --- PHC String Round-Trip ---

#[test]
fn phc_string_round_trip_verify_still_works() {
    let config = fast_config();
    let hash = PasswordHash::generate(b"round-trip-test", &config).unwrap();
    let phc = hash.to_phc_string();
    let parsed = PasswordHash::parse(phc).unwrap();

    // The parsed hash should still verify the original password.
    assert!(parsed.verify(b"round-trip-test"));
    // And reject a wrong one.
    assert!(!parsed.verify(b"not-round-trip-test"));
}

#[test]
fn phc_string_format_is_valid() {
    let config = fast_config();
    let hash = PasswordHash::generate(b"test", &config).unwrap();
    let phc = hash.to_phc_string();
    assert!(phc.starts_with("$argon2id$"));
}

// --- Needs Rehash ---

#[test]
fn needs_rehash_false_when_config_matches() {
    let config = fast_config();
    let hash = PasswordHash::generate(b"pw", &config).unwrap();
    assert!(!hash.needs_rehash(&config));
}

#[test]
fn needs_rehash_true_when_memory_insufficient() {
    let weak_config = fast_config();
    let hash = PasswordHash::generate(b"pw", &weak_config).unwrap();

    let strong_config = PasswordConfig::new()
        .with_memory_kib(65_536)
        .with_iterations(1)
        .with_parallelism(1);
    assert!(hash.needs_rehash(&strong_config));
}

#[test]
fn needs_rehash_true_when_iterations_insufficient() {
    let config = fast_config();
    let hash = PasswordHash::generate(b"pw", &config).unwrap();

    let stronger = PasswordConfig::new()
        .with_memory_kib(entropy_auth::local::password::MIN_MEMORY_KIB)
        .with_iterations(3)
        .with_parallelism(1);
    assert!(hash.needs_rehash(&stronger));
}

#[test]
fn needs_rehash_true_when_parallelism_insufficient() {
    let config = fast_config();
    let hash = PasswordHash::generate(b"pw", &config).unwrap();

    let stronger = PasswordConfig::new()
        .with_memory_kib(entropy_auth::local::password::MIN_MEMORY_KIB)
        .with_iterations(1)
        .with_parallelism(4);
    assert!(hash.needs_rehash(&stronger));
}

// --- Config Validation ---

#[test]
fn generate_rejects_low_memory() {
    let config = PasswordConfig::new()
        .with_memory_kib(1024)
        .with_iterations(1)
        .with_parallelism(1);
    assert!(PasswordHash::generate(b"test", &config).is_err());
}

#[test]
fn generate_rejects_zero_iterations() {
    let config = PasswordConfig::new()
        .with_memory_kib(entropy_auth::local::password::MIN_MEMORY_KIB)
        .with_iterations(0)
        .with_parallelism(1);
    assert!(PasswordHash::generate(b"test", &config).is_err());
}

#[test]
fn generate_rejects_zero_parallelism() {
    let config = PasswordConfig::new()
        .with_memory_kib(entropy_auth::local::password::MIN_MEMORY_KIB)
        .with_iterations(1)
        .with_parallelism(0);
    assert!(PasswordHash::generate(b"test", &config).is_err());
}

// --- Error Type ---

#[test]
fn password_error_display_is_descriptive() {
    let err = PasswordHash::parse("garbage").unwrap_err();
    let msg = err.to_string();
    assert!(
        msg.starts_with("password:"),
        "error should be prefixed: {msg}"
    );
}