Reviewed-on: #429 Co-authored-by: miteruzo <miteruzo@naver.com>
このコミットはプルリクエスト #429 でマージされました。
このコミットが含まれているのは:
@@ -0,0 +1,220 @@
|
||||
require 'cgi'
|
||||
|
||||
# SQL fixtures use the migration contract without inventing application models.
|
||||
# Include explicitly; these helpers must not affect unrelated examples.
|
||||
module TagGroupRecords
|
||||
GROUP_TABLES = [
|
||||
'tag_groups', 'tag_group_tags', 'tag_group_groups',
|
||||
'tag_group_placements', 'tag_group_versions'].freeze
|
||||
WRITE_TABLES = GROUP_TABLES + [
|
||||
'wiki_pages', 'wiki_revisions', 'wiki_versions', 'wiki_lines',
|
||||
'wiki_revision_lines', 'tag_names', 'tag_description_versions',
|
||||
'tag_descriptions']
|
||||
|
||||
def group_connection = ApplicationRecord.connection
|
||||
|
||||
def group_rows table, conditions = { }
|
||||
clauses = conditions.map do |column, value|
|
||||
"#{ group_connection.quote_column_name(column) } = #{ group_connection.quote(value) }"
|
||||
end
|
||||
sql = "SELECT * FROM #{ group_connection.quote_table_name(table) }"
|
||||
sql += " WHERE #{ clauses.join(' AND ') }" if clauses.any?
|
||||
group_connection.select_all(sql).to_a.sort_by { |row| row.values.map(&:to_s) }
|
||||
end
|
||||
|
||||
def insert_group_row table, attributes
|
||||
columns = attributes.keys.map { group_connection.quote_column_name(_1) }.join(', ')
|
||||
values = attributes.values.map { group_connection.quote(_1) }.join(', ')
|
||||
group_connection.insert(
|
||||
"INSERT INTO #{ group_connection.quote_table_name(table) } (#{ columns }) " +
|
||||
"VALUES (#{ values })")
|
||||
end
|
||||
|
||||
def group_state
|
||||
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)
|
||||
injected = false
|
||||
subscriber = lambda do |*args|
|
||||
sql = args.last.fetch(:sql)
|
||||
next unless sql.match?(/\A\s*(?:INSERT INTO|UPDATE)\s+`?#{ table }`?\s/i)
|
||||
|
||||
injected = true
|
||||
raise failure, 'injected after SQL write'
|
||||
end
|
||||
begin
|
||||
ActiveSupport::Notifications.subscribed(subscriber, 'sql.active_record') { yield }
|
||||
rescue failure
|
||||
# Inspect the database after the operation's transaction unwinds.
|
||||
end
|
||||
aggregate_failures do
|
||||
expect(injected).to be(true), "No INSERT/UPDATE reached #{ table }"
|
||||
expect(group_state).to eq(before_state)
|
||||
end
|
||||
end
|
||||
|
||||
def group_version group_id, number
|
||||
row = group_rows('tag_group_versions', tag_group_id: group_id, version_no: number).sole
|
||||
row.merge('members' => JSON.parse(row.fetch('members')),
|
||||
'placements' => JSON.parse(row.fetch('placements')))
|
||||
end
|
||||
|
||||
def group_tag name
|
||||
tag = create(:tag, primary_name: name)
|
||||
TagVersioning.ensure_snapshot!(tag, created_by_user: user)
|
||||
tag.reload
|
||||
end
|
||||
|
||||
def legacy_group name, body
|
||||
# Historical fixtures preserve blank lines independently of today's writer.
|
||||
body = body.sub(/\n+\z/, '')
|
||||
page = WikiPage.create!(
|
||||
tag_name: create(:tag_name, name: "タググループ:#{ name }"),
|
||||
body:, created_user: user, updated_user: user)
|
||||
WikiVersionRecorder.record!(page:, event_type: :create, created_by_user: user)
|
||||
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
|
||||
|
||||
def seed_group name:, placements: [], members: [], description: nil, id: nil
|
||||
now = Time.current.change(usec: 0)
|
||||
attributes = {
|
||||
name:, description:, version_no: 1,
|
||||
created_by_user_id: user.id, updated_by_user_id: user.id,
|
||||
created_at: now, updated_at: now }
|
||||
attributes[:id] = id if id
|
||||
group_id = insert_group_row('tag_groups', attributes)
|
||||
insert_group_row('tag_group_versions', {
|
||||
tag_group_id: group_id, version_no: 1, event_type: 'create',
|
||||
name:, description:, members: members.to_json, placements: placements.to_json,
|
||||
created_by_user_id: user.id, created_at: now })
|
||||
members.each do |member|
|
||||
tag_member = member.key?('tag_id')
|
||||
table = tag_member ? 'tag_group_tags' : 'tag_group_groups'
|
||||
key = tag_member ? :tag_id : :child_tag_group_id
|
||||
value = member.fetch(tag_member ? 'tag_id' : 'tag_group_id')
|
||||
insert_group_row(table, {
|
||||
tag_group_id: group_id, key => value,
|
||||
created_by_user_id: user.id, created_at: now })
|
||||
end
|
||||
position = 0
|
||||
append = lambda do |nodes, parent|
|
||||
nodes.each do |node|
|
||||
position += 1
|
||||
own_position = position
|
||||
insert_group_row('tag_group_placements', {
|
||||
tag_group_id: group_id, position:, parent_position: parent,
|
||||
section_name: node['section_name'], description: node['description'],
|
||||
tag_id: node['tag_id'], child_tag_group_id: node['tag_group_id'],
|
||||
created_by_user_id: user.id, updated_by_user_id: user.id,
|
||||
created_at: now, updated_at: now })
|
||||
append.call(node.fetch('children'), own_position)
|
||||
end
|
||||
end
|
||||
append.call(placements, nil)
|
||||
group_id
|
||||
end
|
||||
|
||||
def without_group_legacy_reads
|
||||
queries = []
|
||||
subscriber = lambda do |*args|
|
||||
sql = args.last.fetch(:sql)
|
||||
if (sql.match?(/\bSELECT\b/i) &&
|
||||
sql.match?(/\bwiki_(pages|revisions|versions|lines|revision_lines)\b/i))
|
||||
queries << sql
|
||||
end
|
||||
end
|
||||
ActiveSupport::Notifications.subscribed(subscriber, 'sql.active_record') { yield }
|
||||
expect(queries).to be_empty
|
||||
end
|
||||
|
||||
def group_title_path name
|
||||
"/wiki/title/#{ CGI.escapeURIComponent("タググループ:#{ name }") }"
|
||||
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
|
||||
current = group_rows('tag_groups', id: group_id).sole
|
||||
version = group_version(group_id, number)
|
||||
expect(current).to include('version_no' => number, 'description' => description),
|
||||
@group_migration_messages&.join("\n")
|
||||
expect(version).to include(
|
||||
'name' => current.fetch('name'), 'description' => description,
|
||||
'version_no' => number, 'placements' => placements)
|
||||
expect(version.fetch('members')).to match_array(members)
|
||||
expect(group_rows('tag_group_tags', tag_group_id: group_id).map { _1.fetch('tag_id') })
|
||||
.to match_array(members.filter_map { _1['tag_id'] })
|
||||
children = group_rows('tag_group_groups', tag_group_id: group_id)
|
||||
expect(children.map { _1.fetch('child_tag_group_id') })
|
||||
.to match_array(members.filter_map { _1['tag_group_id'] })
|
||||
|
||||
rows = group_rows('tag_group_placements', tag_group_id: group_id)
|
||||
tree = lambda do |parent|
|
||||
rows.select { _1['parent_position'] == parent }.sort_by { _1.fetch('position') }
|
||||
.map do |row|
|
||||
node = if row['tag_id']
|
||||
{ 'tag_id' => row.fetch('tag_id') }
|
||||
elsif row['child_tag_group_id']
|
||||
{ 'tag_group_id' => row.fetch('child_tag_group_id') }
|
||||
else
|
||||
{ 'section_name' => row.fetch('section_name'), 'description' => row['description'] }
|
||||
end
|
||||
node.merge('children' => tree.call(row.fetch('position')))
|
||||
end
|
||||
end
|
||||
expect(tree.call(nil)).to eq(placements)
|
||||
end
|
||||
end
|
||||
新しいイシューから参照
ユーザーをブロックする