coven/storage/cloud/
test_utils.rs1use std::collections::HashMap;
9use std::sync::Mutex;
10
11use async_trait::async_trait;
12
13use super::{CloudHome, CloudHomeError, CloudHomeJoinInfo};
14
15pub struct InMemoryCloudHome {
18 writes: Mutex<HashMap<String, Vec<u8>>>,
19 deletes: Mutex<Vec<String>>,
20}
21
22impl InMemoryCloudHome {
23 pub fn new() -> Self {
24 Self {
25 writes: Mutex::new(HashMap::new()),
26 deletes: Mutex::new(Vec::new()),
27 }
28 }
29
30 pub fn keys(&self) -> Vec<String> {
33 self.writes.lock().unwrap().keys().cloned().collect()
34 }
35
36 pub fn get(&self, key: &str) -> Option<Vec<u8>> {
40 self.writes.lock().unwrap().get(key).cloned()
41 }
42
43 pub fn len(&self) -> usize {
45 self.writes.lock().unwrap().len()
46 }
47
48 pub fn is_empty(&self) -> bool {
50 self.writes.lock().unwrap().is_empty()
51 }
52
53 pub fn deletes_seen(&self) -> Vec<String> {
55 self.deletes.lock().unwrap().clone()
56 }
57}
58
59impl Default for InMemoryCloudHome {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65#[async_trait]
66impl CloudHome for InMemoryCloudHome {
67 async fn write(&self, key: &str, data: Vec<u8>) -> Result<(), CloudHomeError> {
68 self.writes.lock().unwrap().insert(key.to_string(), data);
69 Ok(())
70 }
71
72 async fn read(&self, key: &str) -> Result<Vec<u8>, CloudHomeError> {
73 self.writes
74 .lock()
75 .unwrap()
76 .get(key)
77 .cloned()
78 .ok_or_else(|| CloudHomeError::NotFound(key.to_string()))
79 }
80
81 async fn read_range(&self, key: &str, start: u64, end: u64) -> Result<Vec<u8>, CloudHomeError> {
82 let data = self.read(key).await?;
83 let s = start as usize;
84 let e = (end as usize).min(data.len());
85 if s > data.len() {
86 return Err(CloudHomeError::NotFound(format!("range past end of {key}")));
87 }
88 Ok(data[s..e].to_vec())
89 }
90
91 async fn list(&self, prefix: &str) -> Result<Vec<String>, CloudHomeError> {
92 Ok(self
93 .writes
94 .lock()
95 .unwrap()
96 .keys()
97 .filter(|k| k.starts_with(prefix))
98 .cloned()
99 .collect())
100 }
101
102 async fn delete(&self, key: &str) -> Result<(), CloudHomeError> {
103 self.writes.lock().unwrap().remove(key);
104 self.deletes.lock().unwrap().push(key.to_string());
105 Ok(())
106 }
107
108 async fn exists(&self, key: &str) -> Result<bool, CloudHomeError> {
109 Ok(self.writes.lock().unwrap().contains_key(key))
110 }
111
112 async fn grant_access(&self, _member_id: &str) -> Result<CloudHomeJoinInfo, CloudHomeError> {
113 Err(CloudHomeError::Storage(
114 "InMemoryCloudHome does not grant access".into(),
115 ))
116 }
117
118 async fn revoke_access(&self, _member_id: &str) -> Result<(), CloudHomeError> {
119 Ok(())
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[tokio::test]
128 async fn write_then_read_roundtrips() {
129 let h = InMemoryCloudHome::new();
130 h.write("foo", b"hello".to_vec()).await.unwrap();
131 assert_eq!(h.read("foo").await.unwrap(), b"hello");
132 assert!(h.exists("foo").await.unwrap());
133 assert!(!h.exists("bar").await.unwrap());
134 }
135
136 #[tokio::test]
137 async fn read_range_returns_a_slice() {
138 let h = InMemoryCloudHome::new();
139 h.write("k", b"0123456789".to_vec()).await.unwrap();
140 assert_eq!(h.read_range("k", 2, 5).await.unwrap(), b"234");
141 }
142
143 #[tokio::test]
144 async fn list_filters_by_prefix() {
145 let h = InMemoryCloudHome::new();
146 h.write("a/x", vec![1]).await.unwrap();
147 h.write("a/y", vec![2]).await.unwrap();
148 h.write("b/x", vec![3]).await.unwrap();
149 let mut got = h.list("a/").await.unwrap();
150 got.sort();
151 assert_eq!(got, vec!["a/x".to_string(), "a/y".to_string()]);
152 }
153
154 #[tokio::test]
155 async fn delete_removes_and_records() {
156 let h = InMemoryCloudHome::new();
157 h.write("k", vec![1]).await.unwrap();
158 h.delete("k").await.unwrap();
159 assert!(matches!(
160 h.read("k").await,
161 Err(CloudHomeError::NotFound(_))
162 ));
163 assert_eq!(h.deletes_seen(), vec!["k".to_string()]);
164 }
165}