summaryrefslogtreecommitdiff
path: root/spec/models
diff options
context:
space:
mode:
Diffstat (limited to 'spec/models')
-rw-r--r--spec/models/event_spec.rb354
-rw-r--r--spec/models/wiki_page/meta_spec.rb430
-rw-r--r--spec/models/wiki_page/slug_spec.rb94
-rw-r--r--spec/models/wiki_page_spec.rb30
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