oauth2-passkey 0.6.1

OAuth2 and Passkey authentication library for Rust web applications
Documentation
//! Test utilities for passkey module tests
//!
//! This module provides helper functions for setting up and tearing down test data
//! for passkey-related tests. It leverages the in-memory GENERIC_DATA_STORE and
//! GENERIC_CACHE_STORE to create isolated test environments.

use crate::passkey::errors::PasskeyError;
use crate::passkey::types::{CredentialId, PublicKeyCredentialUserEntity, StoredOptions};
use crate::passkey::{PasskeyCredential, PasskeyStore};
use crate::session::UserId;
use crate::storage::{CacheData, CacheErrorConversion, CacheKey, CachePrefix, GENERIC_CACHE_STORE};
use crate::userdb::{User, UserStore};
use chrono::Utc;
use std::time::SystemTime;

// Use the existing types for tests, don't create new ones

/// Test data for creating a credential
pub struct TestCredentialData {
    pub credential_id: String,
    pub user_id: String,
    pub user_handle: String,
    pub name: String,
    pub display_name: String,
    pub public_key: String,
    pub aaguid: String,
    pub counter: u32,
}

impl TestCredentialData {
    /// Create new test credential data from string slices
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        credential_id: &str,
        user_id: &str,
        user_handle: &str,
        name: &str,
        display_name: &str,
        public_key: &str,
        aaguid: &str,
        counter: u32,
    ) -> Self {
        Self {
            credential_id: credential_id.to_string(),
            user_id: user_id.to_string(),
            user_handle: user_handle.to_string(),
            name: name.to_string(),
            display_name: display_name.to_string(),
            public_key: public_key.to_string(),
            aaguid: aaguid.to_string(),
            counter,
        }
    }
}

/// Insert a test user in the database for testing
pub async fn insert_test_user(
    user_id: UserId,
    account: &str,
    label: &str,
    is_admin: bool,
) -> Result<User, PasskeyError> {
    let user = User {
        sequence_number: None,
        id: user_id.as_str().to_string(),
        account: account.to_string(),
        label: label.to_string(),
        is_admin,
        created_at: Utc::now(),
        updated_at: Utc::now(),
    };

    UserStore::upsert_user(user)
        .await
        .map_err(|e| PasskeyError::Storage(e.to_string()))
}

/// Insert a test passkey credential in the database
pub async fn insert_test_credential(data: TestCredentialData) -> Result<(), PasskeyError> {
    let now = Utc::now();

    let credential = PasskeyCredential {
        sequence_number: None,
        credential_id: data.credential_id.clone(),
        user_id: data.user_id,
        public_key: data.public_key,
        aaguid: data.aaguid,
        rp_id: "localhost".to_string(),
        counter: data.counter,
        user: PublicKeyCredentialUserEntity {
            user_handle: data.user_handle,
            name: data.name,
            display_name: data.display_name,
        },
        created_at: now,
        updated_at: now,
        last_used_at: now,
    };

    PasskeyStore::store_credential(
        CredentialId::new(data.credential_id).expect("Valid credential ID"),
        credential,
    )
    .await
}

/// Insert a test user and then a test passkey credential
/// This ensures the foreign key constraint is satisfied
pub async fn insert_test_user_and_credential(data: TestCredentialData) -> Result<(), PasskeyError> {
    // Ensure stores are initialized to prevent race conditions
    UserStore::init()
        .await
        .map_err(|e| PasskeyError::Storage(e.to_string()))?;
    PasskeyStore::init().await?;

    // First create the user
    insert_test_user(
        UserId::new(data.user_id.clone()).expect("Valid user ID"),
        &data.name,
        &data.display_name,
        false,
    )
    .await?;

    // Then create the credential
    insert_test_credential(data).await
}

/// Delete a test credential by its ID
pub async fn delete_test_credential(
    credential_id: crate::passkey::CredentialId,
) -> Result<(), PasskeyError> {
    PasskeyStore::delete_credential_by(crate::passkey::CredentialSearchField::CredentialId(
        credential_id,
    ))
    .await
}

/// Remove a key from the cache store
pub async fn remove_from_cache(
    cache_prefix: CachePrefix,
    cache_key: CacheKey,
) -> Result<(), PasskeyError> {
    GENERIC_CACHE_STORE
        .lock()
        .await
        .remove(cache_prefix, cache_key)
        .await
        .map_err(PasskeyError::convert_storage_error)
}

/// Clean up test credential data
pub async fn cleanup_test_credential(
    credential_id: crate::passkey::CredentialId,
) -> Result<(), PasskeyError> {
    delete_test_credential(credential_id).await
}

/// Create a test challenge in the cache store
pub async fn create_test_challenge(
    challenge_type: &str,
    id: &str,
    challenge: &str,
    user_handle: &str,
    name: &str,
    display_name: &str,
    ttl: u64,
) -> Result<(), PasskeyError> {
    let now = SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs();

    let stored_options = StoredOptions {
        challenge: challenge.to_string(),
        user: PublicKeyCredentialUserEntity {
            user_handle: user_handle.to_string(),
            name: name.to_string(),
            display_name: display_name.to_string(),
        },
        timestamp: now,
        ttl,
    };

    let cache_data = CacheData {
        value: serde_json::to_string(&stored_options)
            .map_err(|e| PasskeyError::Storage(e.to_string()))?,
    };

    let cache_prefix = CachePrefix::new(challenge_type.to_string())
        .map_err(PasskeyError::convert_storage_error)?;
    let cache_key = CacheKey::new(id.to_string()).map_err(PasskeyError::convert_storage_error)?;

    GENERIC_CACHE_STORE
        .lock()
        .await
        .put_with_ttl(cache_prefix, cache_key, cache_data, ttl as usize)
        .await
        .map_err(PasskeyError::convert_storage_error)
}

/// Check cache store for a specific key
pub async fn check_cache_exists(cache_prefix: CachePrefix, cache_key: CacheKey) -> bool {
    matches!(
        GENERIC_CACHE_STORE
            .lock()
            .await
            .get(cache_prefix, cache_key)
            .await,
        Ok(Some(_))
    )
}