From 20666482efb3c3710dadd12df51b513078b691e0 Mon Sep 17 00:00:00 2001 From: Robert R George Date: Wed, 13 Sep 2023 02:22:53 -0700 Subject: [PATCH] Added admin api for managing tags (#26872) --- .../api/v1/admin/tags_controller.rb | 74 +++++++++ app/models/tag.rb | 1 + app/serializers/rest/admin/tag_serializer.rb | 2 +- config/routes/api.rb | 2 + spec/requests/api/v1/admin/tags_spec.rb | 141 ++++++++++++++++++ 5 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 app/controllers/api/v1/admin/tags_controller.rb create mode 100644 spec/requests/api/v1/admin/tags_spec.rb diff --git a/app/controllers/api/v1/admin/tags_controller.rb b/app/controllers/api/v1/admin/tags_controller.rb new file mode 100644 index 0000000000..6a7c9f5bf3 --- /dev/null +++ b/app/controllers/api/v1/admin/tags_controller.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +class Api::V1::Admin::TagsController < Api::BaseController + include Authorization + before_action -> { authorize_if_got_token! :'admin:read' }, only: [:index, :show] + before_action -> { authorize_if_got_token! :'admin:write' }, only: :update + + before_action :set_tags, only: :index + before_action :set_tag, except: :index + + after_action :insert_pagination_headers, only: :index + after_action :verify_authorized + + LIMIT = 100 + PAGINATION_PARAMS = %i(limit).freeze + + def index + authorize :tag, :index? + render json: @tags, each_serializer: REST::Admin::TagSerializer + end + + def show + authorize @tag, :show? + render json: @tag, serializer: REST::Admin::TagSerializer + end + + def update + authorize @tag, :update? + @tag.update!(tag_params.merge(reviewed_at: Time.now.utc)) + render json: @tag, serializer: REST::Admin::TagSerializer + end + + private + + def set_tag + @tag = Tag.find(params[:id]) + end + + def set_tags + @tags = Tag.all.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def tag_params + params.permit(:display_name, :trendable, :usable, :listable) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_tags_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v1_admin_tags_url(pagination_params(min_id: pagination_since_id)) unless @tags.empty? + end + + def pagination_max_id + @tags.last.id + end + + def pagination_since_id + @tags.first.id + end + + def records_continue? + @tags.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end +end diff --git a/app/models/tag.rb b/app/models/tag.rb index 7668f16cee..672d80c8b8 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -20,6 +20,7 @@ # class Tag < ApplicationRecord + include Paginable has_and_belongs_to_many :statuses has_and_belongs_to_many :accounts diff --git a/app/serializers/rest/admin/tag_serializer.rb b/app/serializers/rest/admin/tag_serializer.rb index 425ba4ba34..54dbbe30ad 100644 --- a/app/serializers/rest/admin/tag_serializer.rb +++ b/app/serializers/rest/admin/tag_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class REST::Admin::TagSerializer < REST::TagSerializer - attributes :id, :trendable, :usable, :requires_review + attributes :id, :trendable, :usable, :requires_review, :listable def id object.id.to_s diff --git a/config/routes/api.rb b/config/routes/api.rb index 66eb82f59a..0fe9f69abc 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -280,6 +280,8 @@ namespace :api, format: false do post :test end end + + resources :tags, only: [:index, :show, :update] end end diff --git a/spec/requests/api/v1/admin/tags_spec.rb b/spec/requests/api/v1/admin/tags_spec.rb new file mode 100644 index 0000000000..031be17f52 --- /dev/null +++ b/spec/requests/api/v1/admin/tags_spec.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Tags' do + let(:role) { UserRole.find_by(name: 'Admin') } + let(:user) { Fabricate(:user, role: role) } + let(:scopes) { 'admin:read admin:write' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:tag) { Fabricate(:tag) } + let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } + + describe 'GET /api/v1/admin/tags' do + subject do + get '/api/v1/admin/tags', headers: headers, params: params + end + + let(:params) { {} } + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', '' + + it 'returns http success' do + subject + + expect(response).to have_http_status(200) + end + + context 'when there are no tags' do + it 'returns an empty list' do + subject + + expect(body_as_json).to be_empty + end + end + + context 'when there are tagss' do + let!(:tags) do + [ + Fabricate(:tag), + Fabricate(:tag), + Fabricate(:tag), + Fabricate(:tag), + ] + end + + it 'returns the expected tags' do + subject + tags.each do |tag| + expect(body_as_json.find { |item| item[:id] == tag.id.to_s && item[:name] == tag.name }).to_not be_nil + end + end + + context 'with limit param' do + let(:params) { { limit: 2 } } + + it 'returns only the requested number of tags' do + subject + + expect(body_as_json.size).to eq(params[:limit]) + end + end + end + end + + describe 'GET /api/v1/admin/tags/:id' do + subject do + get "/api/v1/admin/tags/#{tag.id}", headers: headers + end + + let!(:tag) { Fabricate(:tag) } + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', '' + + it 'returns http success' do + subject + + expect(response).to have_http_status(200) + end + + it 'returns expected tag content' do + subject + + expect(body_as_json[:id].to_i).to eq(tag.id) + expect(body_as_json[:name]).to eq(tag.name) + end + + context 'when the requested tag does not exist' do + it 'returns http not found' do + get '/api/v1/admin/tags/-1', headers: headers + + expect(response).to have_http_status(404) + end + end + end + + describe 'PUT /api/v1/admin/tags/:id' do + subject do + put "/api/v1/admin/tags/#{tag.id}", headers: headers, params: params + end + + let!(:tag) { Fabricate(:tag) } + let(:params) { { display_name: tag.name.upcase } } + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong scope', 'admin:read' + it_behaves_like 'forbidden for wrong role', '' + + it 'returns http success' do + subject + + expect(response).to have_http_status(200) + end + + it 'returns updated tag' do + subject + + expect(body_as_json[:id].to_i).to eq(tag.id) + expect(body_as_json[:name]).to eq(tag.name.upcase) + end + + context 'when the updated display name is invalid' do + let(:params) { { display_name: tag.name + tag.id.to_s } } + + it 'returns http unprocessable content' do + subject + + expect(response).to have_http_status(422) + end + end + + context 'when the requested tag does not exist' do + it 'returns http not found' do + get '/api/v1/admin/tags/-1', headers: headers + + expect(response).to have_http_status(404) + end + end + end +end