このコミットが含まれているのは:
2026-10-10 13:42:24 +09:00
コミット 20b5abbd2c
5個のファイルの変更、312行の追加、1行の削除
+73
ファイルの表示
@@ -212,6 +212,79 @@ RSpec.describe 'Tag group Wiki facade reads', type: :request do
expect(json.fetch('body').strip).to eq('独立した説明')
end
context 'when Help, description and group share the same numeric ID' do
let(:colliding_help) do
Wiki::Commit.create_legacy_content!(
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 all three resources without deduplicating their numeric ID' 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_help.id, colliding_help.title],
[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_help.current_revision.id, colliding_help.title],
[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 colliding group, Help and description IDs by title' do
help = Wiki::Commit.create_legacy_content!(
tag_name: create(:tag_name, name: 'Help:ID衝突'), body: 'help', created_by_user: user)
+59
ファイルの表示
@@ -115,6 +115,65 @@ RSpec.describe 'Tag group Wiki facade writes', type: :request do
put "/wiki/#{ page.id }", params: { title:, body: }, headers: request_headers
end
[1, 3].each do |repetitions|
it "keeps identical Markdown unchanged after #{ repetitions } Wiki PUTs" do
body = page.body
travel_to(1.hour.from_now) do
expect_noop_group_update(group_id: page.id, page:) do
repetitions.times do
update_group(body:)
expect(response).to have_http_status(:ok)
end
end
end
end
end
it 'keeps identical Markdown unchanged with a stale base_revision_id' do
update_group
expect(response).to have_http_status(:ok)
body = page.reload.body
expect(page.current_revision.id).not_to eq(old_revision.id)
travel_to(1.hour.from_now) do
expect_noop_group_update(group_id: page.id, page:) do
put "/wiki/#{ page.id }",
params: { title: page.title, body:, base_revision_id: old_revision.id },
headers: headers
end
end
end
it 'keeps the original editor on identical Markdown from another member' do
editor = create(:user, :member)
body = page.body
travel_to(1.hour.from_now) do
expect_noop_group_update(group_id: page.id, page:) do
update_group(body:, request_headers: { 'X-Transfer-Code' => editor.inheritance_code })
end
end
end
it 'does not add history for Wiki content already changed through Native' do
changed_members = [{ 'tag_id' => other_tag.id }]
changed_placements = [{ 'tag_id' => other_tag.id, 'children' => [] }]
put "/tag_groups/#{ page.id }",
params: { name: '更新', description: nil,
members: changed_members, placements: changed_placements },
headers:, as: :json
expect(response).to have_http_status(:ok)
expect_group_snapshot(
page.id, number: 2,
members: [{ 'tag_id' => other_tag.id, 'version_no' => other_tag.version_no }],
placements: changed_placements)
body = page.reload.body
expect(body).to eq('- 伊地知ニジカ')
travel_to(1.hour.from_now) do
expect_noop_group_update(group_id: page.id, page:) { update_group(body:) }
end
end
it 'replaces current membership and placement, retaining the old snapshot' do
old = group_version(page.id, 1)
update_group
+149
ファイルの表示
@@ -2,6 +2,7 @@ require 'rails_helper'
RSpec.describe 'Tag group native API', type: :request do
include TagGroupRecords
include ActiveSupport::Testing::TimeHelpers
let!(:user) { create_member_user! }
let!(:group_id) { seed_group(name: '専用API', description: '説明') }
@@ -242,6 +243,154 @@ RSpec.describe 'Tag group native API', type: :request do
{ 'tag_group_id' => child_id, 'version_no' => 1 }]
end
it 'treats identical native content with unplaced membership as a no-op' do
unplaced_payload = payload.merge(
name: '専用API', members: members + [{ 'tag_id' => unplaced.id }])
native_write(:update, unplaced_payload)
expect(response).to have_http_status(:ok)
expect_group_snapshot(
group_id, number: 2, description: '全体の説明', placements:,
members: snapshot_members + [
{ '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
expect_noop_group_update(group_id:, page:) do
native_write(:update, unplaced_payload)
end
end
end
context 'no-op updates against synchronised state' do
let(:synchronised_payload) do
{ name: '構造化', description: '全体の説明',
members: [{ 'tag_id' => tag.id }, { 'tag_group_id' => child_id }], placements: }
end
let(:additional_tag) { group_tag('追加対象タグ') }
let(:compatibility_page) do
WikiPage.joins(:tag_name).find_by!(tag_names: { name: 'タググループ:構造化' })
end
before do
native_write(:update, synchronised_payload)
expect(response).to have_http_status(:ok)
expect(compatibility_page.body).to include('- 掲載タグ', '- タググループ:子')
end
[1, 3].each do |repetitions|
it "keeps identical native content unchanged after #{ repetitions } PUTs" do
travel_to(1.hour.from_now) do
expect_noop_group_update(group_id:, page: compatibility_page) do
repetitions.times do
native_write(:update, synchronised_payload)
expect(response).to have_http_status(:ok)
end
end
end
end
end
it 'treats membership order alone as a no-op' do
travel_to(1.hour.from_now) do
expect_noop_group_update(group_id:, page: compatibility_page) do
native_write(:update, synchronised_payload.merge(
members: synchronised_payload.fetch(:members).reverse))
end
end
end
it 'keeps the original editor when another member submits identical content' do
editor = create(:user, :member)
travel_to(1.hour.from_now) do
expect_noop_group_update(group_id:, page: compatibility_page) do
put "/tag_groups/#{ group_id }", params: synchronised_payload,
headers: { 'X-Transfer-Code' => editor.inheritance_code }, as: :json
end
end
end
it 'does not add history for Native content already changed through Wiki' do
put "/wiki/#{ group_id }",
params: { title: 'タググループ:構造化', body: '- 未掲載タグ' }, headers: headers
expect(response).to have_http_status(:ok)
expect(compatibility_page.reload.body).to eq('- 未掲載タグ')
same_content = {
name: '構造化', description: nil, members: [{ 'tag_id' => unplaced.id }],
placements: [{ 'tag_id' => unplaced.id, 'children' => [] }] }
expect_group_snapshot(
group_id, number: 3, description: nil,
members: [{ 'tag_id' => unplaced.id, 'version_no' => unplaced.version_no }],
placements: same_content.fetch(:placements))
travel_to(1.hour.from_now) do
expect_noop_group_update(group_id:, page: compatibility_page) do
native_write(:update, same_content)
end
end
end
['name', 'description', 'membership addition', 'membership removal',
'Section name', 'Section description', 'placement order', 'placement hierarchy']
.each do |change|
it "records a real #{ change } change in both histories" do
changed = synchronised_payload.deep_dup
section = changed.fetch(:placements).first
children = section.fetch('children')
case change
when 'name' then changed[:name] = '実際の改名'
when 'description' then changed[:description] = '実際の説明変更'
when 'membership addition'
expect(changed.fetch(:members)).not_to include('tag_id' => additional_tag.id)
changed[:members] << { 'tag_id' => additional_tag.id }
children << { 'tag_id' => additional_tag.id, 'children' => [] }
when 'membership removal'
changed[:members].reject! { _1.key?('tag_group_id') }
children.first['children'] = []
when 'Section name' then section['section_name'] = '別区分'
when 'Section description' then section['description'] = '別の区分説明'
when 'placement order' then children.reverse!
when 'placement hierarchy'
children << children.first.fetch('children').pop
end
old_versions = group_rows('tag_group_versions', tag_group_id: group_id)
group = TagGroup.find(group_id)
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) }
expect(response).to have_http_status(:ok)
expected_members = changed.fetch(:members).map do |member|
target = if member.key?('tag_id')
Tag.find(member.fetch('tag_id'))
else
TagGroup.find(member.fetch('tag_group_id'))
end
member.merge('version_no' => target.version_no)
end
expect_group_snapshot(
group_id, number: number + 1, members: expected_members,
placements: changed.fetch(:placements), description: changed.fetch(:description))
expect(group.reload.name).to eq(changed.fetch(:name))
versions = group_rows('tag_group_versions', tag_group_id: group_id)
expect(versions.size).to eq(old_versions.size + 1)
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))
expect(response).to have_http_status(:ok)
expect(json.fetch('body')).to eq(page.body)
end
end
end
[:create, :update].each do |operation|
it "synchronises Wiki current state and histories on native #{ operation }" do
native_write(operation)
+31
ファイルの表示
@@ -34,6 +34,37 @@ module TagGroupRecords
WRITE_TABLES.to_h { |table| [table, group_rows(table)] }
end
def expect_noop_group_update group_id:, page:
before_state = group_state
group = TagGroup.find(group_id)
version_id = TagGroupVersion.find_by!(
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 = []
tables = Regexp.union(WRITE_TABLES)
subscriber = lambda do |*args|
sql = args.last.fetch(:sql)
if sql.match?(/\A\s*(?:INSERT INTO|UPDATE|DELETE FROM)\s+`?(?:#{ tables })`?\s/i)
writes << sql
end
end
aggregate_failures 'no-op current state, history and SQL writes' do
ActiveSupport::Notifications.subscribed(subscriber, 'sql.active_record') { yield }
expect(response).to have_http_status(:ok)
after_state = group_state
before_state.each do |table, rows|
expect(after_state.fetch(table)).to eq(rows), "No-op changed #{ table }"
end
expect(TagGroupVersion.find_by!(
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
end
end
def expect_atomic_group_write table
before_state = group_state
failure = Class.new(StandardError)