diff --git a/backend/spec/db/tag_name_constraints_spec.rb b/backend/spec/db/tag_name_constraints_spec.rb new file mode 100644 index 0000000..02ba449 --- /dev/null +++ b/backend/spec/db/tag_name_constraints_spec.rb @@ -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 diff --git a/backend/spec/db/tag_version_names_spec.rb b/backend/spec/db/tag_version_names_spec.rb new file mode 100644 index 0000000..3c351b4 --- /dev/null +++ b/backend/spec/db/tag_version_names_spec.rb @@ -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 diff --git a/backend/spec/models/language_spec.rb b/backend/spec/models/language_spec.rb index 7f76a80..9a2edcb 100644 --- a/backend/spec/models/language_spec.rb +++ b/backend/spec/models/language_spec.rb @@ -1,8 +1,15 @@ require 'rails_helper' RSpec.describe Language, type: :model do - it 'accepts supported language codes' do - expect(described_class.new(code: 'en', name: 'English')).to be_valid + 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 @@ -13,11 +20,13 @@ RSpec.describe Language, type: :model do } end - it 'does not treat French as a supported language' do - expect { - described_class.create!(code: 'fr', name: 'French') - }.to raise_error { |error| - expect(error).to be_a(ArgumentError).or be_a(ActiveRecord::RecordInvalid) - } + ['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 diff --git a/backend/spec/models/locale_spec.rb b/backend/spec/models/locale_spec.rb index a4d2794..62fbd55 100644 --- a/backend/spec/models/locale_spec.rb +++ b/backend/spec/models/locale_spec.rb @@ -1,18 +1,33 @@ require 'rails_helper' RSpec.describe Locale, type: :model 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' } - end + include_context 'English locale' def create_english_locale! prepare_english_reference! described_class.create!(code: 'en', language_code: 'en', - script_code: 'Latn', name: 'English') + 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 @@ -39,6 +54,7 @@ RSpec.describe Locale, type: :model do 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:, @@ -54,6 +70,7 @@ RSpec.describe Locale, type: :model do 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', @@ -73,6 +90,7 @@ RSpec.describe Locale, type: :model do 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') @@ -88,12 +106,24 @@ RSpec.describe Locale, type: :model do 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', name: 'English') + script_code: 'Latn', region_code: 826, + variants: 'x-rspec', name: 'English') - expect(locale.reload).to have_attributes(code: 'en-x-rspec', language_code: 'en') + 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 diff --git a/backend/spec/models/script_spec.rb b/backend/spec/models/script_spec.rb index ccd91ff..20365c1 100644 --- a/backend/spec/models/script_spec.rb +++ b/backend/spec/models/script_spec.rb @@ -4,17 +4,32 @@ RSpec.describe Script, type: :model do include_context 'English locale' it 'selects locales by script code, not language code' do - expect(described_class.find('Latn').locales).to include(locale) - expect(described_class.find('Jpan').locales).to include(Locale.nipponese) - expect(described_class.find('Jpan').locales).not_to include(locale) + 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.new(code: 'Latn', name: 'Latin')).to be_valid + 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 diff --git a/backend/spec/models/tag_name_sanitisation_rule_spec.rb b/backend/spec/models/tag_name_sanitisation_rule_spec.rb index 0790421..9cebc51 100644 --- a/backend/spec/models/tag_name_sanitisation_rule_spec.rb +++ b/backend/spec/models/tag_name_sanitisation_rule_spec.rb @@ -38,6 +38,8 @@ RSpec.describe TagNameSanitisationRule, type: :model 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') @@ -50,6 +52,8 @@ RSpec.describe TagNameSanitisationRule, type: :model do 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') diff --git a/backend/spec/models/tag_name_spec.rb b/backend/spec/models/tag_name_spec.rb index 8d01412..8239d65 100644 --- a/backend/spec/models/tag_name_spec.rb +++ b/backend/spec/models/tag_name_spec.rb @@ -4,6 +4,8 @@ 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') @@ -24,7 +26,15 @@ RSpec.describe TagName, type: :model do name = create(:tag_name) expect(name.reload).to have_attributes( - language_code: 'ja', script_code: 'Jpan', primary_flg: true, tag_id: nil) + 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 @@ -39,7 +49,7 @@ RSpec.describe TagName, type: :model do expect(tag.reload.tag_name_id).to eq(name.id) end - it 'creates a complete ordinary tag factory under every active language' do + 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| @@ -47,6 +57,13 @@ RSpec.describe TagName, type: :model do 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 @@ -95,6 +112,8 @@ RSpec.describe TagName, type: :model do 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) @@ -113,6 +132,8 @@ RSpec.describe TagName, type: :model do 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') diff --git a/backend/spec/models/tag_spec.rb b/backend/spec/models/tag_spec.rb index 7493d2a..1d08b42 100644 --- a/backend/spec/models/tag_spec.rb +++ b/backend/spec/models/tag_spec.rb @@ -25,17 +25,18 @@ RSpec.describe Tag, type: :model do end describe '.normalise_tags!' do + include_context 'English locale' + it 'canonicalises aliases in the supplied language only' do - Language.find_or_create_by!(code: 'en') { _1.name = 'English' } - Script.find_or_create_by!(code: 'Latn') { _1.name = 'Latin' } + 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.create!( - code: 'en', language_code: 'en', script_code: 'Latn', name: 'English') + english_locale = locale nipponese_alias = create(:tag_name, :alias, name: 'shared_alias', tag: nipponese) english_alias = create(:tag_name, :alias, name: 'shared_alias', @@ -219,6 +220,8 @@ RSpec.describe Tag, type: :model 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) @@ -229,6 +232,8 @@ RSpec.describe Tag, type: :model do 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!( @@ -246,6 +251,8 @@ RSpec.describe Tag, type: :model do 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) @@ -254,10 +261,13 @@ RSpec.describe Tag, type: :model do 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', name: 'English') + 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) @@ -267,6 +277,8 @@ RSpec.describe Tag, type: :model do 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') @@ -282,6 +294,8 @@ RSpec.describe Tag, type: :model do 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 @@ -297,6 +311,8 @@ RSpec.describe Tag, type: :model do 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') @@ -311,6 +327,8 @@ RSpec.describe Tag, type: :model do 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!( @@ -328,6 +346,8 @@ RSpec.describe Tag, type: :model do 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') @@ -342,6 +362,8 @@ RSpec.describe Tag, type: :model do 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) @@ -363,6 +385,21 @@ RSpec.describe Tag, type: :model do 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 '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 @@ -417,6 +454,8 @@ RSpec.describe Tag, type: :model 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, @@ -497,18 +536,15 @@ RSpec.describe Tag, type: :model do .to eq([[1, 'create'], [2, 'discard']]) expect(versions.first).to eq(original_version) expect(versions.last.created_by_user).to eq(user) - expect(versions.last.names).to include( - hash_including('language_code' => 'ja', 'name' => source_name, - 'primary_flg' => true, 'auto_generated' => false), - hash_including('language_code' => 'ja', 'name' => source_alias.name, - 'primary_flg' => false, 'auto_generated' => false)) + 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']) - target_aliases = target_versions.last.names.filter_map { |name| - name['name'] if name['language_code'] == 'ja' && !name['primary_flg'] - } + target_aliases = target_versions.last.names.fetch('ja').fetch('aliases').pluck('name') expect(target_aliases).to contain_exactly('merge_alias', source_name) end diff --git a/backend/spec/models/version_record_spec.rb b/backend/spec/models/version_record_spec.rb index b5cfffc..561c19b 100644 --- a/backend/spec/models/version_record_spec.rb +++ b/backend/spec/models/version_record_spec.rb @@ -12,12 +12,11 @@ RSpec.describe VersionRecord, type: :model do name: primary_tag_name_for(tag, 'ja').name, category: tag.category, aliases: '', - names: [{ language_code: 'ja', name: primary_tag_name_for(tag, 'ja').name, - script_code: 'Jpan', primary_flg: true }], + 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') @@ -32,12 +31,11 @@ RSpec.describe VersionRecord, type: :model do name: primary_tag_name_for(tag, 'ja').name, category: tag.category, aliases: '', - names: [{ language_code: 'ja', name: primary_tag_name_for(tag, 'ja').name, - script_code: 'Jpan', primary_flg: true }], + 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! diff --git a/backend/spec/requests/locale_write_paths_spec.rb b/backend/spec/requests/locale_write_paths_spec.rb index b5eb012..48f2ed1 100644 --- a/backend/spec/requests/locale_write_paths_spec.rb +++ b/backend/spec/requests/locale_write_paths_spec.rb @@ -17,12 +17,51 @@ RSpec.describe 'Locale propagation on write paths', type: :request do 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: '') @@ -33,6 +72,8 @@ RSpec.describe 'Locale propagation on write paths', type: :request do 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) @@ -46,6 +87,8 @@ RSpec.describe 'Locale propagation on write paths', type: :request do 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: []) @@ -59,6 +102,8 @@ RSpec.describe 'Locale propagation on write paths', type: :request do 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') @@ -68,6 +113,8 @@ RSpec.describe 'Locale propagation on write paths', type: :request do 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') @@ -80,6 +127,8 @@ RSpec.describe 'Locale propagation on write paths', type: :request do 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) @@ -92,6 +141,8 @@ RSpec.describe 'Locale propagation on write paths', type: :request do 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( @@ -106,11 +157,11 @@ RSpec.describe 'Locale propagation on write paths', type: :request do end context 'with existing names in both languages' do - let!(:english_name) 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 + 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') @@ -118,6 +169,10 @@ RSpec.describe 'Locale propagation on write paths', type: :request do 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: '' } @@ -131,6 +186,10 @@ RSpec.describe 'Locale propagation on write paths', type: :request do 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) @@ -144,6 +203,10 @@ RSpec.describe 'Locale propagation on write paths', type: :request do 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' } @@ -153,6 +216,10 @@ RSpec.describe 'Locale propagation on write paths', type: :request do end it 'resolves the English parent tag' do + pending '日本語以外の Locale 有効化は別課題で実装する' + locale + english_tag + nipponese_tag child = create(:tag) put "/tags/#{ child.id }", params: { @@ -165,6 +232,10 @@ RSpec.describe 'Locale propagation on write paths', type: :request do 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) @@ -177,12 +248,14 @@ RSpec.describe 'Locale propagation on write paths', type: :request do end context 'when renaming or updating aliases in English' do - let!(:english_name) 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) } + 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) @@ -196,6 +269,8 @@ RSpec.describe 'Locale propagation on write paths', type: :request do 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) @@ -228,6 +303,8 @@ RSpec.describe 'Locale propagation on write paths', type: :request do 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' } @@ -240,6 +317,8 @@ RSpec.describe 'Locale propagation on write paths', type: :request do 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, @@ -255,6 +334,8 @@ RSpec.describe 'Locale propagation on write paths', type: :request do 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) diff --git a/backend/spec/requests/tag_versions_spec.rb b/backend/spec/requests/tag_versions_spec.rb index eebeca1..f105773 100644 --- a/backend/spec/requests/tag_versions_spec.rb +++ b/backend/spec/requests/tag_versions_spec.rb @@ -36,12 +36,10 @@ RSpec.describe 'TagVersions API', type: :request do category: category, deprecated_at: deprecated_at, aliases: Array(aliases).join(' '), - names: [{ language_code: 'ja', name:, script_code: 'Jpan', - primary_flg: true, - auto_generated: false }] + Array(aliases).map { |alias_name| - { language_code: 'ja', name: alias_name, script_code: 'Jpan', - primary_flg: false, auto_generated: false } - }, + 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) diff --git a/backend/spec/requests/tags_spec.rb b/backend/spec/requests/tags_spec.rb index be64b53..c8bfd54 100644 --- a/backend/spec/requests/tags_spec.rb +++ b/backend/spec/requests/tags_spec.rb @@ -19,15 +19,11 @@ RSpec.describe 'Tags API', type: :request do end def version_primary_name(version, language_code) - version.names.find { |name| - name['language_code'] == language_code && name['primary_flg'] - }.fetch('name') + version.names.fetch(language_code).fetch('name') end def version_alias_names(version, language_code) - version.names.filter_map { |name| - name['name'] if name['language_code'] == language_code && !name['primary_flg'] - } + version.names.fetch(language_code).fetch('aliases').pluck('name') end def dummy_material_upload(filename: 'dummy.png', type: 'image/png', body: 'dummy') diff --git a/backend/spec/services/post_bulk_creator_spec.rb b/backend/spec/services/post_bulk_creator_spec.rb index 9cd5d7b..d5167bb 100644 --- a/backend/spec/services/post_bulk_creator_spec.rb +++ b/backend/spec/services/post_bulk_creator_spec.rb @@ -4,6 +4,7 @@ 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) @@ -65,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 diff --git a/backend/spec/services/post_create_plan_spec.rb b/backend/spec/services/post_create_plan_spec.rb index c5521b3..7d190ab 100644 --- a/backend/spec/services/post_create_plan_spec.rb +++ b/backend/spec/services/post_create_plan_spec.rb @@ -14,6 +14,8 @@ RSpec.describe PostCreatePlan 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) @@ -39,6 +41,8 @@ RSpec.describe PostCreatePlan do 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') diff --git a/backend/spec/services/post_creator_spec.rb b/backend/spec/services/post_creator_spec.rb index 2fa36cc..624577d 100644 --- a/backend/spec/services/post_creator_spec.rb +++ b/backend/spec/services/post_creator_spec.rb @@ -81,6 +81,8 @@ RSpec.describe PostCreator do [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', diff --git a/backend/spec/services/tag_version_recorder_spec.rb b/backend/spec/services/tag_version_recorder_spec.rb index 9e62183..e403ef6 100644 --- a/backend/spec/services/tag_version_recorder_spec.rb +++ b/backend/spec/services/tag_version_recorder_spec.rb @@ -1,45 +1,96 @@ require 'rails_helper' RSpec.describe TagVersionRecorder do - let(:user) { create_member_user! } - let!(:tag) { create(:tag, primary_name: '日本語名') } include_context 'English locale' + let(:user) { create_member_user! } + let!(:tag) { create(:tag, primary_name: '日本語名') } + before do - create(:tag_name, :alias, tag:, name: '別名') + 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 every language primary name and alias deterministically' do + 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.names).to eq([ - { 'language_code' => 'en', 'name' => 'English name', 'script_code' => 'Latn', - 'primary_flg' => true, 'auto_generated' => false }, - { 'language_code' => 'en', 'name' => 'English alias', 'script_code' => 'Latn', - 'primary_flg' => false, 'auto_generated' => false }, - { 'language_code' => 'ja', 'name' => '日本語名', 'script_code' => 'Jpan', - 'primary_flg' => true, 'auto_generated' => false }, - { 'language_code' => 'ja', 'name' => '別名', 'script_code' => 'Jpan', - 'primary_flg' => false, 'auto_generated' => false } - ]) + expect(version.reload.names).to eq('ja' => nipponese_snapshot) end - it 'creates a version for an English-only name change and preserves prior names' do + it 'does not create a version for an unchanged Nipponese snapshot' do original = described_class.record!(tag:, event_type: :create, created_by_user: user) - TagName.find_by!(tag:, language_code: 'en', primary_flg: true).update!(name: 'Renamed') + + 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).to include( - hash_including('language_code' => 'en', 'name' => 'English name')) - expect(updated.names).to include(hash_including('language_code' => 'en', 'name' => 'Renamed')) + 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) diff --git a/backend/spec/support/locale_context.rb b/backend/spec/support/locale_context.rb index 92ef3db..3241f11 100644 --- a/backend/spec/support/locale_context.rb +++ b/backend/spec/support/locale_context.rb @@ -1,10 +1,21 @@ RSpec.shared_context 'English locale' do - let!(: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