このコミットが含まれているのは:
@@ -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
|
||||
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする