Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
heartwood crates radicle src storage refs sigrefs git properties.rs
use std::path::Path;

use crypto::test::signer::MockSigner;
use crypto::{PublicKey, Signer as _, signature};
use qcheck::TestResult;
use qcheck_macros::quickcheck;
use radicle_core::{NodeId, RepoId};
use radicle_git_metadata::author::{Author, Time};
use radicle_oid::Oid;
use tempfile::TempDir;

use crate::git;
use crate::storage::refs::sigrefs::VerifiedCommit;
use crate::storage::refs::sigrefs::read::{SignedRefsReader, Tip};
use crate::storage::refs::sigrefs::write::{SignedRefsWriter, Update};
use crate::storage::refs::{IDENTITY_ROOT, Refs};

use super::Committer;

/// Newtype wrapper around [`Vec`] to keep the [`Arbitrary`] implementation
/// bounded to a smaller size.
#[derive(Clone, Debug)]
struct BoundedVec<T>(Vec<T>);

impl<T: qcheck::Arbitrary> qcheck::Arbitrary for BoundedVec<T> {
    fn arbitrary(g: &mut qcheck::Gen) -> Self {
        let size = usize::arbitrary(g) % 24;
        BoundedVec((0..size).map(|_| T::arbitrary(g)).collect())
    }

    fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
        Box::new(self.0.shrink().map(BoundedVec))
    }
}

struct Verifier {
    key: PublicKey,
}

impl Verifier {
    fn new(signer: &MockSigner) -> Self {
        Self {
            key: *signer.public_key(),
        }
    }
}

impl signature::Verifier<crypto::Signature> for Verifier {
    fn verify(&self, msg: &[u8], signature: &crypto::Signature) -> Result<(), signature::Error> {
        self.key
            .verify(msg, signature)
            .map_err(signature::Error::from_source)
    }
}

fn mock_author() -> Author {
    Author {
        name: "testy".to_string(),
        email: "testy@example.com".to_string(),
        time: Time::new(6400, 0),
    }
}

fn mock_committer() -> Committer {
    Committer::new(mock_author())
}

/// A helper structure that sets up a minimal Radicle repository.
struct Repository {
    raw: git::raw::Repository,
    rid: RepoId,
    root: Oid,
    _tmp: TempDir,
}

impl Repository {
    fn new() -> Repository {
        let dir = TempDir::new().unwrap();
        let repo = git::raw::Repository::init_bare(dir.path()).unwrap();
        let (root, rid) = Self::write_identity(&repo);
        Repository {
            raw: repo,
            rid,
            root,
            _tmp: dir,
        }
    }

    /// Writes a mock blob that represents the identity document, and creates a
    /// commit in the repository that contains this blob.
    fn write_identity(repo: &git::raw::Repository) -> (Oid, RepoId) {
        let blob = repo.blob(b"identity root").unwrap();
        let empty = {
            let empty = repo.treebuilder(None).unwrap();
            let tree = empty.write().unwrap();
            repo.find_tree(tree).unwrap()
        };
        let tree = {
            let mut tb = git::raw::build::TreeUpdateBuilder::new();
            tb.upsert(
                Path::new("embeds").join(*crate::identity::doc::PATH),
                blob,
                git::raw::FileMode::Blob,
            );
            let tree = tb.create_updated(repo, &empty).unwrap();
            repo.find_tree(tree).unwrap()
        };
        let author = git::raw::Signature::now("testy", "testy@example.com").unwrap();
        let root = repo
            .commit(
                Some(IDENTITY_ROOT.as_str()),
                &author,
                &author,
                "identity root",
                &tree,
                &[],
            )
            .unwrap();
        (root.into(), RepoId::from(blob))
    }
}

fn write_log(
    refs: Refs,
    rid: RepoId,
    namespace: NodeId,
    signer: &MockSigner,
    repo: &git::raw::Repository,
) -> Update {
    SignedRefsWriter::new(refs, rid, namespace, repo, signer)
        .write(
            mock_committer(),
            "test commit".to_string(),
            "test reflog".to_string(),
        )
        .unwrap()
}

fn read_log(
    rid: RepoId,
    namespace: NodeId,
    verifier: &Verifier,
    repo: &git::raw::Repository,
) -> VerifiedCommit {
    SignedRefsReader::new(rid, Tip::Reference(namespace), repo, verifier)
        .read()
        .unwrap()
}

#[quickcheck]
fn initial_commit_roundtrip(mut refs: Refs) -> bool {
    let Repository {
        raw: repo,
        rid,
        root,
        _tmp,
    } = Repository::new();
    refs.insert(IDENTITY_ROOT.to_ref_string(), root);
    let signer = MockSigner::default();
    let namespace = *signer.public_key();
    let verifier = Verifier::new(&signer);

    let update = write_log(refs.clone(), rid, namespace, &signer, &repo);
    let head_oid = match update {
        Update::Changed { ref entry, .. } => entry.oid(),
        Update::Unchanged { .. } => return false,
    };

    let verified_commit = read_log(rid, namespace, &verifier, &repo);
    let head = *verified_commit.commit().oid();
    let parent = verified_commit.commit().parent().copied();
    let new_refs = verified_commit.into_sigrefs_at(namespace);

    head == head_oid && parent.is_none() && *new_refs == refs
}

#[quickcheck]
fn chain_roundtrip(chain: BoundedVec<Refs>) -> TestResult {
    let chain = chain.0;
    if chain.is_empty() {
        return TestResult::discard();
    }

    let Repository {
        raw: repo,
        rid,
        root,
        _tmp,
    } = Repository::new();
    let signer = MockSigner::default();
    let namespace = *signer.public_key();
    let verifier = Verifier::new(&signer);

    let mut last_changed_head = None;
    let mut expected_parent = None;

    for mut refs in chain {
        refs.insert(IDENTITY_ROOT.to_ref_string(), root);
        let update = write_log(refs.clone(), rid, namespace, &signer, &repo);

        if let Update::Changed { ref entry, .. } = update {
            last_changed_head = Some(entry.oid());
        }

        let verified_commit = read_log(rid, namespace, &verifier, &repo);
        let head = *verified_commit.commit().oid();
        let parent = verified_commit.commit().parent().copied();
        let new_refs = verified_commit.into_sigrefs_at(namespace);

        if refs != *new_refs {
            return TestResult::failed();
        }

        if let Some(expected_head) = last_changed_head {
            if head != expected_head {
                return TestResult::error(format!(
                    "expected commit to be {expected_head}, but found {head}"
                ));
            }
            if parent != expected_parent {
                return TestResult::error(format!(
                    "expected parent commit to be {expected_parent:?}, but found {parent:?}"
                ));
            }
        }
        expected_parent = Some(head);
    }

    TestResult::passed()
}

#[quickcheck]
fn idempotent_write(mut refs: Refs) -> bool {
    let Repository {
        raw: repo,
        rid,
        root,
        _tmp,
    } = Repository::new();
    refs.insert(IDENTITY_ROOT.to_ref_string(), root);
    let signer = MockSigner::default();
    let namespace = *signer.public_key();

    let first = write_log(refs.clone(), rid, namespace, &signer, &repo);
    let head_oid = match first {
        Update::Changed { ref entry, .. } => entry.oid(),
        Update::Unchanged { .. } => return false,
    };

    let second = write_log(refs, rid, namespace, &signer, &repo);
    matches!(second, Update::Unchanged { verified } if *verified.commit().oid() == head_oid)
}