From 99ae691ba1cab116ceed94dd618fb91c8f66f567 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Sun, 4 Oct 2026 23:52:26 +0900 Subject: [PATCH] #422 --- backend/config/routes.rb | 4 +- .../spec/requests/tag_descriptions_spec.rb | 289 +++++++++--------- 2 files changed, 140 insertions(+), 153 deletions(-) diff --git a/backend/config/routes.rb b/backend/config/routes.rb index 7e3ab03..4402503 100644 --- a/backend/config/routes.rb +++ b/backend/config/routes.rb @@ -27,8 +27,8 @@ Rails.application.routes.draw do get :deerjikists put :deerjikists, action: :update_deerjikists - get :description, action: 'tag_descriptions#show' - put :description, action: 'tag_descriptions#update' + get :description, to: 'tag_descriptions#show' + put :description, to: 'tag_descriptions#update' end end diff --git a/backend/spec/requests/tag_descriptions_spec.rb b/backend/spec/requests/tag_descriptions_spec.rb index 88d1a83..a0746e5 100644 --- a/backend/spec/requests/tag_descriptions_spec.rb +++ b/backend/spec/requests/tag_descriptions_spec.rb @@ -42,10 +42,10 @@ RSpec.describe 'Tag description native API', type: :request do expect(request.path_parameters).to include(controller: 'tag_descriptions', action:) end - def native_write method, body:, request_headers: headers - public_send(method, endpoint, params: { body: }, headers: request_headers) - action = method == :post ? 'create' : 'update' - expect(request.path_parameters).to include(controller: 'tag_descriptions', action:) + def native_write body:, request_headers: headers + put endpoint, params: { body: }, headers: request_headers + expect(request.path_parameters).to include( + controller: 'tag_descriptions', action: 'update') end def expect_current description @@ -71,9 +71,8 @@ RSpec.describe 'Tag description native API', type: :request do controller: 'tag_descriptions', action: 'show_by_title', name: 'native_description') end - it 'routes POST by Tag ID to create' do - expect(post: '/tags/123/description').to route_to( - controller: 'tag_descriptions', action: 'create', id: '123') + it 'does not route POST by Tag ID' do + expect(post: '/tags/123/description').not_to be_routable end it 'routes PUT by Tag ID to update' do @@ -203,170 +202,158 @@ RSpec.describe 'Tag description native API', type: :request do end end - describe 'POST /tags/:id/description' do - before { tag } + describe 'PUT /tags/:id/description' do + context 'without an existing description' do + before { tag } - it 'creates both current resources and histories with matching compatibility IDs' do - expect { native_write(:post, body: 'created natively') } - .to change(TagDescription, :count).by(1) - .and change(TagDescriptionVersion, :count).by(1) - .and change(WikiPage, :count).by(1) - .and change(WikiRevision, :count).by(1) - .and change(WikiVersion, :count).by(1) - expect(response).to have_http_status(:created) - description = TagDescription.find_by!(tag_id: tag.id, language_code: 'ja') - page = WikiPage.find(description.id) - revision = page.current_revision - version = TagDescriptionVersion.find(revision.id) - expect(description).to have_attributes( - body: 'created natively', version_no: 1, - created_by_user_id: editor.id, updated_by_user_id: editor.id) - expect(version).to have_attributes( - tag_id: tag.id, language_code: 'ja', body: description.body, - version_no: 1, event_type: 'create', created_by_user_id: editor.id) - expect(page).to have_attributes(tag_name_id: tag_name.id, body: description.body) - expect(revision.body).to eq(version.body) - expect(page.wiki_versions.last.body).to eq(version.body) - expect_current(description) + it 'creates both current resources and histories with matching compatibility IDs' do + expect { native_write(body: 'created natively') } + .to change(TagDescription, :count).by(1) + .and change(TagDescriptionVersion, :count).by(1) + .and change(WikiPage, :count).by(1) + .and change(WikiRevision, :count).by(1) + .and change(WikiVersion, :count).by(1) + expect(response).to have_http_status(:created) + description = TagDescription.find_by!(tag_id: tag.id, language_code: 'ja') + page = WikiPage.find(description.id) + revision = page.current_revision + version = TagDescriptionVersion.find(revision.id) + expect(description).to have_attributes( + body: 'created natively', version_no: 1, + created_by_user_id: editor.id, updated_by_user_id: editor.id) + expect(version).to have_attributes( + tag_id: tag.id, language_code: 'ja', body: description.body, + version_no: 1, event_type: 'create', created_by_user_id: editor.id) + expect(page).to have_attributes(tag_name_id: tag_name.id, body: description.body) + expect(revision.body).to eq(version.body) + expect(page.wiki_versions.last.body).to eq(version.body) + expect_current(description) - get "/wiki/#{ description.id }" - expect(response).to have_http_status(:ok) - expect(json).to include('body' => description.body, 'revision_id' => version.id) - end + get "/wiki/#{ description.id }" + expect(response).to have_http_status(:ok) + expect(json).to include('body' => description.body, 'revision_id' => version.id) + end - it 'returns the existing validation error format on duplicate creation' do - existing = TagDescription.create!( - tag:, language_code: 'ja', body: 'existing', version_no: 1, - created_by_user_id: author.id, updated_by_user_id: author.id) - before_state = stored_state - native_write(:post, body: 'duplicate') - expect(response).to have_http_status(:unprocessable_entity) - expect(json).to include('type' => 'validation_error', 'errors' => a_kind_of(Hash), - 'base_errors' => a_kind_of(Array), 'message' => a_kind_of(String)) - expect(stored_state).to eq(before_state) - expect(existing.reload.body).to eq('existing') - end - - [WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion].each do |model| - it "rolls back native create when #{ model.name } persistence fails" do - headers - before_state = stored_state - aggregate_failures do - fail_save(model) { native_write(:post, body: 'rollback create') } - expect(stored_state).to eq(before_state) + [WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion].each do |model| + it "rolls back native create when #{ model.name } persistence fails" do + headers + before_state = stored_state + aggregate_failures do + fail_save(model) { native_write(body: 'rollback create') } + expect(stored_state).to eq(before_state) + end end end end - end - describe 'PUT /tags/:id/description' do - let!(:page) do - tag - Wiki::Commit.create_content!(tag_name:, body: 'before', created_by_user: author) - end - let!(:description) do - TagDescription.find_by!(tag_id: tag.id, language_code: 'ja') - end - - it 'writes matching histories when the body changes' do - expect(page.reload.version_no).to eq(1) - expect(description.version_no).to eq(1) - expect { native_write(:put, body: 'after') } - .to change(WikiRevision, :count).by(1) - .and change(WikiVersion, :count).by(1) - .and change(TagDescriptionVersion, :count).by(1) - expect(response).to have_http_status(:ok) - expect(description.reload).to have_attributes( - body: 'after', version_no: 2, - created_by_user_id: author.id, updated_by_user_id: editor.id) - expect(page.reload).to have_attributes(body: 'after', version_no: 2) - - revision = page.current_revision - version = TagDescriptionVersion.find(revision.id) - expect(version).to have_attributes( - tag_id: tag.id, language_code: 'ja', version_no: 2, event_type: 'update', - body: 'after', created_by_user_id: editor.id) - expect(revision.body).to eq(version.body) - expect(page.wiki_versions.order(:version_no).last.body).to eq(version.body) - get "/wiki/#{ description.id }" - expect(response).to have_http_status(:ok) - expect(json).to include('body' => 'after', 'revision_id' => version.id) - end - - ['before', "before\n\n"].each do |body| - it 'does not write histories when the normalised body is unchanged' do - before_state = stored_state - page_state = page.reload.attributes - description_state = description.reload.attributes - current_revision_id = page.current_revision.id - - native_write(:put, body:) - - expect(response).to have_http_status(:ok) - expect(stored_state).to eq(before_state) - expect(page.reload.attributes).to eq(page_state) - expect(description.reload.attributes).to eq(description_state) - expect(page.current_revision.id).to eq(current_revision_id) + context 'with an existing description' do + let!(:page) do + tag + Wiki::Commit.create_content!(tag_name:, body: 'before', created_by_user: author) + end + let!(:description) do + TagDescription.find_by!(tag_id: tag.id, language_code: 'ja') end - end - [WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion].each do |model| - it "rolls back native update when #{ model.name } persistence fails" do - headers - before_state = stored_state - aggregate_failures do - fail_save(model) { native_write(:put, body: 'rollback update') } + it 'writes matching histories when the body changes' do + expect(page.reload.version_no).to eq(1) + expect(description.version_no).to eq(1) + expect { native_write(body: 'after') } + .to change(WikiRevision, :count).by(1) + .and change(WikiVersion, :count).by(1) + .and change(TagDescriptionVersion, :count).by(1) + expect(response).to have_http_status(:ok) + expect(description.reload).to have_attributes( + body: 'after', version_no: 2, + created_by_user_id: author.id, updated_by_user_id: editor.id) + expect(page.reload).to have_attributes(body: 'after', version_no: 2) + + revision = page.current_revision + version = TagDescriptionVersion.find(revision.id) + expect(version).to have_attributes( + tag_id: tag.id, language_code: 'ja', version_no: 2, event_type: 'update', + body: 'after', created_by_user_id: editor.id) + expect(revision.body).to eq(version.body) + expect(page.wiki_versions.order(:version_no).last.body).to eq(version.body) + get "/wiki/#{ description.id }" + expect(response).to have_http_status(:ok) + expect(json).to include('body' => 'after', 'revision_id' => version.id) + end + + ['before', "before\n\n"].each do |body| + it 'does not write histories when the normalised body is unchanged' do + before_state = stored_state + page_state = page.reload.attributes + description_state = description.reload.attributes + current_revision_id = page.current_revision.id + + native_write(body:) + + expect(response).to have_http_status(:ok) expect(stored_state).to eq(before_state) + expect(page.reload.attributes).to eq(page_state) + expect(description.reload.attributes).to eq(description_state) + expect(page.current_revision.id).to eq(current_revision_id) + end + end + + [WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion].each do |model| + it "rolls back native update when #{ model.name } persistence fails" do + headers + before_state = stored_state + aggregate_failures do + fail_save(model) { native_write(body: 'rollback update') } + expect(stored_state).to eq(before_state) + end end end end end describe 'write access and validation' do - [:post, :put].each do |method| - context "with #{ method.upcase }" do - before do - tag - if method == :put - Wiki::Commit.create_content!(tag_name:, body: 'existing', created_by_user: author) - end - end + before { tag } - it 'requires authentication without changing either system' do - before_state = stored_state - native_write(method, body: 'unauthorised', request_headers: { }) - expect(response).to have_http_status(:unauthorized) - expect(stored_state).to eq(before_state) - end + it 'requires authentication without changing either system' do + before_state = stored_state + native_write(body: 'unauthorised', request_headers: { }) + expect(response).to have_http_status(:unauthorized) + expect(stored_state).to eq(before_state) + end - it 'rejects a guest without changing either system' do - guest = create(:user, :guest) - before_state = stored_state - native_write(method, body: 'forbidden', - request_headers: { 'X-Transfer-Code' => guest.inheritance_code }) - expect(response).to have_http_status(:forbidden) - expect(stored_state).to eq(before_state) - end + it 'rejects a guest without changing either system' do + guest = create(:user, :guest) + before_state = stored_state + native_write(body: 'forbidden', + request_headers: { 'X-Transfer-Code' => guest.inheritance_code }) + expect(response).to have_http_status(:forbidden) + expect(stored_state).to eq(before_state) + end - it 'rejects a banned member without changing either system' do - banned = create(:user, :member, :banned) - before_state = stored_state - native_write(method, body: 'forbidden', - request_headers: { 'X-Transfer-Code' => banned.inheritance_code }) - expect(response).to have_http_status(:forbidden) - expect(stored_state).to eq(before_state) - end + it 'rejects a banned member without changing either system' do + banned = create(:user, :member, :banned) + before_state = stored_state + native_write(body: 'forbidden', + request_headers: { 'X-Transfer-Code' => banned.inheritance_code }) + expect(response).to have_http_status(:forbidden) + expect(stored_state).to eq(before_state) + end - it 'rejects blank body using the existing validation error format' do - headers - before_state = stored_state - native_write(method, body: '') - expect(response).to have_http_status(:unprocessable_entity) - expect(json).to include('type' => 'validation_error', - 'errors' => include('body' => a_kind_of(Array))) - expect(stored_state).to eq(before_state) - end - end + it 'rejects blank body using the existing validation error format' do + headers + before_state = stored_state + native_write(body: '') + expect(response).to have_http_status(:unprocessable_entity) + expect(json).to include('type' => 'validation_error', + 'errors' => include('body' => a_kind_of(Array))) + expect(stored_state).to eq(before_state) + end + + it 'returns 404 when PUT targets a missing Tag' do + missing_id = Tag.maximum(:id).to_i + 1 + put "/tags/#{ missing_id }/description", + params: { body: 'missing' }, + headers: headers + expect(response).to have_http_status(:not_found) end end end