better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
use super::*;
use better_auth_core::{
    FieldArray, FieldObject,
    field_value::{FieldObjectSource, FieldPropertyDescriptor},
};

#[derive(Debug)]
struct Call {
    name: String,
    receiver: FieldValue,
    arguments: Vec<FieldValue>,
}

#[derive(Clone, Default)]
struct Calls(Arc<Mutex<Vec<Call>>>);

impl Calls {
    fn record(&self, name: impl Into<String>, receiver: &FieldValue, arguments: &[FieldValue]) {
        self.0.lock().unwrap().push(Call {
            name: name.into(),
            receiver: receiver.clone(),
            arguments: arguments.to_vec(),
        });
    }

    fn drain(&self) -> Vec<Call> {
        std::mem::take(&mut *self.0.lock().unwrap())
    }
}

struct Properties {
    fields: FieldObject,
    calls: Calls,
    failure: Option<(&'static str, FieldValue)>,
}

impl FieldObjectSource for Properties {
    fn own_keys(&self) -> AuthResult<Vec<String>> {
        self.fields.own_keys()
    }

    fn own_property_descriptor(&self, name: &str) -> AuthResult<Option<FieldPropertyDescriptor>> {
        self.fields.own_property_descriptor(name)
    }

    fn get(&self, _: &str) -> AuthResult<Option<FieldValue>> {
        Err(AuthError::internal(
            "Property reads must retain the native receiver",
        ))
    }

    fn get_with_receiver(
        &self,
        name: &str,
        receiver: &FieldValue,
    ) -> AuthResult<Option<FieldValue>> {
        if !self.fields.has_property(name)? {
            return Ok(None);
        }
        self.calls.record(format!("get:{name}"), receiver, &[]);
        if let Some((failed, error)) = &self.failure
            && name == *failed
        {
            return Err(AuthError::from_thrown(error.clone()));
        }
        self.fields.get(name)
    }

    fn set(&self, name: String, value: FieldValue) -> AuthResult<Option<FieldValue>> {
        self.fields.set(name, value)
    }

    fn delete(&self, name: &str) -> AuthResult<Option<FieldValue>> {
        self.fields.delete(name)
    }

    fn snapshot_fields(&self) -> AuthResult<FieldMap> {
        Err(AuthError::internal(
            "Method calls must not snapshot native properties",
        ))
    }

    fn identity(&self) -> usize {
        std::ptr::from_ref(self) as usize
    }
}

fn properties(fields: FieldMap, calls: &Calls) -> FieldObject {
    FieldObject::from_source(Arc::new(Properties {
        fields: fields.into(),
        calls: calls.clone(),
        failure: None,
    }))
}

#[tokio::test]
async fn put_invokes_the_original_includes_and_consumes_its_native_truthiness() {
    for (result, expected) in [
        (FieldValue::from(FieldMap::new()), r#"["changed"]"#),
        (FieldValue::from(""), r#"["changed",["key"]]"#),
    ] {
        let cache = Arc::new(Cache::default());
        let id: FieldValue = vec!["key".into()].into();
        let key = record(id.clone());
        let calls = Calls::default();
        let observed = calls.clone();
        let includes = FieldFunction::contextual(move |receiver, arguments| {
            observed.record("call:includes", receiver, arguments);
            receiver.set_model_property("0", "changed".into())?;
            Ok(result.clone())
        });
        let index: FieldValue = FieldArray::from_values_with_properties(
            vec!["before".into()],
            properties(
                FieldMap::from([("includes".into(), includes.into())]),
                &calls,
            ),
        )
        .into();
        cache.seed(INDEX, index.clone());
        put(cache.clone(), &key, false).await.unwrap();
        let observations = calls.drain();
        assert_eq!(
            observations
                .iter()
                .map(|call| call.name.as_str())
                .collect::<Vec<_>>(),
            ["get:includes", "call:includes"]
        );
        assert!(
            observations
                .iter()
                .all(|call| call.receiver.strict_equals(&index))
        );
        assert!(observations.first().unwrap().arguments.is_empty());
        let arguments = &observations.get(1).unwrap().arguments;
        assert_eq!(arguments.len(), 1);
        assert!(arguments.first().unwrap().strict_equals(&id));
        assert_eq!(cache.value(INDEX), Some(expected.into()));
        let serialized = super::super::super::serialize(&key).unwrap();
        assert_eq!(
            *cache.writes.lock().unwrap(),
            [
                Write::Set("api-key:hash".into(), serialized.clone()),
                Write::Set("api-key:by-id:key".into(), serialized),
                Write::Set(INDEX.into(), expected.into()),
            ]
        );
    }
}

#[tokio::test]
async fn remove_passes_the_native_predicate_and_keeps_the_custom_filter_result_until_serialization()
{
    let cache = Arc::new(Cache::default());
    let id: FieldValue = vec!["key".into()].into();
    let key = record(id.clone());
    let calls = Calls::default();
    let observed = calls.clone();
    let to_json = FieldFunction::contextual(move |receiver, arguments| {
        observed.record("call:toJSON", receiver, arguments);
        Ok("custom-reference".into())
    });
    let result: FieldValue = properties(
        FieldMap::from([
            ("length".into(), "0".into()),
            ("toJSON".into(), to_json.into()),
        ]),
        &calls,
    )
    .into();
    let returned = result.clone();
    let observed = calls.clone();
    let predicate_results = Arc::new(Mutex::new(Vec::new()));
    let compared = predicate_results.clone();
    let filter = FieldFunction::contextual(move |receiver, arguments| {
        observed.record("call:filter", receiver, arguments);
        let [FieldValue::Function(predicate)] = arguments else {
            return Err(AuthError::internal(
                "Filter argument must remain a native function",
            ));
        };
        *compared
            .lock()
            .map_err(|_| AuthError::internal("Predicate observation lock poisoned"))? = vec![
            predicate.call_with(
                &"ignored receiver".into(),
                &[id.clone(), 9.into(), receiver.clone()],
            )?,
            predicate.call_with(receiver, &[vec!["key".into()].into()])?,
            predicate.call()?,
        ];
        receiver.set_model_property("0", "changed".into())?;
        Ok(returned.clone())
    });
    let index: FieldValue = FieldArray::from_values_with_properties(
        vec!["before".into()],
        properties(FieldMap::from([("filter".into(), filter.into())]), &calls),
    )
    .into();
    cache.seed("api-key:hash", "stored-hash".into());
    cache.seed("api-key:by-id:key", "stored-id".into());
    cache.seed(INDEX, index.clone());
    remove_cached(cache.clone(), &key, false).await.unwrap();
    let observations = calls.drain();
    assert_eq!(
        observations
            .iter()
            .map(|call| call.name.as_str())
            .collect::<Vec<_>>(),
        [
            "get:filter",
            "call:filter",
            "get:length",
            "get:toJSON",
            "call:toJSON"
        ]
    );
    assert!(
        observations
            .iter()
            .take(2)
            .all(|call| call.receiver.strict_equals(&index))
    );
    assert!(
        observations
            .iter()
            .skip(2)
            .all(|call| call.receiver.strict_equals(&result))
    );
    assert_eq!(
        observations.last().unwrap().arguments,
        [FieldValue::from("")]
    );
    assert_eq!(
        *predicate_results.lock().unwrap(),
        [false.into(), true.into(), true.into()]
    );
    assert_eq!(index.model_property("0").unwrap(), "changed".into());
    assert_eq!(cache.value("api-key:hash"), None);
    assert_eq!(cache.value("api-key:by-id:key"), None);
    assert_eq!(cache.value(INDEX), Some(r#""custom-reference""#.into()));
    assert_eq!(
        *cache.writes.lock().unwrap(),
        [
            Write::Delete("api-key:hash".into()),
            Write::Delete("api-key:by-id:key".into()),
            Write::Set(INDEX.into(), r#""custom-reference""#.into()),
        ]
    );
}

#[tokio::test]
async fn original_getter_and_method_errors_preserve_completed_cache_writes_and_deletes() {
    for insert in [true, false] {
        for getter in [true, false] {
            let cache = Arc::new(Cache::default());
            let key = record("key".into());
            let method = if insert { "includes" } else { "filter" };
            let original: FieldValue =
                FieldMap::from([("nativeError".into(), method.into())]).into();
            let error = original.clone();
            let calls = Calls::default();
            let observed = calls.clone();
            let callback = FieldFunction::contextual(move |receiver, arguments| {
                observed.record(format!("call:{method}"), receiver, arguments);
                Err(AuthError::from_thrown(error.clone()))
            });
            let index: FieldValue = FieldArray::from_values_with_properties(
                vec!["key".into()],
                FieldObject::from_source(Arc::new(Properties {
                    fields: FieldMap::from([(method.into(), callback.into())]).into(),
                    calls: calls.clone(),
                    failure: getter.then(|| (method, original.clone())),
                })),
            )
            .into();
            cache.seed("api-key:hash", "stored-hash".into());
            cache.seed("api-key:by-id:key", "stored-id".into());
            cache.seed(INDEX, index.clone());
            let error = if insert {
                put(cache.clone(), &key, false).await.unwrap_err()
            } else {
                remove_cached(cache.clone(), &key, false).await.unwrap_err()
            };
            let observations = calls.drain();
            assert!(
                error
                    .raw_value()
                    .is_some_and(|value| value.strict_equals(&original))
            );
            assert_eq!(observations.len(), if getter { 1 } else { 2 });
            assert_eq!(observations.first().unwrap().name, format!("get:{method}"));
            assert!(
                observations
                    .iter()
                    .all(|call| call.receiver.strict_equals(&index))
            );
            if !getter {
                let call = observations.get(1).unwrap();
                assert_eq!(call.name, format!("call:{method}"));
                assert_eq!(call.arguments.len(), 1);
            }
            assert!(cache.value(INDEX).unwrap().strict_equals(&index));
            let expected = if insert {
                let serialized = super::super::super::serialize(&key).unwrap();
                assert_eq!(cache.value("api-key:hash"), Some(serialized.clone()));
                assert_eq!(cache.value("api-key:by-id:key"), Some(serialized.clone()));
                vec![
                    Write::Set("api-key:hash".into(), serialized.clone()),
                    Write::Set("api-key:by-id:key".into(), serialized),
                ]
            } else {
                assert_eq!(cache.value("api-key:hash"), None);
                assert_eq!(cache.value("api-key:by-id:key"), None);
                vec![
                    Write::Delete("api-key:hash".into()),
                    Write::Delete("api-key:by-id:key".into()),
                ]
            };
            assert_eq!(*cache.writes.lock().unwrap(), expected);
        }
    }
}