entropy-auth 2026.7.31

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

//! API key, HMAC request signing, and bearer token tests through the public API.

use entropy_auth::{
    ApiKey, HmacRequestSigner, HmacRequestVerifier, Timestamp, extract_bearer_token,
};

// --- API Key: Generate and Verify ---

#[test]
fn api_key_generate_verify_returns_true() {
    let (key, hash) = ApiKey::generate().unwrap();
    assert!(
        key.verify(&hash),
        "generated key should verify against its own hash",
    );
}

#[test]
fn api_key_generate_parse_verify_returns_true() {
    let (key, hash) = ApiKey::generate().unwrap();
    let key_str = key.as_str().to_owned();

    let parsed = ApiKey::parse(&key_str).unwrap();
    assert!(
        parsed.verify(&hash),
        "parsed key should verify against the original hash",
    );
}

#[test]
fn api_key_wrong_key_verify_fails() {
    let (_, hash_a) = ApiKey::generate().unwrap();
    let (key_b, _) = ApiKey::generate().unwrap();

    assert!(
        !key_b.verify(&hash_a),
        "a different key should not verify against another key's hash",
    );
}

#[test]
fn api_key_format_is_correct() {
    let (key, _) = ApiKey::generate().unwrap();
    let full = key.as_str();
    assert!(full.starts_with("esw_"));

    let parts: Vec<&str> = full.splitn(3, '_').collect();
    assert_eq!(parts.len(), 3);
    assert_eq!(parts[0], "esw");
    assert_eq!(parts[1].len(), 8, "prefix should be 8 hex chars");
    assert_eq!(parts[2].len(), 64, "secret should be 64 hex chars");
}

#[test]
fn api_key_prefix_matches_hash_prefix() {
    let (key, hash) = ApiKey::generate().unwrap();
    assert_eq!(key.prefix(), hash.prefix());
}

// --- HMAC Request Signing ---

#[test]
fn hmac_sign_verify_ok() {
    let secret = b"shared-secret-key".to_vec();
    let signer = HmacRequestSigner::new(secret.clone());
    let verifier = HmacRequestVerifier::new(secret);

    let method = "POST";
    let path = "/api/v1/data";
    let timestamp = "1700000000";
    let body = b"request body";

    let signature = signer.sign(method, path, timestamp, body);
    verifier
        .verify(method, path, timestamp, body, &signature)
        .expect("valid signature should verify");
}

#[test]
fn hmac_sign_tamper_signature_verify_err() {
    let secret = b"shared-secret-key".to_vec();
    let signer = HmacRequestSigner::new(secret.clone());
    let verifier = HmacRequestVerifier::new(secret);

    let signature = signer.sign("POST", "/api", "1700000000", b"body");

    // Tamper by flipping a character in the hex signature.
    let mut tampered_bytes = signature.clone().into_bytes();
    tampered_bytes[0] ^= 0x01;
    let tampered = String::from_utf8(tampered_bytes).unwrap();

    let result = verifier.verify("POST", "/api", "1700000000", b"body", &tampered);
    assert!(result.is_err());
}

#[test]
fn hmac_sign_tamper_body_verify_err() {
    let secret = b"shared-secret-key".to_vec();
    let signer = HmacRequestSigner::new(secret.clone());
    let verifier = HmacRequestVerifier::new(secret);

    let signature = signer.sign("POST", "/api", "1700000000", b"original body");
    let result = verifier.verify("POST", "/api", "1700000000", b"tampered body", &signature);
    assert!(result.is_err());
}

#[test]
fn hmac_verify_expired_timestamp_with_max_age() {
    let secret = b"shared-secret-key".to_vec();
    let signer = HmacRequestSigner::new(secret.clone());
    let verifier = HmacRequestVerifier::new(secret).with_max_age(300);

    // Use a timestamp from the distant past.
    let signature = signer.sign("GET", "/", "1000000000", b"");
    let result = verifier.verify("GET", "/", "1000000000", b"", &signature);

    assert!(result.is_err());
    let err = result.unwrap_err();
    assert!(
        err.is_timestamp_expired(),
        "stale timestamp should produce TimestampExpired",
    );
}

#[test]
fn hmac_verify_recent_timestamp_with_max_age() {
    let secret = b"shared-secret-key".to_vec();
    let signer = HmacRequestSigner::new(secret.clone());
    let verifier = HmacRequestVerifier::new(secret).with_max_age(300);

    let now = Timestamp::now().unix_epoch_secs().to_string();
    let signature = signer.sign("GET", "/", &now, b"");
    verifier
        .verify("GET", "/", &now, b"", &signature)
        .expect("current timestamp should be accepted");
}

#[test]
fn hmac_verify_without_max_age_accepts_old_timestamp() {
    let secret = b"shared-secret-key".to_vec();
    let signer = HmacRequestSigner::new(secret.clone());
    let verifier = HmacRequestVerifier::new(secret); // no max_age

    let signature = signer.sign("GET", "/", "1000000000", b"");
    verifier
        .verify("GET", "/", "1000000000", b"", &signature)
        .expect("without max_age, any timestamp should be accepted");
}

// --- Bearer Token Extraction ---

#[test]
fn bearer_valid_extraction() {
    let token = extract_bearer_token("Bearer my-secret-token").unwrap();
    assert_eq!(token, "my-secret-token");
}

#[test]
fn bearer_missing_prefix_returns_err() {
    let result = extract_bearer_token("Basic dXNlcjpwYXNz");
    assert!(result.is_err());
    let err = result.unwrap_err();
    assert!(err.is_missing_prefix());
}

#[test]
fn bearer_empty_token_returns_err() {
    let result = extract_bearer_token("Bearer ");
    assert!(result.is_err());
    let err = result.unwrap_err();
    assert!(err.is_empty_token());
}

#[test]
fn bearer_empty_header_returns_err() {
    let result = extract_bearer_token("");
    assert!(result.is_err());
    let err = result.unwrap_err();
    assert!(err.is_missing_prefix());
}

#[test]
fn bearer_lowercase_rejected() {
    let result = extract_bearer_token("bearer xyz");
    assert!(result.is_err());
    assert!(result.unwrap_err().is_missing_prefix());
}

#[test]
fn bearer_jwt_like_token_accepted() {
    let token = extract_bearer_token("Bearer eyJhbGciOiJIUzI1NiJ9.payload.sig").unwrap();
    assert_eq!(token, "eyJhbGciOiJIUzI1NiJ9.payload.sig");
}