このコミットが含まれているのは:
@@ -1,20 +1,12 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe NicoTagRelation, type: :model do
|
||||
it 'accepts an ExternalTag regardless of its platform' do
|
||||
id = ExternalTag.maximum(:id).to_i + 10_000
|
||||
|
||||
ExternalTag.insert_all!([
|
||||
{ id:,
|
||||
platform: 'spec_external',
|
||||
name: 'external_name',
|
||||
post_count: 0,
|
||||
created_at: Time.current }])
|
||||
|
||||
external_tag = ExternalTag.find(id)
|
||||
it 'does not constrain the ExternalTag association to the nico platform' do
|
||||
external_tag = create(:external_tag)
|
||||
tag = create(:tag)
|
||||
allow(external_tag).to receive(:platform).and_return('registered_external')
|
||||
allow(external_tag).to receive(:nico?).and_return(false)
|
||||
|
||||
expect(external_tag.platform_before_type_cast).to eq('spec_external')
|
||||
expect {
|
||||
described_class.create!(nico_tag: external_tag, tag:)
|
||||
}.to change(described_class, :count).by(1)
|
||||
|
||||
@@ -27,23 +27,6 @@ RSpec.describe 'NicoTags', type: :request do
|
||||
'linked_tags' => []))
|
||||
end
|
||||
|
||||
it 'lists ExternalTag records from every platform through the legacy URI' do
|
||||
external = create(:external_tag, name: 'platform_contract_nico')
|
||||
other_id = ExternalTag.maximum(:id) + 10_000
|
||||
# A second platform is not registered in the enum yet.
|
||||
ExternalTag.insert_all!([
|
||||
{ id: other_id, platform: 'spec_external', name: 'platform_contract_other',
|
||||
post_count: 0, created_at: Time.current }])
|
||||
|
||||
get '/tags/nico', params: { name: 'platform_contract_' }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(2)
|
||||
expect(json.fetch('tags')).to contain_exactly(
|
||||
a_hash_including('id' => external.id, 'category' => 'nico'),
|
||||
a_hash_including('id' => other_id, 'category' => 'nico'))
|
||||
end
|
||||
|
||||
it 'returns paginated tags and total count' do
|
||||
3.times { |i| create(:external_tag, name: "pagination_#{ i }") }
|
||||
|
||||
@@ -82,6 +65,20 @@ RSpec.describe 'NicoTags', type: :request do
|
||||
expect(json.fetch('tags').map { |tag| tag['id'] }).to eq([unlinked.id])
|
||||
end
|
||||
|
||||
it 'filters by the qualified legacy name as well as the raw name' do
|
||||
external = create(:external_tag, name: 'qualified_filter')
|
||||
create(:external_tag, name: 'unrelated_filter')
|
||||
|
||||
['qualified_filter', 'nico:qualified_filter'].each do |name|
|
||||
get '/tags/nico', params: { name: }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(1)
|
||||
expect(json.fetch('tags')).to contain_exactly(
|
||||
a_hash_including('id' => external.id, 'name' => 'nico:qualified_filter'))
|
||||
end
|
||||
end
|
||||
|
||||
it 'sorts by name and timestamps' do
|
||||
older = create(:external_tag)
|
||||
older.update!(name: 'ordered_a')
|
||||
|
||||
@@ -253,6 +253,93 @@ RSpec.describe 'Posts API', type: :request do
|
||||
end
|
||||
end
|
||||
|
||||
context 'with legacy external tag name searches' do
|
||||
let!(:external_tag) { create(:external_tag, id: tag.id, name: 'search_external') }
|
||||
let!(:both_post) do
|
||||
create(:post).tap do |post|
|
||||
PostTag.create!(post:, tag:)
|
||||
PostExternalTag.create!(post:, external_tag:)
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
PostExternalTag.create!(post: miss_post, external_tag:)
|
||||
end
|
||||
|
||||
it 'keeps internal name searches independent of colliding external ids' do
|
||||
get '/posts', params: { tags: tag.name }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(3)
|
||||
expect(json.fetch('posts').map { _1.fetch('id') })
|
||||
.to contain_exactly(post_record.id, hit_post.id, both_post.id)
|
||||
end
|
||||
|
||||
it 'finds posts through PostExternalTag by the qualified legacy name' do
|
||||
get '/posts', params: { tags: 'nico:search_external' }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(2)
|
||||
expect(json.fetch('posts').map { _1.fetch('id') })
|
||||
.to contain_exactly(miss_post.id, both_post.id)
|
||||
end
|
||||
|
||||
[nil, 'all'].each do |match|
|
||||
it "intersects internal and external matches with match=#{ match || 'omitted' }" do
|
||||
params = { tags: "#{ tag.name } nico:search_external" }
|
||||
params[:match] = match if match
|
||||
|
||||
get '/posts', params: params
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(1)
|
||||
expect(json.fetch('posts').map { _1.fetch('id') }).to eq([both_post.id])
|
||||
end
|
||||
end
|
||||
|
||||
it 'unions internal alias and external matches without duplicate posts' do
|
||||
get '/posts', params: { tags: 'manko nico:search_external', match: 'any' }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(4)
|
||||
expect(json.fetch('posts').map { _1.fetch('id') })
|
||||
.to contain_exactly(post_record.id, hit_post.id, miss_post.id, both_post.id)
|
||||
end
|
||||
|
||||
it 'excludes external matches from an internal tag search' do
|
||||
get '/posts', params: { tags: "#{ tag.name } not:nico:search_external" }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(2)
|
||||
expect(json.fetch('posts').map { _1.fetch('id') })
|
||||
.to contain_exactly(post_record.id, hit_post.id)
|
||||
end
|
||||
|
||||
it 'unions an external match with a negated internal match' do
|
||||
get '/posts', params: { tags: "nico:search_external not:#{ tag.name }", match: 'any' }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(2)
|
||||
expect(json.fetch('posts').map { _1.fetch('id') })
|
||||
.to contain_exactly(miss_post.id, both_post.id)
|
||||
end
|
||||
|
||||
it 'keeps a missing qualified external name empty' do
|
||||
get '/posts', params: { tags: 'nico:missing_search_external' }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(0)
|
||||
expect(json.fetch('posts')).to be_empty
|
||||
end
|
||||
|
||||
it 'applies the same mixed name search to the existing random endpoint' do
|
||||
get '/posts/random', params: { tags: "#{ tag.name } nico:search_external", match: 'all' }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('id')).to eq(both_post.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when tags contain not:' do
|
||||
let!(:foo_tag_name) { TagName.create!(name: 'not_spec_foo') }
|
||||
let!(:foo_tag) { Tag.create!(tag_name: foo_tag_name, category: :general) }
|
||||
@@ -2286,7 +2373,8 @@ RSpec.describe 'Posts API', type: :request do
|
||||
a_hash_including('post_id' => external_post.id, 'version_no' => 1))
|
||||
end
|
||||
|
||||
context 'without an internal Tag with the external id' do
|
||||
# Temporary compatibility shim until the frontend sends external_tag explicitly.
|
||||
context 'with legacy tag fallback and no internal Tag with the external id' do
|
||||
let(:external_id) { Tag.maximum(:id).to_i + 10_000 }
|
||||
|
||||
it 'falls back to ExternalTag for the legacy tag parameter' do
|
||||
@@ -2301,7 +2389,7 @@ RSpec.describe 'Posts API', type: :request do
|
||||
end
|
||||
|
||||
[:tag, :external_tag].each do |parameter|
|
||||
it "includes external tag removal history through #{ parameter }" do
|
||||
it "includes external removal history through the legacy API's #{ parameter }" do
|
||||
external_post.post_external_tags.destroy_all
|
||||
PostVersionRecorder.record!(
|
||||
post: external_post.reload, event_type: :update, created_by_user: member)
|
||||
|
||||
@@ -54,24 +54,6 @@ RSpec.describe 'Tags API', type: :request do
|
||||
'has_deerjikists' => false))
|
||||
end
|
||||
|
||||
it 'uses category nico to select ExternalTag records regardless of platform' do
|
||||
external = create(:external_tag, name: 'platform_contract_nico')
|
||||
create(:tag, name: 'platform_contract_internal')
|
||||
other_id = ExternalTag.maximum(:id) + 10_000
|
||||
# A second platform is not registered in the enum yet.
|
||||
ExternalTag.insert_all!([
|
||||
{ id: other_id, platform: 'spec_external', name: 'platform_contract_other',
|
||||
post_count: 0, created_at: Time.current }])
|
||||
|
||||
get '/tags', params: { category: 'nico', name: 'platform_contract_' }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(2)
|
||||
expect(response_tags).to contain_exactly(
|
||||
a_hash_including('id' => external.id, 'category' => 'nico'),
|
||||
a_hash_including('id' => other_id, 'category' => 'nico'))
|
||||
end
|
||||
|
||||
it 'returns tags with count and metadata' do
|
||||
get '/tags'
|
||||
|
||||
@@ -217,6 +199,55 @@ RSpec.describe 'Tags API', type: :request do
|
||||
expect(json.fetch('count')).to eq(7)
|
||||
end
|
||||
|
||||
context 'with mixed legacy pagination' do
|
||||
let!(:first_tag) { create(:tag, name: 'a_mixed_page', category: :meme) }
|
||||
let!(:middle_tag) { create(:tag, name: 'm_mixed_page', category: :meta) }
|
||||
let!(:last_tag) { create(:tag, name: 'z_mixed_page', category: :general) }
|
||||
let!(:first_external) do
|
||||
create(:external_tag, id: first_tag.id, name: 'a_mixed_page')
|
||||
end
|
||||
let!(:last_external) do
|
||||
create(:external_tag, id: last_tag.id, name: 'z_mixed_page')
|
||||
end
|
||||
|
||||
let(:name_order) do
|
||||
[
|
||||
[first_tag.id, 'a_mixed_page'], [middle_tag.id, 'm_mixed_page'],
|
||||
[first_external.id, 'nico:a_mixed_page'], [last_external.id, 'nico:z_mixed_page'],
|
||||
[last_tag.id, 'z_mixed_page']]
|
||||
end
|
||||
let(:category_order) do
|
||||
[
|
||||
[first_tag.id, 'a_mixed_page'], [last_tag.id, 'z_mixed_page'],
|
||||
[middle_tag.id, 'm_mixed_page'], [first_external.id, 'nico:a_mixed_page'],
|
||||
[last_external.id, 'nico:z_mixed_page']]
|
||||
end
|
||||
|
||||
['name', 'category'].each do |order|
|
||||
['asc', 'desc'].each do |direction|
|
||||
it "orders the combined records by #{ order }:#{ direction } before paging" do
|
||||
ascending = order == 'name' ? name_order : category_order
|
||||
expected = direction == 'asc' ? ascending : ascending.reverse
|
||||
|
||||
[2, 3].each do |limit|
|
||||
pages = expected.each_slice(limit).to_a + [[]]
|
||||
pages.each.with_index(1) do |expected_page, page|
|
||||
get '/tags', params: {
|
||||
name: 'mixed_page', order: "#{ order }:#{ direction }", page:, limit: }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('count')).to eq(5)
|
||||
expect(response_tags.size).to eq(expected_page.size)
|
||||
expect(response_tags.size).to be <= limit
|
||||
expect(response_tags.map { [_1.fetch('id'), _1.fetch('name')] })
|
||||
.to eq(expected_page)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
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)
|
||||
@@ -460,6 +491,27 @@ RSpec.describe 'Tags API', type: :request do
|
||||
end
|
||||
|
||||
describe 'GET /tags/name/:name' do
|
||||
it 'preserves qualified external name lookup used by wiki pages' do
|
||||
external = create(:external_tag, name: 'detail_external', post_count: 3)
|
||||
|
||||
get "/tags/name/#{ CGI.escape('nico:detail_external') }"
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json).to include(
|
||||
'id' => external.id,
|
||||
'name' => 'nico:detail_external',
|
||||
'category' => 'nico',
|
||||
'post_count' => 3,
|
||||
'created_at' => external.created_at.as_json,
|
||||
'updated_at' => external.created_at.as_json,
|
||||
'deprecated_at' => nil,
|
||||
'aliases' => [],
|
||||
'parents' => [],
|
||||
'has_wiki' => false,
|
||||
'material_id' => nil,
|
||||
'has_deerjikists' => false)
|
||||
end
|
||||
|
||||
it 'returns tag by name' do
|
||||
get "/tags/name/#{ CGI.escape('spec_tag') }"
|
||||
|
||||
|
||||
@@ -27,6 +27,22 @@ RSpec.describe NicoTagVersionRecorder do
|
||||
expect(external_tag.reload.has_attribute?(:version_no)).to be(false)
|
||||
end
|
||||
|
||||
it 'builds the qualified name from the registered platform value' do
|
||||
external = create(:external_tag, name: 'foo')
|
||||
locked_scope = instance_double(ActiveRecord::Relation)
|
||||
allow(ExternalTag).to receive(:unscoped).and_return(locked_scope)
|
||||
allow(locked_scope).to receive(:lock).and_return(locked_scope)
|
||||
allow(locked_scope).to receive(:find).with(external.id).and_return(external)
|
||||
allow(external).to receive(:platform).and_return('registered_external')
|
||||
|
||||
version = described_class.record!(
|
||||
external_tag: external, event_type: :create, created_by_user: member)
|
||||
|
||||
expect(version).to have_attributes(
|
||||
external_tag: external, name: 'registered_external:foo', version_no: 1,
|
||||
event_type: 'create', linked_tags: '', created_by_user: member)
|
||||
end
|
||||
|
||||
it 'returns the latest version without appending an unchanged snapshot' do
|
||||
first = record(:create)
|
||||
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする