pub mod update;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::num::{NonZeroU32, NonZeroUsize};
use std::ops::{Deref, Not};
use std::path::Path;
use std::str::FromStr;
use std::sync::LazyLock;
use crate::git::Oid;
use nonempty::NonEmpty;
use radicle_cob::type_name::{TypeName, TypeNameParse};
use serde::{de, Deserialize, Serialize};
use thiserror::Error;
use crate::canonical::formatter::CanonicalFormatter;
use crate::cob::identity;
use crate::crypto;
use crate::crypto::Signature;
use crate::git;
use crate::git::canonical::rules;
use crate::git::raw::ErrorExt as _;
use crate::identity::{project::Project, Did};
use crate::node::device::Device;
use crate::storage;
use crate::storage::{ReadRepository, RepositoryError};
pub use crypto::PublicKey;
pub use radicle_core::repo::*;
use super::crefs::{self, RawCanonicalRefs};
use super::CanonicalRefs;
pub static PATH: LazyLock<&Path> = LazyLock::new(|| Path::new("radicle.json"));
pub const MAX_STRING_LENGTH: usize = 255;
pub const MAX_DELEGATES: usize = 255;
pub const IDENTITY_VERSION: Version = Version(NonZeroU32::new(1).unwrap());
#[derive(Error, Debug)]
pub enum DocError {
#[error("json: {0}")]
Json(#[from] serde_json::Error),
#[error(transparent)]
Delegates(#[from] DelegatesError),
#[error(transparent)]
Threshold(#[from] ThresholdError),
#[error("git: {0}")]
Git(#[from] git::raw::Error),
#[error("missing identity document")]
Missing,
}
#[derive(Debug, Error)]
#[error("invalid delegates: {0}")]
pub struct DelegatesError(&'static str);
#[derive(Debug, Error)]
#[error("invalid threshold `{0}`: {1}")]
pub struct ThresholdError(usize, &'static str);
impl DocError {
pub fn is_not_found(&self) -> bool {
match self {
Self::Git(e) => e.is_not_found(),
_ => false,
}
}
}
#[derive(Debug, Error)]
pub enum DefaultBranchRuleError {
#[error("could not create rule due to the reference name being invalid: {0}")]
Pattern(#[from] rules::PatternError),
#[error("could not load `xyz.radicle.project` to get default branch name: {0}")]
Payload(#[from] PayloadError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct Version(NonZeroU32);
impl Version {
pub fn new(n: u32) -> Result<Version, VersionError> {
match NonZeroU32::new(n) {
None => Err(VersionError::ZeroVersion),
Some(n) if n > IDENTITY_VERSION.into() => Err(VersionError::UnknownVersion(n)),
Some(n) => Ok(Version(n)),
}
}
pub fn number(&self) -> NonZeroU32 {
self.0
}
pub fn is_valid_version(v: &u32) -> bool {
0 < *v && *v <= IDENTITY_VERSION.into()
}
fn skip_serializing(&self) -> bool {
u32::from(*self) <= 1
}
}
impl From<Version> for NonZeroU32 {
fn from(Version(n): Version) -> Self {
n
}
}
impl From<Version> for u32 {
fn from(Version(n): Version) -> Self {
n.into()
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum VersionError {
#[error("the version 0 is not supported")]
ZeroVersion,
#[error("unknown identity document version {0}, only version {IDENTITY_VERSION} is supported")]
UnknownVersion(NonZeroU32),
}
impl VersionError {
pub fn verbose(&self) -> String {
const UNKNOWN_VERSION_ERROR: &str = r#"
Perhaps a new version of the identity document is released which is not supported by the current client.
See https://radicle.xyz for the latest versions of Radicle.
The CLI command `rad id migrate` will help to migrate to an up-to-date versions."#;
match self {
err @ Self::ZeroVersion => err.to_string(),
err @ Self::UnknownVersion(_) => format!("{err}{UNKNOWN_VERSION_ERROR}"),
}
}
}
impl TryFrom<u32> for Version {
type Error = VersionError;
fn try_from(n: u32) -> Result<Self, Self::Error> {
Version::new(n)
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<'de> Deserialize<'de> for Version {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
u32::deserialize(deserializer)
.and_then(|v| Version::new(v).map_err(|e| de::Error::custom(e.to_string())))
}
}
fn missing_version() -> Version {
unsafe { Version(NonZeroU32::new_unchecked(1)) }
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PayloadId(TypeName);
impl fmt::Display for PayloadId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl FromStr for PayloadId {
type Err = TypeNameParse;
fn from_str(s: &str) -> Result<Self, Self::Err> {
TypeName::from_str(s).map(Self)
}
}
impl PayloadId {
pub fn project() -> Self {
Self(
TypeName::from_str("xyz.radicle.project")
.expect("PayloadId::project: type name is valid"),
)
}
pub fn canonical_refs() -> Self {
Self(
TypeName::from_str("xyz.radicle.crefs")
.expect("PayloadId::canonical_refs: type name is valid"),
)
}
}
#[derive(Debug, Error)]
pub enum PayloadError {
#[error("json: {0}")]
Json(#[from] serde_json::Error),
#[error("payload '{0}' not found in identity document")]
NotFound(PayloadId),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Payload {
value: serde_json::Value,
}
impl Payload {
pub fn as_object_mut(
&mut self,
) -> Option<&mut serde_json::value::Map<String, serde_json::Value>> {
self.value.as_object_mut()
}
}
impl From<serde_json::Value> for Payload {
fn from(value: serde_json::Value) -> Self {
Self { value }
}
}
impl Deref for Payload {
type Target = serde_json::Value;
fn deref(&self) -> &Self::Target {
&self.value
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DocAt {
pub commit: Oid,
pub blob: Oid,
pub doc: Doc,
}
impl Deref for DocAt {
type Target = Doc;
fn deref(&self) -> &Self::Target {
&self.doc
}
}
impl From<DocAt> for Doc {
fn from(value: DocAt) -> Self {
value.doc
}
}
impl AsRef<Doc> for DocAt {
fn as_ref(&self) -> &Doc {
&self.doc
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum Visibility {
#[default]
Public,
Private {
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
allow: BTreeSet<Did>,
},
}
#[derive(Error, Debug)]
#[error("'{0}' is not a valid visibility type")]
pub struct VisibilityParseError(String);
impl FromStr for Visibility {
type Err = VisibilityParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"public" => Ok(Visibility::Public),
"private" => Ok(Visibility::private([])),
_ => Err(VisibilityParseError(s.to_owned())),
}
}
}
impl Visibility {
pub fn is_public(&self) -> bool {
matches!(self, Self::Public)
}
pub fn is_private(&self) -> bool {
matches!(self, Self::Private { .. })
}
pub fn private(allow: impl IntoIterator<Item = Did>) -> Self {
Self::Private {
allow: BTreeSet::from_iter(allow),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RawDoc {
#[serde(default = "missing_version")]
version: Version,
pub payload: BTreeMap<PayloadId, Payload>,
pub delegates: Vec<Did>,
pub threshold: usize,
#[serde(default)]
pub visibility: Visibility,
}
impl TryFrom<RawDoc> for Doc {
type Error = DocError;
fn try_from(doc: RawDoc) -> Result<Self, Self::Error> {
doc.verified()
}
}
impl RawDoc {
pub fn new(
project: Project,
delegates: Vec<Did>,
threshold: usize,
visibility: Visibility,
) -> Self {
let project =
serde_json::to_value(project).expect("Doc::initial: payload must be serializable");
Self {
version: IDENTITY_VERSION,
payload: BTreeMap::from_iter([(PayloadId::project(), Payload::from(project))]),
delegates,
threshold,
visibility,
}
}
pub fn version(&self) -> &Version {
&self.version
}
pub fn project(&self) -> Result<Project, PayloadError> {
let value = self
.payload
.get(&PayloadId::project())
.ok_or_else(|| PayloadError::NotFound(PayloadId::project()))?;
let proj: Project = serde_json::from_value((**value).clone())?;
Ok(proj)
}
pub fn is_delegate(&self, did: &Did) -> bool {
self.delegates.contains(did)
}
pub fn delegate(&mut self, did: Did) {
self.delegates.push(did)
}
pub fn rescind(&mut self, did: &Did) -> Result<bool, DocError> {
let (matches, delegates) = self.delegates.iter().partition(|d| *d == did);
self.delegates = delegates;
Ok(matches.is_empty().not())
}
pub fn from_json(bytes: &[u8]) -> Result<Self, DocError> {
serde_json::from_slice(bytes).map_err(DocError::from)
}
pub fn verified(self) -> Result<Doc, DocError> {
let RawDoc {
version,
payload,
delegates,
threshold,
visibility,
} = self;
let delegates = Delegates::new(delegates)?;
let threshold = Threshold::new(threshold, &delegates)?;
Ok(Doc {
version,
payload,
delegates,
threshold,
visibility,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "Vec<Did>")]
pub struct Delegates(NonEmpty<Did>);
impl AsRef<NonEmpty<Did>> for Delegates {
fn as_ref(&self) -> &NonEmpty<Did> {
&self.0
}
}
impl From<Did> for Delegates {
fn from(did: Did) -> Self {
Self(NonEmpty::new(did))
}
}
impl TryFrom<Vec<Did>> for Delegates {
type Error = DelegatesError;
fn try_from(dids: Vec<Did>) -> Result<Self, Self::Error> {
Delegates::new(dids)
}
}
impl IntoIterator for Delegates {
type Item = <NonEmpty<Did> as IntoIterator>::Item;
type IntoIter = <NonEmpty<Did> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl Delegates {
pub fn new(delegates: impl IntoIterator<Item = Did>) -> Result<Self, DelegatesError> {
let delegates = delegates
.into_iter()
.try_fold(Vec::<Did>::new(), |mut dids, did| {
if !dids.contains(&did) {
if dids.len() >= MAX_DELEGATES {
return Err(DelegatesError("number of delegates cannot exceed 255"));
}
dids.push(did);
}
Ok(dids)
})?;
NonEmpty::from_vec(delegates)
.map(Self)
.ok_or(DelegatesError("delegate list cannot be empty"))
}
pub fn first(&self) -> &Did {
self.0.first()
}
pub fn iter(&self) -> impl Iterator<Item = &Did> {
self.0.iter()
}
pub fn contains(&self, did: &Did) -> bool {
self.0.contains(did)
}
pub fn is_only(&self, did: &Did) -> bool {
self.0.tail.is_empty() && &self.0.head == did
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
false
}
}
impl<'a> From<&'a Delegates> for &'a NonEmpty<Did> {
fn from(ds: &'a Delegates) -> Self {
&ds.0
}
}
impl From<Delegates> for NonEmpty<Did> {
fn from(ds: Delegates) -> Self {
ds.0
}
}
impl From<Delegates> for Vec<Did> {
fn from(Delegates(ds): Delegates) -> Self {
ds.into()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(transparent)]
pub struct Threshold(NonZeroUsize);
impl From<Threshold> for usize {
fn from(Threshold(t): Threshold) -> Self {
t.get()
}
}
impl AsRef<NonZeroUsize> for Threshold {
fn as_ref(&self) -> &NonZeroUsize {
&self.0
}
}
impl fmt::Display for Threshold {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Threshold {
pub const MIN: Threshold = Threshold(NonZeroUsize::MIN);
pub fn new(t: usize, delegates: &Delegates) -> Result<Self, ThresholdError> {
if t > MAX_DELEGATES {
Err(ThresholdError(t, "threshold cannot exceed 255"))
} else if t > delegates.len() {
Err(ThresholdError(
t,
"threshold cannot exceed number of delegates",
))
} else {
NonZeroUsize::new(t)
.map(Self)
.ok_or(ThresholdError(t, "threshold cannot be zero"))
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(try_from = "RawDoc")]
pub struct Doc {
#[serde(skip_serializing_if = "Version::skip_serializing")]
version: Version,
payload: BTreeMap<PayloadId, Payload>,
delegates: Delegates,
threshold: Threshold,
#[serde(default, skip_serializing_if = "Visibility::is_public")]
visibility: Visibility,
}
impl Doc {
pub fn initial(project: Project, delegate: Did, visibility: Visibility) -> Self {
let project =
serde_json::to_value(project).expect("Doc::initial: payload must be serializable");
Self {
version: IDENTITY_VERSION,
payload: BTreeMap::from_iter([(PayloadId::project(), Payload::from(project))]),
delegates: Delegates(NonEmpty::new(delegate)),
threshold: Threshold(NonZeroUsize::MIN),
visibility,
}
}
pub fn from_blob(blob: &git::raw::Blob) -> Result<Self, DocError> {
RawDoc::from_json(blob.content())?.verified()
}
pub fn edit(self) -> RawDoc {
let Doc {
version,
payload,
delegates,
threshold,
visibility,
} = self;
RawDoc {
version,
payload,
delegates: delegates.into(),
threshold: threshold.into(),
visibility,
}
}
pub fn with_edits<F>(self, f: F) -> Result<Self, DocError>
where
F: FnOnce(&mut RawDoc),
{
let mut raw = self.edit();
f(&mut raw);
raw.verified()
}
pub fn version(&self) -> &Version {
&self.version
}
pub fn payload(&self) -> &BTreeMap<PayloadId, Payload> {
&self.payload
}
pub fn project(&self) -> Result<Project, PayloadError> {
let value = self
.payload
.get(&PayloadId::project())
.ok_or_else(|| PayloadError::NotFound(PayloadId::project()))?;
let proj: Project = serde_json::from_value((**value).clone())?;
Ok(proj)
}
pub fn default_branch_rule(
&self,
) -> Result<(rules::Pattern, rules::ValidRule), DefaultBranchRuleError> {
let proj = self.project()?;
let refname = proj.default_branch();
let pattern = rules::Pattern::try_from(git::refs::branch(refname).to_owned())?;
let rule = rules::Rule::new(
rules::ResolvedDelegates::Delegates(self.delegates.clone()),
self.threshold,
);
Ok((pattern, rule))
}
pub fn visibility(&self) -> &Visibility {
&self.visibility
}
pub fn is_public(&self) -> bool {
self.visibility.is_public()
}
pub fn is_private(&self) -> bool {
self.visibility.is_private()
}
pub fn threshold(&self) -> usize {
self.threshold.into()
}
pub fn threshold_nonzero(&self) -> &NonZeroUsize {
&self.threshold.0
}
pub fn delegates(&self) -> &Delegates {
&self.delegates
}
pub fn is_delegate(&self, did: &Did) -> bool {
self.delegates.contains(did)
}
pub fn is_visible_to(&self, did: &Did) -> bool {
match &self.visibility {
Visibility::Public => true,
Visibility::Private { allow } => allow.contains(did) || self.is_delegate(did),
}
}
pub fn verify_signature(
&self,
key: &PublicKey,
signature: &Signature,
blob: Oid,
) -> Result<(), PublicKey> {
if !self.is_delegate(&key.into()) {
return Err(*key);
}
if key.verify(AsRef::<[u8]>::as_ref(&blob), signature).is_err() {
return Err(*key);
}
Ok(())
}
pub fn is_majority(&self, votes: usize) -> bool {
votes >= self.majority()
}
pub fn majority(&self) -> usize {
self.delegates.len() / 2 + 1
}
pub(crate) fn blob_at<R: ReadRepository>(
commit: Oid,
repo: &R,
) -> Result<git::raw::Blob<'_>, DocError> {
let path = Path::new("embeds").join(*PATH);
repo.blob_at(commit, path.as_path()).map_err(DocError::from)
}
pub fn encode(&self) -> Result<(git::Oid, Vec<u8>), DocError> {
let mut buf = Vec::new();
let mut serializer =
serde_json::Serializer::with_formatter(&mut buf, CanonicalFormatter::new());
self.serialize(&mut serializer)?;
let oid = git::raw::Oid::hash_object(git::raw::ObjectType::Blob, &buf)?;
Ok((oid.into(), buf))
}
pub fn sign<G>(&self, signer: &G) -> Result<(git::Oid, Vec<u8>, Signature), DocError>
where
G: crypto::signature::Signer<crypto::Signature>,
{
let (oid, bytes) = self.encode()?;
let sig = signer.sign(oid.as_ref());
Ok((oid, bytes, sig))
}
pub fn signature_of<G>(&self, signer: &G) -> Result<Signature, DocError>
where
G: crypto::signature::Signer<crypto::Signature>,
{
let (_, _, sig) = self.sign(signer)?;
Ok(sig)
}
pub fn load_at<R: ReadRepository>(commit: Oid, repo: &R) -> Result<DocAt, DocError> {
let blob = Self::blob_at(commit, repo)?;
let doc = Self::from_blob(&blob)?;
Ok(DocAt {
commit,
doc,
blob: blob.id().into(),
})
}
pub fn init<G>(
&self,
repo: &storage::git::Repository,
signer: &Device<G>,
) -> Result<git::Oid, RepositoryError>
where
G: crypto::signature::Signer<crypto::Signature>,
{
let cob = identity::Identity::initialize(self, repo, signer)?;
let id_ref = git::refs::storage::id(signer.public_key());
let cob_ref = git::refs::storage::cob(
signer.public_key(),
&crate::cob::identity::TYPENAME,
&cob.id,
);
repo.backend.reference_symbolic(
id_ref.as_str(),
cob_ref.as_str(),
false,
"Create `rad/id` reference to point to new identity COB",
)?;
Ok(*cob.id)
}
}
#[derive(Debug, Error)]
pub enum CanonicalRefsError {
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
CanonicalRefs(#[from] rules::ValidationError),
#[error(transparent)]
DefaultBranch(#[from] DefaultBranchRuleError),
}
impl crefs::GetCanonicalRefs for Doc {
type Error = CanonicalRefsError;
fn canonical_refs(&self) -> Result<Option<CanonicalRefs>, Self::Error> {
self.raw_canonical_refs().and_then(|raw| {
raw.map(|raw| {
raw.try_into_canonical_refs(&mut || self.delegates.clone())
.map_err(CanonicalRefsError::from)
.and_then(|mut crefs| {
self.default_branch_rule()
.map_err(CanonicalRefsError::from)
.map(|rule| {
crefs.extend([rule]);
crefs
})
})
})
.transpose()
})
}
fn raw_canonical_refs(&self) -> Result<Option<RawCanonicalRefs>, Self::Error> {
let value = self.payload.get(&PayloadId::canonical_refs());
let crefs = value
.map(|value| {
serde_json::from_value((**value).clone()).map_err(CanonicalRefsError::from)
})
.transpose()?;
Ok(crefs)
}
}
impl crefs::GetCanonicalRefs for RawDoc {
type Error = CanonicalRefsError;
fn canonical_refs(&self) -> Result<Option<CanonicalRefs>, Self::Error> {
Ok(None)
}
fn raw_canonical_refs(&self) -> Result<Option<RawCanonicalRefs>, Self::Error> {
let value = self.payload.get(&PayloadId::canonical_refs());
let crefs = value
.map(|value| {
serde_json::from_value((**value).clone()).map_err(CanonicalRefsError::from)
})
.transpose()?;
Ok(crefs)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use serde_json::json;
use crate::assert_matches;
use crate::rad;
use crate::storage::git::transport;
use crate::storage::git::Storage;
use crate::storage::{ReadStorage as _, RemoteId, WriteStorage as _};
use crate::test::arbitrary;
use crate::test::arbitrary::gen;
use crate::test::fixtures;
use super::*;
use qcheck_macros::quickcheck;
#[test]
fn test_duplicate_dids() {
let delegate = Device::mock_from_seed([0xff; 32]);
let did = Did::from(delegate.public_key());
let mut doc = RawDoc::new(gen::<Project>(1), vec![did], 1, Visibility::Public);
doc.delegate(did);
let doc = doc.verified().unwrap();
assert!(doc.delegates().len() == 1, "Duplicate DID was not removed");
assert!(doc.delegates().first() == &did)
}
#[test]
fn test_max_delegates() {
let delegates = (0..MAX_DELEGATES + 1).map(gen).collect::<Vec<Did>>();
let doc = RawDoc::new(
gen::<Project>(1),
delegates[0..MAX_DELEGATES].into(),
1,
Visibility::Public,
);
assert_matches!(doc.verified(), Ok(_));
let doc = RawDoc::new(gen::<Project>(1), delegates, 1, Visibility::Public);
assert_matches!(doc.verified(), Err(DocError::Delegates(DelegatesError(_))));
}
#[test]
fn test_is_valid_version() {
assert!(!Version::is_valid_version(&0));
let current = IDENTITY_VERSION.number();
assert!(Version::is_valid_version(¤t.into()));
let next = current.checked_add(1).unwrap();
assert!(!Version::is_valid_version(&next.into()));
}
#[test]
fn test_future_version_error() {
let v = Version(NonZeroU32::MAX).to_string();
assert_eq!(
serde_json::from_str::<Version>(&v)
.expect_err("should fail to deserialize")
.to_string(),
VersionError::UnknownVersion(NonZeroU32::MAX).to_string(),
)
}
#[test]
fn test_parse_version() {
let v1 = json!(
{
"payload": {
"xyz.radicle.project": {
"defaultBranch": "master",
"description": "Radicle Heartwood Protocol & Stack",
"name": "heartwood"
}
},
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM"
],
"threshold": 1
}
);
let doc = serde_json::from_str::<RawDoc>(&v1.to_string()).unwrap();
let payload = [(
PayloadId::project(),
Payload {
value: json!({
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"defaultBranch": "master",
}),
},
)]
.into_iter()
.collect::<BTreeMap<_, _>>();
let delegates = vec![
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT"
.parse::<Did>()
.unwrap(),
"did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW"
.parse::<Did>()
.unwrap(),
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM"
.parse::<Did>()
.unwrap(),
];
assert_eq!(
doc,
RawDoc {
version: IDENTITY_VERSION,
payload: payload.clone(),
delegates: delegates.clone(),
threshold: 1,
visibility: Visibility::Public,
}
);
let verified = serde_json::from_str::<Doc>(&v1.to_string()).unwrap();
let delegates = Delegates(NonEmpty::from_vec(delegates).unwrap());
assert_eq!(
verified,
Doc {
version: IDENTITY_VERSION,
threshold: Threshold::new(1, &delegates).unwrap(),
payload: payload.clone(),
delegates,
visibility: Visibility::Public,
}
);
}
#[test]
fn test_canonical_example() {
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 (repo, _) = fixtures::repository(tempdir.path().join("working"));
let (id, _, _) = rad::init(
&repo,
"heartwood".try_into().unwrap(),
"Radicle Heartwood Protocol & Stack",
git::fmt::refname!("master"),
Visibility::default(),
&delegate,
&storage,
)
.unwrap();
assert_eq!(
delegate.public_key().to_human(),
String::from("z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi")
);
assert_eq!(
(*id).to_string(),
"d96f425412c9f8ad5d9a9a05c9831d0728e2338d"
);
assert_eq!(id.urn(), String::from("rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji"));
}
#[test]
fn test_not_found() {
let tempdir = tempfile::tempdir().unwrap();
let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
let remote = arbitrary::gen::<RemoteId>(1);
let proj = arbitrary::gen::<RepoId>(1);
let repo = storage.create(proj).unwrap();
let oid = git::raw::Oid::from_str("2d52a53ce5e4f141148a5f770cfd3ead2d6a45b8").unwrap();
let err = repo.identity_head_of(&remote).unwrap_err();
{
use crate::git::raw::ErrorExt as _;
assert!(err.is_not_found());
}
let err = Doc::load_at(oid.into(), &repo).unwrap_err();
assert!(err.is_not_found());
}
#[test]
fn test_canonical_doc() {
let tempdir = tempfile::tempdir().unwrap();
let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
transport::local::register(storage.clone());
let (working, _) = fixtures::repository(tempdir.path().join("working"));
let delegate = Device::mock_from_seed([0xff; 32]);
let (rid, doc, _) = rad::init(
&working,
"heartwood".try_into().unwrap(),
"Radicle Heartwood Protocol & Stack",
git::fmt::refname!("master"),
Visibility::default(),
&delegate,
&storage,
)
.unwrap();
let repo = storage.repository(rid).unwrap();
assert_eq!(doc, repo.identity_doc().unwrap().doc);
}
#[quickcheck]
fn prop_encode_decode(doc: Doc) {
let (_, bytes) = doc.encode().unwrap();
assert_eq!(RawDoc::from_json(&bytes).unwrap().verified().unwrap(), doc);
}
#[test]
fn test_visibility_json() {
use std::str::FromStr;
assert_eq!(
serde_json::to_value(Visibility::Public).unwrap(),
serde_json::json!({ "type": "public" })
);
assert_eq!(
serde_json::to_value(Visibility::private([])).unwrap(),
serde_json::json!({ "type": "private" })
);
assert_eq!(
serde_json::to_value(Visibility::private([Did::from_str(
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT"
)
.unwrap()]))
.unwrap(),
serde_json::json!({ "type": "private", "allow": ["did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT"] })
);
}
}