httpd: Add patch creation endpoint

Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
Sebastian Martinez 2023-03-02 17:48:19 +01:00 committed by Alexis Sellier
parent 793c085094
commit 05d2487a47
No known key found for this signature in database
3 changed files with 144 additions and 23 deletions

View File

@ -34,6 +34,10 @@ pub enum Error {
#[error(transparent)]
CobIssue(#[from] radicle::cob::issue::Error),
/// Cob patch error.
#[error(transparent)]
CobPatch(#[from] radicle::cob::patch::Error),
/// Cob store error.
#[error(transparent)]
CobStore(#[from] radicle::cob::store::Error),

View File

@ -12,13 +12,12 @@ use serde::{Deserialize, Serialize};
use serde_json::json;
use tower_http::set_header::SetResponseHeaderLayer;
use radicle::cob::issue::{Action, Issues};
use radicle::cob::patch::Patches;
use radicle::cob::{issue, patch};
use radicle::cob::{thread, ActorId, Tag};
use radicle::identity::Id;
use radicle::node::NodeId;
use radicle::storage::git::paths;
use radicle::storage::{ReadRepository, ReadStorage};
use radicle::storage::{ReadRepository, ReadStorage, WriteRepository};
use radicle_surf::{Glob, Oid, Repository};
use crate::api::error::Error;
@ -57,7 +56,10 @@ pub fn router(ctx: Context) -> Router {
"/projects/:project/issues/:id",
patch(issue_update_handler).get(issue_handler),
)
.route("/projects/:project/patches", get(patches_handler))
.route(
"/projects/:project/patches",
post(patch_create_handler).get(patches_handler),
)
.route("/projects/:project/patches/:id", get(patch_handler))
.with_state(ctx)
}
@ -81,9 +83,9 @@ async fn project_root_handler(
let Ok((_, doc)) = repo.identity_doc() else { return None };
let Ok(doc) = doc.verified() else { return None };
let Ok(payload) = doc.project() else { return None };
let Ok(issues) = Issues::open(ctx.profile.public_key, &repo) else { return None };
let Ok(issues) = issue::Issues::open(ctx.profile.public_key, &repo) else { return None };
let Ok(issues) = issues.counts() else { return None };
let Ok(patches) = Patches::open(ctx.profile.public_key, &repo) else { return None };
let Ok(patches) = patch::Patches::open(ctx.profile.public_key, &repo) else { return None };
let Ok(patches) = patches.counts() else { return None };
let delegates = doc.delegates;
@ -393,7 +395,7 @@ async fn issues_handler(
let per_page = per_page.unwrap_or(10);
let storage = &ctx.profile.storage;
let repo = storage.repository(project)?;
let issues = Issues::open(ctx.profile.public_key, &repo)?;
let issues = issue::Issues::open(ctx.profile.public_key, &repo)?;
let mut issues: Vec<_> = issues.all()?.filter_map(|r| r.ok()).collect::<Vec<_>>();
issues.sort_by(|(_, a, _), (_, b, _)| b.timestamp().cmp(&a.timestamp()));
let issues = issues
@ -430,7 +432,7 @@ async fn issue_create_handler(
.signer()
.map_err(|_| Error::Auth("Unauthorized"))?;
let repo = storage.repository(project)?;
let mut issues = Issues::open(ctx.profile.public_key, &repo)?;
let mut issues = issue::Issues::open(ctx.profile.public_key, &repo)?;
let issue = issues
.create(
issue.title,
@ -453,7 +455,7 @@ async fn issue_update_handler(
State(ctx): State<Context>,
AuthBearer(token): AuthBearer,
Path((project, issue_id)): Path<(Id, Oid)>,
Json(action): Json<Action>,
Json(action): Json<issue::Action>,
) -> impl IntoResponse {
ctx.sessions
.write()
@ -464,24 +466,24 @@ async fn issue_update_handler(
let storage = &ctx.profile.storage;
let signer = ctx.profile.signer().unwrap();
let repo = storage.repository(project)?;
let mut issues = Issues::open(ctx.profile.public_key, &repo)?;
let mut issues = issue::Issues::open(ctx.profile.public_key, &repo)?;
let mut issue = issues.get_mut(&issue_id.into())?;
match action {
Action::Assign { add, remove } => {
issue::Action::Assign { add, remove } => {
issue.assign(add, &signer)?;
issue.unassign(remove, &signer)?;
}
Action::Lifecycle { state } => {
issue::Action::Lifecycle { state } => {
issue.lifecycle(state, &signer)?;
}
Action::Tag { add, remove } => {
issue::Action::Tag { add, remove } => {
issue.tag(add, remove, &signer)?;
}
Action::Edit { title } => {
issue::Action::Edit { title } => {
issue.edit(title, &signer)?;
}
Action::Thread { action } => match action {
issue::Action::Thread { action } => match action {
thread::Action::Comment { body, reply_to } => {
if let Some(reply_to) = reply_to {
issue.comment(body, reply_to, &signer)?;
@ -512,13 +514,62 @@ async fn issue_handler(
) -> impl IntoResponse {
let storage = &ctx.profile.storage;
let repo = storage.repository(project)?;
let issue = Issues::open(ctx.profile.public_key, &repo)?
let issue = issue::Issues::open(ctx.profile.public_key, &repo)?
.get(&issue_id.into())?
.ok_or(Error::NotFound)?;
Ok::<_, Error>(Json(api::json::issue(issue_id.into(), issue)))
}
#[derive(Deserialize, Serialize)]
pub struct PatchCreate {
pub title: String,
pub description: String,
pub target: Oid,
pub oid: Oid,
pub tags: Vec<Tag>,
}
/// Create a new patch.
/// `POST /projects/:project/patches`
async fn patch_create_handler(
State(ctx): State<Context>,
AuthBearer(token): AuthBearer,
Path(project): Path<Id>,
Json(patch): Json<PatchCreate>,
) -> impl IntoResponse {
ctx.sessions
.read()
.await
.get(&token)
.ok_or(Error::Auth("Unauthorized"))?;
let storage = &ctx.profile.storage;
let signer = ctx
.profile
.signer()
.map_err(|_| Error::Auth("Unauthorized"))?;
let repo = storage.repository(project)?;
let mut patches = patch::Patches::open(ctx.profile.public_key, &repo)?;
let base_oid = repo.raw().merge_base(*patch.target, *patch.oid)?;
let patch = patches
.create(
patch.title,
patch.description,
patch::MergeTarget::default(),
base_oid,
patch.oid,
&patch.tags,
&signer,
)
.map_err(Error::from)?;
Ok::<_, Error>((
StatusCode::CREATED,
Json(json!({ "success": true, "id": patch.id.to_string() })),
))
}
/// Get project patches list.
/// `GET /projects/:project/patches`
async fn patches_handler(
@ -531,7 +582,7 @@ async fn patches_handler(
let per_page = per_page.unwrap_or(10);
let storage = &ctx.profile.storage;
let repo = storage.repository(project)?;
let patches = Patches::open(ctx.profile.public_key, &repo)?;
let patches = patch::Patches::open(ctx.profile.public_key, &repo)?;
let mut patches = patches.all()?.filter_map(|r| r.ok()).collect::<Vec<_>>();
patches.sort_by(|(_, a, _), (_, b, _)| b.timestamp().cmp(&a.timestamp()));
let patches = patches
@ -552,7 +603,7 @@ async fn patch_handler(
) -> impl IntoResponse {
let storage = &ctx.profile.storage;
let repo = storage.repository(project)?;
let patch = Patches::open(ctx.profile.public_key, &repo)?
let patch = patch::Patches::open(ctx.profile.public_key, &repo)?
.get(&patch_id.into())?
.ok_or(Error::NotFound)?;
@ -566,7 +617,7 @@ mod routes {
use pretty_assertions::assert_eq;
use serde_json::json;
use crate::test::{self, get, patch, post, HEAD, HEAD_1, ISSUE_ID, PATCH_ID};
use crate::test::{self, get, patch, post, HEAD, HEAD_1, ISSUE_ID, PATCH_ID, SESSION_ID};
const CREATED_ISSUE_ID: &str = "745052a1603000b9566445753d7e2fee1ff5041f";
@ -1081,7 +1132,7 @@ mod routes {
&app,
"/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/issues",
Some(Body::from(body)),
Some("u9MGAkkfkMOv0uDDB2WeUHBT7HbsO2Dy".to_string()),
Some(SESSION_ID.to_string()),
)
.await;
@ -1144,7 +1195,7 @@ mod routes {
&app,
format!("/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/issues/{ISSUE_ID}"),
Some(Body::from(body)),
Some("u9MGAkkfkMOv0uDDB2WeUHBT7HbsO2Dy".to_string()),
Some(SESSION_ID.to_string()),
)
.await;
@ -1214,7 +1265,7 @@ mod routes {
&app,
format!("/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/issues/{ISSUE_ID}"),
Some(Body::from(body)),
Some("u9MGAkkfkMOv0uDDB2WeUHBT7HbsO2Dy".to_string()),
Some(SESSION_ID.to_string()),
)
.await;
@ -1328,4 +1379,69 @@ mod routes {
)
);
}
#[tokio::test]
async fn test_projects_create_patches() {
let tmp = tempfile::tempdir().unwrap();
let ctx = test::seed(tmp.path());
let app = super::router(ctx.to_owned());
test::create_session(ctx).await;
let body = serde_json::to_vec(&json!({
"title": "Update README",
"description": "Do some changes to README",
"target": HEAD,
"oid": HEAD_1,
"tags": [],
}))
.unwrap();
let response = post(
&app,
"/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/patches",
Some(Body::from(body)),
Some(SESSION_ID.to_string()),
)
.await;
assert_eq!(response.status(), StatusCode::CREATED);
assert_eq!(
response.json().await,
json!(
{
"success": true,
"id": "cd370c8a263e8b0c9836d6a5dd3bf7a633d69acf",
}
)
);
let response = get(
&app,
"/projects/rad:z4FucBZHZMCsxTyQE1dfE2YR59Qbp/patches/cd370c8a263e8b0c9836d6a5dd3bf7a633d69acf",
)
.await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.json().await,
json!(
{
"id": "cd370c8a263e8b0c9836d6a5dd3bf7a633d69acf",
"author": {
"id": "did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
},
"title": "Update README",
"description": "Do some changes to README",
"state": "proposed",
"target": "delegates",
"tags": [],
"revisions": [
{
"id": "a65512fc3b8ee7ec50f053c40266538fa11b82dd",
"description": "",
"reviews": [],
}
],
}
)
);
}
}

View File

@ -25,6 +25,7 @@ pub const HEAD: &str = "1e978d19f251cd9821d9d9a76d1bd436bf0690d5";
pub const HEAD_1: &str = "f604ce9fd5b7cc77b7609beda45ea8760bee78f7";
pub const PATCH_ID: &str = "afb3063f8f0343fa31d2a0d55bac2a6f4a77125e";
pub const ISSUE_ID: &str = "331569cd5e4dcc55104363ebce92c78b0e5d67d4";
pub const SESSION_ID: &str = "u9MGAkkfkMOv0uDDB2WeUHBT7HbsO2Dy";
const PASSWORD: &str = "radicle";
@ -144,7 +145,7 @@ pub async fn create_session(ctx: Context) {
let issued_at = OffsetDateTime::now_utc();
let mut sessions = ctx.sessions().write().await;
sessions.insert(
String::from("u9MGAkkfkMOv0uDDB2WeUHBT7HbsO2Dy"),
String::from(SESSION_ID),
auth::Session {
status: auth::AuthState::Authorized,
public_key: ctx.profile().public_key,