use cache_kit::backend::{CacheBackend, InMemoryBackend};
use cache_kit::feed::GenericFeeder;
use cache_kit::repository::InMemoryRepository;
use cache_kit::{CacheEntity, CacheExpander, CacheStrategy};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
struct User {
id: String,
name: String,
email: String,
}
impl CacheEntity for User {
type Key = String;
fn cache_key(&self) -> Self::Key {
self.id.clone()
}
fn cache_prefix() -> &'static str {
"user"
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
struct Product {
id: String,
name: String,
price: f64,
}
impl CacheEntity for Product {
type Key = String;
fn cache_key(&self) -> Self::Key {
self.id.clone()
}
fn cache_prefix() -> &'static str {
"product"
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
struct Order {
id: String,
user_id: String,
total: f64,
}
impl CacheEntity for Order {
type Key = String;
fn cache_key(&self) -> Self::Key {
self.id.clone()
}
fn cache_prefix() -> &'static str {
"order"
}
}
#[tokio::test]
async fn test_end_to_end_cache_flow() {
let backend = InMemoryBackend::new();
let expander = CacheExpander::new(backend.clone());
let mut repo = InMemoryRepository::new();
let user = User {
id: "user_123".to_string(),
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
};
repo.insert(user.id.clone(), user.clone());
let mut feeder = GenericFeeder::new("user_123".to_string());
expander
.with::<User, _, _>(&mut feeder, &repo, CacheStrategy::Refresh)
.await
.expect("First cache operation should succeed");
assert!(feeder.data.is_some(), "Data should be loaded from DB");
let loaded_user = feeder.data.unwrap();
assert_eq!(loaded_user.id, "user_123");
assert_eq!(loaded_user.name, "Alice");
assert_eq!(loaded_user.email, "alice@example.com");
let cache_key = "user:user_123";
let cached_data = backend
.clone()
.get(cache_key)
.await
.expect("Cache get should not error");
assert!(
cached_data.is_some(),
"Cache should be populated after first call"
);
let mut feeder2 = GenericFeeder::new("user_123".to_string());
expander
.with::<User, _, _>(&mut feeder2, &repo, CacheStrategy::Refresh)
.await
.expect("Second cache operation should succeed");
assert!(feeder2.data.is_some(), "Data should be loaded from cache");
let cached_user = feeder2.data.unwrap();
assert_eq!(cached_user.id, "user_123");
assert_eq!(cached_user.name, "Alice");
assert_eq!(cached_user, loaded_user, "Cached data should match DB data");
}
#[tokio::test]
async fn test_multiple_entities() {
let backend = InMemoryBackend::new();
let expander = CacheExpander::new(backend.clone());
let mut user_repo = InMemoryRepository::new();
let user = User {
id: "u1".to_string(),
name: "Bob".to_string(),
email: "bob@example.com".to_string(),
};
user_repo.insert(user.id.clone(), user.clone());
let mut product_repo = InMemoryRepository::new();
let product = Product {
id: "p1".to_string(),
name: "Laptop".to_string(),
price: 999.99,
};
product_repo.insert(product.id.clone(), product.clone());
let mut order_repo = InMemoryRepository::new();
let order = Order {
id: "o1".to_string(),
user_id: "u1".to_string(),
total: 999.99,
};
order_repo.insert(order.id.clone(), order.clone());
let mut user_feeder = GenericFeeder::new("u1".to_string());
expander
.with::<User, _, _>(&mut user_feeder, &user_repo, CacheStrategy::Refresh)
.await
.expect("User cache operation should succeed");
let mut product_feeder = GenericFeeder::new("p1".to_string());
expander
.with::<Product, _, _>(&mut product_feeder, &product_repo, CacheStrategy::Refresh)
.await
.expect("Product cache operation should succeed");
let mut order_feeder = GenericFeeder::new("o1".to_string());
expander
.with::<Order, _, _>(&mut order_feeder, &order_repo, CacheStrategy::Refresh)
.await
.expect("Order cache operation should succeed");
let user_cache_key = "user:u1";
let product_cache_key = "product:p1";
let order_cache_key = "order:o1";
assert!(
backend.clone().get(user_cache_key).await.unwrap().is_some(),
"User should be cached"
);
assert!(
backend
.clone()
.get(product_cache_key)
.await
.unwrap()
.is_some(),
"Product should be cached"
);
assert!(
backend
.clone()
.get(order_cache_key)
.await
.unwrap()
.is_some(),
"Order should be cached"
);
assert_ne!(user_cache_key, product_cache_key);
assert_ne!(user_cache_key, order_cache_key);
assert_ne!(product_cache_key, order_cache_key);
assert!(user_feeder.data.is_some());
assert!(product_feeder.data.is_some());
assert!(order_feeder.data.is_some());
let retrieved_user = user_feeder.data.unwrap();
let retrieved_product = product_feeder.data.unwrap();
let retrieved_order = order_feeder.data.unwrap();
assert_eq!(retrieved_user.name, "Bob");
assert_eq!(retrieved_product.name, "Laptop");
assert_eq!(retrieved_order.total, 999.99);
assert_eq!(
backend.len().await,
3,
"Should have exactly 3 cached entities"
);
}
#[tokio::test]
async fn test_ttl_expiration() {
use cache_kit::observability::TtlPolicy;
let backend = InMemoryBackend::new();
let expander = CacheExpander::new(backend.clone())
.with_ttl_policy(TtlPolicy::Fixed(Duration::from_secs(1)));
let mut repo = InMemoryRepository::new();
let user = User {
id: "user_ttl".to_string(),
name: "Charlie".to_string(),
email: "charlie@example.com".to_string(),
};
repo.insert(user.id.clone(), user.clone());
let mut feeder = GenericFeeder::new("user_ttl".to_string());
let expander = expander;
expander
.with::<User, _, _>(&mut feeder, &repo, CacheStrategy::Refresh)
.await
.expect("Cache operation should succeed");
assert!(
feeder.data.is_some(),
"Data should be cached with TTL of 1 second"
);
assert_eq!(feeder.data.unwrap().name, "Charlie");
let mut feeder2 = GenericFeeder::new("user_ttl".to_string());
expander
.with::<User, _, _>(&mut feeder2, &repo, CacheStrategy::Fresh)
.await
.expect("Fresh strategy should succeed");
assert!(
feeder2.data.is_some(),
"Data should be retrievable immediately"
);
tokio::time::sleep(Duration::from_secs(2)).await;
let mut feeder3 = GenericFeeder::new("user_ttl".to_string());
expander
.with::<User, _, _>(&mut feeder3, &repo, CacheStrategy::Fresh)
.await
.expect("Fresh strategy should succeed");
assert!(
feeder3.data.is_none(),
"Data should be expired and not retrievable via Fresh strategy"
);
let mut feeder4 = GenericFeeder::new("user_ttl".to_string());
expander
.with::<User, _, _>(&mut feeder4, &repo, CacheStrategy::Refresh)
.await
.expect("Refresh strategy should succeed");
assert!(
feeder4.data.is_some(),
"Data should be re-cached after expiration"
);
}
#[tokio::test]
async fn test_cache_invalidation() {
let backend = InMemoryBackend::new();
let expander = CacheExpander::new(backend.clone());
let stale_user = User {
id: "user_inv".to_string(),
name: "Stale Name".to_string(),
email: "stale@example.com".to_string(),
};
let bytes = stale_user.serialize_for_cache().unwrap();
backend
.clone()
.set("user:user_inv", bytes, None)
.await
.expect("Pre-populating cache should succeed");
let mut repo = InMemoryRepository::new();
let fresh_user = User {
id: "user_inv".to_string(),
name: "Fresh Name".to_string(),
email: "fresh@example.com".to_string(),
};
repo.insert(fresh_user.id.clone(), fresh_user.clone());
let mut feeder_stale = GenericFeeder::new("user_inv".to_string());
expander
.with::<User, _, _>(&mut feeder_stale, &repo, CacheStrategy::Fresh)
.await
.expect("Fresh strategy should succeed");
assert!(feeder_stale.data.is_some());
assert_eq!(
feeder_stale.data.unwrap().name,
"Stale Name",
"Cache should have stale data"
);
let mut feeder_fresh = GenericFeeder::new("user_inv".to_string());
expander
.with::<User, _, _>(&mut feeder_fresh, &repo, CacheStrategy::Invalidate)
.await
.expect("Invalidate strategy should succeed");
assert!(feeder_fresh.data.is_some());
assert_eq!(
feeder_fresh.data.unwrap().name,
"Fresh Name",
"Cache should be refreshed with fresh data"
);
let mut feeder_verify = GenericFeeder::new("user_inv".to_string());
expander
.with::<User, _, _>(&mut feeder_verify, &repo, CacheStrategy::Fresh)
.await
.expect("Fresh strategy should succeed");
assert!(feeder_verify.data.is_some());
assert_eq!(
feeder_verify.data.unwrap().name,
"Fresh Name",
"Cache should still have fresh data"
);
}
#[tokio::test]
async fn test_concurrent_operations() {
let backend = InMemoryBackend::new();
let expander = Arc::new(Mutex::new(CacheExpander::new(backend.clone())));
let repo = Arc::new({
let mut r = InMemoryRepository::new();
for i in 0..10 {
let user = User {
id: format!("user_{}", i),
name: format!("User {}", i),
email: format!("user{}@example.com", i),
};
r.insert(user.id.clone(), user);
}
r
});
let mut handles = vec![];
for i in 0..10 {
let expander_clone = Arc::clone(&expander);
let repo_clone = Arc::clone(&repo);
let handle = tokio::spawn(async move {
for j in 0..5 {
let user_id = format!("user_{}", (i + j) % 10);
let mut feeder = GenericFeeder::new(user_id);
let result = expander_clone
.lock()
.await
.with::<User, _, _>(&mut feeder, &*repo_clone, CacheStrategy::Refresh)
.await;
assert!(result.is_ok(), "Concurrent cache operation should succeed");
if let Some(user) = feeder.data {
assert!(user.name.starts_with("User "));
assert!(user.email.contains("@example.com"));
}
}
});
handles.push(handle);
}
for handle in handles {
handle.await.expect("Thread should not panic");
}
assert!(
!backend.is_empty().await,
"Cache should be populated after concurrent operations"
);
assert!(
backend.len().await <= 10,
"Cache should have at most 10 entries (one per user)"
);
for i in 0..10 {
let cache_key = format!("user:user_{}", i);
let cached_data = backend.clone().get(&cache_key).await.unwrap();
assert!(
cached_data.is_some(),
"User {} should be cached after concurrent operations",
i
);
let user = User::deserialize_from_cache(&cached_data.unwrap()).unwrap();
assert_eq!(user.id, format!("user_{}", i));
assert_eq!(user.name, format!("User {}", i));
}
}