diff options
Diffstat (limited to 'spec/models')
-rw-r--r-- | spec/models/event_spec.rb | 354 | ||||
-rw-r--r-- | spec/models/wiki_page/meta_spec.rb | 430 | ||||
-rw-r--r-- | spec/models/wiki_page/slug_spec.rb | 94 | ||||
-rw-r--r-- | spec/models/wiki_page_spec.rb | 30 |
4 files changed, 769 insertions, 139 deletions
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 97ea32a120d..b2676a79b55 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -99,6 +99,39 @@ describe Event do end end + describe '#target_title' do + let_it_be(:project) { create(:project) } + + let(:author) { project.owner } + let(:target) { nil } + + let(:event) do + described_class.new(project: project, + target: target, + author_id: author.id) + end + + context 'for an issue' do + let(:title) { generate(:title) } + let(:issue) { create(:issue, title: title, project: project) } + let(:target) { issue } + + it 'delegates to issue title' do + expect(event.target_title).to eq(title) + end + end + + context 'for a wiki page' do + let(:title) { generate(:wiki_page_title) } + let(:wiki_page) { create(:wiki_page, title: title, project: project) } + let(:event) { create(:wiki_page_event, project: project, wiki_page: wiki_page) } + + it 'delegates to wiki page title' do + expect(event.target_title).to eq(wiki_page.title) + end + end + end + describe '#membership_changed?' do context "created" do subject { build(:event, :created).membership_changed? } @@ -148,13 +181,16 @@ describe Event do end describe '#visible_to_user?' do - let(:project) { create(:project, :public) } - let(:non_member) { create(:user) } - let(:member) { create(:user) } - let(:guest) { create(:user) } - let(:author) { create(:author) } - let(:assignee) { create(:user) } - let(:admin) { create(:admin) } + let_it_be(:non_member) { create(:user) } + let_it_be(:member) { create(:user) } + let_it_be(:guest) { create(:user) } + let_it_be(:author) { create(:author) } + let_it_be(:assignee) { create(:user) } + let_it_be(:admin) { create(:admin) } + let_it_be(:public_project) { create(:project, :public) } + let_it_be(:private_project) { create(:project, :private) } + + let(:project) { public_project } let(:issue) { create(:issue, project: project, author: author, assignees: [assignee]) } let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee]) } let(:project_snippet) { create(:project_snippet, :public, project: project, author: author) } @@ -165,36 +201,77 @@ describe Event do let(:note_on_project_snippet) { create(:note_on_project_snippet, author: author, noteable: project_snippet, project: project) } let(:note_on_personal_snippet) { create(:note_on_personal_snippet, author: author, noteable: personal_snippet, project: nil) } let(:milestone_on_project) { create(:milestone, project: project) } - let(:event) { described_class.new(project: project, target: target, author_id: author.id) } + let(:event) do + described_class.new(project: project, + target: target, + author_id: author.id) + end before do project.add_developer(member) project.add_guest(guest) end + def visible_to_all + { + logged_out: true, + non_member: true, + guest: true, + member: true, + admin: true + } + end + + def visible_to_none + visible_to_all.transform_values { |_| false } + end + + def visible_to_none_except(*roles) + visible_to_none.merge(roles.map { |role| [role, true] }.to_h) + end + + def visible_to_all_except(*roles) + visible_to_all.merge(roles.map { |role| [role, false] }.to_h) + end + + shared_examples 'visibility examples' do + it 'has the correct visibility' do + expect({ + logged_out: event.visible_to_user?(nil), + non_member: event.visible_to_user?(non_member), + guest: event.visible_to_user?(guest), + member: event.visible_to_user?(member), + admin: event.visible_to_user?(admin) + }).to match(visibility) + end + end + + shared_examples 'visible to assignee' do |visible| + it { expect(event.visible_to_user?(assignee)).to eq(visible) } + end + + shared_examples 'visible to author' do |visible| + it { expect(event.visible_to_user?(author)).to eq(visible) } + end + + shared_examples 'visible to assignee and author' do |visible| + include_examples 'visible to assignee', visible + include_examples 'visible to author', visible + end + context 'commit note event' do let(:project) { create(:project, :public, :repository) } let(:target) { note_on_commit } - it do - aggregate_failures do - expect(event.visible_to_user?(non_member)).to eq true - expect(event.visible_to_user?(member)).to eq true - expect(event.visible_to_user?(guest)).to eq true - expect(event.visible_to_user?(admin)).to eq true - end + include_examples 'visibility examples' do + let(:visibility) { visible_to_all } end context 'private project' do let(:project) { create(:project, :private, :repository) } - it do - aggregate_failures do - expect(event.visible_to_user?(non_member)).to eq false - expect(event.visible_to_user?(member)).to eq true - expect(event.visible_to_user?(guest)).to eq false - expect(event.visible_to_user?(admin)).to eq true - end + include_examples 'visibility examples' do + let(:visibility) { visible_to_none_except(:member, :admin) } end end end @@ -203,27 +280,19 @@ describe Event do context 'for non confidential issues' do let(:target) { issue } - it do - expect(event.visible_to_user?(non_member)).to eq true - expect(event.visible_to_user?(author)).to eq true - expect(event.visible_to_user?(assignee)).to eq true - expect(event.visible_to_user?(member)).to eq true - expect(event.visible_to_user?(guest)).to eq true - expect(event.visible_to_user?(admin)).to eq true + include_examples 'visibility examples' do + let(:visibility) { visible_to_all } end + include_examples 'visible to assignee and author', true end context 'for confidential issues' do let(:target) { confidential_issue } - it do - expect(event.visible_to_user?(non_member)).to eq false - expect(event.visible_to_user?(author)).to eq true - expect(event.visible_to_user?(assignee)).to eq true - expect(event.visible_to_user?(member)).to eq true - expect(event.visible_to_user?(guest)).to eq false - expect(event.visible_to_user?(admin)).to eq true + include_examples 'visibility examples' do + let(:visibility) { visible_to_none_except(:member, :admin) } end + include_examples 'visible to assignee and author', true end end @@ -231,105 +300,99 @@ describe Event do context 'on non confidential issues' do let(:target) { note_on_issue } - it do - expect(event.visible_to_user?(non_member)).to eq true - expect(event.visible_to_user?(author)).to eq true - expect(event.visible_to_user?(assignee)).to eq true - expect(event.visible_to_user?(member)).to eq true - expect(event.visible_to_user?(guest)).to eq true - expect(event.visible_to_user?(admin)).to eq true + include_examples 'visibility examples' do + let(:visibility) { visible_to_all } end + include_examples 'visible to assignee and author', true end context 'on confidential issues' do let(:target) { note_on_confidential_issue } - it do - expect(event.visible_to_user?(non_member)).to eq false - expect(event.visible_to_user?(author)).to eq true - expect(event.visible_to_user?(assignee)).to eq true - expect(event.visible_to_user?(member)).to eq true - expect(event.visible_to_user?(guest)).to eq false - expect(event.visible_to_user?(admin)).to eq true + include_examples 'visibility examples' do + let(:visibility) { visible_to_none_except(:member, :admin) } end + include_examples 'visible to assignee and author', true end context 'private project' do - let(:project) { create(:project, :private) } + let(:project) { private_project } let(:target) { note_on_issue } - it do - expect(event.visible_to_user?(non_member)).to eq false - expect(event.visible_to_user?(author)).to eq false - expect(event.visible_to_user?(assignee)).to eq false - expect(event.visible_to_user?(member)).to eq true - expect(event.visible_to_user?(guest)).to eq true - expect(event.visible_to_user?(admin)).to eq true + include_examples 'visibility examples' do + let(:visibility) { visible_to_none_except(:guest, :member, :admin) } end + + include_examples 'visible to assignee and author', false end end context 'merge request diff note event' do - let(:project) { create(:project, :public) } let(:merge_request) { create(:merge_request, source_project: project, author: author, assignees: [assignee]) } let(:note_on_merge_request) { create(:legacy_diff_note_on_merge_request, noteable: merge_request, project: project) } let(:target) { note_on_merge_request } - it do - expect(event.visible_to_user?(non_member)).to eq true - expect(event.visible_to_user?(author)).to eq true - expect(event.visible_to_user?(assignee)).to eq true - expect(event.visible_to_user?(member)).to eq true - expect(event.visible_to_user?(guest)).to eq true - expect(event.visible_to_user?(admin)).to eq true + context 'public project' do + let(:project) { public_project } + + include_examples 'visibility examples' do + let(:visibility) { visible_to_all } + end + + include_examples 'visible to assignee', true end context 'private project' do - let(:project) { create(:project, :private) } + let(:project) { private_project } - it do - expect(event.visible_to_user?(non_member)).to eq false - expect(event.visible_to_user?(author)).to eq false - expect(event.visible_to_user?(assignee)).to eq false - expect(event.visible_to_user?(member)).to eq true - expect(event.visible_to_user?(guest)).to eq false - expect(event.visible_to_user?(admin)).to eq true + include_examples 'visibility examples' do + let(:visibility) { visible_to_none_except(:member, :admin) } end + + include_examples 'visible to assignee', false end end context 'milestone event' do let(:target) { milestone_on_project } - it do - expect(event.visible_to_user?(nil)).to be_truthy - expect(event.visible_to_user?(non_member)).to be_truthy - expect(event.visible_to_user?(member)).to be_truthy - expect(event.visible_to_user?(guest)).to be_truthy - expect(event.visible_to_user?(admin)).to be_truthy + include_examples 'visibility examples' do + let(:visibility) { visible_to_all } end context 'on public project with private issue tracker and merge requests' do let(:project) { create(:project, :public, :issues_private, :merge_requests_private) } - it do - expect(event.visible_to_user?(nil)).to be_falsy - expect(event.visible_to_user?(non_member)).to be_falsy - expect(event.visible_to_user?(member)).to be_truthy - expect(event.visible_to_user?(guest)).to be_truthy - expect(event.visible_to_user?(admin)).to be_truthy + include_examples 'visibility examples' do + let(:visibility) { visible_to_all_except(:logged_out, :non_member) } end end context 'on private project' do let(:project) { create(:project, :private) } - it do - expect(event.visible_to_user?(nil)).to be_falsy - expect(event.visible_to_user?(non_member)).to be_falsy - expect(event.visible_to_user?(member)).to be_truthy - expect(event.visible_to_user?(guest)).to be_truthy - expect(event.visible_to_user?(admin)).to be_truthy + include_examples 'visibility examples' do + let(:visibility) { visible_to_all_except(:logged_out, :non_member) } + end + end + end + + context 'wiki-page event', :aggregate_failures do + let(:event) { create(:wiki_page_event, project: project) } + + context 'on private project', :aggregate_failures do + let(:project) { create(:project, :wiki_repo) } + + include_examples 'visibility examples' do + let(:visibility) { visible_to_all_except(:logged_out, :non_member) } + end + end + + context 'wiki-page event on public project', :aggregate_failures do + let(:project) { create(:project, :public, :wiki_repo) } + + include_examples 'visibility examples' do + let(:visibility) { visible_to_all } end end end @@ -337,79 +400,98 @@ describe Event do context 'project snippet note event' do let(:target) { note_on_project_snippet } - it do - expect(event.visible_to_user?(nil)).to be_truthy - expect(event.visible_to_user?(non_member)).to be_truthy - expect(event.visible_to_user?(author)).to be_truthy - expect(event.visible_to_user?(member)).to be_truthy - expect(event.visible_to_user?(guest)).to be_truthy - expect(event.visible_to_user?(admin)).to be_truthy + include_examples 'visibility examples' do + let(:visibility) { visible_to_all } end context 'on public project with private snippets' do let(:project) { create(:project, :public, :snippets_private) } - it do - expect(event.visible_to_user?(nil)).to be_falsy - expect(event.visible_to_user?(non_member)).to be_falsy - - # Normally, we'd expect the author of a comment to be able to view it. - # However, this doesn't seem to be the case for comments on snippets. - expect(event.visible_to_user?(author)).to be_falsy - - expect(event.visible_to_user?(member)).to be_truthy - expect(event.visible_to_user?(guest)).to be_truthy - expect(event.visible_to_user?(admin)).to be_truthy + include_examples 'visibility examples' do + let(:visibility) { visible_to_none_except(:guest, :member, :admin) } end + # Normally, we'd expect the author of a comment to be able to view it. + # However, this doesn't seem to be the case for comments on snippets. + include_examples 'visible to author', false end context 'on private project' do let(:project) { create(:project, :private) } - it do - expect(event.visible_to_user?(nil)).to be_falsy - expect(event.visible_to_user?(non_member)).to be_falsy - - # Normally, we'd expect the author of a comment to be able to view it. - # However, this doesn't seem to be the case for comments on snippets. - expect(event.visible_to_user?(author)).to be_falsy - - expect(event.visible_to_user?(member)).to be_truthy - expect(event.visible_to_user?(guest)).to be_truthy - expect(event.visible_to_user?(admin)).to be_truthy + include_examples 'visibility examples' do + let(:visibility) { visible_to_none_except(:guest, :member, :admin) } end + # Normally, we'd expect the author of a comment to be able to view it. + # However, this doesn't seem to be the case for comments on snippets. + include_examples 'visible to author', false end end context 'personal snippet note event' do let(:target) { note_on_personal_snippet } - it do - expect(event.visible_to_user?(nil)).to be_truthy - expect(event.visible_to_user?(non_member)).to be_truthy - expect(event.visible_to_user?(author)).to be_truthy - expect(event.visible_to_user?(admin)).to be_truthy + include_examples 'visibility examples' do + let(:visibility) { visible_to_all } end + include_examples 'visible to author', true context 'on internal snippet' do let(:personal_snippet) { create(:personal_snippet, :internal, author: author) } - it do - expect(event.visible_to_user?(nil)).to be_falsy - expect(event.visible_to_user?(non_member)).to be_truthy - expect(event.visible_to_user?(author)).to be_truthy - expect(event.visible_to_user?(admin)).to be_truthy + include_examples 'visibility examples' do + let(:visibility) { visible_to_all_except(:logged_out) } end end context 'on private snippet' do let(:personal_snippet) { create(:personal_snippet, :private, author: author) } - it do - expect(event.visible_to_user?(nil)).to be_falsy - expect(event.visible_to_user?(non_member)).to be_falsy - expect(event.visible_to_user?(author)).to be_truthy - expect(event.visible_to_user?(admin)).to be_truthy + include_examples 'visibility examples' do + let(:visibility) { visible_to_none_except(:admin) } + end + include_examples 'visible to author', true + end + end + end + + describe '.for_wiki_page' do + let_it_be(:events) do + [ + create(:closed_issue_event), + create(:wiki_page_event), + create(:closed_issue_event), + create(:event, :created), + create(:wiki_page_event) + ] + end + + it 'only contains the wiki page events' do + wiki_events = events.select(&:wiki_page?) + + expect(described_class.for_wiki_page).to match_array(wiki_events) + end + end + + describe '#wiki_page and #wiki_page?' do + let_it_be(:project) { create(:project, :repository) } + + context 'for a wiki page event' do + let(:wiki_page) do + create(:wiki_page, :with_real_page, project: project) + end + + subject(:event) { create(:wiki_page_event, project: project, wiki_page: wiki_page) } + + it { is_expected.to have_attributes(wiki_page?: be_truthy, wiki_page: wiki_page) } + end + + [:issue, :user, :merge_request, :snippet, :milestone, nil].each do |kind| + context "for a #{kind} event" do + it 'is nil' do + target = create(kind) if kind + event = create(:event, project: project, target: target) + + expect(event).to have_attributes(wiki_page: be_nil, wiki_page?: be_falsy) end end end diff --git a/spec/models/wiki_page/meta_spec.rb b/spec/models/wiki_page/meta_spec.rb new file mode 100644 index 00000000000..f9bfc31ba64 --- /dev/null +++ b/spec/models/wiki_page/meta_spec.rb @@ -0,0 +1,430 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe WikiPage::Meta do + let_it_be(:project) { create(:project) } + let_it_be(:other_project) { create(:project) } + + describe 'Associations' do + it { is_expected.to belong_to(:project) } + it { is_expected.to have_many(:slugs) } + it { is_expected.to have_many(:events) } + + it 'can find slugs' do + meta = create(:wiki_page_meta) + slugs = create_list(:wiki_page_slug, 3, wiki_page_meta: meta) + + expect(meta.slugs).to match_array(slugs) + end + end + + describe 'Validations' do + subject do + described_class.new(title: 'some title', project: project) + end + + it { is_expected.to validate_presence_of(:project_id) } + it { is_expected.to validate_presence_of(:title) } + + it 'is forbidden to add extremely long titles' do + expect do + create(:wiki_page_meta, project: project, title: FFaker::Lorem.characters(300)) + end.to raise_error(ActiveRecord::ValueTooLong) + end + + it 'is forbidden to have two records for the same project with the same canonical_slug' do + the_slug = generate(:sluggified_title) + create(:wiki_page_meta, canonical_slug: the_slug, project: project) + + in_violation = build(:wiki_page_meta, canonical_slug: the_slug, project: project) + + expect(in_violation).not_to be_valid + end + end + + describe '#canonical_slug' do + subject { described_class.find(meta.id) } + + let_it_be(:meta) do + described_class.create(title: generate(:wiki_page_title), project: project) + end + + context 'there are no slugs' do + it { is_expected.to have_attributes(canonical_slug: be_nil) } + end + + it 'can be set on initialization' do + meta = create(:wiki_page_meta, canonical_slug: 'foo') + + expect(meta.canonical_slug).to eq('foo') + end + + context 'we have some non-canonical slugs' do + before do + create_list(:wiki_page_slug, 2, wiki_page_meta: subject) + end + + it { is_expected.to have_attributes(canonical_slug: be_nil) } + + it 'issues at most one query' do + expect { subject.canonical_slug }.not_to exceed_query_limit(1) + end + + it 'issues no queries if we already know the slug' do + subject.canonical_slug + + expect { subject.canonical_slug }.not_to exceed_query_limit(0) + end + end + + context 'we have a canonical slug' do + before do + create_list(:wiki_page_slug, 2, wiki_page_meta: subject) + end + + it 'has the correct value' do + slug = create(:wiki_page_slug, :canonical, wiki_page_meta: subject) + + is_expected.to have_attributes(canonical_slug: slug.slug) + end + end + + describe 'canonical_slug=' do + shared_examples 'canonical_slug setting examples' do + # Constant overhead of two queries for the transaction + let(:upper_query_limit) { query_limit + 2 } + let(:lower_query_limit) { [upper_query_limit - 1, 0].max} + let(:other_slug) { generate(:sluggified_title) } + + it 'changes it to the correct value' do + subject.canonical_slug = slug + + expect(subject).to have_attributes(canonical_slug: slug) + end + + it 'ensures the slug is in the db' do + subject.canonical_slug = slug + + expect(subject.slugs.canonical.where(slug: slug)).to exist + end + + it 'issues at most N queries' do + expect { subject.canonical_slug = slug }.not_to exceed_query_limit(upper_query_limit) + end + + it 'issues fewer queries if we already know the current slug' do + subject.canonical_slug = other_slug + + expect { subject.canonical_slug = slug }.not_to exceed_query_limit(lower_query_limit) + end + end + + context 'the slug is not known to us' do + let(:slug) { generate(:sluggified_title) } + let(:query_limit) { 8 } + + include_examples 'canonical_slug setting examples' + end + + context 'the slug is already in the DB (but not canonical)' do + let_it_be(:slug_record) { create(:wiki_page_slug, wiki_page_meta: meta) } + let(:slug) { slug_record.slug } + let(:query_limit) { 4 } + + include_examples 'canonical_slug setting examples' + end + + context 'the slug is already in the DB (and canonical)' do + let_it_be(:slug_record) { create(:wiki_page_slug, :canonical, wiki_page_meta: meta) } + let(:slug) { slug_record.slug } + let(:query_limit) { 4 } + + include_examples 'canonical_slug setting examples' + end + + context 'the slug is up to date and in the DB' do + let(:slug) { generate(:sluggified_title) } + + before do + subject.canonical_slug = slug + end + + include_examples 'canonical_slug setting examples' do + let(:other_slug) { slug } + let(:upper_query_limit) { 0 } + end + end + end + end + + describe '.find_or_create' do + let(:old_title) { generate(:wiki_page_title) } + let(:last_known_slug) { generate(:sluggified_title) } + let(:current_slug) { wiki_page.slug } + let(:title) { wiki_page.title } + let(:wiki_page) { create(:wiki_page, project: project) } + + def find_record + described_class.find_or_create(last_known_slug, wiki_page) + end + + def create_previous_version(title = old_title, slug = last_known_slug) + create(:wiki_page_meta, title: title, project: project, canonical_slug: slug) + end + + def create_context + # Ensure that we behave nicely with respect to other projects + # We have: + # - page in other project with same canonical_slug + create(:wiki_page_meta, project: other_project, canonical_slug: wiki_page.slug) + + # - page in same project with different canonical_slug, but with + # an old slug that = canonical_slug + different_slug = generate(:sluggified_title) + create(:wiki_page_meta, project: project, canonical_slug: different_slug) + .slugs.create(slug: wiki_page.slug) + end + + shared_examples 'metadata examples' do + it 'establishes the correct state', :aggregate_failures do + create_context + + meta = find_record + + expect(meta).to have_attributes( + valid?: true, + canonical_slug: wiki_page.slug, + title: wiki_page.title, + project: wiki_page.wiki.project + ) + expect(meta.slugs.where(slug: last_known_slug)).to exist + expect(meta.slugs.canonical.where(slug: wiki_page.slug)).to exist + end + + it 'makes a reasonable number of DB queries' do + expect(project).to eq(wiki_page.wiki.project) + + expect { find_record }.not_to exceed_query_limit(query_limit) + end + end + + context 'the slug is too long' do + let(:last_known_slug) { FFaker::Lorem.characters(2050) } + + it 'raises an error' do + expect { find_record }.to raise_error ActiveRecord::ValueTooLong + end + end + + context 'a conflicting record exists' do + before do + create(:wiki_page_meta, project: project, canonical_slug: last_known_slug) + create(:wiki_page_meta, project: project, canonical_slug: current_slug) + end + + it 'raises an error' do + expect { find_record }.to raise_error(ActiveRecord::RecordInvalid) + end + end + + context 'no existing record exists' do + include_examples 'metadata examples' do + # The base case is 5 queries: + # - 2 for the outer transaction + # - 1 to find the metadata object if it exists + # - 1 to create it if it does not + # - 1 to insert last_known_slug and current_slug + # + # (Log has been edited for clarity) + # SAVEPOINT active_record_2 + # + # SELECT * FROM wiki_page_meta + # INNER JOIN wiki_page_slugs + # ON wiki_page_slugs.wiki_page_meta_id = wiki_page_meta.id + # WHERE wiki_page_meta.project_id = ? + # AND wiki_page_slugs.canonical = TRUE + # AND wiki_page_slugs.slug IN (?,?) + # LIMIT 2 + # + # INSERT INTO wiki_page_meta (project_id, title) VALUES (?, ?) RETURNING id + # + # INSERT INTO wiki_page_slugs (wiki_page_meta_id,slug,canonical) + # VALUES (?, ?, ?) (?, ?, ?) + # ON CONFLICT DO NOTHING RETURNING id + # + # RELEASE SAVEPOINT active_record_2 + let(:query_limit) { 5 } + end + end + + context 'the last_known_slug is the same as the current slug, as on creation' do + let(:last_known_slug) { current_slug } + + include_examples 'metadata examples' do + # Identical to the base case. + let(:query_limit) { 5 } + end + end + + context 'a record exists in the DB in the correct state' do + let(:last_known_slug) { current_slug } + let(:old_title) { title } + + before do + create_previous_version + end + + include_examples 'metadata examples' do + # We just need to do the initial query, and the outer transaction + # SAVEPOINT active_record_2 + # + # SELECT * FROM wiki_page_meta + # INNER JOIN wiki_page_slugs + # ON wiki_page_slugs.wiki_page_meta_id = wiki_page_meta.id + # WHERE wiki_page_meta.project_id = ? + # AND wiki_page_slugs.canonical = TRUE + # AND wiki_page_slugs.slug = ? + # LIMIT 2 + # + # RELEASE SAVEPOINT active_record_2 + let(:query_limit) { 3 } + end + end + + context 'we need to update the slug, but not the title' do + let(:old_title) { title } + + before do + create_previous_version + end + + include_examples 'metadata examples' do + # Here we need: + # - 2 for the outer transaction + # - 1 to find the record + # - 1 to insert the new slug + # - 3 to set canonical state correctly + # + # SAVEPOINT active_record_2 + # + # SELECT * FROM wiki_page_meta + # INNER JOIN wiki_page_slugs + # ON wiki_page_slugs.wiki_page_meta_id = wiki_page_meta.id + # WHERE wiki_page_meta.project_id = ? + # AND wiki_page_slugs.canonical = TRUE + # AND wiki_page_slugs.slug = ? + # LIMIT 1 + # + # INSERT INTO wiki_page_slugs (wiki_page_meta_id,slug,canonical) + # VALUES (?, ?, ?) ON CONFLICT DO NOTHING RETURNING id + # + # SELECT * FROM wiki_page_slugs + # WHERE wiki_page_slugs.wiki_page_meta_id = ? + # AND wiki_page_slugs.slug = ? + # LIMIT 1 + # UPDATE wiki_page_slugs SET canonical = FALSE WHERE wiki_page_meta_id = ? + # UPDATE wiki_page_slugs SET canonical = TRUE WHERE id = ? + # + # RELEASE SAVEPOINT active_record_2 + let(:query_limit) { 7 } + end + end + + context 'we need to update the title, but not the slug' do + let(:last_known_slug) { wiki_page.slug } + + before do + create_previous_version + end + + include_examples 'metadata examples' do + # Same as minimal case, plus one query to update the title. + # + # SAVEPOINT active_record_2 + # + # SELECT * FROM wiki_page_meta + # INNER JOIN wiki_page_slugs + # ON wiki_page_slugs.wiki_page_meta_id = wiki_page_meta.id + # WHERE wiki_page_meta.project_id = ? + # AND wiki_page_slugs.canonical = TRUE + # AND wiki_page_slugs.slug = ? + # LIMIT 1 + # + # UPDATE wiki_page_meta SET title = ? WHERE id = ? + # + # RELEASE SAVEPOINT active_record_2 + let(:query_limit) { 4 } + end + end + + context 'we want to change the slug back to a previous version' do + let(:slug_1) { 'foo' } + let(:slug_2) { 'bar' } + + let(:wiki_page) { create(:wiki_page, title: slug_1, project: project) } + let(:last_known_slug) { slug_2 } + + before do + meta = create_previous_version(title, slug_1) + meta.canonical_slug = slug_2 + end + + include_examples 'metadata examples' do + let(:query_limit) { 7 } + end + end + + context 'we want to change the slug a bunch of times' do + let(:slugs) { generate_list(:sluggified_title, 3) } + + before do + meta = create_previous_version + slugs.each { |slug| meta.canonical_slug = slug } + end + + include_examples 'metadata examples' do + let(:query_limit) { 7 } + end + end + + context 'we need to update the title and the slug' do + before do + create_previous_version + end + + include_examples 'metadata examples' do + # -- outer transaction + # SAVEPOINT active_record_2 + # + # -- to find the record + # SELECT * FROM wiki_page_meta + # INNER JOIN wiki_page_slugs + # ON wiki_page_slugs.wiki_page_meta_id = wiki_page_meta.id + # WHERE wiki_page_meta.project_id = ? + # AND wiki_page_slugs.canonical = TRUE + # AND wiki_page_slugs.slug IN (?,?) + # LIMIT 2 + # + # -- to update the title + # UPDATE wiki_page_meta SET title = ? WHERE id = ? + # + # -- to update slug + # INSERT INTO wiki_page_slugs (wiki_page_meta_id,slug,canonical) + # VALUES (?, ?, ?) ON CONFLICT DO NOTHING RETURNING id + # + # UPDATE wiki_page_slugs SET canonical = FALSE WHERE wiki_page_meta_id = ? + # + # SELECT * FROM wiki_page_slugs + # WHERE wiki_page_slugs.wiki_page_meta_id = ? + # AND wiki_page_slugs.slug = ? + # LIMIT 1 + # + # UPDATE wiki_page_slugs SET canonical = TRUE WHERE id = ? + # + # RELEASE SAVEPOINT active_record_2 + let(:query_limit) { 8 } + end + end + end +end diff --git a/spec/models/wiki_page/slug_spec.rb b/spec/models/wiki_page/slug_spec.rb new file mode 100644 index 00000000000..324dea6b320 --- /dev/null +++ b/spec/models/wiki_page/slug_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe WikiPage::Slug do + let_it_be(:meta) { create(:wiki_page_meta) } + + describe 'Associations' do + it { is_expected.to belong_to(:wiki_page_meta) } + + it 'refers correctly to the wiki_page_meta' do + created = create(:wiki_page_slug, wiki_page_meta: meta) + + expect(created.reload.wiki_page_meta).to eq(meta) + end + end + + describe 'scopes' do + describe 'canonical' do + subject { described_class.canonical } + + context 'there are no slugs' do + it { is_expected.to be_empty } + end + + context 'there are some non-canonical slugs' do + before do + create(:wiki_page_slug) + end + + it { is_expected.to be_empty } + end + + context 'there is at least one canonical slugs' do + before do + create(:wiki_page_slug, :canonical) + end + + it { is_expected.not_to be_empty } + end + end + end + + describe 'Validations' do + let(:canonical) { false } + + subject do + build(:wiki_page_slug, canonical: canonical, wiki_page_meta: meta) + end + + it { is_expected.to validate_presence_of(:slug) } + it { is_expected.to validate_uniqueness_of(:slug).scoped_to(:wiki_page_meta_id) } + + describe 'only_one_slug_can_be_canonical_per_meta_record' do + context 'there are no other slugs' do + it { is_expected.to be_valid } + + context 'the current slug is canonical' do + let(:canonical) { true } + + it { is_expected.to be_valid } + end + end + + context 'there are other slugs, but they are not canonical' do + before do + create(:wiki_page_slug, wiki_page_meta: meta) + end + + it { is_expected.to be_valid } + + context 'the current slug is canonical' do + let(:canonical) { true } + + it { is_expected.to be_valid } + end + end + + context 'there is already a canonical slug' do + before do + create(:wiki_page_slug, canonical: true, wiki_page_meta: meta) + end + + it { is_expected.to be_valid } + + context 'the current slug is canonical' do + let(:canonical) { true } + + it { is_expected.not_to be_valid } + end + end + end + end +end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 42a7d567613..e8e80a8c7f4 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -606,12 +606,36 @@ describe WikiPage do expect(subject).to eq(subject) end - it 'returns false for updated wiki page' do + it 'returns true for updated wiki page' do subject.update(content: "Updated content") - updated_page = wiki.find_page('test page') + updated_page = wiki.find_page(existing_page.slug) expect(updated_page).not_to be_nil - expect(updated_page).not_to eq(subject) + expect(updated_page).to eq(subject) + end + + it 'returns false for a completely different wiki page' do + other_page = create(:wiki_page) + + expect(subject.slug).not_to eq(other_page.slug) + expect(subject.project).not_to eq(other_page.project) + expect(subject).not_to eq(other_page) + end + + it 'returns false for page with different slug on same project' do + other_page = create(:wiki_page, project: subject.project) + + expect(subject.slug).not_to eq(other_page.slug) + expect(subject.project).to eq(other_page.project) + expect(subject).not_to eq(other_page) + end + + it 'returns false for page with the same slug on a different project' do + other_page = create(:wiki_page, title: existing_page.slug) + + expect(subject.slug).to eq(other_page.slug) + expect(subject.project).not_to eq(other_page.project) + expect(subject).not_to eq(other_page) end end |