このコミットが含まれているのは:
@@ -0,0 +1,23 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Schema after Wiki removal' do
|
||||
it 'has no legacy Wiki tables or name indirection columns' do
|
||||
connection = ApplicationRecord.connection
|
||||
['wiki_pages', 'wiki_revisions', 'wiki_lines', 'wiki_revision_lines', 'wiki_versions']
|
||||
.each do |table|
|
||||
expect(connection.table_exists?(table)).to be(false)
|
||||
end
|
||||
expect(connection.column_exists?(:tags, :tag_name_id)).to be(false)
|
||||
expect(connection.column_exists?(:tag_names, :canonical_id)).to be(false)
|
||||
end
|
||||
|
||||
it 'requires TagName ownership while keeping the native resources and history' do
|
||||
connection = ApplicationRecord.connection
|
||||
expect(TagName.columns_hash.fetch('tag_id').null).to be(false)
|
||||
expect(connection.foreign_key_exists?(:tag_names, :tags)).to be(true)
|
||||
['tag_descriptions', 'tag_description_versions', 'tag_groups', 'tag_group_versions']
|
||||
.each do |table|
|
||||
expect(connection.table_exists?(table)).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,7 +4,8 @@ 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:)
|
||||
TagGroupOperation.create!(
|
||||
Locale.nipponese, name:, description: nil, members:, placements:, actor:)
|
||||
end
|
||||
|
||||
it 'retains version IDs and snapshots after destroying an empty group' do
|
||||
@@ -16,7 +17,8 @@ RSpec.describe TagGroup, type: :model do
|
||||
|
||||
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'))))
|
||||
expect(TagGroupRepr.historical(
|
||||
Locale.nipponese, TagGroupVersion.find(versions.first.fetch('id'))))
|
||||
.to include('id' => group.id, 'name' => 'history owner', 'members' => [])
|
||||
end
|
||||
|
||||
@@ -26,14 +28,8 @@ RSpec.describe TagGroup, type: :model do
|
||||
parent = create_group(
|
||||
name: 'parent group',
|
||||
members: [{ 'tag_group_id' => child.id }],
|
||||
placements: [{
|
||||
'section_name' => 'section',
|
||||
'description' => nil,
|
||||
'children' => [{
|
||||
'tag_group_id' => child.id,
|
||||
'children' => []
|
||||
}]
|
||||
}])
|
||||
placements: [{ 'section_name' => 'section', 'description' => nil,
|
||||
'children' => [{ 'tag_group_id' => child.id, 'children' => [] }] }])
|
||||
|
||||
versions = parent.versions.order(:version_no).map(&:attributes)
|
||||
|
||||
|
||||
@@ -34,6 +34,66 @@ RSpec.describe TagNameSanitisationRule, type: :model do
|
||||
described_class.create!(priority: 10, source_pattern: 'foo_', replacement: 'foo')
|
||||
end
|
||||
|
||||
[:primary, :alias].each do |source_kind|
|
||||
it "keeps the primary identity when a #{ source_kind } collides within its Tag" do
|
||||
tag = create(:tag, primary_name: source_kind == :primary ? 'temporary' : 'foobar')
|
||||
primary = tag.tag_name('ja')
|
||||
alias_name = create(:tag_name, :alias, tag:,
|
||||
name: source_kind == :primary ? 'foobar' : 'temporary_alias')
|
||||
source = source_kind == :primary ? primary : alias_name
|
||||
source.update_columns(name: 'foo_bar')
|
||||
|
||||
expect { described_class.apply! }.to change(TagName, :count).by(-1)
|
||||
.and change(Tag, :count).by(0)
|
||||
|
||||
expect(primary.reload).to have_attributes(
|
||||
name: 'foobar', primary_flg: true, tag_id: tag.id)
|
||||
expect(TagName.exists?(alias_name.id)).to be(false)
|
||||
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true))
|
||||
.to contain_exactly(primary)
|
||||
expect(tag.tag_versions.order(:version_no).last.names.fetch('ja'))
|
||||
.to include('name' => 'foobar', 'aliases' => [])
|
||||
end
|
||||
end
|
||||
|
||||
it 'deduplicates colliding aliases without changing the primary name' do
|
||||
tag = create(:tag, primary_name: 'primary')
|
||||
primary = tag.tag_name('ja')
|
||||
kept = create(:tag_name, :alias, tag:, name: 'foobar')
|
||||
removed = create(:tag_name, :alias, tag:, name: 'temporary_alias')
|
||||
removed.update_columns(name: 'foo_bar')
|
||||
|
||||
described_class.apply!
|
||||
|
||||
expect(primary.reload).to have_attributes(name: 'primary', primary_flg: true)
|
||||
expect(kept.reload.tag_id).to eq(tag.id)
|
||||
expect(TagName.exists?(removed.id)).to be(false)
|
||||
expect(tag.tag_versions.order(:version_no).last.names.fetch('ja').fetch('aliases'))
|
||||
.to eq([{ 'name' => 'foobar', 'script_code' => 'Jpan' }])
|
||||
end
|
||||
|
||||
it 'rolls back a collision merge when the source owns a description' do
|
||||
user = create_member_user!
|
||||
target = create(:tag, primary_name: 'foobar')
|
||||
source = create(:tag, primary_name: 'temporary')
|
||||
TagDescriptionOperation.create!(
|
||||
Locale.nipponese, tag: source, body: 'retained', created_by_user: user)
|
||||
source.tag_name('ja').update_columns(name: 'foo_bar')
|
||||
post = create(:post)
|
||||
PostTag.create!(post:, tag: source)
|
||||
models = [Tag, TagName, TagVersion, TagDescription, TagDescriptionVersion, PostTag]
|
||||
snapshots = models.map { |model| model.order(*Array(model.primary_key)).map(&:attributes) }
|
||||
|
||||
expect { described_class.apply! }.to raise_error(ActiveRecord::RecordInvalid)
|
||||
|
||||
after_state = models.map do |model|
|
||||
model.order(*Array(model.primary_key)).map(&:attributes)
|
||||
end
|
||||
expect(after_state).to eq(snapshots)
|
||||
expect(post.reload.tags).to contain_exactly(source)
|
||||
expect(target.reload.name('ja')).to eq('foobar')
|
||||
end
|
||||
|
||||
context 'when only another language has the sanitised name' do
|
||||
include_context 'English locale'
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe User, type: :model do
|
||||
it 'nullifies description authorship without deleting native content or history' do
|
||||
author = create_member_user!
|
||||
editor = create_member_user!
|
||||
tag = create(:tag)
|
||||
description = TagDescriptionOperation.create!(
|
||||
Locale.nipponese, tag:, body: 'initial', created_by_user: author)
|
||||
TagDescriptionOperation.update!(
|
||||
Locale.nipponese, tag:, body: 'edited', updated_by_user: editor)
|
||||
versions = TagDescriptionVersion.where(tag_id: tag.id).order(:version_no)
|
||||
snapshots = versions.map(&:attributes)
|
||||
|
||||
expect(author.created_tag_descriptions).to contain_exactly(description)
|
||||
expect(editor.updated_tag_descriptions).to contain_exactly(description)
|
||||
expect { author.destroy! }.to change(TagDescription, :count).by(0)
|
||||
.and change(TagDescriptionVersion, :count).by(0)
|
||||
expect(description.reload).to have_attributes(
|
||||
body: 'edited', created_by_user_id: nil, updated_by_user_id: editor.id)
|
||||
expect { editor.destroy! }.not_to change(TagDescription, :count)
|
||||
expect(description.reload.updated_by_user_id).to be_nil
|
||||
expect(versions.reload.map(&:attributes)).to eq(snapshots)
|
||||
end
|
||||
end
|
||||
@@ -26,7 +26,8 @@ RSpec.describe 'Tag description Wiki facade writes', type: :request do
|
||||
|
||||
context 'with an existing description' do
|
||||
let!(:description) do
|
||||
TagDescriptionOperation.create!(tag:, body: 'before', created_by_user: user)
|
||||
TagDescriptionOperation.create!(
|
||||
Locale.nipponese, tag:, body: 'before', created_by_user: user)
|
||||
end
|
||||
let!(:first_version) { current_description_version(description) }
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Tag group locale contracts', type: :request do
|
||||
include TagGroupRecords
|
||||
include_context 'Ainu locale contract'
|
||||
|
||||
let!(:user) { create_member_user! }
|
||||
let!(:tag) { create(:tag, primary_name: 'japanese_member') }
|
||||
let!(:ainu_name) { add_ainu_name(tag, 'ainu_member') }
|
||||
let(:headers) { { 'X-Transfer-Code' => user.inheritance_code } }
|
||||
let(:members) { [{ 'tag_id' => tag.id }] }
|
||||
let(:placements) do
|
||||
[{ 'section_name' => 'section', 'description' => nil, 'children' => [
|
||||
{ 'tag_id' => tag.id, 'children' => [
|
||||
{ 'tag_id' => tag.id, 'children' => [] }] }] }]
|
||||
end
|
||||
let!(:group) do
|
||||
TagVersioning.ensure_snapshot!(tag, created_by_user: user)
|
||||
TagGroupOperation.create!(
|
||||
ainu_locale, name: 'locale group', description: nil, members:, placements:, actor: user)
|
||||
end
|
||||
|
||||
it 'uses selected-language names in native collection and nested placements' do
|
||||
["/tag_groups/#{ group.id }", '/tag_groups'].each do |path|
|
||||
get path, params: { locale: 'ain' }
|
||||
expect(response).to have_http_status(:ok)
|
||||
representation = json.is_a?(Array) ? json.sole : json
|
||||
expect(representation.fetch('members').sole.fetch('tag').fetch('name'))
|
||||
.to eq('ainu_member')
|
||||
node = representation.fetch('placements').sole.fetch('children').sole
|
||||
expect(node.fetch('tag').fetch('name')).to eq('ainu_member')
|
||||
expect(node.fetch('children').sole.fetch('tag').fetch('name')).to eq('ainu_member')
|
||||
expect(response.body).not_to include('japanese_member')
|
||||
end
|
||||
end
|
||||
|
||||
it 'uses snapshot names recursively for history and current names for the latest view' do
|
||||
version = group.versions.sole
|
||||
ainu_name.update!(name: 'renamed_ainu')
|
||||
get "/wiki/#{ group.id }", params: { locale: 'ain' }
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('body')).to include('renamed_ainu')
|
||||
get "/wiki/#{ group.id }", params: { locale: 'ain', version: version.id }
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('body').scan('ainu_member').length).to eq(2)
|
||||
expect(json.fetch('body')).not_to include('renamed_ainu', 'japanese_member')
|
||||
get "/wiki/#{ group.id }/diff", params: { locale: 'ain', from: version.id }
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('diff').map { _1.fetch('type') }.uniq).to eq(['context'])
|
||||
end
|
||||
|
||||
['タググループ:', 'tag_group:', 'TAG_GROUP:'].each do |prefix|
|
||||
it "round trips #{ prefix } through create, lookup, search and update" do
|
||||
post '/wiki', params: {
|
||||
locale: 'ain', title: "#{ prefix }parent", body: '- tag_group:locale group' },
|
||||
headers: headers
|
||||
expect(response).to have_http_status(:created)
|
||||
id = json.fetch('id')
|
||||
expect(json.fetch('body')).to include('タググループ:locale group')
|
||||
path = "/wiki/title/#{ CGI.escapeURIComponent("#{ prefix }parent") }"
|
||||
get path, params: { locale: 'ain' }
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('id')).to eq(id)
|
||||
get "#{ path }/exists", params: { locale: 'ain' }
|
||||
expect(response).to have_http_status(:no_content)
|
||||
get '/wiki/search', params: { locale: 'ain', title: "#{ prefix }parent" }
|
||||
expect(json.map { _1.fetch('id') }).to eq([id])
|
||||
put "/wiki/#{ id }", params: {
|
||||
locale: 'ain', title: "#{ prefix }renamed", body: '- ainu_member' }, headers: headers
|
||||
expect(response).to have_http_status(:ok)
|
||||
get "/tag_groups/#{ id }", params: { locale: 'ain' }
|
||||
expect(json.fetch('members').sole.fetch('tag').fetch('name')).to eq('ainu_member')
|
||||
end
|
||||
end
|
||||
|
||||
it 'resolves Markdown names in the requested language and rolls back invalid input' do
|
||||
parsed = TagGroupMarkdownParser.parse(ainu_locale, body: '- ainu_member')
|
||||
expect(parsed.fetch(:members)).to eq(members)
|
||||
expect do
|
||||
TagGroupMarkdownParser.parse(ainu_locale, body: '- japanese_member')
|
||||
end.to raise_error(TagGroupMarkdownParser::InvalidDocument)
|
||||
before_state = group_state
|
||||
put "/wiki/#{ group.id }", params: {
|
||||
locale: 'ain', title: 'タググループ:partial rename', body: '- japanese_member' },
|
||||
headers: headers
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(group_state).to eq(before_state)
|
||||
end
|
||||
|
||||
it 'passes locale into native validation and facade compatibility checks' do
|
||||
post '/tag_groups', params: {
|
||||
locale: 'ain', name: 'native', description: nil, members:, placements: },
|
||||
headers:, as: :json
|
||||
expect(response).to have_http_status(:created)
|
||||
id = json.fetch('id')
|
||||
expect(json.fetch('members').sole.fetch('tag').fetch('name')).to eq('ainu_member')
|
||||
get "/wiki/#{ id }", params: { locale: 'ain' }
|
||||
expect(response).to have_http_status(:ok)
|
||||
body = json.fetch('body')
|
||||
put "/wiki/#{ id }", params: { locale: 'ain', title: 'tag_group:native', body: },
|
||||
headers: headers
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(TagGroup.find(id).version_no).to eq(1)
|
||||
end
|
||||
|
||||
it 'does not substitute current or Japanese names for a missing historical language' do
|
||||
other = create(:tag, primary_name: 'historical_japanese')
|
||||
TagVersioning.ensure_snapshot!(other, created_by_user: user)
|
||||
old_group = TagGroupOperation.create!(
|
||||
Locale.nipponese, name: 'old group', description: nil,
|
||||
members: [{ 'tag_id' => other.id }],
|
||||
placements: [{ 'tag_id' => other.id, 'children' => [] }], actor: user)
|
||||
add_ainu_name(other, 'current_ainu')
|
||||
version = old_group.versions.sole
|
||||
representation = TagGroupRepr.historical(ainu_locale, version)
|
||||
expect(representation.fetch('members').sole.fetch('tag').fetch('name')).to be_nil
|
||||
expect(representation.fetch('placements').sole.fetch('tag').fetch('name')).to be_nil
|
||||
get "/wiki/#{ old_group.id }", params: { locale: 'ain', version: version.id }
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.body).not_to include('historical_japanese', 'current_ainu')
|
||||
end
|
||||
end
|
||||
@@ -115,6 +115,15 @@ RSpec.describe 'Tag group Markdown round trips through the Wiki facade', type: :
|
||||
MARKDOWN
|
||||
end
|
||||
|
||||
it 'round trips a top-level list followed by heading Sections' do
|
||||
check_round_trip(
|
||||
members: [tag_member(series), tag_member(deer)],
|
||||
placements: [tag_node(series),
|
||||
{ 'section_name' => '区分', 'description' => nil,
|
||||
'children' => [tag_node(deer)] }],
|
||||
markdown: "- ぼざろクリーチャーシリーズ\n\n## 区分\n\n- 洗澡鹿")
|
||||
end
|
||||
|
||||
it 'rejects mixed top-level heading Sections and Tags without losing structure' do
|
||||
id = seed_group(
|
||||
name: '混在構造', members: [tag_member(series)],
|
||||
|
||||
@@ -10,8 +10,9 @@ RSpec.describe 'Tag group Wiki facade reads', type: :request do
|
||||
let!(:page) { create_markdown_group('読取り契約', '- 旧タグ') }
|
||||
let!(:first_revision) { page.versions.sole }
|
||||
let!(:last_revision) do
|
||||
parsed = TagGroupMarkdownParser.parse(body: '- 新タグ')
|
||||
parsed = TagGroupMarkdownParser.parse(Locale.nipponese, body: '- 新タグ')
|
||||
TagGroupOperation.update!(
|
||||
Locale.nipponese,
|
||||
group: page, name: page.name, actor: user,
|
||||
**parsed.slice(:description, :members, :placements))
|
||||
page.versions.order(:version_no).last
|
||||
@@ -67,7 +68,8 @@ RSpec.describe 'Tag group Wiki facade reads', type: :request do
|
||||
end
|
||||
|
||||
it 'combines Tag Groups and Tag Descriptions without Help in the unfiltered list' do
|
||||
TagDescriptionOperation.create!(tag: new_tag, body: 'tag description', created_by_user: user)
|
||||
TagDescriptionOperation.create!(
|
||||
Locale.nipponese, tag: new_tag, body: 'tag description', created_by_user: user)
|
||||
get '/wiki'
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.map { _1.fetch('title') }).to include(
|
||||
@@ -87,6 +89,7 @@ RSpec.describe 'Tag group Wiki facade reads', type: :request do
|
||||
it 'does not restore a stale namespaced Wiki page after a group rename' do
|
||||
group = create_markdown_group('旧名称', '正本')
|
||||
TagGroupOperation.update!(
|
||||
Locale.nipponese,
|
||||
group:, name: '新名称', description: '正本', members: [], placements: [], actor: user)
|
||||
|
||||
get group_title_path('旧名称')
|
||||
@@ -99,7 +102,8 @@ RSpec.describe 'Tag group Wiki facade reads', type: :request do
|
||||
end
|
||||
|
||||
it 'orders mixed resource history by timestamp rather than independent IDs' do
|
||||
TagDescriptionOperation.create!(tag: new_tag, body: 'description', created_by_user: user)
|
||||
TagDescriptionOperation.create!(
|
||||
Locale.nipponese, tag: new_tag, body: 'description', created_by_user: user)
|
||||
TagGroupVersion.update_all(created_at: 3.days.ago)
|
||||
TagDescriptionVersion.update_all(created_at: 1.day.ago)
|
||||
|
||||
|
||||
@@ -248,6 +248,7 @@ RSpec.describe 'TagVersions API', type: :request do
|
||||
message: 'init')
|
||||
|
||||
TagDescriptionOperation.update!(
|
||||
Locale.nipponese,
|
||||
tag: wiki_page.tag,
|
||||
body: 'after',
|
||||
updated_by_user: member,
|
||||
|
||||
@@ -1402,11 +1402,8 @@ RSpec.describe 'Tags API', type: :request do
|
||||
message: 'init')
|
||||
|
||||
TagDescriptionOperation.update!(
|
||||
tag: wiki_page.tag,
|
||||
body: 'wiki body before',
|
||||
updated_by_user: member_user,
|
||||
message: 'init'
|
||||
)
|
||||
Locale.nipponese, tag: wiki_page.tag, body: 'wiki body before',
|
||||
updated_by_user: member_user, message: 'init')
|
||||
|
||||
wiki_history = description_versions(wiki_page).map(&:attributes)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ RSpec.describe 'Wiki conflict handling', type: :request do
|
||||
stale_id = current_description_version(page).id
|
||||
|
||||
TagDescriptionOperation.update!(
|
||||
Locale.nipponese,
|
||||
tag: page.tag,
|
||||
body: 'second',
|
||||
updated_by_user: user,
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Wiki and native API locale contracts', type: :request do
|
||||
include WikiRecords
|
||||
include TagGroupRecords
|
||||
include_context 'Ainu locale contract'
|
||||
|
||||
let!(:user) { create_member_user! }
|
||||
let!(:tag) { create(:tag, primary_name: 'japanese_title') }
|
||||
let!(:ainu_name) { add_ainu_name(tag, 'ainu_title') }
|
||||
let(:headers) { { 'X-Transfer-Code' => user.inheritance_code } }
|
||||
let!(:japanese) do
|
||||
TagDescriptionOperation.create!(
|
||||
Locale.nipponese, tag:, body: 'Japanese body', created_by_user: user)
|
||||
end
|
||||
let!(:ainu) do
|
||||
TagDescriptionOperation.create!(
|
||||
ainu_locale, tag:, body: 'Ainu body', created_by_user: user)
|
||||
end
|
||||
|
||||
['/wiki', '/wiki/search'].each do |path|
|
||||
it "filters #{ path } by the selected language" do
|
||||
get path, params: { locale: 'ain' }
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.map { _1.fetch('id') }).to eq([ainu.id])
|
||||
expect(json.sole).to include('title' => 'ainu_title', 'body' => 'Ainu body')
|
||||
|
||||
get path, params: { locale: 'ain', title: 'japanese' }
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
it 'uses the existing default locale when no locale is specified' do
|
||||
get '/wiki'
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.map { _1.fetch('id') }).to eq([japanese.id])
|
||||
end
|
||||
|
||||
it 'resolves titles and aliases only in the selected language in both APIs' do
|
||||
['/wiki/title/japanese_title', '/tags/name/japanese_title/description'].each do |path|
|
||||
get path, params: { locale: 'ain' }
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
['/wiki/title/ainu_title', '/tags/name/ainu_title/description'].each do |path|
|
||||
get path, params: { locale: 'ain' }
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('body')).to eq('Ainu body')
|
||||
end
|
||||
get '/wiki/title/ainu_title/exists', params: { locale: 'ain' }
|
||||
expect(response).to have_http_status(:no_content)
|
||||
get '/wiki/title/japanese_title/exists', params: { locale: 'ain' }
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
|
||||
it 'does not disclose another language through ID, existence, diff or history' do
|
||||
revision = current_description_version(japanese)
|
||||
["/wiki/#{ japanese.id }", "/wiki/#{ japanese.id }/exists",
|
||||
"/wiki/#{ japanese.id }/diff"].each do |path|
|
||||
get path, params: { locale: 'ain', from: revision.id }
|
||||
expect(response).to have_http_status(:not_found)
|
||||
expect(response.body).not_to include('Japanese body')
|
||||
end
|
||||
get '/wiki/changes', params: { locale: 'ain', id: japanese.id }
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json).to eq([])
|
||||
get '/wiki/changes', params: { locale: 'ain' }
|
||||
expect(json.map { _1.fetch('revision_id') })
|
||||
.to eq([current_description_version(ainu).id])
|
||||
end
|
||||
|
||||
it 'does not update a description addressed through another language ID' do
|
||||
snapshot = [japanese.attributes, ainu.attributes, ainu_name.attributes]
|
||||
expect do
|
||||
put "/wiki/#{ japanese.id }", params: {
|
||||
locale: 'ain', title: 'renamed', body: 'wrong target' }, headers: headers
|
||||
end.not_to change(TagDescriptionVersion, :count)
|
||||
expect(response).to have_http_status(:not_found)
|
||||
expect([japanese.reload.attributes, ainu.reload.attributes, ainu_name.reload.attributes])
|
||||
.to eq(snapshot)
|
||||
end
|
||||
|
||||
[:wiki, :native].each do |api|
|
||||
it "creates the selected language through #{ api } without changing Japanese history" do
|
||||
other = create(:tag, primary_name: 'other_japanese')
|
||||
add_ainu_name(other, 'other_ainu')
|
||||
original = TagDescriptionOperation.create!(
|
||||
Locale.nipponese, tag: other, body: 'original', created_by_user: user)
|
||||
snapshot = original.attributes
|
||||
expect do
|
||||
if api == :wiki
|
||||
post '/wiki', params: { locale: 'ain', title: 'other_ainu', body: 'created' },
|
||||
headers: headers
|
||||
else
|
||||
put "/tags/#{ other.id }/description", params: { locale: 'ain', body: 'created' },
|
||||
headers: headers
|
||||
end
|
||||
end.to change(TagDescription, :count).by(1)
|
||||
.and change(TagDescriptionVersion, :count).by(1)
|
||||
expect(response).to have_http_status(:created)
|
||||
created = TagDescription.find_by!(tag: other, language_code: 'ain')
|
||||
expect(created).to have_attributes(body: 'created', version_no: 1)
|
||||
expect(current_description_version(created)).to have_attributes(language_code: 'ain')
|
||||
expect(original.reload.attributes).to eq(snapshot)
|
||||
end
|
||||
end
|
||||
|
||||
it 'shares selected-language edits and revision identity across both APIs' do
|
||||
snapshot = japanese.attributes
|
||||
first = current_description_version(ainu)
|
||||
put "/tags/#{ tag.id }/description", params: { locale: 'ain', body: 'native edit' },
|
||||
headers: headers
|
||||
expect(response).to have_http_status(:ok)
|
||||
second = current_description_version(ainu)
|
||||
expect(second).to have_attributes(language_code: 'ain', version_no: 2)
|
||||
expect(second.id).not_to eq(first.id)
|
||||
get "/wiki/#{ ainu.id }", params: { locale: 'ain' }
|
||||
expect(json).to include('body' => 'native edit', 'revision_id' => second.id)
|
||||
|
||||
put "/wiki/#{ ainu.id }", params: {
|
||||
locale: 'ain', title: 'ainu_title', body: 'facade edit',
|
||||
base_revision_id: second.id }, headers: headers
|
||||
expect(response).to have_http_status(:ok)
|
||||
get "/tags/#{ tag.id }/description", params: { locale: 'ain' }
|
||||
expect(json).to include('body' => 'facade edit', 'language_code' => 'ain', 'version_no' => 3)
|
||||
expect(japanese.reload.attributes).to eq(snapshot)
|
||||
end
|
||||
|
||||
it 'rejects a stale selected-language revision without a partial title or body edit' do
|
||||
stale = current_description_version(ainu)
|
||||
TagDescriptionOperation.update!(
|
||||
ainu_locale, tag:, body: 'current', updated_by_user: user)
|
||||
snapshot = [ainu.reload.attributes, ainu_name.attributes, japanese.attributes]
|
||||
expect do
|
||||
put "/wiki/#{ ainu.id }", params: {
|
||||
locale: 'ain', title: 'conflicting_name', body: 'current',
|
||||
base_revision_id: stale.id }, headers: headers
|
||||
end.to change(TagDescriptionVersion, :count).by(0).and change(TagVersion, :count).by(0)
|
||||
expect(response).to have_http_status(:conflict)
|
||||
expect([ainu.reload.attributes, ainu_name.reload.attributes, japanese.reload.attributes])
|
||||
.to eq(snapshot)
|
||||
end
|
||||
|
||||
it 'scopes historical show and diff to the selected description' do
|
||||
first = current_description_version(ainu)
|
||||
TagDescriptionOperation.update!(
|
||||
ainu_locale, tag:, body: 'later', updated_by_user: user)
|
||||
get "/wiki/#{ ainu.id }", params: { locale: 'ain', version: first.id }
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json).to include('body' => 'Ainu body', 'revision_id' => first.id)
|
||||
get "/wiki/#{ ainu.id }/diff", params: { locale: 'ain', from: first.id }
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.fetch('newer_revision_id')).to eq(current_description_version(ainu).id)
|
||||
get "/wiki/#{ ainu.id }/diff", params: {
|
||||
locale: 'ain', from: current_description_version(japanese).id }
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
@@ -232,7 +232,9 @@ RSpec.describe 'Wiki API', type: :request do
|
||||
context 'when conflict' do
|
||||
it 'returns 409 when base_revision_id mismatches' do
|
||||
# 先に別ユーザ(同じ member でもOK)が 1 回更新して先頭を進める
|
||||
TagDescriptionOperation.update!(tag: page.tag, body: "zzz", updated_by_user: member, message: 'other edit')
|
||||
TagDescriptionOperation.update!(
|
||||
Locale.nipponese, tag: page.tag, body: 'zzz',
|
||||
updated_by_user: member, message: 'other edit')
|
||||
page.reload
|
||||
|
||||
stale_id = description_versions(page).order(:id).first.id # わざと古い id
|
||||
@@ -314,12 +316,14 @@ RSpec.describe 'Wiki API', type: :request do
|
||||
|
||||
describe 'GET /wiki/changes' do
|
||||
let!(:rev1) do
|
||||
TagDescriptionOperation.update!(tag: page.tag, body: "a\nb", updated_by_user: user, message: 'r1')
|
||||
TagDescriptionOperation.update!(
|
||||
Locale.nipponese, tag: page.tag, body: "a\nb", updated_by_user: user, message: 'r1')
|
||||
current_description_version(page)
|
||||
end
|
||||
|
||||
let!(:rev2) do
|
||||
TagDescriptionOperation.update!(tag: page.tag, body: "a\nc", updated_by_user: user, message: 'r2')
|
||||
TagDescriptionOperation.update!(
|
||||
Locale.nipponese, tag: page.tag, body: "a\nc", updated_by_user: user, message: 'r2')
|
||||
current_description_version(page)
|
||||
end
|
||||
|
||||
@@ -354,7 +358,7 @@ RSpec.describe 'Wiki API', type: :request do
|
||||
it 'returns empty array when page has no revisions and filtered by id' do
|
||||
# 別ページを作って revision 無し
|
||||
tn2 = create(:tag_name, name: 'spec_no_rev')
|
||||
# 異常データ: revision 無し WikiPage を直接作る
|
||||
# 異常データ: Version のない TagDescription を直接作る
|
||||
p2 = TagDescription.create!(
|
||||
tag: tn2.tag, language_code: 'ja', body: 'init', version_no: 1)
|
||||
|
||||
@@ -392,12 +396,14 @@ RSpec.describe 'Wiki API', type: :request do
|
||||
|
||||
describe 'GET /wiki/:id/diff' do
|
||||
let!(:rev_a) do
|
||||
TagDescriptionOperation.update!(tag: page.tag, body: "a\nb\nc", updated_by_user: user, message: 'A')
|
||||
TagDescriptionOperation.update!(
|
||||
Locale.nipponese, tag: page.tag, body: "a\nb\nc", updated_by_user: user, message: 'A')
|
||||
current_description_version(page)
|
||||
end
|
||||
|
||||
let!(:rev_b) do
|
||||
TagDescriptionOperation.update!(tag: page.tag, body: "a\nx\nc", updated_by_user: user, message: 'B')
|
||||
TagDescriptionOperation.update!(
|
||||
Locale.nipponese, tag: page.tag, body: "a\nx\nc", updated_by_user: user, message: 'B')
|
||||
current_description_version(page)
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
RSpec.shared_context 'Ainu locale contract' do
|
||||
let(:ainu_locale) { instance_double(Locale, language_code: 'ain', script_code: 'Jpan') }
|
||||
|
||||
before do
|
||||
# Only the foreign-key reference is seeded; no Locale or enum is enabled.
|
||||
Language.connection.execute(<<~SQL)
|
||||
INSERT IGNORE INTO languages (code, name, created_at)
|
||||
VALUES ('ain', 'Ainu', CURRENT_TIMESTAMP)
|
||||
SQL
|
||||
allow(Locale).to receive(:find_by).and_call_original
|
||||
allow(Locale).to receive(:find_by).with(code: 'ain').and_return(ainu_locale)
|
||||
end
|
||||
|
||||
def add_ainu_name tag, name
|
||||
TagName.insert_all!([{
|
||||
tag_id: tag.id, language_code: 'ain', script_code: 'Jpan', name:,
|
||||
primary_flg: true, auto_generated: false,
|
||||
created_at: Time.current, updated_at: Time.current }])
|
||||
tag.tag_names.reset
|
||||
TagName.find_by!(tag_id: tag.id, language_code: 'ain', name:)
|
||||
end
|
||||
end
|
||||
@@ -94,13 +94,14 @@ module TagGroupRecords
|
||||
tag.reload
|
||||
end
|
||||
|
||||
def create_markdown_group name, body
|
||||
parsed = TagGroupMarkdownParser.parse(body:)
|
||||
TagGroupOperation.create!(name:, actor: user, **parsed.slice(:description, :members, :placements))
|
||||
def create_markdown_group name, body, locale: Locale.nipponese
|
||||
parsed = TagGroupMarkdownParser.parse(locale, body:)
|
||||
TagGroupOperation.create!(
|
||||
locale, name:, actor: user, **parsed.slice(:description, :members, :placements))
|
||||
end
|
||||
|
||||
def group_markdown group
|
||||
TagGroupMarkdownRenderer.render(TagGroupRepr.one(group.reload))
|
||||
def group_markdown group, locale: Locale.nipponese
|
||||
TagGroupMarkdownRenderer.render(TagGroupRepr.one(locale, group.reload))
|
||||
end
|
||||
|
||||
def seed_group name:, placements: [], members: [], description: nil, id: nil
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module WikiRecords
|
||||
def seed_description(tag_name:, body:, created_by_user:, message: nil)
|
||||
TagDescriptionOperation.create!(tag: tag_name.tag, body:, created_by_user:, message:)
|
||||
def seed_description(tag_name:, body:, created_by_user:, message: nil, locale: Locale.nipponese)
|
||||
TagDescriptionOperation.create!(locale, tag: tag_name.tag, body:, created_by_user:, message:)
|
||||
end
|
||||
|
||||
def description_versions(description)
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする