#![cfg(feature = "redis")]
use armature_cache::{CacheConfig, CacheError, CacheStore, RedisCache, TaggedCache};
use armature_testkit::containers::RedisContainer;
use std::sync::Arc;
use std::time::Duration;
#[tokio::test]
async fn redis_operation_timeout_is_enforced() {
armature_testkit::skip_if_no_docker!();
let redis = RedisContainer::start().await;
let config = CacheConfig::redis(redis.url())
.unwrap()
.with_connection_timeout(Duration::from_secs(5))
.with_operation_timeout(Duration::from_millis(200));
let cache = RedisCache::new(config).await.expect("connect to redis");
let client = redis::Client::open(redis.url()).unwrap();
let mut side = client
.get_multiplexed_async_connection()
.await
.expect("side connection");
tokio::spawn(async move {
let _: redis::RedisResult<()> = redis::cmd("DEBUG")
.arg("SLEEP")
.arg(2.0_f64)
.query_async(&mut side)
.await;
});
tokio::time::sleep(Duration::from_millis(100)).await;
let err = cache
.get_json("some-key")
.await
.expect_err("op against a stalled server must time out, not block");
assert!(
matches!(err, CacheError::Timeout),
"expected CacheError::Timeout, got: {err:?}"
);
}
#[tokio::test]
async fn redis_normal_operations_work_with_tuning() {
armature_testkit::skip_if_no_docker!();
let redis = RedisContainer::start().await;
let config = CacheConfig::redis(redis.url())
.unwrap()
.with_key_prefix("armature-cache-test")
.with_connection_timeout(Duration::from_secs(5))
.with_operation_timeout(Duration::from_secs(3))
.with_max_connections(4);
let cache = RedisCache::new(config).await.expect("connect to redis");
cache
.set_json("k", "\"value\"".to_string(), Some(Duration::from_secs(60)))
.await
.unwrap();
assert_eq!(
cache.get_json("k").await.unwrap(),
Some("\"value\"".to_string())
);
assert_eq!(cache.increment("counter", 5).await.unwrap(), 5);
assert_eq!(cache.increment("counter", 3).await.unwrap(), 8);
cache.delete("k").await.unwrap();
assert_eq!(cache.get_json("k").await.unwrap(), None);
}
#[tokio::test]
async fn redis_native_set_primitives_round_trip() {
armature_testkit::skip_if_no_docker!();
let redis = RedisContainer::start().await;
let config = CacheConfig::redis(redis.url()).unwrap();
let cache = RedisCache::new(config).await.expect("connect to redis");
cache.set_add("myset", "a").await.unwrap();
cache.set_add("myset", "b").await.unwrap();
cache.set_add("myset", "a").await.unwrap();
let mut members = cache.set_members("myset").await.unwrap();
members.sort();
assert_eq!(members, vec!["a".to_string(), "b".to_string()]);
cache.set_remove("myset", "a").await.unwrap();
assert_eq!(
cache.set_members("myset").await.unwrap(),
vec!["b".to_string()]
);
let mut conn = redis::Client::open(redis.url())
.unwrap()
.get_multiplexed_async_connection()
.await
.unwrap();
let raw: Vec<String> = redis::AsyncCommands::smembers(&mut conn, "myset")
.await
.unwrap();
assert_eq!(raw, vec!["b".to_string()]);
}
#[tokio::test]
async fn redis_tagged_cache_tag_index_visible_across_instances() {
armature_testkit::skip_if_no_docker!();
let redis = RedisContainer::start().await;
let config = || CacheConfig::redis(redis.url()).unwrap();
let cache_a = Arc::new(RedisCache::new(config()).await.expect("connect a"));
let cache_b = Arc::new(RedisCache::new(config()).await.expect("connect b"));
let instance_a = TaggedCache::new(cache_a);
let instance_b = TaggedCache::new(cache_b);
instance_a
.set_with_tags(
"user:1",
"\"Alice\"".to_string(),
&["users"],
Some(Duration::from_secs(60)),
)
.await
.unwrap();
let keys = instance_b.get_keys_by_tag("users").await.unwrap();
assert_eq!(keys, vec!["user:1".to_string()]);
instance_b.invalidate_tag("users").await.unwrap();
assert_eq!(instance_a.get("user:1").await.unwrap(), None);
assert!(instance_a.list_tags().await.unwrap().is_empty());
}
#[tokio::test]
async fn redis_clear_with_key_prefix_is_scoped_not_flushdb() {
armature_testkit::skip_if_no_docker!();
let redis = RedisContainer::start().await;
let mine_config = CacheConfig::redis(redis.url())
.unwrap()
.with_key_prefix("service-a");
let mine = RedisCache::new(mine_config).await.expect("connect mine");
let other_config = CacheConfig::redis(redis.url())
.unwrap()
.with_key_prefix("service-b");
let other = RedisCache::new(other_config).await.expect("connect other");
for i in 0..20 {
mine.set_json(&format!("k{i}"), "\"v\"".to_string(), None)
.await
.unwrap();
}
other
.set_json("k0", "\"other-value\"".to_string(), None)
.await
.unwrap();
mine.clear().await.unwrap();
for i in 0..20 {
assert_eq!(
mine.get_json(&format!("k{i}")).await.unwrap(),
None,
"service-a's own key {i} should have been cleared"
);
}
assert_eq!(
other.get_json("k0").await.unwrap(),
Some("\"other-value\"".to_string()),
"service-b's key must survive service-a's scoped clear()"
);
}
#[tokio::test]
async fn redis_clear_without_key_prefix_still_flushes_whole_db() {
armature_testkit::skip_if_no_docker!();
let redis = RedisContainer::start().await;
let config = CacheConfig::redis(redis.url()).unwrap();
let cache = RedisCache::new(config).await.expect("connect");
cache
.set_json("unscoped-key", "\"v\"".to_string(), None)
.await
.unwrap();
cache.clear().await.unwrap();
assert_eq!(cache.get_json("unscoped-key").await.unwrap(), None);
}
#[tokio::test]
async fn redis_supports_atomic_sets_is_true() {
armature_testkit::skip_if_no_docker!();
let redis = RedisContainer::start().await;
let config = CacheConfig::redis(redis.url()).unwrap();
let cache = RedisCache::new(config).await.expect("connect");
assert!(cache.supports_atomic_sets());
}