rustauth-plugins 0.2.0

Official RustAuth plugin modules.
Documentation
use std::sync::Arc;

use http::StatusCode;
use rustauth_core::db::{DbAdapter, DbValue, FindMany, MemoryAdapter, Where};
use rustauth_plugins::siwe::{EnsLookupArgs, EnsLookupResult};

use super::{nonce, options, response_json, router, verify, WALLET};

#[tokio::test]
async fn verify_endpoint_creates_anonymous_user_wallet_account_and_session(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(MemoryAdapter::new());
    let router = router(adapter.clone(), options())?;
    nonce(&router, WALLET, Some(1)).await?;

    let response = verify(
        &router,
        WALLET,
        Some(1),
        "valid_message",
        "valid_signature",
        None,
    )
    .await?;

    assert_eq!(response.status(), StatusCode::OK);
    let body = response_json(&response)?;
    let user_id = body["user"]["id"].as_str().ok_or("missing user id")?;
    assert_eq!(body["success"], true);
    assert_eq!(body["user"]["walletAddress"], WALLET);
    assert_eq!(adapter.len("user").await, 1);
    assert_eq!(adapter.len("session").await, 1);
    assert_eq!(adapter.len("wallet_address").await, 1);
    let accounts = adapter.records("account").await;
    assert!(accounts.iter().any(|record| {
        record.get("user_id") == Some(&DbValue::String(user_id.to_owned()))
            && record.get("provider_id") == Some(&DbValue::String("siwe".to_owned()))
            && record.get("account_id") == Some(&DbValue::String(format!("{WALLET}:1")))
    }));
    Ok(())
}

#[tokio::test]
async fn verify_endpoint_uses_email_and_ens_metadata_when_configured(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(MemoryAdapter::new());
    let opts = options()
        .anonymous(false)
        .ens_lookup(|_args: EnsLookupArgs| async {
            Ok(Some(EnsLookupResult {
                name: "vitalik.eth".to_owned(),
                avatar: "https://example.com/avatar.png".to_owned(),
            }))
        });
    let router = router(adapter.clone(), opts)?;
    nonce(&router, WALLET, Some(1)).await?;

    let response = verify(
        &router,
        WALLET,
        Some(1),
        "valid_message",
        "valid_signature",
        Some("user@example.com"),
    )
    .await?;

    assert_eq!(response.status(), StatusCode::OK);
    let users = adapter.records("user").await;
    assert_eq!(
        users.first().and_then(|record| record.get("email")),
        Some(&DbValue::String("user@example.com".to_owned()))
    );
    assert_eq!(
        users.first().and_then(|record| record.get("name")),
        Some(&DbValue::String("vitalik.eth".to_owned()))
    );
    assert_eq!(
        users.first().and_then(|record| record.get("image")),
        Some(&DbValue::String(
            "https://example.com/avatar.png".to_owned()
        ))
    );
    Ok(())
}

#[tokio::test]
async fn verify_endpoint_reuses_same_wallet_and_adds_second_chain(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(MemoryAdapter::new());
    let router = router(adapter.clone(), options())?;

    nonce(&router, WALLET, Some(1)).await?;
    let first = verify(
        &router,
        WALLET,
        Some(1),
        "valid_message",
        "valid_signature",
        None,
    )
    .await?;
    nonce(&router, WALLET, Some(137)).await?;
    let second = verify(
        &router,
        WALLET,
        Some(137),
        "valid_message",
        "valid_signature",
        None,
    )
    .await?;

    assert_eq!(first.status(), StatusCode::OK);
    assert_eq!(second.status(), StatusCode::OK);
    assert_eq!(adapter.len("user").await, 1);
    let wallet_records = adapter
        .find_many(
            FindMany::new("wallet_address")
                .where_clause(Where::new("address", DbValue::String(WALLET.to_owned()))),
        )
        .await?;
    assert_eq!(wallet_records.len(), 2);
    assert!(wallet_records.iter().any(|record| {
        record.get("chain_id") == Some(&DbValue::Number(1))
            && record.get("is_primary") == Some(&DbValue::Boolean(true))
    }));
    assert!(wallet_records.iter().any(|record| {
        record.get("chain_id") == Some(&DbValue::Number(137))
            && record.get("is_primary") == Some(&DbValue::Boolean(false))
    }));
    Ok(())
}

#[tokio::test]
async fn verify_endpoint_reuses_same_wallet_when_case_changes(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(MemoryAdapter::new());
    let router = router(adapter.clone(), options())?;

    nonce(&router, &WALLET.to_lowercase(), Some(1)).await?;
    let first = verify(
        &router,
        &WALLET.to_lowercase(),
        Some(1),
        "valid_message",
        "valid_signature",
        None,
    )
    .await?;
    nonce(&router, &WALLET.to_uppercase(), Some(1)).await?;
    let second = verify(
        &router,
        &WALLET.to_uppercase(),
        Some(1),
        "valid_message",
        "valid_signature",
        None,
    )
    .await?;

    assert_eq!(first.status(), StatusCode::OK);
    assert_eq!(second.status(), StatusCode::OK);
    assert_eq!(adapter.len("user").await, 1);
    assert_eq!(adapter.len("wallet_address").await, 1);
    Ok(())
}

#[tokio::test]
async fn verify_endpoint_reuses_same_wallet_on_same_chain() -> Result<(), Box<dyn std::error::Error>>
{
    let adapter = Arc::new(MemoryAdapter::new());
    let router = router(adapter.clone(), options())?;

    nonce(&router, WALLET, Some(1)).await?;
    let first = response_json(
        &verify(
            &router,
            WALLET,
            Some(1),
            "valid_message",
            "valid_signature",
            None,
        )
        .await?,
    )?;
    nonce(&router, WALLET, Some(1)).await?;
    let second = response_json(
        &verify(
            &router,
            WALLET,
            Some(1),
            "valid_message",
            "valid_signature",
            None,
        )
        .await?,
    )?;

    assert_eq!(first["user"]["id"], second["user"]["id"]);
    assert_eq!(adapter.len("user").await, 1);
    assert_eq!(adapter.len("wallet_address").await, 1);
    Ok(())
}