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");
let (object_bytes, object_provenance) = database.get_object_with_provenance(object).unwrap();
assert_eq!(object_bytes, b"exact bytes");
assert_eq!(object_provenance.author, "Kennedy");
assert_eq!(object_provenance.source, "create");
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]);
}