diff --git a/Changelog.md b/Changelog.md index 1139b6333..7b47bf454 100644 --- a/Changelog.md +++ b/Changelog.md @@ -21,6 +21,7 @@ * Increase time to wait before showing the hovercard [#7319](https://github.com/diaspora/diaspora/pull/7319) * Remove some unused color-theme overrides [#7325](https://github.com/diaspora/diaspora/pull/7325) * Change color of author-name on hover [#7326](https://github.com/diaspora/diaspora/pull/7326) +* Add like and reshare services [#7337](https://github.com/diaspora/diaspora/pull/7337) ## Bug fixes * Fix path to `bundle` in `script/server` [#7281](https://github.com/diaspora/diaspora/pull/7281) diff --git a/app/controllers/likes_controller.rb b/app/controllers/likes_controller.rb index 8f9197c3c..20fe1dd69 100644 --- a/app/controllers/likes_controller.rb +++ b/app/controllers/likes_controller.rb @@ -11,59 +11,34 @@ class LikesController < ApplicationController :json def create - begin - @like = if target - current_user.like!(target) - end - rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid => e - # do nothing - end - - if @like - respond_to do |format| - format.html { render :nothing => true, :status => 201 } - format.mobile { redirect_to post_path(@like.post_id) } - format.json { render :json => @like.as_api_response(:backbone), :status => 201 } - end - else - render text: I18n.t("likes.create.error"), status: 422 + like = like_service.create(params[:post_id]) + rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid + render text: I18n.t("likes.create.error"), status: 422 + else + respond_to do |format| + format.html { render nothing: true, status: 201 } + format.mobile { redirect_to post_path(like.post_id) } + format.json { render json: like.as_api_response(:backbone), status: 201 } end end def destroy - begin - @like = Like.find_by_id_and_author_id!(params[:id], current_user.person.id) - rescue ActiveRecord::RecordNotFound + if like_service.destroy(params[:id]) + render nothing: true, status: 204 + else render text: I18n.t("likes.destroy.error"), status: 404 - return - end - - current_user.retract(@like) - respond_to do |format| - format.json { render :nothing => true, :status => 204 } end end - #I can go when the old stream goes. def index - @likes = target.likes.includes(:author => :profile) - @people = @likes.map(&:author) - - respond_to do |format| - format.all { render :layout => false } - format.json { render :json => @likes.as_api_response(:backbone) } - end + render json: like_service.find_for_post(params[:post_id]) + .includes(author: :profile) + .as_api_response(:backbone) end private - def target - @target ||= if params[:post_id] - current_user.find_visible_shareable_by_id(Post, params[:post_id]) || raise(ActiveRecord::RecordNotFound.new) - else - Comment.find(params[:comment_id]).tap do |comment| - raise(ActiveRecord::RecordNotFound.new) unless current_user.find_visible_shareable_by_id(Post, comment.commentable_id) - end - end + def like_service + @like_service ||= LikeService.new(current_user) end end diff --git a/app/controllers/reshares_controller.rb b/app/controllers/reshares_controller.rb index 7345e6343..4add93174 100644 --- a/app/controllers/reshares_controller.rb +++ b/app/controllers/reshares_controller.rb @@ -3,30 +3,22 @@ class ResharesController < ApplicationController respond_to :json def create - post = Post.where(:guid => params[:root_guid]).first - if post.is_a? Reshare - @reshare = current_user.build_post(:reshare, :root_guid => post.absolute_root.guid) - else - @reshare = current_user.build_post(:reshare, :root_guid => params[:root_guid]) - end - - if @reshare.save - current_user.dispatch_post(@reshare) - render :json => ExtremePostPresenter.new(@reshare, current_user), :status => 201 - else - render text: I18n.t("reshares.create.error"), status: 422 - end + reshare = reshare_service.create(params[:root_guid]) + rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid + render text: I18n.t("reshares.create.error"), status: 422 + else + render json: ExtremePostPresenter.new(reshare, current_user), status: 201 end def index - @reshares = target.reshares.includes(author: :profile) - render json: @reshares.as_api_response(:backbone) + render json: reshare_service.find_for_post(params[:post_id]) + .includes(author: :profile) + .as_api_response(:backbone) end private - def target - @target ||= current_user.find_visible_shareable_by_id(Post, params[:post_id]) || - raise(ActiveRecord::RecordNotFound.new) + def reshare_service + @reshare_service ||= ReshareService.new(current_user) end end diff --git a/app/services/like_service.rb b/app/services/like_service.rb new file mode 100644 index 000000000..411440475 --- /dev/null +++ b/app/services/like_service.rb @@ -0,0 +1,33 @@ +class LikeService + def initialize(user=nil) + @user = user + end + + def create(post_id) + post = post_service.find!(post_id) + user.like!(post) + end + + def destroy(like_id) + like = Like.find(like_id) + if user.owns?(like) + user.retract(like) + true + else + false + end + end + + def find_for_post(post_id) + likes = post_service.find!(post_id).likes + user ? likes.order("author_id = #{user.person.id} DESC") : likes + end + + private + + attr_reader :user + + def post_service + @post_service ||= PostService.new(user) + end +end diff --git a/app/services/reshare_service.rb b/app/services/reshare_service.rb new file mode 100644 index 000000000..01d725647 --- /dev/null +++ b/app/services/reshare_service.rb @@ -0,0 +1,24 @@ +class ReshareService + def initialize(user=nil) + @user = user + end + + def create(post_id) + post = post_service.find!(post_id) + post = post.absolute_root if post.is_a? Reshare + user.reshare!(post) + end + + def find_for_post(post_id) + reshares = post_service.find!(post_id).reshares + user ? reshares.order("author_id = #{user.person.id} DESC") : reshares + end + + private + + attr_reader :user + + def post_service + @post_service ||= PostService.new(user) + end +end diff --git a/app/views/likes/_likes.haml b/app/views/likes/_likes.haml deleted file mode 100644 index a960df447..000000000 --- a/app/views/likes/_likes.haml +++ /dev/null @@ -1,6 +0,0 @@ --# Copyright (c) 2010-2011, Diaspora Inc. This file is --# licensed under the Affero General Public License version 3 or later. See --# the COPYRIGHT file. - -- @people[0..17].each do |person| - = person_image_link(person, size: :thumb_small) diff --git a/app/views/likes/index.html.haml b/app/views/likes/index.html.haml deleted file mode 100644 index 3fe13daaa..000000000 --- a/app/views/likes/index.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render 'likes', :likes => @likes diff --git a/spec/controllers/likes_controller_spec.rb b/spec/controllers/likes_controller_spec.rb index 928b1f64c..b9935a70f 100644 --- a/spec/controllers/likes_controller_spec.rb +++ b/spec/controllers/likes_controller_spec.rb @@ -1,8 +1,8 @@ - # Copyright (c) 2010-2011, Diaspora Inc. This file is +# Copyright (c) 2010-2011, Diaspora Inc. This file is # licensed under the Affero General Public License version 3 or later. See # the COPYRIGHT file. -describe LikesController, :type => :controller do +describe LikesController, type: :controller do before do @alices_aspect = alice.aspects.where(:name => "generic").first @bobs_aspect = bob.aspects.where(:name => "generic").first @@ -10,137 +10,114 @@ describe LikesController, :type => :controller do sign_in(alice, scope: :user) end - [Comment, Post].each do |class_const| - context class_const.to_s do - let(:id_field){ - "#{class_const.to_s.underscore}_id" - } + describe "#create" do + let(:like_hash) { + {post_id: @target.id} + } - describe '#create' do - let(:like_hash) { - {:positive => 1, - id_field => "#{@target.id}"} - } - let(:dislike_hash) { - {:positive => 0, - id_field => "#{@target.id}"} - } + context "on my own post" do + it "succeeds" do + @target = alice.post :status_message, text: "AWESOME", to: @alices_aspect.id + post :create, like_hash.merge(format: :json) + expect(response.code).to eq("201") + end + end - context "on my own post" do - it 'succeeds' do - @target = alice.post :status_message, :text => "AWESOME", :to => @alices_aspect.id - @target = alice.comment!(@target, "hey") if class_const == Comment - post :create, like_hash.merge(:format => :json) - expect(response.code).to eq('201') - end - end - - context "on a post from a contact" do - before do - @target = bob.post(:status_message, :text => "AWESOME", :to => @bobs_aspect.id) - @target = bob.comment!(@target, "hey") if class_const == Comment - end - - it 'likes' do - post :create, like_hash - expect(response.code).to eq('201') - end - - it 'dislikes' do - post :create, dislike_hash - expect(response.code).to eq('201') - end - - it "doesn't post multiple times" do - alice.like!(@target) - post :create, dislike_hash - expect(response.code).to eq('422') - end - end - - context "on a post from a stranger" do - before do - @target = eve.post :status_message, :text => "AWESOME", :to => eve.aspects.first.id - @target = eve.comment!(@target, "hey") if class_const == Comment - end - - it "doesn't post" do - expect(alice).not_to receive(:like!) - post :create, like_hash - expect(response.code).to eq('422') - end - end - - context "when an the exception is raised" do - before do - @target = alice.post :status_message, :text => "AWESOME", :to => @alices_aspect.id - @target = alice.comment!(@target, "hey") if class_const == Comment - end - - it "should be catched when it means that the target is not found" do - params = like_hash.merge(format: :json, id_field => -1) - post :create, params - expect(response.code).to eq('422') - end - - it "should not be catched when it is unexpected" do - @target = alice.post :status_message, :text => "AWESOME", :to => @alices_aspect.id - @target = alice.comment!(@target, "hey") if class_const == Comment - allow(alice).to receive(:like!).and_raise("something") - allow(@controller).to receive(:current_user).and_return(alice) - expect { post :create, like_hash.merge(:format => :json) }.to raise_error("something") - end - end + context "on a post from a contact" do + before do + @target = bob.post(:status_message, text: "AWESOME", to: @bobs_aspect.id) end - describe '#index' do - before do - @message = alice.post(:status_message, :text => "hey", :to => @alices_aspect.id) - @message = alice.comment!(@message, "hey") if class_const == Comment - end - - it 'returns a 404 for a post not visible to the user' do - sign_in eve - expect{get :index, id_field => @message.id}.to raise_error(ActiveRecord::RecordNotFound) - end - - it 'returns an array of likes for a post' do - like = bob.like!(@message) - get :index, id_field => @message.id - expect(assigns[:likes].map(&:id)).to eq(@message.likes.map(&:id)) - end - - it 'returns an empty array for a post with no likes' do - get :index, id_field => @message.id - expect(assigns[:likes]).to eq([]) - end + it "likes" do + post :create, like_hash + expect(response.code).to eq("201") end - describe '#destroy' do - before do - @message = bob.post(:status_message, :text => "hey", :to => @alices_aspect.id) - @message = bob.comment!(@message, "hey") if class_const == Comment - @like = alice.like!(@message) - end + it "doesn't post multiple times" do + alice.like!(@target) + post :create, like_hash + expect(response.code).to eq("422") + end + end - it 'lets a user destroy their like' do - current_user = controller.send(:current_user) - expect(current_user).to receive(:retract).with(@like) + context "on a post from a stranger" do + before do + @target = eve.post :status_message, text: "AWESOME", to: eve.aspects.first.id + end - delete :destroy, :format => :json, id_field => @like.target_id, :id => @like.id - expect(response.status).to eq(204) - end + it "doesn't post" do + expect(alice).not_to receive(:like!) + post :create, like_hash + expect(response.code).to eq("422") + end + end - it 'does not let a user destroy other likes' do - like2 = eve.like!(@message) - like_count = Like.count + context "when an the exception is raised" do + before do + @target = alice.post :status_message, text: "AWESOME", to: @alices_aspect.id + end - delete :destroy, :format => :json, id_field => like2.target_id, :id => like2.id - expect(response.status).to eq(404) - expect(response.body).to eq(I18n.t("likes.destroy.error")) - expect(Like.count).to eq(like_count) - end + it "should be catched when it means that the target is not found" do + params = like_hash.merge(format: :json, post_id: -1) + post :create, params + expect(response.code).to eq("422") + end + + it "should not be catched when it is unexpected" do + @target = alice.post :status_message, text: "AWESOME", to: @alices_aspect.id + allow(alice).to receive(:like!).and_raise("something") + allow(@controller).to receive(:current_user).and_return(alice) + expect { post :create, like_hash.merge(format: :json) }.to raise_error("something") end end end + + describe "#index" do + before do + @message = alice.post(:status_message, text: "hey", to: @alices_aspect.id) + end + + it "returns a 404 for a post not visible to the user" do + sign_in eve + expect { + get :index, post_id: @message.id + }.to raise_error(ActiveRecord::RecordNotFound) + end + + it "returns an array of likes for a post" do + bob.like!(@message) + get :index, post_id: @message.id + expect(JSON.parse(response.body).map {|h| h["id"] }).to match_array(@message.likes.map(&:id)) + end + + it "returns an empty array for a post with no likes" do + get :index, post_id: @message.id + expect(JSON.parse(response.body).map(&:id)).to eq([]) + end + end + + describe "#destroy" do + before do + @message = bob.post(:status_message, text: "hey", to: @alices_aspect.id) + @like = alice.like!(@message) + end + + it "lets a user destroy their like" do + current_user = controller.send(:current_user) + expect(current_user).to receive(:retract).with(@like) + + delete :destroy, format: :json, post_id: @message.id, id: @like.id + expect(response.status).to eq(204) + end + + it "does not let a user destroy other likes" do + like2 = eve.like!(@message) + like_count = Like.count + + delete :destroy, format: :json, post_id: @message.id, id: like2.id + expect(response.status).to eq(404) + expect(response.body).to eq(I18n.t("likes.destroy.error")) + expect(Like.count).to eq(like_count) + end + end end diff --git a/spec/controllers/reshares_controller_spec.rb b/spec/controllers/reshares_controller_spec.rb index 2dc48daca..38a65b94e 100644 --- a/spec/controllers/reshares_controller_spec.rb +++ b/spec/controllers/reshares_controller_spec.rb @@ -31,8 +31,9 @@ describe ResharesController, :type => :controller do }.to change(Reshare, :count).by(1) end - it 'calls dispatch' do - expect(bob).to receive(:dispatch_post) + it "federates" do + allow_any_instance_of(Participation::Generator).to receive(:create!) + expect(Diaspora::Federation::Dispatcher).to receive(:defer_dispatch) post_request! end diff --git a/spec/services/comment_service_spec.rb b/spec/services/comment_service_spec.rb index 930e7c6f4..9ec6facff 100644 --- a/spec/services/comment_service_spec.rb +++ b/spec/services/comment_service_spec.rb @@ -26,7 +26,7 @@ describe CommentService do it "fail if the user can not see the post" do expect { - CommentService.new(eve).create("unknown id", "hi") + CommentService.new(eve).create(post.id, "hi") }.to raise_error ActiveRecord::RecordNotFound end end diff --git a/spec/services/like_service_spec.rb b/spec/services/like_service_spec.rb new file mode 100644 index 000000000..2d711b612 --- /dev/null +++ b/spec/services/like_service_spec.rb @@ -0,0 +1,121 @@ +describe LikeService do + let(:post) { alice.post(:status_message, text: "hello", to: alice.aspects.first) } + + describe "#create" do + it "creates a like on my own post" do + expect { + LikeService.new(alice).create(post.id) + }.not_to raise_error + end + + it "creates a like on a post of a contact" do + expect { + LikeService.new(bob).create(post.id) + }.not_to raise_error + end + + it "attaches the like to the post" do + like = LikeService.new(alice).create(post.id) + expect(post.likes.first.id).to eq(like.id) + end + + it "fails if the post does not exist" do + expect { + LikeService.new(bob).create("unknown id") + }.to raise_error ActiveRecord::RecordNotFound + end + + it "fails if the user can't see the post" do + expect { + LikeService.new(eve).create(post.id) + }.to raise_error ActiveRecord::RecordNotFound + end + + it "fails if the user already liked the post" do + LikeService.new(alice).create(post.id) + expect { + LikeService.new(alice).create(post.id) + }.to raise_error ActiveRecord::RecordInvalid + end + end + + describe "#destroy" do + let(:like) { LikeService.new(bob).create(post.id) } + + it "lets the user destroy their own like" do + result = LikeService.new(bob).destroy(like.id) + expect(result).to be_truthy + end + + it "doesn't let the parent author destroy others likes" do + result = LikeService.new(alice).destroy(like.id) + expect(result).to be_falsey + end + + it "doesn't let someone destroy others likes" do + result = LikeService.new(eve).destroy(like.id) + expect(result).to be_falsey + end + + it "fails if the like doesn't exist" do + expect { + LikeService.new(bob).destroy("unknown id") + }.to raise_error ActiveRecord::RecordNotFound + end + end + + describe "#find_for_post" do + context "with user" do + it "returns likes for a public post" do + post = alice.post(:status_message, text: "hello", public: true) + like = LikeService.new(alice).create(post.id) + expect(LikeService.new(eve).find_for_post(post.id)).to include(like) + end + + it "returns likes for a visible private post" do + like = LikeService.new(alice).create(post.id) + expect(LikeService.new(bob).find_for_post(post.id)).to include(like) + end + + it "doesn't return likes for a private post the user can not see" do + LikeService.new(alice).create(post.id) + expect { + LikeService.new(eve).find_for_post(post.id) + }.to raise_error ActiveRecord::RecordNotFound + end + + it "returns the user's like first" do + post = alice.post(:status_message, text: "hello", public: true) + [alice, bob, eve].map {|user| LikeService.new(user).create(post.id) } + + [alice, bob, eve].each do |user| + expect( + LikeService.new(user).find_for_post(post.id).first.author.id + ).to be user.person.id + end + end + end + + context "without user" do + it "returns likes for a public post" do + post = alice.post(:status_message, text: "hello", public: true) + like = LikeService.new(alice).create(post.id) + expect(LikeService.new.find_for_post(post.id)).to include(like) + end + + it "doesn't return likes a for private post" do + LikeService.new(alice).create(post.id) + expect { + LikeService.new.find_for_post(post.id) + }.to raise_error Diaspora::NonPublic + end + end + + it "returns all likes of a post" do + post = alice.post(:status_message, text: "hello", public: true) + likes = [alice, bob, eve].map {|user| LikeService.new(user).create(post.id) } + + expect(LikeService.new.find_for_post(post.id)).to match_array(likes) + end + end +end diff --git a/spec/services/reshare_service.rb b/spec/services/reshare_service.rb new file mode 100644 index 000000000..4541b64e2 --- /dev/null +++ b/spec/services/reshare_service.rb @@ -0,0 +1,107 @@ +describe ReshareService do + let(:post) { alice.post(:status_message, text: "hello", public: true) } + + describe "#create" do + it "doesn't create a reshare of my own post" do + expect { + ReshareService.new(alice).create(post.id) + }.not_to raise_error + end + + it "creates a reshare of a post of a contact" do + expect { + ReshareService.new(bob).create(post.id) + }.not_to raise_error + end + + it "attaches the reshare to the post" do + reshare = ReshareService.new(bob).create(post.id) + expect(post.reshares.first.id).to eq(reshare.id) + end + + it "reshares the original post when called with a reshare" do + reshare = ReshareService.new(bob).create(post.id) + reshare2 = ReshareService.new(eve).create(reshare.id) + expect(post.reshares.map(&:id)).to include(reshare2.id) + end + + it "fails if the post does not exist" do + expect { + ReshareService.new(bob).create("unknown id") + }.to raise_error ActiveRecord::RecordNotFound + end + + it "fails if the post is not public" do + post = alice.post(:status_message, text: "hello", to: alice.aspects.first) + + expect { + ReshareService.new(bob).create(post.id) + }.to raise_error ActiveRecord::RecordInvalid + end + + it "fails if the user already reshared the post" do + ReshareService.new(bob).create(post.id) + expect { + ReshareService.new(bob).create(post.id) + }.to raise_error ActiveRecord::RecordInvalid + end + + it "fails if the user already reshared the original post" do + reshare = ReshareService.new(bob).create(post.id) + expect { + ReshareService.new(bob).create(reshare.id) + }.to raise_error ActiveRecord::RecordInvalid + end + end + + describe "#find_for_post" do + context "with user" do + it "returns reshares for a public post" do + reshare = ReshareService.new(bob).create(post.id) + expect(ReshareService.new(eve).find_for_post(post.id)).to include(reshare) + end + + it "returns reshares for a visible private post" do + post = alice.post(:status_message, text: "hello", to: alice.aspects.first) + expect(ReshareService.new(bob).find_for_post(post.id)).to be_empty + end + + it "doesn't return reshares for a private post the user can not see" do + post = alice.post(:status_message, text: "hello", to: alice.aspects.first) + expect { + ReshareService.new(eve).find_for_post(post.id) + }.to raise_error ActiveRecord::RecordNotFound + end + + it "returns the user's reshare first" do + [alice, bob, eve].map {|user| ReshareService.new(user).create(post.id) } + + [alice, bob, eve].each do |user| + expect( + ReshareService.new(user).find_for_post(post.id).first.author.id + ).to be user.person.id + end + end + end + + context "without user" do + it "returns reshares for a public post" do + reshare = ReshareService.new(alice).create(post.id) + expect(ReshareService.new.find_for_post(post.id)).to include(reshare) + end + + it "doesn't return reshares a for private post" do + post = alice.post(:status_message, text: "hello", to: alice.aspects.first) + expect { + ReshareService.new.find_for_post(post.id) + }.to raise_error Diaspora::NonPublic + end + end + + it "returns all reshares of a post" do + reshares = [alice, bob, eve].map {|user| ReshareService.new(user).create(post.id) } + + expect(ReshareService.new.find_for_post(post.id)).to match_array(reshares) + end + end +end