diff --git a/Changelog.md b/Changelog.md index a6101927a..e3bad07fe 100644 --- a/Changelog.md +++ b/Changelog.md @@ -81,7 +81,7 @@ We recommend setting up new pods using Ruby 3.3, and updating existing pods to t * Tell users that there is no help in mobile version, allow to switch to desktop [#8407](https://github.com/diaspora/diaspora/pull/8407) * Add Smart App Banner on iOS devices [#8409](https://github.com/diaspora/diaspora/pull/8409) * Add a more detailed modal when reporting a post or a comment [#8035](https://github.com/diaspora/diaspora/pull/8035) -* Re-introduce likes on comments [#8203](https://github.com/diaspora/diaspora/pull/8203) [#8442](https://github.com/diaspora/diaspora/pull/8442) +* Re-introduce likes on comments [#8203](https://github.com/diaspora/diaspora/pull/8203) [#8439](https://github.com/diaspora/diaspora/pull/8439) [#8442](https://github.com/diaspora/diaspora/pull/8442) * New redesigned registration page [#8285](https://github.com/diaspora/diaspora/pull/8285) * Allow comments to be fetched [#8441](https://github.com/diaspora/diaspora/pull/8441) diff --git a/app/controllers/api/v1/likes_controller.rb b/app/controllers/api/v1/likes_controller.rb index 4e0103bd3..94cf41f5e 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,59 @@ 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_is_for_post(post_guid, comment_guid) + true + else + render_error 404, "Comment not found for the given post" + false + end + end + + def comment_is_for_post(post_guid, comment_guid) + comments = comment_service.find_for_post(post_guid) + comments.exists?(guid: comment_guid) + 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