diff options
author | Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> | 2015-04-23 12:08:03 +0300 |
---|---|---|
committer | Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> | 2015-04-23 12:08:03 +0300 |
commit | 71f6143552a47209d4d83c35260db608cac7de1a (patch) | |
tree | b5c16ae980c71adc7af6ef803369d2f0f33d4bb3 /spec | |
parent | 63c5911961909b12b328b4182ba0f4b0e13c1bd6 (diff) | |
parent | aac27550457eaf0503ce9bf7b04c18141ed317af (diff) | |
download | gitlab-ce-71f6143552a47209d4d83c35260db608cac7de1a.tar.gz |
Merge branch 'master' into new-sidebar
Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
Conflicts:
app/controllers/snippets_controller.rb
Diffstat (limited to 'spec')
23 files changed, 1506 insertions, 791 deletions
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 015a66f7fa0..d4cf6540080 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -249,6 +249,16 @@ describe ApplicationHelper do expect(link_to('Example', 'http://example.foo/bar')). to eq '<a href="http://example.foo/bar">Example</a>' end + + it 'should not raise an error when given a bad URI' do + expect { link_to('default', 'if real=1 RANDOM; if real>1 IDLHS; if real>500 LHS') }. + not_to raise_error + end + + it 'should not raise an error when given a bad mailto URL' do + expect { link_to('email', 'mailto://foo.bar@example.es?subject=Subject%20Line') }. + not_to raise_error + end end describe 'markup_render' do diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 944e743675c..64f130e4ae4 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -2,449 +2,27 @@ require 'spec_helper' describe GitlabMarkdownHelper do include ApplicationHelper - include IssuesHelper - - # TODO: Properly test this - def can?(*) - true - end let!(:project) { create(:project) } - let(:empty_project) { create(:empty_project) } let(:user) { create(:user, username: 'gfm') } let(:commit) { project.repository.commit } - let(:earlier_commit){ project.repository.commit("HEAD~2") } let(:issue) { create(:issue, project: project) } let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:snippet) { create(:project_snippet, project: project) } - let(:member) { project.project_members.where(user_id: user).first } # Helper expects a current_user method. let(:current_user) { user } - def url_helper(image_name) - File.join(root_url, 'assets', image_name) - end - before do # Helper expects a @project instance variable @project = project - @ref = 'markdown' - @repository = project.repository - @request.host = Gitlab.config.gitlab.host end describe "#gfm" do - it "should return unaltered text if project is nil" do - actual = "Testing references: ##{issue.iid}" - - expect(gfm(actual)).not_to eq(actual) - - @project = nil - expect(gfm(actual)).to eq(actual) - end - - it "should not alter non-references" do - actual = expected = "_Please_ *stop* 'helping' and all the other b*$#%' you do." - expect(gfm(actual)).to eq(expected) - end - - it "should not touch HTML entities" do - allow(@project.issues).to receive(:where). - with(id: '39').and_return([issue]) - actual = 'We'll accept good pull requests.' - expect(gfm(actual)).to eq("We'll accept good pull requests.") - end - it "should forward HTML options to links" do expect(gfm("Fixed in #{commit.id}", @project, class: 'foo')). - to have_selector('a.gfm.foo') - end - - describe "referencing a commit range" do - let(:expected) { namespace_project_compare_path(project.namespace, project, from: earlier_commit.id, to: commit.id) } - - it "should link using a full id" do - actual = "What happened in #{earlier_commit.id}...#{commit.id}" - expect(gfm(actual)).to match(expected) - end - - it "should link using a short id" do - actual = "What happened in #{earlier_commit.short_id}...#{commit.short_id}" - expected = namespace_project_compare_path(project.namespace, project, from: earlier_commit.short_id, to: commit.short_id) - expect(gfm(actual)).to match(expected) - end - - it "should link inclusively" do - actual = "What happened in #{earlier_commit.id}..#{commit.id}" - expected = namespace_project_compare_path(project.namespace, project, from: "#{earlier_commit.id}^", to: commit.id) - expect(gfm(actual)).to match(expected) - end - - it "should link with adjacent text" do - actual = "(see #{earlier_commit.id}...#{commit.id})" - expect(gfm(actual)).to match(expected) - end - - it "should keep whitespace intact" do - actual = "Changes #{earlier_commit.id}...#{commit.id} dramatically" - expected = /Changes <a.+>#{earlier_commit.id}...#{commit.id}<\/a> dramatically/ - expect(gfm(actual)).to match(expected) - end - - it "should not link with an invalid id" do - actual = expected = "What happened in #{earlier_commit.id.reverse}...#{commit.id.reverse}" - expect(gfm(actual)).to eq(expected) - end - - it "should include a title attribute" do - actual = "What happened in #{earlier_commit.id}...#{commit.id}" - expect(gfm(actual)).to match(/title="Commits #{earlier_commit.id} through #{commit.id}"/) - end - - it "should include standard gfm classes" do - actual = "What happened in #{earlier_commit.id}...#{commit.id}" - expect(gfm(actual)).to match(/class="\s?gfm gfm-commit_range\s?"/) - end - end - - describe "referencing a commit" do - let(:expected) { namespace_project_commit_path(project.namespace, project, commit) } - - it "should link using a full id" do - actual = "Reverts #{commit.id}" - expect(gfm(actual)).to match(expected) - end - - it "should link using a short id" do - actual = "Backported from #{commit.short_id}" - expect(gfm(actual)).to match(expected) - end - - it "should link with adjacent text" do - actual = "Reverted (see #{commit.id})" - expect(gfm(actual)).to match(expected) - end - - it "should keep whitespace intact" do - actual = "Changes #{commit.id} dramatically" - expected = /Changes <a.+>#{commit.id}<\/a> dramatically/ - expect(gfm(actual)).to match(expected) - end - - it "should not link with an invalid id" do - actual = expected = "What happened in #{commit.id.reverse}" - expect(gfm(actual)).to eq(expected) - end - - it "should include a title attribute" do - actual = "Reverts #{commit.id}" - expect(gfm(actual)).to match(/title="#{commit.link_title}"/) - end - - it "should include standard gfm classes" do - actual = "Reverts #{commit.id}" - expect(gfm(actual)).to match(/class="\s?gfm gfm-commit\s?"/) - end - end - - describe "referencing a team member" do - let(:actual) { "@#{user.username} you are right." } - let(:expected) { user_path(user) } - - before do - project.team << [user, :master] - end - - it "should link using a simple name" do - expect(gfm(actual)).to match(expected) - end - - it "should link using a name with dots" do - user.update_attributes(name: "alphA.Beta") - expect(gfm(actual)).to match(expected) - end - - it "should link using name with underscores" do - user.update_attributes(name: "ping_pong_king") - expect(gfm(actual)).to match(expected) - end - - it "should link with adjacent text" do - actual = "Mail the admin (@#{user.username})" - expect(gfm(actual)).to match(expected) - end - - it "should keep whitespace intact" do - actual = "Yes, @#{user.username} is right." - expected = /Yes, <a.+>@#{user.username}<\/a> is right/ - expect(gfm(actual)).to match(expected) - end - - it "should not link with an invalid id" do - actual = expected = "@#{user.username.reverse} you are right." - expect(gfm(actual)).to eq(expected) - end - - it "should include standard gfm classes" do - expect(gfm(actual)).to match(/class="\s?gfm gfm-project_member\s?"/) - end - end - - # Shared examples for referencing an object - # - # Expects the following attributes to be available in the example group: - # - # - object - The object itself - # - reference - The object reference string (e.g., #1234, $1234, !1234) - # - # Currently limited to Snippets, Issues and MergeRequests - shared_examples 'referenced object' do - let(:actual) { "Reference to #{reference}" } - let(:expected) { polymorphic_path([project.namespace, project, object]) } - - it "should link using a valid id" do - expect(gfm(actual)).to match(expected) - end - - it "should link with adjacent text" do - # Wrap the reference in parenthesis - expect(gfm(actual.gsub(reference, "(#{reference})"))).to match(expected) - - # Append some text to the end of the reference - expect(gfm(actual.gsub(reference, "#{reference}, right?"))). - to match(expected) - end - - it "should keep whitespace intact" do - actual = "Referenced #{reference} already." - expected = /Referenced <a.+>[^\s]+<\/a> already/ - expect(gfm(actual)).to match(expected) - end - - it "should not link with an invalid id" do - # Modify the reference string so it's still parsed, but is invalid - reference.gsub!(/^(.)(\d+)$/, '\1' + ('\2' * 2)) - expect(gfm(actual)).to eq(actual) - end - - it "should include a title attribute" do - title = "#{object.class.to_s.titlecase}: #{object.title}" - expect(gfm(actual)).to match(/title="#{title}"/) - end - - it "should include standard gfm classes" do - css = object.class.to_s.underscore - expect(gfm(actual)).to match(/class="\s?gfm gfm-#{css}\s?"/) - end - end - - # Shared examples for referencing an object in a different project - # - # Expects the following attributes to be available in the example group: - # - # - object - The object itself - # - reference - The object reference string (e.g., #1234, $1234, !1234) - # - other_project - The project that owns the target object - # - # Currently limited to Snippets, Issues and MergeRequests - shared_examples 'cross-project referenced object' do - let(:project_path) { @other_project.path_with_namespace } - let(:full_reference) { "#{project_path}#{reference}" } - let(:actual) { "Reference to #{full_reference}" } - let(:expected) do - if object.is_a?(Commit) - namespace_project_commit_path(@other_project.namespace, @other_project, object) - else - polymorphic_path([@other_project.namespace, @other_project, object]) - end - end - - it 'should link using a valid id' do - expect(gfm(actual)).to match( - /#{expected}.*#{Regexp.escape(full_reference)}/ - ) - end - - it 'should link with adjacent text' do - # Wrap the reference in parenthesis - expect(gfm(actual.gsub(full_reference, "(#{full_reference})"))).to( - match(expected) - ) - - # Append some text to the end of the reference - expect(gfm(actual.gsub(full_reference, "#{full_reference}, right?"))). - to(match(expected)) - end - - it 'should keep whitespace intact' do - actual = "Referenced #{full_reference} already." - expected = /Referenced <a.+>[^\s]+<\/a> already/ - expect(gfm(actual)).to match(expected) - end - - it 'should not link with an invalid id' do - # Modify the reference string so it's still parsed, but is invalid - if object.is_a?(Commit) - reference.gsub!(/^(.).+$/, '\1' + '12345abcd') - else - reference.gsub!(/^(.)(\d+)$/, '\1' + ('\2' * 2)) - end - expect(gfm(actual)).to eq(actual) - end - - it 'should include a title attribute' do - if object.is_a?(Commit) - title = object.link_title - else - title = "#{object.class.to_s.titlecase}: #{object.title}" - end - expect(gfm(actual)).to match(/title="#{title}"/) - end - - it 'should include standard gfm classes' do - css = object.class.to_s.underscore - expect(gfm(actual)).to match(/class="\s?gfm gfm-#{css}\s?"/) - end - end - - describe "referencing an issue" do - let(:object) { issue } - let(:reference) { "##{issue.iid}" } - - include_examples 'referenced object' - end - - context 'cross-repo references' do - before(:all) do - @other_project = create(:project, :public) - @commit2 = @other_project.repository.commit - @issue2 = create(:issue, project: @other_project) - @merge_request2 = create(:merge_request, - source_project: @other_project, - target_project: @other_project) - end - - describe 'referencing an issue in another project' do - let(:object) { @issue2 } - let(:reference) { "##{@issue2.iid}" } - - include_examples 'cross-project referenced object' - end - - describe 'referencing an merge request in another project' do - let(:object) { @merge_request2 } - let(:reference) { "!#{@merge_request2.iid}" } - - include_examples 'cross-project referenced object' - end - - describe 'referencing a commit in another project' do - let(:object) { @commit2 } - let(:reference) { "@#{@commit2.id}" } - - include_examples 'cross-project referenced object' - end - end - - describe "referencing a Jira issue" do - let(:actual) { "Reference to JIRA-#{issue.iid}" } - let(:expected) { "http://jira.example/browse/JIRA-#{issue.iid}" } - let(:reference) { "JIRA-#{issue.iid}" } - - before do - jira = @project.create_jira_service if @project.jira_service.nil? - properties = {"title"=>"JIRA tracker", "project_url"=>"http://jira.example/issues/?jql=project=A", "issues_url"=>"http://jira.example/browse/:id", "new_issue_url"=>"http://jira.example/secure/CreateIssue.jspa"} - jira.update_attributes(properties: properties, active: true) - end - - after do - @project.jira_service.destroy! unless @project.jira_service.nil? - end - - it "should link using a valid id" do - expect(gfm(actual)).to match(expected) - end - - it "should link with adjacent text" do - # Wrap the reference in parenthesis - expect(gfm(actual.gsub(reference, "(#{reference})"))).to match(expected) - - # Append some text to the end of the reference - expect(gfm(actual.gsub(reference, "#{reference}, right?"))). - to match(expected) - end - - it "should keep whitespace intact" do - actual = "Referenced #{reference} already." - expected = /Referenced <a.+>[^\s]+<\/a> already/ - expect(gfm(actual)).to match(expected) - end - - it "should not link with an invalid id" do - # Modify the reference string so it's still parsed, but is invalid - invalid_reference = actual.gsub(/(\d+)$/, "r45") - expect(gfm(invalid_reference)).to eq(invalid_reference) - end - - it "should include a title attribute" do - title = "Issue in JIRA tracker" - expect(gfm(actual)).to match(/title="#{title}"/) - end - - it "should include standard gfm classes" do - expect(gfm(actual)).to match(/class="\s?gfm gfm-issue\s?"/) - end - end - - describe "referencing a merge request" do - let(:object) { merge_request } - let(:reference) { "!#{merge_request.iid}" } - - include_examples 'referenced object' - end - - describe "referencing a snippet" do - let(:object) { snippet } - let(:reference) { "$#{snippet.id}" } - let(:actual) { "Reference to #{reference}" } - let(:expected) { namespace_project_snippet_path(project.namespace, project, object) } - - it "should link using a valid id" do - expect(gfm(actual)).to match(expected) - end - - it "should link with adjacent text" do - # Wrap the reference in parenthesis - expect(gfm(actual.gsub(reference, "(#{reference})"))).to match(expected) - - # Append some text to the end of the reference - expect(gfm(actual.gsub(reference, "#{reference}, right?"))).to match(expected) - end - - it "should keep whitespace intact" do - actual = "Referenced #{reference} already." - expected = /Referenced <a.+>[^\s]+<\/a> already/ - expect(gfm(actual)).to match(expected) - end - - it "should not link with an invalid id" do - # Modify the reference string so it's still parsed, but is invalid - reference.gsub!(/^(.)(\d+)$/, '\1' + ('\2' * 2)) - expect(gfm(actual)).to eq(actual) - end - - it "should include a title attribute" do - title = "Snippet: #{object.title}" - expect(gfm(actual)).to match(/title="#{title}"/) - end - - it "should include standard gfm classes" do - css = object.class.to_s.underscore - expect(gfm(actual)).to match(/class="\s?gfm gfm-snippet\s?"/) - end - + to have_selector('a.gfm.foo') end describe "referencing multiple objects" do @@ -466,90 +44,159 @@ describe GitlabMarkdownHelper do end end - describe "emoji" do - it "matches at the start of a string" do - expect(gfm(":+1:")).to match(/<img/) + context 'parse_tasks: true' do + before(:all) do + @source_text_asterisk = <<-EOT.strip_heredoc + * [ ] valid unchecked task + * [x] valid lowercase checked task + * [X] valid uppercase checked task + * [ ] valid unchecked nested task + * [x] valid checked nested task + + [ ] not an unchecked task - no list item + [x] not a checked task - no list item + + * [ ] not an unchecked task - too many spaces + * [x ] not a checked task - too many spaces + * [] not an unchecked task - no spaces + * Not a task [ ] - not at beginning + EOT + + @source_text_dash = <<-EOT.strip_heredoc + - [ ] valid unchecked task + - [x] valid lowercase checked task + - [X] valid uppercase checked task + - [ ] valid unchecked nested task + - [x] valid checked nested task + EOT + end + + it 'should render checkboxes at beginning of asterisk list items' do + rendered_text = markdown(@source_text_asterisk, parse_tasks: true) + + expect(rendered_text).to match(/<input.*checkbox.*valid unchecked task/) + expect(rendered_text).to match( + /<input.*checkbox.*valid lowercase checked task/ + ) + expect(rendered_text).to match( + /<input.*checkbox.*valid uppercase checked task/ + ) end - it "matches at the end of a string" do - expect(gfm("This gets a :-1:")).to match(/<img/) - end + it 'should render checkboxes at beginning of dash list items' do + rendered_text = markdown(@source_text_dash, parse_tasks: true) - it "matches with adjacent text" do - expect(gfm("+1 (:+1:)")).to match(/<img/) + expect(rendered_text).to match(/<input.*checkbox.*valid unchecked task/) + expect(rendered_text).to match( + /<input.*checkbox.*valid lowercase checked task/ + ) + expect(rendered_text).to match( + /<input.*checkbox.*valid uppercase checked task/ + ) end - it "has a title attribute" do - expect(gfm(":-1:")).to match(/title=":-1:"/) - end + it 'should render checkboxes for nested tasks' do + rendered_text = markdown(@source_text_asterisk, parse_tasks: true) - it "has an alt attribute" do - expect(gfm(":-1:")).to match(/alt=":-1:"/) + expect(rendered_text).to match( + /<input.*checkbox.*valid unchecked nested task/ + ) + expect(rendered_text).to match( + /<input.*checkbox.*valid checked nested task/ + ) end - it "has an emoji class" do - expect(gfm(":+1:")).to match('class="emoji"') - end + it 'should not be confused by whitespace before bullets' do + rendered_text_asterisk = markdown(@source_text_asterisk, + parse_tasks: true) + rendered_text_dash = markdown(@source_text_dash, parse_tasks: true) - it "sets height and width" do - actual = gfm(":+1:") - expect(actual).to match(/width="20"/) - expect(actual).to match(/height="20"/) + expect(rendered_text_asterisk).to match( + /<input.*checkbox.*valid unchecked nested task/ + ) + expect(rendered_text_asterisk).to match( + /<input.*checkbox.*valid checked nested task/ + ) + expect(rendered_text_dash).to match( + /<input.*checkbox.*valid unchecked nested task/ + ) + expect(rendered_text_dash).to match( + /<input.*checkbox.*valid checked nested task/ + ) end - it "keeps whitespace intact" do - expect(gfm('This deserves a :+1: big time.')). - to match(/deserves a <img.+> big time/) - end + it 'should not render checkboxes outside of list items' do + rendered_text = markdown(@source_text_asterisk, parse_tasks: true) - it "ignores invalid emoji" do - expect(gfm(":invalid-emoji:")).not_to match(/<img/) + expect(rendered_text).not_to match( + /<input.*checkbox.*not an unchecked task - no list item/ + ) + expect(rendered_text).not_to match( + /<input.*checkbox.*not a checked task - no list item/ + ) end - it "should work independent of reference links (i.e. without @project being set)" do - @project = nil - expect(gfm(":+1:")).to match(/<img/) + it 'should not render checkboxes with invalid formatting' do + rendered_text = markdown(@source_text_asterisk, parse_tasks: true) + + expect(rendered_text).not_to match( + /<input.*checkbox.*not an unchecked task - too many spaces/ + ) + expect(rendered_text).not_to match( + /<input.*checkbox.*not a checked task - too many spaces/ + ) + expect(rendered_text).not_to match( + /<input.*checkbox.*not an unchecked task - no spaces/ + ) + expect(rendered_text).not_to match( + /Not a task.*<input.*checkbox.*not at beginning/ + ) end end end - describe "#link_to_gfm" do + describe '#link_to_gfm' do let(:commit_path) { namespace_project_commit_path(project.namespace, project, commit) } let(:issues) { create_list(:issue, 2, project: project) } - it "should handle references nested in links with all the text" do + it 'should handle references nested in links with all the text' do actual = link_to_gfm("This should finally fix ##{issues[0].iid} and ##{issues[1].iid} for real", commit_path) + doc = Nokogiri::HTML.parse(actual) - # Break the result into groups of links with their content, without - # closing tags - groups = actual.split("</a>") + # Make sure we didn't create invalid markup + expect(doc.errors).to be_empty # Leading commit link - expect(groups[0]).to match(/href="#{commit_path}"/) - expect(groups[0]).to match(/This should finally fix $/) + expect(doc.css('a')[0].attr('href')).to eq commit_path + expect(doc.css('a')[0].text).to eq 'This should finally fix ' # First issue link - expect(groups[1]). - to match(/href="#{namespace_project_issue_path(project.namespace, project, issues[0])}"/) - expect(groups[1]).to match(/##{issues[0].iid}$/) + expect(doc.css('a')[1].attr('href')). + to eq namespace_project_issue_path(project.namespace, project, issues[0]) + expect(doc.css('a')[1].text).to eq "##{issues[0].iid}" # Internal commit link - expect(groups[2]).to match(/href="#{commit_path}"/) - expect(groups[2]).to match(/ and /) + expect(doc.css('a')[2].attr('href')).to eq commit_path + expect(doc.css('a')[2].text).to eq ' and ' # Second issue link - expect(groups[3]). - to match(/href="#{namespace_project_issue_path(project.namespace, project, issues[1])}"/) - expect(groups[3]).to match(/##{issues[1].iid}$/) + expect(doc.css('a')[3].attr('href')). + to eq namespace_project_issue_path(project.namespace, project, issues[1]) + expect(doc.css('a')[3].text).to eq "##{issues[1].iid}" # Trailing commit link - expect(groups[4]).to match(/href="#{commit_path}"/) - expect(groups[4]).to match(/ for real$/) + expect(doc.css('a')[4].attr('href')).to eq commit_path + expect(doc.css('a')[4].text).to eq ' for real' end - it "should forward HTML options" do + it 'should forward HTML options' do actual = link_to_gfm("Fixed in #{commit.id}", commit_path, class: 'foo') - expect(actual).to have_selector 'a.gfm.gfm-commit.foo' + doc = Nokogiri::HTML.parse(actual) + + expect(doc.css('a')).to satisfy do |v| + # 'foo' gets added to all links + v.all? { |a| a.attr('class').match(/foo$/) } + end end it "escapes HTML passed in as the body" do @@ -560,20 +207,7 @@ describe GitlabMarkdownHelper do end describe "#markdown" do - it "should handle references in paragraphs" do - actual = "\n\nLorem ipsum dolor sit amet. #{commit.id} Nam pulvinar sapien eget.\n" - expected = namespace_project_commit_path(project.namespace, project, commit) - expect(markdown(actual)).to match(expected) - end - - it "should handle references in headers" do - actual = "\n# Working around ##{issue.iid}\n## Apply !#{merge_request.iid}" - - expect(markdown(actual, no_header_anchors: true)). - to match(%r{<h1[^<]*>Working around <a.+>##{issue.iid}</a></h1>}) - expect(markdown(actual, no_header_anchors: true)). - to match(%r{<h2[^<]*>Apply <a.+>!#{merge_request.iid}</a></h2>}) - end + # TODO (rspeicher) - This block tests multiple different contexts. Break this up! it "should add ids and links to headers" do # Test every rule except nested tags. @@ -590,35 +224,15 @@ describe GitlabMarkdownHelper do ) end - it "should handle references in lists" do - project.team << [user, :master] - - actual = "\n* dark: ##{issue.iid}\n* light by @#{member.user.username}" - - expect(markdown(actual)). - to match(%r{<li>dark: <a.+>##{issue.iid}</a></li>}) - expect(markdown(actual)). - to match(%r{<li>light by <a.+>@#{member.user.username}</a></li>}) - end - - it "should not link the apostrophe to issue 39" do - project.team << [user, :master] - allow(project.issues). - to receive(:where).with(iid: '39').and_return([issue]) - - actual = "Yes, it is @#{member.user.username}'s task." - expected = /Yes, it is <a.+>@#{member.user.username}<\/a>'s task/ - expect(markdown(actual)).to match(expected) - end + # REFERENCES (PART TWO: THE REVENGE) --------------------------------------- - it "should not link the apostrophe to issue 39 in code blocks" do - project.team << [user, :master] - allow(project.issues). - to receive(:where).with(iid: '39').and_return([issue]) + it "should handle references in headers" do + actual = "\n# Working around ##{issue.iid}\n## Apply !#{merge_request.iid}" - actual = "Yes, `it is @#{member.user.username}'s task.`" - expected = /Yes, <code>it is @gfm\'s task.<\/code>/ - expect(markdown(actual)).to match(expected) + expect(markdown(actual, no_header_anchors: true)). + to match(%r{<h1[^<]*>Working around <a.+>##{issue.iid}</a></h1>}) + expect(markdown(actual, no_header_anchors: true)). + to match(%r{<h2[^<]*>Apply <a.+>!#{merge_request.iid}</a></h2>}) end it "should handle references in <em>" do @@ -628,118 +242,120 @@ describe GitlabMarkdownHelper do to match(%r{Apply <em><a.+>!#{merge_request.iid}</a></em>}) end - it "should handle tables" do - actual = %Q{| header 1 | header 2 | -| -------- | -------- | -| cell 1 | cell 2 | -| cell 3 | cell 4 |} - - expect(markdown(actual)).to match(/\A<table/) - end + # CODE BLOCKS ------------------------------------------------------------- it "should leave code blocks untouched" do + allow(helper).to receive(:current_user).and_return(user) allow(helper).to receive(:user_color_scheme_class).and_return(:white) target_html = "<pre class=\"code highlight white plaintext\"><code>some code from $#{snippet.id}\nhere too\n</code></pre>\n" - expect(helper.markdown("\n some code from $#{snippet.id}\n here too\n")). + expect(markdown("\n some code from $#{snippet.id}\n here too\n")). to eq(target_html) - expect(helper.markdown("\n```\nsome code from $#{snippet.id}\nhere too\n```\n")). + expect(markdown("\n```\nsome code from $#{snippet.id}\nhere too\n```\n")). to eq(target_html) end it "should leave inline code untouched" do - expect(markdown("\nDon't use `$#{snippet.id}` here.\n")).to eq( - "<p>Don't use <code>$#{snippet.id}</code> here.</p>\n" - ) + expect(markdown("Don't use `$#{snippet.id}` here.")). + to eq "<p>Don't use <code>$#{snippet.id}</code> here.</p>\n" end + # REF-LIKE AUTOLINKS? ----------------------------------------------------- + # Basically: Don't parse references inside `<a>` tags. + it "should leave ref-like autolinks untouched" do expect(markdown("look at http://example.tld/#!#{merge_request.iid}")).to eq("<p>look at <a href=\"http://example.tld/#!#{merge_request.iid}\">http://example.tld/#!#{merge_request.iid}</a></p>\n") end it "should leave ref-like href of 'manual' links untouched" do - expect(markdown("why not [inspect !#{merge_request.iid}](http://example.tld/#!#{merge_request.iid})")).to eq("<p>why not <a href=\"http://example.tld/#!#{merge_request.iid}\">inspect </a><a class=\"gfm gfm-merge_request \" href=\"#{namespace_project_merge_request_path(project.namespace, project, merge_request)}\" title=\"Merge Request: #{merge_request.title}\">!#{merge_request.iid}</a><a href=\"http://example.tld/#!#{merge_request.iid}\"></a></p>\n") + expect(markdown("why not [inspect !#{merge_request.iid}](http://example.tld/#!#{merge_request.iid})")).to eq("<p>why not <a href=\"http://example.tld/#!#{merge_request.iid}\">inspect </a><a href=\"#{namespace_project_merge_request_path(project.namespace, project, merge_request)}\" title=\"Merge Request: #{merge_request.title}\" class=\"gfm gfm-merge_request\">!#{merge_request.iid}</a><a href=\"http://example.tld/#!#{merge_request.iid}\"></a></p>\n") end it "should leave ref-like src of images untouched" do expect(markdown("screen shot: ![some image](http://example.tld/#!#{merge_request.iid})")).to eq("<p>screen shot: <img src=\"http://example.tld/#!#{merge_request.iid}\" alt=\"some image\"></p>\n") end - it "should generate absolute urls for refs" do - expect(markdown("##{issue.iid}")).to include(namespace_project_issue_path(project.namespace, project, issue)) - end + # RELATIVE URLS ----------------------------------------------------------- + # TODO (rspeicher): These belong in a relative link filter spec - it "should generate absolute urls for emoji" do - expect(markdown(':smile:')).to( - include(%(src="#{Gitlab.config.gitlab.url}/assets/emoji/#{Emoji.emoji_filename('smile')}.png)) - ) - end + context 'relative links' do + context 'with a valid repository' do + before do + @repository = project.repository + @ref = 'markdown' + end - it "should generate absolute urls for emoji if relative url is present" do - allow(Gitlab.config.gitlab).to receive(:url).and_return('http://localhost/gitlab/root') - expect(markdown(":smile:")).to include("src=\"http://localhost/gitlab/root/assets/emoji/#{Emoji.emoji_filename('smile')}.png") - end + it "should handle relative urls for a file in master" do + actual = "[GitLab API doc](doc/api/README.md)\n" + expected = "<p><a href=\"/#{project.path_with_namespace}/blob/#{@ref}/doc/api/README.md\">GitLab API doc</a></p>\n" + expect(markdown(actual)).to match(expected) + end - it "should generate absolute urls for emoji if asset_host is present" do - allow(Gitlab::Application.config).to receive(:asset_host).and_return("https://cdn.example.com") - ActionView::Base.any_instance.stub_chain(:config, :asset_host).and_return("https://cdn.example.com") - expect(markdown(":smile:")).to include("src=\"https://cdn.example.com/assets/emoji/#{Emoji.emoji_filename('smile')}.png") - end + it "should handle relative urls for a file in master with an anchor" do + actual = "[GitLab API doc](doc/api/README.md#section)\n" + expected = "<p><a href=\"/#{project.path_with_namespace}/blob/#{@ref}/doc/api/README.md#section\">GitLab API doc</a></p>\n" + expect(markdown(actual)).to match(expected) + end + it "should not handle relative urls for the current file with an anchor" do + actual = "[GitLab API doc](#section)\n" + expected = "<p><a href=\"#section\">GitLab API doc</a></p>\n" + expect(markdown(actual)).to match(expected) + end - it "should handle relative urls for a file in master" do - actual = "[GitLab API doc](doc/api/README.md)\n" - expected = "<p><a href=\"/#{project.path_with_namespace}/blob/#{@ref}/doc/api/README.md\">GitLab API doc</a></p>\n" - expect(markdown(actual)).to match(expected) - end + it "should handle relative urls for a directory in master" do + actual = "[GitLab API doc](doc/api)\n" + expected = "<p><a href=\"/#{project.path_with_namespace}/tree/#{@ref}/doc/api\">GitLab API doc</a></p>\n" + expect(markdown(actual)).to match(expected) + end - it "should handle relative urls for a file in master with an anchor" do - actual = "[GitLab API doc](doc/api/README.md#section)\n" - expected = "<p><a href=\"/#{project.path_with_namespace}/blob/#{@ref}/doc/api/README.md#section\">GitLab API doc</a></p>\n" - expect(markdown(actual)).to match(expected) - end + it "should handle absolute urls" do + actual = "[GitLab](https://www.gitlab.com)\n" + expected = "<p><a href=\"https://www.gitlab.com\">GitLab</a></p>\n" + expect(markdown(actual)).to match(expected) + end - it "should not handle relative urls for the current file with an anchor" do - actual = "[GitLab API doc](#section)\n" - expected = "<p><a href=\"#section\">GitLab API doc</a></p>\n" - expect(markdown(actual)).to match(expected) - end + it "should handle relative urls in reference links for a file in master" do + actual = "[GitLab API doc][GitLab readme]\n [GitLab readme]: doc/api/README.md\n" + expected = "<p><a href=\"/#{project.path_with_namespace}/blob/#{@ref}/doc/api/README.md\">GitLab API doc</a></p>\n" + expect(markdown(actual)).to match(expected) + end - it "should handle relative urls for a directory in master" do - actual = "[GitLab API doc](doc/api)\n" - expected = "<p><a href=\"/#{project.path_with_namespace}/tree/#{@ref}/doc/api\">GitLab API doc</a></p>\n" - expect(markdown(actual)).to match(expected) - end + it "should handle relative urls in reference links for a directory in master" do + actual = "[GitLab API doc directory][GitLab readmes]\n [GitLab readmes]: doc/api/\n" + expected = "<p><a href=\"/#{project.path_with_namespace}/tree/#{@ref}/doc/api\">GitLab API doc directory</a></p>\n" + expect(markdown(actual)).to match(expected) + end - it "should handle absolute urls" do - actual = "[GitLab](https://www.gitlab.com)\n" - expected = "<p><a href=\"https://www.gitlab.com\">GitLab</a></p>\n" - expect(markdown(actual)).to match(expected) - end + it "should not handle malformed relative urls in reference links for a file in master" do + actual = "[GitLab readme]: doc/api/README.md\n" + expected = "" + expect(markdown(actual)).to match(expected) + end - it "should handle relative urls in reference links for a file in master" do - actual = "[GitLab API doc][GitLab readme]\n [GitLab readme]: doc/api/README.md\n" - expected = "<p><a href=\"/#{project.path_with_namespace}/blob/#{@ref}/doc/api/README.md\">GitLab API doc</a></p>\n" - expect(markdown(actual)).to match(expected) - end + it 'should allow whitelisted HTML tags from the user' do + actual = '<dl><dt>Term</dt><dd>Definition</dd></dl>' + expect(markdown(actual)).to match(actual) + end + end - it "should handle relative urls in reference links for a directory in master" do - actual = "[GitLab API doc directory][GitLab readmes]\n [GitLab readmes]: doc/api/\n" - expected = "<p><a href=\"/#{project.path_with_namespace}/tree/#{@ref}/doc/api\">GitLab API doc directory</a></p>\n" - expect(markdown(actual)).to match(expected) - end + context 'with an empty repository' do + before do + @project = create(:empty_project) + @repository = @project.repository + end - it "should not handle malformed relative urls in reference links for a file in master" do - actual = "[GitLab readme]: doc/api/README.md\n" - expected = "" - expect(markdown(actual)).to match(expected) + it "should not touch relative urls" do + actual = "[GitLab API doc][GitLab readme]\n [GitLab readme]: doc/api/README.md\n" + expected = "<p><a href=\"doc/api/README.md\">GitLab API doc</a></p>\n" + expect(markdown(actual)).to match(expected) + end + end end - it 'should allow whitelisted HTML tags from the user' do - actual = '<dl><dt>Term</dt><dd>Definition</dd></dl>' - expect(markdown(actual)).to match(actual) - end + # SANITIZATION ------------------------------------------------------------ + # TODO (rspeicher): These are testing SanitizationFilter, not `markdown` it 'should sanitize tags that are not whitelisted' do actual = '<textarea>no inputs allowed</textarea> <blink>no blinks</blink>' @@ -767,20 +383,7 @@ describe GitlabMarkdownHelper do end end - describe 'markdown for empty repository' do - before do - @project = empty_project - @repository = empty_project.repository - end - - it "should not touch relative urls" do - actual = "[GitLab API doc][GitLab readme]\n [GitLab readme]: doc/api/README.md\n" - expected = "<p><a href=\"doc/api/README.md\">GitLab API doc</a></p>\n" - expect(markdown(actual)).to match(expected) - end - end - - describe "#render_wiki_content" do + describe '#render_wiki_content' do before do @wiki = double('WikiPage') allow(@wiki).to receive(:content).and_return('wiki content') @@ -803,114 +406,4 @@ describe GitlabMarkdownHelper do helper.render_wiki_content(@wiki) end end - - describe '#gfm_with_tasks' do - before(:all) do - @source_text_asterisk = <<EOT.gsub(/^\s{8}/, '') - * [ ] valid unchecked task - * [x] valid lowercase checked task - * [X] valid uppercase checked task - * [ ] valid unchecked nested task - * [x] valid checked nested task - - [ ] not an unchecked task - no list item - [x] not a checked task - no list item - - * [ ] not an unchecked task - too many spaces - * [x ] not a checked task - too many spaces - * [] not an unchecked task - no spaces - * Not a task [ ] - not at beginning -EOT - - @source_text_dash = <<EOT.gsub(/^\s{8}/, '') - - [ ] valid unchecked task - - [x] valid lowercase checked task - - [X] valid uppercase checked task - - [ ] valid unchecked nested task - - [x] valid checked nested task -EOT - end - - it 'should render checkboxes at beginning of asterisk list items' do - rendered_text = markdown(@source_text_asterisk, parse_tasks: true) - - expect(rendered_text).to match(/<input.*checkbox.*valid unchecked task/) - expect(rendered_text).to match( - /<input.*checkbox.*valid lowercase checked task/ - ) - expect(rendered_text).to match( - /<input.*checkbox.*valid uppercase checked task/ - ) - end - - it 'should render checkboxes at beginning of dash list items' do - rendered_text = markdown(@source_text_dash, parse_tasks: true) - - expect(rendered_text).to match(/<input.*checkbox.*valid unchecked task/) - expect(rendered_text).to match( - /<input.*checkbox.*valid lowercase checked task/ - ) - expect(rendered_text).to match( - /<input.*checkbox.*valid uppercase checked task/ - ) - end - - it 'should render checkboxes for nested tasks' do - rendered_text = markdown(@source_text_asterisk, parse_tasks: true) - - expect(rendered_text).to match( - /<input.*checkbox.*valid unchecked nested task/ - ) - expect(rendered_text).to match( - /<input.*checkbox.*valid checked nested task/ - ) - end - - it 'should not be confused by whitespace before bullets' do - rendered_text_asterisk = markdown(@source_text_asterisk, - parse_tasks: true) - rendered_text_dash = markdown(@source_text_dash, parse_tasks: true) - - expect(rendered_text_asterisk).to match( - /<input.*checkbox.*valid unchecked nested task/ - ) - expect(rendered_text_asterisk).to match( - /<input.*checkbox.*valid checked nested task/ - ) - expect(rendered_text_dash).to match( - /<input.*checkbox.*valid unchecked nested task/ - ) - expect(rendered_text_dash).to match( - /<input.*checkbox.*valid checked nested task/ - ) - end - - it 'should not render checkboxes outside of list items' do - rendered_text = markdown(@source_text_asterisk, parse_tasks: true) - - expect(rendered_text).not_to match( - /<input.*checkbox.*not an unchecked task - no list item/ - ) - expect(rendered_text).not_to match( - /<input.*checkbox.*not a checked task - no list item/ - ) - end - - it 'should not render checkboxes with invalid formatting' do - rendered_text = markdown(@source_text_asterisk, parse_tasks: true) - - expect(rendered_text).not_to match( - /<input.*checkbox.*not an unchecked task - too many spaces/ - ) - expect(rendered_text).not_to match( - /<input.*checkbox.*not a checked task - too many spaces/ - ) - expect(rendered_text).not_to match( - /<input.*checkbox.*not an unchecked task - no spaces/ - ) - expect(rendered_text).not_to match( - /Not a task.*<input.*checkbox.*not at beginning/ - ) - end - end end diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index 1e64a201942..0b7e3b1d11f 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' describe LabelsHelper do - it { expect(text_color_for_bg('#EEEEEE')).to eq('#333') } - it { expect(text_color_for_bg('#222E2E')).to eq('#FFF') } + it { expect(text_color_for_bg('#EEEEEE')).to eq('#333333') } + it { expect(text_color_for_bg('#222E2E')).to eq('#FFFFFF') } end diff --git a/spec/javascripts/shortcuts_issuable_spec.js.coffee b/spec/javascripts/shortcuts_issuable_spec.js.coffee new file mode 100644 index 00000000000..57dcc2161d3 --- /dev/null +++ b/spec/javascripts/shortcuts_issuable_spec.js.coffee @@ -0,0 +1,83 @@ +#= require jquery +#= require jasmine-fixture + +#= require shortcuts_issuable + +describe 'ShortcutsIssuable', -> + beforeEach -> + @shortcut = new ShortcutsIssuable() + + describe '#replyWithSelectedText', -> + # Stub window.getSelection to return the provided String. + stubSelection = (text) -> + window.getSelection = -> text + + beforeEach -> + @selector = 'form.js-main-target-form textarea#note_note' + affix(@selector) + + describe 'with empty selection', -> + it 'does nothing', -> + stubSelection('') + @shortcut.replyWithSelectedText() + expect($(@selector).val()).toBe('') + + describe 'with any selection', -> + beforeEach -> + stubSelection('Selected text.') + + it 'leaves existing input intact', -> + $(@selector).val('This text was already here.') + expect($(@selector).val()).toBe('This text was already here.') + + @shortcut.replyWithSelectedText() + expect($(@selector).val()). + toBe("This text was already here.\n> Selected text.\n\n") + + it 'triggers `input`', -> + triggered = false + $(@selector).on 'input', -> triggered = true + @shortcut.replyWithSelectedText() + + expect(triggered).toBe(true) + + it 'triggers `focus`', -> + focused = false + $(@selector).on 'focus', -> focused = true + @shortcut.replyWithSelectedText() + + expect(focused).toBe(true) + + describe 'with a one-line selection', -> + it 'quotes the selection', -> + stubSelection('This text has been selected.') + + @shortcut.replyWithSelectedText() + + expect($(@selector).val()). + toBe("> This text has been selected.\n\n") + + describe 'with a multi-line selection', -> + it 'quotes the selected lines as a group', -> + stubSelection( + """ + Selected line one. + + Selected line two. + Selected line three. + + """ + ) + + @shortcut.replyWithSelectedText() + + expect($(@selector).val()). + toBe( + """ + > Selected line one. + > Selected line two. + > Selected line three. + + + """ + ) diff --git a/spec/javascripts/stat_graph_contributors_graph_spec.js b/spec/javascripts/stat_graph_contributors_graph_spec.js index 1090cb7f620..78d39f1b428 100644 --- a/spec/javascripts/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/stat_graph_contributors_graph_spec.js @@ -1,3 +1,5 @@ +//= require stat_graph_contributors_graph + describe("ContributorsGraph", function () { describe("#set_x_domain", function () { it("set the x_domain", function () { diff --git a/spec/javascripts/stat_graph_contributors_util_spec.js b/spec/javascripts/stat_graph_contributors_util_spec.js index 9c1b588861d..ee90892eb48 100644 --- a/spec/javascripts/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/stat_graph_contributors_util_spec.js @@ -1,3 +1,5 @@ +//= require stat_graph_contributors_util + describe("ContributorsStatGraphUtil", function () { describe("#parse_log", function () { diff --git a/spec/javascripts/stat_graph_spec.js b/spec/javascripts/stat_graph_spec.js index b589af34610..4c652910cd6 100644 --- a/spec/javascripts/stat_graph_spec.js +++ b/spec/javascripts/stat_graph_spec.js @@ -1,3 +1,5 @@ +//= require stat_graph + describe("StatGraph", function () { describe("#get_log", function () { diff --git a/spec/javascripts/support/jasmine.yml b/spec/javascripts/support/jasmine.yml index 9bfa261a356..f4b01c9f27a 100644 --- a/spec/javascripts/support/jasmine.yml +++ b/spec/javascripts/support/jasmine.yml @@ -1,76 +1,20 @@ -# src_files +# path to parent directory of spec_files +# relative path from Rails.root # -# Return an array of filepaths relative to src_dir to include before jasmine specs. -# Default: [] +# Alternatively accept an array of directory to include external spec files +# spec_dir: +# - spec/javascripts +# - ../engine/spec/javascripts # -# EXAMPLE: -# -# src_files: -# - lib/source1.js -# - lib/source2.js -# - dist/**/*.js -# -src_files: - - assets/application.js - -# stylesheets -# -# Return an array of stylesheet filepaths relative to src_dir to include before jasmine specs. -# Default: [] -# -# EXAMPLE: -# -# stylesheets: -# - css/style.css -# - stylesheets/*.css -# -stylesheets: - - stylesheets/**/*.css +# defaults to spec/javascripts +spec_dir: spec/javascripts -# helpers -# -# Return an array of filepaths relative to spec_dir to include before jasmine specs. -# Default: ["helpers/**/*.js"] -# -# EXAMPLE: -# -# helpers: -# - helpers/**/*.js -# +# list of file expressions to include as helpers into spec runner +# relative path from spec_dir helpers: - - helpers/**/*.js + - "helpers/**/*.{js.coffee,js,coffee}" -# spec_files -# -# Return an array of filepaths relative to spec_dir to include. -# Default: ["**/*[sS]pec.js"] -# -# EXAMPLE: -# -# spec_files: -# - **/*[sS]pec.js -# +# list of file expressions to include as specs into spec runner +# relative path from spec_dir spec_files: - - '**/*[sS]pec.js' - -# src_dir -# -# Source directory path. Your src_files must be returned relative to this path. Will use root if left blank. -# Default: project root -# -# EXAMPLE: -# -# src_dir: public -# -src_dir: - -# spec_dir -# -# Spec directory path. Your spec_files must be returned relative to this path. -# Default: spec/javascripts -# -# EXAMPLE: -# -# spec_dir: spec/javascripts -# -spec_dir: spec/javascripts + - "**/*[Ss]pec.{js.coffee,js,coffee}" diff --git a/spec/javascripts/support/jasmine_helper.rb b/spec/javascripts/support/jasmine_helper.rb index b4919802afe..4d73aec5a31 100644 --- a/spec/javascripts/support/jasmine_helper.rb +++ b/spec/javascripts/support/jasmine_helper.rb @@ -8,4 +8,8 @@ # config.boot_files = lambda { ['/absolute/path/to/boot_dir/file.js'] } #end # - +#Example: prevent PhantomJS auto install, uses PhantomJS already on your path. +#Jasmine.configure do |config| +# config.prevent_phantom_js_auto_install = true +#end +# diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb index 1c4503ae0ef..67378328336 100644 --- a/spec/lib/gitlab/google_code_import/importer_spec.rb +++ b/spec/lib/gitlab/google_code_import/importer_spec.rb @@ -57,10 +57,11 @@ describe Gitlab::GoogleCodeImport::Importer do expect(issue.label_names).to include("Type: Enhancement") expect(issue.title).to eq("Scrolling through tasks") expect(issue.state).to eq("closed") - expect(issue.description).to include("schattenpr...") + expect(issue.description).to include("schattenpr\\.\\.\\.") expect(issue.description).to include("November 18, 2009 00:20") - expect(issue.description).to include('I like to scroll through the tasks with my scrollwheel \(like in fluxbox\).') - expect(issue.description).to include('Patch is attached that adds two new mouse\-actions \(next\_taskprev\_task\)') + expect(issue.description).to include("Google Code") + expect(issue.description).to include('I like to scroll through the tasks with my scrollwheel (like in fluxbox).') + expect(issue.description).to include('Patch is attached that adds two new mouse-actions (next_task+prev_task)') expect(issue.description).to include('that can be used for exactly that purpose.') expect(issue.description).to include('all the best!') expect(issue.description).to include('[tint2_task_scrolling.diff](https://storage.googleapis.com/google-code-attachments/tint2/issue-169/comment-0/tint2_task_scrolling.diff)') diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb new file mode 100644 index 00000000000..5ebdc8926e2 --- /dev/null +++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb @@ -0,0 +1,128 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe CommitRangeReferenceFilter do + include ReferenceFilterSpecHelper + + let(:project) { create(:project) } + let(:commit1) { project.repository.commit } + let(:commit2) { project.repository.commit("HEAD~2") } + + it 'requires project context' do + expect { described_class.call('Commit Range 1c002d..d200c1', {}) }. + to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Commit Range #{commit1.id}..#{commit2.id}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + let(:reference) { "#{commit1.id}...#{commit2.id}" } + let(:reference2) { "#{commit1.id}..#{commit2.id}" } + + it 'links to a valid two-dot reference' do + doc = filter("See #{reference2}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_compare_url(project.namespace, project, from: "#{commit1.id}^", to: commit2.id) + end + + it 'links to a valid three-dot reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id) + end + + it 'links to a valid short ID' do + reference = "#{commit1.short_id}...#{commit2.id}" + reference2 = "#{commit1.id}...#{commit2.short_id}" + + expect(filter("See #{reference}").css('a').first.text).to eq reference + expect(filter("See #{reference2}").css('a').first.text).to eq reference2 + end + + it 'links with adjacent text' do + doc = filter("See (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs' do + exp = act = "See #{commit1.id.reverse}...#{commit2.id}" + + expect(project).to receive(:valid_repo?).and_return(true) + expect(project.repository).to receive(:commit).with(commit1.id.reverse) + expect(filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = filter("See #{reference}") + expect(doc.css('a').first.attr('title')).to eq "Commits #{commit1.id} through #{commit2.id}" + end + + it 'includes default classes' do + doc = filter("See #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range' + end + + it 'includes an optional custom class' do + doc = filter("See #{reference}", reference_class: 'custom') + expect(doc.css('a').first.attr('class')).to include 'custom' + end + + it 'supports an :only_path option' do + doc = filter("See #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id, only_path: true) + end + end + + context 'cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, namespace: namespace) } + let(:commit1) { project.repository.commit } + let(:commit2) { project.repository.commit("HEAD~2") } + let(:reference) { "#{project2.path_with_namespace}@#{commit1.id}...#{commit2.id}" } + + context 'when user can access reference' do + before { allow_cross_reference! } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: commit2.id) + end + + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Fixed #{project2.path_with_namespace}##{commit1.id.reverse}...#{commit2.id}" + expect(filter(act).to_html).to eq exp + + exp = act = "Fixed #{project2.path_with_namespace}##{commit1.id}...#{commit2.id.reverse}" + expect(filter(act).to_html).to eq exp + end + end + + context 'when user cannot access reference' do + before { disallow_cross_reference! } + + it 'ignores valid references' do + exp = act = "See #{reference}" + + expect(filter(act).to_html).to eq exp + end + end + end + end +end diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb new file mode 100644 index 00000000000..71fd2db2c58 --- /dev/null +++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb @@ -0,0 +1,118 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe CommitReferenceFilter do + include ReferenceFilterSpecHelper + + let(:project) { create(:project) } + let(:commit) { project.repository.commit } + + it 'requires project context' do + expect { described_class.call('Commit 1c002d', {}) }. + to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Commit #{commit.id}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + let(:reference) { commit.id } + + # Let's test a variety of commit SHA sizes just to be paranoid + [6, 8, 12, 18, 20, 32, 40].each do |size| + it "links to a valid reference of #{size} characters" do + doc = filter("See #{reference[0...size]}") + + expect(doc.css('a').first.text).to eq reference[0...size] + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_commit_url(project.namespace, project, reference) + end + end + + it 'links with adjacent text' do + doc = filter("See (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs' do + exp = act = "See #{reference.reverse}" + + expect(project).to receive(:valid_repo?).and_return(true) + expect(project.repository).to receive(:commit).with(reference.reverse) + expect(filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = filter("See #{reference}") + expect(doc.css('a').first.attr('title')).to eq commit.link_title + end + + it 'escapes the title attribute' do + allow_any_instance_of(Commit).to receive(:title).and_return(%{"></a>whatever<a title="}) + + doc = filter("See #{reference}") + expect(doc.text).to eq "See #{commit.id}" + end + + it 'includes default classes' do + doc = filter("See #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit' + end + + it 'includes an optional custom class' do + doc = filter("See #{reference}", reference_class: 'custom') + expect(doc.css('a').first.attr('class')).to include 'custom' + end + + it 'supports an :only_path context' do + doc = filter("See #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.namespace_project_commit_url(project.namespace, project, reference, only_path: true) + end + end + + context 'cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, namespace: namespace) } + let(:commit) { project.repository.commit } + let(:reference) { "#{project2.path_with_namespace}@#{commit.id}" } + + context 'when user can access reference' do + before { allow_cross_reference! } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id) + end + + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Committed #{project2.path_with_namespace}##{commit.id.reverse}" + expect(filter(act).to_html).to eq exp + end + end + + context 'when user cannot access reference' do + before { disallow_cross_reference! } + + it 'ignores valid references' do + exp = act = "See #{reference}" + + expect(filter(act).to_html).to eq exp + end + end + end + end +end diff --git a/spec/lib/gitlab/markdown/cross_project_reference_spec.rb b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb new file mode 100644 index 00000000000..4698d6138c2 --- /dev/null +++ b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe CrossProjectReference do + # context in the html-pipeline sense, not in the rspec sense + let(:context) do + { + current_user: double('user'), + project: double('project') + } + end + + include described_class + + describe '#project_from_ref' do + context 'when no project was referenced' do + it 'returns the project from context' do + expect(project_from_ref(nil)).to eq context[:project] + end + end + + context 'when referenced project does not exist' do + it 'returns nil' do + expect(project_from_ref('invalid/reference')).to be_nil + end + end + + context 'when referenced project exists' do + let(:project2) { double('referenced project') } + + before do + expect(Project).to receive(:find_with_namespace). + with('cross/reference').and_return(project2) + end + + context 'and the user has permission to read it' do + it 'returns the referenced project' do + expect(self).to receive(:user_can_reference_project?). + with(project2).and_return(true) + + expect(project_from_ref('cross/reference')).to eq project2 + end + end + + context 'and the user does not have permission to read it' do + it 'returns nil' do + expect(self).to receive(:user_can_reference_project?). + with(project2).and_return(false) + + expect(project_from_ref('cross/reference')).to be_nil + end + end + end + end + end +end diff --git a/spec/lib/gitlab/markdown/emoji_filter_spec.rb b/spec/lib/gitlab/markdown/emoji_filter_spec.rb new file mode 100644 index 00000000000..18d55c4818f --- /dev/null +++ b/spec/lib/gitlab/markdown/emoji_filter_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe EmojiFilter do + def filter(html, contexts = {}) + described_class.call(html, contexts) + end + + before do + ActionController::Base.asset_host = 'https://foo.com' + end + + it 'replaces supported emoji' do + doc = filter('<p>:heart:</p>') + expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/2764.png' + end + + it 'ignores unsupported emoji' do + exp = act = '<p>:foo:</p>' + doc = filter(act) + expect(doc.to_html).to match Regexp.escape(exp) + end + + it 'correctly encodes the URL' do + doc = filter('<p>:+1:</p>') + expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/1F44D.png' + end + + it 'matches at the start of a string' do + doc = filter(':+1:') + expect(doc.css('img').size).to eq 1 + end + + it 'matches at the end of a string' do + doc = filter('This gets a :-1:') + expect(doc.css('img').size).to eq 1 + end + + it 'matches with adjacent text' do + doc = filter('+1 (:+1:)') + expect(doc.css('img').size).to eq 1 + end + + it 'matches multiple emoji in a row' do + doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:') + expect(doc.css('img').size).to eq 3 + end + + it 'has a title attribute' do + doc = filter(':-1:') + expect(doc.css('img').first.attr('title')).to eq ':-1:' + end + + it 'has an alt attribute' do + doc = filter(':-1:') + expect(doc.css('img').first.attr('alt')).to eq ':-1:' + end + + it 'has an align attribute' do + doc = filter(':8ball:') + expect(doc.css('img').first.attr('align')).to eq 'absmiddle' + end + + it 'has an emoji class' do + doc = filter(':cat:') + expect(doc.css('img').first.attr('class')).to eq 'emoji' + end + + it 'has height and width attributes' do + doc = filter(':dog:') + img = doc.css('img').first + + expect(img.attr('width')).to eq '20' + expect(img.attr('height')).to eq '20' + end + + it 'keeps whitespace intact' do + doc = filter('This deserves a :+1:, big time.') + + expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/) + end + + it 'uses a custom asset_root context' do + root = Gitlab.config.gitlab.url + 'gitlab/root' + + doc = filter(':smile:', asset_root: root) + expect(doc.css('img').first.attr('src')).to start_with(root) + end + + it 'uses a custom asset_host context' do + ActionController::Base.asset_host = 'https://cdn.example.com' + + doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?') + expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com') + end + end +end diff --git a/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb new file mode 100644 index 00000000000..27e930ef7da --- /dev/null +++ b/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe ExternalIssueReferenceFilter do + include ReferenceFilterSpecHelper + + def helper + IssuesHelper + end + + let(:project) { create(:empty_project) } + let(:issue) { double('issue', iid: 123) } + + context 'JIRA issue references' do + let(:reference) { "JIRA-#{issue.iid}" } + + before do + jira = project.create_jira_service + + props = { + 'title' => 'JIRA tracker', + 'project_url' => 'http://jira.example/issues/?jql=project=A', + 'issues_url' => 'http://jira.example/browse/:id', + 'new_issue_url' => 'http://jira.example/secure/CreateIssue.jspa' + } + + jira.update_attributes(properties: props, active: true) + end + + after do + project.jira_service.destroy + end + + it 'requires project context' do + expect { described_class.call('Issue JIRA-123', {}) }. + to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Issue JIRA-#{issue.iid}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + + it 'ignores valid references when using default tracker' do + expect(project).to receive(:default_issues_tracker?).and_return(true) + + exp = act = "Issue #{reference}" + expect(filter(act).to_html).to eq exp + end + + %w(pre code a style).each do |elem| + it "ignores references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Issue #{reference}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + + it 'links to a valid reference' do + doc = filter("Issue #{reference}") + expect(doc.css('a').first.attr('href')) + .to eq helper.url_for_issue(reference, project) + end + + it 'links to the external tracker' do + doc = filter("Issue #{reference}") + link = doc.css('a').first.attr('href') + + expect(link).to eq "http://jira.example/browse/#{reference}" + end + + it 'links with adjacent text' do + doc = filter("Issue (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/) + end + + it 'includes a title attribute' do + doc = filter("Issue #{reference}") + expect(doc.css('a').first.attr('title')).to eq "Issue in JIRA tracker" + end + + it 'escapes the title attribute' do + allow(project.external_issue_tracker).to receive(:title). + and_return(%{"></a>whatever<a title="}) + + doc = filter("Issue #{reference}") + expect(doc.text).to eq "Issue #{reference}" + end + + it 'includes default classes' do + doc = filter("Issue #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue' + end + + it 'includes an optional custom class' do + doc = filter("Issue #{reference}", reference_class: 'custom') + expect(doc.css('a').first.attr('class')).to include 'custom' + end + + it 'supports an :only_path context' do + doc = filter("Issue #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).to eq helper.url_for_issue("#{reference}", project, only_path: true) + end + end + end +end diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb new file mode 100644 index 00000000000..f95b37d6954 --- /dev/null +++ b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb @@ -0,0 +1,133 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe IssueReferenceFilter do + include ReferenceFilterSpecHelper + + def helper + IssuesHelper + end + + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + + it 'requires project context' do + expect { described_class.call('Issue #123', {}) }. + to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Issue ##{issue.iid}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + let(:reference) { "##{issue.iid}" } + + it 'ignores valid references when using non-default tracker' do + expect(project).to receive(:issue_exists?).with(issue.iid).and_return(false) + + exp = act = "Issue ##{issue.iid}" + expect(filter(act).to_html).to eq exp + end + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq helper.url_for_issue(issue.iid, project) + end + + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid issue IDs' do + exp = act = "Fixed ##{issue.iid + 1}" + + expect(project).to receive(:issue_exists?).with(issue.iid + 1) + expect(filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = filter("Issue #{reference}") + expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}" + end + + it 'escapes the title attribute' do + issue.update_attribute(:title, %{"></a>whatever<a title="}) + + doc = filter("Issue #{reference}") + expect(doc.text).to eq "Issue #{reference}" + end + + it 'includes default classes' do + doc = filter("Issue #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue' + end + + it 'includes an optional custom class' do + doc = filter("Issue #{reference}", reference_class: 'custom') + expect(doc.css('a').first.attr('class')).to include 'custom' + end + + it 'supports an :only_path context' do + doc = filter("Issue #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq helper.url_for_issue(issue.iid, project, only_path: true) + end + end + + context 'cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, namespace: namespace) } + let(:issue) { create(:issue, project: project2) } + let(:reference) { "#{project2.path_with_namespace}##{issue.iid}" } + + context 'when user can access reference' do + before { allow_cross_reference! } + + it 'ignores valid references when cross-reference project uses external tracker' do + expect_any_instance_of(Project).to receive(:issue_exists?). + with(issue.iid).and_return(false) + + exp = act = "Issue ##{issue.iid}" + expect(filter(act).to_html).to eq exp + end + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq helper.url_for_issue(issue.iid, project2) + end + + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid issue IDs on the referenced project' do + exp = act = "Fixed #{project2.path_with_namespace}##{issue.iid + 1}" + + expect(filter(act).to_html).to eq exp + end + end + + context 'when user cannot access reference' do + before { disallow_cross_reference! } + + it 'ignores valid references' do + exp = act = "See #{reference}" + + expect(filter(act).to_html).to eq exp + end + end + end + end +end diff --git a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb new file mode 100644 index 00000000000..c84e568e172 --- /dev/null +++ b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb @@ -0,0 +1,148 @@ +require 'spec_helper' +require 'html/pipeline' + +module Gitlab::Markdown + describe LabelReferenceFilter do + include ReferenceFilterSpecHelper + + let(:project) { create(:empty_project) } + let(:label) { create(:label, project: project) } + let(:reference) { "~#{label.id}" } + + it 'requires project context' do + expect { described_class.call('Label ~123', {}) }. + to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Label #{reference}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + + it 'includes default classes' do + doc = filter("Label #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label' + end + + it 'includes an optional custom class' do + doc = filter("Label #{reference}", reference_class: 'custom') + expect(doc.css('a').first.attr('class')).to include 'custom' + end + + it 'supports an :only_path context' do + doc = filter("Label #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.namespace_project_issues_url(project.namespace, project, label_name: label.name, only_path: true) + end + + describe 'label span element' do + it 'includes default classes' do + doc = filter("Label #{reference}") + expect(doc.css('a span').first.attr('class')).to eq 'label color-label' + end + + it 'includes a style attribute' do + doc = filter("Label #{reference}") + expect(doc.css('a span').first.attr('style')).to match(/\Abackground-color: #\h{6}; color: #\h{6}\z/) + end + end + + context 'Integer-based references' do + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: label.name) + end + + it 'links with adjacent text' do + doc = filter("Label (#{reference}.)") + expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) + end + + it 'ignores invalid label IDs' do + exp = act = "Label ~#{label.id + 1}" + + expect(filter(act).to_html).to eq exp + end + end + + context 'String-based single-word references' do + let(:label) { create(:label, name: 'gfm', project: project) } + let(:reference) { "~#{label.name}" } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.text).to eq 'See gfm' + end + + it 'links with adjacent text' do + doc = filter("Label (#{reference}.)") + expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) + end + + it 'ignores invalid label names' do + exp = act = "Label ~#{label.name.reverse}" + + expect(filter(act).to_html).to eq exp + end + end + + context 'String-based multi-word references in quotes' do + let(:label) { create(:label, name: 'gfm references', project: project) } + + context 'in single quotes' do + let(:reference) { "~'#{label.name}'" } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.text).to eq 'See gfm references' + end + + it 'links with adjacent text' do + doc = filter("Label (#{reference}.)") + expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) + end + + it 'ignores invalid label names' do + exp = act = "Label ~'#{label.name.reverse}'" + + expect(filter(act).to_html).to eq exp + end + end + + context 'in double quotes' do + let(:reference) { %(~"#{label.name}") } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_issues_url(project.namespace, project, label_name: label.name) + expect(doc.text).to eq 'See gfm references' + end + + it 'links with adjacent text' do + doc = filter("Label (#{reference}.)") + expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\))) + end + + it 'ignores invalid label names' do + exp = act = %(Label ~"#{label.name.reverse}") + + expect(filter(act).to_html).to eq exp + end + end + end + end +end diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb new file mode 100644 index 00000000000..0f66442269b --- /dev/null +++ b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb @@ -0,0 +1,114 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe MergeRequestReferenceFilter do + include ReferenceFilterSpecHelper + + let(:project) { create(:project) } + let(:merge) { create(:merge_request, source_project: project) } + + it 'requires project context' do + expect { described_class.call('MergeRequest !123', {}) }. + to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Merge !#{merge.iid}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + let(:reference) { "!#{merge.iid}" } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_merge_request_url(project.namespace, project, merge) + end + + it 'links with adjacent text' do + doc = filter("Merge (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid merge IDs' do + exp = act = "Merge !#{merge.iid + 1}" + + expect(filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = filter("Merge #{reference}") + expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}" + end + + it 'escapes the title attribute' do + merge.update_attribute(:title, %{"></a>whatever<a title="}) + + doc = filter("Merge #{reference}") + expect(doc.text).to eq "Merge #{reference}" + end + + it 'includes default classes' do + doc = filter("Merge #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request' + end + + it 'includes an optional custom class' do + doc = filter("Merge #{reference}", reference_class: 'custom') + expect(doc.css('a').first.attr('class')).to include 'custom' + end + + it 'supports an :only_path context' do + doc = filter("Merge #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.namespace_project_merge_request_url(project.namespace, project, merge, only_path: true) + end + end + + context 'cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:project, namespace: namespace) } + let(:merge) { create(:merge_request, source_project: project2) } + let(:reference) { "#{project2.path_with_namespace}!#{merge.iid}" } + + context 'when user can access reference' do + before { allow_cross_reference! } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_merge_request_url(project2.namespace, + project, merge) + end + + it 'links with adjacent text' do + doc = filter("Merge (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid merge IDs on the referenced project' do + exp = act = "Merge #{project2.path_with_namespace}!#{merge.iid + 1}" + + expect(filter(act).to_html).to eq exp + end + end + + context 'when user cannot access reference' do + before { disallow_cross_reference! } + + it 'ignores valid references' do + exp = act = "See #{reference}" + + expect(filter(act).to_html).to eq exp + end + end + end + end +end diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb new file mode 100644 index 00000000000..79533a90b55 --- /dev/null +++ b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb @@ -0,0 +1,112 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe SnippetReferenceFilter do + include ReferenceFilterSpecHelper + + let(:project) { create(:empty_project) } + let(:snippet) { create(:project_snippet, project: project) } + let(:reference) { "$#{snippet.id}" } + + it 'requires project context' do + expect { described_class.call('Snippet $123', {}) }. + to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Snippet #{reference}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_snippet_url(project.namespace, project, snippet) + end + + it 'links with adjacent text' do + doc = filter("Snippet (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid snippet IDs' do + exp = act = "Snippet $#{snippet.id + 1}" + + expect(filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = filter("Snippet #{reference}") + expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}" + end + + it 'escapes the title attribute' do + snippet.update_attribute(:title, %{"></a>whatever<a title="}) + + doc = filter("Snippet #{reference}") + expect(doc.text).to eq "Snippet #{reference}" + end + + it 'includes default classes' do + doc = filter("Snippet #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet' + end + + it 'includes an optional custom class' do + doc = filter("Snippet #{reference}", reference_class: 'custom') + expect(doc.css('a').first.attr('class')).to include 'custom' + end + + it 'supports an :only_path context' do + doc = filter("Snippet #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.namespace_project_snippet_url(project.namespace, project, snippet, only_path: true) + end + end + + context 'cross-project reference' do + let(:namespace) { create(:namespace, name: 'cross-reference') } + let(:project2) { create(:empty_project, namespace: namespace) } + let(:snippet) { create(:project_snippet, project: project2) } + let(:reference) { "#{project2.path_with_namespace}$#{snippet.id}" } + + context 'when user can access reference' do + before { allow_cross_reference! } + + it 'links to a valid reference' do + doc = filter("See #{reference}") + + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) + end + + it 'links with adjacent text' do + doc = filter("See (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) + end + + it 'ignores invalid snippet IDs on the referenced project' do + exp = act = "See #{project2.path_with_namespace}$#{snippet.id + 1}" + + expect(filter(act).to_html).to eq exp + end + end + + context 'when user cannot access reference' do + before { disallow_cross_reference! } + + it 'ignores valid references' do + exp = act = "See #{reference}" + + expect(filter(act).to_html).to eq exp + end + end + end + end +end diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb new file mode 100644 index 00000000000..a5eb927072e --- /dev/null +++ b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb @@ -0,0 +1,98 @@ +require 'spec_helper' + +module Gitlab::Markdown + describe UserReferenceFilter do + include ReferenceFilterSpecHelper + + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + it 'requires project context' do + expect { described_class.call('Example @mention', {}) }. + to raise_error(ArgumentError, /:project/) + end + + it 'ignores invalid users' do + exp = act = 'Hey @somebody' + expect(filter(act).to_html).to eq(exp) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Hey @#{user.username}</#{elem}>" + expect(filter(act).to_html).to eq exp + end + end + + context 'mentioning a user' do + it 'links to a User' do + doc = filter("Hey @#{user.username}") + expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) + end + + # TODO (rspeicher): This test might be overkill + it 'links to a User with a period' do + user = create(:user, name: 'alphA.Beta') + + doc = filter("Hey @#{user.username}") + expect(doc.css('a').length).to eq 1 + end + + # TODO (rspeicher): This test might be overkill + it 'links to a User with an underscore' do + user = create(:user, name: 'ping_pong_king') + + doc = filter("Hey @#{user.username}") + expect(doc.css('a').length).to eq 1 + end + end + + context 'mentioning a group' do + let(:group) { create(:group) } + let(:user) { create(:user) } + + it 'links to a Group that the current user can read' do + group.add_user(user, Gitlab::Access::DEVELOPER) + + doc = filter("Hey @#{group.name}", current_user: user) + expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) + end + + it 'ignores references to a Group that the current user cannot read' do + doc = filter("Hey @#{group.name}", current_user: user) + expect(doc.to_html).to eq "Hey @#{group.name}" + end + end + + it 'links with adjacent text' do + skip 'TODO (rspeicher): Re-enable when usernames can\'t end in periods.' + doc = filter("Mention me (@#{user.username}.)") + expect(doc.to_html).to match(/\(<a.+>@#{user.username}<\/a>\.\)/) + end + + it 'supports a special @all mention' do + doc = filter("Hey @all") + expect(doc.css('a').length).to eq 1 + expect(doc.css('a').first.attr('href')) + .to eq urls.namespace_project_url(project.namespace, project) + end + + it 'includes default classes' do + doc = filter("Hey @#{user.username}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member' + end + + it 'includes an optional custom class' do + doc = filter("Hey @#{user.username}", reference_class: 'custom') + expect(doc.css('a').first.attr('class')).to include 'custom' + end + + it 'supports an :only_path context' do + doc = filter("Hey @#{user.username}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.user_path(user) + end + end +end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index c9fb62b61ae..6fba140f69d 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -74,7 +74,7 @@ describe Gitlab::ReferenceExtractor do end it 'handles all possible kinds of references' do - accessors = Gitlab::Markdown::TYPES.map { |t| "#{t}s".to_sym } + accessors = described_class::TYPES.map { |t| "#{t}s".to_sym } expect(subject).to respond_to(*accessors) end @@ -106,6 +106,15 @@ describe Gitlab::ReferenceExtractor do expect(subject.merge_requests).to eq([@m1, @m0]) end + it 'accesses valid labels' do + @l0 = create(:label, title: 'one', project: project) + @l1 = create(:label, title: 'two', project: project) + @l2 = create(:label) + + subject.analyze("~#{@l0.id}, ~999, ~#{@l2.id}, ~#{@l1.id}") + expect(subject.labels).to eq([@l0, @l1]) + end + it 'accesses valid snippets' do @s0 = create(:project_snippet, project: project) @s1 = create(:project_snippet, project: project) diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index e6d5545f812..327f3e6d23c 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -110,17 +110,22 @@ describe API::API, api: true do end it 'should return 400 error if name not given' do - post api('/users', admin), email: 'test@example.com', password: 'pass1234' + post api('/users', admin), attributes_for(:user).except(:name) expect(response.status).to eq(400) end it 'should return 400 error if password not given' do - post api('/users', admin), email: 'test@example.com', name: 'test' + post api('/users', admin), attributes_for(:user).except(:password) expect(response.status).to eq(400) end - it "should return 400 error if email not given" do - post api('/users', admin), password: 'pass1234', name: 'test' + it 'should return 400 error if email not given' do + post api('/users', admin), attributes_for(:user).except(:email) + expect(response.status).to eq(400) + end + + it 'should return 400 error if username not given' do + post api('/users', admin), attributes_for(:user).except(:username) expect(response.status).to eq(400) end diff --git a/spec/support/reference_filter_spec_helper.rb b/spec/support/reference_filter_spec_helper.rb new file mode 100644 index 00000000000..bcee5715cad --- /dev/null +++ b/spec/support/reference_filter_spec_helper.rb @@ -0,0 +1,47 @@ +# Common methods and setup for Gitlab::Markdown reference filter specs +# +# Must be included into specs manually +module ReferenceFilterSpecHelper + extend ActiveSupport::Concern + + included do + before { set_default_url_options } + end + + # Allow *_url helpers to work + def set_default_url_options + Rails.application.routes.default_url_options = { + host: 'example.foo' + } + end + + # Shortcut to Rails' auto-generated routes helpers, to avoid including the + # module + def urls + Rails.application.routes.url_helpers + end + + # Perform `call` on the described class + # + # Automatically passes the current `project` value to the context if none is + # provided. + # + # html - String text to pass to the filter's `call` method. + # contexts - Hash context for the filter. (default: {project: project}) + # + # Returns the String text returned by the filter's `call` method. + def filter(html, contexts = {}) + contexts.reverse_merge!(project: project) + described_class.call(html, contexts) + end + + def allow_cross_reference! + allow_any_instance_of(described_class). + to receive(:user_can_reference_project?).and_return(true) + end + + def disallow_cross_reference! + allow_any_instance_of(described_class). + to receive(:user_can_reference_project?).and_return(false) + end +end |