//! `spectra admin verify|check|rebuild` against a real ledger, run as the //! built binary in a hermetic environment: a temporary data directory and //! `SPECTRA_HOME`, never the user's own. use spectra_core::event::NewEvent; use spectra_core::keys::KeyPair; use spectra_core::ledger::Ledger; use std::path::Path; use std::process::Command; /// The binary with every directory it might read pointed at `dir`. fn hermetic(dir: &Path) -> Command { let mut c = Command::new(env!("CARGO_BIN_EXE_spectra")); c.env("SPECTRA_DATA", dir.join("data")) .env("SPECTRA_HOME", dir.join("owner-home")) .env("HOME", dir) .env("XDG_DATA_HOME", dir.join("xdg")) .env("NO_COLOR", "1"); c } async fn ledger_with_events(data: &Path, n: usize) { std::fs::create_dir_all(data).unwrap(); let instance = KeyPair::instance(data, "test").unwrap(); let ledger = Ledger::open(&data.join("spectra.db"), Some(instance.clone())).await.unwrap(); // As `spectra init` records it: signed by the instance key. ledger .append_one(spectra_core::principals::signed_event( &instance, "system", "identity.instance", spectra_core::principals::identity_payload(&instance.identity), )) .await .unwrap(); for i in 0..n { ledger.append_one(NewEvent::system("test.message", serde_json::json!({ "n": i }))).await.unwrap(); } ledger.checkpoint().await.unwrap(); } #[tokio::test] async fn verify_check_and_rebuild_on_a_clean_and_a_tampered_ledger() { let dir = tempfile::tempdir().unwrap(); let data = dir.path().join("data"); ledger_with_events(&data, 40).await; let out = hermetic(dir.path()).args(["admin", "verify", "--json"]).output().unwrap(); assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("one JSON document on stdout"); assert_eq!(v["events"], 42); assert_eq!(v["findings"].as_array().unwrap().len(), 0); assert_eq!(v["uncovered"], 0); assert!(hermetic(dir.path()).args(["admin", "check"]).status().unwrap().success()); assert!(hermetic(dir.path()).args(["admin", "rebuild"]).status().unwrap().success()); let pool = sqlx::SqlitePool::connect(&format!("sqlite://{}", data.join("spectra.db").display())).await.unwrap(); sqlx::query("UPDATE events SET actor = 'agent:mallory' WHERE seq = 10").execute(&pool).await.unwrap(); pool.close().await; let out = hermetic(dir.path()).args(["admin", "verify"]).output().unwrap(); assert_eq!(out.status.code(), Some(1)); let text = String::from_utf8_lossy(&out.stdout); assert!(text.contains("does not verify") && text.contains("#10") && text.contains("HashMismatch"), "{text}"); } #[test] fn a_missing_data_directory_says_what_to_do() { let dir = tempfile::tempdir().unwrap(); let out = hermetic(dir.path()).args(["admin", "verify"]).output().unwrap(); assert_eq!(out.status.code(), Some(1)); assert!(String::from_utf8_lossy(&out.stderr).contains("spectra init")); }