use anyhow::Result;
use nntp_proxy::cache::ArticleCache;
use nntp_proxy::protocol::RequestKind;
use nntp_proxy::types::{BackendId, MessageId};
use std::sync::Arc;
use std::time::Duration;
use tokio::time::timeout;
use super::article_response_bytes;
async fn wait_until_cache_miss(cache: &ArticleCache, msgid: &MessageId<'_>, within: Duration) {
timeout(within, async {
loop {
if cache.get(msgid).await.is_none() {
return;
}
tokio::task::yield_now().await;
}
})
.await
.unwrap_or_else(|_| panic!("Timed out waiting for {msgid} to expire from cache"));
}
#[tokio::test]
async fn test_cache_hit() -> Result<()> {
let cache = Arc::new(ArticleCache::new(20000, Duration::from_secs(300)));
let msgid = MessageId::from_borrowed("<test@example.com>").unwrap();
let buffer = b"220 0 <test@example.com>\r\nSubject: Test\r\n\r\nBody\r\n.\r\n".to_vec();
cache
.upsert_ingest(
msgid.clone(),
buffer.clone(),
BackendId::from_index(0),
0.into(),
)
.await;
let retrieved = cache.get(&msgid).await;
assert!(retrieved.is_some());
assert_eq!(
article_response_bytes(&retrieved.unwrap(), RequestKind::Article, &msgid).unwrap(),
buffer
);
Ok(())
}
#[tokio::test]
async fn test_cache_miss() -> Result<()> {
let cache = Arc::new(ArticleCache::new(20000, Duration::from_secs(300)));
let msgid = MessageId::from_borrowed("<nonexistent@example.com>").unwrap();
let result = cache.get(&msgid).await;
assert!(result.is_none());
Ok(())
}
#[tokio::test]
async fn test_multiple_cache_entries() -> Result<()> {
let cache = Arc::new(ArticleCache::new(20000, Duration::from_secs(300)));
for i in 1..=5 {
let msg_str = format!("<test{i}@example.com>");
let msgid = MessageId::from_borrowed(&msg_str).unwrap();
let buffer = format!("220 Body {i}\r\n.\r\n").into_bytes();
cache
.upsert_ingest(msgid, buffer, BackendId::from_index(0), 0.into())
.await;
}
cache.sync().await;
let stats = cache.stats();
assert_eq!(stats.entry_count, 5);
for i in 1..=5 {
let msg_str = format!("<test{i}@example.com>");
let msgid = MessageId::from_borrowed(&msg_str).unwrap();
assert!(cache.get(&msgid).await.is_some());
}
Ok(())
}
#[tokio::test]
async fn test_cache_ttl_expiration() -> Result<()> {
let cache = Arc::new(ArticleCache::new(20000, Duration::from_millis(100)));
let msgid = MessageId::from_borrowed("<test@example.com>").unwrap();
let buffer = b"220 0 <test@example.com>\r\nBody\r\n.\r\n".to_vec();
cache
.upsert_ingest(msgid.clone(), buffer, BackendId::from_index(0), 0.into())
.await;
assert!(cache.get(&msgid).await.is_some());
wait_until_cache_miss(&cache, &msgid, Duration::from_secs(1)).await;
Ok(())
}
#[tokio::test]
async fn test_cache_capacity_limit() -> Result<()> {
let cache = Arc::new(ArticleCache::new(2, Duration::from_secs(300)));
for i in 1..=3 {
let msg_str = format!("<test{i}@example.com>");
let msgid = MessageId::from_borrowed(&msg_str).unwrap();
let buffer = format!("220 Body {i}\r\n.\r\n").into_bytes();
cache
.upsert_ingest(msgid, buffer, BackendId::from_index(0), 0.into())
.await;
}
cache.sync().await;
let stats = cache.stats();
assert!(
stats.entry_count <= 2,
"Cache should respect capacity limit, got {}",
stats.entry_count
);
Ok(())
}
#[tokio::test]
async fn test_cache_different_message_id_formats() -> Result<()> {
let cache = Arc::new(ArticleCache::new(20000, Duration::from_secs(300)));
let test_cases = vec![
"<simple@example.com>",
"<complex+tag@sub.domain.com>",
"<123456@news.server>",
"<$uuid@host>",
];
for msg_str in test_cases {
let msgid = MessageId::from_borrowed(msg_str).unwrap();
let buffer = format!("220 {msg_str}\r\n.\r\n").into_bytes();
cache
.upsert_ingest(msgid.clone(), buffer, BackendId::from_index(0), 0.into())
.await;
assert!(cache.get(&msgid).await.is_some());
}
Ok(())
}
#[tokio::test]
async fn test_cache_stats() -> Result<()> {
let cache = Arc::new(ArticleCache::new(100_000, Duration::from_secs(300)));
let stats = cache.stats();
assert_eq!(stats.entry_count, 0);
for i in 1..=10 {
let msg_str = format!("<test{i}@example.com>");
let msgid = MessageId::from_borrowed(&msg_str).unwrap();
let mut buffer = vec![0u8; 1024];
buffer[0..3].copy_from_slice(b"220");
cache
.upsert_ingest(msgid, buffer, BackendId::from_index(0), 0.into())
.await;
}
cache.sync().await;
let stats = cache.stats();
assert_eq!(stats.entry_count, 10);
assert!(stats.weighted_size > 0);
Ok(())
}
#[tokio::test]
async fn test_zero_allocation_lookup() -> Result<()> {
let cache = Arc::new(ArticleCache::new(20000, Duration::from_secs(300)));
let msgid1 = MessageId::from_borrowed("<test@example.com>").unwrap();
let buffer = b"220 Body\r\n.\r\n".to_vec();
cache
.upsert_ingest(msgid1, buffer, BackendId::from_index(0), 0.into())
.await;
let msgid2 = MessageId::from_borrowed("<test@example.com>").unwrap();
assert!(cache.get(&msgid2).await.is_some());
Ok(())
}