use std::path::Path;
use std::process::Command;
use lithair_core::engine::events::{EventEnvelope, EventStore};
use lithair_core::engine::persistence::calculate_crc32;
fn make_good_store(dir: &Path, n: u64) {
std::fs::create_dir_all(dir).expect("create data dir");
let path = dir.to_string_lossy().to_string();
let mut store = EventStore::new(&path).expect("open store");
for i in 0..n {
let env = EventEnvelope {
event_type: "Article.Created".to_string(),
event_id: format!("evt-{}", i),
timestamp: 1_700_000_000 + i,
payload: format!(r#"{{"id":"art-{}","title":"t{}"}}"#, i, i),
aggregate_id: Some(format!("art-{}", i)),
event_hash: None,
previous_hash: None,
};
store.append_envelope(&env).expect("append");
}
store.flush_events().expect("flush");
}
fn tamper_first_record(dir: &Path) {
let log = dir.join("events.raftlog");
let original = std::fs::read_to_string(&log).expect("read log");
let mut lines: Vec<String> = original.lines().map(|s| s.to_string()).collect();
assert!(!lines.is_empty(), "log has lines to tamper");
let first = &lines[0];
assert!(first.len() > 9 && first.as_bytes()[8] == b':', "CRC32-prefixed line");
let json = &first[9..];
let tampered = json.replacen(r#"title\":\"t0\""#, r#"title\":\"HACKED\""#, 1);
assert_ne!(tampered, json, "tamper changed the payload (line was: {})", json);
let crc = calculate_crc32(tampered.as_bytes());
lines[0] = format!("{:08x}:{}", crc, tampered);
let mut rebuilt = lines.join("\n");
rebuilt.push('\n');
std::fs::write(&log, rebuilt.as_bytes()).expect("write tampered log");
}
fn run_verify(data_dir: &Path) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_lithair"))
.arg("verify")
.arg(data_dir)
.output()
.expect("run lithair verify")
}
#[test]
fn verify_good_store_exits_zero() {
let tmp = tempfile::tempdir().expect("tmpdir");
let dir = tmp.path().join("good");
make_good_store(&dir, 10);
let out = run_verify(&dir);
let stdout = String::from_utf8_lossy(&out.stdout);
assert_eq!(out.status.code(), Some(0), "valid chain exits 0; stdout:\n{}", stdout);
assert!(stdout.contains("total events:"), "summary lists total events:\n{}", stdout);
assert!(stdout.contains("Result: OK"), "reports OK:\n{}", stdout);
assert!(
stdout.contains("total events: 10") || stdout.contains("total events: 10\n"),
"reports the 10 events:\n{}",
stdout
);
}
#[test]
fn verify_tampered_store_exits_one() {
let tmp = tempfile::tempdir().expect("tmpdir");
let dir = tmp.path().join("tampered");
make_good_store(&dir, 10);
tamper_first_record(&dir);
let out = run_verify(&dir);
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert_eq!(
out.status.code(),
Some(1),
"tampered chain exits 1; stdout:\n{}\nstderr:\n{}",
stdout,
stderr
);
assert!(stdout.contains("INVALID"), "reports INVALID on stdout:\n{}", stdout);
assert!(
stdout.contains("first bad hash") || stdout.contains("first broken link"),
"points at the first offending event on stdout:\n{}",
stdout
);
}
#[test]
fn verify_missing_dir_exits_two() {
let tmp = tempfile::tempdir().expect("tmpdir");
let missing = tmp.path().join("does-not-exist");
let out = run_verify(&missing);
let stderr = String::from_utf8_lossy(&out.stderr);
assert_eq!(out.status.code(), Some(2), "unreadable store exits 2");
assert!(stderr.contains("does not exist"), "explains the missing dir:\n{}", stderr);
}