1pub mod engine;
9pub mod types;
10
11pub use engine::{BucketStats, EngineSnapshot, ObjectEngine};
12pub use types::{etag_of, valid_bucket_name, ObjectError, ObjectMeta, DEFAULT_CONTENT_TYPE};
13
14#[cfg(test)]
15mod tests {
16 use super::*;
17
18 fn seeded() -> ObjectEngine {
19 let e = ObjectEngine::new();
20 e.create_bucket("media").unwrap();
21 e.put(
22 "media",
23 "a.txt",
24 b"hello".to_vec(),
25 Some("text/plain".into()),
26 serde_json::json!({"k": 1}),
27 )
28 .unwrap();
29 e.put(
30 "media",
31 "img/1.png",
32 b"\x89PNG\x00".to_vec(),
33 Some("image/png".into()),
34 serde_json::Value::Null,
35 )
36 .unwrap();
37 e.put(
38 "media",
39 "img/2.png",
40 b"\x89PNG\x01".to_vec(),
41 Some("image/png".into()),
42 serde_json::Value::Null,
43 )
44 .unwrap();
45 e
46 }
47
48 #[test]
49 fn put_get_roundtrip_and_etag() {
50 let e = seeded();
51 let (data, meta) = e.get("media", "a.txt").unwrap().unwrap();
52 assert_eq!(data, b"hello");
53 assert_eq!(meta.content_type, "text/plain");
54 assert_eq!(meta.size, 5);
55 assert_eq!(meta.etag, etag_of(b"hello"));
56 assert_eq!(meta.metadata, serde_json::json!({"k": 1}));
57 assert_ne!(etag_of(b"hello"), etag_of(b"world"));
59 assert_eq!(etag_of(b"hello"), etag_of(b"hello"));
60 }
61
62 #[test]
63 fn head_without_body() {
64 let e = seeded();
65 let meta = e.head("media", "img/1.png").unwrap().unwrap();
66 assert_eq!(meta.content_type, "image/png");
67 assert_eq!(meta.size, 5);
68 assert!(e.head("media", "nope").unwrap().is_none());
69 }
70
71 #[test]
72 fn overwrite_updates_etag_and_size() {
73 let e = seeded();
74 let first = e.head("media", "a.txt").unwrap().unwrap().etag;
75 e.put(
76 "media",
77 "a.txt",
78 b"hello world".to_vec(),
79 None,
80 serde_json::Value::Null,
81 )
82 .unwrap();
83 let meta = e.head("media", "a.txt").unwrap().unwrap();
84 assert_eq!(meta.size, 11);
85 assert_ne!(meta.etag, first);
86 assert_eq!(meta.content_type, DEFAULT_CONTENT_TYPE);
88 assert_eq!(e.bucket_stats("media").unwrap().objects, 3);
89 }
90
91 #[test]
92 fn prefix_listing_is_sorted() {
93 let e = seeded();
94 let imgs = e.list("media", "img/", None).unwrap();
95 let keys: Vec<&str> = imgs.iter().map(|m| m.key.as_str()).collect();
96 assert_eq!(keys, vec!["img/1.png", "img/2.png"]);
97
98 let all = e.list("media", "", None).unwrap();
99 assert_eq!(all.len(), 3);
100 let limited = e.list("media", "", Some(1)).unwrap();
101 assert_eq!(limited.len(), 1);
102 assert_eq!(limited[0].key, "a.txt");
103 }
104
105 #[test]
106 fn delete_and_stats() {
107 let e = seeded();
108 let stats = e.bucket_stats("media").unwrap();
109 assert_eq!(stats.objects, 3);
110 assert_eq!(stats.bytes, 5 + 5 + 5);
111 assert!(e.delete("media", "a.txt").unwrap());
112 assert!(!e.delete("media", "a.txt").unwrap());
113 assert_eq!(e.bucket_stats("media").unwrap().objects, 2);
114 }
115
116 #[test]
117 fn bucket_validation_and_errors() {
118 let e = ObjectEngine::new();
119 assert!(matches!(
120 e.create_bucket("BadName"),
121 Err(ObjectError::InvalidBucketName(_))
122 ));
123 e.create_bucket("ok").unwrap();
124 assert!(matches!(
125 e.create_bucket("ok"),
126 Err(ObjectError::BucketExists(_))
127 ));
128 assert!(matches!(
130 e.get("nope", "k"),
131 Err(ObjectError::BucketNotFound(_))
132 ));
133 assert!(matches!(
134 e.list("nope", "", None),
135 Err(ObjectError::BucketNotFound(_))
136 ));
137 }
138
139 #[test]
140 fn snapshot_roundtrip() {
141 let e = seeded();
142 let bytes = serde_json::to_vec(&e.snapshot()).unwrap();
143 let restored = ObjectEngine::new();
144 restored.load_snapshot(serde_json::from_slice(&bytes).unwrap());
145 let (data, meta) = restored.get("media", "img/2.png").unwrap().unwrap();
146 assert_eq!(data, b"\x89PNG\x01");
147 assert_eq!(meta.etag, etag_of(b"\x89PNG\x01"));
148 assert_eq!(restored.bucket_stats("media").unwrap().objects, 3);
149 }
150
151 #[test]
152 fn put_auto_creates_bucket() {
153 let e = ObjectEngine::new();
154 e.put("auto", "k", b"hi".to_vec(), None, serde_json::Value::Null)
156 .unwrap();
157 assert_eq!(e.list_buckets(), vec!["auto"]);
158 assert_eq!(e.get("auto", "k").unwrap().unwrap().0, b"hi");
159 assert!(matches!(
161 e.put("BAD NAME", "k", vec![], None, serde_json::Value::Null),
162 Err(ObjectError::InvalidBucketName(_))
163 ));
164 assert!(e.bucket_stats("BAD NAME").is_none());
165 }
166
167 #[test]
170 fn bucket_name_rules() {
171 assert!(valid_bucket_name("my-bucket.1"));
172 assert!(valid_bucket_name("a"));
173 assert!(!valid_bucket_name("")); assert!(!valid_bucket_name("Upper")); assert!(!valid_bucket_name("has space"));
176 assert!(!valid_bucket_name("under_score")); assert!(!valid_bucket_name(&"x".repeat(64))); assert!(valid_bucket_name(&"x".repeat(63)));
179 }
180
181 #[test]
184 fn empty_and_binary_payloads() {
185 let e = ObjectEngine::new();
186 e.create_bucket("b").unwrap();
187 let m = e
189 .put("b", "empty", vec![], None, serde_json::Value::Null)
190 .unwrap();
191 assert_eq!(m.size, 0);
192 assert_eq!(m.etag, etag_of(&[]));
193 let (d, _) = e.get("b", "empty").unwrap().unwrap();
194 assert!(d.is_empty());
195 let raw: Vec<u8> = (0u16..=255).map(|x| x as u8).collect();
197 e.put(
198 "b",
199 "blob",
200 raw.clone(),
201 Some("application/octet-stream".into()),
202 serde_json::Value::Null,
203 )
204 .unwrap();
205 let (back, _) = e.get("b", "blob").unwrap().unwrap();
206 assert_eq!(back, raw);
207 }
208
209 #[test]
210 fn default_content_type_when_none() {
211 let e = ObjectEngine::new();
212 e.create_bucket("b").unwrap();
213 let m = e
214 .put("b", "k", b"x".to_vec(), None, serde_json::Value::Null)
215 .unwrap();
216 assert_eq!(m.content_type, DEFAULT_CONTENT_TYPE);
217 }
218
219 #[test]
220 fn etag_is_content_addressed() {
221 let e = ObjectEngine::new();
223 e.create_bucket("a").unwrap();
224 e.create_bucket("b").unwrap();
225 let m1 = e
226 .put(
227 "a",
228 "k1",
229 b"identical".to_vec(),
230 None,
231 serde_json::Value::Null,
232 )
233 .unwrap();
234 let m2 = e
235 .put(
236 "b",
237 "k2",
238 b"identical".to_vec(),
239 None,
240 serde_json::Value::Null,
241 )
242 .unwrap();
243 assert_eq!(m1.etag, m2.etag);
244 let m3 = e
245 .put(
246 "a",
247 "k3",
248 b"different".to_vec(),
249 None,
250 serde_json::Value::Null,
251 )
252 .unwrap();
253 assert_ne!(m1.etag, m3.etag);
254 }
255
256 #[test]
259 fn keys_with_slashes_and_bucket_isolation() {
260 let e = ObjectEngine::new();
261 e.create_bucket("one").unwrap();
262 e.create_bucket("two").unwrap();
263 e.put(
264 "one",
265 "a/b/c/deep.txt",
266 b"1".to_vec(),
267 None,
268 serde_json::Value::Null,
269 )
270 .unwrap();
271 e.put(
272 "two",
273 "a/b/c/deep.txt",
274 b"2".to_vec(),
275 None,
276 serde_json::Value::Null,
277 )
278 .unwrap();
279 assert_eq!(e.get("one", "a/b/c/deep.txt").unwrap().unwrap().0, b"1");
281 assert_eq!(e.get("two", "a/b/c/deep.txt").unwrap().unwrap().0, b"2");
282 }
283
284 #[test]
285 fn prefix_listing_variants() {
286 let e = ObjectEngine::new();
287 e.create_bucket("b").unwrap();
288 for k in ["a/1", "a/2", "a/3", "b/1", "c"] {
289 e.put("b", k, b"x".to_vec(), None, serde_json::Value::Null)
290 .unwrap();
291 }
292 assert_eq!(e.list("b", "a/", None).unwrap().len(), 3);
293 assert_eq!(e.list("b", "a/", Some(2)).unwrap().len(), 2); assert_eq!(e.list("b", "", None).unwrap().len(), 5); assert!(e.list("b", "zzz", None).unwrap().is_empty()); let keys: Vec<String> = e
298 .list("b", "", None)
299 .unwrap()
300 .into_iter()
301 .map(|m| m.key)
302 .collect();
303 let mut sorted = keys.clone();
304 sorted.sort();
305 assert_eq!(keys, sorted);
306 }
307
308 #[test]
309 fn get_and_delete_missing() {
310 let e = ObjectEngine::new();
311 e.create_bucket("b").unwrap();
312 assert!(e.get("b", "missing").unwrap().is_none());
313 assert!(e.head("b", "missing").unwrap().is_none());
314 assert!(!e.delete("b", "missing").unwrap());
315 }
316
317 #[test]
318 fn bucket_lifecycle_and_stats() {
319 let e = ObjectEngine::new();
320 assert!(matches!(
321 e.create_bucket("UP"),
322 Err(ObjectError::InvalidBucketName(_))
323 ));
324 e.create_bucket("a").unwrap();
325 e.create_bucket("b").unwrap();
326 assert!(matches!(
327 e.create_bucket("a"),
328 Err(ObjectError::BucketExists(_))
329 ));
330 assert_eq!(e.list_buckets(), vec!["a", "b"]);
331 e.put("a", "k", b"hello".to_vec(), None, serde_json::Value::Null)
332 .unwrap();
333 e.put("a", "k2", b"hi".to_vec(), None, serde_json::Value::Null)
334 .unwrap();
335 let s = e.bucket_stats("a").unwrap();
336 assert_eq!(s.objects, 2);
337 assert_eq!(s.bytes, 7); e.drop_bucket("a").unwrap();
339 assert!(matches!(
340 e.drop_bucket("a"),
341 Err(ObjectError::BucketNotFound(_))
342 ));
343 assert!(e.bucket_stats("a").is_none());
344 assert!(matches!(
346 e.get("a", "k"),
347 Err(ObjectError::BucketNotFound(_))
348 ));
349 assert!(matches!(
350 e.list("a", "", None),
351 Err(ObjectError::BucketNotFound(_))
352 ));
353 }
354
355 #[test]
356 fn metadata_is_preserved() {
357 let e = ObjectEngine::new();
358 e.create_bucket("b").unwrap();
359 let meta = serde_json::json!({"author": "andrew", "tags": ["a", "b"]});
360 e.put(
361 "b",
362 "k",
363 b"x".to_vec(),
364 Some("text/plain".into()),
365 meta.clone(),
366 )
367 .unwrap();
368 assert_eq!(e.head("b", "k").unwrap().unwrap().metadata, meta);
369 }
370}