rustauth-core 0.2.0

Core types and primitives for RustAuth.
Documentation
use time::Duration;

use super::*;

use rustauth_core::options::{DeleteUserOptions, UserOptions};

#[tokio::test]
async fn delete_user_route_deletes_user_accounts_and_sessions_with_password(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(RouteAdapter::default());
    let now = OffsetDateTime::now_utc();
    adapter.insert_user(user(now)).await;
    adapter
        .insert_account(credential_account_record(
            "user_1",
            &fast_hash_password("secret123")?,
            now,
        ))
        .await?;
    adapter
        .insert_session(session(now, now + Duration::hours(1)))
        .await;
    adapter
        .insert_session(Session {
            id: "session_2".to_owned(),
            token: "token_2".to_owned(),
            ..session(now, now + Duration::hours(1))
        })
        .await;
    let router = router_with_options(
        adapter.clone(),
        RustAuthOptions {
            user: UserOptions {
                delete_user: DeleteUserOptions::builder().enabled(true),
                ..UserOptions::default()
            },
            ..RustAuthOptions::default()
        },
    )?;
    let cookie = signed_session_cookie("token_1")?;

    let response = router
        .handle_async(json_request(
            Method::POST,
            "/api/auth/delete-user",
            r#"{"password":"secret123"}"#,
            Some(&cookie),
        )?)
        .await?;

    assert_eq!(response.status(), StatusCode::OK);
    let body: Value = serde_json::from_slice(response.body())?;
    assert_eq!(body["success"], true);
    assert_eq!(body["message"], "User deleted");
    assert!(adapter.is_empty("user").await);
    assert!(adapter.is_empty("account").await);
    assert!(adapter.is_empty("session").await);
    assert!(set_cookie_values(&response).iter().any(|cookie| cookie
        .starts_with("rustauth.session_token=;")
        && cookie.contains("Max-Age=0")));
    Ok(())
}

#[tokio::test]
async fn delete_user_route_rejects_wrong_password() -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(RouteAdapter::default());
    let now = OffsetDateTime::now_utc();
    adapter.insert_user(user(now)).await;
    adapter
        .insert_account(credential_account_record(
            "user_1",
            &fast_hash_password("secret123")?,
            now,
        ))
        .await?;
    adapter
        .insert_session(session(now, now + Duration::hours(1)))
        .await;
    let router = router_with_options(
        adapter.clone(),
        RustAuthOptions {
            user: UserOptions {
                delete_user: DeleteUserOptions::builder().enabled(true),
                ..UserOptions::default()
            },
            ..RustAuthOptions::default()
        },
    )?;
    let cookie = signed_session_cookie("token_1")?;

    let response = router
        .handle_async(json_request(
            Method::POST,
            "/api/auth/delete-user",
            r#"{"password":"wrong-password"}"#,
            Some(&cookie),
        )?)
        .await?;

    assert_eq!(response.status(), StatusCode::BAD_REQUEST);
    let body: Value = serde_json::from_slice(response.body())?;
    assert_eq!(body["code"], "INVALID_PASSWORD");
    assert!(contains_record_string(&adapter, "user", "email", "ada@example.com").await?);
    Ok(())
}

#[tokio::test]
async fn delete_user_route_rejects_stale_session_without_password(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(RouteAdapter::default());
    let now = OffsetDateTime::now_utc();
    adapter.insert_user(user(now)).await;
    adapter
        .insert_session(session(now - Duration::hours(48), now + Duration::hours(1)))
        .await;
    let router = router_with_options(
        adapter.clone(),
        RustAuthOptions {
            session: rustauth_core::options::SessionOptions {
                fresh_age: Some(time::Duration::seconds(60 * 60)),
                ..Default::default()
            },
            user: UserOptions {
                delete_user: DeleteUserOptions::builder().enabled(true),
                ..UserOptions::default()
            },
            ..RustAuthOptions::default()
        },
    )?;
    let cookie = signed_session_cookie("token_1")?;

    let response = router
        .handle_async(json_request(
            Method::POST,
            "/api/auth/delete-user",
            "{}",
            Some(&cookie),
        )?)
        .await?;

    assert_eq!(response.status(), StatusCode::BAD_REQUEST);
    let body: Value = serde_json::from_slice(response.body())?;
    assert_eq!(body["code"], "SESSION_EXPIRED");
    Ok(())
}

#[tokio::test]
async fn delete_user_route_sends_verification_instead_of_immediate_delete(
) -> Result<(), Box<dyn std::error::Error>> {
    use rustauth_core::options::DeleteAccountVerificationEmail;
    use std::sync::{Arc, Mutex};

    let adapter = Arc::new(RouteAdapter::default());
    let now = OffsetDateTime::now_utc();
    adapter.insert_user(user(now)).await;
    adapter
        .insert_session(session(now, now + Duration::hours(1)))
        .await;
    let sent = Arc::new(Mutex::new(false));
    struct RecordingSender(Arc<Mutex<bool>>);
    impl rustauth_core::options::SendDeleteAccountVerification for RecordingSender {
        fn send_delete_account_verification(
            &self,
            payload: DeleteAccountVerificationEmail,
            _: Option<&http::Request<Vec<u8>>>,
        ) -> Result<(), rustauth_core::error::RustAuthError> {
            assert!(payload.url.contains("/delete-user/callback?token="));
            assert!(payload.url.contains("callbackURL=%2Fdone"));
            *self
                .0
                .lock()
                .map_err(|_| rustauth_core::error::RustAuthError::Api("lock".into()))? = true;
            Ok(())
        }
    }
    let router = router_with_options(
        adapter.clone(),
        RustAuthOptions {
            user: UserOptions {
                delete_user: DeleteUserOptions::builder()
                    .enabled(true)
                    .send_delete_account_verification(RecordingSender(Arc::clone(&sent))),
                ..UserOptions::default()
            },
            ..RustAuthOptions::default()
        },
    )?;
    let cookie = signed_session_cookie("token_1")?;

    let response = router
        .handle_async(json_request(
            Method::POST,
            "/api/auth/delete-user",
            r#"{"callbackURL":"/done"}"#,
            Some(&cookie),
        )?)
        .await?;

    assert_eq!(response.status(), StatusCode::OK);
    let body: Value = serde_json::from_slice(response.body())?;
    assert_eq!(body["message"], "Verification email sent");
    assert!(*sent.lock().map_err(|_| "lock")?);
    assert!(contains_record_string(&adapter, "user", "email", "ada@example.com").await?);
    Ok(())
}

#[tokio::test]
async fn delete_user_route_rejects_expired_token() -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(RouteAdapter::default());
    let now = OffsetDateTime::now_utc();
    adapter.insert_user(user(now)).await;
    adapter
        .insert_account(credential_account_record(
            "user_1",
            &fast_hash_password("secret123")?,
            now,
        ))
        .await?;
    adapter
        .insert_session(session(now, now + Duration::hours(1)))
        .await;
    adapter
        .create(
            Create::new("verification")
                .data("id", DbValue::String("verification_1".to_owned()))
                .data(
                    "identifier",
                    DbValue::String("delete-account-delete_token".to_owned()),
                )
                .data("value", DbValue::String("user_1".to_owned()))
                .data("expires_at", DbValue::Timestamp(now - Duration::hours(1)))
                .data("created_at", DbValue::Timestamp(now - Duration::hours(2)))
                .data("updated_at", DbValue::Timestamp(now - Duration::hours(2))),
        )
        .await?;
    let router = router_with_options(
        adapter.clone(),
        RustAuthOptions {
            user: UserOptions {
                delete_user: DeleteUserOptions::builder().enabled(true),
                ..UserOptions::default()
            },
            ..RustAuthOptions::default()
        },
    )?;
    let cookie = signed_session_cookie("token_1")?;

    let response = router
        .handle_async(json_request(
            Method::POST,
            "/api/auth/delete-user",
            r#"{"token":"delete_token"}"#,
            Some(&cookie),
        )?)
        .await?;

    assert_eq!(response.status(), StatusCode::NOT_FOUND);
    let body: Value = serde_json::from_slice(response.body())?;
    assert_eq!(body["code"], "INVALID_TOKEN");
    assert!(contains_record_string(&adapter, "user", "email", "ada@example.com").await?);
    assert!(contains_record_string(&adapter, "account", "user_id", "user_1").await?);
    assert!(contains_record_string(&adapter, "session", "token", "token_1").await?);
    Ok(())
}