#![cfg(feature = "redis_store")]
macro_rules! skip_without_redis {
() => {
if std::env::var("CACHED_REDIS_CONNECTION_STRING").is_err() {
return;
}
};
}
mod sync_tests {
use cached::{ConcurrentCached, RedisCache};
use std::time::Duration;
fn build(prefix: &str) -> RedisCache<String, String> {
RedisCache::<String, String>::builder(prefix)
.namespace("")
.ttl(Duration::from_secs(60))
.build()
.expect("build RedisCache")
}
fn raw_conn(cache: &RedisCache<String, String>) -> redis::Connection {
redis::Client::open(cache.connection_string().reveal())
.expect("raw client")
.get_connection()
.expect("raw connection")
}
fn raw_set(cache: &RedisCache<String, String>, key: &[u8]) {
let mut conn = raw_conn(cache);
redis::cmd("SET")
.arg(key)
.arg("planted")
.query::<()>(&mut conn)
.expect("raw SET");
}
fn raw_del(cache: &RedisCache<String, String>, key: &[u8]) {
let mut conn = raw_conn(cache);
redis::cmd("DEL")
.arg(key)
.query::<i64>(&mut conn)
.expect("raw DEL");
}
fn raw_exists(cache: &RedisCache<String, String>, key: &[u8]) -> bool {
let mut conn = raw_conn(cache);
redis::cmd("EXISTS")
.arg(key)
.query::<i64>(&mut conn)
.expect("raw EXISTS")
== 1
}
#[test]
fn clear_removes_own_keys_despite_a_non_utf8_key_in_scope() {
skip_without_redis!();
let cache = build("v3binclear_sync");
let binary_key: &[u8] = b":v3binclear_sync:\xff";
raw_del(&cache, binary_key);
cache.cache_clear().expect("initial clear");
cache
.cache_set("k1".to_string(), "v1".to_string())
.expect("set k1");
cache
.cache_set("k2".to_string(), "v2".to_string())
.expect("set k2");
raw_set(&cache, binary_key);
let cleared = cache.cache_clear();
raw_del(&cache, binary_key);
cleared.expect("cache_clear must succeed with a non-UTF-8 key in scope");
assert_eq!(
cache.cache_get(&"k1".to_string()).expect("get k1"),
None,
"cache_clear must remove its own entries even when a non-UTF-8 key \
shares the scope"
);
assert_eq!(
cache.cache_get(&"k2".to_string()).expect("get k2"),
None,
"cache_clear must remove every one of its own entries"
);
}
#[test]
fn clear_deletes_the_non_utf8_key_itself() {
skip_without_redis!();
let cache = build("v3binclear_sync_del");
let binary_key: &[u8] = b":v3binclear_sync_del:\xfe\xff";
raw_del(&cache, binary_key);
cache.cache_clear().expect("initial clear");
cache
.cache_set("k".to_string(), "v".to_string())
.expect("set k");
raw_set(&cache, binary_key);
let cleared = cache.cache_clear();
let still_there = raw_exists(&cache, binary_key);
raw_del(&cache, binary_key);
cleared.expect("cache_clear must succeed");
assert!(
!still_there,
"a non-UTF-8 key inside the cache's own scope must be deleted by \
cache_clear, otherwise the failure never self-heals"
);
}
#[test]
fn clear_over_many_batches_is_not_partially_applied() {
skip_without_redis!();
const KEYS: usize = 250;
let cache = build("v3binclear_sync_batched");
let binary_key: &[u8] = b":v3binclear_sync_batched:\x80bad";
raw_del(&cache, binary_key);
cache.cache_clear().expect("initial clear");
for i in 0..KEYS {
cache
.cache_set(format!("k{i}"), format!("v{i}"))
.expect("cache_set");
}
raw_set(&cache, binary_key);
let cleared = cache.cache_clear();
raw_del(&cache, binary_key);
cleared.expect("cache_clear must succeed across multiple SCAN batches");
for i in 0..KEYS {
assert_eq!(
cache.cache_get(&format!("k{i}")).expect("cache_get"),
None,
"key k{i} survived a clear that spanned several SCAN batches"
);
}
}
#[test]
fn reset_removes_own_keys_despite_a_non_utf8_key_in_scope() {
skip_without_redis!();
let cache = build("v3binclear_sync_reset");
let binary_key: &[u8] = b":v3binclear_sync_reset:\xff\xfe";
raw_del(&cache, binary_key);
cache.cache_reset().expect("initial reset");
cache
.cache_set("k".to_string(), "v".to_string())
.expect("set k");
raw_set(&cache, binary_key);
let reset = cache.cache_reset();
raw_del(&cache, binary_key);
reset.expect("cache_reset must succeed with a non-UTF-8 key in scope");
assert_eq!(
cache.cache_get(&"k".to_string()).expect("get k"),
None,
"cache_reset must remove its own entries (it delegates to cache_clear)"
);
}
#[test]
fn clear_spares_a_non_utf8_key_outside_the_scope() {
skip_without_redis!();
let cache = build("v3binclear_sync_scope");
let outside_key: &[u8] = b":v3binclear_sync_scope_other:\xff";
cache.cache_clear().expect("initial clear");
cache
.cache_set("k".to_string(), "v".to_string())
.expect("set k");
raw_set(&cache, outside_key);
let cleared = cache.cache_clear();
let survived = raw_exists(&cache, outside_key);
raw_del(&cache, outside_key);
cleared.expect("cache_clear must succeed");
assert!(
survived,
"cache_clear must not reach a non-UTF-8 key outside its own scope"
);
assert_eq!(
cache.cache_get(&"k".to_string()).expect("get k"),
None,
"cache_clear must still remove its own entry"
);
}
}
#[cfg(feature = "redis_tokio")]
mod async_tests {
use cached::time::Duration;
use cached::{AsyncRedisCache, ConcurrentCachedAsync};
async fn build(prefix: &str) -> AsyncRedisCache<String, String> {
AsyncRedisCache::<String, String>::builder(prefix)
.namespace("")
.ttl(Duration::from_secs(60))
.build()
.await
.expect("build AsyncRedisCache")
}
fn raw_conn(cache: &AsyncRedisCache<String, String>) -> redis::Connection {
redis::Client::open(cache.connection_string().reveal())
.expect("raw client")
.get_connection()
.expect("raw connection")
}
fn raw_set(cache: &AsyncRedisCache<String, String>, key: &[u8]) {
let mut conn = raw_conn(cache);
redis::cmd("SET")
.arg(key)
.arg("planted")
.query::<()>(&mut conn)
.expect("raw SET");
}
fn raw_del(cache: &AsyncRedisCache<String, String>, key: &[u8]) {
let mut conn = raw_conn(cache);
redis::cmd("DEL")
.arg(key)
.query::<i64>(&mut conn)
.expect("raw DEL");
}
fn raw_exists(cache: &AsyncRedisCache<String, String>, key: &[u8]) -> bool {
let mut conn = raw_conn(cache);
redis::cmd("EXISTS")
.arg(key)
.query::<i64>(&mut conn)
.expect("raw EXISTS")
== 1
}
#[tokio::test]
async fn async_clear_removes_own_keys_despite_a_non_utf8_key_in_scope() {
skip_without_redis!();
let cache = build("v3binclear_async").await;
let binary_key: &[u8] = b":v3binclear_async:\xff";
raw_del(&cache, binary_key);
cache.async_cache_clear().await.expect("initial clear");
cache
.async_cache_set("k1".to_string(), "v1".to_string())
.await
.expect("set k1");
cache
.async_cache_set("k2".to_string(), "v2".to_string())
.await
.expect("set k2");
raw_set(&cache, binary_key);
let cleared = cache.async_cache_clear().await;
let still_there = raw_exists(&cache, binary_key);
raw_del(&cache, binary_key);
cleared.expect("async_cache_clear must succeed with a non-UTF-8 key in scope");
assert!(
!still_there,
"async_cache_clear must delete the non-UTF-8 key in its own scope"
);
assert_eq!(
cache
.async_cache_get(&"k1".to_string())
.await
.expect("get k1"),
None,
"async_cache_clear must remove its own entries even when a non-UTF-8 \
key shares the scope"
);
assert_eq!(
cache
.async_cache_get(&"k2".to_string())
.await
.expect("get k2"),
None,
"async_cache_clear must remove every one of its own entries"
);
}
#[tokio::test]
async fn async_clear_over_many_batches_is_not_partially_applied() {
skip_without_redis!();
const KEYS: usize = 250;
let cache = build("v3binclear_async_batched").await;
let binary_key: &[u8] = b":v3binclear_async_batched:\x80bad";
raw_del(&cache, binary_key);
cache.async_cache_clear().await.expect("initial clear");
for i in 0..KEYS {
cache
.async_cache_set(format!("k{i}"), format!("v{i}"))
.await
.expect("async_cache_set");
}
raw_set(&cache, binary_key);
let cleared = cache.async_cache_clear().await;
raw_del(&cache, binary_key);
cleared.expect("async_cache_clear must succeed across multiple SCAN batches");
for i in 0..KEYS {
assert_eq!(
cache
.async_cache_get(&format!("k{i}"))
.await
.expect("async_cache_get"),
None,
"key k{i} survived an async clear that spanned several SCAN batches"
);
}
}
#[tokio::test]
async fn async_reset_removes_own_keys_despite_a_non_utf8_key_in_scope() {
skip_without_redis!();
let cache = build("v3binclear_async_reset").await;
let binary_key: &[u8] = b":v3binclear_async_reset:\xff\xfe";
raw_del(&cache, binary_key);
cache.async_cache_reset().await.expect("initial reset");
cache
.async_cache_set("k".to_string(), "v".to_string())
.await
.expect("set k");
raw_set(&cache, binary_key);
let reset = cache.async_cache_reset().await;
raw_del(&cache, binary_key);
reset.expect("async_cache_reset must succeed with a non-UTF-8 key in scope");
assert_eq!(
cache
.async_cache_get(&"k".to_string())
.await
.expect("get k"),
None,
"async_cache_reset must remove its own entries (it delegates to \
async_cache_clear)"
);
}
#[tokio::test]
async fn async_clear_spares_a_non_utf8_key_outside_the_scope() {
skip_without_redis!();
let cache = build("v3binclear_async_scope").await;
let outside_key: &[u8] = b":v3binclear_async_scope_other:\xff";
cache.async_cache_clear().await.expect("initial clear");
cache
.async_cache_set("k".to_string(), "v".to_string())
.await
.expect("set k");
raw_set(&cache, outside_key);
let cleared = cache.async_cache_clear().await;
let survived = raw_exists(&cache, outside_key);
raw_del(&cache, outside_key);
cleared.expect("async_cache_clear must succeed");
assert!(
survived,
"async_cache_clear must not reach a non-UTF-8 key outside its own scope"
);
assert_eq!(
cache
.async_cache_get(&"k".to_string())
.await
.expect("get k"),
None,
"async_cache_clear must still remove its own entry"
);
}
}