rustauth-passkey 0.3.0

Server-side passkey plugin for RustAuth.
Documentation
use rustauth_core::context::create_auth_context;
use rustauth_core::db::DbAdapter;
use rustauth_core::options::RustAuthOptions;
use rustauth_passkey::{passkey, PasskeyOptions, PasskeySchemaOptions};
use rustauth_sqlx::SqliteAdapter;
use sqlx::sqlite::SqlitePoolOptions;

#[tokio::test]
async fn sqlite_schema_migration_creates_passkeys_table_and_columns(
) -> Result<(), Box<dyn std::error::Error>> {
    let context = create_auth_context(RustAuthOptions {
        plugins: vec![passkey(PasskeyOptions::default())],
        secret: Some("secret-a-at-least-32-chars-long!!".to_owned()),
        ..RustAuthOptions::default()
    })?;
    let pool = SqlitePoolOptions::new()
        .max_connections(1)
        .connect("sqlite::memory:")
        .await?;
    let adapter = SqliteAdapter::with_schema(pool.clone(), context.db_schema.clone());

    adapter.create_schema(&context.db_schema, None).await?;

    let table_count: i64 = sqlx::query_scalar(
        "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'passkeys'",
    )
    .fetch_one(&pool)
    .await?;
    assert_eq!(table_count, 1);

    let columns = sqlx::query_scalar::<_, String>("SELECT name FROM pragma_table_info('passkeys')")
        .fetch_all(&pool)
        .await?;
    assert!(columns.iter().any(|column| column == "credential_id"));
    assert!(columns.iter().any(|column| column == "webauthn_credential"));
    assert!(columns
        .iter()
        .all(|column| !column.contains(char::is_uppercase)));

    let unique_credential_indexes: i64 = sqlx::query_scalar(
        "SELECT COUNT(*) FROM pragma_index_list('passkeys') il \
         JOIN pragma_index_info(il.name) ii \
         WHERE il.\"unique\" = 1 AND ii.name = 'credential_id'",
    )
    .fetch_one(&pool)
    .await?;
    assert_eq!(unique_credential_indexes, 1);

    Ok(())
}

#[tokio::test]
async fn sqlite_schema_migration_uses_custom_passkey_schema_names(
) -> Result<(), Box<dyn std::error::Error>> {
    let context = create_auth_context(RustAuthOptions {
        plugins: vec![passkey(
            PasskeyOptions::default().schema(
                PasskeySchemaOptions::new()
                    .table_name("auth_passkeys")
                    .field_name("public_key", "publicKey")
                    .field_name("credential_id", "credentialID")
                    .field_name("user_id", "userId"),
            ),
        )],
        secret: Some("secret-a-at-least-32-chars-long!!".to_owned()),
        ..RustAuthOptions::default()
    })?;
    let pool = SqlitePoolOptions::new()
        .max_connections(1)
        .connect("sqlite::memory:")
        .await?;
    let adapter = SqliteAdapter::with_schema(pool.clone(), context.db_schema.clone());

    adapter.create_schema(&context.db_schema, None).await?;

    let table_count: i64 = sqlx::query_scalar(
        "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'auth_passkeys'",
    )
    .fetch_one(&pool)
    .await?;
    assert_eq!(table_count, 1);

    let columns =
        sqlx::query_scalar::<_, String>("SELECT name FROM pragma_table_info('auth_passkeys')")
            .fetch_all(&pool)
            .await?;
    assert!(columns.iter().any(|column| column == "publicKey"));
    assert!(columns.iter().any(|column| column == "credentialID"));
    assert!(columns.iter().any(|column| column == "userId"));
    assert!(columns.iter().all(|column| column != "public_key"));
    assert!(columns.iter().all(|column| column != "credential_id"));

    let unique_credential_indexes: i64 = sqlx::query_scalar(
        "SELECT COUNT(*) FROM pragma_index_list('auth_passkeys') il \
         JOIN pragma_index_info(il.name) ii \
         WHERE il.\"unique\" = 1 AND ii.name = 'credentialID'",
    )
    .fetch_one(&pool)
    .await?;
    assert_eq!(unique_credential_indexes, 1);

    Ok(())
}