多言語化の基盤作り (#55) (#421)

Reviewed-on: #421
Co-authored-by: miteruzo <miteruzo@naver.com>
このコミットはプルリクエスト #421 でマージされました。
このコミットが含まれているのは:
2026-10-03 02:00:09 +09:00
committed by みてるぞ
コミット e6ab941774
91個のファイルの変更、3370行の追加、1223行の削除
+1
ファイルの表示
@@ -11,6 +11,7 @@ RSpec.describe 'discarded tag cleanup migrations' do
end
context 'with legacy records' do
# Reproduce the historical schema required by these pre-multilingual migrations.
self.use_transactional_tests = false
before do
+36
ファイルの表示
@@ -0,0 +1,36 @@
require 'rails_helper'
RSpec.describe 'TagName database invariants' do
def insert_name!(**attributes)
TagName.insert_all!([{ name: 'constraint_name', language_code: 'ja',
script_code: 'Jpan', primary_flg: true,
created_at: Time.current, updated_at: Time.current,
**attributes }])
end
it 'defaults persisted names to human-confirmed provenance' do
insert_name!
expect(TagName.find_by!(name: 'constraint_name').auto_generated).to be(false)
end
[[true, true], [true, false], [false, false]].each do |primary_flg, auto_generated|
it "allows primary=#{ primary_flg }, auto_generated=#{ auto_generated }" do
expect { insert_name!(primary_flg:, auto_generated:) }
.to change(TagName, :count).by(1)
end
end
it 'rejects auto-generated aliases at the database boundary' do
expect { insert_name!(primary_flg: false, auto_generated: true) }
.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
end
it 'rejects a second primary for the same tag and language' do
tag = create(:tag)
expect { insert_name!(tag_id: tag.id, auto_generated: false) }
.to raise_error(ActiveRecord::RecordNotUnique)
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true).count).to eq(1)
end
end
+91
ファイルの表示
@@ -0,0 +1,91 @@
require 'rails_helper'
RSpec.describe 'TagVersion names database constraint' do
let(:tag) { create(:tag) }
let(:names) do
{ 'ja' => { 'name' => 'snapshot_name', 'script_code' => 'Jpan',
'auto_generated' => false, 'aliases' => [] } }
end
let(:alias_name) { { 'name' => 'snapshot_alias', 'script_code' => 'Jpan' } }
def insert_snapshot!(snapshot)
TagVersion.insert_all!([{ tag_id: tag.id, version_no: 1, event_type: 'create',
name: 'snapshot_name', category: 'general',
aliases: '', parent_tag_ids: '', names: snapshot,
created_at: Time.current }])
end
it 'accepts grouped Nipponese names with an empty alias list' do
expect { insert_snapshot!(names) }.to change(TagVersion, :count).by(1)
expect(TagVersion.find_by!(tag_id: tag.id).names).to eq(names)
end
it 'accepts aliases containing only their name and script' do
names['ja']['aliases'] = [alias_name]
expect { insert_snapshot!(names) }.to change(TagVersion, :count).by(1)
end
it 'rejects a flat array root' do
expect { insert_snapshot!([names.fetch('ja')]) }
.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
end
it 'rejects an empty object root' do
expect { insert_snapshot!({ }) }
.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
end
it 'rejects a language entry that is not an object' do
expect { insert_snapshot!({ 'ja' => 'not an object' }) }
.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
end
['name', 'script_code', 'auto_generated', 'aliases'].each do |field|
it "rejects a primary entry missing #{ field }" do
names['ja'].delete(field)
expect { insert_snapshot!(names) }
.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
end
end
it 'rejects an empty primary name' do
names['ja']['name'] = ''
expect { insert_snapshot!(names) }
.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
end
it 'rejects incorrect script casing' do
names['ja']['script_code'] = 'JPAN'
expect { insert_snapshot!(names) }
.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
end
['unknown', 'primary_flg', 'language_code'].each do |field|
it "rejects the extra primary field #{ field }" do
names['ja'][field] = true
expect { insert_snapshot!(names) }
.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
end
end
['unknown', 'primary_flg', 'auto_generated'].each do |field|
it "rejects the extra alias field #{ field }" do
names['ja']['aliases'] = [alias_name.merge(field => true)]
expect { insert_snapshot!(names) }
.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
end
end
it 'rejects duplicate alias objects' do
names['ja']['aliases'] = [alias_name, alias_name.deep_dup]
expect { insert_snapshot!(names) }
.to raise_error(ActiveRecord::StatementInvalid, /check constraint/i)
end
end
+9 -1
ファイルの表示
@@ -1,5 +1,13 @@
FactoryBot.define do
factory :tag_name do
name { "tag-#{SecureRandom.hex(4)}" }
language_code { 'ja' }
script_code { 'Jpan' }
primary_flg { true }
auto_generated { false }
sequence(:name) { |number| "tag-#{ SecureRandom.hex(4) }-#{ number }" }
trait :alias do
primary_flg { false }
end
end
end
+29 -3
ファイルの表示
@@ -1,15 +1,41 @@
FactoryBot.define do
factory :tag do
transient do
name { nil }
primary_name { nil }
primary_tag_name do
attributes = primary_name.nil? ? { } : { name: primary_name }
build(:tag_name, **attributes)
end
end
category { :general }
post_count { 0 }
association :tag_name
after(:build) do |tag, evaluator|
tag.name = evaluator.name if evaluator.name.present?
evaluator.primary_tag_name.primary_flg = true
tag.tag_names.target << evaluator.primary_tag_name
end
before(:create) do |tag, evaluator|
evaluator.primary_tag_name.save!
# Compatibility write only; ownership is established through tag_id below.
tag.tag_name_id = evaluator.primary_tag_name.id
end
after(:create) do |tag, evaluator|
evaluator.primary_tag_name.update!(tag:, primary_flg: true)
Locale.where(deprecated_at: nil).order(:language_code, :created_at, :script_code).to_a
.uniq(&:language_code).each do |locale|
next if tag.tag_names.exists?(language_code: locale.language_code,
primary_flg: true)
create(:tag_name,
tag:, language_code: locale.language_code,
script_code: locale.script_code,
name: TagName.generate_name(locale, tag, evaluator.primary_tag_name),
primary_flg: true, auto_generated: true)
end
end
end
end
+32
ファイルの表示
@@ -0,0 +1,32 @@
require 'rails_helper'
RSpec.describe Language, type: :model do
it 'accepts Nipponese as the current supported language' do
expect(described_class.find('ja')).to be_valid
expect(described_class.where(deprecated_at: nil).pluck(:code)).to eq(['ja'])
end
it 'accepts English when multilingual support is enabled' do
pending '日本語以外の Language 有効化は別課題で実装する'
expect { described_class.create!(code: 'en', name: 'English') }
.to change(described_class, :count).by(1)
end
it 'rejects an unsupported language code' do
expect {
described_class.create!(code: 'xx', name: 'Unsupported')
}.to raise_error { |error|
expect(error).to be_a(ArgumentError).or be_a(ActiveRecord::RecordInvalid)
}
end
['en', 'fr'].each do |code|
it "does not register #{ code } as a currently supported language" do
expect {
described_class.create!(code:, name: 'Unsupported')
}.to raise_error { |error|
expect(error).to be_a(ArgumentError).or be_a(ActiveRecord::RecordInvalid)
}
end
end
end
+129
ファイルの表示
@@ -0,0 +1,129 @@
require 'rails_helper'
RSpec.describe Locale, type: :model do
include_context 'English locale'
def create_english_locale!
prepare_english_reference!
described_class.create!(code: 'en', language_code: 'en',
script_code: 'Latn', region_code: 826,
variants: '', name: 'English')
end
it 'keeps the active Nipponese locale consistent with its identity tuple' do
expect(described_class.where(deprecated_at: nil).pluck(:code)).to eq(['ja'])
expect(described_class.nipponese).to have_attributes(
code: 'ja', language_code: 'ja', script_code: 'Jpan',
region_code: 392, variants: '')
end
it 'rejects a duplicate locale identity tuple at the database boundary' do
expect {
described_class.insert_all!([{ code: 'ja-duplicate', language_code: 'ja',
script_code: 'Jpan', region_code: 392,
variants: '', name: '日本語',
created_at: Time.current }])
}.to raise_error(ActiveRecord::RecordNotUnique)
end
it 'generates a primary name for every existing tag using the name generator' do
pending '日本語以外の Locale 有効化は別課題で実装する'
first = create(:tag, primary_name: 'first_existing_tag')
second = create(:tag, primary_name: 'second_existing_tag')
allow(TagName).to receive(:generate_name).and_call_original
allow(TagName).to receive(:generate_name)
.with(kind_of(described_class), first, primary_tag_name_for(first, 'ja'))
.and_return('name_from_generator')
locale = create_english_locale!
expect(TagName).to have_received(:generate_name)
.with(locale, first, primary_tag_name_for(first, 'ja'))
expect(TagName).to have_received(:generate_name)
.with(locale, second, primary_tag_name_for(second, 'ja'))
expect(first.tag_names.find_by!(language_code: 'en', primary_flg: true))
.to have_attributes(name: 'name_from_generator', tag_id: first.id,
language_code: 'en', script_code: 'Latn',
primary_flg: true, auto_generated: true)
expect(second.tag_names.find_by!(language_code: 'en', primary_flg: true))
.to have_attributes(tag_id: second.id, language_code: 'en',
script_code: 'Latn', primary_flg: true,
auto_generated: true)
expect(TagName.where(language_code: 'en', primary_flg: true,
tag_id: [first.id, second.id]).count).to eq(2)
end
it 'does not duplicate an existing primary name in the new language' do
pending '日本語以外の Locale 有効化は別課題で実装する'
prepare_english_reference!
tag = create(:tag, primary_name: 'already_named')
existing = create(:tag_name, name: 'existing_english', tag:,
language_code: 'en', script_code: 'Latn')
create_english_locale!
expect(TagName.where(tag_id: tag.id, language_code: 'en', primary_flg: true))
.to contain_exactly(existing)
expect(existing.reload).to have_attributes(
tag_id: tag.id, language_code: 'en', script_code: 'Latn',
primary_flg: true, auto_generated: false)
end
it 'creates a primary name when the language has only an alias' do
pending '日本語以外の Locale 有効化は別課題で実装する'
prepare_english_reference!
tag = create(:tag, primary_name: 'alias_only_tag')
alias_name = create(:tag_name, :alias, name: 'alias_en',
tag:,
language_code: 'en', script_code: 'Latn')
create_english_locale!
expect(alias_name.reload).to have_attributes(
tag_id: tag.id,
language_code: 'en', script_code: 'Latn', primary_flg: false)
generated = TagName.find_by!(tag_id: tag.id, language_code: 'en', primary_flg: true)
expect(generated).to have_attributes(tag_id: tag.id, language_code: 'en',
script_code: 'Latn', primary_flg: true,
auto_generated: true)
expect(TagName.where(tag_id: tag.id, language_code: 'en', primary_flg: true).count).to eq(1)
end
it 'rolls back the locale and all generated names when backfill fails' do
pending '日本語以外の Locale 有効化は別課題で実装する'
prepare_english_reference!
first = create(:tag, primary_name: 'atomic_first')
second = create(:tag, primary_name: 'atomic_second')
allow(TagName).to receive(:generate_name).and_call_original
allow(TagName).to receive(:generate_name)
.with(kind_of(described_class), second, primary_tag_name_for(second, 'ja'))
.and_raise('backfill failed')
expect { create_english_locale! }.to raise_error('backfill failed')
expect(described_class.exists?(code: 'en')).to be(false)
expect(TagName.where(language_code: 'en', tag_id: [first.id, second.id])).to be_empty
end
it 'accepts a stable locale code outside the historical enum values' do
pending '日本語以外の Locale 有効化は別課題で実装する'
prepare_english_reference!
locale = described_class.create!(
code: 'en-x-rspec', language_code: 'en',
script_code: 'Latn', region_code: 826,
variants: 'x-rspec', name: 'English')
expect(locale.reload).to have_attributes(
code: 'en-x-rspec', language_code: 'en', script_code: 'Latn',
region_code: 826, variants: 'x-rspec')
end
it 'registers the future British English locale tuple' do
pending '日本語以外の Locale 有効化は別課題で実装する'
expect(locale).to have_attributes(code: 'en', language_code: 'en',
script_code: 'Latn', region_code: 826,
variants: '')
end
end
+1 -1
ファイルの表示
@@ -2,7 +2,7 @@ require 'rails_helper'
RSpec.describe MaterialExportItem, type: :model do
let(:user) { create(:user, :member) }
let(:tag) { Tag.create!(tag_name: TagName.create!(name: 'export_item'), category: :material) }
let(:tag) { create(:tag, primary_name: 'export_item', category: :material) }
let(:material) do
Material.create!(tag:, url: 'https://example.com/material',
created_by_user: user, updated_by_user: user)
+1 -1
ファイルの表示
@@ -18,7 +18,7 @@ RSpec.describe Post, type: :model do
expect(post.snapshot_tags_json).to eq([
{ 'tag_id' => tag.id,
'version_no' => tag.version_no,
'name' => tag.name,
'name' => primary_tag_name_for(tag, 'ja').name,
'category' => tag.category,
'sections' => [{ 'begin_ms' => 2000, 'end_ms' => nil }] },
{ 'external_tag_id' => external.id }])
+2 -2
ファイルの表示
@@ -1,8 +1,8 @@
require 'rails_helper'
RSpec.describe PostVersion, type: :model do
let!(:tag_name) { TagName.create!(name: 'post_version_spec_tag') }
let!(:tag) { Tag.create!(tag_name: tag_name, category: :general) }
let!(:tag_name) { create(:tag_name, name: 'post_version_spec_tag') }
let!(:tag) { create(:tag, primary_tag_name: tag_name, category: :general) }
let!(:post_record) do
Post.create!(title: 'spec post', url: 'https://example.com/post-version-spec').tap do |post|
+35
ファイルの表示
@@ -0,0 +1,35 @@
require 'rails_helper'
RSpec.describe Script, type: :model do
include_context 'English locale'
it 'selects locales by script code, not language code' do
expect(described_class.find('Jpan').locales).to contain_exactly(Locale.nipponese)
end
it 'accepts supported script codes and rejects unsupported ones' do
expect(described_class.find('Jpan')).to be_valid
expect(described_class.where(deprecated_at: nil).pluck(:code)).to eq(['Jpan'])
expect {
described_class.create!(code: 'Zzzz', name: 'Unsupported')
}.to raise_error { |error|
expect(error).to be_a(ArgumentError).or be_a(ActiveRecord::RecordInvalid)
}
end
it 'does not register Latin as a currently supported script' do
expect {
described_class.create!(code: 'Latn', name: 'Latin')
}.to raise_error { |error|
expect(error).to be_a(ArgumentError).or be_a(ActiveRecord::RecordInvalid)
}
end
it 'selects Latin locales when multilingual support is enabled' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
expect(described_class.find('Latn').locales).to contain_exactly(locale)
expect(described_class.find('Jpan').locales).not_to include(locale)
end
end
+5 -5
ファイルの表示
@@ -2,8 +2,8 @@ require 'rails_helper'
RSpec.describe TagImplication, type: :model do
it 'rejects a parent tag that would create a cycle' do
child = create(:tag, name: 'tag_implication_cycle_child')
parent = create(:tag, name: 'tag_implication_cycle_parent')
child = create(:tag, primary_name: 'tag_implication_cycle_child')
parent = create(:tag, primary_name: 'tag_implication_cycle_parent')
described_class.create!(tag: child, parent_tag: parent)
@@ -17,9 +17,9 @@ RSpec.describe TagImplication, type: :model do
end
it 'terminates even when existing data already contains a cycle' do
child = create(:tag, name: 'tag_implication_existing_cycle_child')
parent = create(:tag, name: 'tag_implication_existing_cycle_parent')
ancestor = create(:tag, name: 'tag_implication_existing_cycle_ancestor')
child = create(:tag, primary_name: 'tag_implication_existing_cycle_child')
parent = create(:tag, primary_name: 'tag_implication_existing_cycle_parent')
ancestor = create(:tag, primary_name: 'tag_implication_existing_cycle_ancestor')
described_class.create!(tag: parent, parent_tag: ancestor)
described_class.insert_all!(
+64 -20
ファイルの表示
@@ -31,12 +31,48 @@ RSpec.describe TagNameSanitisationRule, type: :model do
describe '.apply!' do
before do
described_class.create!(priority: 10, source_pattern: '_', replacement: '')
described_class.create!(priority: 10, source_pattern: 'foo_', replacement: 'foo')
end
context 'when only another language has the sanitised name' do
include_context 'English locale'
it 'keeps both names when sanitisation produces a cross-language match' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
nipponese = create(:tag_name, name: 'foobar')
english = create(:tag_name, name: 'temporary',
language_code: 'en', script_code: 'Latn')
english.update_columns(name: 'foo_bar')
expect { described_class.apply! }.not_to change(TagName, :count)
expect(english.reload).to have_attributes(name: 'foobar', language_code: 'en')
expect(nipponese.reload).to have_attributes(name: 'foobar', language_code: 'ja')
end
it 'renames without merging tags or deleting either language identity' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
nipponese = create(:tag, primary_name: 'foobar')
english_name = create(:tag_name, name: 'temporary',
language_code: 'en', script_code: 'Latn')
english = create(:tag, primary_tag_name: english_name)
english_name.update_columns(name: 'foo_bar')
described_class.apply!
expect(english_name.reload).to have_attributes(
name: 'foobar', language_code: 'en', tag_id: english.id)
expect(primary_tag_name_for(nipponese.reload, 'ja')).to have_attributes(
name: 'foobar', language_code: 'ja', tag_id: nipponese.id)
expect(primary_tag_name_for(english.reload, 'en')).to eq(english_name)
end
end
context 'when no conflicting tag_name exists' do
let!(:tag_name) do
TagName.create!(name: 'tmp').tap do |tn|
create(:tag_name, name: 'tmp').tap do |tn|
tn.update_columns(name: 'foo_bar', updated_at: Time.current)
end
end
@@ -47,10 +83,10 @@ RSpec.describe TagNameSanitisationRule, type: :model do
end
end
context 'when a conflicting canonical tag_name exists' do
let!(:existing) { TagName.create!(name: 'foobar') }
context 'when a conflicting primary tag name exists' do
let!(:existing) { create(:tag_name, name: 'foobar') }
let!(:source) do
TagName.create!(name: 'tmp').tap do |tn|
create(:tag_name, name: 'tmp').tap do |tn|
tn.update_columns(name: 'foo_bar', updated_at: Time.current)
end
end
@@ -63,48 +99,56 @@ RSpec.describe TagNameSanitisationRule, type: :model do
end
context 'when the source tag_name has a tag and the existing one has no tag' do
let!(:existing) { TagName.create!(name: 'foobar') }
let!(:source_tag) { create(:tag, name: 'tmp', category: :general) }
let!(:source_tag_name_id) { source_tag.tag_name_id }
let!(:existing) { create(:tag_name, name: 'foobar') }
let!(:source_tag) { create(:tag, primary_name: 'tmp', category: :general) }
let!(:source_tag_name_id) { primary_tag_name_for(source_tag, 'ja').id }
before do
source_tag.tag_name.update_columns(name: 'foo_bar', updated_at: Time.current)
primary_tag_name_for(source_tag, 'ja').update_columns(
name: 'foo_bar', updated_at: Time.current)
end
it 'moves the tag to the existing tag_name' do
described_class.apply!
expected_tag_name_id = existing.canonical_id || existing.id
expect(source_tag.reload.tag_name_id).to eq(expected_tag_name_id)
expect(existing.reload).to have_attributes(
tag_id: source_tag.id, language_code: 'ja', primary_flg: true)
expect(primary_tag_name_for(source_tag, 'ja')).to eq(existing)
expect(primary_tag_name_for(source_tag.reload, 'ja')).to eq(existing)
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false)
end
end
context 'when the sanitised name is an alias of an existing tag' do
let!(:existing_tag) { create(:tag) }
let!(:existing_primary) { primary_tag_name_for(existing_tag, 'ja') }
let!(:alias_name) do
TagName.create!(name: 'foobar', canonical: existing_tag.tag_name)
create(:tag_name, :alias, name: 'foobar', tag: existing_tag)
end
let!(:source) do
TagName.create!(name: 'tmp').tap do |tn|
create(:tag_name, name: 'tmp').tap do |tn|
tn.update_columns(name: 'foo_bar', updated_at: Time.current)
end
end
it 'deletes only the source and preserves the alias and its canonical tag' do
it 'deletes only the source and preserves the alias and its owning tag' do
described_class.apply!
expect(TagName.unscoped.exists?(source.id)).to be(false)
expect(alias_name.reload.canonical).to eq(existing_tag.tag_name)
expect(alias_name.reload).to have_attributes(
tag_id: existing_tag.id, language_code: 'ja', primary_flg: false)
expect(TagName.find_by!(tag_id: existing_tag.id,
language_code: 'ja', primary_flg: true))
.to eq(existing_primary)
expect(Tag.find(existing_tag.id)).to eq(existing_tag)
end
end
context 'when both source and existing tag_names have tags' do
let!(:existing_tn) { TagName.create!(name: 'foobar') }
let!(:existing_tag) { Tag.create!(tag_name: existing_tn, category: :general) }
let!(:existing_tn) { create(:tag_name, name: 'foobar') }
let!(:existing_tag) { create(:tag, primary_tag_name: existing_tn, category: :general) }
let!(:source_tn) { TagName.create!(name: 'tmp') }
let!(:source_tag) { Tag.create!(tag_name: source_tn, category: :general) }
let!(:source_tn) { create(:tag_name, name: 'tmp') }
let!(:source_tag) { create(:tag, primary_tag_name: source_tn, category: :general) }
let!(:source_tag_name_id) { source_tn.id }
before do
@@ -120,7 +164,7 @@ RSpec.describe TagNameSanitisationRule, type: :model do
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false)
expect(post.reload.tags).to contain_exactly(existing_tag)
expect(existing_tag.reload.name).to eq('foobar')
expect(primary_tag_name_for(existing_tag.reload, 'ja').name).to eq('foobar')
end
end
end
+171
ファイルの表示
@@ -0,0 +1,171 @@
require 'rails_helper'
RSpec.describe TagName, type: :model do
include_context 'English locale'
it 'allows the same name in different languages' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
create(:tag_name, name: 'shared_name')
english = build(:tag_name, name: 'shared_name',
language_code: 'en', script_code: 'Latn')
expect(english).to be_valid
expect { english.save! }.to change(described_class, :count).by(1)
end
it 'rejects a duplicate name within the same language' do
create(:tag_name, name: 'shared_name')
duplicate = build(:tag_name, name: 'shared_name')
expect(duplicate).to be_invalid
expect(duplicate.errors.of_kind?(:name, :taken)).to be(true)
end
it 'creates Nipponese factory names without requiring a tag' do
name = create(:tag_name)
expect(name.reload).to have_attributes(
language_code: 'ja', script_code: 'Jpan', primary_flg: true,
auto_generated: false, tag_id: nil)
end
it 'defaults ordinary persisted human names to confirmed provenance' do
name = described_class.create!(name: 'ordinary_human_name', language_code: 'ja',
script_code: 'Jpan', primary_flg: true)
expect(name.reload.auto_generated).to be(false)
end
it 'persists both sides of the representative tag factory association' do
name = create(:tag_name)
tag = create(:tag, primary_tag_name: name)
expect(name.reload).to have_attributes(
tag_id: tag.id, language_code: 'ja', script_code: 'Jpan', primary_flg: true)
expect(name.tag).to eq(tag)
expect(described_class.find_by!(tag_id: tag.id, language_code: 'ja', primary_flg: true))
.to eq(name)
expect(tag.reload.tag_name_id).to eq(name.id)
end
it 'creates a complete ordinary tag factory under the active Nipponese language' do
tag = create(:tag, primary_name: 'factory_multilingual')
Locale.where(deprecated_at: nil).distinct.pluck(:language_code).each do |language_code|
expect(TagName.where(tag:, language_code:, primary_flg: true).count).to eq(1)
end
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true))
.to contain_exactly(have_attributes(auto_generated: false))
end
it 'completes an ordinary tag factory under future active languages' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
tag = create(:tag, primary_name: 'future_factory')
expect(TagName.where(tag:, language_code: 'en', primary_flg: true))
.to contain_exactly(have_attributes(auto_generated: true))
end
it 'builds a primary name without persisting the tag or its name' do
tag = nil
expect {
tag = build(:tag, primary_name: 'built_primary')
}.to change(Tag, :count).by(0).and change(described_class, :count).by(0)
expect(tag).to be_new_record
expect(tag.tag_names.target).to contain_exactly(have_attributes(
name: 'built_primary', language_code: 'ja', script_code: 'Jpan',
primary_flg: true))
end
it 'persists an alias owned by the same tag as its primary name' do
primary_name = create(:tag_name)
tag = create(:tag, primary_tag_name: primary_name)
alias_name = create(:tag_name, :alias, name: 'valid_alias', tag:)
expect(alias_name.reload).to have_attributes(
tag_id: tag.id,
primary_flg: false, language_code: 'ja')
primary = described_class.find_by!(tag_id: alias_name.tag_id,
language_code: alias_name.language_code,
primary_flg: true)
expect(primary).to eq(primary_name)
end
describe '.canonicalise' do
it 'leaves detached aliases unchanged' do
create(:tag_name, :alias, name: 'detached_alias')
expect(described_class.canonicalise(Locale.nipponese, ['detached_alias']))
.to eq(['detached_alias'])
end
it 'does not resolve a detached alias through another detached primary' do
create(:tag_name, :alias, name: 'detached_alias')
create(:tag_name, name: 'unrelated_detached_primary')
expect(described_class.canonicalise(Locale.nipponese, ['detached_alias']))
.to eq(['detached_alias'])
end
it 'resolves the primary name of the alias language on the same tag' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
tag = create(:tag, primary_name: '日本語正本')
english_primary = primary_tag_name_for(tag, 'en')
english_primary.update!(name: 'english_primary', auto_generated: false)
english_alias = create(:tag_name, :alias, tag:, name: 'english_alias',
language_code: 'en', script_code: 'Latn')
representative_id = tag.tag_name_id
primary = described_class.find_by!(tag_id: english_alias.tag_id,
language_code: english_alias.language_code,
primary_flg: true)
expect(primary).to eq(english_primary)
expect(described_class.canonicalise(locale, [english_alias.name]))
.to eq([english_primary.name])
expect(tag.reload.tag_name_id).to eq(representative_id)
end
it 'resolves only aliases in the requested language' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
nipponese = create(:tag, primary_name: 'nipponese_canonical')
english_name = create(:tag_name, name: 'english_canonical',
language_code: 'en', script_code: 'Latn')
english = create(:tag, primary_tag_name: english_name)
nipponese_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: nipponese)
english_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: english,
language_code: 'en', script_code: 'Latn')
expect(nipponese_alias.reload).to have_attributes(
tag_id: nipponese.id,
primary_flg: false, language_code: 'ja')
expect(english_alias.reload).to have_attributes(
tag_id: english.id,
primary_flg: false, language_code: 'en')
expect(described_class.canonicalise(locale, ['shared_alias']))
.to eq(['english_canonical'])
expect(described_class.canonicalise(Locale.nipponese, ['shared_alias']))
.to eq(['nipponese_canonical'])
end
end
it 'rejects the generated-name prefix in human input' do
human_name = build(:tag_name, name: 'Tag_#999', auto_generated: false)
expect(human_name).to be_invalid
end
it 'allows the generated-name prefix for system-created names' do
generated_name = build(:tag_name, name: 'Tag_#1000', auto_generated: true)
expect { generated_name.save! }.to change(described_class, :count).by(1)
end
end
+352 -96
ファイルの表示
@@ -4,12 +4,12 @@ RSpec.describe Tag, type: :model do
describe 'external tag separation' do
['nico:reserved', 'NiCo:reserved'].each do |name|
it "rejects the reserved prefix #{ name } for internal tags" do
tag = build(:tag, name:)
tag = build(:tag, primary_name: name)
expect(tag).to be_invalid
expect(tag.errors[:name]).to be_present
expect {
described_class.normalise_tags!([name])
described_class.normalise_tags!(Locale.nipponese, [name])
}.to raise_error(Tag::NicoTagNormalisationError)
end
end
@@ -25,128 +25,161 @@ RSpec.describe Tag, type: :model do
end
describe '.normalise_tags!' do
include_context 'English locale'
it 'canonicalises aliases in the supplied language only' do
pending '日本語以外の Locale 有効化は別課題で実装する'
prepare_english_reference!
nipponese = create(:tag, primary_name: 'nipponese_canonical')
english_name = create(:tag_name, name: 'english_canonical',
language_code: 'en', script_code: 'Latn')
english = create(:tag, primary_tag_name: english_name)
primary_tag_name_for(english, 'ja').update!(
name: 'english_in_nipponese', auto_generated: false)
english_locale = locale
nipponese_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: nipponese)
english_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: english,
language_code: 'en', script_code: 'Latn')
expect(nipponese_alias.reload).to have_attributes(
tag_id: nipponese.id,
primary_flg: false, language_code: 'ja')
expect(english_alias.reload).to have_attributes(
tag_id: english.id,
primary_flg: false, language_code: 'en')
expect(described_class.normalise_tags!(
english_locale, ['shared_alias'],
with_tagme: false, with_no_deerjikist: false)).to eq([english])
expect(described_class.normalise_tags!(
Locale.nipponese, ['shared_alias'],
with_tagme: false, with_no_deerjikist: false)).to eq([nipponese])
end
it 'rejects deprecated tags when deny_deprecated is enabled' do
tag_name = TagName.create!(name: 'normalise deprecated tag')
deprecated_tag = Tag.create!(
tag_name:,
category: :general,
deprecated_at: 1.day.from_now
)
tag_name = create(:tag_name, name: 'normalise deprecated tag')
deprecated_tag = create(:tag,
primary_tag_name: tag_name,
category: :general,
deprecated_at: 1.day.from_now)
expect {
described_class.normalise_tags!(
[deprecated_tag.name],
deny_deprecated: true
)
described_class.normalise_tags!(Locale.nipponese,
[primary_tag_name_for(deprecated_tag, 'ja').name],
deny_deprecated: true)
}.to raise_error(Tag::DeprecatedTagNormalisationError) { |error|
expect(error.tag_names).to eq([deprecated_tag.name])
expect(error.tag_names).to eq([primary_tag_name_for(deprecated_tag, 'ja').name])
}
end
it 'rejects invalid section literals instead of treating them as zero' do
expect {
described_class.normalise_tags!(
['normalise_invalid_section[1:aa-2:00]'],
with_sections: true
)
described_class.normalise_tags!(Locale.nipponese,
['normalise_invalid_section[1:aa-2:00]'],
with_sections: true)
}.to raise_error(Tag::SectionLiteralParseError)
end
it 'parses open-ended section literals' do
result = described_class.normalise_tags!(
['伊地知ニジカ[1:00-]'],
with_sections: true
)
result = described_class.normalise_tags!(Locale.nipponese,
['伊地知ニジカ[1:00-]'],
with_sections: true)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
tag = result.fetch(:tags).find do |candidate|
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
end
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
end
it 'parses omitted begin as zero' do
result = described_class.normalise_tags!(
['伊地知ニジカ[-1:00]'],
with_sections: true
)
result = described_class.normalise_tags!(Locale.nipponese,
['伊地知ニジカ[-1:00]'],
with_sections: true)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
tag = result.fetch(:tags).find do |candidate|
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
end
expect(result.fetch(:sections).fetch(tag.id)).to eq([[0, 60_000]])
end
it 'treats fully open section literals as plain tags' do
result = described_class.normalise_tags!(
['伊地知ニジカ[-]'],
with_sections: true
)
result = described_class.normalise_tags!(Locale.nipponese,
['伊地知ニジカ[-]'],
with_sections: true)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
tag = result.fetch(:tags).find do |candidate|
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
end
expect(result.fetch(:sections)[tag.id]).to be_nil
end
it 'treats [0:00-] as a plain tag' do
result = described_class.normalise_tags!(
['伊地知ニジカ[0:00-]'],
with_sections: true
)
result = described_class.normalise_tags!(Locale.nipponese,
['伊地知ニジカ[0:00-]'],
with_sections: true)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
tag = result.fetch(:tags).find do |candidate|
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
end
expect(result.fetch(:sections)[tag.id]).to be_nil
end
it 'expands zero-width sections to one millisecond' do
result = described_class.normalise_tags!(
['伊地知ニジカ[1:00-1:00]'],
with_sections: true
)
result = described_class.normalise_tags!(Locale.nipponese,
['伊地知ニジカ[1:00-1:00]'],
with_sections: true)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
tag = result.fetch(:tags).find do |candidate|
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
end
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, 60_001]])
end
it 'swaps reversed section boundaries' do
result = described_class.normalise_tags!(
['伊地知ニジカ[2:00-1:00]'],
with_sections: true
)
result = described_class.normalise_tags!(Locale.nipponese,
['伊地知ニジカ[2:00-1:00]'],
with_sections: true)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
tag = result.fetch(:tags).find do |candidate|
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
end
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, 120_000]])
end
it 'merges open-ended sections over later bounded sections' do
result = described_class.normalise_tags!(
['伊地知ニジカ[1:00-][2:00-3:00]'],
with_sections: true
)
result = described_class.normalise_tags!(Locale.nipponese,
['伊地知ニジカ[1:00-][2:00-3:00]'],
with_sections: true)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
tag = result.fetch(:tags).find do |candidate|
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
end
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
end
it 'merges adjacent bounded and open-ended sections' do
result = described_class.normalise_tags!(
['伊地知ニジカ[1:00-3:00][3:00-]'],
with_sections: true
)
result = described_class.normalise_tags!(Locale.nipponese,
['伊地知ニジカ[1:00-3:00][3:00-]'],
with_sections: true)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
tag = result.fetch(:tags).find do |candidate|
primary_tag_name_for(candidate, 'ja').name == '伊地知ニジカ'
end
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
end
end
describe '.expand_parent_tags' do
it 'expands through multiple deprecated parents to an active ancestor' do
child = create(:tag, name: 'expand_child')
deprecated_parent = create(
:tag,
name: 'expand_deprecated_parent',
deprecated_at: Time.current
)
deprecated_grandparent = create(
:tag,
name: 'expand_deprecated_grandparent',
deprecated_at: Time.current
)
active_ancestor = create(:tag, name: 'expand_active_ancestor')
child = create(:tag, primary_name: 'expand_child')
deprecated_parent = create(:tag,
primary_name: 'expand_deprecated_parent',
deprecated_at: Time.current)
deprecated_grandparent = create(:tag,
primary_name: 'expand_deprecated_grandparent',
deprecated_at: Time.current)
active_ancestor = create(:tag, primary_name: 'expand_active_ancestor')
TagImplication.create!(tag: child, parent_tag: deprecated_parent)
TagImplication.create!(tag: deprecated_parent, parent_tag: deprecated_grandparent)
TagImplication.create!(tag: deprecated_grandparent, parent_tag: active_ancestor)
@@ -163,8 +196,8 @@ RSpec.describe Tag, type: :model do
end
it 'terminates when implications contain a cycle' do
first = create(:tag, name: 'expand_cycle_first')
second = create(:tag, name: 'expand_cycle_second')
first = create(:tag, primary_name: 'expand_cycle_first')
second = create(:tag, primary_name: 'expand_cycle_second')
TagImplication.create!(tag: first, parent_tag: second)
now = Time.current
TagImplication.insert_all!(
@@ -183,55 +216,268 @@ RSpec.describe Tag, type: :model do
end
describe '.find_or_create_by_tag_name!' do
context 'with an explicit locale' do
include_context 'English locale'
it 'creates the representative name with locale attributes and ownership' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
tag = described_class.find_or_create_by_tag_name!(
locale, 'english_name', category: :character)
expect(primary_tag_name_for(tag, 'en')).to have_attributes(
language_code: 'en', script_code: 'Latn',
primary_flg: true, tag_id: tag.id)
expect(tag.category).to eq('character')
end
it 'uses the English primary as the source for a generated Nipponese name' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
allow(TagName).to receive(:generate_name).and_return('generated_nipponese')
tag = described_class.find_or_create_by_tag_name!(
locale, 'english_origin', category: :general)
expect(TagName.where(tag:, language_code: 'en', primary_flg: true))
.to contain_exactly(have_attributes(
name: 'english_origin', auto_generated: false))
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true))
.to contain_exactly(have_attributes(
name: 'generated_nipponese', auto_generated: true))
source_tag_name = TagName.find_by!(tag:, language_code: 'en', primary_flg: true)
expect(TagName).to have_received(:generate_name)
.with(Locale.nipponese, tag, source_tag_name)
end
it 'gives a new tag one primary name for every active language' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
tag = described_class.find_or_create_by_tag_name!(
Locale.nipponese, 'new_multilingual_tag', category: :general)
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true).count).to eq(1)
expect(TagName.where(tag:, language_code: 'en', primary_flg: true).count).to eq(1)
end
it 'creates one primary per language when multiple locales use English' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
described_class.find_or_create_by_tag_name!(
locale, 'before_second_english_locale', category: :general)
Locale.create!(code: 'en-x-rspec', language_code: 'en',
script_code: 'Latn', region_code: 826,
variants: 'x-rspec', name: 'English')
tag = described_class.find_or_create_by_tag_name!(
locale, 'one_english_primary', category: :general)
expect(TagName.where(tag:, language_code: 'en', primary_flg: true).count).to eq(1)
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true).count).to eq(1)
end
it 'rolls back a new tag and requested name if name generation fails' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
original_tag_count = described_class.count
allow(TagName).to receive(:generate_name).and_raise('generation failed')
expect {
described_class.find_or_create_by_tag_name!(
locale, 'atomic_english_name', category: :general)
}.to raise_error('generation failed')
expect(described_class.count).to eq(original_tag_count)
expect(TagName.where(language_code: 'en', name: 'atomic_english_name')).to be_empty
expect(described_class.joins(:tag_names)
.where(tag_names: { name: 'atomic_english_name' })).to be_empty
end
it 'keeps a detached requested name detached if generation fails' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
detached = create(:tag_name, :alias, name: 'atomic_detached',
language_code: 'en', script_code: 'Latn')
original_tag_count = described_class.count
allow(TagName).to receive(:generate_name).and_raise('generation failed')
expect {
described_class.find_or_create_by_tag_name!(
locale, detached.name, category: :general)
}.to raise_error('generation failed')
expect(described_class.count).to eq(original_tag_count)
expect(detached.reload).to have_attributes(tag_id: nil, primary_flg: false)
end
it 'promotes a detached alias into the requested primary name' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
detached = create(:tag_name, :alias, name: 'detached_english',
language_code: 'en', script_code: 'Latn')
tag = described_class.find_or_create_by_tag_name!(
locale, detached.name, category: :general)
expect(detached.reload).to have_attributes(
tag_id: tag.id, primary_flg: true, canonical_id: nil,
auto_generated: false)
expect(TagName.where(tag:, language_code: 'en', primary_flg: true))
.to contain_exactly(detached)
end
it 'creates separate tag identities for the same name in different languages' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
nipponese = described_class.find_or_create_by_tag_name!(
Locale.nipponese, 'same_name', category: :general)
english = described_class.find_or_create_by_tag_name!(
locale, 'same_name', category: :character)
expect(english).not_to eq(nipponese)
expect(primary_tag_name_for(english, 'en')).to have_attributes(
name: 'same_name', tag_id: english.id, language_code: 'en', primary_flg: true)
expect(primary_tag_name_for(nipponese, 'ja')).to have_attributes(
name: 'same_name', tag_id: nipponese.id, language_code: 'ja', primary_flg: true)
expect(described_class.find_or_create_by_tag_name!(
locale, 'same_name', category: :general)).to eq(english)
expect(described_class.find_or_create_by_tag_name!(
Locale.nipponese, 'same_name', category: :general)).to eq(nipponese)
end
it 'preserves the V1 representative when looking up another primary language' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
tag = create(:tag, primary_name: '日本語代表名')
representative_id = tag.tag_name_id
english = primary_tag_name_for(tag, 'en')
english.update!(name: 'english_primary', auto_generated: false)
found = described_class.find_or_create_by_tag_name!(
locale, english.name, category: :general)
expect(found).to eq(tag)
expect(tag.reload.tag_name_id).to eq(representative_id)
expect(english.reload.tag_id).to eq(tag.id)
end
it 'normalises names using the supplied locale' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
tags = described_class.normalise_tags!(
locale, ['character:normalised_english'],
with_tagme: false, with_no_deerjikist: false)
expect(tags.length).to eq(1)
expect(primary_tag_name_for(tags.first, 'en')).to have_attributes(
name: 'normalised_english', language_code: 'en', script_code: 'Latn',
primary_flg: true, tag_id: tags.first.id)
end
end
it 'creates a tag and name with the requested category after stripping whitespace' do
tag = nil
expect {
tag = described_class.find_or_create_by_tag_name!(
tag = described_class.find_or_create_by_tag_name!(Locale.nipponese,
' lookup_new ', category: :character)
}.to change(Tag, :count).by(1).and change(TagName, :count).by(1)
expect(tag.name).to eq('lookup_new')
expect(primary_tag_name_for(tag, 'ja').name).to eq('lookup_new')
expect(tag.category).to eq('character')
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true).count).to eq(1)
expect(primary_tag_name_for(tag, 'ja').auto_generated).to be(false)
end
it 'reuses the canonical tag for an alias without changing its category' do
it 'promotes a detached Nipponese alias into a human-confirmed primary' do
detached = create(:tag_name, :alias, name: 'detached_nipponese')
tag = described_class.find_or_create_by_tag_name!(
Locale.nipponese, detached.name, category: :general)
expect(detached.reload).to have_attributes(
tag_id: tag.id, primary_flg: true, canonical_id: nil,
auto_generated: false)
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true))
.to contain_exactly(detached)
end
it 'reuses the owning tag for an alias without changing its category' do
tag = create(:tag, category: :character)
alias_name = TagName.create!(name: 'lookup_alias', canonical: tag.tag_name)
representative_id = tag.tag_name_id
alias_name = create(:tag_name, :alias, name: 'lookup_alias', tag:)
expect(alias_name.reload).to have_attributes(
tag_id: tag.id,
primary_flg: false, language_code: 'ja')
found = nil
expect {
found = described_class.find_or_create_by_tag_name!(
found = described_class.find_or_create_by_tag_name!(Locale.nipponese,
alias_name.name, category: :general)
}.to change(Tag, :count).by(0).and change(TagName, :count).by(0)
expect(found).to eq(tag)
expect(found.category).to eq('character')
expect(tag.reload.tag_name_id).to eq(representative_id)
end
it 'creates a tag for an existing canonical name reached through an alias' do
canonical = create(:tag_name)
alias_name = TagName.create!(name: 'lookup_alias', canonical:)
tag = nil
it 'reuses the owning tag through another alias' do
tag = create(:tag)
primary = primary_tag_name_for(tag, 'ja')
alias_name = create(:tag_name, :alias, name: 'lookup_alias', tag:)
other_alias = create(:tag_name, :alias, name: 'lookup_other_alias', tag:)
expect([alias_name.reload, other_alias.reload]).to all(have_attributes(
tag_id: tag.id,
primary_flg: false, language_code: 'ja'))
expect {
tag = described_class.find_or_create_by_tag_name!(
found = described_class.find_or_create_by_tag_name!(Locale.nipponese,
alias_name.name, category: :general)
}.to change(Tag, :count).by(1).and change(TagName, :count).by(0)
expect(found).to eq(tag)
}.to change(Tag, :count).by(0).and change(TagName, :count).by(0)
expect(tag.tag_name).to eq(canonical)
expect(primary_tag_name_for(tag.reload, 'ja')).to eq(primary)
end
end
describe '.merge_tags!' do
let!(:target_tag) { create(:tag, category: :general) }
let!(:source_tag) { create(:tag, category: :general) }
let!(:source_tag_name) { source_tag.tag_name }
let!(:source_tag_name) { primary_tag_name_for(source_tag, 'ja') }
let!(:post_record) do
Post.create!(url: 'https://example.com/posts/1', title: 'test post')
end
context 'with names in multiple languages' do
include_context 'English locale'
it 'keeps the target primary and moves source names as aliases' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
english_primary = primary_tag_name_for(target_tag, 'en')
source_primary = primary_tag_name_for(source_tag, 'en')
source_alias = create(:tag_name, :alias, tag: source_tag,
name: 'source_english_alias', language_code: 'en', script_code: 'Latn')
expect([english_primary, source_primary]).to all(have_attributes(
primary_flg: true, auto_generated: true))
described_class.merge_tags!(target_tag, [source_tag])
expect(primary_tag_name_for(target_tag.reload, 'en')).to eq(english_primary)
expect(TagName.where(tag: target_tag, language_code: 'en', primary_flg: true))
.to contain_exactly(english_primary)
expect([source_primary.reload, source_alias.reload]).to all(have_attributes(
tag_id: target_tag.id, language_code: 'en', primary_flg: false))
expect(TagName.where(tag: target_tag, language_code: 'ja', primary_flg: true))
.to contain_exactly(primary_tag_name_for(target_tag, 'ja'))
expect(source_tag_name.reload).to have_attributes(
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
end
end
context 'when merging a simple source tag' do
let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) }
@@ -243,7 +489,8 @@ RSpec.describe Tag, type: :model do
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
expect(target_link).to be_present
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
expect(source_tag_name.reload).to have_attributes(
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
expect(target_tag.reload.post_count).to eq(1)
end
end
@@ -269,15 +516,16 @@ RSpec.describe Tag, type: :model do
expect(target_post_tag.reload.sections).to contain_exactly(target_section)
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
expect(source_tag_name.reload).to have_attributes(
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
expect(target_tag.reload.post_count).to eq(1)
end
end
it 'keeps source history and records the new target alias after deleting the source' do
user = create_member_user!
source_name = source_tag.name
source_alias = TagName.create!(name: 'merge_alias', canonical: source_tag_name)
source_name = primary_tag_name_for(source_tag, 'ja').name
source_alias = create(:tag_name, :alias, name: 'merge_alias', tag: source_tag)
TagVersioning.ensure_snapshot!(source_tag, created_by_user: user)
original_version = source_tag.tag_versions.first
@@ -287,13 +535,17 @@ RSpec.describe Tag, type: :model do
expect(versions.pluck(:version_no, :event_type))
.to eq([[1, 'create'], [2, 'discard']])
expect(versions.first).to eq(original_version)
expect(versions.last).to have_attributes(
name: source_name, aliases: source_alias.name, created_by_user: user)
expect(versions.last.created_by_user).to eq(user)
expect(versions.last.names.fetch('ja')).to eq(
'name' => source_name, 'script_code' => 'Jpan',
'auto_generated' => false,
'aliases' => [{ 'name' => source_alias.name, 'script_code' => 'Jpan' }])
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
target_versions = target_tag.tag_versions.order(:version_no)
expect(target_versions.pluck(:event_type)).to eq(['create', 'update'])
expect(target_versions.last.aliases.split).to eq([source_name])
target_aliases = target_versions.last.names.fetch('ja').fetch('aliases').pluck('name')
expect(target_aliases).to contain_exactly('merge_alias', source_name)
end
it 'deletes source relationships while preserving unrelated relationships' do
@@ -347,7 +599,8 @@ RSpec.describe Tag, type: :model do
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
expect(PostTag.exists?(post: post_record, tag: source_tag)).to be(false)
expect(target_link).to be_present
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
expect(source_tag_name.reload).to have_attributes(
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
expect(target_tag.reload.post_count).to eq(1)
end
end
@@ -378,7 +631,8 @@ RSpec.describe Tag, type: :model do
expect(target_link).to be_present
expect(Tag.unscoped.exists?(source_tag.id)).to be(false)
expect(source_tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
expect(source_tag_name.reload).to have_attributes(
tag_id: target_tag.id, language_code: 'ja', primary_flg: false)
expect(target_tag.reload.post_count).to eq(1)
end
end
@@ -397,7 +651,7 @@ RSpec.describe Tag, type: :model do
it 'rolls back earlier deletions, links, and history when a later source has a wiki' do
earlier_source = create(:tag)
earlier_name = earlier_source.tag_name
earlier_name = primary_tag_name_for(earlier_source, 'ja')
source_section = create(:post_tag_section, post: post_record,
tag: source_tag,
begin_ms: 1000, end_ms: 2000)
@@ -407,7 +661,8 @@ RSpec.describe Tag, type: :model do
}.to raise_error(ActiveRecord::RecordInvalid)
expect(Tag.unscoped.exists?(earlier_source.id)).to be(true)
expect(earlier_name.reload.canonical_id).to be_nil
expect(earlier_name.reload).to have_attributes(
tag_id: earlier_source.id, language_code: 'ja', primary_flg: true)
expect(TagVersion.where(tag_id: [earlier_source.id, source_tag.id, target_tag.id]))
.to be_empty
expect(Tag.unscoped.exists?(source_tag.id)).to be(true)
@@ -416,7 +671,8 @@ RSpec.describe Tag, type: :model do
expect(source_post_tag.sections).to contain_exactly(source_section)
expect(PostTag.find_by(post: post_record, tag: target_tag)).to be_nil
expect(source_tag.reload.post_count).to eq(1)
expect(source_tag_name.reload.canonical_id).to be_nil
expect(source_tag_name.reload).to have_attributes(
tag_id: source_tag.id, language_code: 'ja', primary_flg: true)
expect(target_tag.reload.post_count).to eq(0)
end
end
+9 -7
ファイルの表示
@@ -1,7 +1,7 @@
require 'rails_helper'
RSpec.describe VersionRecord, type: :model do
let!(:tag) { create(:tag, name: 'version_record_tag') }
let!(:tag) { create(:tag, primary_name: 'version_record_tag') }
let!(:nico_tag) { create(:external_tag, name: 'version_record_tag') }
it 'makes TagVersion read only after create' do
@@ -9,13 +9,14 @@ RSpec.describe VersionRecord, type: :model do
tag: tag,
version_no: 1,
event_type: 'create',
name: tag.name,
name: primary_tag_name_for(tag, 'ja').name,
category: tag.category,
aliases: '',
names: { ja: { name: primary_tag_name_for(tag, 'ja').name,
script_code: 'Jpan', auto_generated: false, aliases: [] } },
parent_tag_ids: '',
created_at: Time.current,
created_by_user: nil
)
created_by_user: nil)
expect {
version.update!(name: 'changed')
@@ -27,13 +28,14 @@ RSpec.describe VersionRecord, type: :model do
tag: tag,
version_no: 1,
event_type: 'create',
name: tag.name,
name: primary_tag_name_for(tag, 'ja').name,
category: tag.category,
aliases: '',
names: { ja: { name: primary_tag_name_for(tag, 'ja').name,
script_code: 'Jpan', auto_generated: false, aliases: [] } },
parent_tag_ids: '',
created_at: Time.current,
created_by_user: nil
)
created_by_user: nil)
expect {
version.destroy!
+1 -1
ファイルの表示
@@ -30,7 +30,7 @@ RSpec.describe 'Deerjikists API', type: :request do
expect(json['tag']).to be_a(Hash)
expect(json['tag']['id']).to eq(tag1.id)
expect(json['tag']['name']).to eq(tag1.name)
expect(json['tag']['name']).to eq(primary_tag_name_for(tag1, 'ja').name)
end
end
+1 -1
ファイルの表示
@@ -15,7 +15,7 @@ RSpec.describe 'error responses', type: :request do
it 'returns a stable field-error payload for unprocessable requests' do
member = create(:user, :member)
tag = create(:tag, :general, name: 'error_response_tag')
tag = create(:tag, category: :general, primary_name: 'error_response_tag')
sign_in_as(member)
patch "/tags/#{ tag.id }", params: { category: 'nico' }
+7 -8
ファイルの表示
@@ -898,23 +898,22 @@ RSpec.describe 'Gekanator learning API', type: :request do
describe 'GET /gekanator/questions' do
it 'omits questions for deprecated tags' do
active_tag = Tag.create!(name: 'active_question_tag', category: :general)
deprecated_tag = Tag.create!(
name: 'deprecated_question_tag',
category: :general,
deprecated_at: Time.current
)
active_tag = create(:tag, primary_name: 'active_question_tag', category: :general)
deprecated_tag = create(:tag,
primary_name: 'deprecated_question_tag',
category: :general,
deprecated_at: Time.current)
[active_tag, deprecated_tag].each do |question_tag|
GekanatorQuestion.create!(
text: "#{ question_tag.name }?",
text: "#{ primary_tag_name_for(question_tag, 'ja').name }?",
kind: 'tag',
source: 'admin_curated',
status: 'accepted',
priority_weight: 1.0,
condition: {
type: 'tag',
key: "#{ question_tag.category }:#{ question_tag.name }"
key: "#{ question_tag.category }:#{ primary_tag_name_for(question_tag, 'ja').name }"
},
created_by: admin
)
+5 -6
ファイルの表示
@@ -4,12 +4,11 @@ require 'rails_helper'
RSpec.describe 'Gekanator posts API', type: :request do
describe 'GET /gekanator/posts' do
it 'omits deprecated tags and returns the stored similarity cosine' do
active_tag = Tag.create!(name: 'active tag', category: :general)
deprecated_tag = Tag.create!(
name: 'deprecated tag',
category: :general,
deprecated_at: Time.current
)
active_tag = create(:tag, primary_name: 'active tag', category: :general)
deprecated_tag = create(:tag,
primary_name: 'deprecated tag',
category: :general,
deprecated_at: Time.current)
post_record = Post.create!(title: 'source', url: 'https://example.com/source')
target_post = Post.create!(title: 'target', url: 'https://example.com/target')
+363
ファイルの表示
@@ -0,0 +1,363 @@
require 'rails_helper'
RSpec.describe 'Locale propagation on write paths', type: :request do
include_context 'English locale'
let(:member) { create(:user, :member) }
before do
sign_in_as(member)
end
def expect_localised_tag(name, expected_locale)
tag_name = TagName.find_by!(name:, language_code: expected_locale.language_code)
expect(tag_name).to have_attributes(
script_code: expected_locale.script_code, primary_flg: true)
expect(tag_name.tag_id).to be_present
tag_name.tag
end
it 'confirms a generated Nipponese primary when a human renames it' do
tag = create(:tag, primary_name: 'generated_source')
primary = primary_tag_name_for(tag, 'ja')
primary.update!(name: "Tag_##{ tag.id }", auto_generated: true)
patch "/tags/#{ tag.id }", params: { locale: 'ja', name: '人間が確定した名称' }
expect(response).to have_http_status(:ok), response.body
expect(primary.reload).to have_attributes(
name: '人間が確定した名称', auto_generated: false)
end
it 'rolls back Nipponese edits when an alias conflicts with another primary' do
tag = create(:tag, primary_name: 'target_before_collision')
create(:tag_name, :alias, tag:, name: 'existing_alias')
other_tag = create(:tag, primary_name: 'other_primary')
other_primary = primary_tag_name_for(other_tag, 'ja')
primary_name_before = primary_tag_name_for(tag, 'ja').name
aliases_before = tag.tag_names.where(primary_flg: false).order(:name).pluck(:name)
category_before = tag.category
put "/tags/#{ tag.id }", params: {
locale: 'ja', name: 'renamed_before_collision', category: 'character',
deprecated: false, aliases: 'other_primary', parent_tags: '' }
aggregate_failures do
expect(response).to have_http_status(:unprocessable_entity)
expect(other_primary.reload).to have_attributes(tag_id: other_tag.id, primary_flg: true)
expect(TagName.where(tag: other_tag, language_code: 'ja', primary_flg: true))
.to contain_exactly(other_primary)
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq(primary_name_before)
expect(tag.category).to eq(category_before)
expect(tag.tag_names.where(primary_flg: false).order(:name).pluck(:name))
.to eq(aliases_before)
end
end
['en', nil].each do |requested_locale|
context "with locale #{ requested_locale.inspect }" do
let(:locale_params) { requested_locale ? { locale: requested_locale } : { } }
let(:expected_locale) { requested_locale ? locale : Locale.nipponese }
it 'creates post tags in the requested language or Nipponese fallback' do
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
locale if requested_locale
post '/posts', params: locale_params.merge(
title: 'Locale post', url: 'https://example.com/locale-post',
tags: 'locale_post_tag', parent_post_ids: '')
expect(response).to have_http_status(:created), response.body
tag = expect_localised_tag('locale_post_tag', expected_locale)
expect(Post.find(json.fetch('id')).tags).to include(tag)
end
it 'updates post tags in the requested language or Nipponese fallback' do
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
locale if requested_locale
record = create(:post)
PostVersionRecorder.record!(post: record, event_type: :create, created_by_user: member)
put "/posts/#{ record.id }", params: locale_params.merge(
title: record.title, tags: 'locale_updated_tag', parent_post_ids: '',
base_version_no: record.reload.version_no)
expect(response).to have_http_status(:ok), response.body
tag = expect_localised_tag('locale_updated_tag', expected_locale)
expect(record.reload.tags).to include(tag)
end
it 'passes the resolved locale to bulk creation' do
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
locale if requested_locale
# Worker propagation is exercised in post_bulk_creator_spec.
expect_any_instance_of(PostBulkCreator).to receive(:run)
.with(expected_locale).and_return(results: [])
manifest = [{ title: 'Bulk locale', url: 'https://example.com/bulk-locale',
tags: 'bulk_locale_tag', parent_post_ids: '' }]
post '/posts/bulk', params: locale_params.merge(posts: JSON.generate(manifest)),
headers: { 'CONTENT_TYPE' => 'multipart/form-data' }
expect(response).to have_http_status(:ok), response.body
end
it 'creates materials with tags in the resolved locale' do
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
locale if requested_locale
post '/materials', params: locale_params.merge(
tag: 'locale_material', url: 'https://example.com/material')
expect(response).to have_http_status(:created), response.body
tag = expect_localised_tag('locale_material', expected_locale)
expect(Material.find(json.fetch('id')).tag).to eq(tag)
end
it 'updates materials with tags in the resolved locale' do
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
locale if requested_locale
tag = create(:tag, category: :material)
material = Material.create!(tag:, url: 'https://example.com/material')
put "/materials/#{ material.id }", params: locale_params.merge(
tag: 'locale_material_updated', url: material.url)
expect(response).to have_http_status(:ok), response.body
resolved_tag = expect_localised_tag('locale_material_updated', expected_locale)
expect(material.reload.tag).to eq(resolved_tag)
end
it 'links Nico external tags to internal tags in the resolved locale' do
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
locale if requested_locale
external = create(:external_tag)
NicoTagVersionRecorder.record!(
external_tag: external, event_type: :create, created_by_user: member)
put "/tags/nico/#{ external.id }", params: locale_params.merge(tags: 'locale_link')
expect(response).to have_http_status(:ok), response.body
tag = expect_localised_tag('locale_link', expected_locale)
expect(external.reload.linked_tags).to contain_exactly(tag)
end
it 'creates parent tags in the resolved locale' do
pending '日本語以外の Locale 有効化は別課題で実装する' if requested_locale
locale if requested_locale
tag = create(:tag)
put "/tags/#{ tag.id }", params: locale_params.merge(
name: primary_tag_name_for(tag, 'ja').name, category: tag.category, deprecated: false,
aliases: '', parent_tags: 'locale_parent')
expect(response).to have_http_status(:ok), response.body
parent = expect_localised_tag('locale_parent', expected_locale)
expect(TagImplication.where(tag:).pluck(:parent_tag_id)).to eq([parent.id])
end
end
end
context 'with existing names in both languages' do
let(:english_name) do
create(:tag_name, name: 'shared_name', language_code: 'en', script_code: 'Latn')
end
let(:english_tag) { create(:tag, primary_tag_name: english_name, category: :material) }
let(:nipponese_tag) do
create(:tag, primary_name: 'temporary_nipponese', category: :general).tap do |tag|
# Isolate lookup from the separately tested uniqueness validation.
primary_tag_name_for(tag, 'ja').update_columns(name: 'shared_name')
end
end
it 'plans and creates the post using the English identity and category' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
english_tag
nipponese_tag
post '/posts', params: {
locale: 'en', title: 'English identity',
url: 'https://example.com/english-identity', tags: 'shared_name', parent_post_ids: '' }
expect(response).to have_http_status(:created), response.body
record = Post.find(json.fetch('id'))
expect(record.tags).to include(english_tag)
expect(record.tags).not_to include(nipponese_tag)
expect(english_tag.reload.category).to eq('material')
expect(nipponese_tag.reload.category).to eq('general')
end
it 'updates the post using the English identity' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
english_tag
nipponese_tag
record = create(:post)
PostVersionRecorder.record!(post: record, event_type: :create, created_by_user: member)
put "/posts/#{ record.id }", params: {
locale: 'en', title: record.title, tags: 'shared_name', parent_post_ids: '',
base_version_no: record.reload.version_no }
expect(response).to have_http_status(:ok), response.body
expect(record.reload.tags).to include(english_tag)
expect(record.tags).not_to include(nipponese_tag)
end
it 'resolves the English material tag' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
english_tag
nipponese_tag
post '/materials', params: {
locale: 'en', tag: 'shared_name', url: 'https://example.com/english-material' }
expect(response).to have_http_status(:created), response.body
expect(Material.find(json.fetch('id')).tag).to eq(english_tag)
expect(Material.where(tag: nipponese_tag)).to be_empty
end
it 'resolves the English parent tag' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
english_tag
nipponese_tag
child = create(:tag)
put "/tags/#{ child.id }", params: {
locale: 'en', name: primary_tag_name_for(child, 'ja').name,
category: child.category, deprecated: false,
aliases: '', parent_tags: 'shared_name' }
expect(response).to have_http_status(:ok), response.body
expect(TagImplication.where(tag: child).pluck(:parent_tag_id)).to eq([english_tag.id])
end
it 'resolves the English internal tag for a Nico link' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
english_tag
nipponese_tag
external = create(:external_tag)
NicoTagVersionRecorder.record!(
external_tag: external, event_type: :create, created_by_user: member)
put "/tags/nico/#{ external.id }", params: { locale: 'en', tags: 'shared_name' }
expect(response).to have_http_status(:ok), response.body
expect(external.reload.linked_tags).to contain_exactly(english_tag)
end
end
context 'when renaming or updating aliases in English' do
let(:english_name) do
create(:tag_name, name: 'english_original', language_code: 'en', script_code: 'Latn')
end
let(:tag) { create(:tag, primary_tag_name: english_name) }
it 'marks a human-renamed generated primary as no longer auto-generated' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
generated_tag = create(:tag, primary_name: 'rename_generated_source')
generated_name = primary_tag_name_for(generated_tag, 'en')
expect(generated_name.auto_generated).to be(true)
patch "/tags/#{ generated_tag.id }", params: {
locale: 'en', name: 'human_english_name' }
expect(response).to have_http_status(:ok), response.body
expect(generated_name.reload).to have_attributes(
name: 'human_english_name', auto_generated: false)
end
it 'does not steal another tag primary for an alias' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
other_tag = create(:tag, primary_name: 'other_nipponese')
other_primary = primary_tag_name_for(other_tag, 'en')
other_primary.update!(name: 'other_english_primary', auto_generated: false)
create(:tag_name, :alias, tag:, name: 'existing_english_alias',
language_code: 'en', script_code: 'Latn')
primary_name_before = primary_tag_name_for(tag, 'en').name
aliases_before =
tag.tag_names.where(language_code: 'en', primary_flg: false)
.order(:name).pluck(:name)
category_before = tag.category
put "/tags/#{ tag.id }", params: {
locale: 'en', name: 'renamed_before_alias_collision',
category: 'character', deprecated: false,
aliases: 'other_english_primary', parent_tags: '' }
aggregate_failures do
expect(response).to have_http_status(:unprocessable_entity)
expect(other_primary.reload).to have_attributes(
tag_id: other_tag.id, language_code: 'en', primary_flg: true)
expect(TagName.where(tag: other_tag, language_code: 'en', primary_flg: true))
.to contain_exactly(other_primary)
expect(primary_tag_name_for(tag.reload, 'en').name).to eq(primary_name_before)
expect(tag.category).to eq(category_before)
aliases_after =
tag.tag_names.where(language_code: 'en', primary_flg: false)
.order(:name).pluck(:name)
expect(aliases_after).to eq(aliases_before)
end
end
it 'allows a rename to a name already used in Nipponese' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
nipponese = create(:tag, primary_name: 'rename_target')
patch "/tags/#{ tag.id }", params: { locale: 'en', name: 'rename_target' }
expect(response).to have_http_status(:ok), response.body
expect(primary_tag_name_for(tag.reload, 'en')).to have_attributes(
name: 'rename_target', language_code: 'en', tag_id: tag.id)
expect(primary_tag_name_for(nipponese.reload, 'ja')).to have_attributes(
name: 'rename_target', language_code: 'ja', tag_id: nipponese.id)
end
it 'creates aliases in the requested language' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
put "/tags/#{ tag.id }", params: {
locale: 'en', name: english_name.name,
category: tag.category, deprecated: false,
aliases: 'english_alias', parent_tags: '' }
expect(response).to have_http_status(:ok), response.body
expect(TagName.find_by!(language_code: 'en', name: 'english_alias'))
.to have_attributes(
script_code: 'Latn',
tag_id: tag.id, primary_flg: false, language_code: 'en')
primary = TagName.find_by!(tag_id: tag.id, language_code: 'en', primary_flg: true)
expect(primary).to have_attributes(name: 'english_original', script_code: 'Latn')
end
it 'does not take an alias from another language' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
nipponese = create(:tag, primary_name: 'nipponese_owner')
nipponese_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: nipponese)
expect(nipponese_alias.reload).to have_attributes(
tag_id: nipponese.id,
primary_flg: false, language_code: 'ja')
put "/tags/#{ tag.id }", params: {
locale: 'en', name: english_name.name,
category: tag.category, deprecated: false,
aliases: 'shared_alias', parent_tags: '' }
expect(response).to have_http_status(:ok), response.body
expect(nipponese_alias.reload).to have_attributes(
tag_id: nipponese.id,
primary_flg: false, language_code: 'ja')
expect(TagName.find_by!(language_code: 'en', name: 'shared_alias'))
.to have_attributes(
tag_id: tag.id, script_code: 'Latn',
primary_flg: false, language_code: 'en')
expect(TagName.where(name: 'shared_alias').pluck(:language_code, :tag_id))
.to contain_exactly(['ja', nipponese.id], ['en', tag.id])
end
end
end
+25 -23
ファイルの表示
@@ -26,10 +26,10 @@ RSpec.describe 'Materials API', type: :request do
describe 'GET /materials' do
let!(:tag_a) do
Tag.create!(tag_name: TagName.create!(name: 'material_index_a'), category: :material)
create(:tag, primary_name: 'material_index_a', category: :material)
end
let!(:tag_b) do
Tag.create!(tag_name: TagName.create!(name: 'material_index_b'), category: :material)
create(:tag, primary_name: 'material_index_b', category: :material)
end
let!(:material_a) do
@@ -116,20 +116,20 @@ RSpec.describe 'Materials API', type: :request do
it 'filters by descendant tags and returns stable parent tag groups' do
root =
Tag.create!(tag_name: TagName.create!(name: 'material_scope_root'),
create(:tag, primary_name: 'material_scope_root',
category: :material)
child_b =
Tag.create!(tag_name: TagName.create!(name: 'material_scope_b'),
create(:tag, primary_name: 'material_scope_b',
category: :material)
child_a =
Tag.create!(tag_name: TagName.create!(name: 'material_scope_a'),
create(:tag, primary_name: 'material_scope_a',
category: :material)
deprecated =
Tag.create!(tag_name: TagName.create!(name: 'material_scope_old'),
create(:tag, primary_name: 'material_scope_old',
category: :material,
deprecated_at: Time.current)
grandchild =
Tag.create!(tag_name: TagName.create!(name: 'material_scope_grandchild'),
create(:tag, primary_name: 'material_scope_grandchild',
category: :material)
root_material =
build_material(tag: root, user: member_user,
@@ -179,7 +179,7 @@ RSpec.describe 'Materials API', type: :request do
describe 'GET /materials/:id' do
let!(:tag) do
Tag.create!(tag_name: TagName.create!(name: 'material_show'), category: :material)
create(:tag, primary_name: 'material_show', category: :material)
end
let!(:material) do
build_material(tag:, user: member_user, file: dummy_upload(filename: 'show.png'))
@@ -269,7 +269,7 @@ RSpec.describe 'Materials API', type: :request do
expect(response).to have_http_status(:created)
material = Material.order(:id).last
expect(material.tag.name).to eq('material_create_new')
expect(primary_tag_name_for(material.tag, 'ja').name).to eq('material_create_new')
expect(material.tag.category).to eq('material')
expect(material.created_by_user).to eq(member_user)
expect(material.updated_by_user).to eq(member_user)
@@ -297,7 +297,8 @@ RSpec.describe 'Materials API', type: :request do
expect(response).to have_http_status(:created)
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: 'material_create_versioned_tag' })
tag = TagName.find_by!(name: 'material_create_versioned_tag',
language_code: 'ja', primary_flg: true).tag
version = tag.tag_versions.order(:version_no).last
expect(version.event_type).to eq('create')
@@ -323,8 +324,8 @@ RSpec.describe 'Materials API', type: :request do
end
it 'returns 422 when the existing tag is not material/character' do
general_tag_name = TagName.create!(name: 'material_create_general_tag')
Tag.create!(tag_name: general_tag_name, category: :general)
general_tag_name = create(:tag_name, name: 'material_create_general_tag')
create(:tag, primary_tag_name: general_tag_name, category: :general)
post '/materials', params: {
tag: 'material_create_general_tag',
@@ -345,7 +346,7 @@ RSpec.describe 'Materials API', type: :request do
expect(response).to have_http_status(:created)
material = Material.order(:id).last
expect(material.tag.name).to eq('material_create_url_only')
expect(primary_tag_name_for(material.tag, 'ja').name).to eq('material_create_url_only')
expect(material.url).to eq('https://example.com/material-source')
expect(material.file.attached?).to be(false)
end
@@ -384,7 +385,7 @@ RSpec.describe 'Materials API', type: :request do
describe 'PUT /materials/:id' do
let!(:tag) do
Tag.create!(tag_name: TagName.create!(name: 'material_update_old'), category: :material)
create(:tag, primary_name: 'material_update_old', category: :material)
end
let!(:material) do
build_material(tag:, user: member_user, file: dummy_upload(filename: 'old.png'))
@@ -463,7 +464,7 @@ RSpec.describe 'Materials API', type: :request do
expect(response).to have_http_status(:ok)
material.reload
expect(material.tag.name).to eq('material_update_new')
expect(primary_tag_name_for(material.tag, 'ja').name).to eq('material_update_new')
expect(material.tag.category).to eq('material')
expect(material.url).to eq('https://example.com/updated-source')
expect(material.updated_by_user).to eq(member_user)
@@ -497,7 +498,8 @@ RSpec.describe 'Materials API', type: :request do
expect(response).to have_http_status(:ok)
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: 'material_update_versioned_tag' })
tag = TagName.find_by!(name: 'material_update_versioned_tag',
language_code: 'ja', primary_flg: true).tag
version = tag.tag_versions.order(:version_no).last
expect(version.event_type).to eq('create')
@@ -508,7 +510,7 @@ RSpec.describe 'Materials API', type: :request do
it 'backfills a create tag_version for an existing material tag without history' do
existing_tag =
Tag.create!(tag_name: TagName.create!(name: 'material_update_existing_no_history'),
create(:tag, primary_name: 'material_update_existing_no_history',
category: :material)
expect(existing_tag.tag_versions).to be_empty
@@ -531,7 +533,7 @@ RSpec.describe 'Materials API', type: :request do
it 'backfills a create tag_version for an existing character tag without history' do
existing_tag =
Tag.create!(tag_name: TagName.create!(name: 'material_update_character_no_history'),
create(:tag, primary_name: 'material_update_character_no_history',
category: :character)
expect(existing_tag.tag_versions).to be_empty
@@ -563,7 +565,7 @@ RSpec.describe 'Materials API', type: :request do
expect(response).to have_http_status(:ok)
material.reload
expect(material.tag.name).to eq('material_update_remove_file')
expect(primary_tag_name_for(material.tag, 'ja').name).to eq('material_update_remove_file')
expect(material.url).to eq('https://example.com/updated-source')
expect(material.updated_by_user).to eq(member_user)
expect(material.file.attached?).to be(false)
@@ -651,8 +653,8 @@ RSpec.describe 'Materials API', type: :request do
end
describe 'GET /materials/download.zip' do
let!(:tag_a) { Tag.create!(tag_name: TagName.create!(name: 'zip_a'), category: :material) }
let!(:tag_b) { Tag.create!(tag_name: TagName.create!(name: 'zip_b'), category: :material) }
let!(:tag_a) { create(:tag, primary_name: 'zip_a', category: :material) }
let!(:tag_b) { create(:tag, primary_name: 'zip_b', category: :material) }
let!(:material_a) do
build_material(tag: tag_a, user: member_user,
file: dummy_upload(filename: 'a.png', body: 'zip-a'))
@@ -701,7 +703,7 @@ RSpec.describe 'Materials API', type: :request do
describe 'GET /materials/versions' do
let!(:tag) do
Tag.create!(tag_name: TagName.create!(name: 'material_history'), category: :material)
create(:tag, primary_name: 'material_history', category: :material)
end
let!(:material) do
build_material(tag:, user: member_user, file: dummy_upload(filename: 'history.png'))
@@ -776,7 +778,7 @@ RSpec.describe 'Materials API', type: :request do
describe 'DELETE /materials/:id' do
let!(:tag) do
Tag.create!(tag_name: TagName.create!(name: 'material_destroy'), category: :material)
create(:tag, primary_name: 'material_destroy', category: :material)
end
let!(:material) do
build_material(tag:, user: member_user, file: dummy_upload(filename: 'destroy.png'))
+9 -9
ファイルの表示
@@ -44,10 +44,10 @@ RSpec.describe 'NicoTags', type: :request do
unlinked.update!(name: 'search_unlinked')
other = create(:external_tag)
other.update!(name: 'other')
destination = create(:tag, :general)
destination.tag_name.update!(name: 'destination_search')
destination = create(:tag, category: :general)
primary_tag_name_for(destination, 'ja').update!(name: 'destination_search')
NicoTagRelation.create!(nico_tag: linked, tag: destination)
NicoTagRelation.create!(nico_tag: other, tag: create(:tag, :general))
NicoTagRelation.create!(nico_tag: other, tag: create(:tag, category: :general))
get '/tags/nico', params: {
name: 'search_',
@@ -151,7 +151,7 @@ RSpec.describe 'NicoTags', type: :request do
it '404 when only an internal tag exists for the target id' do
sign_in_as(member)
non_nico = create(:tag, :general)
non_nico = create(:tag, category: :general)
expect(ExternalTag.exists?(non_nico.id)).to be(false)
patch "/tags/nico/#{non_nico.id}", params: { tags: 'a b' }
expect(response).to have_http_status(:not_found)
@@ -162,18 +162,18 @@ RSpec.describe 'NicoTags', type: :request do
nico_tag = create(:external_tag, name: 'nico_tags_spec_source')
linked_a_name = TagName.create!(name: 'nico_linked_a')
linked_a = Tag.create!(tag_name: linked_a_name, category: :general)
linked_a_name = create(:tag_name, name: 'nico_linked_a')
linked_a = create(:tag, primary_tag_name: linked_a_name, category: :general)
linked_b_name = TagName.create!(name: 'nico_linked_b')
linked_b = Tag.create!(tag_name: linked_b_name, category: :general)
linked_b_name = create(:tag_name, name: 'nico_linked_b')
linked_b = create(:tag, primary_tag_name: linked_b_name, category: :general)
NicoTagVersionRecorder.record!(external_tag: nico_tag,
event_type: :create, created_by_user: admin)
expect {
patch "/tags/nico/#{nico_tag.id}", params: {
tags: " #{linked_a.name}\n#{linked_b.name} "
tags: " #{ linked_a_name.name }\n#{ linked_b_name.name } "
}
}.to change(TagVersion, :count).by(2)
.and change(NicoTagVersion, :count).by(1)
+102 -84
ファイルの表示
@@ -113,8 +113,8 @@ RSpec.describe 'Posts API', type: :request do
'sections' => [])
end
let!(:tag_name) { TagName.create!(name: 'spec_tag') }
let!(:tag) { Tag.create!(tag_name: tag_name, category: :general) }
let!(:tag_name) { create(:tag_name, name: 'spec_tag') }
let!(:tag) { create(:tag, primary_tag_name: tag_name, category: :general) }
let!(:post_record) do
Post.create!(title: 'spec post', url: 'https://example.com/spec').tap do |p|
@@ -125,11 +125,11 @@ RSpec.describe 'Posts API', type: :request do
describe 'GET /posts' do
let!(:user) { create_member_user! }
let!(:tag_name) { TagName.create!(name: 'spec_tag') }
let!(:tag) { Tag.create!(tag_name:, category: :general) }
let!(:tag_name2) { TagName.create!(name: 'unko') }
let!(:tag2) { Tag.create!(tag_name: tag_name2, category: :deerjikist) }
let!(:alias_tag_name) { TagName.create!(name: 'manko', canonical: tag_name) }
let!(:tag_name) { create(:tag_name, name: 'spec_tag') }
let!(:tag) { create(:tag, primary_tag_name: tag_name, category: :general) }
let!(:tag_name2) { create(:tag_name, name: 'unko') }
let!(:tag2) { create(:tag, primary_tag_name: tag_name2, category: :deerjikist) }
let!(:alias_tag_name) { create(:tag_name, :alias, name: 'manko', tag:) }
let!(:hit_post) do
Post.create!(uploaded_user: user, title: 'hello spec world',
@@ -267,7 +267,7 @@ RSpec.describe 'Posts API', type: :request do
end
it 'keeps internal name searches independent of colliding external ids' do
get '/posts', params: { tags: tag.name }
get '/posts', params: { tags: primary_tag_name_for(tag, 'ja').name }
expect(response).to have_http_status(:ok)
expect(json.fetch('count')).to eq(3)
@@ -286,7 +286,7 @@ RSpec.describe 'Posts API', type: :request do
[nil, 'all'].each do |match|
it "intersects internal and external matches with match=#{ match || 'omitted' }" do
params = { tags: "#{ tag.name } nico:search_external" }
params = { tags: "#{ primary_tag_name_for(tag, 'ja').name } nico:search_external" }
params[:match] = match if match
get '/posts', params: params
@@ -307,7 +307,8 @@ RSpec.describe 'Posts API', type: :request do
end
it 'excludes external matches from an internal tag search' do
get '/posts', params: { tags: "#{ tag.name } not:nico:search_external" }
get '/posts', params: {
tags: "#{ primary_tag_name_for(tag, 'ja').name } not:nico:search_external" }
expect(response).to have_http_status(:ok)
expect(json.fetch('count')).to eq(2)
@@ -316,7 +317,9 @@ RSpec.describe 'Posts API', type: :request do
end
it 'unions an external match with a negated internal match' do
get '/posts', params: { tags: "nico:search_external not:#{ tag.name }", match: 'any' }
get '/posts', params: {
tags: "nico:search_external not:#{ primary_tag_name_for(tag, 'ja').name }",
match: 'any' }
expect(response).to have_http_status(:ok)
expect(json.fetch('count')).to eq(2)
@@ -333,7 +336,9 @@ RSpec.describe 'Posts API', type: :request do
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' }
get '/posts/random', params: {
tags: "#{ primary_tag_name_for(tag, 'ja').name } nico:search_external",
match: 'all' }
expect(response).to have_http_status(:ok)
expect(json.fetch('id')).to eq(both_post.id)
@@ -341,17 +346,17 @@ RSpec.describe 'Posts API', type: :request do
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) }
let!(:foo_tag_name) { create(:tag_name, name: 'not_spec_foo') }
let!(:foo_tag) { create(:tag, primary_tag_name: foo_tag_name, category: :general) }
let!(:bar_tag_name) { TagName.create!(name: 'not_spec_bar') }
let!(:bar_tag) { Tag.create!(tag_name: bar_tag_name, category: :general) }
let!(:bar_tag_name) { create(:tag_name, name: 'not_spec_bar') }
let!(:bar_tag) { create(:tag, primary_tag_name: bar_tag_name, category: :general) }
let!(:baz_tag_name) { TagName.create!(name: 'not_spec_baz') }
let!(:baz_tag) { Tag.create!(tag_name: baz_tag_name, category: :general) }
let!(:baz_tag_name) { create(:tag_name, name: 'not_spec_baz') }
let!(:baz_tag) { create(:tag, primary_tag_name: baz_tag_name, category: :general) }
let!(:foo_alias_tag_name) do
TagName.create!(name: 'not_spec_foo_alias', canonical: foo_tag_name)
create(:tag_name, :alias, name: 'not_spec_foo_alias', tag: foo_tag)
end
let!(:foo_only_post) do
@@ -739,7 +744,9 @@ RSpec.describe 'Posts API', type: :request do
.find { _1['name'] == 'nico:post_index_external' }
expect(post_json.fetch('tags')).to include(
a_hash_including('id' => tag.id, 'name' => tag.name, 'category' => 'general'))
a_hash_including('id' => tag.id,
'name' => primary_tag_name_for(tag, 'ja').name,
'category' => 'general'))
expect(external_json).not_to be_nil
expect_external_tag_json(external_json, external_tag)
end
@@ -772,11 +779,10 @@ RSpec.describe 'Posts API', type: :request do
end
it 'omits deprecated tags' do
deprecated_tag = Tag.create!(
name: 'deprecated_post_tag',
category: :general,
deprecated_at: Time.current
)
deprecated_tag = create(:tag,
primary_name: 'deprecated_post_tag',
category: :general,
deprecated_at: Time.current)
PostTag.create!(post: post_record, tag: deprecated_tag)
request
@@ -853,9 +859,9 @@ RSpec.describe 'Posts API', type: :request do
tags =
15.times.map do |i|
tag_name = TagName.create!(name: "show_query_tag_#{ i }")
tag = Tag.create!(tag_name:, category: :general)
TagName.create!(name: "show_query_alias_#{ i }", canonical: tag_name)
tag_name = create(:tag_name, name: "show_query_tag_#{ i }")
tag = create(:tag, primary_tag_name: tag_name, category: :general)
create(:tag_name, :alias, name: "show_query_alias_#{ i }", tag:)
PostTag.create!(post: post_record, tag:)
tag
end
@@ -910,7 +916,9 @@ RSpec.describe 'Posts API', type: :request do
expect(response).to have_http_status(:ok)
expect(json.fetch('tags')).to include(
a_hash_including('id' => tag.id, 'name' => tag.name, 'category' => 'general'))
a_hash_including('id' => tag.id,
'name' => primary_tag_name_for(tag, 'ja').name,
'category' => 'general'))
external_json =
json
.fetch('tags')
@@ -1000,7 +1008,7 @@ RSpec.describe 'Posts API', type: :request do
describe 'POST /posts' do
let(:member) { create(:user, :member) }
let!(:alias_tag_name) { TagName.create!(name: 'manko', canonical: tag_name) }
let!(:alias_tag_name) { create(:tag_name, :alias, name: 'manko', tag:) }
it '401 when not logged in' do
sign_out
@@ -1157,11 +1165,10 @@ RSpec.describe 'Posts API', type: :request do
end
it 'rejects a deprecated tag specified directly' do
Tag.create!(
name: 'deprecated_direct_tag',
category: :general,
deprecated_at: Time.current
)
create(:tag,
primary_name: 'deprecated_direct_tag',
category: :general,
deprecated_at: Time.current)
sign_in_as(member)
post '/posts', params: post_write_params(
@@ -1178,18 +1185,16 @@ RSpec.describe 'Posts API', type: :request do
end
it 'expands through multiple deprecated parent tags and saves active ancestors' do
child = Tag.create!(name: 'active_child', category: :general)
deprecated_parent = Tag.create!(
name: 'deprecated_parent',
category: :general,
deprecated_at: Time.current
)
deprecated_grandparent = Tag.create!(
name: 'deprecated_grandparent',
category: :general,
deprecated_at: Time.current
)
active_grandparent = Tag.create!(name: 'active_grandparent', category: :general)
child = create(:tag, primary_name: 'active_child', category: :general)
deprecated_parent = create(:tag,
primary_name: 'deprecated_parent',
category: :general,
deprecated_at: Time.current)
deprecated_grandparent = create(:tag,
primary_name: 'deprecated_grandparent',
category: :general,
deprecated_at: Time.current)
active_grandparent = create(:tag, primary_name: 'active_grandparent', category: :general)
TagImplication.create!(tag: child, parent_tag: deprecated_parent)
TagImplication.create!(tag: deprecated_parent, parent_tag: deprecated_grandparent)
TagImplication.create!(tag: deprecated_grandparent, parent_tag: active_grandparent)
@@ -1203,7 +1208,8 @@ RSpec.describe 'Posts API', type: :request do
)
expect(response).to have_http_status(:created)
saved_names = Post.find(json.fetch('id')).tags.map(&:name)
saved_names = TagName.where(tag_id: Post.find(json.fetch('id')).tags.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name)
expect(saved_names).to include('active_child', 'active_grandparent')
expect(saved_names).not_to include('deprecated_parent', 'deprecated_grandparent')
end
@@ -1306,7 +1312,8 @@ RSpec.describe 'Posts API', type: :request do
expect(response).to have_http_status(:created)
created_post = Post.find(json.fetch('id'))
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: '伊地知ニジカ' })
tag = TagName.find_by!(name: '伊地知ニジカ',
language_code: 'ja', primary_flg: true).tag
section = PostTagSection.find_by!(post: created_post, tag:)
expect(section.begin_ms).to eq(60_000)
@@ -1326,7 +1333,8 @@ RSpec.describe 'Posts API', type: :request do
expect(response).to have_http_status(:created)
created_post = Post.find(json.fetch('id'))
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: '伊地知ニジカ' })
tag = TagName.find_by!(name: '伊地知ニジカ',
language_code: 'ja', primary_flg: true).tag
expect(PostTagSection.find_by(post: created_post, tag:)).to be_nil
end
@@ -1343,7 +1351,8 @@ RSpec.describe 'Posts API', type: :request do
expect(response).to have_http_status(:created)
created_post = Post.find(json.fetch('id'))
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: '伊地知ニジカ' })
tag = TagName.find_by!(name: '伊地知ニジカ',
language_code: 'ja', primary_flg: true).tag
expect(PostTagSection.find_by(post: created_post, tag:)).to be_nil
end
@@ -1622,6 +1631,7 @@ RSpec.describe 'Posts API', type: :request do
expect(arguments[:thumbnails].keys).to eq([0])
expect(arguments[:host]).to eq('http://www.example.com')
end
expect(creator).to have_received(:run).with(Locale.nipponese)
end
it 'rejects malformed manifests as a request-level error' do
@@ -1658,8 +1668,8 @@ RSpec.describe 'Posts API', type: :request do
create(:post_tag_section, post: post_record, tag:,
begin_ms: 1000, end_ms: 2000)
tn2 = TagName.create!(name: 'spec_tag_2')
replacement_tag = Tag.create!(tag_name: tn2, category: :general)
tn2 = create(:tag_name, name: 'spec_tag_2')
replacement_tag = create(:tag, primary_tag_name: tn2, category: :general)
put "/posts/#{post_record.id}", params: post_update_params(
post_record,
@@ -1707,11 +1717,10 @@ RSpec.describe 'Posts API', type: :request do
end
it 'rejects a deprecated tag specified directly' do
Tag.create!(
name: 'deprecated_update_tag',
category: :general,
deprecated_at: Time.current
)
create(:tag,
primary_name: 'deprecated_update_tag',
category: :general,
deprecated_at: Time.current)
sign_in_as(member)
put "/posts/#{ post_record.id }", params: post_update_params(
@@ -1966,7 +1975,7 @@ RSpec.describe 'Posts API', type: :request do
put "/posts/#{post_record.id}", params: post_write_params(
base_version_no: base_version.version_no,
title: 'updated by me',
tags: "spec_tag #{Tag.no_deerjikist.name}")
tags: "spec_tag #{primary_tag_name_for(Tag.no_deerjikist, 'ja').name}")
expect(response).to have_http_status(:conflict)
@@ -1986,7 +1995,8 @@ RSpec.describe 'Posts API', type: :request do
base_version = create_post_version_for!(post_record.reload)
current_tag = Tag.find_or_create_by_tag_name!('current_added_tag', category: :general)
current_tag = Tag.find_or_create_by_tag_name!(
Locale.nipponese, 'current_added_tag', category: :general)
PostTag.create!(post: post_record, tag: current_tag, created_user: member)
PostVersionRecorder.record!(
@@ -1997,7 +2007,8 @@ RSpec.describe 'Posts API', type: :request do
put "/posts/#{post_record.id}", params: post_write_params(
base_version_no: base_version.version_no,
title: post_record.title,
tags: "spec_tag #{Tag.no_deerjikist.name} incoming_added_tag")
tags: ['spec_tag', primary_tag_name_for(Tag.no_deerjikist, 'ja').name,
'incoming_added_tag'].join(' '))
expect(response).to have_http_status(:conflict)
@@ -2015,7 +2026,8 @@ RSpec.describe 'Posts API', type: :request do
base_version = create_post_version_for!(post_record.reload)
current_tag = Tag.find_or_create_by_tag_name!('current_merge_tag', category: :general)
current_tag = Tag.find_or_create_by_tag_name!(
Locale.nipponese, 'current_merge_tag', category: :general)
PostTag.create!(post: post_record, tag: current_tag, created_user: member)
PostVersionRecorder.record!(
@@ -2026,15 +2038,17 @@ RSpec.describe 'Posts API', type: :request do
put "/posts/#{post_record.id}", params: post_write_params(
base_version_no: base_version.version_no,
title: post_record.title,
tags: "spec_tag #{Tag.no_deerjikist.name} incoming_merge_tag",
tags: ['spec_tag', primary_tag_name_for(Tag.no_deerjikist, 'ja').name,
'incoming_merge_tag'].join(' '),
merge: '1')
expect(response).to have_http_status(:ok)
names = post_record.reload.tags.map(&:name)
names = TagName.where(tag_id: post_record.reload.tags.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name)
expect(names).to include('spec_tag')
expect(names).to include(Tag.no_deerjikist.name)
expect(names).to include(primary_tag_name_for(Tag.no_deerjikist, 'ja').name)
expect(names).to include('current_merge_tag')
expect(names).to include('incoming_merge_tag')
end
@@ -2057,14 +2071,15 @@ RSpec.describe 'Posts API', type: :request do
put "/posts/#{post_record.id}", params: post_write_params(
base_version_no: base_version.version_no,
title: post_record.title,
tags: "spec_tag #{ Tag.no_deerjikist.name }")
tags: "spec_tag #{ primary_tag_name_for(Tag.no_deerjikist, 'ja').name }")
expect(response).to have_http_status(:ok)
names = post_record.reload.tags.map(&:name)
names = TagName.where(tag_id: post_record.reload.tags.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name)
expect(names).to include('spec_tag')
expect(names).to include(Tag.no_deerjikist.name)
expect(names).to include(primary_tag_name_for(Tag.no_deerjikist, 'ja').name)
expect(post_record.external_tags).to contain_exactly(nico_tag)
end
@@ -2079,14 +2094,15 @@ RSpec.describe 'Posts API', type: :request do
put "/posts/#{post_record.id}", params: post_write_params(
base_version_no: base_version.version_no,
title: 'updated title',
tags: "spec_tag #{ Tag.no_deerjikist.name }")
tags: "spec_tag #{ primary_tag_name_for(Tag.no_deerjikist, 'ja').name }")
expect(response).to have_http_status(:ok)
names = post_record.reload.tags.map(&:name)
names = TagName.where(tag_id: post_record.reload.tags.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name)
expect(names).to include('spec_tag')
expect(names).to include(Tag.no_deerjikist.name)
expect(names).to include(primary_tag_name_for(Tag.no_deerjikist, 'ja').name)
expect(post_record.external_tags).to contain_exactly(nico_tag)
external_json =
@@ -2102,7 +2118,8 @@ RSpec.describe 'Posts API', type: :request do
sign_in_as(member)
nico_tag = create_nico_tag!('nico:relation_source')
linked_tag = Tag.find_or_create_by_tag_name!('relation_linked_tag', category: :general)
linked_tag = Tag.find_or_create_by_tag_name!(
Locale.nipponese, 'relation_linked_tag', category: :general)
NicoTagRelation.create!(nico_tag:, tag: linked_tag)
PostExternalTag.create!(post: post_record, external_tag: nico_tag)
@@ -2113,16 +2130,17 @@ RSpec.describe 'Posts API', type: :request do
put "/posts/#{post_record.id}", params: post_write_params(
base_version_no: base_version.version_no,
title: post_record.title,
tags: "spec_tag #{ Tag.no_deerjikist.name }")
tags: "spec_tag #{ primary_tag_name_for(Tag.no_deerjikist, 'ja').name }")
expect(response).to have_http_status(:ok)
names = post_record.reload.tags.map(&:name)
names = TagName.where(tag_id: post_record.reload.tags.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name)
expect(post_record.external_tags).to contain_exactly(nico_tag)
expect(names).to include('spec_tag')
expect(names).to include(Tag.no_deerjikist.name)
expect(names).not_to include(linked_tag.name)
expect(names).to include(primary_tag_name_for(Tag.no_deerjikist, 'ja').name)
expect(names).not_to include(primary_tag_name_for(linked_tag, 'ja').name)
end
it 'force-updates stale posts without base_version_no' do
@@ -2138,7 +2156,7 @@ RSpec.describe 'Posts API', type: :request do
put "/posts/#{post_record.id}", params: post_write_params(
title: 'forced title',
tags: "spec_tag #{Tag.no_deerjikist.name}",
tags: "spec_tag #{primary_tag_name_for(Tag.no_deerjikist, 'ja').name}",
force: '1')
expect(response).to have_http_status(:ok)
@@ -2191,8 +2209,8 @@ RSpec.describe 'Posts API', type: :request do
let(:oc_from) { Time.zone.local(2019, 12, 31, 0, 0, 0) }
let(:oc_before) { Time.zone.local(2020, 1, 1, 0, 0, 0) }
let!(:tag_name2) { TagName.create!(name: 'spec_tag_2') }
let!(:tag2) { Tag.create!(tag_name: tag_name2, category: :general) }
let!(:tag_name2) { create(:tag_name, name: 'spec_tag_2') }
let!(:tag2) { create(:tag, primary_tag_name: tag_name2, category: :general) }
def snapshot_tags(post)
post.snapshot_tag_names.join(' ')
@@ -2417,7 +2435,7 @@ RSpec.describe 'Posts API', type: :request do
expect(response).to have_http_status(:ok)
expect(json.fetch('count')).to eq(3)
expect(json.fetch('versions').first.fetch('tags')).to include(
'name' => tag2.name, 'type' => 'context')
'name' => primary_tag_name_for(tag2, 'ja').name, 'type' => 'context')
end
it 'filters versions by tag when the current snapshot includes the tag' do
@@ -2550,8 +2568,8 @@ RSpec.describe 'Posts API', type: :request do
sign_in_as(member)
base_version = create_post_version_for!(post_record)
tag_name2 = TagName.create!(name: 'spec_tag_2')
Tag.create!(tag_name: tag_name2, category: :general)
tag_name2 = create(:tag_name, name: 'spec_tag_2')
create(:tag, primary_tag_name: tag_name2, category: :general)
expect do
put "/posts/#{post_record.id}", params: post_write_params(
@@ -2715,8 +2733,8 @@ RSpec.describe 'Posts API', type: :request do
base_version = create_post_version_for!(post_record.reload)
tag_name2 = TagName.create!(name: 'spec_tag_2')
tag2 = Tag.create!(tag_name: tag_name2, category: :general)
tag_name2 = create(:tag_name, name: 'spec_tag_2')
tag2 = create(:tag, primary_tag_name: tag_name2, category: :general)
expect {
put "/posts/#{post_record.id}", params: post_write_params(
+13 -9
ファイルの表示
@@ -3,13 +3,13 @@ require 'rails_helper'
RSpec.describe 'TagVersions API', type: :request do
let(:member) { create(:user, :member, name: 'version member') }
let!(:tag) { create(:tag, name: 'tag_versions_target', category: :general) }
let!(:other_tag) { create(:tag, name: 'tag_versions_other', category: :general) }
let!(:tag) { create(:tag, primary_name: 'tag_versions_target', category: :general) }
let!(:other_tag) { create(:tag, primary_name: 'tag_versions_other', category: :general) }
let!(:parent_shared) { create(:tag, name: 'parent_shared', category: :general) }
let!(:parent_old) { create(:tag, name: 'parent_old', category: :general) }
let!(:parent_new) { create(:tag, name: 'parent_new', category: :general) }
let!(:other_parent) { create(:tag, name: 'other_parent', category: :general) }
let!(:parent_shared) { create(:tag, primary_name: 'parent_shared', category: :general) }
let!(:parent_old) { create(:tag, primary_name: 'parent_old', category: :general) }
let!(:parent_new) { create(:tag, primary_name: 'parent_new', category: :general) }
let!(:other_parent) { create(:tag, primary_name: 'other_parent', category: :general) }
let(:t_v1) { Time.zone.local(2020, 1, 1, 12, 0, 0) }
let(:t_v2) { Time.zone.local(2020, 1, 2, 12, 0, 0) }
@@ -36,6 +36,10 @@ RSpec.describe 'TagVersions API', type: :request do
category: category,
deprecated_at: deprecated_at,
aliases: Array(aliases).join(' '),
names: { ja: { name:, script_code: 'Jpan', auto_generated: false,
aliases: Array(aliases).sort.map { |alias_name|
{ name: alias_name, script_code: 'Jpan' }
} } },
parent_tag_ids: Array(parent_tags).map(&:id).join(' '),
created_by_user: created_by_user,
created_at: created_at)
@@ -211,7 +215,7 @@ RSpec.describe 'TagVersions API', type: :request do
end
it 'returns empty when the specified tag has no versions' do
fresh_tag = create(:tag, name: 'no_versions_tag', category: :general)
fresh_tag = create(:tag, primary_name: 'no_versions_tag', category: :general)
get '/tags/versions', params: { id: fresh_tag.id }
@@ -232,8 +236,8 @@ RSpec.describe 'TagVersions API', type: :request do
end
it 'does not create tag versions by wiki updates when tag has no versions yet' do
wiki_tag_name = TagName.create!(name: 'tag_versions_from_wiki')
wiki_tag = Tag.create!(tag_name: wiki_tag_name, category: :general)
wiki_tag_name = create(:tag_name, name: 'tag_versions_from_wiki')
wiki_tag = create(:tag, primary_tag_name: wiki_tag_name, category: :general)
wiki_page =
Wiki::Commit.create_content!(
+7 -7
ファイルの表示
@@ -8,13 +8,13 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
end
def create_tag! name:, category: :general
tag_name = TagName.create!(name:)
Tag.create!(tag_name:, category:)
tag_name = create(:tag_name, name:)
create(:tag, primary_tag_name: tag_name, category:)
end
def create_wiki_for_tag! tag:, body: 'wiki body', user: member_user
Wiki::Commit.create_content!(
tag_name: tag.tag_name,
tag_name: primary_tag_name_for(tag, 'ja'),
body:,
created_by_user: user,
message: 'init')
@@ -43,7 +43,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
wiki_page.reload
version = wiki_page.wiki_versions.order(:version_no).last
expect(tag.name).to eq('patch_tag_wiki_after')
expect(primary_tag_name_for(tag, 'ja').name).to eq('patch_tag_wiki_after')
expect(wiki_page.title).to eq('patch_tag_wiki_after')
expect(version).to have_attributes(
@@ -72,7 +72,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
tag.reload
wiki_page.reload
expect(tag.name).to eq('patch_tag_category_only')
expect(primary_tag_name_for(tag, 'ja').name).to eq('patch_tag_category_only')
expect(tag.category).to eq('meme')
expect(wiki_page.wiki_versions.count).to eq(before_wiki_versions)
end
@@ -101,7 +101,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
wiki_page.reload
version = wiki_page.wiki_versions.order(:version_no).last
expect(tag.name).to eq('put_tag_wiki_after')
expect(primary_tag_name_for(tag, 'ja').name).to eq('put_tag_wiki_after')
expect(wiki_page.title).to eq('put_tag_wiki_after')
expect(version).to have_attributes(
@@ -134,7 +134,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
tag.reload
wiki_page.reload
expect(tag.name).to eq('put_tag_category_only')
expect(primary_tag_name_for(tag, 'ja').name).to eq('put_tag_category_only')
expect(tag.category).to eq('meme')
expect(wiki_page.wiki_versions.count).to eq(before_wiki_versions)
end
+2 -2
ファイルの表示
@@ -12,7 +12,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do
let(:guest) { create(:user, role: :guest) }
before do
tag.tag_name.update!(name: 'deerjika')
primary_tag_name_for(tag, 'ja').update!(name: 'deerjika')
end
describe 'GET /tags/:id/deerjikists' do
@@ -344,7 +344,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do
end
before do
other_tag.tag_name.update!(name: 'existing-deerjikist')
primary_tag_name_for(other_tag, 'ja').update!(name: 'existing-deerjikist')
end
it 'returns an indexed 422 error and rolls back the complete replacement' do
+245 -229
ファイルの表示
@@ -2,13 +2,13 @@ require 'cgi'
require 'rails_helper'
RSpec.describe 'Tags API', type: :request do
let!(:tn) { TagName.create!(name: 'spec_tag') }
let!(:tag) { Tag.create!(tag_name: tn, category: :general) }
let!(:alias_tn) { TagName.create!(name: 'unko', canonical: tn) }
let!(:tn) { create(:tag_name, name: 'spec_tag') }
let!(:tag) { create(:tag, primary_tag_name: tn, category: :general) }
let!(:alias_tn) { create(:tag_name, :alias, name: 'unko', tag:) }
let!(:post) { Post.create!(url: 'https://example.com/unkounkounko') }
let!(:post_tag) { PostTag.create!(post:, tag:) }
let!(:tn2) { TagName.create!(name: 'unknown') }
let!(:tag2) { Tag.create!(tag_name: tn2, category: :general) }
let!(:tn2) { create(:tag_name, name: 'unknown') }
let!(:tag2) { create(:tag, primary_tag_name: tn2, category: :general) }
def response_tags
json.fetch('tags')
@@ -18,6 +18,14 @@ RSpec.describe 'Tags API', type: :request do
response_tags.map { |t| t.fetch('name') }
end
def version_primary_name(version, language_code)
version.names.fetch(language_code).fetch('name')
end
def version_alias_names(version, language_code)
version.names.fetch(language_code).fetch('aliases').pluck('name')
end
def dummy_material_upload(filename: 'dummy.png', type: 'image/png', body: 'dummy')
Rack::Test::UploadedFile.new(StringIO.new(body), type, original_filename: filename)
end
@@ -38,7 +46,9 @@ RSpec.describe 'Tags API', type: :request do
expect(response).to have_http_status(:ok)
expect(json.fetch('count')).to eq(2)
expect(response_tags).to contain_exactly(
a_hash_including('id' => tag.id, 'name' => tag.name, 'category' => 'general'),
a_hash_including('id' => tag.id,
'name' => primary_tag_name_for(tag, 'ja').name,
'category' => 'general'),
a_hash_including(
'id' => external.id,
'name' => 'nico:spec_external',
@@ -91,7 +101,7 @@ RSpec.describe 'Tags API', type: :request do
end
it 'filters tags by category' do
meme = Tag.create!(tag_name: TagName.create!(name: 'meme_only'), category: :meme)
meme = create(:tag, primary_name: 'meme_only', category: :meme)
get '/tags', params: { category: 'meme' }
@@ -101,30 +111,29 @@ RSpec.describe 'Tags API', type: :request do
end
it 'filters tags by deprecated state' do
deprecated_tag = Tag.create!(
name: 'deprecated_filter',
category: :general,
deprecated_at: 1.day.from_now
)
active_tag = Tag.create!(name: 'active_filter', category: :general)
deprecated_tag = create(:tag,
primary_name: 'deprecated_filter',
category: :general,
deprecated_at: 1.day.from_now)
active_tag = create(:tag, primary_name: 'active_filter', category: :general)
get '/tags', params: { name: '_filter', deprecated: '1' }
expect(response).to have_http_status(:ok)
expect(response_names).to include(deprecated_tag.name)
expect(response_names).not_to include(active_tag.name)
expect(response_names).to include(primary_tag_name_for(deprecated_tag, 'ja').name)
expect(response_names).not_to include(primary_tag_name_for(active_tag, 'ja').name)
get '/tags', params: { name: '_filter', deprecated: '0' }
expect(response).to have_http_status(:ok)
expect(response_names).to include(active_tag.name)
expect(response_names).not_to include(deprecated_tag.name)
expect(response_names).to include(primary_tag_name_for(active_tag, 'ja').name)
expect(response_names).not_to include(primary_tag_name_for(deprecated_tag, 'ja').name)
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 = create(:tag, primary_name: 'pc_low', category: :general)
mid = create(:tag, primary_name: 'pc_mid', category: :general)
high = create(:tag, primary_name: 'pc_high', category: :general)
low.update_columns(post_count: 1)
mid.update_columns(post_count: 3)
@@ -142,8 +151,8 @@ RSpec.describe 'Tags API', type: :request do
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_tag = create(:tag, primary_name: 'created_old', category: :general)
new_tag = create(:tag, primary_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)
@@ -162,8 +171,8 @@ RSpec.describe 'Tags API', type: :request do
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_tag = create(:tag, primary_name: 'updated_old', category: :general)
new_tag = create(:tag, primary_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)
@@ -182,12 +191,12 @@ RSpec.describe 'Tags API', type: :request do
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)
create(:tag, primary_name: 'cat_deerjikist', category: :deerjikist)
create(:tag, primary_name: 'cat_meme', category: :meme)
create(:tag, primary_name: 'cat_character', category: :character)
create(:tag, primary_name: 'cat_general', category: :general)
create(:tag, primary_name: 'cat_material', category: :material)
create(:tag, primary_name: 'cat_meta', category: :meta)
create(:external_tag, name: 'cat_nico')
get '/tags', params: { name: 'cat_', order: 'category:asc', limit: 20 }
@@ -202,20 +211,20 @@ RSpec.describe 'Tags API', type: :request do
context 'with mixed legacy pagination' do
let!(:first_tag) do
create(:tag,
tag_name: create(:tag_name, name: 'a_mixed_page'),
category: :meme)
primary_tag_name: create(:tag_name, name: 'a_mixed_page'),
category: :meme)
end
let!(:middle_tag) do
create(:tag,
tag_name: create(:tag_name, name: 'm_mixed_page'),
category: :meta)
primary_tag_name: create(:tag_name, name: 'm_mixed_page'),
category: :meta)
end
let!(:last_tag) do
create(:tag,
tag_name: create(:tag_name, name: 'z_mixed_page'),
category: :general)
primary_tag_name: create(:tag_name, name: 'z_mixed_page'),
category: :general)
end
let!(:first_external) do
@@ -265,7 +274,7 @@ RSpec.describe 'Tags API', type: :request do
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)
create(:tag, primary_tag_name: create(:tag_name, name:), category: :general)
end
get '/tags', params: { name: 'pag_', order: 'name:asc', page: 2, limit: 2 }
@@ -276,8 +285,8 @@ RSpec.describe 'Tags API', type: :request do
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 = create(:tag, primary_name: 'fallback_low', category: :general)
high = create(:tag, primary_name: 'fallback_high', category: :general)
low.update_columns(post_count: 1)
high.update_columns(post_count: 9)
@@ -290,7 +299,7 @@ RSpec.describe 'Tags API', type: :request do
it 'normalises invalid page and limit' do
%w[norm_a norm_b].each do |name|
Tag.create!(tag_name: TagName.create!(name:), category: :general)
create(:tag, primary_tag_name: create(:tag_name, name:), category: :general)
end
get '/tags', params: { name: 'norm_', order: 'name:asc', page: 0, limit: 0 }
@@ -302,10 +311,9 @@ RSpec.describe 'Tags API', type: :request do
end
it 'returns aliases and parent tags' do
parent_tag = Tag.create!(
tag_name: TagName.create!(name: 'index_parent_tag'),
category: :meme
)
parent_tag = create(:tag,
primary_tag_name: create(:tag_name, name: 'index_parent_tag'),
category: :meme)
TagImplication.create!(tag:, parent_tag:)
get '/tags', params: { name: 'spec_tag' }
@@ -349,10 +357,9 @@ RSpec.describe 'Tags API', type: :request do
end
it 'returns aliases and parent tags' do
parent_tag = Tag.create!(
tag_name: TagName.create!(name: 'show_parent_tag'),
category: :character
)
parent_tag = create(:tag,
primary_tag_name: create(:tag_name, name: 'show_parent_tag'),
category: :character)
TagImplication.create!(tag:, parent_tag:)
request
@@ -398,7 +405,7 @@ RSpec.describe 'Tags API', type: :request do
it 'prefers Tag when Tag and ExternalTag have the same id' do
internal = create(
:tag,
tag_name: create(:tag_name, name: 'internal_collision'))
primary_tag_name: create(:tag_name, name: 'internal_collision'))
create(
:external_tag,
id: internal.id,
@@ -416,7 +423,7 @@ RSpec.describe 'Tags API', type: :request do
describe 'GET /tags/autocomplete' do
it 'combines internal and external matches without conflating equal ids' do
internal = Tag.create!(category: :general, name: 'mixed_internal', post_count: 2)
internal = create(:tag, category: :general, primary_name: 'mixed_internal', post_count: 2)
external = create(:external_tag, id: internal.id,
name: 'mixed_external', post_count: 3)
@@ -455,7 +462,7 @@ RSpec.describe 'Tags API', type: :request do
it 'limits the combined results to 20 and sorts ties by displayed name' do
11.times do |i|
name = "combined_#{ i.to_s.rjust(2, '0') }"
Tag.create!(category: :general, name:, post_count: 1)
create(:tag, category: :general, primary_name: name, post_count: 1)
create(:external_tag, name:, post_count: 1)
end
@@ -467,34 +474,39 @@ RSpec.describe 'Tags API', type: :request do
end
it 'excludes external tags but preserves internal alias matches when nico is false' do
internal = Tag.create!(category: :general, name: 'switch_internal', post_count: 1)
alias_target = Tag.create!(category: :general, name: 'alias_target', post_count: 1)
TagName.create!(name: 'switch_alias', canonical: alias_target.tag_name)
internal = create(:tag, category: :general, primary_name: 'switch_internal', post_count: 1)
alias_target = create(:tag, category: :general, primary_name: 'alias_target', post_count: 1)
create(:tag_name, :alias, name: 'switch_alias', tag: alias_target)
create(:external_tag, name: 'switch_external', post_count: 1)
get '/tags/autocomplete', params: { q: 'switch', nico: '0' }
expect(response).to have_http_status(:ok)
expect(json).to contain_exactly(
a_hash_including('id' => internal.id, 'name' => internal.name),
a_hash_including('id' => alias_target.id, 'name' => alias_target.name,
a_hash_including('id' => internal.id,
'name' => primary_tag_name_for(internal, 'ja').name),
a_hash_including('id' => alias_target.id,
'name' => primary_tag_name_for(alias_target, 'ja').name,
'matched_alias' => 'switch_alias'))
end
['%', '_'].each do |wildcard|
it "treats #{ wildcard } literally for canonical, alias, and external names" do
it "treats #{ wildcard } literally for primary, alias, and external names" do
literal = "literal#{ wildcard }match"
Tag.create!(category: :general, name: literal, post_count: 1)
alias_target = Tag.create!(category: :general, name: 'literal_alias_target', post_count: 1)
TagName.create!(name: "#{ literal }_alias", canonical: alias_target.tag_name)
create(:tag, category: :general, primary_name: literal, post_count: 1)
alias_target = create(
:tag, category: :general, primary_name: 'literal_alias_target', post_count: 1)
create(:tag_name, :alias, name: "#{ literal }_alias",
tag: alias_target)
create(:external_tag, name: literal, post_count: 1)
Tag.create!(category: :general, name: 'literalXmatch', post_count: 2)
create(:tag, category: :general, primary_name: 'literalXmatch', post_count: 2)
create(:external_tag, name: 'literalXmatch', post_count: 2)
get '/tags/autocomplete', params: { q: "literal#{ wildcard }" }
expect(json.map { |row| row.fetch('name') })
.to contain_exactly(literal, alias_target.name, "nico:#{ literal }")
.to contain_exactly(
literal, primary_tag_name_for(alias_target, 'ja').name, "nico:#{ literal }")
end
end
@@ -510,7 +522,7 @@ RSpec.describe 'Tags API', type: :request do
expect(t['matched_alias']).to be(nil)
end
it 'returns matching canonical tags by q with aliases' do
it 'returns matching primary tag names by q with aliases' do
get '/tags/autocomplete', params: { q: 'unk' }
expect(response).to have_http_status(:ok)
@@ -523,11 +535,10 @@ RSpec.describe 'Tags API', type: :request do
end
it 'omits deprecated tags' do
deprecated_tag = Tag.create!(
name: 'spec_deprecated',
category: :general,
deprecated_at: Time.current
)
deprecated_tag = create(:tag,
primary_name: 'spec_deprecated',
category: :general,
deprecated_at: Time.current)
deprecated_tag.update_columns(post_count: 1)
get '/tags/autocomplete', params: { q: 'spec_', present: '0' }
@@ -621,7 +632,7 @@ RSpec.describe 'Tags API', type: :request do
expect(response).to have_http_status(:ok)
tag.reload
expect(tag.name).to eq('new')
expect(primary_tag_name_for(tag, 'ja').name).to eq('new')
expect(tag.category).to eq('general')
body = JSON.parse(response.body)
@@ -636,7 +647,7 @@ RSpec.describe 'Tags API', type: :request do
expect(response).to have_http_status(:ok)
tag.reload
expect(tag.name).to eq('spec_tag')
expect(primary_tag_name_for(tag, 'ja').name).to eq('spec_tag')
expect(tag.category).to eq('meme')
end
@@ -646,7 +657,7 @@ RSpec.describe 'Tags API', type: :request do
expect(response).to have_http_status(:ok)
tag.reload
expect(tag.name).to eq('spec_tag')
expect(primary_tag_name_for(tag, 'ja').name).to eq('spec_tag')
expect(tag.category).to eq('general')
end
@@ -656,7 +667,7 @@ RSpec.describe 'Tags API', type: :request do
expect(response).to have_http_status(:ok)
tag.reload
expect(tag.name).to eq('n')
expect(primary_tag_name_for(tag, 'ja').name).to eq('n')
expect(tag.category).to eq('meta')
end
@@ -669,7 +680,7 @@ RSpec.describe 'Tags API', type: :request do
patch "/tags/#{tag.id}", params: { name: 'new', category: 'nico' }
expect(response).to have_http_status(:unprocessable_entity)
expect(tag.reload.name).to eq('spec_tag')
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
expect(tag.category).to eq('general')
end
@@ -684,11 +695,11 @@ RSpec.describe 'Tags API', type: :request do
expect(versions.map(&:event_type)).to eq(['create', 'update'])
expect(versions.first.name).to eq('spec_tag')
expect(version_primary_name(versions.first, 'ja')).to eq('spec_tag')
expect(versions.first.category).to eq('general')
expect(versions.first.aliases.split).to include('unko')
expect(version_alias_names(versions.first, 'ja')).to include('unko')
expect(versions.second.name).to eq('new_tag_name')
expect(version_primary_name(versions.second, 'ja')).to eq('new_tag_name')
expect(versions.second.category).to eq('meme')
expect(versions.second.created_by_user_id).to eq(member_user.id)
end
@@ -719,7 +730,7 @@ RSpec.describe 'Tags API', type: :request do
it 'PATCH で tag の name を変更すると対応する wiki version を作成する' do
wiki_page =
Wiki::Commit.create_content!(
tag_name: tag.tag_name,
tag_name: primary_tag_name_for(tag, 'ja'),
body: 'wiki body before',
created_by_user: member_user,
message: 'init')
@@ -747,7 +758,7 @@ RSpec.describe 'Tags API', type: :request do
it 'tag の category だけを変更しても wiki version は作成しない' do
wiki_page =
Wiki::Commit.create_content!(
tag_name: tag.tag_name,
tag_name: primary_tag_name_for(tag, 'ja'),
body: 'wiki body before',
created_by_user: member_user,
message: 'init')
@@ -774,7 +785,8 @@ RSpec.describe 'Tags API', type: :request do
}
expect(response).to have_http_status(:ok)
expect(TagName.find_by!(name: 'spec_tag').canonical).to eq(tag.reload.tag_name)
expect(TagName.find_by!(language_code: 'ja', name: 'spec_tag'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
patch "/tags/#{ tag.id }", params: { name: 'spec_tag' }
@@ -782,15 +794,17 @@ RSpec.describe 'Tags API', type: :request do
tag.reload
expect(tag.name).to eq('spec_tag')
expect(tag.tag_name.canonical_id).to be_nil
expect(TagName.find_by!(name: 'patch_roundtrip_target').canonical).to eq(tag.tag_name)
expect(primary_tag_name_for(tag, 'ja').name).to eq('spec_tag')
expect(primary_tag_name_for(tag, 'ja')).to have_attributes(
tag_id: tag.id, language_code: 'ja', primary_flg: true)
expect(TagName.find_by!(language_code: 'ja', name: 'patch_roundtrip_target'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
end
it '別 tag の正規名には変更できない' do
wiki_page =
Wiki::Commit.create_content!(
tag_name: tag.tag_name,
tag_name: primary_tag_name_for(tag, 'ja'),
body: 'patch collision wiki',
created_by_user: member_user,
message: 'init')
@@ -802,9 +816,10 @@ RSpec.describe 'Tags API', type: :request do
'name' => ['その名前は既に使はれてゐます.']
)
expect(tag.reload.name).to eq('spec_tag')
expect(tag.tag_name.aliases.map(&:name)).to contain_exactly('unko')
expect(wiki_page.reload.tag_name).to eq(tag.tag_name)
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
expect(TagName.where(tag_id: tag.id, language_code: 'ja', primary_flg: false).pluck(:name))
.to contain_exactly('unko')
expect(wiki_page.reload.tag_name).to eq(primary_tag_name_for(tag, 'ja'))
end
it 'system tag の name は変更できない' do
@@ -813,34 +828,34 @@ RSpec.describe 'Tags API', type: :request do
patch "/tags/#{ system_tag.id }", params: { name: 'patch_system_tag_renamed' }
expect(response).to have_http_status(:unprocessable_entity)
expect(system_tag.reload.name).to eq('bot操作')
expect(primary_tag_name_for(system_tag.reload, 'ja').name).to eq('bot操作')
end
end
end
describe 'GET /tags/with-depth' do
let!(:root_meme) do
Tag.create!(tag_name: TagName.create!(name: 'depth_a_root_meme'), category: :meme)
create(:tag, primary_name: 'depth_a_root_meme', category: :meme)
end
let!(:root_material) do
Tag.create!(tag_name: TagName.create!(name: 'depth_b_root_material'), category: :material)
create(:tag, primary_name: 'depth_b_root_material', category: :material)
end
let!(:hidden_general_root) do
Tag.create!(tag_name: TagName.create!(name: 'depth_hidden_general_root'), category: :general)
create(:tag, primary_name: 'depth_hidden_general_root', category: :general)
end
let!(:child_character) do
Tag.create!(tag_name: TagName.create!(name: 'depth_child_character'), category: :character)
create(:tag, primary_name: 'depth_child_character', category: :character)
end
let!(:grandchild_material) do
Tag.create!(tag_name: TagName.create!(name: 'depth_grandchild_material'), category: :material)
create(:tag, primary_name: 'depth_grandchild_material', category: :material)
end
let!(:child_general) do
Tag.create!(tag_name: TagName.create!(name: 'depth_child_general'), category: :general)
create(:tag, primary_name: 'depth_child_general', category: :general)
end
before do
@@ -883,15 +898,13 @@ RSpec.describe 'Tags API', type: :request do
end
it 'passes through deprecated tags when finding children' do
deprecated_middle = Tag.create!(
name: 'depth_deprecated_middle',
category: :character,
deprecated_at: Time.current
)
visible_descendant = Tag.create!(
name: 'depth_visible_descendant',
category: :material
)
deprecated_middle = create(:tag,
primary_name: 'depth_deprecated_middle',
category: :character,
deprecated_at: Time.current)
visible_descendant = create(:tag,
primary_name: 'depth_visible_descendant',
category: :material)
TagImplication.create!(parent_tag: root_material, tag: deprecated_middle)
TagImplication.create!(parent_tag: deprecated_middle, tag: visible_descendant)
@@ -903,24 +916,20 @@ RSpec.describe 'Tags API', type: :request do
end
it 'passes through multiple deprecated tags for roots and has_children' do
active_child = Tag.create!(
name: 'depth_active_child_below_deprecated',
category: :character
)
deprecated_parent = Tag.create!(
name: 'depth_deprecated_parent',
category: :character,
deprecated_at: Time.current
)
deprecated_grandparent = Tag.create!(
name: 'depth_deprecated_grandparent',
category: :material,
deprecated_at: Time.current
)
active_ancestor = Tag.create!(
name: 'depth_active_ancestor',
category: :meme
)
active_child = create(:tag,
primary_name: 'depth_active_child_below_deprecated',
category: :character)
deprecated_parent = create(:tag,
primary_name: 'depth_deprecated_parent',
category: :character,
deprecated_at: Time.current)
deprecated_grandparent = create(:tag,
primary_name: 'depth_deprecated_grandparent',
category: :material,
deprecated_at: Time.current)
active_ancestor = create(:tag,
primary_name: 'depth_active_ancestor',
category: :meme)
TagImplication.create!(tag: active_child, parent_tag: deprecated_parent)
TagImplication.create!(tag: deprecated_parent, parent_tag: deprecated_grandparent)
TagImplication.create!(tag: deprecated_grandparent, parent_tag: active_ancestor)
@@ -945,15 +954,13 @@ RSpec.describe 'Tags API', type: :request do
end
it 'treats an active tag with only deprecated ancestors as a root' do
active_child = Tag.create!(
name: 'depth_root_below_deprecated',
category: :character
)
deprecated_parent = Tag.create!(
name: 'depth_root_deprecated_parent',
category: :material,
deprecated_at: Time.current
)
active_child = create(:tag,
primary_name: 'depth_root_below_deprecated',
category: :character)
deprecated_parent = create(:tag,
primary_name: 'depth_root_deprecated_parent',
category: :material,
deprecated_at: Time.current)
TagImplication.create!(tag: active_child, parent_tag: deprecated_parent)
get '/tags/with-depth'
@@ -967,16 +974,14 @@ RSpec.describe 'Tags API', type: :request do
end
it 'terminates when deprecated implications contain a cycle' do
first = Tag.create!(
name: 'depth_cycle_first',
category: :character,
deprecated_at: Time.current
)
second = Tag.create!(
name: 'depth_cycle_second',
category: :material,
deprecated_at: Time.current
)
first = create(:tag,
primary_name: 'depth_cycle_first',
category: :character,
deprecated_at: Time.current)
second = create(:tag,
primary_name: 'depth_cycle_second',
category: :material,
deprecated_at: Time.current)
TagImplication.create!(tag: first, parent_tag: root_material)
TagImplication.create!(tag: second, parent_tag: first)
now = Time.current
@@ -1002,19 +1007,19 @@ RSpec.describe 'Tags API', type: :request do
let!(:material_user) { create_member_user! }
let!(:root_tag) do
Tag.create!(tag_name: TagName.create!(name: 'materials_root'), category: :material)
create(:tag, primary_name: 'materials_root', category: :material)
end
let!(:child_a_tag) do
Tag.create!(tag_name: TagName.create!(name: 'materials_child_a'), category: :material)
create(:tag, primary_name: 'materials_child_a', category: :material)
end
let!(:child_b_tag) do
Tag.create!(tag_name: TagName.create!(name: 'materials_child_b'), category: :character)
create(:tag, primary_name: 'materials_child_b', category: :character)
end
let!(:grandchild_tag) do
Tag.create!(tag_name: TagName.create!(name: 'materials_grandchild'), category: :material)
create(:tag, primary_name: 'materials_grandchild', category: :material)
end
let!(:root_material) do
@@ -1036,7 +1041,7 @@ RSpec.describe 'Tags API', type: :request do
end
it 'returns a tag tree with nested materials sorted by child name' do
get "/tags/name/#{ CGI.escape(root_tag.name) }/materials"
get "/tags/name/#{ CGI.escape(primary_tag_name_for(root_tag, 'ja').name) }/materials"
expect(response).to have_http_status(:ok)
@@ -1127,7 +1132,7 @@ RSpec.describe 'Tags API', type: :request do
}
expect(response).to have_http_status(:unprocessable_entity)
expect(tag.reload.name).to eq('spec_tag')
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
end
it 'category が空なら 422 を返す' do
@@ -1139,7 +1144,7 @@ RSpec.describe 'Tags API', type: :request do
}
expect(response).to have_http_status(:unprocessable_entity)
expect(tag.reload.name).to eq('spec_tag')
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
expect(tag.category).to eq('general')
end
@@ -1158,14 +1163,12 @@ RSpec.describe 'Tags API', type: :request do
end
it 'name, category, aliases, parent tags をまとめて更新できる' do
old_parent = Tag.create!(
tag_name: TagName.create!(name: 'put_old_parent'),
category: :general
)
kept_parent = Tag.create!(
tag_name: TagName.create!(name: 'put_kept_parent'),
category: :general
)
old_parent = create(:tag,
primary_tag_name: create(:tag_name, name: 'put_old_parent'),
category: :general)
kept_parent = create(:tag,
primary_tag_name: create(:tag_name, name: 'put_kept_parent'),
category: :general)
TagImplication.create!(tag:, parent_tag: old_parent)
TagImplication.create!(tag:, parent_tag: kept_parent)
@@ -1181,22 +1184,24 @@ RSpec.describe 'Tags API', type: :request do
tag.reload
expect(tag.name).to eq('put_renamed_tag')
expect(primary_tag_name_for(tag, 'ja').name).to eq('put_renamed_tag')
expect(tag.category).to eq('meme')
expect(TagName.find_by(name: 'put_alias_a').canonical).to eq(tag.tag_name)
expect(TagName.find_by(name: 'put_alias_b').canonical).to eq(tag.tag_name)
expect(TagName.find_by!(language_code: 'ja', name: 'put_alias_a'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
expect(TagName.find_by!(language_code: 'ja', name: 'put_alias_b'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
old_name_alias = TagName.find_by(name: 'spec_tag')
old_name_alias = TagName.find_by(language_code: 'ja', name: 'spec_tag')
expect(old_name_alias).to be_present
expect(old_name_alias.canonical).to eq(tag.tag_name)
expect(old_name_alias).to have_attributes(
tag_id: tag.id, language_code: 'ja', primary_flg: false)
expect(alias_tn.reload.canonical).to be_nil
expect(alias_tn.reload.tag_id).to be_nil
expect(tag.parents.map(&:name)).to contain_exactly(
'put_kept_parent',
'put_new_parent'
)
expect(TagName.where(tag_id: tag.parents.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name))
.to contain_exactly('put_kept_parent', 'put_new_parent')
expect(TagImplication.where(tag:, parent_tag: old_parent)).not_to exist
@@ -1226,7 +1231,8 @@ RSpec.describe 'Tags API', type: :request do
tag.reload
expect(TagName.find_by(name: 'put_alias_self_test').canonical).to eq(tag.tag_name)
expect(TagName.find_by!(language_code: 'ja', name: 'put_alias_self_test'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
expect(json['aliases']).to include('put_alias_self_test')
expect(json['aliases']).not_to include('spec_tag')
end
@@ -1263,13 +1269,13 @@ RSpec.describe 'Tags API', type: :request do
expect(versions.map(&:event_type)).to eq(['create', 'update'])
expect(versions.first.name).to eq('spec_tag')
expect(version_primary_name(versions.first, 'ja')).to eq('spec_tag')
expect(versions.first.category).to eq('general')
expect(versions.first.aliases.split).to include('unko')
expect(version_alias_names(versions.first, 'ja')).to include('unko')
expect(versions.second.name).to eq('put_versioned_tag')
expect(version_primary_name(versions.second, 'ja')).to eq('put_versioned_tag')
expect(versions.second.category).to eq('meta')
expect(versions.second.aliases.split).to include('spec_tag')
expect(version_alias_names(versions.second, 'ja')).to include('spec_tag')
expect(versions.second.created_by_user_id).to eq(member_user.id)
end
@@ -1296,35 +1302,37 @@ RSpec.describe 'Tags API', type: :request do
tag.reload
expect(tag.name).to eq('spec_tag')
expect(TagName.find_by!(name: 'put_roundtrip_b').canonical).to eq(tag.tag_name)
expect(tag.tag_name.aliases.map(&:name)).to contain_exactly('put_roundtrip_b', 'unko')
expect(tag.tag_name.aliases.map(&:name)).not_to include('spec_tag')
expect(alias_tn.reload.canonical).to eq(tag.tag_name)
expect(primary_tag_name_for(tag, 'ja').name).to eq('spec_tag')
expect(TagName.find_by!(language_code: 'ja', name: 'put_roundtrip_b'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
expect(TagName.where(tag_id: tag.id, language_code: 'ja', primary_flg: false).pluck(:name))
.to contain_exactly('put_roundtrip_b', 'unko')
expect(TagName.where(tag_id: tag.id, language_code: 'ja', primary_flg: false).pluck(:name))
.not_to include('spec_tag')
expect(alias_tn.reload).to have_attributes(
tag_id: tag.id, language_code: 'ja', primary_flg: false)
version = tag.tag_versions.order(:version_no).last
expect(version.event_type).to eq('update')
expect(version.name).to eq('spec_tag')
expect(version.aliases.split).to contain_exactly('put_roundtrip_b', 'unko')
expect(version_primary_name(version, 'ja')).to eq('spec_tag')
expect(version_alias_names(version, 'ja')).to contain_exactly('put_roundtrip_b', 'unko')
end
it 'parent tag の snapshot も作成する' do
old_parent = Tag.create!(
tag_name: TagName.create!(name: 'put_snapshot_old_parent'),
category: :general
)
new_parent = Tag.create!(
tag_name: TagName.create!(name: 'put_snapshot_new_parent'),
category: :general
)
old_parent = create(:tag,
primary_tag_name: create(:tag_name, name: 'put_snapshot_old_parent'),
category: :general)
new_parent = create(:tag,
primary_tag_name: create(:tag_name, name: 'put_snapshot_new_parent'),
category: :general)
TagImplication.create!(tag:, parent_tag: old_parent)
put "/tags/#{ tag.id }", params: {
name: 'spec_tag',
category: 'general',
aliases: 'unko',
parent_tags: new_parent.name,
parent_tags: primary_tag_name_for(new_parent, 'ja').name,
deprecated: '0',
}
@@ -1347,13 +1355,13 @@ RSpec.describe 'Tags API', type: :request do
expect(response).to have_http_status(:unprocessable_entity)
expect(tag.reload.name).to eq('spec_tag')
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
expect(tag.category).to eq('general')
end
it 'system tag の name は変更できない' do
system_tag = Tag.tagme
old_name = system_tag.name
old_name = primary_tag_name_for(system_tag, 'ja').name
old_category = system_tag.category
expect {
@@ -1368,14 +1376,14 @@ RSpec.describe 'Tags API', type: :request do
expect(response).to have_http_status(:unprocessable_entity)
expect(system_tag.reload.name).to eq(old_name)
expect(primary_tag_name_for(system_tag.reload, 'ja').name).to eq(old_name)
expect(system_tag.category).to eq(old_category)
end
it 'wiki を持つ tag を更新すると wiki version も作成する' do
wiki_page =
Wiki::Commit.create_content!(
tag_name: tag.tag_name,
tag_name: primary_tag_name_for(tag, 'ja'),
body: 'wiki body before',
created_by_user: member_user,
message: 'init')
@@ -1411,10 +1419,10 @@ RSpec.describe 'Tags API', type: :request do
)
end
it 'wiki を持つ tag を旧 alias へ戻しても wiki を新 canonical へ移す' do
it 'wiki を持つ tag を旧 alias へ戻しても wiki を新しい正本名へ移す' do
wiki_page =
Wiki::Commit.create_content!(
tag_name: tag.tag_name,
tag_name: primary_tag_name_for(tag, 'ja'),
body: 'wiki body before',
created_by_user: member_user,
message: 'init')
@@ -1443,9 +1451,11 @@ RSpec.describe 'Tags API', type: :request do
tag.reload
expect(wiki_page.reload.tag_name).to eq(tag.tag_name)
expect(TagName.find_by!(name: 'put_wiki_roundtrip_b').wiki_page).to be_nil
expect(TagName.find_by!(name: 'put_wiki_roundtrip_b').canonical).to eq(tag.tag_name)
expect(wiki_page.reload.tag_name).to eq(primary_tag_name_for(tag, 'ja'))
expect(TagName.find_by!(language_code: 'ja', name: 'put_wiki_roundtrip_b').wiki_page)
.to be_nil
expect(TagName.find_by!(language_code: 'ja', name: 'put_wiki_roundtrip_b'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
versions = wiki_page.wiki_versions.order(:version_no).last(2)
@@ -1454,16 +1464,16 @@ RSpec.describe 'Tags API', type: :request do
end
it '別名を他 tag から奪った場合、奪はれた側の tag version も作成する' do
old_owner = Tag.create!(
tag_name: TagName.create!(name: 'put_alias_old_owner'),
category: :general
)
stolen_alias = TagName.create!(
name: 'put_stolen_alias',
canonical: old_owner.tag_name
)
old_owner = create(:tag,
primary_tag_name: create(:tag_name, name: 'put_alias_old_owner'),
category: :general)
stolen_alias = create(:tag_name, :alias,
name: 'put_stolen_alias',
tag: old_owner)
expect(old_owner.tag_name.aliases.map(&:name)).to include('put_stolen_alias')
expect(TagName.where(tag_id: old_owner.id, language_code: 'ja',
primary_flg: false).pluck(:name))
.to include('put_stolen_alias')
expect {
put "/tags/#{ tag.id }", params: {
@@ -1479,30 +1489,32 @@ RSpec.describe 'Tags API', type: :request do
expect(response).to have_http_status(:ok)
expect(stolen_alias.reload.canonical).to eq(tag.tag_name)
expect(old_owner.reload.tag_name.aliases.map(&:name)).not_to include('put_stolen_alias')
expect(stolen_alias.reload).to have_attributes(
tag_id: tag.id, language_code: 'ja', primary_flg: false)
expect(TagName.where(tag_id: old_owner.id, language_code: 'ja',
primary_flg: false).pluck(:name))
.not_to include('put_stolen_alias')
old_owner_versions = old_owner.tag_versions.order(:version_no)
expect(old_owner_versions.first.event_type).to eq('create')
expect(old_owner_versions.first.aliases.split).to include('put_stolen_alias')
expect(version_alias_names(old_owner_versions.first, 'ja')).to include('put_stolen_alias')
expect(old_owner_versions.second.event_type).to eq('update')
expect(old_owner_versions.second.aliases.split).not_to include('put_stolen_alias')
expect(version_alias_names(old_owner_versions.second, 'ja'))
.not_to include('put_stolen_alias')
end
it '別 tag の alias 名を rename で奪へる' do
old_owner = Tag.create!(
tag_name: TagName.create!(name: 'put_alias_collision_owner'),
category: :general
)
stolen_alias = TagName.create!(
name: 'put_alias_collision_name',
canonical: old_owner.tag_name
)
old_owner = create(:tag,
primary_tag_name: create(:tag_name, name: 'put_alias_collision_owner'),
category: :general)
stolen_alias = create(:tag_name, :alias,
name: 'put_alias_collision_name',
tag: old_owner)
wiki_page =
Wiki::Commit.create_content!(
tag_name: tag.tag_name,
tag_name: primary_tag_name_for(tag, 'ja'),
body: 'put collision wiki',
created_by_user: member_user,
message: 'init')
@@ -1515,29 +1527,33 @@ RSpec.describe 'Tags API', type: :request do
deprecated: '0',
}
expect(response).to have_http_status(:ok)
expect(response).to have_http_status(:ok), response.body
tag.reload
old_owner.reload
stolen_alias.reload
expect(tag.name).to eq('put_alias_collision_name')
expect(stolen_alias.canonical_id).to be_nil
expect(TagName.find_by!(name: 'spec_tag').canonical).to eq(tag.tag_name)
expect(old_owner.tag_name.aliases.map(&:name)).not_to include('put_alias_collision_name')
expect(primary_tag_name_for(tag, 'ja').name).to eq('put_alias_collision_name')
expect(stolen_alias).to have_attributes(
tag_id: tag.id, language_code: 'ja', primary_flg: true)
expect(TagName.find_by!(language_code: 'ja', name: 'spec_tag'))
.to have_attributes(tag_id: tag.id, primary_flg: false)
expect(TagName.where(tag_id: old_owner.id, language_code: 'ja',
primary_flg: false).pluck(:name))
.not_to include('put_alias_collision_name')
old_owner_versions = old_owner.tag_versions.order(:version_no)
expect(old_owner_versions.last.event_type).to eq('update')
expect(old_owner_versions.last.aliases.split).not_to include('put_alias_collision_name')
expect(wiki_page.reload.tag_name).to eq(tag.tag_name)
expect(version_alias_names(old_owner_versions.last, 'ja'))
.not_to include('put_alias_collision_name')
expect(wiki_page.reload.tag_name).to eq(primary_tag_name_for(tag, 'ja'))
end
it 'parent_tags に指定すると循環する tag は 422 にする' do
child = Tag.create!(
tag_name: TagName.create!(name: 'put_cycle_child'),
category: :general
)
child = create(:tag,
primary_tag_name: create(:tag_name, name: 'put_cycle_child'),
category: :general)
TagImplication.create!(tag: child, parent_tag: tag)
@@ -1545,7 +1561,7 @@ RSpec.describe 'Tags API', type: :request do
name: 'spec_tag',
category: 'general',
aliases: 'unko',
parent_tags: child.name,
parent_tags: primary_tag_name_for(child, 'ja').name,
deprecated: '0',
}
@@ -1557,7 +1573,7 @@ RSpec.describe 'Tags API', type: :request do
it 'tag の name を変更すると対応する wiki version を作成する' do
wiki_page =
Wiki::Commit.create_content!(
tag_name: tag.tag_name,
tag_name: primary_tag_name_for(tag, 'ja'),
body: 'wiki body before',
created_by_user: member_user,
message: 'init')
+2 -2
ファイルの表示
@@ -396,7 +396,7 @@ RSpec.describe 'Theatres API', type: :request do
end
it 'finalizes skip when votes reach majority and stores voters and tag snapshots' do
tag = create(:tag, name: 'skip-target')
tag = create(:tag, primary_name: 'skip-target')
PostTag.create!(post: niconico_post, tag:)
TheatreSkipVote.create!(theatre:, post: niconico_post, user: member)
@@ -503,7 +503,7 @@ RSpec.describe 'Theatres API', type: :request do
end
it 'returns tag penalties and candidate weights for the current watchers' do
tag = create(:tag, name: 'heavy-tag')
tag = create(:tag, primary_name: 'heavy-tag')
PostTag.create!(post: second_niconico_post, tag:)
event = TheatreSkipEvent.create!(
theatre:,
+1 -1
ファイルの表示
@@ -10,7 +10,7 @@ RSpec.describe 'Wiki conflict handling', type: :request do
it 'returns 409 when base_revision_id is stale' do
page =
Wiki::Commit.create_content!(
tag_name: TagName.create!(name: 'wiki_conflict_request'),
tag_name: create(:tag_name, name: 'wiki_conflict_request'),
body: 'first',
created_by_user: user,
message: 'init')
+1 -1
ファイルの表示
@@ -10,7 +10,7 @@ RSpec.describe 'Wiki history integrity', type: :request do
def create_wiki_page title:, body: 'body', message: 'init', user: self.user
Wiki::Commit.create_content!(
tag_name: TagName.create!(name: title),
tag_name: create(:tag_name, name: title),
body:,
created_by_user: user,
message:)
+16 -16
ファイルの表示
@@ -10,7 +10,7 @@ RSpec.describe 'Wiki API', type: :request do
let!(:user) { create_member_user! }
let!(:tn) { TagName.create!(name: 'spec_wiki_title') }
let!(:tn) { create(:tag_name, name: 'spec_wiki_title') }
let!(:page) do
Wiki::Commit.create_content!(
tag_name: tn,
@@ -19,11 +19,10 @@ RSpec.describe 'Wiki API', type: :request do
message: 'init')
end
let!(:tag) do
Tag.create!(
tag_name: tn,
category: :general,
deprecated_at: Time.zone.local(2026, 6, 1)
)
create(:tag,
primary_tag_name: tn,
category: :general,
deprecated_at: Time.zone.local(2026, 6, 1))
end
describe 'GET /wiki' do
@@ -208,7 +207,7 @@ RSpec.describe 'Wiki API', type: :request do
{ 'X-Transfer-Code' => user.inheritance_code }
end
let!(:test_tag_name) { TagName.create!(name: 'TestPage') }
let!(:test_tag_name) { create(:tag_name, name: 'TestPage') }
let!(:page) do
Wiki::Commit.create_content!(
@@ -275,8 +274,8 @@ RSpec.describe 'Wiki API', type: :request do
end
it 'wiki body だけを変更しても tag version は作成しない' do
linked_tag_name = TagName.create!(name: 'wiki_body_only_tag')
linked_tag = Tag.create!(tag_name: linked_tag_name, category: :general)
linked_tag_name = create(:tag_name, name: 'wiki_body_only_tag')
linked_tag = create(:tag, primary_tag_name: linked_tag_name, category: :general)
TagVersionRecorder.record!(
tag: linked_tag,
@@ -359,13 +358,13 @@ RSpec.describe 'Wiki API', type: :request do
describe 'GET /wiki/search' do
before do
Wiki::Commit.create_content!(
tag_name: TagName.create!(name: 'spec_wiki_title_2'),
tag_name: create(:tag_name, name: 'spec_wiki_title_2'),
body: 'search body 2',
created_by_user: user,
message: 'init')
Wiki::Commit.create_content!(
tag_name: TagName.create!(name: 'unrelated_title'),
tag_name: create(:tag_name, name: 'unrelated_title'),
body: 'unrelated body',
created_by_user: user,
message: 'init')
@@ -434,7 +433,7 @@ RSpec.describe 'Wiki API', type: :request do
it 'returns empty array when page has no revisions and filtered by id' do
# 別ページを作って revision 無し
tn2 = TagName.create!(name: 'spec_no_rev')
tn2 = create(:tag_name, name: 'spec_no_rev')
# 異常データ: revision 無し WikiPage を直接作る
p2 = WikiPage.create!(
tag_name: tn2,
@@ -514,7 +513,7 @@ RSpec.describe 'Wiki API', type: :request do
describe 'Wiki::Commit.redirect!' do
it 'raises because redirect revisions are deprecated' do
target_tag_name = TagName.create!(name: 'redirect_deprecated_target')
target_tag_name = create(:tag_name, name: 'redirect_deprecated_target')
target =
Wiki::Commit.create_content!(
tag_name: target_tag_name,
@@ -535,8 +534,8 @@ RSpec.describe 'Wiki API', type: :request do
end
it 'wiki title を変更すると対応する tag の version を作成する' do
linked_tag_name = TagName.create!(name: 'wiki_linked_tag_for_version')
linked_tag = Tag.create!(tag_name: linked_tag_name, category: :general)
linked_tag_name = create(:tag_name, name: 'wiki_linked_tag_for_version')
linked_tag = create(:tag, primary_tag_name: linked_tag_name, category: :general)
linked_page =
Wiki::Commit.create_content!(
@@ -564,7 +563,8 @@ RSpec.describe 'Wiki API', type: :request do
expect(response).to have_http_status(:ok)
linked_tag.reload
expect(linked_tag.name).to eq('wiki_linked_tag_for_version_renamed')
expect(primary_tag_name_for(linked_tag, 'ja').name)
.to eq('wiki_linked_tag_for_version_renamed')
versions = linked_tag.tag_versions.order(:version_no)
+1 -1
ファイルの表示
@@ -9,7 +9,7 @@ RSpec.describe 'Wiki title collision', type: :request do
def create_wiki_page title:, body:
Wiki::Commit.create_content!(
tag_name: TagName.create!(name: title),
tag_name: create(:tag_name, name: title),
body:,
created_by_user: user,
message: 'init')
+1 -1
ファイルの表示
@@ -2,7 +2,7 @@ require 'rails_helper'
RSpec.describe MaterialSyncImporter do
let(:user) { create(:user, :member) }
let(:tag) { Tag.create!(tag_name: TagName.create!(name: 'sync_tag'), category: :material) }
let(:tag) { create(:tag, primary_name: 'sync_tag', category: :material) }
def tempfile_for body
Tempfile.new(['material-sync-importer', '.png']).tap do |file|
+1 -1
ファイルの表示
@@ -65,7 +65,7 @@ RSpec.describe NicoTagVersionRecorder do
end
it 'records sorted linked internal names and later link removal' do
tags = ['z_link', 'a_link'].map { |name| Tag.create!(name:, category: :general) }
tags = ['z_link', 'a_link'].map { |name| create(:tag, primary_name: name, category: :general) }
tags.each { |tag| NicoTagRelation.create!(nico_tag: external_tag, tag:) }
expect(record(:create).linked_tags).to eq('a_link z_link')
+33 -1
ファイルの表示
@@ -1,7 +1,10 @@
require 'rails_helper'
RSpec.describe PostBulkCreator do
include_context 'English locale'
it 'limits workers to two and keeps failures in their request slots' do
locale = Locale.nipponese
actor = instance_double(User, id: 123)
allow(User).to receive(:find).with(123) {
instance_double(User, id: 123)
@@ -9,6 +12,7 @@ RSpec.describe PostBulkCreator do
mutex = Mutex.new
active = 0
maximum_active = 0
creators = []
allow(PostCreatePreflight).to receive(:new) do |attributes:, **|
preflight = instance_double(PostCreatePreflight)
@@ -28,6 +32,7 @@ RSpec.describe PostBulkCreator do
end
allow(PostCreator).to receive(:new) do |attributes:, **|
creator = instance_double(PostCreator)
mutex.synchronize { creators << creator }
if attributes[:title] == 'broken'
allow(creator).to receive(:create!).and_raise(StandardError, 'broken')
else
@@ -45,7 +50,12 @@ RSpec.describe PostBulkCreator do
results = described_class.new(
actor:,
posts:,
thumbnails: { }).run.fetch(:results)
thumbnails: { }).run(locale).fetch(:results)
expect(creators.length).to eq(posts.length)
creators.each do |creator|
expect(creator).to have_received(:create!).with(locale).once
end
expect(maximum_active).to eq(2)
expect(results.length).to eq(posts.length)
@@ -56,4 +66,26 @@ RSpec.describe PostBulkCreator do
expect(results[2].dig(:post, :id)).to eq(3)
expect(results[3].dig(:post, :id)).to eq(4)
end
it 'passes a future English locale to preflight and creation in every worker' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
actor = instance_double(User, id: 123)
allow(User).to receive(:find).with(123).and_return(actor)
preflight = instance_double(PostCreatePreflight)
creator = instance_double(PostCreator)
post = instance_double(Post, id: 1)
allow(PostCreatePreflight).to receive(:new).and_return(preflight)
allow(preflight).to receive(:run).with(locale).and_return(existing_post_id: nil)
allow(PostCreator).to receive(:new).and_return(creator)
allow(creator).to receive(:create!).with(locale).and_return(post)
results = described_class.new(
actor:, posts: [{ title: 'future post' }], thumbnails: { }).run(locale)
expect(results.fetch(:results)).to contain_exactly(
hash_including(status: 'created', post: { id: 1 }))
expect(preflight).to have_received(:run).with(locale).once
expect(creator).to have_received(:create!).with(locale).once
end
end
+66 -9
ファイルの表示
@@ -2,7 +2,7 @@ require 'rails_helper'
RSpec.describe PostCreatePlan do
def create_tag! name, category
Tag.create!(name:, category:)
create(:tag, primary_name: name, category:)
end
before do
@@ -10,6 +10,63 @@ RSpec.describe PostCreatePlan do
create_tag!('ニジラー情報不詳', :meta)
end
context 'with an explicit locale' do
include_context 'English locale'
it 'resolves the matching language identity and its parents' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
english_name = create(:tag_name, name: 'shared_name',
language_code: 'en', script_code: 'Latn')
english = create(:tag, primary_tag_name: english_name, category: :character)
nipponese = create(:tag, primary_name: 'temporary_nipponese', category: :general)
# The DB permits this identity; validation has its own contract spec.
primary_tag_name_for(nipponese, 'ja').update_columns(name: 'shared_name')
english_parent_name = create(:tag_name, name: 'english_parent',
language_code: 'en', script_code: 'Latn')
english_parent = create(:tag, primary_tag_name: english_parent_name,
category: :material)
nipponese_parent = create(:tag, primary_name: 'nipponese_parent', category: :general)
TagImplication.create!(tag: english, parent_tag: english_parent)
TagImplication.create!(tag: nipponese, parent_tag: nipponese_parent)
plan = described_class.new(attributes: { tags: 'shared_name' }).build!(locale)
expect(plan[:direct_tag_specs]).to eq(
[{ name: 'shared_name', category: :character }])
expect(plan[:post_tag_specs]).to include(
{ name: primary_tag_name_for(english_parent, 'en').name, category: :material })
expect(plan[:post_tag_specs].pluck(:name))
.not_to include(primary_tag_name_for(nipponese_parent, 'ja').name)
end
it 'resolves aliases within the requested language' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
nipponese = create(:tag, primary_name: 'nipponese_canonical')
english_name = create(:tag_name, name: 'english_canonical',
language_code: 'en', script_code: 'Latn')
english = create(:tag, primary_tag_name: english_name, category: :character)
nipponese_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: nipponese)
english_alias = create(:tag_name, :alias, name: 'shared_alias',
tag: english,
language_code: 'en', script_code: 'Latn')
expect(nipponese_alias.reload).to have_attributes(
tag_id: nipponese.id,
primary_flg: false, language_code: 'ja')
expect(english_alias.reload).to have_attributes(
tag_id: english.id,
primary_flg: false, language_code: 'en')
plan = described_class.new(attributes: { tags: 'shared_alias' }).build!(locale)
expect(plan[:direct_tag_specs]).to eq(
[{ name: english_name.name, category: :character }])
end
end
it 'plans direct and existing default tags without persisting records' do
counts = [TagName.count, Tag.count]
@@ -18,7 +75,7 @@ RSpec.describe PostCreatePlan do
url: 'https://example.com/post',
title: 'title',
tags: 'character:new_character',
parent_post_ids: '' }).build!
parent_post_ids: '' }).build!(Locale.nipponese)
expect(plan[:tags]).to eq('new_character')
expect(plan[:direct_tag_specs]).to eq(
@@ -29,9 +86,9 @@ RSpec.describe PostCreatePlan do
expect([TagName.count, Tag.count]).to eq(counts)
end
it 'resolves aliases and keeps tag sections separate from canonical names' do
canonical = create_tag!('虹夏', :character)
TagName.create!(name: 'にじか', canonical: canonical.tag_name)
it 'resolves aliases and keeps tag sections separate from primary names' do
tag = create_tag!('虹夏', :character)
create(:tag_name, :alias, name: 'にじか', tag:)
create_tag!('動画', :meta)
plan = described_class.new(
@@ -40,7 +97,7 @@ RSpec.describe PostCreatePlan do
title: 'video',
tags: '動画 にじか[0:10-0:20]',
duration: '1:00',
parent_post_ids: '' }).build!
parent_post_ids: '' }).build!(Locale.nipponese)
expect(plan[:tags].split).to include('動画', '虹夏[0:10-0:20]')
expect(plan[:display_tags]).to include(
@@ -55,7 +112,7 @@ RSpec.describe PostCreatePlan do
counts = [Tag.count, TagName.count, ExternalTag.count]
expect {
described_class.new(attributes: { tags: 'NiCo:reserved' }).build!
described_class.new(attributes: { tags: 'NiCo:reserved' }).build!(Locale.nipponese)
}.to raise_error(ActiveRecord::RecordInvalid) { |error|
expect(error.record.errors[:tags]).to be_present
}
@@ -73,7 +130,7 @@ RSpec.describe PostCreatePlan do
url: 'https://example.com/post',
title: 'title',
tags: long_name,
parent_post_ids: '' }).build!
parent_post_ids: '' }).build!(Locale.nipponese)
}.to raise_error(ActiveRecord::RecordInvalid) { |error|
expect(error.record.errors[:tags]).not_to be_empty
}
@@ -87,7 +144,7 @@ RSpec.describe PostCreatePlan do
title: 'title',
tags: 'ordinary_tag',
duration: 'invalid',
parent_post_ids: '' }).build!
parent_post_ids: '' }).build!(Locale.nipponese)
expect(plan[:duration]).to eq('invalid')
expect(plan[:video_ms]).to be_nil
+32 -4
ファイルの表示
@@ -14,7 +14,6 @@ RSpec.describe PostCreator do
end
before do
allow(Tag).to receive(:normalise_tags!).and_return({ tags: [], sections: {} })
allow(TagVersioning).to receive(:record_tag_snapshots!)
allow(Tag).to receive(:expand_parent_tags).and_return([])
allow(PostVersionRecorder).to receive(:record!)
@@ -34,7 +33,7 @@ RSpec.describe PostCreator do
url: 'https://example.com/post',
thumbnail: real_thumbnail_upload,
thumbnail_base: 'https://example.com/thumb.jpg',
tags: '' }).create!
tags: '' }).create!(Locale.nipponese)
expect(post.thumbnail).to be_attached
expect(post.thumbnail_base).to eq('https://example.com/thumb.jpg')
@@ -54,7 +53,7 @@ RSpec.describe PostCreator do
title: 'title',
url: 'https://example.com/post',
thumbnail_base: 'https://example.com/thumb.jpg',
tags: '' }).create!
tags: '' }).create!(Locale.nipponese)
expect(post.thumbnail_base).to eq('https://example.com/thumb.jpg')
expect(post.thumbnail).to be_attached
@@ -72,7 +71,36 @@ RSpec.describe PostCreator do
tags: '' })
post_count = Post.count
expect { creator.create! }.to raise_error(Post::RemoteThumbnailFetchFailed)
expect { creator.create!(Locale.nipponese) }
.to raise_error(Post::RemoteThumbnailFetchFailed)
expect(Post.count).to eq(post_count)
end
context 'with an explicit locale' do
include_context 'English locale'
[false, true].each do |planned|
it "passes locale to tag creation with planned attributes: #{ planned }" do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
attributes = {
title: 'Locale propagation',
url: 'https://example.com/locale-post',
tags: 'character:locale_character' }
if planned
specs = [{ name: 'locale_character', category: :character }]
attributes.merge!(snapshot_tag_specs: specs, post_tag_specs: specs)
end
allow(Tag).to receive(:find_or_create_by_tag_name!).and_call_original
post = described_class.new(actor:, attributes:).create!(locale)
expect(Tag).to have_received(:find_or_create_by_tag_name!)
.with(locale, 'locale_character', category: :character).at_least(:once)
tag_name = TagName.find_by!(language_code: 'en', name: 'locale_character')
expect(tag_name).to have_attributes(script_code: 'Latn', primary_flg: true)
expect(post.tags).to include(tag_name.tag)
end
end
end
end
+1 -1
ファイルの表示
@@ -64,7 +64,7 @@ RSpec.describe PostImportPreviewer do
end
it 'applies metadata to automatic fields and recognises metadata tags' do
Tag.create!(name: 'known-tag', category: :general)
create(:tag, primary_name: 'known-tag', category: :general)
allow(PostMetadataFetcher).to receive(:fetch).and_return(
title: 'metadata title',
thumbnail_base: 'https://example.com/thumb.jpg',
+101
ファイルの表示
@@ -0,0 +1,101 @@
require 'rails_helper'
RSpec.describe TagVersionRecorder do
include_context 'English locale'
let(:user) { create_member_user! }
let!(:tag) { create(:tag, primary_name: '日本語名') }
before do
create(:tag_name, :alias, tag:, name: 'alias_z')
create(:tag_name, :alias, tag:, name: 'alias_a')
end
def nipponese_snapshot
{ 'name' => '日本語名', 'script_code' => 'Jpan', 'auto_generated' => false,
'aliases' => [{ 'name' => 'alias_a', 'script_code' => 'Jpan' },
{ 'name' => 'alias_z', 'script_code' => 'Jpan' }] }
end
def prepare_english_names!
locale
primary_tag_name_for(tag, 'en').update!(name: 'English name', auto_generated: false)
create(:tag_name, :alias, tag:, language_code: 'en', script_code: 'Latn',
name: 'English alias')
end
it 'snapshots the Nipponese primary and sorted aliases in grouped names' do
version = described_class.record!(tag:, event_type: :create, created_by_user: user)
expect(version.reload.names).to eq('ja' => nipponese_snapshot)
end
it 'does not create a version for an unchanged Nipponese snapshot' do
original = described_class.record!(tag:, event_type: :create, created_by_user: user)
unchanged = described_class.record!(tag:, event_type: :update, created_by_user: user)
expect(unchanged.version_no).to eq(original.version_no)
expect(tag.reload.tag_versions.count).to eq(1)
end
it 'snapshots an unconfirmed system primary with auto_generated true' do
primary_tag_name_for(tag, 'ja').update!(auto_generated: true)
version = described_class.record!(tag:, event_type: :create, created_by_user: user)
expect(version.names.fetch('ja')).to eq(nipponese_snapshot.merge('auto_generated' => true))
end
it 'keeps a past snapshot after current primary and alias changes' do
original = described_class.record!(tag:, event_type: :create, created_by_user: user)
primary_tag_name_for(tag, 'ja').update!(name: '改名後')
tag.tag_names.find_by!(name: 'alias_z').destroy!
updated = described_class.record!(tag:, event_type: :update, created_by_user: user)
expect(original.reload.names).to eq('ja' => nipponese_snapshot)
expect(updated.names.fetch('ja')).to include('name' => '改名後')
expect(updated.names.fetch('ja').fetch('aliases'))
.to eq([{ 'name' => 'alias_a', 'script_code' => 'Jpan' }])
expect(updated.version_no).to eq(2)
end
it 'snapshots future languages together without a request locale argument' do
pending '日本語以外の Locale 有効化は別課題で実装する'
prepare_english_names!
version = described_class.record!(tag:, event_type: :create, created_by_user: user)
expect(version.names).to eq(
'ja' => nipponese_snapshot,
'en' => { 'name' => 'English name', 'script_code' => 'Latn',
'auto_generated' => false,
'aliases' => [{ 'name' => 'English alias', 'script_code' => 'Latn' }] })
end
it 'creates a version for an English-only change and preserves prior names' do
pending '日本語以外の Locale 有効化は別課題で実装する'
prepare_english_names!
original = described_class.record!(tag:, event_type: :create, created_by_user: user)
primary_tag_name_for(tag, 'en').update!(name: 'Renamed')
updated = described_class.record!(tag:, event_type: :update, created_by_user: user)
expect(updated.version_no).to eq(2)
expect(original.reload.names.fetch('en')).to include('name' => 'English name')
expect(updated.names.fetch('en')).to include('name' => 'Renamed')
expect(updated.names.fetch('ja')).to eq(nipponese_snapshot)
end
it 'does not create a version for an unchanged multilingual snapshot' do
pending '日本語以外の Locale 有効化は別課題で実装する'
prepare_english_names!
described_class.record!(tag:, event_type: :create, created_by_user: user)
unchanged = described_class.record!(tag:, event_type: :update, created_by_user: user)
expect(unchanged.version_no).to eq(1)
expect(tag.reload.tag_versions.count).to eq(1)
end
end
+5 -5
ファイルの表示
@@ -6,7 +6,7 @@ RSpec.describe Wiki::Commit do
def create_page title:, body: 'initial body'
described_class.create_content!(
tag_name: TagName.create!(name: title),
tag_name: create(:tag_name, name: title),
body:,
created_by_user: user,
message: 'init')
@@ -16,7 +16,7 @@ RSpec.describe Wiki::Commit do
it 'creates page, revision, and version with normalised body' do
expect {
described_class.create_content!(
tag_name: TagName.create!(name: 'commit_integrity_create'),
tag_name: create(:tag_name, name: 'commit_integrity_create'),
body: "a\r\nb\r\n\r\n",
created_by_user: user,
message: 'init')
@@ -36,7 +36,7 @@ RSpec.describe Wiki::Commit do
end
it 'rejects body that becomes blank after normalisation' do
tag_name = TagName.create!(name: 'commit_integrity_blank')
tag_name = create(:tag_name, name: 'commit_integrity_blank')
expect {
described_class.create_content!(
@@ -76,8 +76,8 @@ RSpec.describe Wiki::Commit do
end
it 'does not record tag_version on body-only wiki update' do
tag_name = TagName.create!(name: 'commit_integrity_linked_tag')
tag = Tag.create!(tag_name:, category: :general)
tag_name = create(:tag_name, name: 'commit_integrity_linked_tag')
tag = create(:tag, primary_tag_name: tag_name, category: :general)
page =
described_class.create_content!(
+5 -5
ファイルの表示
@@ -4,7 +4,7 @@ RSpec.describe Wiki::Commit do
let(:user) { create_member_user! }
def create_page(title: 'commit_spec_page', body: 'initial body')
tag_name = TagName.create!(name: title)
tag_name = create(:tag_name, name: title)
Wiki::Commit.create_content!(
tag_name:,
@@ -80,8 +80,8 @@ RSpec.describe Wiki::Commit do
end
it 'does not record tag version when corresponding tag has no versions' do
tag_name = TagName.create!(name: 'commit_linked_tag_without_versions')
tag = Tag.create!(tag_name:, category: :general)
tag_name = create(:tag_name, name: 'commit_linked_tag_without_versions')
tag = create(:tag, primary_tag_name: tag_name, category: :general)
page =
described_class.create_content!(
@@ -107,8 +107,8 @@ RSpec.describe Wiki::Commit do
end
it 'does not record tag version when corresponding tag has no versions' do
tag_name = TagName.create!(name: 'commit_linked_tag_without_versions')
tag = Tag.create!(tag_name:, category: :general)
tag_name = create(:tag_name, name: 'commit_linked_tag_without_versions')
tag = create(:tag, primary_tag_name: tag_name, category: :general)
page =
described_class.create_content!(
+1 -1
ファイルの表示
@@ -5,7 +5,7 @@ RSpec.describe WikiVersionRecorder do
def create_page title:, body: 'body'
Wiki::Commit.create_content!(
tag_name: TagName.create!(name: title),
tag_name: create(:tag_name, name: title),
body:,
created_by_user: user,
message: 'init')
+4 -2
ファイルの表示
@@ -172,7 +172,8 @@ RSpec.describe Youtube::Sync do
Tag.video
Tag.no_deerjikist
deerjikist_tag = Tag.find_or_create_by_tag_name!('テスト投稿者', category: :deerjikist)
deerjikist_tag = Tag.find_or_create_by_tag_name!(
Locale.nipponese, 'テスト投稿者', category: :deerjikist)
Deerjikist.create!(
platform: 'youtube',
code: 'UC_MAPPED',
@@ -226,7 +227,8 @@ RSpec.describe Youtube::Sync do
)
PostTag.create!(post:, tag: Tag.no_deerjikist)
deerjikist_tag = Tag.find_or_create_by_tag_name!('後から判明した投稿者', category: :deerjikist)
deerjikist_tag = Tag.find_or_create_by_tag_name!(
Locale.nipponese, '後から判明した投稿者', category: :deerjikist)
Deerjikist.create!(
platform: 'youtube',
code: 'UC_MAPPED_LATER',
+22
ファイルの表示
@@ -0,0 +1,22 @@
RSpec.shared_context 'English locale' do
def prepare_english_reference!
Language.find_or_create_by!(code: 'en') { _1.name = 'English' }
Script.find_or_create_by!(code: 'Latn') { _1.name = 'Latin' }
region_class = Class.new(ApplicationRecord) { self.table_name = 'regions' }
region_class.find_or_create_by!(code: 826) do |record|
record.alpha_code = 'GB'
record.name = 'United Kingdom'
end
end
let(:locale) do
prepare_english_reference!
Locale.find_or_create_by!(code: 'en') do |record|
record.language_code = 'en'
record.script_code = 'Latn'
record.region_code = 826
record.variants = ''
record.name = 'English'
end
end
end
+4
ファイルの表示
@@ -1,4 +1,8 @@
module TestRecords
def primary_tag_name_for(tag, language_code)
TagName.find_by!(tag_id: tag.id, language_code:, primary_flg: true)
end
def create_member_user!
User.create!(name: 'spec user',
inheritance_code: SecureRandom.hex(16),
+29 -15
ファイルの表示
@@ -8,7 +8,7 @@ RSpec.describe 'nico:sync' do
end
def create_tag!(name, category:)
Tag.find_or_create_by_tag_name!(name, category:)
Tag.find_or_create_by_tag_name!(Locale.nipponese, name, category:)
end
def link_nico_to_tag!(nico_tag, tag)
@@ -16,17 +16,22 @@ RSpec.describe 'nico:sync' do
end
def create_nico_sanitisation_rules!
TagNameSanitisationRule.create!(priority: 20,
source_pattern: '\\?',
replacement: '_')
rules = [
[20, '\\?', '_'],
[40, '_$', ''],
[45, '^([^:]+\\:)?_', '\\1']]
TagNameSanitisationRule.create!(priority: 40,
source_pattern: '_$',
replacement: '')
priorities = rules.map(&:first)
source_patterns = rules.map { _1[1] }
TagNameSanitisationRule.create!(priority: 45,
source_pattern: '^([^:]+\\:)?_',
replacement: '\\1')
scope = TagNameSanitisationRule.unscoped
scope.where(priority: priorities)
.or(scope.where(source_pattern: source_patterns))
.delete_all
rules.each do |priority, source_pattern, replacement|
TagNameSanitisationRule.create!(priority:, source_pattern:, replacement:)
end
end
it '既存 post を見つけて、nico tag と linked tag を追加し、差分が出たら bot を付ける' do
@@ -64,7 +69,8 @@ RSpec.describe 'nico:sync' do
run_rake_task('nico:sync')
post.reload
active_tag_names = post.tags.joins(:tag_name).pluck('tag_names.name')
active_tag_names = TagName.where(tag_id: post.tags.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name)
expect(active_tag_names).to include('spec_kept')
expect(post.external_tags).to contain_exactly(nico)
@@ -434,7 +440,9 @@ RSpec.describe 'nico:sync' do
external = post.external_tags.sole
expect(external.name).to eq('raw tag[]')
expect(post.tags.map(&:name)).not_to include('bot操作')
expect(TagName.where(tag_id: post.tags.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name))
.not_to include('bot操作')
expect(post.post_versions.order(:version_no).last.tags_json)
.to include('external_tag_id' => external.id)
@@ -446,7 +454,9 @@ RSpec.describe 'nico:sync' do
run_nico_sync_with_tags!([])
}.to change(PostVersion, :count).by(1)
expect(post.reload.external_tags).to be_empty
expect(post.tags.map(&:name)).not_to include('bot操作')
expect(TagName.where(tag_id: post.tags.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name))
.not_to include('bot操作')
end
it 'サニタイズ後の既存外部タグを再利用し、その連携タグを記載する' do
@@ -563,7 +573,9 @@ RSpec.describe 'nico:sync' do
name: 'AAA')
expect(ExternalTag.exists?(platform: :nico, name: 'AAA?')).to be(false)
expect(post.tags.map(&:name)).not_to include('bot操作')
expect(TagName.where(tag_id: post.tags.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name))
.not_to include('bot操作')
expect(post.post_versions.order(:version_no).last.tags_json)
.to include('external_tag_id' => external.id)
@@ -577,7 +589,9 @@ RSpec.describe 'nico:sync' do
}.to change(PostVersion, :count).by(1)
expect(post.reload.external_tags).to be_empty
expect(post.tags.map(&:name)).not_to include('bot操作')
expect(TagName.where(tag_id: post.tags.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name))
.not_to include('bot操作')
end
it 'nico: prefix を含む従来の規則で外部タグ名をサニタイズする' do
+4 -4
ファイルの表示
@@ -6,10 +6,10 @@ RSpec.describe 'post_similarity:calc' do
it 'calculates similarities from active tags only' do
# 必要最低限のデータ
t1 = Tag.create!(name: "t1")
t2 = Tag.create!(name: "t2")
t3 = Tag.create!(name: "t3")
deprecated_tag = Tag.create!(name: 'deprecated', deprecated_at: Time.current)
t1 = create(:tag, primary_name: 't1')
t2 = create(:tag, primary_name: 't2')
t3 = create(:tag, primary_name: 't3')
deprecated_tag = create(:tag, primary_name: 'deprecated', deprecated_at: Time.current)
p1 = Post.create!(url: "https://example.com/1")
p2 = Post.create!(url: "https://example.com/2")
+4 -4
ファイルの表示
@@ -6,10 +6,10 @@ RSpec.describe 'tag_similarity:calc' do
it 'calculates similarities for active tags only' do
# 必要最低限のデータ
t1 = Tag.create!(name: "t1")
t2 = Tag.create!(name: "t2")
t3 = Tag.create!(name: "t3")
deprecated_tag = Tag.create!(name: 'deprecated', deprecated_at: Time.current)
t1 = create(:tag, primary_name: 't1')
t2 = create(:tag, primary_name: 't2')
t3 = create(:tag, primary_name: 't3')
deprecated_tag = create(:tag, primary_name: 'deprecated', deprecated_at: Time.current)
p1 = Post.create!(url: "https://example.com/1")
p2 = Post.create!(url: "https://example.com/2")