use crate::{BadRequest, Cache, HttpCacheError};
use std::sync::Arc;
use http_cache::*;
use reqwest::Client;
use reqwest_middleware::ClientBuilder;
#[cfg(any(feature = "streaming", feature = "rate-limiting"))]
use wiremock::matchers::path;
use wiremock::{
matchers::{header, method},
Mock, MockServer, ResponseTemplate,
};
fn create_cache_manager() -> CACacheManager {
let cache_dir = tempfile::tempdir().expect("Failed to create temp dir");
let path = cache_dir.path().to_path_buf();
std::mem::forget(cache_dir);
CACacheManager::new(path, true)
}
pub(crate) fn build_mock(
cache_control_val: &str,
body: &[u8],
status: u16,
expect: u64,
) -> Mock {
Mock::given(method(GET))
.respond_with(
ResponseTemplate::new(status)
.insert_header("cache-control", cache_control_val)
.set_body_bytes(body),
)
.expect(expect)
}
const GET: &str = "GET";
const TEST_BODY: &[u8] = b"test";
const CACHEABLE_PUBLIC: &str = "max-age=86400, public";
#[test]
#[allow(clippy::default_constructed_unit_structs)]
fn test_errors() -> Result<()> {
let br = BadRequest::default();
assert_eq!(format!("{:?}", br.clone()), "BadRequest",);
assert_eq!(
br.to_string(),
"Request object is not cloneable. Are you passing a streaming body?"
.to_string(),
);
let reqwest_err = HttpCacheError::cache("test cache error".to_string());
assert!(format!("{:?}", &reqwest_err).contains("Cache"));
assert_eq!(
reqwest_err.to_string(),
"Cache error: test cache error".to_string(),
);
Ok(())
}
#[tokio::test]
async fn default_mode() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock(CACHEABLE_PUBLIC, TEST_BODY, 200, 1);
let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: Default::default(),
}))
.build();
client.get(url.clone()).send().await?;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_some());
let res = client.get(url).send().await?;
assert_eq!(res.bytes().await?, TEST_BODY);
Ok(())
}
#[tokio::test]
async fn default_mode_with_options() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock(CACHEABLE_PUBLIC, TEST_BODY, 200, 1);
let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: HttpCacheOptions {
cache_options: Some(CacheOptions {
shared: false,
..Default::default()
}),
..HttpCacheOptions::default()
},
}))
.build();
client.get(url.clone()).send().await?;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_some());
Ok(())
}
#[tokio::test]
async fn no_cache_mode() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock(CACHEABLE_PUBLIC, TEST_BODY, 200, 2);
let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::NoCache,
manager: manager.clone(),
options: Default::default(),
}))
.build();
client.get(url.clone()).send().await?;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_some());
client.get(url).send().await?;
Ok(())
}
#[tokio::test]
async fn reload_mode() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock(CACHEABLE_PUBLIC, TEST_BODY, 200, 2);
let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Reload,
manager: manager.clone(),
options: HttpCacheOptions {
cache_options: Some(CacheOptions {
shared: false,
..Default::default()
}),
..HttpCacheOptions::default()
},
}))
.build();
client.get(url.clone()).send().await?;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_some());
client.get(url).send().await?;
Ok(())
}
#[tokio::test]
async fn custom_cache_key() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock(CACHEABLE_PUBLIC, TEST_BODY, 200, 1);
let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: HttpCacheOptions {
cache_key: Some(Arc::new(|req: &http::request::Parts| {
format!("{}:{}:{:?}:test", req.method, req.uri, req.version)
})),
..Default::default()
},
}))
.build();
client.get(url.clone()).send().await?;
let data = CacheManager::get(
&manager,
&format!("{}:{}:{:?}:test", GET, &url, http::Version::HTTP_11),
)
.await?;
assert!(data.is_some());
Ok(())
}
#[tokio::test]
async fn custom_cache_mode_fn() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock(CACHEABLE_PUBLIC, TEST_BODY, 200, 2);
let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/test.css", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::NoStore,
manager: manager.clone(),
options: HttpCacheOptions {
cache_key: None,
cache_options: None,
cache_mode_fn: Some(Arc::new(|req: &http::request::Parts| {
if req.uri.path().ends_with(".css") {
CacheMode::Default
} else {
CacheMode::NoStore
}
})),
cache_bust: None,
cache_status_headers: true,
max_ttl: None,
..Default::default()
},
}))
.build();
client.get(url.clone()).send().await?;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_some());
let url = format!("{}/", &mock_server.uri());
client.get(url.clone()).send().await?;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_none());
Ok(())
}
#[tokio::test]
async fn override_cache_mode() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock(CACHEABLE_PUBLIC, TEST_BODY, 200, 2);
let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/test.css", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: Default::default(),
}))
.build();
client.get(url.clone()).send().await?;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_some());
let url = format!("{}/", &mock_server.uri());
client.get(url.clone()).with_extension(CacheMode::NoStore).send().await?;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_none());
Ok(())
}
#[tokio::test]
async fn no_status_headers() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock(CACHEABLE_PUBLIC, TEST_BODY, 200, 1);
let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/test.css", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: HttpCacheOptions {
cache_key: None,
cache_options: None,
cache_mode_fn: None,
cache_bust: None,
cache_status_headers: false,
max_ttl: None,
..Default::default()
},
}))
.build();
let res = client.get(url.clone()).send().await?;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_some());
assert!(res.headers().get(XCACHELOOKUP).is_none());
assert!(res.headers().get(XCACHE).is_none());
Ok(())
}
#[tokio::test]
async fn cache_bust() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock(CACHEABLE_PUBLIC, TEST_BODY, 200, 2);
let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: HttpCacheOptions {
cache_key: None,
cache_options: None,
cache_mode_fn: None,
cache_bust: Some(Arc::new(
|req: &http::request::Parts, _, _| {
if req.uri.path().ends_with("/bust-cache") {
vec![format!(
"{}:{}://{}:{}/",
GET,
req.uri.scheme_str().unwrap(),
req.uri.host().unwrap(),
req.uri.port_u16().unwrap_or(80)
)]
} else {
Vec::new()
}
},
)),
cache_status_headers: true,
max_ttl: None,
..Default::default()
},
}))
.build();
client.get(url.clone()).send().await?;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_some());
client.get(format!("{}/bust-cache", &mock_server.uri())).send().await?;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_none());
Ok(())
}
#[tokio::test]
async fn delete_after_non_get_head_method_request() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock(CACHEABLE_PUBLIC, TEST_BODY, 200, 1);
let _mock_guard = mock_server.register_as_scoped(m).await;
let post_mock = Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_bytes(TEST_BODY))
.expect(1);
let _post_guard = mock_server.register_as_scoped(post_mock).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: Default::default(),
}))
.build();
client.get(url.clone()).send().await?;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_some());
client.post(url.clone()).send().await?;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_none());
Ok(())
}
#[tokio::test]
async fn default_mode_no_cache_response() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock("no-cache", TEST_BODY, 200, 2);
let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: Default::default(),
}))
.build();
client.get(url.clone()).send().await?;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_some());
let res = client.get(url).send().await?;
assert_eq!(res.bytes().await?, TEST_BODY);
Ok(())
}
#[tokio::test]
async fn removes_warning() -> Result<()> {
let mock_server = MockServer::start().await;
let m = Mock::given(method(GET))
.respond_with(
ResponseTemplate::new(200)
.insert_header("cache-control", CACHEABLE_PUBLIC)
.insert_header("warning", "101 Test")
.set_body_bytes(TEST_BODY),
)
.expect(1);
let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: Default::default(),
}))
.build();
client.get(url.clone()).send().await?;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_some());
let res = client.get(url).send().await?;
assert!(res.headers().get("warning").is_none());
assert_eq!(res.bytes().await?, TEST_BODY);
Ok(())
}
#[tokio::test]
async fn no_store_mode() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock(CACHEABLE_PUBLIC, TEST_BODY, 200, 2);
let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::NoStore,
manager: manager.clone(),
options: Default::default(),
}))
.build();
client.get(url.clone()).send().await?;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_none());
let res = client.get(url).send().await?;
assert_eq!(res.bytes().await?, TEST_BODY);
Ok(())
}
#[tokio::test]
async fn force_cache_mode() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock("max-age=0, public", TEST_BODY, 200, 1);
let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::ForceCache,
manager: manager.clone(),
options: Default::default(),
}))
.build();
client.get(url.clone()).send().await?;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_some());
let res = client.get(url).send().await?;
assert_eq!(res.bytes().await?, TEST_BODY);
Ok(())
}
#[tokio::test]
async fn ignore_rules_mode() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock("no-store, max-age=0, public", TEST_BODY, 200, 1);
let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::IgnoreRules,
manager: manager.clone(),
options: Default::default(),
}))
.build();
client.get(url.clone()).send().await?;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_some());
let res = client.get(url).send().await?;
assert_eq!(res.bytes().await?, TEST_BODY);
Ok(())
}
#[tokio::test]
async fn revalidation_304() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock("public, must-revalidate", TEST_BODY, 200, 1);
let m_304 = Mock::given(method(GET))
.respond_with(ResponseTemplate::new(304))
.expect(1);
let mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: Default::default(),
}))
.build();
client.get(url.clone()).send().await?;
drop(mock_guard);
let _mock_guard = mock_server.register_as_scoped(m_304).await;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_some());
let res = client.get(url).send().await?;
assert_eq!(res.bytes().await?, TEST_BODY);
Ok(())
}
#[tokio::test]
async fn revalidation_200() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock("max-age=0, must-revalidate", TEST_BODY, 200, 1);
let m_200 = build_mock("max-age=0, must-revalidate", b"updated", 200, 1);
let mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: Default::default(),
}))
.build();
client.get(url.clone()).send().await?;
drop(mock_guard);
let _mock_guard = mock_server.register_as_scoped(m_200).await;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_some());
let res = client.get(url).send().await?;
assert_eq!(res.bytes().await?, &b"updated"[..]);
Ok(())
}
#[tokio::test]
async fn no_duplicate_headers_on_revalidation() -> Result<()> {
let mock_server = MockServer::start().await;
let m = Mock::given(method(GET))
.and(header("x-test", "test"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("cache-control", "max-age=0, must-revalidate")
.set_body_bytes(TEST_BODY),
)
.expect(1);
let m_revalidation = Mock::given(method(GET))
.and(header("x-test", "test"))
.respond_with(ResponseTemplate::new(304))
.expect(1);
let mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: Default::default(),
}))
.build();
client.get(url.clone()).header("x-test", "test").send().await?;
drop(mock_guard);
let _mock_guard = mock_server.register_as_scoped(m_revalidation).await;
let res = client.get(url).header("x-test", "test").send().await?;
assert_eq!(res.bytes().await?, TEST_BODY);
Ok(())
}
#[tokio::test]
async fn revalidation_500() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock("public, must-revalidate", TEST_BODY, 200, 1);
let m_500 = Mock::given(method(GET))
.respond_with(ResponseTemplate::new(500))
.expect(1);
let mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: Default::default(),
}))
.build();
client.get(url.clone()).send().await?;
drop(mock_guard);
let _mock_guard = mock_server.register_as_scoped(m_500).await;
let data =
CacheManager::get(&manager, &format!("{}:{}", GET, &url_parse(&url)?))
.await?;
assert!(data.is_some());
let res = client.get(url).send().await?;
assert!(res.headers().get("warning").is_some());
assert_eq!(res.bytes().await?, TEST_BODY);
Ok(())
}
#[cfg(test)]
mod only_if_cached_mode {
use super::*;
#[tokio::test]
async fn miss() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock(CACHEABLE_PUBLIC, TEST_BODY, 200, 0);
let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::OnlyIfCached,
manager: manager.clone(),
options: Default::default(),
}))
.build();
let res = client.get(url.clone()).send().await?;
assert_eq!(res.status(), 504);
let data = CacheManager::get(
&manager,
&format!("{}:{}", GET, &url_parse(&url)?),
)
.await?;
assert!(data.is_none());
Ok(())
}
#[tokio::test]
async fn hit() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock(CACHEABLE_PUBLIC, TEST_BODY, 200, 1);
let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: Default::default(),
}))
.build();
client.get(url.clone()).send().await?;
let data = CacheManager::get(
&manager,
&format!("{}:{}", GET, &url_parse(&url)?),
)
.await?;
assert!(data.is_some());
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::OnlyIfCached,
manager: manager.clone(),
options: Default::default(),
}))
.build();
let res = client.get(url).send().await?;
assert_eq!(res.bytes().await?, TEST_BODY);
Ok(())
}
}
#[tokio::test]
async fn head_request_caching() -> Result<()> {
let mock_server = MockServer::start().await;
let m = Mock::given(method("HEAD"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("cache-control", CACHEABLE_PUBLIC)
.insert_header("content-type", "text/plain")
.insert_header("content-length", "4"), )
.expect(1);
let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: Default::default(),
}))
.build();
let res = client.head(url.clone()).send().await?;
assert_eq!(res.status(), 200);
assert_eq!(res.headers().get("content-type").unwrap(), "text/plain");
let body = res.bytes().await?;
assert_eq!(body.len(), 0);
let data =
CacheManager::get(&manager, &format!("HEAD:{}", &url_parse(&url)?))
.await?;
assert!(data.is_some());
let cached_response = data.unwrap().0;
assert_eq!(cached_response.status, 200);
assert_eq!(cached_response.body.len(), 0);
Ok(())
}
#[tokio::test]
async fn head_request_cached_like_get() -> Result<()> {
let mock_server = MockServer::start().await;
let m_get = Mock::given(method("GET"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("cache-control", CACHEABLE_PUBLIC)
.insert_header("content-type", "text/plain")
.insert_header("etag", "\"12345\"")
.set_body_bytes(TEST_BODY),
)
.expect(1);
let m_head = Mock::given(method("HEAD"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("cache-control", CACHEABLE_PUBLIC)
.insert_header("content-type", "text/plain")
.insert_header("etag", "\"12345\"")
.insert_header("content-length", "4"),
)
.expect(1);
let mock_guard_get = mock_server.register_as_scoped(m_get).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: Default::default(),
}))
.build();
let get_res = client.get(url.clone()).send().await?;
assert_eq!(get_res.status(), 200);
assert_eq!(get_res.bytes().await?, TEST_BODY);
drop(mock_guard_get);
let _mock_guard_head = mock_server.register_as_scoped(m_head).await;
let head_res = client.head(url.clone()).send().await?;
assert_eq!(head_res.status(), 200);
assert_eq!(head_res.headers().get("etag").unwrap(), "\"12345\"");
let get_data =
CacheManager::get(&manager, &format!("GET:{}", &url_parse(&url)?))
.await?;
assert!(get_data.is_some());
let head_data =
CacheManager::get(&manager, &format!("HEAD:{}", &url_parse(&url)?))
.await?;
assert!(head_data.is_some());
Ok(())
}
#[tokio::test]
async fn put_request_invalidates_cache() -> Result<()> {
let mock_server = MockServer::start().await;
let m_get = Mock::given(method("GET"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("cache-control", CACHEABLE_PUBLIC)
.set_body_bytes(TEST_BODY),
)
.expect(1);
let m_put = Mock::given(method("PUT"))
.respond_with(ResponseTemplate::new(204))
.expect(1);
let mock_guard_get = mock_server.register_as_scoped(m_get).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: Default::default(),
}))
.build();
client.get(url.clone()).send().await?;
let data =
CacheManager::get(&manager, &format!("GET:{}", &url_parse(&url)?))
.await?;
assert!(data.is_some());
drop(mock_guard_get);
let _mock_guard_put = mock_server.register_as_scoped(m_put).await;
let put_res = client.put(url.clone()).send().await?;
assert_eq!(put_res.status(), 204);
let data =
CacheManager::get(&manager, &format!("GET:{}", &url_parse(&url)?))
.await?;
assert!(data.is_none());
Ok(())
}
#[tokio::test]
async fn patch_request_invalidates_cache() -> Result<()> {
let mock_server = MockServer::start().await;
let m_get = Mock::given(method("GET"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("cache-control", CACHEABLE_PUBLIC)
.set_body_bytes(TEST_BODY),
)
.expect(1);
let m_patch = Mock::given(method("PATCH"))
.respond_with(ResponseTemplate::new(200))
.expect(1);
let mock_guard_get = mock_server.register_as_scoped(m_get).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: Default::default(),
}))
.build();
client.get(url.clone()).send().await?;
let data =
CacheManager::get(&manager, &format!("GET:{}", &url_parse(&url)?))
.await?;
assert!(data.is_some());
drop(mock_guard_get);
let _mock_guard_patch = mock_server.register_as_scoped(m_patch).await;
let patch_res = client.patch(url.clone()).send().await?;
assert_eq!(patch_res.status(), 200);
let data =
CacheManager::get(&manager, &format!("GET:{}", &url_parse(&url)?))
.await?;
assert!(data.is_none());
Ok(())
}
#[tokio::test]
async fn delete_request_invalidates_cache() -> Result<()> {
let mock_server = MockServer::start().await;
let m_get = Mock::given(method("GET"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("cache-control", CACHEABLE_PUBLIC)
.set_body_bytes(TEST_BODY),
)
.expect(1);
let m_delete = Mock::given(method("DELETE"))
.respond_with(ResponseTemplate::new(204))
.expect(1);
let mock_guard_get = mock_server.register_as_scoped(m_get).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: Default::default(),
}))
.build();
client.get(url.clone()).send().await?;
let data =
CacheManager::get(&manager, &format!("GET:{}", &url_parse(&url)?))
.await?;
assert!(data.is_some());
drop(mock_guard_get);
let _mock_guard_delete = mock_server.register_as_scoped(m_delete).await;
let delete_res = client.delete(url.clone()).send().await?;
assert_eq!(delete_res.status(), 204);
let data =
CacheManager::get(&manager, &format!("GET:{}", &url_parse(&url)?))
.await?;
assert!(data.is_none());
Ok(())
}
#[tokio::test]
async fn options_request_not_cached() -> Result<()> {
let mock_server = MockServer::start().await;
let m = Mock::given(method("OPTIONS"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("allow", "GET, POST, PUT, DELETE")
.insert_header("cache-control", CACHEABLE_PUBLIC), )
.expect(2); let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/", &mock_server.uri());
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: Default::default(),
}))
.build();
let res1 =
client.request(reqwest::Method::OPTIONS, url.clone()).send().await?;
assert_eq!(res1.status(), 200);
let data =
CacheManager::get(&manager, &format!("OPTIONS:{}", &url_parse(&url)?))
.await?;
assert!(data.is_none());
let res2 =
client.request(reqwest::Method::OPTIONS, url.clone()).send().await?;
assert_eq!(res2.status(), 200);
Ok(())
}
#[cfg(feature = "streaming")]
#[tokio::test]
async fn test_multipart_form_cloning_issue() -> Result<()> {
let manager = CACacheManager::new(".cache".into(), true);
let mock_server = MockServer::start().await;
let mock = Mock::given(method("POST"))
.and(path("/api/upload"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("content-type", "application/json")
.insert_header("cache-control", "no-cache") .set_body_bytes(r#"{"status": "uploaded"}"#),
)
.expect(1);
let _mock_guard = mock_server.register_as_scoped(mock).await;
let client = ClientBuilder::new(
Client::builder()
.build()
.expect("should be able to construct reqwest client"),
)
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager,
options: Default::default(),
}))
.build();
use bytes::Bytes;
use futures_util::stream;
use reqwest::Body;
let file_content = b"fake file content for testing";
let stream = stream::iter(vec![Ok::<_, reqwest::Error>(Bytes::from(
file_content.to_vec(),
))]);
let body = Body::wrap_stream(stream);
let url = format!("{}/api/upload", mock_server.uri());
let result = client
.post(&url)
.header("Accept", "application/json")
.header("api-key", "test-key")
.header("content-type", "application/octet-stream")
.body(body)
.send()
.await;
match result {
Ok(response) => {
assert_eq!(response.status(), 200);
}
Err(e) => {
panic!("Expected graceful fallback, but got error: {}", e);
}
}
Ok(())
}
#[cfg(all(test, feature = "streaming"))]
mod streaming_tests {
use super::*;
use crate::{HttpCacheStreamInterface, HttpStreamingCache, StreamingBody};
use bytes::Bytes;
use http::{Request, Response};
use http_body::Body;
use http_body_util::{BodyExt, Full};
use http_cache::StreamingManager;
async fn create_streaming_cache_manager() -> StreamingManager {
StreamingManager::with_temp_dir(1000)
.await
.expect("Failed to create streaming cache manager")
}
#[tokio::test]
async fn test_streaming_cache_basic_operations() -> Result<()> {
let manager = create_streaming_cache_manager().await;
let cache = HttpStreamingCache {
mode: CacheMode::Default,
manager,
options: Default::default(),
};
let request = Request::builder()
.uri("https://example.com/test")
.header("user-agent", "test-agent")
.body(())
.unwrap();
let (parts, _) = request.into_parts();
let analysis = cache.analyze_request(&parts, None)?;
assert!(!analysis.cache_key.is_empty());
assert!(analysis.should_cache);
let cached_response =
cache.lookup_cached_response(&analysis.cache_key).await?;
assert!(cached_response.is_none());
let response = Response::builder()
.status(200)
.header("content-type", "application/json")
.header("cache-control", "max-age=3600")
.body(Full::new(Bytes::from("streaming test data")))
.unwrap();
let cached_response =
cache.process_response(analysis.clone(), response, None).await?;
assert_eq!(cached_response.status(), 200);
let body_bytes =
cached_response.into_body().collect().await?.to_bytes();
assert_eq!(body_bytes, "streaming test data");
let cached_response =
cache.lookup_cached_response(&analysis.cache_key).await?;
assert!(cached_response.is_some());
if let Some((response, _policy)) = cached_response {
let body_bytes = response.into_body().collect().await?.to_bytes();
assert_eq!(body_bytes, "streaming test data");
}
Ok(())
}
#[tokio::test]
async fn test_streaming_cache_large_response() -> Result<()> {
let manager = create_streaming_cache_manager().await;
let cache = HttpStreamingCache {
mode: CacheMode::Default,
manager,
options: Default::default(),
};
let large_data = "x".repeat(1024 * 1024);
let request = Request::builder()
.uri("https://example.com/large")
.body(())
.unwrap();
let (parts, _) = request.into_parts();
let analysis = cache.analyze_request(&parts, None)?;
let response = Response::builder()
.status(200)
.header("content-type", "text/plain")
.header("cache-control", "max-age=3600")
.body(Full::new(Bytes::from(large_data.clone())))
.unwrap();
let cached_response =
cache.process_response(analysis.clone(), response, None).await?;
assert_eq!(cached_response.status(), 200);
let body_bytes =
cached_response.into_body().collect().await?.to_bytes();
assert_eq!(body_bytes.len(), 1024 * 1024);
assert_eq!(body_bytes, large_data.as_bytes());
let cached_response =
cache.lookup_cached_response(&analysis.cache_key).await?;
assert!(cached_response.is_some());
Ok(())
}
#[tokio::test]
async fn test_streaming_cache_empty_response() -> Result<()> {
let manager = create_streaming_cache_manager().await;
let cache = HttpStreamingCache {
mode: CacheMode::Default,
manager,
options: Default::default(),
};
let request = Request::builder()
.uri("https://example.com/empty")
.body(())
.unwrap();
let (parts, _) = request.into_parts();
let analysis = cache.analyze_request(&parts, None)?;
let response = Response::builder()
.status(204)
.header("cache-control", "max-age=3600")
.body(Full::new(Bytes::new()))
.unwrap();
let cached_response =
cache.process_response(analysis.clone(), response, None).await?;
assert_eq!(cached_response.status(), 204);
let body_bytes =
cached_response.into_body().collect().await?.to_bytes();
assert!(body_bytes.is_empty());
let cached_response =
cache.lookup_cached_response(&analysis.cache_key).await?;
assert!(cached_response.is_some());
Ok(())
}
#[tokio::test]
async fn test_streaming_cache_no_cache_mode() -> Result<()> {
let manager = create_streaming_cache_manager().await;
let cache = HttpStreamingCache {
mode: CacheMode::NoStore,
manager,
options: Default::default(),
};
let request = Request::builder()
.uri("https://example.com/no-cache")
.body(())
.unwrap();
let (parts, _) = request.into_parts();
let analysis = cache.analyze_request(&parts, None)?;
assert!(!analysis.should_cache);
Ok(())
}
#[tokio::test]
async fn test_streaming_body_operations() -> Result<()> {
let data = Bytes::from("test streaming body data");
let buffered_body: StreamingBody<Full<Bytes>> =
StreamingBody::buffered(data.clone());
assert!(!buffered_body.is_end_stream());
let size_hint = buffered_body.size_hint();
assert_eq!(size_hint.exact(), Some(data.len() as u64));
let collected = buffered_body.collect().await?.to_bytes();
assert_eq!(collected, data);
Ok(())
}
#[tokio::test]
async fn custom_response_cache_mode_fn() -> Result<()> {
let mock_server = MockServer::start().await;
let no_cache_mock = Mock::given(method(GET))
.and(path("/api/data"))
.respond_with(
ResponseTemplate::new(200)
.insert_header(
"cache-control",
"no-cache, no-store, must-revalidate",
)
.insert_header("pragma", "no-cache")
.set_body_bytes(TEST_BODY),
)
.expect(2);
let rate_limit_mock = Mock::given(method(GET))
.and(path("/api/rate-limited"))
.respond_with(
ResponseTemplate::new(429)
.insert_header("cache-control", "public, max-age=300")
.insert_header("retry-after", "60")
.set_body_bytes(b"Rate limit exceeded"),
)
.expect(2);
let _no_cache_guard =
mock_server.register_as_scoped(no_cache_mock).await;
let _rate_limit_guard =
mock_server.register_as_scoped(rate_limit_mock).await;
let manager = create_cache_manager();
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: HttpCacheOptions {
response_cache_mode_fn: Some(Arc::new(
|_request_parts, response| {
match response.status {
200..=299 => Some(CacheMode::ForceCache),
429 => Some(CacheMode::NoStore),
_ => None, }
},
)),
..Default::default()
},
}))
.build();
let success_url = format!("{}/api/data", &mock_server.uri());
let response = client.get(&success_url).send().await?;
assert_eq!(response.status(), 200);
let cache_key = format!("{}:{}", GET, &url_parse(&success_url)?);
let cached_data = CacheManager::get(&manager, &cache_key).await?;
assert!(cached_data.is_some());
let (cached_response, _) = cached_data.unwrap();
assert_eq!(cached_response.body, TEST_BODY);
let rate_limit_url = format!("{}/api/rate-limited", &mock_server.uri());
let response = client.get(&rate_limit_url).send().await?;
assert_eq!(response.status(), 429);
let cache_key = format!("{}:{}", GET, &url_parse(&rate_limit_url)?);
let cached_data = CacheManager::get(&manager, &cache_key).await?;
assert!(cached_data.is_none());
let response = client.get(&success_url).send().await?;
assert_eq!(response.status(), 200);
let response = client.get(&rate_limit_url).send().await?;
assert_eq!(response.status(), 429);
Ok(())
}
#[tokio::test]
async fn streaming_with_different_cache_modes() -> Result<()> {
let manager = create_streaming_cache_manager().await;
let cache_no_cache = HttpStreamingCache {
mode: CacheMode::NoCache,
manager: manager.clone(),
options: Default::default(),
};
let request = Request::builder()
.uri("https://example.com/streaming-no-cache")
.header("user-agent", "test-agent")
.body(())
.unwrap();
let (parts, _) = request.into_parts();
let analysis = cache_no_cache.analyze_request(&parts, None)?;
assert!(!analysis.cache_key.is_empty());
let cache_force = HttpStreamingCache {
mode: CacheMode::ForceCache,
manager: manager.clone(),
options: Default::default(),
};
let request2 = Request::builder()
.uri("https://example.com/streaming-force-cache")
.header("user-agent", "test-agent")
.body(())
.unwrap();
let (parts2, _) = request2.into_parts();
let analysis2 = cache_force.analyze_request(&parts2, None)?;
assert!(!analysis2.cache_key.is_empty());
assert!(analysis2.should_cache);
Ok(())
}
#[tokio::test]
async fn streaming_with_custom_cache_options() -> Result<()> {
let manager = create_streaming_cache_manager().await;
let cache = HttpStreamingCache {
mode: CacheMode::Default,
manager,
options: HttpCacheOptions {
cache_key: Some(Arc::new(|req: &http::request::Parts| {
format!("stream:{}:{}", req.method, req.uri)
})),
cache_options: Some(CacheOptions {
shared: false,
..Default::default()
}),
cache_mode_fn: Some(Arc::new(|req: &http::request::Parts| {
if req.uri.path().contains("stream") {
CacheMode::ForceCache
} else {
CacheMode::Default
}
})),
cache_bust: None,
cache_status_headers: false,
max_ttl: None,
..Default::default()
},
};
let request = Request::builder()
.uri("https://example.com/streaming-custom")
.header("user-agent", "test-agent")
.body(())
.unwrap();
let (parts, _) = request.into_parts();
let analysis = cache.analyze_request(&parts, None)?;
assert_eq!(
analysis.cache_key,
"stream:GET:https://example.com/streaming-custom"
);
assert!(analysis.should_cache);
Ok(())
}
#[tokio::test]
async fn streaming_error_handling() -> Result<()> {
let manager = create_streaming_cache_manager().await;
let cache = HttpStreamingCache {
mode: CacheMode::Default,
manager,
options: Default::default(),
};
let request =
Request::builder().uri("not-a-valid-uri").body(()).unwrap();
let (parts, _) = request.into_parts();
let result = cache.analyze_request(&parts, None);
assert!(result.is_ok());
Ok(())
}
#[tokio::test]
async fn streaming_concurrent_access() -> Result<()> {
use tokio::task::JoinSet;
let manager = create_streaming_cache_manager().await;
let cache = Arc::new(HttpStreamingCache {
mode: CacheMode::Default,
manager,
options: Default::default(),
});
let mut join_set = JoinSet::new();
for i in 0..10 {
let cache_clone = cache.clone();
join_set.spawn(async move {
let request = Request::builder()
.uri(format!("https://example.com/concurrent-{i}"))
.header("user-agent", "test-agent")
.body(())
.unwrap();
let (parts, _) = request.into_parts();
cache_clone.analyze_request(&parts, None)
});
}
let mut results = Vec::new();
while let Some(result) = join_set.join_next().await {
results.push(result.unwrap());
}
assert_eq!(results.len(), 10);
for result in results {
assert!(result.is_ok());
}
Ok(())
}
#[tokio::test]
async fn streaming_with_request_extensions() -> Result<()> {
let manager = create_streaming_cache_manager().await;
let cache = HttpStreamingCache {
mode: CacheMode::Default,
manager,
options: Default::default(),
};
let mut request = Request::builder()
.uri("https://example.com/with-extensions")
.header("user-agent", "test-agent")
.body(())
.unwrap();
request.extensions_mut().insert("custom_data".to_string());
let (parts, _) = request.into_parts();
let analysis = cache.analyze_request(&parts, None)?;
assert!(!analysis.cache_key.is_empty());
assert!(analysis.should_cache);
Ok(())
}
#[tokio::test]
async fn streaming_cache_with_vary_headers() -> Result<()> {
let manager = create_streaming_cache_manager().await;
let cache = HttpStreamingCache {
mode: CacheMode::Default,
manager,
options: Default::default(),
};
let request = Request::builder()
.uri("https://example.com/vary-test")
.header("user-agent", "test-agent")
.header("accept-encoding", "gzip, deflate")
.header("accept-language", "en-US,en;q=0.9")
.body(())
.unwrap();
let (parts, _) = request.into_parts();
let analysis = cache.analyze_request(&parts, None)?;
let response = Response::builder()
.status(200)
.header("content-type", "application/json")
.header("cache-control", "max-age=3600")
.header("vary", "Accept-Encoding, Accept-Language")
.body(Full::new(Bytes::from("vary test data")))
.unwrap();
let cached_response =
cache.process_response(analysis.clone(), response, None).await?;
assert_eq!(cached_response.status(), 200);
let body_bytes =
cached_response.into_body().collect().await?.to_bytes();
assert_eq!(body_bytes, "vary test data");
let cached_response =
cache.lookup_cached_response(&analysis.cache_key).await?;
assert!(cached_response.is_some());
Ok(())
}
#[tokio::test]
async fn streaming_cache_with_multiple_vary_headers() -> Result<()> {
let manager = create_streaming_cache_manager().await;
let cache = HttpStreamingCache {
mode: CacheMode::Default,
manager,
options: Default::default(),
};
let request = Request::builder()
.uri("https://example.com/multiple-vary-test")
.header("user-agent", "test-agent")
.header("accept-encoding", "gzip")
.header("accept-language", "en-US")
.body(())
.unwrap();
let (parts, _) = request.into_parts();
let analysis = cache.analyze_request(&parts, None)?;
let mut response = Response::builder()
.status(200)
.header("content-type", "application/json")
.header("cache-control", "max-age=3600")
.body(Full::new(Bytes::from("multiple vary test data")))
.unwrap();
let headers = response.headers_mut();
headers.append("vary", "Prefer".parse().unwrap());
headers.append("vary", "Accept".parse().unwrap());
headers.append("vary", "Range".parse().unwrap());
headers.append("vary", "Accept-Encoding".parse().unwrap());
headers.append("vary", "Accept-Language".parse().unwrap());
headers.append("vary", "Accept-Datetime".parse().unwrap());
let cached_response =
cache.process_response(analysis.clone(), response, None).await?;
assert_eq!(cached_response.status(), 200);
let body_bytes =
cached_response.into_body().collect().await?.to_bytes();
assert_eq!(body_bytes, "multiple vary test data");
let cached_result =
cache.lookup_cached_response(&analysis.cache_key).await?;
assert!(cached_result.is_some());
if let Some((response, _policy)) = cached_result {
let vary_values: Vec<_> = response
.headers()
.get_all("vary")
.iter()
.map(|v| v.to_str().unwrap())
.collect();
assert_eq!(
vary_values.len(),
6,
"Expected 6 Vary headers, got {}: {:?}",
vary_values.len(),
vary_values
);
assert!(vary_values.contains(&"Prefer"));
assert!(vary_values.contains(&"Accept"));
assert!(vary_values.contains(&"Range"));
assert!(vary_values.contains(&"Accept-Encoding"));
assert!(vary_values.contains(&"Accept-Language"));
assert!(vary_values.contains(&"Accept-Datetime"));
}
Ok(())
}
#[cfg(feature = "rate-limiting")]
#[tokio::test]
async fn test_streaming_with_rate_limiting() -> Result<()> {
use crate::{CacheAwareRateLimiter, StreamingCache};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
#[derive(Debug)]
struct MockStreamingRateLimiter {
calls: Arc<Mutex<Vec<String>>>,
delay: Duration,
}
impl MockStreamingRateLimiter {
fn new(delay: Duration) -> Self {
Self { calls: Arc::new(Mutex::new(Vec::new())), delay }
}
}
impl CacheAwareRateLimiter for MockStreamingRateLimiter {
fn until_key_ready(
&self,
key: &str,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = ()> + Send + '_>,
> {
let key = key.to_string();
Box::pin(async move {
self.calls.lock().unwrap().push(key);
if self.delay > Duration::ZERO {
tokio::time::sleep(self.delay).await;
}
})
}
fn check_key(&self, _key: &str) -> bool {
true }
}
let manager = create_streaming_cache_manager().await;
let rate_limiter =
MockStreamingRateLimiter::new(Duration::from_millis(50));
let call_counter = rate_limiter.calls.clone();
let options = HttpCacheOptions {
rate_limiter: Some(Arc::new(rate_limiter)),
..HttpCacheOptions::default()
};
let client = ClientBuilder::new(Client::new())
.with(StreamingCache::with_options(
manager,
CacheMode::Default,
options,
))
.build();
let mock_server = MockServer::start().await;
let url = format!("{}/streaming-rate-limited", mock_server.uri());
Mock::given(method("GET"))
.and(path("/streaming-rate-limited"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("cache-control", "no-cache")
.set_body_bytes(b"streaming response"),
)
.expect(2)
.mount(&mock_server)
.await;
let start = Instant::now();
let _response1 = client.get(&url).send().await?;
let first_duration = start.elapsed();
assert_eq!(call_counter.lock().unwrap().len(), 1);
assert!(
first_duration >= Duration::from_millis(50),
"First request should be rate limited"
);
let start = Instant::now();
let _response2 = client.get(&url).send().await?;
let second_duration = start.elapsed();
assert_eq!(call_counter.lock().unwrap().len(), 2);
assert!(
second_duration >= Duration::from_millis(50),
"Second request should also be rate limited"
);
Ok(())
}
#[cfg(feature = "rate-limiting")]
#[tokio::test]
async fn test_streaming_cache_hit_bypasses_rate_limiting() -> Result<()> {
use crate::{CacheAwareRateLimiter, StreamingCache};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
#[derive(Debug)]
struct MockStreamingRateLimiter {
calls: Arc<Mutex<Vec<String>>>,
delay: Duration,
}
impl MockStreamingRateLimiter {
fn new(delay: Duration) -> Self {
Self { calls: Arc::new(Mutex::new(Vec::new())), delay }
}
}
impl CacheAwareRateLimiter for MockStreamingRateLimiter {
fn until_key_ready(
&self,
key: &str,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = ()> + Send + '_>,
> {
let key = key.to_string();
Box::pin(async move {
self.calls.lock().unwrap().push(key);
if self.delay > Duration::ZERO {
tokio::time::sleep(self.delay).await;
}
})
}
fn check_key(&self, _key: &str) -> bool {
true }
}
let manager = create_streaming_cache_manager().await;
let rate_limiter =
MockStreamingRateLimiter::new(Duration::from_millis(50));
let call_counter = rate_limiter.calls.clone();
let options = HttpCacheOptions {
rate_limiter: Some(Arc::new(rate_limiter)),
..HttpCacheOptions::default()
};
let client = ClientBuilder::new(Client::new())
.with(StreamingCache::with_options(
manager,
CacheMode::Default,
options,
))
.build();
let mock_server = MockServer::start().await;
let url = format!("{}/streaming-cacheable", mock_server.uri());
Mock::given(method("GET"))
.and(path("/streaming-cacheable"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("cache-control", "public, max-age=3600")
.set_body_bytes(b"cacheable streaming response"),
)
.expect(1) .mount(&mock_server)
.await;
let start = Instant::now();
let response1 = client.get(&url).send().await?;
let first_duration = start.elapsed();
assert_eq!(response1.status(), 200);
assert_eq!(call_counter.lock().unwrap().len(), 1);
assert!(
first_duration >= Duration::from_millis(50),
"First request should be rate limited"
);
call_counter.lock().unwrap().clear();
let response2 = client.get(&url).send().await?;
assert_eq!(response2.status(), 200);
assert_eq!(call_counter.lock().unwrap().len(), 0);
Ok(())
}
}
#[cfg(all(test, feature = "rate-limiting"))]
mod rate_limiting_tests {
use super::*;
use crate::{CacheAwareRateLimiter, DomainRateLimiter, Quota};
use std::num::NonZero;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
#[derive(Debug)]
struct MockRateLimiter {
calls: Arc<Mutex<Vec<String>>>,
delay: Duration,
}
impl MockRateLimiter {
fn new(delay: Duration) -> Self {
Self { calls: Arc::new(Mutex::new(Vec::new())), delay }
}
}
impl CacheAwareRateLimiter for MockRateLimiter {
fn until_key_ready(
&self,
key: &str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + '_>>
{
let key = key.to_string();
Box::pin(async move {
self.calls.lock().unwrap().push(key);
if !self.delay.is_zero() {
tokio::time::sleep(self.delay).await;
}
})
}
fn check_key(&self, _key: &str) -> bool {
true
}
}
#[tokio::test]
async fn test_cache_with_rate_limiting_cache_hit() -> Result<()> {
let mock_server = MockServer::start().await;
let url = format!("{}/test", mock_server.uri());
build_mock(CACHEABLE_PUBLIC, TEST_BODY, 200, 1)
.mount(&mock_server)
.await;
let rate_limiter = MockRateLimiter::new(Duration::from_millis(10));
let call_counter = rate_limiter.calls.clone();
let options = HttpCacheOptions {
rate_limiter: Some(Arc::new(rate_limiter)),
..HttpCacheOptions::default()
};
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: create_cache_manager(),
options,
}))
.build();
let start = Instant::now();
let response1 = client.get(&url).send().await?;
let first_duration = start.elapsed();
assert_eq!(response1.status(), 200);
assert_eq!(call_counter.lock().unwrap().len(), 1);
assert!(first_duration >= Duration::from_millis(10));
call_counter.lock().unwrap().clear();
let response2 = client.get(&url).send().await?;
assert_eq!(response2.status(), 200);
assert_eq!(call_counter.lock().unwrap().len(), 0);
let body1 = response1.bytes().await?;
let body2 = response2.bytes().await?;
assert_eq!(body1, body2);
assert_eq!(body1, TEST_BODY);
Ok(())
}
#[tokio::test]
async fn test_cache_with_rate_limiting_domain_based() -> Result<()> {
let mock_server1 = MockServer::start().await;
let mock_server2 = MockServer::start().await;
let url1 = format!("{}/test1", mock_server1.uri());
let url2 = format!("{}/test2", mock_server2.uri());
build_mock("no-cache", b"server1", 200, 1).mount(&mock_server1).await;
build_mock("no-cache", b"server2", 200, 1).mount(&mock_server2).await;
let rate_limiter = MockRateLimiter::new(Duration::from_millis(1));
let call_counter = rate_limiter.calls.clone();
let options = HttpCacheOptions {
rate_limiter: Some(Arc::new(rate_limiter)),
..HttpCacheOptions::default()
};
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: create_cache_manager(),
options,
}))
.build();
let _response1 = client.get(&url1).send().await?;
let _response2 = client.get(&url2).send().await?;
let calls = call_counter.lock().unwrap().clone();
assert_eq!(calls.len(), 2);
assert!(
calls[0].contains("127.0.0.1") || calls[0].contains("localhost")
);
assert!(
calls[1].contains("127.0.0.1") || calls[1].contains("localhost")
);
Ok(())
}
#[tokio::test]
async fn test_rate_limiting_with_governor() -> Result<()> {
let mock_server = MockServer::start().await;
let url = format!("{}/test", mock_server.uri());
build_mock("no-cache", TEST_BODY, 200, 2).mount(&mock_server).await;
let quota = Quota::per_second(NonZero::new(2).unwrap());
let rate_limiter = DomainRateLimiter::new(quota);
let options = HttpCacheOptions {
rate_limiter: Some(Arc::new(rate_limiter)),
..HttpCacheOptions::default()
};
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: create_cache_manager(),
options,
}))
.build();
let start = Instant::now();
let _response1 = client.get(&url).send().await?;
let first_duration = start.elapsed();
let _response2 = client.get(&url).send().await?;
let second_duration = start.elapsed();
assert!(first_duration < Duration::from_millis(50));
assert!(second_duration < Duration::from_millis(100));
Ok(())
}
#[tokio::test]
async fn test_direct_rate_limiter_behavior() -> Result<()> {
let mock_server = MockServer::start().await;
let url = format!("{}/test", mock_server.uri());
build_mock("no-cache", TEST_BODY, 200, 2).mount(&mock_server).await;
let quota = Quota::per_second(NonZero::new(5).unwrap());
let rate_limiter = DomainRateLimiter::new(quota);
let options = HttpCacheOptions {
rate_limiter: Some(Arc::new(rate_limiter)),
..HttpCacheOptions::default()
};
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: create_cache_manager(),
options,
}))
.build();
let _response1 = client.get(&url).send().await?;
let _response2 = client.get(&url).send().await?;
Ok(())
}
#[tokio::test]
async fn test_no_rate_limiting_by_default() -> Result<()> {
let mock_server = MockServer::start().await;
let url = format!("{}/test", mock_server.uri());
build_mock("no-cache", TEST_BODY, 200, 1).mount(&mock_server).await;
let options = HttpCacheOptions::default();
assert!(options.rate_limiter.is_none());
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: create_cache_manager(),
options,
}))
.build();
let start = Instant::now();
let _response = client.get(&url).send().await?;
let duration = start.elapsed();
assert!(duration < Duration::from_millis(100));
Ok(())
}
#[tokio::test]
async fn test_rate_limiting_only_on_network_requests() -> Result<()> {
let mock_server = MockServer::start().await;
let url = format!("{}/test", mock_server.uri());
build_mock(CACHEABLE_PUBLIC, TEST_BODY, 200, 1)
.mount(&mock_server)
.await;
let rate_limiter = MockRateLimiter::new(Duration::from_millis(20));
let call_counter = rate_limiter.calls.clone();
let options = HttpCacheOptions {
rate_limiter: Some(Arc::new(rate_limiter)),
..HttpCacheOptions::default()
};
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: create_cache_manager(),
options,
}))
.build();
let start = Instant::now();
let _response1 = client.get(&url).send().await?;
let first_duration = start.elapsed();
assert_eq!(call_counter.lock().unwrap().len(), 1);
assert!(first_duration >= Duration::from_millis(20));
call_counter.lock().unwrap().clear();
let _response2 = client.get(&url).send().await?;
assert_eq!(call_counter.lock().unwrap().len(), 0);
let _response3 = client.get(&url).send().await?;
assert_eq!(call_counter.lock().unwrap().len(), 0);
Ok(())
}
}
#[tokio::test]
async fn test_metadata_retrieval_through_extensions() -> Result<()> {
let mock_server = MockServer::start().await;
Mock::given(method(GET))
.respond_with(
ResponseTemplate::new(200)
.insert_header("cache-control", CACHEABLE_PUBLIC)
.set_body_bytes(TEST_BODY),
)
.expect(1) .mount(&mock_server)
.await;
let url = format!("{}/metadata-test", mock_server.uri());
let options = HttpCacheOptions {
metadata_provider: Some(Arc::new(|_request_parts, _response_parts| {
Some(b"test-metadata-value".to_vec())
})),
..Default::default()
};
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: create_cache_manager(),
options,
}))
.build();
let response1 = client.get(&url).send().await?;
assert_eq!(response1.status(), 200);
let metadata1 = response1.extensions().get::<HttpCacheMetadata>();
assert!(
metadata1.is_some(),
"Metadata should be present when response is cached"
);
assert_eq!(
metadata1.unwrap().as_slice(),
b"test-metadata-value",
"Metadata value should match what was stored"
);
let response2 = client.get(&url).send().await?;
assert_eq!(response2.status(), 200);
let metadata2 = response2.extensions().get::<HttpCacheMetadata>();
assert!(
metadata2.is_some(),
"Metadata should be present in response extensions on cache hit"
);
let metadata_value = metadata2.unwrap();
assert_eq!(
metadata_value.as_slice(),
b"test-metadata-value",
"Metadata value should match what was stored"
);
Ok(())
}
#[cfg(all(
feature = "manager-cacache",
feature = "manager-cacache-bincode",
feature = "http-headers-compat"
))]
mod bincode_migration {
use super::*;
use http_cache::{HttpVersion, Url};
use serde::Serialize;
use std::collections::HashMap;
use std::str::FromStr;
#[derive(Serialize)]
struct LegacyStore {
response: LegacyTestResponse,
policy: http_cache_semantics::CachePolicy,
}
#[derive(Serialize)]
struct LegacyTestResponse {
body: Vec<u8>,
headers: HashMap<String, String>,
status: u16,
url: Url,
version: HttpVersion,
}
#[tokio::test]
async fn migration_from_bincode_cache() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock(CACHEABLE_PUBLIC, b"fresh from server", 200, 0);
let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/legacy-endpoint", &mock_server.uri());
let parsed_url = Url::from_str(&url)?;
let cache_dir = tempfile::tempdir().unwrap();
let cache_path = cache_dir.path().to_path_buf();
let cache_key = format!("GET:{}", &parsed_url);
let mut headers = HashMap::new();
headers
.insert("content-type".to_string(), "application/json".to_string());
headers
.insert("cache-control".to_string(), CACHEABLE_PUBLIC.to_string());
let legacy_body = b"legacy bincode response";
let req = http::Request::get(url.as_str()).body(())?;
let res = http::Response::builder()
.status(200)
.header("cache-control", CACHEABLE_PUBLIC)
.body(legacy_body.to_vec())?;
let policy = http_cache_semantics::CachePolicy::new(&req, &res);
let legacy_store = LegacyStore {
response: LegacyTestResponse {
body: legacy_body.to_vec(),
headers,
status: 200,
url: parsed_url.clone(),
version: HttpVersion::Http11,
},
policy,
};
let bytes = bincode::serialize(&legacy_store).unwrap();
cacache::write(&cache_path, &cache_key, bytes).await.unwrap();
let manager = CACacheManager::new(cache_path.clone(), true);
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::ForceCache,
manager: manager.clone(),
options: Default::default(),
}))
.build();
let res = client.get(&url).send().await?;
assert_eq!(res.status(), 200);
let body = res.bytes().await?;
assert_eq!(
body.as_ref(),
legacy_body,
"Response should come from the legacy bincode cache entry"
);
Ok(())
}
#[tokio::test]
async fn migration_new_entries_work_after_upgrade() -> Result<()> {
let mock_server = MockServer::start().await;
let m = build_mock(CACHEABLE_PUBLIC, b"new postcard data", 200, 1);
let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/new-endpoint", &mock_server.uri());
let cache_dir = tempfile::tempdir().unwrap();
let manager = CACacheManager::new(cache_dir.path().to_path_buf(), true);
let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: manager.clone(),
options: Default::default(),
}))
.build();
let res = client.get(&url).send().await?;
assert_eq!(res.status(), 200);
assert_eq!(res.bytes().await?, &b"new postcard data"[..]);
let data =
CacheManager::get(&manager, &format!("GET:{}", &url_parse(&url)?))
.await?;
assert!(data.is_some(), "New entry should be cached with postcard");
let res = client.get(&url).send().await?;
assert_eq!(res.status(), 200);
assert_eq!(res.bytes().await?, &b"new postcard data"[..]);
Ok(())
}
}