rustauth-plugins 0.2.0

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

use rustauth_core::crypto::SecretEntry;
use rustauth_core::db::MemoryAdapter;
use rustauth_core::options::RustAuthOptions;
use rustauth_plugins::email_otp::{EmailOtpOptions, OtpStorage};

use super::common::*;

#[tokio::test]
async fn server_create_and_get_otp_returns_recoverable_plain_value() {
    let adapter = Arc::new(MemoryAdapter::new());
    let sender = CaptureSender::default();
    let router = router(adapter.clone(), sender, EmailOtpOptions::default()).unwrap();

    let create = router
        .handle_async(
            json_request(
                "/email-otp/create-verification-otp",
                r#"{"email":"ada@example.com","type":"email-verification"}"#,
                None,
            )
            .unwrap(),
        )
        .await
        .unwrap();
    let otp: String = serde_json::from_slice(create.body()).unwrap();
    assert!(
        verification_value(&adapter, "email-verification-otp-ada@example.com")
            .await
            .is_some()
    );

    let get = router
        .handle_async(
            get_json_request(
                "/email-otp/get-verification-otp?email=ada%40example.com&type=email-verification",
                "",
                None,
            )
            .unwrap(),
        )
        .await
        .unwrap();
    let get_body: Value = serde_json::from_slice(get.body()).unwrap();

    assert_eq!(create.status(), StatusCode::OK);
    assert_eq!(get_body["otp"], otp);
}

#[tokio::test]
async fn server_get_otp_uses_query_and_handles_percent_encoded_email() {
    let adapter = Arc::new(MemoryAdapter::new());
    let router = router(
        adapter,
        CaptureSender::default(),
        EmailOtpOptions::default(),
    )
    .unwrap();

    let create = router
        .handle_async(
            json_request(
                "/email-otp/create-verification-otp",
                r#"{"email":"ada+tag@example.com","type":"email-verification"}"#,
                None,
            )
            .unwrap(),
        )
        .await
        .unwrap();
    let otp: String = serde_json::from_slice(create.body()).unwrap();

    let get = router
        .handle_async(
            get_json_request(
                "/email-otp/get-verification-otp?email=ada%2Btag%40example.com&type=email-verification",
                "",
                None,
            )
            .unwrap(),
        )
        .await
        .unwrap();
    let get_body: Value = serde_json::from_slice(get.body()).unwrap();

    assert_eq!(get.status(), StatusCode::OK);
    assert_eq!(get_body["otp"], otp);
}

#[tokio::test]
async fn server_get_otp_rejects_non_recoverable_hashed_storage() {
    let adapter = Arc::new(MemoryAdapter::new());
    let router = router(
        adapter,
        CaptureSender::default(),
        EmailOtpOptions {
            store_otp: OtpStorage::Hashed,
            ..EmailOtpOptions::default()
        },
    )
    .unwrap();

    router
        .handle_async(
            json_request(
                "/email-otp/create-verification-otp",
                r#"{"email":"ada@example.com","type":"email-verification"}"#,
                None,
            )
            .unwrap(),
        )
        .await
        .unwrap();
    let get = router
        .handle_async(
            get_json_request(
                "/email-otp/get-verification-otp?email=ada%40example.com&type=email-verification",
                "",
                None,
            )
            .unwrap(),
        )
        .await
        .unwrap();
    let body: Value = serde_json::from_slice(get.body()).unwrap();

    assert_eq!(get.status(), StatusCode::BAD_REQUEST);
    assert_eq!(body["code"], "INVALID_OTP");
    assert_eq!(
        body["message"],
        "OTP is hashed, cannot return the plain text OTP"
    );
}

#[tokio::test]
async fn server_get_otp_returns_encrypted_value_with_secret_rotation() {
    let adapter = Arc::new(MemoryAdapter::new());
    let router = router_with_auth_options(
        adapter,
        CaptureSender::default(),
        EmailOtpOptions {
            store_otp: OtpStorage::Encrypted,
            ..EmailOtpOptions::default()
        },
        RustAuthOptions {
            secrets: vec![
                SecretEntry::new(2, "current-secret-for-email-otp-tests-2"),
                SecretEntry::new(1, "previous-secret-for-email-otp-tests-1"),
            ],
            ..RustAuthOptions::default()
        },
    )
    .unwrap();

    let create = router
        .handle_async(
            json_request(
                "/email-otp/create-verification-otp",
                r#"{"email":"ada@example.com","type":"email-verification"}"#,
                None,
            )
            .unwrap(),
        )
        .await
        .unwrap();
    let otp: String = serde_json::from_slice(create.body()).unwrap();
    let get = router
        .handle_async(
            get_json_request(
                "/email-otp/get-verification-otp?email=ada%40example.com&type=email-verification",
                "",
                None,
            )
            .unwrap(),
        )
        .await
        .unwrap();
    let get_body: Value = serde_json::from_slice(get.body()).unwrap();

    assert_eq!(get.status(), StatusCode::OK);
    assert_eq!(get_body["otp"], otp);
}

#[tokio::test]
async fn server_create_get_and_check_support_change_email_type() {
    let adapter = Arc::new(MemoryAdapter::new());
    create_user(&adapter, "ada@example.com", true).await;
    let router = router(
        adapter,
        CaptureSender::default(),
        EmailOtpOptions::default(),
    )
    .unwrap();

    let create = router
        .handle_async(
            json_request(
                "/email-otp/create-verification-otp",
                r#"{"email":"ada@example.com","type":"change-email"}"#,
                None,
            )
            .unwrap(),
        )
        .await
        .unwrap();
    let otp: String = serde_json::from_slice(create.body()).unwrap();

    let get = router
        .handle_async(
            get_json_request(
                "/email-otp/get-verification-otp?email=ada%40example.com&type=change-email",
                "",
                None,
            )
            .unwrap(),
        )
        .await
        .unwrap();
    let get_body: Value = serde_json::from_slice(get.body()).unwrap();

    let check = router
        .handle_async(
            json_request(
                "/email-otp/check-verification-otp",
                &format!(r#"{{"email":"ada@example.com","type":"change-email","otp":"{otp}"}}"#),
                None,
            )
            .unwrap(),
        )
        .await
        .unwrap();

    assert_eq!(get_body["otp"], otp);
    assert_eq!(check.status(), StatusCode::OK);
}