use fmt::{Component, RefString};
use radicle_git_ref_format::refname;
use crate::{ObjectId, TypeName, object, test::arbitrary::Invalid};
#[cfg(feature = "git2")]
mod git {
use std::ops::ControlFlow;
use crypto::test::signer::MockSigner;
use crypto::{PublicKey, Signer};
use nonempty::{NonEmpty, nonempty};
use qcheck::Arbitrary;
use crate::{
Create, Entry, ObjectId, TypeName, Update, Updated, Version, create, get, list, update,
};
use crate::test;
#[test]
fn roundtrip() {
let storage = test::Storage::new();
let signer = r#gen::<MockSigner>(1);
let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
let proj = test::RemoteProject {
project: proj,
person: terry,
};
let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
let cob = create::<NonEmpty<Entry>, _, _>(
&storage,
&signer,
Some(proj.project.content_id),
vec![],
signer.public_key(),
Create {
contents: nonempty!(Vec::new()),
type_name: typename.clone(),
message: "creating xyz.rad.issue".to_string(),
embeds: vec![],
version: Version::default(),
},
)
.unwrap();
let expected = get(&storage, &typename, cob.id())
.unwrap()
.expect("BUG: cob was missing");
assert_eq!(cob, expected);
}
#[test]
fn list_cobs() {
let storage = test::Storage::new();
let signer = r#gen::<MockSigner>(1);
let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
let proj = test::RemoteProject {
project: proj,
person: terry,
};
let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
let issue_1 = create::<NonEmpty<Entry>, _, _>(
&storage,
&signer,
Some(proj.project.content_id),
vec![],
signer.public_key(),
Create {
contents: nonempty!(b"issue 1".to_vec()),
type_name: typename.clone(),
message: "creating xyz.rad.issue".to_string(),
embeds: vec![],
version: Version::default(),
},
)
.unwrap();
let issue_2 = create(
&storage,
&signer,
Some(proj.project.content_id),
vec![],
signer.public_key(),
Create {
contents: nonempty!(b"issue 2".to_vec()),
type_name: typename.clone(),
message: "commenting xyz.rad.issue".to_string(),
embeds: vec![],
version: Version::default(),
},
)
.unwrap();
let mut expected = list(&storage, &typename).unwrap();
expected.sort_by(|x, y| x.id().cmp(y.id()));
let mut actual = vec![issue_1, issue_2];
actual.sort_by(|x, y| x.id().cmp(y.id()));
assert_eq!(actual, expected);
}
#[test]
fn update_cob() {
let storage = test::Storage::new();
let signer = r#gen::<MockSigner>(1);
let terry = test::Person::new(&storage, "terry", *signer.public_key()).unwrap();
let proj = test::Project::new(&storage, "discworld", *signer.public_key()).unwrap();
let proj = test::RemoteProject {
project: proj,
person: terry,
};
let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
let cob = create::<NonEmpty<Entry>, _, _>(
&storage,
&signer,
Some(proj.project.content_id),
vec![],
signer.public_key(),
Create {
contents: nonempty!(Vec::new()),
type_name: typename.clone(),
message: "creating xyz.rad.issue".to_string(),
embeds: vec![],
version: Version::default(),
},
)
.unwrap();
let not_expected = get::<NonEmpty<Entry>, _>(&storage, &typename, cob.id())
.unwrap()
.expect("BUG: cob was missing");
let Updated { object, .. } = update(
&storage,
&signer,
Some(proj.project.content_id),
vec![],
signer.public_key(),
Update {
changes: nonempty!(b"issue 1".to_vec()),
object_id: *cob.id(),
type_name: typename.clone(),
embeds: vec![],
message: "commenting xyz.rad.issue".to_string(),
},
)
.unwrap();
let expected = get(&storage, &typename, object.id())
.unwrap()
.expect("BUG: cob was missing");
assert_ne!(object, not_expected);
assert_eq!(object, expected, "{object:#?} {expected:#?}");
}
#[test]
fn traverse_cobs() {
let storage = test::Storage::new();
let neil_signer = r#gen::<MockSigner>(2);
let neil = test::Person::new(&storage, "gaiman", *neil_signer.public_key()).unwrap();
let terry_signer = r#gen::<MockSigner>(1);
let terry = test::Person::new(&storage, "pratchett", *terry_signer.public_key()).unwrap();
let proj = test::Project::new(&storage, "discworld", *terry_signer.public_key()).unwrap();
let terry_proj = test::RemoteProject {
project: proj.clone(),
person: terry,
};
let neil_proj = test::RemoteProject {
project: proj,
person: neil,
};
let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
let cob = create::<NonEmpty<Entry>, _, _>(
&storage,
&terry_signer,
Some(terry_proj.project.content_id),
vec![],
terry_signer.public_key(),
Create {
contents: nonempty!(b"issue 1".to_vec()),
type_name: typename.clone(),
message: "creating xyz.rad.issue".to_string(),
embeds: vec![],
version: Version::default(),
},
)
.unwrap();
copy_to(
storage.as_raw(),
terry_signer.public_key(),
&neil_proj,
&typename,
*cob.id(),
)
.unwrap();
let Updated { object, .. } = update::<NonEmpty<Entry>, _, _>(
&storage,
&neil_signer,
Some(neil_proj.project.content_id),
vec![],
neil_signer.public_key(),
Update {
changes: nonempty!(b"issue 2".to_vec()),
object_id: *cob.id(),
type_name: typename,
embeds: vec![],
message: "commenting on xyz.rad.issue".to_string(),
},
)
.unwrap();
let root = object.history.root().id;
// traverse over the history and filter by changes that were only authorized by terry
let contents = object
.history()
.traverse(Vec::new(), &[root], |mut acc, _, entry| {
if entry.author() == terry_signer.public_key() {
acc.push(entry.contents().head.clone());
}
ControlFlow::Continue(acc)
});
assert_eq!(contents, vec![b"issue 1".to_vec()]);
// traverse over the history and filter by changes that were only authorized by neil
let contents = object
.history()
.traverse(Vec::new(), &[root], |mut acc, _, entry| {
acc.push(entry.contents().head.clone());
ControlFlow::Continue(acc)
});
assert_eq!(contents, vec![b"issue 1".to_vec(), b"issue 2".to_vec()]);
}
fn copy_to(
repo: &git2::Repository,
from: &PublicKey,
to: &test::RemoteProject,
typename: &TypeName,
object: ObjectId,
) -> Result<(), git2::Error> {
let original = {
let name = format!("refs/rad/{from}/cobs/{typename}/{object}");
let r = repo.find_reference(&name)?;
r.target().unwrap()
};
let name = format!(
"refs/rad/{}/cobs/{}/{}",
to.identifier().to_path(),
typename,
object
);
repo.reference(&name, original, false, "copying object reference")?;
Ok(())
}
fn r#gen<T: Arbitrary>(size: usize) -> T {
let mut r#gen = qcheck::Gen::new(size);
T::arbitrary(&mut r#gen)
}
}
#[quickcheck]
fn parse_refstr(oid: ObjectId, typename: TypeName) {
let suffix = refname!("refs/cobs")
.and(Component::from(&typename))
.and(Component::from(&oid));
// refs/cobs/<typename>/<object_id> gives back the <typename> and <object_id>
assert_eq!(object::parse_refstr(&suffix), Some((typename.clone(), oid)));
// strips a single namespace
assert_eq!(
object::parse_refstr(&refname!("refs/namespaces/a").join(&suffix)),
Some((typename.clone(), oid))
);
// strips multiple namespaces
assert_eq!(
object::parse_refstr(&refname!("refs/namespaces/a/refs/namespaces/b").join(&suffix)),
Some((typename.clone(), oid))
);
// ignores the extra path
assert_eq!(
object::parse_refstr(
&refname!("refs/namespaces/a/refs/namespaces/b")
.join(suffix)
.and(refname!("more/paths"))
),
Some((typename, oid))
);
}
/// Note: an invalid type name is also an invalid reference string, it
/// cannot start or end with a '.', and cannot have '..'.
#[quickcheck]
fn invalid_parse_refstr(oid: Invalid<ObjectId>, typename: TypeName) {
let oid = RefString::try_from(oid.value).unwrap();
let typename = Component::from(&typename);
let suffix = refname!("refs/cobs").and(typename).and(oid);
// All parsing will fail because `oid` is not a valid ObjectId
assert_eq!(object::parse_refstr(&suffix), None);
assert_eq!(
object::parse_refstr(&refname!("refs/namespaces/a").join(&suffix)),
None
);
assert_eq!(
object::parse_refstr(&refname!("refs/namespaces/a/refs/namespaces/b").join(&suffix)),
None
);
assert_eq!(
object::parse_refstr(
&refname!("refs/namespaces/a/refs/namespaces/b")
.join(suffix)
.and(refname!("more/paths"))
),
None
);
}