diff --git a/ee/spec/models/wiki_page_spec.rb b/ee/spec/models/wiki_page_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f113b7d567eb02583f1dc700ed785ad884ad3b9b --- /dev/null +++ b/ee/spec/models/wiki_page_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe WikiPage, feature_category: :wiki do + it_behaves_like 'wiki_page', :group +end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 2ce4c5b177a9b86ccaafbba5e79db0475ff2e78f..d48f582a9ad004079802afde76a30dc73c46191d 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -3,1131 +3,5 @@ require "spec_helper" RSpec.describe WikiPage, feature_category: :wiki do - let(:user) { create(:user) } - let(:container) { create(:project) } - let(:wiki) { container.wiki } - - def create_file_in_repository(path:) - wiki.create_wiki_repository - wiki.repository.create_file( - user, path, 'test content', - branch_name: wiki.default_branch, - message: 'test commit' - ) - - title = Pathname(path).sub_ext('').to_s - wiki.find_page(title) - end - - def create_wiki_page(container, attrs = {}) - page = build_wiki_page(container, attrs) - - page.create(message: (attrs[:message] || 'test commit')) - - container.wiki.find_page(page.slug) - end - - def build_wiki_page(container, attrs = {}) - wiki_page_attrs = { container: container, content: 'test content' }.merge(attrs) - - build(:wiki_page, wiki_page_attrs) - end - - def force_wiki_change_branch - old_default_branch = wiki.default_branch - wiki.repository.add_branch(user, 'another_branch', old_default_branch) - wiki.repository.rm_branch(user, old_default_branch) - wiki.repository.expire_status_cache - - wiki.container.clear_memoization(:wiki) - end - - # Use for groups of tests that do not modify their `subject`. - # - # include_context 'subject is persisted page', title: 'my title' - shared_context 'subject is persisted page' do |attrs = {}| - let(:persisted_page) { create_wiki_page(container, attrs) } - - subject { persisted_page } - end - - describe '#front_matter' do - let(:wiki_page) { create(:wiki_page, container: container, content: content) } - - shared_examples 'a page without front-matter' do - it { expect(wiki_page).to have_attributes(front_matter: {}, content: content) } - end - - shared_examples 'a page with front-matter' do - let(:front_matter) { { title: 'Foo', slugs: %w[slug_a slug_b] } } - - it { expect(wiki_page.front_matter).to eq(front_matter) } - it { expect(wiki_page.front_matter_title).to eq(front_matter[:title]) } - end - - context 'the wiki page has front matter' do - let(:content) do - <<~MD - --- - title: Foo - slugs: - - slug_a - - slug_b - --- - - My actual content - MD - end - - it_behaves_like 'a page with front-matter' - - it 'strips the front matter from the content' do - expect(wiki_page.content.strip).to eq('My actual content') - end - end - - context 'the wiki page does not have front matter' do - let(:content) { 'My actual content' } - - it_behaves_like 'a page without front-matter' - end - - context 'the wiki page has fenced blocks, but nothing in them' do - let(:content) do - <<~MD - --- - --- - - My actual content - MD - end - - it_behaves_like 'a page without front-matter' - end - - context 'the wiki page has invalid YAML type in fenced blocks' do - let(:content) do - <<~MD - --- - this isn't YAML - --- - - My actual content - MD - end - - it_behaves_like 'a page without front-matter' - end - - context 'the wiki page has a disallowed class in fenced block' do - let(:content) do - <<~MD - --- - date: 2010-02-11 11:02:57 - --- - - My actual content - MD - end - - it_behaves_like 'a page without front-matter' - end - - context 'the wiki page has invalid YAML in fenced block' do - let(:content) do - <<~MD - --- - invalid-use-of-reserved-indicator: @text - --- - - My actual content - MD - end - - it_behaves_like 'a page without front-matter' - end - end - - describe '.unhyphenize' do - it 'removes hyphens from a name' do - name = 'a-name--with-hyphens' - - expect(described_class.unhyphenize(name)).to eq('a name with hyphens') - end - end - - describe "#initialize" do - context "when initialized with an existing page" do - include_context 'subject is persisted page', title: 'test initialization' - - it "sets the slug attribute" do - expect(subject.slug).to eq("test-initialization") - end - - it "sets the title attribute" do - expect(subject.title).to eq("test initialization") - end - - it "sets the formatted content attribute" do - expect(subject.content).to eq("test content") - end - - it "sets the format attribute" do - expect(subject.format).to eq(:markdown) - end - - it "sets the message attribute" do - expect(subject.message).to eq("test commit") - end - - it "sets the version attribute" do - expect(subject.version).to be_a Gitlab::Git::WikiPageVersion - end - end - end - - describe "validations" do - subject { build_wiki_page(container) } - - it "validates presence of title" do - subject.attributes.delete(:title) - - expect(subject).not_to be_valid - expect(subject.errors.messages).to eq(title: ["can't be blank"]) - end - - it "does not validate presence of content" do - subject.attributes.delete(:content) - - expect(subject).to be_valid - end - - describe '#validate_content_size_limit' do - context 'with a new page' do - before do - stub_application_setting(wiki_page_max_content_bytes: 10) - end - - it 'accepts content below the limit' do - subject.attributes[:content] = 'a' * 10 - - expect(subject).to be_valid - end - - it 'rejects content exceeding the limit' do - subject.attributes[:content] = 'a' * 11 - - expect(subject).not_to be_valid - expect(subject.errors.messages).to eq( - content: ['is too long (11 B). The maximum size is 10 B.'] - ) - end - - it 'counts content size in bytes rather than characters' do - subject.attributes[:content] = '💩💩💩' - - expect(subject).not_to be_valid - expect(subject.errors.messages).to eq( - content: ['is too long (12 B). The maximum size is 10 B.'] - ) - end - end - - context 'with an existing page exceeding the limit' do - include_context 'subject is persisted page' - - before do - subject - stub_application_setting(wiki_page_max_content_bytes: 11) - end - - it 'accepts content when it has not changed' do - expect(subject).to be_valid - end - - it 'rejects content when it has changed' do - subject.attributes[:content] = 'a' * 12 - - expect(subject).not_to be_valid - expect(subject.errors.messages).to eq( - content: ['is too long (12 B). The maximum size is 11 B.'] - ) - end - end - end - - describe '#validate_path_limits' do - let(:max_title) { Gitlab::WikiPages::MAX_TITLE_BYTES } - let(:max_directory) { Gitlab::WikiPages::MAX_DIRECTORY_BYTES } - - where(:character) do - ['a', 'ä', '🙈'] - end - - with_them do - let(:size) { character.bytesize.to_f } - let(:valid_title) { character * (max_title / size).floor } - let(:valid_directory) { character * (max_directory / size).floor } - let(:invalid_title) { character * ((max_title + 1) / size).ceil } - let(:invalid_directory) { character * ((max_directory + 1) / size).ceil } - - it 'accepts page titles below the limit' do - subject.title = valid_title - - expect(subject).to be_valid - end - - it 'accepts directories below the limit' do - subject.title = valid_directory + '/foo' - - expect(subject).to be_valid - end - - it 'accepts a path with page title and directory below the limit' do - subject.title = "#{valid_directory}/#{valid_title}" - - expect(subject).to be_valid - end - - it 'rejects page titles exceeding the limit' do - subject.title = invalid_title - - expect(subject).not_to be_valid - expect(subject.errors[:title]).to contain_exactly( - "exceeds the limit of #{max_title} bytes" - ) - end - - it 'rejects directories exceeding the limit' do - subject.title = "#{invalid_directory}/#{invalid_directory}2/foo" - - expect(subject).not_to be_valid - expect(subject.errors[:title]).to contain_exactly( - "exceeds the limit of #{max_directory} bytes for directory name \"#{invalid_directory}\"", - "exceeds the limit of #{max_directory} bytes for directory name \"#{invalid_directory}2\"" - ) - end - - it 'rejects a page with both title and directory exceeding the limit' do - subject.title = "#{invalid_directory}/#{invalid_title}" - - expect(subject).not_to be_valid - expect(subject.errors[:title]).to contain_exactly( - "exceeds the limit of #{max_title} bytes", - "exceeds the limit of #{max_directory} bytes for directory name \"#{invalid_directory}\"" - ) - end - end - - context 'with an existing page title exceeding the limit' do - subject do - title = 'a' * (max_title + 1) - wiki.create_page(title, 'content') - wiki.find_page(title) - end - - it 'accepts the exceeding title length when unchanged' do - expect(subject).to be_valid - end - - it 'rejects the exceeding title length when changed' do - subject.title = 'b' * (max_title + 1) - - expect(subject).not_to be_valid - expect(subject.errors).to include(:title) - end - end - end - end - - describe "#create" do - let(:attributes) do - { - title: SecureRandom.hex, - content: "Home Page", - format: "markdown", - message: 'Custom Commit Message' - } - end - - let(:title) { attributes[:title] } - - subject { build_wiki_page(container) } - - context "with valid attributes" do - it "saves the wiki page" do - subject.create(attributes) - - expect(wiki.find_page(title)).not_to be_nil - end - - it "returns true" do - expect(subject.create(attributes)).to eq(true) - end - - it 'saves the wiki page with message' do - subject.create(attributes) - - expect(wiki.find_page(title).message).to eq 'Custom Commit Message' - end - - it 'if the title is preceded by a / it is removed' do - subject.create(attributes.merge(title: '/New Page')) - - expect(wiki.find_page('New Page')).not_to be_nil - end - end - - context "with invalid attributes" do - it 'does not create the page' do - expect { subject.create(title: '') }.not_to change { wiki.list_pages.length } - end - end - - context "with front matter context" do - let(:attributes) do - { - title: SecureRandom.hex, - content: "---\nxxx: abc\n---\nHome Page", - format: "markdown", - message: 'Custom Commit Message' - } - end - - it 'create the page with front matter' do - subject.create(attributes) - expect(wiki.find_page(title).front_matter).to eq({ xxx: "abc" }) - end - end - - context "with existing page" do - let(:title) { 'Existing Page' } - - it 'do not create the page with the same title' do - page = create_wiki_page(container, title: title, content: 'content') - - subject.create(attributes.merge(title: title)) - expect(subject.create(attributes.merge(title: title))).to be_falsy - expect(wiki.find_page(title).content).to eq(page.content) - end - - it 'do not create the page with the same title, even if the orginal path contains spaces' do - page = create_file_in_repository(path: "#{title}.md") - - subject.create(attributes.merge(title: title)) - expect(subject.create(attributes.merge(title: title))).to be_falsy - expect(wiki.find_page(title).content).to eq(page.content) - end - end - - context 'when the repository fails' do - it 'do not create the page if the repository raise an error' do - page = build_wiki_page(container) - - allow(Gitlab::GitalyClient).to receive(:call) do - raise GRPC::Unavailable, 'Gitaly broken in this spec' - end - - saved = page.create(attributes) - - # unstub - allow(Gitlab::GitalyClient).to receive(:call).and_call_original - - expect(saved).to be(false) - expect(page.errors.messages[:base]).to include(/Gitaly broken in this spec/) - expect(wiki.find_page(title)).to be_nil - end - end - end - - describe "dot in the title" do - let(:title) { 'Index v1.2.3' } - - describe "#create" do - subject { build_wiki_page(container) } - - it "saves the wiki page and returns true", :aggregate_failures do - attributes = { title: title, content: "Home Page", format: "markdown" } - - expect(subject.create(attributes)).to eq(true) - expect(wiki.find_page(title)).not_to be_nil - end - end - - describe '#update' do - subject { create_wiki_page(container, title: title) } - - it 'updates the content of the page and returns true', :aggregate_failures do - expect(subject.update(content: 'new content')).to be_truthy - - page = wiki.find_page(title) - - expect([subject.content, page.content]).to all(eq('new content')) - end - end - end - - describe "#update" do - let!(:original_title) { subject.title } - - subject { create_wiki_page(container) } - - context "with valid attributes" do - it "updates the content of the page" do - new_content = "new content" - - subject.update(content: new_content) - page = wiki.find_page(original_title) - - expect([subject.content, page.content]).to all(eq("new content")) - end - - it "updates the title of the page" do - new_title = "Index v.1.2.4" - - subject.update(title: new_title) - page = wiki.find_page(new_title) - - expect([subject.title, page.title]).to all(eq(new_title)) - end - - describe 'updating front_matter' do - shared_examples 'able to update front-matter' do - it 'updates the wiki-page front-matter' do - content = subject.content - subject.update(front_matter: { slugs: ['x'] }) - page = wiki.find_page(original_title) - - expect([subject, page]).to all( - have_attributes( - front_matter: include(slugs: include('x')), - content: content - )) - end - end - - it_behaves_like 'able to update front-matter' - - context 'the front matter is too long' do - let(:new_front_matter) do - { - title: generate(:wiki_page_title), - slugs: Array.new(51).map { FFaker::Lorem.characters(512) } - } - end - - it 'raises an error' do - expect { subject.update(front_matter: new_front_matter) }.to raise_error(described_class::FrontMatterTooLong) - end - end - - it 'updates the wiki-page front-matter and content together' do - content = 'totally new content' - subject.update(content: content, front_matter: { slugs: ['x'] }) - page = wiki.find_page(original_title) - - expect([subject, page]).to all( - have_attributes( - front_matter: include(slugs: include('x')), - content: content - )) - end - end - - it "returns true" do - expect(subject.update(content: "more content")).to be_truthy - end - end - - context 'with same last commit sha' do - it 'returns true' do - expect(subject.update(content: 'more content', last_commit_sha: subject.last_commit_sha)).to be_truthy - end - end - - context 'with different last commit sha' do - it 'raises exception' do - expect { subject.update(content: 'more content', last_commit_sha: 'xxx') }.to raise_error(WikiPage::PageChangedError) - end - end - - describe 'in subdir' do - it 'keeps the page in the same dir when the content is updated' do - title = 'foo/Existing Page' - page = create_wiki_page(container, title: title) - - expect(page.slug).to eq 'foo/Existing-Page' - expect(page.update(title: title, content: 'new_content')).to be_truthy - - page = wiki.find_page(title) - - expect(page.slug).to eq 'foo/Existing-Page' - expect(page.content).to eq 'new_content' - end - end - - context 'when renaming a page' do - it 'raises an error if the page already exists' do - existing_page = create_wiki_page(container) - - expect { subject.update(title: existing_page.title, content: 'new_content') }.to raise_error(WikiPage::PageRenameError) - expect(subject.title).to eq original_title - expect(subject.content).to eq 'new_content' # We don't revert the content - end - - it 'updates the content and rename the file' do - new_title = 'Renamed Page' - new_content = 'updated content' - - expect(subject.update(title: new_title, content: new_content)).to be_truthy - - page = wiki.find_page(new_title) - - expect(page).not_to be_nil - expect(page.content).to eq new_content - end - end - - context 'when moving a page' do - it 'raises an error if the page already exists' do - wiki.create_page('foo/Existing Page', 'content') - - expect { subject.update(title: 'foo/Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError) - expect(subject.title).to eq original_title - expect(subject.content).to eq 'new_content' - end - - it 'raises an error if the page already exists even if it contains spaces in the orginal path' do - create_file_in_repository(path: 'foo/Existing Page.md') - - expect { subject.update(title: 'foo/Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError) - expect(subject.title).to eq original_title - expect(subject.content).to eq 'new_content' - end - - it 'updates the content and moves the file' do - new_title = 'foo/Other Page' - new_content = 'new_content' - - expect(subject.update(title: new_title, content: new_content)).to be_truthy - - page = wiki.find_page(new_title) - - expect(page).not_to be_nil - expect(page.content).to eq new_content - end - - context 'when page combine with directory' do - it 'moving the file and directory' do - wiki.create_page('testpage/testtitle', 'content') - wiki.create_page('testpage', 'content') - - page = wiki.find_page('testpage') - page.update(title: 'testfolder/testpage') - - page = wiki.find_page('testfolder/testpage/testtitle') - - expect(page.slug).to eq 'testfolder/testpage/testtitle' - end - end - - describe 'in subdir' do - it 'moves the page to the root folder if the title is preceded by /' do - page = create_wiki_page(container, title: 'foo/Existing Page') - - expect(page.slug).to eq 'foo/Existing-Page' - expect(page.update(title: '/Existing Page', content: 'new_content')).to be_truthy - expect(page.slug).to eq 'Existing-Page' - end - - it 'does nothing if it has the same title' do - page = create_wiki_page(container, title: 'foo/Another Existing Page') - - original_path = page.slug - - expect(page.update(title: 'Another Existing Page', content: 'new_content')).to be_truthy - expect(page.slug).to eq original_path - end - - it 'moves the page to the another folder if the original path has spaces' do - page = create_file_in_repository(path: 'Existing Folder/Existing Page.md') - - original_path = page.slug - - expect(page.update(title: 'Existing Page', content: 'new_content')).to be_truthy - expect(page.slug).not_to eq original_path - expect(page.slug).to eq "Existing-Folder/Existing-Page" - end - end - - context 'in root dir' do - it 'does nothing if the title is preceded by /' do - original_path = subject.slug - - expect(subject.update(title: "/#{subject.title}", content: 'new_content')).to be_truthy - expect(subject.slug).to eq original_path - end - end - end - - context "with invalid attributes" do - it 'aborts update if title blank' do - expect(subject.update(title: '', content: 'new_content')).to be_falsey - expect(subject.content).to eq 'new_content' - - page = wiki.find_page(original_title) - - expect(page.content).to eq 'test content' - end - end - - context 'when the repository fails' do - it 'do not update the page if the repository raise an error' do - page = create_wiki_page(container) - - allow(Gitlab::GitalyClient).to receive(:call) do - raise GRPC::Unavailable, 'Gitaly broken in this spec' - end - - saved = page.update(content: "new content") - - # unstub - allow(Gitlab::GitalyClient).to receive(:call).and_call_original - - expect(saved).to be(false) - expect(page.errors.messages[:base]).to include(/Gitaly broken in this spec/) - - page_found = wiki.find_page(original_title) - - expect(page_found.content).to eq 'test content' - end - end - end - - describe "#delete" do - it "deletes the page and returns true", :aggregate_failures do - page = create_wiki_page(container) - - expect do - expect(page.delete).to eq(true) - end.to change { wiki.list_pages.length }.by(-1) - end - - context 'when the repository fails' do - it 'do not delete the page if the repository raise an error' do - page = create_wiki_page(container) - - allow(Gitlab::GitalyClient).to receive(:call) do - raise GRPC::Unavailable, 'Gitaly broken in this spec' - end - - deleted = page.delete - - # unstub - allow(Gitlab::GitalyClient).to receive(:call).and_call_original - - expect(deleted).to be(false) - expect(wiki.error_message).to match(/Gitaly broken in this spec/) - expect(wiki.list_pages.length).to be(1) - end - end - end - - describe "#versions" do - let(:subject) { create_wiki_page(container) } - - before do - 3.times { |i| subject.update(content: "content #{i}") } - end - - context 'number of versions is less than the default paginiated per page' do - it "returns an array of all commits for the page" do - expect(subject.versions).to be_a(::CommitCollection) - expect(subject.versions.length).to eq(4) - expect(subject.versions.first.id).to eql(subject.last_version.id) - end - end - - context 'number of versions is more than the default paginiated per page' do - before do - allow(Kaminari.config).to receive(:default_per_page).and_return(3) - end - - it "returns an arrary containing the first page of commits for the page" do - expect(subject.versions).to be_a(::CommitCollection) - expect(subject.versions.length).to eq(3) - expect(subject.versions.first.id).to eql(subject.last_version.id) - end - - it "returns an arrary containing the second page of commits for the page with options[:page] = 2" do - versions = subject.versions(page: 2) - expect(versions).to be_a(::CommitCollection) - expect(versions.length).to eq(1) - end - end - - context "wiki repository's default is updated" do - before do - force_wiki_change_branch - end - - it "returns the correct versions in the default branch" do - page = container.wiki.find_page(subject.title) - - expect(page.versions).to be_a(::CommitCollection) - expect(page.versions.length).to eq(4) - expect(page.versions.first.id).to eql(page.last_version.id) - - page.update(content: "final content") - expect(page.versions.length).to eq(5) - end - end - end - - describe "#count_versions" do - let(:subject) { create_wiki_page(container) } - - it "returns the total numbers of commits" do - expect do - 3.times { |i| subject.update(content: "content #{i}") } - end.to change(subject, :count_versions).from(1).to(4) - end - - context "wiki repository's default is updated" do - before do - subject - force_wiki_change_branch - end - - it "returns the correct number of versions in the default branch" do - page = container.wiki.find_page(subject.title) - expect(page.count_versions).to eq(1) - - page.update(content: "final content") - expect(page.count_versions).to eq(2) - end - end - end - - describe '#title_changed?' do - using RSpec::Parameterized::TableSyntax - - let(:unsaved_page) { build_wiki_page(container, title: 'test page') } - let(:existing_page) { create_wiki_page(container, title: 'test page') } - let(:directory_page) { create_wiki_page(container, title: 'parent directory/child page') } - let(:page_with_special_characters) { create_wiki_page(container, title: 'test+page') } - - let(:untitled_page) { described_class.new(wiki) } - - where(:page, :title, :changed) do - :untitled_page | nil | false - :untitled_page | 'new title' | true - - :unsaved_page | nil | true - :unsaved_page | 'test page' | true - :unsaved_page | 'test-page' | true - :unsaved_page | 'test+page' | true - :unsaved_page | 'new title' | true - - :existing_page | nil | false - :existing_page | 'test page' | false - :existing_page | 'test-page' | false - :existing_page | '/test page' | false - :existing_page | '/test-page' | false - :existing_page | 'test+page' | true - :existing_page | ' test page ' | true - :existing_page | 'new title' | true - :existing_page | 'new-title' | true - - :directory_page | nil | false - :directory_page | 'parent directory/child page' | false - :directory_page | 'parent-directory/child page' | false - :directory_page | 'parent-directory/child-page' | false - :directory_page | 'child page' | false - :directory_page | 'child-page' | false - :directory_page | '/child page' | true - :directory_page | 'parent directory/other' | true - :directory_page | 'parent-directory/other' | true - :directory_page | 'parent-directory / child-page' | true - :directory_page | 'other directory/child page' | true - :directory_page | 'other-directory/child page' | true - - :page_with_special_characters | nil | false - :page_with_special_characters | 'test+page' | false - :page_with_special_characters | 'test-page' | true - :page_with_special_characters | 'test page' | true - end - - with_them do - it 'returns the expected value' do - subject = public_send(page) - subject.title = title if title - - expect(subject.title_changed?).to be(changed) - end - end - end - - describe '#content_changed?' do - context 'with a new page' do - subject { build_wiki_page(container) } - - it 'returns true if content is set' do - subject.attributes[:content] = 'new' - - expect(subject.content_changed?).to be(true) - end - - it 'returns false if content is blank' do - subject.attributes[:content] = ' ' - - expect(subject.content_changed?).to be(false) - end - end - - context 'with an existing page' do - include_context 'subject is persisted page' - - it 'returns false' do - expect(subject.content_changed?).to be(false) - end - - it 'returns false if content is set to the same value' do - subject.attributes[:content] = 'test content' - - expect(subject.content_changed?).to be(false) - end - - it 'returns true if content is changed' do - subject.attributes[:content] = 'new' - - expect(subject.content_changed?).to be(true) - end - - it 'returns true if content is changed to a blank string' do - subject.attributes[:content] = ' ' - - expect(subject.content_changed?).to be(true) - end - - it 'returns false if only the newline format has changed from LF to CRLF' do - expect(subject.page).to receive(:text_data).and_return("foo\nbar") - - subject.attributes[:content] = "foo\r\nbar" - - expect(subject.content_changed?).to be(false) - end - - it 'returns false if only the newline format has changed from CRLF to LF' do - expect(subject.page).to receive(:text_data).and_return("foo\r\nbar") - - subject.attributes[:content] = "foo\nbar" - - expect(subject.content_changed?).to be(false) - end - end - end - - describe '#path' do - it 'returns the path when persisted' do - existing_page = create_wiki_page(container, title: 'path test') - - expect(existing_page.path).to eq('path-test.md') - end - - it 'returns nil when not persisted' do - unsaved_page = build_wiki_page(container, title: 'path test') - - expect(unsaved_page.path).to be_nil - end - end - - describe '#directory' do - context 'when the page is at the root directory' do - include_context 'subject is persisted page', title: 'directory test' - - it 'returns an empty string' do - expect(subject.directory).to eq('') - end - end - - context 'when the page is inside an actual directory' do - include_context 'subject is persisted page', title: 'dir_1/dir_1_1/directory test' - - it 'returns the full directory hierarchy' do - expect(subject.directory).to eq('dir_1/dir_1_1') - end - end - end - - describe '#historical?' do - let!(:container) { create(:project) } - - subject { create_wiki_page(container) } - - let(:wiki) { subject.wiki } - let(:old_version) { subject.versions.last.id } - let(:old_page) { wiki.find_page(subject.title, old_version) } - let(:latest_version) { subject.versions.first.id } - let(:latest_page) { wiki.find_page(subject.title, latest_version) } - - before do - 3.times { |i| subject.update(content: "content #{i}") } - end - - it 'returns true when requesting an old version' do - expect(old_page.historical?).to be_truthy - end - - it 'returns false when requesting latest version' do - expect(latest_page.historical?).to be_falsy - end - - it 'returns false when version is nil' do - expect(latest_page).to receive(:version) { nil } - - expect(latest_page.historical?).to be_falsy - end - - it 'returns false when the last version is nil' do - expect(old_page).to receive(:last_version) { nil } - - expect(old_page.historical?).to be_falsy - end - - it 'returns false when the version is nil' do - expect(old_page).to receive(:version) { nil } - - expect(old_page.historical?).to be_falsy - end - end - - describe '#persisted?' do - it 'returns true for a persisted page' do - expect(create_wiki_page(container)).to be_persisted - end - - it 'returns false for an unpersisted page' do - expect(build_wiki_page(container)).not_to be_persisted - end - end - - describe '#to_partial_path' do - it 'returns the relative path to the partial to be used' do - expect(build_wiki_page(container).to_partial_path).to eq('shared/wikis/wiki_page') - end - end - - describe '#==' do - include_context 'subject is persisted page' - - it 'returns true for identical wiki page' do - expect(subject).to eq(subject) - end - - it 'returns true for updated wiki page' do - subject.update(content: "Updated content") - updated_page = wiki.find_page(subject.slug) - - expect(updated_page).not_to be_nil - 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.container).not_to eq(other_page.container) - expect(subject).not_to eq(other_page) - end - - it 'returns false for page with different slug on same container' do - other_page = create_wiki_page(container) - - expect(subject.slug).not_to eq(other_page.slug) - expect(subject.container).to eq(other_page.container) - expect(subject).not_to eq(other_page) - end - - it 'returns false for page with the same slug on a different container' do - other_page = create(:wiki_page, title: subject.slug) - - expect(subject.slug).to eq(other_page.slug) - expect(subject.container).not_to eq(other_page.container) - expect(subject).not_to eq(other_page) - end - end - - describe '#last_commit_sha' do - include_context 'subject is persisted page' - - it 'returns commit sha' do - expect(subject.last_commit_sha).to eq subject.last_version.sha - end - - it 'is changed after page updated' do - last_commit_sha_before_update = subject.last_commit_sha - - subject.update(content: "new content") - page = wiki.find_page(subject.title) - - expect(page.last_commit_sha).not_to eq last_commit_sha_before_update - end - end - - describe '#hook_attrs' do - subject { build_wiki_page(container) } - - it 'includes specific attributes' do - keys = subject.hook_attrs.keys - expect(keys).not_to include(:content) - expect(keys).to include(:version_id) - end - end - - describe '#version_commit_timestamp' do - context 'for a new page' do - it 'returns nil' do - expect(build_wiki_page(container).version_commit_timestamp).to be_nil - end - end - - context 'for page that exists' do - it 'returns the timestamp of the commit' do - existing_page = create_wiki_page(container) - - expect(existing_page.version_commit_timestamp).to eq(existing_page.version.commit.committed_date) - end - end - end - - describe '#diffs' do - include_context 'subject is persisted page' - - it 'returns a diff instance' do - diffs = subject.diffs(foo: 'bar') - - expect(diffs).to be_a(Gitlab::Diff::FileCollection::WikiPage) - expect(diffs.diffable).to be_a(Commit) - expect(diffs.diffable.id).to eq(subject.version.id) - expect(diffs.project).to be(subject.wiki) - expect(diffs.diff_options).to include( - expanded: true, - paths: [subject.path], - foo: 'bar' - ) - end - end - - describe "#human_title" do - context "with front matter title" do - let(:front_matter_title) { "abc" } - let(:content_with_front_matter_title) { "---\ntitle: #{front_matter_title}\n---\nHome Page" } - let(:wiki_page) { create(:wiki_page, container: container, content: content_with_front_matter_title) } - - it 'returns the front matter title' do - expect(wiki_page.human_title).to eq front_matter_title - end - end - end + it_behaves_like 'wiki_page', :project end diff --git a/spec/support/shared_examples/models/wiki_page_shared_examples.rb b/spec/support/shared_examples/models/wiki_page_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..8a46387caf90b00edefbb114079c64b235259bfd --- /dev/null +++ b/spec/support/shared_examples/models/wiki_page_shared_examples.rb @@ -0,0 +1,1149 @@ +# frozen_string_literal: true + +require "spec_helper" + +# rubocop:disable Rails/SaveBang -- None of the offenses are ActiveRecord calls +RSpec.shared_examples 'wiki_page' do |container_type| + let(:user) { create(:user) } + let(:owner) { create(:user) } + let(:container) { create(container_type) } + let(:wiki) { container.wiki } + + def create_file_in_repository(path:) + wiki.create_wiki_repository + wiki.repository.create_file( + user, path, 'test content', + branch_name: wiki.default_branch, + message: 'test commit' + ) + + title = Pathname(path).sub_ext('').to_s + wiki.find_page(title) + end + + def create_wiki_page(container, attrs = {}) + page = build_wiki_page(container, attrs) + + page.create(message: attrs[:message] || 'test commit') + + container.wiki.find_page(page.slug) + end + + def build_wiki_page(container, attrs = {}) + wiki_page_attrs = { container: container, content: 'test content' }.merge(attrs) + + build(:wiki_page, wiki_page_attrs) + end + + def force_wiki_change_branch + old_default_branch = wiki.default_branch + wiki.repository.add_branch(user, 'another_branch', old_default_branch) + wiki.repository.rm_branch(user, old_default_branch) + wiki.repository.expire_status_cache + + wiki.container.clear_memoization(:wiki) + end + + before do + container.add_owner(owner) + end + + # Use for groups of tests that do not modify their `subject`. + # + # include_context 'when subject is a persisted page', title: 'my title' + shared_context 'when subject is a persisted page' do |attrs = {}| + let(:persisted_page) { create_wiki_page(container, attrs) } + + subject { persisted_page } + end + + describe '#front_matter' do + let(:wiki_page) { create(:wiki_page, container: container, content: content) } + + shared_examples 'a page without front-matter' do + it { expect(wiki_page).to have_attributes(front_matter: {}, content: content) } + end + + shared_examples 'a page with front-matter' do + let(:front_matter) { { title: 'Foo', slugs: %w[slug_a slug_b] } } + + it { expect(wiki_page.front_matter).to eq(front_matter) } + it { expect(wiki_page.front_matter_title).to eq(front_matter[:title]) } + end + + context 'when the wiki page has front matter' do + let(:content) do + <<~MD + --- + title: Foo + slugs: + - slug_a + - slug_b + --- + + My actual content + MD + end + + it_behaves_like 'a page with front-matter' + + it 'strips the front matter from the content' do + expect(wiki_page.content.strip).to eq('My actual content') + end + end + + context 'when the wiki page does not have front matter' do + let(:content) { 'My actual content' } + + it_behaves_like 'a page without front-matter' + end + + context 'when the wiki page has fenced blocks, but nothing in them' do + let(:content) do + <<~MD + --- + --- + + My actual content + MD + end + + it_behaves_like 'a page without front-matter' + end + + context 'when the wiki page has invalid YAML type in fenced blocks' do + let(:content) do + <<~MD + --- + this isn't YAML + --- + + My actual content + MD + end + + it_behaves_like 'a page without front-matter' + end + + context 'when the wiki page has a disallowed class in fenced block' do + let(:content) do + <<~MD + --- + date: 2010-02-11 11:02:57 + --- + + My actual content + MD + end + + it_behaves_like 'a page without front-matter' + end + + context 'when the wiki page has invalid YAML in fenced block' do + let(:content) do + <<~MD + --- + invalid-use-of-reserved-indicator: @text + --- + + My actual content + MD + end + + it_behaves_like 'a page without front-matter' + end + end + + describe '.unhyphenize' do + it 'removes hyphens from a name' do + name = 'a-name--with-hyphens' + + expect(described_class.unhyphenize(name)).to eq('a name with hyphens') + end + end + + describe "#initialize" do + context "when initialized with an existing page" do + include_context 'when subject is a persisted page', title: 'test initialization' + + it "sets the slug attribute" do + expect(subject.slug).to eq("test-initialization") + end + + it "sets the title attribute" do + expect(subject.title).to eq("test initialization") + end + + it "sets the formatted content attribute" do + expect(subject.content).to eq("test content") + end + + it "sets the format attribute" do + expect(subject.format).to eq(:markdown) + end + + it "sets the message attribute" do + expect(subject.message).to eq("test commit") + end + + it "sets the version attribute" do + expect(subject.version).to be_a Gitlab::Git::WikiPageVersion + end + end + end + + describe "validations" do + subject { build_wiki_page(container) } + + it "validates presence of title" do + subject.attributes.delete(:title) + + expect(subject).not_to be_valid + expect(subject.errors.messages).to eq(title: ["can't be blank"]) + end + + it "does not validate presence of content" do + subject.attributes.delete(:content) + + expect(subject).to be_valid + end + + describe '#validate_content_size_limit' do + context 'with a new page' do + before do + stub_application_setting(wiki_page_max_content_bytes: 10) + end + + it 'accepts content below the limit' do + subject.attributes[:content] = 'a' * 10 + + expect(subject).to be_valid + end + + it 'rejects content exceeding the limit' do + subject.attributes[:content] = 'a' * 11 + + expect(subject).not_to be_valid + expect(subject.errors.messages).to eq( + content: ['is too long (11 B). The maximum size is 10 B.'] + ) + end + + it 'counts content size in bytes rather than characters' do + subject.attributes[:content] = '💩💩💩' + + expect(subject).not_to be_valid + expect(subject.errors.messages).to eq( + content: ['is too long (12 B). The maximum size is 10 B.'] + ) + end + end + + context 'with an existing page exceeding the limit' do + include_context 'when subject is a persisted page' + + before do + subject + stub_application_setting(wiki_page_max_content_bytes: 11) + end + + it 'accepts content when it has not changed' do + expect(subject).to be_valid + end + + it 'rejects content when it has changed' do + subject.attributes[:content] = 'a' * 12 + + expect(subject).not_to be_valid + expect(subject.errors.messages).to eq( + content: ['is too long (12 B). The maximum size is 11 B.'] + ) + end + end + end + + describe '#validate_path_limits' do + let(:max_title) { Gitlab::WikiPages::MAX_TITLE_BYTES } + let(:max_directory) { Gitlab::WikiPages::MAX_DIRECTORY_BYTES } + + where(:character) do + ['a', 'ä', '🙈'] + end + + with_them do + let(:size) { character.bytesize.to_f } + let(:valid_title) { character * (max_title / size).floor } + let(:valid_directory) { character * (max_directory / size).floor } + let(:invalid_title) { character * ((max_title + 1) / size).ceil } + let(:invalid_directory) { character * ((max_directory + 1) / size).ceil } + + it 'accepts page titles below the limit' do + subject.title = valid_title + + expect(subject).to be_valid + end + + it 'accepts directories below the limit' do + subject.title = "#{valid_directory}/foo" + + expect(subject).to be_valid + end + + it 'accepts a path with page title and directory below the limit' do + subject.title = "#{valid_directory}/#{valid_title}" + + expect(subject).to be_valid + end + + it 'rejects page titles exceeding the limit' do + subject.title = invalid_title + + expect(subject).not_to be_valid + expect(subject.errors[:title]).to contain_exactly( + "exceeds the limit of #{max_title} bytes" + ) + end + + it 'rejects directories exceeding the limit' do + subject.title = "#{invalid_directory}/#{invalid_directory}2/foo" + + expect(subject).not_to be_valid + expect(subject.errors[:title]).to contain_exactly( + "exceeds the limit of #{max_directory} bytes for directory name \"#{invalid_directory}\"", + "exceeds the limit of #{max_directory} bytes for directory name \"#{invalid_directory}2\"" + ) + end + + it 'rejects a page with both title and directory exceeding the limit' do + subject.title = "#{invalid_directory}/#{invalid_title}" + + expect(subject).not_to be_valid + expect(subject.errors[:title]).to contain_exactly( + "exceeds the limit of #{max_title} bytes", + "exceeds the limit of #{max_directory} bytes for directory name \"#{invalid_directory}\"" + ) + end + end + + context 'with an existing page title exceeding the limit' do + subject do + title = 'a' * (max_title + 1) + wiki.create_page(title, 'content') + wiki.find_page(title) + end + + it 'accepts the exceeding title length when unchanged' do + expect(subject).to be_valid + end + + it 'rejects the exceeding title length when changed' do + subject.title = 'b' * (max_title + 1) + + expect(subject).not_to be_valid + expect(subject.errors).to include(:title) + end + end + end + end + + describe "#create" do + let(:attributes) do + { + title: SecureRandom.hex, + content: "Home Page", + format: "markdown", + message: 'Custom Commit Message' + } + end + + let(:title) { attributes[:title] } + + subject { build_wiki_page(container) } + + context "with valid attributes" do + it "saves the wiki page" do + subject.create(attributes) + + expect(wiki.find_page(title)).not_to be_nil + end + + it "returns true" do + expect(subject.create(attributes)).to be(true) + end + + it 'saves the wiki page with message' do + subject.create(attributes) + + expect(wiki.find_page(title).message).to eq 'Custom Commit Message' + end + + it 'if the title is preceded by a / it is removed' do + subject.create(attributes.merge(title: '/New Page')) + + expect(wiki.find_page('New Page')).not_to be_nil + end + end + + context "with invalid attributes" do + it 'does not create the page' do + expect { subject.create(title: '') }.not_to change { wiki.list_pages.length } + end + end + + context "with front matter context" do + let(:attributes) do + { + title: SecureRandom.hex, + content: "---\nxxx: abc\n---\nHome Page", + format: "markdown", + message: 'Custom Commit Message' + } + end + + it 'create the page with front matter' do + subject.create(attributes) + expect(wiki.find_page(title).front_matter).to eq({ xxx: "abc" }) + end + end + + context "with existing page" do + let(:title) { 'Existing Page' } + + it 'do not create the page with the same title' do + page = create_wiki_page(container, title: title, content: 'content') + + subject.create(attributes.merge(title: title)) + expect(subject.create(attributes.merge(title: title))).to be_falsy + expect(wiki.find_page(title).content).to eq(page.content) + end + + it 'do not create the page with the same title, even if the orginal path contains spaces' do + page = create_file_in_repository(path: "#{title}.md") + + subject.create(attributes.merge(title: title)) + expect(subject.create(attributes.merge(title: title))).to be_falsy + expect(wiki.find_page(title).content).to eq(page.content) + end + end + + context 'when the repository fails' do + it 'do not create the page if the repository raise an error' do + page = build_wiki_page(container) + + allow(Gitlab::GitalyClient).to receive(:call) do + raise GRPC::Unavailable, 'Gitaly broken in this spec' + end + + saved = page.create(attributes) + + # unstub + allow(Gitlab::GitalyClient).to receive(:call).and_call_original + + expect(saved).to be(false) + expect(page.errors.messages[:base]).to include(/Gitaly broken in this spec/) + expect(wiki.find_page(title)).to be_nil + end + end + end + + describe "dot in the title" do + let(:title) { 'Index v1.2.3' } + + describe "#create" do + subject { build_wiki_page(container) } + + it "saves the wiki page and returns true", :aggregate_failures do + attributes = { title: title, content: "Home Page", format: "markdown" } + + expect(subject.create(attributes)).to be(true) + expect(wiki.find_page(title)).not_to be_nil + end + end + + describe '#update' do + subject { create_wiki_page(container, title: title) } + + it 'updates the content of the page and returns true', :aggregate_failures do + expect(subject.update(content: 'new content')).to be_truthy + + page = wiki.find_page(title) + + expect([subject.content, page.content]).to all(eq('new content')) + end + end + end + + describe "#update" do + let!(:original_title) { subject.title } + + subject { create_wiki_page(container) } + + context "with valid attributes" do + it "updates the content of the page" do + new_content = "new content" + + subject.update(content: new_content) + page = wiki.find_page(original_title) + + expect([subject.content, page.content]).to all(eq("new content")) + end + + it "updates the title of the page" do + new_title = "Index v.1.2.4" + + subject.update(title: new_title) + page = wiki.find_page(new_title) + + expect([subject.title, page.title]).to all(eq(new_title)) + end + + describe 'updating front_matter' do + shared_examples 'able to update front-matter' do + it 'updates the wiki-page front-matter' do + content = subject.content + subject.update(front_matter: { slugs: ['x'] }) + page = wiki.find_page(original_title) + + expect([subject, page]).to all( + have_attributes( + front_matter: include(slugs: include('x')), + content: content + )) + end + end + + it_behaves_like 'able to update front-matter' + + context 'when the front matter is too long' do + let(:new_front_matter) do + { + title: generate(:wiki_page_title), + slugs: Array.new(51).map { FFaker::Lorem.characters(512) } + } + end + + it 'raises an error' do + expect do + subject.update(front_matter: new_front_matter) + end.to raise_error(described_class::FrontMatterTooLong) + end + end + + it 'updates the wiki-page front-matter and content together' do + content = 'totally new content' + subject.update(content: content, front_matter: { slugs: ['x'] }) + page = wiki.find_page(original_title) + + expect([subject, page]).to all( + have_attributes( + front_matter: include(slugs: include('x')), + content: content + )) + end + end + + it "returns true" do + expect(subject.update(content: "more content")).to be_truthy + end + end + + context 'with same last commit sha' do + it 'returns true' do + expect(subject.update(content: 'more content', last_commit_sha: subject.last_commit_sha)).to be_truthy + end + end + + context 'with different last commit sha' do + it 'raises exception' do + expect do + subject.update(content: 'more content', last_commit_sha: 'xxx') + end.to raise_error(WikiPage::PageChangedError) + end + end + + describe 'in subdir' do + it 'keeps the page in the same dir when the content is updated' do + title = 'foo/Existing Page' + page = create_wiki_page(container, title: title) + + expect(page.slug).to eq 'foo/Existing-Page' + expect(page.update(title: title, content: 'new_content')).to be_truthy + + page = wiki.find_page(title) + + expect(page.slug).to eq 'foo/Existing-Page' + expect(page.content).to eq 'new_content' + end + end + + context 'when renaming a page' do + it 'raises an error if the page already exists' do + existing_page = create_wiki_page(container) + + expect do + subject.update(title: existing_page.title, content: 'new_content') + end.to raise_error(WikiPage::PageRenameError) + expect(subject.title).to eq original_title + expect(subject.content).to eq 'new_content' # We don't revert the content + end + + it 'updates the content and rename the file' do + new_title = 'Renamed Page' + new_content = 'updated content' + + expect(subject.update(title: new_title, content: new_content)).to be_truthy + + page = wiki.find_page(new_title) + + expect(page).not_to be_nil + expect(page.content).to eq new_content + end + end + + context 'when moving a page' do + it 'raises an error if the page already exists' do + wiki.create_page('foo/Existing Page', 'content') + + expect do + subject.update(title: 'foo/Existing Page', content: 'new_content') + end.to raise_error(WikiPage::PageRenameError) + expect(subject.title).to eq original_title + expect(subject.content).to eq 'new_content' + end + + it 'raises an error if the page already exists even if it contains spaces in the orginal path' do + create_file_in_repository(path: 'foo/Existing Page.md') + + expect do + subject.update(title: 'foo/Existing Page', content: 'new_content') + end.to raise_error(WikiPage::PageRenameError) + expect(subject.title).to eq original_title + expect(subject.content).to eq 'new_content' + end + + it 'updates the content and moves the file' do + new_title = 'foo/Other Page' + new_content = 'new_content' + + expect(subject.update(title: new_title, content: new_content)).to be_truthy + + page = wiki.find_page(new_title) + + expect(page).not_to be_nil + expect(page.content).to eq new_content + end + + context 'when page combine with directory' do + it 'moving the file and directory' do + wiki.create_page('testpage/testtitle', 'content') + wiki.create_page('testpage', 'content') + + page = wiki.find_page('testpage') + page.update(title: 'testfolder/testpage') + + page = wiki.find_page('testfolder/testpage/testtitle') + + expect(page.slug).to eq 'testfolder/testpage/testtitle' + end + end + + describe 'in subdir' do + it 'moves the page to the root folder if the title is preceded by /' do + page = create_wiki_page(container, title: 'foo/Existing Page') + + expect(page.slug).to eq 'foo/Existing-Page' + expect(page.update(title: '/Existing Page', content: 'new_content')).to be_truthy + expect(page.slug).to eq 'Existing-Page' + end + + it 'does nothing if it has the same title' do + page = create_wiki_page(container, title: 'foo/Another Existing Page') + + original_path = page.slug + + expect(page.update(title: 'Another Existing Page', content: 'new_content')).to be_truthy + expect(page.slug).to eq original_path + end + + it 'moves the page to the another folder if the original path has spaces' do + page = create_file_in_repository(path: 'Existing Folder/Existing Page.md') + + original_path = page.slug + + expect(page.update(title: 'Existing Page', content: 'new_content')).to be_truthy + expect(page.slug).not_to eq original_path + expect(page.slug).to eq "Existing-Folder/Existing-Page" + end + end + + context 'in root dir' do + it 'does nothing if the title is preceded by /' do + original_path = subject.slug + + expect(subject.update(title: "/#{subject.title}", content: 'new_content')).to be_truthy + expect(subject.slug).to eq original_path + end + end + end + + context "with invalid attributes" do + it 'aborts update if title blank' do + expect(subject.update(title: '', content: 'new_content')).to be_falsey + expect(subject.content).to eq 'new_content' + + page = wiki.find_page(original_title) + + expect(page.content).to eq 'test content' + end + end + + context 'when the repository fails' do + it 'do not update the page if the repository raise an error' do + page = create_wiki_page(container) + + allow(Gitlab::GitalyClient).to receive(:call) do + raise GRPC::Unavailable, 'Gitaly broken in this spec' + end + + saved = page.update(content: "new content") + + # unstub + allow(Gitlab::GitalyClient).to receive(:call).and_call_original + + expect(saved).to be(false) + expect(page.errors.messages[:base]).to include(/Gitaly broken in this spec/) + + page_found = wiki.find_page(original_title) + + expect(page_found.content).to eq 'test content' + end + end + end + + describe "#delete" do + it "deletes the page and returns true", :aggregate_failures do + page = create_wiki_page(container) + + expect do + expect(page.delete).to be(true) + end.to change { wiki.list_pages.length }.by(-1) + end + + context 'when the repository fails' do + it 'do not delete the page if the repository raise an error' do + page = create_wiki_page(container) + + allow(Gitlab::GitalyClient).to receive(:call) do + raise GRPC::Unavailable, 'Gitaly broken in this spec' + end + + deleted = page.delete + + # unstub + allow(Gitlab::GitalyClient).to receive(:call).and_call_original + + expect(deleted).to be(false) + expect(wiki.error_message).to match(/Gitaly broken in this spec/) + expect(wiki.list_pages.length).to be(1) + end + end + end + + describe "#versions" do + subject { create_wiki_page(container) } + + before do + 3.times { |i| subject.update(content: "content #{i}") } + end + + context 'when number of versions is less than the default paginiated per page' do + it "returns an array of all commits for the page" do + expect(subject.versions).to be_a(::CommitCollection) + expect(subject.versions.length).to eq(4) + expect(subject.versions.first.id).to eql(subject.last_version.id) + end + end + + context 'when number of versions is more than the default paginiated per page' do + before do + allow(Kaminari.config).to receive(:default_per_page).and_return(3) + end + + it "returns an arrary containing the first page of commits for the page" do + expect(subject.versions).to be_a(::CommitCollection) + expect(subject.versions.length).to eq(3) + expect(subject.versions.first.id).to eql(subject.last_version.id) + end + + it "returns an arrary containing the second page of commits for the page with options[:page] = 2" do + versions = subject.versions(page: 2) + expect(versions).to be_a(::CommitCollection) + expect(versions.length).to eq(1) + end + end + + context "when wiki repository's default is updated" do + before do + force_wiki_change_branch + end + + it "returns the correct versions in the default branch" do + page = container.wiki.find_page(subject.title) + + expect(page.versions).to be_a(::CommitCollection) + expect(page.versions.length).to eq(4) + expect(page.versions.first.id).to eql(page.last_version.id) + + page.update(content: "final content") + expect(page.versions.length).to eq(5) + end + end + end + + describe "#count_versions" do + subject { create_wiki_page(container) } + + it "returns the total numbers of commits" do + expect do + 3.times { |i| subject.update(content: "content #{i}") } + end.to change { subject.count_versions }.from(1).to(4) + end + + context "when wiki repository's default is updated" do + before do + subject + force_wiki_change_branch + end + + it "returns the correct number of versions in the default branch" do + page = container.wiki.find_page(subject.title) + expect(page.count_versions).to eq(1) + + page.update(content: "final content") + expect(page.count_versions).to eq(2) + end + end + end + + describe '#title_changed?' do + using RSpec::Parameterized::TableSyntax + + let(:unsaved_page) { build_wiki_page(container, title: 'test page') } + let(:existing_page) { create_wiki_page(container, title: 'test page') } + let(:directory_page) { create_wiki_page(container, title: 'parent directory/child page') } + let(:page_with_special_characters) { create_wiki_page(container, title: 'test+page') } + + let(:untitled_page) { described_class.new(wiki) } + + where(:page, :title, :changed) do + :untitled_page | nil | false + :untitled_page | 'new title' | true + + :unsaved_page | nil | true + :unsaved_page | 'test page' | true + :unsaved_page | 'test-page' | true + :unsaved_page | 'test+page' | true + :unsaved_page | 'new title' | true + + :existing_page | nil | false + :existing_page | 'test page' | false + :existing_page | 'test-page' | false + :existing_page | '/test page' | false + :existing_page | '/test-page' | false + :existing_page | 'test+page' | true + :existing_page | ' test page ' | true + :existing_page | 'new title' | true + :existing_page | 'new-title' | true + + :directory_page | nil | false + :directory_page | 'parent directory/child page' | false + :directory_page | 'parent-directory/child page' | false + :directory_page | 'parent-directory/child-page' | false + :directory_page | 'child page' | false + :directory_page | 'child-page' | false + :directory_page | '/child page' | true + :directory_page | 'parent directory/other' | true + :directory_page | 'parent-directory/other' | true + :directory_page | 'parent-directory / child-page' | true + :directory_page | 'other directory/child page' | true + :directory_page | 'other-directory/child page' | true + + :page_with_special_characters | nil | false + :page_with_special_characters | 'test+page' | false + :page_with_special_characters | 'test-page' | true + :page_with_special_characters | 'test page' | true + end + + with_them do + it 'returns the expected value' do + subject = public_send(page) + subject.title = title if title + + expect(subject.title_changed?).to be(changed) + end + end + end + + describe '#content_changed?' do + context 'with a new page' do + subject { build_wiki_page(container) } + + it 'returns true if content is set' do + subject.attributes[:content] = 'new' + + expect(subject.content_changed?).to be(true) + end + + it 'returns false if content is blank' do + subject.attributes[:content] = ' ' + + expect(subject.content_changed?).to be(false) + end + end + + context 'with an existing page' do + include_context 'when subject is a persisted page' + + it 'returns false' do + expect(subject.content_changed?).to be(false) + end + + it 'returns false if content is set to the same value' do + subject.attributes[:content] = 'test content' + + expect(subject.content_changed?).to be(false) + end + + it 'returns true if content is changed' do + subject.attributes[:content] = 'new' + + expect(subject.content_changed?).to be(true) + end + + it 'returns true if content is changed to a blank string' do + subject.attributes[:content] = ' ' + + expect(subject.content_changed?).to be(true) + end + + it 'returns false if only the newline format has changed from LF to CRLF' do + expect(subject.page).to receive(:text_data).and_return("foo\nbar") + + subject.attributes[:content] = "foo\r\nbar" + + expect(subject.content_changed?).to be(false) + end + + it 'returns false if only the newline format has changed from CRLF to LF' do + expect(subject.page).to receive(:text_data).and_return("foo\r\nbar") + + subject.attributes[:content] = "foo\nbar" + + expect(subject.content_changed?).to be(false) + end + end + end + + describe '#path' do + it 'returns the path when persisted' do + existing_page = create_wiki_page(container, title: 'path test') + + expect(existing_page.path).to eq('path-test.md') + end + + it 'returns nil when not persisted' do + unsaved_page = build_wiki_page(container, title: 'path test') + + expect(unsaved_page.path).to be_nil + end + end + + describe '#directory' do + context 'when the page is at the root directory' do + include_context 'when subject is a persisted page', title: 'directory test' + + it 'returns an empty string' do + expect(subject.directory).to eq('') + end + end + + context 'when the page is inside an actual directory' do + include_context 'when subject is a persisted page', title: 'dir_1/dir_1_1/directory test' + + it 'returns the full directory hierarchy' do + expect(subject.directory).to eq('dir_1/dir_1_1') + end + end + end + + describe '#historical?' do + let!(:container) { create(container_type) } + let(:wiki) { subject.wiki } + let(:old_version) { subject.versions.last.id } + let(:old_page) { wiki.find_page(subject.title, old_version) } + let(:latest_version) { subject.versions.first.id } + let(:latest_page) { wiki.find_page(subject.title, latest_version) } + + subject { create_wiki_page(container) } + + before do + 3.times { |i| subject.update(content: "content #{i}") } + end + + it 'returns true when requesting an old version' do + expect(old_page.historical?).to be_truthy + end + + it 'returns false when requesting latest version' do + expect(latest_page.historical?).to be_falsy + end + + it 'returns false when version is nil' do + expect(latest_page).to receive(:version).and_return(nil) + + expect(latest_page.historical?).to be_falsy + end + + it 'returns false when the last version is nil' do + expect(old_page).to receive(:last_version).and_return(nil) + + expect(old_page.historical?).to be_falsy + end + + it 'returns false when the version is nil' do + expect(old_page).to receive(:version).and_return(nil) + + expect(old_page.historical?).to be_falsy + end + end + + describe '#persisted?' do + it 'returns true for a persisted page' do + expect(create_wiki_page(container)).to be_persisted + end + + it 'returns false for an unpersisted page' do + expect(build_wiki_page(container)).not_to be_persisted + end + end + + describe '#to_partial_path' do + it 'returns the relative path to the partial to be used' do + expect(build_wiki_page(container).to_partial_path).to eq('shared/wikis/wiki_page') + end + end + + describe '#==' do + include_context 'when subject is a persisted page' + + it 'returns true for identical wiki page' do + expect(subject == subject).to be(true) + end + + it 'returns true for updated wiki page' do + subject.update(content: "Updated content") + updated_page = wiki.find_page(subject.slug) + + expect(updated_page).not_to be_nil + 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.container).not_to eq(other_page.container) + expect(subject).not_to eq(other_page) + end + + it 'returns false for page with different slug on same container' do + other_page = create_wiki_page(container) + + expect(subject.slug).not_to eq(other_page.slug) + expect(subject.container).to eq(other_page.container) + expect(subject).not_to eq(other_page) + end + + it 'returns false for page with the same slug on a different container' do + other_page = create(:wiki_page, title: subject.slug) + + expect(subject.slug).to eq(other_page.slug) + expect(subject.container).not_to eq(other_page.container) + expect(subject).not_to eq(other_page) + end + end + + describe '#last_commit_sha' do + include_context 'when subject is a persisted page' + + it 'returns commit sha' do + expect(subject.last_commit_sha).to eq subject.last_version.sha + end + + it 'is changed after page updated' do + last_commit_sha_before_update = subject.last_commit_sha + + subject.update(content: "new content") + page = wiki.find_page(subject.title) + + expect(page.last_commit_sha).not_to eq last_commit_sha_before_update + end + end + + describe '#hook_attrs' do + subject { build_wiki_page(container) } + + it 'includes specific attributes' do + keys = subject.hook_attrs.keys + expect(keys).not_to include(:content) + expect(keys).to include(:version_id) + end + end + + describe '#version_commit_timestamp' do + context 'for a new page' do + it 'returns nil' do + expect(build_wiki_page(container).version_commit_timestamp).to be_nil + end + end + + context 'for page that exists' do + it 'returns the timestamp of the commit' do + existing_page = create_wiki_page(container) + + expect(existing_page.version_commit_timestamp).to eq(existing_page.version.commit.committed_date) + end + end + end + + describe '#diffs' do + include_context 'when subject is a persisted page' + + it 'returns a diff instance' do + diffs = subject.diffs(foo: 'bar') + + expect(diffs).to be_a(Gitlab::Diff::FileCollection::WikiPage) + expect(diffs.diffable).to be_a(Commit) + expect(diffs.diffable.id).to eq(subject.version.id) + expect(diffs.project).to be(subject.wiki) + expect(diffs.diff_options).to include( + expanded: true, + paths: [subject.path], + foo: 'bar' + ) + end + end + + describe "#human_title" do + context "with front matter title" do + let(:front_matter_title) { "abc" } + let(:content_with_front_matter_title) { "---\ntitle: #{front_matter_title}\n---\nHome Page" } + let(:wiki_page) { create(:wiki_page, container: container, content: content_with_front_matter_title) } + + it 'returns the front matter title' do + expect(wiki_page.human_title).to eq front_matter_title + end + end + end +end +# rubocop:enable Rails/SaveBang