From 1e1130e21136d270f2f45e582b29c8db532ecc5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonne=20Ha=C3=9F?= Date: Fri, 24 Nov 2023 15:46:24 +0100 Subject: [PATCH 1/2] add API routes for comment likes --- app/controllers/api/v1/likes_controller.rb | 88 +++++- config/routes.rb | 3 +- .../api/comments_controller_spec.rb | 2 +- spec/integration/api/likes_controller_spec.rb | 267 +++++++++++++++++- 4 files changed, 349 insertions(+), 11 deletions(-) diff --git a/app/controllers/api/v1/likes_controller.rb b/app/controllers/api/v1/likes_controller.rb index 4e0103bd3..25b461bd8 100644 --- a/app/controllers/api/v1/likes_controller.rb +++ b/app/controllers/api/v1/likes_controller.rb @@ -23,7 +23,10 @@ module Api post = post_service.find!(params.require(:post_id)) raise ActiveRecord::RecordInvalid unless post.public? || private_read? - likes_query = like_service.find_for_post(params[:post_id]) + likes_query = find_likes + + return unless likes_query + likes_page = index_pager(likes_query).response likes_page[:data] = likes_page[:data].map {|x| like_json(x) } render_paged_api_response likes_page @@ -33,31 +36,42 @@ module Api post = post_service.find!(params.require(:post_id)) raise ActiveRecord::RecordInvalid unless post.public? || private_read? - like_service.create_for_post(params[:post_id]) + if params[:comment_id].present? + create_for_comment + else + create_for_post + end rescue ActiveRecord::RecordInvalid => e if e.message == "Validation failed: Target has already been taken" return render_error 409, "Like already exists" end raise - else - head :no_content end def destroy post = post_service.find!(params.require(:post_id)) raise ActiveRecord::RecordInvalid unless post.public? || private_read? - success = like_service.unlike_post(params[:post_id]) - if success - head :no_content + if params[:comment_id].present? + destroy_for_comment else - render_error 410, "Like doesn’t exist" + destroy_for_post end end private + def find_likes + if params[:comment_id].present? + return unless comment_and_post_validate(params[:post_id], params[:comment_id]) + + like_service.find_for_comment(params[:comment_id]) + else + like_service.find_for_post(params[:post_id]) + end + end + def like_service @like_service ||= LikeService.new(current_user) end @@ -66,9 +80,67 @@ module Api @post_service ||= PostService.new(current_user) end + def comment_service + @comment_service ||= CommentService.new(current_user) + end + def like_json(like) LikesPresenter.new(like).as_api_json end + + def create_for_post + like_service.create_for_post(params[:post_id]) + + head :no_content + end + + def create_for_comment + return unless comment_and_post_validate(params[:post_id], params[:comment_id]) + + like_service.create_for_comment(params[:comment_id]) + + head :no_content + end + + def destroy_for_post + if like_service.unlike_post(params[:post_id]) + head :no_content + else + render_error 410, "Like doesn’t exist" + end + end + + def destroy_for_comment + return unless comment_and_post_validate(params[:post_id], params[:comment_id]) + + if like_service.unlike_comment(params[:comment_id]) + head :no_content + else + render_error 410, "Like doesn’t exist" + end + end + + def comment_and_post_validate(post_guid, comment_guid) + if !comment_exists(comment_guid) || !comment_is_for_post(post_guid, comment_guid) + render_error 404, "Comment not found for the given post" + false + else + true + end + end + + def comment_exists(comment_guid) + comment = comment_service.find!(comment_guid) + comment ? true : false + rescue ActiveRecord::RecordNotFound + false + end + + def comment_is_for_post(post_guid, comment_guid) + comments = comment_service.find_for_post(post_guid) + comment = comments.find {|comment| comment[:guid] == comment_guid } + comment ? true : false + end end end end diff --git a/config/routes.rb b/config/routes.rb index 45f992209..3109f6193 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -231,7 +231,8 @@ Rails.application.routes.draw do resources :photos, only: %i[show index create destroy] resources :posts, only: %i[show create destroy] do resources :comments, only: %i[create index destroy] do - post "report" => "comments#report" + resource :likes, only: %i[show create destroy] + post :report end resource :reshares, only: %i[show create] resource :likes, only: %i[show create destroy] diff --git a/spec/integration/api/comments_controller_spec.rb b/spec/integration/api/comments_controller_spec.rb index 3db9823c2..bfe8cb56a 100644 --- a/spec/integration/api/comments_controller_spec.rb +++ b/spec/integration/api/comments_controller_spec.rb @@ -135,7 +135,7 @@ describe Api::V1::CommentsController do end end - describe "#read" do + describe "#index" do before do @comment_text1 = "This is a comment" @comment_text2 = "This is a comment 2" diff --git a/spec/integration/api/likes_controller_spec.rb b/spec/integration/api/likes_controller_spec.rb index e70381494..3a4916c9e 100644 --- a/spec/integration/api/likes_controller_spec.rb +++ b/spec/integration/api/likes_controller_spec.rb @@ -108,6 +108,88 @@ describe Api::V1::LikesController do expect(response.status).to eq(401) end end + + context "for comments" do + before do + comment = comment_service.create(@status.guid, "This is a comment") + @comment_guid = comment.guid + end + + context "with right post and comment id" do + it "succeeds in getting empty likes" do + get( + api_v1_post_comment_likes_path(post_id: @status.guid, comment_id: @comment_guid), + params: {access_token: access_token_minimum_scopes} + ) + expect(response.status).to eq(200) + likes = response_body(response) + expect(likes.length).to eq(0) + end + + it "succeeds in getting comment likes" do + like_service(bob).create_for_comment(@comment_guid) + like_service(auth.user).create_for_comment(@comment_guid) + like_service(alice).create_for_comment(@comment_guid) + get( + api_v1_post_comment_likes_path(post_id: @status.guid, comment_id: @comment_guid), + params: {access_token: access_token_minimum_scopes} + ) + expect(response.status).to eq(200) + likes = response_body(response) + expect(likes.length).to eq(3) + confirm_like_format(likes, alice) + confirm_like_format(likes, bob) + confirm_like_format(likes, auth.user) + + expect_to_match_json_schema(likes.to_json, "#/definitions/likes") + end + end + + context "with wrong post id" do + it "fails at getting likes" do + get( + api_v1_post_comment_likes_path(post_id: "badguid", comment_id: @comment_guid), + params: {access_token: access_token} + ) + confirm_api_error(response, 404, "Post with provided guid could not be found") + end + end + + context "with wrong comment id" do + it "fails at getting likes" do + get( + api_v1_post_comment_likes_path(post_id: @status.guid, comment_id: "badguid"), + params: {access_token: access_token} + ) + confirm_api_error(response, 404, "Comment not found for the given post") + end + end + + context "with improper credentials" do + before do + comment = comment_service(auth_public_only.user).create(@private_status.guid, "This is a comment") + @comment_guid = comment.guid + end + + context "without private:read scope in token" do + it "fails at getting likes" do + get( + api_v1_post_comment_likes_path(post_id: @private_status.guid, comment_id: @comment_guid), + params: {access_token: access_token_public_only} + ) + confirm_api_error(response, 422, "User is not allowed to like") + end + end + + it "fails without valid token" do + get( + api_v1_post_comment_likes_path(post_id: @private_status.guid, comment_id: @comment_guid), + params: {access_token: invalid_token} + ) + expect(response.status).to eq(401) + end + end + end end describe "#create" do @@ -177,9 +259,98 @@ describe Api::V1::LikesController do expect(response.status).to eq(401) end end + + context "for comments" do + before do + comment = comment_service.create(@status.guid, "This is a comment") + @comment_guid = comment.guid + end + + context "with right post and comment id" do + it "succeeds in liking comment" do + post( + api_v1_post_comment_likes_path(post_id: @status.guid, comment_id: @comment_guid), + params: {access_token: access_token} + ) + expect(response.status).to eq(204) + likes = like_service.find_for_comment(@comment_guid) + expect(likes.length).to eq(1) + expect(likes[0].author.id).to eq(auth.user.person.id) + end + + it "fails in liking already liked comment" do + post( + api_v1_post_comment_likes_path(post_id: @status.guid, comment_id: @comment_guid), + params: {access_token: access_token} + ) + expect(response.status).to eq(204) + + post( + api_v1_post_comment_likes_path(post_id: @status.guid, comment_id: @comment_guid), + params: {access_token: access_token} + ) + confirm_api_error(response, 409, "Like already exists") + + likes = like_service.find_for_comment(@comment_guid) + expect(likes.length).to eq(1) + expect(likes[0].author.id).to eq(auth.user.person.id) + end + end + + context "with wrong post id" do + it "fails at liking comment" do + post( + api_v1_post_comment_likes_path(post_id: 99_999_999, comment_id: @comment_guid), + params: {access_token: access_token} + ) + confirm_api_error(response, 404, "Post with provided guid could not be found") + end + end + + context "with wrong comment id" do + it "fails at liking comment" do + post( + api_v1_post_comment_likes_path(post_id: @status.guid, comment_id: 99_999_999), + params: {access_token: access_token} + ) + confirm_api_error(response, 404, "Comment not found for the given post") + end + end + + context "with improper credentials" do + before do + comment = comment_service(auth_public_only.user).create(@private_status.guid, "This is a comment") + @comment_guid = comment.guid + end + + it "fails in liking private comment without private:read" do + post( + api_v1_post_comment_likes_path(post_id: @private_status.guid, comment_id: @comment_guid), + params: {access_token: access_token_public_only} + ) + expect(response.status).to eq(422) + end + + it "fails in liking post without interactions" do + post( + api_v1_post_comment_likes_path(post_id: @private_status.guid, comment_id: @comment_guid), + params: {access_token: access_token_minimum_scopes} + ) + expect(response.status).to eq(403) + end + + it "fails without valid token" do + get( + api_v1_post_comment_likes_path(post_id: @private_status.guid, comment_id: @comment_guid), + params: {access_token: invalid_token} + ) + expect(response.status).to eq(401) + end + end + end end - describe "#delete" do + describe "#destroy" do before do like_service.create_for_post(@status.guid) end @@ -250,6 +421,96 @@ describe Api::V1::LikesController do expect(response.status).to eq(401) end end + + context "for comments" do + before do + comment = comment_service.create(@status.guid, "This is a comment") + @comment_guid = comment.guid + like_service.create_for_comment(@comment_guid) + end + + context "with right post and comment id" do + it "succeeds at unliking comment" do + delete( + api_v1_post_comment_likes_path(post_id: @status.guid, comment_id: @comment_guid), + params: {access_token: access_token} + ) + expect(response.status).to eq(204) + likes = like_service.find_for_comment(@comment_guid) + expect(likes.length).to eq(0) + end + + it "fails at unliking comment user didn't like" do + delete( + api_v1_post_comment_likes_path(post_id: @status.guid, comment_id: @comment_guid), + params: {access_token: access_token} + ) + expect(response.status).to eq(204) + + delete( + api_v1_post_comment_likes_path(post_id: @status.guid, comment_id: @comment_guid), + params: {access_token: access_token} + ) + confirm_api_error(response, 410, "Like doesn’t exist") + + likes = like_service.find_for_comment(@comment_guid) + expect(likes.length).to eq(0) + end + end + + context "with wrong post id" do + it "fails at unliking comment" do + delete( + api_v1_post_comment_likes_path(post_id: 99_999_999, comment_id: @comment_guid), + params: {access_token: access_token} + ) + confirm_api_error(response, 404, "Post with provided guid could not be found") + end + end + + context "with wrong comment id" do + it "fails at unliking comment" do + delete( + api_v1_post_comment_likes_path(post_id: @status.guid, comment_id: 99_999_999), + params: {access_token: access_token} + ) + confirm_api_error(response, 404, "Comment not found for the given post") + end + end + + context "with improper credentials" do + before do + comment = comment_service(auth_public_only.user).create(@private_status.guid, "This is a comment") + @comment_guid = comment.guid + end + + it "fails at unliking private post without private:read" do + like_service(auth_public_only.user).create_for_post(@private_status.guid) + delete( + api_v1_post_comment_likes_path(post_id: @private_status.guid, comment_id: @comment_guid), + params: {access_token: access_token} + ) + confirm_api_error(response, 404, "Post with provided guid could not be found") + end + + it "fails in unliking post without interactions" do + like_service(auth_minimum_scopes.user).create_for_post(@status.guid) + delete( + api_v1_post_comment_likes_path(post_id: @private_status.guid, comment_id: @comment_guid), + params: {access_token: access_token_minimum_scopes} + ) + expect(response.status).to eq(403) + end + + it "fails without valid token" do + get( + api_v1_post_comment_likes_path(post_id: @private_status.guid, comment_id: @comment_guid), + params: {access_token: invalid_token} + ) + expect(response.status).to eq(401) + end + end + end end private @@ -268,6 +529,10 @@ describe Api::V1::LikesController do LikeService.new(user) end + def comment_service(user=auth.user) + CommentService.new(user) + end + def response_body(response) JSON.parse(response.body) end From 4b49270be7d1407ca7932a1afe4162ddde73d2bf Mon Sep 17 00:00:00 2001 From: Benjamin Neff Date: Wed, 5 Jun 2024 00:14:31 +0200 Subject: [PATCH 2/2] Simplify comment and post validation It's enough to check if the comment exists on the specified post, if it doesn't exist at all, that check will also fail. Also do that check directly on SQL level and just check if the comment exist instead of looping through all comments. --- app/controllers/api/v1/likes_controller.rb | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/app/controllers/api/v1/likes_controller.rb b/app/controllers/api/v1/likes_controller.rb index 25b461bd8..94cf41f5e 100644 --- a/app/controllers/api/v1/likes_controller.rb +++ b/app/controllers/api/v1/likes_controller.rb @@ -121,25 +121,17 @@ module Api end def comment_and_post_validate(post_guid, comment_guid) - if !comment_exists(comment_guid) || !comment_is_for_post(post_guid, comment_guid) + if comment_is_for_post(post_guid, comment_guid) + true + else render_error 404, "Comment not found for the given post" false - else - true end end - def comment_exists(comment_guid) - comment = comment_service.find!(comment_guid) - comment ? true : false - rescue ActiveRecord::RecordNotFound - false - end - def comment_is_for_post(post_guid, comment_guid) comments = comment_service.find_for_post(post_guid) - comment = comments.find {|comment| comment[:guid] == comment_guid } - comment ? true : false + comments.exists?(guid: comment_guid) end end end