#![cfg(feature = "memcached")]
use armature_cache::{CacheConfig, CacheStore, MemcachedCache};
use std::time::Duration;
use testcontainers::GenericImage;
use testcontainers::core::IntoContainerPort;
use testcontainers::runners::AsyncRunner;
async fn connect(url: &str) -> MemcachedCache {
let deadline = std::time::Instant::now() + Duration::from_secs(10);
loop {
match MemcachedCache::new(CacheConfig::memcached(url).unwrap()).await {
Ok(cache) => return cache,
Err(_) if std::time::Instant::now() < deadline => {
tokio::time::sleep(Duration::from_millis(100)).await;
}
Err(e) => panic!("could not connect to memcached at {url}: {e}"),
}
}
}
#[tokio::test]
async fn memcached_increment_create_at_zero_and_atomic() {
armature_testkit::skip_if_no_docker!();
let image = GenericImage::new("memcached", "1.6-alpine").with_exposed_port(11211.tcp());
let container = image.start().await.expect("start memcached container");
let port = container
.get_host_port_ipv4(11211.tcp())
.await
.expect("memcached mapped port");
let url = format!("memcache://127.0.0.1:{port}");
let cache = connect(&url).await;
cache.clear().await.unwrap();
assert_eq!(cache.increment("counter", 5).await.unwrap(), 0);
assert_eq!(cache.increment("counter", 3).await.unwrap(), 3);
assert_eq!(cache.decrement("counter", 2).await.unwrap(), 1);
assert_eq!(cache.decrement("fresh", 5).await.unwrap(), 0);
assert_eq!(
cache.get_json("counter").await.unwrap().as_deref(),
Some("1")
);
}
#[tokio::test]
async fn memcached_increment_never_fabricates_delta_abs() {
armature_testkit::skip_if_no_docker!();
let image = GenericImage::new("memcached", "1.6-alpine").with_exposed_port(11211.tcp());
let container = image.start().await.expect("start memcached container");
let port = container
.get_host_port_ipv4(11211.tcp())
.await
.expect("memcached mapped port");
let url = format!("memcache://127.0.0.1:{port}");
let cache = connect(&url).await;
cache.clear().await.unwrap();
let base: u64 = u64::MAX - 10; cache.set_json("big", base.to_string(), None).await.unwrap();
let returned = cache.increment("big", 1).await.unwrap();
let expected = (base + 1) as i64;
assert_eq!(
returned, expected,
"increment must return the true server counter, not delta.abs()"
);
assert_ne!(returned, 1, "must not fabricate delta.abs()");
}
#[tokio::test]
async fn memcached_mget_batches_and_preserves_order() {
armature_testkit::skip_if_no_docker!();
let image = GenericImage::new("memcached", "1.6-alpine").with_exposed_port(11211.tcp());
let container = image.start().await.expect("start memcached container");
let port = container
.get_host_port_ipv4(11211.tcp())
.await
.expect("memcached mapped port");
let url = format!("memcache://127.0.0.1:{port}");
let cache = connect(&url).await;
cache.clear().await.unwrap();
cache.set_json("a", "1".to_string(), None).await.unwrap();
cache.set_json("c", "3".to_string(), None).await.unwrap();
let got = cache.mget(&["a", "b", "c"]).await.unwrap();
assert_eq!(
got,
vec![Some("1".to_string()), None, Some("3".to_string())]
);
assert!(cache.mget(&[]).await.unwrap().is_empty());
let misses = cache.mget(&["x", "y"]).await.unwrap();
assert_eq!(misses, vec![None, None]);
}
#[tokio::test]
async fn memcached_expire_touches_ttl_in_place() {
armature_testkit::skip_if_no_docker!();
let image = GenericImage::new("memcached", "1.6-alpine").with_exposed_port(11211.tcp());
let container = image.start().await.expect("start memcached container");
let port = container
.get_host_port_ipv4(11211.tcp())
.await
.expect("memcached mapped port");
let url = format!("memcache://127.0.0.1:{port}");
let cache = connect(&url).await;
cache.clear().await.unwrap();
cache
.set_json(
"present",
"payload".to_string(),
Some(Duration::from_secs(1)),
)
.await
.unwrap();
cache
.expire("present", Duration::from_secs(3600))
.await
.unwrap();
assert_eq!(
cache.get_json("present").await.unwrap().as_deref(),
Some("payload")
);
let err = cache
.expire("absent", Duration::from_secs(60))
.await
.unwrap_err();
assert!(
matches!(err, armature_cache::CacheError::NotFound(_)),
"expected NotFound, got {err:?}"
);
}
#[tokio::test]
async fn memcached_get_json_propagates_backend_errors_not_masked_as_miss() {
armature_testkit::skip_if_no_docker!();
let image = GenericImage::new("memcached", "1.6-alpine").with_exposed_port(11211.tcp());
let container = image.start().await.expect("start memcached container");
let port = container
.get_host_port_ipv4(11211.tcp())
.await
.expect("memcached mapped port");
let url = format!("memcache://127.0.0.1:{port}");
let cache = connect(&url).await;
cache.clear().await.unwrap();
assert_eq!(cache.get_json("missing").await.unwrap(), None);
cache.set_json("k", "v".to_string(), None).await.unwrap();
assert_eq!(cache.get_json("k").await.unwrap(), Some("v".to_string()));
container
.stop_with_timeout(Some(0))
.await
.expect("stop memcached container");
let err = cache.get_json("k").await.unwrap_err();
assert!(
matches!(err, armature_cache::CacheError::Memcached(_)),
"expected a propagated Memcached error, got {err:?}"
);
}