rskit-cache 0.2.0-alpha.3

Cache abstraction with explicit store registration and local adapters
Documentation
//! Behavioral tests for cache registry wiring and in-memory cache boundaries.

use std::sync::Arc;
use std::time::{Duration, Instant};

use parking_lot::Mutex;
use rskit_cache::{
    CacheConfig, CacheRegistry, CacheStore, MemoryCache, MemoryConfig, TypedStore, register_memory,
};

#[test]
fn registry_is_empty_without_explicit_registration() {
    let registry = CacheRegistry::new();
    assert!(registry.is_empty());
    assert_eq!(registry.len(), 0);
    assert!(!registry.contains("memory"));
}

#[tokio::test]
async fn explicit_memory_registration_builds_store() {
    let mut registry = CacheRegistry::new();
    register_memory(&mut registry).unwrap();

    let cache = registry.build(&CacheConfig::default()).await.unwrap();
    cache.set("hello", "world", None).await.unwrap();

    assert_eq!(cache.get("hello").await.unwrap().as_deref(), Some("world"));
}

#[tokio::test]
async fn unregistered_store_returns_error() {
    let registry = CacheRegistry::new();
    let err = registry.build(&CacheConfig::default()).await.err().unwrap();
    assert!(err.to_string().contains("not registered"));
}

#[tokio::test]
async fn memory_cache_ttl_zero_is_invalid() {
    let cache = MemoryCache::default();
    let err = cache
        .set("boundary", "value", Some(Duration::ZERO))
        .await
        .unwrap_err();

    assert!(err.to_string().contains("TTL must be greater than zero"));
}

#[tokio::test]
async fn memory_cache_ttl_boundary_expires_after_duration() {
    let now = Arc::new(Mutex::new(Instant::now()));
    let clock = Arc::clone(&now);
    let cache = MemoryCache::new_with_clock(None, None, move || *clock.lock());
    cache
        .set("short", "value", Some(Duration::from_millis(1)))
        .await
        .unwrap();
    *now.lock() += Duration::from_millis(5);

    assert_eq!(cache.get("short").await.unwrap(), None);
}

#[tokio::test]
async fn memory_cache_zero_capacity_is_unbounded() {
    let cache = MemoryCache::new(None, Some(0));
    cache.set("first", "value", None).await.unwrap();
    cache.set("second", "value", None).await.unwrap();

    assert_eq!(cache.get("first").await.unwrap().as_deref(), Some("value"));
    assert_eq!(cache.get("second").await.unwrap().as_deref(), Some("value"));
}

#[tokio::test]
async fn typed_store_round_trips_json_via_cache_trait() {
    #[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq)]
    struct Session {
        user_id: String,
        roles: Vec<String>,
    }

    let cache: Arc<dyn CacheStore> = Arc::new(MemoryCache::default());
    let store = TypedStore::<Session>::new(cache, "sessions");

    let expected = Session {
        user_id: "u1".into(),
        roles: vec!["admin".into()],
    };
    store.set("s1", &expected, None).await.unwrap();

    assert_eq!(store.get("s1").await.unwrap(), Some(expected));
    assert!(store.exists("s1").await.unwrap());
    assert!(store.delete("s1").await.unwrap());
    assert!(!store.exists("s1").await.unwrap());
}

#[test]
fn config_defaults_to_memory_store() {
    let cfg = CacheConfig::default();
    assert_eq!(cfg.store, "memory");
    assert!(cfg.key_prefix.is_none());
    assert!(cfg.memory.max_entries.is_none());
}

#[test]
fn deserialise_cache_config_with_memory_options() {
    let cfg: CacheConfig = serde_json::from_str(
        r#"{"store":"memory","key_prefix":"app","memory":{"max_entries":32}}"#,
    )
    .unwrap();

    assert_eq!(cfg.store, "memory");
    assert_eq!(cfg.key_prefix.as_deref(), Some("app"));
    assert_eq!(cfg.memory.max_entries, Some(32));
}

#[test]
fn memory_config_default_is_unbounded() {
    assert!(MemoryConfig::default().max_entries.is_none());
}