use super::LocalStorage;
use crate::storage::Storage;
fn temp_dir(name: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!("rws_local_storage_{name}_{}", std::process::id()))
}
#[test]
fn put_then_get_roundtrips() {
let dir = temp_dir("roundtrip");
let store = LocalStorage::new(dir.to_str().unwrap());
let key = store.put("file.txt", b"hello world", "text/plain").unwrap();
assert_eq!("file.txt", key);
assert_eq!(b"hello world".to_vec(), store.get("file.txt").unwrap());
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn put_creates_nested_directories() {
let dir = temp_dir("nested");
let store = LocalStorage::new(dir.to_str().unwrap());
store.put("avatars/2026/42.png", b"...png...", "image/png").unwrap();
assert_eq!(b"...png...".to_vec(), store.get("avatars/2026/42.png").unwrap());
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn delete_removes_file_and_is_idempotent() {
let dir = temp_dir("delete");
let store = LocalStorage::new(dir.to_str().unwrap());
store.put("file.txt", b"data", "text/plain").unwrap();
store.delete("file.txt").unwrap();
assert!(store.get("file.txt").is_err());
store.delete("file.txt").unwrap();
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn get_missing_key_is_an_error() {
let dir = temp_dir("missing");
let store = LocalStorage::new(dir.to_str().unwrap());
assert!(store.get("nope.txt").is_err());
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn rejects_path_traversal() {
let dir = temp_dir("traversal");
let store = LocalStorage::new(dir.to_str().unwrap());
assert!(store.put("../escape.txt", b"data", "text/plain").is_err());
assert!(store.get("../escape.txt").is_err());
assert!(store.delete("../escape.txt").is_err());
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn url_without_base_url_falls_back_to_filesystem_path() {
let dir = temp_dir("url_fallback");
let store = LocalStorage::new(dir.to_str().unwrap());
let url = store.url("file.txt");
assert!(url.ends_with("file.txt"));
assert!(url.contains(dir.to_str().unwrap()));
}
#[test]
fn url_with_base_url_uses_prefix() {
let dir = temp_dir("url_prefix");
let store = LocalStorage::new(dir.to_str().unwrap()).with_base_url("/uploads");
assert_eq!("/uploads/avatars/42.png", store.url("avatars/42.png"));
}
#[test]
fn put_stream_writes_as_it_reads_and_replaces_atomically() {
let dir = crate::test_env::TempPath::new("local_put_stream");
std::fs::create_dir_all(&*dir).unwrap();
let store = LocalStorage::new(dir.as_str());
let data: Vec<u8> = (0..300_000u32).map(|i| (i % 251) as u8).collect();
store.put_stream("big/file.bin", &mut &data[..], "application/octet-stream").unwrap();
assert_eq!(data, store.get("big/file.bin").unwrap());
struct Failing(usize);
impl std::io::Read for Failing {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.0 == 0 {
return Err(std::io::Error::other("client went away"));
}
let n = buf.len().min(self.0);
buf[..n].fill(b'x');
self.0 -= n;
Ok(n)
}
}
assert!(store.put_stream("big/file.bin", &mut Failing(1000), "application/octet-stream").is_err());
assert_eq!(data, store.get("big/file.bin").unwrap());
let leftovers: Vec<_> = std::fs::read_dir(dir.join("big")).unwrap().filter_map(|e| e.ok()).map(|e| e.file_name()).collect();
assert_eq!(1, leftovers.len(), "{leftovers:?}");
}