| @@ -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 | |||