use super::*;
#[test]
fn the_car_verifies_and_its_refs_record_matches_the_public_refs() {
for format in ["sha1", "sha256"] {
let fixture = fixture(format);
let settled = settle_fixture(&fixture);
let car = knot_record::car_blocks(&settled.car()).unwrap();
assert_eq!(car.roots, vec![settled.commit]);
let commit = Commit::<String>::from_cbor(&car.blocks[&settled.commit]).unwrap();
assert_eq!(commit.did.as_str(), fixture.source.did.as_str());
assert_eq!(commit.rev().to_string(), settled.rev);
let unsigned = serde_ipld_dagcbor::to_vec(&UnsignedCommit {
did: commit.did.clone(),
version: commit.version,
data: *commit.data(),
rev: commit.rev().clone(),
prev: commit.prev().copied(),
})
.unwrap();
assert!(
knot_runtime::verify(
&fixture.signer.public_key(),
&unsigned,
&knot_runtime::Signature::from_bytes(commit.sig().to_vec()),
),
"{format}: the commit verifies with the knot key"
);
let Some(Ipld::Map(refs)) = record_of(&settled.car(), "org.tangled.git.refs") else {
panic!("{format}: the car carries a refs record");
};
let Some(Ipld::Bytes(listed)) = refs.get("refs") else {
panic!("{format}: refs is bytes");
};
let expected = git(
&fixture.source.export_dir,
&[
"for-each-ref",
"--format=%(objectname) %(refname)",
"refs/heads",
"refs/tags",
],
) + "\n";
assert_eq!(String::from_utf8_lossy(listed), expected);
let oid_width = if format == "sha256" { 64 } else { 40 };
assert!(
expected
.lines()
.all(|line| line.find(' ') == Some(oid_width))
);
assert_eq!(
refs.get("defaultBranch"),
Some(&Ipld::String("main".into()))
);
assert!(record_of(&settled.car(), "org.tangled.repo.manifest").is_some());
}
}
#[test]
fn a_noop_generation_keeps_the_rev_and_a_change_raises_it() {
let fixture = fixture("sha1");
let first = settle_fixture(&fixture);
let again = settle_fixture(&fixture);
assert_eq!((again.commit, &again.rev), (first.commit, &first.rev));
assert_eq!(
again.car(),
first.car(),
"an unchanged repo serves identical bytes"
);
std::fs::write(fixture.work.join("a"), "two").unwrap();
git(&fixture.work, &["commit", "-q", "-am", "two"]);
git(
&fixture.source.export_dir,
&[
"fetch",
"-q",
fixture.work.to_str().unwrap(),
"+refs/heads/main:refs/heads/main",
],
);
let moved = settle_fixture(&fixture);
assert_ne!(moved.commit, first.commit);
assert!(moved.rev > first.rev, "{} > {}", moved.rev, first.rev);
let car = knot_record::car_blocks(&moved.car()).unwrap();
assert_eq!(car.roots, vec![moved.commit], "the moved tip is served");
}
#[test]
fn a_cached_cob_state_is_the_replayed_one_and_reads_no_index() {
let fixture = fixture("sha1");
let source = CobSource::new(&fixture);
let (alice, bob) = (account("alice"), account("bob"));
let cobs = source.cobs(&fixture.signer);
let (ticket, first) = cobs.open_ticket("3lubrptx57d22", &alice, "one");
cobs.edit_ticket(ticket, &alice, first, "two", 20);
let comment = knot_cobs::Comment {
subject: RecordCid::new(knot_types::dag_cbor_cid(b"subject")).unwrap(),
reply: None,
};
let (erased, version) = cobs.open("3lubrptx57d33", &alice, 1, comment, "gone", 30);
cobs.erase::<knot_cobs::Comment>(erased, &alice, version, 40);
cobs.add_collaborator(&bob, &alice, 50);
let store = RepoStore::open(&fixture._root.path().join("store"));
let lock = test_lock(fixture._root.path(), &source.did);
let records = Datastore::at(fixture._root.path());
let cold = cob_state(
&source.index,
&source.repo,
&source.did,
&store,
&records,
&lock,
)
.unwrap();
let elsewhere = RepoStore::open(&fixture._root.path().join("elsewhere"));
let fresh = cob_state(
&source.index,
&source.repo,
&source.did,
&elsewhere,
&records,
&lock,
)
.unwrap();
assert_eq!(state_json(&cold), state_json(&fresh));
assert_eq!(cold.records.len(), 2, "the ticket and the grant");
assert_eq!(cold.versions.len(), 2, "the ticket and the erased comment");
let meta = fixture._root.path().join("blind-meta");
Repo::create(&meta).unwrap();
let blind = Index::new(
meta,
knot_git::Layout::new(fixture._root.path().join("nowhere")),
);
let empty = RepoStore::open(&fixture._root.path().join("empty"));
assert!(
cob_state(&blind, &source.repo, &source.did, &empty, &records, &lock).is_err(),
"this index can't replay the repo"
);
let warm = cob_state(&blind, &source.repo, &source.did, &store, &records, &lock).unwrap();
assert_eq!(state_json(&warm), state_json(&cold));
}
#[test]
fn cob_state_refuses_a_lock_on_another_repo() {
let fixture = fixture("sha1");
let source = CobSource::new(&fixture);
let store = RepoStore::open(&fixture._root.path().join("store"));
let other = RepoDid::new("did:plc:elsewhere").unwrap();
let lock = test_lock(fixture._root.path(), &other);
let refused = cob_state(
&source.index,
&source.repo,
&source.did,
&store,
&Datastore::at(fixture._root.path()),
&lock,
)
.err()
.map(|error| format!("{error:#}"));
assert!(
refused.is_some_and(|error| error.contains("under the lock of")),
"a lock on another repo is not this repo's"
);
}
#[test]
fn a_moved_cob_is_replayed_and_the_others_come_from_their_cache() {
let fixture = fixture("sha1");
let source = CobSource::new(&fixture);
let alice = account("alice");
let cobs = source.cobs(&fixture.signer);
let (moved, first) = cobs.open_ticket("3lubrptx57d22", &alice, "one");
let (kept, _) = cobs.open_ticket("3lubrptx57d33", &alice, "kept");
let (same, same_version) = cobs.open_ticket("3lubrptx57d44", &alice, "same");
let store = RepoStore::open(&fixture._root.path().join("store"));
let lock = test_lock(fixture._root.path(), &source.did);
let records = Datastore::at(fixture._root.path());
cob_state(
&source.index,
&source.repo,
&source.did,
&store,
&records,
&lock,
)
.unwrap();
let kind_dir = fixture._root.path().join("store/cache").join(COB_KIND);
let kept_entry = std::fs::read_dir(&kind_dir)
.unwrap()
.map(|entry| entry.unwrap().path())
.find(|path| path.to_string_lossy().contains(&kept.oid().to_hex()))
.expect("the kept ticket's cached state");
let mut entry: serde_json::Value =
serde_json::from_slice(&std::fs::read(&kept_entry).unwrap()).unwrap();
let marked = crate::test_cobs::record("org.tangled.track.ticket", "from the cache");
entry["payload"]["body"] = encode_body(&marked).into();
std::fs::write(&kept_entry, serde_json::to_vec(&entry).unwrap()).unwrap();
cobs.edit_ticket(moved, &alice, first, "two", 20);
cobs.edit_ticket(same, &alice, same_version, "same", 30);
let state = cob_state(
&source.index,
&source.repo,
&source.did,
&store,
&records,
&lock,
)
.unwrap();
let text = |rkey: &str| {
let key = MstKey::new(format!("org.tangled.track.ticket/{rkey}")).unwrap();
body_text(state.records[&key].as_ref().unwrap())
};
assert_eq!(text("3lubrptx57d22"), "two", "the moved ticket is replayed");
assert_eq!(text("3lubrptx57d33"), "from the cache");
assert_eq!(
state.versions["org.tangled.track.ticket/3lubrptx57d22"].len(),
2
);
assert_eq!(
state.versions["org.tangled.track.ticket/3lubrptx57d44"].len(),
2,
"an edit to the same body moves the ref and adds a version"
);
}
#[test]
fn settle_publishes_exactly_the_live_cobs_and_collaborators() {
let mut fixture = fixture("sha1");
let source = CobSource::new(&fixture);
let (alice, bob) = (account("alice"), account("bob"));
let cobs = source.cobs(&fixture.signer);
let (ticket, first) = cobs.open_ticket("3lubrptx57d22", &alice, "one");
cobs.edit_ticket(ticket, &alice, first, "two", 20);
let comment = knot_cobs::Comment {
subject: RecordCid::new(knot_types::dag_cbor_cid(b"subject")).unwrap(),
reply: None,
};
let (erased, version) = cobs.open("3lubrptx57d33", &alice, 1, comment, "gone", 30);
cobs.erase::<knot_cobs::Comment>(erased, &alice, version, 40);
cobs.add_collaborator(&bob, &alice, 50);
let settled = source.settle(&mut fixture);
assert_eq!(
leaf_keys(&fixture),
[
"org.tangled.git.refs/self",
"org.tangled.graph.collaborator/did:plc:bob",
"org.tangled.repo.manifest/self",
"org.tangled.track.ticket/3lubrptx57d22",
]
);
assert_eq!(texts(&settled.car(), "org.tangled.track.ticket"), ["two"]);
assert!(texts(&settled.car(), "org.tangled.feed.comment").is_empty());
assert_eq!(manifest_name(&settled.car()), "demo");
}
#[test]
fn a_manifest_cob_replaces_the_synthesized_manifest() {
let mut fixture = fixture("sha1");
let source = CobSource::new(&fixture);
let owner = account("owner");
let record = ManifestRecord::new(
fixture.source.owner.clone(),
RepoName::new("from-cob").unwrap(),
ManifestMeta::default(),
UnixSeconds::new(1),
owner.clone(),
)
.unwrap()
.body()
.unwrap();
source.cobs(&fixture.signer).open_manifest(&owner, record);
let settled = source.settle(&mut fixture);
assert_eq!(manifest_name(&settled.car()), "from-cob");
}
#[test]
fn a_cob_only_change_raises_the_rev_and_a_noop_does_not() {
let mut fixture = fixture("sha1");
let source = CobSource::new(&fixture);
let alice = account("alice");
source
.cobs(&fixture.signer)
.open_ticket("3lubrptx57d22", &alice, "one");
let first = source.settle(&mut fixture);
let again = source.settle(&mut fixture);
assert_eq!((again.commit, &again.rev), (first.commit, &first.rev));
source.cobs(&fixture.signer).open::<knot_cobs::Ticket>(
"3lubrptx57d33",
&alice,
2,
knot_cobs::Ticket {
state: knot_record::ticket::TicketState::Open,
tangled_labels: None,
assignees: None,
},
"second",
20,
);
let moved = source.settle(&mut fixture);
assert_ne!(moved.commit, first.commit);
assert!(moved.rev > first.rev, "{} > {}", moved.rev, first.rev);
}
#[test]
fn an_unreadable_cob_keeps_its_published_record_or_is_skipped() {
let mut fixture = fixture("sha1");
let source = CobSource::new(&fixture);
let alice = account("alice");
source
.cobs(&fixture.signer)
.open_ticket("3lubrptx57d22", &alice, "one");
let first = source.settle(&mut fixture);
let ticket = MstKey::new("org.tangled.track.ticket/3lubrptx57d22".into()).unwrap();
let unpublished = MstKey::new("org.tangled.track.ticket/3lubrptx57d33".into()).unwrap();
fixture.source.cobs = Some(CobState {
records: BTreeMap::from([(ticket, None), (unpublished, None)]),
versions: BTreeMap::new(),
});
let settled = settle_fixture(&fixture);
assert_eq!(settled.commit, first.commit, "nothing was deleted or added");
assert_eq!(texts(&settled.car(), "org.tangled.track.ticket"), ["one"]);
assert_eq!(leaf_keys(&fixture).len(), 3);
}
#[test]
fn a_cobs_versions_list_every_change_and_survive_a_build_that_did_not_reopen_it() {
let mut fixture = fixture("sha1");
let source = CobSource::new(&fixture);
let alice = account("alice");
let cobs = source.cobs(&fixture.signer);
let (ticket, first) = cobs.open_ticket("3lubrptx57d22", &alice, "one");
let second = cobs.edit_ticket(ticket, &alice, first, "two", 20);
let comment = knot_cobs::Comment {
subject: RecordCid::new(knot_types::dag_cbor_cid(b"subject")).unwrap(),
reply: None,
};
let (erased, version) = cobs.open("3lubrptx57d33", &alice, 1, comment, "gone", 30);
cobs.erase::<knot_cobs::Comment>(erased, &alice, version, 40);
let settled = source.settle(&mut fixture);
let listed = |settled: &SettledRepo, key: &str| -> Vec<(Option<String>, String)> {
settled
.served_versions(key)
.iter()
.map(|v| {
(
v["cid"].as_str().map(String::from),
v["createdAt"].as_str().unwrap().into(),
)
})
.collect()
};
let ticket_key = "org.tangled.track.ticket/3lubrptx57d22";
let stamps = |at: &str| format!("1970-01-01T00:00:{at}.000000Z");
assert_eq!(
listed(&settled, ticket_key),
[
(Some(first.to_string()), stamps("10")),
(Some(second.to_string()), stamps("20"))
]
);
assert_eq!(
listed(&settled, "org.tangled.feed.comment/3lubrptx57d33"),
[
(Some(version.to_string()), stamps("30")),
(None, stamps("40"))
],
"an erased cob lists its erasure, which produces no version"
);
assert_eq!(
leaf_keys(&fixture).len(),
3,
"the erased cob is still not a record"
);
fixture.source.cobs = None;
let carried = settle_fixture(&fixture);
assert_eq!(
listed(&carried, ticket_key),
listed(&settled, ticket_key),
"a repo this build did not reopen keeps its versions"
);
let uris: std::collections::BTreeSet<&String> = carried
.rows
.version_uri
.iter()
.map(|(_, uri)| uri)
.collect();
assert_eq!(uris.len(), 2);
}
#[test]
fn every_record_in_the_tree_is_served_as_a_get_record_answer() {
let mut fixture = fixture("sha1");
let source = CobSource::new(&fixture);
source
.cobs(&fixture.signer)
.open_ticket("3lubrptx57d22", &account("alice"), "one");
let settled = source.settle(&mut fixture);
let generation = fixture._root.path().join("generation");
emit(&generation, std::slice::from_ref(&settled)).unwrap();
let dir = generation.join(ATPROTO_DIR).join(settled.dir_name());
let file_of = |collection: &str, rkey: &str| -> String {
let row = format!("{}|{collection}|{rkey}|", settled.dir_name());
let (_, file) = settled.rows.record.iter().find(|(r, _)| *r == row).unwrap();
file.clone()
};
let answer = |collection: &str, rkey: &str| -> serde_json::Value {
let file = dir.join(file_of(collection, rkey));
serde_json::from_slice(&std::fs::read(file).expect("a record file")).unwrap()
};
let ticket = answer("org.tangled.track.ticket", "3lubrptx57d22");
assert_eq!(
ticket["uri"],
format!(
"at://{}/org.tangled.track.ticket/3lubrptx57d22",
fixture.source.did
)
);
assert_eq!(ticket["value"]["$type"], "org.tangled.track.ticket");
assert!(ticket["cid"].is_string());
assert_eq!(
answer("org.tangled.repo.manifest", "self")["value"]["declaration"]["name"],
"demo"
);
let maps = std::fs::read_to_string(
generation
.join("nginx/maps")
.join(crate::xrpc::RECORDS_MAPS_FILE),
)
.unwrap();
let cid = ticket["cid"].as_str().unwrap();
let file = record_file("org.tangled.track.ticket", "3lubrptx57d22", cid);
assert_eq!(file, file_of("org.tangled.track.ticket", "3lubrptx57d22"));
for with_cid in ["", cid] {
assert!(maps.contains(&format!(
"{}|org.tangled.track.ticket|3lubrptx57d22|{with_cid} {file};",
settled.dir_name()
)));
}
}
#[test]
fn a_read_back_through_the_prior_seal_is_the_read_back_from_the_chain() {
let mut fixture = fixture("sha1");
let source = CobSource::new(&fixture);
let alice = account("alice");
let open = |fixture: &Fixture, from: usize, count: usize| {
let charset = b"234567abcdefghijklmnopqrstuvwxyz";
let pick = |at: usize| charset[at % charset.len()] as char;
let cobs = source.cobs(&fixture.signer);
for n in from..from + count {
let key = format!("3lubrptx5{}{}{}2", pick(n / 1024), pick(n / 32), pick(n));
cobs.open_ticket(&key, &alice, &format!("ticket {n}"));
}
from + count
};
let prior_of = |car: &[u8], tamper: bool| {
let mut blocks = knot_record::car_blocks(car).unwrap().blocks;
if tamper {
for block in blocks.values_mut() {
*block = bytes::Bytes::from_static(b"\xa1\x61x\x01");
}
}
Prior {
blocks: std::sync::Arc::new(blocks),
versions: BTreeMap::new(),
version_rows: Default::default(),
files: BTreeMap::new(),
}
};
let mut opened = open(&fixture, 40, 80);
let mut car = source.settle(&mut fixture).car();
for round in 0..3 {
opened = open(&fixture, opened, 5 + round * 20);
let settled = source.settle(&mut fixture);
let chain_repo = open_chain(&fixture.chain_root, &source.did).unwrap();
let read = |prior: Option<&Prior>| {
let state =
crate::atproto::chain::read_back(&chain_repo, &source.did, BTreeMap::new(), prior)
.unwrap();
let records: Vec<_> = state
.records
.iter()
.map(|record| record.answer.clone())
.collect();
(state.commit, state.collections, records, state.car)
};
let whole = read(None);
assert_eq!(
whole.3,
settled.car(),
"round {round}: the seal holds what the chain does"
);
let before = crate::atproto::chain::KNOWN_READS.get();
assert!(
read(Some(&prior_of(&car, false))) == whole,
"round {round}: through the prior"
);
let used = crate::atproto::chain::KNOWN_READS.get() - before;
assert!(used > 10, "round {round}: the prior served {used} blocks");
let before = crate::atproto::chain::KNOWN_READS.get();
assert!(
read(Some(&prior_of(&car, true))) == whole,
"round {round}: a tampered prior"
);
assert_eq!(
crate::atproto::chain::KNOWN_READS.get(),
before,
"round {round}: a tampered block is never used"
);
car = settled.car();
}
}