diff --git a/app/controllers/supplejack_api/concerns/stories/multiple.rb b/app/controllers/supplejack_api/concerns/stories/multiple.rb new file mode 100644 index 000000000..4cc487dbe --- /dev/null +++ b/app/controllers/supplejack_api/concerns/stories/multiple.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module SupplejackApi + module Concerns + module Stories + module Multiple + def multiple_add + stories = multiple_params['stories'].each_with_object([]) do |story, stories_array| + set = SupplejackApi::UserSet.custom_find(story['id']) + return render_error_with(I18n.t('errors.story_not_found', id: story['id']), :not_found) unless set + + authorize(set) + stories_array.push(set) + end + + changes = multiple_params['stories'].each_with_object([]) do |story_params, changes_array| + set = stories.find { |s| s.id.to_s == story_params['id'] } + + item_ids = story_params['items'].each_with_object([]) do |item, ids| + item = set.set_items.build(item) + + return render_error_with(item.errors.messages.values.join(', '), :bad_request) unless item.valid? + + ids.push(item.id) + end + + set.save! + + changes_array.push({ + story_id: story_params['id'], + item_ids: item_ids + }) + end + + render json: changes + end + + def multiple_remove + stories = multiple_params['stories'].each_with_object([]) do |story, stories_array| + set = SupplejackApi::UserSet.custom_find(story['id']) + return render_error_with(I18n.t('errors.story_not_found', id: story['id']), :not_found) unless set + + authorize(set) + stories_array.push(set) + end + + multiple_params['stories'].each do |story_params| + set = stories.find { |s| s.id.to_s == story_params['id'] } + + story_params['items'].each do |item| + set_item = set.set_items.find_by_id(item[:id]) + + return render json: { errors: I18n.t('errors.record_not_found', id: item[:id]) }, + status: :not_found unless set_item + + set_item.destroy! + end + + set.save! + end + + head :no_content + end + + private + + def multiple_params + params.permit(:api_key, + stories: [:id, + { items: [:id, :position, :type, :sub_type, :image_url, + :display_collection, :category, :meta, :record_id, + { content: %i[value image_url display_collection category] }] }]) + end + end + end + end +end diff --git a/app/controllers/supplejack_api/stories_controller.rb b/app/controllers/supplejack_api/stories_controller.rb index d87f45180..a9ede0cce 100644 --- a/app/controllers/supplejack_api/stories_controller.rb +++ b/app/controllers/supplejack_api/stories_controller.rb @@ -4,6 +4,7 @@ module SupplejackApi class StoriesController < SupplejackApplicationController include Pundit include Concerns::IgnoreMetrics + include Concerns::Stories::Multiple rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized @@ -69,8 +70,6 @@ def reposition_items end end - private - def story_params fields = [:name, :description, :privacy, :copyright, :cover_thumbnail, { tags: [], subjects: [] }] diff --git a/app/policies/supplejack_api/user_set_policy.rb b/app/policies/supplejack_api/user_set_policy.rb index 4903e474e..e8e9f1120 100644 --- a/app/policies/supplejack_api/user_set_policy.rb +++ b/app/policies/supplejack_api/user_set_policy.rb @@ -20,5 +20,7 @@ def show? end alias update? admin_or_owner? + alias multiple_add? admin_or_owner? + alias multiple_remove? admin_or_owner? end end diff --git a/config/routes.rb b/config/routes.rb index 056cf947d..7677d67d6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -37,11 +37,14 @@ # Stories namespace 'stories' do resources :featured, only: [:index] + post :multiple_add + post :multiple_remove end resources :stories, except: [:new, :edit] do post :reposition_items + resources :items, controller: :story_items, except: [:new, :edit] end end diff --git a/spec/controllers/supplejack_api/stories_controller_spec.rb b/spec/controllers/supplejack_api/stories_controller_spec.rb index 024b2a62f..a60268153 100644 --- a/spec/controllers/supplejack_api/stories_controller_spec.rb +++ b/spec/controllers/supplejack_api/stories_controller_spec.rb @@ -365,5 +365,272 @@ module SupplejackApi end end end + + describe 'POST multiple_add' do + let(:user) { create(:user) } + let(:user_two) { create(:user) } + let!(:story_one) { create(:user_set, user_id: user.id, privacy: 'public') } + let!(:story_two) { create(:user_set, user_id: user.id, privacy: 'hidden') } + + let(:story_item_one) { attributes_for(:story_item) } + let(:story_item_two) { attributes_for(:story_item) } + let(:story_item_three) { attributes_for(:story_item) } + let(:story_item_four) { attributes_for(:story_item) } + let(:story_item_five) { attributes_for(:story_item) } + + context 'valid' do + before do + post :multiple_add, + params: { + api_key: user.api_key, + user_key: user.api_key, + stories: [ + { + id: story_one.id, + items: [ + story_item_one, + story_item_two + ] + }, + { + id: story_two.id, + items: [ + story_item_three, + story_item_four, + story_item_five + ] + } + ] + } + end + + it 'adds multiple story items to multiple stories' do + expect(story_one.reload.set_items.count).to eq 2 + expect(story_two.reload.set_items.count).to eq 3 + end + + it 'returns a serialized response with the stories and new items' do + expect(JSON.parse(response.body).count).to eq 2 + + expect(JSON.parse(response.body).first).to have_key 'story_id' + expect(JSON.parse(response.body).first).to have_key 'item_ids' + + expect(JSON.parse(response.body).last).to have_key 'story_id' + expect(JSON.parse(response.body).last).to have_key 'item_ids' + end + end + + context 'invalid' do + it 'returns an error when given an invalid story id' do + post :multiple_add, + params: { + api_key: user.api_key, + user_key: user.api_key, + stories: [ + { + id: 'a', + items: [ + story_item_one, + story_item_two + ] + }, + { + id: story_two.id, + items: [ + story_item_three, + story_item_four, + story_item_five + ] + } + ] + } + + expect(response.status).to eq 404 + end + + it 'returns an error when given an invalid item' do + post :multiple_add, + params: { + api_key: user.api_key, + user_key: user.api_key, + stories: [ + { + id: story_one.id, + items: [ + story_item_one, + { + type: 'text', + sub_type: 'heading' + } + ] + } + ] + } + + expect(response.status).to eq 400 + expect(JSON.parse(response.body)['errors']).to eq 'Content value is missing: content must contain value field' + end + + it 'returns an error when trying to update stories that you do not have access too' do + post :multiple_add, + params: { + api_key: user_two.api_key, + user_key: user_two.api_key, + stories: [ + { + id: story_one.id, + items: [ + story_item_one, + story_item_two + ] + }, + { + id: story_two.id, + items: [ + story_item_three, + story_item_four, + story_item_five + ] + } + ] + } + + expect(response.status).to eq 401 + expect(JSON.parse(response.body)['errors']).to eq 'Provided user key is not authorized to access this story' + end + end + end + + describe 'POST multiple_delete' do + let(:user) { create(:user) } + let(:user_two) { create(:user) } + + let!(:story_one) { create(:story_with_dnz_story_items, user_id: user.id) } + let!(:story_two) { create(:story_with_dnz_story_items, user_id: user.id) } + + context 'valid' do + it 'deletes multiple story items from multiple stories' do + expect(story_one.reload.set_items.count).to eq 4 + expect(story_two.reload.set_items.count).to eq 4 + + post :multiple_remove, + params: { + api_key: user.api_key, + user_key: user.api_key, + stories: [ + { + id: story_one.id, + items: [ + { id: story_one.set_items.first.id.to_s }, + { id: story_one.set_items.last.id.to_s } + ] + }, + { + id: story_two.id, + items: [ + { id: story_two.set_items.first.id.to_s }, + { id: story_two.set_items.last.id.to_s } + ] + } + ] + } + + expect(story_one.reload.set_items.count).to eq 2 + expect(story_two.reload.set_items.count).to eq 2 + end + + it 'returns a success response' do + post :multiple_remove, + params: { + api_key: user.api_key, + user_key: user.api_key, + stories: [ + { + id: story_one.id, + items: [ + { id: story_one.set_items.first.id.to_s }, + { id: story_one.set_items.last.id.to_s } + ] + }, + { + id: story_two.id, + items: [ + { id: story_two.set_items.first.id.to_s }, + { id: story_two.set_items.last.id.to_s } + ] + } + ] + } + + expect(response.status).to eq 204 + end + end + + context 'invalid' do + it 'returns an error for stories that do not belong to the user' do + expect(story_one.reload.set_items.count).to eq 4 + expect(story_two.reload.set_items.count).to eq 4 + + post :multiple_remove, + params: { + api_key: user_two.api_key, + user_key: user_two.api_key, + stories: [ + { + id: story_one.id, + items: [ + { id: story_one.set_items.first.id.to_s }, + { id: story_one.set_items.last.id.to_s } + ] + }, + { + id: story_two.id, + items: [ + { id: story_two.set_items.first.id.to_s }, + { id: story_two.set_items.last.id.to_s } + ] + } + ] + } + + expect(response.status).to eq 401 + expect(JSON.parse(response.body)['errors']).to eq 'Provided user key is not authorized to access this story' + expect(story_one.reload.set_items.count).to eq 4 + expect(story_two.reload.set_items.count).to eq 4 + end + + it 'returns an error for invalid story items' do + expect(story_one.reload.set_items.count).to eq 4 + expect(story_two.reload.set_items.count).to eq 4 + + post :multiple_remove, + params: { + api_key: user.api_key, + user_key: user.api_key, + stories: [ + { + id: story_one.id, + items: [ + { id: story_one.set_items.first.id.to_s }, + { id: 'a' } + ] + }, + { + id: story_two.id, + items: [ + { id: story_two.set_items.first.id.to_s }, + { id: story_two.set_items.last.id.to_s } + ] + } + ] + } + + expect(response.status).to eq 404 + expect(JSON.parse(response.body)['errors']).to eq 'Record with ID a was not found' + expect(story_one.reload.set_items.count).to eq 3 + expect(story_two.reload.set_items.count).to eq 4 + end + end + end end end diff --git a/spec/policies/supplejack_api/user_set_policy_spec.rb b/spec/policies/supplejack_api/user_set_policy_spec.rb index 44907c6e1..78e965bbb 100644 --- a/spec/policies/supplejack_api/user_set_policy_spec.rb +++ b/spec/policies/supplejack_api/user_set_policy_spec.rb @@ -74,4 +74,56 @@ end end end + + permissions :multiple_add? do + context 'when user is the owner of the story' do + let(:user) { story.user } + + it 'grants access' do + expect(policy).to permit(user, story) + end + end + + context 'when user is an admin' do + let(:admin) { create(:admin_user) } + + it 'grants access' do + expect(policy).to permit(admin, story) + end + end + + context 'when user is not and admin or owner of story' do + let(:user) { create(:user) } + + it 'denies access' do + expect(policy).not_to permit(user, story) + end + end + end + + permissions :multiple_remove? do + context 'when user is the owner of the story' do + let(:user) { story.user } + + it 'grants access' do + expect(policy).to permit(user, story) + end + end + + context 'when user is an admin' do + let(:admin) { create(:admin_user) } + + it 'grants access' do + expect(policy).to permit(admin, story) + end + end + + context 'when user is not and admin or owner of story' do + let(:user) { create(:user) } + + it 'denies access' do + expect(policy).not_to permit(user, story) + end + end + end end