Merge remote-tracking branch 'origin/main' into feature/297
This commit is contained in:
@@ -667,6 +667,93 @@ RSpec.describe 'Posts API', type: :request do
|
||||
expect(types).to include('add')
|
||||
expect(types).to include('remove')
|
||||
end
|
||||
|
||||
it 'filters history by tag' do
|
||||
tn2 = TagName.create!(name: 'history_tag_hit')
|
||||
tag2 = Tag.create!(tag_name: tn2, category: :general)
|
||||
|
||||
tn3 = TagName.create!(name: 'history_tag_miss')
|
||||
tag3 = Tag.create!(tag_name: tn3, category: :general)
|
||||
|
||||
other_post = Post.create!(
|
||||
title: 'other post',
|
||||
url: 'https://example.com/history-other'
|
||||
)
|
||||
|
||||
# hit: add
|
||||
PostTag.create!(post: post_record, tag: tag2, created_user: member)
|
||||
|
||||
# hit: add + remove
|
||||
pt2 = PostTag.create!(post: other_post, tag: tag2, created_user: member)
|
||||
pt2.discard_by!(member)
|
||||
|
||||
# miss: add + remove
|
||||
pt3 = PostTag.create!(post: post_record, tag: tag3, created_user: member)
|
||||
pt3.discard_by!(member)
|
||||
|
||||
get '/posts/changes', params: { tag: tag2.id }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json).to include('changes', 'count')
|
||||
expect(json['count']).to eq(3)
|
||||
|
||||
changes = json.fetch('changes')
|
||||
|
||||
expect(changes.map { |e| e.dig('tag', 'id') }.uniq).to eq([tag2.id])
|
||||
expect(changes.map { |e| e['change_type'] }).to match_array(%w[add add remove])
|
||||
expect(changes.map { |e| e.dig('post', 'id') }).to match_array([
|
||||
post_record.id,
|
||||
other_post.id,
|
||||
other_post.id
|
||||
])
|
||||
end
|
||||
|
||||
it 'filters history by post and tag together' do
|
||||
tn2 = TagName.create!(name: 'history_tag_combo_hit')
|
||||
tag2 = Tag.create!(tag_name: tn2, category: :general)
|
||||
|
||||
tn3 = TagName.create!(name: 'history_tag_combo_miss')
|
||||
tag3 = Tag.create!(tag_name: tn3, category: :general)
|
||||
|
||||
other_post = Post.create!(
|
||||
title: 'other combo post',
|
||||
url: 'https://example.com/history-combo-other'
|
||||
)
|
||||
|
||||
# hit
|
||||
PostTag.create!(post: post_record, tag: tag2, created_user: member)
|
||||
|
||||
# miss by post
|
||||
pt2 = PostTag.create!(post: other_post, tag: tag2, created_user: member)
|
||||
pt2.discard_by!(member)
|
||||
|
||||
# miss by tag
|
||||
pt3 = PostTag.create!(post: post_record, tag: tag3, created_user: member)
|
||||
pt3.discard_by!(member)
|
||||
|
||||
get '/posts/changes', params: { id: post_record.id, tag: tag2.id }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json).to include('changes', 'count')
|
||||
expect(json['count']).to eq(1)
|
||||
|
||||
changes = json.fetch('changes')
|
||||
expect(changes.size).to eq(1)
|
||||
expect(changes[0]['change_type']).to eq('add')
|
||||
expect(changes[0].dig('post', 'id')).to eq(post_record.id)
|
||||
expect(changes[0].dig('tag', 'id')).to eq(tag2.id)
|
||||
end
|
||||
|
||||
it 'returns empty history when tag does not match' do
|
||||
tn2 = TagName.create!(name: 'history_tag_no_hit')
|
||||
tag2 = Tag.create!(tag_name: tn2, category: :general)
|
||||
|
||||
get '/posts/changes', params: { tag: tag2.id }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('changes')).to eq([])
|
||||
expect(json.fetch('count')).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /posts/:id/viewed' do
|
||||
|
||||
@@ -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
|
||||
|
||||
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')
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user