markup_helper_spec.rb 16.2 KB
Newer Older
Jeroen van Baarsen's avatar
Jeroen van Baarsen committed
1
require 'spec_helper'
Riyad Preukschas's avatar
Riyad Preukschas committed
2

3
describe MarkupHelper do
4
  let!(:project) { create(:project, :repository) }
Robert Speicher's avatar
Robert Speicher committed
5

6
  let(:user)          { create(:user, username: 'gfm') }
7
  let(:commit)        { project.commit }
Robert Speicher's avatar
Robert Speicher committed
8
  let(:issue)         { create(:issue, project: project) }
9
  let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
Andrew8xx8's avatar
Andrew8xx8 committed
10
  let(:snippet)       { create(:project_snippet, project: project) }
Robert Speicher's avatar
Robert Speicher committed
11

Riyad Preukschas's avatar
Riyad Preukschas committed
12
  before do
13
    # Ensure the generated reference links aren't redacted
14
    project.add_maintainer(user)
15

Robert Speicher's avatar
Robert Speicher committed
16
    # Helper expects a @project instance variable
17 18 19 20
    helper.instance_variable_set(:@project, project)

    # Stub the `current_user` helper
    allow(helper).to receive(:current_user).and_return(user)
Riyad Preukschas's avatar
Riyad Preukschas committed
21 22
  end

23
  describe "#markdown" do
Robert Speicher's avatar
Robert Speicher committed
24
    describe "referencing multiple objects" do
25
      let(:actual) { "#{merge_request.to_reference} -> #{commit.to_reference} -> #{issue.to_reference}" }
Robert Speicher's avatar
Robert Speicher committed
26

27
      it "links to the merge request" do
28
        expected = urls.project_merge_request_path(project, merge_request)
29
        expect(helper.markdown(actual)).to match(expected)
Riyad Preukschas's avatar
Riyad Preukschas committed
30 31
      end

32
      it "links to the commit" do
33
        expected = urls.project_commit_path(project, commit)
34
        expect(helper.markdown(actual)).to match(expected)
Riyad Preukschas's avatar
Riyad Preukschas committed
35 36
      end

37
      it "links to the issue" do
38
        expected = urls.project_issue_path(project, issue)
39
        expect(helper.markdown(actual)).to match(expected)
Riyad Preukschas's avatar
Riyad Preukschas committed
40 41
      end
    end
42 43 44

    describe "override default project" do
      let(:actual) { issue.to_reference }
45
      let(:second_project) { create(:project, :public) }
46 47
      let(:second_issue) { create(:issue, project: second_project) }

48
      it 'links to the issue' do
49
        expected = urls.project_issue_path(second_project, second_issue)
50 51 52
        expect(markdown(actual, project: second_project)).to match(expected)
      end
    end
Robert Speicher's avatar
Robert Speicher committed
53
  end
Riyad Preukschas's avatar
Riyad Preukschas committed
54

55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
  describe '#markdown_field' do
    let(:attribute) { :title }

    describe 'with already redacted attribute' do
      it 'returns the redacted attribute' do
        commit.redacted_title_html = 'commit title'

        expect(Banzai).not_to receive(:render_field)

        expect(helper.markdown_field(commit, attribute)).to eq('commit title')
      end
    end

    describe 'without redacted attribute' do
      it 'renders the markdown value' do
70
        expect(Banzai).to receive(:render_field).with(commit, attribute, {}).and_call_original
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95

        helper.markdown_field(commit, attribute)
      end
    end
  end

  describe '#link_to_markdown_field' do
    let(:link)    { '/commits/0a1b2c3d' }
    let(:issues)  { create_list(:issue, 2, project: project) }

    it 'handles references nested in links with all the text' do
      allow(commit).to receive(:title).and_return("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real")

      actual = helper.link_to_markdown_field(commit, :title, link)
      doc = Nokogiri::HTML.parse(actual)

      # Make sure we didn't create invalid markup
      expect(doc.errors).to be_empty

      # Leading commit link
      expect(doc.css('a')[0].attr('href')).to eq link
      expect(doc.css('a')[0].text).to eq 'This should finally fix '

      # First issue link
      expect(doc.css('a')[1].attr('href'))
96
        .to eq urls.project_issue_path(project, issues[0])
97 98 99 100 101 102 103 104
      expect(doc.css('a')[1].text).to eq issues[0].to_reference

      # Internal commit link
      expect(doc.css('a')[2].attr('href')).to eq link
      expect(doc.css('a')[2].text).to eq ' and '

      # Second issue link
      expect(doc.css('a')[3].attr('href'))
105
        .to eq urls.project_issue_path(project, issues[1])
106 107 108 109 110 111 112 113 114
      expect(doc.css('a')[3].text).to eq issues[1].to_reference

      # Trailing commit link
      expect(doc.css('a')[4].attr('href')).to eq link
      expect(doc.css('a')[4].text).to eq ' for real'
    end
  end

  describe '#link_to_markdown' do
115
    let(:link)    { '/commits/0a1b2c3d' }
116
    let(:issues)  { create_list(:issue, 2, project: project) }
Riyad Preukschas's avatar
Riyad Preukschas committed
117

118
    it 'handles references nested in links with all the text' do
119
      actual = helper.link_to_markdown("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", link)
120
      doc = Nokogiri::HTML.parse(actual)
Riyad Preukschas's avatar
Riyad Preukschas committed
121

122 123 124
      # Make sure we didn't create invalid markup
      expect(doc.errors).to be_empty

Robert Speicher's avatar
Robert Speicher committed
125
      # Leading commit link
126
      expect(doc.css('a')[0].attr('href')).to eq link
127
      expect(doc.css('a')[0].text).to eq 'This should finally fix '
Riyad Preukschas's avatar
Riyad Preukschas committed
128

Robert Speicher's avatar
Robert Speicher committed
129
      # First issue link
130
      expect(doc.css('a')[1].attr('href'))
131
        .to eq urls.project_issue_path(project, issues[0])
132
      expect(doc.css('a')[1].text).to eq issues[0].to_reference
133

Robert Speicher's avatar
Robert Speicher committed
134
      # Internal commit link
135
      expect(doc.css('a')[2].attr('href')).to eq link
136
      expect(doc.css('a')[2].text).to eq ' and '
137

Robert Speicher's avatar
Robert Speicher committed
138
      # Second issue link
139
      expect(doc.css('a')[3].attr('href'))
140
        .to eq urls.project_issue_path(project, issues[1])
141
      expect(doc.css('a')[3].text).to eq issues[1].to_reference
Robert Speicher's avatar
Robert Speicher committed
142 143

      # Trailing commit link
144
      expect(doc.css('a')[4].attr('href')).to eq link
145
      expect(doc.css('a')[4].text).to eq ' for real'
146 147
    end

148
    it 'forwards HTML options' do
149
      actual = helper.link_to_markdown("Fixed in #{commit.id}", link, class: 'foo')
150 151 152 153 154 155 156 157
      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

158
    it "escapes HTML passed in as the body" do
159
      actual = "This is a <h1>test</h1> - see #{issues[0].to_reference}"
160
      expect(helper.link_to_markdown(actual, link))
161
        .to match('&lt;h1&gt;test&lt;/h1&gt;')
162
    end
163 164 165

    it 'ignores reference links when they are the entire body' do
      text = issues[0].to_reference
166
      act = helper.link_to_markdown(text, '/foo')
167 168
      expect(act).to eq %Q(<a href="/foo">#{issues[0].to_reference}</a>)
    end
SAKATA Sinji's avatar
SAKATA Sinji committed
169

170
    it 'replaces commit message with emoji to link' do
171
      actual = link_to_markdown(':book: Book', '/foo')
172 173
      expect(actual)
        .to eq '<gl-emoji title="open book" data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo"> Book</a>'
SAKATA Sinji's avatar
SAKATA Sinji committed
174
    end
175
  end
176

177 178 179 180 181 182 183 184 185
  describe '#link_to_html' do
    it 'wraps the rendered content in a link' do
      link = '/commits/0a1b2c3d'
      issue = create(:issue, project: project)

      rendered = helper.markdown("This should finally fix #{issue.to_reference} for real", pipeline: :single_line)
      doc = Nokogiri::HTML.parse(rendered)

      expect(doc.css('a')[0].attr('href'))
186
        .to eq urls.project_issue_path(project, issue)
187 188 189 190 191 192 193 194 195 196
      expect(doc.css('a')[0].text).to eq issue.to_reference

      wrapped = helper.link_to_html(rendered, link)
      doc = Nokogiri::HTML.parse(wrapped)

      expect(doc.css('a')[0].attr('href')).to eq link
      expect(doc.css('a')[0].text).to eq 'This should finally fix '
    end
  end

197
  describe '#render_wiki_content' do
198
    before do
skv's avatar
skv committed
199
      @wiki = double('WikiPage')
200
      allow(@wiki).to receive(:content).and_return('wiki content')
201
      allow(@wiki).to receive(:slug).and_return('nested/page')
202
      helper.instance_variable_set(:@project_wiki, @wiki)
203 204
    end

205
    it "uses Wiki pipeline for markdown files" do
206
      allow(@wiki).to receive(:format).and_return(:markdown)
207

208 209
      expect(helper).to receive(:markdown_unsafe).with('wiki content',
        pipeline: :wiki, project: project, project_wiki: @wiki, page_slug: "nested/page",
210
        issuable_state_filter_enabled: true)
211 212 213 214

      helper.render_wiki_content(@wiki)
    end

215 216 217 218 219 220 221 222 223 224 225
    it 'uses Wiki pipeline for markdown files with RedCarpet if feature disabled' do
      stub_feature_flags(commonmark_for_repositories: false)
      allow(@wiki).to receive(:format).and_return(:markdown)

      expect(helper).to receive(:markdown_unsafe).with('wiki content',
        pipeline: :wiki, project: project, project_wiki: @wiki, page_slug: "nested/page",
        issuable_state_filter_enabled: true, markdown_engine: :redcarpet)

      helper.render_wiki_content(@wiki)
    end

226
    it "uses Asciidoctor for asciidoc files" do
227 228
      allow(@wiki).to receive(:format).and_return(:asciidoc)

Douwe Maan's avatar
Douwe Maan committed
229
      expect(helper).to receive(:asciidoc_unsafe).with('wiki content')
230 231 232 233

      helper.render_wiki_content(@wiki)
    end

234
    it "uses the Gollum renderer for all other file types" do
235
      allow(@wiki).to receive(:format).and_return(:rdoc)
skv's avatar
skv committed
236
      formatted_content_stub = double('formatted_content')
237 238
      expect(formatted_content_stub).to receive(:html_safe)
      allow(@wiki).to receive(:formatted_content).and_return(formatted_content_stub)
239 240 241 242

      helper.render_wiki_content(@wiki)
    end
  end
243

Douwe Maan's avatar
Douwe Maan committed
244
  describe 'markup' do
245 246 247 248
    let(:content) { 'Noël' }

    it 'preserves encoding' do
      expect(content.encoding.name).to eq('UTF-8')
Douwe Maan's avatar
Douwe Maan committed
249
      expect(helper.markup('foo.rst', content).encoding.name).to eq('UTF-8')
250 251
    end

252
    it 'delegates to #markdown_unsafe when file name corresponds to Markdown' do
253
      expect(helper).to receive(:gitlab_markdown?).with('foo.md').and_return(true)
Douwe Maan's avatar
Douwe Maan committed
254
      expect(helper).to receive(:markdown_unsafe).and_return('NOEL')
255

Douwe Maan's avatar
Douwe Maan committed
256
      expect(helper.markup('foo.md', content)).to eq('NOEL')
257 258
    end

259
    it 'delegates to #asciidoc_unsafe when file name corresponds to AsciiDoc' do
260
      expect(helper).to receive(:asciidoc?).with('foo.adoc').and_return(true)
Douwe Maan's avatar
Douwe Maan committed
261
      expect(helper).to receive(:asciidoc_unsafe).and_return('NOEL')
262

Douwe Maan's avatar
Douwe Maan committed
263
      expect(helper.markup('foo.adoc', content)).to eq('NOEL')
264
    end
265 266 267 268 269 270 271 272

    it 'uses passed in rendered content' do
      expect(helper).not_to receive(:gitlab_markdown?)
      expect(helper).not_to receive(:markdown_unsafe)

      expect(helper.markup('foo.md', content, rendered: '<p>NOEL</p>')).to eq('<p>NOEL</p>')
    end

273 274 275
    it 'defaults to CommonMark' do
      expect(helper.markup('foo.md', 'x^2')).to include('x^2')
    end
276

277 278
    it 'honors markdown_engine for RedCarpet' do
      expect(helper.markup('foo.md', 'x^2', { markdown_engine: :redcarpet })).to include('x<sup>2</sup>')
279
    end
280 281 282 283 284 285

    it 'uses RedCarpet if feature disabled' do
      stub_feature_flags(commonmark_for_repositories: false)

      expect(helper.markup('foo.md', 'x^2', { markdown_engine: :redcarpet })).to include('x<sup>2</sup>')
    end
286 287
  end

288
  describe '#first_line_in_markdown' do
289 290
    shared_examples_for 'common markdown examples' do
      let(:project_base) { build(:project, :repository) }
291

292 293 294
      it 'displays inline code' do
        object = create_object('Text with `inline code`')
        expected = 'Text with <code>inline code</code>'
295

296 297
        expect(first_line_in_markdown(object, attribute, 100, project: project)).to match(expected)
      end
298

299 300 301
      it 'truncates the text with multiple paragraphs' do
        object = create_object("Paragraph 1\n\nParagraph 2")
        expected = 'Paragraph 1...'
302

303 304
        expect(first_line_in_markdown(object, attribute, 100, project: project)).to match(expected)
      end
305

306 307 308
      it 'displays the first line of a code block' do
        object = create_object("```\nCode block\nwith two lines\n```")
        expected = %r{<pre.+><code><span class="line">Code block\.\.\.</span>\n</code></pre>}
309

310 311
        expect(first_line_in_markdown(object, attribute, 100, project: project)).to match(expected)
      end
312

313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
      it 'truncates a single long line of text' do
        text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars
        object = create_object(text * 4)
        expected = (text * 2).sub(/.{3}/, '...')

        expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(expected)
      end

      it 'preserves a link href when link text is truncated' do
        text = 'The quick brown fox jumped over the lazy dog' # 44 chars
        input = "#{text}#{text}#{text} " # 133 chars
        link_url = 'http://example.com/foo/bar/baz' # 30 chars
        input << link_url
        object = create_object(input)
        expected_link_text = 'http://example...</a>'

        expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(link_url)
        expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(expected_link_text)
      end

      it 'preserves code color scheme' do
        object = create_object("```ruby\ndef test\n  'hello world'\nend\n```")
335
        expected = "<pre class=\"code highlight js-syntax-highlight ruby\">" \
336 337 338 339 340 341
          "<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \
          "</code></pre>"

        expect(first_line_in_markdown(object, attribute, 150, project: project)).to eq(expected)
      end

342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
      context 'when images are allowed' do
        it 'preserves data-src for lazy images' do
          object    = create_object("![ImageTest](/uploads/test.png)")
          image_url = "data-src=\".*/uploads/test.png\""
          text      = first_line_in_markdown(object, attribute, 150, project: project, allow_images: true)

          expect(text).to match(image_url)
          expect(text).to match('<a')
        end
      end

      context 'when images are not allowed' do
        it 'removes any images' do
          object = create_object("![ImageTest](/uploads/test.png)")
          text   = first_line_in_markdown(object, attribute, 150, project: project)
357

358 359 360
          expect(text).not_to match('<img')
          expect(text).not_to match('<a')
        end
361 362 363 364 365 366 367 368
      end

      context 'labels formatting' do
        let(:label_title) { 'this should be ~label_1' }

        def create_and_format_label(project)
          create(:label, title: 'label_1', project: project)
          object = create_object(label_title, project: project)
369

370 371
          first_line_in_markdown(object, attribute, 150, project: project)
        end
372

373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
        it 'preserves style attribute for a label that can be accessed by current_user' do
          project = create(:project, :public)

          expect(create_and_format_label(project)).to match(/span class=.*style=.*/)
        end

        it 'does not style a label that can not be accessed by current_user' do
          project = create(:project, :private)

          expect(create_and_format_label(project)).to eq("<p>#{label_title}</p>")
        end
      end

      it 'truncates Markdown properly' do
        object = create_object("@#{user.username}, can you look at this?\nHello world\n")
        actual = first_line_in_markdown(object, attribute, 100, project: project)

        doc = Nokogiri::HTML.parse(actual)

        # Make sure we didn't create invalid markup
        expect(doc.errors).to be_empty

        # Leading user link
        expect(doc.css('a').length).to eq(1)
        expect(doc.css('a')[0].attr('href')).to eq user_path(user)
        expect(doc.css('a')[0].text).to eq "@#{user.username}"

        expect(doc.content).to eq "@#{user.username}, can you look at this?..."
      end

      it 'truncates Markdown with emoji properly' do
        object = create_object("foo :wink:\nbar :grinning:")
        actual = first_line_in_markdown(object, attribute, 100, project: project)

        doc = Nokogiri::HTML.parse(actual)

        # Make sure we didn't create invalid markup
        # But also account for the 2 errors caused by the unknown `gl-emoji` elements
        expect(doc.errors.length).to eq(2)

        expect(doc.css('gl-emoji').length).to eq(2)
        expect(doc.css('gl-emoji')[0].attr('data-name')).to eq 'wink'
        expect(doc.css('gl-emoji')[1].attr('data-name')).to eq 'grinning'

        expect(doc.content).to eq "foo 😉\nbar 😀"
      end
    end

    context 'when the asked attribute can be redacted' do
      include_examples 'common markdown examples' do
        let(:attribute) { :note }
        def create_object(title, project: project_base)
          build(:note, note: title, project: project)
        end
      end
    end

    context 'when the asked attribute can not be redacted' do
      include_examples 'common markdown examples' do
        let(:attribute) { :body }
        def create_object(title, project: project_base)
          issue = build(:issue, title: title)
          build(:todo, :done, project: project_base, author: user, target: issue)
        end
      end
438
    end
439
  end
440 441 442

  describe '#cross_project_reference' do
    it 'shows the full MR reference' do
443
      expect(helper.cross_project_reference(project, merge_request)).to include(project.full_path)
444 445 446
    end

    it 'shows the full issue reference' do
447
      expect(helper.cross_project_reference(project, issue)).to include(project.full_path)
448 449
    end
  end
450 451 452 453

  def urls
    Gitlab::Routing.url_helpers
  end
Riyad Preukschas's avatar
Riyad Preukschas committed
454
end