From 5581d6e1ccddae1398998494e0885a46bc5c093e Mon Sep 17 00:00:00 2001 From: miteruzo Date: Sun, 15 Mar 2026 02:48:13 +0900 Subject: [PATCH] #61 --- backend/spec/requests/tags_spec.rb | 274 +++++++++++++++++++++++------ 1 file changed, 219 insertions(+), 55 deletions(-) diff --git a/backend/spec/requests/tags_spec.rb b/backend/spec/requests/tags_spec.rb index 70309b7..8dfa51a 100644 --- a/backend/spec/requests/tags_spec.rb +++ b/backend/spec/requests/tags_spec.rb @@ -11,16 +11,180 @@ RSpec.describe 'Tags API', type: :request do let!(:tn2) { TagName.create!(name: 'unknown') } let!(:tag2) { Tag.create!(tag_name: tn2, category: :general) } + def response_tags + json.fetch('tags') + end + + def response_names + response_tags.map { |t| t.fetch('name') } + end + describe 'GET /tags' do - it 'returns tags with name' do + it 'returns tags with count and metadata' do get '/tags' expect(response).to have_http_status(:ok) + expect(json).to include('tags', 'count') + expect(response_tags).to be_an(Array) + expect(json['count']).to be_an(Integer) + expect(json['count']).to be >= response_tags.size + + row = response_tags.find { |t| t['name'] == 'spec_tag' } + expect(row).to include( + 'id' => tag.id, + 'name' => 'spec_tag', + 'category' => 'general', + 'post_count' => 1, + 'has_wiki' => false) + expect(row).to have_key('created_at') + expect(row).to have_key('updated_at') + end - expect(json).to be_an(Array) - expect(json).not_to be_empty - expect(json[0]).to have_key('name') - expect(json.map { |t| t['name'] }).to include('spec_tag') + it 'filters tags by post id' do + get '/tags', params: { post: post.id } + + expect(response).to have_http_status(:ok) + expect(json['count']).to eq(1) + expect(response_names).to eq(['spec_tag']) + end + + it 'filters tags by partial name' do + get '/tags', params: { name: 'spec' } + + expect(response).to have_http_status(:ok) + expect(response_names).to include('spec_tag') + expect(response_names).not_to include('unknown') + end + + it 'filters tags by category' do + meme = Tag.create!(tag_name: TagName.create!(name: 'meme_only'), category: :meme) + + get '/tags', params: { category: 'meme' } + + expect(response).to have_http_status(:ok) + expect(response_names).to eq(['meme_only']) + expect(response_tags.first['id']).to eq(meme.id) + end + + it 'filters tags by post_count range' do + low = Tag.create!(tag_name: TagName.create!(name: 'pc_low'), category: :general) + mid = Tag.create!(tag_name: TagName.create!(name: 'pc_mid'), category: :general) + high = Tag.create!(tag_name: TagName.create!(name: 'pc_high'), category: :general) + + low.update_columns(post_count: 1) + mid.update_columns(post_count: 3) + high.update_columns(post_count: 5) + + get '/tags', params: { + name: 'pc_', + post_count_gte: 2, + post_count_lte: 4, + order: 'post_count:asc', + } + + expect(response).to have_http_status(:ok) + expect(response_names).to eq(['pc_mid']) + end + + it 'filters tags by created_at range' do + old_tag = Tag.create!(tag_name: TagName.create!(name: 'created_old'), category: :general) + new_tag = Tag.create!(tag_name: TagName.create!(name: 'created_new'), category: :general) + + old_time = Time.zone.local(2024, 1, 1, 0, 0, 0) + new_time = Time.zone.local(2024, 2, 1, 0, 0, 0) + + old_tag.update_columns(created_at: old_time, updated_at: old_time) + new_tag.update_columns(created_at: new_time, updated_at: new_time) + + get '/tags', params: { + name: 'created_', + created_from: Time.zone.local(2024, 1, 15, 0, 0, 0).iso8601, + order: 'created_at:asc', + } + + expect(response).to have_http_status(:ok) + expect(response_names).to eq(['created_new']) + end + + it 'filters tags by updated_at range' do + old_tag = Tag.create!(tag_name: TagName.create!(name: 'updated_old'), category: :general) + new_tag = Tag.create!(tag_name: TagName.create!(name: 'updated_new'), category: :general) + + old_time = Time.zone.local(2024, 3, 1, 0, 0, 0) + new_time = Time.zone.local(2024, 4, 1, 0, 0, 0) + + old_tag.update_columns(created_at: old_time, updated_at: old_time) + new_tag.update_columns(created_at: new_time, updated_at: new_time) + + get '/tags', params: { + name: 'updated_', + updated_to: Time.zone.local(2024, 3, 15, 0, 0, 0).iso8601, + order: 'updated_at:asc', + } + + expect(response).to have_http_status(:ok) + expect(response_names).to eq(['updated_old']) + end + + it 'orders tags by custom category order' do + Tag.create!(tag_name: TagName.create!(name: 'cat_deerjikist'), category: :deerjikist) + Tag.create!(tag_name: TagName.create!(name: 'cat_meme'), category: :meme) + Tag.create!(tag_name: TagName.create!(name: 'cat_character'), category: :character) + Tag.create!(tag_name: TagName.create!(name: 'cat_general'), category: :general) + Tag.create!(tag_name: TagName.create!(name: 'cat_material'), category: :material) + Tag.create!(tag_name: TagName.create!(name: 'cat_meta'), category: :meta) + Tag.create!(tag_name: TagName.create!(name: 'nico:cat_nico'), category: :nico) + + get '/tags', params: { name: 'cat_', order: 'category:asc', limit: 20 } + + expect(response).to have_http_status(:ok) + expect(response_names).to eq(%w[ + cat_deerjikist + cat_meme + cat_character + cat_general + cat_material + cat_meta + nico:cat_nico + ]) + end + + it 'paginates and keeps total count' do + %w[pag_a pag_b pag_c].each do |name| + Tag.create!(tag_name: TagName.create!(name:), category: :general) + end + + get '/tags', params: { name: 'pag_', order: 'name:asc', page: 2, limit: 2 } + + expect(response).to have_http_status(:ok) + expect(json['count']).to eq(3) + expect(response_names).to eq(%w[pag_c]) + end + + it 'falls back to default ordering when order is invalid' do + low = Tag.create!(tag_name: TagName.create!(name: 'fallback_low'), category: :general) + high = Tag.create!(tag_name: TagName.create!(name: 'fallback_high'), category: :general) + + low.update_columns(post_count: 1) + high.update_columns(post_count: 9) + + get '/tags', params: { name: 'fallback_', order: 'nope:sideways' } + + expect(response).to have_http_status(:ok) + expect(response_names.first).to eq('fallback_high') + end + + it 'normalises invalid page and limit' do + %w[norm_a norm_b].each do |name| + Tag.create!(tag_name: TagName.create!(name:), category: :general) + end + + get '/tags', params: { name: 'norm_', order: 'name:asc', page: 0, limit: 0 } + + expect(response).to have_http_status(:ok) + expect(json['count']).to eq(2) + expect(response_tags.size).to eq(1) + expect(response_names).to eq(['norm_a']) end end @@ -37,9 +201,13 @@ RSpec.describe 'Tags API', type: :request do expect(response).to have_http_status(:ok) expect(json).to include( - 'id' => tag.id, - 'name' => 'spec_tag', - 'category' => 'general') + 'id' => tag.id, + 'name' => 'spec_tag', + 'category' => 'general', + 'post_count' => 1, + 'has_wiki' => false) + expect(json).to have_key('created_at') + expect(json).to have_key('updated_at') end end @@ -61,7 +229,7 @@ RSpec.describe 'Tags API', type: :request do expect(json).to be_an(Array) expect(json.map { |t| t['name'] }).to include('spec_tag') - t = json.find { |t| t['name'] == 'spec_tag' } + t = json.find { |x| x['name'] == 'spec_tag' } expect(t).to have_key('matched_alias') expect(t['matched_alias']).to be(nil) end @@ -73,9 +241,9 @@ RSpec.describe 'Tags API', type: :request do expect(json).to be_an(Array) expect(json.map { |t| t['name'] }).to include('spec_tag') - t = json.find { |t| t['name'] == 'spec_tag' } + t = json.find { |x| x['name'] == 'spec_tag' } expect(t['matched_alias']).to eq('unko') - expect(json.map { |t| t['name'] }).not_to include('unknown') + expect(json.map { |x| x['name'] }).not_to include('unknown') end end @@ -85,10 +253,14 @@ RSpec.describe 'Tags API', type: :request do expect(response).to have_http_status(:ok) - expect(json).to have_key('id') - expect(json).to have_key('name') - expect(json['id']).to eq(tag.id) - expect(json['name']).to eq('spec_tag') + expect(json).to include( + 'id' => tag.id, + 'name' => 'spec_tag', + 'category' => 'general', + 'post_count' => 1, + 'has_wiki' => false) + expect(json).to have_key('created_at') + expect(json).to have_key('updated_at') end it 'returns 404 when not found' do @@ -97,7 +269,6 @@ RSpec.describe 'Tags API', type: :request do end end - # member? を持つ user を想定(Factory 側で trait 作ってもOK) let(:member_user) { create(:user) } let(:non_member_user) { create(:user) } @@ -110,87 +281,80 @@ RSpec.describe 'Tags API', type: :request do allow(non_member_user).to receive(:gte_member?).and_return(false) end - describe "PATCH /tags/:id" do - context "未ログイン" do + describe 'PATCH /tags/:id' do + context '未ログイン' do before { stub_current_user(nil) } - it "401 を返す" do - patch "/tags/#{tag.id}", params: { name: "new" } + it '401 を返す' do + patch "/tags/#{ tag.id }", params: { name: 'new' } expect(response).to have_http_status(:unauthorized) end end - context "ログインしてゐるが member でない" do + context 'ログインしてゐるが member でない' do before { stub_current_user(non_member_user) } - it "403 を返す" do - patch "/tags/#{tag.id}", params: { name: "new" } + it '403 を返す' do + patch "/tags/#{ tag.id }", params: { name: 'new' } expect(response).to have_http_status(:forbidden) end end - context "member" do + context 'member' do before { stub_current_user(member_user) } - it "name だけ更新できる" do - patch "/tags/#{tag.id}", params: { name: "new" } + it 'name だけ更新できる' do + patch "/tags/#{ tag.id }", params: { name: 'new' } expect(response).to have_http_status(:ok) tag.reload - expect(tag.name).to eq("new") - expect(tag.category).to eq("general") + expect(tag.name).to eq('new') + expect(tag.category).to eq('general') - json = JSON.parse(response.body) - expect(json["id"]).to eq(tag.id) - expect(json["name"]).to eq("new") - expect(json["category"]).to eq("general") + body = JSON.parse(response.body) + expect(body['id']).to eq(tag.id) + expect(body['name']).to eq('new') + expect(body['category']).to eq('general') end - it "category だけ更新できる" do - patch "/tags/#{tag.id}", params: { category: "meme" } + it 'category だけ更新できる' do + patch "/tags/#{ tag.id }", params: { category: 'meme' } expect(response).to have_http_status(:ok) tag.reload - expect(tag.name).to eq("spec_tag") - expect(tag.category).to eq("meme") + expect(tag.name).to eq('spec_tag') + expect(tag.category).to eq('meme') end - it "空文字は presence により無視され、更新は走らない(値が変わらない)" do - patch "/tags/#{tag.id}", params: { name: "", category: " " } + it '空文字は presence により無視され、更新は走らない(値が変わらない)' do + patch "/tags/#{ tag.id }", params: { name: '', category: ' ' } expect(response).to have_http_status(:ok) tag.reload - expect(tag.name).to eq("spec_tag") - expect(tag.category).to eq("general") + expect(tag.name).to eq('spec_tag') + expect(tag.category).to eq('general') end - it "両方更新できる" do - patch "/tags/#{tag.id}", params: { name: "n", category: "meta" } + it '両方更新できる' do + patch "/tags/#{ tag.id }", params: { name: 'n', category: 'meta' } expect(response).to have_http_status(:ok) tag.reload - expect(tag.name).to eq("n") - expect(tag.category).to eq("meta") + expect(tag.name).to eq('n') + expect(tag.category).to eq('meta') end - it "存在しない id だと RecordNotFound になる(通常は 404)" do - # Rails 設定次第で例外がそのまま上がる/404になる - # APIなら rescue_from で 404 にしてることが多いので、その場合は 404 を期待。 - patch "/tags/999999999", params: { name: "x" } - + it '存在しない id だと RecordNotFound になる(通常は 404)' do + patch '/tags/999999999', params: { name: 'x' } expect(response.status).to be_in([404, 500]) end - it "バリデーションで update! が失敗したら(通常は 422 か 500)" do - patch "/tags/#{tag.id}", params: { name: 'new', category: 'nico' } - - # rescue_from の実装次第で変はる: - # - RecordInvalid を 422 にしてるなら 422 - # - 未処理なら 500 + it 'バリデーションで update! が失敗したら(通常は 422 か 500)' do + patch "/tags/#{ tag.id }", params: { name: 'new', category: 'nico' } expect(response.status).to be_in([422, 500]) end end