From 472f38bacfe3d53aae598f69e4d59ce77a60d4d9 Mon Sep 17 00:00:00 2001 From: cloudhead Date: Tue, 3 Oct 2023 10:35:29 +0200 Subject: [PATCH] cob: Introduce `xyz.radicle.id` COB Introduces a new COB type to store repository identity documents. The reason for this change is to: 1. Simplify the code, as the identity document logic resembled a COB, yet it had custom logic. This allows existing COB code to be used for identities. 2. Make identity document update logic more flexible, since COB actions can be added in the future. 3. Re-purpose existing tools around COBs to work on identities, eg. `rad cob`. 4. Unify the concept of an identity change proposal, with regular identity changes. This means we can remove the `id.proposal` COB in favor of using the `id` COB itself. Notes ----- * Each repository has one Identity COB. * The `Proposal` COB has been repurposed into the `Identity` COB. * Identity documents are stored as *embeds* inside the Identity COB actions. * The action that contains new document versions is called `revision`, just like the Patches COB. * The namespaced `rad/id` ref is a symbolic reference to that Identity COB. * The canonical `rad/id` ref is a direct reference to the commit in the Identity COB that contains the latest *accepted* revision of the document. * All commands for managing identities have been folded into `rad id`. Hence `rad delegate` and `rad edit` are removed. * The concept of "rebasing" an identity document is gone. * The `rad id` output has been updated to match the style of other commands. * When a revision has enough signatures, it is automatically adopted as the current identity, there is no longer the need for a "commit" action. * When an identity revision is proposed, and the current identity has a threshold of `1`, that identity is automatically accepted due to the above point. * The idea of "verifying a peer's identity branch" no longer applies, as COBs cannot be verified one history at a time. * Since the root commit of the Identity COB does not have a "Resource" to point to (it would have to point to itself, which is impossible), we've made the resource id optional for COBs. --- radicle-httpd/src/api.rs | 4 +- radicle-httpd/src/api/error.rs | 9 +- radicle-httpd/src/api/v1/delegates.rs | 9 +- radicle-httpd/src/api/v1/projects.rs | 160 +++++++++++--------------- radicle-httpd/src/lib.rs | 1 + radicle-httpd/src/test.rs | 38 ++++-- 6 files changed, 100 insertions(+), 121 deletions(-) diff --git a/radicle-httpd/src/api.rs b/radicle-httpd/src/api.rs index d2ad2c73e..9048c1b9a 100644 --- a/radicle-httpd/src/api.rs +++ b/radicle-httpd/src/api.rs @@ -17,7 +17,7 @@ use tower_http::cors::{self, CorsLayer}; use radicle::cob::patch; use radicle::cob::{issue, Uri}; -use radicle::identity::Id; +use radicle::identity::{DocAt, Id}; use radicle::node::routing::Store; use radicle::storage::{ReadRepository, ReadStorage}; use radicle::Profile; @@ -54,7 +54,7 @@ impl Context { let storage = &self.profile.storage; let repo = storage.repository(id)?; let (_, head) = repo.head()?; - let doc = repo.identity_doc()?.1.verified()?; + let DocAt { doc, .. } = repo.identity_doc()?; let payload = doc.project()?; let delegates = doc.delegates; let issues = issue::Issues::open(&repo)?.counts()?; diff --git a/radicle-httpd/src/api/error.rs b/radicle-httpd/src/api/error.rs index 63535a47b..6a9696fb6 100644 --- a/radicle-httpd/src/api/error.rs +++ b/radicle-httpd/src/api/error.rs @@ -42,9 +42,9 @@ pub enum Error { #[error(transparent)] CobStore(#[from] radicle::cob::store::Error), - /// Identity error. + /// Repository error. #[error(transparent)] - Identity(#[from] radicle::identity::IdentityError), + Repository(#[from] radicle::storage::RepositoryError), /// Project doc error. #[error(transparent)] @@ -115,10 +115,7 @@ impl IntoResponse for Error { tracing::error!("Error: {message}"); if cfg!(debug_assertions) { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Some(format!("{other:?}")), - ) + (StatusCode::INTERNAL_SERVER_ERROR, Some(other.to_string())) } else { (StatusCode::INTERNAL_SERVER_ERROR, None) } diff --git a/radicle-httpd/src/api/v1/delegates.rs b/radicle-httpd/src/api/v1/delegates.rs index 3a5e91195..aa584adf3 100644 --- a/radicle-httpd/src/api/v1/delegates.rs +++ b/radicle-httpd/src/api/v1/delegates.rs @@ -5,7 +5,7 @@ use axum::{Json, Router}; use radicle::cob::issue::Issues; use radicle::cob::patch::Patches; -use radicle::identity::Did; +use radicle::identity::{Did, DocAt}; use radicle::node::routing::Store; use radicle::storage::{ReadRepository, ReadStorage}; @@ -42,12 +42,10 @@ async fn delegates_projects_handler( .filter_map(|id| { let Ok(repo) = storage.repository(id) else { return None }; let Ok((_, head)) = repo.head() else { return None }; - let Ok((_, doc)) = repo.identity_doc() else { return None }; - let Ok(doc) = doc.verified() else { return None }; + let Ok(DocAt { doc, .. }) = repo.identity_doc() else { return None }; let Ok(payload) = doc.project() else { return None }; - let delegates = doc.delegates; - if !delegates.iter().any(|d| *d == delegate) { + if !doc.delegates.iter().any(|d| *d == delegate) { return None; } @@ -56,6 +54,7 @@ async fn delegates_projects_handler( let Ok(patches) = Patches::open(&repo) else { return None }; let Ok(patches) = patches.counts() else { return None }; + let delegates = doc.delegates; let trackings = routing.count(&id).unwrap_or_default(); Some(Info { diff --git a/radicle-httpd/src/api/v1/projects.rs b/radicle-httpd/src/api/v1/projects.rs index c9c05959b..33e2a2a7f 100644 --- a/radicle-httpd/src/api/v1/projects.rs +++ b/radicle-httpd/src/api/v1/projects.rs @@ -14,7 +14,7 @@ use serde_json::{json, Value}; use tower_http::set_header::SetResponseHeaderLayer; use radicle::cob::{issue, patch, Embed, Label, Uri}; -use radicle::identity::{Did, Id}; +use radicle::identity::{Did, DocAt, Id}; use radicle::node::routing::Store; use radicle::node::AliasStore; use radicle::node::NodeId; @@ -89,8 +89,7 @@ async fn project_root_handler( .filter_map(|id| { let Ok(repo) = storage.repository(id) else { return None }; let Ok((_, head)) = repo.head() else { return None }; - let Ok((_, doc)) = repo.identity_doc() else { return None }; - let Ok(doc) = doc.verified() else { return None }; + let Ok(DocAt { doc, .. }) = repo.identity_doc() else { return None }; let Ok(payload) = doc.project() else { return None }; let Ok(issues) = issue::Issues::open(&repo) else { return None }; let Ok(issues) = issues.counts() else { return None }; @@ -686,24 +685,16 @@ async fn issue_update_handler( api::auth::validate(&ctx, &token).await?; let storage = &ctx.profile.storage; - let signer = ctx.profile.signer().unwrap(); + let signer = ctx.profile.signer()?; let repo = storage.repository(project)?; let mut issues = issue::Issues::open(&repo)?; let mut issue = issues.get_mut(&issue_id.into())?; - match action { - issue::Action::Assign { assignees } => { - issue.assign(assignees, &signer)?; - } - issue::Action::Lifecycle { state } => { - issue.lifecycle(state, &signer)?; - } - issue::Action::Label { labels } => { - issue.label(labels, &signer)?; - } - issue::Action::Edit { title } => { - issue.edit(title, &signer)?; - } + let id = match action { + issue::Action::Assign { assignees } => issue.assign(assignees, &signer)?, + issue::Action::Lifecycle { state } => issue.lifecycle(state, &signer)?, + issue::Action::Label { labels } => issue.label(labels, &signer)?, + issue::Action::Edit { title } => issue.edit(title, &signer)?, issue::Action::Comment { body, reply_to, @@ -720,7 +711,7 @@ async fn issue_update_handler( }) .collect(); if let Some(to) = reply_to { - issue.comment(body, to, embeds, &signer)?; + issue.comment(body, to, embeds, &signer)? } else { return Err(Error::BadRequest("`replyTo` missing".to_owned())); } @@ -729,9 +720,7 @@ async fn issue_update_handler( id, reaction, active, - } => { - issue.react(id, reaction, active, &signer)?; - } + } => issue.react(id, reaction, active, &signer)?, issue::Action::CommentEdit { id, body, embeds } => { let embeds: Vec = embeds .into_iter() @@ -743,14 +732,12 @@ async fn issue_update_handler( }) }) .collect(); - issue.edit_comment(id, body, embeds, &signer)?; - } - issue::Action::CommentRedact { id } => { - issue.redact_comment(id, &signer)?; + issue.edit_comment(id, body, embeds, &signer)? } + issue::Action::CommentRedact { id } => issue.redact_comment(id, &signer)?, }; - Ok::<_, Error>(Json(json!({ "success": true }))) + Ok::<_, Error>(Json(json!({ "success": true, "id": id }))) } /// Get project issue. @@ -830,41 +817,27 @@ async fn patch_update_handler( let repo = storage.repository(project)?; let mut patches = patch::Patches::open(&repo)?; let mut patch = patches.get_mut(&patch_id.into())?; - match action { - patch::Action::Edit { title, target } => { - patch.edit(title, target, &signer)?; - } - patch::Action::Label { labels } => { - patch.label(labels, &signer)?; - } - patch::Action::Lifecycle { state } => { - patch.lifecycle(state, &signer)?; - } - patch::Action::Assign { assignees } => { - patch.assign(assignees, &signer)?; - } + let id = match action { + patch::Action::Edit { title, target } => patch.edit(title, target, &signer)?, + patch::Action::Label { labels } => patch.label(labels, &signer)?, + patch::Action::Lifecycle { state } => patch.lifecycle(state, &signer)?, + patch::Action::Assign { assignees } => patch.assign(assignees, &signer)?, patch::Action::Merge { revision, commit } => { // TODO: We should cleanup the stored copy at least. - let _ = patch.merge(revision, commit, &signer)?; + patch.merge(revision, commit, &signer)?.entry } patch::Action::Review { revision, summary, verdict, labels, - } => { - patch.review(revision, verdict, summary, labels, &signer)?; - } + } => *patch.review(revision, verdict, summary, labels, &signer)?, patch::Action::ReviewEdit { review, summary, verdict, - } => { - patch.edit_review(review, summary, verdict, &signer)?; - } - patch::Action::ReviewRedact { review } => { - patch.redact_review(review, &signer)?; - } + } => patch.edit_review(review, summary, verdict, &signer)?, + patch::Action::ReviewRedact { review } => patch.redact_review(review, &signer)?, patch::Action::ReviewComment { review, body, @@ -882,7 +855,7 @@ async fn patch_update_handler( }) }) .collect(); - patch.review_comment(review, body, location, reply_to, embeds, &signer)?; + patch.review_comment(review, body, location, reply_to, embeds, &signer)? } patch::Action::ReviewCommentEdit { review, @@ -900,42 +873,34 @@ async fn patch_update_handler( }) }) .collect(); - patch.edit_review_comment(review, comment, body, embeds, &signer)?; + patch.edit_review_comment(review, comment, body, embeds, &signer)? } patch::Action::ReviewCommentReact { review, comment, reaction, active, - } => { - patch.react_review_comment(review, comment, reaction, active, &signer)?; - } + } => patch.react_review_comment(review, comment, reaction, active, &signer)?, patch::Action::ReviewCommentRedact { review, comment } => { - patch.redact_review_comment(review, comment, &signer)?; + patch.redact_review_comment(review, comment, &signer)? } patch::Action::ReviewCommentResolve { review, comment } => { - patch.resolve_review_comment(review, comment, &signer)?; + patch.resolve_review_comment(review, comment, &signer)? } patch::Action::ReviewCommentUnresolve { review, comment } => { - patch.unresolve_review_comment(review, comment, &signer)?; + patch.unresolve_review_comment(review, comment, &signer)? } patch::Action::Revision { description, base, oid, .. - } => { - patch.update(description, base, oid, &signer)?; - } + } => patch.update(description, base, oid, &signer)?.into(), patch::Action::RevisionEdit { revision, description, - } => { - patch.edit_revision(revision, description, &signer)?; - } - patch::Action::RevisionRedact { revision } => { - patch.redact(revision, &signer)?; - } + } => patch.edit_revision(revision, description, &signer)?, + patch::Action::RevisionRedact { revision } => patch.redact(revision, &signer)?, patch::Action::RevisionComment { revision, body, @@ -953,7 +918,7 @@ async fn patch_update_handler( }) }) .collect(); - patch.comment(revision, body, reply_to, location, embeds, &signer)?; + patch.comment(revision, body, reply_to, location, embeds, &signer)? } patch::Action::RevisionCommentEdit { revision, @@ -971,25 +936,23 @@ async fn patch_update_handler( }) }) .collect(); - patch.comment_edit(revision, comment, body, embeds, &signer)?; + patch.comment_edit(revision, comment, body, embeds, &signer)? } patch::Action::RevisionCommentReact { revision, comment, reaction, active, - } => { - patch.comment_react(revision, comment, reaction, active, &signer)?; - } + } => patch.comment_react(revision, comment, reaction, active, &signer)?, patch::Action::RevisionCommentRedact { revision, comment } => { - patch.comment_redact(revision, comment, &signer)?; + patch.comment_redact(revision, comment, &signer)? } _ => { todo!(); } }; - Ok::<_, Error>(Json(json!({ "success": true }))) + Ok::<_, Error>(Json(json!({ "success": true, "id": id }))) } /// Get project patches list. @@ -2058,7 +2021,7 @@ mod routes { #[tokio::test] async fn test_projects_issues_create() { - const CREATED_ISSUE_ID: &str = "e712eb0b5874d5256022fb620f26caf847d96723"; + const CREATED_ISSUE_ID: &str = "8b42657072f6192cba9e08561582576a975656cd"; let tmp = tempfile::tempdir().unwrap(); let ctx = contributor(tmp.path()); @@ -2166,7 +2129,7 @@ mod routes { .await; assert_eq!(response.status(), StatusCode::OK); - assert_eq!(response.json().await, json!({ "success": true })); + assert_eq!(response.success().await, true); let body = serde_json::to_vec(&json!({ "type": "comment.react", @@ -2199,11 +2162,11 @@ mod routes { ) .await; - assert_eq!(response.json().await, json!({ "success": true })); + assert_eq!(response.success().await, true); let body = serde_json::to_vec(&json!({ "type": "comment.redact", - "id": "6fe9fdec2ec9f6436f2875dbcbedb95dd215b863", + "id": "918fff44966e4305523c077c80ac93b1196f1a7e", })) .unwrap(); @@ -2215,7 +2178,7 @@ mod routes { ) .await; - assert_eq!(response.json().await, json!({ "success": true })); + assert_eq!(response.success().await, true); let response = get( &app, @@ -2284,7 +2247,7 @@ mod routes { .await; assert_eq!(response.status(), StatusCode::OK); - assert_eq!(response.json().await, json!({ "success": true })); + assert_eq!(response.success().await, true); let response = get( &app, @@ -2423,7 +2386,7 @@ mod routes { #[tokio::test] async fn test_projects_create_patches() { - const CREATED_PATCH_ID: &str = "9cffd66099cceb0439a0f67c4aa99bde5e868eaa"; + const CREATED_PATCH_ID: &str = "beaed2e1d3b9b01ef10326a9a1c951799ba5fb25"; let tmp = tempfile::tempdir().unwrap(); let ctx = contributor(tmp.path()); @@ -2686,7 +2649,7 @@ mod routes { "reviews": [], }, { - "id": "b1f68feacb7040b089a77c1a0bff60a0411e6c1e", + "id": "341ba93c6db54e5891fbd3be4a4f64f4715681fa", "author": { "id": CONTRIBUTOR_DID, }, @@ -2857,10 +2820,12 @@ mod routes { .await; assert_eq!(response.status(), StatusCode::OK); + + let comment_id = response.id().await.to_string(); let comment_react_body = serde_json::to_vec(&json!({ "type": "revision.comment.react", "revision": CONTRIBUTOR_PATCH_ID, - "comment": CONTRIBUTOR_COMMENT_1, + "comment": comment_id, "reaction": "🚀", "active": true })) @@ -2876,7 +2841,7 @@ mod routes { let comment_edit = serde_json::to_vec(&json!({ "type": "revision.comment.edit", "revision": CONTRIBUTOR_PATCH_ID, - "comment": CONTRIBUTOR_COMMENT_1, + "comment": comment_id, "body": "EDIT: This is a root level comment", "embeds": [ { @@ -2899,7 +2864,7 @@ mod routes { "type": "revision.comment", "revision": CONTRIBUTOR_PATCH_ID, "body": "This is a root level comment", - "replyTo": CONTRIBUTOR_COMMENT_1, + "replyTo": comment_id, "embeds": [], })) .unwrap(); @@ -2912,6 +2877,7 @@ mod routes { .await; assert_eq!(response.status(), StatusCode::OK); + let comment_id_2 = response.id().await.to_string(); let response = get( &app, @@ -2946,7 +2912,7 @@ mod routes { ], "discussions": [ { - "id": CONTRIBUTOR_COMMENT_1, + "id": comment_id, "author": { "id": CONTRIBUTOR_DID, }, @@ -2963,7 +2929,7 @@ mod routes { "resolved": false, }, { - "id": CONTRIBUTOR_COMMENT_2, + "id": comment_id_2, "author": { "id": CONTRIBUTOR_DID, }, @@ -2971,7 +2937,7 @@ mod routes { "embeds": [], "reactions": [], "timestamp": TIMESTAMP, - "replyTo": CONTRIBUTOR_COMMENT_1, + "replyTo": comment_id, "resolved": false, }, ], @@ -3006,9 +2972,10 @@ mod routes { assert_eq!(response.status(), StatusCode::OK); + let review_id = response.id().await.to_string(); let review_comment_body = serde_json::to_vec(&json!({ "type": "review.comment", - "review": CONTRIBUTOR_PATCH_REVIEW, + "review": review_id, "body": "This is a comment on a review", "embeds": [], "location": { @@ -3023,7 +2990,7 @@ mod routes { } })) .unwrap(); - patch( + let response = patch( &app, format!("/projects/{CONTRIBUTOR_RID}/patches/{CONTRIBUTOR_PATCH_ID}"), Some(Body::from(review_comment_body)), @@ -3031,10 +2998,11 @@ mod routes { ) .await; + let comment_id = response.id().await.to_string(); let review_comment_edit_body = serde_json::to_vec(&json!({ "type": "review.comment.edit", - "review": CONTRIBUTOR_PATCH_REVIEW, - "comment": CONTRIBUTOR_COMMENT_3, + "review": review_id, + "comment": comment_id, "embeds": [], "body": "EDIT: This is a comment on a review", })) @@ -3049,8 +3017,8 @@ mod routes { let review_react_body = serde_json::to_vec(&json!({ "type": "review.comment.react", - "review": CONTRIBUTOR_PATCH_REVIEW, - "comment": CONTRIBUTOR_COMMENT_3, + "review": review_id, + "comment": comment_id, "reaction": "🚀", "active": true })) @@ -3065,8 +3033,8 @@ mod routes { let review_resolve_body = serde_json::to_vec(&json!({ "type": "review.comment.resolve", - "review": CONTRIBUTOR_PATCH_REVIEW, - "comment": CONTRIBUTOR_COMMENT_3, + "review": review_id, + "comment": comment_id, })) .unwrap(); patch( @@ -3118,7 +3086,7 @@ mod routes { "verdict": "accept", "summary": "A small review", "comments": [[ - CONTRIBUTOR_COMMENT_3, + comment_id, { "author": CONTRIBUTOR_NID, "location": { diff --git a/radicle-httpd/src/lib.rs b/radicle-httpd/src/lib.rs index 83b138c80..050d4fd8c 100644 --- a/radicle-httpd/src/lib.rs +++ b/radicle-httpd/src/lib.rs @@ -143,6 +143,7 @@ pub mod logger { pub fn subscriber() -> impl tracing::Subscriber { tracing_subscriber::FmtSubscriber::builder() .with_target(false) + .with_max_level(tracing::Level::DEBUG) .finish() } } diff --git a/radicle-httpd/src/test.rs b/radicle-httpd/src/test.rs index c95ec271c..395c7eecf 100644 --- a/radicle-httpd/src/test.rs +++ b/radicle-httpd/src/test.rs @@ -34,20 +34,16 @@ pub const HEAD: &str = "e8c676b9e3b42308dc9d218b70faa5408f8e58ca"; pub const PARENT: &str = "ee8d6a29304623a78ebfa5eeed5af674d0e58f83"; pub const INITIAL_COMMIT: &str = "f604ce9fd5b7cc77b7609beda45ea8760bee78f7"; pub const DID: &str = "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"; -pub const ISSUE_ID: &str = "0b0b8ca3b75e109971f87d92c1a6c930e87484c6"; -pub const ISSUE_DISCUSSION_ID: &str = "7466975f0bef37b459887824a9655f3e78262522"; -pub const ISSUE_COMMENT_ID: &str = "24ee306c508cd731a8427612dbdd826209096f99"; +pub const ISSUE_ID: &str = "4f98396a1ac987af59ec069de9b80d9917b27050"; +pub const ISSUE_DISCUSSION_ID: &str = "ceafc6629ec8dc0a17644fb5a66726aaafc3ed1c"; +pub const ISSUE_COMMENT_ID: &str = "59d35e164a21502bc91ad3391ce49baa32ea6a74"; pub const SESSION_ID: &str = "u9MGAkkfkMOv0uDDB2WeUHBT7HbsO2Dy"; pub const TIMESTAMP: u64 = 1671125284; pub const CONTRIBUTOR_RID: &str = "rad:z4XaCmN3jLSeiMvW15YTDpNbDHFhG"; pub const CONTRIBUTOR_DID: &str = "did:key:z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8"; pub const CONTRIBUTOR_NID: &str = "z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8"; -pub const CONTRIBUTOR_ISSUE_ID: &str = "7466975f0bef37b459887824a9655f3e78262522"; -pub const CONTRIBUTOR_PATCH_ID: &str = "e651ae5869a2c1ac8ad4f6deae4cc835656ffa25"; -pub const CONTRIBUTOR_PATCH_REVIEW: &str = "ee3eeba95f4ec418b3d0714e18e0d1ff605dc0e6"; -pub const CONTRIBUTOR_COMMENT_1: &str = "d8ff07edbc8d2229e54e70f2f5bc31614287f0dc"; -pub const CONTRIBUTOR_COMMENT_2: &str = "f3fca0add53f85bc51a85198efed3273fe13b88e"; -pub const CONTRIBUTOR_COMMENT_3: &str = "06990ff59faa12463f693dae7a98eb33d75afd2e"; +pub const CONTRIBUTOR_ISSUE_ID: &str = "ceafc6629ec8dc0a17644fb5a66726aaafc3ed1c"; +pub const CONTRIBUTOR_PATCH_ID: &str = "4ff2ec53a2d165da7f54705023e847d4f9230bc3"; /// Create a new profile. pub fn profile(home: &Path, seed: [u8; 32]) -> radicle::Profile { @@ -96,6 +92,8 @@ fn seed_with_signer(dir: &Path, profile: radicle::Profile, signer: &G let routing_db = dir.join("radicle").join("node").join("routing.db"); let addresses_db = dir.join("radicle").join("node").join("addresses.db"); + crate::logger::init().ok(); + TrackingStore::Config::open(tracking_db).unwrap(); RoutingStore::Table::open(routing_db).unwrap(); AddressStore::Book::open(addresses_db).unwrap(); @@ -191,7 +189,7 @@ fn seed_with_signer(dir: &Path, profile: radicle::Profile, signer: &G let storage = &profile.storage; let repo = storage.repository(id).unwrap(); let mut issues = Issues::open(&repo).unwrap(); - let _ = issues + let issue = issues .create( "Issue #1".to_string(), "Change 'hello world' to 'hello everyone'".to_string(), @@ -201,12 +199,13 @@ fn seed_with_signer(dir: &Path, profile: radicle::Profile, signer: &G signer, ) .unwrap(); + tracing::debug!(target: "test", "Contributor issue: {}", issue.id()); // eq. rad patch open let mut patches = Patches::open(&repo).unwrap(); let oid = radicle::git::Oid::from_str(HEAD).unwrap(); let base = radicle::git::Oid::from_str(PARENT).unwrap(); - let _ = patches + let patch = patches .create( "A new `hello world`", "change `hello world` in README to something else", @@ -217,6 +216,7 @@ fn seed_with_signer(dir: &Path, profile: radicle::Profile, signer: &G signer, ) .unwrap(); + tracing::debug!(target: "test", "Contributor patch: {}", patch.id()); let options = crate::Options { aliases: std::collections::HashMap::new(), @@ -318,10 +318,24 @@ pub struct Response(axum::response::Response); impl Response { pub async fn json(self) -> Value { - let body = hyper::body::to_bytes(self.0.into_body()).await.unwrap(); + let body = self.body().await; serde_json::from_slice(&body).unwrap() } + pub async fn id(self) -> radicle::git::Oid { + let json = self.json().await; + let string = json["id"].as_str().unwrap(); + + radicle::git::Oid::from_str(string).unwrap() + } + + pub async fn success(self) -> bool { + let json = self.json().await; + let success = json["success"].as_bool(); + + success.unwrap_or(false) + } + pub fn status(&self) -> axum::http::StatusCode { self.0.status() }