#![allow(dead_code)]
use std::collections::HashMap;
use std::time::Duration;
use async_trait::async_trait;
use tokio::sync::Mutex;
use zeroize::Zeroizing;
use cachekit::backend::{Backend, HealthStatus};
use cachekit::client::SharedBackend;
use cachekit::error::BackendError;
use cachekit::{interop, serializer, CacheKit, CachekitError, SwrRead};
use serde::de::DeserializeOwned;
use serde::Deserialize;
#[derive(Debug, Default, Clone)]
pub struct MockBackend {
pub store: std::sync::Arc<Mutex<HashMap<String, Vec<u8>>>>,
}
impl MockBackend {
pub fn shared() -> SharedBackend {
let mock = Self::default();
Self::into_shared(mock)
}
pub fn new_with_handle() -> (SharedBackend, Self) {
let mock = Self::default();
let handle = mock.clone();
(Self::into_shared(mock), handle)
}
fn into_shared(mock: Self) -> SharedBackend {
#[cfg(not(any(target_arch = "wasm32", feature = "unsync")))]
{
std::sync::Arc::new(mock)
}
#[cfg(any(target_arch = "wasm32", feature = "unsync"))]
{
std::rc::Rc::new(mock)
}
}
}
#[cfg_attr(not(any(target_arch = "wasm32", feature = "unsync")), async_trait)]
#[cfg_attr(any(target_arch = "wasm32", feature = "unsync"), async_trait(?Send))]
impl Backend for MockBackend {
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, BackendError> {
Ok(self.store.lock().await.get(key).cloned())
}
async fn set(
&self,
key: &str,
value: Vec<u8>,
_ttl: Option<Duration>,
) -> Result<(), BackendError> {
self.store.lock().await.insert(key.to_owned(), value);
Ok(())
}
async fn delete(&self, key: &str) -> Result<bool, BackendError> {
Ok(self.store.lock().await.remove(key).is_some())
}
async fn exists(&self, key: &str) -> Result<bool, BackendError> {
Ok(self.store.lock().await.contains_key(key))
}
async fn health(&self) -> Result<HealthStatus, BackendError> {
Ok(HealthStatus {
is_healthy: true,
latency_ms: 0.0,
backend_type: "mock".to_owned(),
details: HashMap::new(),
})
}
}
pub type Read<T> = (&'static str, Result<T, CachekitError>);
pub fn decode_both<T: DeserializeOwned>(bytes: &[u8]) -> [Read<T>; 2] {
[
(
"serializer::deserialize",
serializer::deserialize::<T>(bytes),
),
("interop::deserialize", interop::deserialize::<T>(bytes)),
]
}
pub fn runtime() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime")
}
pub async fn interop_reads<T: DeserializeOwned>(
client: &CacheKit,
key: &str,
) -> [Read<Option<T>>; 2] {
[
("CacheKit::interop_get", client.interop_get::<T>(key).await),
(
"CacheKit::interop_get_swr",
client
.interop_get_swr::<T>(key)
.await
.map(|read| match read {
SwrRead::Fresh(v) => Some(v),
SwrRead::Miss => None,
SwrRead::Stale(..) => panic!("with L1 off no read is stale"),
}),
),
]
}
pub async fn every_read<T: DeserializeOwned>(client: &CacheKit, key: &str) -> Vec<Read<Option<T>>> {
let mut reads = vec![("CacheKit::get", client.get::<T>(key).await)];
reads.extend(interop_reads::<T>(client, key).await);
reads
}
#[derive(Debug, Deserialize)]
pub struct EnvelopedValue {
user_id: u64,
name: String,
active: bool,
}
pub fn read_every_path<T: DeserializeOwned>(bytes: &[u8]) -> Vec<Read<T>> {
const KEY: &str = "forged:entry";
let runtime = runtime();
let (backend, handle) = MockBackend::new_with_handle();
let client = CacheKit::builder()
.backend(backend)
.no_l1()
.build()
.expect("client builds");
let found = |r: Result<Option<T>, CachekitError>| {
r.map(|v| v.expect("the forged entry must be found, not missed"))
};
let mut reads = Vec::from(decode_both::<T>(bytes));
runtime.block_on(async {
handle
.store
.lock()
.await
.insert(KEY.to_owned(), bytes.to_vec());
for (path, result) in every_read::<T>(&client, KEY).await {
reads.push((path, found(result)));
}
});
reads
}
#[cfg(feature = "encryption")]
pub fn encrypting_client_holding(
master_key_hex: &str,
tenant_id: &str,
namespace: Option<&str>,
stored_key: &str,
bytes: Vec<u8>,
) -> CacheKit {
let (backend, handle) = MockBackend::new_with_handle();
let mut builder = CacheKit::builder()
.backend(backend)
.no_l1()
.encryption(master_key_hex, tenant_id)
.expect("master key and tenant");
if let Some(ns) = namespace {
builder = builder.namespace(ns);
}
handle
.store
.try_lock()
.expect("fresh store")
.insert(stored_key.to_owned(), bytes);
builder.build().expect("client builds")
}
pub const AUTH_FAILURE: &str = "decrypt failed: Authentication verification failed";
pub const SHORT_CIPHERTEXT: &str =
"decrypt failed: Invalid ciphertext format: Ciphertext too short";
pub struct EnvGuard {
saved: Vec<(&'static str, Option<Zeroizing<String>>)>,
}
impl EnvGuard {
pub fn set(vars: &[(&'static str, Option<&str>)]) -> Self {
let saved = vars
.iter()
.map(|(name, _)| match std::env::var(name) {
Ok(v) => (*name, Some(Zeroizing::new(v))),
Err(std::env::VarError::NotPresent) => (*name, None),
Err(std::env::VarError::NotUnicode(_)) => {
panic!("EnvGuard: {name} holds a non-UTF-8 value; refusing to clobber it")
}
})
.collect();
for (name, value) in vars {
match value {
Some(v) => std::env::set_var(name, v),
None => std::env::remove_var(name),
}
}
Self { saved }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
for (name, value) in &self.saved {
match value {
Some(v) => std::env::set_var(name, v.as_str()),
None => std::env::remove_var(name),
}
}
}
}
#[derive(Debug, Default, Clone)]
pub struct FailingBackend;
impl FailingBackend {
pub fn shared() -> SharedBackend {
#[cfg(not(any(target_arch = "wasm32", feature = "unsync")))]
{
std::sync::Arc::new(Self)
}
#[cfg(any(target_arch = "wasm32", feature = "unsync"))]
{
std::rc::Rc::new(Self)
}
}
}
#[cfg_attr(not(any(target_arch = "wasm32", feature = "unsync")), async_trait)]
#[cfg_attr(any(target_arch = "wasm32", feature = "unsync"), async_trait(?Send))]
impl Backend for FailingBackend {
async fn get(&self, _key: &str) -> Result<Option<Vec<u8>>, BackendError> {
Err(BackendError::transient("down"))
}
async fn set(&self, _: &str, _: Vec<u8>, _: Option<Duration>) -> Result<(), BackendError> {
Err(BackendError::transient("down"))
}
async fn delete(&self, _key: &str) -> Result<bool, BackendError> {
Err(BackendError::transient("down"))
}
async fn exists(&self, _key: &str) -> Result<bool, BackendError> {
Err(BackendError::transient("down"))
}
async fn health(&self) -> Result<HealthStatus, BackendError> {
Ok(HealthStatus {
is_healthy: false,
latency_ms: 0.0,
backend_type: "failing".to_owned(),
details: HashMap::new(),
})
}
}