pub(super) use super::cob_state::{COB_KIND, encode_body};
pub(super) use knot_types::{RecordCid, UnixSeconds};
mod chain;
mod pool;
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use ipld_core::ipld::Ipld;
use jacquard_repo::commit::{Commit, UnsignedCommit};
use knot_git::Repo;
use knot_index::Index;
use knot_record::chain as record_chain;
use knot_record::manifest::{ManifestMeta, ManifestRecord};
use knot_runtime::Signer;
use knot_secrets::{MasterKey, SealedStore};
use knot_types::{AccountDid, KnotId, MstKey, OwnerDid, RecordBody, RepoDid, RepoName};
use crate::datastore::Datastore;
use crate::plan::repo_key;
use crate::store::RepoStore;
pub(crate) use super::chain::{block_on, open_chain};
pub(crate) use super::cob_state::StoredState;
pub(crate) use super::pool::SEAL_KIND;
pub(crate) use super::*;
pub(crate) fn git(dir: &Path, args: &[&str]) -> String {
let output = crate::test_git()
.current_dir(dir)
.args(["-c", "user.name=T", "-c", "user.email=t@example.test"])
.args(args)
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.output()
.expect("run git");
assert!(
output.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).unwrap().trim().to_string()
}
pub(crate) struct Fixture {
pub(crate) _root: tempfile::TempDir,
pub(crate) chain_root: PathBuf,
pub(crate) work: PathBuf,
pub(crate) signer: knot_runtime::K256Signer,
pub(crate) source: RepoSource,
}
pub(crate) fn fixture(object_format: &str) -> Fixture {
let root = tempfile::tempdir().unwrap();
let work = root.path().join("work");
std::fs::create_dir_all(&work).unwrap();
git(
&work,
&[
"init",
"-q",
"-b",
"main",
&format!("--object-format={object_format}"),
],
);
std::fs::write(work.join("a"), "one").unwrap();
git(&work, &["add", "-A"]);
git(&work, &["commit", "-q", "-m", "one"]);
git(&work, &["tag", "-a", "v1", "-m", "v1"]);
let export_dir = root.path().join("export");
git(
root.path(),
&[
"clone",
"-q",
"--bare",
work.to_str().unwrap(),
export_dir.to_str().unwrap(),
],
);
let knot = KnotId::new("did:web:knot.test").unwrap();
let secrets = SealedStore::open(
root.path().join("sealed.bin"),
&MasterKey::new(vec![7; 32]).unwrap(),
Box::new(knot_runtime::OsEntropy),
)
.unwrap();
secrets.ensure(&knot).unwrap();
Fixture {
chain_root: root.path().join("atproto"),
signer: secrets.signer(&knot).unwrap(),
source: RepoSource {
did: RepoDid::new("did:plc:abcdefghijklmnopqrstuvwx").unwrap(),
owner: OwnerDid::new("did:plc:owner").unwrap(),
name: RepoName::new("demo").unwrap(),
export_dir,
cobs: Some(CobState::default()),
served: RepoStore::served(&root.path().join("repo")),
verify: false,
},
work,
_root: root,
}
}
impl SettledRepo {
pub(crate) fn car(&self) -> Vec<u8> {
std::fs::read(self.pool.join(self.commit.to_string()).join("repo.car")).unwrap()
}
pub(crate) fn pooled(&self, file: &str) -> PathBuf {
let name = format!("{}/", self.dir_name());
self.pool.join(file.strip_prefix(&name).unwrap())
}
pub(crate) fn served_versions(&self, key: &str) -> Vec<serde_json::Value> {
let row = format!("at://{}/{key}|100a|0", self.did);
let (_, file) = self.rows.version.iter().find(|(r, _)| *r == row).unwrap();
let page: serde_json::Value =
serde_json::from_slice(&std::fs::read(self.pooled(file)).unwrap()).unwrap();
page["versions"].as_array().unwrap().clone()
}
}
pub(crate) fn pooled_files(settled: &SettledRepo, subdir: &str) -> usize {
std::fs::read_dir(settled.pool.join(subdir)).map_or(0, Iterator::count)
}
pub(crate) fn settle_fixture(fixture: &Fixture) -> SettledRepo {
settle(&fixture.chain_root, &fixture.signer, &fixture.source).unwrap()
}
pub(crate) fn record_of(car: &[u8], collection: &str) -> Option<Ipld> {
knot_record::car_blocks(car)
.unwrap()
.blocks
.values()
.filter_map(|block| serde_ipld_dagcbor::from_slice::<Ipld>(block).ok())
.find(|value| {
matches!(value, Ipld::Map(map)
if matches!(map.get("$type"), Some(Ipld::String(s)) if s == collection))
})
}
pub(crate) struct CobSource {
pub(crate) index: Index,
pub(crate) repo: Repo,
pub(crate) did: RepoDid,
}
impl CobSource {
pub(crate) fn new(fixture: &Fixture) -> Self {
let layout = knot_git::Layout::new(fixture._root.path().join("repos"));
let meta = fixture._root.path().join("meta");
Repo::create(&meta).unwrap();
let did = fixture.source.did.clone();
Self {
repo: layout.create(&did).unwrap(),
index: Index::new(meta, layout),
did,
}
}
pub(crate) fn cobs<'a>(&'a self, signer: &'a dyn Signer) -> crate::test_cobs::Cobs<'a> {
crate::test_cobs::Cobs {
repo: &self.repo,
did: &self.did,
signer,
}
}
pub(crate) fn settle(&self, fixture: &mut Fixture) -> SettledRepo {
let store = RepoStore::open(&fixture._root.path().join("repo-store"));
let lock = test_lock(fixture._root.path(), &self.did);
let records = Datastore::at(fixture._root.path());
fixture.source.cobs =
Some(cob_state(&self.index, &self.repo, &self.did, &store, &records, &lock).unwrap());
settle_fixture(fixture)
}
}
pub(crate) fn account(name: &str) -> AccountDid {
AccountDid::new(format!("did:plc:{name}")).unwrap()
}
pub(crate) fn test_lock(root: &Path, did: &RepoDid) -> crate::repolock::RepoLock {
crate::repolock::acquire(
root,
&repo_key(did.as_str()),
crate::repolock::LockMode::Lifetime,
)
.unwrap()
}
pub(crate) fn texts(car: &[u8], collection: &str) -> Vec<String> {
knot_record::car_blocks(car)
.unwrap()
.blocks
.values()
.filter_map(|block| serde_ipld_dagcbor::from_slice::<Ipld>(block).ok())
.filter_map(|value| match value {
Ipld::Map(map) if map.get("$type") == Some(&Ipld::String(collection.into())) => {
match map.get("text") {
Some(Ipld::String(text)) => Some(text.clone()),
_ => None,
}
}
_ => None,
})
.collect()
}
pub(crate) fn manifest_name(car: &[u8]) -> String {
let Some(Ipld::Map(manifest)) = record_of(car, "org.tangled.repo.manifest") else {
panic!("the car carries a manifest record");
};
let Some(Ipld::Map(declaration)) = manifest.get("declaration") else {
panic!("the manifest declares the repo");
};
match declaration.get("name") {
Some(Ipld::String(name)) => name.clone(),
other => panic!("declaration name is {other:?}"),
}
}
pub(crate) fn leaf_keys(fixture: &Fixture) -> Vec<String> {
let chain_repo = open_chain(&fixture.chain_root, &fixture.source.did).unwrap();
block_on(record_chain::leaves(&chain_repo))
.unwrap()
.keys()
.map(|key| key.as_str().to_string())
.collect()
}
pub(crate) fn state_json(state: &CobState) -> serde_json::Value {
serde_json::to_value(StoredState::of(state, BTreeSet::new())).unwrap()
}
pub(crate) fn body_text(body: &RecordBody) -> String {
let Ipld::Map(fields) = serde_ipld_dagcbor::from_slice(body.as_bytes()).unwrap() else {
panic!("a record is a map");
};
match fields.get("text") {
Some(Ipld::String(text)) => text.clone(),
other => panic!("text is {other:?}"),
}
}
pub(crate) fn assert_serves_like(generation: &Path, scratch: &Path) {
let served = served_tree(generation);
let scratch = served_tree(scratch);
assert!(scratch.len() > 10, "{} files", scratch.len());
for (path, bytes) in &scratch {
assert!(served.get(path) == Some(bytes), "{path} differs");
}
}
pub(crate) fn served_tree(generation: &Path) -> BTreeMap<String, Vec<u8>> {
fn visit(root: &Path, dir: &Path, into: &mut BTreeMap<String, Vec<u8>>) {
for entry in std::fs::read_dir(dir).unwrap() {
let path = entry.unwrap().path();
if path.is_dir() {
visit(root, &path, into);
} else {
let relative = path.strip_prefix(root).unwrap().to_string_lossy();
into.insert(relative.into_owned(), std::fs::read(&path).unwrap());
}
}
}
let mut tree = BTreeMap::new();
visit(generation, &generation.join(ATPROTO_DIR), &mut tree);
visit(generation, &generation.join("nginx/maps"), &mut tree);
tree
}
pub(crate) fn build(
fixture: &mut Fixture,
source: &CobSource,
repo: &str,
name: &str,
) -> (SettledRepo, PathBuf) {
let repo_dir = fixture._root.path().join(repo);
let store = RepoStore::open(&repo_dir);
let lock = test_lock(fixture._root.path(), &source.did);
let records = Datastore::at(fixture._root.path());
fixture.source.cobs = Some(
cob_state(
&source.index,
&source.repo,
&source.did,
&store,
&records,
&lock,
)
.unwrap(),
);
fixture.source.served = RepoStore::served(&repo_dir);
let settled = settle_fixture(fixture);
let generation = fixture._root.path().join(name);
emit(&generation, std::slice::from_ref(&settled)).unwrap();
(settled, generation)
}
pub(crate) fn a_pooled(settled: &SettledRepo, subdir: &str) -> PathBuf {
let mut files: Vec<PathBuf> = std::fs::read_dir(settled.pool.join(subdir))
.unwrap()
.map(|entry| entry.unwrap().path())
.collect();
files.sort();
files.remove(0)
}
pub(crate) fn cut_short(path: &Path, bytes: &[u8]) {
std::fs::remove_file(path).unwrap();
std::fs::write(path, bytes).unwrap();
crate::cas::seal_file(path).unwrap();
}