use chrono::Utc;
use kcode_kweb_db::{Config, KwebDb, NodeData, NoopGossip, Owner, Provenance, WriterId};
use std::{fs, io::Write, sync::Arc};
fn provenance(label: &str) -> Provenance {
Provenance {
author: "Recovery".into(),
source: label.into(),
source_created_at: Utc::now(),
data: String::new(),
}
}
fn data(name: &str, objects: Vec<kcode_kweb_db::ObjectId>) -> NodeData {
NodeData {
short_name: name.into(),
short_description: String::new(),
long_description: String::new(),
owner: Owner::SelfNode,
fixed_connections: [None; 3],
objects,
}
}
fn config(key: [u8; 32]) -> Config {
Config {
signing_key: key,
writers_by_priority: vec![WriterId::from_signing_key(&key)],
gossip: Arc::new(NoopGossip),
}
}
#[test]
fn restart_repairs_torn_tail_and_rebuilds_nodes() {
let root = tempfile::tempdir().unwrap();
let key = [31; 32];
let database = KwebDb::open(root.path(), config(key)).unwrap();
let mut transaction = database.start_transaction(provenance("create")).unwrap();
let node = transaction
.create_node(data("Durable node", Vec::new()))
.unwrap();
transaction.finalize().unwrap();
drop(database);
fs::remove_dir_all(root.path().join("nodes")).unwrap();
let mut ledger = fs::OpenOptions::new()
.append(true)
.open(root.path().join("ledger.kwl"))
.unwrap();
ledger.write_all(&123_u64.to_be_bytes()[..5]).unwrap();
ledger.sync_all().unwrap();
drop(ledger);
let reopened = KwebDb::open(root.path(), config(key)).unwrap();
assert_eq!(
reopened.get_node(node).unwrap().data.short_name,
"Durable node"
);
assert!(root.path().join("nodes").is_dir());
}
#[test]
fn complete_ledger_corruption_is_rejected() {
let root = tempfile::tempdir().unwrap();
let key = [32; 32];
let database = KwebDb::open(root.path(), config(key)).unwrap();
let mut transaction = database.start_transaction(provenance("create")).unwrap();
transaction
.create_node(data("Corrupt me", Vec::new()))
.unwrap();
transaction.finalize().unwrap();
drop(database);
let path = root.path().join("ledger.kwl");
let mut bytes = fs::read(&path).unwrap();
let last = bytes.len() - 1;
bytes[last] ^= 1;
fs::write(path, bytes).unwrap();
assert!(KwebDb::open(root.path(), config(key)).is_err());
}
#[test]
fn object_corruption_is_detected_on_read_and_restart() {
let root = tempfile::tempdir().unwrap();
let key = [33; 32];
let database = KwebDb::open(root.path(), config(key)).unwrap();
let mut transaction = database.start_transaction(provenance("object")).unwrap();
let object = transaction.create_object(b"payload".to_vec()).unwrap();
transaction
.create_node(data("Object node", vec![object]))
.unwrap();
let transaction_id = transaction.finalize().unwrap();
let text = object.to_string();
let path = root
.path()
.join("objects")
.join(&text[..2])
.join(&text[2..])
.join(format!("{transaction_id}.kwo"));
let mut bytes = fs::read(&path).unwrap();
let last = bytes.len() - 1;
bytes[last] ^= 1;
fs::write(path, bytes).unwrap();
assert!(database.get_object(object).is_err());
drop(database);
assert!(KwebDb::open(root.path(), config(key)).is_err());
}
#[test]
fn second_opener_is_rejected_until_first_drops() {
let root = tempfile::tempdir().unwrap();
let key = [34; 32];
let first = KwebDb::open(root.path(), config(key)).unwrap();
assert!(KwebDb::open(root.path(), config(key)).is_err());
drop(first);
assert!(KwebDb::open(root.path(), config(key)).is_ok());
}