better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
use super::{SessionIssueError, issue_selected_user_session_optional, issue_user_session};
use crate::plugins::{
    admin::AdminPlugin,
    test_helpers::{create_test_config, initialize_test_context},
};
use better_auth_core::{
    AuthContext, AuthError, AuthResult, CreateUser, FieldMap, FieldValue, RequestMeta,
    store::{
        EphemeralStore, StatelessSchema,
        database_hooks::{DatabaseHookContext, DatabaseHookUpdate, DatabaseHooks},
    },
};
use std::sync::{Arc, Mutex};

#[derive(Default)]
struct SessionOwners(Mutex<Vec<FieldValue>>);

#[better_auth_core::database_hooks()]
impl DatabaseHooks<StatelessSchema> for SessionOwners {
    async fn before_create_session(
        &self,
        fields: &mut FieldMap,
        _: &DatabaseHookContext<'_, StatelessSchema>,
    ) -> AuthResult<DatabaseHookUpdate<FieldMap>> {
        self.0
            .lock()
            .unwrap()
            .push(fields.get("userId").cloned().unwrap_or_default());
        Ok(DatabaseHookUpdate::Continue)
    }
}

async fn context(owners: &Arc<SessionOwners>) -> AuthResult<AuthContext<StatelessSchema>> {
    let config = Arc::new(create_test_config());
    let store = EphemeralStore::new(config.clone()).with_hooks(vec![owners.clone()]);
    initialize_test_context(config, Arc::new(store), &[&AdminPlugin::new()]).await
}

#[tokio::test]
async fn selected_users_without_an_admin_identity_reach_session_storage() -> AuthResult<()> {
    let owners = Arc::new(SessionOwners::default());
    let ctx = context(&owners).await?;
    let ids = [
        FieldValue::Undefined,
        FieldValue::Null,
        false.into(),
        0.0.into(),
        "".into(),
        "missing-user".into(),
        7.0.into(),
    ];
    for id in &ids {
        let user: FieldValue = if id.is_undefined() {
            Vec::<FieldValue>::new().into()
        } else {
            FieldMap::from([("id".into(), id.clone())]).into()
        };
        let issued = issue_selected_user_session_optional(
            &ctx,
            user.clone(),
            &RequestMeta::default(),
            ctx.config.session.expires_in(),
        )
        .await
        .map_err(SessionIssueError::into_auth_error)?
        .ok_or_else(|| AuthError::internal("Expected created Session"))?;
        assert!(issued.user_value()?.strict_equals(&user));
        let stored = ctx
            .database
            .get_session(issued.session_property("token")?.as_str().unwrap())
            .await?
            .ok_or_else(|| AuthError::internal("Expected persisted Session"))?;
        assert_eq!(stored.id.field_value(), issued.session_property("id")?);
        assert_eq!(
            stored.user_id.field_value(),
            issued.session_property("userId")?
        );
    }
    assert_eq!(*owners.0.lock().unwrap(), ids);
    assert!(matches!(
        issue_user_session(&ctx, "missing-user", None, None).await,
        Err(SessionIssueError::Auth(AuthError::UserNotFound))
    ));
    Ok(())
}

#[tokio::test]
async fn selected_banned_user_is_rejected_before_session_hooks() -> AuthResult<()> {
    let owners = Arc::new(SessionOwners::default());
    let ctx = context(&owners).await?;
    let user = ctx
        .database
        .create_user(CreateUser {
            email: Some("banned@session.test".into()),
            banned: Some(true),
            ..Default::default()
        })
        .await?;
    let result = issue_selected_user_session_optional(
        &ctx,
        FieldMap::from(user.clone()).into(),
        &RequestMeta::default(),
        ctx.config.session.expires_in(),
    )
    .await;
    let Err(error @ SessionIssueError::Banned { .. }) = result else {
        panic!("Session admission must reject a banned user");
    };
    let message = error.banned_message().unwrap().to_owned();
    let error = error.into_auth_error();
    assert!(error.is_api_error_instance(better_auth_core::ApiErrorClass::BetterAuth));
    assert_eq!(error.status_value()?, FieldValue::from("FORBIDDEN"));
    assert_eq!(
        error.body_value()?.json()?,
        Some(serde_json::json!({"message": message, "code": "BANNED_USER"}))
    );
    assert!(owners.0.lock().unwrap().is_empty());
    assert!(
        ctx.database
            .get_user_sessions(user.id.typed()?)
            .await?
            .is_empty()
    );
    Ok(())
}