From 604d887164382a6779b20517f3be48c43de55429 Mon Sep 17 00:00:00 2001 From: realaravinth Date: Sun, 20 Feb 2022 01:04:13 +0530 Subject: [PATCH] feat: REST endpoint to delete comment by ID DESCRIPTION Each comment is uniquely identified by database assigned, serially incremented ID. Access controlled REST endpoint is added to delete comment by ID. ERRORS RETURNED - Gist doesn't exist: 404 GistNotFound - Gist is private and requesting user is not owner or is not visible to user: 404 GistNotFound - Gist exists and is visible to requesting user but comment doesn't exist: 404 CommentNotFound - Gist exists and is visible to requesting user is not comment owner : 401 UnauthorizedOperation --- src/api/v1/gists.rs | 95 ++++++++++++++++++++++++++++++++++++++++++++ src/api/v1/routes.rs | 22 +++++++++- 2 files changed, 116 insertions(+), 1 deletion(-) diff --git a/src/api/v1/gists.rs b/src/api/v1/gists.rs index fbe01cc..c591b6b 100644 --- a/src/api/v1/gists.rs +++ b/src/api/v1/gists.rs @@ -50,6 +50,7 @@ pub fn services(cfg: &mut web::ServiceConfig) { cfg.service(post_comment); cfg.service(get_comment); cfg.service(get_gist_comments); + cfg.service(delete_comment); } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -212,6 +213,33 @@ async fn get_gist_comments( } } +#[my_codegen::delete( + path = "crate::V1_API_ROUTES.gist.delete_comment", + wrap = "super::get_auth_middleware()" +)] +async fn delete_comment( + path: web::Path, + id: Identity, + db: crate::DB, +) -> ServiceResult { + let gist = db.get_gist(&path.gist).await?; + let comment = db.get_comment_by_id(path.comment_id).await?; + let username = id.identity().unwrap(); + if username != comment.owner { + match gist.visibility { + GistVisibility::Public | GistVisibility::Unlisted => { + Err(ServiceError::UnauthorizedOperation( + "This user is not the owner of the comment to delete it".into(), + )) + } + GistVisibility::Private => Err(ServiceError::GistNotFound), + } + } else { + db.delete_comment(&username, comment.id).await?; + Ok(HttpResponse::Ok()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -684,5 +712,72 @@ mod tests { ); } } + + /* + * +++++++++++++++++++++++++++++++++++ + * DELETE COMMENT + * +++++++++++++++++++++++++++++++++++ + */ + + let mut delete_comment_component = GetCommentPath { + gist: "gistdoesntexist".into(), + username: NAME.into(), + comment_id: 34234234, + }; + + println!("delete comments; unauthenticated, gist does't exist"); + let del_comment_path = V1_API_ROUTES + .gist + .get_delete_comment_route(&delete_comment_component); + let resp = delete_request!(&app, &del_comment_path); + assert_eq!(resp.status(), StatusCode::FOUND); + + println!("delete comments; authenticated, gist does't exist"); + let resp = delete_request!(&app, &del_comment_path, cookies.clone()); + assert_eq!(resp.status(), ServiceError::GistNotFound.status_code()); + let err: ErrorToResponse = test::read_body_json(resp).await; + assert_eq!(err.error, format!("{}", ServiceError::GistNotFound)); + + println!("delete comments; authenticated, comment doesn't exist"); + delete_comment_component.gist = gist_id.clone(); + let del_comment_path = V1_API_ROUTES + .gist + .get_delete_comment_route(&delete_comment_component); + let resp = delete_request!(&app, &del_comment_path, cookies.clone()); + assert_eq!(resp.status(), ServiceError::CommentNotFound.status_code()); + let err: ErrorToResponse = test::read_body_json(resp).await; + assert_eq!(err.error, format!("{}", ServiceError::CommentNotFound)); + + println!("delete comments; authenticated but comment_owner != user and gist is public"); + delete_comment_component.gist = gist_id.clone(); + delete_comment_component.comment_id = comment_ids.get(0).as_ref().unwrap().0.id; + let del_comment_path = V1_API_ROUTES + .gist + .get_delete_comment_route(&delete_comment_component); + let resp = delete_request!(&app, &del_comment_path, cookies2.clone()); + assert_eq!( + resp.status(), + ServiceError::UnauthorizedOperation("".into()).status_code() + ); + let err: ErrorToResponse = test::read_body_json(resp).await; + assert!(err.error.contains(&format!( + "{}", + ServiceError::UnauthorizedOperation("".into()) + ))); + + println!("delete comments; authenticated but comment_owner != user and gist is private"); + delete_comment_component.gist = private.clone(); + delete_comment_component.comment_id = comment_ids.last().as_ref().unwrap().0.id; + let del_comment_path = V1_API_ROUTES + .gist + .get_delete_comment_route(&delete_comment_component); + let resp = delete_request!(&app, &del_comment_path, cookies2.clone()); + assert_eq!(resp.status(), ServiceError::GistNotFound.status_code()); + let err: ErrorToResponse = test::read_body_json(resp).await; + assert_eq!(err.error, format!("{}", ServiceError::GistNotFound)); + + println!("delete comments; authenticated comment_owner == owner"); + let resp = delete_request!(&app, &del_comment_path, cookies.clone()); + assert_eq!(resp.status(), StatusCode::OK); } } diff --git a/src/api/v1/routes.rs b/src/api/v1/routes.rs index 9282ea9..bbcccd4 100644 --- a/src/api/v1/routes.rs +++ b/src/api/v1/routes.rs @@ -77,6 +77,8 @@ pub struct Gist { pub get_comment: &'static str, /// get gist comments pub get_gist_comments: &'static str, + /// delete comment + pub delete_comment: &'static str, } impl Gist { @@ -86,6 +88,7 @@ impl Gist { let get_file = "/api/v1/gist/profile/{username}/{gist}/contents/{file}"; let post_comment = "/api/v1/gist/profile/{username}/{gist}/comments"; let get_comment = "/api/v1/gist/profile/{username}/{gist}/comment/{comment_id}"; + let delete_comment = get_comment; let get_gist_comments = post_comment; Gist { new, @@ -93,6 +96,7 @@ impl Gist { post_comment, get_comment, get_gist_comments, + delete_comment, } } @@ -117,13 +121,18 @@ impl Gist { self.get_post_comment_route(components) } - /// get post_comment route with placeholders replaced with values provided. + /// get get_comment route with placeholders replaced with values provided. pub fn get_get_comment_route(&self, components: &GetCommentPath) -> String { self.get_comment .replace("{username}", &components.username) .replace("{gist}", &components.gist) .replace("{comment_id}", &components.comment_id.to_string()) } + + /// get delete_comment route with placeholders replaced with values provided. + pub fn get_delete_comment_route(&self, components: &GetCommentPath) -> String { + self.get_get_comment_route(components) + } } /// Account management routes @@ -225,6 +234,7 @@ mod tests { let post_comment = format!("/api/v1/gist/profile/{NAME}/{GIST}/comments"); let get_gist_comments = format!("/api/v1/gist/profile/{NAME}/{GIST}/comments"); let get_comment = format!("/api/v1/gist/profile/{NAME}/{GIST}/comment/{COMMENT_ID}"); + let delete_comment = format!("/api/v1/gist/profile/{NAME}/{GIST}/comment/{COMMENT_ID}"); let get_file_component = GetFilePath { file: FILE.into(), @@ -260,5 +270,15 @@ mod tests { get_gist_comments, ROUTES.gist.get_post_comment_route(&get_gist_comments_path) ); + + let delete_comment_path = GetCommentPath { + gist: GIST.into(), + username: NAME.into(), + comment_id: COMMENT_ID, + }; + assert_eq!( + delete_comment, + ROUTES.gist.get_delete_comment_route(&delete_comment_path) + ); } }