better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
use super::*;
use better_auth_core::user_fields::{UserFieldFactory, UserFieldType};
use better_auth_core::{SchemaValue, UpdateAccount};

const UPDATED: f64 = 2_000_000_000_000.0;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Mutation {
    None,
    Provider,
    Replacement,
}

struct MutatingHasher<S: AuthSchema> {
    state: Arc<State>,
    database: Arc<dyn AuthStore<S>>,
    account_id: FieldValue,
    user_id: SchemaValue<String>,
    mutation: Mutation,
    after_callback: Mutex<Option<FieldMap>>,
}

#[async_trait]
impl<S: AuthSchema> PasswordHasher for MutatingHasher<S> {
    async fn hash(&self, password: &str) -> AuthResult<String> {
        self.state.hash(password).await
    }

    async fn verify(&self, _: &str, _: &str) -> AuthResult<bool> {
        Err(AuthError::internal(
            "The native verifier must receive the original hash",
        ))
    }

    async fn verify_native(&self, hash: &FieldValue, password: &str) -> AuthResult<FieldValue> {
        let result = self.state.verify_native(hash, password).await?;
        match self.mutation {
            Mutation::None => {}
            Mutation::Provider => {
                let _ = self
                    .database
                    .update_account_by_id_value(
                        &self.account_id,
                        UpdateAccount {
                            provider_id: "changed-provider".into(),
                            ..Default::default()
                        },
                    )
                    .await?;
            }
            Mutation::Replacement => {
                self.database.delete_account_value(&self.account_id).await?;
                let _ = self
                    .database
                    .create_account(CreateAccount {
                        user_id: self.user_id.clone(),
                        account_id: self.user_id.clone(),
                        provider_id: "credential".into(),
                        password: Some("replacement-hash".to_owned()).into(),
                        ..Default::default()
                    })
                    .await?;
            }
        }
        let records = self
            .database
            .get_user_accounts(self.user_id.typed()?)
            .await?;
        assert_eq!(records.len(), 1);
        *self.after_callback.lock().unwrap() = records
            .into_iter()
            .next()
            .map(|account| account.field_values())
            .transpose()?;
        Ok(result)
    }
}

async fn check_selected_account<S: AuthSchema>(
    database: Arc<dyn AuthStore<S>>,
    mutation: Mutation,
) -> AuthResult<()> {
    let mut config = test_helpers::create_test_config().disable_session_refresh(true);
    let _ = config.account.additional_fields.insert(
        "updatedAt".into(),
        UserFieldConfig {
            field_type: UserFieldType::Date,
            required: Some(true),
            on_update: Some(UserFieldFactory::new(|| {
                Ok(better_auth_core::FieldDate::from_milliseconds(UPDATED).into())
            })),
            ..Default::default()
        },
    );
    let raw =
        test_helpers::initialize_test_context(Arc::new(config.clone()), database.clone(), &[])
            .await?;
    let user = raw
        .database
        .create_user(
            CreateUser::new()
                .with_name("Owner")
                .with_email("owner@selected-account.test"),
        )
        .await?;
    let account = raw
        .database
        .create_account(CreateAccount {
            user_id: user.id.clone(),
            account_id: user.id.clone(),
            provider_id: "credential".into(),
            password: Some("stored-hash".to_owned()).into(),
            ..Default::default()
        })
        .await?;
    let session =
        test_helpers::create_session(&raw, user.id.typed()?.clone(), chrono::Duration::hours(1))
            .await;
    let sessions = raw
        .database
        .get_user_sessions(user.id.typed()?)
        .await?
        .into_iter()
        .map(FieldMap::from)
        .collect::<Vec<_>>();
    let state = Arc::new(State::default());
    *state.projected.lock().unwrap() = native_hash();
    *state.result.lock().unwrap() = true.into();
    let hasher = Arc::new(MutatingHasher {
        state: state.clone(),
        database: raw.database.clone(),
        account_id: account.id.field_value(),
        user_id: user.id.clone(),
        mutation,
        after_callback: Mutex::new(None),
    });
    let output_state = state.clone();
    let _ =
        config.account.additional_fields.insert(
            "password".into(),
            UserFieldConfig {
                required: Some(false),
                returned: Some(false),
                transform: Some(FieldTransforms {
                    input: None,
                    output: Some(UserFieldTransform::new(move |value| {
                        output_state.events.lock().unwrap().push(
                            if value == "stored-hash".into() {
                                "projection"
                            } else {
                                assert_eq!(value, "new-hash".into());
                                "projection-updated"
                            },
                        );
                        Ok(output_state.projected.lock().unwrap().clone())
                    })),
                }),
                ..Default::default()
            },
        );
    let email = EmailPasswordPlugin::new().password_hasher(hasher.clone());
    let password = PasswordManagementPlugin::new();
    let context =
        test_helpers::initialize_test_context(Arc::new(config), database, &[&email, &password])
            .await?;
    let expected_user = serde_json::to_value(context.user_view(&user).await?)?;
    let req = test_helpers::create_auth_json_request_no_query(
        HttpMethod::Post,
        "/change-password",
        Some(session.token.typed()?),
        Some(json!({"currentPassword":PASSWORD,"newPassword":PASSWORD})),
    );
    let response = password
        .on_request(&req, context.initialize_request_context().await?.as_ref())
        .await?
        .unwrap();
    assert_eq!(
        response.body.json()?,
        Some(json!({"token":null,"user":expected_user}))
    );
    assert_eq!(
        *state.events.lock().unwrap(),
        if mutation == Mutation::Replacement {
            vec!["projection", "hash", "verify"]
        } else {
            vec!["projection", "hash", "verify", "projection-updated"]
        }
    );
    let mut expected = hasher.after_callback.lock().unwrap().take().unwrap();
    if mutation != Mutation::Replacement {
        let _ = expected.insert("password".into(), "new-hash".into());
        let _ = expected.insert(
            "updatedAt".into(),
            better_auth_core::FieldDate::from_milliseconds(UPDATED).into(),
        );
    } else {
        assert_ne!(expected.get("id"), Some(&account.id.field_value()));
    }
    assert_eq!(
        raw.database
            .get_user_accounts(user.id.typed()?)
            .await?
            .into_iter()
            .map(|account| account.field_values())
            .collect::<AuthResult<Vec<_>>>()?,
        vec![expected]
    );
    assert_eq!(
        raw.database
            .get_user_sessions(user.id.typed()?)
            .await?
            .into_iter()
            .map(FieldMap::from)
            .collect::<Vec<_>>(),
        sessions
    );
    assert_eq!(
        raw.database
            .get_user_by_id(user.id.typed()?)
            .await?
            .unwrap(),
        user
    );
    Ok(())
}

#[tokio::test]
async fn change_password_updates_the_first_account_after_verifier_mutation() -> AuthResult<()> {
    for mutation in [Mutation::None, Mutation::Provider, Mutation::Replacement] {
        let memory: Arc<dyn AuthStore<StatelessSchema>> = Arc::new(EphemeralStore::default());
        check_selected_account(memory, mutation).await?;
        check_selected_account(test_helpers::create_test_database().await, mutation).await?;
    }
    Ok(())
}