このコミットが含まれているのは:
2026-10-15 20:20:00 +09:00
コミット 1e464107a8
36個のファイルの変更、566行の追加、2082行の削除
-34
ファイルの表示
@@ -1,34 +0,0 @@
require 'rails_helper'
require_relative '../../db/migrate/20261003010000_migrate_tag_descriptions_from_wiki_pages'
RSpec.describe 'Tag description migration identity' do
it 'preserves every revision ID and repeated body in an independent sequence' do
user = create_member_user!
tag_name = create(:tag_name, name: 'description_migration')
page = Wiki::Commit.create_legacy_content!(
tag_name:, body: 'A', created_by_user: user)
revisions = [page.current_revision]
['A', 'B', 'B', 'A'].each do |body|
revisions << Wiki::Commit.legacy_content!(
page:, body:, created_user: user, base_revision_id: revisions.last.id)
end
tag = create(:tag, primary_tag_name: tag_name)
legacy_state = [page.reload.attributes, revisions.map(&:attributes)]
expect(page.wiki_versions.count).to eq(3)
migration = MigrateTagDescriptionsFromWikiPages.new
migration.suppress_messages { migration.up }
versions = TagDescriptionVersion.where(tag_id: tag.id, language_code: 'ja')
.order(:version_no)
expect(versions.pluck(:id)).to eq(revisions.map(&:id))
expect(versions.pluck(:version_no)).to eq([1, 2, 3, 4, 5])
expect(versions.pluck(:body)).to eq(['A', 'A', 'B', 'B', 'A'])
expect(versions.pluck(:created_by_user_id)).to eq([user.id] * 5)
expect(versions.pluck(:created_at)).to eq(revisions.map(&:created_at))
expect(TagDescription.find(page.id)).to have_attributes(
tag_id: tag.id, language_code: 'ja', version_no: 5, body: 'A')
expect([page.reload.attributes, revisions.map { |revision| revision.reload.attributes }])
.to eq(legacy_state)
end
end
-119
ファイルの表示
@@ -1,119 +0,0 @@
require 'rails_helper'
RSpec.describe 'Tag group migration identity and structure' do
include TagGroupRecords
let!(:user) { create_member_user! }
before { I18n.locale = :ja }
it 'preserves page and revision IDs, timestamps and repeated historical structures' do
tag = group_tag('洗澡鹿')
body = "説明文\n\n- 洗澡鹿\n- 洗澡鹿"
page = legacy_group('伊地知ニジカ亜種', body)
revisions = [page.current_revision]
[body, '# 空の区分', body].each do |text|
revisions << Wiki::Commit.legacy_content!(page:, body: text, created_user: user)
end
before_state = group_state.except(*TagGroupRecords::GROUP_TABLES)
migrate_groups
versions = group_rows('tag_group_versions', tag_group_id: page.id)
.sort_by { _1.fetch('version_no') }
expect(versions.map { _1.fetch('id') }).to eq(revisions.map(&:id))
expect(versions.map { _1.fetch('version_no') }).to eq([1, 2, 3, 4])
expect(versions.map { _1.fetch('created_at') }).to eq(revisions.map(&:created_at))
expect(versions.map { _1.fetch('created_by_user_id') }).to eq([user.id] * 4)
expect(versions.map { _1.fetch('event_type') }).to eq(['create', 'update', 'update', 'update'])
expect(group_version(page.id, 1).fetch('placements')).to eq(
group_version(page.id, 2).fetch('placements'))
expect(group_version(page.id, 3).fetch('placements')).to eq([
{ 'section_name' => '空の区分', 'description' => nil, 'children' => [] }])
expect_group_snapshot(
page.id, number: 4, description: '説明文',
members: [{ 'tag_id' => tag.id, 'version_no' => tag.version_no }],
placements: [{ 'tag_id' => tag.id, 'children' => [] }] * 2)
expect(group_rows('tag_groups', id: page.id).sole).to include(
'created_at' => page.created_at, 'updated_at' => page.reload.updated_at)
expect(group_state.except(*TagGroupRecords::GROUP_TABLES)).to eq(before_state)
end
it 'migrates nested list Sections and child groups as different placement kinds' do
series = group_tag('ぼざろクリーチャーシリーズ')
deer = group_tag('洗澡鹿')
nijika = group_tag('伊地知ニジカ')
child = legacy_group('伊地知ニジカ亜種', <<~MARKDOWN)
- ぼざろクリーチャーシリーズ
- 伊地知ニジカ亜種:
- 洗澡鹿
MARKDOWN
parent = legacy_group('ぼざろクリーチャーシリーズのキャラクター', <<~MARKDOWN)
- ぼざろクリーチャーシリーズ
- 伊地知ニジカ
- タググループ:伊地知ニジカ亜種
MARKDOWN
migrate_groups
expect_group_snapshot(
child.id, number: 1,
members: [series, deer].map { { 'tag_id' => _1.id, 'version_no' => _1.version_no } },
placements: [{ 'tag_id' => series.id, 'children' => [
{ 'section_name' => '伊地知ニジカ亜種', 'description' => nil,
'children' => [{ 'tag_id' => deer.id, 'children' => [] }] }] }])
expect_group_snapshot(
parent.id, number: 1,
members: [
{ 'tag_id' => series.id, 'version_no' => series.version_no },
{ 'tag_id' => nijika.id, 'version_no' => nijika.version_no },
{ 'tag_group_id' => child.id, 'version_no' => 1 }],
placements: [{ 'tag_id' => series.id, 'children' => [
{ 'tag_id' => nijika.id, 'children' => [
{ 'tag_group_id' => child.id, 'children' => [] }] }] }])
end
it 'preserves heading order, group paragraphs and Section descriptions' do
names = ['タグ希望', 'キャラクター名希望', '楽曲情報希望', 'bot操作', 'ニジラー情報不詳']
tags = names.map { group_tag(_1) }
markdown = <<~MARKDOWN
全体の説明
# 要求
要求の説明
- タグ希望
- キャラクター名希望
- 楽曲情報希望
# 情報
- bot操作
- ニジラー情報不詳
MARKDOWN
page = legacy_group('メタタグ', markdown)
expect(page.current_revision.body).to eq(markdown.chomp)
migrate_groups
leaves = tags.map { { 'tag_id' => _1.id, 'children' => [] } }
leaves.first['children'] = leaves[1..2]
expect_group_snapshot(
page.id, number: 1, description: '全体の説明',
members: tags.map { { 'tag_id' => _1.id, 'version_no' => _1.version_no } },
placements: [
{ 'section_name' => '要求', 'description' => '要求の説明',
'children' => [leaves.first] },
{ 'section_name' => '情報', 'description' => nil,
'children' => leaves[3..4] }])
end
it 'leaves Help in the legacy domain' do
page = Wiki::Commit.create_legacy_content!(
tag_name: create(:tag_name, name: 'Help:移行対象外'),
body: 'Help 本文', created_by_user: user)
before_state = group_state
migrate_groups
expect(group_state).to eq(before_state)
expect(page.reload.body).to eq('Help 本文')
end
end
+2
ファイルの表示
@@ -6,6 +6,8 @@ FactoryBot.define do
auto_generated { false } auto_generated { false }
sequence(:name) { |number| "tag-#{ SecureRandom.hex(4) }-#{ number }" } sequence(:name) { |number| "tag-#{ SecureRandom.hex(4) }-#{ number }" }
association :tag, primary_tag_name: nil
trait :alias do trait :alias do
primary_flg { false } primary_flg { false }
end end
+6 -7
ファイルの表示
@@ -4,7 +4,7 @@ FactoryBot.define do
primary_name { nil } primary_name { nil }
primary_tag_name do primary_tag_name do
attributes = primary_name.nil? ? { } : { name: primary_name } attributes = primary_name.nil? ? { } : { name: primary_name }
build(:tag_name, **attributes) build(:tag_name, tag: nil, **attributes)
end end
end end
@@ -12,17 +12,16 @@ FactoryBot.define do
post_count { 0 } post_count { 0 }
after(:build) do |tag, evaluator| after(:build) do |tag, evaluator|
next unless evaluator.primary_tag_name
evaluator.primary_tag_name.primary_flg = true evaluator.primary_tag_name.primary_flg = true
evaluator.primary_tag_name.tag = tag
tag.tag_names.target << evaluator.primary_tag_name tag.tag_names.target << evaluator.primary_tag_name
end end
before(:create) do |tag, evaluator|
evaluator.primary_tag_name.save!
# Compatibility write only; ownership is established through tag_id below.
tag.tag_name_id = evaluator.primary_tag_name.id
end
after(:create) do |tag, evaluator| after(:create) do |tag, evaluator|
next unless evaluator.primary_tag_name
evaluator.primary_tag_name.update!(tag:, primary_flg: true) evaluator.primary_tag_name.update!(tag:, primary_flg: true)
Locale.where(deprecated_at: nil).order(:language_code, :created_at, :script_code).to_a Locale.where(deprecated_at: nil).order(:language_code, :created_at, :script_code).to_a
-9
ファイルの表示
@@ -1,9 +0,0 @@
FactoryBot.define do
factory :wiki_page do
title { "TestPage" }
association :created_user, factory: :user
association :updated_user, factory: :user
body { ' ' }
end
end
+23 -70
ファイルの表示
@@ -1,84 +1,37 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe 'Shared Tag and ExternalTag IDs', type: :model do RSpec.describe 'Shared Tag and ExternalTag IDs', type: :model do
include TagGroupRecords it 'allocates distinct increasing IDs across the two tables' do
let(:user) { create_member_user! } tag = create(:tag)
external = create(:external_tag)
next_tag = create(:tag)
def create_resource resource expect(external.id).to be > tag.id
if resource == :tag expect(next_tag.id).to be > external.id
Tag.find_or_create_by_tag_name!(Locale.nipponese, 'shared_id_new', category: :general)
else
ExternalTag.create!(platform: :nico, name: 'shared_id_new')
end
end end
[:tag, :external_tag].each do |resource| it 'does not reuse physically deleted IDs in either table' do
it "starts #{ resource } at 1 when both tables are empty" do tag = create(:tag)
expect(Tag.count).to eq(0) external = create(:external_tag)
expect(ExternalTag.count).to eq(0) TagName.where(tag_id: tag.id).delete_all
expect(create_resource(resource).id).to eq(1) tag.destroy!
end external.destroy!
it "allocates #{ resource } above the other table's maximum" do expect(create(:tag).id).to be > external.id
create(:tag, id: resource == :tag ? 1 : 20) expect(create(:external_tag).id).to be > external.id
create(:external_tag, id: resource == :external_tag ? 1 : 20)
expect(create_resource(resource).id).to eq(21)
end
it "uses the current maximum after deleting earlier #{ resource } records" do
tag = create(:tag, id: 100)
TagName.where(tag_id: tag.id).update_all(tag_id: nil)
tag.destroy!
create(:external_tag, id: 200).destroy!
expect(Tag.unscoped.count).to eq(0)
expect(ExternalTag.unscoped.count).to eq(0)
expect(create_resource(resource).id).to eq(1)
end
it "preserves legacy overlaps and gaps when creating #{ resource }" do
[1, 2, 5].each { create(:tag, id: _1) }
[1, 3, 8].each { create(:external_tag, id: _1) }
tags = Tag.order(:id).map(&:attributes)
external_tags = ExternalTag.order(:id).map(&:attributes)
record = create_resource(resource)
aggregate_failures do
expect(record.id).to eq(9)
expect(Tag.where(id: [1, 2, 5]).order(:id).map(&:attributes)).to eq(tags)
expect(ExternalTag.where(id: [1, 3, 8]).order(:id).map(&:attributes)).to eq(external_tags)
expect(Tag.exists?(id: 3)).to be(false)
expect(ExternalTag.exists?(id: 2)).to be(false)
end
end
it "ignores the description and group ID space when creating #{ resource }" do
tag = create(:tag, id: 1)
create(:external_tag, id: 2)
TagDescription.create!(id: 100, tag:, language_code: 'ja', body: '', version_no: 1)
seed_group(name: 'separate_space', id: 200)
expect(create_resource(resource).id).to eq(3)
end
end end
it 'preserves overlapping IDs during updates without rewriting the other resource' do it 'retains existing overlapping IDs without renumbering on update or insert' do
tag = create(:tag, id: 5) tag = create(:tag)
external = create(:external_tag, id: 5) external = create(:external_tag)
external.update_columns(id: tag.id)
tag.update!(category: :meta) tag.update!(category: :meta)
external.update!(name: 'updated_external') external.update!(name: 'updated_external')
before_tag = tag.reload.attributes
before_external = external.reload.attributes
expect(tag.reload.id).to eq(5) expect(create(:tag).id).to be > tag.id
expect(external.reload.id).to eq(5) expect(tag.reload.attributes).to eq(before_tag)
expect(tag.category).to eq('meta') expect(external.reload.attributes).to eq(before_external)
expect(external.name).to eq('updated_external')
expect(Tag.count).to eq(1)
expect(ExternalTag.count).to eq(1)
end
it 'allocates distinct IDs for successive creations across both tables' do
records = [:tag, :external_tag].map { create_resource(_1) }
expect(records.map(&:id)).to eq([1, 2])
end end
end end
+23
ファイルの表示
@@ -0,0 +1,23 @@
require 'rails_helper'
RSpec.describe TagGroup, type: :model do
let(:actor) { create_member_user! }
def create_group name:, members: [], placements: []
TagGroupOperation.create!(name:, description: nil, members:, placements:, actor:)
end
it 'retains version IDs and snapshots after destroying an empty group' do
group = create_group(name: 'history owner')
versions = group.versions.order(:version_no).map(&:attributes)
expect { group.destroy! }.to change(described_class, :count).by(-1)
.and change(TagGroupVersion, :count).by(0)
expect(TagGroupVersion.where(tag_group_id: group.id).order(:version_no).map(&:attributes))
.to eq(versions)
expect(TagGroupRepr.historical(TagGroupVersion.find(versions.first.fetch('id'))))
.to include('id' => group.id, 'name' => 'history owner', 'members' => [])
end
end
-19
ファイルの表示
@@ -98,25 +98,6 @@ RSpec.describe TagNameSanitisationRule, type: :model do
end end
end end
context 'when the source tag_name has a tag and the existing one has no tag' do
let!(:existing) { create(:tag_name, name: 'foobar') }
let!(:source_tag) { create(:tag, primary_name: 'tmp', category: :general) }
let!(:source_tag_name_id) { primary_tag_name_for(source_tag, 'ja').id }
before do
primary_tag_name_for(source_tag, 'ja').update_columns(
name: 'foo_bar', updated_at: Time.current)
end
it 'moves the tag to the existing tag_name' do
described_class.apply!
expect(existing.reload).to have_attributes(
tag_id: source_tag.id, language_code: 'ja', primary_flg: true)
expect(primary_tag_name_for(source_tag, 'ja')).to eq(existing)
expect(primary_tag_name_for(source_tag.reload, 'ja')).to eq(existing)
expect(TagName.unscoped.exists?(source_tag_name_id)).to be(false)
end
end
context 'when the sanitised name is an alias of an existing tag' do context 'when the sanitised name is an alias of an existing tag' do
let!(:existing_tag) { create(:tag) } let!(:existing_tag) { create(:tag) }
+18 -16
ファイルの表示
@@ -22,16 +22,23 @@ RSpec.describe TagName, type: :model do
expect(duplicate.errors.of_kind?(:name, :taken)).to be(true) expect(duplicate.errors.of_kind?(:name, :taken)).to be(true)
end end
it 'creates Nipponese factory names without requiring a tag' do it 'creates Nipponese factory names with an owning tag' do
name = create(:tag_name) name = create(:tag_name)
expect(name.reload).to have_attributes( expect(name.reload).to have_attributes(
language_code: 'ja', script_code: 'Jpan', primary_flg: true, language_code: 'ja', script_code: 'Jpan', primary_flg: true,
auto_generated: false, tag_id: nil) auto_generated: false, tag_id: name.tag.id)
end
it 'rejects an ownerless name at model and database boundaries' do
name = build(:tag_name, tag: nil)
expect(name).to be_invalid
expect { name.save!(validate: false) }.to raise_error(ActiveRecord::NotNullViolation)
end end
it 'defaults ordinary persisted human names to confirmed provenance' do it 'defaults ordinary persisted human names to confirmed provenance' do
name = described_class.create!(name: 'ordinary_human_name', language_code: 'ja', name = described_class.create!(tag: create(:tag, primary_tag_name: nil),
name: 'ordinary_human_name', language_code: 'ja',
script_code: 'Jpan', primary_flg: true) script_code: 'Jpan', primary_flg: true)
expect(name.reload.auto_generated).to be(false) expect(name.reload.auto_generated).to be(false)
@@ -46,7 +53,7 @@ RSpec.describe TagName, type: :model do
expect(name.tag).to eq(tag) expect(name.tag).to eq(tag)
expect(described_class.find_by!(tag_id: tag.id, language_code: 'ja', primary_flg: true)) expect(described_class.find_by!(tag_id: tag.id, language_code: 'ja', primary_flg: true))
.to eq(name) .to eq(name)
expect(tag.reload.tag_name_id).to eq(name.id) expect(tag.reload.tag_name('ja')&.id).to eq(name.id)
end end
it 'creates a complete ordinary tag factory under the active Nipponese language' do it 'creates a complete ordinary tag factory under the active Nipponese language' do
@@ -96,19 +103,14 @@ RSpec.describe TagName, type: :model do
end end
describe '.canonicalise' do describe '.canonicalise' do
it 'leaves detached aliases unchanged' do
create(:tag_name, :alias, name: 'detached_alias')
expect(described_class.canonicalise(Locale.nipponese, ['detached_alias']))
.to eq(['detached_alias'])
end
it 'does not resolve a detached alias through another detached primary' do it 'resolves an alias to its owning primary name' do
create(:tag_name, :alias, name: 'detached_alias') tag = create(:tag, primary_name: 'canonical')
create(:tag_name, name: 'unrelated_detached_primary') create(:tag_name, :alias, tag:, name: 'alias')
expect(described_class.canonicalise(Locale.nipponese, ['detached_alias'])) expect(described_class.canonicalise(Locale.nipponese, ['alias', 'unknown']))
.to eq(['detached_alias']) .to eq(['canonical', 'unknown'])
end end
it 'resolves the primary name of the alias language on the same tag' do it 'resolves the primary name of the alias language on the same tag' do
@@ -119,7 +121,7 @@ RSpec.describe TagName, type: :model do
english_primary.update!(name: 'english_primary', auto_generated: false) english_primary.update!(name: 'english_primary', auto_generated: false)
english_alias = create(:tag_name, :alias, tag:, name: 'english_alias', english_alias = create(:tag_name, :alias, tag:, name: 'english_alias',
language_code: 'en', script_code: 'Latn') language_code: 'en', script_code: 'Latn')
representative_id = tag.tag_name_id representative_id = tag.tag_name('ja')&.id
primary = described_class.find_by!(tag_id: english_alias.tag_id, primary = described_class.find_by!(tag_id: english_alias.tag_id,
language_code: english_alias.language_code, language_code: english_alias.language_code,
@@ -128,7 +130,7 @@ RSpec.describe TagName, type: :model do
expect(primary).to eq(english_primary) expect(primary).to eq(english_primary)
expect(described_class.canonicalise(locale, [english_alias.name])) expect(described_class.canonicalise(locale, [english_alias.name]))
.to eq([english_primary.name]) .to eq([english_primary.name])
expect(tag.reload.tag_name_id).to eq(representative_id) expect(tag.reload.tag_name('ja')&.id).to eq(representative_id)
end end
it 'resolves only aliases in the requested language' do it 'resolves only aliases in the requested language' do
+6 -54
ファイルの表示
@@ -1,6 +1,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Tag, type: :model do RSpec.describe Tag, type: :model do
include WikiRecords
describe '#has_description?' do describe '#has_description?' do
let(:tag) { create(:tag) } let(:tag) { create(:tag) }
@@ -14,12 +15,6 @@ RSpec.describe Tag, type: :model do
expect(tag.has_description?(language_code: 'ja')).to be(false) expect(tag.has_description?(language_code: 'ja')).to be(false)
end end
it 'does not use a Wiki page as evidence of a description' do
user = create_member_user!
Wiki::Commit.create_legacy_content!(tag_name: tag.tag_name('ja'), body: 'legacy',
created_by_user: user)
expect(tag.has_description?(language_code: 'ja')).to be(false)
end
it 'reuses preloaded descriptions without reading Wiki or querying again' do it 'reuses preloaded descriptions without reading Wiki or querying again' do
TagDescription.create!(tag:, language_code: 'ja', body: '', version_no: 1) TagDescription.create!(tag:, language_code: 'ja', body: '', version_no: 1)
@@ -334,38 +329,7 @@ RSpec.describe Tag, type: :model do
.where(tag_names: { name: 'atomic_english_name' })).to be_empty .where(tag_names: { name: 'atomic_english_name' })).to be_empty
end end
it 'keeps a detached requested name detached if generation fails' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
detached = create(:tag_name, :alias, name: 'atomic_detached',
language_code: 'en', script_code: 'Latn')
original_tag_count = described_class.count
allow(TagName).to receive(:generate_name).and_raise('generation failed')
expect {
described_class.find_or_create_by_tag_name!(
locale, detached.name, category: :general)
}.to raise_error('generation failed')
expect(described_class.count).to eq(original_tag_count)
expect(detached.reload).to have_attributes(tag_id: nil, primary_flg: false)
end
it 'promotes a detached alias into the requested primary name' do
pending '日本語以外の Locale 有効化は別課題で実装する'
locale
detached = create(:tag_name, :alias, name: 'detached_english',
language_code: 'en', script_code: 'Latn')
tag = described_class.find_or_create_by_tag_name!(
locale, detached.name, category: :general)
expect(detached.reload).to have_attributes(
tag_id: tag.id, primary_flg: true, canonical_id: nil,
auto_generated: false)
expect(TagName.where(tag:, language_code: 'en', primary_flg: true))
.to contain_exactly(detached)
end
it 'creates separate tag identities for the same name in different languages' do it 'creates separate tag identities for the same name in different languages' do
pending '日本語以外の Locale 有効化は別課題で実装する' pending '日本語以外の Locale 有効化は別課題で実装する'
@@ -390,7 +354,7 @@ RSpec.describe Tag, type: :model do
pending '日本語以外の Locale 有効化は別課題で実装する' pending '日本語以外の Locale 有効化は別課題で実装する'
locale locale
tag = create(:tag, primary_name: '日本語代表名') tag = create(:tag, primary_name: '日本語代表名')
representative_id = tag.tag_name_id representative_id = tag.tag_name('ja')&.id
english = primary_tag_name_for(tag, 'en') english = primary_tag_name_for(tag, 'en')
english.update!(name: 'english_primary', auto_generated: false) english.update!(name: 'english_primary', auto_generated: false)
@@ -398,7 +362,7 @@ RSpec.describe Tag, type: :model do
locale, english.name, category: :general) locale, english.name, category: :general)
expect(found).to eq(tag) expect(found).to eq(tag)
expect(tag.reload.tag_name_id).to eq(representative_id) expect(tag.reload.tag_name('ja')&.id).to eq(representative_id)
expect(english.reload.tag_id).to eq(tag.id) expect(english.reload.tag_id).to eq(tag.id)
end end
@@ -430,22 +394,10 @@ RSpec.describe Tag, type: :model do
expect(primary_tag_name_for(tag, 'ja').auto_generated).to be(false) expect(primary_tag_name_for(tag, 'ja').auto_generated).to be(false)
end end
it 'promotes a detached Nipponese alias into a human-confirmed primary' do
detached = create(:tag_name, :alias, name: 'detached_nipponese')
tag = described_class.find_or_create_by_tag_name!(
Locale.nipponese, detached.name, category: :general)
expect(detached.reload).to have_attributes(
tag_id: tag.id, primary_flg: true, canonical_id: nil,
auto_generated: false)
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true))
.to contain_exactly(detached)
end
it 'reuses the owning tag for an alias without changing its category' do it 'reuses the owning tag for an alias without changing its category' do
tag = create(:tag, category: :character) tag = create(:tag, category: :character)
representative_id = tag.tag_name_id representative_id = tag.tag_name('ja')&.id
alias_name = create(:tag_name, :alias, name: 'lookup_alias', tag:) alias_name = create(:tag_name, :alias, name: 'lookup_alias', tag:)
expect(alias_name.reload).to have_attributes( expect(alias_name.reload).to have_attributes(
tag_id: tag.id, tag_id: tag.id,
@@ -459,7 +411,7 @@ RSpec.describe Tag, type: :model do
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) expect(tag.reload.tag_name('ja')&.id).to eq(representative_id)
end end
it 'reuses the owning tag through another alias' do it 'reuses the owning tag through another alias' do
@@ -683,7 +635,7 @@ RSpec.describe Tag, type: :model do
let!(:wiki_page) do let!(:wiki_page) do
admin = create_admin_user! admin = create_admin_user!
Wiki::Commit.create_content!( seed_description(
tag_name: source_tag_name, tag_name: source_tag_name,
body: 'source wiki body', body: 'source wiki body',
created_by_user: admin, created_by_user: admin,
+7 -8
ファイルの表示
@@ -222,28 +222,27 @@ RSpec.describe 'Materials API', type: :request do
expect(queries).to be_empty expect(queries).to be_empty
end end
it 'reads the description even when the legacy revision has different content' do it 'reads the description even when the stored version has different content' do
page = seed_wiki_page(tag_name: tag.tag_name('ja'), body: 'legacy body', page = seed_description(tag_name: tag.tag_name('ja'), body: 'legacy body',
created_by_user: member_user) created_by_user: member_user)
TagDescription.find_by!(tag_id: tag.id, language_code: 'ja') TagDescription.find_by!(tag_id: tag.id, language_code: 'ja')
.update!(body: 'description body') .update!(body: 'description body')
get_material_without_legacy_reads get_material_without_legacy_reads
expect(json.fetch('wiki_page_body')).to eq('description body') expect(json.fetch('wiki_page_body')).to eq('description body')
expect(page.current_revision.body).to eq('legacy body') expect(current_description_version(page).body).to eq('legacy body')
end end
it 'uses tag_id rather than description ID without a Wiki page or TagName' do it 'uses tag_id rather than description ID without a Wiki page or TagName' do
description = TagDescription.create!(id: tag.id + 1000, tag:, language_code: 'ja', description = TagDescription.create!(tag:, language_code: 'ja',
body: 'independent body', version_no: 1) body: 'independent body', version_no: 1)
tag.tag_names.update_all(tag_id: nil) tag.tag_names.delete_all
get_material_without_legacy_reads get_material_without_legacy_reads
expect(description.id).not_to eq(tag.id)
expect(json.fetch('wiki_page_body')).to eq('independent body') expect(json.fetch('wiki_page_body')).to eq('independent body')
expect(json.fetch('tag').fetch('has_wiki')).to be(true) expect(json.fetch('tag').fetch('has_wiki')).to be(true)
end end
it 'returns nil when only legacy Wiki content exists' do it 'returns nil when only description history remains' do
seed_wiki_page(tag_name: tag.tag_name('ja'), body: 'legacy only', seed_description(tag_name: tag.tag_name('ja'), body: 'legacy only',
created_by_user: member_user) created_by_user: member_user)
TagDescription.where(tag_id: tag.id).delete_all TagDescription.where(tag_id: tag.id).delete_all
get_material_without_legacy_reads get_material_without_legacy_reads
+7 -83
ファイルの表示
@@ -4,34 +4,21 @@ RSpec.describe 'Tag description Wiki compatibility', type: :request do
include WikiRecords include WikiRecords
let!(:user) { create_member_user! } let!(:user) { create_member_user! }
let!(:tag_name) { create(:tag_name, name: 'description_contract') } let!(:tag_name) { create(:tag_name, name: 'description_contract') }
let!(:page) do let!(:tag) { tag_name.tag }
seed_wiki_page(
tag_name:, body: 'old-revision', created_by_user: user)
end
let!(:revisions) do
first = page.current_revision
second = Wiki::Commit.legacy_content!(page:, body: 'old-second', created_user: user)
third = Wiki::Commit.legacy_content!(page:, body: 'old-third', created_user: user)
[first, second, third]
end
let!(:tag) { create(:tag, primary_tag_name: tag_name) }
let!(:versions) do let!(:versions) do
revisions.each_with_index.map do |revision, index| ['A', 'A', 'new-version'].each_with_index.map do |body, index|
TagDescriptionVersion.create!( TagDescriptionVersion.create!(
id: revision.id, tag_id: tag.id, language_code: 'ja', tag_id: tag.id, language_code: 'ja', version_no: index + 1,
version_no: index + 1, event_type: index.zero? ? 'create' : 'update', event_type: index.zero? ? 'create' : 'update', body:,
body: index == 2 ? 'new-version' : 'A', created_by_user_id: user.id, created_at: Time.current)
created_by_user_id: user.id, created_at: revision.created_at)
end end
end end
let!(:description) do let!(:description) do
TagDescription.create!( TagDescription.create!(
id: page.id, tag:, language_code: 'ja', version_no: 3, body: 'new', tag:, language_code: 'ja', version_no: 3, body: 'new',
created_by_user_id: user.id, updated_by_user_id: user.id) created_by_user_id: user.id, updated_by_user_id: user.id)
end end
before { page.update_columns(body: 'old') }
# Observe real SELECTs, including cached queries, only during the request. # Observe real SELECTs, including cached queries, only during the request.
def without_legacy_reads def without_legacy_reads
queries = [] queries = []
@@ -70,7 +57,7 @@ RSpec.describe 'Tag description Wiki compatibility', type: :request do
it 'does not fall back to a revision absent from new history' do it 'does not fall back to a revision absent from new history' do
versions.first.destroy! versions.first.destroy!
without_legacy_reads do without_legacy_reads do
get "/wiki/#{ description.id }", params: { version: revisions.first.id } get "/wiki/#{ description.id }", params: { version: versions.first.id }
end end
expect(response).to have_http_status(:not_found) expect(response).to have_http_status(:not_found)
end end
@@ -132,16 +119,6 @@ RSpec.describe 'Tag description Wiki compatibility', type: :request do
'pred' => versions.first.id, 'succ' => versions.last.id) 'pred' => versions.first.id, 'succ' => versions.last.id)
end end
it 'ignores changes to same-title legacy content and revision metadata' do
get "/wiki/title/#{ tag_name.name }"
original = json
page.update_columns(body: 'changed legacy body')
revisions.last.update_columns(message: 'legacy message', created_at: 1.day.ago)
revisions.last.wiki_lines.update_all(body: 'changed legacy revision')
get "/wiki/title/#{ tag_name.name }"
expect(json).to eq(original)
expect(json.fetch('body')).to eq('new')
end
end end
describe 'exists' do describe 'exists' do
@@ -155,28 +132,6 @@ RSpec.describe 'Tag description Wiki compatibility', type: :request do
expect(response).to have_http_status(:no_content) expect(response).to have_http_status(:no_content)
end end
context 'without a legacy page' do
before do
WikiVersion.where(wiki_page_id: page.id).delete_all
page.destroy!
end
it 'recognises the description ID alone' do
get "/wiki/#{ description.id }/exists"
expect(response).to have_http_status(:no_content)
end
it 'recognises the TagName alone' do
get "/wiki/title/#{ tag_name.name }/exists"
expect(response).to have_http_status(:no_content)
end
it 'shows the description by title' do
get "/wiki/title/#{ tag_name.name }"
expect(response).to have_http_status(:ok)
expect(json).to include('body' => 'new', 'revision_id' => versions.last.id)
end
end
end end
describe 'diff' do describe 'diff' do
@@ -229,39 +184,8 @@ RSpec.describe 'Tag description Wiki compatibility', type: :request do
'title' => tag_name.name) 'title' => tag_name.name)
end end
it 'ignores legacy-only revisions and metadata changes' do
get '/wiki/changes', params: { id: description.id }
original = json
revisions.first.update_columns(message: 'legacy changed', created_at: 1.day.ago)
WikiRevision.create!(
wiki_page: page, created_user: user, kind: :content,
tree_sha256: Digest::SHA256.hexdigest('legacy only'), lines_count: 0)
get '/wiki/changes', params: { id: description.id }
expect(json).to eq(original)
expect(json.map { |row| row.fetch('revision_id') }).to eq(versions.reverse.map(&:id))
end
end end
describe 'mixed changes' do
it 'excludes unmigrated history and migrated legacy-only revisions' do
legacy_page = seed_wiki_page(
tag_name: create(:tag_name, name: 'Help_mixed'),
body: 'help', created_by_user: user)
legacy_revision = legacy_page.current_revision
extra = WikiRevision.create!(
wiki_page: page, created_user: user, kind: :content,
tree_sha256: Digest::SHA256.hexdigest('legacy only'), lines_count: 0)
get '/wiki/changes'
expect(response).to have_http_status(:ok)
migrated = json.select { |row| row.fetch('wiki_page').fetch('id') == description.id }
expect(migrated.map { |row| row.fetch('revision_id') }).to eq(versions.reverse.map(&:id))
expect(json.map { |row| row.fetch('revision_id') }).not_to include(extra.id)
expect(json).not_to include(include(
'revision_id' => legacy_revision.id,
'wiki_page' => include('id' => legacy_page.id, 'title' => 'Help_mixed')))
end
end
describe 'index' do describe 'index' do
[nil, 'description_contract'].each do |title| [nil, 'description_contract'].each do |title|
+59 -247
ファイルの表示
@@ -1,280 +1,92 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe 'Tag description Wiki dual writes', type: :request do RSpec.describe 'Tag description Wiki facade writes', type: :request do
include WikiRecords include WikiRecords
let!(:user) { create_member_user! } let!(:user) { create_member_user! }
let!(:tag_name) { create(:tag_name, name: 'description_write') } let!(:tag) { create(:tag, primary_name: 'description_write') }
let(:tag) { create(:tag, primary_tag_name: tag_name) }
let(:headers) { { 'X-Transfer-Code' => user.inheritance_code } } let(:headers) { { 'X-Transfer-Code' => user.inheritance_code } }
def stored_state def stored_state
[WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion, [Tag, TagName, TagVersion, TagDescription, TagDescriptionVersion].map do |model|
TagName, TagVersion].map do |model|
model.order(:id).map(&:attributes) model.order(:id).map(&:attributes)
end end
end end
# Fail after a real INSERT/UPDATE, exercising rollback rather than a mock write. it 'creates a native description and exposes its version ID as revision ID' do
def fail_save model
failure = Class.new(StandardError)
callback = ->(_record) { raise failure, 'injected persistence failure' }
model.set_callback(:save, :after, callback)
expect { yield }.to raise_error(failure)
ensure
model.skip_callback(:save, :after, callback)
end
describe 'POST /wiki' do
before { tag }
def create_description
post '/wiki', params: { title: tag_name.name, body: 'initial', message: 'create' },
headers: headers
end
it 'creates a description with matching Wiki content and histories' do
expect { create_description }
.to change(WikiPage, :count).by(1)
.and change(WikiRevision, :count).by(1)
.and change(WikiVersion, :count).by(1)
.and change(TagDescription, :count).by(1)
.and change(TagDescriptionVersion, :count).by(1)
expect(response).to have_http_status(:created)
description = TagDescription.find_by!(tag_id: tag.id, language_code: 'ja')
expect(description).to have_attributes(body: 'initial', version_no: 1)
page = WikiPage.find_by!(tag_name:)
expect(page.body).to eq(description.body)
expect(page.current_revision.body).to eq(description.body)
expect(page.wiki_versions.sole.body).to eq(description.body)
expect(json.fetch('id')).to eq(description.id)
expect(TagDescriptionVersion.find_by!(tag_id: tag.id, language_code: 'ja'))
.to have_attributes(body: 'initial', version_no: 1, created_by_user_id: user.id)
end
it 'reuses an existing compatibility page and synchronises its histories' do
page = Wiki::Commit.create_legacy_content!(
tag_name:, body: 'legacy', created_by_user: user)
expect { create_description }
.to change(WikiPage, :count).by(0)
.and change(WikiRevision, :count).by(1)
.and change(WikiVersion, :count).by(1)
expect(response).to have_http_status(:created)
description = TagDescription.find_by!(tag_id: tag.id)
expect(page.reload.body).to eq(description.body)
expect(TagDescriptionVersion.find(page.current_revision.id).body).to eq(page.body)
end
['missing_description', 'orphan_description'].each do |title|
it "rejects the bare title #{ title } without writing any resource" do
create(:tag_name, name: title) if title == 'orphan_description'
before_state = stored_state
post '/wiki', params: { title:, body: 'initial' }, headers: headers
expect(response).to have_http_status(:unprocessable_entity)
expect(json).to include('type' => 'validation_error')
expect(stored_state).to eq(before_state)
end
end
[false, true].each do |linked|
it "rejects namespaced creation with Tag present: #{ linked }" do
name = create(:tag_name, name: 'Help:frozen')
create(:tag, primary_tag_name: name) if linked
before_state = stored_state
post '/wiki', params: { title: name.name, body: 'legacy' }, headers: headers
expect(response.status).to be_between(400, 499)
expect(stored_state).to eq(before_state)
end
end
[WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion].each do |model|
it "rolls back new description creation when #{ model.name } persistence fails" do
before_state = stored_state
aggregate_failures do
fail_save(model) { create_description }
expect(stored_state).to eq(before_state)
end
end
end
end
it 'creates missing compatibility data without writing an ID-colliding Help page' do
description = TagDescription.create!(
id: 500, tag:, language_code: 'ja', body: 'before', version_no: 1)
help = WikiPage.create!(id: description.id,
tag_name: create(:tag_name, name: 'Help:unrelated'),
body: 'unrelated', created_user: user, updated_user: user)
Wiki::Commit.legacy_content!(page: help, body: help.body, created_user: user)
before_help = help.attributes
expect do expect do
put "/wiki/#{ description.id }", params: { title: tag_name.name, body: 'after' }, post '/wiki', params: { title: tag.name('ja'), body: 'initial' }, headers: headers
headers: headers end.to change(TagDescription, :count).by(1)
end.to change(WikiPage, :count).by(1) .and change(TagDescriptionVersion, :count).by(1)
.and change(WikiRevision, :count).by(1) expect(response).to have_http_status(:created)
.and change(WikiVersion, :count).by(1) description = TagDescription.find(json.fetch('id'))
expect(response).to have_http_status(:ok) expect(description).to have_attributes(tag_id: tag.id, body: 'initial', version_no: 1)
expect(description.reload).to have_attributes(id: 500, body: 'after') get "/wiki/#{ description.id }"
expect(help.reload.attributes).to eq(before_help) expect(json.fetch('revision_id')).to eq(current_description_version(description).id)
compatibility = WikiPage.find_by!(tag_name:)
expect(compatibility.body).to eq(description.body)
expect(compatibility.current_revision.body).to eq(description.body)
end end
describe 'PUT /wiki/:id' do context 'with an existing description' do
let!(:page) do
tag
seed_wiki_page(
tag_name:, body: 'before', created_by_user: user)
end
let!(:first_revision) { page.current_revision }
let!(:description) do let!(:description) do
TagDescription.find_by!(tag_id: tag.id, language_code: 'ja') TagDescriptionOperation.create!(tag:, body: 'before', created_by_user: user)
end end
let!(:first_version) { current_description_version(description) }
def update_description body: 'after', title: tag_name.name, base: first_revision.id def update_description body: 'after', title: tag.name('ja'), base: first_version.id
put "/wiki/#{ page.id }", put "/wiki/#{ description.id }", params: { title:, body:, base_revision_id: base },
params: { title:, body:, message: 'edit', base_revision_id: base },
headers: headers headers: headers
end end
it 'increments the independent description sequence and shares revision identity' do it 'records a body change without changing resource identity or old history' do
expect(page.reload.version_no).to eq(1) snapshot = first_version.attributes
expect(description.version_no).to eq(1) expect { update_description }.to change(TagDescriptionVersion, :count).by(1)
expect { update_description }
.to change(WikiPage, :count).by(0)
.and change(WikiRevision, :count).by(1)
.and change(WikiVersion, :count).by(1)
.and change(TagDescriptionVersion, :count).by(1)
expect(response).to have_http_status(:ok)
revision = page.reload.current_revision
version = TagDescriptionVersion.find(revision.id)
expect(description.reload).to have_attributes(body: 'after', version_no: 2)
expect(version).to have_attributes(
body: 'after', version_no: 2, event_type: 'update', created_by_user_id: user.id)
expect(page).to have_attributes(body: 'after', version_no: 2)
expect(revision.body).to eq(version.body)
expect(page.wiki_versions.order(:version_no).last.body).to eq(version.body)
end
it 'uses description identity even when its Wiki TagName no longer owns the Tag' do
tag_name.update_columns(tag_id: nil)
expect { update_description }
.to change(WikiPage, :count).by(0)
.and change(WikiRevision, :count).by(1)
.and change(WikiVersion, :count).by(1)
.and change(TagDescriptionVersion, :count).by(1)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(description.reload).to have_attributes(body: 'after', version_no: 2) expect(description.reload).to have_attributes(body: 'after', version_no: 2)
expect(TagDescriptionVersion.find(page.reload.current_revision.id)) expect(first_version.reload.attributes).to eq(snapshot)
.to have_attributes(tag_id: tag.id, body: 'after', version_no: 2) expect(current_description_version(description)).to have_attributes(
body: 'after', event_type: 'update', created_by_user_id: user.id)
end end
it 'does not add body history for a title-only update' do ['before', "before\r\n\r\n"].each do |body|
revision_count = WikiRevision.count it "does not write for normalised-equivalent body #{ body.inspect }" do
wiki_version_count = WikiVersion.count
description_version_count = TagDescriptionVersion.count
wiki_version_no = page.reload.version_no
description_version_no = description.version_no
current_revision_id = page.current_revision.id
page_state = page.attributes
description_state = description.attributes
update_description(body: 'before', title: 'description_renamed')
expect(response).to have_http_status(:ok)
expect(tag_name.reload.name).to eq('description_renamed')
expect(WikiRevision.count).to eq(revision_count)
expect(WikiVersion.count).to eq(wiki_version_count)
expect(TagDescriptionVersion.count).to eq(description_version_count)
expect(page.reload).to have_attributes(body: 'before', version_no: wiki_version_no)
expect(page.current_revision.id).to eq(current_revision_id)
expect(page.reload.attributes).to eq(page_state)
expect(description.reload).to have_attributes(
body: 'before', version_no: description_version_no)
expect(description.reload.attributes).to eq(description_state)
end
it 'does not add body history for a TagName rename through tags' do
revision_count = WikiRevision.count
wiki_version_count = WikiVersion.count
description_version_count = TagDescriptionVersion.count
page_version_no = page.reload.version_no
current_revision_id = page.current_revision.id
patch "/tags/#{ tag.id }",
params: { name: 'description_tag_renamed' },
headers: headers
expect(response).to have_http_status(:ok)
expect(tag_name.reload.name).to eq('description_tag_renamed')
expect(WikiRevision.count).to eq(revision_count)
expect(WikiVersion.count).to eq(wiki_version_count)
expect(TagDescriptionVersion.count).to eq(description_version_count)
expect(page.reload.version_no).to eq(page_version_no)
expect(page.current_revision.id).to eq(current_revision_id)
expect(description.reload).to have_attributes(
body: 'before', version_no: 1)
end
it 'keeps both systems and the title unchanged on a stale revision conflict' do
update_description(body: 'current body', base: first_revision.id)
expect(response).to have_http_status(:ok)
stale_revision_id = first_revision.id
before_state = stored_state
update_description(
body: 'conflicting body',
title: 'conflicting_rename',
base: stale_revision_id)
expect(response).to have_http_status(:conflict)
expect(stored_state).to eq(before_state)
expect(tag_name.reload.name).not_to eq('conflicting_rename')
end
[WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion].each do |model|
it "rolls back both systems when #{ model.name } persistence fails" do
before_state = stored_state before_state = stored_state
aggregate_failures do update_description(body:)
fail_save(model) do expect(response).to have_http_status(:ok)
update_description(body: 'rollback body', title: 'rollback_rename') expect(stored_state).to eq(before_state)
end
expect(stored_state).to eq(before_state)
end
end end
end end
[['identical body', 'before'], it 'records a title-only edit in Tag history without adding body history' do
['normalised-equivalent body', "before\n\n"]].each do |label, body| snapshot = description.attributes
it "does not write histories for #{ label }" do expect { update_description(body: 'before', title: 'renamed') }
before_state = stored_state .to change(TagVersion, :count).by(2)
page_state = page.reload.attributes .and change(TagDescriptionVersion, :count).by(0)
description_state = description.reload.attributes expect(response).to have_http_status(:ok)
current_revision_id = page.current_revision.id expect(tag.reload.name('ja')).to eq('renamed')
wiki_lines = WikiLine.order(:id).map(&:attributes) expect(description.reload.attributes).to eq(snapshot)
revision_lines = WikiRevisionLine.order(:id).map(&:attributes) end
page_version_no = page.version_no
description_version_no = description.version_no
update_description(body:)
['conflicting body', 'after'].each do |body|
it "rejects a stale revision even for #{ body } without a partial rename" do
update_description
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(WikiRevision.count).to eq(before_state[1].length) before_state = stored_state
expect(WikiVersion.count).to eq(before_state[2].length) update_description(body:, title: 'conflicting rename')
expect(TagDescriptionVersion.count).to eq(before_state[4].length) expect(response).to have_http_status(:conflict)
expect(page.reload.version_no).to eq(page_version_no) expect(stored_state).to eq(before_state)
expect(description.reload.version_no).to eq(description_version_no) end
expect(page.current_revision.id).to eq(current_revision_id) end
expect(page.reload.attributes).to eq(page_state)
expect(description.reload.attributes).to eq(description_state) [TagDescription, TagDescriptionVersion].each do |model|
expect(WikiLine.order(:id).map(&:attributes)).to eq(wiki_lines) it "rolls back the rename and body after #{ model.name } persistence fails" do
expect(WikiRevisionLine.order(:id).map(&:attributes)).to eq(revision_lines) before_state = stored_state
failure = Class.new(StandardError)
callback = ->(_record) { raise failure }
model.set_callback(:save, :after, callback)
begin
expect { update_description(title: 'rollback rename') }.to raise_error(failure)
expect(stored_state).to eq(before_state)
ensure
model.skip_callback(:save, :after, callback)
end
end end
end end
end end
+20 -68
ファイルの表示
@@ -1,6 +1,8 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe 'Tag description native API', type: :request do RSpec.describe 'Tag description native API', type: :request do
include WikiRecords
include TagGroupRecords
let!(:author) { create_member_user! } let!(:author) { create_member_user! }
let(:editor) { create(:user, :member, name: 'description editor') } let(:editor) { create(:user, :member, name: 'description editor') }
let(:tag_name) { create(:tag_name, name: 'native_description') } let(:tag_name) { create(:tag_name, name: 'native_description') }
@@ -9,8 +11,7 @@ RSpec.describe 'Tag description native API', type: :request do
let(:endpoint) { "/tags/#{ tag.id }/description" } let(:endpoint) { "/tags/#{ tag.id }/description" }
def stored_state def stored_state
[WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion, [TagDescription, TagDescriptionVersion, TagName, TagVersion].map do |model|
WikiLine, WikiRevisionLine, TagName, TagVersion].map do |model|
model.order(:id).map(&:attributes) model.order(:id).map(&:attributes)
end end
end end
@@ -84,7 +85,7 @@ RSpec.describe 'Tag description native API', type: :request do
describe 'native reads' do describe 'native reads' do
let!(:description) do let!(:description) do
TagDescription.create!( TagDescription.create!(
id: tag.id + 1_000_000, tag:, language_code: 'ja', body: 'native current', tag:, language_code: 'ja', body: 'native current',
version_no: 1, created_by_user_id: author.id, updated_by_user_id: editor.id) version_no: 1, created_by_user_id: author.id, updated_by_user_id: editor.id)
end end
let!(:version) do let!(:version) do
@@ -95,15 +96,14 @@ RSpec.describe 'Tag description native API', type: :request do
let(:name_endpoint) { "/tags/name/#{ tag_name.name }/description" } let(:name_endpoint) { "/tags/name/#{ tag_name.name }/description" }
it 'reads the current resource by Tag ID without a WikiPage' do it 'reads the current resource by Tag ID without a WikiPage' do
expect(description.id).not_to eq(tag.id)
expect(WikiPage.exists?(id: description.id)).to be(false)
without_legacy_reads { native_get(endpoint) } without_legacy_reads { native_get(endpoint) }
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect_current(description) expect_current(description)
end end
it 'does not interpret a compatibility handle as a Tag ID' do it 'does not interpret a compatibility handle as a Tag ID' do
other_tag = create(:tag, id: description.id, primary_name: 'handle_collision') other_tag = create(:tag, primary_name: 'handle_collision')
description.update_columns(id: other_tag.id)
other = TagDescription.create!( other = TagDescription.create!(
tag: other_tag, language_code: 'ja', body: 'other current', version_no: 1, tag: other_tag, language_code: 'ja', body: 'other current', version_no: 1,
created_by_user_id: author.id, updated_by_user_id: author.id) created_by_user_id: author.id, updated_by_user_id: author.id)
@@ -153,32 +153,6 @@ RSpec.describe 'Tag description native API', type: :request do
expect_current(description) expect_current(description)
end end
[:id, :name].each do |lookup|
it "ignores divergent legacy body and history for #{ lookup } lookup" do
page = WikiPage.create!(
id: description.id, tag_name:, body: 'old current',
created_user: author, updated_user: author)
revision = WikiRevision.create!(
wiki_page: page, created_user: author, kind: :content,
lines_count: 1, tree_sha256: Digest::SHA256.hexdigest('old revision'))
line = WikiLine.create!(body: 'old revision', sha256: revision.tree_sha256)
WikiRevisionLine.create!(wiki_revision: revision, wiki_line: line, position: 0)
WikiVersionRecorder.record!(page:, event_type: :create, created_by_user: author)
path = lookup == :id ? endpoint : name_endpoint
action = lookup == :id ? 'show' : 'show_by_title'
without_legacy_reads { native_get(path, action:) }
expect(response).to have_http_status(:ok)
expect_current(description)
original = json
page.update_columns(body: 'changed old current')
line.update_columns(body: 'changed old revision')
revision.update_columns(message: 'changed old history', created_at: 1.day.ago)
without_legacy_reads { native_get(path, action:) }
expect(response).to have_http_status(:ok)
expect(json).to eq(original)
end
end
end end
describe 'missing native resources' do describe 'missing native resources' do
@@ -206,44 +180,24 @@ RSpec.describe 'Tag description native API', type: :request do
context 'without an existing description' do context 'without an existing description' do
before { tag } before { tag }
it 'creates a description with synchronised Wiki content and histories' do
unrelated = Wiki::Commit.create_legacy_content!( it 'creates native history that is immediately available through the facade' do
tag_name: create(:tag_name, name: 'Help:unrelated_native_description'),
body: 'unrelated body', created_by_user: author)
unrelated_state = [unrelated.attributes,
unrelated.wiki_revisions.map(&:attributes),
unrelated.wiki_versions.map(&:attributes)]
expect { native_write(body: 'created natively') } expect { native_write(body: 'created natively') }
.to change(TagDescription, :count).by(1) .to change(TagDescription, :count).by(1)
.and change(TagDescriptionVersion, :count).by(1) .and change(TagDescriptionVersion, :count).by(1)
.and change(WikiPage, :count).by(1)
.and change(WikiRevision, :count).by(1)
.and change(WikiVersion, :count).by(1)
expect(response).to have_http_status(:created) expect(response).to have_http_status(:created)
description = TagDescription.find_by!(tag_id: tag.id, language_code: 'ja') description = TagDescription.find_by!(tag_id: tag.id, language_code: 'ja')
page = WikiPage.find_by!(tag_name: tag.tag_name('ja'))
revision = page.current_revision
version = TagDescriptionVersion.find(revision.id)
expect(description).to have_attributes(
body: 'created natively', version_no: 1,
created_by_user_id: editor.id, updated_by_user_id: editor.id)
expect(version).to have_attributes(
tag_id: tag.id, language_code: 'ja', body: description.body,
version_no: 1, event_type: 'create', created_by_user_id: editor.id)
expect(page).to have_attributes(tag_name_id: tag_name.id, body: description.body)
expect(revision.body).to eq(version.body)
expect(page.wiki_versions.last.body).to eq(version.body)
unrelated.reload
expect([unrelated.attributes, unrelated.wiki_revisions.map(&:attributes),
unrelated.wiki_versions.map(&:attributes)]).to eq(unrelated_state)
expect_current(description) expect_current(description)
version = current_description_version(description)
expect(version).to have_attributes(
body: 'created natively', version_no: 1, event_type: 'create',
created_by_user_id: editor.id)
get "/wiki/#{ description.id }" get "/wiki/#{ description.id }"
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(json).to include('body' => description.body, 'revision_id' => version.id) expect(json).to include('body' => description.body, 'revision_id' => version.id)
end end
[WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion].each do |model| [TagDescription, TagDescriptionVersion].each do |model|
it "rolls back native create when #{ model.name } persistence fails" do it "rolls back native create when #{ model.name } persistence fails" do
headers headers
before_state = stored_state before_state = stored_state
@@ -258,7 +212,7 @@ RSpec.describe 'Tag description native API', type: :request do
context 'with an existing description' do context 'with an existing description' do
let!(:page) do let!(:page) do
tag tag
Wiki::Commit.create_content!(tag_name:, body: 'before', created_by_user: author) seed_description(tag_name:, body: 'before', created_by_user: author)
end end
let!(:description) do let!(:description) do
TagDescription.find_by!(tag_id: tag.id, language_code: 'ja') TagDescription.find_by!(tag_id: tag.id, language_code: 'ja')
@@ -268,22 +222,20 @@ RSpec.describe 'Tag description native API', type: :request do
expect(page.reload.version_no).to eq(1) expect(page.reload.version_no).to eq(1)
expect(description.version_no).to eq(1) expect(description.version_no).to eq(1)
expect { native_write(body: 'after') } expect { native_write(body: 'after') }
.to change(WikiRevision, :count).by(1) .to change(TagDescriptionVersion, :count).by(1)
.and change(WikiVersion, :count).by(1)
.and change(TagDescriptionVersion, :count).by(1)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(description.reload).to have_attributes( expect(description.reload).to have_attributes(
body: 'after', version_no: 2, body: 'after', version_no: 2,
created_by_user_id: author.id, updated_by_user_id: editor.id) created_by_user_id: author.id, updated_by_user_id: editor.id)
expect(page.reload).to have_attributes(body: 'after', version_no: 2) expect(page.reload).to have_attributes(body: 'after', version_no: 2)
revision = page.current_revision revision = current_description_version(page)
version = TagDescriptionVersion.find(revision.id) version = TagDescriptionVersion.find(revision.id)
expect(version).to have_attributes( expect(version).to have_attributes(
tag_id: tag.id, language_code: 'ja', version_no: 2, event_type: 'update', tag_id: tag.id, language_code: 'ja', version_no: 2, event_type: 'update',
body: 'after', created_by_user_id: editor.id) body: 'after', created_by_user_id: editor.id)
expect(revision.body).to eq(version.body) expect(revision.body).to eq(version.body)
expect(page.wiki_versions.order(:version_no).last.body).to eq(version.body) expect(description_versions(page).order(:version_no).last.body).to eq(version.body)
get "/wiki/#{ description.id }" get "/wiki/#{ description.id }"
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(json).to include('body' => 'after', 'revision_id' => version.id) expect(json).to include('body' => 'after', 'revision_id' => version.id)
@@ -294,7 +246,7 @@ RSpec.describe 'Tag description native API', type: :request do
before_state = stored_state before_state = stored_state
page_state = page.reload.attributes page_state = page.reload.attributes
description_state = description.reload.attributes description_state = description.reload.attributes
current_revision_id = page.current_revision.id current_revision_id = current_description_version(page).id
native_write(body:) native_write(body:)
@@ -302,11 +254,11 @@ RSpec.describe 'Tag description native API', type: :request do
expect(stored_state).to eq(before_state) expect(stored_state).to eq(before_state)
expect(page.reload.attributes).to eq(page_state) expect(page.reload.attributes).to eq(page_state)
expect(description.reload.attributes).to eq(description_state) expect(description.reload.attributes).to eq(description_state)
expect(page.current_revision.id).to eq(current_revision_id) expect(current_description_version(page).id).to eq(current_revision_id)
end end
end end
[WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion].each do |model| [TagDescription, TagDescriptionVersion].each do |model|
it "rolls back native update when #{ model.name } persistence fails" do it "rolls back native update when #{ model.name } persistence fails" do
headers headers
before_state = stored_state before_state = stored_state
+11 -145
ファイルの表示
@@ -7,21 +7,17 @@ RSpec.describe 'Tag group Wiki facade reads', type: :request do
let!(:user) { create_member_user! } let!(:user) { create_member_user! }
let!(:old_tag) { group_tag('旧タグ') } let!(:old_tag) { group_tag('旧タグ') }
let!(:new_tag) { group_tag('新タグ') } let!(:new_tag) { group_tag('新タグ') }
let!(:page) { legacy_group('読取り契約', '- 旧タグ') } let!(:page) { create_markdown_group('読取り契約', '- 旧タグ') }
let!(:first_revision) { page.current_revision } let!(:first_revision) { page.versions.sole }
let!(:last_revision) do let!(:last_revision) do
Wiki::Commit.legacy_content!(page:, body: '- 新タグ', created_user: user) parsed = TagGroupMarkdownParser.parse(body: '- 新タグ')
TagGroupOperation.update!(
group: page, name: page.name, actor: user,
**parsed.slice(:description, :members, :placements))
page.versions.order(:version_no).last
end end
before do before { I18n.locale = :ja }
I18n.locale = :ja
migrate_groups
page.update_columns(body: 'legacy stale current')
page.wiki_revisions.each do |revision|
revision.wiki_lines.update_all(body: 'legacy stale revision')
end
page.wiki_versions.update_all(body: 'legacy stale version')
end
[:id, :title].each do |lookup| [:id, :title].each do |lookup|
context "with #{ lookup } lookup" do context "with #{ lookup } lookup" do
@@ -71,36 +67,13 @@ RSpec.describe 'Tag group Wiki facade reads', type: :request do
end end
it 'combines Tag Groups and Tag Descriptions without Help in the unfiltered list' do it 'combines Tag Groups and Tag Descriptions without Help in the unfiltered list' do
help = seed_wiki_page(
tag_name: create(:tag_name, name: 'Help:混在'), body: 'help', created_by_user: user)
TagDescriptionOperation.create!(tag: new_tag, body: 'tag description', created_by_user: user) TagDescriptionOperation.create!(tag: new_tag, body: 'tag description', created_by_user: user)
get '/wiki' get '/wiki'
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(json.map { _1.fetch('title') }).to include( expect(json.map { _1.fetch('title') }).to include(
'タググループ:読取り契約', '新タグ') 'タググループ:読取り契約', '新タグ')
expect(json.map { _1.fetch('title') }).not_to include(help.title)
end end
it 'excludes Help when its ID collides with a new group' do
help = seed_wiki_page(
tag_name: create(:tag_name, name: 'Help:ID重複'), body: 'help body',
created_by_user: user)
seed_group(name: '別 ID重複', id: help.id, description: 'group body')
get '/wiki'
expect(response).to have_http_status(:ok)
expect(json.map { _1.fetch('title') }).to include('タググループ:別 ID重複')
expect(json.map { _1.fetch('title') }).not_to include(help.title)
get "/wiki/title/#{ CGI.escapeURIComponent(help.title) }"
expect(response).to have_http_status(:not_found)
get '/wiki/changes'
expect(response).to have_http_status(:ok)
expect(json.map { _1.dig('wiki_page', 'title') }).to include(
'タググループ:別 ID重複')
expect(json.map { _1.dig('wiki_page', 'title') }).not_to include(help.title)
end
it 'finds groups by a partial prefix of their displayed title' do it 'finds groups by a partial prefix of their displayed title' do
get '/wiki', params: { title: 'タググル' } get '/wiki', params: { title: 'タググル' }
@@ -112,8 +85,9 @@ RSpec.describe 'Tag group Wiki facade reads', type: :request do
end end
it 'does not restore a stale namespaced Wiki page after a group rename' do it 'does not restore a stale namespaced Wiki page after a group rename' do
old_page = legacy_group('旧名称', '- 旧本文') group = create_markdown_group('旧名称', '正本')
seed_group(name: '新名称', id: old_page.id, description: '正本') TagGroupOperation.update!(
group:, name: '新名称', description: '正本', members: [], placements: [], actor: user)
get group_title_path('旧名称') get group_title_path('旧名称')
expect(response).to have_http_status(:not_found) expect(response).to have_http_status(:not_found)
@@ -125,12 +99,8 @@ RSpec.describe 'Tag group Wiki facade reads', type: :request do
end end
it 'orders mixed resource history by timestamp rather than independent IDs' do it 'orders mixed resource history by timestamp rather than independent IDs' do
help = seed_wiki_page(
tag_name: create(:tag_name, name: 'Help:時系列'), body: 'help',
created_by_user: user)
TagDescriptionOperation.create!(tag: new_tag, body: 'description', created_by_user: user) TagDescriptionOperation.create!(tag: new_tag, body: 'description', created_by_user: user)
TagGroupVersion.update_all(created_at: 3.days.ago) TagGroupVersion.update_all(created_at: 3.days.ago)
WikiRevision.where(wiki_page_id: help.id).update_all(created_at: 2.days.ago)
TagDescriptionVersion.update_all(created_at: 1.day.ago) TagDescriptionVersion.update_all(created_at: 1.day.ago)
get '/wiki/changes' get '/wiki/changes'
@@ -139,7 +109,6 @@ RSpec.describe 'Tag group Wiki facade reads', type: :request do
timestamps = json.map { Time.iso8601(_1.fetch('timestamp')) } timestamps = json.map { Time.iso8601(_1.fetch('timestamp')) }
expect(timestamps).to eq(timestamps.sort.reverse) expect(timestamps).to eq(timestamps.sort.reverse)
titles = json.map { _1.dig('wiki_page', 'title') } titles = json.map { _1.dig('wiki_page', 'title') }
expect(titles).not_to include(help.title)
expect(titles.index('新タグ')).to be < titles.index('タググループ:読取り契約') expect(titles.index('新タグ')).to be < titles.index('タググループ:読取り契約')
end end
@@ -169,19 +138,6 @@ RSpec.describe 'Tag group Wiki facade reads', type: :request do
'wiki_page' => include('id' => page.id, 'title' => 'タググループ:読取り契約')) 'wiki_page' => include('id' => page.id, 'title' => 'タググループ:読取り契約'))
end end
it 'includes group history but excludes all legacy-only revisions' do
extra = Wiki::Commit.legacy_content!(page:, body: 'legacy only', created_user: user)
help = seed_wiki_page(
tag_name: create(:tag_name, name: 'Help:変更一覧'), body: 'help', created_by_user: user)
get '/wiki/changes'
expect(response).to have_http_status(:ok)
rows = json.select { _1.fetch('wiki_page').fetch('title') == 'タググループ:読取り契約' }
expect(rows.map { _1.fetch('revision_id') }).to eq([last_revision.id, first_revision.id])
expect(rows.map { _1.fetch('revision_id') }).not_to include(extra.id)
expect(json).not_to include(include(
'revision_id' => help.current_revision.id,
'wiki_page' => include('title' => help.title)))
end
[false, true].each do |explicit| [false, true].each do |explicit|
it "diffs structured snapshots with #{ explicit ? 'explicit' : 'current' } target" do it "diffs structured snapshots with #{ explicit ? 'explicit' : 'current' } target" do
@@ -215,95 +171,5 @@ RSpec.describe 'Tag group Wiki facade reads', type: :request do
expect(json.fetch('body').strip).to eq('独立した説明') expect(json.fetch('body').strip).to eq('独立した説明')
end end
context 'when Help, description and group share the same numeric ID' do
let(:colliding_help) do
seed_wiki_page(
tag_name: create(:tag_name, name: 'Help:三者ID衝突'),
body: 'help', created_by_user: user)
end
let(:colliding_group_id) do
seed_group(name: '三者ID衝突', id: colliding_help.id, description: 'group')
end
let(:colliding_description) do
TagDescription.create!(
id: colliding_help.id, tag: new_tag, language_code: 'ja',
body: 'description', version_no: 1)
end
let(:colliding_description_version) do
TagDescriptionVersion.create!(
tag_id: new_tag.id, language_code: 'ja', version_no: 1,
event_type: 'create', body: 'description', created_by_user_id: user.id,
created_at: Time.current)
end
before do
colliding_group_id
colliding_description
colliding_description_version
end
it 'lists both new resources sharing a numeric ID without legacy Help' do
get '/wiki'
expect(response).to have_http_status(:ok)
rows = json.select { _1.fetch('id') == colliding_help.id }
expect(rows.map { [_1.fetch('id'), _1.fetch('title')] }).to contain_exactly(
[colliding_description.id, '新タグ'],
[colliding_group_id, 'タググループ:三者ID衝突'])
end
it 'lists each resource history without deduplicating the shared resource ID' do
get '/wiki/changes'
expect(response).to have_http_status(:ok)
rows = json.select { _1.fetch('wiki_page').fetch('id') == colliding_help.id }
histories = rows.map do |row|
[row.fetch('revision_id'), row.fetch('wiki_page').fetch('title')]
end
expect(histories).to contain_exactly(
[colliding_description_version.id, '新タグ'],
[group_version(colliding_group_id, 1).fetch('id'), 'タググループ:三者ID衝突'])
end
it 'still excludes legacy copies of an actually migrated group and its history' do
extra = Wiki::Commit.legacy_content!(page:, body: 'legacy only', created_user: user)
get '/wiki'
expect(response).to have_http_status(:ok)
rows = json.select { _1.fetch('id') == page.id }
expect(rows.map { _1.fetch('title') }).to eq(['タググループ:読取り契約'])
get '/wiki/changes'
expect(response).to have_http_status(:ok)
rows = json.select { _1.fetch('wiki_page').fetch('id') == page.id }
expect(rows.map { _1.fetch('revision_id') })
.to contain_exactly(first_revision.id, last_revision.id)
expect(rows.map { _1.fetch('revision_id') }).not_to include(extra.id)
expect(rows.map { _1.fetch('wiki_page').fetch('title') })
.to all(eq('タググループ:読取り契約'))
end
end
it 'resolves new resources by title while excluding colliding legacy Help' do
help = seed_wiki_page(
tag_name: create(:tag_name, name: 'Help:ID衝突'), body: 'help', created_by_user: user)
seed_group(name: 'ID衝突', id: help.id, description: 'group')
TagDescription.create!(
id: help.id, tag: new_tag, language_code: 'ja', body: 'description', version_no: 1)
TagDescriptionVersion.create!(
tag_id: new_tag.id, language_code: 'ja', version_no: 1,
event_type: 'create', body: 'description', created_at: Time.current)
{ 'タググループ:ID衝突' => 'group', '新タグ' => 'description' }
.each do |title, body|
get "/wiki/title/#{ CGI.escapeURIComponent(title) }"
expect(response).to have_http_status(:ok)
expect(json.fetch('body').strip).to eq(body)
end
without_group_legacy_reads do
get "/wiki/title/#{ CGI.escapeURIComponent(help.title) }"
end
expect(response).to have_http_status(:not_found)
end
end end
+24 -33
ファイルの表示
@@ -33,11 +33,10 @@ RSpec.describe 'Tag group Wiki facade writes', type: :request do
end end
end end
it 'creates the group, snapshot and matching compatibility histories' do it 'creates the group, snapshot and native version history' do
expect { create_group } expect { create_group }
.to change(WikiPage, :count).by(1) .to change(TagGroup, :count).by(1)
.and change(WikiRevision, :count).by(1) .and change(TagGroupVersion, :count).by(1)
.and change(WikiVersion, :count).by(1)
expect(response).to have_http_status(:created) expect(response).to have_http_status(:created)
group = group_rows('tag_groups', name: '新規').sole group = group_rows('tag_groups', name: '新規').sole
expect(json).to include('id' => group.fetch('id'), 'title' => 'タググループ:新規') expect(json).to include('id' => group.fetch('id'), 'title' => 'タググループ:新規')
@@ -45,10 +44,9 @@ RSpec.describe 'Tag group Wiki facade writes', type: :request do
version = group_version(group.fetch('id'), 1) version = group_version(group.fetch('id'), 1)
expect(version).to include('event_type' => 'create', 'created_by_user_id' => user.id) expect(version).to include('event_type' => 'create', 'created_by_user_id' => user.id)
expect(group).to include('created_by_user_id' => user.id, 'updated_by_user_id' => user.id) expect(group).to include('created_by_user_id' => user.id, 'updated_by_user_id' => user.id)
page = WikiPage.joins(:tag_name).find_by!(tag_names: { name: 'タググループ:新規' }) get group_title_path('新規')
expect(page.body).to eq('- 洗澡鹿') expect(response).to have_http_status(:ok)
expect(page.current_revision.body).to eq(page.body) expect(json.fetch('body')).to eq('- 洗澡鹿')
expect(page.wiki_versions.sole.body).to eq(page.body)
end end
it 'keeps repeated placements but only one membership and one snapshot member' do it 'keeps repeated placements but only one membership and one snapshot member' do
@@ -108,10 +106,9 @@ RSpec.describe 'Tag group Wiki facade writes', type: :request do
end end
context 'updating a migrated group' do context 'updating a migrated group' do
let!(:page) { legacy_group('更新', '- 洗澡鹿') } let!(:page) { create_markdown_group('更新', '- 洗澡鹿') }
let!(:old_revision) { page.current_revision } let!(:old_revision) { page.versions.order(:version_no).last }
before { migrate_groups }
def update_group body: '- 伊地知ニジカ', title: 'タググループ:更新', request_headers: headers def update_group body: '- 伊地知ニジカ', title: 'タググループ:更新', request_headers: headers
put "/wiki/#{ page.id }", params: { title:, body: }, headers: request_headers put "/wiki/#{ page.id }", params: { title:, body: }, headers: request_headers
@@ -119,9 +116,9 @@ RSpec.describe 'Tag group Wiki facade writes', type: :request do
[1, 3].each do |repetitions| [1, 3].each do |repetitions|
it "keeps identical Markdown unchanged after #{ repetitions } Wiki PUTs" do it "keeps identical Markdown unchanged after #{ repetitions } Wiki PUTs" do
body = page.body body = group_markdown(page)
travel_to(1.hour.from_now) do travel_to(1.hour.from_now) do
expect_noop_group_update(group_id: page.id, page:) do expect_noop_group_update(group_id: page.id) do
repetitions.times do repetitions.times do
update_group(body:) update_group(body:)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
@@ -134,13 +131,13 @@ RSpec.describe 'Tag group Wiki facade writes', type: :request do
it 'keeps identical Markdown unchanged with a stale base_revision_id' do it 'keeps identical Markdown unchanged with a stale base_revision_id' do
update_group update_group
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
body = page.reload.body body = group_markdown(page)
expect(page.current_revision.id).not_to eq(old_revision.id) expect(page.versions.order(:version_no).last.id).not_to eq(old_revision.id)
travel_to(1.hour.from_now) do travel_to(1.hour.from_now) do
expect_noop_group_update(group_id: page.id, page:) do expect_noop_group_update(group_id: page.id) do
put "/wiki/#{ page.id }", put "/wiki/#{ page.id }",
params: { title: page.title, body:, base_revision_id: old_revision.id }, params: { title: "タググループ:#{ page.name }", body:, base_revision_id: old_revision.id },
headers: headers headers: headers
end end
end end
@@ -148,9 +145,9 @@ RSpec.describe 'Tag group Wiki facade writes', type: :request do
it 'keeps the original editor on identical Markdown from another member' do it 'keeps the original editor on identical Markdown from another member' do
editor = create(:user, :member) editor = create(:user, :member)
body = page.body body = group_markdown(page)
travel_to(1.hour.from_now) do travel_to(1.hour.from_now) do
expect_noop_group_update(group_id: page.id, page:) do expect_noop_group_update(group_id: page.id) do
update_group(body:, request_headers: { 'X-Transfer-Code' => editor.inheritance_code }) update_group(body:, request_headers: { 'X-Transfer-Code' => editor.inheritance_code })
end end
end end
@@ -168,11 +165,11 @@ RSpec.describe 'Tag group Wiki facade writes', type: :request do
page.id, number: 2, page.id, number: 2,
members: [{ 'tag_id' => other_tag.id, 'version_no' => other_tag.version_no }], members: [{ 'tag_id' => other_tag.id, 'version_no' => other_tag.version_no }],
placements: changed_placements) placements: changed_placements)
body = page.reload.body body = group_markdown(page)
expect(body).to eq('- 伊地知ニジカ') expect(body).to eq('- 伊地知ニジカ')
travel_to(1.hour.from_now) do travel_to(1.hour.from_now) do
expect_noop_group_update(group_id: page.id, page:) { update_group(body:) } expect_noop_group_update(group_id: page.id) { update_group(body:) }
end end
end end
@@ -187,16 +184,13 @@ RSpec.describe 'Tag group Wiki facade writes', type: :request do
expect(group_version(page.id, 1)).to eq(old) expect(group_version(page.id, 1)).to eq(old)
expect(group_version(page.id, 2)).to include( expect(group_version(page.id, 2)).to include(
'event_type' => 'update', 'created_by_user_id' => user.id) 'event_type' => 'update', 'created_by_user_id' => user.id)
expect(page.reload.body.strip).to eq('- 伊地知ニジカ') expect(group_markdown(page).strip).to eq('- 伊地知ニジカ')
expect(page.wiki_revisions.count).to eq(2) expect(page.versions.count).to eq(2)
expect(page.wiki_versions.count).to eq(2)
expect(page.current_revision.body).to eq(page.body)
expect(page.wiki_versions.order(:version_no).last.body).to eq(page.body)
end end
it 'accepts the current base_revision_id used by the existing frontend' do it 'accepts the current base_revision_id used by the existing frontend' do
put "/wiki/#{ page.id }", put "/wiki/#{ page.id }",
params: { title: page.title, body: '- 伊地知ニジカ', params: { title: "タググループ:#{ page.name }", body: '- 伊地知ニジカ',
base_revision_id: old_revision.id }, headers: headers base_revision_id: old_revision.id }, headers: headers
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(group_rows('tag_groups', id: page.id).sole.fetch('version_no')).to eq(2) expect(group_rows('tag_groups', id: page.id).sole.fetch('version_no')).to eq(2)
@@ -207,12 +201,12 @@ RSpec.describe 'Tag group Wiki facade writes', type: :request do
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
put "/wiki/#{ page.id }", put "/wiki/#{ page.id }",
params: { title: page.title, body: '- 洗澡鹿', params: { title: "タググループ:#{ page.name }", body: '- 洗澡鹿',
base_revision_id: old_revision.id }, headers: headers base_revision_id: old_revision.id }, headers: headers
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(group_rows('tag_groups', id: page.id).sole.fetch('version_no')).to eq(3) expect(group_rows('tag_groups', id: page.id).sole.fetch('version_no')).to eq(3)
expect(page.reload.body.strip).to eq('- 洗澡鹿') expect(group_markdown(page).strip).to eq('- 洗澡鹿')
end end
it 'records the editor and update time' do it 'records the editor and update time' do
@@ -257,9 +251,7 @@ RSpec.describe 'Tag group Wiki facade writes', type: :request do
let!(:page) do let!(:page) do
next unless operation == :update next unless operation == :update
legacy = legacy_group('更新', '- 洗澡鹿') create_markdown_group('更新', '- 洗澡鹿')
seed_group(name: '更新', id: legacy.id, members:, placements:)
legacy
end end
let!(:child_id) { seed_group(name: '子グループ', description: '子') } let!(:child_id) { seed_group(name: '子グループ', description: '子') }
@@ -296,7 +288,6 @@ RSpec.describe 'Tag group Wiki facade writes', type: :request do
end end
tables = TagGroupRecords::GROUP_TABLES tables = TagGroupRecords::GROUP_TABLES
tables += ['wiki_pages', 'wiki_revisions', 'wiki_versions']
tables.each do |table| tables.each do |table|
it "rolls back all writes after a real #{ table } write fails" do it "rolls back all writes after a real #{ table } write fails" do
expect_atomic_group_write(table) { write_group(operation, headers) } expect_atomic_group_write(table) { write_group(operation, headers) }
+18 -145
ファイルの表示
@@ -24,11 +24,6 @@ RSpec.describe 'Tag group write integrity', type: :request do
put "/wiki/#{ id }", params: { title:, body:, base_revision_id: base }, headers: headers put "/wiki/#{ id }", params: { title:, body:, base_revision_id: base }, headers: headers
end end
def create_help body
seed_wiki_page(
tag_name: create(:tag_name, name: 'Help:既存'), body:, created_by_user: user)
end
def expect_no_writes def expect_no_writes
before_state = group_state before_state = group_state
yield yield
@@ -37,66 +32,28 @@ RSpec.describe 'Tag group write integrity', type: :request do
end end
context 'resource identity' do context 'resource identity' do
it 'rejects removing the prefix from a migrated group atomically' do
page = legacy_group('更新', '- 洗澡鹿')
seed_group(name: '更新', id: page.id, members:, placements:)
expect_no_writes do
put_wiki(page.id, title: '更新', base: page.current_revision.id)
end
end
it 'rejects an unprefixed update to an independent group' do it 'rejects an unprefixed update to an independent group' do
id = seed_group(name: '独立', members:, placements:) id = seed_group(name: '独立', members:, placements:)
expect_no_writes { put_wiki(id, title: '独立') } expect_no_writes { put_wiki(id, title: '独立') }
end end
it 'does not revive legacy writes when the compatibility title is stale' do
page = legacy_group('更新', '- 洗澡鹿')
seed_group(name: '更新', id: page.id, members:, placements:)
page.tag_name.update!(name: '古い別名')
expect_no_writes { put_wiki(page.id, title: '古い別名') } it 'renames the group and its facade title through the operation' do
end page = create_markdown_group('更新', '- 洗澡鹿')
it 'renames the group and its compatibility page through the operation' do
page = legacy_group('更新', '- 洗澡鹿')
seed_group(name: '更新', id: page.id, members:, placements:)
put_wiki(page.id, title: 'タググループ:改名') put_wiki(page.id, title: 'タググループ:改名')
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(TagGroup.find(page.id)).to have_attributes(name: '改名', version_no: 2) expect(TagGroup.find(page.id)).to have_attributes(name: '改名', version_no: 2)
expect(page.reload.title).to eq('タググループ:改名') expect("タググループ:#{ page.reload.name }").to eq('タググループ:改名')
expect(page.body).to eq('- 伊地知ニジカ') expect(group_markdown(page)).to eq('- 伊地知ニジカ')
end end
it 'rejects a standalone Help update despite a colliding group ID' do
page = create_help('Help body')
id = seed_group(name: '別グループ', id: page.id, members:, placements:)
group_before = TagGroup.find(id).attributes
version_before = group_version(id, 1)
expect_no_writes { put_wiki(id, title: 'Help:改名', body: 'Help after') }
expect(page.reload.body).to eq('Help body')
expect(page.title).to eq('Help:既存')
expect(TagGroup.find(id).attributes).to eq(group_before)
expect(group_version(id, 1)).to eq(version_before)
end
it 'rejects an ambiguous unprefixed rename when Help and group IDs collide' do
page = create_help('Help body')
seed_group(name: '更新', id: page.id, members:, placements:)
expect_no_writes { put_wiki(page.id, title: '更新') }
expect_no_writes do
put_wiki(page.id, title: '更新', base: page.current_revision.id)
end
end
it 'keeps a description update separate from a colliding group' do it 'keeps a description update separate from a colliding group' do
page = Wiki::Commit.create_content!( page = seed_description(
tag_name: tag.tag_name('ja'), body: 'description before', created_by_user: user) tag_name: tag.tag_name('ja'), body: 'description before', created_by_user: user)
description = TagDescription.find_by!(tag_id: tag.id) description = TagDescription.find_by!(tag_id: tag.id)
id = seed_group(name: '更新', id: description.id, members:, placements:) id = seed_group(name: '更新', id: description.id, members:, placements:)
@@ -110,106 +67,22 @@ RSpec.describe 'Tag group write integrity', type: :request do
expect(TagGroup.find(id).attributes).to eq(group_before) expect(TagGroup.find(id).attributes).to eq(group_before)
end end
it 'does not route a colliding Help write into an unrelated description' do end
page = create_help('Help before')
id = seed_group(name: '更新', id: page.id, members:, placements:)
description = TagDescription.create!(
id:, tag:, language_code: 'ja', version_no: 1, body: 'description')
expect_no_writes { put_wiki(id, title: 'Help:既存', body: 'Help after') }
expect(page.reload.body).to eq('Help before') it 'rolls back a facade create when its saved structure cannot be rendered' do
expect(description.reload.body).to eq('description') allow(TagGroupMarkdownRenderer).to receive(:render)
expect(TagGroup.find(id).version_no).to eq(1) .and_raise(TagGroupMarkdownRenderer::UnsupportedStructure, 'unsupported')
put_wiki(id, title: '洗澡鹿', body: 'description after') expect_no_writes do
expect(response).to have_http_status(:ok) post '/wiki', params: { title: 'タググループ:新規', body: '- 洗澡鹿' }, headers: headers
expect(description.reload.body).to eq('description after')
put_wiki(id, title: 'タググループ:更新')
expect(response).to have_http_status(:ok)
expect(TagGroup.find(id).version_no).to eq(2)
expect(page.reload.body).to eq('Help before')
expect(description.reload.body).to eq('description after')
end end
end end
context 'compatibility synchronisation' do it 'rolls back a facade update when its saved structure cannot be rendered' do
it 'synchronises a native rename without modifying an ID-colliding Help page' do id = seed_group(name: '更新', members:, placements:)
help = create_help('Help before') allow(TagGroupMarkdownRenderer).to receive(:render)
id = seed_group(name: '更新', id: help.id, members:, placements:) .and_raise(TagGroupMarkdownRenderer::UnsupportedStructure, 'unsupported')
help_before = help.attributes expect_no_writes { put_wiki(id, title: 'タググループ:改名') }
expect do
put "/tag_groups/#{ id }", params: native_payload(name: '改名'), headers:, as: :json
end.to change(WikiPage, :count).by(1)
.and change(WikiRevision, :count).by(1)
.and change(WikiVersion, :count).by(1)
expect(response).to have_http_status(:ok)
expect(TagGroup.find(id).name).to eq('改名')
expect(help.reload.attributes).to eq(help_before)
end
['wiki_lines', 'wiki_revision_lines'].each do |table|
it "rolls back native and compatibility state on #{ table } failure" do
expect_atomic_group_write(table) do
post '/tag_groups', params: native_payload, headers:, as: :json
end
end
end
it 'rolls back a renderer failure instead of treating it as unsupported structure' do
allow(TagGroupMarkdownRenderer).to receive(:render).and_raise('renderer failure')
before_state = group_state
expect do
post '/tag_groups', params: native_payload, headers:, as: :json
end.to raise_error(RuntimeError, 'renderer failure')
expect(group_state).to eq(before_state)
end
it 'preserves unplaced membership and reports the skipped sync with its version' do
page = legacy_group('更新', '- 洗澡鹿')
id = seed_group(name: '更新', id: page.id, members:, placements:)
wiki_before = group_state.slice(*TagGroupRecords::WRITE_TABLES.drop(5))
warnings = []
allow(Rails.logger).to receive(:warn) { warnings << _1 }
put "/tag_groups/#{ id }",
params: native_payload(member_rows: members + [{ 'tag_id' => other_tag.id }]),
headers:, as: :json
expect(response).to have_http_status(:ok)
expect(json.fetch('members').map { _1.fetch('tag').fetch('id') })
.to contain_exactly(tag.id, other_tag.id)
expect(group_version(id, 2).fetch('members')).to include(
{ 'tag_id' => other_tag.id, 'version_no' => other_tag.version_no })
expect(group_state.slice(*TagGroupRecords::WRITE_TABLES.drop(5))).to eq(wiki_before)
payload = JSON.parse(warnings.grep(/tag_group_wiki_sync_skipped/).sole.split(' ', 2).last)
expect(payload).to include(
'status' => 'unsupported_structure', 'tag_group_id' => id,
'version_no' => 2, 'user_id' => user.id,
'reason' => '掲載位置のない membership は Markdown にできません.')
end
it 'preserves a list Section description and records why no Wiki was created' do
nodes = [{ 'tag_id' => tag.id, 'children' => [
{ 'section_name' => '区分', 'description' => '構造化の説明', 'children' => [] }] }]
warnings = []
allow(Rails.logger).to receive(:warn) { warnings << _1 }
before_count = WikiPage.count
post '/tag_groups', params: native_payload(nodes:), headers:, as: :json
expect(response).to have_http_status(:created)
id = json.fetch('id')
expect(group_version(id, 1).fetch('placements')).to eq(nodes)
expect(json.fetch('placements').sole.fetch('children').sole)
.to include('description' => '構造化の説明')
expect(WikiPage.count).to eq(before_count)
expect(warnings.grep(/tag_group_wiki_sync_skipped/).sole)
.to include('リスト内 Section の説明文は Markdown にできません.')
end
end end
context 'identifier Markdown' do context 'identifier Markdown' do
@@ -293,10 +166,10 @@ RSpec.describe 'Tag group write integrity', type: :request do
expect(group_version(id, 2)).to include('description' => '説明を追加', 'members' => members) expect(group_version(id, 2)).to include('description' => '説明を追加', 'members' => members)
end end
it 'rolls back membership removal and placement edits on a late sync failure' do it 'rolls back membership removal and placement edits on a version persistence failure' do
new_members = [{ 'tag_id' => tag.id }] new_members = [{ 'tag_id' => tag.id }]
new_nodes = [{ 'tag_id' => tag.id, 'children' => [] }] new_nodes = [{ 'tag_id' => tag.id, 'children' => [] }]
expect_atomic_group_write('wiki_versions') do expect_atomic_group_write('tag_group_versions') do
put "/tag_groups/#{ id }", put "/tag_groups/#{ id }",
params: native_payload(nodes: new_nodes, member_rows: new_members), headers:, as: :json params: native_payload(nodes: new_nodes, member_rows: new_members), headers:, as: :json
end end
+19 -39
ファイルの表示
@@ -234,8 +234,7 @@ RSpec.describe 'Tag group native API', type: :request do
context 'compatibility writes for representable structures' do context 'compatibility writes for representable structures' do
let(:group_id) do let(:group_id) do
page = legacy_group('専用API', '説明') seed_group(name: '専用API', description: '説明')
seed_group(name: '専用API', id: page.id, description: '説明')
end end
let(:members) { [{ 'tag_id' => tag.id }, { 'tag_group_id' => child_id }] } let(:members) { [{ 'tag_id' => tag.id }, { 'tag_group_id' => child_id }] }
let(:snapshot_members) do let(:snapshot_members) do
@@ -252,10 +251,9 @@ RSpec.describe 'Tag group native API', type: :request do
group_id, number: 2, description: '全体の説明', placements:, group_id, number: 2, description: '全体の説明', placements:,
members: snapshot_members + [ members: snapshot_members + [
{ 'tag_id' => unplaced.id, 'version_no' => unplaced.version_no }]) { 'tag_id' => unplaced.id, 'version_no' => unplaced.version_no }])
page = WikiPage.joins(:tag_name).find_by!(tag_names: { name: 'タググループ:専用API' })
travel_to(1.hour.from_now) do travel_to(1.hour.from_now) do
expect_noop_group_update(group_id:, page:) do expect_noop_group_update(group_id:) do
native_write(:update, unplaced_payload) native_write(:update, unplaced_payload)
end end
end end
@@ -267,20 +265,18 @@ RSpec.describe 'Tag group native API', type: :request do
members: [{ 'tag_id' => tag.id }, { 'tag_group_id' => child_id }], placements: } members: [{ 'tag_id' => tag.id }, { 'tag_group_id' => child_id }], placements: }
end end
let(:additional_tag) { group_tag('追加対象タグ') } let(:additional_tag) { group_tag('追加対象タグ') }
let(:compatibility_page) do
WikiPage.joins(:tag_name).find_by!(tag_names: { name: 'タググループ:構造化' })
end
before do before do
native_write(:update, synchronised_payload) native_write(:update, synchronised_payload)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(compatibility_page.body).to include('- 掲載タグ', '- タググループ:子') expect(group_markdown(TagGroup.find(group_id)))
.to include('- 掲載タグ', '- タググループ:子')
end end
[1, 3].each do |repetitions| [1, 3].each do |repetitions|
it "keeps identical native content unchanged after #{ repetitions } PUTs" do it "keeps identical native content unchanged after #{ repetitions } PUTs" do
travel_to(1.hour.from_now) do travel_to(1.hour.from_now) do
expect_noop_group_update(group_id:, page: compatibility_page) do expect_noop_group_update(group_id:) do
repetitions.times do repetitions.times do
native_write(:update, synchronised_payload) native_write(:update, synchronised_payload)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
@@ -292,7 +288,7 @@ RSpec.describe 'Tag group native API', type: :request do
it 'treats membership order alone as a no-op' do it 'treats membership order alone as a no-op' do
travel_to(1.hour.from_now) do travel_to(1.hour.from_now) do
expect_noop_group_update(group_id:, page: compatibility_page) do expect_noop_group_update(group_id:) do
native_write(:update, synchronised_payload.merge( native_write(:update, synchronised_payload.merge(
members: synchronised_payload.fetch(:members).reverse)) members: synchronised_payload.fetch(:members).reverse))
end end
@@ -302,7 +298,7 @@ RSpec.describe 'Tag group native API', type: :request do
it 'keeps the original editor when another member submits identical content' do it 'keeps the original editor when another member submits identical content' do
editor = create(:user, :member) editor = create(:user, :member)
travel_to(1.hour.from_now) do travel_to(1.hour.from_now) do
expect_noop_group_update(group_id:, page: compatibility_page) do expect_noop_group_update(group_id:) do
put "/tag_groups/#{ group_id }", params: synchronised_payload, put "/tag_groups/#{ group_id }", params: synchronised_payload,
headers: { 'X-Transfer-Code' => editor.inheritance_code }, as: :json headers: { 'X-Transfer-Code' => editor.inheritance_code }, as: :json
end end
@@ -313,7 +309,7 @@ RSpec.describe 'Tag group native API', type: :request do
put "/wiki/#{ group_id }", put "/wiki/#{ group_id }",
params: { title: 'タググループ:構造化', body: '- 未掲載タグ' }, headers: headers params: { title: 'タググループ:構造化', body: '- 未掲載タグ' }, headers: headers
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(compatibility_page.reload.body).to eq('- 未掲載タグ') expect(group_markdown(TagGroup.find(group_id))).to eq('- 未掲載タグ')
same_content = { same_content = {
name: '構造化', description: nil, members: [{ 'tag_id' => unplaced.id }], name: '構造化', description: nil, members: [{ 'tag_id' => unplaced.id }],
placements: [{ 'tag_id' => unplaced.id, 'children' => [] }] } placements: [{ 'tag_id' => unplaced.id, 'children' => [] }] }
@@ -323,7 +319,7 @@ RSpec.describe 'Tag group native API', type: :request do
placements: same_content.fetch(:placements)) placements: same_content.fetch(:placements))
travel_to(1.hour.from_now) do travel_to(1.hour.from_now) do
expect_noop_group_update(group_id:, page: compatibility_page) do expect_noop_group_update(group_id:) do
native_write(:update, same_content) native_write(:update, same_content)
end end
end end
@@ -332,7 +328,7 @@ RSpec.describe 'Tag group native API', type: :request do
['name', 'description', 'membership addition', 'membership removal', ['name', 'description', 'membership addition', 'membership removal',
'Section name', 'Section description', 'placement order', 'placement hierarchy'] 'Section name', 'Section description', 'placement order', 'placement hierarchy']
.each do |change| .each do |change|
it "records a real #{ change } change in both histories" do it "records a real #{ change } change in native history" do
changed = synchronised_payload.deep_dup changed = synchronised_payload.deep_dup
section = changed.fetch(:placements).first section = changed.fetch(:placements).first
children = section.fetch('children') children = section.fetch('children')
@@ -356,11 +352,6 @@ RSpec.describe 'Tag group native API', type: :request do
old_versions = group_rows('tag_group_versions', tag_group_id: group_id) old_versions = group_rows('tag_group_versions', tag_group_id: group_id)
group = TagGroup.find(group_id) group = TagGroup.find(group_id)
number = group.version_no number = group.version_no
page = compatibility_page
old_revision_id = page.current_revision.id
old_wiki_version_id = page.wiki_versions.order(version_no: :desc).first.id
revision_count = page.wiki_revisions.count
version_count = page.wiki_versions.count
travel_to(1.hour.from_now) { native_write(:update, changed) } travel_to(1.hour.from_now) { native_write(:update, changed) }
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
@@ -379,23 +370,17 @@ RSpec.describe 'Tag group native API', type: :request do
versions = group_rows('tag_group_versions', tag_group_id: group_id) versions = group_rows('tag_group_versions', tag_group_id: group_id)
expect(versions.size).to eq(old_versions.size + 1) expect(versions.size).to eq(old_versions.size + 1)
expect(versions.select { _1.fetch('version_no') <= number }).to eq(old_versions) expect(versions.select { _1.fetch('version_no') <= number }).to eq(old_versions)
expect(page.wiki_revisions.count).to eq(revision_count + 1)
expect(page.wiki_versions.count).to eq(version_count + 1)
expect(page.reload.current_revision.id).not_to eq(old_revision_id)
expect(page.wiki_versions.order(version_no: :desc).first.id)
.not_to eq(old_wiki_version_id)
get group_title_path(changed.fetch(:name)) get group_title_path(changed.fetch(:name))
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(json.fetch('body')).to eq(page.body) expect(json.fetch('body')).to eq(group_markdown(group))
end end
end end
end end
it 'creates native state and its compatibility histories' do it 'creates native state and its native history' do
expect { native_write(:create) } expect { native_write(:create) }
.to change(WikiPage, :count).by(1) .to change(TagGroup, :count).by(1)
.and change(WikiRevision, :count).by(1) .and change(TagGroupVersion, :count).by(1)
.and change(WikiVersion, :count).by(1)
expect(response).to have_http_status(:created) expect(response).to have_http_status(:created)
expect_group_snapshot( expect_group_snapshot(
json.fetch('id'), number: 1, members: snapshot_members, json.fetch('id'), number: 1, members: snapshot_members,
@@ -410,18 +395,13 @@ RSpec.describe 'Tag group native API', type: :request do
number = operation == :create ? 1 : 2 number = operation == :create ? 1 : 2
expect_group_snapshot( expect_group_snapshot(
id, number:, members: snapshot_members, placements:, description: '全体の説明') id, number:, members: snapshot_members, placements:, description: '全体の説明')
page = WikiPage.joins(:tag_name).find_by!(tag_names: { name: 'タググループ:構造化' }) expect(TagGroup.find(id).versions.count).to eq(number)
expect(page.body).to include('全体の説明', '# 区分', '- 掲載タグ', '- タググループ:子')
expect(page.current_revision.body).to eq(page.body)
expect(page.wiki_versions.order(:version_no).last.body).to eq(page.body)
expect(page.wiki_revisions.count).to eq(number)
expect(page.wiki_versions.count).to eq(number)
get group_title_path('構造化') get group_title_path('構造化')
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(json.fetch('body')).to eq(page.body) expect(json.fetch('body')).to include('全体の説明', '# 区分', '- 掲載タグ')
end end
(TagGroupRecords::GROUP_TABLES + ['wiki_pages', 'wiki_revisions', 'wiki_versions']) TagGroupRecords::GROUP_TABLES
.each do |table| .each do |table|
it "rolls back native #{ operation } after #{ table } writes" do it "rolls back native #{ operation } after #{ table } writes" do
expect_atomic_group_write(table) { native_write(operation) } expect_atomic_group_write(table) { native_write(operation) }
@@ -429,12 +409,12 @@ RSpec.describe 'Tag group native API', type: :request do
end end
end end
it 'assigns the next group ID from the existing maximum' do it 'assigns the next group ID above the existing maximum' do
child_id child_id
expected_id = [TagGroup.maximum(:id).to_i, TagDescription.maximum(:id).to_i].max + 1 expected_id = [TagGroup.maximum(:id).to_i, TagDescription.maximum(:id).to_i].max + 1
native_write(:create) native_write(:create)
expect(response).to have_http_status(:created) expect(response).to have_http_status(:created)
expect(json.fetch('id')).to eq(expected_id) expect(json.fetch('id')).to be >= expected_id
end end
TagGroupRecords::GROUP_TABLES.each do |table| TagGroupRecords::GROUP_TABLES.each do |table|
+42 -84
ファイルの表示
@@ -1,101 +1,59 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe 'Tag resource ID allocation', type: :request do RSpec.describe 'Tag resource ID allocation', type: :request do
include TagGroupRecords
include WikiRecords include WikiRecords
let!(:user) { create_member_user! } let!(:user) { create_member_user! }
let(:headers) { { 'X-Transfer-Code' => user.inheritance_code } } let(:headers) { { 'X-Transfer-Code' => user.inheritance_code } }
def seed_description id def create_description name
tag = create(:tag, primary_name: "description_id_#{ id }") tag = create(:tag, primary_name: name)
TagDescription.create!(id:, tag:, language_code: 'ja', body: 'existing', version_no: 1) post '/wiki', params: { title: tag.name('ja'), body: 'description' }, headers: headers
end
def create_resource resource
if resource == :description
tag = create(:tag, primary_name: 'new_description')
post '/wiki', params: { title: tag.name('ja'), body: 'new description' }, headers: headers
else
post '/tag_groups', params: {
name: 'new_group', description: 'new group', members: [], placements: [] },
headers:, as: :json
end
expect(response).to have_http_status(:created) expect(response).to have_http_status(:created)
TagDescription.find(json.fetch('id'))
end end
[:description, :group].each do |resource| def create_group name
it "keeps #{ resource } independent of the Tag and ExternalTag ID space" do post '/tag_groups', params: { name:, members: [], placements: [] }, headers:, as: :json
create(:tag, id: 100) expect(response).to have_http_status(:created)
create(:external_tag, id: 200) TagGroup.find(json.fetch('id'))
seed_description(1) end
seed_group(name: 'shared_space', id: 2)
create_resource(resource) it 'shares resource and revision IDs while keeping independent version numbers' do
description = create_description('first')
expect(json.fetch('id')).to eq(3) group = create_group('second')
end other = create_description('third')
expect([description.id, group.id, other.id]).to eq(
it "uses the other resource's larger maximum when allocating #{ resource }" do [description.id, group.id, other.id].sort.uniq)
seed_description(resource == :description ? 1 : 20) versions = [current_description_version(description), group.versions.sole,
seed_group(name: 'other_space', id: resource == :group ? 1 : 20) current_description_version(other)]
create_resource(resource) expect(versions.map(&:id)).to eq(versions.map(&:id).sort.uniq)
expect(json.fetch('id')).to eq(21) expect(versions.map(&:version_no)).to eq([1, 1, 1])
end [description, group, other].zip(versions).each do |resource, version|
get "/wiki/#{ resource.id }"
it "starts #{ resource } at 1 even after previous allocated rows were removed" do expect(response).to have_http_status(:ok)
expect(TagDescription.count).to eq(0) expect(json.fetch('revision_id')).to eq(version.id)
expect(TagGroup.count).to eq(0)
if resource == :description
seed_description(100).destroy!
else
id = seed_group(name: 'removed', id: 100)
TagGroup.find(id).destroy!
end
create_resource(resource)
expect(json.fetch('id')).to eq(1)
end
it "allocates #{ resource } above both maxima, preserving legacy overlaps and gaps" do
[1, 2, 5].each { seed_description(_1) }
[1, 3, 8].each { seed_group(name: "group_#{ _1 }", id: _1) }
descriptions = TagDescription.order(:id).map(&:attributes)
groups = TagGroup.order(:id).map(&:attributes)
create_resource(resource)
expect(json.fetch('id')).to eq(9)
expect(TagDescription.where(id: [1, 2, 5]).order(:id).map(&:attributes)).to eq(descriptions)
expect(TagGroup.where(id: [1, 3, 8]).order(:id).map(&:attributes)).to eq(groups)
expect(TagDescription.exists?(id: 3)).to be(false)
expect(TagGroup.exists?(id: 2)).to be(false)
end end
end end
it 'preserves group ID and unrelated rows on update' do it 'does not reuse a deleted description ID for a group or vice versa' do
id = seed_group(name: 'existing', id: 5, description: 'before') description = create_description('deleted')
other_id = seed_group(name: 'other', id: 8, description: 'other') description.destroy!
other = TagGroup.find(other_id).attributes group = create_group('deleted group')
put "/tag_groups/#{ id }", params: { expect(group.id).to be > description.id
name: 'renamed', description: 'after', members: [], placements: [] }, headers:, as: :json group.destroy!
expect(create_description('replacement').id).to be > group.id
end
it 'preserves resource and old revision identity on an update' do
description = create_description('stable')
original = current_description_version(description)
snapshot = original.attributes
put "/wiki/#{ description.id }", params: {
title: 'stable', body: 'after', base_revision_id: original.id }, headers: headers
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(json.fetch('id')).to eq(id) expect(json.fetch('id')).to eq(description.id)
expect(TagGroup.find(id).description).to eq('after') expect(description.reload).to have_attributes(body: 'after', version_no: 2)
expect(TagGroup.find(other_id).attributes).to eq(other) expect(original.reload.attributes).to eq(snapshot)
expect(TagGroup.count).to eq(2) expect(current_description_version(description).id).to be > original.id
end
it 'preserves description identity on a compatibility update' do
tag = create(:tag, primary_name: 'existing_description')
page = seed_wiki_page(tag_name: tag.tag_name('ja'), body: 'before', created_by_user: user)
description = TagDescription.find_by!(tag_id: tag.id, language_code: 'ja')
id = description.id
put "/wiki/#{ id }", params: {
title: tag.name('ja'), body: 'after', base_revision_id: page.current_revision.id },
headers: headers
expect(response).to have_http_status(:ok)
expect(description.reload.id).to eq(id)
expect(description.body).to eq('after')
expect(TagDescription.count).to eq(1)
end end
end end
+6 -5
ファイルの表示
@@ -1,6 +1,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe 'TagVersions API', type: :request do RSpec.describe 'TagVersions API', type: :request do
include WikiRecords
let(:member) { create(:user, :member, name: 'version member') } let(:member) { create(:user, :member, name: 'version member') }
let!(:tag) { create(:tag, primary_name: 'tag_versions_target', category: :general) } let!(:tag) { create(:tag, primary_name: 'tag_versions_target', category: :general) }
@@ -240,18 +241,18 @@ RSpec.describe 'TagVersions API', type: :request do
wiki_tag = create(:tag, primary_tag_name: wiki_tag_name, category: :general) wiki_tag = create(:tag, primary_tag_name: wiki_tag_name, category: :general)
wiki_page = wiki_page =
Wiki::Commit.create_content!( seed_description(
tag_name: wiki_tag_name, tag_name: wiki_tag_name,
body: 'before', body: 'before',
created_by_user: member, created_by_user: member,
message: 'init') message: 'init')
Wiki::Commit.content!( TagDescriptionOperation.update!(
page: wiki_page, tag: wiki_page.tag,
body: 'after', body: 'after',
created_user: member, updated_by_user: member,
message: 'edit', message: 'edit',
base_revision_id: wiki_page.current_revision.id) base_revision_id: current_description_version(wiki_page).id)
get '/tags/versions', params: { id: wiki_tag.id } get '/tags/versions', params: { id: wiki_tag.id }
+15 -15
ファイルの表示
@@ -14,7 +14,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
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
seed_wiki_page( seed_description(
tag_name: primary_tag_name_for(tag, 'ja'), tag_name: primary_tag_name_for(tag, 'ja'),
body:, body:,
created_by_user: user, created_by_user: user,
@@ -29,7 +29,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
it 'renames the current Wiki resource without adding body history' do it 'renames the current Wiki resource without adding body history' do
tag = create_tag!(name: 'patch_tag_wiki_before') tag = create_tag!(name: 'patch_tag_wiki_before')
wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before') wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before')
wiki_history = wiki_page.wiki_versions.map(&:attributes) wiki_history = description_versions(wiki_page).map(&:attributes)
expect { expect {
patch "/tags/#{ tag.id }", params: { patch "/tags/#{ tag.id }", params: {
@@ -37,7 +37,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
} }
} }
.to change(TagVersion, :count).by(2) .to change(TagVersion, :count).by(2)
.and change(WikiVersion, :count).by(0) .and change(TagDescriptionVersion, :count).by(0)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
@@ -45,16 +45,16 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
wiki_page.reload wiki_page.reload
expect(primary_tag_name_for(tag, 'ja').name).to eq('patch_tag_wiki_after') expect(primary_tag_name_for(tag, 'ja').name).to eq('patch_tag_wiki_after')
expect(wiki_page.title).to eq('patch_tag_wiki_after') expect(wiki_page.tag.name('ja')).to eq('patch_tag_wiki_after')
expect(wiki_page.wiki_versions.reload.map(&:attributes)).to eq(wiki_history) expect(description_versions(wiki_page).reload.map(&:attributes)).to eq(wiki_history)
end end
it 'does not record wiki_version when only category changes' do it 'does not record wiki_version when only category changes' do
tag = create_tag!(name: 'patch_tag_category_only') tag = create_tag!(name: 'patch_tag_category_only')
wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before') wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before')
before_wiki_versions = wiki_page.wiki_versions.count before_wiki_versions = description_versions(wiki_page).count
expect { expect {
patch "/tags/#{ tag.id }", params: { patch "/tags/#{ tag.id }", params: {
@@ -70,7 +70,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
expect(primary_tag_name_for(tag, 'ja').name).to eq('patch_tag_category_only') expect(primary_tag_name_for(tag, 'ja').name).to eq('patch_tag_category_only')
expect(tag.category).to eq('meme') expect(tag.category).to eq('meme')
expect(wiki_page.wiki_versions.count).to eq(before_wiki_versions) expect(description_versions(wiki_page).count).to eq(before_wiki_versions)
end end
end end
@@ -78,7 +78,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
it 'renames the current Wiki resource without adding body history' do it 'renames the current Wiki resource without adding body history' do
tag = create_tag!(name: 'put_tag_wiki_before') tag = create_tag!(name: 'put_tag_wiki_before')
wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before') wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before')
wiki_history = wiki_page.wiki_versions.map(&:attributes) wiki_history = description_versions(wiki_page).map(&:attributes)
expect { expect {
put "/tags/#{ tag.id }", params: { put "/tags/#{ tag.id }", params: {
@@ -90,7 +90,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
} }
} }
.to change(TagVersion, :count).by(2) .to change(TagVersion, :count).by(2)
.and change(WikiVersion, :count).by(0) .and change(TagDescriptionVersion, :count).by(0)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
@@ -98,16 +98,16 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
wiki_page.reload wiki_page.reload
expect(primary_tag_name_for(tag, 'ja').name).to eq('put_tag_wiki_after') expect(primary_tag_name_for(tag, 'ja').name).to eq('put_tag_wiki_after')
expect(wiki_page.title).to eq('put_tag_wiki_after') expect(wiki_page.tag.name('ja')).to eq('put_tag_wiki_after')
expect(wiki_page.wiki_versions.reload.map(&:attributes)).to eq(wiki_history) expect(description_versions(wiki_page).reload.map(&:attributes)).to eq(wiki_history)
end end
it 'does not record wiki_version when only category changes' do it 'does not record wiki_version when only category changes' do
tag = create_tag!(name: 'put_tag_category_only') tag = create_tag!(name: 'put_tag_category_only')
wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before') wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before')
before_wiki_versions = wiki_page.wiki_versions.count before_wiki_versions = description_versions(wiki_page).count
expect { expect {
put "/tags/#{ tag.id }", params: { put "/tags/#{ tag.id }", params: {
@@ -127,14 +127,14 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
expect(primary_tag_name_for(tag, 'ja').name).to eq('put_tag_category_only') expect(primary_tag_name_for(tag, 'ja').name).to eq('put_tag_category_only')
expect(tag.category).to eq('meme') expect(tag.category).to eq('meme')
expect(wiki_page.wiki_versions.count).to eq(before_wiki_versions) expect(description_versions(wiki_page).count).to eq(before_wiki_versions)
end end
it 'does not record wiki_version when only aliases change' do it 'does not record wiki_version when only aliases change' do
tag = create_tag!(name: 'put_tag_alias_only') tag = create_tag!(name: 'put_tag_alias_only')
wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before') wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before')
before_wiki_versions = wiki_page.wiki_versions.count before_wiki_versions = description_versions(wiki_page).count
expect { expect {
put "/tags/#{ tag.id }", params: { put "/tags/#{ tag.id }", params: {
@@ -148,7 +148,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
.to change(TagVersion, :count).by(2) .to change(TagVersion, :count).by(2)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(wiki_page.reload.wiki_versions.count).to eq(before_wiki_versions) expect(description_versions(wiki_page).count).to eq(before_wiki_versions)
end end
end end
end end
+34 -34
ファイルの表示
@@ -2,6 +2,7 @@ require 'cgi'
require 'rails_helper' require 'rails_helper'
RSpec.describe 'Tags API', type: :request do RSpec.describe 'Tags API', type: :request do
include WikiRecords
let!(:tn) { create(:tag_name, name: 'spec_tag') } let!(:tn) { create(:tag_name, name: 'spec_tag') }
let!(:tag) { create(:tag, primary_tag_name: tn, category: :general) } let!(:tag) { create(:tag, primary_tag_name: tn, category: :general) }
let!(:alias_tn) { create(:tag_name, :alias, name: 'unko', tag:) } let!(:alias_tn) { create(:tag_name, :alias, name: 'unko', tag:) }
@@ -745,13 +746,13 @@ RSpec.describe 'Tags API', type: :request do
it 'PATCH の名称変更で Wiki の本文履歴を追加しない' do it 'PATCH の名称変更で Wiki の本文履歴を追加しない' do
wiki_page = wiki_page =
Wiki::Commit.create_content!( seed_description(
tag_name: primary_tag_name_for(tag, 'ja'), tag_name: primary_tag_name_for(tag, 'ja'),
body: 'wiki body before', body: 'wiki body before',
created_by_user: member_user, created_by_user: member_user,
message: 'init') message: 'init')
wiki_history = wiki_page.wiki_versions.map(&:attributes) wiki_history = description_versions(wiki_page).map(&:attributes)
expect { expect {
patch "/tags/#{ tag.id }", params: { patch "/tags/#{ tag.id }", params: {
@@ -759,23 +760,23 @@ RSpec.describe 'Tags API', type: :request do
} }
} }
.to change(TagVersion, :count).by(2) .to change(TagVersion, :count).by(2)
.and change(WikiVersion, :count).by(0) .and change(TagDescriptionVersion, :count).by(0)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(wiki_page.reload.title).to eq('patch_wiki_renamed_tag') expect(wiki_page.reload.tag.name('ja')).to eq('patch_wiki_renamed_tag')
expect(wiki_page.wiki_versions.reload.map(&:attributes)).to eq(wiki_history) expect(description_versions(wiki_page).reload.map(&:attributes)).to eq(wiki_history)
end end
it 'tag の category だけを変更しても wiki version は作成しない' do it 'tag の category だけを変更しても wiki version は作成しない' do
wiki_page = wiki_page =
Wiki::Commit.create_content!( seed_description(
tag_name: primary_tag_name_for(tag, 'ja'), tag_name: primary_tag_name_for(tag, 'ja'),
body: 'wiki body before', body: 'wiki body before',
created_by_user: member_user, created_by_user: member_user,
message: 'init') message: 'init')
before_wiki_version_count = wiki_page.reload.wiki_versions.count before_wiki_version_count = description_versions(wiki_page).count
expect { expect {
patch "/tags/#{ tag.id }", params: { patch "/tags/#{ tag.id }", params: {
@@ -784,7 +785,7 @@ RSpec.describe 'Tags API', type: :request do
}.to change(TagVersion, :count).by(2) }.to change(TagVersion, :count).by(2)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(wiki_page.reload.wiki_versions.count).to eq(before_wiki_version_count) expect(description_versions(wiki_page).count).to eq(before_wiki_version_count)
end end
it 'full update で旧名 alias が残った tag を PATCH で旧名へ戻せる' do it 'full update で旧名 alias が残った tag を PATCH で旧名へ戻せる' do
@@ -815,7 +816,7 @@ RSpec.describe 'Tags API', type: :request do
it '別 tag の正規名には変更できない' do it '別 tag の正規名には変更できない' do
wiki_page = wiki_page =
Wiki::Commit.create_content!( seed_description(
tag_name: primary_tag_name_for(tag, 'ja'), tag_name: primary_tag_name_for(tag, 'ja'),
body: 'patch collision wiki', body: 'patch collision wiki',
created_by_user: member_user, created_by_user: member_user,
@@ -831,7 +832,7 @@ RSpec.describe 'Tags API', type: :request do
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag') expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
expect(TagName.where(tag_id: tag.id, language_code: 'ja', primary_flg: false).pluck(:name)) expect(TagName.where(tag_id: tag.id, language_code: 'ja', primary_flg: false).pluck(:name))
.to contain_exactly('unko') .to contain_exactly('unko')
expect(wiki_page.reload.tag_name).to eq(primary_tag_name_for(tag, 'ja')) expect(wiki_page.reload.tag.tag_name('ja')).to eq(primary_tag_name_for(tag, 'ja'))
end end
it 'system tag の name は変更できない' do it 'system tag の name は変更できない' do
@@ -1209,7 +1210,7 @@ RSpec.describe 'Tags API', type: :request do
expect(old_name_alias).to have_attributes( expect(old_name_alias).to have_attributes(
tag_id: tag.id, language_code: 'ja', primary_flg: false) tag_id: tag.id, language_code: 'ja', primary_flg: false)
expect(alias_tn.reload.tag_id).to be_nil expect(TagName.exists?(alias_tn.id)).to be(false)
expect(TagName.where(tag_id: tag.parents.select(:id), expect(TagName.where(tag_id: tag.parents.select(:id),
language_code: 'ja', primary_flg: true).pluck(:name)) language_code: 'ja', primary_flg: true).pluck(:name))
@@ -1394,20 +1395,20 @@ RSpec.describe 'Tags API', type: :request do
it 'Wiki を持つ Tag の名称変更で本文履歴を追加しない' do it 'Wiki を持つ Tag の名称変更で本文履歴を追加しない' do
wiki_page = wiki_page =
Wiki::Commit.create_content!( seed_description(
tag_name: primary_tag_name_for(tag, 'ja'), tag_name: primary_tag_name_for(tag, 'ja'),
body: 'wiki body before', body: 'wiki body before',
created_by_user: member_user, created_by_user: member_user,
message: 'init') message: 'init')
Wiki::Commit.content!( TagDescriptionOperation.update!(
page: wiki_page, tag: wiki_page.tag,
body: 'wiki body before', body: 'wiki body before',
created_user: member_user, updated_by_user: member_user,
message: 'init' message: 'init'
) )
wiki_history = wiki_page.wiki_versions.map(&:attributes) wiki_history = description_versions(wiki_page).map(&:attributes)
expect { expect {
put "/tags/#{ tag.id }", params: { put "/tags/#{ tag.id }", params: {
@@ -1419,22 +1420,22 @@ RSpec.describe 'Tags API', type: :request do
} }
} }
.to change(TagVersion, :count).by(2) .to change(TagVersion, :count).by(2)
.and change(WikiVersion, :count).by(0) .and change(TagDescriptionVersion, :count).by(0)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(wiki_page.reload.title).to eq('put_wiki_version_tag') expect(wiki_page.reload.tag.name('ja')).to eq('put_wiki_version_tag')
expect(wiki_page.wiki_versions.reload.map(&:attributes)).to eq(wiki_history) expect(description_versions(wiki_page).reload.map(&:attributes)).to eq(wiki_history)
end end
it 'wiki を持つ tag を旧 alias へ戻しても wiki を新しい正本名へ移す' do it 'wiki を持つ tag を旧 alias へ戻しても wiki を新しい正本名へ移す' do
wiki_page = wiki_page =
Wiki::Commit.create_content!( seed_description(
tag_name: primary_tag_name_for(tag, 'ja'), tag_name: primary_tag_name_for(tag, 'ja'),
body: 'wiki body before', body: 'wiki body before',
created_by_user: member_user, created_by_user: member_user,
message: 'init') message: 'init')
wiki_history = wiki_page.wiki_versions.map(&:attributes) wiki_history = description_versions(wiki_page).map(&:attributes)
expect { expect {
put "/tags/#{ tag.id }", params: { put "/tags/#{ tag.id }", params: {
@@ -1454,20 +1455,19 @@ RSpec.describe 'Tags API', type: :request do
} }
} }
.to change(TagVersion, :count).by(3) .to change(TagVersion, :count).by(3)
.and change(WikiVersion, :count).by(0) .and change(TagDescriptionVersion, :count).by(0)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
tag.reload tag.reload
expect(wiki_page.reload.tag_name).to eq(primary_tag_name_for(tag, 'ja')) expect(wiki_page.reload.tag.tag_name('ja')).to eq(primary_tag_name_for(tag, 'ja'))
expect(TagName.find_by!(language_code: 'ja', name: 'put_wiki_roundtrip_b').wiki_page) expect(wiki_page.reload.tag_id).to eq(tag.id)
.to be_nil
expect(TagName.find_by!(language_code: 'ja', name: 'put_wiki_roundtrip_b')) expect(TagName.find_by!(language_code: 'ja', name: 'put_wiki_roundtrip_b'))
.to have_attributes(tag_id: tag.id, primary_flg: false) .to have_attributes(tag_id: tag.id, primary_flg: false)
expect(wiki_page.title).to eq('spec_tag') expect(wiki_page.tag.name('ja')).to eq('spec_tag')
expect(wiki_page.wiki_versions.reload.map(&:attributes)).to eq(wiki_history) expect(description_versions(wiki_page).reload.map(&:attributes)).to eq(wiki_history)
end end
it '別名を他 tag から奪った場合、奪はれた側の tag version も作成する' do it '別名を他 tag から奪った場合、奪はれた側の tag version も作成する' do
@@ -1520,7 +1520,7 @@ RSpec.describe 'Tags API', type: :request do
name: 'put_alias_collision_name', name: 'put_alias_collision_name',
tag: old_owner) tag: old_owner)
wiki_page = wiki_page =
Wiki::Commit.create_content!( seed_description(
tag_name: primary_tag_name_for(tag, 'ja'), tag_name: primary_tag_name_for(tag, 'ja'),
body: 'put collision wiki', body: 'put collision wiki',
created_by_user: member_user, created_by_user: member_user,
@@ -1554,7 +1554,7 @@ RSpec.describe 'Tags API', type: :request do
expect(old_owner_versions.last.event_type).to eq('update') expect(old_owner_versions.last.event_type).to eq('update')
expect(version_alias_names(old_owner_versions.last, 'ja')) expect(version_alias_names(old_owner_versions.last, 'ja'))
.not_to include('put_alias_collision_name') .not_to include('put_alias_collision_name')
expect(wiki_page.reload.tag_name).to eq(primary_tag_name_for(tag, 'ja')) expect(wiki_page.reload.tag.tag_name('ja')).to eq(primary_tag_name_for(tag, 'ja'))
end end
it 'parent_tags に指定すると循環する tag は 422 にする' do it 'parent_tags に指定すると循環する tag は 422 にする' do
@@ -1579,13 +1579,13 @@ RSpec.describe 'Tags API', type: :request do
it 'Tag の名称変更で Wiki の本文履歴を追加しない' do it 'Tag の名称変更で Wiki の本文履歴を追加しない' do
wiki_page = wiki_page =
Wiki::Commit.create_content!( seed_description(
tag_name: primary_tag_name_for(tag, 'ja'), tag_name: primary_tag_name_for(tag, 'ja'),
body: 'wiki body before', body: 'wiki body before',
created_by_user: member_user, created_by_user: member_user,
message: 'init') message: 'init')
wiki_history = wiki_page.wiki_versions.map(&:attributes) wiki_history = description_versions(wiki_page).map(&:attributes)
expect { expect {
put "/tags/#{ tag.id }", params: { put "/tags/#{ tag.id }", params: {
@@ -1597,12 +1597,12 @@ RSpec.describe 'Tags API', type: :request do
} }
} }
.to change(TagVersion, :count).by(2) .to change(TagVersion, :count).by(2)
.and change(WikiVersion, :count).by(0) .and change(TagDescriptionVersion, :count).by(0)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(wiki_page.reload.title).to eq('put_wiki_renamed_tag') expect(wiki_page.reload.tag.name('ja')).to eq('put_wiki_renamed_tag')
expect(wiki_page.wiki_versions.reload.map(&:attributes)).to eq(wiki_history) expect(description_versions(wiki_page).reload.map(&:attributes)).to eq(wiki_history)
end end
end end
end end
+6 -6
ファイルの表示
@@ -10,18 +10,18 @@ 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 =
seed_wiki_page( seed_description(
tag_name: create(:tag, primary_name: 'wiki_conflict_request').tag_name('ja'), tag_name: create(:tag, primary_name: 'wiki_conflict_request').tag_name('ja'),
body: 'first', body: 'first',
created_by_user: user, created_by_user: user,
message: 'init') message: 'init')
stale_id = page.current_revision.id stale_id = current_description_version(page).id
Wiki::Commit.content!( TagDescriptionOperation.update!(
page:, tag: page.tag,
body: 'second', body: 'second',
created_user: user, updated_by_user: user,
message: 'other edit', message: 'other edit',
base_revision_id: stale_id) base_revision_id: stale_id)
@@ -36,6 +36,6 @@ RSpec.describe 'Wiki conflict handling', type: :request do
page.reload page.reload
expect(page.body).to eq('second') expect(page.body).to eq('second')
expect(page.current_revision.message).to eq('other edit') expect(current_description_version(page)).to have_attributes(body: 'second', version_no: 2)
end end
end end
+1 -17
ファイルの表示
@@ -7,27 +7,11 @@ RSpec.describe 'Wiki creation freeze', type: :request do
let!(:user) { create_member_user! } let!(:user) { create_member_user! }
let(:headers) { { 'X-Transfer-Code' => user.inheritance_code } } let(:headers) { { 'X-Transfer-Code' => user.inheritance_code } }
it 'creates missing compatibility history when updating an independent group' do
id = seed_group(name: 'independent', description: 'before')
expect do
put "/tag_groups/#{ id }", params: {
name: 'independent', description: 'after', members: [], placements: [] },
headers:, as: :json
end.to change(WikiPage, :count).by(1)
.and change(WikiRevision, :count).by(1)
.and change(WikiVersion, :count).by(1)
expect(response).to have_http_status(:ok)
expect(TagGroup.find(id).description).to eq('after')
page = WikiPage.joins(:tag_name).find_by!(tag_names: { name: 'タググループ:independent' })
expect(page.body).to include('after')
expect(page.current_revision.body).to eq(page.body)
expect(page.wiki_versions.sole.body).to eq(page.body)
end
context 'existing description write protection' do context 'existing description write protection' do
let!(:tag) { create(:tag, primary_name: 'protected_description') } let!(:tag) { create(:tag, primary_name: 'protected_description') }
let!(:page) do let!(:page) do
seed_wiki_page(tag_name: tag.tag_name('ja'), body: 'before', created_by_user: user) seed_description(tag_name: tag.tag_name('ja'), body: 'before', created_by_user: user)
end end
[nil, :guest, :banned].each do |role| [nil, :guest, :banned].each do |role|
-170
ファイルの表示
@@ -1,170 +0,0 @@
require 'cgi'
require 'rails_helper'
RSpec.describe 'Wiki history integrity', type: :request do
include WikiRecords
let!(:user) { create_member_user! }
def auth_headers user
{ 'X-Transfer-Code' => user.inheritance_code }
end
def create_wiki_page title:, body: 'body', message: 'init', user: self.user
seed_wiki_page(
tag_name: create(:tag, primary_name: title).tag_name('ja'),
body:,
created_by_user: user,
message:)
end
describe 'POST /wiki' do
it 'rejects frozen creation without partial page or history' do
expect do
post '/wiki', params: { title: 'Help:wiki_history_create_atomic', body: 'initial' },
headers: auth_headers(user)
end.to change(WikiPage, :count).by(0)
.and change(WikiRevision, :count).by(0)
.and change(WikiVersion, :count).by(0)
expect(response.status).to be_between(400, 499)
end
it 'returns 422 and creates nothing when normalised body is blank' do
expect {
post '/wiki',
params: {
title: 'Help:wiki_history_blank_body',
body: "\r\n\r\n",
message: 'blank',
},
headers: auth_headers(user)
}
.not_to change(WikiPage, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(WikiPage.joins(:tag_name)
.where(tag_names: { name: 'Help:wiki_history_blank_body' })).not_to exist
end
it 'rejects frozen creation without modifying an existing page or history' do
create_wiki_page(title: 'Help:wiki_history_duplicate_title', body: 'first')
expect do
post '/wiki',
params: { title: 'Help:wiki_history_duplicate_title',
body: 'second', message: 'duplicate' },
headers: auth_headers(user)
end.to change(WikiPage, :count).by(0)
.and change(WikiRevision, :count).by(0)
.and change(WikiVersion, :count).by(0)
expect(response.status).to be_between(400, 499)
expect(WikiPage.joins(:tag_name)
.where(tag_names: { name: 'Help:wiki_history_duplicate_title' }).count).to eq(1)
end
end
describe 'PUT /wiki/:id' do
it 'updates body and records wiki_revision and wiki_version' do
page = create_wiki_page(title: 'wiki_history_update_body', body: 'before')
current_id = page.current_revision.id
expect {
put "/wiki/#{ page.id }",
params: {
title: 'wiki_history_update_body',
body: 'after',
message: 'edit body',
base_revision_id: current_id,
},
headers: auth_headers(user)
}
.to change(WikiRevision, :count).by(1)
.and change(WikiVersion, :count).by(1)
expect(response).to have_http_status(:ok)
page.reload
version = page.wiki_versions.order(:version_no).last
expect(page.title).to eq('wiki_history_update_body')
expect(page.body).to eq('after')
expect(version).to have_attributes(
event_type: 'update',
title: 'wiki_history_update_body',
body: 'after',
reason: 'edit body',
created_by_user_id: user.id
)
end
it 'renames title and records wiki_version with new title' do
page = create_wiki_page(title: 'wiki_history_rename_before', body: 'before')
current_id = page.current_revision.id
expect {
put "/wiki/#{ page.id }",
params: {
title: 'wiki_history_rename_after',
body: 'after',
message: 'rename',
base_revision_id: current_id,
},
headers: auth_headers(user)
}
.to change(WikiRevision, :count).by(1)
.and change(WikiVersion, :count).by(1)
expect(response).to have_http_status(:ok)
page.reload
version = page.wiki_versions.order(:version_no).last
expect(page.title).to eq('wiki_history_rename_after')
expect(page.body).to eq('after')
expect(version).to have_attributes(
event_type: 'update',
title: 'wiki_history_rename_after',
body: 'after',
reason: 'rename',
created_by_user_id: user.id
)
end
it 'does not change title, body, revision, or version on stale base_revision_id' do
page = create_wiki_page(title: 'wiki_history_conflict_page', body: 'first')
stale_id = page.current_revision.id
Wiki::Commit.content!(
page:,
body: 'second',
created_user: user,
message: 'other edit',
base_revision_id: stale_id)
page.reload
current_title = page.title
current_body = page.body
revision_count = page.wiki_revisions.count
version_count = page.wiki_versions.count
put "/wiki/#{ page.id }",
params: {
title: 'wiki_history_conflict_renamed',
body: 'third',
message: 'stale edit',
base_revision_id: stale_id,
},
headers: auth_headers(user)
expect(response).to have_http_status(:conflict)
page.reload
expect(page.title).to eq(current_title)
expect(page.body).to eq(current_body)
expect(page.wiki_revisions.count).to eq(revision_count)
expect(page.wiki_versions.count).to eq(version_count)
end
end
end
+3 -3
ファイルの表示
@@ -28,15 +28,15 @@ RSpec.describe 'Wiki resource ordering', type: :request do
it 'breaks timestamp ties by resource kind and ID while retaining legacy overlaps' do it 'breaks timestamp ties by resource kind and ID while retaining legacy overlaps' do
time = Time.utc(2026, 1, 1) time = Time.utc(2026, 1, 1)
tag = create(:tag, primary_name: 'tied') tag = create(:tag, primary_name: 'tied')
TagDescription.create!(id: 51, tag:, language_code: 'ja', body: 'current', description = TagDescription.create!(tag:, language_code: 'ja', body: 'current',
version_no: 1, updated_at: time) version_no: 1, updated_at: time)
group = TagGroup.find(seed_group(name: 'tied', id: 51)) group = TagGroup.find(seed_group(name: 'tied', id: description.id))
group.update_columns(updated_at: time) group.update_columns(updated_at: time)
2.times do 2.times do
get '/wiki', params: { title: 'tied' } get '/wiki', params: { title: 'tied' }
expect(json.map { [_1.fetch('resource_type'), _1.fetch('id')] }) expect(json.map { [_1.fetch('resource_type'), _1.fetch('id')] })
.to eq([['tag_description', 51], ['tag_group', 51]]) .to eq([['tag_description', description.id], ['tag_group', description.id]])
end end
end end
end end
+35 -63
ファイルの表示
@@ -19,7 +19,7 @@ RSpec.describe 'Wiki API', type: :request do
deprecated_at: Time.zone.local(2026, 6, 1)) deprecated_at: Time.zone.local(2026, 6, 1))
end end
let!(:page) do let!(:page) do
seed_wiki_page( seed_description(
tag_name: tn, tag_name: tn,
body: 'init', body: 'init',
created_by_user: user, created_by_user: user,
@@ -113,9 +113,9 @@ RSpec.describe 'Wiki API', type: :request do
it "rejects frozen creation of #{ title } without history writes" do it "rejects frozen creation of #{ title } without history writes" do
expect do expect do
post endpoint, params: { title:, body: 'initial' }, headers: auth_headers(member) post endpoint, params: { title:, body: 'initial' }, headers: auth_headers(member)
end.to change(WikiPage, :count).by(0) end.to change(TagDescription, :count).by(0)
.and change(WikiRevision, :count).by(0) .and change(TagDescriptionVersion, :count).by(0)
.and change(WikiVersion, :count).by(0)
expect(response.status).to be_between(400, 499) expect(response.status).to be_between(400, 499)
end end
end end
@@ -132,7 +132,7 @@ RSpec.describe 'Wiki API', type: :request do
let!(:test_tag_name) { create(:tag, primary_name: 'TestPage').tag_name('ja') } let!(:test_tag_name) { create(:tag, primary_name: 'TestPage').tag_name('ja') }
let!(:page) do let!(:page) do
seed_wiki_page( seed_description(
tag_name: test_tag_name, tag_name: test_tag_name,
body: "a\nb", body: "a\nb",
created_by_user: member, created_by_user: member,
@@ -166,7 +166,7 @@ RSpec.describe 'Wiki API', type: :request do
context 'when success' do context 'when success' do
it 'creates new revision and returns 200' do it 'creates new revision and returns 200' do
current_id = page.wiki_revisions.maximum(:id) current_id = description_versions(page).maximum(:id)
expect do expect do
put "/wiki/#{page.id}", put "/wiki/#{page.id}",
@@ -174,26 +174,22 @@ RSpec.describe 'Wiki API', type: :request do
base_revision_id: current_id }, base_revision_id: current_id },
headers: auth_headers(member) headers: auth_headers(member)
end end
.to change(WikiRevision, :count).by(1) .to change(TagDescriptionVersion, :count).by(1)
.and change(WikiVersion, :count).by(1)
version = page.wiki_versions.order(:version_no).last
version = description_versions(page).order(:version_no).last
expect(version).to have_attributes( expect(version).to have_attributes(
event_type: 'update', event_type: 'update',
title: 'TestPage',
body: "x\ny", body: "x\ny",
created_by_user_id: member.id created_by_user_id: member.id)
)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
page.reload page.reload
rev = page.current_revision rev = current_description_version(page)
expect(rev).to be_content
expect(rev.message).to eq('edit')
expect(page.body).to eq("x\ny") expect(page.body).to eq("x\ny")
expect(rev.base_revision_id).to eq(current_id) expect(rev.version_no).to eq(2)
end end
it 'wiki body だけを変更しても tag version は作成しない' do it 'wiki body だけを変更しても tag version は作成しない' do
@@ -206,13 +202,13 @@ RSpec.describe 'Wiki API', type: :request do
created_by_user: member) created_by_user: member)
linked_page = linked_page =
seed_wiki_page( seed_description(
tag_name: linked_tag_name, tag_name: linked_tag_name,
body: 'before', body: 'before',
created_by_user: member, created_by_user: member,
message: 'init') message: 'init')
current_id = linked_page.current_revision.id current_id = current_description_version(linked_page).id
before_count = linked_tag.reload.tag_versions.count before_count = linked_tag.reload.tag_versions.count
expect { expect {
@@ -225,8 +221,8 @@ RSpec.describe 'Wiki API', type: :request do
}, },
headers: auth_headers(member) headers: auth_headers(member)
} }
.to change(WikiRevision, :count).by(1) .to change(TagDescriptionVersion, :count).by(1)
.and change(WikiVersion, :count).by(1)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(linked_tag.reload.tag_versions.count).to eq(before_count) expect(linked_tag.reload.tag_versions.count).to eq(before_count)
@@ -236,10 +232,10 @@ RSpec.describe 'Wiki API', type: :request do
context 'when conflict' do context 'when conflict' do
it 'returns 409 when base_revision_id mismatches' do it 'returns 409 when base_revision_id mismatches' do
# 先に別ユーザ(同じ member でもOK)が 1 回更新して先頭を進める # 先に別ユーザ(同じ member でもOK)が 1 回更新して先頭を進める
Wiki::Commit.content!(page: page, body: "zzz", created_user: member, message: 'other edit') TagDescriptionOperation.update!(tag: page.tag, body: "zzz", updated_by_user: member, message: 'other edit')
page.reload page.reload
stale_id = page.wiki_revisions.order(:id).first.id # わざと古い id stale_id = description_versions(page).order(:id).first.id # わざと古い id
put "/wiki/#{page.id}", put "/wiki/#{page.id}",
params: { title: 'TestPage', body: 'x', base_revision_id: stale_id }, params: { title: 'TestPage', body: 'x', base_revision_id: stale_id },
headers: auth_headers(member) headers: auth_headers(member)
@@ -281,13 +277,13 @@ RSpec.describe 'Wiki API', type: :request do
describe 'GET /wiki/search' do describe 'GET /wiki/search' do
before do before do
search_tag = create(:tag, primary_name: 'spec_wiki_title_2') search_tag = create(:tag, primary_name: 'spec_wiki_title_2')
seed_wiki_page( seed_description(
tag_name: search_tag.tag_name('ja'), tag_name: search_tag.tag_name('ja'),
body: 'search body 2', body: 'search body 2',
created_by_user: user, created_by_user: user,
message: 'init') message: 'init')
seed_wiki_page( seed_description(
tag_name: create(:tag_name, 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,
@@ -318,13 +314,13 @@ RSpec.describe 'Wiki API', type: :request do
describe 'GET /wiki/changes' do describe 'GET /wiki/changes' do
let!(:rev1) do let!(:rev1) do
Wiki::Commit.content!(page: page, body: "a\nb", created_user: user, message: 'r1') TagDescriptionOperation.update!(tag: page.tag, body: "a\nb", updated_by_user: user, message: 'r1')
page.current_revision current_description_version(page)
end end
let!(:rev2) do let!(:rev2) do
Wiki::Commit.content!(page: page, body: "a\nc", created_user: user, message: 'r2') TagDescriptionOperation.update!(tag: page.tag, body: "a\nc", updated_by_user: user, message: 'r2')
page.current_revision current_description_version(page)
end end
it 'returns latest revisions (optionally filtered by page id)' do it 'returns latest revisions (optionally filtered by page id)' do
@@ -359,11 +355,8 @@ RSpec.describe 'Wiki API', type: :request do
# 別ページを作って revision 無し # 別ページを作って revision 無し
tn2 = create(:tag_name, name: 'spec_no_rev') tn2 = create(:tag_name, name: 'spec_no_rev')
# 異常データ: revision 無し WikiPage を直接作る # 異常データ: revision 無し WikiPage を直接作る
p2 = WikiPage.create!( p2 = TagDescription.create!(
tag_name: tn2, tag: tn2.tag, language_code: 'ja', body: 'init', version_no: 1)
body: 'init',
created_user: user,
updated_user: user)
get "/wiki/changes?id=#{p2.id}" get "/wiki/changes?id=#{p2.id}"
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
@@ -399,13 +392,13 @@ RSpec.describe 'Wiki API', type: :request do
describe 'GET /wiki/:id/diff' do describe 'GET /wiki/:id/diff' do
let!(:rev_a) do let!(:rev_a) do
Wiki::Commit.content!(page: page, body: "a\nb\nc", created_user: user, message: 'A') TagDescriptionOperation.update!(tag: page.tag, body: "a\nb\nc", updated_by_user: user, message: 'A')
page.current_revision current_description_version(page)
end end
let!(:rev_b) do let!(:rev_b) do
Wiki::Commit.content!(page: page, body: "a\nx\nc", created_user: user, message: 'B') TagDescriptionOperation.update!(tag: page.tag, body: "a\nx\nc", updated_by_user: user, message: 'B')
page.current_revision current_description_version(page)
end end
it 'returns diff json between revisions' do it 'returns diff json between revisions' do
@@ -431,44 +424,23 @@ RSpec.describe 'Wiki API', type: :request do
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(json['older_revision_id']).to eq(rev_a.id) expect(json['older_revision_id']).to eq(rev_a.id)
expect(json['newer_revision_id']).to eq(page.current_revision.id) expect(json['newer_revision_id']).to eq(current_description_version(page).id)
end end
end end
describe 'Wiki::Commit.redirect!' do
it 'raises because redirect revisions are deprecated' do
target_tag_name = create(:tag_name, name: 'redirect_deprecated_target')
target =
seed_wiki_page(
tag_name: target_tag_name,
body: 'target',
created_by_user: user,
message: 'init')
expect {
Wiki::Commit.redirect!(
page: page,
redirect_page: target,
created_user: user,
message: 'redirect',
base_revision_id: page.current_revision.id
)
}.to raise_error(RuntimeError, '廃止しました.')
end
end
it 'wiki title を変更すると対応する tag の version を作成する' do it 'wiki title を変更すると対応する tag の version を作成する' do
linked_tag_name = create(:tag_name, name: 'wiki_linked_tag_for_version') linked_tag_name = create(:tag_name, name: 'wiki_linked_tag_for_version')
linked_tag = create(:tag, primary_tag_name: linked_tag_name, category: :general) linked_tag = create(:tag, primary_tag_name: linked_tag_name, category: :general)
linked_page = linked_page =
seed_wiki_page( seed_description(
tag_name: linked_tag_name, tag_name: linked_tag_name,
body: 'before', body: 'before',
created_by_user: user, created_by_user: user,
message: 'init') message: 'init')
current_id = linked_page.current_revision.id current_id = current_description_version(linked_page).id
expect { expect {
put "/wiki/#{ linked_page.id }", put "/wiki/#{ linked_page.id }",
@@ -480,8 +452,8 @@ RSpec.describe 'Wiki API', type: :request do
}, },
headers: auth_headers(user) headers: auth_headers(user)
} }
.to change(WikiRevision, :count).by(1) .to change(TagDescriptionVersion, :count).by(1)
.and change(WikiVersion, :count).by(1)
.and change { linked_tag.reload.tag_versions.count }.by(2) .and change { linked_tag.reload.tag_versions.count }.by(2)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
+10 -11
ファイルの表示
@@ -9,7 +9,7 @@ RSpec.describe 'Wiki title collision', type: :request do
end end
def create_wiki_page title:, body: def create_wiki_page title:, body:
seed_wiki_page( seed_description(
tag_name: create(:tag_name, name: title), tag_name: create(:tag_name, name: title),
body:, body:,
created_by_user: user, created_by_user: user,
@@ -20,9 +20,9 @@ RSpec.describe 'Wiki title collision', type: :request do
source = create_wiki_page(title: 'wiki_collision_source', body: 'source body') source = create_wiki_page(title: 'wiki_collision_source', body: 'source body')
create_wiki_page(title: 'wiki_collision_target', body: 'target body') create_wiki_page(title: 'wiki_collision_target', body: 'target body')
source_revision_count = source.wiki_revisions.count source_revision_count = description_versions(source).count
source_version_count = source.wiki_versions.count source_version_count = description_versions(source).count
old_title = source.title old_title = source.tag.reload.name('ja')
old_body = source.body old_body = source.body
put "/wiki/#{ source.id }", put "/wiki/#{ source.id }",
@@ -30,7 +30,7 @@ RSpec.describe 'Wiki title collision', type: :request do
title: 'wiki_collision_target', title: 'wiki_collision_target',
body: 'new body', body: 'new body',
message: 'rename collision', message: 'rename collision',
base_revision_id: source.current_revision.id, base_revision_id: current_description_version(source).id,
}, },
headers: auth_headers(user) headers: auth_headers(user)
@@ -38,10 +38,10 @@ RSpec.describe 'Wiki title collision', type: :request do
source.reload source.reload
expect(source.title).to eq(old_title) expect(source.tag.reload.name('ja')).to eq(old_title)
expect(source.body).to eq(old_body) expect(source.body).to eq(old_body)
expect(source.wiki_revisions.count).to eq(source_revision_count) expect(description_versions(source).count).to eq(source_revision_count)
expect(source.wiki_versions.count).to eq(source_version_count) expect(description_versions(source).count).to eq(source_version_count)
end end
it 'rejects frozen creation with an existing title without adding history' do it 'rejects frozen creation with an existing title without adding history' do
@@ -52,9 +52,8 @@ RSpec.describe 'Wiki title collision', type: :request do
params: { title: 'Help:wiki_collision_create', body: 'new body', params: { title: 'Help:wiki_collision_create', body: 'new body',
message: 'duplicate create' }, message: 'duplicate create' },
headers: auth_headers(user) headers: auth_headers(user)
end.to change(WikiPage, :count).by(0) end.to change(TagDescription, :count).by(0)
.and change(WikiRevision, :count).by(0) .and change(TagDescriptionVersion, :count).by(0)
.and change(WikiVersion, :count).by(0)
expect(response.status).to be_between(400, 499) expect(response.status).to be_between(400, 499)
end end
+16 -69
ファイルの表示
@@ -1,78 +1,25 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe 'Unmigrated Wiki compatibility', type: :request do RSpec.describe 'Removed standalone Wiki resources', type: :request do
include WikiRecords
let!(:user) { create_member_user! }
let!(:tag_name) { create(:tag_name, name: 'Help_contract') }
let!(:page) do
seed_wiki_page(tag_name:, body: 'before', created_by_user: user)
end
let!(:first_revision) { page.current_revision }
let!(:last_revision) do
Wiki::Commit.legacy_content!(page:, body: 'after', created_user: user,
message: 'legacy edit', base_revision_id: first_revision.id)
end
before { expect(TagDescription.exists?(id: page.id)).to be(false) }
it 'rejects standalone updates even after a Tag is linked to the legacy page' do
create(:tag, primary_tag_name: tag_name)
original = page.reload.attributes
expect do
put "/wiki/#{ page.id }",
params: { title: 'legacy_tag_renamed', body: 'changed legacy body',
message: 'legacy rename', base_revision_id: last_revision.id },
headers: { 'X-Transfer-Code' => user.inheritance_code }
end.to change(WikiRevision, :count).by(0)
.and change(WikiVersion, :count).by(0)
.and change(TagDescription, :count).by(0)
.and change(TagDescriptionVersion, :count).by(0)
expect(response).to have_http_status(:unprocessable_entity)
expect(page.reload.attributes).to eq(original)
expect(page.current_revision).to have_attributes(message: 'legacy edit')
end
include TagGroupRecords include TagGroupRecords
let(:user) { create_member_user! }
[:id, :title].each do |lookup| ['Help:old', 'ヘルプ:旧ページ'].each do |title|
[nil, :historical].each do |edition| it "does not resolve #{ title } as a Wiki resource" do
it "rejects legacy #{ lookup } #{ edition || 'current' } without legacy SELECTs" do path = "/wiki/title/#{ CGI.escapeURIComponent(title) }"
path = lookup == :id ? "/wiki/#{ page.id }" : "/wiki/title/#{ tag_name.name }"
params = edition ? { version: first_revision.id } : { }
without_group_legacy_reads { get path, params: }
expect(response).to have_http_status(:not_found)
end
end
it "does not recognise legacy #{ lookup } existence" do
path = lookup == :id ? "/wiki/#{ page.id }" : "/wiki/title/#{ tag_name.name }"
without_group_legacy_reads { get "#{ path }/exists" }
expect(response).to have_http_status(:not_found)
end
end
['/wiki', '/wiki/search', '/wiki/changes'].each do |path|
it "excludes unmigrated content from #{ path } without legacy SELECTs" do
without_group_legacy_reads { get path } without_group_legacy_reads { get path }
expect(response).to have_http_status(:ok)
expect(json).to eq([])
end
end
it 'returns empty scoped history without legacy SELECTs' do
without_group_legacy_reads { get '/wiki/changes', params: { id: page.id } }
expect(response).to have_http_status(:ok)
expect(json).to eq([])
end
[false, true].each do |explicit|
it "rejects legacy diff with #{ explicit ? 'explicit' : 'current' } target" do
params = { from: first_revision.id }
params[:to] = last_revision.id if explicit
without_group_legacy_reads { get "/wiki/#{ page.id }/diff", params: }
expect(response).to have_http_status(:not_found) expect(response).to have_http_status(:not_found)
get "#{ path }/exists"
expect(response).to have_http_status(:not_found)
end
it "rejects standalone creation of #{ title } without writes" do
user
before_state = group_state
post '/wiki', params: { title:, body: 'body' },
headers: { 'X-Transfer-Code' => user.inheritance_code }
expect(response).to have_http_status(:unprocessable_entity)
expect(group_state).to eq(before_state)
end end
end end
end end
+26 -13
ファイルの表示
@@ -9,9 +9,9 @@ RSpec.describe 'Shared resource IDs across MySQL connections', type: :model do
expect(ApplicationRecord.connection.adapter_name).to match(/mysql/i) expect(ApplicationRecord.connection.adapter_name).to match(/mysql/i)
expect(ApplicationRecord.connection_pool.size).to be >= 3 expect(ApplicationRecord.connection_pool.size).to be >= 3
@prefix = "shared_ids_#{ SecureRandom.hex(8) }" @prefix = "shared_ids_#{ SecureRandom.hex(8) }"
@names = [0, 1].map { create(:tag_name, name: "#{ @prefix }_#{ _1 }") } @tag = create(:tag, primary_name: "#{ @prefix }_owner")
@tag = Tag.create!(tag_name_id: @names.first.id, category: :general) @group = TagGroup.create!(name: "#{ @prefix }_owner", version_no: 1)
@names.first.update!(tag_id: @tag.id) @tag_ids = Queue.new
@threads = [] @threads = []
@release = Queue.new @release = Queue.new
end end
@@ -23,17 +23,21 @@ RSpec.describe 'Shared resource IDs across MySQL connections', type: :model do
raise 'Shared ID worker did not finish' unless thread.join(15) raise 'Shared ID worker did not finish' unless thread.join(15)
end end
ensure ensure
TagDescriptionVersion.where(tag_id: @tag.id).delete_all if @tag
if @group
TagGroupVersion.where(tag_group_id: @group.id).delete_all
@group.delete
end
if @prefix if @prefix
ExternalTag.where(name: "#{ @prefix }_external").delete_all ExternalTag.where(name: "#{ @prefix }_external").delete_all
TagGroup.where(name: "#{ @prefix }_group").delete_all TagGroup.where(name: "#{ @prefix }_group").delete_all
end end
TagDescription.where(tag_id: @tag.id).delete_all if @tag TagDescription.where(tag_id: @tag.id).delete_all if @tag
if @names ids = []
ids = @names.map(&:id) ids << @tag_ids.pop until @tag_ids.empty? if @tag_ids
TagName.where(id: ids).update_all(tag_id: nil) ids << @tag.id if @tag
Tag.where(tag_name_id: ids).delete_all TagName.where(tag_id: ids).delete_all
TagName.where(id: ids).delete_all Tag.where(id: ids).delete_all
end
end end
end end
@@ -51,11 +55,19 @@ RSpec.describe 'Shared resource IDs across MySQL connections', type: :model do
def create_resource kind def create_resource kind
case kind case kind
when :tag when :tag
Tag.create!(tag_name_id: @names.last.id, category: :general) Tag.create!(category: :general).tap { @tag_ids << _1.id }
when :external when :external
ExternalTag.create!(platform: :nico, name: "#{ @prefix }_external") ExternalTag.create!(platform: :nico, name: "#{ @prefix }_external")
when :description when :description
TagDescription.create!(tag: @tag, language_code: 'ja', body: '', version_no: 1) TagDescription.create!(tag: @tag, language_code: 'ja', body: '', version_no: 1)
when :description_version
TagDescriptionVersion.create!(
tag_id: @tag.id, language_code: 'ja', body: 'body', version_no: 1,
event_type: 'create', created_at: Time.current)
when :group_version
TagGroupVersion.create!(
tag_group: @group, name: @group.name, version_no: 1, event_type: 'create',
members: [], placements: [], created_at: Time.current)
when :group when :group
TagGroup.create!(name: "#{ @prefix }_group", version_no: 1) TagGroup.create!(name: "#{ @prefix }_group", version_no: 1)
end end
@@ -88,7 +100,8 @@ RSpec.describe 'Shared resource IDs across MySQL connections', type: :model do
thread.value thread.value
end end
{ tags: [:external, :tag], wiki: [:description, :group] }.each do |space, kinds| { tags: [:external, :tag], wiki: [:description, :group],
wiki_history: [:description_version, :group_version] }.each do |space, kinds|
[false, true].each do |rollback| [false, true].each do |rollback|
it "serialises #{ space } inserts through #{ rollback ? 'rollback' : 'commit' }" do it "serialises #{ space } inserts through #{ rollback ? 'rollback' : 'commit' }" do
tables = SharedResourceIdAllocator::SPACES.fetch(space.to_s) tables = SharedResourceIdAllocator::SPACES.fetch(space.to_s)
@@ -96,7 +109,7 @@ RSpec.describe 'Shared resource IDs across MySQL connections', type: :model do
ApplicationRecord.connection.select_value("SELECT MAX(id) FROM #{ table }").to_i ApplicationRecord.connection.select_value("SELECT MAX(id) FROM #{ table }").to_i
end end
first, (first_id, connection_id, lock_name) = hold_creation(kinds.first, rollback:) first, (first_id, connection_id, lock_name) = hold_creation(kinds.first, rollback:)
expect(first_id).to eq(maxima.max + 1) expect(first_id).to be > maxima.max
connection = ApplicationRecord.connection connection = ApplicationRecord.connection
quoted_lock = connection.quote(lock_name) quoted_lock = connection.quote(lock_name)
expect(connection.select_value("SELECT IS_USED_LOCK(#{ quoted_lock })").to_i) expect(connection.select_value("SELECT IS_USED_LOCK(#{ quoted_lock })").to_i)
@@ -110,7 +123,7 @@ RSpec.describe 'Shared resource IDs across MySQL connections', type: :model do
expect(await_queue(started)).not_to eq(connection_id) expect(await_queue(started)).not_to eq(connection_id)
@release << true @release << true
expect(result(first)).to eq(first_id) expect(result(first)).to eq(first_id)
expect(result(second)).to eq(rollback ? first_id : first_id + 1) expect(result(second)).to be > first_id
expect(connection.select_value("SELECT IS_FREE_LOCK(#{ quoted_lock })").to_i).to eq(1) expect(connection.select_value("SELECT IS_FREE_LOCK(#{ quoted_lock })").to_i).to eq(1)
end end
end end
+109 -7
ファイルの表示
@@ -1,6 +1,100 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe SharedResourceIdAllocator do RSpec.describe SharedResourceIdAllocator do
let(:tag) { create(:tag) }
def resource table, id: nil, number: 1
case table
when 'tags'
Tag.new(id:, category: :general)
when 'external_tags'
ExternalTag.new(id:, platform: :nico, name: "external #{ number }")
when 'tag_descriptions'
TagDescription.new(id:, tag:, language_code: 'ja', body: 'body', version_no: number)
when 'tag_groups'
TagGroup.new(id:, name: "group #{ number }", version_no: number)
when 'tag_description_versions'
TagDescriptionVersion.new(
id:, tag_id: tag.id, language_code: 'ja', body: 'body', version_no: number,
event_type: 'create', created_at: Time.current)
when 'tag_group_versions'
TagGroupVersion.new(
id:, tag_group_id: group.id, name: group.name, version_no: number,
event_type: 'create', members: [], placements: [], created_at: Time.current)
end
end
let(:group) { TagGroup.create!(name: 'history owner', version_no: 1) }
described_class::SPACES.each do |space, tables|
tables.permutation.each do |first_table, second_table|
it "keeps deleted #{ first_table } IDs reserved for #{ second_table }" do
first = resource(first_table)
first.save!
used_id = first.id
first.class.where(id: first.id).delete_all
second = resource(second_table)
second.save!
expect(second.id).to be > used_id
end
it "advances #{ space } after an explicit INSERT in #{ first_table }" do
first = resource(first_table)
first.save!
high = first.id + 100
explicit = resource(first_table, id: high, number: 2)
first.class.where(id: first.id).delete_all
explicit.save!
explicit.class.where(id: explicit.id).delete_all
second = resource(second_table)
second.save!
expect(second.id).to be > high
end
end
it "rejects explicit IDs below the reserved #{ space } position" do
first = resource(tables.first)
first.save!
first.class.where(id: first.id).delete_all
expect { resource(tables.last, id: first.id).save! }
.to raise_error(ActiveRecord::RecordNotUnique)
end
it "preserves historical #{ space } overlaps without repairing them" do
first = resource(tables.first)
first.save!
second = resource(tables.last)
second.save!
second.class.where(id: second.id).update_all(id: first.id)
second = second.class.find(first.id)
snapshots = [first.reload.attributes, second.reload.attributes]
next_record = resource(tables.last, number: 2)
next_record.save!
expect(next_record.id).to be > first.id
expect(first.reload.attributes).to eq(snapshots.first)
expect(second.reload.attributes).to eq(snapshots.last)
end
end
it 'reserves a Tag ID referenced only by remaining description history' do
high = [Tag.maximum(:id).to_i, ExternalTag.maximum(:id).to_i].max + 1000
TagDescriptionVersion.create!(
tag_id: high, language_code: 'ja', version_no: 1, body: 'history',
event_type: 'create', created_at: Time.current)
expect(create(:tag).id).to be > high
expect(create(:external_tag).id).to be > high
end
it 'reserves a group ID referenced only by remaining group history' do
high = [TagGroup.maximum(:id).to_i, TagDescription.maximum(:id).to_i].max + 1000
version = resource('tag_group_versions')
version.save!
TagGroupVersion.where(id: version.id).update_all(tag_group_id: high)
expect(TagGroup.create!(name: 'replacement', version_no: 1).id).to be > high
end
end
RSpec.describe SharedResourceIdAllocator, 'lock lifecycle' do
let(:record) { build(:external_tag) } let(:record) { build(:external_tag) }
let(:connection) { double('connection') } let(:connection) { double('connection') }
let(:transaction) { double('transaction', open?: true) } let(:transaction) { double('transaction', open?: true) }
@@ -11,6 +105,14 @@ RSpec.describe SharedResourceIdAllocator do
config = double('config', database: 'test_shared_ids') config = double('config', database: 'test_shared_ids')
allow(connection).to receive_message_chain(:pool, :db_config).and_return(config) allow(connection).to receive_message_chain(:pool, :db_config).and_return(config)
allow(connection).to receive(:quote) { "'#{ _1 }'" } allow(connection).to receive(:quote) { "'#{ _1 }'" }
allow(connection).to receive(:quote_column_name) { "`#{ _1 }`" }
allow(connection).to receive(:execute) { sql << _1 }
allow(connection).to receive(:select_all) do |statement|
tables = described_class::SPACES.values.flatten.select do |table|
statement.include?("'#{ table }'")
end
tables.map { { 'TABLE_NAME' => _1, 'ENGINE' => 'InnoDB', 'AUTO_INCREMENT' => 9 } }
end
allow(connection).to receive(:quote_table_name) { "`#{ _1 }`" } allow(connection).to receive(:quote_table_name) { "`#{ _1 }`" }
allow(connection).to receive(:select_value) do |statement| allow(connection).to receive(:select_value) do |statement|
sql << statement sql << statement
@@ -35,7 +137,6 @@ RSpec.describe SharedResourceIdAllocator do
described_class.assign!(record) described_class.assign!(record)
expect(record.id).to eq(9) expect(record.id).to eq(9)
expect(sql.grep(/RELEASE_LOCK/)).to be_empty expect(sql.grep(/RELEASE_LOCK/)).to be_empty
expect(sql.grep(/FOR UPDATE/).size).to eq(2)
callbacks.fetch(event).call callbacks.fetch(event).call
expect(sql.grep(/RELEASE_LOCK/).size).to eq(1) expect(sql.grep(/RELEASE_LOCK/).size).to eq(1)
end end
@@ -49,22 +150,22 @@ RSpec.describe SharedResourceIdAllocator do
end end
it 'releases a successfully acquired lock when allocation fails and rolls back' do it 'releases a successfully acquired lock when allocation fails and rolls back' do
allow(connection).to receive(:select_value).with(/FOR UPDATE/).and_raise('query failed') allow(connection).to receive(:select_all).and_raise('query failed')
expect { described_class.assign!(record) }.to raise_error(RuntimeError, 'query failed') expect { described_class.assign!(record) }.to raise_error(RuntimeError, 'query failed')
callbacks.fetch(:after_rollback).call callbacks.fetch(:after_rollback).call
expect(sql.grep(/RELEASE_LOCK/).size).to eq(1) expect(sql.grep(/RELEASE_LOCK/).size).to eq(1)
end end
it 'preserves explicitly supplied historical IDs' do it 'accepts an explicit ID above the reserved position' do
record.id = 5 record.id = 50
described_class.assign!(record) described_class.assign!(record)
expect(record.id).to eq(5) expect(record.id).to eq(50)
expect(sql.grep(/FOR UPDATE/)).to be_empty expect(sql.grep(/FOR UPDATE/)).to be_empty
callbacks.fetch(:after_commit).call callbacks.fetch(:after_commit).call
end end
it 'uses the same named lock within each space and different locks between spaces' do it 'uses the same named lock within each space and different locks between spaces' do
locks = [Tag, ExternalTag, TagDescription, TagGroup].map do |model| locks = [Tag, ExternalTag, TagDescription, TagGroup, TagDescriptionVersion, TagGroupVersion].map do |model|
allow(model).to receive(:connection).and_return(connection) allow(model).to receive(:connection).and_return(connection)
allow(model).to receive(:current_transaction).and_return(transaction) allow(model).to receive(:current_transaction).and_return(transaction)
allow(model).to receive(:uncached).and_yield allow(model).to receive(:uncached).and_yield
@@ -76,7 +177,8 @@ RSpec.describe SharedResourceIdAllocator do
end end
expect(locks[0]).to eq(locks[1]) expect(locks[0]).to eq(locks[1])
expect(locks[2]).to eq(locks[3]) expect(locks[2]).to eq(locks[3])
expect(locks[0]).not_to eq(locks[2]) expect(locks[4]).to eq(locks[5])
expect([locks[0], locks[2], locks[4]].uniq.size).to eq(3)
end end
it 'reports lock release failures' do it 'reports lock release failures' do
-185
ファイルの表示
@@ -1,185 +0,0 @@
require 'digest'
require 'rails_helper'
RSpec.describe Wiki::Commit do
include WikiRecords
let(:user) { create_member_user! }
def create_page title:, body: 'initial body'
seed_wiki_page(
tag_name: create(:tag, primary_name: title).tag_name('ja'),
body:,
created_by_user: user,
message: 'init')
end
describe '.create_content!' do
it 'rejects standalone Help creation without adding page or history' do
name = create(:tag_name, name: 'Help:frozen_service')
counts = [WikiPage.count, WikiRevision.count, WikiVersion.count]
expect do
described_class.create_content!(tag_name: name, body: 'initial', created_by_user: user)
end.to raise_error(ActiveRecord::RecordInvalid)
expect([WikiPage.count, WikiRevision.count, WikiVersion.count]).to eq(counts)
end
it 'keeps the internal compatibility writer available' do
name = create(:tag_name, name: 'タググループ:compatibility_service')
page = nil
expect do
page = described_class.create_legacy_content!(
tag_name: name, body: 'initial', created_by_user: user)
end.to change(WikiPage, :count).by(1)
.and change(WikiRevision, :count).by(1)
.and change(WikiVersion, :count).by(1)
expect(page.current_revision.body).to eq('initial')
expect(page.wiki_versions.sole.body).to eq('initial')
end
it 'rejects body that becomes blank after normalisation' do
tag_name = create(:tag, primary_name: 'commit_integrity_blank').tag_name('ja')
expect {
described_class.create_content!(
tag_name:,
body: "\r\n\r\n",
created_by_user: user,
message: 'blank')
}
.to raise_error(ActiveRecord::RecordInvalid)
expect(WikiPage.where(tag_name:)).not_to exist
end
end
describe '.content!' do
it 'rejects standalone Help updates without modifying histories' do
page = seed_wiki_page(tag_name: create(:tag_name, name: 'Help:standalone'),
body: 'before', created_by_user: user)
counts = [WikiRevision.count, WikiVersion.count]
expect do
described_class.content!(page:, body: 'after', created_user: user)
end.to raise_error(ActiveRecord::RecordInvalid)
expect(page.reload.body).to eq('before')
expect([WikiRevision.count, WikiVersion.count]).to eq(counts)
end
it 'updates page body, revision, and version' do
page = create_page(title: 'commit_integrity_update', body: 'before')
current_id = page.current_revision.id
expect {
described_class.content!(
page:,
body: 'after',
created_user: user,
message: 'edit',
base_revision_id: current_id)
}
.to change(WikiRevision, :count).by(1)
.and change(WikiVersion, :count).by(1)
page.reload
version = page.wiki_versions.order(:version_no).last
expect(page.body).to eq('after')
expect(version.body).to eq('after')
expect(version.reason).to eq('edit')
end
it 'does not record tag_version on body-only wiki update' do
tag_name = create(:tag_name, name: 'commit_integrity_linked_tag')
tag = create(:tag, primary_tag_name: tag_name, category: :general)
page =
seed_wiki_page(
tag_name:,
body: 'before',
created_by_user: user,
message: 'init')
TagVersionRecorder.record!(
tag:,
event_type: :create,
created_by_user: user)
before_count = tag.reload.tag_versions.count
described_class.content!(
page:,
body: 'after',
created_user: user,
message: 'edit',
base_revision_id: page.current_revision.id)
expect(tag.reload.tag_versions.count).to eq(before_count)
end
it 'raises conflict and leaves page, revision, and version unchanged' do
page = create_page(title: 'commit_integrity_conflict', body: 'first')
stale_id = page.current_revision.id
described_class.content!(
page:,
body: 'second',
created_user: user,
message: 'second',
base_revision_id: stale_id)
page.reload
before_body = page.body
before_revision_count = page.wiki_revisions.count
before_version_count = page.wiki_versions.count
expect {
described_class.content!(
page:,
body: 'third',
created_user: user,
message: 'stale',
base_revision_id: stale_id)
}
.to raise_error(Wiki::Commit::Conflict)
page.reload
expect(page.body).to eq(before_body)
expect(page.wiki_revisions.count).to eq(before_revision_count)
expect(page.wiki_versions.count).to eq(before_version_count)
end
it 'deduplicates duplicated missing wiki lines' do
page = create_page(title: 'commit_integrity_dedup', body: 'before')
duplicated = 'commit_integrity_duplicate_line'
described_class.content!(
page:,
body: "#{ duplicated }\n#{ duplicated }",
created_user: user,
message: 'dedup',
base_revision_id: page.current_revision.id)
revision = page.reload.current_revision
expect(WikiLine.where(body: duplicated).count).to eq(1)
expect(revision.wiki_revision_lines.count).to eq(2)
expect(revision.wiki_revision_lines.pluck(:wiki_line_id).uniq.size).to eq(1)
end
end
describe '.redirect!' do
it 'raises because redirect revisions are deprecated' do
page = create_page(title: 'commit_integrity_redirect_source', body: 'source')
target = create_page(title: 'commit_integrity_redirect_target', body: 'target')
expect {
described_class.redirect!(
page:,
redirect_page: target,
created_user: user,
message: 'redirect',
base_revision_id: page.current_revision.id)
}
.to raise_error(RuntimeError, '廃止しました.')
end
end
end
-151
ファイルの表示
@@ -1,151 +0,0 @@
require 'rails_helper'
RSpec.describe Wiki::Commit do
include WikiRecords
let(:user) { create_member_user! }
def create_page(title: 'commit_spec_page', body: 'initial body')
tag_name = create(:tag, primary_name: title).tag_name('ja')
seed_wiki_page(
tag_name:,
body:,
created_by_user: user,
message: 'init')
end
describe '.content!' do
it 'stores normalised body in wiki_pages and wiki_versions' do
page = create_page(title: 'commit_normalised_page')
described_class.content!(
page:,
body: "a\r\nb\r\n\r\n",
created_user: user,
message: 'init'
)
page.reload
version = page.wiki_versions.order(:version_no).last
expect(page.body).to eq("a\nb")
expect(version.body).to eq("a\nb")
expect(page.current_revision.lines_count).to eq(2)
end
it 'deduplicates duplicated missing wiki lines before upsert' do
page = create_page(title: 'commit_duplicate_line_page')
duplicated = 'commit_duplicate_line'
described_class.content!(
page:,
body: "#{ duplicated }\n#{ duplicated }",
created_user: user,
message: 'init'
)
page.reload
expect(WikiLine.where(body: duplicated).count).to eq(1)
expect(page.current_revision.lines_count).to eq(2)
expect(page.current_revision.wiki_revision_lines.count).to eq(2)
end
it 'raises conflict when base_revision_id is stale' do
page = create_page(title: 'commit_conflict_page')
first = described_class.content!(
page:,
body: 'first',
created_user: user,
message: 'first'
)
described_class.content!(
page:,
body: 'second',
created_user: user,
message: 'second',
base_revision_id: first.id
)
expect {
described_class.content!(
page:,
body: 'third',
created_user: user,
message: 'third',
base_revision_id: first.id
)
}.to raise_error(Wiki::Commit::Conflict)
end
it 'does not record tag version when corresponding tag has no versions' do
tag_name = create(:tag_name, name: 'commit_linked_tag_without_versions')
tag = create(:tag, primary_tag_name: tag_name, category: :general)
page =
seed_wiki_page(
tag_name:,
body: 'before',
created_by_user: user,
message: 'init')
expect(tag.reload.tag_versions.count).to eq(0)
current_revision_id = page.current_revision.id
expect {
described_class.content!(
page:,
body: 'after',
created_user: user,
message: 'edit',
base_revision_id: current_revision_id)
}.to change(WikiVersion, :count).by(1)
expect(tag.reload.tag_versions.count).to eq(0)
end
it 'does not record tag version when corresponding tag has no versions' do
tag_name = create(:tag_name, name: 'commit_linked_tag_without_versions')
tag = create(:tag, primary_tag_name: tag_name, category: :general)
page =
seed_wiki_page(
tag_name:,
body: 'before',
created_by_user: user,
message: 'init')
current_revision_id = page.current_revision.id
expect {
described_class.content!(
page:,
body: 'after',
created_user: user,
message: 'edit',
base_revision_id: current_revision_id)
}.to change(WikiVersion, :count).by(1)
expect(tag.reload.tag_versions.count).to eq(0)
end
end
describe '.redirect!' do
it 'raises because redirect revisions are deprecated' do
page = create_page(title: 'commit_redirect_source')
target = create_page(title: 'commit_redirect_target')
expect {
described_class.redirect!(
page:,
redirect_page: target,
created_user: user,
message: 'redirect'
)
}.to raise_error(RuntimeError, '廃止しました.')
end
end
end
-100
ファイルの表示
@@ -1,100 +0,0 @@
require 'rails_helper'
RSpec.describe WikiVersionRecorder do
include WikiRecords
let(:user) { create_member_user! }
def create_page title:, body: 'body'
seed_wiki_page(
tag_name: create(:tag_name, name: title),
body:,
created_by_user: user,
message: 'init')
end
describe '.record!' do
it 'records title, body, reason, user, and version number' do
page = create_page(title: 'wiki_version_recorder_basic', body: 'body')
expect {
described_class.record!(
page:,
event_type: :update,
reason: 'manual reason',
created_by_user: user)
}
.to change { page.reload.wiki_versions.count }.by(1)
version = page.wiki_versions.order(:version_no).last
expect(version).to have_attributes(
version_no: 2,
event_type: 'update',
title: 'wiki_version_recorder_basic',
body: 'body',
reason: 'manual reason',
created_by_user_id: user.id
)
end
it 'does not create duplicated update version for identical snapshot' do
page = create_page(title: 'wiki_version_recorder_duplicate', body: 'body')
described_class.record!(
page:,
event_type: :update,
reason: nil,
created_by_user: user)
before_count = page.reload.wiki_versions.count
described_class.record!(
page:,
event_type: :update,
reason: nil,
created_by_user: user)
expect(page.reload.wiki_versions.count).to eq(before_count)
end
it 'creates update version when title changes' do
page = create_page(title: 'wiki_version_recorder_title_before', body: 'body')
page.tag_name.update!(name: 'wiki_version_recorder_title_after')
expect {
described_class.record!(
page:,
event_type: :update,
reason: 'rename',
created_by_user: user)
}
.to change { page.reload.wiki_versions.count }.by(1)
version = page.wiki_versions.order(:version_no).last
expect(version.title).to eq('wiki_version_recorder_title_after')
expect(version.body).to eq('body')
expect(version.reason).to eq('rename')
end
it 'creates update version when body changes' do
page = create_page(title: 'wiki_version_recorder_body', body: 'before')
page.update!(body: 'after')
expect {
described_class.record!(
page:,
event_type: :update,
reason: 'body',
created_by_user: user)
}
.to change { page.reload.wiki_versions.count }.by(1)
version = page.wiki_versions.order(:version_no).last
expect(version.title).to eq('wiki_version_recorder_body')
expect(version.body).to eq('after')
expect(version.reason).to eq('body')
end
end
end
+9 -36
ファイルの表示
@@ -7,8 +7,7 @@ module TagGroupRecords
'tag_groups', 'tag_group_tags', 'tag_group_groups', 'tag_groups', 'tag_group_tags', 'tag_group_groups',
'tag_group_placements', 'tag_group_versions'].freeze 'tag_group_placements', 'tag_group_versions'].freeze
WRITE_TABLES = GROUP_TABLES + [ WRITE_TABLES = GROUP_TABLES + [
'wiki_pages', 'wiki_revisions', 'wiki_versions', 'wiki_lines', 'tag_names', 'tag_description_versions',
'wiki_revision_lines', 'tag_names', 'tag_description_versions',
'tag_descriptions'] 'tag_descriptions']
def group_connection = ApplicationRecord.connection def group_connection = ApplicationRecord.connection
@@ -34,13 +33,11 @@ module TagGroupRecords
WRITE_TABLES.to_h { |table| [table, group_rows(table)] } WRITE_TABLES.to_h { |table| [table, group_rows(table)] }
end end
def expect_noop_group_update group_id:, page: def expect_noop_group_update group_id:
before_state = group_state before_state = group_state
group = TagGroup.find(group_id) group = TagGroup.find(group_id)
version_id = TagGroupVersion.find_by!( version_id = TagGroupVersion.find_by!(
tag_group_id: group_id, version_no: group.version_no).id tag_group_id: group_id, version_no: group.version_no).id
revision_id = page.reload.current_revision.id
wiki_version_id = page.wiki_versions.order(version_no: :desc).first.id
writes = [] writes = []
tables = Regexp.union(WRITE_TABLES) tables = Regexp.union(WRITE_TABLES)
subscriber = lambda do |*args| subscriber = lambda do |*args|
@@ -59,8 +56,6 @@ module TagGroupRecords
end end
expect(TagGroupVersion.find_by!( expect(TagGroupVersion.find_by!(
tag_group_id: group_id, version_no: group.reload.version_no).id).to eq(version_id) tag_group_id: group_id, version_no: group.reload.version_no).id).to eq(version_id)
expect(page.reload.current_revision.id).to eq(revision_id)
expect(page.wiki_versions.order(version_no: :desc).first.id).to eq(wiki_version_id)
expect(writes).to be_empty expect(writes).to be_empty
end end
end end
@@ -99,25 +94,13 @@ module TagGroupRecords
tag.reload tag.reload
end end
def legacy_group name, body def create_markdown_group name, body
# Historical fixtures preserve blank lines independently of today's writer. parsed = TagGroupMarkdownParser.parse(body:)
body = body.sub(/\n+\z/, '') TagGroupOperation.create!(name:, actor: user, **parsed.slice(:description, :members, :placements))
page = WikiPage.create!( end
tag_name: create(:tag_name, name: "タググループ:#{ name }"),
body:, created_user: user, updated_user: user) def group_markdown group
WikiVersionRecorder.record!(page:, event_type: :create, created_by_user: user) TagGroupMarkdownRenderer.render(TagGroupRepr.one(group.reload))
lines = body.split("\n")
shas = lines.map { Digest::SHA256.hexdigest(_1) }
revision = WikiRevision.create!(
wiki_page: page, kind: :content, created_user: user, lines_count: lines.size,
tree_sha256: Digest::SHA256.hexdigest(shas.join(',')))
lines.each_with_index do |line, position|
sha = shas.fetch(position)
line_id = WikiLine.find_by(sha256: sha)&.id || insert_group_row('wiki_lines', {
body: line, sha256: sha, created_at: Time.current, updated_at: Time.current })
WikiRevisionLine.create!(wiki_revision: revision, wiki_line_id: line_id, position:)
end
page
end end
def seed_group name:, placements: [], members: [], description: nil, id: nil def seed_group name:, placements: [], members: [], description: nil, id: nil
@@ -176,16 +159,6 @@ module TagGroupRecords
"/wiki/title/#{ CGI.escapeURIComponent("タググループ:#{ name }") }" "/wiki/title/#{ CGI.escapeURIComponent("タググループ:#{ name }") }"
end end
def migrate_groups
require_relative '../../db/migrate/20261008010000_migrate_tag_groups_from_wiki_pages'
migration = MigrateTagGroupsFromWikiPages.new
@group_migration_messages = []
allow(migration).to receive(:say) do |message, *_args|
@group_migration_messages << message if message.start_with?('Wiki #', 'Tag Group')
end
migration.suppress_messages { migration.up }
end
def expect_group_snapshot group_id, number:, placements:, members:, description: nil def expect_group_snapshot group_id, number:, placements:, members:, description: nil
current = group_rows('tag_groups', id: group_id).sole current = group_rows('tag_groups', id: group_id).sole
version = group_version(group_id, number) version = group_version(group_id, number)
+11 -17
ファイルの表示
@@ -1,20 +1,14 @@
# Historical records must remain seedable after application creation is frozen.
module WikiRecords module WikiRecords
def seed_wiki_page(tag_name:, body:, created_by_user:, message: nil) def seed_description(tag_name:, body:, created_by_user:, message: nil)
page = WikiPage.create!(tag_name:, body:, TagDescriptionOperation.create!(tag: tag_name.tag, body:, created_by_user:, message:)
created_user: created_by_user, updated_user: created_by_user) end
revision = Wiki::Commit.legacy_content!(
page:, body:, created_user: created_by_user, message:) def description_versions(description)
if tag_name.tag && tag_name.language_code == 'ja' TagDescriptionVersion.where(
TagDescription.create!( tag_id: description.tag_id, language_code: description.language_code)
id: page.id, tag: tag_name.tag, language_code: 'ja', body: page.reload.body, end
version_no: 1, created_by_user_id: created_by_user.id,
updated_by_user_id: created_by_user.id) def current_description_version(description)
TagDescriptionVersion.create!( description_versions(description).find_by!(version_no: description.reload.version_no)
id: revision.id, tag_id: tag_name.tag_id, language_code: 'ja',
version_no: 1, event_type: 'create', body: page.body,
created_by_user_id: created_by_user.id, created_at: revision.created_at)
end
page
end end
end end