use aperture_cli::atomic::{atomic_write, atomic_write_sync, DirLock};
use aperture_cli::response_cache::{CacheConfig, CacheKey, CachedRequestInfo, ResponseCache};
use std::collections::HashMap;
use std::path::Path;
use std::time::Duration;
use tempfile::TempDir;
#[tokio::test]
async fn test_atomic_write_produces_valid_json_file() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("data.json");
let data = serde_json::json!({
"key": "value",
"nested": { "a": 1, "b": [1, 2, 3] }
});
let json = serde_json::to_string_pretty(&data).unwrap();
atomic_write(&path, json.as_bytes()).await.unwrap();
let content = tokio::fs::read_to_string(&path).await.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(parsed, data);
}
#[tokio::test]
async fn test_atomic_write_no_temp_files_remain() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("target.bin");
atomic_write(&path, b"binary data here").await.unwrap();
let entries: Vec<_> = std::fs::read_dir(dir.path())
.unwrap()
.filter_map(Result::ok)
.collect();
assert_eq!(entries.len(), 1, "Only the target file should remain");
assert_eq!(
entries[0].file_name().to_string_lossy().as_ref(),
"target.bin"
);
}
#[test]
fn test_atomic_write_sync_to_nonexistent_parent_fails() {
let result = atomic_write_sync(Path::new("/nonexistent/dir/file.txt"), b"data");
assert!(result.is_err());
}
#[tokio::test]
async fn test_atomic_write_to_nonexistent_parent_fails_no_residue() {
let dir = TempDir::new().unwrap();
let bad_path = dir.path().join("nonexistent_subdir").join("file.txt");
let result = atomic_write(&bad_path, b"data").await;
assert!(result.is_err());
let entries: Vec<_> = std::fs::read_dir(dir.path())
.unwrap()
.filter_map(Result::ok)
.collect();
assert_eq!(entries.len(), 0, "No residual files should be left");
}
#[tokio::test]
async fn test_concurrent_atomic_writes_produce_valid_content() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("concurrent.json");
let mut handles = Vec::new();
for i in 0..50 {
let p = path.clone();
handles.push(tokio::spawn(async move {
let data = serde_json::json!({
"writer": i,
"payload": "x".repeat(500),
});
let json = serde_json::to_string_pretty(&data).unwrap();
atomic_write(&p, json.as_bytes()).await.unwrap();
}));
}
for handle in handles {
handle.await.unwrap();
}
let content = tokio::fs::read_to_string(&path).await.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content)
.expect("File content should be valid JSON, not corrupted by interleaving");
assert!(parsed.get("writer").is_some());
assert!(parsed.get("payload").is_some());
let payload = parsed["payload"].as_str().unwrap();
assert_eq!(payload.len(), 500, "Payload should be complete");
let entries: Vec<_> = std::fs::read_dir(dir.path())
.unwrap()
.filter_map(Result::ok)
.collect();
assert_eq!(entries.len(), 1, "Only target file should remain");
}
#[tokio::test]
async fn test_concurrent_response_cache_store_no_corruption() {
let dir = TempDir::new().unwrap();
let config = CacheConfig {
cache_dir: dir.path().to_path_buf(),
default_ttl: Duration::from_secs(300),
max_entries: 100,
enabled: true,
allow_authenticated: false,
};
let cache = ResponseCache::new(config).unwrap();
let cache = std::sync::Arc::new(cache);
let mut handles = Vec::new();
for i in 0..20 {
let cache = cache.clone();
handles.push(tokio::spawn(async move {
let key = CacheKey {
api_name: "test_api".to_string(),
operation_id: format!("op_{i}"),
request_hash: format!("{i:016x}"),
};
let body = format!(r#"{{"writer": {i}, "data": "{}"}}"#, "y".repeat(200));
let headers = HashMap::new();
let request_info = CachedRequestInfo {
method: "GET".to_string(),
url: format!("https://api.example.com/resource/{i}"),
headers: HashMap::new(),
body_hash: None,
};
cache
.store(&key, &body, 200, &headers, request_info, None)
.await
.unwrap();
}));
}
for handle in handles {
handle.await.unwrap();
}
let mut valid_count = 0;
let mut entries = tokio::fs::read_dir(dir.path()).await.unwrap();
while let Some(entry) = entries.next_entry().await.unwrap() {
let name = entry.file_name().to_string_lossy().to_string();
if name.ends_with("_cache.json") {
let content = tokio::fs::read_to_string(entry.path()).await.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content)
.unwrap_or_else(|e| panic!("Cache file {name} is corrupt: {e}"));
assert!(parsed.get("body").is_some());
valid_count += 1;
}
}
assert_eq!(valid_count, 20, "All 20 cache entries should be valid");
}
#[tokio::test]
async fn test_concurrent_store_same_key_no_corruption() {
let dir = TempDir::new().unwrap();
let config = CacheConfig {
cache_dir: dir.path().to_path_buf(),
default_ttl: Duration::from_secs(300),
max_entries: 100,
enabled: true,
allow_authenticated: false,
};
let cache = ResponseCache::new(config).unwrap();
let cache = std::sync::Arc::new(cache);
let mut handles = Vec::new();
for i in 0..20 {
let cache = cache.clone();
handles.push(tokio::spawn(async move {
let key = CacheKey {
api_name: "test_api".to_string(),
operation_id: "same_op".to_string(),
request_hash: "same_hash".to_string(),
};
let body = format!(r#"{{"writer": {i}, "payload": "{}"}}"#, "z".repeat(300));
let headers = HashMap::new();
let request_info = CachedRequestInfo {
method: "GET".to_string(),
url: "https://api.example.com/resource".to_string(),
headers: HashMap::new(),
body_hash: None,
};
cache
.store(&key, &body, 200, &headers, request_info, None)
.await
.unwrap();
}));
}
for handle in handles {
handle.await.unwrap();
}
let key = CacheKey {
api_name: "test_api".to_string(),
operation_id: "same_op".to_string(),
request_hash: "same_hash".to_string(),
};
let cache_file = dir.path().join(key.to_filename());
let content = tokio::fs::read_to_string(&cache_file).await.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content)
.expect("Cache file should be valid JSON after concurrent writes to same key");
let body_str = parsed["body"].as_str().unwrap();
let body_parsed: serde_json::Value = serde_json::from_str(body_str).unwrap();
assert!(body_parsed.get("writer").is_some());
let payload = body_parsed["payload"].as_str().unwrap();
assert_eq!(
payload.len(),
300,
"Payload should be complete, not truncated"
);
}
#[tokio::test]
async fn test_crash_before_rename_leaves_original_intact() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("important.json");
let original = r#"{"status": "original"}"#;
atomic_write(&path, original.as_bytes()).await.unwrap();
let orphan_tmp = dir.path().join(".important.json.00000000deadbeef.tmp");
tokio::fs::write(&orphan_tmp, b"incomplete garbage data")
.await
.unwrap();
let content = tokio::fs::read_to_string(&path).await.unwrap();
assert_eq!(content, original, "Original file must not be corrupted");
let updated = r#"{"status": "updated"}"#;
atomic_write(&path, updated.as_bytes()).await.unwrap();
let content = tokio::fs::read_to_string(&path).await.unwrap();
assert_eq!(content, updated);
}
#[tokio::test]
async fn test_sequential_atomic_writes_preserve_integrity() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("important.json");
let original = r#"{"status": "original"}"#;
atomic_write(&path, original.as_bytes()).await.unwrap();
let content = tokio::fs::read_to_string(&path).await.unwrap();
assert_eq!(content, original);
let updated = r#"{"status": "updated", "extra": "data"}"#;
atomic_write(&path, updated.as_bytes()).await.unwrap();
let content = tokio::fs::read_to_string(&path).await.unwrap();
assert_eq!(content, updated);
let entries: Vec<_> = std::fs::read_dir(dir.path())
.unwrap()
.filter_map(Result::ok)
.collect();
assert_eq!(entries.len(), 1);
}
#[test]
fn test_dir_lock_blocks_concurrent_try_acquire() {
let dir = TempDir::new().unwrap();
let lock1 = DirLock::acquire(dir.path()).unwrap();
let result = DirLock::try_acquire(dir.path()).unwrap();
assert!(result.is_none(), "Should not acquire while lock is held");
drop(lock1);
let result = DirLock::try_acquire(dir.path()).unwrap();
assert!(result.is_some(), "Should acquire after lock is released");
}
#[test]
fn test_dir_lock_creates_lock_file() {
let dir = TempDir::new().unwrap();
let lock_path = dir.path().join(".aperture.lock");
assert!(!lock_path.exists());
let _lock = DirLock::acquire(dir.path()).unwrap();
assert!(lock_path.exists());
}
#[test]
fn test_os_filesystem_atomic_write() {
use aperture_cli::fs::{FileSystem, OsFileSystem};
let dir = TempDir::new().unwrap();
let path = dir.path().join("fs_atomic.txt");
let fs = OsFileSystem;
fs.atomic_write(&path, b"via filesystem trait").unwrap();
let content = fs.read_to_string(&path).unwrap();
assert_eq!(content, "via filesystem trait");
}