entropy-auth 2026.7.31

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

//! Verifies that all public re-exports are accessible as an external consumer.

// --- Crypto Re-exports ---

#[test]
fn crypto_sha256_accessible_via_reexport() {
    let hash = entropy_auth::Sha256::digest(b"hello");
    assert_eq!(hash.len(), 32);
}

#[test]
fn crypto_sha512_accessible_via_reexport() {
    let hash = entropy_auth::Sha512::digest(b"hello");
    assert_eq!(hash.len(), 64);
}

#[test]
fn crypto_hmac_sha256_accessible_via_reexport() {
    let tag = entropy_auth::HmacSha256::mac(b"key", b"data");
    assert_eq!(tag.len(), 32);
    assert!(entropy_auth::HmacSha256::verify(b"key", b"data", &tag));
}

#[test]
fn crypto_hmac_sha512_accessible_via_reexport() {
    let tag = entropy_auth::HmacSha512::mac(b"key", b"data");
    assert_eq!(tag.len(), 64);
    assert!(entropy_auth::HmacSha512::verify(b"key", b"data", &tag));
}

#[test]
fn crypto_fill_random_accessible_via_reexport() {
    let mut buf = [0u8; 16];
    entropy_auth::fill_random(&mut buf).unwrap();
    // Extremely unlikely all bytes remain zero after CSPRNG fill.
    assert_ne!(buf, [0u8; 16]);
}

#[test]
fn crypto_random_token_hex_accessible_via_reexport() {
    let token = entropy_auth::random_token_hex(16).unwrap();
    assert_eq!(token.len(), 32);
}

#[test]
fn crypto_random_token_base64url_accessible_via_reexport() {
    let token = entropy_auth::random_token_base64url(16).unwrap();
    assert!(!token.is_empty());
}

// --- Encoding Re-exports ---

#[test]
fn encoding_base64_round_trip_via_reexport() {
    let encoded = entropy_auth::base64_encode(b"test data");
    let decoded = entropy_auth::base64_decode(&encoded).unwrap();
    assert_eq!(decoded, b"test data");
}

#[test]
fn encoding_base64url_round_trip_via_reexport() {
    let encoded = entropy_auth::base64url_encode(b"test data");
    let decoded = entropy_auth::base64url_decode(&encoded).unwrap();
    assert_eq!(decoded, b"test data");
}

#[test]
fn encoding_hex_round_trip_via_reexport() {
    let encoded = entropy_auth::hex_encode(b"\xde\xad\xbe\xef");
    let decoded = entropy_auth::hex_decode(&encoded).unwrap();
    assert_eq!(decoded, b"\xde\xad\xbe\xef");
}

#[test]
fn encoding_url_round_trip_via_reexport() {
    let encoded = entropy_auth::url_encode("hello world");
    let decoded = entropy_auth::url_decode(&encoded).unwrap();
    assert_eq!(decoded, "hello world");
}

#[test]
fn encoding_url_encode_component_via_reexport() {
    let encoded = entropy_auth::url_encode_component("a=b&c=d");
    assert!(encoded.contains("%3D"));
    assert!(encoded.contains("%26"));
}

#[test]
fn encoding_error_types_accessible() {
    // Trigger each error type to prove they are importable.
    let _: Result<Vec<u8>, entropy_auth::Base64DecodeError> = entropy_auth::base64_decode("!!!");
    let _: Result<Vec<u8>, entropy_auth::HexDecodeError> = entropy_auth::hex_decode("zz");
    let _: Result<String, entropy_auth::UrlDecodeError> = entropy_auth::url_decode("%ZZ");
}

// --- Password Re-exports ---

#[test]
fn password_config_default_via_reexport() {
    let config = entropy_auth::PasswordConfig::default();
    assert_eq!(config.memory_kib(), 65_536);
    assert_eq!(config.iterations(), 3);
    assert_eq!(config.parallelism(), 4);
}

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

#[test]
fn password_error_type_accessible() {
    let result: Result<entropy_auth::PasswordHash, entropy_auth::PasswordError> =
        entropy_auth::PasswordHash::parse("invalid");
    assert!(result.is_err());
}

// --- Session Re-exports ---

#[test]
fn session_config_default_via_reexport() {
    let config = entropy_auth::SessionConfig::default();
    assert_eq!(config.token_bytes(), 32);
    assert_eq!(config.lifetime_secs(), 3600);
}

#[test]
fn session_create_and_validate_via_reexport() {
    let config = entropy_auth::SessionConfig::default();
    let (token, session) = entropy_auth::Session::create(&config).unwrap();
    let now = entropy_auth::Timestamp::now();
    assert_eq!(
        session.validate(&token, &now),
        entropy_auth::SessionValidation::Valid,
    );
}

#[test]
fn session_error_type_accessible() {
    let _: fn() -> entropy_auth::SessionError = || {
        let config = entropy_auth::SessionConfig::default().with_allow_refresh(false);
        let (_, mut session) = entropy_auth::Session::create(&config).unwrap();
        let now = entropy_auth::Timestamp::now();
        session.refresh(&config, &now).unwrap_err()
    };
}

// --- API Re-exports ---

#[test]
fn api_key_generate_via_reexport() {
    let (key, hash) = entropy_auth::ApiKey::generate().unwrap();
    assert!(key.verify(&hash));
}

#[test]
fn api_key_error_accessible() {
    let _: Result<entropy_auth::ApiKey, entropy_auth::ApiKeyError> =
        entropy_auth::ApiKey::parse("bad");
}

#[test]
fn api_key_hash_accessible() {
    let (_, hash) = entropy_auth::ApiKey::generate().unwrap();
    let _prefix: &str = hash.prefix();
    let _secret_hash: &[u8; 32] = hash.secret_hash();
}

#[test]
fn hmac_request_signer_via_reexport() {
    let signer = entropy_auth::HmacRequestSigner::new(b"secret".to_vec());
    let sig = signer.sign("GET", "/", "0", b"");
    assert_eq!(sig.len(), 64);
}

#[test]
fn hmac_request_verifier_via_reexport() {
    let verifier = entropy_auth::HmacRequestVerifier::new(b"secret".to_vec());
    let signer = entropy_auth::HmacRequestSigner::new(b"secret".to_vec());
    let sig = signer.sign("GET", "/", "0", b"");
    assert!(verifier.verify("GET", "/", "0", b"", &sig).is_ok());
}

#[test]
fn hmac_auth_error_accessible() {
    let _: Result<(), entropy_auth::HmacAuthError> =
        entropy_auth::HmacRequestVerifier::new(b"s".to_vec()).verify("GET", "/", "0", b"", "bad");
}

#[test]
fn bearer_token_extraction_via_reexport() {
    let token = entropy_auth::extract_bearer_token("Bearer xyz").unwrap();
    assert_eq!(token, "xyz");
}

#[test]
fn bearer_error_accessible() {
    let _: Result<&str, entropy_auth::BearerError> = entropy_auth::extract_bearer_token("nope");
}

// --- JSON + JWT Re-exports ---

#[test]
fn json_value_parse_via_reexport() {
    let val = entropy_auth::JsonValue::parse(r#"{"key":"value"}"#).unwrap();
    assert_eq!(val.get_str("key"), Some("value"));
}

#[test]
fn json_parse_error_accessible() {
    let _: Result<entropy_auth::JsonValue, entropy_auth::JsonParseError> =
        entropy_auth::JsonValue::parse("bad json");
}

#[test]
fn jwt_types_accessible_via_reexport() {
    // Just prove the types are importable.
    let _: fn() -> &'static str = || {
        let _ = entropy_auth::JwtAlgorithm::HS256;
        "ok"
    };
}

// --- OAuth Re-exports ---

#[test]
fn oauth_config_builder_via_reexport() {
    let config = entropy_auth::OAuthConfig::builder("client-id", "client-secret")
        .authorization_endpoint("https://auth.example.com/authorize")
        .token_endpoint("https://auth.example.com/token")
        .redirect_uri("https://app.example.com/callback")
        .scope("openid")
        .build()
        .unwrap();
    assert_eq!(config.client_id(), "client-id");
}

#[test]
fn oauth_config_error_accessible() {
    let _: Result<entropy_auth::OAuthConfig, entropy_auth::OAuthConfigError> =
        entropy_auth::OAuthConfig::builder("", "s")
            .authorization_endpoint("https://x.com/a")
            .token_endpoint("https://x.com/t")
            .redirect_uri("https://x.com/cb")
            .build();
}

#[test]
fn authorization_request_via_reexport() {
    let config = entropy_auth::OAuthConfig::builder("cid", "cs")
        .authorization_endpoint("https://auth.example.com/authorize")
        .token_endpoint("https://auth.example.com/token")
        .redirect_uri("https://app.example.com/callback")
        .build()
        .unwrap();
    let req = entropy_auth::AuthorizationRequest::build(&config).unwrap();
    assert!(req.url().contains("client_id=cid"));
}

#[test]
fn pkce_challenge_via_reexport() {
    let pkce = entropy_auth::PkceChallenge::generate().unwrap();
    assert_eq!(pkce.method(), "S256");
    assert!(!pkce.verifier().is_empty());
    assert!(!pkce.challenge().is_empty());
}

#[test]
fn token_response_via_reexport() {
    let json = r#"{"access_token":"tok","token_type":"Bearer"}"#;
    let resp = entropy_auth::TokenResponse::parse(json).unwrap();
    assert_eq!(resp.access_token(), "tok");
}

#[test]
fn token_response_error_accessible() {
    let _: Result<entropy_auth::TokenResponse, entropy_auth::TokenResponseError> =
        entropy_auth::TokenResponse::parse("bad");
}

#[test]
fn refresh_request_via_reexport() {
    let config = entropy_auth::OAuthConfig::builder("cid", "cs")
        .authorization_endpoint("https://auth.example.com/authorize")
        .token_endpoint("https://auth.example.com/token")
        .redirect_uri("https://app.example.com/callback")
        .build()
        .unwrap();
    let req = entropy_auth::RefreshRequest::new(&config, "refresh-tok");
    assert!(req.body().contains("grant_type=refresh_token"));
}

// --- Provider Re-exports ---

#[test]
fn auth_provider_kind_accessible() {
    let kind = entropy_auth::AuthProviderKind::Local;
    assert_eq!(kind.to_string(), "local");
}

#[test]
fn auth_identity_accessible() {
    let id = entropy_auth::AuthIdentity::new("sub-1")
        .with_email("a@b.com")
        .with_display_name("Alice")
        .with_claim("role", "admin");
    assert_eq!(id.subject(), "sub-1");
    assert_eq!(id.email(), Some("a@b.com"));
    assert_eq!(id.display_name(), Some("Alice"));
    assert_eq!(id.claims().len(), 1);
}

#[test]
fn auth_result_accessible() {
    let identity = entropy_auth::AuthIdentity::new("user-1");
    let ts = entropy_auth::Timestamp::now();
    let result = entropy_auth::AuthResult::new(identity, entropy_auth::AuthProviderKind::OAuth, ts);
    assert_eq!(result.provider(), entropy_auth::AuthProviderKind::OAuth);
    assert!(result.session().is_none());
}

#[test]
fn auth_error_accessible() {
    // AuthError is an opaque struct with pub(crate) constructors, so
    // external code cannot create instances directly. Verify the type is
    // importable and the public query/display API compiles.
    fn assert_auth_error_api(err: &entropy_auth::AuthError) {
        let _ = err.is_invalid_credentials();
        let _ = err.is_token_expired();
        let _ = err.is_invalid_token();
        let _ = err.is_invalid_configuration();
        let _ = err.is_crypto();
        let _ = err.is_session_expired();
        let _ = err.is_provider();
        let _ = err.detail();
        let _ = err.to_string();
        let _ = format!("{err:?}");
    }
    // Prove the function compiles — it is only called if an AuthError is
    // obtained from the crate (e.g. via a fallible API).
    let _ = assert_auth_error_api;
}

// --- Misc Re-exports ---

#[test]
fn constant_time_eq_via_reexport() {
    assert!(entropy_auth::constant_time_eq(b"abc", b"abc"));
    assert!(!entropy_auth::constant_time_eq(b"abc", b"xyz"));
}

#[test]
fn timestamp_via_reexport() {
    let ts = entropy_auth::Timestamp::now();
    assert!(ts.unix_epoch_secs() > 0);
}

#[test]
fn zeroizing_via_reexport() {
    let z = entropy_auth::Zeroizing::new(vec![1u8, 2, 3]);
    assert_eq!(&*z, &[1, 2, 3]);
}

#[test]
fn generate_token_via_reexport() {
    let token = entropy_auth::generate_token(16).unwrap();
    assert_eq!(token.len(), 32);
}

#[test]
fn generate_token_with_hash_via_reexport() {
    let (token, hash) = entropy_auth::generate_token_with_hash(16).unwrap();
    assert_eq!(token.len(), 32);
    assert_eq!(hash.len(), 32);
}

#[test]
fn credential_types_accessible_via_reexport() {
    let _store = entropy_auth::InMemoryCredentialStore::new();
    let creds = entropy_auth::Credentials::new("testuser", b"testpass".to_vec()).unwrap();
    assert_eq!(creds.username().as_str(), "testuser");
}

#[test]
fn in_memory_store_error_is_nameable_via_reexport() {
    // The associated `Error` type of `CredentialStore for
    // InMemoryCredentialStore` must be nameable from outside the crate.
    fn assert_error<E: std::error::Error>() {}
    assert_error::<entropy_auth::InMemoryStoreError>();
}