oauth2-passkey 0.6.0

OAuth2 and Passkey authentication library for Rust web applications
Documentation
use super::*;
use crate::oauth2::types::{OidcTokenResponse, OidcUserInfo};
use serde_json::json;

/// Test successful deserialization of OIDC user info JSON
///
/// This test verifies that `OidcUserInfo` can be correctly deserialized from
/// a JSON response containing all required fields. It creates a mock JSON response
/// in memory and tests the serde deserialization.
///
#[test]
fn test_oidc_userinfo_deserialization() {
    // Test successful deserialization of OIDC user info
    let json_data = json!({
        "sub": "123456789",
        "email": "test@example.com",
        "email_verified": true,
        "name": "Test User",
        "given_name": "Test",
        "family_name": "User",
        "picture": "https://example.com/pic.jpg",
        "locale": "en"
    });

    let json_str = serde_json::to_string(&json_data)
        .expect("JSON serialization should not fail for valid data");
    let user_info: Result<OidcUserInfo, _> = serde_json::from_str(&json_str);

    assert!(
        user_info.is_ok(),
        "Should successfully deserialize valid Google user info"
    );
    let user_info = user_info.expect("Already verified result is Ok");
    assert_eq!(user_info.email, Some("test@example.com".to_string()));
    assert_eq!(user_info.name, Some("Test User".to_string()));
}

/// Test successful deserialization of OIDC token response with id_token
///
/// This test verifies that `OidcTokenResponse` can be correctly deserialized from
/// a JSON response that includes an id_token field. It creates a mock JSON response
/// in memory and tests the serde deserialization of all fields.
///
#[test]
fn test_oidc_token_response_deserialization() {
    // Test successful deserialization of OIDC token response with id_token
    let json_data = json!({
        "access_token": "ya29.access_token_value",
        "expires_in": 3599,
        "scope": "openid email profile",
        "token_type": "Bearer",
        "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE2NzAyOGE4MzI5Y2QwOTU0Y2JmYWMwNGI2MWI3OGZkYThlMzVjOGMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhdWQiOiJjbGllbnRfaWQiLCJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoxNjA5NDYyODAwLCJpYXQiOjE2MDk0NTkyMDB9.signature"
    });

    let json_str = serde_json::to_string(&json_data)
        .expect("JSON serialization should not fail for valid data");
    let token_response: Result<OidcTokenResponse, _> = serde_json::from_str(&json_str);

    assert!(
        token_response.is_ok(),
        "Should successfully deserialize valid OIDC token response"
    );
    let token_response = token_response.expect("Already verified result is Ok");
    assert_eq!(token_response.access_token, "ya29.access_token_value");
    assert!(token_response.id_token.is_some(), "Should have id_token");
}

/// Test deserialization of OIDC token response without id_token
///
/// This test verifies that `OidcTokenResponse` can be correctly deserialized from
/// a JSON response that omits the optional id_token field. It creates a mock JSON
/// response in memory and verifies the id_token field is None.
///
#[test]
fn test_oidc_token_response_missing_id_token() {
    // Test deserialization of OIDC token response without id_token
    let json_data = json!({
        "access_token": "ya29.access_token_value",
        "expires_in": 3599,
        "scope": "openid email profile",
        "token_type": "Bearer"
        // Missing id_token field
    });

    let json_str = serde_json::to_string(&json_data)
        .expect("JSON serialization should not fail for valid data");
    let token_response: Result<OidcTokenResponse, _> = serde_json::from_str(&json_str);

    assert!(
        token_response.is_ok(),
        "Should successfully deserialize token response without id_token"
    );
    let token_response = token_response.expect("Already verified result is Ok");
    assert_eq!(token_response.access_token, "ya29.access_token_value");
    assert!(
        token_response.id_token.is_none(),
        "Should not have id_token"
    );
}

/// Test deserialization of OIDC token response without expires_in
///
/// Per RFC 6749 §5.1 `expires_in` is RECOMMENDED, not REQUIRED. This test
/// verifies that `OidcTokenResponse` can be correctly deserialized from a
/// JSON response that omits it (e.g. older Keycloak builds, some Ory Hydra
/// configs).
///
#[test]
fn test_oidc_token_response_missing_expires_in() {
    let json_data = json!({
        "access_token": "ya29.access_token_value",
        "scope": "openid email profile",
        "token_type": "Bearer",
        "id_token": "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20ifQ.signature"
        // Missing expires_in field
    });

    let json_str = serde_json::to_string(&json_data)
        .expect("JSON serialization should not fail for valid data");
    let token_response: Result<OidcTokenResponse, _> = serde_json::from_str(&json_str);

    assert!(
        token_response.is_ok(),
        "Should successfully deserialize token response without expires_in"
    );
    let token_response = token_response.expect("Already verified result is Ok");
    assert_eq!(token_response.access_token, "ya29.access_token_value");
    assert!(
        token_response.id_token.is_some(),
        "id_token should still be present"
    );
}

/// Test OIDC user info deserialization with missing required fields
///
/// This test verifies that deserializing OIDC user info JSON fails appropriately
/// when required fields are missing from the response.
///
#[test]
fn test_oidc_userinfo_deserialization_missing_required_fields() {
    // Test deserialization failure when required fields are missing
    let json_data = json!({
        "id": "123456789",
        // Missing required fields: email, name, etc.
        "verified_email": true,
        "picture": "https://example.com/pic.jpg"
    });

    let json_str = serde_json::to_string(&json_data).expect("JSON serialization should not fail");
    let user_info: Result<OidcUserInfo, _> = serde_json::from_str(&json_str);

    assert!(
        user_info.is_err(),
        "Should fail to deserialize when required fields are missing"
    );
}

/// Test OIDC user info deserialization with malformed JSON
///
/// This test verifies that attempting to deserialize malformed JSON to OidcUserInfo
/// returns a JsonError as expected.
///
#[test]
fn test_oidc_userinfo_deserialization_invalid_json() {
    // Test deserialization failure with malformed JSON
    let invalid_json = r#"{"id": "123", "email":}"#; // Malformed JSON

    let user_info: Result<OidcUserInfo, _> = serde_json::from_str(invalid_json);

    assert!(
        user_info.is_err(),
        "Should fail to deserialize malformed JSON"
    );
}

/// Test OIDC token response deserialization with missing access_token
///
/// This test verifies that attempting to deserialize an OIDC token response without
/// the required access_token field returns a deserialization error.
///
#[test]
fn test_oidc_token_response_missing_access_token() {
    // Test deserialization failure when access_token is missing
    let json_data = json!({
        // Missing access_token
        "expires_in": 3599,
        "scope": "openid email profile",
        "token_type": "Bearer",
        "id_token": "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20ifQ.signature"
    });

    let json_str = serde_json::to_string(&json_data).expect("JSON serialization should not fail");
    let token_response: Result<OidcTokenResponse, _> = serde_json::from_str(&json_str);

    assert!(
        token_response.is_err(),
        "Should fail to deserialize when access_token is missing"
    );
}

/// Test OIDC token response deserialization with malformed JSON
///
/// This test verifies that attempting to deserialize malformed JSON to OidcTokenResponse
/// returns a JsonError as expected.
///
#[test]
fn test_oidc_token_response_invalid_json() {
    // Test deserialization failure with malformed JSON
    let invalid_json = r#"{"access_token": "token", "expires_in":}"#; // Malformed JSON

    let token_response: Result<OidcTokenResponse, _> = serde_json::from_str(invalid_json);

    assert!(
        token_response.is_err(),
        "Should fail to deserialize malformed JSON"
    );
}

/// Tests for business logic validation in exchange_code_for_token function
///
/// This test validates the critical business logic for id_token validation
/// that is used in exchange_code_for_token()
///
#[test]
fn test_id_token_validation_logic() {
    // This test validates the critical business logic for id_token validation
    // that is used in exchange_code_for_token()

    // Test case 1: Missing id_token should return error
    let missing_id_token: Option<String> = None;
    let result = missing_id_token
        .ok_or_else(|| OAuth2Error::TokenExchange("ID token not present in response".to_string()));

    assert!(
        result.is_err(),
        "Should return error when id_token is missing"
    );
    match result {
        Err(OAuth2Error::TokenExchange(msg)) => {
            assert_eq!(msg, "ID token not present in response");
        }
        _ => panic!("Expected TokenExchange error with specific message"),
    }

    // Test case 2: Present id_token should succeed
    let present_id_token: Option<String> = Some(
        "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20ifQ.signature"
            .to_string(),
    );
    let result = present_id_token
        .ok_or_else(|| OAuth2Error::TokenExchange("ID token not present in response".to_string()));

    assert!(result.is_ok(), "Should succeed when id_token is present");
    let id_token = result.expect("Already verified result is Ok");
    assert_eq!(
        id_token,
        "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20ifQ.signature"
    );
}