Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
heartwood crates radicle src storage refs sigrefs write test mock.rs
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::str::FromStr as _;

use radicle_core::{NodeId, RepoId};
use radicle_git_metadata::author::{Author, Time};
use radicle_git_metadata::commit::CommitData;
use radicle_git_metadata::commit::headers::Headers;
use radicle_git_metadata::commit::trailers::OwnedTrailer;
use radicle_oid::Oid;

use crate::git;
use crate::identity::doc;
use crate::storage::HasRepoId;
use crate::storage::refs::sigrefs::git::{object, reference};
use crate::storage::refs::{REFS_BLOB_PATH, Refs, SIGNATURE_BLOB_PATH, SIGREFS_BRANCH};

const MOCKED_IDENTITY: u8 = 99u8;

enum WriteTreeBehavior {
    /// [`object::Writer::write_tree`] returns `Ok(oid)`.
    Ok(Oid),
    /// [`object::Writer::write_tree`] returns `Err(…)`.
    Error,
}

/// [`object::Writer::write_commit`] returns `Ok(oid)`.
struct WriteCommitBehavior(Oid);

enum WriteReferenceBehavior {
    /// [`reference::Writer::write_reference`] returns `Ok(())`.
    Ok,
    /// [`reference::Writer::write_reference`] returns `Err(…)`.
    Error,
}

enum BlobBehavior {
    /// [`object::Reader::read_blob`] returns `Ok(Some(blob))`.
    Present(Vec<u8>),
    /// [`object::Reader::read_blob`] returns `Ok(None)`.
    Missing,
    /// [`object::Reader::read_blob`] returns `Err(…)`.
    Error,
}

enum RefBehavior {
    /// [`reference::Reader::find_reference`] returns `Ok(Some(oid))`.
    Present(Oid),
    /// [`reference::Reader::find_reference`] returns `Ok(None)`.
    Missing,
    /// [`reference::Reader::find_reference`] returns `Err(…)`.
    Error,
}

pub struct MockRepository {
    commits: HashMap<Oid, CommitBehavior>,
    blobs: HashMap<(Oid, PathBuf), BlobBehavior>,
    references: HashMap<String, RefBehavior>,
    write_tree: Option<WriteTreeBehavior>,
    write_commit: Option<WriteCommitBehavior>,
    write_reference: Option<WriteReferenceBehavior>,
}

enum CommitBehavior {
    /// [`object::Reader::read_commit`] returns `Ok(Some(bytes))`.
    Present(Box<CommitData<Oid, Oid>>),
}

impl MockRepository {
    pub fn new() -> MockRepository {
        MockRepository {
            commits: HashMap::new(),
            blobs: HashMap::new(),
            references: HashMap::new(),
            write_tree: None,
            write_commit: None,
            write_reference: None,
        }
        .with_identity(oid(MOCKED_IDENTITY))
    }

    pub fn with_commit(mut self, oid: Oid, data: CommitData<Oid, Oid>) -> Self {
        self.commits
            .insert(oid, CommitBehavior::Present(Box::new(data)));
        self
    }

    pub fn with_rad_sigrefs(mut self, namespace: &NodeId, commit: Oid) -> MockRepository {
        self.references
            .insert(sigrefs_ref_name(namespace), RefBehavior::Present(commit));
        self
    }

    pub fn with_missing_rad_sigrefs(mut self, namespace: &NodeId) -> MockRepository {
        self.references
            .insert(sigrefs_ref_name(namespace), RefBehavior::Missing);
        self
    }

    pub fn with_rad_sigrefs_error(mut self, namespace: &NodeId) -> MockRepository {
        self.references
            .insert(sigrefs_ref_name(namespace), RefBehavior::Error);
        self
    }

    pub fn with_refs(
        self,
        commit: Oid,
        refs: impl IntoIterator<Item = (git::fmt::RefString, Oid)>,
    ) -> MockRepository {
        let refs = Refs::from(refs.into_iter());
        self.with_blob(commit, Path::new(REFS_BLOB_PATH), refs.canonical())
    }

    pub fn with_refs_error(self, commit: Oid) -> MockRepository {
        self.with_blob_error(commit, Path::new(REFS_BLOB_PATH))
    }

    pub fn with_missing_refs(self, commit: Oid) -> MockRepository {
        self.with_missing_blob(commit, Path::new(REFS_BLOB_PATH))
    }

    pub fn with_invalid_refs(self, commit: Oid) -> MockRepository {
        self.with_blob(
            commit,
            Path::new(REFS_BLOB_PATH),
            b"NOT VALID REFS\n".to_vec(),
        )
    }

    pub fn with_signature(self, commit: Oid, sig_id: u8) -> MockRepository {
        self.with_blob(commit, Path::new(SIGNATURE_BLOB_PATH), vec![sig_id; 64])
    }

    pub fn with_signature_error(self, commit: Oid) -> MockRepository {
        self.with_blob_error(commit, Path::new(SIGNATURE_BLOB_PATH))
    }

    pub fn with_missing_signature(self, commit: Oid) -> MockRepository {
        self.with_missing_blob(commit, Path::new(SIGNATURE_BLOB_PATH))
    }

    pub fn with_invalid_signature(self, commit: Oid) -> MockRepository {
        let bytes = vec![0u8; 1];
        assert!(crypto::Signature::from_str(std::str::from_utf8(&bytes).unwrap()).is_err());
        self.with_blob(commit, Path::new(SIGNATURE_BLOB_PATH), bytes)
    }

    pub fn with_identity(self, commit: Oid) -> Self {
        self.with_blob(commit, &identity_path(), vec![])
    }

    fn with_blob(mut self, commit: Oid, path: &Path, bytes: Vec<u8>) -> Self {
        self.blobs
            .insert((commit, path.to_path_buf()), BlobBehavior::Present(bytes));
        self
    }

    fn with_blob_error(mut self, commit: Oid, path: &Path) -> Self {
        self.blobs
            .insert((commit, path.to_path_buf()), BlobBehavior::Error);
        self
    }

    fn with_missing_blob(mut self, commit: Oid, path: &Path) -> Self {
        self.blobs
            .insert((commit, path.to_path_buf()), BlobBehavior::Missing);
        self
    }

    pub fn with_write_tree_error(mut self) -> Self {
        self.write_tree = Some(WriteTreeBehavior::Error);
        self
    }

    pub fn with_write_tree_ok(mut self, oid: Oid) -> Self {
        self.write_tree = Some(WriteTreeBehavior::Ok(oid));
        self
    }

    pub fn with_write_commit_ok(mut self, oid: Oid) -> Self {
        self.write_commit = Some(WriteCommitBehavior(oid));
        self
    }

    pub fn with_write_reference_ok(mut self) -> Self {
        self.write_reference = Some(WriteReferenceBehavior::Ok);
        self
    }

    pub fn with_write_reference_error(mut self) -> Self {
        self.write_reference = Some(WriteReferenceBehavior::Error);
        self
    }
}

impl HasRepoId for MockRepository {
    fn rid(&self) -> radicle_core::RepoId {
        rid()
    }
}

impl object::Reader for MockRepository {
    fn read_commit(&self, oid: &Oid) -> Result<Option<Vec<u8>>, object::error::ReadCommit> {
        match self.commits.get(oid) {
            Some(CommitBehavior::Present(data)) => Ok(Some(data.to_string().as_bytes().to_vec())),
            None => Ok(None),
        }
    }

    fn read_blob(
        &self,
        commit: &Oid,
        path: &Path,
    ) -> Result<Option<object::Blob>, object::error::ReadBlob> {
        let key = (*commit, path.to_path_buf());
        match self.blobs.get(&key) {
            Some(BlobBehavior::Present(bytes)) => Ok(Some(object::Blob {
                oid: *commit,
                bytes: bytes.clone(),
            })),
            Some(BlobBehavior::Missing) | None => Ok(None),
            Some(BlobBehavior::Error) => Err(object::error::ReadBlob::other(
                std::io::Error::other("mock blob error"),
            )),
        }
    }
}

impl reference::Reader for MockRepository {
    fn find_reference(
        &self,
        reference: &git::fmt::Namespaced,
    ) -> Result<Option<Oid>, reference::error::FindReference> {
        match self.references.get(reference.as_str()) {
            Some(RefBehavior::Present(oid)) => Ok(Some(*oid)),
            Some(RefBehavior::Missing) | None => Ok(None),
            Some(RefBehavior::Error) => Err(reference::error::FindReference::other(
                std::io::Error::other("mock reference error"),
            )),
        }
    }
}

impl object::Writer for MockRepository {
    fn write_tree(
        &self,
        _refs: object::RefsEntry,
        _signature: object::SignatureEntry,
    ) -> Result<Oid, object::error::WriteTree> {
        match &self.write_tree {
            Some(WriteTreeBehavior::Ok(oid)) => Ok(*oid),
            Some(WriteTreeBehavior::Error) | None => Err(object::error::WriteTree::write_error(
                std::io::Error::other("mock write_tree error"),
            )),
        }
    }

    fn write_commit(&self, _bytes: &[u8]) -> Result<Oid, object::error::WriteCommit> {
        match &self.write_commit {
            Some(WriteCommitBehavior(oid)) => Ok(*oid),
            None => Err(object::error::WriteCommit::other(std::io::Error::other(
                "mock write_commit error",
            ))),
        }
    }
}

impl reference::Writer for MockRepository {
    fn write_reference(
        &self,
        _reference: &git::fmt::Namespaced,
        _commit: Oid,
        _parent: Option<Oid>,
        _reflog: String,
    ) -> Result<(), reference::error::WriteReference> {
        match &self.write_reference {
            Some(WriteReferenceBehavior::Ok) => Ok(()),
            Some(WriteReferenceBehavior::Error) | None => {
                Err(reference::error::WriteReference::other(
                    std::io::Error::other("mock write_reference error"),
                ))
            }
        }
    }
}

/// Always signs successfully, returning a fixed 64-byte signature.
pub struct AlwaysSign;

impl AlwaysSign {
    const SIGNATURE: [u8; 64] = [1u8; 64];

    pub fn signature() -> crypto::Signature {
        crypto::Signature::from(Self::SIGNATURE)
    }
}

impl crypto::signature::Signer<crypto::Signature> for AlwaysSign {
    fn try_sign(&self, _msg: &[u8]) -> Result<crypto::Signature, crypto::signature::Error> {
        Ok(Self::signature())
    }
}

impl crypto::signature::Verifier<crypto::Signature> for AlwaysSign {
    fn verify(
        &self,
        _msg: &[u8],
        _sig: &crypto::Signature,
    ) -> Result<(), crypto::signature::Error> {
        Ok(())
    }
}

/// Always fails to sign.
pub struct NeverSign;

impl crypto::signature::Signer<crypto::Signature> for NeverSign {
    fn try_sign(&self, _msg: &[u8]) -> Result<crypto::Signature, crypto::signature::Error> {
        Err(crypto::signature::Error::new())
    }
}

/// Construct an [`Oid`] from a single repeated byte.
///
/// `oid(1) != oid(2)` is guaranteed; use distinct values for distinct objects.
pub fn oid(n: u8) -> Oid {
    Oid::from_sha1([n; 20])
}

/// A fixed [`RepoId`] for tests.
pub fn rid() -> RepoId {
    RepoId::from(oid(MOCKED_IDENTITY))
}

/// A fixed [`NodeId`] for tests.
pub fn node_id() -> NodeId {
    NodeId::from([1u8; 32])
}

pub fn refs_heads_main() -> git::fmt::RefString {
    git::fmt::refname!("refs/heads/main")
}

/// A minimal [`radicle_git_metadata::author::Author`] for use in tests.
pub fn author() -> Author {
    Author {
        name: "test".to_owned(),
        email: "test@example.com".to_owned(),
        time: Time::new(0, 0),
    }
}

pub fn commit_data(parents: impl IntoIterator<Item = Oid>) -> CommitData<Oid, Oid> {
    let tree = oid(0);
    let author = author();
    let message = "test\n".to_owned();

    CommitData::new::<_, _, OwnedTrailer>(
        tree,
        parents,
        author.clone(),
        author,
        Headers::new(),
        message,
        vec![],
    )
}

fn sigrefs_ref_name(namespace: &NodeId) -> String {
    SIGREFS_BRANCH
        .with_namespace(git::fmt::Component::from(namespace))
        .as_str()
        .to_owned()
}

fn identity_path() -> PathBuf {
    Path::new("embeds").join(*doc::PATH)
}