better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
use super::*;
use better_auth_core::{FieldDate, FieldFunction};

#[test]
fn token_record_preserves_presence_order_and_slot_mutation() {
    let mut tokens = OAuthTokenSet::from_fields(FieldMap::from([
        ("extension".into(), 7.0.into()),
        ("idToken".into(), FieldValue::Undefined),
        ("accessToken".into(), FieldValue::Null),
        ("scopes".into(), vec!["openid".into()].into()),
    ]));
    assert!(!tokens.additional_fields.contains_key("accessToken"));
    assert!(!tokens.additional_fields.contains_key("idToken"));
    assert!(tokens.refresh_token.is_undefined());
    assert!(matches!(
        tokens.id_token,
        SchemaValue::Dynamic(FieldValue::Undefined)
    ));
    assert!(tokens.access_token.field_value().is_null());
    let fields = tokens.fields();
    assert_eq!(
        fields.keys().map(String::as_str).collect::<Vec<_>>(),
        ["extension", "idToken", "accessToken", "scopes"]
    );
    assert!(fields.get("idToken").is_some_and(FieldValue::is_undefined));
    tokens.access_token = Some("replacement".to_owned()).into();
    tokens.id_token = SchemaValue::Undefined;
    tokens.field_order.push("refreshToken".into());
    let fields = tokens.fields();
    assert_eq!(fields.get("accessToken"), Some(&"replacement".into()));
    assert!(!fields.contains_key("idToken"));
    assert!(!fields.contains_key("refreshToken"));
    assert_eq!(fields.get("extension"), Some(&7.0.into()));
}

#[test]
fn native_token_handles_survive_request_conversion() {
    let object: FieldValue = FieldMap::from([("claim".into(), true.into())]).into();
    let array: FieldValue = vec!["openid".into(), FieldValue::Null, 3.0.into()].into();
    let date: FieldValue = FieldDate::from_milliseconds(1_700_000_000_000.0).into();
    let fields = FieldMap::from([
        ("accessToken".into(), object.clone()),
        ("refreshToken".into(), 42.0.into()),
        ("accessTokenExpiresAt".into(), date.clone()),
        ("scopes".into(), array.clone()),
        ("raw".into(), object.clone()),
        ("custom".into(), array.clone()),
    ]);
    let tokens = OAuthTokenSet::from_fields(fields);
    let request = OAuthUserInfoRequest::from(tokens.clone());
    for actual in [tokens.fields(), request.fields()] {
        for (name, expected) in [
            ("accessToken", &object),
            ("raw", &object),
            ("scopes", &array),
            ("custom", &array),
            ("accessTokenExpiresAt", &date),
        ] {
            assert!(
                actual
                    .get(name)
                    .is_some_and(|value| value.strict_equals(expected)),
                "{name}"
            );
        }
        assert_eq!(actual.get("refreshToken"), Some(&42.0.into()));
    }
}

#[test]
fn scope_consumers_apply_join_and_trim_at_their_own_boundaries() -> AuthResult<()> {
    let scopes = SchemaValue::Dynamic(vec![" openid ".into(), FieldValue::Null, 7.0.into()].into());
    assert_eq!(
        joined_scopes(&scopes.field_value())?.display_utf16()?,
        " openid ,,7".into()
    );
    assert!(merge_scopes(&"email".into(), &scopes).is_err());
    let scopes =
        SchemaValue::Dynamic(vec![" openid ".into(), "email".into(), "profile".into()].into());
    assert_eq!(
        merge_scopes(&"email, openid".into(), &scopes)?.display_utf16()?,
        "email,openid,profile".into()
    );
    assert!(joined_scopes(&7.0.into()).is_err());
    assert!(joined_scopes(&FieldValue::Null)?.is_undefined());
    Ok(())
}

#[test]
fn callback_metadata_preserves_real_presence_order_and_extension_fields() -> AuthResult<()> {
    let tokens = OAuthTokenSet::from_fields(FieldMap::from([
        ("accessToken".into(), "access".into()),
        ("user".into(), "overridden".into()),
    ]));
    let request = crate::plugins::oauth::handlers::callback_user_request(
        &tokens.fields().into(),
        &FieldValue::Undefined,
        None,
    )?;
    assert!(request.user.field_value().is_undefined());
    assert!(request.fields().contains_key("user"));
    assert!(!request.fields().contains_key("expectedIdTokenNonce"));
    for input in [Some("null"), Some("invalid JSON")] {
        let request = crate::plugins::oauth::handlers::callback_user_request(
            &tokens.fields().into(),
            &input.map(FieldValue::from).unwrap_or_default(),
            None,
        )?;
        assert!(
            request
                .fields()
                .get("user")
                .is_some_and(FieldValue::is_undefined)
        );
    }
    let tokens =
        OAuthTokenSet::from_fields(FieldMap::from([("accessToken".into(), "access".into())]));
    let request = crate::plugins::oauth::handlers::callback_user_request(
        &tokens.fields().into(),
        &r#"{"name":{"firstName":"Ada"},"custom":{"retained":true}}"#.into(),
        Some("nonce"),
    )?;
    let fields = request.fields();
    assert_eq!(
        fields.keys().map(String::as_str).collect::<Vec<_>>(),
        ["accessToken", "expectedIdTokenNonce", "user"]
    );
    assert!(
        request
            .user
            .field_value()
            .model_property("custom")?
            .model_property("retained")?
            .is_truthy()
    );
    assert!(
        fields
            .get("user")
            .is_some_and(|value| value.strict_equals(&request.user.field_value()))
    );
    let existing = OAuthTokenSet::from_fields(FieldMap::from([
        ("user".into(), "old user".into()),
        ("custom".into(), true.into()),
        ("expectedIdTokenNonce".into(), "old nonce".into()),
    ]));
    let request = crate::plugins::oauth::handlers::callback_user_request(
        &existing.fields().into(),
        &"false".into(),
        Some("new nonce"),
    )?;
    assert_eq!(
        request
            .fields()
            .keys()
            .map(String::as_str)
            .collect::<Vec<_>>(),
        ["user", "custom", "expectedIdTokenNonce"]
    );
    assert_eq!(request.user.field_value(), false.into());
    assert_eq!(
        request.expected_id_token_nonce.field_value(),
        "new nonce".into()
    );
    Ok(())
}

#[test]
fn scope_join_ignores_to_string_and_retains_custom_method_results() -> AuthResult<()> {
    let scopes: FieldValue = vec!["openid".into(), "email".into()].into();
    scopes.set_model_property(
        "toString",
        FieldFunction::contextual(|_, _| {
            Err(AuthError::internal("Scope joining must not call toString"))
        })
        .into(),
    )?;
    assert_eq!(joined_scopes(&scopes)?, "openid,email".into());

    for scopes in [
        scopes,
        FieldMap::new().into(),
        FieldDate::from_milliseconds(0.0).into(),
        FieldFunction::contextual(|_, _| Ok(FieldValue::Undefined)).into(),
    ] {
        let result: FieldValue = FieldMap::from([("native".into(), true.into())]).into();
        let expected_receiver = scopes.clone();
        let expected_result = result.clone();
        scopes.set_model_property(
            "join",
            FieldFunction::contextual(move |receiver, arguments| {
                assert!(receiver.strict_equals(&expected_receiver));
                assert_eq!(arguments, &[FieldValue::from(",")]);
                Ok(expected_result.clone())
            })
            .into(),
        )?;
        let joined = joined_scopes(&scopes);
        scopes.set_model_property("join", FieldValue::Undefined)?;
        assert!(joined?.strict_equals(&result));
    }
    Ok(())
}

#[test]
fn scope_join_rejects_noncallable_array_overrides_and_preserves_original_errors() -> AuthResult<()>
{
    let scopes: FieldValue = vec!["openid".into(), "email".into()].into();
    for method in [FieldValue::Undefined, FieldValue::Null, false.into()] {
        scopes.set_model_property("join", method)?;
        assert!(
            matches!(joined_scopes(&scopes), Err(AuthError::TypeError(message)) if message == "tokens.scopes?.join is not a function")
        );
    }
    let original: FieldValue = FieldMap::from([("message".into(), "join error".into())]).into();
    let error = original.clone();
    scopes.set_model_property(
        "join",
        FieldFunction::contextual(move |_, _| Err(AuthError::from_thrown(error.clone()))).into(),
    )?;
    let result = joined_scopes(&scopes);
    assert!(
        matches!(result, Err(error) if error.raw_value().is_some_and(|value| value.strict_equals(&original)))
    );
    for nullish in [FieldValue::Undefined, FieldValue::Null] {
        assert!(joined_scopes(&nullish)?.is_undefined());
    }
    Ok(())
}