use super::log_payload::{DurabilityMode, LogPayloadStorage, SNAPSHOT_MAGIC, SNAPSHOT_VERSION};
use super::traits::PayloadStorage;
use serde_json::json;
use tempfile::TempDir;
fn create_test_storage() -> (LogPayloadStorage, TempDir) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let storage = LogPayloadStorage::new(temp_dir.path()).expect("Failed to create storage");
(storage, temp_dir)
}
#[test]
fn test_store_and_retrieve_payload() {
let (mut storage, _temp) = create_test_storage();
let payload = json!({"name": "test", "value": 42});
storage.store(1, &payload).expect("Store failed");
let retrieved = storage.retrieve(1).expect("Retrieve failed");
assert_eq!(retrieved, Some(payload));
}
#[test]
fn test_delete_payload() {
let (mut storage, _temp) = create_test_storage();
let payload = json!({"key": "value"});
storage.store(1, &payload).expect("Store failed");
storage.delete(1).expect("Delete failed");
let retrieved = storage.retrieve(1).expect("Retrieve failed");
assert_eq!(retrieved, None);
}
#[test]
fn test_ids_returns_all_stored_ids() {
let (mut storage, _temp) = create_test_storage();
for i in 1..=5 {
storage.store(i, &json!({"id": i})).expect("Store failed");
}
let mut ids = storage.ids();
ids.sort_unstable();
assert_eq!(ids, vec![1, 2, 3, 4, 5]);
}
#[test]
fn test_create_snapshot_creates_file() {
let (mut storage, temp) = create_test_storage();
for i in 1..=10 {
storage.store(i, &json!({"id": i})).expect("Store failed");
}
storage.create_snapshot().expect("Snapshot creation failed");
let snapshot_path = temp.path().join("payloads.snapshot");
assert!(snapshot_path.exists(), "Snapshot file should exist");
}
#[test]
fn test_create_snapshot_has_correct_magic() {
let (mut storage, temp) = create_test_storage();
storage
.store(1, &json!({"test": true}))
.expect("Store failed");
storage.create_snapshot().expect("Snapshot creation failed");
let snapshot_path = temp.path().join("payloads.snapshot");
let data = std::fs::read(&snapshot_path).expect("Read snapshot failed");
assert_eq!(&data[0..4], SNAPSHOT_MAGIC, "Magic bytes mismatch");
}
#[test]
fn test_create_snapshot_has_correct_version() {
let (mut storage, temp) = create_test_storage();
storage
.store(1, &json!({"test": true}))
.expect("Store failed");
storage.create_snapshot().expect("Snapshot creation failed");
let snapshot_path = temp.path().join("payloads.snapshot");
let data = std::fs::read(&snapshot_path).expect("Read snapshot failed");
assert_eq!(data[4], SNAPSHOT_VERSION, "Version mismatch");
}
#[test]
fn test_load_from_snapshot_restores_index() {
let temp = TempDir::new().expect("Failed to create temp dir");
{
let mut storage = LogPayloadStorage::new(temp.path()).expect("Create failed");
for i in 1..=100 {
storage.store(i, &json!({"id": i})).expect("Store failed");
}
storage.create_snapshot().expect("Snapshot failed");
}
let storage = LogPayloadStorage::new(temp.path()).expect("Reopen failed");
assert_eq!(storage.ids().len(), 100);
for i in 1..=100 {
let payload = storage.retrieve(i).expect("Retrieve failed");
assert!(payload.is_some(), "Payload {i} should exist");
}
}
#[test]
fn test_load_from_snapshot_plus_delta_wal() {
let temp = TempDir::new().expect("Failed to create temp dir");
{
let mut storage = LogPayloadStorage::new(temp.path()).expect("Create failed");
for i in 1..=50 {
storage
.store(i, &json!({"id": i, "phase": 1}))
.expect("Store failed");
}
storage.create_snapshot().expect("Snapshot failed");
for i in 51..=100 {
storage
.store(i, &json!({"id": i, "phase": 2}))
.expect("Store failed");
}
storage.flush().expect("Flush failed");
}
let storage = LogPayloadStorage::new(temp.path()).expect("Reopen failed");
assert_eq!(storage.ids().len(), 100);
let p1 = storage.retrieve(25).expect("Retrieve failed").unwrap();
assert_eq!(p1["phase"], 1);
let p2 = storage.retrieve(75).expect("Retrieve failed").unwrap();
assert_eq!(p2["phase"], 2);
}
#[test]
fn test_load_from_snapshot_with_deletes_in_delta() {
let temp = TempDir::new().expect("Failed to create temp dir");
{
let mut storage = LogPayloadStorage::new(temp.path()).expect("Create failed");
for i in 1..=50 {
storage.store(i, &json!({"id": i})).expect("Store failed");
}
storage.create_snapshot().expect("Snapshot failed");
for i in 1..=10 {
storage.delete(i).expect("Delete failed");
}
storage.flush().expect("Flush failed");
}
let storage = LogPayloadStorage::new(temp.path()).expect("Reopen failed");
assert_eq!(storage.ids().len(), 40);
for i in 1..=10 {
assert!(storage.retrieve(i).expect("Retrieve failed").is_none());
}
for i in 11..=50 {
assert!(storage.retrieve(i).expect("Retrieve failed").is_some());
}
}
#[test]
fn test_should_create_snapshot_false_when_fresh() {
let (storage, _temp) = create_test_storage();
assert!(!storage.should_create_snapshot());
}
#[test]
fn test_should_create_snapshot_true_after_threshold() {
let (mut storage, temp) = create_test_storage();
let large_payload = json!({"data": "x".repeat(10_000)});
for i in 1..=1100 {
storage.store(i, &large_payload).expect("Store failed");
}
assert!(
!storage.should_create_snapshot(),
"Auto-snapshot should have already fired, resetting the heuristic"
);
let snapshot_path = temp.path().join("payloads.snapshot");
assert!(
snapshot_path.exists(),
"Snapshot file should exist after exceeding WAL threshold"
);
}
#[test]
fn test_should_create_snapshot_false_after_recent_snapshot() {
let (mut storage, _temp) = create_test_storage();
for i in 1..=100 {
storage.store(i, &json!({"id": i})).expect("Store failed");
}
storage.create_snapshot().expect("Snapshot failed");
assert!(!storage.should_create_snapshot());
}
#[test]
fn test_snapshot_crc_validation() {
let temp = TempDir::new().expect("Failed to create temp dir");
{
let mut storage = LogPayloadStorage::new(temp.path()).expect("Create failed");
storage
.store(1, &json!({"test": true}))
.expect("Store failed");
storage.create_snapshot().expect("Snapshot failed");
}
let snapshot_path = temp.path().join("payloads.snapshot");
let mut data = std::fs::read(&snapshot_path).expect("Read failed");
if let Some(last) = data.last_mut() {
*last ^= 0xFF; }
std::fs::write(&snapshot_path, &data).expect("Write failed");
let storage = LogPayloadStorage::new(temp.path()).expect("Should recover via WAL");
assert!(storage.retrieve(1).expect("Retrieve failed").is_some());
}
#[test]
fn test_snapshot_with_empty_storage() {
let (mut storage, temp) = create_test_storage();
storage.create_snapshot().expect("Snapshot failed");
let snapshot_path = temp.path().join("payloads.snapshot");
assert!(snapshot_path.exists());
let storage = LogPayloadStorage::new(temp.path()).expect("Reopen failed");
assert_eq!(storage.ids().len(), 0);
}
#[test]
fn test_wal_position_stored_in_snapshot() {
let temp = TempDir::new().expect("Failed to create temp dir");
{
let mut storage = LogPayloadStorage::new(temp.path()).expect("Create failed");
for i in 1..=50 {
storage.store(i, &json!({"id": i})).expect("Store failed");
}
storage.create_snapshot().expect("Snapshot failed");
}
let snapshot_path = temp.path().join("payloads.snapshot");
let data = std::fs::read(&snapshot_path).expect("Read failed");
let wal_pos = u64::from_le_bytes(data[5..13].try_into().unwrap());
assert!(wal_pos > 0, "WAL position should be recorded");
}
#[test]
fn test_snapshot_malicious_entry_count_dos_prevention() {
let temp = TempDir::new().expect("Failed to create temp dir");
let snapshot_path = temp.path().join("payloads.snapshot");
let mut malicious_data = Vec::new();
malicious_data.extend_from_slice(b"VSNP"); malicious_data.push(1); malicious_data.extend_from_slice(&0u64.to_le_bytes()); malicious_data.extend_from_slice(&u64::MAX.to_le_bytes()); malicious_data.extend_from_slice(&0u32.to_le_bytes());
std::fs::create_dir_all(temp.path()).expect("Create dir failed");
std::fs::write(&snapshot_path, &malicious_data).expect("Write failed");
let wal_path = temp.path().join("payloads.log");
std::fs::write(&wal_path, []).expect("Create WAL failed");
let result = LogPayloadStorage::new(temp.path());
assert!(
result.is_ok(),
"Should handle malicious snapshot gracefully"
);
let storage = result.unwrap();
assert_eq!(storage.ids().len(), 0); }
#[test]
fn test_snapshot_truncated_data() {
let temp = TempDir::new().expect("Failed to create temp dir");
let snapshot_path = temp.path().join("payloads.snapshot");
let mut truncated_data = Vec::new();
truncated_data.extend_from_slice(b"VSNP"); truncated_data.push(1); truncated_data.extend_from_slice(&100u64.to_le_bytes()); truncated_data.extend_from_slice(&10u64.to_le_bytes());
std::fs::create_dir_all(temp.path()).expect("Create dir failed");
std::fs::write(&snapshot_path, &truncated_data).expect("Write failed");
let wal_path = temp.path().join("payloads.log");
std::fs::write(&wal_path, []).expect("Create WAL failed");
let result = LogPayloadStorage::new(temp.path());
assert!(
result.is_ok(),
"Should handle truncated snapshot gracefully"
);
}
#[test]
fn test_snapshot_wrong_magic() {
let temp = TempDir::new().expect("Failed to create temp dir");
let snapshot_path = temp.path().join("payloads.snapshot");
let mut bad_magic = Vec::new();
bad_magic.extend_from_slice(b"HACK"); bad_magic.push(1);
bad_magic.extend_from_slice(&0u64.to_le_bytes());
bad_magic.extend_from_slice(&0u64.to_le_bytes());
bad_magic.extend_from_slice(&0u32.to_le_bytes());
std::fs::create_dir_all(temp.path()).expect("Create dir failed");
std::fs::write(&snapshot_path, &bad_magic).expect("Write failed");
let wal_path = temp.path().join("payloads.log");
std::fs::write(&wal_path, []).expect("Create WAL failed");
let result = LogPayloadStorage::new(temp.path());
assert!(result.is_ok(), "Should handle wrong magic gracefully");
}
#[test]
fn test_snapshot_unsupported_version() {
let temp = TempDir::new().expect("Failed to create temp dir");
let snapshot_path = temp.path().join("payloads.snapshot");
let mut future_version = Vec::new();
future_version.extend_from_slice(b"VSNP");
future_version.push(255); future_version.extend_from_slice(&0u64.to_le_bytes());
future_version.extend_from_slice(&0u64.to_le_bytes());
future_version.extend_from_slice(&0u32.to_le_bytes());
std::fs::create_dir_all(temp.path()).expect("Create dir failed");
std::fs::write(&snapshot_path, &future_version).expect("Write failed");
let wal_path = temp.path().join("payloads.log");
std::fs::write(&wal_path, []).expect("Create WAL failed");
let result = LogPayloadStorage::new(temp.path());
assert!(
result.is_ok(),
"Should handle unsupported version gracefully"
);
}
#[test]
fn test_snapshot_random_garbage() {
let temp = TempDir::new().expect("Failed to create temp dir");
let snapshot_path = temp.path().join("payloads.snapshot");
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let garbage: Vec<u8> = (0..100).map(|i| (i * 17 + 31) as u8).collect();
std::fs::create_dir_all(temp.path()).expect("Create dir failed");
std::fs::write(&snapshot_path, &garbage).expect("Write failed");
let wal_path = temp.path().join("payloads.log");
std::fs::write(&wal_path, []).expect("Create WAL failed");
let result = LogPayloadStorage::new(temp.path());
assert!(result.is_ok(), "Should handle garbage data gracefully");
}
#[test]
fn test_snapshot_entry_count_overflow() {
let temp = TempDir::new().expect("Failed to create temp dir");
let snapshot_path = temp.path().join("payloads.snapshot");
let overflow_count = (usize::MAX / 16) as u64 + 1;
let mut overflow_data = Vec::new();
overflow_data.extend_from_slice(b"VSNP");
overflow_data.push(1);
overflow_data.extend_from_slice(&0u64.to_le_bytes());
overflow_data.extend_from_slice(&overflow_count.to_le_bytes());
overflow_data.extend_from_slice(&0u32.to_le_bytes());
std::fs::create_dir_all(temp.path()).expect("Create dir failed");
std::fs::write(&snapshot_path, &overflow_data).expect("Write failed");
let wal_path = temp.path().join("payloads.log");
std::fs::write(&wal_path, []).expect("Create WAL failed");
let result = LogPayloadStorage::new(temp.path());
assert!(result.is_ok(), "Should handle overflow gracefully");
}
#[test]
fn test_store_and_retrieve_with_fsync_mode() {
let temp = TempDir::new().expect("Failed to create temp dir");
let mut storage = LogPayloadStorage::new_with_durability(temp.path(), DurabilityMode::Fsync)
.expect("Create failed");
let payload = json!({"durability": "fsync"});
storage.store(1, &payload).expect("Store failed");
let retrieved = storage.retrieve(1).expect("Retrieve failed");
assert_eq!(retrieved, Some(payload));
}
#[test]
fn test_delete_persists_with_fsync_mode() {
let temp = TempDir::new().expect("Failed to create temp dir");
let mut storage = LogPayloadStorage::new_with_durability(temp.path(), DurabilityMode::Fsync)
.expect("Create failed");
let payload = json!({"to_delete": true});
storage.store(1, &payload).expect("Store failed");
storage.delete(1).expect("Delete failed");
assert_eq!(storage.retrieve(1).expect("Retrieve failed"), None);
drop(storage);
let reopened = LogPayloadStorage::new(temp.path()).expect("Reopen failed");
assert_eq!(reopened.retrieve(1).expect("Retrieve failed"), None);
}
#[test]
fn test_durability_mode_none_does_not_crash() {
let temp = TempDir::new().expect("Failed to create temp dir");
let mut storage = LogPayloadStorage::new_with_durability(temp.path(), DurabilityMode::None)
.expect("Create failed");
for i in 1..=10 {
storage.store(i, &json!({"id": i})).expect("Store failed");
}
storage.delete(5).expect("Delete failed");
storage.flush().expect("Flush failed");
assert_eq!(storage.ids().len(), 9);
assert!(storage.retrieve(1).expect("Retrieve failed").is_some());
assert!(storage.retrieve(5).expect("Retrieve failed").is_none());
}
#[test]
fn test_durability_mode_flush_only() {
let temp = TempDir::new().expect("Failed to create temp dir");
let mut storage =
LogPayloadStorage::new_with_durability(temp.path(), DurabilityMode::FlushOnly)
.expect("Create failed");
storage
.store(1, &json!({"mode": "flush_only"}))
.expect("Store failed");
storage.flush().expect("Flush failed");
let retrieved = storage.retrieve(1).expect("Retrieve failed");
assert_eq!(retrieved, Some(json!({"mode": "flush_only"})));
}
#[test]
fn test_new_defaults_to_fsync() {
let temp = TempDir::new().expect("Failed to create temp dir");
let mut storage = LogPayloadStorage::new(temp.path()).expect("Create failed");
let payload = json!({"default": true});
storage.store(1, &payload).expect("Store failed");
drop(storage);
let reopened = LogPayloadStorage::new(temp.path()).expect("Reopen failed");
assert_eq!(
reopened.retrieve(1).expect("Retrieve failed"),
Some(payload)
);
}
#[test]
fn test_auto_snapshot_triggers_after_threshold() {
let temp = TempDir::new().expect("Failed to create temp dir");
let mut storage = LogPayloadStorage::new(temp.path()).expect("Create failed");
let large_payload = json!({"data": "x".repeat(10_000)});
for i in 1..=1100 {
storage.store(i, &large_payload).expect("Store failed");
}
let snapshot_path = temp.path().join("payloads.snapshot");
assert!(
snapshot_path.exists(),
"Snapshot file should be auto-created after exceeding WAL threshold"
);
assert!(
!storage.should_create_snapshot(),
"should_create_snapshot must be false immediately after auto-snapshot"
);
}
#[test]
fn test_auto_snapshot_data_survives_reopen() {
let temp = TempDir::new().expect("Failed to create temp dir");
{
let mut storage = LogPayloadStorage::new(temp.path()).expect("Create failed");
let large_payload = json!({"data": "x".repeat(10_000)});
for i in 1..=1100 {
storage.store(i, &large_payload).expect("Store failed");
}
}
let storage = LogPayloadStorage::new(temp.path()).expect("Reopen failed");
assert_eq!(storage.ids().len(), 1100);
assert!(storage.retrieve(1).expect("Retrieve failed").is_some());
assert!(storage.retrieve(1100).expect("Retrieve failed").is_some());
}
#[test]
fn test_no_auto_snapshot_below_threshold() {
let (mut storage, temp) = create_test_storage();
for i in 1..=10 {
storage.store(i, &json!({"id": i})).expect("Store failed");
}
let snapshot_path = temp.path().join("payloads.snapshot");
assert!(
!snapshot_path.exists(),
"Snapshot file should NOT be created when WAL is below threshold"
);
}