| @@ -238,21 +238,26 @@ class TagsController < ApplicationController | |||||
| TagVersioning.ensure_snapshot!(tag, created_by_user: current_user) | TagVersioning.ensure_snapshot!(tag, created_by_user: current_user) | ||||
| old_name = tag.name | old_name = tag.name | ||||
| name_changed = name != old_name | |||||
| wiki_page = tag.tag_name.wiki_page if name_changed | |||||
| tag.update!(category:) | tag.update!(category:) | ||||
| tag.tag_name.update!(name:) | tag.tag_name.update!(name:) | ||||
| alias_names << old_name if name != old_name | |||||
| alias_names << old_name if name_changed | |||||
| alias_names.delete(name) | alias_names.delete(name) | ||||
| update_aliases!(tag, alias_names) | update_aliases!(tag, alias_names) | ||||
| update_parent_tags!(tag, parent_names) | update_parent_tags!(tag, parent_names) | ||||
| tag.reload | tag.reload | ||||
| record_tag_version!(tag, | |||||
| event_type: :update, | |||||
| created_by_user: current_user, | |||||
| name_changed: name.present? && name != old_name) | |||||
| record_tag_version!( | |||||
| tag, | |||||
| event_type: :update, | |||||
| created_by_user: current_user, | |||||
| name_changed:, | |||||
| wiki_page:) | |||||
| end | end | ||||
| render json: TagRepr.base(tag.reload) | render json: TagRepr.base(tag.reload) | ||||
| @@ -277,6 +282,7 @@ class TagsController < ApplicationController | |||||
| old_name = tag.name | old_name = tag.name | ||||
| name_changed = name.present? && name != old_name | name_changed = name.present? && name != old_name | ||||
| wiki_page = tag.tag_name.wiki_page if name_changed | |||||
| tag.tag_name.update!(name:) if name.present? | tag.tag_name.update!(name:) if name.present? | ||||
| tag.update!(category:) if category.present? | tag.update!(category:) if category.present? | ||||
| @@ -287,7 +293,8 @@ class TagsController < ApplicationController | |||||
| tag, | tag, | ||||
| event_type: :update, | event_type: :update, | ||||
| created_by_user: current_user, | created_by_user: current_user, | ||||
| name_changed:) | |||||
| name_changed:, | |||||
| wiki_page:) | |||||
| end | end | ||||
| render json: TagRepr.base(tag.reload) | render json: TagRepr.base(tag.reload) | ||||
| @@ -309,7 +316,7 @@ class TagsController < ApplicationController | |||||
| material: material.as_json&.merge(file:, content_type:)) | material: material.as_json&.merge(file:, content_type:)) | ||||
| end | end | ||||
| def record_tag_version! tag, event_type:, created_by_user:, name_changed: false | |||||
| def record_tag_version! tag, event_type:, created_by_user:, name_changed: false, wiki_page: nil | |||||
| if tag.nico? | if tag.nico? | ||||
| NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user:) | NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user:) | ||||
| return | return | ||||
| @@ -319,10 +326,13 @@ class TagsController < ApplicationController | |||||
| return unless name_changed | return unless name_changed | ||||
| wiki_page = tag.tag_name.wiki_page | |||||
| wiki_page ||= tag.tag_name.wiki_page | |||||
| return unless wiki_page&.wiki_versions&.exists? | return unless wiki_page&.wiki_versions&.exists? | ||||
| WikiVersionRecorder.record!(page: wiki_page, event_type: :update, created_by_user:) | |||||
| WikiVersionRecorder.record!( | |||||
| page: wiki_page, | |||||
| event_type: :update, | |||||
| created_by_user:) | |||||
| end | end | ||||
| def update_aliases! tag, alias_names | def update_aliases! tag, alias_names | ||||
| @@ -0,0 +1,160 @@ | |||||
| require 'rails_helper' | |||||
| RSpec.describe 'Tag and wiki history integrity', type: :request do | |||||
| let(:member_user) { create(:user, role: 'member') } | |||||
| def stub_current_user user | |||||
| allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user) | |||||
| end | |||||
| def create_tag! name:, category: :general | |||||
| tag_name = TagName.create!(name:) | |||||
| Tag.create!(tag_name:, category:) | |||||
| end | |||||
| def create_wiki_for_tag! tag:, body: 'wiki body', user: member_user | |||||
| Wiki::Commit.create_content!( | |||||
| tag_name: tag.tag_name, | |||||
| body:, | |||||
| created_by_user: user, | |||||
| message: 'init') | |||||
| end | |||||
| before do | |||||
| stub_current_user(member_user) | |||||
| end | |||||
| describe 'PATCH /tags/:id' do | |||||
| it 'records wiki_version when tag name changes and tag has wiki' do | |||||
| tag = create_tag!(name: 'patch_tag_wiki_before') | |||||
| wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before') | |||||
| expect { | |||||
| patch "/tags/#{ tag.id }", params: { | |||||
| name: 'patch_tag_wiki_after', | |||||
| } | |||||
| } | |||||
| .to change(TagVersion, :count).by(2) | |||||
| .and change(WikiVersion, :count).by(1) | |||||
| expect(response).to have_http_status(:ok) | |||||
| tag.reload | |||||
| wiki_page.reload | |||||
| version = wiki_page.wiki_versions.order(:version_no).last | |||||
| expect(tag.name).to eq('patch_tag_wiki_after') | |||||
| expect(wiki_page.title).to eq('patch_tag_wiki_after') | |||||
| expect(version).to have_attributes( | |||||
| event_type: 'update', | |||||
| title: 'patch_tag_wiki_after', | |||||
| body: 'wiki body before', | |||||
| created_by_user_id: member_user.id | |||||
| ) | |||||
| end | |||||
| it 'does not record wiki_version when only category changes' do | |||||
| tag = create_tag!(name: 'patch_tag_category_only') | |||||
| wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before') | |||||
| before_wiki_versions = wiki_page.wiki_versions.count | |||||
| expect { | |||||
| patch "/tags/#{ tag.id }", params: { | |||||
| category: 'meme', | |||||
| } | |||||
| } | |||||
| .to change(TagVersion, :count).by(2) | |||||
| expect(response).to have_http_status(:ok) | |||||
| tag.reload | |||||
| wiki_page.reload | |||||
| expect(tag.name).to eq('patch_tag_category_only') | |||||
| expect(tag.category).to eq('meme') | |||||
| expect(wiki_page.wiki_versions.count).to eq(before_wiki_versions) | |||||
| end | |||||
| end | |||||
| describe 'PUT /tags/:id' do | |||||
| it 'records wiki_version when tag name changes and tag has wiki' do | |||||
| tag = create_tag!(name: 'put_tag_wiki_before') | |||||
| wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before') | |||||
| expect { | |||||
| put "/tags/#{ tag.id }", params: { | |||||
| name: 'put_tag_wiki_after', | |||||
| category: 'general', | |||||
| aliases: '', | |||||
| parent_tags: '', | |||||
| } | |||||
| } | |||||
| .to change(TagVersion, :count).by(2) | |||||
| .and change(WikiVersion, :count).by(1) | |||||
| expect(response).to have_http_status(:ok) | |||||
| tag.reload | |||||
| wiki_page.reload | |||||
| version = wiki_page.wiki_versions.order(:version_no).last | |||||
| expect(tag.name).to eq('put_tag_wiki_after') | |||||
| expect(wiki_page.title).to eq('put_tag_wiki_after') | |||||
| expect(version).to have_attributes( | |||||
| event_type: 'update', | |||||
| title: 'put_tag_wiki_after', | |||||
| body: 'wiki body before', | |||||
| created_by_user_id: member_user.id | |||||
| ) | |||||
| end | |||||
| it 'does not record wiki_version when only category changes' do | |||||
| tag = create_tag!(name: 'put_tag_category_only') | |||||
| wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before') | |||||
| before_wiki_versions = wiki_page.wiki_versions.count | |||||
| expect { | |||||
| put "/tags/#{ tag.id }", params: { | |||||
| name: 'put_tag_category_only', | |||||
| category: 'meme', | |||||
| aliases: '', | |||||
| parent_tags: '', | |||||
| } | |||||
| } | |||||
| .to change(TagVersion, :count).by(2) | |||||
| expect(response).to have_http_status(:ok) | |||||
| tag.reload | |||||
| wiki_page.reload | |||||
| expect(tag.name).to eq('put_tag_category_only') | |||||
| expect(tag.category).to eq('meme') | |||||
| expect(wiki_page.wiki_versions.count).to eq(before_wiki_versions) | |||||
| end | |||||
| it 'does not record wiki_version when only aliases change' do | |||||
| tag = create_tag!(name: 'put_tag_alias_only') | |||||
| wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before') | |||||
| before_wiki_versions = wiki_page.wiki_versions.count | |||||
| expect { | |||||
| put "/tags/#{ tag.id }", params: { | |||||
| name: 'put_tag_alias_only', | |||||
| category: 'general', | |||||
| aliases: 'put_tag_alias_only_alias', | |||||
| parent_tags: '', | |||||
| } | |||||
| } | |||||
| .to change(TagVersion, :count).by(2) | |||||
| expect(response).to have_http_status(:ok) | |||||
| expect(wiki_page.reload.wiki_versions.count).to eq(before_wiki_versions) | |||||
| end | |||||
| end | |||||
| end | |||||
| @@ -0,0 +1,27 @@ | |||||
| require 'rails_helper' | |||||
| RSpec.describe 'Wiki body search', type: :request do | |||||
| let!(:user) { create_member_user! } | |||||
| it 'searches wiki pages by body text' do | |||||
| pending 'Wiki 本文検索実装時に有効化する' | |||||
| Wiki::Commit.create_content!( | |||||
| tag_name: TagName.create!(name: 'wiki_body_search_hit'), | |||||
| body: 'unique body keyword for wiki search', | |||||
| created_by_user: user, | |||||
| message: 'init') | |||||
| Wiki::Commit.create_content!( | |||||
| tag_name: TagName.create!(name: 'wiki_body_search_miss'), | |||||
| body: 'ordinary body', | |||||
| created_by_user: user, | |||||
| message: 'init') | |||||
| get '/wiki/search', params: { body: 'unique body keyword' } | |||||
| expect(response).to have_http_status(:ok) | |||||
| expect(json.map { |page| page['title'] }).to include('wiki_body_search_hit') | |||||
| expect(json.map { |page| page['title'] }).not_to include('wiki_body_search_miss') | |||||
| end | |||||
| end | |||||
| @@ -0,0 +1,42 @@ | |||||
| require 'rails_helper' | |||||
| RSpec.describe 'Wiki conflict handling', type: :request do | |||||
| let!(:user) { create_member_user! } | |||||
| def auth_headers user | |||||
| { 'X-Transfer-Code' => user.inheritance_code } | |||||
| end | |||||
| it 'returns 409 when base_revision_id is stale' do | |||||
| page = | |||||
| Wiki::Commit.create_content!( | |||||
| tag_name: TagName.create!(name: 'wiki_conflict_request'), | |||||
| body: 'first', | |||||
| created_by_user: user, | |||||
| message: 'init') | |||||
| stale_id = page.current_revision.id | |||||
| Wiki::Commit.content!( | |||||
| page:, | |||||
| body: 'second', | |||||
| created_user: user, | |||||
| message: 'other edit', | |||||
| base_revision_id: stale_id) | |||||
| put "/wiki/#{ page.id }", | |||||
| params: { | |||||
| title: 'wiki_conflict_request', | |||||
| body: 'third', | |||||
| message: 'stale', | |||||
| base_revision_id: stale_id, | |||||
| }, | |||||
| headers: auth_headers(user) | |||||
| expect(response).to have_http_status(:conflict) | |||||
| page.reload | |||||
| expect(page.body).to eq('second') | |||||
| expect(page.current_revision.message).to eq('other edit') | |||||
| end | |||||
| end | |||||
| @@ -0,0 +1,196 @@ | |||||
| require 'cgi' | |||||
| require 'rails_helper' | |||||
| RSpec.describe 'Wiki history integrity', type: :request do | |||||
| let!(:user) { create_member_user! } | |||||
| def auth_headers user | |||||
| { 'X-Transfer-Code' => user.inheritance_code } | |||||
| end | |||||
| def create_wiki_page title:, body: 'body', message: 'init', user: self.user | |||||
| Wiki::Commit.create_content!( | |||||
| tag_name: TagName.create!(name: title), | |||||
| body:, | |||||
| created_by_user: user, | |||||
| message:) | |||||
| end | |||||
| describe 'POST /wiki' do | |||||
| it 'creates wiki_page, wiki_revision, and wiki_version atomically' do | |||||
| expect { | |||||
| post '/wiki', | |||||
| params: { | |||||
| title: 'wiki_history_create_atomic', | |||||
| body: "a\nb\nc", | |||||
| message: 'initial commit', | |||||
| }, | |||||
| headers: auth_headers(user) | |||||
| } | |||||
| .to change(WikiPage, :count).by(1) | |||||
| .and change(WikiRevision, :count).by(1) | |||||
| .and change(WikiVersion, :count).by(1) | |||||
| expect(response).to have_http_status(:created) | |||||
| page = WikiPage.find(json.fetch('id')) | |||||
| revision = page.current_revision | |||||
| version = page.wiki_versions.order(:version_no).last | |||||
| expect(page.title).to eq('wiki_history_create_atomic') | |||||
| expect(page.body).to eq("a\nb\nc") | |||||
| expect(revision).to be_content | |||||
| expect(revision.message).to eq('initial commit') | |||||
| expect(revision.lines_count).to eq(3) | |||||
| expect(version).to have_attributes( | |||||
| version_no: 1, | |||||
| event_type: 'create', | |||||
| title: 'wiki_history_create_atomic', | |||||
| body: "a\nb\nc", | |||||
| reason: 'initial commit', | |||||
| created_by_user_id: user.id | |||||
| ) | |||||
| end | |||||
| it 'returns 422 and creates nothing when normalised body is blank' do | |||||
| expect { | |||||
| post '/wiki', | |||||
| params: { | |||||
| title: 'wiki_history_blank_body', | |||||
| body: "\r\n\r\n", | |||||
| message: 'blank', | |||||
| }, | |||||
| headers: auth_headers(user) | |||||
| } | |||||
| .not_to change(WikiPage, :count) | |||||
| expect(response).to have_http_status(:unprocessable_entity) | |||||
| expect(WikiPage.joins(:tag_name).where(tag_names: { name: 'wiki_history_blank_body' })).not_to exist | |||||
| end | |||||
| it 'returns 422 and creates no partial page when title already exists' do | |||||
| create_wiki_page(title: 'wiki_history_duplicate_title', body: 'first') | |||||
| expect { | |||||
| post '/wiki', | |||||
| params: { | |||||
| title: 'wiki_history_duplicate_title', | |||||
| body: 'second', | |||||
| message: 'duplicate', | |||||
| }, | |||||
| headers: auth_headers(user) | |||||
| } | |||||
| .not_to change(WikiPage, :count) | |||||
| expect(response).to have_http_status(:unprocessable_entity) | |||||
| expect(WikiPage.joins(:tag_name).where(tag_names: { name: 'wiki_history_duplicate_title' }).count).to eq(1) | |||||
| end | |||||
| end | |||||
| describe 'PUT /wiki/:id' do | |||||
| it 'updates body and records wiki_revision and wiki_version' do | |||||
| page = create_wiki_page(title: 'wiki_history_update_body', body: 'before') | |||||
| current_id = page.current_revision.id | |||||
| expect { | |||||
| put "/wiki/#{ page.id }", | |||||
| params: { | |||||
| title: 'wiki_history_update_body', | |||||
| body: 'after', | |||||
| message: 'edit body', | |||||
| base_revision_id: current_id, | |||||
| }, | |||||
| headers: auth_headers(user) | |||||
| } | |||||
| .to change(WikiRevision, :count).by(1) | |||||
| .and change(WikiVersion, :count).by(1) | |||||
| expect(response).to have_http_status(:ok) | |||||
| page.reload | |||||
| version = page.wiki_versions.order(:version_no).last | |||||
| expect(page.title).to eq('wiki_history_update_body') | |||||
| expect(page.body).to eq('after') | |||||
| expect(version).to have_attributes( | |||||
| event_type: 'update', | |||||
| title: 'wiki_history_update_body', | |||||
| body: 'after', | |||||
| reason: 'edit body', | |||||
| created_by_user_id: user.id | |||||
| ) | |||||
| end | |||||
| it 'renames title and records wiki_version with new title' do | |||||
| page = create_wiki_page(title: 'wiki_history_rename_before', body: 'before') | |||||
| current_id = page.current_revision.id | |||||
| expect { | |||||
| put "/wiki/#{ page.id }", | |||||
| params: { | |||||
| title: 'wiki_history_rename_after', | |||||
| body: 'after', | |||||
| message: 'rename', | |||||
| base_revision_id: current_id, | |||||
| }, | |||||
| headers: auth_headers(user) | |||||
| } | |||||
| .to change(WikiRevision, :count).by(1) | |||||
| .and change(WikiVersion, :count).by(1) | |||||
| expect(response).to have_http_status(:ok) | |||||
| page.reload | |||||
| version = page.wiki_versions.order(:version_no).last | |||||
| expect(page.title).to eq('wiki_history_rename_after') | |||||
| expect(page.body).to eq('after') | |||||
| expect(version).to have_attributes( | |||||
| event_type: 'update', | |||||
| title: 'wiki_history_rename_after', | |||||
| body: 'after', | |||||
| reason: 'rename', | |||||
| created_by_user_id: user.id | |||||
| ) | |||||
| end | |||||
| it 'does not change title, body, revision, or version on stale base_revision_id' do | |||||
| page = create_wiki_page(title: 'wiki_history_conflict_page', body: 'first') | |||||
| stale_id = page.current_revision.id | |||||
| Wiki::Commit.content!( | |||||
| page:, | |||||
| body: 'second', | |||||
| created_user: user, | |||||
| message: 'other edit', | |||||
| base_revision_id: stale_id) | |||||
| page.reload | |||||
| current_title = page.title | |||||
| current_body = page.body | |||||
| revision_count = page.wiki_revisions.count | |||||
| version_count = page.wiki_versions.count | |||||
| put "/wiki/#{ page.id }", | |||||
| params: { | |||||
| title: 'wiki_history_conflict_renamed', | |||||
| body: 'third', | |||||
| message: 'stale edit', | |||||
| base_revision_id: stale_id, | |||||
| }, | |||||
| headers: auth_headers(user) | |||||
| expect(response).to have_http_status(:conflict) | |||||
| page.reload | |||||
| expect(page.title).to eq(current_title) | |||||
| expect(page.body).to eq(current_body) | |||||
| expect(page.wiki_revisions.count).to eq(revision_count) | |||||
| expect(page.wiki_versions.count).to eq(version_count) | |||||
| end | |||||
| end | |||||
| end | |||||
| @@ -0,0 +1,37 @@ | |||||
| require 'rails_helper' | |||||
| RSpec.describe 'Wiki restore', type: :request do | |||||
| let!(:user) { create_member_user! } | |||||
| def auth_headers user | |||||
| { 'X-Transfer-Code' => user.inheritance_code } | |||||
| end | |||||
| it 'restores wiki page to previous version' do | |||||
| pending 'Wiki 版巻き戻し API 実装時に有効化する' | |||||
| page = | |||||
| Wiki::Commit.create_content!( | |||||
| tag_name: TagName.create!(name: 'wiki_restore_page'), | |||||
| body: 'v1', | |||||
| created_by_user: user, | |||||
| message: 'init') | |||||
| v1 = page.wiki_versions.order(:version_no).last | |||||
| Wiki::Commit.content!( | |||||
| page:, | |||||
| body: 'v2', | |||||
| created_user: user, | |||||
| message: 'edit', | |||||
| base_revision_id: page.current_revision.id) | |||||
| post "/wiki/#{ page.id }/restore", | |||||
| params: { version_no: v1.version_no }, | |||||
| headers: auth_headers(user) | |||||
| expect(response).to have_http_status(:ok) | |||||
| expect(page.reload.body).to eq('v1') | |||||
| expect(page.wiki_versions.order(:version_no).last.event_type).to eq('restore') | |||||
| end | |||||
| end | |||||
| @@ -0,0 +1,62 @@ | |||||
| require 'rails_helper' | |||||
| RSpec.describe 'Wiki title collision', type: :request do | |||||
| let!(:user) { create_member_user! } | |||||
| def auth_headers user | |||||
| { 'X-Transfer-Code' => user.inheritance_code } | |||||
| end | |||||
| def create_wiki_page title:, body: | |||||
| Wiki::Commit.create_content!( | |||||
| tag_name: TagName.create!(name: title), | |||||
| body:, | |||||
| created_by_user: user, | |||||
| message: 'init') | |||||
| end | |||||
| it 'returns 422 when renaming wiki title to existing title' do | |||||
| source = create_wiki_page(title: 'wiki_collision_source', body: 'source body') | |||||
| create_wiki_page(title: 'wiki_collision_target', body: 'target body') | |||||
| source_revision_count = source.wiki_revisions.count | |||||
| source_version_count = source.wiki_versions.count | |||||
| old_title = source.title | |||||
| old_body = source.body | |||||
| put "/wiki/#{ source.id }", | |||||
| params: { | |||||
| title: 'wiki_collision_target', | |||||
| body: 'new body', | |||||
| message: 'rename collision', | |||||
| base_revision_id: source.current_revision.id, | |||||
| }, | |||||
| headers: auth_headers(user) | |||||
| expect(response).to have_http_status(:unprocessable_entity) | |||||
| source.reload | |||||
| expect(source.title).to eq(old_title) | |||||
| expect(source.body).to eq(old_body) | |||||
| expect(source.wiki_revisions.count).to eq(source_revision_count) | |||||
| expect(source.wiki_versions.count).to eq(source_version_count) | |||||
| end | |||||
| it 'returns 422 when creating wiki with existing title' do | |||||
| create_wiki_page(title: 'wiki_collision_create', body: 'already exists') | |||||
| expect { | |||||
| post '/wiki', | |||||
| params: { | |||||
| title: 'wiki_collision_create', | |||||
| body: 'new body', | |||||
| message: 'duplicate create', | |||||
| }, | |||||
| headers: auth_headers(user) | |||||
| } | |||||
| .not_to change(WikiPage, :count) | |||||
| expect(response).to have_http_status(:unprocessable_entity) | |||||
| end | |||||
| end | |||||
| @@ -0,0 +1,173 @@ | |||||
| require 'digest' | |||||
| require 'rails_helper' | |||||
| RSpec.describe Wiki::Commit do | |||||
| let(:user) { create_member_user! } | |||||
| def create_page title:, body: 'initial body' | |||||
| described_class.create_content!( | |||||
| tag_name: TagName.create!(name: title), | |||||
| body:, | |||||
| created_by_user: user, | |||||
| message: 'init') | |||||
| end | |||||
| describe '.create_content!' do | |||||
| it 'creates page, revision, and version with normalised body' do | |||||
| expect { | |||||
| described_class.create_content!( | |||||
| tag_name: TagName.create!(name: 'commit_integrity_create'), | |||||
| body: "a\r\nb\r\n\r\n", | |||||
| created_by_user: user, | |||||
| message: 'init') | |||||
| } | |||||
| .to change(WikiPage, :count).by(1) | |||||
| .and change(WikiRevision, :count).by(1) | |||||
| .and change(WikiVersion, :count).by(1) | |||||
| page = WikiPage.joins(:tag_name).find_by!(tag_names: { name: 'commit_integrity_create' }) | |||||
| revision = page.current_revision | |||||
| version = page.wiki_versions.order(:version_no).last | |||||
| expect(page.body).to eq("a\nb") | |||||
| expect(revision.lines_count).to eq(2) | |||||
| expect(version.body).to eq("a\nb") | |||||
| expect(version.reason).to eq('init') | |||||
| end | |||||
| it 'rejects body that becomes blank after normalisation' do | |||||
| tag_name = TagName.create!(name: 'commit_integrity_blank') | |||||
| expect { | |||||
| described_class.create_content!( | |||||
| tag_name:, | |||||
| body: "\r\n\r\n", | |||||
| created_by_user: user, | |||||
| message: 'blank') | |||||
| } | |||||
| .to raise_error(ActiveRecord::RecordInvalid) | |||||
| expect(WikiPage.where(tag_name:)).not_to exist | |||||
| end | |||||
| end | |||||
| describe '.content!' do | |||||
| it 'updates page body, revision, and version' do | |||||
| page = create_page(title: 'commit_integrity_update', body: 'before') | |||||
| current_id = page.current_revision.id | |||||
| expect { | |||||
| described_class.content!( | |||||
| page:, | |||||
| body: 'after', | |||||
| created_user: user, | |||||
| message: 'edit', | |||||
| base_revision_id: current_id) | |||||
| } | |||||
| .to change(WikiRevision, :count).by(1) | |||||
| .and change(WikiVersion, :count).by(1) | |||||
| page.reload | |||||
| version = page.wiki_versions.order(:version_no).last | |||||
| expect(page.body).to eq('after') | |||||
| expect(version.body).to eq('after') | |||||
| expect(version.reason).to eq('edit') | |||||
| end | |||||
| it 'does not record tag_version on body-only wiki update' do | |||||
| tag_name = TagName.create!(name: 'commit_integrity_linked_tag') | |||||
| tag = Tag.create!(tag_name:, category: :general) | |||||
| page = | |||||
| described_class.create_content!( | |||||
| tag_name:, | |||||
| body: 'before', | |||||
| created_by_user: user, | |||||
| message: 'init') | |||||
| TagVersionRecorder.record!( | |||||
| tag:, | |||||
| event_type: :create, | |||||
| created_by_user: user) | |||||
| before_count = tag.reload.tag_versions.count | |||||
| described_class.content!( | |||||
| page:, | |||||
| body: 'after', | |||||
| created_user: user, | |||||
| message: 'edit', | |||||
| base_revision_id: page.current_revision.id) | |||||
| expect(tag.reload.tag_versions.count).to eq(before_count) | |||||
| end | |||||
| it 'raises conflict and leaves page, revision, and version unchanged' do | |||||
| page = create_page(title: 'commit_integrity_conflict', body: 'first') | |||||
| stale_id = page.current_revision.id | |||||
| described_class.content!( | |||||
| page:, | |||||
| body: 'second', | |||||
| created_user: user, | |||||
| message: 'second', | |||||
| base_revision_id: stale_id) | |||||
| page.reload | |||||
| before_body = page.body | |||||
| before_revision_count = page.wiki_revisions.count | |||||
| before_version_count = page.wiki_versions.count | |||||
| expect { | |||||
| described_class.content!( | |||||
| page:, | |||||
| body: 'third', | |||||
| created_user: user, | |||||
| message: 'stale', | |||||
| base_revision_id: stale_id) | |||||
| } | |||||
| .to raise_error(Wiki::Commit::Conflict) | |||||
| page.reload | |||||
| expect(page.body).to eq(before_body) | |||||
| expect(page.wiki_revisions.count).to eq(before_revision_count) | |||||
| expect(page.wiki_versions.count).to eq(before_version_count) | |||||
| end | |||||
| it 'deduplicates duplicated missing wiki lines' do | |||||
| page = create_page(title: 'commit_integrity_dedup', body: 'before') | |||||
| duplicated = 'commit_integrity_duplicate_line' | |||||
| described_class.content!( | |||||
| page:, | |||||
| body: "#{ duplicated }\n#{ duplicated }", | |||||
| created_user: user, | |||||
| message: 'dedup', | |||||
| base_revision_id: page.current_revision.id) | |||||
| revision = page.reload.current_revision | |||||
| expect(WikiLine.where(body: duplicated).count).to eq(1) | |||||
| expect(revision.wiki_revision_lines.count).to eq(2) | |||||
| expect(revision.wiki_revision_lines.pluck(:wiki_line_id).uniq.size).to eq(1) | |||||
| end | |||||
| end | |||||
| describe '.redirect!' do | |||||
| it 'raises because redirect revisions are deprecated' do | |||||
| page = create_page(title: 'commit_integrity_redirect_source', body: 'source') | |||||
| target = create_page(title: 'commit_integrity_redirect_target', body: 'target') | |||||
| expect { | |||||
| described_class.redirect!( | |||||
| page:, | |||||
| redirect_page: target, | |||||
| created_user: user, | |||||
| message: 'redirect', | |||||
| base_revision_id: page.current_revision.id) | |||||
| } | |||||
| .to raise_error(RuntimeError, '廃止しました.') | |||||
| end | |||||
| end | |||||
| end | |||||
| @@ -0,0 +1,99 @@ | |||||
| require 'rails_helper' | |||||
| RSpec.describe WikiVersionRecorder do | |||||
| let(:user) { create_member_user! } | |||||
| def create_page title:, body: 'body' | |||||
| Wiki::Commit.create_content!( | |||||
| tag_name: TagName.create!(name: title), | |||||
| body:, | |||||
| created_by_user: user, | |||||
| message: 'init') | |||||
| end | |||||
| describe '.record!' do | |||||
| it 'records title, body, reason, user, and version number' do | |||||
| page = create_page(title: 'wiki_version_recorder_basic', body: 'body') | |||||
| expect { | |||||
| described_class.record!( | |||||
| page:, | |||||
| event_type: :update, | |||||
| reason: 'manual reason', | |||||
| created_by_user: user) | |||||
| } | |||||
| .to change { page.reload.wiki_versions.count }.by(1) | |||||
| version = page.wiki_versions.order(:version_no).last | |||||
| expect(version).to have_attributes( | |||||
| version_no: 2, | |||||
| event_type: 'update', | |||||
| title: 'wiki_version_recorder_basic', | |||||
| body: 'body', | |||||
| reason: 'manual reason', | |||||
| created_by_user_id: user.id | |||||
| ) | |||||
| end | |||||
| it 'does not create duplicated update version for identical snapshot' do | |||||
| page = create_page(title: 'wiki_version_recorder_duplicate', body: 'body') | |||||
| described_class.record!( | |||||
| page:, | |||||
| event_type: :update, | |||||
| reason: nil, | |||||
| created_by_user: user) | |||||
| before_count = page.reload.wiki_versions.count | |||||
| described_class.record!( | |||||
| page:, | |||||
| event_type: :update, | |||||
| reason: nil, | |||||
| created_by_user: user) | |||||
| expect(page.reload.wiki_versions.count).to eq(before_count) | |||||
| end | |||||
| it 'creates update version when title changes' do | |||||
| page = create_page(title: 'wiki_version_recorder_title_before', body: 'body') | |||||
| page.tag_name.update!(name: 'wiki_version_recorder_title_after') | |||||
| expect { | |||||
| described_class.record!( | |||||
| page:, | |||||
| event_type: :update, | |||||
| reason: 'rename', | |||||
| created_by_user: user) | |||||
| } | |||||
| .to change { page.reload.wiki_versions.count }.by(1) | |||||
| version = page.wiki_versions.order(:version_no).last | |||||
| expect(version.title).to eq('wiki_version_recorder_title_after') | |||||
| expect(version.body).to eq('body') | |||||
| expect(version.reason).to eq('rename') | |||||
| end | |||||
| it 'creates update version when body changes' do | |||||
| page = create_page(title: 'wiki_version_recorder_body', body: 'before') | |||||
| page.update!(body: 'after') | |||||
| expect { | |||||
| described_class.record!( | |||||
| page:, | |||||
| event_type: :update, | |||||
| reason: 'body', | |||||
| created_by_user: user) | |||||
| } | |||||
| .to change { page.reload.wiki_versions.count }.by(1) | |||||
| version = page.wiki_versions.order(:version_no).last | |||||
| expect(version.title).to eq('wiki_version_recorder_body') | |||||
| expect(version.body).to eq('after') | |||||
| expect(version.reason).to eq('body') | |||||
| end | |||||
| end | |||||
| end | |||||