better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
use super::*;
use better_auth_core::{
    FieldArray, FieldDate, FieldObject, FromFieldMap,
    field_value::{FieldObjectSource, FieldPropertyDescriptor},
};
use std::sync::{OnceLock, Weak};

use crate::plugins::test_helpers::native_observation::{NativeObservation, assert_replay};

type Observer = Arc<Mutex<NativeObservation>>;

#[derive(Clone, Default)]
struct Receiver(Arc<OnceLock<Weak<FieldValue>>>);

impl Receiver {
    fn bind(&self, value: FieldValue) -> Arc<FieldValue> {
        let value = Arc::new(value);
        self.0.set(Arc::downgrade(&value)).unwrap();
        value
    }

    fn get(&self) -> Arc<FieldValue> {
        self.0.get().unwrap().upgrade().unwrap()
    }
}

struct Properties {
    fields: FieldMap,
    observer: Observer,
    receiver: Receiver,
    failure: Option<FieldValue>,
}

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

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

    fn get(&self, _: &str) -> AuthResult<Option<FieldValue>> {
        Err(AuthError::internal("Native getter requires its receiver"))
    }

    fn get_with_receiver(
        &self,
        name: &str,
        receiver: &FieldValue,
    ) -> AuthResult<Option<FieldValue>> {
        let Some(value) = self.fields.get(name) else {
            return Ok(None);
        };
        self.observer.lock().unwrap().call(
            &format!("get:{name}"),
            receiver,
            &self.receiver.get(),
            &[],
        )?;
        if let Some(error) = &self.failure {
            return Err(AuthError::from_thrown(error.clone()));
        }
        Ok(Some(value.clone()))
    }

    fn set(&self, _: String, _: FieldValue) -> AuthResult<Option<FieldValue>> {
        Err(AuthError::internal("Contract accessor has no setter"))
    }

    fn delete(&self, _: &str) -> AuthResult<Option<FieldValue>> {
        Err(AuthError::internal(
            "Contract does not delete named accessors",
        ))
    }

    fn snapshot_fields(&self) -> AuthResult<FieldMap> {
        Err(AuthError::internal(
            "Native accessors must not be flattened",
        ))
    }

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

struct Storage {
    values: Mutex<Vec<(FieldValue, FieldValue)>>,
    observer: Observer,
}

impl Storage {
    fn snapshot(&self) -> FieldValue {
        self.values
            .lock()
            .unwrap()
            .iter()
            .map(|(key, value)| vec![key.clone(), value.clone()].into())
            .collect::<Vec<FieldValue>>()
            .into()
    }

    fn value(&self, key: &FieldValue) -> Option<FieldValue> {
        self.values
            .lock()
            .unwrap()
            .iter()
            .find(|(stored, _)| stored.same_value_zero(key))
            .map(|(_, value)| value.clone())
    }
}

#[async_trait]
impl SecondaryStorage for Storage {
    async fn get(&self, key: &str) -> AuthResult<Option<Value>> {
        self.get_native(&key.into())
            .await?
            .map(|value| value.json())
            .transpose()
            .map(Option::flatten)
    }

    async fn get_native(&self, key: &FieldValue) -> AuthResult<Option<FieldValue>> {
        let mut observer = self.observer.lock().unwrap();
        let key_observation = observer.observe(key)?;
        observer.record("storage.get", [("key".into(), key_observation)]);
        Ok(self.value(key))
    }

    async fn set_native(
        &self,
        key: &FieldValue,
        value: &FieldValue,
        ttl: Option<f64>,
    ) -> AuthResult<()> {
        let mut observer = self.observer.lock().unwrap();
        let key_observation = observer.observe(key)?;
        let value_observation = observer.observe(value)?;
        let ttl = observer.observe(&ttl.map(FieldValue::from).unwrap_or_default())?;
        observer.record(
            "storage.set",
            [
                ("key".into(), key_observation),
                ("value".into(), value_observation),
                ("ttl".into(), ttl),
            ],
        );
        let mut values = self.values.lock().unwrap();
        if let Some((_, stored)) = values
            .iter_mut()
            .find(|(stored, _)| stored.same_value_zero(key))
        {
            *stored = value.clone();
        } else {
            values.push((key.clone(), value.clone()));
        }
        Ok(())
    }

    async fn delete(&self, key: &str) -> AuthResult<()> {
        self.delete_native(&key.into()).await
    }

    async fn delete_native(&self, key: &FieldValue) -> AuthResult<()> {
        let mut observer = self.observer.lock().unwrap();
        let key_observation = observer.observe(key)?;
        observer.record("storage.delete", [("key".into(), key_observation)]);
        self.values
            .lock()
            .unwrap()
            .retain(|(stored, _)| !stored.same_value_zero(key));
        Ok(())
    }

    async fn get_and_delete(&self, _: &str) -> AuthResult<Option<Value>> {
        Err(AuthError::internal(
            "API Key reference contract does not consume atomic cache values",
        ))
    }
}

fn dynamic_result(observer: &Observer) -> Arc<FieldValue> {
    let expected = Receiver::default();
    let receiver = expected.clone();
    let observed = observer.clone();
    let to_json = FieldFunction::contextual(move |this, arguments| {
        observed
            .lock()
            .unwrap()
            .call("call:toJSON", this, &receiver.get(), arguments)?;
        Ok("custom-reference".into())
    });
    expected.bind(
        FieldObject::from_source(Arc::new(Properties {
            fields: FieldMap::from([
                ("length".into(), "0".into()),
                ("toJSON".into(), to_json.into()),
            ]),
            observer: observer.clone(),
            receiver: expected.clone(),
            failure: None,
        }))
        .into(),
    )
}

async fn run_case(scenario: &Value) -> AuthResult<Value> {
    let operation = scenario["operation"].as_str().unwrap();
    assert!(matches!(operation, "put" | "remove"));
    let insert = operation == "put";
    let failure = scenario.get("failure").and_then(Value::as_str);
    assert!(matches!(failure, None | Some("getter" | "method")));
    let getter_error = failure == Some("getter");
    let method_error = failure == Some("method");
    let truthy = scenario.get("result").and_then(Value::as_str) == Some("truthy");
    let observer = Observer::default();
    let id: FieldValue = vec!["key".into()].into();
    let key = ApiKey::from_field_values(FieldMap::from([
        ("id".into(), id.clone()),
        ("key".into(), "hash".into()),
        ("referenceId".into(), "owner".into()),
        ("configId".into(), "default".into()),
        (
            "createdAt".into(),
            FieldDate::from_milliseconds(2_000_000_000_000.0).into(),
        ),
        (
            "updatedAt".into(),
            FieldDate::from_milliseconds(2_000_000_000_000.0).into(),
        ),
    ]))?;
    let original: FieldValue = FieldMap::from([
        ("nativeError".into(), operation.into()),
        ("ownUndefined".into(), FieldValue::Undefined),
        (
            "detail".into(),
            FieldMap::from([(
                "stage".into(),
                failure.map(FieldValue::from).unwrap_or_default(),
            )])
            .into(),
        ),
    ])
    .into();
    let dynamic = dynamic_result(&observer);
    let expected = Receiver::default();
    let receiver = expected.clone();
    let observed = observer.clone();
    let error = original.clone();
    let method = if insert { "includes" } else { "filter" };
    let callback = FieldFunction::contextual(move |this, arguments| {
        observed.lock().unwrap().call(
            &format!("call:{method}"),
            this,
            &receiver.get(),
            arguments,
        )?;
        assert!(this.strict_equals(&receiver.get()));
        assert_eq!(arguments.len(), 1);
        if insert {
            let same_id = arguments.first().unwrap().strict_equals(&id);
            assert!(same_id);
            observed
                .lock()
                .unwrap()
                .record("includes:id", [("sameId".into(), same_id.into())]);
        } else {
            let [FieldValue::Function(predicate)] = arguments else {
                return Err(AuthError::internal(
                    "Filter must receive a native predicate",
                ));
            };
            let results: FieldValue = vec![
                predicate.call_with(
                    &"ignored receiver".into(),
                    &[id.clone(), 9.into(), this.clone()],
                )?,
                predicate.call_with(this, &[vec!["key".into()].into()])?,
                predicate.call()?,
            ]
            .into();
            let mut observer = observed.lock().unwrap();
            let results = observer.observe(&results)?;
            observer.record("filter:predicate", [("results".into(), results)]);
        }
        if method_error {
            return Err(AuthError::from_thrown(error.clone()));
        }
        this.set_model_property("0", "changed".into())?;
        Ok(if !insert {
            dynamic.as_ref().clone()
        } else if truthy {
            FieldMap::new().into()
        } else {
            "".into()
        })
    });
    let index = expected.bind(
        FieldArray::from_values_with_properties(
            vec!["before".into()],
            FieldObject::from_source(Arc::new(Properties {
                fields: FieldMap::from([(method.into(), callback.into())]),
                observer: observer.clone(),
                receiver: expected.clone(),
                failure: getter_error.then(|| original.clone()),
            })),
        )
        .into(),
    );
    let storage = Arc::new(Storage {
        values: Mutex::new(vec![
            ("api-key:hash".into(), "stored-hash".into()),
            ("api-key:by-id:key".into(), "stored-id".into()),
            (INDEX.into(), index.as_ref().clone()),
        ]),
        observer: observer.clone(),
    });
    let before = observer.lock().unwrap().observe(
        &FieldMap::from([
            ("record".into(), key.field_values()?.into()),
            ("cache".into(), storage.snapshot()),
        ])
        .into(),
    )?;
    let outcome = if insert {
        put(storage.clone(), &key, false).await
    } else {
        remove_cached(storage.clone(), &key, false).await
    };
    let mut observer = observer.lock().unwrap();
    let outcome = observer.outcome(outcome.map(|()| FieldValue::Undefined), &original)?;
    let after = observer.observe(
        &FieldMap::from([
            ("record".into(), key.field_values()?.into()),
            ("cache".into(), storage.snapshot()),
            ("originalIndex".into(), index.as_ref().clone()),
            (
                "indexSameOriginal".into(),
                storage
                    .value(&INDEX.into())
                    .is_some_and(|stored| stored.strict_equals(&index))
                    .into(),
            ),
        ])
        .into(),
    )?;
    Ok(json!({
        "before":before,
        "outcome":outcome,
        "events":observer.events,
        "after":after,
        "functionIds":observer.function_ids(),
    }))
}

#[tokio::test]
async fn native_reference_methods_match_complete_upstream_runtime() -> AuthResult<()> {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../tests/fixtures/api-key-reference-native-1.7.6.json");
    let fixture: Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
    assert_eq!(fixture["version"], "1.7.6");
    let cases = fixture["cases"].as_array().unwrap();
    assert_eq!(cases.len(), 7);
    for case in cases {
        let scenario = &case["scenario"];
        let name = scenario["name"].as_str().unwrap();
        let actual = run_case(scenario).await?;
        assert_replay(&actual, &case["runtime"], name);
    }
    Ok(())
}