better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
#![allow(
    clippy::expect_used,
    reason = "Fixture setup and response assertions require successful fallible construction."
)]
use super::*;
use crate::plugins::organization::{OrganizationPlugin, OrganizationTeamsConfig};
use crate::plugins::test_helpers::{
    create_auth_json_request_no_query, create_test_config, create_test_context_with_plugins,
    create_user_and_session,
};
use better_auth_core::{
    CreateMember, CreateOrganization, CreateUser, user_fields::UserFieldConfig,
};
use serde_json::{Value, json};
use std::sync::{
    Arc,
    atomic::{AtomicUsize, Ordering},
};

#[derive(Default)]
struct Callbacks {
    limits: AtomicUsize,
    updates: AtomicUsize,
}

#[async_trait::async_trait]
impl OrganizationPolicy for Callbacks {
    async fn maximum_teams(
        &self,
        _: OrganizationTeamLimit<'_>,
        _: OrganizationEndpoint<'_>,
    ) -> AuthResult<Option<usize>> {
        self.limits.fetch_add(1, Ordering::SeqCst);
        Ok(Some(100))
    }
}

#[async_trait::async_trait]
impl OrganizationHooks for Callbacks {
    async fn before_update_team(
        &self,
        _: &mut better_auth_core::UpdateTeam,
        _: OrganizationTeamEvent<'_>,
    ) -> AuthResult<()> {
        self.updates.fetch_add(1, Ordering::SeqCst);
        Ok(())
    }
}

#[tokio::test]
async fn invalid_team_input_precedes_policies_and_preserves_null_errors() {
    let callbacks = Arc::new(Callbacks::default());
    let mut config = OrganizationConfig {
        teams: OrganizationTeamsConfig {
            enabled: true,
            ..Default::default()
        },
        policy: Some(callbacks.clone()),
        hooks: Some(callbacks.clone()),
        ..Default::default()
    };
    let ctx = create_test_context_with_plugins(
        create_test_config(),
        &[&OrganizationPlugin::with_config(config.clone())],
    )
    .await;
    let (user, session) = create_user_and_session(
        &ctx,
        CreateUser::new()
            .with_email("team-input@example.com")
            .with_name("Fixture"),
        chrono::Duration::hours(1),
    )
    .await;
    let org = ctx
        .database
        .create_organization(CreateOrganization::new("Input", "team-input"))
        .await
        .unwrap();
    ctx.database
        .create_member(CreateMember {
            organization_id: org.id.clone(),
            user_id: user.id,
            role: "owner".into(),
            additional_fields: Default::default(),
        })
        .await
        .unwrap();
    ctx.database
        .update_session_active_organization(
            session.token.typed().unwrap(),
            Some(org.id.typed().unwrap()),
        )
        .await
        .unwrap();
    config.schema.team.fields_mut().insert(
        "label".into(),
        UserFieldConfig {
            required: Some(true),
            ..Default::default()
        },
    );
    let create = |body| {
        create_auth_json_request_no_query(
            HttpMethod::Post,
            "/organization/create-team",
            Some(session.token.typed().unwrap()),
            Some(body),
        )
    };
    for (body, field, received) in [
        (json!({"name":"Team"}), "label", "undefined"),
        (
            json!({"name":"Team", "label":"valid", "organizationId":null}),
            "organizationId",
            "null",
        ),
    ] {
        let response = handle_team_request(&create(body), &ctx, &config)
            .await
            .unwrap_err()
            .to_auth_response()
            .expect("fixture response succeeds");
        assert_eq!(
            response.api_error_status().unwrap(),
            better_auth_core::SchemaValue::Typed(400)
        );
        assert_eq!(
            response.api_error_body().unwrap().json().unwrap().unwrap(),
            json!({
                "code":"VALIDATION_ERROR",
                "message":format!("[body.{field}] Invalid input: expected string, received {received}")
            })
        );
    }
    assert_eq!(callbacks.limits.load(Ordering::SeqCst), 0);
    let response = handle_team_request(
        &create(json!({"name":"Team", "label":"valid"})),
        &ctx,
        &config,
    )
    .await
    .unwrap()
    .unwrap();
    assert_eq!(response.status, 200);
    assert_eq!(callbacks.limits.load(Ordering::SeqCst), 1);
    let team = serde_json::from_slice::<Value>(&response.body.bytes().unwrap()).unwrap();
    let update = |data| {
        create_auth_json_request_no_query(
            HttpMethod::Post,
            "/organization/update-team",
            Some(session.token.typed().unwrap()),
            Some(json!({"teamId":team["id"], "data":data})),
        )
    };
    for name_policy in [None, Some(true), Some(false)] {
        if let Some(input) = name_policy {
            config.schema.team.fields_mut().insert(
                "name".into(),
                UserFieldConfig {
                    required: Some(true),
                    input: Some(input),
                    ..Default::default()
                },
            );
        }
        for field in ["name", "organizationId"] {
            let response = handle_team_request(&update(json!({field:null})), &ctx, &config)
                .await
                .unwrap_err()
                .to_auth_response()
                .expect("fixture response succeeds");
            assert_eq!(
                response.api_error_status().unwrap(),
                better_auth_core::SchemaValue::Typed(400)
            );
            assert_eq!(
                response.api_error_body().unwrap().json().unwrap().unwrap(),
                json!({
                    "code":"VALIDATION_ERROR",
                    "message":format!("[body.data.{field}] Invalid input: expected string, received null")
                })
            );
        }
    }
    assert_eq!(callbacks.updates.load(Ordering::SeqCst), 0);
    let response = handle_team_request(&update(json!({})), &ctx, &config)
        .await
        .unwrap()
        .unwrap();
    assert_eq!(response.status, 200);
    assert_eq!(callbacks.updates.load(Ordering::SeqCst), 1);
    assert_eq!(
        serde_json::from_slice::<Value>(&response.body.bytes().unwrap()).unwrap()["name"],
        "Team"
    );
}