diff options
Diffstat (limited to 'spec/lib/banzai')
10 files changed, 506 insertions, 7 deletions
diff --git a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb index 1f886059bf6..81aa8d35ebc 100644 --- a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb +++ b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb @@ -28,7 +28,7 @@ RSpec.describe Banzai::Filter::AssetProxyFilter do stub_application_setting(asset_proxy_enabled: true) stub_application_setting(asset_proxy_secret_key: 'shared-secret') stub_application_setting(asset_proxy_url: 'https://assets.example.com') - stub_application_setting(asset_proxy_whitelist: %w(gitlab.com *.mydomain.com)) + stub_application_setting(asset_proxy_allowlist: %w(gitlab.com *.mydomain.com)) described_class.initialize_settings @@ -39,16 +39,26 @@ RSpec.describe Banzai::Filter::AssetProxyFilter do expect(Gitlab.config.asset_proxy.domain_regexp).to eq(/^(gitlab\.com|.*?\.mydomain\.com)$/i) end - context 'when whitelist is empty' do + context 'when allowlist is empty' do it 'defaults to the install domain' do stub_application_setting(asset_proxy_enabled: true) - stub_application_setting(asset_proxy_whitelist: []) + stub_application_setting(asset_proxy_allowlist: []) described_class.initialize_settings expect(Gitlab.config.asset_proxy.allowlist).to eq [Gitlab.config.gitlab.host] end end + + it 'supports deprecated whitelist settings' do + stub_application_setting(asset_proxy_enabled: true) + stub_application_setting(asset_proxy_whitelist: %w(foo.com bar.com)) + stub_application_setting(asset_proxy_allowlist: []) + + described_class.initialize_settings + + expect(Gitlab.config.asset_proxy.allowlist).to eq %w(foo.com bar.com) + end end context 'when properly configured' do diff --git a/spec/lib/banzai/filter/custom_emoji_filter_spec.rb b/spec/lib/banzai/filter/custom_emoji_filter_spec.rb new file mode 100644 index 00000000000..ca8c9750e7f --- /dev/null +++ b/spec/lib/banzai/filter/custom_emoji_filter_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::Filter::CustomEmojiFilter do + include FilterSpecHelper + + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:custom_emoji) { create(:custom_emoji, name: 'tanuki', group: group) } + let_it_be(:custom_emoji2) { create(:custom_emoji, name: 'happy_tanuki', group: group, file: 'https://foo.bar/happy.png') } + + it 'replaces supported name custom emoji' do + doc = filter('<p>:tanuki:</p>', project: project) + + expect(doc.css('gl-emoji').first.attributes['title'].value).to eq('tanuki') + expect(doc.css('gl-emoji img').size).to eq 1 + end + + it 'ignores non existent custom emoji' do + exp = act = '<p>:foo:</p>' + doc = filter(act) + + expect(doc.to_html).to match Regexp.escape(exp) + end + + it 'correctly uses the custom emoji URL' do + doc = filter('<p>:tanuki:</p>') + + expect(doc.css('img').first.attributes['src'].value).to eq(custom_emoji.file) + end + + it 'matches with adjacent text' do + doc = filter('tanuki (:tanuki:)') + + expect(doc.css('img').size).to eq 1 + end + + it 'matches multiple same custom emoji' do + doc = filter(':tanuki: :tanuki:') + + expect(doc.css('img').size).to eq 2 + end + + it 'matches multiple custom emoji' do + doc = filter(':tanuki: (:happy_tanuki:)') + + expect(doc.css('img').size).to eq 2 + end + + it 'does not match enclosed colons' do + doc = filter('tanuki:tanuki:') + + expect(doc.css('img').size).to be 0 + end + + it 'keeps whitespace intact' do + doc = filter('This deserves a :tanuki:, big time.') + + expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/) + end + + it 'does not match emoji in a string' do + doc = filter("'2a00:tanuki:100::1'") + + expect(doc.css('gl-emoji').size).to eq 0 + end + + it 'does not do N+1 query' do + create(:custom_emoji, name: 'party-parrot', group: group) + + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + filter('<p>:tanuki:</p>') + end + + expect do + filter('<p>:tanuki: :party-parrot:</p>') + end.not_to exceed_all_query_limit(control_count.count) + end +end diff --git a/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb b/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb new file mode 100644 index 00000000000..2d7089853cf --- /dev/null +++ b/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb @@ -0,0 +1,223 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::Filter::FeatureFlagReferenceFilter do + include FilterSpecHelper + + let_it_be(:project) { create(:project, :public) } + let_it_be(:feature_flag) { create(:operations_feature_flag, project: project) } + let_it_be(:reference) { feature_flag.to_reference } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Feature Flag #{reference}</#{elem}>" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'with internal reference' do + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project, feature_flag) + end + + it 'links with adjacent text' do + doc = reference_filter("Feature Flag (#{reference}.)") + + expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(reference)}</a>\.\)}) + end + + it 'ignores invalid feature flag IIDs' do + exp = act = "Check [feature_flag:#{non_existing_record_id}]" + + expect(reference_filter(act).to_html).to eq exp + end + + it 'includes a title attribute' do + doc = reference_filter("Feature Flag #{reference}") + + expect(doc.css('a').first.attr('title')).to eq feature_flag.name + end + + it 'escapes the title attribute' do + allow(feature_flag).to receive(:name).and_return(%{"></a>whatever<a title="}) + doc = reference_filter("Feature Flag #{reference}") + + expect(doc.text).to eq "Feature Flag #{reference}" + end + + it 'includes default classes' do + doc = reference_filter("Feature Flag #{reference}") + + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-feature_flag has-tooltip' + end + + it 'includes a data-project attribute' do + doc = reference_filter("Feature Flag #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-feature-flag attribute' do + doc = reference_filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-feature-flag') + expect(link.attr('data-feature-flag')).to eq feature_flag.id.to_s + end + + it 'supports an :only_path context' do + doc = reference_filter("Feature Flag #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.edit_project_feature_flag_url(project, feature_flag.iid, only_path: true) + end + end + + context 'with cross-project / cross-namespace complete reference' do + let_it_be(:namespace) { create(:namespace) } + let_it_be(:project2) { create(:project, :public, namespace: namespace) } + let_it_be(:feature_flag) { create(:operations_feature_flag, project: project2) } + let_it_be(:reference) { "[feature_flag:#{project2.full_path}/#{feature_flag.iid}]" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project2, feature_flag) + end + + it 'produces a valid text in a link' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.css('a').first.text).to eql(reference) + end + + it 'produces a valid text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.text).to eql("See (#{reference}.)") + end + + it 'ignores invalid feature flag IIDs on the referenced project' do + exp = act = "Check [feature_flag:#{non_existing_record_id}]" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'with cross-project / same-namespace complete reference' do + let_it_be(:namespace) { create(:namespace) } + let_it_be(:project) { create(:project, :public, namespace: namespace) } + let_it_be(:project2) { create(:project, :public, namespace: namespace) } + let_it_be(:feature_flag) { create(:operations_feature_flag, project: project2) } + let_it_be(:reference) { "[feature_flag:#{project2.full_path}/#{feature_flag.iid}]" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project2, feature_flag) + end + + it 'produces a valid text in a link' do + doc = reference_filter("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)") + + expect(doc.css('a').first.text).to eql("[feature_flag:#{project2.path}/#{feature_flag.iid}]") + end + + it 'produces a valid text' do + doc = reference_filter("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)") + + expect(doc.text).to eql("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)") + end + + it 'ignores invalid feature flag IIDs on the referenced project' do + exp = act = "Check [feature_flag:#{non_existing_record_id}]" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'with cross-project shorthand reference' do + let_it_be(:namespace) { create(:namespace) } + let_it_be(:project) { create(:project, :public, namespace: namespace) } + let_it_be(:project2) { create(:project, :public, namespace: namespace) } + let_it_be(:feature_flag) { create(:operations_feature_flag, project: project2) } + let_it_be(:reference) { "[feature_flag:#{project2.path}/#{feature_flag.iid}]" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project2, feature_flag) + end + + it 'produces a valid text in a link' do + doc = reference_filter("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)") + + expect(doc.css('a').first.text).to eql("[feature_flag:#{project2.path}/#{feature_flag.iid}]") + end + + it 'produces a valid text' do + doc = reference_filter("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)") + + expect(doc.text).to eql("See ([feature_flag:#{project2.path}/#{feature_flag.iid}].)") + end + + it 'ignores invalid feature flag IDs on the referenced project' do + exp = act = "Check [feature_flag:#{non_existing_record_id}]" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'with cross-project URL reference' do + let_it_be(:namespace) { create(:namespace, name: 'cross-reference') } + let_it_be(:project2) { create(:project, :public, namespace: namespace) } + let_it_be(:feature_flag) { create(:operations_feature_flag, project: project2) } + let_it_be(:reference) { urls.edit_project_feature_flag_url(project2, feature_flag) } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.edit_project_feature_flag_url(project2, feature_flag) + end + + it 'links with adjacent text' do + doc = reference_filter("See (#{reference}.)") + + expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(feature_flag.to_reference(project))}</a>\.\)}) + end + + it 'ignores invalid feature flag IIDs on the referenced project' do + act = "See #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to match(%r{<a.+>#{Regexp.escape(invalidate_reference(reference))}</a>}) + end + end + + context 'with group context' do + let_it_be(:group) { create(:group) } + + it 'links to a valid reference' do + reference = "[feature_flag:#{project.full_path}/#{feature_flag.iid}]" + result = reference_filter("See #{reference}", { project: nil, group: group } ) + + expect(result.css('a').first.attr('href')).to eq(urls.edit_project_feature_flag_url(project, feature_flag)) + end + + it 'ignores internal references' do + exp = act = "See [feature_flag:#{feature_flag.iid}]" + + expect(reference_filter(act, project: nil, group: group).to_html).to eq exp + end + end +end diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb index df78a3321ba..811c2aca342 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb @@ -216,7 +216,7 @@ RSpec.describe Banzai::Filter::MergeRequestReferenceFilter do end context 'URL reference for a commit' do - let(:mr) { create(:merge_request, :with_diffs) } + let(:mr) { create(:merge_request) } let(:reference) do urls.project_merge_request_url(mr.project, mr) + "/diffs?commit_id=#{mr.diff_head_sha}" end diff --git a/spec/lib/banzai/filter/truncate_source_filter_spec.rb b/spec/lib/banzai/filter/truncate_source_filter_spec.rb index b0c6d91daa8..d5eb8b738b1 100644 --- a/spec/lib/banzai/filter/truncate_source_filter_spec.rb +++ b/spec/lib/banzai/filter/truncate_source_filter_spec.rb @@ -22,7 +22,7 @@ RSpec.describe Banzai::Filter::TruncateSourceFilter do it 'truncates UTF-8 text by bytes, on a character boundary' do utf8_text = '日本語の文字が大きい' - truncated = '日…' + truncated = '日...' expect(filter(utf8_text, limit: truncated.bytesize)).to eq(truncated) expect(filter(utf8_text, limit: utf8_text.bytesize)).to eq(utf8_text) diff --git a/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb b/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb index 41a91c56f3b..ad4256c2045 100644 --- a/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb @@ -3,11 +3,14 @@ require 'spec_helper' RSpec.describe Banzai::Pipeline::BroadcastMessagePipeline do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + before do stub_commonmark_sourcepos_disabled end - subject { described_class.to_html(exp, project: spy) } + subject { described_class.to_html(exp, project: project) } context "allows `a` elements" do let(:exp) { "<a>Link</a>" } diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb index 9391ca386cf..bcee6f8f65d 100644 --- a/spec/lib/banzai/pipeline/full_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb @@ -131,4 +131,16 @@ RSpec.describe Banzai::Pipeline::FullPipeline do expect(output).to include("test [[<em>TOC</em>]]") end end + + describe 'backslash escapes' do + let_it_be(:project) { create(:project, :public) } + let_it_be(:issue) { create(:issue, project: project) } + + it 'does not convert an escaped reference' do + markdown = "\\#{issue.to_reference}" + output = described_class.to_html(markdown, project: project) + + expect(output).to include("<span>#</span>#{issue.iid}") + end + end end diff --git a/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb new file mode 100644 index 00000000000..241d6db4f11 --- /dev/null +++ b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline do + using RSpec::Parameterized::TableSyntax + + describe 'backslash escapes' do + let_it_be(:project) { create(:project, :public) } + let_it_be(:issue) { create(:issue, project: project) } + + def correct_html_included(markdown, expected) + result = described_class.call(markdown, {}) + + expect(result[:output].to_html).to include(expected) + + result + end + + context 'when feature flag honor_escaped_markdown is disabled' do + before do + stub_feature_flags(honor_escaped_markdown: false) + end + + it 'does not escape the markdown' do + result = described_class.call(%q(\!), project: project) + output = result[:output].to_html + + expect(output).to eq('<p data-sourcepos="1:1-1:2">!</p>') + expect(result[:escaped_literals]).to be_falsey + end + end + + # Test strings taken from https://spec.commonmark.org/0.29/#backslash-escapes + describe 'CommonMark tests', :aggregate_failures do + it 'converts all ASCII punctuation to literals' do + markdown = %q(\!\"\#\$\%\&\'\*\+\,\-\.\/\:\;\<\=\>\?\@\[\]\^\_\`\{\|\}\~) + %q[\(\)\\\\] + punctuation = %w(! " # $ % & ' * + , - . / : ; < = > ? @ [ \\ ] ^ _ ` { | } ~) + %w[( )] + + result = described_class.call(markdown, project: project) + output = result[:output].to_html + + punctuation.each { |char| expect(output).to include("<span>#{char}</span>") } + expect(result[:escaped_literals]).to be_truthy + end + + it 'does not convert other characters to literals' do + markdown = %q(\→\A\a\ \3\φ\«) + expected = '\→\A\a\ \3\φ\«' + + result = correct_html_included(markdown, expected) + expect(result[:escaped_literals]).to be_falsey + end + + describe 'escaped characters are treated as regular characters and do not have their usual Markdown meanings' do + where(:markdown, :expected) do + %q(\*not emphasized*) | %q(<span>*</span>not emphasized*) + %q(\<br/> not a tag) | %q(<span><</span>br/> not a tag) + %q!\[not a link](/foo)! | %q!<span>[</span>not a link](/foo)! + %q(\`not code`) | %q(<span>`</span>not code`) + %q(1\. not a list) | %q(1<span>.</span> not a list) + %q(\# not a heading) | %q(<span>#</span> not a heading) + %q(\[foo]: /url "not a reference") | %q(<span>[</span>foo]: /url "not a reference") + %q(\ö not a character entity) | %q(<span>&</span>ouml; not a character entity) + end + + with_them do + it 'keeps them as literals' do + correct_html_included(markdown, expected) + end + end + end + + it 'backslash is itself escaped, the following character is not' do + markdown = %q(\\\\*emphasis*) + expected = %q(<span>\</span><em>emphasis</em>) + + correct_html_included(markdown, expected) + end + + it 'backslash at the end of the line is a hard line break' do + markdown = <<~MARKDOWN + foo\\ + bar + MARKDOWN + expected = "foo<br>\nbar" + + correct_html_included(markdown, expected) + end + + describe 'backslash escapes do not work in code blocks, code spans, autolinks, or raw HTML' do + where(:markdown, :expected) do + %q(`` \[\` ``) | %q(<code>\[\`</code>) + %q( \[\]) | %Q(<code>\\[\\]\n</code>) + %Q(~~~\n\\[\\]\n~~~) | %Q(<code>\\[\\]\n</code>) + %q(<http://example.com?find=\*>) | %q(<a href="http://example.com?find=%5C*">http://example.com?find=\*</a>) + %q[<a href="/bar\/)">] | %q[<a href="/bar%5C/)">] + end + + with_them do + it { correct_html_included(markdown, expected) } + end + end + + describe 'work in all other contexts, including URLs and link titles, link references, and info strings in fenced code blocks' do + where(:markdown, :expected) do + %q![foo](/bar\* "ti\*tle")! | %q(<a href="/bar*" title="ti*tle">foo</a>) + %Q![foo]\n\n[foo]: /bar\\* "ti\\*tle"! | %q(<a href="/bar*" title="ti*tle">foo</a>) + %Q(``` foo\\+bar\nfoo\n```) | %Q(<code lang="foo+bar">foo\n</code>) + end + + with_them do + it { correct_html_included(markdown, expected) } + end + end + end + end +end diff --git a/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb b/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb index f0498f41b61..c628d8d5b41 100644 --- a/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb @@ -30,6 +30,6 @@ RSpec.describe Banzai::Pipeline::PreProcessPipeline do result = described_class.call(text, limit: 12) - expect(result[:output]).to eq('foo foo f…') + expect(result[:output]).to eq('foo foo f...') end end diff --git a/spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb b/spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb new file mode 100644 index 00000000000..288eb9ae360 --- /dev/null +++ b/spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::ReferenceParser::FeatureFlagParser do + include ReferenceParserHelpers + + subject { described_class.new(Banzai::RenderContext.new(project, user)) } + + let(:link) { empty_html_link } + + describe '#nodes_visible_to_user' do + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + let(:feature_flag) { create(:operations_feature_flag, project: project) } + + context 'when the link has a data-issue attribute' do + before do + link['data-feature-flag'] = feature_flag.id.to_s + end + + it_behaves_like "referenced feature visibility", "issues", "merge_requests" do + before do + project.add_developer(user) if enable_user? + end + end + end + end + + describe '#referenced_by' do + let_it_be(:project) { create(:project, :public) } + let_it_be(:user) { create(:user) } + let_it_be(:feature_flag) { create(:operations_feature_flag, project: project) } + + describe 'when the link has a data-feature-flag attribute' do + context 'using an existing feature flag ID' do + it 'returns an Array of feature flags' do + link['data-feature-flag'] = feature_flag.id.to_s + + expect(subject.referenced_by([link])).to eq([feature_flag]) + end + end + + context 'using a non-existing feature flag ID' do + it 'returns an empty Array' do + link['data-feature-flag'] = '' + + expect(subject.referenced_by([link])).to eq([]) + end + end + end + end +end |