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_with_options` helper # -> HTML::Pipeline # -> Sanitize # -> RelativeLink # -> Emoji # -> Table of Contents # -> Autolinks # -> Rinku (http, https, ftp) # -> Other schemes # -> ExternalLink # -> References # -> TaskList # -> `html_safe` # -> Template # # See the MarkdownFeature class for setup details. describe 'GitLab Markdown', feature: true do include ActionView::Helpers::TagHelper include ActionView::Helpers::UrlHelper include Capybara::Node::Matchers include GitlabMarkdownHelper # `markdown` calls these two methods def current_user @feat.user end def user_color_scheme_class :white end # Let's only parse this thing once before(:all) do @feat = MarkdownFeature.new # `markdown` expects a `@project` variable @project = @feat.project @md = markdown(@feat.raw_markdown) @doc = Nokogiri::HTML::DocumentFragment.parse(@md) end after(:all) do @feat.teardown end # Given a header ID, goes to that element's parent (the header itself), then # its next sibling element (the body). def get_section(id) @doc.at_css("##{id}").parent.next_element end # Sometimes it can be useful to see the parsed output of the Markdown document # for debugging. Uncomment this block to write the output to # tmp/capybara/markdown_spec.html. # # it 'writes to a file' do # File.open(Rails.root.join('tmp/capybara/markdown_spec.html'), 'w') do |file| # file.puts @md # end # end describe 'Markdown' do describe 'No Intra Emphasis' do it 'does not parse emphasis inside of words' do body = get_section('no-intra-emphasis') expect(body.to_html).not_to match('foobarbaz') end end describe 'Tables' do it 'parses table Markdown' do body = get_section('tables') expect(body).to have_selector('th:contains("Header")') expect(body).to have_selector('th:contains("Row")') expect(body).to have_selector('th:contains("Example")') end it 'allows Markdown in tables' do expect(@doc.at_css('td:contains("Baz")').children.to_html). to eq 'Baz' end end describe 'Fenced Code Blocks' do it 'parses fenced code blocks' do expect(@doc).to have_selector('pre.code.highlight.white.c') expect(@doc).to have_selector('pre.code.highlight.white.python') end end describe 'Strikethrough' do it 'parses strikethroughs' do expect(@doc).to have_selector(%{del:contains("and this text doesn't")}) end end describe 'Superscript' do it 'parses superscript' do body = get_section('superscript') expect(body.to_html).to match('1st') expect(body.to_html).to match('2nd') end end end describe 'HTML::Pipeline' do describe 'SanitizationFilter' do it 'uses a permissive whitelist' do expect(@doc).to have_selector('b:contains("b tag")') expect(@doc).to have_selector('em:contains("em tag")') expect(@doc).to have_selector('code:contains("code tag")') expect(@doc).to have_selector('kbd:contains("s")') expect(@doc).to have_selector('strike:contains(Emoji)') expect(@doc).to have_selector('img[src*="smile.png"]') expect(@doc).to have_selector('br') expect(@doc).to have_selector('hr') end it 'permits span elements' do expect(@doc).to have_selector('span:contains("span tag")') end it 'permits table alignment' 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' 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 it 'removes `rel` attribute from links' do body = get_section('sanitizationfilter') expect(body).not_to have_selector('a[rel="bookmark"]') end it "removes `href` from `a` elements if it's fishy" do expect(@doc).not_to have_selector('a[href*="javascript"]') end end describe 'Escaping' do let(:table) { @doc.css('table').last.at_css('tbody') } it 'escapes non-tag angle brackets' do 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 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 describe 'EmojiFilter' do it 'parses Emoji' do expect(@doc).to have_selector('img.emoji', count: 10) end end describe 'TableOfContentsFilter' do it 'creates anchors inside header elements' do expect(@doc).to have_selector('h1 a#gitlab-markdown') expect(@doc).to have_selector('h2 a#markdown') expect(@doc).to have_selector('h3 a#autolinkfilter') end end describe 'AutolinkFilter' do let(:list) { get_section('autolinkfilter').next_element } def item(index) list.at_css("li:nth-child(#{index})") end it 'autolinks http://' do expect(item(1).children.first.name).to eq 'a' expect(item(1).children.first['href']).to eq 'http://about.gitlab.com/' end it 'autolinks https://' do expect(item(2).children.first.name).to eq 'a' expect(item(2).children.first['href']).to eq 'https://google.com/' end it 'autolinks ftp://' do expect(item(3).children.first.name).to eq 'a' expect(item(3).children.first['href']).to eq 'ftp://ftp.us.debian.org/debian/' end it 'autolinks smb://' do expect(item(4).children.first.name).to eq 'a' expect(item(4).children.first['href']).to eq 'smb://foo/bar/baz' end it 'autolinks irc://' do expect(item(5).children.first.name).to eq 'a' expect(item(5).children.first['href']).to eq 'irc://irc.freenode.net/git' end it 'autolinks short, invalid URLs' do expect(item(6).children.first.name).to eq 'a' expect(item(6).children.first['href']).to eq 'http://localhost:3000' end %w(code a kbd).each do |elem| it "ignores links inside '#{elem}' element" do body = get_section('autolinkfilter') expect(body).not_to have_selector("#{elem} a") end end end describe 'ExternalLinkFilter' do let(:links) { get_section('externallinkfilter').next_element } it 'adds nofollow to external link' do expect(links.css('a').first.to_html).to match 'nofollow' end it 'ignores internal link' do expect(links.css('a').last.to_html).not_to match 'nofollow' end end describe 'ReferenceFilter' do it 'handles references in headers' do header = @doc.at_css('#reference-filters-eg-1').parent expect(header.css('a').size).to eq 2 end it "handles references in Markdown" do body = get_section('reference-filters-eg-1') expect(body).to have_selector('em a.gfm-merge_request', count: 1) end it 'parses user references' do body = get_section('userreferencefilter') expect(body).to have_selector('a.gfm.gfm-project_member', count: 3) end it 'parses issue references' do body = get_section('issuereferencefilter') expect(body).to have_selector('a.gfm.gfm-issue', count: 2) end it 'parses merge request references' do body = get_section('mergerequestreferencefilter') expect(body).to have_selector('a.gfm.gfm-merge_request', count: 2) end it 'parses snippet references' do body = get_section('snippetreferencefilter') expect(body).to have_selector('a.gfm.gfm-snippet', count: 2) end it 'parses commit range references' do body = get_section('commitrangereferencefilter') expect(body).to have_selector('a.gfm.gfm-commit_range', count: 2) end it 'parses commit references' do body = get_section('commitreferencefilter') expect(body).to have_selector('a.gfm.gfm-commit', count: 2) end it 'parses label references' do body = get_section('labelreferencefilter') expect(body).to have_selector('a.gfm.gfm-label', count: 3) end end describe 'Task Lists' do it 'generates task lists' do body = get_section('task-lists') expect(body).to have_selector('ul.task-list', count: 2) expect(body).to have_selector('li.task-list-item', count: 7) expect(body).to have_selector('input[checked]', count: 3) end end end end # This is a helper class used by the GitLab Markdown feature spec # # Because the feature spec only cares about the output of the Markdown, and the # test setup and teardown and parsing is fairly expensive, we only want to do it # once. Unfortunately RSpec will not let you access `let`s in a `before(:all)` # block, so we fake it by encapsulating all the shared setup in this class. # # The class renders `spec/fixtures/markdown.md.erb` using ERB, allowing for # reference to the factory-created objects. class MarkdownFeature include FactoryGirl::Syntax::Methods def initialize DatabaseCleaner.start end def teardown DatabaseCleaner.clean end def user @user ||= create(:user) end def group unless @group @group = create(:group) @group.add_user(user, Gitlab::Access::DEVELOPER) end @group end # Direct references ---------------------------------------------------------- def project @project ||= create(:project) end def issue @issue ||= create(:issue, project: project) end def merge_request @merge_request ||= create(:merge_request, :simple, source_project: project) end def snippet @snippet ||= create(:project_snippet, project: project) end def commit @commit ||= project.commit end def commit_range unless @commit_range commit2 = project.commit('HEAD~3') @commit_range = CommitRange.new("#{commit.id}...#{commit2.id}", project) end @commit_range end def simple_label @simple_label ||= create(:label, name: 'gfm', project: project) end def label @label ||= create(:label, name: 'awaiting feedback', project: project) end # Cross-references ----------------------------------------------------------- def xproject unless @xproject namespace = create(:namespace, name: 'cross-reference') @xproject = create(:project, namespace: namespace) @xproject.team << [user, :developer] end @xproject end def xissue @xissue ||= create(:issue, project: xproject) end def xmerge_request @xmerge_request ||= create(:merge_request, :simple, source_project: xproject) end def xsnippet @xsnippet ||= create(:project_snippet, project: xproject) end def xcommit @xcommit ||= xproject.commit end def xcommit_range unless @xcommit_range xcommit2 = xproject.commit('HEAD~2') @xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject) end @xcommit_range end def raw_markdown fixture = Rails.root.join('spec/fixtures/markdown.md.erb') ERB.new(File.read(fixture)).result(binding) end end