| @@ -11,16 +11,180 @@ RSpec.describe 'Tags API', type: :request do | |||||
| let!(:tn2) { TagName.create!(name: 'unknown') } | let!(:tn2) { TagName.create!(name: 'unknown') } | ||||
| let!(:tag2) { Tag.create!(tag_name: tn2, category: :general) } | 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 | describe 'GET /tags' do | ||||
| it 'returns tags with name' do | |||||
| it 'returns tags with count and metadata' do | |||||
| get '/tags' | get '/tags' | ||||
| expect(response).to have_http_status(:ok) | 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 | ||||
| end | end | ||||
| @@ -37,9 +201,13 @@ RSpec.describe 'Tags API', type: :request do | |||||
| expect(response).to have_http_status(:ok) | expect(response).to have_http_status(:ok) | ||||
| expect(json).to include( | 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 | ||||
| end | end | ||||
| @@ -61,7 +229,7 @@ RSpec.describe 'Tags API', type: :request do | |||||
| expect(json).to be_an(Array) | expect(json).to be_an(Array) | ||||
| expect(json.map { |t| t['name'] }).to include('spec_tag') | 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).to have_key('matched_alias') | ||||
| expect(t['matched_alias']).to be(nil) | expect(t['matched_alias']).to be(nil) | ||||
| end | end | ||||
| @@ -73,9 +241,9 @@ RSpec.describe 'Tags API', type: :request do | |||||
| expect(json).to be_an(Array) | expect(json).to be_an(Array) | ||||
| expect(json.map { |t| t['name'] }).to include('spec_tag') | 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(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 | ||||
| end | end | ||||
| @@ -85,10 +253,14 @@ RSpec.describe 'Tags API', type: :request do | |||||
| expect(response).to have_http_status(:ok) | 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 | end | ||||
| it 'returns 404 when not found' do | it 'returns 404 when not found' do | ||||
| @@ -97,7 +269,6 @@ RSpec.describe 'Tags API', type: :request do | |||||
| end | end | ||||
| end | end | ||||
| # member? を持つ user を想定(Factory 側で trait 作ってもOK) | |||||
| let(:member_user) { create(:user) } | let(:member_user) { create(:user) } | ||||
| let(:non_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) | allow(non_member_user).to receive(:gte_member?).and_return(false) | ||||
| end | end | ||||
| describe "PATCH /tags/:id" do | |||||
| context "未ログイン" do | |||||
| describe 'PATCH /tags/:id' do | |||||
| context '未ログイン' do | |||||
| before { stub_current_user(nil) } | 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) | expect(response).to have_http_status(:unauthorized) | ||||
| end | end | ||||
| end | end | ||||
| context "ログインしてゐるが member でない" do | |||||
| context 'ログインしてゐるが member でない' do | |||||
| before { stub_current_user(non_member_user) } | 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) | expect(response).to have_http_status(:forbidden) | ||||
| end | end | ||||
| end | end | ||||
| context "member" do | |||||
| context 'member' do | |||||
| before { stub_current_user(member_user) } | 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) | expect(response).to have_http_status(:ok) | ||||
| tag.reload | 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 | 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) | expect(response).to have_http_status(:ok) | ||||
| tag.reload | 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 | 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) | expect(response).to have_http_status(:ok) | ||||
| tag.reload | 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 | 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) | expect(response).to have_http_status(:ok) | ||||
| tag.reload | 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 | 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]) | expect(response.status).to be_in([404, 500]) | ||||
| end | 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]) | expect(response.status).to be_in([422, 500]) | ||||
| end | end | ||||
| end | end | ||||