gitlab_markdown_helper_spec.rb 14.6 KB
Newer Older
Riyad Preukschas's avatar
Riyad Preukschas committed
1 2
require "spec_helper"

randx's avatar
randx committed
3
describe GitlabMarkdownHelper do
4
  include ApplicationHelper
5
  include IssuesHelper
6

7
  let!(:project) { create(:project_with_code) }
Robert Speicher's avatar
Robert Speicher committed
8

9
  let(:user)          { create(:user, username: 'gfm') }
10
  let(:commit)        { project.repository.commit }
Robert Speicher's avatar
Robert Speicher committed
11
  let(:issue)         { create(:issue, project: project) }
12
  let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
Andrew8xx8's avatar
Andrew8xx8 committed
13
  let(:snippet)       { create(:project_snippet, project: project) }
Robert Speicher's avatar
Robert Speicher committed
14 15
  let(:member)        { project.users_projects.where(user_id: user).first }

Riyad Preukschas's avatar
Riyad Preukschas committed
16
  before do
Robert Speicher's avatar
Robert Speicher committed
17 18
    # Helper expects a @project instance variable
    @project = project
Riyad Preukschas's avatar
Riyad Preukschas committed
19 20 21
  end

  describe "#gfm" do
Robert Speicher's avatar
Robert Speicher committed
22
    it "should return unaltered text if project is nil" do
23
      actual = "Testing references: ##{issue.iid}"
Robert Speicher's avatar
Robert Speicher committed
24 25 26

      gfm(actual).should_not == actual

27
      @project = nil
Robert Speicher's avatar
Robert Speicher committed
28 29
      gfm(actual).should == actual
    end
30

Robert Speicher's avatar
Robert Speicher committed
31 32 33 34 35 36
    it "should not alter non-references" do
      actual = expected = "_Please_ *stop* 'helping' and all the other b*$#%' you do."
      gfm(actual).should == expected
    end

    it "should not touch HTML entities" do
37
      @project.issues.stub(:where).with(id: '39').and_return([issue])
Robert Speicher's avatar
Robert Speicher committed
38 39 40 41 42 43
      actual = expected = "We'll accept good pull requests."
      gfm(actual).should == expected
    end

    it "should forward HTML options to links" do
      gfm("Fixed in #{commit.id}", class: "foo").should have_selector("a.gfm.foo")
44 45
    end

Riyad Preukschas's avatar
Riyad Preukschas committed
46
    describe "referencing a commit" do
Robert Speicher's avatar
Robert Speicher committed
47 48
      let(:expected) { project_commit_path(project, commit) }

Riyad Preukschas's avatar
Riyad Preukschas committed
49
      it "should link using a full id" do
Robert Speicher's avatar
Robert Speicher committed
50 51
        actual = "Reverts #{commit.id}"
        gfm(actual).should match(expected)
Riyad Preukschas's avatar
Riyad Preukschas committed
52 53 54
      end

      it "should link using a short id" do
Robert Speicher's avatar
Robert Speicher committed
55 56 57 58 59 60 61
        actual = "Backported from #{commit.short_id(6)}"
        gfm(actual).should match(expected)
      end

      it "should link with adjacent text" do
        actual = "Reverted (see #{commit.id})"
        gfm(actual).should match(expected)
Riyad Preukschas's avatar
Riyad Preukschas committed
62 63
      end

Robert Speicher's avatar
Robert Speicher committed
64 65 66 67
      it "should keep whitespace intact" do
        actual   = "Changes #{commit.id} dramatically"
        expected = /Changes <a.+>#{commit.id}<\/a> dramatically/
        gfm(actual).should match(expected)
Riyad Preukschas's avatar
Riyad Preukschas committed
68 69 70
      end

      it "should not link with an invalid id" do
Robert Speicher's avatar
Robert Speicher committed
71 72 73 74 75 76 77 78 79 80 81 82
        actual = expected = "What happened in #{commit.id.reverse}"
        gfm(actual).should == expected
      end

      it "should include a title attribute" do
        actual = "Reverts #{commit.id}"
        gfm(actual).should match(/title="#{commit.link_title}"/)
      end

      it "should include standard gfm classes" do
        actual = "Reverts #{commit.id}"
        gfm(actual).should match(/class="\s?gfm gfm-commit\s?"/)
Riyad Preukschas's avatar
Riyad Preukschas committed
83 84 85 86
      end
    end

    describe "referencing a team member" do
87
      let(:actual)   { "@#{user.username} you are right." }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
88
      let(:expected) { user_path(user) }
Riyad Preukschas's avatar
Riyad Preukschas committed
89

Robert Speicher's avatar
Robert Speicher committed
90
      before do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
91
        project.team << [user, :master]
Riyad Preukschas's avatar
Riyad Preukschas committed
92 93
      end

Robert Speicher's avatar
Robert Speicher committed
94 95 96
      it "should link using a simple name" do
        gfm(actual).should match(expected)
      end
Riyad Preukschas's avatar
Riyad Preukschas committed
97

Robert Speicher's avatar
Robert Speicher committed
98 99 100
      it "should link using a name with dots" do
        user.update_attributes(name: "alphA.Beta")
        gfm(actual).should match(expected)
Riyad Preukschas's avatar
Riyad Preukschas committed
101 102 103
      end

      it "should link using name with underscores" do
Robert Speicher's avatar
Robert Speicher committed
104 105
        user.update_attributes(name: "ping_pong_king")
        gfm(actual).should match(expected)
Riyad Preukschas's avatar
Riyad Preukschas committed
106 107
      end

Robert Speicher's avatar
Robert Speicher committed
108
      it "should link with adjacent text" do
109
        actual = "Mail the admin (@#{user.username})"
Robert Speicher's avatar
Robert Speicher committed
110 111
        gfm(actual).should match(expected)
      end
Riyad Preukschas's avatar
Riyad Preukschas committed
112

Robert Speicher's avatar
Robert Speicher committed
113
      it "should keep whitespace intact" do
114 115
        actual   = "Yes, @#{user.username} is right."
        expected = /Yes, <a.+>@#{user.username}<\/a> is right/
Robert Speicher's avatar
Robert Speicher committed
116
        gfm(actual).should match(expected)
Riyad Preukschas's avatar
Riyad Preukschas committed
117 118
      end

Robert Speicher's avatar
Robert Speicher committed
119
      it "should not link with an invalid id" do
120
        actual = expected = "@#{user.username.reverse} you are right."
Robert Speicher's avatar
Robert Speicher committed
121
        gfm(actual).should == expected
Riyad Preukschas's avatar
Riyad Preukschas committed
122 123
      end

Robert Speicher's avatar
Robert Speicher committed
124 125
      it "should include standard gfm classes" do
        gfm(actual).should match(/class="\s?gfm gfm-team_member\s?"/)
Riyad Preukschas's avatar
Riyad Preukschas committed
126 127 128
      end
    end

Robert Speicher's avatar
Robert Speicher committed
129 130 131 132 133 134 135 136 137 138 139 140 141 142
    # 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, object]) }

      it "should link using a valid id" do
        gfm(actual).should match(expected)
Riyad Preukschas's avatar
Riyad Preukschas committed
143 144
      end

Robert Speicher's avatar
Robert Speicher committed
145 146 147
      it "should link with adjacent text" do
        # Wrap the reference in parenthesis
        gfm(actual.gsub(reference, "(#{reference})")).should match(expected)
Riyad Preukschas's avatar
Riyad Preukschas committed
148

Robert Speicher's avatar
Robert Speicher committed
149 150
        # Append some text to the end of the reference
        gfm(actual.gsub(reference, "#{reference}, right?")).should match(expected)
Riyad Preukschas's avatar
Riyad Preukschas committed
151 152
      end

Robert Speicher's avatar
Robert Speicher committed
153 154 155 156
      it "should keep whitespace intact" do
        actual   = "Referenced #{reference} already."
        expected = /Referenced <a.+>[^\s]+<\/a> already/
        gfm(actual).should match(expected)
Riyad Preukschas's avatar
Riyad Preukschas committed
157 158
      end

Robert Speicher's avatar
Robert Speicher committed
159 160 161 162
      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))
        gfm(actual).should == actual
Riyad Preukschas's avatar
Riyad Preukschas committed
163 164
      end

Robert Speicher's avatar
Robert Speicher committed
165 166 167
      it "should include a title attribute" do
        title = "#{object.class.to_s.titlecase}: #{object.title}"
        gfm(actual).should match(/title="#{title}"/)
Riyad Preukschas's avatar
Riyad Preukschas committed
168 169
      end

Robert Speicher's avatar
Robert Speicher committed
170 171 172
      it "should include standard gfm classes" do
        css = object.class.to_s.underscore
        gfm(actual).should match(/class="\s?gfm gfm-#{css}\s?"/)
Riyad Preukschas's avatar
Riyad Preukschas committed
173
      end
Robert Speicher's avatar
Robert Speicher committed
174
    end
Riyad Preukschas's avatar
Riyad Preukschas committed
175

Robert Speicher's avatar
Robert Speicher committed
176 177
    describe "referencing an issue" do
      let(:object)    { issue }
178
      let(:reference) { "##{issue.iid}" }
Riyad Preukschas's avatar
Riyad Preukschas committed
179

Robert Speicher's avatar
Robert Speicher committed
180 181
      include_examples 'referenced object'
    end
Riyad Preukschas's avatar
Riyad Preukschas committed
182

Robert Speicher's avatar
Robert Speicher committed
183 184
    describe "referencing a merge request" do
      let(:object)    { merge_request }
185
      let(:reference) { "!#{merge_request.iid}" }
Robert Speicher's avatar
Robert Speicher committed
186 187

      include_examples 'referenced object'
Riyad Preukschas's avatar
Riyad Preukschas committed
188 189 190
    end

    describe "referencing a snippet" do
Robert Speicher's avatar
Robert Speicher committed
191 192
      let(:object)    { snippet }
      let(:reference) { "$#{snippet.id}" }
Andrew8xx8's avatar
Andrew8xx8 committed
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
      let(:actual)   { "Reference to #{reference}" }
      let(:expected) { project_snippet_path(project, object) }

      it "should link using a valid id" do
        gfm(actual).should match(expected)
      end

      it "should link with adjacent text" do
        # Wrap the reference in parenthesis
        gfm(actual.gsub(reference, "(#{reference})")).should match(expected)

        # Append some text to the end of the reference
        gfm(actual.gsub(reference, "#{reference}, right?")).should match(expected)
      end

      it "should keep whitespace intact" do
        actual   = "Referenced #{reference} already."
        expected = /Referenced <a.+>[^\s]+<\/a> already/
        gfm(actual).should 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))
        gfm(actual).should == actual
      end

      it "should include a title attribute" do
        title = "Snippet: #{object.title}"
        gfm(actual).should match(/title="#{title}"/)
      end

      it "should include standard gfm classes" do
        css = object.class.to_s.underscore
        gfm(actual).should match(/class="\s?gfm gfm-snippet\s?"/)
      end
Riyad Preukschas's avatar
Riyad Preukschas committed
229

Robert Speicher's avatar
Robert Speicher committed
230
    end
Riyad Preukschas's avatar
Riyad Preukschas committed
231

Robert Speicher's avatar
Robert Speicher committed
232
    describe "referencing multiple objects" do
233
      let(:actual) { "!#{merge_request.iid} -> #{commit.id} -> ##{issue.iid}" }
Robert Speicher's avatar
Robert Speicher committed
234 235 236 237

      it "should link to the merge request" do
        expected = project_merge_request_path(project, merge_request)
        gfm(actual).should match(expected)
Riyad Preukschas's avatar
Riyad Preukschas committed
238 239
      end

Robert Speicher's avatar
Robert Speicher committed
240 241 242
      it "should link to the commit" do
        expected = project_commit_path(project, commit)
        gfm(actual).should match(expected)
Riyad Preukschas's avatar
Riyad Preukschas committed
243 244
      end

Robert Speicher's avatar
Robert Speicher committed
245 246 247
      it "should link to the issue" do
        expected = project_issue_path(project, issue)
        gfm(actual).should match(expected)
Riyad Preukschas's avatar
Riyad Preukschas committed
248 249
      end
    end
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275

    describe "emoji" do
      it "matches at the start of a string" do
        gfm(":+1:").should match(/<img/)
      end

      it "matches at the end of a string" do
        gfm("This gets a :-1:").should match(/<img/)
      end

      it "matches with adjacent text" do
        gfm("+1 (:+1:)").should match(/<img/)
      end

      it "has a title attribute" do
        gfm(":-1:").should match(/title=":-1:"/)
      end

      it "has an alt attribute" do
        gfm(":-1:").should match(/alt=":-1:"/)
      end

      it "has an emoji class" do
        gfm(":+1:").should match('class="emoji"')
      end

276 277 278 279 280 281
      it "sets height and width" do
        actual = gfm(":+1:")
        actual.should match(/width="20"/)
        actual.should match(/height="20"/)
      end

282 283 284 285 286 287 288
      it "keeps whitespace intact" do
        gfm("This deserves a :+1: big time.").should match(/deserves a <img.+\/> big time/)
      end

      it "ignores invalid emoji" do
        gfm(":invalid-emoji:").should_not match(/<img/)
      end
289

Johannes Schleifenbaum's avatar
Johannes Schleifenbaum committed
290
      it "should work independent of reference links (i.e. without @project being set)" do
291 292 293
        @project = nil
        gfm(":+1:").should match(/<img/)
      end
294
    end
Robert Speicher's avatar
Robert Speicher committed
295
  end
Riyad Preukschas's avatar
Riyad Preukschas committed
296

Robert Speicher's avatar
Robert Speicher committed
297 298 299
  describe "#link_to_gfm" do
    let(:commit_path) { project_commit_path(project, commit) }
    let(:issues)      { create_list(:issue, 2, project: project) }
Riyad Preukschas's avatar
Riyad Preukschas committed
300

Robert Speicher's avatar
Robert Speicher committed
301
    it "should handle references nested in links with all the text" do
302
      actual = link_to_gfm("This should finally fix ##{issues[0].iid} and ##{issues[1].iid} for real", commit_path)
Riyad Preukschas's avatar
Riyad Preukschas committed
303

Robert Speicher's avatar
Robert Speicher committed
304 305 306
      # Break the result into groups of links with their content, without
      # closing tags
      groups = actual.split("</a>")
Riyad Preukschas's avatar
Riyad Preukschas committed
307

Robert Speicher's avatar
Robert Speicher committed
308 309 310
      # Leading commit link
      groups[0].should match(/href="#{commit_path}"/)
      groups[0].should match(/This should finally fix $/)
Riyad Preukschas's avatar
Riyad Preukschas committed
311

Robert Speicher's avatar
Robert Speicher committed
312
      # First issue link
313
      groups[1].should match(/href="#{project_issue_url(project, issues[0])}"/)
314
      groups[1].should match(/##{issues[0].iid}$/)
315

Robert Speicher's avatar
Robert Speicher committed
316 317 318
      # Internal commit link
      groups[2].should match(/href="#{commit_path}"/)
      groups[2].should match(/ and /)
319

Robert Speicher's avatar
Robert Speicher committed
320
      # Second issue link
321
      groups[3].should match(/href="#{project_issue_url(project, issues[1])}"/)
322
      groups[3].should match(/##{issues[1].iid}$/)
Robert Speicher's avatar
Robert Speicher committed
323 324 325 326

      # Trailing commit link
      groups[4].should match(/href="#{commit_path}"/)
      groups[4].should match(/ for real$/)
327 328 329
    end

    it "should forward HTML options" do
Robert Speicher's avatar
Robert Speicher committed
330 331
      actual = link_to_gfm("Fixed in #{commit.id}", commit_path, class: 'foo')
      actual.should have_selector 'a.gfm.gfm-commit.foo'
332
    end
333 334

    it "escapes HTML passed in as the body" do
335
      actual = "This is a <h1>test</h1> - see ##{issues[0].iid}"
336 337
      link_to_gfm(actual, commit_path).should match('&lt;h1&gt;test&lt;/h1&gt;')
    end
338
  end
339 340 341

  describe "#markdown" do
    it "should handle references in paragraphs" do
342 343 344
      actual = "\n\nLorem ipsum dolor sit amet. #{commit.id} Nam pulvinar sapien eget.\n"
      expected = project_commit_path(project, commit)
      markdown(actual).should match(expected)
345 346 347
    end

    it "should handle references in headers" do
348
      actual = "\n# Working around ##{issue.iid}\n## Apply !#{merge_request.iid}"
Robert Speicher's avatar
Robert Speicher committed
349

350 351
      markdown(actual).should match(%r{<h1[^<]*>Working around <a.+>##{issue.iid}</a></h1>})
      markdown(actual).should match(%r{<h2[^<]*>Apply <a.+>!#{merge_request.iid}</a></h2>})
352 353 354
    end

    it "should handle references in lists" do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
355
      project.team << [user, :master]
Robert Speicher's avatar
Robert Speicher committed
356

357
      actual = "\n* dark: ##{issue.iid}\n* light by @#{member.user.username}"
Robert Speicher's avatar
Robert Speicher committed
358

359
      markdown(actual).should match(%r{<li>dark: <a.+>##{issue.iid}</a></li>})
360
      markdown(actual).should match(%r{<li>light by <a.+>@#{member.user.username}</a></li>})
361 362 363
    end

    it "should handle references in <em>" do
364
      actual = "Apply _!#{merge_request.iid}_ ASAP"
Robert Speicher's avatar
Robert Speicher committed
365

366
      markdown(actual).should match(%r{Apply <em><a.+>!#{merge_request.iid}</a></em>})
367 368 369
    end

    it "should leave code blocks untouched" do
370
      helper.stub(:user_color_scheme_class).and_return(:white)
371

372
      helper.markdown("\n    some code from $#{snippet.id}\n    here too\n").should include("<div class=\"white\"><div class=\"highlight\"><pre><span class=\"n\">some</span> <span class=\"n\">code</span> <span class=\"n\">from</span> $#{snippet.id}\n<span class=\"n\">here</span> <span class=\"n\">too</span>\n</pre></div></div>")
373

374
      helper.markdown("\n```\nsome code from $#{snippet.id}\nhere too\n```\n").should include("<div class=\"white\"><div class=\"highlight\"><pre><span class=\"n\">some</span> <span class=\"n\">code</span> <span class=\"n\">from</span> $#{snippet.id}\n<span class=\"n\">here</span> <span class=\"n\">too</span>\n</pre></div></div>")
375 376 377
    end

    it "should leave inline code untouched" do
Robert Speicher's avatar
Robert Speicher committed
378
      markdown("\nDon't use `$#{snippet.id}` here.\n").should == "<p>Don&#39;t use <code>$#{snippet.id}</code> here.</p>\n"
379
    end
380

381
    it "should leave ref-like autolinks untouched" do
382
      markdown("look at http://example.tld/#!#{merge_request.iid}").should == "<p>look at <a href=\"http://example.tld/#!#{merge_request.iid}\">http://example.tld/#!#{merge_request.iid}</a></p>\n"
383 384 385
    end

    it "should leave ref-like href of 'manual' links untouched" do
386
      markdown("why not [inspect !#{merge_request.iid}](http://example.tld/#!#{merge_request.iid})").should == "<p>why not <a href=\"http://example.tld/#!#{merge_request.iid}\">inspect </a><a href=\"#{project_merge_request_url(project, merge_request)}\" class=\"gfm gfm-merge_request \" title=\"Merge Request: #{merge_request.title}\">!#{merge_request.iid}</a><a href=\"http://example.tld/#!#{merge_request.iid}\"></a></p>\n"
387 388 389
    end

    it "should leave ref-like src of images untouched" do
390
      markdown("screen shot: ![some image](http://example.tld/#!#{merge_request.iid})").should == "<p>screen shot: <img src=\"http://example.tld/#!#{merge_request.iid}\" alt=\"some image\"></p>\n"
391 392
    end

393
    it "should generate absolute urls for refs" do
394
      markdown("##{issue.iid}").should include(project_issue_url(project, issue))
395
    end
396 397 398 399

    it "should generate absolute urls for emoji" do
      markdown(":smile:").should include("src=\"#{url_to_image("emoji/smile")}")
    end
400
  end
401 402 403 404 405 406 407

  describe "#render_wiki_content" do
    before do
      @wiki = stub('WikiPage')
      @wiki.stub(:content).and_return('wiki content')
    end

Ben Bodenmiller's avatar
Ben Bodenmiller committed
408
    it "should use GitLab Flavored Markdown for markdown files" do
409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424
      @wiki.stub(:format).and_return(:markdown)

      helper.should_receive(:markdown).with('wiki content')

      helper.render_wiki_content(@wiki)
    end

    it "should use the Gollum renderer for all other file types" do
      @wiki.stub(:format).and_return(:rdoc)
      formatted_content_stub = stub('formatted_content')
      formatted_content_stub.should_receive(:html_safe)
      @wiki.stub(:formatted_content).and_return(formatted_content_stub)

      helper.render_wiki_content(@wiki)
    end
  end
Riyad Preukschas's avatar
Riyad Preukschas committed
425
end