このコミットが含まれているのは:
@@ -453,7 +453,6 @@ class WikiPagesController < ApplicationController
|
||||
|
||||
WikiPage
|
||||
.joins(:tag_name)
|
||||
.where.not(id: descriptions.select(:id))
|
||||
.where.not('tag_names.name LIKE ?', "#{ TagGroupRepr::PREFIX }%")
|
||||
.where('tag_names.tag_id IS NULL OR tag_names.tag_id NOT IN (?)',
|
||||
descriptions.select(:tag_id))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする