radicle/canonical/rules: Reorganise test suite
Reorganises the rules test suite into a `test` module and a `helper` sub-module. The former holds the original tests, removing the `test_` prefix. The latter holds helper functions for test setup.
This commit is contained in:
parent
998f527bd4
commit
455138f4a7
|
|
@ -9,6 +9,9 @@
|
||||||
//! the first matched rule, and this can be used to calculate the
|
//! the first matched rule, and this can be used to calculate the
|
||||||
//! [`Canonical::quorum`].
|
//! [`Canonical::quorum`].
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test;
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
|
@ -624,509 +627,3 @@ pub enum CanonicalError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Git(#[from] crate::git::raw::Error),
|
Git(#[from] crate::git::raw::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[allow(clippy::unwrap_used)]
|
|
||||||
mod tests {
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use nonempty::nonempty;
|
|
||||||
|
|
||||||
use crate::Storage;
|
|
||||||
use crate::crypto::{Signer, test::signer::MockSigner};
|
|
||||||
use crate::git;
|
|
||||||
use crate::git::fmt::RefString;
|
|
||||||
use crate::git::fmt::qualified_pattern;
|
|
||||||
use crate::identity::Visibility;
|
|
||||||
use crate::identity::doc::Doc;
|
|
||||||
use crate::node::device::Device;
|
|
||||||
use crate::rad;
|
|
||||||
use crate::storage::refs::{IDENTITY_BRANCH, IDENTITY_ROOT, SIGREFS_BRANCH, SIGREFS_PARENT};
|
|
||||||
use crate::storage::{ReadStorage, git::transport};
|
|
||||||
use crate::test::{arbitrary, fixtures};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn roundtrip(rule: &Rule<Allowed, usize>) {
|
|
||||||
let json = serde_json::to_string(rule).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
*rule,
|
|
||||||
serde_json::from_str(&json).unwrap(),
|
|
||||||
"failed to roundtrip: {json}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn did(s: &str) -> Did {
|
|
||||||
s.parse().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn raw_pattern(qp: QualifiedPattern<'static>) -> RawPattern {
|
|
||||||
qp
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pattern(qp: QualifiedPattern<'static>) -> Pattern {
|
|
||||||
Pattern::new(qp).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_from_doc(doc: &Doc) -> doc::Delegates {
|
|
||||||
doc.delegates().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tag(name: RefString, head: git::raw::Oid, repo: &git::raw::Repository) -> git::Oid {
|
|
||||||
let commit = fixtures::commit(name.as_str(), &[head], repo);
|
|
||||||
let target = repo.find_object(commit.into(), None).unwrap();
|
|
||||||
let tagger = repo.signature().unwrap();
|
|
||||||
repo.tag(name.as_str(), &target, &tagger, name.as_str(), false)
|
|
||||||
.unwrap()
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_roundtrip() {
|
|
||||||
let rule1 = Rule::new(Allowed::Delegates, 1);
|
|
||||||
let rule2 = Rule::new(Allowed::Delegates, 1);
|
|
||||||
let rule3 = Rule::new(Allowed::Delegates, 1);
|
|
||||||
let mut rule4 = Rule::new(
|
|
||||||
Allowed::Set(nonempty![
|
|
||||||
did("did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K"),
|
|
||||||
did("did:key:z6MknG1nYDftMYUQ7eTBSGgqB2PL1xK5Pif33J3sRym3e8ye"),
|
|
||||||
]),
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
rule4.add_extensions(
|
|
||||||
serde_json::json!({
|
|
||||||
"foo": "bar",
|
|
||||||
"quux": 5,
|
|
||||||
})
|
|
||||||
.as_object()
|
|
||||||
.cloned()
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
roundtrip(&rule1);
|
|
||||||
roundtrip(&rule2);
|
|
||||||
roundtrip(&rule3);
|
|
||||||
roundtrip(&rule4);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_deserialization() {
|
|
||||||
let examples = r#"
|
|
||||||
{
|
|
||||||
"refs/heads/main": {
|
|
||||||
"threshold": 2,
|
|
||||||
"allow": [
|
|
||||||
"did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K",
|
|
||||||
"did:key:z6MknG1nYDftMYUQ7eTBSGgqB2PL1xK5Pif33J3sRym3e8ye"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"refs/tags/releases/*": {
|
|
||||||
"threshold": 2,
|
|
||||||
"allow": [
|
|
||||||
"did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56",
|
|
||||||
"did:key:z6Mkq2E5Se5H9gk1DsL1EMwR2t4CqSg3GFkNN2UeG4FNqXoP",
|
|
||||||
"did:key:z6MkqRmXW5fbP9hJ1Y8j2N4CgVdJ2XJ6TsyXYf3FQ2NJgXax"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"refs/heads/development": {
|
|
||||||
"threshold": 1,
|
|
||||||
"allow": [
|
|
||||||
"did:key:z6MkhH7ENYE62JAjTiRZPU71MGZ6xCwnbyHHWfrBu3fr6PVG"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"refs/heads/release/*": {
|
|
||||||
"threshold": 1,
|
|
||||||
"allow": "delegates"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#;
|
|
||||||
let expected = [
|
|
||||||
(
|
|
||||||
raw_pattern(qualified_pattern!("refs/heads/main")),
|
|
||||||
Rule::new(
|
|
||||||
Allowed::Set(nonempty![
|
|
||||||
did("did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K"),
|
|
||||||
did("did:key:z6MknG1nYDftMYUQ7eTBSGgqB2PL1xK5Pif33J3sRym3e8ye"),
|
|
||||||
]),
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
raw_pattern(qualified_pattern!("refs/tags/releases/*")),
|
|
||||||
Rule::new(
|
|
||||||
Allowed::Set(nonempty![
|
|
||||||
did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"),
|
|
||||||
did("did:key:z6Mkq2E5Se5H9gk1DsL1EMwR2t4CqSg3GFkNN2UeG4FNqXoP"),
|
|
||||||
did("did:key:z6MkqRmXW5fbP9hJ1Y8j2N4CgVdJ2XJ6TsyXYf3FQ2NJgXax")
|
|
||||||
]),
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
raw_pattern(qualified_pattern!("refs/heads/development")),
|
|
||||||
Rule::new(
|
|
||||||
Allowed::Set(nonempty![did(
|
|
||||||
"did:key:z6MkhH7ENYE62JAjTiRZPU71MGZ6xCwnbyHHWfrBu3fr6PVG"
|
|
||||||
)]),
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
raw_pattern(qualified_pattern!("refs/heads/release/*")),
|
|
||||||
Rule::new(Allowed::Delegates, 1),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.collect::<RawRules>();
|
|
||||||
let rules = serde_json::from_str::<BTreeMap<RawPattern, RawRule>>(examples)
|
|
||||||
.unwrap()
|
|
||||||
.into();
|
|
||||||
assert_eq!(expected, rules)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_order() {
|
|
||||||
assert!(
|
|
||||||
pattern(qualified_pattern!("refs/heads/a/b/c/d/*"))
|
|
||||||
< pattern(qualified_pattern!("refs/heads/*/x")),
|
|
||||||
"example 1"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
pattern(qualified_pattern!("refs/heads/a"))
|
|
||||||
< pattern(qualified_pattern!("refs/heads/*")),
|
|
||||||
"example 2.a"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
pattern(qualified_pattern!("refs/heads/abc"))
|
|
||||||
< pattern(qualified_pattern!("refs/heads/a*")),
|
|
||||||
"example 2.a"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
pattern(qualified_pattern!("refs/heads/a/b/*"))
|
|
||||||
< pattern(qualified_pattern!("refs/heads/a/*/c")),
|
|
||||||
"example 2.a"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
pattern(qualified_pattern!("refs/heads/aa*"))
|
|
||||||
< pattern(qualified_pattern!("refs/heads/a*")),
|
|
||||||
"example 2.b.A"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
pattern(qualified_pattern!("refs/heads/a*b"))
|
|
||||||
< pattern(qualified_pattern!("refs/heads/a*")),
|
|
||||||
"example 2.b.B"
|
|
||||||
);
|
|
||||||
|
|
||||||
let pattern01 = pattern(qualified_pattern!("refs/tags/*"));
|
|
||||||
let pattern02 = pattern(qualified_pattern!("refs/tags/v1"));
|
|
||||||
let pattern04 = pattern(qualified_pattern!("refs/tags/v1.0.0"));
|
|
||||||
let pattern05 = pattern(qualified_pattern!("refs/tags/release/v1.0.0"));
|
|
||||||
let pattern03 = pattern(qualified_pattern!("refs/heads/main"));
|
|
||||||
let pattern06 = pattern(qualified_pattern!("refs/tags/*/v1.0.0"));
|
|
||||||
|
|
||||||
let pattern07 = pattern(qualified_pattern!("refs/tags/x*"));
|
|
||||||
let pattern08 = pattern(qualified_pattern!("refs/tags/xx*"));
|
|
||||||
|
|
||||||
let pattern09 = pattern(qualified_pattern!("refs/foos/*"));
|
|
||||||
|
|
||||||
let pattern10 = pattern(qualified_pattern!("refs/heads/a"));
|
|
||||||
let pattern11 = pattern(qualified_pattern!("refs/heads/b"));
|
|
||||||
|
|
||||||
let pattern12 = pattern(qualified_pattern!("refs/heads/a/*"));
|
|
||||||
let pattern13 = pattern(qualified_pattern!("refs/heads/b/*"));
|
|
||||||
|
|
||||||
let pattern14 = pattern(qualified_pattern!("refs/heads/a/*/ab"));
|
|
||||||
let pattern15 = pattern(qualified_pattern!("refs/heads/a/*/a"));
|
|
||||||
|
|
||||||
let pattern16 = pattern(qualified_pattern!("refs/heads/a/*/b"));
|
|
||||||
let pattern17 = pattern(qualified_pattern!("refs/heads/a/*/a"));
|
|
||||||
|
|
||||||
// Test priority for path specificity
|
|
||||||
assert!(
|
|
||||||
pattern06 < pattern02,
|
|
||||||
"match for 06 is always more specific since it has more components"
|
|
||||||
);
|
|
||||||
assert!(pattern02 < pattern01, "match for 02 is also match for 01");
|
|
||||||
assert!(pattern08 < pattern07, "match for 08 is also match for 07");
|
|
||||||
// Test equality
|
|
||||||
assert!(pattern02 == pattern02);
|
|
||||||
// Test lexicographical fallback when paths are equally specific
|
|
||||||
assert!(pattern02 < pattern04);
|
|
||||||
assert!(pattern03 < pattern01);
|
|
||||||
assert!(pattern09 < pattern01);
|
|
||||||
assert!(pattern10 < pattern11);
|
|
||||||
assert!(pattern12 < pattern13);
|
|
||||||
assert!(pattern15 < pattern14);
|
|
||||||
assert!(
|
|
||||||
pattern17 < pattern16,
|
|
||||||
"matches have same length, but lexicographically, 'a' < 'b'"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test example from docs
|
|
||||||
let pattern18 = pattern(qualified_pattern!("refs/tags/release/candidates/*"));
|
|
||||||
let pattern19 = pattern(qualified_pattern!("refs/tags/release/*"));
|
|
||||||
let pattern20 = pattern(qualified_pattern!("refs/tags/*"));
|
|
||||||
|
|
||||||
assert!(pattern18 < pattern19);
|
|
||||||
assert!(pattern19 < pattern20);
|
|
||||||
|
|
||||||
let pattern21 = pattern(qualified_pattern!("refs/heads/dev"));
|
|
||||||
|
|
||||||
assert!(pattern21 < pattern03);
|
|
||||||
|
|
||||||
let mut patterns = [
|
|
||||||
pattern01.clone(),
|
|
||||||
pattern02.clone(),
|
|
||||||
pattern03.clone(),
|
|
||||||
pattern04.clone(),
|
|
||||||
pattern05.clone(),
|
|
||||||
pattern06.clone(),
|
|
||||||
];
|
|
||||||
patterns.sort();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
patterns,
|
|
||||||
[
|
|
||||||
pattern05, pattern06, pattern03, pattern02, pattern04, pattern01
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_deserialize_extensions() {
|
|
||||||
let example = r#"
|
|
||||||
{
|
|
||||||
"threshold": 2,
|
|
||||||
"allow": [
|
|
||||||
"did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K",
|
|
||||||
"did:key:z6MknG1nYDftMYUQ7eTBSGgqB2PL1xK5Pif33J3sRym3e8ye"
|
|
||||||
],
|
|
||||||
"foo": "bar",
|
|
||||||
"quux": 5
|
|
||||||
}
|
|
||||||
"#;
|
|
||||||
let rule = serde_json::from_str::<Rule<Allowed, usize>>(example).unwrap();
|
|
||||||
assert!(!rule.extensions().is_empty());
|
|
||||||
let extensions = rule.extensions();
|
|
||||||
assert_eq!(
|
|
||||||
extensions.get("foo"),
|
|
||||||
Some(serde_json::Value::String("bar".to_string())).as_ref()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
extensions.get("quux"),
|
|
||||||
Some(serde_json::Value::Number(5.into())).as_ref()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_rule_validate_success() {
|
|
||||||
let doc = arbitrary::r#gen::<Doc>(1);
|
|
||||||
let delegates = Allowed::Set(doc.delegates().as_ref().clone());
|
|
||||||
let threshold = doc.majority();
|
|
||||||
|
|
||||||
let rule = Rule::new(delegates, threshold);
|
|
||||||
let result = rule.validate(&mut || resolve_from_doc(&doc));
|
|
||||||
assert!(result.is_ok(), "failed to validate doc: {result:?}");
|
|
||||||
|
|
||||||
let rule = Rule::new(Allowed::Delegates, 1);
|
|
||||||
let result = rule.validate(&mut || resolve_from_doc(&doc));
|
|
||||||
assert!(result.is_ok(), "failed to validate doc: {result:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_rule_validate_failures() {
|
|
||||||
let doc = arbitrary::r#gen::<Doc>(1);
|
|
||||||
let pattern = pattern(qualified_pattern!("refs/heads/main"));
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
Rule::new(Allowed::Delegates, 256).validate(&mut || resolve_from_doc(&doc)),
|
|
||||||
Err(ValidationError::Threshold(_))
|
|
||||||
));
|
|
||||||
|
|
||||||
let threshold = doc.delegates().len().saturating_add(1);
|
|
||||||
assert!(matches!(
|
|
||||||
Rule::new(Allowed::Delegates, threshold).validate(&mut || resolve_from_doc(&doc)),
|
|
||||||
Err(ValidationError::Threshold(_))
|
|
||||||
));
|
|
||||||
|
|
||||||
let delegates = NonEmpty::from_vec(arbitrary::vec::<Did>(256)).unwrap();
|
|
||||||
assert!(matches!(
|
|
||||||
Rule::new(delegates.into(), 1).validate(&mut || resolve_from_doc(&doc)),
|
|
||||||
Err(ValidationError::Delegates(_))
|
|
||||||
));
|
|
||||||
|
|
||||||
let delegates = nonempty![
|
|
||||||
did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"),
|
|
||||||
did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56")
|
|
||||||
];
|
|
||||||
let expected = Rule {
|
|
||||||
allow: ResolvedDelegates::Set(
|
|
||||||
doc::Delegates::new(nonempty![did(
|
|
||||||
"did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"
|
|
||||||
)])
|
|
||||||
.unwrap(),
|
|
||||||
),
|
|
||||||
threshold: doc::Threshold::MIN,
|
|
||||||
extensions: json::Map::new(),
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
Rule::new(delegates.into(), 1)
|
|
||||||
.validate(&mut || resolve_from_doc(&doc))
|
|
||||||
.unwrap(),
|
|
||||||
expected,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Duplicate rules are overwritten
|
|
||||||
let rules = vec![
|
|
||||||
(
|
|
||||||
pattern.clone().into_inner(),
|
|
||||||
Rule::new(Allowed::Delegates, 1),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
pattern.clone().into_inner(),
|
|
||||||
Rule::new(doc.delegates().as_ref().clone().into(), 1),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
let expected = [(
|
|
||||||
pattern,
|
|
||||||
Rule::new(
|
|
||||||
ResolvedDelegates::Set(doc.delegates().clone()),
|
|
||||||
doc::Threshold::MIN,
|
|
||||||
),
|
|
||||||
)]
|
|
||||||
.into_iter()
|
|
||||||
.collect::<Rules>();
|
|
||||||
assert_eq!(
|
|
||||||
Rules::from_raw(rules, &mut || resolve_from_doc(&doc)).unwrap(),
|
|
||||||
expected
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_canonical() {
|
|
||||||
let tempdir = tempfile::tempdir().unwrap();
|
|
||||||
let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
|
|
||||||
|
|
||||||
transport::local::register(storage.clone());
|
|
||||||
|
|
||||||
let delegate = Device::mock_from_seed([0xff; 32]);
|
|
||||||
let contributor = MockSigner::from_seed([0xfe; 32]);
|
|
||||||
let (repo, head) = fixtures::repository(tempdir.path().join("working"));
|
|
||||||
let (rid, doc, _) = rad::init(
|
|
||||||
&repo,
|
|
||||||
"heartwood".try_into().unwrap(),
|
|
||||||
"Radicle Heartwood Protocol & Stack",
|
|
||||||
git::fmt::refname!("master"),
|
|
||||||
Visibility::default(),
|
|
||||||
&delegate,
|
|
||||||
&storage,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut doc = doc.edit();
|
|
||||||
// Ensure there is a second delegate for testing overlapping rules
|
|
||||||
doc.delegate(contributor.public_key().into());
|
|
||||||
|
|
||||||
// Create tags and keep track of their OIDs
|
|
||||||
//
|
|
||||||
// follows the `refs/tags/release/candidates/*` rule
|
|
||||||
let failing_tag = git::fmt::refname!("release/candidates/v1.0");
|
|
||||||
let tags = [
|
|
||||||
// follows the `refs/tags/*` rule
|
|
||||||
git::fmt::refname!("v1.0"),
|
|
||||||
// follows the `refs/tags/release/*` rule
|
|
||||||
git::fmt::refname!("release/v1.0"),
|
|
||||||
failing_tag.clone(),
|
|
||||||
// follows the `refs/tags/*` rule
|
|
||||||
git::fmt::refname!("qa/v1.0"),
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.map(|name| {
|
|
||||||
(
|
|
||||||
git::fmt::lit::refs_tags(name.clone()).into(),
|
|
||||||
tag(name, head, &repo),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<BTreeMap<Qualified, _>>();
|
|
||||||
|
|
||||||
git::push(
|
|
||||||
&repo,
|
|
||||||
&rad::REMOTE_NAME,
|
|
||||||
[
|
|
||||||
(
|
|
||||||
&git::fmt::qualified!("refs/tags/v1.0"),
|
|
||||||
&git::fmt::qualified!("refs/tags/v1.0"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
&git::fmt::qualified!("refs/tags/release/v1.0"),
|
|
||||||
&git::fmt::qualified!("refs/tags/release/v1.0"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
&git::fmt::qualified!("refs/tags/release/candidates/v1.0"),
|
|
||||||
&git::fmt::qualified!("refs/tags/release/candidates/v1.0"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
&git::fmt::qualified!("refs/tags/qa/v1.0"),
|
|
||||||
&git::fmt::qualified!("refs/tags/qa/v1.0"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let rules = Rules::from_raw(
|
|
||||||
[
|
|
||||||
(
|
|
||||||
raw_pattern(qualified_pattern!("refs/tags/*")),
|
|
||||||
Rule::new(Allowed::Delegates, 1),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
raw_pattern(qualified_pattern!("refs/tags/release/*")),
|
|
||||||
Rule::new(Allowed::Delegates, 1),
|
|
||||||
),
|
|
||||||
// Ensure that none of the other rules apply by ensuring we need
|
|
||||||
// both delegates to get the quorum of the
|
|
||||||
// `refs/tags/release/candidates/v1.0` reference
|
|
||||||
(
|
|
||||||
raw_pattern(qualified_pattern!("refs/tags/release/candidates/*")),
|
|
||||||
Rule::new(Allowed::Delegates, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
&mut || resolve_from_doc(&doc.clone().verified().unwrap()),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// All tags should succeed at getting their canonical commit other than the
|
|
||||||
// candidates tag.
|
|
||||||
let stored = storage.repository(rid).unwrap();
|
|
||||||
let failing = git::fmt::Qualified::from(git::fmt::lit::refs_tags(failing_tag));
|
|
||||||
for (refname, oid) in tags.into_iter() {
|
|
||||||
let canonical = rules
|
|
||||||
.canonical(refname.clone(), &stored)
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
panic!("there should be a matching rule for {refname}, rules: {rules:#?}")
|
|
||||||
});
|
|
||||||
if refname == failing {
|
|
||||||
assert!(canonical.find_objects().unwrap().quorum().is_err());
|
|
||||||
} else {
|
|
||||||
assert_eq!(
|
|
||||||
canonical
|
|
||||||
.find_objects()
|
|
||||||
.unwrap()
|
|
||||||
.quorum()
|
|
||||||
.unwrap_or_else(|e| panic!("quorum error for {refname}: {e}")),
|
|
||||||
canonical::Quorum {
|
|
||||||
refname,
|
|
||||||
object: canonical::Object::Tag { id: oid },
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_special_branches() {
|
|
||||||
assert!(Pattern::new((*IDENTITY_BRANCH).clone().into()).is_err());
|
|
||||||
assert!(Pattern::new((*SIGREFS_BRANCH).clone().into()).is_err());
|
|
||||||
assert!(Pattern::new((*SIGREFS_PARENT).clone().into()).is_err());
|
|
||||||
assert!(Pattern::new((*IDENTITY_ROOT).clone().into()).is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,479 @@
|
||||||
|
mod helper;
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use nonempty::nonempty;
|
||||||
|
|
||||||
|
use crate::Storage;
|
||||||
|
use crate::crypto::{Signer, test::signer::MockSigner};
|
||||||
|
use crate::git;
|
||||||
|
use crate::git::fmt::qualified_pattern;
|
||||||
|
use crate::identity::Visibility;
|
||||||
|
use crate::identity::doc::Doc;
|
||||||
|
use crate::node::device::Device;
|
||||||
|
use crate::rad;
|
||||||
|
use crate::storage::refs::{IDENTITY_BRANCH, IDENTITY_ROOT, SIGREFS_BRANCH, SIGREFS_PARENT};
|
||||||
|
use crate::storage::{ReadStorage, git::transport};
|
||||||
|
use crate::test::{arbitrary, fixtures};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip() {
|
||||||
|
use helper::*;
|
||||||
|
|
||||||
|
let rule1 = Rule::new(Allowed::Delegates, 1);
|
||||||
|
let rule2 = Rule::new(Allowed::Delegates, 1);
|
||||||
|
let rule3 = Rule::new(Allowed::Delegates, 1);
|
||||||
|
let mut rule4 = Rule::new(
|
||||||
|
Allowed::Set(nonempty![
|
||||||
|
did("did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K"),
|
||||||
|
did("did:key:z6MknG1nYDftMYUQ7eTBSGgqB2PL1xK5Pif33J3sRym3e8ye"),
|
||||||
|
]),
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
rule4.add_extensions(
|
||||||
|
serde_json::json!({
|
||||||
|
"foo": "bar",
|
||||||
|
"quux": 5,
|
||||||
|
})
|
||||||
|
.as_object()
|
||||||
|
.cloned()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
roundtrip(&rule1);
|
||||||
|
roundtrip(&rule2);
|
||||||
|
roundtrip(&rule3);
|
||||||
|
roundtrip(&rule4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialization() {
|
||||||
|
use helper::*;
|
||||||
|
|
||||||
|
let examples = r#"
|
||||||
|
{
|
||||||
|
"refs/heads/main": {
|
||||||
|
"threshold": 2,
|
||||||
|
"allow": [
|
||||||
|
"did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K",
|
||||||
|
"did:key:z6MknG1nYDftMYUQ7eTBSGgqB2PL1xK5Pif33J3sRym3e8ye"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"refs/tags/releases/*": {
|
||||||
|
"threshold": 2,
|
||||||
|
"allow": [
|
||||||
|
"did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56",
|
||||||
|
"did:key:z6Mkq2E5Se5H9gk1DsL1EMwR2t4CqSg3GFkNN2UeG4FNqXoP",
|
||||||
|
"did:key:z6MkqRmXW5fbP9hJ1Y8j2N4CgVdJ2XJ6TsyXYf3FQ2NJgXax"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"refs/heads/development": {
|
||||||
|
"threshold": 1,
|
||||||
|
"allow": [
|
||||||
|
"did:key:z6MkhH7ENYE62JAjTiRZPU71MGZ6xCwnbyHHWfrBu3fr6PVG"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"refs/heads/release/*": {
|
||||||
|
"threshold": 1,
|
||||||
|
"allow": "delegates"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let expected = [
|
||||||
|
(
|
||||||
|
raw_pattern(qualified_pattern!("refs/heads/main")),
|
||||||
|
Rule::new(
|
||||||
|
Allowed::Set(nonempty![
|
||||||
|
did("did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K"),
|
||||||
|
did("did:key:z6MknG1nYDftMYUQ7eTBSGgqB2PL1xK5Pif33J3sRym3e8ye"),
|
||||||
|
]),
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
raw_pattern(qualified_pattern!("refs/tags/releases/*")),
|
||||||
|
Rule::new(
|
||||||
|
Allowed::Set(nonempty![
|
||||||
|
did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"),
|
||||||
|
did("did:key:z6Mkq2E5Se5H9gk1DsL1EMwR2t4CqSg3GFkNN2UeG4FNqXoP"),
|
||||||
|
did("did:key:z6MkqRmXW5fbP9hJ1Y8j2N4CgVdJ2XJ6TsyXYf3FQ2NJgXax")
|
||||||
|
]),
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
raw_pattern(qualified_pattern!("refs/heads/development")),
|
||||||
|
Rule::new(
|
||||||
|
Allowed::Set(nonempty![did(
|
||||||
|
"did:key:z6MkhH7ENYE62JAjTiRZPU71MGZ6xCwnbyHHWfrBu3fr6PVG"
|
||||||
|
)]),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
raw_pattern(qualified_pattern!("refs/heads/release/*")),
|
||||||
|
Rule::new(Allowed::Delegates, 1),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect::<RawRules>();
|
||||||
|
let rules = serde_json::from_str::<BTreeMap<RawPattern, RawRule>>(examples)
|
||||||
|
.unwrap()
|
||||||
|
.into();
|
||||||
|
assert_eq!(expected, rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ordering() {
|
||||||
|
use helper::*;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
pattern(qualified_pattern!("refs/heads/a/b/c/d/*"))
|
||||||
|
< pattern(qualified_pattern!("refs/heads/*/x")),
|
||||||
|
"example 1"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
pattern(qualified_pattern!("refs/heads/a")) < pattern(qualified_pattern!("refs/heads/*")),
|
||||||
|
"example 2.a"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
pattern(qualified_pattern!("refs/heads/abc"))
|
||||||
|
< pattern(qualified_pattern!("refs/heads/a*")),
|
||||||
|
"example 2.a"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
pattern(qualified_pattern!("refs/heads/a/b/*"))
|
||||||
|
< pattern(qualified_pattern!("refs/heads/a/*/c")),
|
||||||
|
"example 2.a"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
pattern(qualified_pattern!("refs/heads/aa*"))
|
||||||
|
< pattern(qualified_pattern!("refs/heads/a*")),
|
||||||
|
"example 2.b.A"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
pattern(qualified_pattern!("refs/heads/a*b"))
|
||||||
|
< pattern(qualified_pattern!("refs/heads/a*")),
|
||||||
|
"example 2.b.B"
|
||||||
|
);
|
||||||
|
|
||||||
|
let pattern01 = pattern(qualified_pattern!("refs/tags/*"));
|
||||||
|
let pattern02 = pattern(qualified_pattern!("refs/tags/v1"));
|
||||||
|
let pattern04 = pattern(qualified_pattern!("refs/tags/v1.0.0"));
|
||||||
|
let pattern05 = pattern(qualified_pattern!("refs/tags/release/v1.0.0"));
|
||||||
|
let pattern03 = pattern(qualified_pattern!("refs/heads/main"));
|
||||||
|
let pattern06 = pattern(qualified_pattern!("refs/tags/*/v1.0.0"));
|
||||||
|
|
||||||
|
let pattern07 = pattern(qualified_pattern!("refs/tags/x*"));
|
||||||
|
let pattern08 = pattern(qualified_pattern!("refs/tags/xx*"));
|
||||||
|
|
||||||
|
let pattern09 = pattern(qualified_pattern!("refs/foos/*"));
|
||||||
|
|
||||||
|
let pattern10 = pattern(qualified_pattern!("refs/heads/a"));
|
||||||
|
let pattern11 = pattern(qualified_pattern!("refs/heads/b"));
|
||||||
|
|
||||||
|
let pattern12 = pattern(qualified_pattern!("refs/heads/a/*"));
|
||||||
|
let pattern13 = pattern(qualified_pattern!("refs/heads/b/*"));
|
||||||
|
|
||||||
|
let pattern14 = pattern(qualified_pattern!("refs/heads/a/*/ab"));
|
||||||
|
let pattern15 = pattern(qualified_pattern!("refs/heads/a/*/a"));
|
||||||
|
|
||||||
|
let pattern16 = pattern(qualified_pattern!("refs/heads/a/*/b"));
|
||||||
|
let pattern17 = pattern(qualified_pattern!("refs/heads/a/*/a"));
|
||||||
|
|
||||||
|
// Test priority for path specificity
|
||||||
|
assert!(
|
||||||
|
pattern06 < pattern02,
|
||||||
|
"match for 06 is always more specific since it has more components"
|
||||||
|
);
|
||||||
|
assert!(pattern02 < pattern01, "match for 02 is also match for 01");
|
||||||
|
assert!(pattern08 < pattern07, "match for 08 is also match for 07");
|
||||||
|
// Test equality
|
||||||
|
assert!(pattern02 == pattern02);
|
||||||
|
// Test lexicographical fallback when paths are equally specific
|
||||||
|
assert!(pattern02 < pattern04);
|
||||||
|
assert!(pattern03 < pattern01);
|
||||||
|
assert!(pattern09 < pattern01);
|
||||||
|
assert!(pattern10 < pattern11);
|
||||||
|
assert!(pattern12 < pattern13);
|
||||||
|
assert!(pattern15 < pattern14);
|
||||||
|
assert!(
|
||||||
|
pattern17 < pattern16,
|
||||||
|
"matches have same length, but lexicographically, 'a' < 'b'"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test example from docs
|
||||||
|
let pattern18 = pattern(qualified_pattern!("refs/tags/release/candidates/*"));
|
||||||
|
let pattern19 = pattern(qualified_pattern!("refs/tags/release/*"));
|
||||||
|
let pattern20 = pattern(qualified_pattern!("refs/tags/*"));
|
||||||
|
|
||||||
|
assert!(pattern18 < pattern19);
|
||||||
|
assert!(pattern19 < pattern20);
|
||||||
|
|
||||||
|
let pattern21 = pattern(qualified_pattern!("refs/heads/dev"));
|
||||||
|
|
||||||
|
assert!(pattern21 < pattern03);
|
||||||
|
|
||||||
|
let mut patterns = [
|
||||||
|
pattern01.clone(),
|
||||||
|
pattern02.clone(),
|
||||||
|
pattern03.clone(),
|
||||||
|
pattern04.clone(),
|
||||||
|
pattern05.clone(),
|
||||||
|
pattern06.clone(),
|
||||||
|
];
|
||||||
|
patterns.sort();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
patterns,
|
||||||
|
[
|
||||||
|
pattern05, pattern06, pattern03, pattern02, pattern04, pattern01
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_extensions() {
|
||||||
|
let example = r#"
|
||||||
|
{
|
||||||
|
"threshold": 2,
|
||||||
|
"allow": [
|
||||||
|
"did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K",
|
||||||
|
"did:key:z6MknG1nYDftMYUQ7eTBSGgqB2PL1xK5Pif33J3sRym3e8ye"
|
||||||
|
],
|
||||||
|
"foo": "bar",
|
||||||
|
"quux": 5
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let rule = serde_json::from_str::<Rule<Allowed, usize>>(example).unwrap();
|
||||||
|
assert!(!rule.extensions().is_empty());
|
||||||
|
let extensions = rule.extensions();
|
||||||
|
assert_eq!(
|
||||||
|
extensions.get("foo"),
|
||||||
|
Some(serde_json::Value::String("bar".to_string())).as_ref()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
extensions.get("quux"),
|
||||||
|
Some(serde_json::Value::Number(5.into())).as_ref()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rule_validate_success() {
|
||||||
|
use helper::*;
|
||||||
|
|
||||||
|
let doc = arbitrary::r#gen::<Doc>(1);
|
||||||
|
let delegates = Allowed::Set(doc.delegates().as_ref().clone());
|
||||||
|
let threshold = doc.majority();
|
||||||
|
|
||||||
|
let rule = Rule::new(delegates, threshold);
|
||||||
|
let result = rule.validate(&mut || resolve_from_doc(&doc));
|
||||||
|
assert!(result.is_ok(), "failed to validate doc: {result:?}");
|
||||||
|
|
||||||
|
let rule = Rule::new(Allowed::Delegates, 1);
|
||||||
|
let result = rule.validate(&mut || resolve_from_doc(&doc));
|
||||||
|
assert!(result.is_ok(), "failed to validate doc: {result:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rule_validate_failures() {
|
||||||
|
use helper::*;
|
||||||
|
|
||||||
|
let doc = arbitrary::r#gen::<Doc>(1);
|
||||||
|
let pattern = pattern(qualified_pattern!("refs/heads/main"));
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
Rule::new(Allowed::Delegates, 256).validate(&mut || resolve_from_doc(&doc)),
|
||||||
|
Err(ValidationError::Threshold(_))
|
||||||
|
));
|
||||||
|
|
||||||
|
let threshold = doc.delegates().len().saturating_add(1);
|
||||||
|
assert!(matches!(
|
||||||
|
Rule::new(Allowed::Delegates, threshold).validate(&mut || resolve_from_doc(&doc)),
|
||||||
|
Err(ValidationError::Threshold(_))
|
||||||
|
));
|
||||||
|
|
||||||
|
let delegates = NonEmpty::from_vec(arbitrary::vec::<Did>(256)).unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
Rule::new(delegates.into(), 1).validate(&mut || resolve_from_doc(&doc)),
|
||||||
|
Err(ValidationError::Delegates(_))
|
||||||
|
));
|
||||||
|
|
||||||
|
let delegates = nonempty![
|
||||||
|
did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"),
|
||||||
|
did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56")
|
||||||
|
];
|
||||||
|
let expected = Rule {
|
||||||
|
allow: ResolvedDelegates::Set(
|
||||||
|
doc::Delegates::new(nonempty![did(
|
||||||
|
"did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"
|
||||||
|
)])
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
threshold: doc::Threshold::MIN,
|
||||||
|
extensions: json::Map::new(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
Rule::new(delegates.into(), 1)
|
||||||
|
.validate(&mut || resolve_from_doc(&doc))
|
||||||
|
.unwrap(),
|
||||||
|
expected,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Duplicate rules are overwritten
|
||||||
|
let rules = vec![
|
||||||
|
(
|
||||||
|
pattern.clone().into_inner(),
|
||||||
|
Rule::new(Allowed::Delegates, 1),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
pattern.clone().into_inner(),
|
||||||
|
Rule::new(doc.delegates().as_ref().clone().into(), 1),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
let expected = [(
|
||||||
|
pattern,
|
||||||
|
Rule::new(
|
||||||
|
ResolvedDelegates::Set(doc.delegates().clone()),
|
||||||
|
doc::Threshold::MIN,
|
||||||
|
),
|
||||||
|
)]
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Rules>();
|
||||||
|
assert_eq!(
|
||||||
|
Rules::from_raw(rules, &mut || resolve_from_doc(&doc)).unwrap(),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn canonical() {
|
||||||
|
use helper::*;
|
||||||
|
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
|
||||||
|
|
||||||
|
transport::local::register(storage.clone());
|
||||||
|
|
||||||
|
let delegate = Device::mock_from_seed([0xff; 32]);
|
||||||
|
let contributor = MockSigner::from_seed([0xfe; 32]);
|
||||||
|
let (repo, head) = fixtures::repository(tempdir.path().join("working"));
|
||||||
|
let (rid, doc, _) = rad::init(
|
||||||
|
&repo,
|
||||||
|
"heartwood".try_into().unwrap(),
|
||||||
|
"Radicle Heartwood Protocol & Stack",
|
||||||
|
git::fmt::refname!("master"),
|
||||||
|
Visibility::default(),
|
||||||
|
&delegate,
|
||||||
|
&storage,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut doc = doc.edit();
|
||||||
|
// Ensure there is a second delegate for testing overlapping rules
|
||||||
|
doc.delegate(contributor.public_key().into());
|
||||||
|
|
||||||
|
// Create tags and keep track of their OIDs
|
||||||
|
//
|
||||||
|
// follows the `refs/tags/release/candidates/*` rule
|
||||||
|
let failing_tag = git::fmt::refname!("release/candidates/v1.0");
|
||||||
|
let tags = [
|
||||||
|
// follows the `refs/tags/*` rule
|
||||||
|
git::fmt::refname!("v1.0"),
|
||||||
|
// follows the `refs/tags/release/*` rule
|
||||||
|
git::fmt::refname!("release/v1.0"),
|
||||||
|
failing_tag.clone(),
|
||||||
|
// follows the `refs/tags/*` rule
|
||||||
|
git::fmt::refname!("qa/v1.0"),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|name| {
|
||||||
|
(
|
||||||
|
git::fmt::lit::refs_tags(name.clone()).into(),
|
||||||
|
tag(name, head, &repo),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<BTreeMap<Qualified, _>>();
|
||||||
|
|
||||||
|
git::push(
|
||||||
|
&repo,
|
||||||
|
&rad::REMOTE_NAME,
|
||||||
|
[
|
||||||
|
(
|
||||||
|
&git::fmt::qualified!("refs/tags/v1.0"),
|
||||||
|
&git::fmt::qualified!("refs/tags/v1.0"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
&git::fmt::qualified!("refs/tags/release/v1.0"),
|
||||||
|
&git::fmt::qualified!("refs/tags/release/v1.0"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
&git::fmt::qualified!("refs/tags/release/candidates/v1.0"),
|
||||||
|
&git::fmt::qualified!("refs/tags/release/candidates/v1.0"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
&git::fmt::qualified!("refs/tags/qa/v1.0"),
|
||||||
|
&git::fmt::qualified!("refs/tags/qa/v1.0"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let rules = Rules::from_raw(
|
||||||
|
[
|
||||||
|
(
|
||||||
|
raw_pattern(qualified_pattern!("refs/tags/*")),
|
||||||
|
Rule::new(Allowed::Delegates, 1),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
raw_pattern(qualified_pattern!("refs/tags/release/*")),
|
||||||
|
Rule::new(Allowed::Delegates, 1),
|
||||||
|
),
|
||||||
|
// Ensure that none of the other rules apply by ensuring we need
|
||||||
|
// both delegates to get the quorum of the
|
||||||
|
// `refs/tags/release/candidates/v1.0` reference
|
||||||
|
(
|
||||||
|
raw_pattern(qualified_pattern!("refs/tags/release/candidates/*")),
|
||||||
|
Rule::new(Allowed::Delegates, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
&mut || resolve_from_doc(&doc.clone().verified().unwrap()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// All tags should succeed at getting their canonical commit other than the
|
||||||
|
// candidates tag.
|
||||||
|
let stored = storage.repository(rid).unwrap();
|
||||||
|
let failing = git::fmt::Qualified::from(git::fmt::lit::refs_tags(failing_tag));
|
||||||
|
for (refname, oid) in tags.into_iter() {
|
||||||
|
let canonical = rules
|
||||||
|
.canonical(refname.clone(), &stored)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
panic!("there should be a matching rule for {refname}, rules: {rules:#?}")
|
||||||
|
});
|
||||||
|
if refname == failing {
|
||||||
|
assert!(canonical.find_objects().unwrap().quorum().is_err());
|
||||||
|
} else {
|
||||||
|
assert_eq!(
|
||||||
|
canonical
|
||||||
|
.find_objects()
|
||||||
|
.unwrap()
|
||||||
|
.quorum()
|
||||||
|
.unwrap_or_else(|e| panic!("quorum error for {refname}: {e}")),
|
||||||
|
canonical::Quorum {
|
||||||
|
refname,
|
||||||
|
object: canonical::Object::Tag { id: oid },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn special_branches() {
|
||||||
|
assert!(Pattern::new((*IDENTITY_BRANCH).clone().into()).is_err());
|
||||||
|
assert!(Pattern::new((*SIGREFS_BRANCH).clone().into()).is_err());
|
||||||
|
assert!(Pattern::new((*SIGREFS_PARENT).clone().into()).is_err());
|
||||||
|
assert!(Pattern::new((*IDENTITY_ROOT).clone().into()).is_err());
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
use radicle_git_ref_format::refspec::QualifiedPattern;
|
||||||
|
|
||||||
|
use crate::git;
|
||||||
|
use crate::git::canonical::rules::{Allowed, Pattern, RawPattern, Rule};
|
||||||
|
use crate::git::fmt::RefString;
|
||||||
|
use crate::identity::doc::{self, Doc};
|
||||||
|
use crate::prelude::Did;
|
||||||
|
use crate::test::fixtures;
|
||||||
|
|
||||||
|
pub fn roundtrip(rule: &Rule<Allowed, usize>) {
|
||||||
|
let json = serde_json::to_string(rule).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
*rule,
|
||||||
|
serde_json::from_str(&json).unwrap(),
|
||||||
|
"failed to roundtrip: {json}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn did(s: &str) -> Did {
|
||||||
|
s.parse().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn raw_pattern(qp: QualifiedPattern<'static>) -> RawPattern {
|
||||||
|
qp
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pattern(qp: QualifiedPattern<'static>) -> Pattern {
|
||||||
|
Pattern::new(qp).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_from_doc(doc: &Doc) -> doc::Delegates {
|
||||||
|
doc.delegates().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tag(name: RefString, head: git::raw::Oid, repo: &git::raw::Repository) -> git::Oid {
|
||||||
|
let commit = fixtures::commit(name.as_str(), &[head], repo);
|
||||||
|
let target = repo.find_object(commit.into(), None).unwrap();
|
||||||
|
let tagger = repo.signature().unwrap();
|
||||||
|
repo.tag(name.as_str(), &target, &tagger, name.as_str(), false)
|
||||||
|
.unwrap()
|
||||||
|
.into()
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue