better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
#![expect(
    clippy::unwrap_used,
    clippy::panic_in_result_fn,
    reason = "Native method contract setup and frozen observations must remain valid."
)]

use super::*;
use async_trait::async_trait;
use better_auth_core::{FieldDate, FieldFunction, FieldObject};
use serde_json::json;
use std::sync::{Mutex, OnceLock, Weak};

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

mod create;
mod values;

type Observer = Arc<Mutex<NativeObservation>>;

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

#[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 observed = observer.observe(key)?;
        observer.record("storage.get", [("key".into(), observed)]);
        Ok(self
            .entries
            .lock()
            .unwrap()
            .iter()
            .find(|(stored, _)| stored.same_value_zero(key))
            .map(|(_, value)| value.clone()))
    }

    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 entries = self.entries.lock().unwrap();
        if let Some((_, stored)) = entries
            .iter_mut()
            .find(|(stored, _)| stored.same_value_zero(key))
        {
            *stored = value.clone();
        } else {
            entries.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 observed = observer.observe(key)?;
        observer.record("storage.delete", [("key".into(), observed)]);
        self.entries
            .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 date methods do not consume atomic cache values",
        ))
    }
}

fn seed() -> FieldMap {
    FieldMap::from([
        ("id".into(), "method-key".into()),
        ("key".into(), "method-secret".into()),
        ("referenceId".into(), "owner".into()),
        ("configId".into(), "default".into()),
        ("createdAt".into(), FieldDate::from_milliseconds(0.0).into()),
        ("updatedAt".into(), FieldDate::from_milliseconds(0.0).into()),
        ("expiresAt".into(), FieldValue::Null),
        ("lastRefillAt".into(), FieldValue::Null),
        ("lastRequest".into(), FieldValue::Null),
        (
            "metadata".into(),
            FieldMap::from([("retained".into(), true.into())]).into(),
        ),
    ])
}

async fn run_case(scenario: &Value) -> AuthResult<Value> {
    let field = scenario["field"].as_str().unwrap();
    let mode = scenario["mode"].as_str().unwrap();
    let observer = Observer::default();
    let original: FieldValue =
        FieldMap::from([("cause".into(), "original date method failure".into())]).into();
    let (receiver, _returned) = values::receiver(&observer, &original, mode);
    let mut fields = seed();
    let _ = fields.insert(field.into(), receiver.as_ref().clone());
    let key = ApiKey::from_field_values(fields.clone())?;
    let before = observer.lock().unwrap().observe(&fields.into())?;
    let storage = Arc::new(Storage {
        entries: Mutex::new(vec![("api-key:by-ref:owner".into(), "[\"older\"]".into())]),
        observer: observer.clone(),
    });
    let result = put(storage.clone(), &key, true).await;
    let fields = key.field_values()?;
    assert!(fields.get(field).unwrap().strict_equals(&receiver));
    assert_eq!(observer.lock().unwrap().observe(&fields.into())?, before);
    let mut observer = observer.lock().unwrap();
    let outcome = observer.outcome(result.map(|()| FieldValue::Undefined), &original)?;
    let entries = storage
        .entries
        .lock()
        .unwrap()
        .iter()
        .map(|(key, value)| vec![key.clone(), value.clone()].into())
        .collect::<Vec<FieldValue>>();
    let entries = observer.observe(&entries.into())?;
    Ok(
        json!({"before":before,"outcome":outcome,"events":observer.events,
        "entries":entries,"functionIds":observer.function_ids()}),
    )
}

#[tokio::test]
async fn native_date_methods_match_complete_upstream_cache_writes() -> AuthResult<()> {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../tests/fixtures/api-key-date-methods-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 fields = [
        "createdAt",
        "updatedAt",
        "expiresAt",
        "lastRefillAt",
        "lastRequest",
    ];
    let modes = [
        "object-string",
        "object-undefined",
        "object-result",
        "date-to-json",
        "invalid-date-iso",
        "get-throws",
        "call-throws",
    ];
    let names = fields
        .into_iter()
        .flat_map(|field| modes.into_iter().map(move |mode| format!("{field}/{mode}")))
        .chain(["expiresAt/ttl-before-serialization".into()])
        .collect::<Vec<_>>();
    let cases = fixture["cases"].as_array().unwrap();
    assert_eq!(cases.len(), names.len());
    for (case, name) in cases.iter().zip(names) {
        assert_eq!(case["name"], name);
        assert_replay(&run_case(case).await?, &case["runtime"], &name);
    }
    Ok(())
}

#[test]
fn ttl_samples_clock_before_expiration_conversion_and_preserves_original_failure() -> AuthResult<()>
{
    for fails in [false, true] {
        let events = Arc::new(Mutex::new(Vec::<&'static str>::new()));
        let clock = Arc::new(std::sync::atomic::AtomicI64::new(2_000_000_000_000));
        let original = FieldValue::from(FieldMap::from([(
            "cause".into(),
            "expiry conversion".into(),
        )]));
        let projected = Arc::new(FieldValue::from(FieldMap::new()));
        let expected = Arc::downgrade(&projected);
        let observed = events.clone();
        let advanced = clock.clone();
        let failure = original.clone();
        projected.set_model_property(
            "valueOf",
            FieldFunction::contextual(move |receiver, arguments| {
                assert!(receiver.strict_equals(&expected.upgrade().unwrap()));
                assert!(arguments.is_empty());
                observed.lock().unwrap().push("valueOf");
                advanced.store(2_000_000_060_000, std::sync::atomic::Ordering::SeqCst);
                if fails {
                    Err(AuthError::from_thrown(failure.clone()))
                } else {
                    Ok(2_000_000_060_000.0.into())
                }
            })
            .into(),
        )?;
        let mut fields = seed();
        let _ = fields.insert("expiresAt".into(), projected.as_ref().clone());
        let key = ApiKey::from_field_values(fields)?;
        let result = ttl(&key, || {
            events.lock().unwrap().push("Date.now");
            clock.load(std::sync::atomic::Ordering::SeqCst) as f64
        });
        assert_eq!(*events.lock().unwrap(), ["Date.now", "valueOf"]);
        if fails {
            assert!(
                result
                    .unwrap_err()
                    .into_thrown_value()?
                    .strict_equals(&original)
            );
        } else {
            assert_eq!(result?, Some(60));
        }
        assert!(key.expires_at.field_value().strict_equals(&projected));
    }
    Ok(())
}