#![cfg(feature = "cache-redis")]
use std::time::Duration;
use rs_zero::{
cache_redis::{RedisBreakerConfig, RedisCacheConfig, RedisClusterConfig},
lock::{DistributedLock, LockError, RedisDistributedLock, RedisLockConfig},
};
#[test]
fn redis_lock_defaults_require_ttl_and_namespace() {
let config = RedisLockConfig {
namespace: "locks".to_string(),
..RedisLockConfig::default()
};
config.validate().expect("valid config");
let invalid = RedisLockConfig {
namespace: " ".to_string(),
..config
};
assert!(matches!(
invalid.validate().expect_err("namespace required"),
LockError::InvalidConfig(message) if message.contains("namespace")
));
}
#[tokio::test]
async fn redis_lock_rejects_zero_ttl_before_redis_io() {
let lock = RedisDistributedLock::new(RedisLockConfig::default()).expect("lock backend");
let error = lock
.acquire("unit:{zero-ttl}", Duration::ZERO)
.await
.expect_err("zero ttl");
assert!(matches!(error, LockError::InvalidConfig(message) if message.contains("ttl")));
}
#[tokio::test]
async fn redis_cluster_lock_requires_hash_tag_before_redis_io() {
let lock = RedisDistributedLock::new(RedisLockConfig {
redis: RedisCacheConfig {
url: "redis://127.0.0.1:6379".to_string(),
cluster: RedisClusterConfig {
enabled: true,
..RedisClusterConfig::default()
},
..RedisCacheConfig::default()
},
..RedisLockConfig::default()
})
.expect("lock backend");
let error = lock
.acquire("order:123", Duration::from_secs(1))
.await
.expect_err("missing hash tag");
assert!(matches!(error, LockError::InvalidConfig(message) if message.contains("hash tag")));
}
#[tokio::test]
async fn redis_lock_breaker_fast_fails_after_backend_error() {
let lock = RedisDistributedLock::new(RedisLockConfig {
redis: RedisCacheConfig {
url: "redis://127.0.0.1:1".to_string(),
connect_timeout: Duration::from_millis(50),
command_timeout: Duration::from_millis(50),
..RedisCacheConfig::default()
},
breaker: RedisBreakerConfig::fast_failure(1, Duration::from_secs(60)),
..RedisLockConfig::default()
})
.expect("lock backend");
let first = lock
.try_acquire("unit:{breaker}", Duration::from_secs(1))
.await
.expect_err("connection should fail");
assert!(first.to_string().contains("connection") || first.to_string().contains("redis"));
let second = lock
.try_acquire("unit:{breaker}", Duration::from_secs(1))
.await
.expect_err("breaker should reject");
assert!(matches!(second, LockError::BreakerOpen(_)));
}
#[cfg(feature = "observability")]
#[test]
fn redis_lock_metrics_use_low_cardinality_command_labels() {
let metrics = rs_zero::observability::MetricsRegistry::new();
metrics.record_redis_command(
rs_zero::observability::RedisMetricLabels::new("LOCK_ACQUIRE", "primary", "success"),
Duration::from_millis(1),
);
let text = metrics.render_prometheus();
assert!(text.contains("command=\"LOCK_ACQUIRE\",shard=\"primary\",result=\"success\""));
assert!(!text.contains("order:123"));
assert!(!text.contains("redis://"));
}