kcode-kweb-db 1.0.1

A disk-first convergent signed-DAG store for Kweb nodes and objects
Documentation
use chrono::Utc;
use kcode_kweb_db::{
    Config, Error, Gossip, KwebDb, NodeData, NodeId, NoopGossip, ObjectPayload, Owner, Provenance,
    TransactionPackage, WriterId,
};
use std::sync::{Arc, Mutex};

#[derive(Default)]
struct Recorder(Mutex<Vec<TransactionPackage>>);

impl Gossip for Recorder {
    fn announce(&self, package: TransactionPackage) -> bool {
        self.0.lock().unwrap().push(package);
        true
    }
}

fn provenance(label: &str) -> Provenance {
    Provenance {
        author: "Kennedy".into(),
        source: label.into(),
        source_created_at: Utc::now(),
        data: String::new(),
    }
}

fn data(name: &str) -> NodeData {
    NodeData {
        short_name: name.into(),
        short_description: "A test node".into(),
        long_description: "Complete node data for an integration test.".into(),
        owner: Owner::SelfNode,
        fixed_connections: Vec::new(),
        recent_connections: Vec::new(),
        objects: Vec::new(),
    }
}

fn config(key: [u8; 32], writers: Vec<WriterId>, gossip: Arc<dyn Gossip>) -> Config {
    Config {
        signing_key: key,
        writers_by_priority: writers,
        gossip,
    }
}

#[test]
fn complete_replacement_order_objects_history_and_drop() {
    let root = tempfile::tempdir().unwrap();
    let key = [1; 32];
    let writer = WriterId::from_signing_key(&key);
    let recorder = Arc::new(Recorder::default());
    let database = KwebDb::open(root.path(), config(key, vec![writer], recorder.clone())).unwrap();

    let mut create = database.start_transaction(provenance("create")).unwrap();
    let object = create.create_object(b"exact bytes".to_vec()).unwrap();
    let first = create.create_node(data("First node")).unwrap();
    let second = create.create_node(data("Second node")).unwrap();
    let third = create.create_node(data("Third node")).unwrap();
    create.finalize().unwrap();

    let mut update = database.start_transaction(provenance("replace")).unwrap();
    let mut replacement = data("First replaced");
    replacement.fixed_connections = vec![second, third];
    replacement.recent_connections = vec![third, second];
    replacement.objects = vec![object];
    update.update_node(first, replacement.clone()).unwrap();
    update.finalize().unwrap();
    assert_eq!(database.get_node(first).unwrap().data, replacement);
    assert_eq!(database.get_object(object).unwrap(), b"exact bytes");
    assert_eq!(database.get_node_history(first).unwrap().entries.len(), 2);

    let mut clear = database.start_transaction(provenance("clear")).unwrap();
    let cleared = data("First cleared");
    clear.update_node(first, cleared.clone()).unwrap();
    clear.finalize().unwrap();
    assert_eq!(database.get_node(first).unwrap().data, cleared);

    let mut dropped = database.start_transaction(provenance("drop")).unwrap();
    let dropped_id = dropped.create_node(data("Dropped node")).unwrap();
    drop(dropped);
    assert!(database.get_node(dropped_id).is_err());
    assert_eq!(recorder.0.lock().unwrap().len(), 3);
}

#[test]
fn reserved_ids_support_circular_creation_and_fail_closed() {
    let root = tempfile::tempdir().unwrap();
    let key = [3; 32];
    let writer = WriterId::from_signing_key(&key);
    let database =
        KwebDb::open(root.path(), config(key, vec![writer], Arc::new(NoopGossip))).unwrap();

    let mut create = database
        .start_transaction(provenance("circular-create"))
        .unwrap();
    let first = create.reserve_node_id().unwrap();
    let second = create.reserve_node_id().unwrap();
    let mut first_data = data("First circular");
    first_data.fixed_connections = vec![second];
    let mut second_data = data("Second circular");
    second_data.recent_connections = vec![first];
    create.create_reserved_node(first, first_data).unwrap();
    create.create_reserved_node(second, second_data).unwrap();
    create.finalize().unwrap();

    assert_eq!(
        database.get_node(first).unwrap().data.fixed_connections,
        vec![second]
    );
    assert_eq!(
        database.get_node(second).unwrap().data.recent_connections,
        vec![first]
    );

    let mut misuse = database
        .start_transaction(provenance("reservation-misuse"))
        .unwrap();
    let unreserved = NodeId::from_bytes([0x7f, 1, 2, 3, 4, 5]).unwrap();
    assert!(matches!(
        misuse.create_reserved_node(unreserved, data("Unreserved node")),
        Err(Error::InvalidInput(message)) if message.contains("was not reserved")
    ));
    let reserved = misuse.reserve_node_id().unwrap();
    misuse
        .create_reserved_node(reserved, data("Reserved node"))
        .unwrap();
    assert!(matches!(
        misuse.create_reserved_node(reserved, data("Duplicate node")),
        Err(Error::InvalidInput(message)) if message.contains("already been materialized")
    ));
    misuse.finalize().unwrap();

    let mut unfinished = database
        .start_transaction(provenance("unfinished-reservation"))
        .unwrap();
    unfinished.reserve_node_id().unwrap();
    assert!(matches!(
        unfinished.finalize(),
        Err(Error::InvalidInput(message)) if message.contains("unmaterialized reserved node")
    ));
}

#[test]
fn identifiers_packages_and_config_fail_closed() {
    let root = tempfile::tempdir().unwrap();
    let key = [2; 32];
    assert!(KwebDb::open(root.path(), config(key, Vec::new(), Arc::new(NoopGossip))).is_err());

    let node = kcode_kweb_db::NodeId::from_bytes([0, 1, 2, 3, 4, 5]).unwrap();
    let object = kcode_kweb_db::ObjectId::from_bytes([128, 1, 2, 3, 4, 5]).unwrap();
    assert_eq!(node.to_string().len(), 8);
    assert_eq!(object.to_string().len(), 8);
    assert!(object.to_string().parse::<kcode_kweb_db::NodeId>().is_err());
    assert!(kcode_kweb_db::NodeId::from_bytes([128, 1, 2, 3, 4, 5]).is_err());
    assert!(kcode_kweb_db::ObjectId::from_bytes([0, 1, 2, 3, 4, 5]).is_err());

    let package = TransactionPackage {
        transaction: vec![0, 1, 255],
        objects: vec![ObjectPayload {
            id: object,
            bytes: vec![2, 3, 4],
        }],
    };
    assert_eq!(package.transaction, vec![0, 1, 255]);
    assert_eq!(package.objects[0].bytes, vec![2, 3, 4]);
}