このコミットが含まれているのは:
2026-09-26 20:23:11 +09:00
コミット 5abb6cca98
39個のファイルの変更、1005行の追加、377行の削除
+3 -1
ファイルの表示
@@ -213,6 +213,8 @@ class PostsController < ApplicationController
return head :unauthorized unless current_user return head :unauthorized unless current_user
return head :forbidden unless current_user.gte_member? return head :forbidden unless current_user.gte_member?
locale = resolve_locale!
preflight = PostCreatePreflight.new( preflight = PostCreatePreflight.new(
attributes: post_create_attributes, attributes: post_create_attributes,
thumbnail: params[:thumbnail], thumbnail: params[:thumbnail],
@@ -242,7 +244,7 @@ class PostsController < ApplicationController
:post_tag_specs, :post_tag_specs,
:tag_sections, :tag_sections,
:normalised_parent_post_ids).symbolize_keys).merge( :normalised_parent_post_ids).symbolize_keys).merge(
thumbnail: params[:thumbnail])).create! thumbnail: params[:thumbnail])).create!(locale)
post.reload post.reload
render json: PostRepr.base(post), status: :created render json: PostRepr.base(post), status: :created
+4 -4
ファイルの表示
@@ -6,7 +6,7 @@ class PostBulkCreator
@host = host @host = host
end end
def run def run locale
results = Array.new(@posts.length) results = Array.new(@posts.length)
mutex = Mutex.new mutex = Mutex.new
next_index = 0 next_index = 0
@@ -27,7 +27,7 @@ class PostBulkCreator
break if index >= @posts.length break if index >= @posts.length
attributes = @posts[index] attributes = @posts[index]
results[index] = create_row(actor, attributes, index) results[index] = create_row(locale, actor, attributes, index)
rescue StandardError => e rescue StandardError => e
Rails.logger.error( Rails.logger.error(
"post_bulk_creator_worker_failure #{ { error: e.class.name, "post_bulk_creator_worker_failure #{ { error: e.class.name,
@@ -61,7 +61,7 @@ class PostBulkCreator
private private
def create_row actor, attributes, index def create_row locale, actor, attributes, index
preflight = preflight =
PostCreatePreflight.new( PostCreatePreflight.new(
attributes: attributes, attributes: attributes,
@@ -76,7 +76,7 @@ class PostBulkCreator
post = PostCreator.new( post = PostCreator.new(
actor: actor, actor: actor,
attributes: normalised_attributes(attributes, preflight, index)).create! attributes: normalised_attributes(attributes, preflight, index)).create!(locale)
result = { result = {
status: 'created', status: 'created',
post: { id: post.id } } post: { id: post.id } }
+1 -1
ファイルの表示
@@ -9,7 +9,7 @@ class PostCreator
@field_warnings = { } @field_warnings = { }
end end
def create!(locale) def create! locale
thumbnail_attachment = prepare_thumbnail_attachment thumbnail_attachment = prepare_thumbnail_attachment
post = Post.new(title: @attributes[:title].presence, post = Post.new(title: @attributes[:title].presence,
url: @attributes[:url], url: @attributes[:url],
生成ファイル
+40 -2
ファイルの表示
@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do ActiveRecord::Schema[8.0].define(version: 2026_09_22_030000) do
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "record_type", null: false t.string "record_type", null: false
@@ -138,6 +138,24 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
t.index ["ip_address"], name: "index_ip_addresses_on_ip_address", unique: true t.index ["ip_address"], name: "index_ip_addresses_on_ip_address", unique: true
end end
create_table "languages", primary_key: "code", id: { type: :string, limit: 16 }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false
t.datetime "deprecated_at"
t.datetime "created_at", null: false
t.index ["deprecated_at"], name: "index_languages_on_deprecated_at"
end
create_table "locales", primary_key: "code", id: { type: :string, limit: 32 }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "language_code", limit: 16, null: false
t.string "script_code", limit: 4, null: false
t.string "name", null: false
t.datetime "deprecated_at"
t.datetime "created_at", null: false
t.index ["deprecated_at"], name: "index_locales_on_deprecated_at"
t.index ["language_code"], name: "index_locales_on_language_code"
t.index ["script_code"], name: "fk_rails_0b74ce96a8"
end
create_table "material_export_items", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| create_table "material_export_items", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "material_id", null: false t.bigint "material_id", null: false
t.string "profile", default: "legacy_drive", null: false t.string "profile", default: "legacy_drive", null: false
@@ -388,6 +406,13 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
t.check_constraint "`version_no` > 0", name: "chk_posts_version_no_positive" t.check_constraint "`version_no` > 0", name: "chk_posts_version_no_positive"
end end
create_table "scripts", primary_key: "code", id: { type: :string, limit: 4 }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false
t.datetime "deprecated_at"
t.datetime "created_at", null: false
t.index ["deprecated_at"], name: "index_scripts_on_deprecated_at"
end
create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "user_id", null: false t.bigint "user_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
@@ -420,12 +445,20 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
end end
create_table "tag_names", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| create_table "tag_names", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "tag_id"
t.string "language_code", limit: 16, null: false
t.string "name", null: false t.string "name", null: false
t.string "script_code", limit: 4, null: false
t.boolean "primary_flg", null: false
t.bigint "canonical_id" t.bigint "canonical_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.virtual "primary_tag_id", type: :bigint, as: "(case when `primary_flg` then `tag_id` else NULL end)"
t.index ["canonical_id"], name: "index_tag_names_on_canonical_id" t.index ["canonical_id"], name: "index_tag_names_on_canonical_id"
t.index ["name"], name: "index_tag_names_on_name", unique: true t.index ["language_code", "name"], name: "index_tag_names_on_language_code_and_name", unique: true
t.index ["primary_tag_id", "language_code"], name: "index_tag_names_on_primary_tag_id_and_language_code", unique: true
t.index ["script_code"], name: "fk_rails_dd783b3d1c"
t.index ["tag_id"], name: "index_tag_names_on_tag_id"
end end
create_table "tag_similarities", primary_key: ["tag_id", "target_tag_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| create_table "tag_similarities", primary_key: ["tag_id", "target_tag_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
@@ -701,6 +734,8 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
add_foreign_key "gekanator_question_suggestions", "users" add_foreign_key "gekanator_question_suggestions", "users"
add_foreign_key "gekanator_questions", "gekanator_question_suggestions" add_foreign_key "gekanator_questions", "gekanator_question_suggestions"
add_foreign_key "gekanator_questions", "users", column: "created_by_id" add_foreign_key "gekanator_questions", "users", column: "created_by_id"
add_foreign_key "locales", "languages", column: "language_code", primary_key: "code"
add_foreign_key "locales", "scripts", column: "script_code", primary_key: "code"
add_foreign_key "material_export_items", "materials" add_foreign_key "material_export_items", "materials"
add_foreign_key "material_export_items", "users", column: "created_by_user_id" add_foreign_key "material_export_items", "users", column: "created_by_user_id"
add_foreign_key "material_import_blocks", "users", column: "created_by_user_id" add_foreign_key "material_import_blocks", "users", column: "created_by_user_id"
@@ -734,7 +769,10 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_21_230000) do
add_foreign_key "settings", "users" add_foreign_key "settings", "users"
add_foreign_key "tag_implications", "tags" add_foreign_key "tag_implications", "tags"
add_foreign_key "tag_implications", "tags", column: "parent_tag_id" add_foreign_key "tag_implications", "tags", column: "parent_tag_id"
add_foreign_key "tag_names", "languages", column: "language_code", primary_key: "code"
add_foreign_key "tag_names", "scripts", column: "script_code", primary_key: "code"
add_foreign_key "tag_names", "tag_names", column: "canonical_id" add_foreign_key "tag_names", "tag_names", column: "canonical_id"
add_foreign_key "tag_names", "tags"
add_foreign_key "tag_similarities", "tags" add_foreign_key "tag_similarities", "tags"
add_foreign_key "tag_similarities", "tags", column: "target_tag_id" add_foreign_key "tag_similarities", "tags", column: "target_tag_id"
add_foreign_key "tag_versions", "users", column: "created_by_user_id" add_foreign_key "tag_versions", "users", column: "created_by_user_id"
+17 -1
ファイルの表示
@@ -1,5 +1,21 @@
FactoryBot.define do FactoryBot.define do
factory :tag_name do factory :tag_name do
name { "tag-#{SecureRandom.hex(4)}" } language_code { 'ja' }
script_code { 'Jpan' }
primary_flg { true }
sequence(:name) { |number| "tag-#{ SecureRandom.hex(4) }-#{ number }" }
trait :alias do
primary_flg { false }
after(:create) do |tag_name|
owner = tag_name.canonical&.tag
raise ArgumentError, 'alias requires a canonical tag' unless owner
# The alias validation still rejects tag_id; its contract is tested separately.
tag_name.update_columns(tag_id: owner.id)
tag_name.association(:tag).reset
end
end
end end
end end
+5
ファイルの表示
@@ -6,10 +6,15 @@ FactoryBot.define do
category { :general } category { :general }
post_count { 0 } post_count { 0 }
association :tag_name association :tag_name
after(:build) do |tag, evaluator| after(:build) do |tag, evaluator|
tag.name = evaluator.name if evaluator.name.present? tag.name = evaluator.name if evaluator.name.present?
end end
after(:create) do |tag|
tag.tag_name.update!(tag:) if tag.tag_name.tag_id != tag.id
end
end end
end end
+69
ファイルの表示
@@ -0,0 +1,69 @@
require 'rails_helper'
RSpec.describe Locale, type: :model do
def prepare_french_reference!
Language.find_or_create_by!(code: 'fr') { _1.name = 'French' }
Script.find_or_create_by!(code: 'Latn') { _1.name = 'Latin' }
end
def create_french_locale!
prepare_french_reference!
described_class.create!(code: 'fr', language_code: 'fr',
script_code: 'Latn', name: 'French')
end
it 'generates a primary name for every existing tag using the name generator' do
first = create(:tag, name: 'first_existing_tag')
second = create(:tag, 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, first.name)
.and_return('name_from_generator')
locale = create_french_locale!
expect(TagName).to have_received(:generate_name).with(locale, first, first.name)
expect(TagName).to have_received(:generate_name).with(locale, second, second.name)
expect(first.tag_names.find_by!(language_code: 'fr', primary_flg: true))
.to have_attributes(name: 'name_from_generator', tag_id: first.id,
language_code: 'fr', script_code: 'Latn',
primary_flg: true, canonical_id: nil)
expect(second.tag_names.find_by!(language_code: 'fr', primary_flg: true))
.to have_attributes(tag_id: second.id, language_code: 'fr',
script_code: 'Latn', primary_flg: true, canonical_id: nil)
expect(TagName.where(language_code: 'fr', 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
prepare_french_reference!
tag = create(:tag, name: 'already_named')
existing = create(:tag_name, name: 'nom_existant', tag:,
language_code: 'fr', script_code: 'Latn')
create_french_locale!
expect(TagName.where(tag_id: tag.id, language_code: 'fr', primary_flg: true))
.to contain_exactly(existing)
expect(existing.reload).to have_attributes(
tag_id: tag.id, language_code: 'fr', script_code: 'Latn', primary_flg: true)
end
it 'creates a primary name when the language has only an alias' do
prepare_french_reference!
tag = create(:tag, name: 'alias_only_tag')
alias_name = create(:tag_name, :alias, name: 'alias_fr',
canonical: tag.tag_name,
language_code: 'fr', script_code: 'Latn')
create_french_locale!
expect(alias_name.reload).to have_attributes(
canonical_id: tag.tag_name_id, tag_id: tag.id,
language_code: 'fr', script_code: 'Latn', primary_flg: false)
generated = TagName.find_by!(tag_id: tag.id, language_code: 'fr', primary_flg: true)
expect(generated).to have_attributes(tag_id: tag.id, language_code: 'fr',
script_code: 'Latn', primary_flg: true)
expect(TagName.where(tag_id: tag.id, language_code: 'fr', primary_flg: true).count).to eq(1)
end
end
+1 -1
ファイルの表示
@@ -2,7 +2,7 @@ require 'rails_helper'
RSpec.describe MaterialExportItem, type: :model do RSpec.describe MaterialExportItem, type: :model do
let(:user) { create(:user, :member) } let(:user) { create(:user, :member) }
let(:tag) { Tag.create!(tag_name: TagName.create!(name: 'export_item'), category: :material) } let(:tag) { create(:tag, name: 'export_item', category: :material) }
let(:material) do let(:material) do
Material.create!(tag:, url: 'https://example.com/material', Material.create!(tag:, url: 'https://example.com/material',
created_by_user: user, updated_by_user: user) created_by_user: user, updated_by_user: user)
+2 -2
ファイルの表示
@@ -1,8 +1,8 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe PostVersion, type: :model do RSpec.describe PostVersion, type: :model do
let!(:tag_name) { TagName.create!(name: 'post_version_spec_tag') } let!(:tag_name) { create(:tag_name, name: 'post_version_spec_tag') }
let!(:tag) { Tag.create!(tag_name: tag_name, category: :general) } let!(:tag) { create(:tag, tag_name: tag_name, category: :general) }
let!(:post_record) do let!(:post_record) do
Post.create!(title: 'spec post', url: 'https://example.com/post-version-spec').tap do |post| Post.create!(title: 'spec post', url: 'https://example.com/post-version-spec').tap do |post|
+42 -10
ファイルの表示
@@ -34,9 +34,41 @@ RSpec.describe TagNameSanitisationRule, type: :model do
described_class.create!(priority: 10, source_pattern: '_', replacement: '') described_class.create!(priority: 10, source_pattern: '_', replacement: '')
end 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
japanese = 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(japanese.reload).to have_attributes(name: 'foobar', language_code: 'ja')
end
it 'renames without merging tags or deleting either language identity' do
japanese = create(:tag, name: 'foobar')
english_name = create(:tag_name, name: 'temporary',
language_code: 'en', script_code: 'Latn')
english = create(:tag, 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(japanese.reload.tag_name).to have_attributes(
name: 'foobar', language_code: 'ja', tag_id: japanese.id)
expect(english.reload.tag_name_id).to eq(english_name.id)
end
end
context 'when no conflicting tag_name exists' do context 'when no conflicting tag_name exists' do
let!(:tag_name) 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) tn.update_columns(name: 'foo_bar', updated_at: Time.current)
end end
end end
@@ -48,9 +80,9 @@ RSpec.describe TagNameSanitisationRule, type: :model do
end end
context 'when a conflicting canonical tag_name exists' do context 'when a conflicting canonical tag_name exists' do
let!(:existing) { TagName.create!(name: 'foobar') } let!(:existing) { create(:tag_name, name: 'foobar') }
let!(:source) do 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) tn.update_columns(name: 'foo_bar', updated_at: Time.current)
end end
end end
@@ -63,7 +95,7 @@ RSpec.describe TagNameSanitisationRule, type: :model do
end end
context 'when the source tag_name has a tag and the existing one has no tag' do context 'when the source tag_name has a tag and the existing one has no tag' do
let!(:existing) { TagName.create!(name: 'foobar') } let!(:existing) { create(:tag_name, name: 'foobar') }
let!(:source_tag) { create(:tag, name: 'tmp', category: :general) } let!(:source_tag) { create(:tag, name: 'tmp', category: :general) }
let!(:source_tag_name_id) { source_tag.tag_name_id } let!(:source_tag_name_id) { source_tag.tag_name_id }
@@ -82,10 +114,10 @@ RSpec.describe TagNameSanitisationRule, type: :model do
context 'when the sanitised name is an alias of an existing tag' do context 'when the sanitised name is an alias of an existing tag' do
let!(:existing_tag) { create(:tag) } let!(:existing_tag) { create(:tag) }
let!(:alias_name) do let!(:alias_name) do
TagName.create!(name: 'foobar', canonical: existing_tag.tag_name) create(:tag_name, :alias, name: 'foobar', canonical: existing_tag.tag_name)
end end
let!(:source) do 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) tn.update_columns(name: 'foo_bar', updated_at: Time.current)
end end
end end
@@ -100,11 +132,11 @@ RSpec.describe TagNameSanitisationRule, type: :model do
end end
context 'when both source and existing tag_names have tags' do context 'when both source and existing tag_names have tags' do
let!(:existing_tn) { TagName.create!(name: 'foobar') } let!(:existing_tn) { create(:tag_name, name: 'foobar') }
let!(:existing_tag) { Tag.create!(tag_name: existing_tn, category: :general) } let!(:existing_tag) { create(:tag, tag_name: existing_tn, category: :general) }
let!(:source_tn) { TagName.create!(name: 'tmp') } let!(:source_tn) { create(:tag_name, name: 'tmp') }
let!(:source_tag) { Tag.create!(tag_name: source_tn, category: :general) } let!(:source_tag) { create(:tag, tag_name: source_tn, category: :general) }
let!(:source_tag_name_id) { source_tn.id } let!(:source_tag_name_id) { source_tn.id }
before do before do
+73
ファイルの表示
@@ -0,0 +1,73 @@
require 'rails_helper'
RSpec.describe TagName, type: :model do
include_context 'English locale'
it 'allows the same name in different languages' do
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 Japanese 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, tag_id: nil)
end
it 'persists both sides of the representative tag factory association' do
tag = create(:tag)
expect(tag.reload.tag_name.reload.tag_id).to eq(tag.id)
expect(tag.tag_name.tag).to eq(tag)
end
it 'accepts an alias owned by the canonical tag' do
tag = create(:tag)
alias_name = build(:tag_name, name: 'valid_alias', canonical: tag.tag_name,
tag:, primary_flg: false)
expect(alias_name).to have_attributes(
canonical_id: tag.tag_name_id, tag_id: tag.id,
primary_flg: false, language_code: 'ja')
expect(alias_name).to be_valid
end
describe '.canonicalise' do
it 'resolves only aliases in the requested language' do
japanese = create(:tag, name: 'japanese_canonical')
english_name = create(:tag_name, name: 'english_canonical',
language_code: 'en', script_code: 'Latn')
english = create(:tag, tag_name: english_name)
japanese_alias = create(:tag_name, :alias, name: 'shared_alias',
canonical: japanese.tag_name)
english_alias = create(:tag_name, :alias, name: 'temporary_alias',
canonical: english_name,
language_code: 'en', script_code: 'Latn')
english_alias.update_columns(name: 'shared_alias')
expect(japanese_alias.reload).to have_attributes(
canonical_id: japanese.tag_name_id, tag_id: japanese.id,
primary_flg: false, language_code: 'ja')
expect(english_alias.reload).to have_attributes(
canonical_id: english_name.id, 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(['japanese_canonical'])
end
end
end
+143 -57
ファイルの表示
@@ -9,7 +9,7 @@ RSpec.describe Tag, type: :model do
expect(tag).to be_invalid expect(tag).to be_invalid
expect(tag.errors[:name]).to be_present expect(tag.errors[:name]).to be_present
expect { expect {
described_class.normalise_tags!([name]) described_class.normalise_tags!(Locale.nipponese, [name])
}.to raise_error(Tag::NicoTagNormalisationError) }.to raise_error(Tag::NicoTagNormalisationError)
end end
end end
@@ -25,19 +25,49 @@ RSpec.describe Tag, type: :model do
end end
describe '.normalise_tags!' do describe '.normalise_tags!' do
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' }
Locale.insert_all!([
{ code: 'en', language_code: 'en', script_code: 'Latn',
name: 'English', created_at: Time.current }]) unless Locale.exists?(code: 'en')
english_locale = Locale.find('en')
japanese = create(:tag, name: 'japanese_canonical')
english_name = create(:tag_name, name: 'english_canonical',
language_code: 'en', script_code: 'Latn')
english = create(:tag, tag_name: english_name)
japanese_alias = create(:tag_name, :alias, name: 'shared_alias',
canonical: japanese.tag_name)
english_alias = create(:tag_name, :alias, name: 'temporary_alias',
canonical: english_name,
language_code: 'en', script_code: 'Latn')
english_alias.update_columns(name: 'shared_alias')
expect(japanese_alias.reload).to have_attributes(
canonical_id: japanese.tag_name_id, tag_id: japanese.id,
primary_flg: false, language_code: 'ja')
expect(english_alias.reload).to have_attributes(
canonical_id: english_name.id, 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([japanese])
end
it 'rejects deprecated tags when deny_deprecated is enabled' do it 'rejects deprecated tags when deny_deprecated is enabled' do
tag_name = TagName.create!(name: 'normalise deprecated tag') tag_name = create(:tag_name, name: 'normalise deprecated tag')
deprecated_tag = Tag.create!( deprecated_tag = create(:tag,
tag_name:, tag_name:,
category: :general, category: :general,
deprecated_at: 1.day.from_now deprecated_at: 1.day.from_now)
)
expect { expect {
described_class.normalise_tags!( described_class.normalise_tags!(Locale.nipponese,
[deprecated_tag.name], [deprecated_tag.name],
deny_deprecated: true deny_deprecated: true)
)
}.to raise_error(Tag::DeprecatedTagNormalisationError) { |error| }.to raise_error(Tag::DeprecatedTagNormalisationError) { |error|
expect(error.tag_names).to eq([deprecated_tag.name]) expect(error.tag_names).to eq([deprecated_tag.name])
} }
@@ -45,88 +75,79 @@ RSpec.describe Tag, type: :model do
it 'rejects invalid section literals instead of treating them as zero' do it 'rejects invalid section literals instead of treating them as zero' do
expect { expect {
described_class.normalise_tags!( described_class.normalise_tags!(Locale.nipponese,
['normalise_invalid_section[1:aa-2:00]'], ['normalise_invalid_section[1:aa-2:00]'],
with_sections: true with_sections: true)
)
}.to raise_error(Tag::SectionLiteralParseError) }.to raise_error(Tag::SectionLiteralParseError)
end end
it 'parses open-ended section literals' do it 'parses open-ended section literals' do
result = described_class.normalise_tags!( result = described_class.normalise_tags!(Locale.nipponese,
['伊地知ニジカ[1:00-]'], ['伊地知ニジカ[1:00-]'],
with_sections: true with_sections: true)
)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' } tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]]) expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
end end
it 'parses omitted begin as zero' do it 'parses omitted begin as zero' do
result = described_class.normalise_tags!( result = described_class.normalise_tags!(Locale.nipponese,
['伊地知ニジカ[-1:00]'], ['伊地知ニジカ[-1:00]'],
with_sections: true with_sections: true)
)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' } tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections).fetch(tag.id)).to eq([[0, 60_000]]) expect(result.fetch(:sections).fetch(tag.id)).to eq([[0, 60_000]])
end end
it 'treats fully open section literals as plain tags' do it 'treats fully open section literals as plain tags' do
result = described_class.normalise_tags!( result = described_class.normalise_tags!(Locale.nipponese,
['伊地知ニジカ[-]'], ['伊地知ニジカ[-]'],
with_sections: true with_sections: true)
)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' } tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections)[tag.id]).to be_nil expect(result.fetch(:sections)[tag.id]).to be_nil
end end
it 'treats [0:00-] as a plain tag' do it 'treats [0:00-] as a plain tag' do
result = described_class.normalise_tags!( result = described_class.normalise_tags!(Locale.nipponese,
['伊地知ニジカ[0:00-]'], ['伊地知ニジカ[0:00-]'],
with_sections: true with_sections: true)
)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' } tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections)[tag.id]).to be_nil expect(result.fetch(:sections)[tag.id]).to be_nil
end end
it 'expands zero-width sections to one millisecond' do it 'expands zero-width sections to one millisecond' do
result = described_class.normalise_tags!( result = described_class.normalise_tags!(Locale.nipponese,
['伊地知ニジカ[1:00-1:00]'], ['伊地知ニジカ[1:00-1:00]'],
with_sections: true with_sections: true)
)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' } tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, 60_001]]) expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, 60_001]])
end end
it 'swaps reversed section boundaries' do it 'swaps reversed section boundaries' do
result = described_class.normalise_tags!( result = described_class.normalise_tags!(Locale.nipponese,
['伊地知ニジカ[2:00-1:00]'], ['伊地知ニジカ[2:00-1:00]'],
with_sections: true with_sections: true)
)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' } tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, 120_000]]) expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, 120_000]])
end end
it 'merges open-ended sections over later bounded sections' do it 'merges open-ended sections over later bounded sections' do
result = described_class.normalise_tags!( result = described_class.normalise_tags!(Locale.nipponese,
['伊地知ニジカ[1:00-][2:00-3:00]'], ['伊地知ニジカ[1:00-][2:00-3:00]'],
with_sections: true with_sections: true)
)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' } tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]]) expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
end end
it 'merges adjacent bounded and open-ended sections' do it 'merges adjacent bounded and open-ended sections' do
result = described_class.normalise_tags!( result = described_class.normalise_tags!(Locale.nipponese,
['伊地知ニジカ[1:00-3:00][3:00-]'], ['伊地知ニジカ[1:00-3:00][3:00-]'],
with_sections: true with_sections: true)
)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' } tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]]) expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
@@ -183,11 +204,65 @@ RSpec.describe Tag, type: :model do
end end
describe '.find_or_create_by_tag_name!' do 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
tag = described_class.find_or_create_by_tag_name!(
locale, 'english_name', category: :character)
expect(tag.tag_name.reload).to have_attributes(
language_code: 'en', script_code: 'Latn',
primary_flg: true, tag_id: tag.id)
expect(tag.category).to eq('character')
end
it 'creates separate tag identities for the same name in different languages' do
japanese = 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(japanese)
expect(english.tag_name.language_code).to eq('en')
expect(japanese.tag_name.language_code).to eq('ja')
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(japanese)
end
it 'preserves the V1 representative when looking up another primary language' do
tag = create(:tag, name: '日本語代表名')
representative_id = tag.tag_name_id
english = create(:tag_name, name: 'english_primary', tag:,
language_code: 'en', script_code: 'Latn')
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
tags = described_class.normalise_tags!(
locale, ['character:normalised_english'],
with_tagme: false, with_no_deerjikist: false)
expect(tags.length).to eq(1)
expect(tags.first.tag_name).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 it 'creates a tag and name with the requested category after stripping whitespace' do
tag = nil tag = nil
expect { 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) ' lookup_new ', category: :character)
}.to change(Tag, :count).by(1).and change(TagName, :count).by(1) }.to change(Tag, :count).by(1).and change(TagName, :count).by(1)
@@ -197,27 +272,38 @@ RSpec.describe Tag, type: :model do
it 'reuses the canonical tag for an alias without changing its category' do it 'reuses the canonical tag for an alias without changing its category' do
tag = create(:tag, category: :character) 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', canonical: tag.tag_name)
expect(alias_name.reload).to have_attributes(
canonical_id: tag.tag_name_id, tag_id: tag.id,
primary_flg: false, language_code: 'ja')
found = nil found = nil
expect { 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) alias_name.name, category: :general)
}.to change(Tag, :count).by(0).and change(TagName, :count).by(0) }.to change(Tag, :count).by(0).and change(TagName, :count).by(0)
expect(found).to eq(tag) expect(found).to eq(tag)
expect(found.category).to eq('character') expect(found.category).to eq('character')
expect(tag.reload.tag_name_id).to eq(representative_id)
end end
it 'creates a tag for an existing canonical name reached through an alias' do it 'reuses the canonical tag through another alias' do
canonical = create(:tag_name) tag = create(:tag)
alias_name = TagName.create!(name: 'lookup_alias', canonical:) canonical = tag.tag_name
tag = nil alias_name = create(:tag_name, :alias, name: 'lookup_alias', canonical:)
other_alias = create(:tag_name, :alias, name: 'lookup_other_alias', canonical:)
expect([alias_name.reload, other_alias.reload]).to all(have_attributes(
canonical_id: canonical.id, tag_id: tag.id,
primary_flg: false, language_code: 'ja'))
expect { 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) 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(tag.tag_name).to eq(canonical)
end end
@@ -277,7 +363,7 @@ RSpec.describe Tag, type: :model do
it 'keeps source history and records the new target alias after deleting the source' do it 'keeps source history and records the new target alias after deleting the source' do
user = create_member_user! user = create_member_user!
source_name = source_tag.name source_name = source_tag.name
source_alias = TagName.create!(name: 'merge_alias', canonical: source_tag_name) source_alias = create(:tag_name, :alias, name: 'merge_alias', canonical: source_tag_name)
TagVersioning.ensure_snapshot!(source_tag, created_by_user: user) TagVersioning.ensure_snapshot!(source_tag, created_by_user: user)
original_version = source_tag.tag_versions.first original_version = source_tag.tag_versions.first
+5 -6
ファイルの表示
@@ -898,12 +898,11 @@ RSpec.describe 'Gekanator learning API', type: :request do
describe 'GET /gekanator/questions' do describe 'GET /gekanator/questions' do
it 'omits questions for deprecated tags' do it 'omits questions for deprecated tags' do
active_tag = Tag.create!(name: 'active_question_tag', category: :general) active_tag = create(:tag, name: 'active_question_tag', category: :general)
deprecated_tag = Tag.create!( deprecated_tag = create(:tag,
name: 'deprecated_question_tag', name: 'deprecated_question_tag',
category: :general, category: :general,
deprecated_at: Time.current deprecated_at: Time.current)
)
[active_tag, deprecated_tag].each do |question_tag| [active_tag, deprecated_tag].each do |question_tag|
GekanatorQuestion.create!( GekanatorQuestion.create!(
+5 -6
ファイルの表示
@@ -4,12 +4,11 @@ require 'rails_helper'
RSpec.describe 'Gekanator posts API', type: :request do RSpec.describe 'Gekanator posts API', type: :request do
describe 'GET /gekanator/posts' do describe 'GET /gekanator/posts' do
it 'omits deprecated tags and returns the stored similarity cosine' do it 'omits deprecated tags and returns the stored similarity cosine' do
active_tag = Tag.create!(name: 'active tag', category: :general) active_tag = create(:tag, name: 'active tag', category: :general)
deprecated_tag = Tag.create!( deprecated_tag = create(:tag,
name: 'deprecated tag', name: 'deprecated tag',
category: :general, category: :general,
deprecated_at: Time.current deprecated_at: Time.current)
)
post_record = Post.create!(title: 'source', url: 'https://example.com/source') post_record = Post.create!(title: 'source', url: 'https://example.com/source')
target_post = Post.create!(title: 'target', url: 'https://example.com/target') target_post = Post.create!(title: 'target', url: 'https://example.com/target')
+232
ファイルの表示
@@ -0,0 +1,232 @@
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
expect(tag_name.tag.tag_name_id).to eq(tag_name.id)
tag_name.tag
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 Japanese fallback' do
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 Japanese fallback' do
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
# 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
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
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
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
tag = create(:tag)
put "/tags/#{ tag.id }", params: locale_params.merge(
name: tag.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, tag_name: english_name, category: :material) }
let!(:japanese_tag) do
create(:tag, name: 'temporary_japanese', category: :general).tap do |tag|
# Isolate lookup from the separately tested uniqueness validation.
tag.tag_name.update_columns(name: 'shared_name')
end
end
it 'plans and creates the post using the English identity and category' do
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(japanese_tag)
expect(english_tag.reload.category).to eq('material')
expect(japanese_tag.reload.category).to eq('general')
end
it 'updates the post using the English identity' do
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(japanese_tag)
end
it 'resolves the English material tag' do
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: japanese_tag)).to be_empty
end
it 'resolves the English parent tag' do
child = create(:tag)
put "/tags/#{ child.id }", params: {
locale: 'en', name: child.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
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, tag_name: english_name) }
it 'allows a rename to a name already used in Japanese' do
japanese = create(:tag, name: 'rename_target')
patch "/tags/#{ tag.id }", params: { locale: 'en', name: 'rename_target' }
expect(response).to have_http_status(:ok), response.body
expect(tag.reload.tag_name).to have_attributes(
name: 'rename_target', language_code: 'en', tag_id: tag.id)
expect(japanese.reload.tag_name).to have_attributes(
name: 'rename_target', language_code: 'ja', tag_id: japanese.id)
end
it 'creates aliases in the requested language' do
put "/tags/#{ tag.id }", params: {
locale: 'en', name: tag.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', canonical_id: english_name.id,
tag_id: tag.id, primary_flg: false, language_code: 'en')
expect(tag.reload.tag_name_id).to eq(english_name.id)
end
it 'does not take an alias from another language' do
japanese = create(:tag, name: 'japanese_owner')
japanese_alias = create(:tag_name, :alias, name: 'shared_alias',
canonical: japanese.tag_name)
expect(japanese_alias.reload).to have_attributes(
canonical_id: japanese.tag_name_id, tag_id: japanese.id,
primary_flg: false, language_code: 'ja')
put "/tags/#{ tag.id }", params: {
locale: 'en', name: tag.name, category: tag.category, deprecated: false,
aliases: 'shared_alias', parent_tags: '' }
expect(response).to have_http_status(:ok), response.body
expect(japanese_alias.reload).to have_attributes(
canonical_id: japanese.tag_name_id, tag_id: japanese.id,
primary_flg: false, language_code: 'ja')
expect(TagName.find_by!(language_code: 'en', name: 'shared_alias'))
.to have_attributes(
canonical_id: english_name.id, tag_id: tag.id,
primary_flg: false, language_code: 'en')
end
end
end
+17 -17
ファイルの表示
@@ -26,10 +26,10 @@ RSpec.describe 'Materials API', type: :request do
describe 'GET /materials' do describe 'GET /materials' do
let!(:tag_a) do let!(:tag_a) do
Tag.create!(tag_name: TagName.create!(name: 'material_index_a'), category: :material) create(:tag, name: 'material_index_a', category: :material)
end end
let!(:tag_b) do let!(:tag_b) do
Tag.create!(tag_name: TagName.create!(name: 'material_index_b'), category: :material) create(:tag, name: 'material_index_b', category: :material)
end end
let!(:material_a) do 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 it 'filters by descendant tags and returns stable parent tag groups' do
root = root =
Tag.create!(tag_name: TagName.create!(name: 'material_scope_root'), create(:tag, name: 'material_scope_root',
category: :material) category: :material)
child_b = child_b =
Tag.create!(tag_name: TagName.create!(name: 'material_scope_b'), create(:tag, name: 'material_scope_b',
category: :material) category: :material)
child_a = child_a =
Tag.create!(tag_name: TagName.create!(name: 'material_scope_a'), create(:tag, name: 'material_scope_a',
category: :material) category: :material)
deprecated = deprecated =
Tag.create!(tag_name: TagName.create!(name: 'material_scope_old'), create(:tag, name: 'material_scope_old',
category: :material, category: :material,
deprecated_at: Time.current) deprecated_at: Time.current)
grandchild = grandchild =
Tag.create!(tag_name: TagName.create!(name: 'material_scope_grandchild'), create(:tag, name: 'material_scope_grandchild',
category: :material) category: :material)
root_material = root_material =
build_material(tag: root, user: member_user, build_material(tag: root, user: member_user,
@@ -179,7 +179,7 @@ RSpec.describe 'Materials API', type: :request do
describe 'GET /materials/:id' do describe 'GET /materials/:id' do
let!(:tag) do let!(:tag) do
Tag.create!(tag_name: TagName.create!(name: 'material_show'), category: :material) create(:tag, name: 'material_show', category: :material)
end end
let!(:material) do let!(:material) do
build_material(tag:, user: member_user, file: dummy_upload(filename: 'show.png')) build_material(tag:, user: member_user, file: dummy_upload(filename: 'show.png'))
@@ -323,8 +323,8 @@ RSpec.describe 'Materials API', type: :request do
end end
it 'returns 422 when the existing tag is not material/character' do it 'returns 422 when the existing tag is not material/character' do
general_tag_name = TagName.create!(name: 'material_create_general_tag') general_tag_name = create(:tag_name, name: 'material_create_general_tag')
Tag.create!(tag_name: general_tag_name, category: :general) create(:tag, tag_name: general_tag_name, category: :general)
post '/materials', params: { post '/materials', params: {
tag: 'material_create_general_tag', tag: 'material_create_general_tag',
@@ -384,7 +384,7 @@ RSpec.describe 'Materials API', type: :request do
describe 'PUT /materials/:id' do describe 'PUT /materials/:id' do
let!(:tag) do let!(:tag) do
Tag.create!(tag_name: TagName.create!(name: 'material_update_old'), category: :material) create(:tag, name: 'material_update_old', category: :material)
end end
let!(:material) do let!(:material) do
build_material(tag:, user: member_user, file: dummy_upload(filename: 'old.png')) build_material(tag:, user: member_user, file: dummy_upload(filename: 'old.png'))
@@ -508,7 +508,7 @@ RSpec.describe 'Materials API', type: :request do
it 'backfills a create tag_version for an existing material tag without history' do it 'backfills a create tag_version for an existing material tag without history' do
existing_tag = existing_tag =
Tag.create!(tag_name: TagName.create!(name: 'material_update_existing_no_history'), create(:tag, name: 'material_update_existing_no_history',
category: :material) category: :material)
expect(existing_tag.tag_versions).to be_empty expect(existing_tag.tag_versions).to be_empty
@@ -531,7 +531,7 @@ RSpec.describe 'Materials API', type: :request do
it 'backfills a create tag_version for an existing character tag without history' do it 'backfills a create tag_version for an existing character tag without history' do
existing_tag = existing_tag =
Tag.create!(tag_name: TagName.create!(name: 'material_update_character_no_history'), create(:tag, name: 'material_update_character_no_history',
category: :character) category: :character)
expect(existing_tag.tag_versions).to be_empty expect(existing_tag.tag_versions).to be_empty
@@ -651,8 +651,8 @@ RSpec.describe 'Materials API', type: :request do
end end
describe 'GET /materials/download.zip' do describe 'GET /materials/download.zip' do
let!(:tag_a) { Tag.create!(tag_name: TagName.create!(name: 'zip_a'), category: :material) } let!(:tag_a) { create(:tag, name: 'zip_a', category: :material) }
let!(:tag_b) { Tag.create!(tag_name: TagName.create!(name: 'zip_b'), category: :material) } let!(:tag_b) { create(:tag, name: 'zip_b', category: :material) }
let!(:material_a) do let!(:material_a) do
build_material(tag: tag_a, user: member_user, build_material(tag: tag_a, user: member_user,
file: dummy_upload(filename: 'a.png', body: 'zip-a')) file: dummy_upload(filename: 'a.png', body: 'zip-a'))
@@ -701,7 +701,7 @@ RSpec.describe 'Materials API', type: :request do
describe 'GET /materials/versions' do describe 'GET /materials/versions' do
let!(:tag) do let!(:tag) do
Tag.create!(tag_name: TagName.create!(name: 'material_history'), category: :material) create(:tag, name: 'material_history', category: :material)
end end
let!(:material) do let!(:material) do
build_material(tag:, user: member_user, file: dummy_upload(filename: 'history.png')) build_material(tag:, user: member_user, file: dummy_upload(filename: 'history.png'))
@@ -776,7 +776,7 @@ RSpec.describe 'Materials API', type: :request do
describe 'DELETE /materials/:id' do describe 'DELETE /materials/:id' do
let!(:tag) do let!(:tag) do
Tag.create!(tag_name: TagName.create!(name: 'material_destroy'), category: :material) create(:tag, name: 'material_destroy', category: :material)
end end
let!(:material) do let!(:material) do
build_material(tag:, user: member_user, file: dummy_upload(filename: 'destroy.png')) build_material(tag:, user: member_user, file: dummy_upload(filename: 'destroy.png'))
+4 -4
ファイルの表示
@@ -162,11 +162,11 @@ RSpec.describe 'NicoTags', type: :request do
nico_tag = create(:external_tag, name: 'nico_tags_spec_source') nico_tag = create(:external_tag, name: 'nico_tags_spec_source')
linked_a_name = TagName.create!(name: 'nico_linked_a') linked_a_name = create(:tag_name, name: 'nico_linked_a')
linked_a = Tag.create!(tag_name: linked_a_name, category: :general) linked_a = create(:tag, tag_name: linked_a_name, category: :general)
linked_b_name = TagName.create!(name: 'nico_linked_b') linked_b_name = create(:tag_name, name: 'nico_linked_b')
linked_b = Tag.create!(tag_name: linked_b_name, category: :general) linked_b = create(:tag, tag_name: linked_b_name, category: :general)
NicoTagVersionRecorder.record!(external_tag: nico_tag, NicoTagVersionRecorder.record!(external_tag: nico_tag,
event_type: :create, created_by_user: admin) event_type: :create, created_by_user: admin)
+55 -56
ファイルの表示
@@ -113,8 +113,8 @@ RSpec.describe 'Posts API', type: :request do
'sections' => []) 'sections' => [])
end end
let!(:tag_name) { TagName.create!(name: 'spec_tag') } let!(:tag_name) { create(:tag_name, name: 'spec_tag') }
let!(:tag) { Tag.create!(tag_name: tag_name, category: :general) } let!(:tag) { create(:tag, tag_name: tag_name, category: :general) }
let!(:post_record) do let!(:post_record) do
Post.create!(title: 'spec post', url: 'https://example.com/spec').tap do |p| 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 describe 'GET /posts' do
let!(:user) { create_member_user! } let!(:user) { create_member_user! }
let!(:tag_name) { TagName.create!(name: 'spec_tag') } let!(:tag_name) { create(:tag_name, name: 'spec_tag') }
let!(:tag) { Tag.create!(tag_name:, category: :general) } let!(:tag) { create(:tag, tag_name:, category: :general) }
let!(:tag_name2) { TagName.create!(name: 'unko') } let!(:tag_name2) { create(:tag_name, name: 'unko') }
let!(:tag2) { Tag.create!(tag_name: tag_name2, category: :deerjikist) } let!(:tag2) { create(:tag, tag_name: tag_name2, category: :deerjikist) }
let!(:alias_tag_name) { TagName.create!(name: 'manko', canonical: tag_name) } let!(:alias_tag_name) { create(:tag_name, :alias, name: 'manko', canonical: tag_name) }
let!(:hit_post) do let!(:hit_post) do
Post.create!(uploaded_user: user, title: 'hello spec world', Post.create!(uploaded_user: user, title: 'hello spec world',
@@ -341,17 +341,17 @@ RSpec.describe 'Posts API', type: :request do
end end
context 'when tags contain not:' do context 'when tags contain not:' do
let!(:foo_tag_name) { TagName.create!(name: 'not_spec_foo') } let!(:foo_tag_name) { create(:tag_name, name: 'not_spec_foo') }
let!(:foo_tag) { Tag.create!(tag_name: foo_tag_name, category: :general) } let!(:foo_tag) { create(:tag, tag_name: foo_tag_name, category: :general) }
let!(:bar_tag_name) { TagName.create!(name: 'not_spec_bar') } let!(:bar_tag_name) { create(:tag_name, name: 'not_spec_bar') }
let!(:bar_tag) { Tag.create!(tag_name: bar_tag_name, category: :general) } let!(:bar_tag) { create(:tag, tag_name: bar_tag_name, category: :general) }
let!(:baz_tag_name) { TagName.create!(name: 'not_spec_baz') } let!(:baz_tag_name) { create(:tag_name, name: 'not_spec_baz') }
let!(:baz_tag) { Tag.create!(tag_name: baz_tag_name, category: :general) } let!(:baz_tag) { create(:tag, tag_name: baz_tag_name, category: :general) }
let!(:foo_alias_tag_name) do 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', canonical: foo_tag_name)
end end
let!(:foo_only_post) do let!(:foo_only_post) do
@@ -772,11 +772,10 @@ RSpec.describe 'Posts API', type: :request do
end end
it 'omits deprecated tags' do it 'omits deprecated tags' do
deprecated_tag = Tag.create!( deprecated_tag = create(:tag,
name: 'deprecated_post_tag', name: 'deprecated_post_tag',
category: :general, category: :general,
deprecated_at: Time.current deprecated_at: Time.current)
)
PostTag.create!(post: post_record, tag: deprecated_tag) PostTag.create!(post: post_record, tag: deprecated_tag)
request request
@@ -853,9 +852,9 @@ RSpec.describe 'Posts API', type: :request do
tags = tags =
15.times.map do |i| 15.times.map do |i|
tag_name = TagName.create!(name: "show_query_tag_#{ i }") tag_name = create(:tag_name, name: "show_query_tag_#{ i }")
tag = Tag.create!(tag_name:, category: :general) tag = create(:tag, tag_name:, category: :general)
TagName.create!(name: "show_query_alias_#{ i }", canonical: tag_name) create(:tag_name, :alias, name: "show_query_alias_#{ i }", canonical: tag_name)
PostTag.create!(post: post_record, tag:) PostTag.create!(post: post_record, tag:)
tag tag
end end
@@ -1000,7 +999,7 @@ RSpec.describe 'Posts API', type: :request do
describe 'POST /posts' do describe 'POST /posts' do
let(:member) { create(:user, :member) } 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', canonical: tag_name) }
it '401 when not logged in' do it '401 when not logged in' do
sign_out sign_out
@@ -1157,11 +1156,10 @@ RSpec.describe 'Posts API', type: :request do
end end
it 'rejects a deprecated tag specified directly' do it 'rejects a deprecated tag specified directly' do
Tag.create!( create(:tag,
name: 'deprecated_direct_tag', name: 'deprecated_direct_tag',
category: :general, category: :general,
deprecated_at: Time.current deprecated_at: Time.current)
)
sign_in_as(member) sign_in_as(member)
post '/posts', params: post_write_params( post '/posts', params: post_write_params(
@@ -1178,18 +1176,16 @@ RSpec.describe 'Posts API', type: :request do
end end
it 'expands through multiple deprecated parent tags and saves active ancestors' do it 'expands through multiple deprecated parent tags and saves active ancestors' do
child = Tag.create!(name: 'active_child', category: :general) child = create(:tag, name: 'active_child', category: :general)
deprecated_parent = Tag.create!( deprecated_parent = create(:tag,
name: 'deprecated_parent', name: 'deprecated_parent',
category: :general, category: :general,
deprecated_at: Time.current deprecated_at: Time.current)
) deprecated_grandparent = create(:tag,
deprecated_grandparent = Tag.create!( name: 'deprecated_grandparent',
name: 'deprecated_grandparent', category: :general,
category: :general, deprecated_at: Time.current)
deprecated_at: Time.current active_grandparent = create(:tag, name: 'active_grandparent', category: :general)
)
active_grandparent = Tag.create!(name: 'active_grandparent', category: :general)
TagImplication.create!(tag: child, parent_tag: deprecated_parent) TagImplication.create!(tag: child, parent_tag: deprecated_parent)
TagImplication.create!(tag: deprecated_parent, parent_tag: deprecated_grandparent) TagImplication.create!(tag: deprecated_parent, parent_tag: deprecated_grandparent)
TagImplication.create!(tag: deprecated_grandparent, parent_tag: active_grandparent) TagImplication.create!(tag: deprecated_grandparent, parent_tag: active_grandparent)
@@ -1622,6 +1618,7 @@ RSpec.describe 'Posts API', type: :request do
expect(arguments[:thumbnails].keys).to eq([0]) expect(arguments[:thumbnails].keys).to eq([0])
expect(arguments[:host]).to eq('http://www.example.com') expect(arguments[:host]).to eq('http://www.example.com')
end end
expect(creator).to have_received(:run).with(Locale.nipponese)
end end
it 'rejects malformed manifests as a request-level error' do it 'rejects malformed manifests as a request-level error' do
@@ -1658,8 +1655,8 @@ RSpec.describe 'Posts API', type: :request do
create(:post_tag_section, post: post_record, tag:, create(:post_tag_section, post: post_record, tag:,
begin_ms: 1000, end_ms: 2000) begin_ms: 1000, end_ms: 2000)
tn2 = TagName.create!(name: 'spec_tag_2') tn2 = create(:tag_name, name: 'spec_tag_2')
replacement_tag = Tag.create!(tag_name: tn2, category: :general) replacement_tag = create(:tag, tag_name: tn2, category: :general)
put "/posts/#{post_record.id}", params: post_update_params( put "/posts/#{post_record.id}", params: post_update_params(
post_record, post_record,
@@ -1707,11 +1704,10 @@ RSpec.describe 'Posts API', type: :request do
end end
it 'rejects a deprecated tag specified directly' do it 'rejects a deprecated tag specified directly' do
Tag.create!( create(:tag,
name: 'deprecated_update_tag', name: 'deprecated_update_tag',
category: :general, category: :general,
deprecated_at: Time.current deprecated_at: Time.current)
)
sign_in_as(member) sign_in_as(member)
put "/posts/#{ post_record.id }", params: post_update_params( put "/posts/#{ post_record.id }", params: post_update_params(
@@ -1986,7 +1982,8 @@ RSpec.describe 'Posts API', type: :request do
base_version = create_post_version_for!(post_record.reload) 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) PostTag.create!(post: post_record, tag: current_tag, created_user: member)
PostVersionRecorder.record!( PostVersionRecorder.record!(
@@ -2015,7 +2012,8 @@ RSpec.describe 'Posts API', type: :request do
base_version = create_post_version_for!(post_record.reload) 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) PostTag.create!(post: post_record, tag: current_tag, created_user: member)
PostVersionRecorder.record!( PostVersionRecorder.record!(
@@ -2102,7 +2100,8 @@ RSpec.describe 'Posts API', type: :request do
sign_in_as(member) sign_in_as(member)
nico_tag = create_nico_tag!('nico:relation_source') 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) NicoTagRelation.create!(nico_tag:, tag: linked_tag)
PostExternalTag.create!(post: post_record, external_tag: nico_tag) PostExternalTag.create!(post: post_record, external_tag: nico_tag)
@@ -2191,8 +2190,8 @@ RSpec.describe 'Posts API', type: :request do
let(:oc_from) { Time.zone.local(2019, 12, 31, 0, 0, 0) } 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(:oc_before) { Time.zone.local(2020, 1, 1, 0, 0, 0) }
let!(:tag_name2) { TagName.create!(name: 'spec_tag_2') } let!(:tag_name2) { create(:tag_name, name: 'spec_tag_2') }
let!(:tag2) { Tag.create!(tag_name: tag_name2, category: :general) } let!(:tag2) { create(:tag, tag_name: tag_name2, category: :general) }
def snapshot_tags(post) def snapshot_tags(post)
post.snapshot_tag_names.join(' ') post.snapshot_tag_names.join(' ')
@@ -2550,8 +2549,8 @@ RSpec.describe 'Posts API', type: :request do
sign_in_as(member) sign_in_as(member)
base_version = create_post_version_for!(post_record) base_version = create_post_version_for!(post_record)
tag_name2 = TagName.create!(name: 'spec_tag_2') tag_name2 = create(:tag_name, name: 'spec_tag_2')
Tag.create!(tag_name: tag_name2, category: :general) create(:tag, tag_name: tag_name2, category: :general)
expect do expect do
put "/posts/#{post_record.id}", params: post_write_params( put "/posts/#{post_record.id}", params: post_write_params(
@@ -2715,8 +2714,8 @@ RSpec.describe 'Posts API', type: :request do
base_version = create_post_version_for!(post_record.reload) base_version = create_post_version_for!(post_record.reload)
tag_name2 = TagName.create!(name: 'spec_tag_2') tag_name2 = create(:tag_name, name: 'spec_tag_2')
tag2 = Tag.create!(tag_name: tag_name2, category: :general) tag2 = create(:tag, tag_name: tag_name2, category: :general)
expect { expect {
put "/posts/#{post_record.id}", params: post_write_params( put "/posts/#{post_record.id}", params: post_write_params(
+2 -2
ファイルの表示
@@ -232,8 +232,8 @@ RSpec.describe 'TagVersions API', type: :request do
end end
it 'does not create tag versions by wiki updates when tag has no versions yet' do 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_name = create(:tag_name, name: 'tag_versions_from_wiki')
wiki_tag = Tag.create!(tag_name: wiki_tag_name, category: :general) wiki_tag = create(:tag, tag_name: wiki_tag_name, category: :general)
wiki_page = wiki_page =
Wiki::Commit.create_content!( Wiki::Commit.create_content!(
+2 -2
ファイルの表示
@@ -8,8 +8,8 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
end end
def create_tag! name:, category: :general def create_tag! name:, category: :general
tag_name = TagName.create!(name:) tag_name = create(:tag_name, name:)
Tag.create!(tag_name:, category:) create(:tag, tag_name:, category:)
end end
def create_wiki_for_tag! tag:, body: 'wiki body', user: member_user def create_wiki_for_tag! tag:, body: 'wiki body', user: member_user
+129 -150
ファイルの表示
@@ -2,13 +2,13 @@ require 'cgi'
require 'rails_helper' require 'rails_helper'
RSpec.describe 'Tags API', type: :request do RSpec.describe 'Tags API', type: :request do
let!(:tn) { TagName.create!(name: 'spec_tag') } let!(:tn) { create(:tag_name, name: 'spec_tag') }
let!(:tag) { Tag.create!(tag_name: tn, category: :general) } let!(:tag) { create(:tag, tag_name: tn, category: :general) }
let!(:alias_tn) { TagName.create!(name: 'unko', canonical: tn) } let!(:alias_tn) { create(:tag_name, :alias, name: 'unko', canonical: tn) }
let!(:post) { Post.create!(url: 'https://example.com/unkounkounko') } let!(:post) { Post.create!(url: 'https://example.com/unkounkounko') }
let!(:post_tag) { PostTag.create!(post:, tag:) } let!(:post_tag) { PostTag.create!(post:, tag:) }
let!(:tn2) { TagName.create!(name: 'unknown') } let!(:tn2) { create(:tag_name, name: 'unknown') }
let!(:tag2) { Tag.create!(tag_name: tn2, category: :general) } let!(:tag2) { create(:tag, tag_name: tn2, category: :general) }
def response_tags def response_tags
json.fetch('tags') json.fetch('tags')
@@ -91,7 +91,7 @@ RSpec.describe 'Tags API', type: :request do
end end
it 'filters tags by category' do it 'filters tags by category' do
meme = Tag.create!(tag_name: TagName.create!(name: 'meme_only'), category: :meme) meme = create(:tag, name: 'meme_only', category: :meme)
get '/tags', params: { category: 'meme' } get '/tags', params: { category: 'meme' }
@@ -101,12 +101,11 @@ RSpec.describe 'Tags API', type: :request do
end end
it 'filters tags by deprecated state' do it 'filters tags by deprecated state' do
deprecated_tag = Tag.create!( deprecated_tag = create(:tag,
name: 'deprecated_filter', name: 'deprecated_filter',
category: :general, category: :general,
deprecated_at: 1.day.from_now deprecated_at: 1.day.from_now)
) active_tag = create(:tag, name: 'active_filter', category: :general)
active_tag = Tag.create!(name: 'active_filter', category: :general)
get '/tags', params: { name: '_filter', deprecated: '1' } get '/tags', params: { name: '_filter', deprecated: '1' }
@@ -122,9 +121,9 @@ RSpec.describe 'Tags API', type: :request do
end end
it 'filters tags by post_count range' do it 'filters tags by post_count range' do
low = Tag.create!(tag_name: TagName.create!(name: 'pc_low'), category: :general) low = create(:tag, name: 'pc_low', category: :general)
mid = Tag.create!(tag_name: TagName.create!(name: 'pc_mid'), category: :general) mid = create(:tag, name: 'pc_mid', category: :general)
high = Tag.create!(tag_name: TagName.create!(name: 'pc_high'), category: :general) high = create(:tag, name: 'pc_high', category: :general)
low.update_columns(post_count: 1) low.update_columns(post_count: 1)
mid.update_columns(post_count: 3) mid.update_columns(post_count: 3)
@@ -142,8 +141,8 @@ RSpec.describe 'Tags API', type: :request do
end end
it 'filters tags by created_at range' do it 'filters tags by created_at range' do
old_tag = Tag.create!(tag_name: TagName.create!(name: 'created_old'), category: :general) old_tag = create(:tag, name: 'created_old', category: :general)
new_tag = Tag.create!(tag_name: TagName.create!(name: 'created_new'), category: :general) new_tag = create(:tag, name: 'created_new', category: :general)
old_time = Time.zone.local(2024, 1, 1, 0, 0, 0) old_time = Time.zone.local(2024, 1, 1, 0, 0, 0)
new_time = Time.zone.local(2024, 2, 1, 0, 0, 0) new_time = Time.zone.local(2024, 2, 1, 0, 0, 0)
@@ -162,8 +161,8 @@ RSpec.describe 'Tags API', type: :request do
end end
it 'filters tags by updated_at range' do it 'filters tags by updated_at range' do
old_tag = Tag.create!(tag_name: TagName.create!(name: 'updated_old'), category: :general) old_tag = create(:tag, name: 'updated_old', category: :general)
new_tag = Tag.create!(tag_name: TagName.create!(name: 'updated_new'), category: :general) new_tag = create(:tag, name: 'updated_new', category: :general)
old_time = Time.zone.local(2024, 3, 1, 0, 0, 0) old_time = Time.zone.local(2024, 3, 1, 0, 0, 0)
new_time = Time.zone.local(2024, 4, 1, 0, 0, 0) new_time = Time.zone.local(2024, 4, 1, 0, 0, 0)
@@ -182,12 +181,12 @@ RSpec.describe 'Tags API', type: :request do
end end
it 'orders tags by custom category order' do it 'orders tags by custom category order' do
Tag.create!(tag_name: TagName.create!(name: 'cat_deerjikist'), category: :deerjikist) create(:tag, name: 'cat_deerjikist', category: :deerjikist)
Tag.create!(tag_name: TagName.create!(name: 'cat_meme'), category: :meme) create(:tag, name: 'cat_meme', category: :meme)
Tag.create!(tag_name: TagName.create!(name: 'cat_character'), category: :character) create(:tag, name: 'cat_character', category: :character)
Tag.create!(tag_name: TagName.create!(name: 'cat_general'), category: :general) create(:tag, name: 'cat_general', category: :general)
Tag.create!(tag_name: TagName.create!(name: 'cat_material'), category: :material) create(:tag, name: 'cat_material', category: :material)
Tag.create!(tag_name: TagName.create!(name: 'cat_meta'), category: :meta) create(:tag, name: 'cat_meta', category: :meta)
create(:external_tag, name: 'cat_nico') create(:external_tag, name: 'cat_nico')
get '/tags', params: { name: 'cat_', order: 'category:asc', limit: 20 } get '/tags', params: { name: 'cat_', order: 'category:asc', limit: 20 }
@@ -202,20 +201,20 @@ RSpec.describe 'Tags API', type: :request do
context 'with mixed legacy pagination' do context 'with mixed legacy pagination' do
let!(:first_tag) do let!(:first_tag) do
create(:tag, create(:tag,
tag_name: create(:tag_name, name: 'a_mixed_page'), tag_name: create(:tag_name, name: 'a_mixed_page'),
category: :meme) category: :meme)
end end
let!(:middle_tag) do let!(:middle_tag) do
create(:tag, create(:tag,
tag_name: create(:tag_name, name: 'm_mixed_page'), tag_name: create(:tag_name, name: 'm_mixed_page'),
category: :meta) category: :meta)
end end
let!(:last_tag) do let!(:last_tag) do
create(:tag, create(:tag,
tag_name: create(:tag_name, name: 'z_mixed_page'), tag_name: create(:tag_name, name: 'z_mixed_page'),
category: :general) category: :general)
end end
let!(:first_external) do let!(:first_external) do
@@ -265,7 +264,7 @@ RSpec.describe 'Tags API', type: :request do
it 'paginates and keeps total count' do it 'paginates and keeps total count' do
%w[pag_a pag_b pag_c].each do |name| %w[pag_a pag_b pag_c].each do |name|
Tag.create!(tag_name: TagName.create!(name:), category: :general) create(:tag, tag_name: create(:tag_name, name:), category: :general)
end end
get '/tags', params: { name: 'pag_', order: 'name:asc', page: 2, limit: 2 } get '/tags', params: { name: 'pag_', order: 'name:asc', page: 2, limit: 2 }
@@ -276,8 +275,8 @@ RSpec.describe 'Tags API', type: :request do
end end
it 'falls back to default ordering when order is invalid' do it 'falls back to default ordering when order is invalid' do
low = Tag.create!(tag_name: TagName.create!(name: 'fallback_low'), category: :general) low = create(:tag, name: 'fallback_low', category: :general)
high = Tag.create!(tag_name: TagName.create!(name: 'fallback_high'), category: :general) high = create(:tag, name: 'fallback_high', category: :general)
low.update_columns(post_count: 1) low.update_columns(post_count: 1)
high.update_columns(post_count: 9) high.update_columns(post_count: 9)
@@ -290,7 +289,7 @@ RSpec.describe 'Tags API', type: :request do
it 'normalises invalid page and limit' do it 'normalises invalid page and limit' do
%w[norm_a norm_b].each do |name| %w[norm_a norm_b].each do |name|
Tag.create!(tag_name: TagName.create!(name:), category: :general) create(:tag, tag_name: create(:tag_name, name:), category: :general)
end end
get '/tags', params: { name: 'norm_', order: 'name:asc', page: 0, limit: 0 } get '/tags', params: { name: 'norm_', order: 'name:asc', page: 0, limit: 0 }
@@ -302,10 +301,9 @@ RSpec.describe 'Tags API', type: :request do
end end
it 'returns aliases and parent tags' do it 'returns aliases and parent tags' do
parent_tag = Tag.create!( parent_tag = create(:tag,
tag_name: TagName.create!(name: 'index_parent_tag'), tag_name: create(:tag_name, name: 'index_parent_tag'),
category: :meme category: :meme)
)
TagImplication.create!(tag:, parent_tag:) TagImplication.create!(tag:, parent_tag:)
get '/tags', params: { name: 'spec_tag' } get '/tags', params: { name: 'spec_tag' }
@@ -349,10 +347,9 @@ RSpec.describe 'Tags API', type: :request do
end end
it 'returns aliases and parent tags' do it 'returns aliases and parent tags' do
parent_tag = Tag.create!( parent_tag = create(:tag,
tag_name: TagName.create!(name: 'show_parent_tag'), tag_name: create(:tag_name, name: 'show_parent_tag'),
category: :character category: :character)
)
TagImplication.create!(tag:, parent_tag:) TagImplication.create!(tag:, parent_tag:)
request request
@@ -416,7 +413,7 @@ RSpec.describe 'Tags API', type: :request do
describe 'GET /tags/autocomplete' do describe 'GET /tags/autocomplete' do
it 'combines internal and external matches without conflating equal ids' 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, name: 'mixed_internal', post_count: 2)
external = create(:external_tag, id: internal.id, external = create(:external_tag, id: internal.id,
name: 'mixed_external', post_count: 3) name: 'mixed_external', post_count: 3)
@@ -455,7 +452,7 @@ RSpec.describe 'Tags API', type: :request do
it 'limits the combined results to 20 and sorts ties by displayed name' do it 'limits the combined results to 20 and sorts ties by displayed name' do
11.times do |i| 11.times do |i|
name = "combined_#{ i.to_s.rjust(2, '0') }" name = "combined_#{ i.to_s.rjust(2, '0') }"
Tag.create!(category: :general, name:, post_count: 1) create(:tag, category: :general, name:, post_count: 1)
create(:external_tag, name:, post_count: 1) create(:external_tag, name:, post_count: 1)
end end
@@ -467,9 +464,9 @@ RSpec.describe 'Tags API', type: :request do
end end
it 'excludes external tags but preserves internal alias matches when nico is false' do 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) internal = create(:tag, category: :general, name: 'switch_internal', post_count: 1)
alias_target = Tag.create!(category: :general, name: 'alias_target', post_count: 1) alias_target = create(:tag, category: :general, name: 'alias_target', post_count: 1)
TagName.create!(name: 'switch_alias', canonical: alias_target.tag_name) create(:tag_name, :alias, name: 'switch_alias', canonical: alias_target.tag_name)
create(:external_tag, name: 'switch_external', post_count: 1) create(:external_tag, name: 'switch_external', post_count: 1)
get '/tags/autocomplete', params: { q: 'switch', nico: '0' } get '/tags/autocomplete', params: { q: 'switch', nico: '0' }
@@ -484,11 +481,13 @@ RSpec.describe 'Tags API', type: :request do
['%', '_'].each do |wildcard| ['%', '_'].each do |wildcard|
it "treats #{ wildcard } literally for canonical, alias, and external names" do it "treats #{ wildcard } literally for canonical, alias, and external names" do
literal = "literal#{ wildcard }match" literal = "literal#{ wildcard }match"
Tag.create!(category: :general, name: literal, post_count: 1) create(:tag, category: :general, name: literal, post_count: 1)
alias_target = Tag.create!(category: :general, name: 'literal_alias_target', post_count: 1) alias_target = create(
TagName.create!(name: "#{ literal }_alias", canonical: alias_target.tag_name) :tag, category: :general, name: 'literal_alias_target', post_count: 1)
create(:tag_name, :alias, name: "#{ literal }_alias",
canonical: alias_target.tag_name)
create(:external_tag, name: literal, post_count: 1) create(:external_tag, name: literal, post_count: 1)
Tag.create!(category: :general, name: 'literalXmatch', post_count: 2) create(:tag, category: :general, name: 'literalXmatch', post_count: 2)
create(:external_tag, name: 'literalXmatch', post_count: 2) create(:external_tag, name: 'literalXmatch', post_count: 2)
get '/tags/autocomplete', params: { q: "literal#{ wildcard }" } get '/tags/autocomplete', params: { q: "literal#{ wildcard }" }
@@ -523,11 +522,10 @@ RSpec.describe 'Tags API', type: :request do
end end
it 'omits deprecated tags' do it 'omits deprecated tags' do
deprecated_tag = Tag.create!( deprecated_tag = create(:tag,
name: 'spec_deprecated', name: 'spec_deprecated',
category: :general, category: :general,
deprecated_at: Time.current deprecated_at: Time.current)
)
deprecated_tag.update_columns(post_count: 1) deprecated_tag.update_columns(post_count: 1)
get '/tags/autocomplete', params: { q: 'spec_', present: '0' } get '/tags/autocomplete', params: { q: 'spec_', present: '0' }
@@ -820,27 +818,27 @@ RSpec.describe 'Tags API', type: :request do
describe 'GET /tags/with-depth' do describe 'GET /tags/with-depth' do
let!(:root_meme) do let!(:root_meme) do
Tag.create!(tag_name: TagName.create!(name: 'depth_a_root_meme'), category: :meme) create(:tag, name: 'depth_a_root_meme', category: :meme)
end end
let!(:root_material) do let!(:root_material) do
Tag.create!(tag_name: TagName.create!(name: 'depth_b_root_material'), category: :material) create(:tag, name: 'depth_b_root_material', category: :material)
end end
let!(:hidden_general_root) do let!(:hidden_general_root) do
Tag.create!(tag_name: TagName.create!(name: 'depth_hidden_general_root'), category: :general) create(:tag, name: 'depth_hidden_general_root', category: :general)
end end
let!(:child_character) do let!(:child_character) do
Tag.create!(tag_name: TagName.create!(name: 'depth_child_character'), category: :character) create(:tag, name: 'depth_child_character', category: :character)
end end
let!(:grandchild_material) do let!(:grandchild_material) do
Tag.create!(tag_name: TagName.create!(name: 'depth_grandchild_material'), category: :material) create(:tag, name: 'depth_grandchild_material', category: :material)
end end
let!(:child_general) do let!(:child_general) do
Tag.create!(tag_name: TagName.create!(name: 'depth_child_general'), category: :general) create(:tag, name: 'depth_child_general', category: :general)
end end
before do before do
@@ -883,15 +881,13 @@ RSpec.describe 'Tags API', type: :request do
end end
it 'passes through deprecated tags when finding children' do it 'passes through deprecated tags when finding children' do
deprecated_middle = Tag.create!( deprecated_middle = create(:tag,
name: 'depth_deprecated_middle', name: 'depth_deprecated_middle',
category: :character, category: :character,
deprecated_at: Time.current deprecated_at: Time.current)
) visible_descendant = create(:tag,
visible_descendant = Tag.create!( name: 'depth_visible_descendant',
name: 'depth_visible_descendant', category: :material)
category: :material
)
TagImplication.create!(parent_tag: root_material, tag: deprecated_middle) TagImplication.create!(parent_tag: root_material, tag: deprecated_middle)
TagImplication.create!(parent_tag: deprecated_middle, tag: visible_descendant) TagImplication.create!(parent_tag: deprecated_middle, tag: visible_descendant)
@@ -903,24 +899,20 @@ RSpec.describe 'Tags API', type: :request do
end end
it 'passes through multiple deprecated tags for roots and has_children' do it 'passes through multiple deprecated tags for roots and has_children' do
active_child = Tag.create!( active_child = create(:tag,
name: 'depth_active_child_below_deprecated', name: 'depth_active_child_below_deprecated',
category: :character category: :character)
) deprecated_parent = create(:tag,
deprecated_parent = Tag.create!( name: 'depth_deprecated_parent',
name: 'depth_deprecated_parent', category: :character,
category: :character, deprecated_at: Time.current)
deprecated_at: Time.current deprecated_grandparent = create(:tag,
) name: 'depth_deprecated_grandparent',
deprecated_grandparent = Tag.create!( category: :material,
name: 'depth_deprecated_grandparent', deprecated_at: Time.current)
category: :material, active_ancestor = create(:tag,
deprecated_at: Time.current name: 'depth_active_ancestor',
) category: :meme)
active_ancestor = Tag.create!(
name: 'depth_active_ancestor',
category: :meme
)
TagImplication.create!(tag: active_child, parent_tag: deprecated_parent) TagImplication.create!(tag: active_child, parent_tag: deprecated_parent)
TagImplication.create!(tag: deprecated_parent, parent_tag: deprecated_grandparent) TagImplication.create!(tag: deprecated_parent, parent_tag: deprecated_grandparent)
TagImplication.create!(tag: deprecated_grandparent, parent_tag: active_ancestor) TagImplication.create!(tag: deprecated_grandparent, parent_tag: active_ancestor)
@@ -945,15 +937,13 @@ RSpec.describe 'Tags API', type: :request do
end end
it 'treats an active tag with only deprecated ancestors as a root' do it 'treats an active tag with only deprecated ancestors as a root' do
active_child = Tag.create!( active_child = create(:tag,
name: 'depth_root_below_deprecated', name: 'depth_root_below_deprecated',
category: :character category: :character)
) deprecated_parent = create(:tag,
deprecated_parent = Tag.create!( name: 'depth_root_deprecated_parent',
name: 'depth_root_deprecated_parent', category: :material,
category: :material, deprecated_at: Time.current)
deprecated_at: Time.current
)
TagImplication.create!(tag: active_child, parent_tag: deprecated_parent) TagImplication.create!(tag: active_child, parent_tag: deprecated_parent)
get '/tags/with-depth' get '/tags/with-depth'
@@ -967,16 +957,14 @@ RSpec.describe 'Tags API', type: :request do
end end
it 'terminates when deprecated implications contain a cycle' do it 'terminates when deprecated implications contain a cycle' do
first = Tag.create!( first = create(:tag,
name: 'depth_cycle_first', name: 'depth_cycle_first',
category: :character, category: :character,
deprecated_at: Time.current deprecated_at: Time.current)
) second = create(:tag,
second = Tag.create!( name: 'depth_cycle_second',
name: 'depth_cycle_second', category: :material,
category: :material, deprecated_at: Time.current)
deprecated_at: Time.current
)
TagImplication.create!(tag: first, parent_tag: root_material) TagImplication.create!(tag: first, parent_tag: root_material)
TagImplication.create!(tag: second, parent_tag: first) TagImplication.create!(tag: second, parent_tag: first)
now = Time.current now = Time.current
@@ -1002,19 +990,19 @@ RSpec.describe 'Tags API', type: :request do
let!(:material_user) { create_member_user! } let!(:material_user) { create_member_user! }
let!(:root_tag) do let!(:root_tag) do
Tag.create!(tag_name: TagName.create!(name: 'materials_root'), category: :material) create(:tag, name: 'materials_root', category: :material)
end end
let!(:child_a_tag) do let!(:child_a_tag) do
Tag.create!(tag_name: TagName.create!(name: 'materials_child_a'), category: :material) create(:tag, name: 'materials_child_a', category: :material)
end end
let!(:child_b_tag) do let!(:child_b_tag) do
Tag.create!(tag_name: TagName.create!(name: 'materials_child_b'), category: :character) create(:tag, name: 'materials_child_b', category: :character)
end end
let!(:grandchild_tag) do let!(:grandchild_tag) do
Tag.create!(tag_name: TagName.create!(name: 'materials_grandchild'), category: :material) create(:tag, name: 'materials_grandchild', category: :material)
end end
let!(:root_material) do let!(:root_material) do
@@ -1158,14 +1146,12 @@ RSpec.describe 'Tags API', type: :request do
end end
it 'name, category, aliases, parent tags をまとめて更新できる' do it 'name, category, aliases, parent tags をまとめて更新できる' do
old_parent = Tag.create!( old_parent = create(:tag,
tag_name: TagName.create!(name: 'put_old_parent'), tag_name: create(:tag_name, name: 'put_old_parent'),
category: :general category: :general)
) kept_parent = create(:tag,
kept_parent = Tag.create!( tag_name: create(:tag_name, name: 'put_kept_parent'),
tag_name: TagName.create!(name: 'put_kept_parent'), category: :general)
category: :general
)
TagImplication.create!(tag:, parent_tag: old_parent) TagImplication.create!(tag:, parent_tag: old_parent)
TagImplication.create!(tag:, parent_tag: kept_parent) TagImplication.create!(tag:, parent_tag: kept_parent)
@@ -1310,14 +1296,12 @@ RSpec.describe 'Tags API', type: :request do
end end
it 'parent tag の snapshot も作成する' do it 'parent tag の snapshot も作成する' do
old_parent = Tag.create!( old_parent = create(:tag,
tag_name: TagName.create!(name: 'put_snapshot_old_parent'), tag_name: create(:tag_name, name: 'put_snapshot_old_parent'),
category: :general category: :general)
) new_parent = create(:tag,
new_parent = Tag.create!( tag_name: create(:tag_name, name: 'put_snapshot_new_parent'),
tag_name: TagName.create!(name: 'put_snapshot_new_parent'), category: :general)
category: :general
)
TagImplication.create!(tag:, parent_tag: old_parent) TagImplication.create!(tag:, parent_tag: old_parent)
put "/tags/#{ tag.id }", params: { put "/tags/#{ tag.id }", params: {
@@ -1454,14 +1438,12 @@ RSpec.describe 'Tags API', type: :request do
end end
it '別名を他 tag から奪った場合、奪はれた側の tag version も作成する' do it '別名を他 tag から奪った場合、奪はれた側の tag version も作成する' do
old_owner = Tag.create!( old_owner = create(:tag,
tag_name: TagName.create!(name: 'put_alias_old_owner'), tag_name: create(:tag_name, name: 'put_alias_old_owner'),
category: :general category: :general)
) stolen_alias = create(:tag_name, :alias,
stolen_alias = TagName.create!( name: 'put_stolen_alias',
name: 'put_stolen_alias', canonical: old_owner.tag_name)
canonical: old_owner.tag_name
)
expect(old_owner.tag_name.aliases.map(&:name)).to include('put_stolen_alias') expect(old_owner.tag_name.aliases.map(&:name)).to include('put_stolen_alias')
@@ -1492,14 +1474,12 @@ RSpec.describe 'Tags API', type: :request do
end end
it '別 tag の alias 名を rename で奪へる' do it '別 tag の alias 名を rename で奪へる' do
old_owner = Tag.create!( old_owner = create(:tag,
tag_name: TagName.create!(name: 'put_alias_collision_owner'), tag_name: create(:tag_name, name: 'put_alias_collision_owner'),
category: :general category: :general)
) stolen_alias = create(:tag_name, :alias,
stolen_alias = TagName.create!( name: 'put_alias_collision_name',
name: 'put_alias_collision_name', canonical: old_owner.tag_name)
canonical: old_owner.tag_name
)
wiki_page = wiki_page =
Wiki::Commit.create_content!( Wiki::Commit.create_content!(
tag_name: tag.tag_name, tag_name: tag.tag_name,
@@ -1515,7 +1495,7 @@ RSpec.describe 'Tags API', type: :request do
deprecated: '0', deprecated: '0',
} }
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok), response.body
tag.reload tag.reload
old_owner.reload old_owner.reload
@@ -1534,10 +1514,9 @@ RSpec.describe 'Tags API', type: :request do
end end
it 'parent_tags に指定すると循環する tag は 422 にする' do it 'parent_tags に指定すると循環する tag は 422 にする' do
child = Tag.create!( child = create(:tag,
tag_name: TagName.create!(name: 'put_cycle_child'), tag_name: create(:tag_name, name: 'put_cycle_child'),
category: :general category: :general)
)
TagImplication.create!(tag: child, parent_tag: tag) TagImplication.create!(tag: child, parent_tag: tag)
+1 -1
ファイルの表示
@@ -10,7 +10,7 @@ RSpec.describe 'Wiki conflict handling', type: :request do
it 'returns 409 when base_revision_id is stale' do it 'returns 409 when base_revision_id is stale' do
page = page =
Wiki::Commit.create_content!( Wiki::Commit.create_content!(
tag_name: TagName.create!(name: 'wiki_conflict_request'), tag_name: create(:tag_name, name: 'wiki_conflict_request'),
body: 'first', body: 'first',
created_by_user: user, created_by_user: user,
message: 'init') 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 def create_wiki_page title:, body: 'body', message: 'init', user: self.user
Wiki::Commit.create_content!( Wiki::Commit.create_content!(
tag_name: TagName.create!(name: title), tag_name: create(:tag_name, name: title),
body:, body:,
created_by_user: user, created_by_user: user,
message:) message:)
+14 -15
ファイルの表示
@@ -10,7 +10,7 @@ RSpec.describe 'Wiki API', type: :request do
let!(:user) { create_member_user! } 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 let!(:page) do
Wiki::Commit.create_content!( Wiki::Commit.create_content!(
tag_name: tn, tag_name: tn,
@@ -19,11 +19,10 @@ RSpec.describe 'Wiki API', type: :request do
message: 'init') message: 'init')
end end
let!(:tag) do let!(:tag) do
Tag.create!( create(:tag,
tag_name: tn, tag_name: tn,
category: :general, category: :general,
deprecated_at: Time.zone.local(2026, 6, 1) deprecated_at: Time.zone.local(2026, 6, 1))
)
end end
describe 'GET /wiki' do describe 'GET /wiki' do
@@ -208,7 +207,7 @@ RSpec.describe 'Wiki API', type: :request do
{ 'X-Transfer-Code' => user.inheritance_code } { 'X-Transfer-Code' => user.inheritance_code }
end end
let!(:test_tag_name) { TagName.create!(name: 'TestPage') } let!(:test_tag_name) { create(:tag_name, name: 'TestPage') }
let!(:page) do let!(:page) do
Wiki::Commit.create_content!( Wiki::Commit.create_content!(
@@ -275,8 +274,8 @@ RSpec.describe 'Wiki API', type: :request do
end end
it 'wiki body だけを変更しても tag version は作成しない' do it 'wiki body だけを変更しても tag version は作成しない' do
linked_tag_name = TagName.create!(name: 'wiki_body_only_tag') linked_tag_name = create(:tag_name, name: 'wiki_body_only_tag')
linked_tag = Tag.create!(tag_name: linked_tag_name, category: :general) linked_tag = create(:tag, tag_name: linked_tag_name, category: :general)
TagVersionRecorder.record!( TagVersionRecorder.record!(
tag: linked_tag, tag: linked_tag,
@@ -359,13 +358,13 @@ RSpec.describe 'Wiki API', type: :request do
describe 'GET /wiki/search' do describe 'GET /wiki/search' do
before do before do
Wiki::Commit.create_content!( 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', body: 'search body 2',
created_by_user: user, created_by_user: user,
message: 'init') message: 'init')
Wiki::Commit.create_content!( Wiki::Commit.create_content!(
tag_name: TagName.create!(name: 'unrelated_title'), tag_name: create(:tag_name, name: 'unrelated_title'),
body: 'unrelated body', body: 'unrelated body',
created_by_user: user, created_by_user: user,
message: 'init') 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 it 'returns empty array when page has no revisions and filtered by id' do
# 別ページを作って revision 無し # 別ページを作って revision 無し
tn2 = TagName.create!(name: 'spec_no_rev') tn2 = create(:tag_name, name: 'spec_no_rev')
# 異常データ: revision 無し WikiPage を直接作る # 異常データ: revision 無し WikiPage を直接作る
p2 = WikiPage.create!( p2 = WikiPage.create!(
tag_name: tn2, tag_name: tn2,
@@ -514,7 +513,7 @@ RSpec.describe 'Wiki API', type: :request do
describe 'Wiki::Commit.redirect!' do describe 'Wiki::Commit.redirect!' do
it 'raises because redirect revisions are deprecated' 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 = target =
Wiki::Commit.create_content!( Wiki::Commit.create_content!(
tag_name: target_tag_name, tag_name: target_tag_name,
@@ -535,8 +534,8 @@ RSpec.describe 'Wiki API', type: :request do
end end
it 'wiki title を変更すると対応する tag の version を作成する' do it 'wiki title を変更すると対応する tag の version を作成する' do
linked_tag_name = TagName.create!(name: 'wiki_linked_tag_for_version') linked_tag_name = create(:tag_name, name: 'wiki_linked_tag_for_version')
linked_tag = Tag.create!(tag_name: linked_tag_name, category: :general) linked_tag = create(:tag, tag_name: linked_tag_name, category: :general)
linked_page = linked_page =
Wiki::Commit.create_content!( Wiki::Commit.create_content!(
+1 -1
ファイルの表示
@@ -9,7 +9,7 @@ RSpec.describe 'Wiki title collision', type: :request do
def create_wiki_page title:, body: def create_wiki_page title:, body:
Wiki::Commit.create_content!( Wiki::Commit.create_content!(
tag_name: TagName.create!(name: title), tag_name: create(:tag_name, name: title),
body:, body:,
created_by_user: user, created_by_user: user,
message: 'init') message: 'init')
+1 -1
ファイルの表示
@@ -2,7 +2,7 @@ require 'rails_helper'
RSpec.describe MaterialSyncImporter do RSpec.describe MaterialSyncImporter do
let(:user) { create(:user, :member) } let(:user) { create(:user, :member) }
let(:tag) { Tag.create!(tag_name: TagName.create!(name: 'sync_tag'), category: :material) } let(:tag) { create(:tag, name: 'sync_tag', category: :material) }
def tempfile_for body def tempfile_for body
Tempfile.new(['material-sync-importer', '.png']).tap do |file| Tempfile.new(['material-sync-importer', '.png']).tap do |file|
+1 -1
ファイルの表示
@@ -65,7 +65,7 @@ RSpec.describe NicoTagVersionRecorder do
end end
it 'records sorted linked internal names and later link removal' do 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, name:, category: :general) }
tags.each { |tag| NicoTagRelation.create!(nico_tag: external_tag, tag:) } tags.each { |tag| NicoTagRelation.create!(nico_tag: external_tag, tag:) }
expect(record(:create).linked_tags).to eq('a_link z_link') expect(record(:create).linked_tags).to eq('a_link z_link')
+10 -1
ファイルの表示
@@ -1,6 +1,8 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe PostBulkCreator do RSpec.describe PostBulkCreator do
include_context 'English locale'
it 'limits workers to two and keeps failures in their request slots' do it 'limits workers to two and keeps failures in their request slots' do
actor = instance_double(User, id: 123) actor = instance_double(User, id: 123)
allow(User).to receive(:find).with(123) { allow(User).to receive(:find).with(123) {
@@ -9,6 +11,7 @@ RSpec.describe PostBulkCreator do
mutex = Mutex.new mutex = Mutex.new
active = 0 active = 0
maximum_active = 0 maximum_active = 0
creators = []
allow(PostCreatePreflight).to receive(:new) do |attributes:, **| allow(PostCreatePreflight).to receive(:new) do |attributes:, **|
preflight = instance_double(PostCreatePreflight) preflight = instance_double(PostCreatePreflight)
@@ -28,6 +31,7 @@ RSpec.describe PostBulkCreator do
end end
allow(PostCreator).to receive(:new) do |attributes:, **| allow(PostCreator).to receive(:new) do |attributes:, **|
creator = instance_double(PostCreator) creator = instance_double(PostCreator)
mutex.synchronize { creators << creator }
if attributes[:title] == 'broken' if attributes[:title] == 'broken'
allow(creator).to receive(:create!).and_raise(StandardError, 'broken') allow(creator).to receive(:create!).and_raise(StandardError, 'broken')
else else
@@ -45,7 +49,12 @@ RSpec.describe PostBulkCreator do
results = described_class.new( results = described_class.new(
actor:, actor:,
posts:, 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(maximum_active).to eq(2)
expect(results.length).to eq(posts.length) expect(results.length).to eq(posts.length)
+57 -7
ファイルの表示
@@ -2,7 +2,7 @@ require 'rails_helper'
RSpec.describe PostCreatePlan do RSpec.describe PostCreatePlan do
def create_tag! name, category def create_tag! name, category
Tag.create!(name:, category:) create(:tag, name:, category:)
end end
before do before do
@@ -10,6 +10,56 @@ RSpec.describe PostCreatePlan do
create_tag!('ニジラー情報不詳', :meta) create_tag!('ニジラー情報不詳', :meta)
end end
context 'with an explicit locale' do
include_context 'English locale'
it 'resolves the matching language identity and its parents' do
english_name = create(:tag_name, name: 'shared_name',
language_code: 'en', script_code: 'Latn')
english = create(:tag, tag_name: english_name, category: :character)
japanese = create(:tag, name: 'temporary_japanese', category: :general)
# The DB permits this identity; validation has its own contract spec.
japanese.tag_name.update_columns(name: 'shared_name')
english_parent = create(:tag, name: 'english_parent', category: :material)
japanese_parent = create(:tag, name: 'japanese_parent', category: :general)
TagImplication.create!(tag: english, parent_tag: english_parent)
TagImplication.create!(tag: japanese, parent_tag: japanese_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: english_parent.name, category: :material })
expect(plan[:post_tag_specs].pluck(:name)).not_to include(japanese_parent.name)
end
it 'resolves aliases within the requested language' do
japanese = create(:tag, name: 'japanese_canonical')
english_name = create(:tag_name, name: 'english_canonical',
language_code: 'en', script_code: 'Latn')
english = create(:tag, tag_name: english_name, category: :character)
japanese_alias = create(:tag_name, :alias, name: 'shared_alias',
canonical: japanese.tag_name)
english_alias = create(:tag_name, :alias, name: 'temporary_alias',
canonical: english_name,
language_code: 'en', script_code: 'Latn')
english_alias.update_columns(name: 'shared_alias')
expect(japanese_alias.reload).to have_attributes(
canonical_id: japanese.tag_name_id, tag_id: japanese.id,
primary_flg: false, language_code: 'ja')
expect(english_alias.reload).to have_attributes(
canonical_id: english_name.id, 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, category: :character }])
end
end
it 'plans direct and existing default tags without persisting records' do it 'plans direct and existing default tags without persisting records' do
counts = [TagName.count, Tag.count] counts = [TagName.count, Tag.count]
@@ -18,7 +68,7 @@ RSpec.describe PostCreatePlan do
url: 'https://example.com/post', url: 'https://example.com/post',
title: 'title', title: 'title',
tags: 'character:new_character', tags: 'character:new_character',
parent_post_ids: '' }).build! parent_post_ids: '' }).build!(Locale.nipponese)
expect(plan[:tags]).to eq('new_character') expect(plan[:tags]).to eq('new_character')
expect(plan[:direct_tag_specs]).to eq( expect(plan[:direct_tag_specs]).to eq(
@@ -31,7 +81,7 @@ RSpec.describe PostCreatePlan do
it 'resolves aliases and keeps tag sections separate from canonical names' do it 'resolves aliases and keeps tag sections separate from canonical names' do
canonical = create_tag!('虹夏', :character) canonical = create_tag!('虹夏', :character)
TagName.create!(name: 'にじか', canonical: canonical.tag_name) create(:tag_name, :alias, name: 'にじか', canonical: canonical.tag_name)
create_tag!('動画', :meta) create_tag!('動画', :meta)
plan = described_class.new( plan = described_class.new(
@@ -40,7 +90,7 @@ RSpec.describe PostCreatePlan do
title: 'video', title: 'video',
tags: '動画 にじか[0:10-0:20]', tags: '動画 にじか[0:10-0:20]',
duration: '1:00', 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[:tags].split).to include('動画', '虹夏[0:10-0:20]')
expect(plan[:display_tags]).to include( expect(plan[:display_tags]).to include(
@@ -55,7 +105,7 @@ RSpec.describe PostCreatePlan do
counts = [Tag.count, TagName.count, ExternalTag.count] counts = [Tag.count, TagName.count, ExternalTag.count]
expect { 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| }.to raise_error(ActiveRecord::RecordInvalid) { |error|
expect(error.record.errors[:tags]).to be_present expect(error.record.errors[:tags]).to be_present
} }
@@ -73,7 +123,7 @@ RSpec.describe PostCreatePlan do
url: 'https://example.com/post', url: 'https://example.com/post',
title: 'title', title: 'title',
tags: long_name, tags: long_name,
parent_post_ids: '' }).build! parent_post_ids: '' }).build!(Locale.nipponese)
}.to raise_error(ActiveRecord::RecordInvalid) { |error| }.to raise_error(ActiveRecord::RecordInvalid) { |error|
expect(error.record.errors[:tags]).not_to be_empty expect(error.record.errors[:tags]).not_to be_empty
} }
@@ -87,7 +137,7 @@ RSpec.describe PostCreatePlan do
title: 'title', title: 'title',
tags: 'ordinary_tag', tags: 'ordinary_tag',
duration: 'invalid', duration: 'invalid',
parent_post_ids: '' }).build! parent_post_ids: '' }).build!(Locale.nipponese)
expect(plan[:duration]).to eq('invalid') expect(plan[:duration]).to eq('invalid')
expect(plan[:video_ms]).to be_nil expect(plan[:video_ms]).to be_nil
+30 -4
ファイルの表示
@@ -14,7 +14,6 @@ RSpec.describe PostCreator do
end end
before do before do
allow(Tag).to receive(:normalise_tags!).and_return({ tags: [], sections: {} })
allow(TagVersioning).to receive(:record_tag_snapshots!) allow(TagVersioning).to receive(:record_tag_snapshots!)
allow(Tag).to receive(:expand_parent_tags).and_return([]) allow(Tag).to receive(:expand_parent_tags).and_return([])
allow(PostVersionRecorder).to receive(:record!) allow(PostVersionRecorder).to receive(:record!)
@@ -34,7 +33,7 @@ RSpec.describe PostCreator do
url: 'https://example.com/post', url: 'https://example.com/post',
thumbnail: real_thumbnail_upload, thumbnail: real_thumbnail_upload,
thumbnail_base: 'https://example.com/thumb.jpg', thumbnail_base: 'https://example.com/thumb.jpg',
tags: '' }).create! tags: '' }).create!(Locale.nipponese)
expect(post.thumbnail).to be_attached expect(post.thumbnail).to be_attached
expect(post.thumbnail_base).to eq('https://example.com/thumb.jpg') expect(post.thumbnail_base).to eq('https://example.com/thumb.jpg')
@@ -54,7 +53,7 @@ RSpec.describe PostCreator do
title: 'title', title: 'title',
url: 'https://example.com/post', url: 'https://example.com/post',
thumbnail_base: 'https://example.com/thumb.jpg', 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_base).to eq('https://example.com/thumb.jpg')
expect(post.thumbnail).to be_attached expect(post.thumbnail).to be_attached
@@ -72,7 +71,34 @@ RSpec.describe PostCreator do
tags: '' }) tags: '' })
post_count = Post.count 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) expect(Post.count).to eq(post_count)
end 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
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 end
+1 -1
ファイルの表示
@@ -64,7 +64,7 @@ RSpec.describe PostImportPreviewer do
end end
it 'applies metadata to automatic fields and recognises metadata tags' do it 'applies metadata to automatic fields and recognises metadata tags' do
Tag.create!(name: 'known-tag', category: :general) create(:tag, name: 'known-tag', category: :general)
allow(PostMetadataFetcher).to receive(:fetch).and_return( allow(PostMetadataFetcher).to receive(:fetch).and_return(
title: 'metadata title', title: 'metadata title',
thumbnail_base: 'https://example.com/thumb.jpg', thumbnail_base: 'https://example.com/thumb.jpg',
+5 -5
ファイルの表示
@@ -6,7 +6,7 @@ RSpec.describe Wiki::Commit do
def create_page title:, body: 'initial body' def create_page title:, body: 'initial body'
described_class.create_content!( described_class.create_content!(
tag_name: TagName.create!(name: title), tag_name: create(:tag_name, name: title),
body:, body:,
created_by_user: user, created_by_user: user,
message: 'init') message: 'init')
@@ -16,7 +16,7 @@ RSpec.describe Wiki::Commit do
it 'creates page, revision, and version with normalised body' do it 'creates page, revision, and version with normalised body' do
expect { expect {
described_class.create_content!( 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", body: "a\r\nb\r\n\r\n",
created_by_user: user, created_by_user: user,
message: 'init') message: 'init')
@@ -36,7 +36,7 @@ RSpec.describe Wiki::Commit do
end end
it 'rejects body that becomes blank after normalisation' do 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 { expect {
described_class.create_content!( described_class.create_content!(
@@ -76,8 +76,8 @@ RSpec.describe Wiki::Commit do
end end
it 'does not record tag_version on body-only wiki update' do it 'does not record tag_version on body-only wiki update' do
tag_name = TagName.create!(name: 'commit_integrity_linked_tag') tag_name = create(:tag_name, name: 'commit_integrity_linked_tag')
tag = Tag.create!(tag_name:, category: :general) tag = create(:tag, tag_name:, category: :general)
page = page =
described_class.create_content!( described_class.create_content!(
+5 -5
ファイルの表示
@@ -4,7 +4,7 @@ RSpec.describe Wiki::Commit do
let(:user) { create_member_user! } let(:user) { create_member_user! }
def create_page(title: 'commit_spec_page', body: 'initial body') 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!( Wiki::Commit.create_content!(
tag_name:, tag_name:,
@@ -80,8 +80,8 @@ RSpec.describe Wiki::Commit do
end end
it 'does not record tag version when corresponding tag has no versions' do it 'does not record tag version when corresponding tag has no versions' do
tag_name = TagName.create!(name: 'commit_linked_tag_without_versions') tag_name = create(:tag_name, name: 'commit_linked_tag_without_versions')
tag = Tag.create!(tag_name:, category: :general) tag = create(:tag, tag_name:, category: :general)
page = page =
described_class.create_content!( described_class.create_content!(
@@ -107,8 +107,8 @@ RSpec.describe Wiki::Commit do
end end
it 'does not record tag version when corresponding tag has no versions' do it 'does not record tag version when corresponding tag has no versions' do
tag_name = TagName.create!(name: 'commit_linked_tag_without_versions') tag_name = create(:tag_name, name: 'commit_linked_tag_without_versions')
tag = Tag.create!(tag_name:, category: :general) tag = create(:tag, tag_name:, category: :general)
page = page =
described_class.create_content!( described_class.create_content!(
+1 -1
ファイルの表示
@@ -5,7 +5,7 @@ RSpec.describe WikiVersionRecorder do
def create_page title:, body: 'body' def create_page title:, body: 'body'
Wiki::Commit.create_content!( Wiki::Commit.create_content!(
tag_name: TagName.create!(name: title), tag_name: create(:tag_name, name: title),
body:, body:,
created_by_user: user, created_by_user: user,
message: 'init') message: 'init')
+4 -2
ファイルの表示
@@ -172,7 +172,8 @@ RSpec.describe Youtube::Sync do
Tag.video Tag.video
Tag.no_deerjikist 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!( Deerjikist.create!(
platform: 'youtube', platform: 'youtube',
code: 'UC_MAPPED', code: 'UC_MAPPED',
@@ -226,7 +227,8 @@ RSpec.describe Youtube::Sync do
) )
PostTag.create!(post:, tag: Tag.no_deerjikist) 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!( Deerjikist.create!(
platform: 'youtube', platform: 'youtube',
code: 'UC_MAPPED_LATER', code: 'UC_MAPPED_LATER',
+13
ファイルの表示
@@ -0,0 +1,13 @@
RSpec.shared_context 'English locale' do
let!(:locale) do
Language.find_or_create_by!(code: 'en') { _1.name = 'English' }
Script.find_or_create_by!(code: 'Latn') { _1.name = 'Latin' }
unless Locale.exists?(code: 'en')
# Reference data only: do not generate names for unrelated existing tags.
Locale.insert_all!([
{ code: 'en', language_code: 'en', script_code: 'Latn',
name: 'English', created_at: Time.current }])
end
Locale.find('en')
end
end
+1 -1
ファイルの表示
@@ -8,7 +8,7 @@ RSpec.describe 'nico:sync' do
end end
def create_tag!(name, category:) 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 end
def link_nico_to_tag!(nico_tag, tag) def link_nico_to_tag!(nico_tag, tag)
+4 -4
ファイルの表示
@@ -6,10 +6,10 @@ RSpec.describe 'post_similarity:calc' do
it 'calculates similarities from active tags only' do it 'calculates similarities from active tags only' do
# 必要最低限のデータ # 必要最低限のデータ
t1 = Tag.create!(name: "t1") t1 = create(:tag, name: 't1')
t2 = Tag.create!(name: "t2") t2 = create(:tag, name: 't2')
t3 = Tag.create!(name: "t3") t3 = create(:tag, name: 't3')
deprecated_tag = Tag.create!(name: 'deprecated', deprecated_at: Time.current) deprecated_tag = create(:tag, name: 'deprecated', deprecated_at: Time.current)
p1 = Post.create!(url: "https://example.com/1") p1 = Post.create!(url: "https://example.com/1")
p2 = Post.create!(url: "https://example.com/2") 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 it 'calculates similarities for active tags only' do
# 必要最低限のデータ # 必要最低限のデータ
t1 = Tag.create!(name: "t1") t1 = create(:tag, name: 't1')
t2 = Tag.create!(name: "t2") t2 = create(:tag, name: 't2')
t3 = Tag.create!(name: "t3") t3 = create(:tag, name: 't3')
deprecated_tag = Tag.create!(name: 'deprecated', deprecated_at: Time.current) deprecated_tag = create(:tag, name: 'deprecated', deprecated_at: Time.current)
p1 = Post.create!(url: "https://example.com/1") p1 = Post.create!(url: "https://example.com/1")
p2 = Post.create!(url: "https://example.com/2") p2 = Post.create!(url: "https://example.com/2")