better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
#![expect(
    clippy::unwrap_used,
    clippy::panic_in_result_fn,
    reason = "The paired native JWT contract asserts exact object identity, callback order, and failure propagation."
)]

use super::native_test_support::{ObservedFields, Trace};
use super::*;
use better_auth_core::{FieldFunction, session::NativeSessionData};
use std::sync::{
    Arc,
    atomic::{AtomicUsize, Ordering},
};

const BASE: &str = "http://jwt-native-callbacks.test";

#[tokio::test]
async fn callbacks_retain_one_root_and_sample_both_clock_boundaries() -> AuthResult<()> {
    let trace = Arc::new(Trace::default());
    let calls = Arc::new(AtomicUsize::new(0));
    let clock_calls = calls.clone();
    let clock_trace = trace.clone();
    let mut context = crate::plugins::test_helpers::create_test_context()
        .await
        .with_clock(move || {
            clock_trace.record("clock");
            Utc.timestamp_millis_opt(
                *[1_700_000_000_123, 1_700_000_005_999]
                    .get(clock_calls.fetch_add(1, Ordering::SeqCst))
                    .unwrap(),
            )
            .single()
            .unwrap()
        });
    let mut config = (*context.config).clone();
    config.base_url = BASE.into();
    context.config = Arc::new(config);
    let old_user: FieldValue = FieldMap::from([("id".into(), "old".into())]).into();
    let child: FieldValue = FieldMap::from([("retained".into(), true.into())]).into();
    let user: FieldValue = FieldMap::from([
        ("id".into(), "replacement".into()),
        ("child".into(), child.clone()),
        ("iat".into(), FieldValue::Undefined),
    ])
    .into();
    let root: FieldValue = FieldMap::from([
        (
            "session".into(),
            FieldMap::from([("token".into(), "before".into())]).into(),
        ),
        ("user".into(), old_user.clone()),
        ("extra".into(), child.clone()),
    ])
    .into();
    let returned: FieldValue = FieldMap::from([("signed".into(), true.into())]).into();
    let payload_root = root.clone();
    let payload_child = child.clone();
    let replacement = user.clone();
    let payload_trace = trace.clone();
    let subject_root = root.clone();
    let subject_user = user.clone();
    let subject_trace = trace.clone();
    let signed = returned.clone();
    let sign_trace = trace.clone();
    let plugin = JwtPlugin::new()
        .define_payload_native(Arc::new(move |data| {
            let root = payload_root.clone();
            let child = payload_child.clone();
            let user = replacement.clone();
            let trace = payload_trace.clone();
            Box::pin(async move {
                trace.record("payload");
                assert!(data.strict_equals(&root));
                assert!(data.model_property("extra")?.strict_equals(&child));
                let _ = data
                    .model_property("session")?
                    .as_object()
                    .unwrap()
                    .set("token", "after".into())?;
                let _ = data.as_object().unwrap().set("user", user.clone())?;
                Ok(user)
            })
        }))
        .get_subject_native(Arc::new(move |data| {
            let root = subject_root.clone();
            let user = subject_user.clone();
            let trace = subject_trace.clone();
            Box::pin(async move {
                trace.record("subject");
                assert!(data.strict_equals(&root));
                assert!(data.model_property("user")?.strict_equals(&user));
                let _ = user.as_object().unwrap().set("id", "final".into())?;
                Ok(FieldValue::Null)
            })
        }))
        .custom_sign_native(Arc::new(move |payload, header, selection| {
            let child = child.clone();
            let returned = signed.clone();
            let trace = sign_trace.clone();
            Box::pin(async move {
                trace.record("sign");
                assert_eq!(
                    payload.enumerable_fields()?,
                    FieldMap::from([
                        ("id".into(), "replacement".into()),
                        ("child".into(), child.clone()),
                        ("iat".into(), FieldValue::Undefined),
                        ("sub".into(), "final".into()),
                        ("exp".into(), 1_700_000_905_i64.into()),
                        ("nbf".into(), FieldValue::Undefined),
                        ("iss".into(), BASE.into()),
                        ("aud".into(), BASE.into()),
                    ])
                );
                assert!(payload.model_property("child")?.strict_equals(&child));
                assert_eq!(
                    payload.as_object().unwrap().own_keys()?,
                    ["iat", "id", "child", "sub", "exp", "nbf", "iss", "aud"]
                );
                assert!(header.is_undefined());
                assert_eq!(
                    selection.as_object().unwrap().own_keys()?,
                    ["signingKeyId", "signingAlgorithm"]
                );
                assert_eq!(
                    selection.enumerable_fields()?,
                    FieldMap::from([
                        ("signingKeyId".into(), FieldValue::Undefined),
                        ("signingAlgorithm".into(), FieldValue::Undefined),
                    ])
                );
                Ok(returned)
            })
        }));
    let data = NativeSessionData::from_native(root.clone(), true);
    let mut endpoint = EndpointContext::new(None, FieldValue::Null, &context);
    endpoint.session = Some(data.clone());
    let token = plugin.sign_session(Some(data), &endpoint).await?;
    assert!(token.strict_equals(&returned));
    assert_eq!(
        root.model_property("session")?.model_property("token")?,
        "after".into()
    );
    assert!(root.model_property("user")?.strict_equals(&user));
    assert_eq!(old_user.model_property("id")?, "old".into());
    assert_eq!(
        *trace.events.lock().unwrap(),
        ["payload", "clock", "subject", "clock", "sign"]
    );
    assert_eq!(calls.load(Ordering::SeqCst), 2);
    Ok(())
}

#[tokio::test]
async fn subject_only_falls_back_for_nullish_values() -> AuthResult<()> {
    let context = crate::plugins::test_helpers::create_test_context().await;
    for subject in [
        FieldValue::Undefined,
        FieldValue::Null,
        false.into(),
        0.into(),
        "".into(),
    ] {
        let expected = if subject.is_null() || subject.is_undefined() {
            "owner".into()
        } else {
            subject.clone()
        };
        let root: FieldValue = FieldMap::from([
            ("session".into(), FieldMap::new().into()),
            (
                "user".into(),
                FieldMap::from([("id".into(), "owner".into())]).into(),
            ),
        ])
        .into();
        let subject_root = root.clone();
        let plugin = JwtPlugin::new()
            .get_subject_native(Arc::new(move |data| {
                assert!(data.strict_equals(&subject_root));
                let subject = subject.clone();
                Box::pin(async move { Ok(subject) })
            }))
            .custom_sign_native(Arc::new(|payload, _, _| {
                Box::pin(async move { payload.model_property("sub") })
            }));
        let endpoint = EndpointContext::new(None, FieldValue::Null, &context);
        assert_eq!(
            plugin
                .sign_session(Some(NativeSessionData::from_native(root, true)), &endpoint)
                .await?,
            expected
        );
    }
    Ok(())
}

#[tokio::test]
async fn callback_failures_keep_identity_order_and_completed_mutations() -> AuthResult<()> {
    for phase in ["payload", "spread", "subject", "sign"] {
        let trace = Arc::new(Trace::default());
        let clock_trace = trace.clone();
        let context = crate::plugins::test_helpers::create_test_context()
            .await
            .with_clock(move || {
                clock_trace.record("clock");
                Utc.timestamp_millis_opt(1_700_000_000_000)
                    .single()
                    .unwrap()
            });
        let failure: FieldValue = FieldMap::from([("phase".into(), phase.into())]).into();
        let root: FieldValue = FieldMap::from([
            ("session".into(), FieldMap::new().into()),
            (
                "user".into(),
                FieldMap::from([("id".into(), "owner".into())]).into(),
            ),
            ("changed".into(), false.into()),
        ])
        .into();
        let payload = ObservedFields::new(
            FieldMap::from([("value".into(), true.into())]),
            trace.clone(),
            (phase == "spread").then(|| ("value", failure.clone())),
        );
        let payload_failure = failure.clone();
        let payload_trace = trace.clone();
        let subject_failure = failure.clone();
        let subject_trace = trace.clone();
        let sign_failure = failure.clone();
        let sign_trace = trace.clone();
        let plugin = JwtPlugin::new()
            .define_payload_native(Arc::new(move |data| {
                let failure = payload_failure.clone();
                let payload = payload.value();
                let trace = payload_trace.clone();
                Box::pin(async move {
                    trace.record("payload");
                    let _ = data.as_object().unwrap().set("changed", true.into())?;
                    if phase == "payload" {
                        return Err(AuthError::from_thrown(failure));
                    }
                    Ok(payload)
                })
            }))
            .get_subject_native(Arc::new(move |_| {
                let trace = subject_trace.clone();
                let failure = subject_failure.clone();
                Box::pin(async move {
                    trace.record("subject");
                    if phase == "subject" {
                        return Err(AuthError::from_thrown(failure));
                    }
                    Ok("owner".into())
                })
            }))
            .custom_sign_native(Arc::new(move |_, _, _| {
                let trace = sign_trace.clone();
                let failure = sign_failure.clone();
                Box::pin(async move {
                    trace.record("sign");
                    Err(AuthError::from_thrown(failure))
                })
            }));
        let endpoint = EndpointContext::new(None, FieldValue::Null, &context);
        let error = plugin
            .sign_session(
                Some(NativeSessionData::from_native(root.clone(), true)),
                &endpoint,
            )
            .await
            .unwrap_err();
        assert!(error.raw_value().unwrap().strict_equals(&failure));
        assert_eq!(root.model_property("changed")?, true.into());
        let expected = ["payload", "clock", "get:value", "subject", "clock", "sign"];
        let last = if phase == "spread" {
            "get:value"
        } else {
            phase
        };
        let length = expected.iter().position(|event| *event == last).unwrap() + 1;
        assert_eq!(
            *trace.events.lock().unwrap(),
            expected.get(..length).unwrap()
        );
    }
    Ok(())
}

#[tokio::test]
async fn signing_caches_default_reads_before_spread_and_preserves_native_leaves() -> AuthResult<()>
{
    let trace = Arc::new(Trace::default());
    let child: FieldValue = FieldMap::from([("leaf".into(), true.into())]).into();
    let source = ObservedFields::new(
        FieldMap::from([
            ("iat".into(), 10.into()),
            ("exp".into(), FieldValue::Null),
            ("nbf".into(), FieldValue::Undefined),
            ("iss".into(), FieldValue::Null),
            ("aud".into(), FieldValue::Undefined),
            ("child".into(), child.clone()),
        ]),
        trace.clone(),
        None,
    );
    let reads = Arc::new(AtomicUsize::new(0));
    source.getter(
        "iat",
        FieldFunction::contextual(move |_, _| {
            Ok(if reads.fetch_add(1, Ordering::SeqCst) == 0 {
                10.into()
            } else {
                20.into()
            })
        }),
    )?;
    let returned: FieldValue = FieldMap::from([("signed".into(), true.into())]).into();
    let signed = returned.clone();
    let plugin = JwtPlugin::new()
        .issuer(BASE.to_owned())
        .audience(BASE.into())
        .custom_sign_native(Arc::new(move |payload, _, _| {
            let child = child.clone();
            let returned = signed.clone();
            Box::pin(async move {
                assert_eq!(
                    payload.enumerable_fields()?,
                    FieldMap::from([
                        ("iat".into(), 10.into()),
                        ("exp".into(), 910.into()),
                        ("nbf".into(), FieldValue::Undefined),
                        ("iss".into(), BASE.into()),
                        ("aud".into(), BASE.into()),
                        ("child".into(), child.clone()),
                    ])
                );
                assert!(payload.model_property("child")?.strict_equals(&child));
                Ok(returned)
            })
        }));
    let context = crate::plugins::test_helpers::create_test_context().await;
    let endpoint = EndpointContext::new(None, FieldValue::Null, &context);
    let token = plugin
        .sign_value_in_endpoint(source.value(), None, &endpoint)
        .await?;
    assert!(token.strict_equals(&returned));
    assert_eq!(
        *trace.events.lock().unwrap(),
        [
            "get:iat",
            "get:exp",
            "get:nbf",
            "get:iss",
            "get:aud",
            "get:iat",
            "get:exp",
            "get:nbf",
            "get:iss",
            "get:aud",
            "get:child"
        ]
    );
    Ok(())
}

use chrono::TimeZone as _;