use super::*;
#[test]
fn test_make_key() {
let prefix = CachePrefix::session();
let key = CacheKey::new("user123".to_string()).unwrap();
let result = InMemoryCacheStore::make_key(prefix, key);
assert_eq!(result, "cache:session:user123");
}
#[tokio::test]
async fn test_put_and_get() {
let mut store = InMemoryCacheStore::new();
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("key1".to_string()).unwrap();
let value = CacheData {
value: "test value".to_string(),
};
let put_result = store.put(prefix, key, value.clone()).await;
assert!(put_result.is_ok());
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("key1".to_string()).unwrap();
let get_result = store.get(prefix, key).await;
assert!(get_result.is_ok());
let retrieved = get_result.unwrap();
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().value, "test value");
}
#[tokio::test]
async fn test_put_with_ttl() {
let mut store = InMemoryCacheStore::new();
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("key2".to_string()).unwrap();
let value = CacheData {
value: "test value with ttl".to_string(),
};
let put_result = store.put_with_ttl(prefix, key, value.clone(), 60).await;
assert!(put_result.is_ok());
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("key2".to_string()).unwrap();
let get_result = store.get(prefix, key).await;
assert!(get_result.is_ok());
let retrieved = get_result.unwrap();
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().value, "test value with ttl");
}
#[tokio::test]
async fn test_remove() {
let mut store = InMemoryCacheStore::new();
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("key3".to_string()).unwrap();
let value = CacheData {
value: "value to remove".to_string(),
};
let _ = store.put(prefix, key, value).await;
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("key3".to_string()).unwrap();
let remove_result = store.remove(prefix, key).await;
assert!(remove_result.is_ok());
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("key3".to_string()).unwrap();
let get_result = store.get(prefix, key).await;
assert!(get_result.is_ok());
let retrieved = get_result.unwrap();
assert!(retrieved.is_none());
}
#[tokio::test]
async fn test_get_nonexistent_key() {
let store = InMemoryCacheStore::new();
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("nonexistent".to_string()).unwrap();
let get_result = store.get(prefix, key).await;
assert!(get_result.is_ok());
let retrieved = get_result.unwrap();
assert!(retrieved.is_none());
}
#[tokio::test]
async fn test_multiple_prefixes() {
let mut store = InMemoryCacheStore::new();
let key1 = CacheKey::new("same_key".to_string()).unwrap();
let key2 = CacheKey::new("same_key".to_string()).unwrap();
let value1 = CacheData {
value: "value for prefix1".to_string(),
};
let value2 = CacheData {
value: "value for prefix2".to_string(),
};
let prefix1 = CachePrefix::new("prefix1".to_string()).unwrap();
let prefix2 = CachePrefix::new("prefix2".to_string()).unwrap();
let _ = store.put(prefix1, key1, value1).await;
let _ = store.put(prefix2, key2, value2).await;
let prefix1 = CachePrefix::new("prefix1".to_string()).unwrap();
let prefix2 = CachePrefix::new("prefix2".to_string()).unwrap();
let key1 = CacheKey::new("same_key".to_string()).unwrap();
let key2 = CacheKey::new("same_key".to_string()).unwrap();
let get1 = store.get(prefix1, key1).await.unwrap().unwrap();
let get2 = store.get(prefix2, key2).await.unwrap().unwrap();
assert_eq!(get1.value, "value for prefix1");
assert_eq!(get2.value, "value for prefix2");
}
#[tokio::test]
async fn test_overwrite_existing_key() {
let mut store = InMemoryCacheStore::new();
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("key1".to_string()).unwrap();
let original_value = CacheData {
value: "original value".to_string(),
};
let new_value = CacheData {
value: "new value".to_string(),
};
let _ = store.put(prefix, key, original_value).await;
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("key1".to_string()).unwrap();
let _ = store.put(prefix, key, new_value).await;
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("key1".to_string()).unwrap();
let retrieved = store.get(prefix, key).await.unwrap().unwrap();
assert_eq!(retrieved.value, "new value");
}
#[tokio::test]
async fn test_remove_nonexistent_key() {
let mut store = InMemoryCacheStore::new();
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("nonexistent".to_string()).unwrap();
let result = store.remove(prefix, key).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_empty_prefix_and_key() {
let mut store = InMemoryCacheStore::new();
let value = CacheData {
value: "test with empty strings".to_string(),
};
let prefix = CachePrefix::new("".to_string()).unwrap();
let key = CacheKey::new("".to_string()).unwrap();
let put_result = store.put(prefix, key, value.clone()).await;
assert!(put_result.is_ok());
let prefix = CachePrefix::new("".to_string()).unwrap();
let key = CacheKey::new("".to_string()).unwrap();
let get_result = store.get(prefix, key).await.unwrap().unwrap();
assert_eq!(get_result.value, "test with empty strings");
}
#[tokio::test]
async fn test_put_without_ttl_never_expires() {
let mut store = InMemoryCacheStore::new();
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("no_ttl".to_string()).unwrap();
let value = CacheData {
value: "persistent".to_string(),
};
store.put(prefix, key, value).await.unwrap();
let internal_key = InMemoryCacheStore::make_key(
CachePrefix::new("test".to_string()).unwrap(),
CacheKey::new("no_ttl".to_string()).unwrap(),
);
let entry = store.entry.get(&internal_key).unwrap();
assert!(entry.expires_at.is_none(), "put() should set no expiration");
assert!(!entry.is_expired(), "Entry without TTL should never expire");
}
#[tokio::test]
async fn test_put_with_ttl_sets_expiration() {
let mut store = InMemoryCacheStore::new();
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("with_ttl".to_string()).unwrap();
let value = CacheData {
value: "expires soon".to_string(),
};
store.put_with_ttl(prefix, key, value, 300).await.unwrap();
let internal_key = InMemoryCacheStore::make_key(
CachePrefix::new("test".to_string()).unwrap(),
CacheKey::new("with_ttl".to_string()).unwrap(),
);
let entry = store.entry.get(&internal_key).unwrap();
assert!(
entry.expires_at.is_some(),
"put_with_ttl(300) should set an expiration"
);
assert!(
!entry.is_expired(),
"Entry with 300s TTL should not be expired yet"
);
}
#[tokio::test]
async fn test_put_with_ttl_zero_means_no_expiration() {
let mut store = InMemoryCacheStore::new();
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("zero_ttl".to_string()).unwrap();
let value = CacheData {
value: "no expiration".to_string(),
};
store.put_with_ttl(prefix, key, value, 0).await.unwrap();
let internal_key = InMemoryCacheStore::make_key(
CachePrefix::new("test".to_string()).unwrap(),
CacheKey::new("zero_ttl".to_string()).unwrap(),
);
let entry = store.entry.get(&internal_key).unwrap();
assert!(
entry.expires_at.is_none(),
"TTL=0 should mean no expiration"
);
assert!(!entry.is_expired());
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("zero_ttl".to_string()).unwrap();
let result = store.get(prefix, key).await.unwrap();
assert_eq!(result.unwrap().value, "no expiration");
}
#[tokio::test]
async fn test_get_returns_none_for_expired_entry() {
let mut store = InMemoryCacheStore::new();
let internal_key = InMemoryCacheStore::make_key(
CachePrefix::new("test".to_string()).unwrap(),
CacheKey::new("expired".to_string()).unwrap(),
);
store.entry.insert(
internal_key,
CacheEntry {
data: CacheData {
value: "old data".to_string(),
},
expires_at: Some(Instant::now() - Duration::from_secs(1)),
},
);
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("expired".to_string()).unwrap();
let result = store.get(prefix, key).await.unwrap();
assert!(
result.is_none(),
"get() should return None for expired entries"
);
}
#[tokio::test]
async fn test_put_if_not_exists_replaces_expired_entry() {
let mut store = InMemoryCacheStore::new();
let internal_key = InMemoryCacheStore::make_key(
CachePrefix::new("test".to_string()).unwrap(),
CacheKey::new("contested".to_string()).unwrap(),
);
store.entry.insert(
internal_key,
CacheEntry {
data: CacheData {
value: "expired data".to_string(),
},
expires_at: Some(Instant::now() - Duration::from_secs(1)),
},
);
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("contested".to_string()).unwrap();
let new_value = CacheData {
value: "fresh data".to_string(),
};
let inserted = store
.put_if_not_exists(prefix, key, new_value, 600)
.await
.unwrap();
assert!(inserted, "Should insert over expired entry");
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("contested".to_string()).unwrap();
let result = store.get(prefix, key).await.unwrap();
assert_eq!(result.unwrap().value, "fresh data");
}
#[tokio::test]
async fn test_put_if_not_exists_rejects_when_live_entry_exists() {
let mut store = InMemoryCacheStore::new();
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("live".to_string()).unwrap();
let original = CacheData {
value: "original".to_string(),
};
store
.put_if_not_exists(prefix, key, original, 3600)
.await
.unwrap();
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("live".to_string()).unwrap();
let replacement = CacheData {
value: "replacement".to_string(),
};
let inserted = store
.put_if_not_exists(prefix, key, replacement, 3600)
.await
.unwrap();
assert!(!inserted, "Should NOT insert when live entry exists");
let prefix = CachePrefix::new("test".to_string()).unwrap();
let key = CacheKey::new("live".to_string()).unwrap();
let result = store.get(prefix, key).await.unwrap();
assert_eq!(result.unwrap().value, "original");
}
mod integration_tests {
use crate::storage::{CacheData, CacheKey, CachePrefix, GENERIC_CACHE_STORE};
use crate::test_utils::init_test_environment;
#[tokio::test]
async fn test_cache_store_integration() {
init_test_environment().await;
let prefix = "integration_test";
let key = "test_key";
let value = CacheData {
value: "integration test value".to_string(),
};
{
let mut cache = GENERIC_CACHE_STORE.lock().await;
let cache_prefix = CachePrefix::new(prefix.to_string()).unwrap();
let cache_key = CacheKey::new(key.to_string()).unwrap();
let put_result = cache.put(cache_prefix, cache_key, value.clone()).await;
assert!(put_result.is_ok(), "Should be able to store data in cache");
}
{
let cache = GENERIC_CACHE_STORE.lock().await;
let cache_prefix = CachePrefix::new(prefix.to_string()).unwrap();
let cache_key = CacheKey::new(key.to_string()).unwrap();
let get_result = cache.get(cache_prefix, cache_key).await;
assert!(
get_result.is_ok(),
"Should be able to retrieve data from cache"
);
let retrieved = get_result.unwrap();
assert!(retrieved.is_some(), "Data should exist in cache");
assert_eq!(retrieved.unwrap().value, "integration test value");
}
{
let mut cache = GENERIC_CACHE_STORE.lock().await;
let cache_prefix = CachePrefix::new(prefix.to_string()).unwrap();
let cache_key = CacheKey::new(key.to_string()).unwrap();
let remove_result = cache.remove(cache_prefix, cache_key).await;
assert!(
remove_result.is_ok(),
"Should be able to remove data from cache"
);
}
{
let cache = GENERIC_CACHE_STORE.lock().await;
let cache_prefix = CachePrefix::new(prefix.to_string()).unwrap();
let cache_key = CacheKey::new(key.to_string()).unwrap();
let get_result = cache.get(cache_prefix, cache_key).await;
assert!(get_result.is_ok(), "Get operation should succeed");
assert!(
get_result.unwrap().is_none(),
"Data should be removed from cache"
);
}
}
#[tokio::test]
async fn test_cache_store_concurrent_access() {
init_test_environment().await;
let prefix = "concurrent_test";
let mut handles = vec![];
for i in 0..5 {
let task_key = format!("key_{i}");
let task_value = CacheData {
value: format!("concurrent_value_{i}"),
};
let handle = tokio::spawn(async move {
{
let mut cache = GENERIC_CACHE_STORE.lock().await;
let cache_prefix = CachePrefix::new(prefix.to_string()).unwrap();
let cache_key = CacheKey::new(task_key.clone()).unwrap();
cache
.put(cache_prefix, cache_key, task_value)
.await
.unwrap();
}
{
let cache = GENERIC_CACHE_STORE.lock().await;
let cache_prefix = CachePrefix::new(prefix.to_string()).unwrap();
let cache_key = CacheKey::new(task_key).unwrap();
let result = cache.get(cache_prefix, cache_key).await.unwrap();
assert!(result.is_some());
result.unwrap().value
}
});
handles.push(handle);
}
for (i, handle) in handles.into_iter().enumerate() {
let result = handle.await.unwrap();
assert_eq!(result, format!("concurrent_value_{i}"));
}
}
#[tokio::test]
async fn test_cache_store_prefix_isolation() {
init_test_environment().await;
let key = "shared_key";
let value1 = CacheData {
value: "value_for_prefix1".to_string(),
};
let value2 = CacheData {
value: "value_for_prefix2".to_string(),
};
let value3 = CacheData {
value: "value_for_prefix3".to_string(),
};
{
let mut cache = GENERIC_CACHE_STORE.lock().await;
let prefix1 = CachePrefix::new("prefix1".to_string()).unwrap();
let prefix2 = CachePrefix::new("prefix2".to_string()).unwrap();
let prefix3 = CachePrefix::new("prefix3".to_string()).unwrap();
let key1 = CacheKey::new(key.to_string()).unwrap();
let key2 = CacheKey::new(key.to_string()).unwrap();
let key3 = CacheKey::new(key.to_string()).unwrap();
cache.put(prefix1, key1, value1).await.unwrap();
cache.put(prefix2, key2, value2).await.unwrap();
cache.put(prefix3, key3, value3).await.unwrap();
}
{
let cache = GENERIC_CACHE_STORE.lock().await;
let prefix1 = CachePrefix::new("prefix1".to_string()).unwrap();
let prefix2 = CachePrefix::new("prefix2".to_string()).unwrap();
let prefix3 = CachePrefix::new("prefix3".to_string()).unwrap();
let key1 = CacheKey::new(key.to_string()).unwrap();
let key2 = CacheKey::new(key.to_string()).unwrap();
let key3 = CacheKey::new(key.to_string()).unwrap();
let result1 = cache.get(prefix1, key1).await.unwrap().unwrap();
assert_eq!(result1.value, "value_for_prefix1");
let result2 = cache.get(prefix2, key2).await.unwrap().unwrap();
assert_eq!(result2.value, "value_for_prefix2");
let result3 = cache.get(prefix3, key3).await.unwrap().unwrap();
assert_eq!(result3.value, "value_for_prefix3");
}
{
let mut cache = GENERIC_CACHE_STORE.lock().await;
let prefix2 = CachePrefix::new("prefix2".to_string()).unwrap();
let key2 = CacheKey::new(key.to_string()).unwrap();
cache.remove(prefix2, key2).await.unwrap();
}
{
let cache = GENERIC_CACHE_STORE.lock().await;
let prefix1 = CachePrefix::new("prefix1".to_string()).unwrap();
let prefix2 = CachePrefix::new("prefix2".to_string()).unwrap();
let prefix3 = CachePrefix::new("prefix3".to_string()).unwrap();
let key1 = CacheKey::new(key.to_string()).unwrap();
let key2 = CacheKey::new(key.to_string()).unwrap();
let key3 = CacheKey::new(key.to_string()).unwrap();
assert!(cache.get(prefix1, key1).await.unwrap().is_some());
assert!(cache.get(prefix3, key3).await.unwrap().is_some());
assert!(cache.get(prefix2, key2).await.unwrap().is_none());
}
}
#[tokio::test]
async fn test_cache_store_ttl_behavior() {
init_test_environment().await;
let prefix = "ttl_test";
let key = "ttl_key";
let value = CacheData {
value: "ttl test value".to_string(),
};
{
let mut cache = GENERIC_CACHE_STORE.lock().await;
let cache_prefix = CachePrefix::new(prefix.to_string()).unwrap();
let cache_key = CacheKey::new(key.to_string()).unwrap();
let put_result = cache
.put_with_ttl(cache_prefix, cache_key, value.clone(), 300)
.await;
assert!(put_result.is_ok(), "put_with_ttl should succeed");
}
{
let cache = GENERIC_CACHE_STORE.lock().await;
let cache_prefix = CachePrefix::new(prefix.to_string()).unwrap();
let cache_key = CacheKey::new(key.to_string()).unwrap();
let get_result = cache.get(cache_prefix, cache_key).await.unwrap();
assert!(get_result.is_some(), "Value should be stored despite TTL");
assert_eq!(get_result.unwrap().value, "ttl test value");
}
let zero_ttl_value = CacheData {
value: "zero ttl value".to_string(),
};
{
let mut cache = GENERIC_CACHE_STORE.lock().await;
let cache_prefix = CachePrefix::new(prefix.to_string()).unwrap();
let cache_key = CacheKey::new("zero_ttl_key".to_string()).unwrap();
let put_result = cache
.put_with_ttl(cache_prefix, cache_key, zero_ttl_value, 0)
.await;
assert!(
put_result.is_ok(),
"put_with_ttl with zero TTL should succeed"
);
}
{
let cache = GENERIC_CACHE_STORE.lock().await;
let cache_prefix = CachePrefix::new(prefix.to_string()).unwrap();
let cache_key = CacheKey::new("zero_ttl_key".to_string()).unwrap();
let get_result = cache.get(cache_prefix, cache_key).await.unwrap();
assert!(
get_result.is_some(),
"Value should be stored even with zero TTL in memory store"
);
}
}
#[tokio::test]
async fn test_cache_store_large_data() {
init_test_environment().await;
let prefix = "large_data_test";
let key = "large_key";
let large_content = "x".repeat(1024 * 1024);
let large_value = CacheData {
value: large_content.clone(),
};
{
let mut cache = GENERIC_CACHE_STORE.lock().await;
let cache_prefix = CachePrefix::new(prefix.to_string()).unwrap();
let cache_key = CacheKey::new(key.to_string()).unwrap();
let put_result = cache.put(cache_prefix, cache_key, large_value).await;
assert!(put_result.is_ok(), "Should be able to store large data");
}
{
let cache = GENERIC_CACHE_STORE.lock().await;
let cache_prefix = CachePrefix::new(prefix.to_string()).unwrap();
let cache_key = CacheKey::new(key.to_string()).unwrap();
let get_result = cache.get(cache_prefix, cache_key).await.unwrap();
assert!(get_result.is_some(), "Large data should be retrievable");
let retrieved = get_result.unwrap();
assert_eq!(
retrieved.value.len(),
1024 * 1024,
"Large data should maintain size"
);
assert_eq!(
retrieved.value, large_content,
"Large data should maintain content"
);
}
}
#[tokio::test]
async fn test_cache_store_special_characters() {
init_test_environment().await;
let prefix = "special_chars_test";
let test_cases = vec![
("key_with_spaces", "value with spaces"),
("key-with-dashes", "value-with-dashes"),
("key_with_émojis", "value with émojis 🚀🔐"),
("key/with/slashes", "value/with/slashes"),
("key:with:colons", "value:with:colons"),
("", "empty_key_test"), ("empty_value_key", ""), ];
{
let mut cache = GENERIC_CACHE_STORE.lock().await;
for (test_key, test_value) in &test_cases {
let cache_data = CacheData {
value: test_value.to_string(),
};
let cache_prefix = CachePrefix::new(prefix.to_string()).unwrap();
let cache_key = CacheKey::new(test_key.to_string()).unwrap();
let put_result = cache.put(cache_prefix, cache_key, cache_data).await;
assert!(
put_result.is_ok(),
"Should handle special characters in key: {test_key}"
);
}
}
{
let cache = GENERIC_CACHE_STORE.lock().await;
for (test_key, expected_value) in &test_cases {
let cache_prefix = CachePrefix::new(prefix.to_string()).unwrap();
let cache_key = CacheKey::new(test_key.to_string()).unwrap();
let get_result = cache.get(cache_prefix, cache_key).await.unwrap();
assert!(
get_result.is_some(),
"Should retrieve value for key: {test_key}"
);
let retrieved = get_result.unwrap();
assert_eq!(
&retrieved.value, expected_value,
"Value should match for key: {test_key}"
);
}
}
}
}