diff --git a/.rubocop.yml b/.rubocop.yml index 19fdc4e42065fb..76a5f3c2e4c7c4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -129,6 +129,10 @@ RSpec/FilePath: - 'spec/config/initializers/rack_attack_spec.rb' # namespaces usually have separate folder - 'spec/lib/sanitize_config_spec.rb' # namespaces usually have separate folder +RSpec/LetSetup: + Exclude: + - spec/imastodon/**/* + # Reason: # https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnamedsubject RSpec/NamedSubject: diff --git a/app/controllers/api/v1/favourite_tags_controller.rb b/app/controllers/api/v1/favourite_tags_controller.rb index bd26bf30c061f9..7740e57ded799d 100644 --- a/app/controllers/api/v1/favourite_tags_controller.rb +++ b/app/controllers/api/v1/favourite_tags_controller.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class Api::V1::FavouriteTagsController < Api::BaseController - before_action :set_account before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:index] before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, except: [:index] before_action :require_user! @@ -9,57 +8,41 @@ class Api::V1::FavouriteTagsController < Api::BaseController respond_to :json def index - render json: current_favourite_tags + current_account = current_user.account + orderd_favourite_tags = current_account.favourite_tags.with_order + + render json: orderd_favourite_tags.map(&:to_json_for_api) end def create - tag = find_or_init_tag - @favourite_tag = FavouriteTag.new(account: @account, tag: tag, visibility: favourite_tag_visibility) - if @favourite_tag.save - render json: @favourite_tag.to_json_for_api - else - render json: find_fav_tag_by(tag).to_json_for_api, status: 409 + current_account = current_user.account + favourite_tag = current_account.favourite_tags.find_by(name: create_params[:name], visibility: create_params[:visibility]) + + if favourite_tag.present? + render json: favourite_tag.to_json_for_api, status: 409 + return end + + favourite_tag = FavouriteTag.new(account: current_account, name: create_params[:name], visibility: create_params[:visibility]) + favourite_tag.save! + render json: favourite_tag.to_json_for_api end def destroy - tag = find_tag - @favourite_tag = find_fav_tag_by(tag) - if @favourite_tag.nil? - render json: { succeeded: false }, status: 404 + current_account = current_user.account + favourite_tag = current_account.favourite_tags.find_by(id: params[:id]) + if favourite_tag.nil? + render json: { error: 'FavouriteTag is not found' }, status: 404 else - @favourite_tag.destroy - render json: { succeeded: true } + favourite_tag.destroy! end end private - def tag_params - params.permit(:tag, :visibility) - end - - def set_account - @account = current_user.account - end - - def find_or_init_tag - Tag.find_or_initialize_by(name: tag_params[:tag]) - end - - def find_tag - Tag.find_by(name: tag_params[:tag]) - end - - def find_fav_tag_by(tag) - @account.favourite_tags.find_by(tag: tag) - end - - def favourite_tag_visibility - tag_params[:visibility].nil? ? 'public' : tag_params[:visibility] - end - - def current_favourite_tags - current_account.favourite_tags.with_order.includes(:tag).map(&:to_json_for_api) + def create_params + params.permit(:name, :visibility) + params[:visibility] ||= 'public' + params end end diff --git a/app/controllers/settings/favourite_tags_controller.rb b/app/controllers/settings/favourite_tags_controller.rb index 4f6dafe2864e60..b552ad69953fa9 100644 --- a/app/controllers/settings/favourite_tags_controller.rb +++ b/app/controllers/settings/favourite_tags_controller.rb @@ -3,33 +3,42 @@ class Settings::FavouriteTagsController < Settings::BaseController layout 'admin' before_action :authenticate_user! - before_action :set_account - before_action :set_favourite_tags, only: [:index, :create] - before_action :set_favourite_tag, only: [:edit, :update, :destroy] def index - @favourite_tag = FavouriteTag.new(tag: Tag.new, visibility: FavouriteTag.visibilities[:public]) + @favourite_tags = current_account.favourite_tags.with_order + @favourite_tag = FavouriteTag.new(visibility: FavouriteTag.visibilities[:public]) end def edit - @favourite_tag + @favourite_tag = current_account.favourite_tags.find(params[:id]) end def create - name = tag_params[:name].delete_prefix('#') - tag = Tag.find_or_initialize_by(name: name) - @favourite_tag = FavouriteTag.new(account: @account, tag: tag, visibility: favourite_tag_params[:visibility], order: favourite_tag_params[:order]) + @favourite_tag = FavouriteTag.new( + account: current_account, + name: create_params[:name].delete_prefix('#'), + order: create_params[:order], + visibility: create_params[:visibility] + ) + if @favourite_tag.save redirect_to settings_favourite_tags_path, notice: I18n.t('generic.changes_saved_msg') else + @favourite_tags = current_account.favourite_tags.with_order render :index end end def update - name = tag_params[:name].delete_prefix('#') - tag = Tag.find_or_initialize_by(name: name) - if @favourite_tag.update(tag: tag, visibility: favourite_tag_params[:visibility], order: favourite_tag_params[:order]) + @favourite_tag = current_account.favourite_tags.find(params[:id]) + + @favourite_tag.update( + name: update_params[:name].delete_prefix('#'), + order: update_params[:order], + visibility: update_params[:visibility] + ) + + if @favourite_tag.save redirect_to settings_favourite_tags_path, notice: I18n.t('generic.changes_saved_msg') else render :edit @@ -37,29 +46,21 @@ def update end def destroy - @favourite_tag.destroy + current_account.favourite_tags.destroy(params[:id]) redirect_to settings_favourite_tags_path end private - def tag_params - params.require(:favourite_tag).require(:tag_attributes).permit(:id, :name) - end - - def favourite_tag_params - params.require(:favourite_tag).permit(:visibility, :order, { tag_attributes: [:id, :name] }) - end - - def set_account - @account = current_user.account + def create_params + params.require(:favourite_tag).permit(:name, :visibility, :order) end - def set_favourite_tag - @favourite_tag = @account.favourite_tags.find(params[:id]) + def update_params + params.require(:favourite_tag).permit(:name, :visibility, :order) end - def set_favourite_tags - @favourite_tags = @account.favourite_tags.with_order.includes(:tag) + def current_account + current_user.account end end diff --git a/app/javascript/mastodon/actions/favourite_tags.js b/app/javascript/mastodon/actions/favourite_tags.js index dc358d277f3240..68f6a301623409 100644 --- a/app/javascript/mastodon/actions/favourite_tags.js +++ b/app/javascript/mastodon/actions/favourite_tags.js @@ -15,7 +15,7 @@ export function refreshFavouriteTags() { export function addFavouriteTags(tag, visibility) { return (dispatch, getState) => { api(getState).post('/api/v1/favourite_tags', { - tag: tag, + name: tag, visibility: visibility, }).then(() => { dispatch(refreshFavouriteTags()); @@ -23,9 +23,9 @@ export function addFavouriteTags(tag, visibility) { }; } -export function removeFavouriteTags(tag) { +export function removeFavouriteTags(id) { return (dispatch, getState) => { - api(getState).delete(`/api/v1/favourite_tags/${tag}`).then(() => { + api(getState).delete(`/api/v1/favourite_tags/${id}`).then(() => { dispatch(refreshFavouriteTags()); }).catch(() => {}); }; diff --git a/app/javascript/mastodon/features/hashtag_timeline/components/favourite_toggle.jsx b/app/javascript/mastodon/features/hashtag_timeline/components/favourite_toggle.jsx index 86a347856438b4..5db6a22210f85c 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/components/favourite_toggle.jsx +++ b/app/javascript/mastodon/features/hashtag_timeline/components/favourite_toggle.jsx @@ -9,7 +9,8 @@ import Button from '../../../components/button'; const messages = defineMessages({ add_favourite_tags_public: { id: 'tag.add_favourite.public', defaultMessage: 'add in the favourite tags (Public)' }, add_favourite_tags_unlisted: { id: 'tag.add_favourite.unlisted', defaultMessage: 'add in the favourite tags (Unlisted)' }, - remove_favourite_tags: { id: 'tag.remove_favourite', defaultMessage: 'Remove from the favourite tags' }, + remove_favourite_tags_public: { id: 'tag.remove_favourite.public', defaultMessage: 'Remove from the favourite tags (Public)' }, + remove_favourite_tags_unlisted: { id: 'tag.remove_favourite.unlisted', defaultMessage: 'Remove from the favourite tags (Unlisted)' }, }); class FavouriteToggle extends React.PureComponent { @@ -18,7 +19,8 @@ class FavouriteToggle extends React.PureComponent { tag: PropTypes.string.isRequired, addFavouriteTags: PropTypes.func.isRequired, removeFavouriteTags: PropTypes.func.isRequired, - isRegistered: PropTypes.bool.isRequired, + unlistedId: PropTypes.number, + publicId: PropTypes.number, intl: PropTypes.object.isRequired, }; @@ -34,25 +36,33 @@ class FavouriteToggle extends React.PureComponent { this.addFavouriteTags('unlisted'); }; - removeFavouriteTags = () => { - this.props.removeFavouriteTags(this.props.tag); + removeFavouriteTags = (id) => { + this.props.removeFavouriteTags(id); }; + removePublic = () => { + this.removeFavouriteTags(this.props.publicId) + } + + removeUnlisted = () => { + this.removeFavouriteTags(this.props.unlistedId) + } + render () { - const { intl, isRegistered } = this.props; + const { intl, unlistedId, publicId } = this.props; return (
- { isRegistered ? -
-
- : -
-
- } +
+ { + publicId != null ?
); } diff --git a/app/javascript/mastodon/features/hashtag_timeline/containers/favourite_toggle_container.js b/app/javascript/mastodon/features/hashtag_timeline/containers/favourite_toggle_container.js index b2d29ed0c84d09..8abd3a74fe6ac4 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/containers/favourite_toggle_container.js +++ b/app/javascript/mastodon/features/hashtag_timeline/containers/favourite_toggle_container.js @@ -4,7 +4,8 @@ import { addFavouriteTags, removeFavouriteTags } from '../../../actions/favourit import FavouriteToggle from '../components/favourite_toggle'; const mapStateToProps = (state, { tag }) => ({ - isRegistered: state.getIn(['favourite_tags', 'tags']).some(t => t.get('name') === tag), + publicId: state.getIn(['favourite_tags', 'tags']).find(t => t.get('name') === tag && t.get('visibility') === 'public')?.get('id'), + unlistedId: state.getIn(['favourite_tags', 'tags']).find(t => t.get('name') === tag && t.get('visibility') === 'unlisted')?.get('id'), }); const mapDispatchToProps = dispatch => ({ @@ -13,8 +14,8 @@ const mapDispatchToProps = dispatch => ({ dispatch(addFavouriteTags(tag, visibility)); }, - removeFavouriteTags (tag) { - dispatch(removeFavouriteTags(tag)); + removeFavouriteTags (id) { + dispatch(removeFavouriteTags(id)); }, }); diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 24a0002f54960f..842e08c3bfa8bd 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -681,7 +681,8 @@ "tabs_bar.notifications": "Notifications", "tag.add_favourite.public": "add in the favourite tags (Public)", "tag.add_favourite.unlisted": "add in the favourite tags (Unlisted)", - "tag.remove_favourite": "Remove from the favourite tags", + "tag.remove_favourite.public": "Remove from the favourite tagss (Public)", + "tag.remove_favourite.unlisted": "Remove from the favourite tagsags (Unlisted)", "time_remaining.days": "{number, plural, one {# day} other {# days}} left", "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", diff --git a/app/javascript/mastodon/locales/ja-IM.json b/app/javascript/mastodon/locales/ja-IM.json index ccdcc3c07662a2..0195cb09a118b0 100644 --- a/app/javascript/mastodon/locales/ja-IM.json +++ b/app/javascript/mastodon/locales/ja-IM.json @@ -357,7 +357,8 @@ "tabs_bar.search": "検索", "tag.add_favourite.public": "スウィーティー☆(公開資料)", "tag.add_favourite.unlisted": "スウィーティー☆(社内資料)", - "tag.remove_favourite": "スウィーティーじゃない", + "tag.remove_favourite.public": "スウィーティーじゃない(公開)", + "tag.remove_favourite.unlisted": "スウィーティーじゃない(未収載)", "time_remaining.days": "残り{number}日", "time_remaining.hours": "残り{number}時間", "time_remaining.minutes": "残り{number}分", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 1f99ca336240fc..89f00cd1a82cb5 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -681,7 +681,8 @@ "tabs_bar.notifications": "通知", "tag.add_favourite.public": "お気に入り登録(公開)", "tag.add_favourite.unlisted": "お気に入り登録(未収載)", - "tag.remove_favourite": "お気に入り解除", + "tag.remove_favourite.public": "お気に入り解除(公開)", + "tag.remove_favourite.unlisted": "お気に入り解除(未収載)", "time_remaining.days": "残り{number}日", "time_remaining.hours": "残り{number}時間", "time_remaining.minutes": "残り{number}分", diff --git a/app/lib/friends/favourite_tags_extension.rb b/app/lib/friends/favourite_tags_extension.rb index e76ba2623b8763..1dd56515aa6ecd 100644 --- a/app/lib/friends/favourite_tags_extension.rb +++ b/app/lib/friends/favourite_tags_extension.rb @@ -17,7 +17,7 @@ module FavouriteTagsExtension def add_default_favourite_tag DEFAULT_TAGS.each_with_index do |tag_name, i| - favourite_tags.create!(visibility: 'unlisted', tag: Tag.find_or_create_by!(name: HashtagNormalizer.new.normalize(tag_name)), order: (DEFAULT_TAGS.length - i)) + favourite_tags.create!(visibility: 'unlisted', name: tag_name, order: (DEFAULT_TAGS.length - i)) end end end diff --git a/app/models/favourite_tag.rb b/app/models/favourite_tag.rb index 42c85b7464ae36..fd2374a8ee56a8 100644 --- a/app/models/favourite_tag.rb +++ b/app/models/favourite_tag.rb @@ -11,23 +11,21 @@ # updated_at :datetime not null # visibility :integer default("public"), not null # order :integer default(0), not null +# name :string # class FavouriteTag < ApplicationRecord enum visibility: { public: 0, unlisted: 1, private: 2, direct: 3 }, _suffix: :visibility belongs_to :account, optional: false - belongs_to :tag, optional: false - accepts_nested_attributes_for :tag + belongs_to :tag, optional: true - validates :tag, uniqueness: { scope: :account } + validates :name, format: { with: Tag::HASHTAG_NAME_RE }, uniqueness: { scope: [:account, :visibility] } validates :visibility, presence: true validates :order, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } scope :with_order, -> { order(order: :desc, id: :asc) } - delegate :name, to: :tag - def to_json_for_api { id: id, @@ -35,4 +33,10 @@ def to_json_for_api visibility: visibility, } end + + def migrate_tag_name! + return if name.present? + + update!(name: tag&.name) + end end diff --git a/app/views/settings/favourite_tags/_form.html.haml b/app/views/settings/favourite_tags/_form.html.haml index 0dc806213d85d0..11092d9cbe2078 100644 --- a/app/views/settings/favourite_tags/_form.html.haml +++ b/app/views/settings/favourite_tags/_form.html.haml @@ -1,7 +1,6 @@ = simple_form_for [:settings, favourite_tag] do |f| = render 'shared/error_messages', object: favourite_tag - = f.simple_fields_for :tag do |ff| - = ff.input :name, placeholder: t('favourite_tags.name_of_tag') + = f.input :name, placeholder: t('favourite_tags.name_of_tag') = f.input :visibility, label: t('simple_form.labels.defaults.setting_default_privacy'), collection: Status.visibilities.keys[0..1], diff --git a/app/views/settings/favourite_tags/index.html.haml b/app/views/settings/favourite_tags/index.html.haml index a990a51d132a40..37fe961c008a52 100644 --- a/app/views/settings/favourite_tags/index.html.haml +++ b/app/views/settings/favourite_tags/index.html.haml @@ -18,7 +18,7 @@ %tr %td = fa_icon 'tag' - = fav_tag.tag.name + = fav_tag.name %td = I18n.t("statuses.visibilities.#{fav_tag.visibility}") %td diff --git a/config/routes/api.rb b/config/routes/api.rb index 86d91aef103495..cbf4e4fb0c21b8 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -284,7 +284,7 @@ resources :tags, only: [:index, :show, :update] end - resources :favourite_tags, only: [:index, :create, :destroy], param: :tag + resources :favourite_tags, only: [:index, :create, :destroy] end namespace :v2 do diff --git a/db/migrate/20250105010419_add_column_name_to_favourite_tags.rb b/db/migrate/20250105010419_add_column_name_to_favourite_tags.rb new file mode 100644 index 00000000000000..5b76201fb6b708 --- /dev/null +++ b/db/migrate/20250105010419_add_column_name_to_favourite_tags.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require Rails.root.join('lib', 'mastodon', 'migration_helpers') + +class AddColumnNameToFavouriteTags < ActiveRecord::Migration[7.0] + include Mastodon::MigrationHelpers + disable_ddl_transaction! + + def change + safety_assured do + add_column :favourite_tags, :name, :string + change_column_null :favourite_tags, :tag_id, true + end + end +end diff --git a/db/migrate/20250105030426_add_index_account_visibility_on_favourite_tags.rb b/db/migrate/20250105030426_add_index_account_visibility_on_favourite_tags.rb new file mode 100644 index 00000000000000..7e2c5471ad1459 --- /dev/null +++ b/db/migrate/20250105030426_add_index_account_visibility_on_favourite_tags.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require Rails.root.join('lib', 'mastodon', 'migration_helpers') + +class AddIndexAccountVisibilityOnFavouriteTags < ActiveRecord::Migration[7.0] + include Mastodon::MigrationHelpers + disable_ddl_transaction! + + def change + safety_assured do + add_index :favourite_tags, [:account_id, :name, :visibility], unique: true + end + end +end diff --git a/db/schema.rb b/db/schema.rb index f274359b66afb6..205dfb37875a38 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_09_07_150100) do +ActiveRecord::Schema[7.0].define(version: 2025_01_05_030426) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -457,8 +457,10 @@ t.datetime "updated_at", precision: nil, null: false t.integer "visibility", default: 0, null: false t.bigint "account_id", null: false - t.bigint "tag_id", null: false + t.bigint "tag_id" t.integer "order", default: 0, null: false + t.string "name" + t.index ["account_id", "name", "visibility"], name: "index_favourite_tags_on_account_id_and_name_and_visibility", unique: true t.index ["account_id", "tag_id"], name: "index_favourite_tags_on_account_id_and_tag_id", unique: true end diff --git a/lib/tasks/imastodon.rake b/lib/tasks/imastodon.rake new file mode 100644 index 00000000000000..ba52828ba353c6 --- /dev/null +++ b/lib/tasks/imastodon.rake @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +namespace :imastodon do + task migrate_favourite_tags: :environment do + FavouriteTag.all.includes(:tag).find_in_batches do |favourite_tags| + Rails.logger.info("imastodon:migrate_favourite_tags: #{favourite_tags.first.id}..#{favourite_tags.last.id}") + favourite_tags.each(&:migrate_tag_name!) + end + end +end diff --git a/spec/fabricators/favourite_tags_fabricator.rb b/spec/fabricators/favourite_tags_fabricator.rb index fb454b0219a6a1..24abb1b724e99b 100644 --- a/spec/fabricators/favourite_tags_fabricator.rb +++ b/spec/fabricators/favourite_tags_fabricator.rb @@ -2,7 +2,7 @@ Fabricator(:favourite_tag) do account - tag + name 'test' visibility 0 order 0 end diff --git a/spec/imastodon/controllers/api/v1/favourite_tags_controller_spec.rb b/spec/imastodon/controllers/api/v1/favourite_tags_controller_spec.rb index 655155d89ba92b..a7af33cd151a0d 100644 --- a/spec/imastodon/controllers/api/v1/favourite_tags_controller_spec.rb +++ b/spec/imastodon/controllers/api/v1/favourite_tags_controller_spec.rb @@ -7,7 +7,6 @@ let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } - let(:tag) { Fabricate(:tag, name: tag_name) } before do allow(controller).to receive(:doorkeeper_token) { token } @@ -19,50 +18,193 @@ it 'returns http success' do get :index expect(response).to have_http_status(:success) + body = JSON.parse(response.body, symbolize_names: true) + expect(body).to match( + [ + { id: be_integer, name: 'デレラジ', visibility: 'unlisted' }, + { id: be_integer, name: 'デレパ', visibility: 'unlisted' }, + { id: be_integer, name: 'imas_mor', visibility: 'unlisted' }, + { id: be_integer, name: 'millionradio', visibility: 'unlisted' }, + { id: be_integer, name: 'sidem', visibility: 'unlisted' }, + ] + ) end end describe 'POST #create' do subject { post :create, params: params } - let(:scopes) { 'write:statuses' } - let(:tag_name) { 'dummy_tag' } - let(:params) do - { - tag: tag_name, - visibility: 'public', - } + let!(:scopes) { 'write:statuses' } + + context '新しいタグをお気に入りタグに登録するとき' do + context '公開範囲をpublicで登録するとき' do + let!(:params) do + { + name: 'お気に入りタグ', + visibility: 'public', + } + end + + it '新しいお気に入りタグのレコードが記録され、ステータスコード200と、作成されたお気に入りタグがレスポンスボディとして返る' do + expect { subject }.to change { user.account.favourite_tags.count }.by(1) + + created = FavouriteTag.last + expect(created.name).to eq('お気に入りタグ') + expect(created.visibility).to eq('public') + + expect(response).to have_http_status(:success) + body = JSON.parse(response.body, symbolize_names: true) + expect(body).to match({ + id: created.id, + name: 'お気に入りタグ', + visibility: 'public', + }) + end + end + + context '公開範囲をunlistedで登録するとき' do + let!(:params) do + { + name: 'お気に入りタグ', + visibility: 'unlisted', + } + end + + it '新しいお気に入りタグのレコードが記録され、ステータスコード200と、作成されたお気に入りタグがレスポンスボディとして返る' do + expect { subject }.to change { user.account.favourite_tags.count }.by(1) + + created = FavouriteTag.last + expect(created.name).to eq('お気に入りタグ') + expect(created.visibility).to eq('unlisted') + + expect(response).to have_http_status(:success) + body = JSON.parse(response.body, symbolize_names: true) + expect(body).to match({ + id: created.id, + name: 'お気に入りタグ', + visibility: 'unlisted', + }) + end + end + end + + context '登録しようとしたタグが既にお気に入りタグに登録されているとき' do + let!(:already_exists) { Fabricate(:favourite_tag, account: user.account, name: '登録済みのお気に入りタグ', visibility: 'public') } + + context '公開範囲も同じとき' do + let!(:params) do + { + name: '登録済みのお気に入りタグ', + visibility: 'public', + } + end + + it '新しいお気に入りタグのレコードは増えず、ステータスコード409と、既存のお気に入りタグがレスポンスボディとして返る' do + expect { subject }.to_not(change { user.account.favourite_tags.count }) + + expect(response).to have_http_status(409) + body = JSON.parse(response.body, symbolize_names: true) + expect(body).to match({ + id: already_exists.id, + name: '登録済みのお気に入りタグ', + visibility: 'public', + }) + end + end + + context '公開範囲が異なるとき' do + let!(:params) do + { + name: '登録済みのお気に入りタグ', + visibility: 'unlisted', + } + end + + it '新しいお気に入りタグのレコードが記録され、ステータスコード200と、作成されたお気に入りタグがレスポンスボディとして返る' do + expect { subject }.to change { user.account.favourite_tags.count }.by(1) + + created = FavouriteTag.last + expect(created.name).to eq('登録済みのお気に入りタグ') + expect(created.visibility).to eq('unlisted') + + expect(response).to have_http_status(:success) + body = JSON.parse(response.body, symbolize_names: true) + expect(body).to match({ + id: created.id, + name: '登録済みのお気に入りタグ', + visibility: 'unlisted', + }) + end + end end - context 'when the tag is a new favourite tag' do - it 'returns http success' do + context '英字のタグを大文字小文字違いで登録しようとしたとき' do + let!(:already_exists) { Fabricate(:favourite_tag, account: user.account, name: 'already_favourited_tag', visibility: 'public') } + + let!(:params) do + { + name: 'Already_favourited_tag', + visibility: 'public', + } + end + + it '新しいお気に入りタグのレコードが記録され、ステータスコード200と、作成されたお気に入りタグがレスポンスボディとして返る' do expect { subject }.to change { user.account.favourite_tags.count }.by(1) + + created = FavouriteTag.last + expect(created.name).to eq('Already_favourited_tag') + expect(created.visibility).to eq('public') + expect(response).to have_http_status(:success) + body = JSON.parse(response.body, symbolize_names: true) + expect(body).to match({ + id: created.id, + name: 'Already_favourited_tag', + visibility: 'public', + }) + end + end + + context '公開範囲が指定されなかった場合' do + let!(:params) do + { + name: 'お気に入りタグ', + } end - it 'responce has created tag' do + it '新しいお気に入りタグのレコードが公開範囲publicとして記録され、ステータスコード200と、作成されたお気に入りタグがレスポンスボディとして返る' do expect { subject }.to change { user.account.favourite_tags.count }.by(1) - expect( - JSON.parse(response.body, symbolize_names: true).except(:id) - ).to eq({ name: tag_name, visibility: 'public' }) + + created = FavouriteTag.last + expect(created.name).to eq('お気に入りタグ') + expect(created.visibility).to eq('public') + + expect(response).to have_http_status(:success) + body = JSON.parse(response.body, symbolize_names: true) + expect(body).to match({ + id: created.id, + name: 'お気に入りタグ', + visibility: 'public', + }) end end - context 'when the tag has already been favourite.' do - before do - Fabricate(:favourite_tag, account: user.account, tag: tag) + context 'validでない名前のタグを登録しようとしたとき' do + let!(:params) do + { + name: 'invalid tag name', + visibility: 'public', + } end - it 'returns http 409' do + it '新しいお気に入りタグのレコードは増えず、ステータスコード422と、バリデーションエラーの内容が返る' do expect { subject }.to_not(change { user.account.favourite_tags.count }) - expect(response).to have_http_status(409) - end - it 'does not create new favourite_tag' do - expect { subject }.to_not(change { user.account.favourite_tags.count }) - expect( - JSON.parse(response.body, symbolize_names: true).except(:id) - ).to eq({ name: tag_name, visibility: 'public' }) + expect(response).to have_http_status(422) + body = JSON.parse(response.body, symbolize_names: true) + expect(body).to match({ + error: 'Validation failed: Name is invalid', + }) end end end @@ -70,42 +212,46 @@ describe 'DELETE #destroy' do subject { delete :destroy, params: params } - let(:scopes) { 'write:statuses' } - let(:params) { { tag: tag_name } } + let!(:scopes) { 'write:statuses' } - context 'when try to destroy the favourite tag' do - let(:tag_name) { 'dummy_tag' } + context '存在するお気に入りタグのIDを指定したとき' do + let!(:favourite_tag) { Fabricate(:favourite_tag, account: user.account) } + let!(:params) { { id: favourite_tag.id } } - before do - Fabricate(:favourite_tag, account: user.account, tag: tag) - end - - it 'returns http success' do + it 'ステータスコード200が返り、お気に入りタグのレコード数が1つ減る' do expect { subject }.to change { user.account.favourite_tags.count }.by(-1) - expect(response).to have_http_status(:success) + expect(response).to have_http_status(204) end + end + + context '存在しないお気に入りタグのIDを指定したとき' do + let!(:params) { { id: 1 } } + + it 'ステータスコード404が返り、お気に入りタグのレコード数は変わらない' do + expect { subject }.to_not(change { user.account.favourite_tags.count }) + expect(response).to have_http_status(404) - it 'responce has success message by json' do - expect { subject }.to change { user.account.favourite_tags.count }.by(-1) expect( JSON.parse(response.body, symbolize_names: true) - ).to eq({ succeeded: true }) + ).to eq({ + error: 'FavouriteTag is not found', + }) end end - context 'when try to destroy an unregistered tag' do - let(:tag_name) { 'unregistered' } + context '自分以外の人のお気に入りタグのIDを指定したとき' do + let!(:favourite_tag) { Fabricate(:favourite_tag) } + let!(:params) { { id: favourite_tag.id } } - it 'returns http 404' do + it 'ステータスコード404が返り、お気に入りタグのレコード数は変わらない' do expect { subject }.to_not(change { user.account.favourite_tags.count }) expect(response).to have_http_status(404) - end - it 'responce has fail message by json' do - expect { subject }.to_not(change { user.account.favourite_tags.count }) expect( JSON.parse(response.body, symbolize_names: true) - ).to eq({ succeeded: false }) + ).to eq({ + error: 'FavouriteTag is not found', + }) end end end diff --git a/spec/imastodon/controllers/settings/favourite_tags_controller_spec.rb b/spec/imastodon/controllers/settings/favourite_tags_controller_spec.rb index 5fa39a2956bb9c..47146dceec79c9 100644 --- a/spec/imastodon/controllers/settings/favourite_tags_controller_spec.rb +++ b/spec/imastodon/controllers/settings/favourite_tags_controller_spec.rb @@ -26,8 +26,7 @@ end describe 'GET #edit' do - let(:tag) { Fabricate(:tag, name: 'dummy_tag') } - let!(:favourite_tag) { Fabricate(:favourite_tag, account: user.account, tag: tag) } + let!(:favourite_tag) { Fabricate(:favourite_tag, account: user.account) } context 'when the favourite tag is found.' do before do @@ -62,20 +61,12 @@ let(:params) do { favourite_tag: { - tag_attributes: { - name: tag_name, - }, + name: tag_name, visibility: 'public', order: 1, }, } end - let!(:tag) { Fabricate(:tag, name: tag_name) } - - it 'after create, tag' do - expect { subject }.to_not change(Tag, :count) - expect(response).to redirect_to(settings_favourite_tags_path) - end it 'after create, favourite tag' do expect { subject }.to change(FavouriteTag, :count).by(1) @@ -84,7 +75,7 @@ context 'when the tag has already been favourite.' do before do - Fabricate(:favourite_tag, account: user.account, tag: tag) + Fabricate(:favourite_tag, account: user.account, name: tag_name) end it 'does not create any tags and should render index template' do @@ -95,8 +86,7 @@ end describe 'PUT #update' do - let(:tag) { Fabricate(:tag, name: 'dummy_tag') } - let!(:favourite_tag) { Fabricate(:favourite_tag, account: user.account, tag: tag) } + let!(:favourite_tag) { Fabricate(:favourite_tag, account: user.account) } context 'The favourite tag can update.' do subject { put :update, params: params } @@ -105,25 +95,18 @@ { id: favourite_tag.id, favourite_tag: { - tag_attributes: { - name: "dummy_tag_#{favourite_tag.id}", - }, + name: "dummy_tag_#{favourite_tag.id}", visibility: 'unlisted', order: 2, }, } end - it 'after update, tag' do - expect { subject }.to change(Tag, :count).by(1) - expect(assigns(:favourite_tag).tag.name).to_not eq('dummy_tag') - expect(response).to redirect_to(settings_favourite_tags_path) - end - it 'after update, favourite tag' do expect { subject }.to_not change(FavouriteTag, :count) expect(assigns(:favourite_tag).visibility).to eq('unlisted') expect(assigns(:favourite_tag).order).to eq(2) + expect(assigns(:favourite_tag).name).to eq("dummy_tag_#{favourite_tag.id}") expect(response).to redirect_to(settings_favourite_tags_path) end end @@ -131,27 +114,24 @@ context 'The favourite tag could not update, because tag has already been registered.' do subject { put :update, params: params } - let(:tag_name) { 'dummy_tag2' } + let(:tag_name) { 'duplicated' } let(:params) do { id: favourite_tag.id, favourite_tag: { - tag_attributes: { - name: tag_name, - }, - visibility: 'unlisted', + name: tag_name, + visibility: 'public', order: 2, }, } end - let(:tag2) { Fabricate(:tag, name: tag_name) } before do - Fabricate(:favourite_tag, account: user.account, tag: tag2) + Fabricate(:favourite_tag, account: user.account, name: 'duplicated') end it 'does not update any tags and should render edit template' do - expect { subject }.to_not change(Tag, :count) + subject expect(response).to render_template(:edit) end end @@ -160,19 +140,13 @@ describe 'DELETE #destroy' do subject { delete :destroy, params: params } - let(:tag) { Fabricate(:tag, name: 'dummy_tag') } - let!(:favourite_tag) { Fabricate(:favourite_tag, account: user.account, tag: tag) } + let!(:favourite_tag) { Fabricate(:favourite_tag, account: user.account, name: 'dummy_tag') } let(:params) do { id: favourite_tag.id, } end - it 'after destroy, tag' do - expect { subject }.to_not change(Tag, :count) - expect(response).to redirect_to(settings_favourite_tags_path) - end - it 'after destroy, favourite tag' do expect { subject }.to change(FavouriteTag, :count).by(-1) expect(response).to redirect_to(settings_favourite_tags_path) diff --git a/spec/imastodon/models/favourite_tags_spec.rb b/spec/imastodon/models/favourite_tags_spec.rb index f92acc7a35fb19..c40def5ebb0ace 100644 --- a/spec/imastodon/models/favourite_tags_spec.rb +++ b/spec/imastodon/models/favourite_tags_spec.rb @@ -5,24 +5,44 @@ RSpec.describe FavouriteTag, type: :model do describe 'validation' do let(:account) { Fabricate :account } - let(:tag) { Fabricate(:tag, name: 'valid_tag') } - it 'valid visibility' do - expect(described_class.new(account: account, tag: tag, visibility: 0)).to be_valid - expect(described_class.new(account: account, tag: tag, visibility: 1)).to be_valid - expect(described_class.new(account: account, tag: tag, visibility: 2)).to be_valid - expect(described_class.new(account: account, tag: tag, visibility: 3)).to be_valid - end + describe 'visibility' do + it '値が0(public)から3(direct)ならvalid' do + expect(described_class.new(account: account, name: 'tag', visibility: 0)).to be_valid + expect(described_class.new(account: account, name: 'tag', visibility: 1)).to be_valid + expect(described_class.new(account: account, name: 'tag', visibility: 2)).to be_valid + expect(described_class.new(account: account, name: 'tag', visibility: 3)).to be_valid + end - context 'when visibility is out of ranges' do - it 'invalid visibility' do - expect { described_class.new(account: account, tag: tag, visibility: 4) }.to raise_error(ArgumentError) + it '値が4(enum上で未定義)ならArgumentErrorをraise' do + expect { described_class.new(account: account, name: 'tag', visibility: 4) }.to raise_error(ArgumentError) end end - context 'when the tag is invalid' do - it 'when tag name is invalid' do - expect(described_class.new(account: account, tag: Tag.new(name: 'test tag'), visibility: 0)).to_not be_valid + describe 'name' do + it 'nameは大文字小文字混ざりで作成できる' do + # Tagモデルは途中から英字が小文字に正規化されるようになったが、お気に入りタグは大文字小文字混ざりで作成したいという要望があるため + favourite_tag = described_class.new(account: account, name: 'Test', visibility: 0) + + expect(favourite_tag).to be_valid + end + + it 'お気に入りタグを作成しようとしたとき、そのタグの名前が不正ならinvalid' do + expect(described_class.new(account: account, name: 'test tag', visibility: 0)).to_not be_valid + end + + it '同じ名前でも公開範囲が異なるならばvalid' do + Fabricate(:favourite_tag, account: account, name: 'test', visibility: :public) + + duplicated = described_class.new(account: account, visibility: :unlisted, order: 100, name: 'test') + expect(duplicated).to be_valid + end + + it '同じ名前、同じ公開範囲で既にお気に入りタグを作成しているならばinvalid' do + Fabricate(:favourite_tag, account: account, name: 'test') + + duplicated = described_class.new(account: account, visibility: :public, order: 100, name: 'test') + expect(duplicated).to_not be_valid end end end @@ -42,19 +62,24 @@ it 'returns an array of recent favourite tags ordered by order and id' do specifieds = [ - Fabricate(:favourite_tag, account: account, order: 9900), - Fabricate(:favourite_tag, account: account, order: 9800), - Fabricate(:favourite_tag, account: account, order: 9800), + Fabricate(:favourite_tag, account: account, name: 'test1', order: 10), + Fabricate(:favourite_tag, account: account, name: 'test2', order: 11), + Fabricate(:favourite_tag, account: account, name: 'test3', order: 10), ] - expect(account.favourite_tags.with_order.limit(3)).to match_array(specifieds) + expect(account.favourite_tags.with_order.limit(3)).to eq( + [ + specifieds[1], + specifieds[0], + specifieds[2], + ] + ) end end end describe 'expect to_json_for_api' do let(:account) { Fabricate :account } - let(:tag) { Tag.new(name: 'test_tag') } - let!(:favourite_tag) { Fabricate(:favourite_tag, account: account, tag: tag) } + let!(:favourite_tag) { Fabricate(:favourite_tag, account: account, name: 'test_tag') } it 'expect to_json_for_api' do json = favourite_tag.to_json_for_api @@ -63,4 +88,32 @@ expect(json[:visibility]).to eq 'public' end end + + describe 'migrate_tag_name!' do + it 'モデルのnameカラムに値が入っていない場合は関連付けのtagからnameをコピーする' do + account = Fabricate(:account) + tag = Tag.create!(name: 'tag') + favourite_tag = described_class.new(account: account, tag: tag, visibility: :public, name: nil) + favourite_tag.save!(validate: false) + + expect(favourite_tag.name).to be_nil + + favourite_tag.migrate_tag_name! + + expect(favourite_tag.name).to eq('tag') + end + + it 'モデルのnameカラムに値が入っている場合は変更しない' do + account = Fabricate(:account) + tag = Tag.create!(name: 'hoge_tag') + favourite_tag = described_class.new(account: account, tag: tag, visibility: :public, name: 'tag') + favourite_tag.save! + + expect(favourite_tag.name).to eq('tag') + + favourite_tag.migrate_tag_name! + + expect(favourite_tag.name).to eq('tag') + end + end end