このコミットが含まれているのは:
2026-10-16 00:15:21 +09:00
コミット f362b915a5
16個のファイルの変更、458行の追加、32行の削除
+2 -1
ファイルの表示
@@ -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) }
+122
ファイルの表示
@@ -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
+9
ファイルの表示
@@ -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)],
+7 -3
ファイルの表示
@@ -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)
+1
ファイルの表示
@@ -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,
+2 -5
ファイルの表示
@@ -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)
+1
ファイルの表示
@@ -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,
+158
ファイルの表示
@@ -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
+12 -6
ファイルの表示
@@ -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