ontoref-code/crates/ontoref-oplog/tests/basic.rs
2026-07-10 01:44:59 +01:00

173 lines
5.4 KiB
Rust

use std::collections::HashSet;
use ed25519_dalek::SigningKey;
use ontoref_types::{
operation::{OpBody, OpPayload, Operation},
Hlc, OpId, PublicKey,
};
use ontoref_oplog::{Error, OpLog};
fn signing_key() -> SigningKey {
SigningKey::from_bytes(&[42u8; 32])
}
fn make_op(parents: Vec<OpId>, timestamp: Hlc, label: &str) -> Operation {
let key = signing_key();
let body = OpBody {
actor: PublicKey::from_signing_key(&key),
parents,
payload: OpPayload::Noop {
label: label.into(),
},
timestamp,
};
Operation::sign(body, &key)
}
#[test]
fn replay_yields_same_heads_for_linear_chain() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("oplog.redb");
let original_heads: Vec<OpId> = {
let log = OpLog::open(&path).unwrap();
let mut prev_id = None;
let mut prev_hlc = Hlc::genesis();
for i in 0..100 {
prev_hlc = Hlc::tick(prev_hlc, 0);
let parents = prev_id.map(|id| vec![id]).unwrap_or_default();
let op = make_op(parents, prev_hlc, &format!("op-{i}"));
prev_id = Some(log.append(&op).unwrap());
}
assert_eq!(log.len().unwrap(), 100);
log.heads().unwrap()
};
// Drop the OpLog (closes the redb handle), reopen the file, re-read heads.
let reopened_heads = OpLog::open(&path).unwrap().heads().unwrap();
assert_eq!(original_heads, reopened_heads);
assert_eq!(original_heads.len(), 1, "a linear chain has exactly one head");
}
#[test]
fn replay_yields_same_heads_for_branching_dag() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("oplog.redb");
let original_heads: HashSet<OpId> = {
let log = OpLog::open(&path).unwrap();
let mut clock = Hlc::genesis();
// Root op.
clock = Hlc::tick(clock, 0);
let root = log.append(&make_op(vec![], clock, "root")).unwrap();
// Two children sharing the same root.
clock = Hlc::tick(clock, 0);
let left = log.append(&make_op(vec![root], clock, "left")).unwrap();
clock = Hlc::tick(clock, 0);
let right = log.append(&make_op(vec![root], clock, "right")).unwrap();
// Merge: a child of both left and right.
clock = Hlc::tick(clock, 0);
let merged = log
.append(&make_op(vec![left, right], clock, "merged"))
.unwrap();
// Extra branch from the root that stays a head.
clock = Hlc::tick(clock, 0);
let stray = log.append(&make_op(vec![root], clock, "stray")).unwrap();
// Expected heads: {merged, stray}.
let expected: HashSet<OpId> = [merged, stray].into_iter().collect();
assert_eq!(
log.heads().unwrap().into_iter().collect::<HashSet<_>>(),
expected
);
expected
};
let reopened: HashSet<OpId> = OpLog::open(&path)
.unwrap()
.heads()
.unwrap()
.into_iter()
.collect();
assert_eq!(reopened, original_heads);
}
#[test]
fn parents_validation_rejects_unknown_parent() {
let dir = tempfile::tempdir().unwrap();
let log = OpLog::open(dir.path().join("oplog.redb")).unwrap();
let bogus = OpId([0xAA; 32]);
let op = make_op(vec![bogus], Hlc::new(1, 0), "orphan");
match log.append(&op) {
Err(Error::UnknownParent(parent)) if parent == bogus => {}
other => panic!("expected UnknownParent({bogus:?}), got {other:?}"),
}
assert_eq!(log.len().unwrap(), 0, "rejected op must not be stored");
}
#[test]
fn parents_validation_accepts_known_parent() {
let dir = tempfile::tempdir().unwrap();
let log = OpLog::open(dir.path().join("oplog.redb")).unwrap();
let root = log
.append(&make_op(vec![], Hlc::new(1, 0), "root"))
.unwrap();
let child = log
.append(&make_op(vec![root], Hlc::new(2, 0), "child"))
.unwrap();
assert!(log.get(&root).unwrap().is_some());
assert!(log.get(&child).unwrap().is_some());
assert_eq!(log.heads().unwrap(), vec![child]);
}
#[test]
fn append_is_idempotent() {
let dir = tempfile::tempdir().unwrap();
let log = OpLog::open(dir.path().join("oplog.redb")).unwrap();
let op = make_op(vec![], Hlc::new(1, 0), "same");
let id1 = log.append(&op).unwrap();
let id2 = log.append(&op).unwrap();
assert_eq!(id1, id2);
assert_eq!(log.len().unwrap(), 1);
}
#[test]
fn append_rejects_bad_signature() {
let dir = tempfile::tempdir().unwrap();
let log = OpLog::open(dir.path().join("oplog.redb")).unwrap();
let mut op = make_op(vec![], Hlc::new(1, 0), "tampered");
// Flip a bit in the signature so it no longer verifies.
op.signature.0[0] ^= 0x01;
match log.append(&op) {
Err(Error::BadSignature(_)) => {}
other => panic!("expected BadSignature, got {other:?}"),
}
}
#[test]
fn append_rejects_non_monotonic_timestamp() {
let dir = tempfile::tempdir().unwrap();
let log = OpLog::open(dir.path().join("oplog.redb")).unwrap();
let root = log
.append(&make_op(vec![], Hlc::new(5, 0), "root"))
.unwrap();
// Child with same timestamp as the parent — must be rejected.
let bad_child = make_op(vec![root], Hlc::new(5, 0), "child-same-time");
match log.append(&bad_child) {
Err(Error::NonMonotonicTimestamp(_)) => {}
other => panic!("expected NonMonotonicTimestamp, got {other:?}"),
}
}