From bce04488ed12175a21f79bc774d9f2230e2fb711 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Thu, 23 Apr 2026 00:05:27 +0900 Subject: [PATCH] #318 --- backend/app/controllers/tags_controller.rb | 6 +- backend/spec/requests/tags_spec.rb | 331 ++++++++++++++++++++- 2 files changed, 323 insertions(+), 14 deletions(-) diff --git a/backend/app/controllers/tags_controller.rb b/backend/app/controllers/tags_controller.rb index b2adf1d..3785a9e 100644 --- a/backend/app/controllers/tags_controller.rb +++ b/backend/app/controllers/tags_controller.rb @@ -325,10 +325,12 @@ class TagsController < ApplicationController with_no_deerjikist: false, deny_nico: true) - TagVersioning.record_tag_snapshots!((tag.parents.to_a + parent_tags).uniq, + old_parent_tags = tag.parents.to_a + + TagVersioning.record_tag_snapshots!((old_parent_tags + parent_tags).uniq, created_by_user: current_user) - tag.tag_implications.destroy_all + tag.reversed_tag_implications.destroy_all parent_tags.each do |parent_tag| next if parent_tag == tag diff --git a/backend/spec/requests/tags_spec.rb b/backend/spec/requests/tags_spec.rb index 42d3728..d3bc594 100644 --- a/backend/spec/requests/tags_spec.rb +++ b/backend/spec/requests/tags_spec.rb @@ -1,7 +1,6 @@ require 'cgi' require 'rails_helper' - RSpec.describe 'Tags API', type: :request do let!(:tn) { TagName.create!(name: 'spec_tag') } let!(:tag) { Tag.create!(tag_name: tn, category: :general) } @@ -197,6 +196,30 @@ RSpec.describe 'Tags API', type: :request do expect(response_tags.size).to eq(1) expect(response_names).to eq(['norm_a']) end + + it 'returns aliases and parent tags' do + parent_tag = Tag.create!( + tag_name: TagName.create!(name: 'index_parent_tag'), + category: :meme + ) + TagImplication.create!(tag:, parent_tag:) + + get '/tags', params: { name: 'spec_tag' } + + expect(response).to have_http_status(:ok) + + row = response_tags.find { |t| t['name'] == 'spec_tag' } + + expect(row['aliases']).to include('unko') + expect(row['parents'].map { |t| t['name'] }).to include('index_parent_tag') + + parent = row['parents'].find { |t| t['name'] == 'index_parent_tag' } + expect(parent).to include( + 'id' => parent_tag.id, + 'name' => 'index_parent_tag', + 'category' => 'meme' + ) + end end describe 'GET /tags/:id' do @@ -220,6 +243,28 @@ RSpec.describe 'Tags API', type: :request do expect(json).to have_key('created_at') expect(json).to have_key('updated_at') end + + it 'returns aliases and parent tags' do + parent_tag = Tag.create!( + tag_name: TagName.create!(name: 'show_parent_tag'), + category: :character + ) + TagImplication.create!(tag:, parent_tag:) + + request + + expect(response).to have_http_status(:ok) + + expect(json['aliases']).to include('unko') + expect(json['parents'].map { |t| t['name'] }).to include('show_parent_tag') + + parent = json['parents'].find { |t| t['name'] == 'show_parent_tag' } + expect(parent).to include( + 'id' => parent_tag.id, + 'name' => 'show_parent_tag', + 'category' => 'character' + ) + end end context 'when tag does not exist' do @@ -359,9 +404,9 @@ RSpec.describe 'Tags API', type: :request do expect(tag.category).to eq('meta') end - it '存在しない id だと RecordNotFound になる(通常は 404)' do + it '存在しない id なら 404 を返す' do patch '/tags/999999999', params: { name: 'x' } - expect(response.status).to be_in([404, 500]) + expect(response).to have_http_status(:not_found) end it 'nico category への変更は 422 を返す' do @@ -401,21 +446,18 @@ RSpec.describe 'Tags API', type: :request do expect(tag.reload.category).to eq('general') end - it 'creates nico tag versions when updating nico tag name' do + it 'returns 422 when updating nico tag name' do nico_tag_name = TagName.create!(name: 'nico:tags_spec_source') nico_tag = Tag.create!(tag_name: nico_tag_name, category: :nico) expect { - patch "/tags/#{nico_tag.id}", params: { name: 'nico:tags_spec_renamed' } - }.to change(NicoTagVersion, :count).by(2) + patch "/tags/#{ nico_tag.id }", params: { name: 'nico:tags_spec_renamed' } + }.not_to change(NicoTagVersion, :count) - expect(response).to have_http_status(:ok) + expect(response).to have_http_status(:unprocessable_entity) - versions = nico_tag.reload.nico_tag_versions.order(:version_no) - expect(versions.map(&:event_type)).to eq(['create', 'update']) - expect(versions.first.name).to eq('nico:tags_spec_source') - expect(versions.second.name).to eq('nico:tags_spec_renamed') - expect(versions.second.created_by_user_id).to eq(member_user.id) + expect(nico_tag.reload.name).to eq('nico:tags_spec_source') + expect(nico_tag.category).to eq('nico') end it 'returns 422 when changing nico tag category to normal category' do @@ -571,4 +613,269 @@ RSpec.describe 'Tags API', type: :request do expect(response).to have_http_status(:not_found) end end + + describe 'PUT /tags/:id' do + context '未ログイン' do + before { stub_current_user(nil) } + + it '401 を返す' do + put "/tags/#{ tag.id }", params: { + name: 'new', + category: 'general', + aliases: '', + parent_tags: '', + } + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'ログインしてゐるが member でない' do + before { stub_current_user(non_member_user) } + + it '403 を返す' do + put "/tags/#{ tag.id }", params: { + name: 'new', + category: 'general', + aliases: '', + parent_tags: '', + } + + expect(response).to have_http_status(:forbidden) + end + end + + context 'member' do + before { stub_current_user(member_user) } + + it '存在しない id なら 404 を返す' do + put '/tags/999999999', params: { + name: 'new', + category: 'general', + aliases: '', + parent_tags: '', + } + + expect(response).to have_http_status(:not_found) + end + + it 'name が空なら 422 を返す' do + put "/tags/#{ tag.id }", params: { + name: '', + category: 'general', + aliases: '', + parent_tags: '', + } + + expect(response).to have_http_status(:unprocessable_entity) + expect(tag.reload.name).to eq('spec_tag') + end + + it 'category が空なら 422 を返す' do + put "/tags/#{ tag.id }", params: { + name: 'new', + category: '', + aliases: '', + parent_tags: '', + } + + expect(response).to have_http_status(:unprocessable_entity) + expect(tag.reload.name).to eq('spec_tag') + expect(tag.category).to eq('general') + end + + it 'name, category, aliases, parent tags をまとめて更新できる' do + old_parent = Tag.create!( + tag_name: TagName.create!(name: 'put_old_parent'), + category: :general + ) + kept_parent = Tag.create!( + tag_name: TagName.create!(name: 'put_kept_parent'), + category: :general + ) + TagImplication.create!(tag:, parent_tag: old_parent) + TagImplication.create!(tag:, parent_tag: kept_parent) + + put "/tags/#{ tag.id }", params: { + name: 'put_renamed_tag', + category: 'meme', + aliases: 'put_alias_a put_alias_b put_alias_a', + parent_tags: 'put_kept_parent put_new_parent', + } + + expect(response).to have_http_status(:ok) + + tag.reload + + expect(tag.name).to eq('put_renamed_tag') + expect(tag.category).to eq('meme') + + expect(TagName.find_by(name: 'put_alias_a').canonical).to eq(tag.tag_name) + expect(TagName.find_by(name: 'put_alias_b').canonical).to eq(tag.tag_name) + + old_name_alias = TagName.find_by(name: 'spec_tag') + expect(old_name_alias).to be_present + expect(old_name_alias.canonical).to eq(tag.tag_name) + + expect(alias_tn.reload.canonical).to be_nil + + expect(tag.parents.map(&:name)).to contain_exactly( + 'put_kept_parent', + 'put_new_parent' + ) + + expect(TagImplication.where(tag:, parent_tag: old_parent)).not_to exist + + expect(json['name']).to eq('put_renamed_tag') + expect(json['category']).to eq('meme') + expect(json['aliases']).to contain_exactly( + 'put_alias_a', + 'put_alias_b', + 'spec_tag' + ) + expect(json['parents'].map { |t| t['name'] }).to contain_exactly( + 'put_kept_parent', + 'put_new_parent' + ) + end + + it 'aliases に現在名を指定しても alias には残さない' do + put "/tags/#{ tag.id }", params: { + name: 'spec_tag', + category: 'general', + aliases: 'spec_tag put_alias_self_test', + parent_tags: '', + } + + expect(response).to have_http_status(:ok) + + tag.reload + + expect(TagName.find_by(name: 'put_alias_self_test').canonical).to eq(tag.tag_name) + expect(json['aliases']).to include('put_alias_self_test') + expect(json['aliases']).not_to include('spec_tag') + end + + it 'parent_tags に自分自身を指定しても自己参照は作らない' do + put "/tags/#{ tag.id }", params: { + name: 'spec_tag', + category: 'general', + aliases: 'unko', + parent_tags: 'spec_tag', + } + + expect(response).to have_http_status(:ok) + + expect(TagImplication.where(tag:, parent_tag: tag)).not_to exist + expect(tag.reload.parents).to eq([]) + end + + it 'initial and update tag versions を作成する' do + expect { + put "/tags/#{ tag.id }", params: { + name: 'put_versioned_tag', + category: 'meta', + aliases: '', + parent_tags: '', + } + }.to change(TagVersion, :count).by(2) + + expect(response).to have_http_status(:ok) + + versions = tag.reload.tag_versions.order(:version_no) + + expect(versions.map(&:event_type)).to eq(['create', 'update']) + + expect(versions.first.name).to eq('spec_tag') + expect(versions.first.category).to eq('general') + expect(versions.first.aliases.split).to include('unko') + + expect(versions.second.name).to eq('put_versioned_tag') + expect(versions.second.category).to eq('meta') + expect(versions.second.aliases.split).to include('spec_tag') + expect(versions.second.created_by_user_id).to eq(member_user.id) + end + + it 'parent tag の snapshot も作成する' do + old_parent = Tag.create!( + tag_name: TagName.create!(name: 'put_snapshot_old_parent'), + category: :general + ) + new_parent = Tag.create!( + tag_name: TagName.create!(name: 'put_snapshot_new_parent'), + category: :general + ) + TagImplication.create!(tag:, parent_tag: old_parent) + + put "/tags/#{ tag.id }", params: { + name: 'spec_tag', + category: 'general', + aliases: 'unko', + parent_tags: new_parent.name, + } + + expect(response).to have_http_status(:ok) + + expect(old_parent.reload.tag_versions.map(&:event_type)).to include('create') + expect(new_parent.reload.tag_versions.map(&:event_type)).to include('create') + end + + it 'normal tag を nico category には変更できない' do + expect { + put "/tags/#{ tag.id }", params: { + name: 'spec_tag', + category: 'nico', + aliases: '', + parent_tags: '', + } + }.not_to change(TagVersion, :count) + + expect(response).to have_http_status(:unprocessable_entity) + + expect(tag.reload.name).to eq('spec_tag') + expect(tag.category).to eq('general') + end + + it 'nico tag は更新できない' do + nico_tag = Tag.create!( + tag_name: TagName.create!(name: 'nico:put_update_all_ng'), + category: :nico + ) + + expect { + put "/tags/#{ nico_tag.id }", params: { + name: 'nico:put_update_all_renamed', + category: 'nico', + aliases: '', + parent_tags: '', + } + }.not_to change(NicoTagVersion, :count) + + expect(response).to have_http_status(:unprocessable_entity) + + expect(nico_tag.reload.name).to eq('nico:put_update_all_ng') + expect(nico_tag.category).to eq('nico') + end + + it 'system tag の name は変更できない' do + system_tag = Tag.tagme + old_name = system_tag.name + old_category = system_tag.category + + expect { + put "/tags/#{ system_tag.id }", params: { + name: 'put_system_tag_renamed', + category: old_category, + aliases: '', + parent_tags: '', + } + }.not_to change(TagVersion, :count) + + expect(response).to have_http_status(:unprocessable_entity) + + expect(system_tag.reload.name).to eq(old_name) + expect(system_tag.category).to eq(old_category) + end + end + end end