a99bf447a2
Cleanup code, and refactor tests that still use Rugged. After this, there should be no Rugged code that access the instance's repositories on non-test environments. There is still some rugged code for other tasks like the repository import task, but since it doesn't access any repository storage path it can stay.
356 lines
11 KiB
Ruby
356 lines
11 KiB
Ruby
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/<filename>.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('foo<em>bar</em>baz')
|
|
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 '<strong>Baz</strong>'
|
|
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 %{<a href="#link-emphasis"><em>text</em></a>}
|
|
|
|
expect(doc.at_css('a[href="#link-strong"]').to_html)
|
|
.to eq %{<a href="#link-strong"><strong>text</strong></a>}
|
|
|
|
expect(doc.at_css('a[href="#link-code"]').to_html)
|
|
.to eq %{<a href="#link-code"><code>text</code></a>}
|
|
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
|