From 173461ac3da1eb917a39a10f297ec8d061068781 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Thu, 8 Nov 2018 23:19:11 -0500 Subject: [PATCH] Reshares API Endpoint complete with full unit tests --- app/controllers/api/v1/reshares_controller.rb | 42 ++++ app/models/user/social_actions.rb | 1 + config/routes.rb | 1 + .../api/reshares_controller_spec.rb | 216 ++++++++++++++++++ ...are_service.rb => reshare_service_spec.rb} | 10 +- 5 files changed, 265 insertions(+), 5 deletions(-) create mode 100644 app/controllers/api/v1/reshares_controller.rb create mode 100644 spec/integration/api/reshares_controller_spec.rb rename spec/services/{reshare_service.rb => reshare_service_spec.rb} (91%) diff --git a/app/controllers/api/v1/reshares_controller.rb b/app/controllers/api/v1/reshares_controller.rb new file mode 100644 index 000000000..903b870d4 --- /dev/null +++ b/app/controllers/api/v1/reshares_controller.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Api + module V1 + class ResharesController < Api::V1::BaseController + before_action only: %i[show] do + require_access_token %w[read] + end + + before_action only: %i[create] do + require_access_token %w[read write] + end + + rescue_from ActiveRecord::RecordNotFound do + render json: I18n.t("api.endpoint_errors.posts.post_not_found"), status: :not_found + end + + rescue_from Diaspora::NonPublic do + render json: I18n.t("api.endpoint_errors.posts.post_not_found"), status: :not_found + end + + def show + reshares = reshare_service.find_for_post(params[:post_id]).map do |r| + {guid: r.guid, author: PersonPresenter.new(r.author).as_api_json} + end + render json: reshares + end + + def create + reshare = reshare_service.create(params[:post_id]) + rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid, RuntimeError + render plain: I18n.t("reshares.create.error"), status: :unprocessable_entity + else + render json: PostPresenter.new(reshare, current_user).as_api_response + end + + def reshare_service + @reshare_service ||= ReshareService.new(current_user) + end + end + end +end diff --git a/app/models/user/social_actions.rb b/app/models/user/social_actions.rb index 7a5b41f0a..e03d84055 100644 --- a/app/models/user/social_actions.rb +++ b/app/models/user/social_actions.rb @@ -24,6 +24,7 @@ module User::SocialActions end def reshare!(target, opts={}) + raise I18n.t("reshares.create.error") if target.author.guid == guid build_post(:reshare, :root_guid => target.guid).tap do |reshare| reshare.save! update_or_create_participation!(target) diff --git a/config/routes.rb b/config/routes.rb index 38cf01e2d..eb24be5fb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -226,6 +226,7 @@ Rails.application.routes.draw do resources :comments, only: %i[create index destroy] do post "report" => "comments#report" end + resource :reshares, only: %i[show create] resource :likes, only: %i[show create destroy] end resources :conversations, only: %i[show index create destroy] do diff --git a/spec/integration/api/reshares_controller_spec.rb b/spec/integration/api/reshares_controller_spec.rb new file mode 100644 index 000000000..e833075fa --- /dev/null +++ b/spec/integration/api/reshares_controller_spec.rb @@ -0,0 +1,216 @@ +# frozen_sTring_literal: true + +require "spec_helper" + +describe Api::V1::ResharesController do + let(:auth) { FactoryGirl.create(:auth_with_read_and_write) } + let!(:access_token) { auth.create_access_token.to_s } + let(:auth_read_only) { FactoryGirl.create(:auth_with_read) } + let!(:access_token_read_only) { auth_read_only.create_access_token.to_s } + + before do + @user_post = auth.user.post( + :status_message, + text: "This is a status message", + public: true, + to: "all" + ) + + @eve_post = eve.post( + :status_message, + text: "This is Bob's status message", + public: true, + to: "all" + ) + + @alice_reshare = ReshareService.new(alice).create(@user_post.id) + end + + describe "#show" do + context "with valid post id" do + it "succeeds" do + get( + api_v1_post_reshares_path(@user_post.guid), + params: {access_token: access_token} + ) + + expect(response.status).to eq(200) + reshares = JSON.parse(response.body) + expect(reshares.length).to eq(1) + reshare = reshares[0] + expect(reshare["guid"]).not_to be_nil + confirm_person_format(reshare["author"], alice) + end + + it "succeeds but empty with private post it can see" do + private_post = auth.user.post( + :status_message, + text: "to aspect only", + public: false, + to: auth.user.aspects.first.id + ) + + get( + api_v1_post_reshares_path(private_post.id), + params: { + access_token: access_token + } + ) + expect(response.status).to eq(200) + reshares = JSON.parse(response.body) + expect(reshares.length).to eq(0) + end + end + + context "with invalid post id" do + it "fails with bad id" do + get( + api_v1_post_reshares_path("999_999_999"), + params: {access_token: access_token} + ) + + expect(response.status).to eq(404) + expect(response.body).to eq(I18n.t("api.endpoint_errors.posts.post_not_found")) + end + + it "fails with private post it shouldn't see" do + private_post = alice.post(:status_message, text: "to aspect only", public: false, to: alice.aspects.first.id) + get( + api_v1_post_reshares_path(private_post.id), + params: { + access_token: access_token + } + ) + expect(response.status).to eq(404) + expect(response.body).to eq(I18n.t("api.endpoint_errors.posts.post_not_found")) + end + end + + context "improper credentials" do + it "fails when not logged in" do + get( + api_v1_post_reshares_path(@user_post.id), + params: { + access_token: "999_999_999" + } + ) + expect(response.status).to eq(401) + end + end + end + + describe "#create" do + context "with valid post id" do + it "succeeds" do + post( + api_v1_post_reshares_path(post_id: @eve_post.guid), + params: {access_token: access_token} + ) + + expect(response.status).to eq(200) + post = JSON.parse(response.body) + expect(post["guid"]).not_to be_nil + expect(post["body"]).to eq(@eve_post.text) + expect(post["post_type"]).to eq("Reshare") + expect(post["author"]["guid"]).to eq(auth.user.guid) + expect(post["root"]["guid"]).to eq(@eve_post.guid) + end + + it "fails to reshare twice" do + reshare_service.create(@eve_post.id) + post( + api_v1_post_reshares_path(post_id: @eve_post.guid), + params: {access_token: access_token} + ) + + expect(response.status).to eq(422) + expect(response.body).to eq(I18n.t("reshares.create.error")) + end + end + + context "with invalid post id" do + it "fails with bad id" do + post( + api_v1_post_reshares_path(post_id: "999_999_999"), + params: {access_token: access_token} + ) + + expect(response.status).to eq(422) + expect(response.body).to eq(I18n.t("reshares.create.error")) + end + + it "fails with own post" do + post( + api_v1_post_reshares_path(post_id: @user_post.guid), + params: {access_token: access_token} + ) + + puts(response.body) + expect(response.status).to eq(422) + expect(response.body).to eq(I18n.t("reshares.create.error")) + end + + it "fails with private post it shouldn't see" do + private_post = alice.post(:status_message, text: "to aspect only", public: false, to: alice.aspects.first.id) + post( + api_v1_post_reshares_path(private_post.id), + params: { + access_token: access_token + } + ) + expect(response.status).to eq(422) + expect(response.body).to eq(I18n.t("reshares.create.error")) + end + + it "fails with private post it can see" do + private_post = alice.post(:status_message, text: "to aspect only", public: false, to: alice.aspects) + get( + api_v1_post_reshares_path(private_post.id), + params: { + access_token: access_token + } + ) + puts(response.body) + expect(response.status).to eq(404) + expect(response.body).to eq(I18n.t("api.endpoint_errors.posts.post_not_found")) + end + end + + context "improper credentials" do + it "fails when not logged in" do + post( + api_v1_post_reshares_path(@eve_post.id), + params: { + access_token: "999_999_999" + } + ) + expect(response.status).to eq(401) + end + + it "fails when logged in read only" do + post( + api_v1_post_reshares_path(@eve_post.id), + params: { + access_token: auth_read_only + } + ) + expect(response.status).to eq(401) + end + end + end + + private + + def reshare_service(user=auth.user) + @reshare_service ||= ReshareService.new(user) + end + + # rubocop:disable Metrics/AbcSize + def confirm_person_format(post_person, user) + expect(post_person["guid"]).to eq(user.guid) + expect(post_person["diaspora_id"]).to eq(user.diaspora_handle) + expect(post_person["name"]).to eq(user.name) + expect(post_person["avatar"]).to eq(user.profile.image_url) + end + # rubocop:enable Metrics/AbcSize +end diff --git a/spec/services/reshare_service.rb b/spec/services/reshare_service_spec.rb similarity index 91% rename from spec/services/reshare_service.rb rename to spec/services/reshare_service_spec.rb index efff50386..ac2d7a8c1 100644 --- a/spec/services/reshare_service.rb +++ b/spec/services/reshare_service_spec.rb @@ -7,7 +7,7 @@ describe ReshareService do it "doesn't create a reshare of my own post" do expect { ReshareService.new(alice).create(post.id) - }.not_to raise_error + }.to raise_error RuntimeError end it "creates a reshare of a post of a contact" do @@ -76,9 +76,9 @@ describe ReshareService do end it "returns the user's reshare first" do - [alice, bob, eve].map {|user| ReshareService.new(user).create(post.id) } + [bob, eve].map {|user| ReshareService.new(user).create(post.id) } - [alice, bob, eve].each do |user| + [bob, eve].each do |user| expect( ReshareService.new(user).find_for_post(post.id).first.author.id ).to be user.person.id @@ -88,7 +88,7 @@ describe ReshareService do context "without user" do it "returns reshares for a public post" do - reshare = ReshareService.new(alice).create(post.id) + reshare = ReshareService.new(bob).create(post.id) expect(ReshareService.new.find_for_post(post.id)).to include(reshare) end @@ -101,7 +101,7 @@ describe ReshareService do end it "returns all reshares of a post" do - reshares = [alice, bob, eve].map {|user| ReshareService.new(user).create(post.id) } + reshares = [bob, eve].map {|user| ReshareService.new(user).create(post.id) } expect(ReshareService.new.find_for_post(post.id)).to match_array(reshares) end