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 # -> `gfm` helper # -> HTML::Pipeline # -> SanitizationFilter # -> Other filters, depending on pipeline # -> `html_safe` # -> Template # # See the MarkdownFeature class for setup details. describe 'GitLab Markdown', :aggregate_failures do include Capybara::Node::Matchers include MarkupHelper include MarkdownMatchers # 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/.html`. def write_markdown(filename = 'markdown_spec') File.open(Rails.root.join("tmp/capybara/#{filename}.html"), 'w') do |file| file.puts @html end end def doc(html = @html) @doc ||= Nokogiri::HTML::DocumentFragment.parse(html) end # Shared behavior that all pipelines should exhibit shared_examples 'all pipelines' do it 'includes extensions' do aggregate_failures 'does not parse emphasis inside of words' do expect(doc.to_html).not_to match('foobarbaz') end aggregate_failures 'parses table Markdown' do expect(doc).to have_selector('th:contains("Header")') expect(doc).to have_selector('th:contains("Row")') expect(doc).to have_selector('th:contains("Example")') end aggregate_failures 'allows Markdown in tables' do expect(doc.at_css('td:contains("Baz")').children.to_html) .to eq 'Baz' end aggregate_failures 'parses fenced code blocks' do expect(doc).to have_selector('pre.code.highlight.js-syntax-highlight.c') expect(doc).to have_selector('pre.code.highlight.js-syntax-highlight.python') end aggregate_failures 'parses mermaid code block' do expect(doc).to have_selector('pre[lang=mermaid] > code.js-render-mermaid') end aggregate_failures 'parses strikethroughs' do expect(doc).to have_selector(%{del:contains("and this text doesn't")}) end end it 'includes SanitizationFilter' do aggregate_failures 'permits b elements' do expect(doc).to have_selector('b:contains("b tag")') end aggregate_failures 'permits em elements' do expect(doc).to have_selector('em:contains("em tag")') end aggregate_failures 'permits code elements' do expect(doc).to have_selector('code:contains("code tag")') end aggregate_failures 'permits kbd elements' do expect(doc).to have_selector('kbd:contains("s")') end aggregate_failures 'permits strike elements' do expect(doc).to have_selector('strike:contains(Emoji)') end aggregate_failures 'permits img elements' do expect(doc).to have_selector('img[data-src*="smile.png"]') end aggregate_failures 'permits br elements' do expect(doc).to have_selector('br') end aggregate_failures 'permits hr elements' do expect(doc).to have_selector('hr') end aggregate_failures 'permits span elements' do expect(doc).to have_selector('span:contains("span tag")') end aggregate_failures 'permits details elements' do expect(doc).to have_selector('details:contains("Hiding the details")') end aggregate_failures 'permits summary elements' do expect(doc).to have_selector('details summary:contains("collapsible")') end aggregate_failures 'permits align attribute in th elements' do expect(doc.at_css('th:contains("Header")')['align']).to eq 'center' expect(doc.at_css('th:contains("Row")')['align']).to eq 'right' expect(doc.at_css('th:contains("Example")')['align']).to eq 'left' end aggregate_failures 'permits align attribute in td elements' do expect(doc.at_css('td:contains("Foo")')['align']).to eq 'center' expect(doc.at_css('td:contains("Bar")')['align']).to eq 'right' expect(doc.at_css('td:contains("Baz")')['align']).to eq 'left' end aggregate_failures 'permits superscript elements' do expect(doc).to have_selector('sup', count: 2) end aggregate_failures 'permits subscript elements' do expect(doc).to have_selector('sub', count: 3) end aggregate_failures 'removes `rel` attribute from links' do expect(doc).not_to have_selector('a[rel="bookmark"]') end aggregate_failures "removes `href` from `a` elements if it's fishy" do expect(doc).not_to have_selector('a[href*="javascript"]') end end describe 'Escaping' do it 'escapes non-tag angle brackets' do table = doc.css('table').last.at_css('tbody') expect(table.at_xpath('.//tr[1]/td[3]').inner_html).to eq '1 < 3 & 5' end end describe 'Edge Cases' do it 'allows markup inside link elements' do aggregate_failures do expect(doc.at_css('a[href="#link-emphasis"]').to_html) .to eq %{text} expect(doc.at_css('a[href="#link-strong"]').to_html) .to eq %{text} expect(doc.at_css('a[href="#link-code"]').to_html) .to eq %{text} end end end it 'includes ExternalLinkFilter' do aggregate_failures 'adds nofollow to external link' do link = doc.at_css('a:contains("Google")') expect(link.attr('rel')).to include('nofollow') end aggregate_failures 'adds noreferrer to external link' do link = doc.at_css('a:contains("Google")') expect(link.attr('rel')).to include('noreferrer') end aggregate_failures 'adds _blank to target attribute for external links' do link = doc.at_css('a:contains("Google")') expect(link.attr('target')).to match('_blank') end aggregate_failures 'ignores internal link' do link = doc.at_css('a:contains("GitLab Root")') expect(link.attr('rel')).not_to match 'nofollow' expect(link.attr('target')).not_to match '_blank' end end end before do @feat = MarkdownFeature.new # `markdown` helper expects a `@project` and `@group` variable @project = @feat.project @group = @feat.group end context 'default pipeline' do before do @html = markdown(@feat.raw_markdown) end it_behaves_like 'all pipelines' it 'includes custom filters' do aggregate_failures 'RelativeLinkFilter' do expect(doc).to parse_relative_links end aggregate_failures 'EmojiFilter' do expect(doc).to parse_emoji end aggregate_failures 'TableOfContentsFilter' do expect(doc).to create_header_links end aggregate_failures 'AutolinkFilter' do expect(doc).to create_autolinks end aggregate_failures 'all reference filters' 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 aggregate_failures 'TaskListFilter' do expect(doc).to parse_task_lists end aggregate_failures 'InlineDiffFilter' do expect(doc).to parse_inline_diffs end aggregate_failures 'VideoLinkFilter' do expect(doc).to parse_video_links end aggregate_failures 'ColorFilter' do expect(doc).to parse_colors end end end context 'wiki pipeline' do before do @project_wiki = @feat.project_wiki @project_wiki_page = @feat.project_wiki_page path = 'images/example.jpg' gitaly_wiki_file = Gitlab::GitalyClient::WikiFile.new(path: path) expect(@project_wiki).to receive(:find_file).with(path).and_return(Gitlab::Git::WikiFile.new(gitaly_wiki_file)) allow(@project_wiki).to receive(:wiki_base_path) { '/namespace1/gitlabhq/wikis' } @html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki, page_slug: @project_wiki_page.slug }) end it_behaves_like 'all pipelines' it 'includes custom filters' do aggregate_failures 'RelativeLinkFilter' do expect(doc).not_to parse_relative_links end aggregate_failures 'EmojiFilter' do expect(doc).to parse_emoji end aggregate_failures 'TableOfContentsFilter' do expect(doc).to create_header_links end aggregate_failures 'AutolinkFilter' do expect(doc).to create_autolinks end aggregate_failures 'all reference filters' 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 aggregate_failures 'TaskListFilter' do expect(doc).to parse_task_lists end aggregate_failures 'GollumTagsFilter' do expect(doc).to parse_gollum_tags end aggregate_failures 'InlineDiffFilter' do expect(doc).to parse_inline_diffs end aggregate_failures 'VideoLinkFilter' do expect(doc).to parse_video_links end aggregate_failures 'ColorFilter' do expect(doc).to parse_colors end end end context 'Redcarpet documents' do before do allow_any_instance_of(Banzai::Filter::MarkdownFilter).to receive(:engine).and_return('Redcarpet') @html = markdown(@feat.raw_markdown) end it 'processes certain elements differently' do aggregate_failures 'parses superscript' do expect(doc).to have_selector('sup', count: 3) end aggregate_failures 'permits style attribute in th elements' 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' end aggregate_failures 'permits style attribute in td elements' do 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 end end # Fake a `current_user` helper def current_user @feat.user end end