BigW Consortium Gitlab

markdown_spec.rb 8.37 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
require 'spec_helper'
require 'erb'

# This feature spec is intended to be a comprehensive exercising of all of
# GitLab's non-standard Markdown parsing and the integration thereof.
#
# These tests should be very high-level. Anything low-level belongs in the specs
# for the corresponding HTML::Pipeline filter or helper method.
#
# The idea is to pass a Markdown document through our entire processing stack.
#
# The process looks like this:
#
#   Raw Markdown
#   -> `markdown` helper
#     -> Redcarpet::Render::GitlabHTML converts Markdown to HTML
#       -> Post-process HTML
18
#         -> `gfm` helper
19
#           -> HTML::Pipeline
20 21
#             -> SanitizationFilter
#             -> Other filters, depending on pipeline
22 23 24 25 26
#           -> `html_safe`
#           -> Template
#
# See the MarkdownFeature class for setup details.

27
describe 'GitLab Markdown', feature: true do
28 29
  include Capybara::Node::Matchers
  include GitlabMarkdownHelper
30
  include MarkdownMatchers
31

32 33 34 35 36 37 38
  # Sometimes it can be useful to see the parsed output of the Markdown document
  # for debugging. Call this method to write the output to
  # `tmp/capybara/<filename>.html`.
  def write_markdown(filename = 'markdown_spec')
    File.open(Rails.root.join("tmp/capybara/#{filename}.html"), 'w') do |file|
      file.puts @html
    end
39 40
  end

41
  def doc(html = @html)
42
    @doc ||= Nokogiri::HTML::DocumentFragment.parse(html)
43 44
  end

45 46 47
  # Shared behavior that all pipelines should exhibit
  shared_examples 'all pipelines' do
    describe 'Redcarpet extensions' do
48
      it 'does not parse emphasis inside of words' do
49
        expect(doc.to_html).not_to match('foo<em>bar</em>baz')
50 51 52
      end

      it 'parses table Markdown' do
53
        aggregate_failures do
54 55 56
          expect(doc).to have_selector('th:contains("Header")')
          expect(doc).to have_selector('th:contains("Row")')
          expect(doc).to have_selector('th:contains("Example")')
57
        end
58 59 60
      end

      it 'allows Markdown in tables' do
61
        expect(doc.at_css('td:contains("Baz")').children.to_html).
62 63 64 65
          to eq '<strong>Baz</strong>'
      end

      it 'parses fenced code blocks' do
66
        aggregate_failures do
Robert Speicher committed
67 68
          expect(doc).to have_selector('pre.code.highlight.js-syntax-highlight.c')
          expect(doc).to have_selector('pre.code.highlight.js-syntax-highlight.python')
69
        end
70 71 72
      end

      it 'parses strikethroughs' do
73
        expect(doc).to have_selector(%{del:contains("and this text doesn't")})
74 75 76
      end

      it 'parses superscript' do
77
        expect(doc).to have_selector('sup', count: 2)
78 79 80 81
      end
    end

    describe 'SanitizationFilter' do
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
      it 'permits b elements' do
        expect(doc).to have_selector('b:contains("b tag")')
      end

      it 'permits em elements' do
        expect(doc).to have_selector('em:contains("em tag")')
      end

      it 'permits code elements' do
        expect(doc).to have_selector('code:contains("code tag")')
      end

      it 'permits kbd elements' do
        expect(doc).to have_selector('kbd:contains("s")')
      end

      it 'permits strike elements' do
        expect(doc).to have_selector('strike:contains(Emoji)')
      end

      it 'permits img elements' do
        expect(doc).to have_selector('img[src*="smile.png"]')
      end

      it 'permits br elements' do
        expect(doc).to have_selector('br')
      end

      it 'permits hr elements' do
        expect(doc).to have_selector('hr')
112 113 114
      end

      it 'permits span elements' do
115
        expect(doc).to have_selector('span:contains("span tag")')
116 117
      end

118
      it 'permits style attribute in th elements' do
119 120 121 122
        aggregate_failures do
          expect(doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center'
          expect(doc.at_css('th:contains("Row")')['style']).to eq 'text-align: right'
          expect(doc.at_css('th:contains("Example")')['style']).to eq 'text-align: left'
123 124
        end
      end
125

126 127
      it 'permits style attribute in td elements' do
        aggregate_failures do
128 129 130 131
          expect(doc.at_css('td:contains("Foo")')['style']).to eq 'text-align: center'
          expect(doc.at_css('td:contains("Bar")')['style']).to eq 'text-align: right'
          expect(doc.at_css('td:contains("Baz")')['style']).to eq 'text-align: left'
        end
132 133 134
      end

      it 'removes `rel` attribute from links' do
135
        expect(doc).not_to have_selector('a[rel="bookmark"]')
136 137 138
      end

      it "removes `href` from `a` elements if it's fishy" do
139
        expect(doc).not_to have_selector('a[href*="javascript"]')
140 141 142 143 144
      end
    end

    describe 'Escaping' do
      it 'escapes non-tag angle brackets' do
145
        table = doc.css('table').last.at_css('tbody')
146 147 148 149
        expect(table.at_xpath('.//tr[1]/td[3]').inner_html).to eq '1 &lt; 3 &amp; 5'
      end
    end

150 151
    describe 'Edge Cases' do
      it 'allows markup inside link elements' do
152 153 154
        aggregate_failures do
          expect(doc.at_css('a[href="#link-emphasis"]').to_html).
            to eq %{<a href="#link-emphasis"><em>text</em></a>}
155

156 157
          expect(doc.at_css('a[href="#link-strong"]').to_html).
            to eq %{<a href="#link-strong"><strong>text</strong></a>}
158

159 160 161
          expect(doc.at_css('a[href="#link-code"]').to_html).
            to eq %{<a href="#link-code"><code>text</code></a>}
        end
162 163 164
      end
    end

165 166
    describe 'ExternalLinkFilter' do
      it 'adds nofollow to external link' do
167
        link = doc.at_css('a:contains("Google")')
Alfredo Sumaran committed
168 169 170 171 172 173
        expect(link.attr('rel')).to include('nofollow')
      end

      it 'adds noreferrer to external link' do
        link = doc.at_css('a:contains("Google")')
        expect(link.attr('rel')).to include('noreferrer')
174 175 176
      end

      it 'ignores internal link' do
177 178
        link = doc.at_css('a:contains("GitLab Root")')
        expect(link.attr('rel')).not_to match 'nofollow'
179 180
      end
    end
181
  end
182

183 184
  before(:all) do
    @feat = MarkdownFeature.new
185

186 187 188
    # `markdown` helper expects a `@project` variable
    @project = @feat.project
  end
189

190 191
  context 'default pipeline' do
    before(:all) do
192 193
      @html = markdown(@feat.raw_markdown)
    end
194

195
    it_behaves_like 'all pipelines'
196

197 198 199
    it 'includes RelativeLinkFilter' do
      expect(doc).to parse_relative_links
    end
200

201 202 203
    it 'includes EmojiFilter' do
      expect(doc).to parse_emoji
    end
204

205 206 207
    it 'includes TableOfContentsFilter' do
      expect(doc).to create_header_links
    end
208

209 210 211
    it 'includes AutolinkFilter' do
      expect(doc).to create_autolinks
    end
212

213 214 215 216 217 218 219 220 221
    it 'includes all reference filters' do
      aggregate_failures do
        expect(doc).to reference_users
        expect(doc).to reference_issues
        expect(doc).to reference_merge_requests
        expect(doc).to reference_snippets
        expect(doc).to reference_commit_ranges
        expect(doc).to reference_commits
        expect(doc).to reference_labels
222
        expect(doc).to reference_milestones
223 224
      end
    end
225

226 227
    it 'includes TaskListFilter' do
      expect(doc).to parse_task_lists
228
    end
229
  end
230

231 232 233 234 235 236 237
  context 'wiki pipeline' do
    before do
      @project_wiki = @feat.project_wiki

      file = Gollum::File.new(@project_wiki.wiki)
      expect(file).to receive(:path).and_return('images/example.jpg')
      expect(@project_wiki).to receive(:find_file).with('images/example.jpg').and_return(file)
238
      allow(@project_wiki).to receive(:wiki_base_path) { '/namespace1/gitlabhq/wikis' }
239 240 241 242 243 244 245 246 247 248 249 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 276 277 278 279 280 281 282

      @html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki })
    end

    it_behaves_like 'all pipelines'

    it 'includes RelativeLinkFilter' do
      expect(doc).not_to parse_relative_links
    end

    it 'includes EmojiFilter' do
      expect(doc).to parse_emoji
    end

    it 'includes TableOfContentsFilter' do
      expect(doc).to create_header_links
    end

    it 'includes AutolinkFilter' do
      expect(doc).to create_autolinks
    end

    it 'includes all reference filters' do
      aggregate_failures do
        expect(doc).to reference_users
        expect(doc).to reference_issues
        expect(doc).to reference_merge_requests
        expect(doc).to reference_snippets
        expect(doc).to reference_commit_ranges
        expect(doc).to reference_commits
        expect(doc).to reference_labels
        expect(doc).to reference_milestones
      end
    end

    it 'includes TaskListFilter' do
      expect(doc).to parse_task_lists
    end

    it 'includes GollumTagsFilter' do
      expect(doc).to parse_gollum_tags
    end
  end

283
  # Fake a `current_user` helper
284 285 286
  def current_user
    @feat.user
  end
287
end