diff options
Diffstat (limited to 'spec/lib/banzai')
33 files changed, 913 insertions, 266 deletions
diff --git a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb index 575d4879f84..934a0a9fd58 100644 --- a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb +++ b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb @@ -31,4 +31,8 @@ RSpec.describe Banzai::Filter::BlockquoteFenceFilter, feature_category: :team_pl end.not_to raise_error end end + + it_behaves_like 'text filter timeout' do + let(:text) { ">>>\ntest\n>>>" } + end end diff --git a/spec/lib/banzai/filter/code_language_filter_spec.rb b/spec/lib/banzai/filter/code_language_filter_spec.rb new file mode 100644 index 00000000000..25f844ee575 --- /dev/null +++ b/spec/lib/banzai/filter/code_language_filter_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::Filter::CodeLanguageFilter, feature_category: :team_planning do + include FilterSpecHelper + + shared_examples 'XSS prevention' do |lang| + it 'escapes HTML tags' do + # This is how a script tag inside a code block is presented to this filter + # after Markdown rendering. + result = filter(%(<pre lang="#{lang}"><code><script>alert(1)</script></code></pre>)) + + # `(1)` symbols are wrapped by lexer tags. + expect(result.to_html).not_to match(%r{<script>alert.*</script>}) + + # `<>` stands for lexer tags like <span ...>, not <s above. + expect(result.to_html).to match(%r{alert(<.*>)?\((<.*>)?1(<.*>)?\)}) + end + end + + context 'when no language is specified' do + it 'does nothing' do + result = filter('<pre><code>def fun end</code></pre>') + + expect(result.to_html.delete("\n")).to eq('<pre><code>def fun end</code></pre>') + end + end + + context 'when lang is specified' do + it 'adds data-canonical-lang and removes lang attribute' do + result = filter('<pre lang="ruby"><code>def fun end</code></pre>') + + expect(result.to_html.delete("\n")) + .to eq('<pre data-canonical-lang="ruby"><code>def fun end</code></pre>') + end + end + + context 'when lang has extra params' do + let(:lang_params) { 'foo-bar-kux' } + let(:xss_lang) { %(ruby data-meta="foo-bar-kux"<script>alert(1)</script>) } + + it 'includes data-lang-params tag with extra information and removes data-meta' do + expected_result = <<~HTML + <pre data-canonical-lang="ruby" data-lang-params="#{lang_params}"> + <code>This is a test</code></pre> + HTML + + result = filter(%(<pre lang="ruby" data-meta="#{lang_params}"><code>This is a test</code></pre>)) + + expect(result.to_html.delete("\n")).to eq(expected_result.delete("\n")) + end + + include_examples 'XSS prevention', 'ruby' + + include_examples 'XSS prevention', + %(ruby data-meta="foo-bar-kux"<script>alert(1)</script>) + + include_examples 'XSS prevention', + %(ruby data-meta="foo-bar-kux"<script>alert(1)</script>) + end + + context 'when multiple param delimiters are used' do + let(:lang) { 'suggestion' } + let(:lang_params) { '-1+10' } + + let(:expected_result) do + <<~HTML + <pre data-canonical-lang="#{lang}" data-lang-params="#{lang_params} more-things"> + <code>This is a test</code></pre> + HTML + end + + context 'when delimiter is colon' do + it 'delimits on the first appearance' do + result = filter(%(<pre lang="#{lang}:#{lang_params} more-things"><code>This is a test</code></pre>)) + + expect(result.to_html.delete("\n")).to eq(expected_result.delete("\n")) + end + end + end +end diff --git a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb index 896f3beb7c2..3d992f962ec 100644 --- a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb @@ -8,16 +8,15 @@ RSpec.describe Banzai::Filter::CommitTrailersFilter, feature_category: :source_c include CommitTrailersSpecHelper let(:secondary_email) { create(:email, :confirmed) } - let(:user) { create(:user) } + let(:user) { create(:user, :public_email) } + let(:email) { FFaker::Internet.email } let(:trailer) { "#{FFaker::Lorem.word}-by:" } - let(:commit_message) { trailer_line(trailer, user.name, user.email) } + let(:commit_message) { trailer_line(trailer, user.name, user.public_email) } let(:commit_message_html) { commit_html(commit_message) } context 'detects' do - let(:email) { FFaker::Internet.email } - context 'trailers in the form of *-by' do where(:commit_trailer) do ["#{FFaker::Lorem.word}-by:", "#{FFaker::Lorem.word}-BY:", "#{FFaker::Lorem.word}-By:"] @@ -42,7 +41,7 @@ RSpec.describe Banzai::Filter::CommitTrailersFilter, feature_category: :source_c expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer) end - it 'GitLab users via a secondary email' do + it 'does not detect GitLab users via a secondary email' do _, message_html = build_commit_message( trailer: trailer, name: secondary_email.user.name, @@ -51,9 +50,8 @@ RSpec.describe Banzai::Filter::CommitTrailersFilter, feature_category: :source_c doc = filter(message_html) - expect_to_have_user_link_with_avatar( + expect_to_have_mailto_link_with_avatar( doc, - user: secondary_email.user, trailer: trailer, email: secondary_email.email ) @@ -185,17 +183,16 @@ RSpec.describe Banzai::Filter::CommitTrailersFilter, feature_category: :source_c it 'preserves the original email used in the commit message' do message, message_html = build_commit_message( trailer: trailer, - name: secondary_email.user.name, - email: secondary_email.email + name: user.name, + email: email ) doc = filter(message_html) - expect_to_have_user_link_with_avatar( + expect_to_have_mailto_link_with_avatar( doc, - user: secondary_email.user, trailer: trailer, - email: secondary_email.email + email: email ) expect(doc.text).to match Regexp.escape(message) end diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb index 3f72896939d..de259342998 100644 --- a/spec/lib/banzai/filter/external_link_filter_spec.rb +++ b/spec/lib/banzai/filter/external_link_filter_spec.rb @@ -2,25 +2,25 @@ require 'spec_helper' -RSpec.shared_examples 'an external link with rel attribute', feature_category: :team_planning do - it 'adds rel="nofollow" to external links' do - expect(doc.at_css('a')).to have_attribute('rel') - expect(doc.at_css('a')['rel']).to include 'nofollow' - end +RSpec.describe Banzai::Filter::ExternalLinkFilter, feature_category: :team_planning do + include FilterSpecHelper - it 'adds rel="noreferrer" to external links' do - expect(doc.at_css('a')).to have_attribute('rel') - expect(doc.at_css('a')['rel']).to include 'noreferrer' - end + shared_examples 'an external link with rel attribute' do + it 'adds rel="nofollow" to external links' do + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to include 'nofollow' + end - it 'adds rel="noopener" to external links' do - expect(doc.at_css('a')).to have_attribute('rel') - expect(doc.at_css('a')['rel']).to include 'noopener' - end -end + it 'adds rel="noreferrer" to external links' do + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to include 'noreferrer' + end -RSpec.describe Banzai::Filter::ExternalLinkFilter do - include FilterSpecHelper + it 'adds rel="noopener" to external links' do + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to include 'noopener' + end + end it 'ignores elements without an href attribute' do exp = act = %q(<a id="ignored">Ignore Me</a>) diff --git a/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb b/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb index db0c10a802b..746fa6c48a5 100644 --- a/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb +++ b/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Banzai::Filter::InlineGrafanaMetricsFilter do +RSpec.describe Banzai::Filter::InlineGrafanaMetricsFilter, feature_category: :metrics do include FilterSpecHelper let_it_be(:project) { create(:project) } @@ -29,6 +29,10 @@ RSpec.describe Banzai::Filter::InlineGrafanaMetricsFilter do ) end + before do + stub_feature_flags(remove_monitor_metrics: false) + end + around do |example| travel_to(Time.utc(2019, 3, 17, 13, 10)) { example.run } end diff --git a/spec/lib/banzai/filter/inline_observability_filter_spec.rb b/spec/lib/banzai/filter/inline_observability_filter_spec.rb index 69a9dc96c2c..81896faced8 100644 --- a/spec/lib/banzai/filter/inline_observability_filter_spec.rb +++ b/spec/lib/banzai/filter/inline_observability_filter_spec.rb @@ -2,25 +2,20 @@ require 'spec_helper' -RSpec.describe Banzai::Filter::InlineObservabilityFilter do +RSpec.describe Banzai::Filter::InlineObservabilityFilter, feature_category: :metrics do include FilterSpecHelper let(:input) { %(<a href="#{url}">example</a>) } let(:doc) { filter(input) } - let(:group) { create(:group) } - let(:user) { create(:user) } - describe '#filter?' do - context 'when the document has an external link' do - let(:url) { 'https://foo.com' } - - it 'leaves regular non-observability links unchanged' do - expect(doc.to_s).to eq(input) - end - end + before do + allow(Gitlab::Observability).to receive(:embeddable_url).and_return('embeddable-url') + stub_config_setting(url: "https://www.gitlab.com") + end - context 'when the document contains an embeddable observability link' do - let(:url) { 'https://observe.gitlab.com/12345' } + describe '#filter?' do + context 'when the document contains a valid observability link' do + let(:url) { "https://www.gitlab.com/groups/some-group/-/observability/test" } it 'leaves the original link unchanged' do expect(doc.at_css('a').to_s).to eq(input) @@ -30,32 +25,34 @@ RSpec.describe Banzai::Filter::InlineObservabilityFilter do node = doc.at_css('.js-render-observability') expect(node).to be_present - expect(node.attribute('data-frame-url').to_s).to eq(url) + expect(node.attribute('data-frame-url').to_s).to eq('embeddable-url') + expect(Gitlab::Observability).to have_received(:embeddable_url).with(url).once end end - context 'when the document contains an embeddable observability link with redirect' do - let(:url) { 'https://observe.gitlab.com@example.com/12345' } + context 'with duplicate URLs' do + let(:url) { "https://www.gitlab.com/groups/some-group/-/observability/test" } + let(:input) { %(<a href="#{url}">example1</a><a href="#{url}">example2</a>) } - it 'leaves the original link unchanged' do - expect(doc.at_css('a').to_s).to eq(input) + where(:embeddable_url) do + [ + 'not-nil', + nil + ] end - it 'does not append an observability charts placeholder' do - node = doc.at_css('.js-render-observability') - - expect(node).not_to be_present - end - end + with_them do + it 'calls Gitlab::Observability.embeddable_url only once' do + allow(Gitlab::Observability).to receive(:embeddable_url).with(url).and_return(embeddable_url) - context 'when the document contains an embeddable observability link with different port' do - let(:url) { 'https://observe.gitlab.com:3000/12345' } - let(:observe_url) { 'https://observe.gitlab.com:3001' } + filter(input) - before do - stub_env('OVERRIDE_OBSERVABILITY_URL', observe_url) + expect(Gitlab::Observability).to have_received(:embeddable_url).with(url).once + end end + end + shared_examples 'does not embed observabilty' do it 'leaves the original link unchanged' do expect(doc.at_css('a').to_s).to eq(input) end @@ -67,40 +64,37 @@ RSpec.describe Banzai::Filter::InlineObservabilityFilter do end end - context 'when the document contains an embeddable observability link with auth/start' do - let(:url) { 'https://observe.gitlab.com/auth/start' } - let(:observe_url) { 'https://observe.gitlab.com' } + context 'when the embeddable url is nil' do + let(:url) { "https://www.gitlab.com/groups/some-group/-/something-else/test" } before do - stub_env('OVERRIDE_OBSERVABILITY_URL', observe_url) + allow(Gitlab::Observability).to receive(:embeddable_url).and_return(nil) end - it 'leaves the original link unchanged' do - expect(doc.at_css('a').to_s).to eq(input) - end + it_behaves_like 'does not embed observabilty' + end - it 'does not append an observability charts placeholder' do - node = doc.at_css('.js-render-observability') + context 'when the document has an unrecognised link' do + let(:url) { "https://www.gitlab.com/groups/some-group/-/something-else/test" } - expect(node).not_to be_present + it_behaves_like 'does not embed observabilty' + + it 'does not build the embeddable url' do + expect(Gitlab::Observability).not_to have_received(:embeddable_url) end end context 'when feature flag is disabled' do - let(:url) { 'https://observe.gitlab.com/12345' } + let(:url) { "https://www.gitlab.com/groups/some-group/-/observability/test" } before do stub_feature_flags(observability_group_tab: false) end - it 'leaves the original link unchanged' do - expect(doc.at_css('a').to_s).to eq(input) - end + it_behaves_like 'does not embed observabilty' - it 'does not append an observability charts placeholder' do - node = doc.at_css('.js-render-observability') - - expect(node).not_to be_present + it 'does not build the embeddable url' do + expect(Gitlab::Observability).not_to have_received(:embeddable_url) end end end diff --git a/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb b/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb index 1fdb29b688e..e14b1362687 100644 --- a/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb +++ b/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb @@ -21,6 +21,10 @@ RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter, feature_categor create(:issue, state, attributes.merge(project: project)) end + def create_item(issuable_type, state, attributes = {}) + create(issuable_type, state, attributes.merge(project: project)) + end + def create_merge_request(state, attributes = {}) create(:merge_request, state, attributes.merge(source_project: project, target_project: project)) end @@ -115,53 +119,128 @@ RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter, feature_categor end end - context 'for issue references' do - it 'ignores open issue references' do - issue = create_issue(:opened) - link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue') + shared_examples 'issue / work item references' do + it 'ignores open references' do + issuable = create_item(issuable_type, :opened) + link = create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type) doc = filter(link, context) - expect(doc.css('a').last.text).to eq(issue.to_reference) + expect(doc.css('a').last.text).to eq(issuable.to_reference) end - it 'appends state to closed issue references' do - link = create_link(closed_issue.to_reference, issue: closed_issue.id, reference_type: 'issue') + it 'appends state to moved references' do + moved_issuable = create_item(issuable_type, :closed, project: project, + moved_to: create_item(issuable_type, :opened)) + link = create_link(moved_issuable.to_reference, "#{issuable_type}": moved_issuable.id, + reference_type: issuable_type) doc = filter(link, context) - expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference} (closed)") + expect(doc.css('a').last.text).to eq("#{moved_issuable.to_reference} (moved)") end - it 'appends state to moved issue references' do - moved_issue = create(:issue, :closed, project: project, moved_to: create_issue(:opened)) - link = create_link(moved_issue.to_reference, issue: moved_issue.id, reference_type: 'issue') + it 'appends state to closed references' do + issuable = create_item(issuable_type, :closed) + link = create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type) doc = filter(link, context) - expect(doc.css('a').last.text).to eq("#{moved_issue.to_reference} (moved)") + expect(doc.css('a').last.text).to eq("#{issuable.to_reference} (closed)") end it 'shows title for references with +' do - issue = create_issue(:opened, title: 'Some issue') - link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+') + issuable = create_item(issuable_type, :opened, title: 'Some issue') + link = create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type, + reference_format: '+') doc = filter(link, context) - expect(doc.css('a').last.text).to eq("#{issue.title} (#{issue.to_reference})") + expect(doc.css('a').last.text).to eq("#{issuable.title} (#{issuable.to_reference})") end it 'truncates long title for references with +' do - issue = create_issue(:opened, title: 'Some issue ' * 10) - link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+') + issuable = create_item(issuable_type, :opened, title: 'Some issue ' * 10) + link = create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type, + reference_format: '+') doc = filter(link, context) - expect(doc.css('a').last.text).to eq("#{issue.title.truncate(50)} (#{issue.to_reference})") + expect(doc.css('a').last.text).to eq("#{issuable.title.truncate(50)} (#{issuable.to_reference})") end it 'shows both title and state for closed references with +' do - issue = create_issue(:closed, title: 'Some issue') - link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+') + issuable = create_item(issuable_type, :closed, title: 'Some issue') + link = create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type, + reference_format: '+') + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq("#{issuable.title} (#{issuable.to_reference} - closed)") + end + + it 'shows title for references with +s' do + issuable = create_item(issuable_type, :opened, title: 'Some issue') + link = create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type, + reference_format: '+s') doc = filter(link, context) - expect(doc.css('a').last.text).to eq("#{issue.title} (#{issue.to_reference} - closed)") + expect(doc.css('a').last.text).to eq("#{issuable.title} (#{issuable.to_reference}) • Unassigned") end + + context 'when extended summary props are present' do + let_it_be(:milestone) { create(:milestone, project: project) } + let_it_be(:assignees) { create_list(:user, 3) } + let_it_be(:issuable) do + create_item(issuable_type, :opened, title: 'Some issue', milestone: milestone, + assignees: assignees) + end + + let_it_be(:link) do + create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type, + reference_format: '+s') + end + + it 'shows extended summary for references with +s' do + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq( + "#{issuable.title} (#{issuable.to_reference}) • #{assignees[0].name}, #{assignees[1].name}+ " \ + "• #{milestone.title}" + ) + end + + describe 'checking N+1' do + let_it_be(:milestone2) { create(:milestone, project: project) } + let_it_be(:assignees2) { create_list(:user, 3) } + + it 'does not have N+1 for extended summary', :use_sql_query_cache do + issuable2 = create_item(issuable_type, :opened, title: 'Another issue', + milestone: milestone2, assignees: assignees2) + link2 = create_link(issuable2.to_reference, "#{issuable_type}": issuable2.id, + reference_type: issuable_type, reference_format: '+s') + + # warm up + filter(link, context) + + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + filter(link, context) + end.count + + expect(control_count).to eq 12 + + expect do + filter("#{link} #{link2}", context) + end.not_to exceed_all_query_limit(control_count) + end + end + end + end + + context 'for work item references' do + let_it_be(:issuable_type) { :work_item } + + it_behaves_like 'issue / work item references' + end + + context 'for issue references' do + let_it_be(:issuable_type) { :issue } + + it_behaves_like 'issue / work item references' end context 'for merge request references' do @@ -235,5 +314,80 @@ RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter, feature_categor expect(doc.css('a').last.text).to eq("#{merge_request.title} (#{merge_request.to_reference})") end + + it 'shows title for references with +s' do + merge_request = create_merge_request(:opened, title: 'Some merge request') + + link = create_link( + merge_request.to_reference, + merge_request: merge_request.id, + reference_type: 'merge_request', + reference_format: '+s' + ) + + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq("#{merge_request.title} (#{merge_request.to_reference}) • Unassigned") + end + + context 'when extended summary props are present' do + let_it_be(:milestone) { create(:milestone, project: project) } + let_it_be(:assignees) { create_list(:user, 2) } + let_it_be(:merge_request) do + create_merge_request(:opened, title: 'Some merge request', milestone: milestone, assignees: assignees) + end + + let_it_be(:link) do + create_link( + merge_request.to_reference, + merge_request: merge_request.id, + reference_type: 'merge_request', + reference_format: '+s' + ) + end + + it 'shows extended summary for references with +s' do + doc = filter(link, context) + + expect(doc.css('a').last.text).to eq( + "#{merge_request.title} (#{merge_request.to_reference}) • #{assignees[0].name}, #{assignees[1].name} • " \ + "#{milestone.title}" + ) + end + + describe 'checking N+1' do + let_it_be(:milestone2) { create(:milestone, project: project) } + let_it_be(:assignees2) { create_list(:user, 3) } + + it 'does not have N+1 for extended summary', :use_sql_query_cache do + merge_request2 = create_merge_request( + :closed, + title: 'Some merge request', + milestone: milestone2, + assignees: assignees2 + ) + + link2 = create_link( + merge_request2.to_reference, + merge_request: merge_request2.id, + reference_type: 'merge_request', + reference_format: '+s' + ) + + # warm up + filter(link, context) + + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + filter(link, context) + end.count + + expect(control_count).to eq 10 + + expect do + filter("#{link} #{link2}", context) + end.not_to exceed_all_query_limit(control_count) + end + end + end end end diff --git a/spec/lib/banzai/filter/kroki_filter_spec.rb b/spec/lib/banzai/filter/kroki_filter_spec.rb index 1cd11161439..3915d9fb8f8 100644 --- a/spec/lib/banzai/filter/kroki_filter_spec.rb +++ b/spec/lib/banzai/filter/kroki_filter_spec.rb @@ -7,42 +7,46 @@ RSpec.describe Banzai::Filter::KrokiFilter, feature_category: :team_planning do it 'replaces nomnoml pre tag with img tag if kroki is enabled' do stub_application_setting(kroki_enabled: true, kroki_url: "http://localhost:8000") - doc = filter("<pre lang='nomnoml'><code>[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]</code></pre>") + doc = filter("<pre data-canonical-lang='nomnoml'><code>[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]</code></pre>") expect(doc.to_s).to eq '<img src="http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KzhUlCITkpNLEqJ1dWNLkgsKsoviUUSs7KLTssvzVHIzS8tyYjligUAMhEd0g==" class="js-render-kroki" data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,W1BpcmF0ZXxleWVDb3VudDogSW50fHJhaWQoKTtwaWxsYWdlKCl8CiAgW2JlYXJkXS0tW3BhcnJvdF0KICBbYmVhcmRdLTo+W2ZvdWwgbW91dGhdCl0=">' end it 'replaces nomnoml pre tag with img tag if both kroki and plantuml are enabled' do - stub_application_setting(kroki_enabled: true, - kroki_url: "http://localhost:8000", - plantuml_enabled: true, - plantuml_url: "http://localhost:8080") - doc = filter("<pre lang='nomnoml'><code>[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]</code></pre>") + stub_application_setting( + kroki_enabled: true, + kroki_url: "http://localhost:8000", + plantuml_enabled: true, + plantuml_url: "http://localhost:8080" + ) + doc = filter("<pre data-canonical-lang='nomnoml'><code>[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]</code></pre>") expect(doc.to_s).to eq '<img src="http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KzhUlCITkpNLEqJ1dWNLkgsKsoviUUSs7KLTssvzVHIzS8tyYjligUAMhEd0g==" class="js-render-kroki" data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,W1BpcmF0ZXxleWVDb3VudDogSW50fHJhaWQoKTtwaWxsYWdlKCl8CiAgW2JlYXJkXS0tW3BhcnJvdF0KICBbYmVhcmRdLTo+W2ZvdWwgbW91dGhdCl0=">' end it 'does not replace nomnoml pre tag with img tag if kroki is disabled' do stub_application_setting(kroki_enabled: false) - doc = filter("<pre lang='nomnoml'><code>[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]</code></pre>") + doc = filter("<pre data-canonical-lang='nomnoml'><code>[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]</code></pre>") - expect(doc.to_s).to eq "<pre lang=\"nomnoml\"><code>[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]</code></pre>" + expect(doc.to_s).to eq "<pre data-canonical-lang=\"nomnoml\"><code>[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]</code></pre>" end it 'does not replace plantuml pre tag with img tag if both kroki and plantuml are enabled' do - stub_application_setting(kroki_enabled: true, - kroki_url: "http://localhost:8000", - plantuml_enabled: true, - plantuml_url: "http://localhost:8080") - doc = filter("<pre lang='plantuml'><code>Bob->Alice : hello</code></pre>") - - expect(doc.to_s).to eq '<pre lang="plantuml"><code>Bob->Alice : hello</code></pre>' + stub_application_setting( + kroki_enabled: true, + kroki_url: "http://localhost:8000", + plantuml_enabled: true, + plantuml_url: "http://localhost:8080" + ) + doc = filter("<pre data-canonical-lang='plantuml'><code>Bob->Alice : hello</code></pre>") + + expect(doc.to_s).to eq '<pre data-canonical-lang="plantuml"><code>Bob->Alice : hello</code></pre>' end it 'adds hidden attribute when content size is large' do stub_application_setting(kroki_enabled: true, kroki_url: "http://localhost:8000") text = '[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]' * 25 - doc = filter("<pre lang='nomnoml'><code>#{text}</code></pre>") + doc = filter("<pre data-canonical-lang='nomnoml'><code>#{text}</code></pre>") expect(doc.to_s).to start_with '<img src="http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KyJyVNQiE5KTSxKidXVjS5ILCrKL4lFFrSyi07LL81RyM0vLckAysRGjxo8avCowaMGjxo8avCowaMGU8lgAE7mIdc=" hidden="" class="js-render-kroki" data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,W1BpcmF0ZXxleWVDb3VudDog' end @@ -50,15 +54,15 @@ RSpec.describe Banzai::Filter::KrokiFilter, feature_category: :team_planning do it 'allows the lang attribute on the code tag to support RST files processed by gitlab-markup gem' do stub_application_setting(kroki_enabled: true, kroki_url: "http://localhost:8000") text = '[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]' * 25 - doc = filter("<pre><code lang='nomnoml'>#{text}</code></pre>") + doc = filter("<pre><code data-canonical-lang='nomnoml'>#{text}</code></pre>") expect(doc.to_s).to start_with '<img src="http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KyJyVNQiE5KTSxKidXVjS5ILCrKL4lFFrSyi07LL81RyM0vLckAysRGjxo8avCowaMGjxo8avCowaMGU8lgAE7mIdc=" hidden="" class="js-render-kroki" data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,W1BpcmF0ZXxleWVDb3VudDog' end it 'verifies diagram type to avoid possible XSS' do stub_application_setting(kroki_enabled: true, kroki_url: "http://localhost:8000") - doc = filter(%(<a><pre lang='f/" onerror=alert(1) onload=alert(1) '><code lang="wavedrom">xss</code></pre></a>)) + doc = filter(%(<a><pre data-canonical-lang='f/" onerror=alert(1) onload=alert(1) '><code data-canonical-lang="wavedrom">xss</code></pre></a>)) - expect(doc.to_s).to eq %(<a><pre lang='f/" onerror=alert(1) onload=alert(1) '><code lang="wavedrom">xss</code></pre></a>) + expect(doc.to_s).to eq %(<a><pre data-canonical-lang='f/" onerror=alert(1) onload=alert(1) '><code data-canonical-lang="wavedrom">xss</code></pre></a>) end end diff --git a/spec/lib/banzai/filter/markdown_engines/base_spec.rb b/spec/lib/banzai/filter/markdown_engines/base_spec.rb new file mode 100644 index 00000000000..e7b32876610 --- /dev/null +++ b/spec/lib/banzai/filter/markdown_engines/base_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::Filter::MarkdownEngines::Base, feature_category: :team_planning do + it 'raise error if render not implemented' do + engine = described_class.new({}) + + expect { engine.render('# hi') }.to raise_error(NotImplementedError) + end + + it 'turns off sourcepos' do + engine = described_class.new({ no_sourcepos: true }) + + expect(engine.send(:sourcepos_disabled?)).to be_truthy + end +end diff --git a/spec/lib/banzai/filter/markdown_engines/common_mark_spec.rb b/spec/lib/banzai/filter/markdown_engines/common_mark_spec.rb new file mode 100644 index 00000000000..74fac75abe8 --- /dev/null +++ b/spec/lib/banzai/filter/markdown_engines/common_mark_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::Filter::MarkdownEngines::CommonMark, feature_category: :team_planning do + it 'defaults to generating sourcepos' do + engine = described_class.new({}) + + expect(engine.render('# hi')).to eq %(<h1 data-sourcepos="1:1-1:4">hi</h1>\n) + end + + it 'turns off sourcepos' do + engine = described_class.new({ no_sourcepos: true }) + + expect(engine.render('# hi')).to eq %(<h1>hi</h1>\n) + end +end diff --git a/spec/lib/banzai/filter/markdown_filter_spec.rb b/spec/lib/banzai/filter/markdown_filter_spec.rb index c79cd58255d..64d65528426 100644 --- a/spec/lib/banzai/filter/markdown_filter_spec.rb +++ b/spec/lib/banzai/filter/markdown_filter_spec.rb @@ -6,20 +6,19 @@ RSpec.describe Banzai::Filter::MarkdownFilter, feature_category: :team_planning include FilterSpecHelper describe 'markdown engine from context' do - it 'defaults to CommonMark' do - expect_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance| - expect(instance).to receive(:render).and_return('test') - end - - filter('test') + it 'finds the correct engine' do + expect(described_class.render_engine(:common_mark)).to eq Banzai::Filter::MarkdownEngines::CommonMark end - it 'uses CommonMark' do - expect_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance| - expect(instance).to receive(:render).and_return('test') - end + it 'defaults to the DEFAULT_ENGINE' do + default_engine = Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE.to_s.classify + default = "Banzai::Filter::MarkdownEngines::#{default_engine}".constantize + + expect(described_class.render_engine(nil)).to eq default + end - filter('test', { markdown_engine: :common_mark }) + it 'raise error for unrecognized engines' do + expect { described_class.render_engine(:foo_bar) }.to raise_error(NameError) end end diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb index 374983e40a1..ded94dd6ce5 100644 --- a/spec/lib/banzai/filter/math_filter_spec.rb +++ b/spec/lib/banzai/filter/math_filter_spec.rb @@ -18,7 +18,7 @@ RSpec.describe Banzai::Filter::MathFilter, feature_category: :team_planning do end shared_examples 'display math' do - let_it_be(:template_prefix_with_pre) { '<pre lang="math" data-math-style="display" class="js-render-math"><code>' } + let_it_be(:template_prefix_with_pre) { '<pre data-canonical-lang="math" data-math-style="display" class="js-render-math"><code>' } let_it_be(:template_prefix_with_code) { '<code data-math-style="display" class="code math js-render-math">' } let(:use_pre_tags) { false } @@ -101,6 +101,7 @@ RSpec.describe Banzai::Filter::MathFilter, feature_category: :team_planning do context 'with valid syntax' do where(:text, :result_template) do "$$\n2+2\n$$" | "<math>2+2\n</math>" + "$$ \n2+2\n$$" | "<math>2+2\n</math>" "$$\n2+2\n3+4\n$$" | "<math>2+2\n3+4\n</math>" end @@ -164,11 +165,11 @@ RSpec.describe Banzai::Filter::MathFilter, feature_category: :team_planning do input = "```plaintext\n2+2\n```" doc = pipeline_filter(input) - expect(doc.to_s).to eq "<pre lang=\"plaintext\"><code>2+2\n</code></pre>" + expect(doc.to_s).to eq "<pre data-canonical-lang=\"plaintext\"><code>2+2\n</code></pre>" end it 'requires the pre to contain both code and math' do - input = '<pre lang="math">something</pre>' + input = '<pre data-canonical-lang="math">something</pre>' doc = pipeline_filter(input) expect(doc.to_s).to eq input @@ -216,9 +217,11 @@ RSpec.describe Banzai::Filter::MathFilter, feature_category: :team_planning do def pipeline_filter(text) context = { project: nil, no_sourcepos: true } + doc = Banzai::Pipeline::PreProcessPipeline.call(text, {}) doc = Banzai::Pipeline::PlainMarkdownPipeline.call(doc[:output], context) - doc = Banzai::Filter::SanitizationFilter.call(doc[:output], context, nil) + doc = Banzai::Filter::CodeLanguageFilter.call(doc[:output], context, nil) + doc = Banzai::Filter::SanitizationFilter.call(doc, context, nil) filter(doc) end diff --git a/spec/lib/banzai/filter/mermaid_filter_spec.rb b/spec/lib/banzai/filter/mermaid_filter_spec.rb index de558a0774d..395a6a0bee5 100644 --- a/spec/lib/banzai/filter/mermaid_filter_spec.rb +++ b/spec/lib/banzai/filter/mermaid_filter_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Banzai::Filter::MermaidFilter, feature_category: :team_planning d include FilterSpecHelper it 'adds `js-render-mermaid` class to the `code` tag' do - doc = filter("<pre class='code highlight js-syntax-highlight mermaid' lang='mermaid' v-pre='true'><code>graph TD;\n A-->B;\n</code></pre>") + doc = filter("<pre class='code highlight js-syntax-highlight mermaid' data-canonical-lang='mermaid' v-pre='true'><code>graph TD;\n A-->B;\n</code></pre>") result = doc.css('code').first expect(result[:class]).to include('js-render-mermaid') diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb index a1eabc23327..8b1e566376c 100644 --- a/spec/lib/banzai/filter/plantuml_filter_spec.rb +++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Banzai::Filter::PlantumlFilter, feature_category: :team_planning it 'replaces plantuml pre tag with img tag' do stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080") - input = '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>' + input = '<pre data-canonical-lang="plantuml"><code>Bob -> Sara : Hello</code></pre>' output = '<img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq" data-diagram="plantuml" data-diagram-src="data:text/plain;base64,Qm9iIC0+IFNhcmEgOiBIZWxsbw==">' doc = filter(input) @@ -18,7 +18,7 @@ RSpec.describe Banzai::Filter::PlantumlFilter, feature_category: :team_planning it 'allows the lang attribute on the code tag to support RST files processed by gitlab-markup gem' do stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080") - input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>' + input = '<pre><code data-canonical-lang="plantuml">Bob -> Sara : Hello</code></pre>' output = '<img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq" data-diagram="plantuml" data-diagram-src="data:text/plain;base64,Qm9iIC0+IFNhcmEgOiBIZWxsbw==">' doc = filter(input) @@ -28,8 +28,8 @@ RSpec.describe Banzai::Filter::PlantumlFilter, feature_category: :team_planning it 'does not replace plantuml pre tag with img tag if disabled' do stub_application_setting(plantuml_enabled: false) - input = '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>' - output = '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>' + input = '<pre data-canonical-lang="plantuml"><code>Bob -> Sara : Hello</code></pre>' + output = '<pre data-canonical-lang="plantuml"><code>Bob -> Sara : Hello</code></pre>' doc = filter(input) expect(doc.to_s).to eq output @@ -38,8 +38,8 @@ RSpec.describe Banzai::Filter::PlantumlFilter, feature_category: :team_planning it 'does not replace plantuml pre tag with img tag if url is invalid' do stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid") - input = '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>' - output = '<pre lang="plantuml"><code>Bob -> Sara : Hello</code></pre>' + input = '<pre data-canonical-lang="plantuml"><code>Bob -> Sara : Hello</code></pre>' + output = '<pre data-canonical-lang="plantuml"><code>Bob -> Sara : Hello</code></pre>' doc = filter(input) expect(doc.to_s).to eq output diff --git a/spec/lib/banzai/filter/references/design_reference_filter_spec.rb b/spec/lib/banzai/filter/references/design_reference_filter_spec.rb index 08de9700cad..d97067de155 100644 --- a/spec/lib/banzai/filter/references/design_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/design_reference_filter_spec.rb @@ -128,10 +128,12 @@ RSpec.describe Banzai::Filter::References::DesignReferenceFilter, feature_catego let(:subject) { filter_instance.data_attributes_for(input_text, project, design) } specify do - is_expected.to include(issue: design.issue_id, - original: input_text, - project: project.id, - design: design.id) + is_expected.to include( + issue: design.issue_id, + original: input_text, + project: project.id, + design: design.id + ) end end diff --git a/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb index d8a97c6c3dc..aadd726ac40 100644 --- a/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb @@ -150,6 +150,15 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter, feature_categor expect(link.attr('href')).to eq(issue_url) end + it 'includes a data-reference-format attribute for extended summary URL references' do + doc = reference_filter("Issue #{issue_url}+s") + link = doc.css('a').first + + expect(link).to have_attribute('data-reference-format') + expect(link.attr('data-reference-format')).to eq('+s') + expect(link.attr('href')).to eq(issue_url) + end + it 'supports an :only_path context' do doc = reference_filter("Issue #{written_reference}", only_path: true) link = doc.css('a').first.attr('href') diff --git a/spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb index 9853d6f4093..156455221cf 100644 --- a/spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb @@ -128,6 +128,15 @@ RSpec.describe Banzai::Filter::References::MergeRequestReferenceFilter, feature_ expect(link.attr('href')).to eq(merge_request_url) end + it 'includes a data-reference-format attribute for extended summary URL references' do + doc = reference_filter("Merge #{merge_request_url}+s") + link = doc.css('a').first + + expect(link).to have_attribute('data-reference-format') + expect(link.attr('data-reference-format')).to eq('+s') + expect(link.attr('href')).to eq(merge_request_url) + end + it 'supports an :only_path context' do doc = reference_filter("Merge #{reference}", only_path: true) link = doc.css('a').first.attr('href') diff --git a/spec/lib/banzai/filter/references/reference_cache_spec.rb b/spec/lib/banzai/filter/references/reference_cache_spec.rb index 7307daca516..577e4471433 100644 --- a/spec/lib/banzai/filter/references/reference_cache_spec.rb +++ b/spec/lib/banzai/filter/references/reference_cache_spec.rb @@ -76,12 +76,11 @@ RSpec.describe Banzai::Filter::References::ReferenceCache, feature_category: :te cache_single.load_records_per_parent end.count - expect(control_count).to eq 1 - + expect(control_count).to eq 3 # Since this is an issue filter that is not batching issue queries # across projects, we have to account for that. # 1 for original issue, 2 for second route/project, 1 for other issue - max_count = control_count + 3 + max_count = control_count + 4 expect do cache.load_references_per_parent(filter.nodes) diff --git a/spec/lib/banzai/filter/references/work_item_reference_filter_spec.rb b/spec/lib/banzai/filter/references/work_item_reference_filter_spec.rb new file mode 100644 index 00000000000..e59e53891bf --- /dev/null +++ b/spec/lib/banzai/filter/references/work_item_reference_filter_spec.rb @@ -0,0 +1,314 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::Filter::References::WorkItemReferenceFilter, feature_category: :team_planning do + include FilterSpecHelper + + let_it_be(:namespace) { create(:namespace, name: 'main-namespace') } + let_it_be(:project) { create(:project, :public, namespace: namespace, path: 'main-project') } + let_it_be(:cross_namespace) { create(:namespace, name: 'cross-namespace') } + let_it_be(:cross_project) { create(:project, :public, namespace: cross_namespace, path: 'cross-project') } + let_it_be(:work_item) { create(:work_item, project: project) } + + def item_url(item) + work_item_path = "/#{item.project.namespace.path}/#{item.project.path}/-/work_items/#{item.iid}" + + "http://#{Gitlab.config.gitlab.host}#{work_item_path}" + end + + it 'subclasses from IssueReferenceFilter' do + expect(described_class.superclass).to eq Banzai::Filter::References::IssueReferenceFilter + end + + shared_examples 'a reference with work item type information' do + it 'contains work-item-type as a data attribute' do + doc = reference_filter("Fixed #{reference}") + + expect(doc.css('a').first.attr('data-work-item-type')).to eq('issue') + end + end + + shared_examples 'a work item reference' do + it_behaves_like 'a reference containing an element node' + + it_behaves_like 'a reference with work item type information' + + it 'links to a valid reference' do + doc = reference_filter("Fixed #{written_reference}") + + expect(doc.css('a').first.attr('href')).to eq work_item_url + end + + it 'links with adjacent text' do + doc = reference_filter("Fixed (#{written_reference}.)") + + expect(doc.text).to match(%r{^Fixed \(.*\.\)}) + end + + it 'includes a title attribute' do + doc = reference_filter("Issue #{written_reference}") + + expect(doc.css('a').first.attr('title')).to eq work_item.title + end + + it 'escapes the title attribute' do + work_item.update_attribute(:title, %("></a>whatever<a title=")) + + doc = reference_filter("Issue #{written_reference}") + + expect(doc.text).not_to include 'whatever' + end + + it 'renders non-HTML tooltips' do + doc = reference_filter("Issue #{written_reference}") + + expect(doc.at_css('a')).not_to have_attribute('data-html') + end + + it 'includes default classes' do + doc = reference_filter("Issue #{written_reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-work_item' + end + + it 'includes a data-project attribute' do + doc = reference_filter("Issue #{written_reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq cross_project.id.to_s + end + + it 'includes a data-issue attribute' do + doc = reference_filter("See #{written_reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-work-item') + expect(link.attr('data-work-item')).to eq work_item.id.to_s + end + + it 'includes data attributes for issuable popover' do + doc = reference_filter("See #{written_reference}") + link = doc.css('a').first + + expect(link.attr('data-project-path')).to eq cross_project.full_path + expect(link.attr('data-iid')).to eq work_item.iid.to_s + end + + it 'includes a data-original attribute' do + doc = reference_filter("See #{written_reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-original') + expect(link.attr('data-original')).to eq inner_text + end + + it 'does not escape the data-original attribute' do + skip if written_reference.start_with?('<a') + + inner_html = 'element <code>node</code> inside' + doc = reference_filter(%(<a href="#{written_reference}">#{inner_html}</a>)) + + expect(doc.children.first.attr('data-original')).to eq inner_html + end + + it 'includes a data-reference-format attribute' do + skip if written_reference.start_with?('<a') + + doc = reference_filter("Issue #{written_reference}+") + link = doc.css('a').first + + expect(link).to have_attribute('data-reference-format') + expect(link.attr('data-reference-format')).to eq('+') + expect(link.attr('href')).to eq(work_item_url) + end + + it 'includes a data-reference-format attribute for URL references' do + doc = reference_filter("Issue #{work_item_url}+") + link = doc.css('a').first + + expect(link).to have_attribute('data-reference-format') + expect(link.attr('data-reference-format')).to eq('+') + expect(link.attr('href')).to eq(work_item_url) + end + + it 'includes a data-reference-format attribute for extended summary URL references' do + doc = reference_filter("Issue #{work_item_url}+s") + link = doc.css('a').first + + expect(link).to have_attribute('data-reference-format') + expect(link.attr('data-reference-format')).to eq('+s') + expect(link.attr('href')).to eq(work_item_url) + end + + it 'does not process links containing issue numbers followed by text' do + href = "#{written_reference}st" + doc = reference_filter("<a href='#{href}'></a>") + link = doc.css('a').first.attr('href') + + expect(link).to eq(href) + end + end + + # Example: + # "See #1" + context 'when standard internal reference' do + it 'is handled by IssueReferenceFilter, not WorkItemReferenceFilter' do + doc = reference_filter("Fixed ##{work_item.iid}") + + expect(doc.css('a')).to be_empty + end + end + + # Example: + # "See cross-namespace/cross-project#1" + context 'when cross-project / cross-namespace complete reference' do + let_it_be(:work_item2) { create(:work_item, project: cross_project) } + let_it_be(:reference) { "#{cross_project.full_path}##{work_item2.iid}" } + + it 'is handled by IssueReferenceFilter, not WorkItemReferenceFilter' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a')).to be_empty + end + end + + # Example: + # "See main-namespace/cross-project#1" + context 'when cross-project / same-namespace complete reference' do + let_it_be(:cross_project) { create(:project, :public, namespace: namespace, path: 'cross-project') } + let_it_be(:work_item) { create(:work_item, project: cross_project) } + let_it_be(:reference) { "#{cross_project.full_path}##{work_item.iid}" } + + it 'is handled by IssueReferenceFilter, not WorkItemReferenceFilter' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a')).to be_empty + end + end + + # Example: + # "See cross-project#1" + context 'when cross-project / same-namespace shorthand reference' do + let_it_be(:cross_project) { create(:project, :public, namespace: namespace, path: 'cross-project') } + let_it_be(:work_item) { create(:work_item, project: cross_project) } + let_it_be(:reference) { "#{cross_project.path}##{work_item.iid}" } + + it 'is handled by IssueReferenceFilter, not WorkItemReferenceFilter' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a')).to be_empty + end + end + + # Example: + # "See http://localhost/cross-namespace/cross-project/-/work_items/1" + context 'when cross-project URL reference' do + let_it_be(:work_item, reload: true) { create(:work_item, project: cross_project) } + let_it_be(:work_item_url) { item_url(work_item) } + let_it_be(:reference) { work_item_url } + let_it_be(:written_reference) { reference } + let_it_be(:inner_text) { written_reference } + + it_behaves_like 'a work item reference' + end + + # Example: + # "See http://localhost/cross-namespace/cross-project/-/work_items/1#note_123" + context 'when cross-project URL reference with comment anchor' do + let_it_be(:work_item) { create(:work_item, project: cross_project) } + let_it_be(:work_item_url) { item_url(work_item) } + let_it_be(:reference) { "#{work_item_url}#note_123" } + + it_behaves_like 'a reference containing an element node' + + it_behaves_like 'a reference with work item type information' + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq reference + end + + it 'link with trailing slash' do + doc = reference_filter("Fixed (#{work_item_url}/.)") + + expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(work_item.to_reference(project))}</a>\.\)}) + end + + it 'links with adjacent text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(work_item.to_reference(project))} \(comment 123\)</a>\.\)}) + end + end + + # Example: + # 'See <a href="cross-namespace/cross-project#1">Reference</a>'' + context 'when cross-project reference in link href' do + let_it_be(:work_item) { create(:work_item, project: cross_project) } + let_it_be(:reference) { work_item.to_reference(project) } + let_it_be(:reference_link) { %(<a href="#{reference}">Reference</a>) } + let_it_be(:work_item_url) { item_url(work_item) } + + it 'is handled by IssueReferenceFilter, not WorkItemReferenceFilter' do + doc = reference_filter("See #{reference_link}") + + expect(doc.css('a').first[:href]).to eq reference + expect(doc.css('a').first[:href]).not_to eq work_item_url + end + end + + # Example: + # 'See <a href=\"http://localhost/cross-namespace/cross-project/-/work_items/1\">Reference</a>'' + context 'when cross-project URL in link href' do + let_it_be(:work_item, reload: true) { create(:work_item, project: cross_project) } + let_it_be(:work_item_url) { item_url(work_item) } + let_it_be(:reference) { work_item_url } + let_it_be(:reference_link) { %(<a href="#{reference}">Reference</a>) } + let_it_be(:written_reference) { reference_link } + let_it_be(:inner_text) { 'Reference' } + + it_behaves_like 'a work item reference' + end + + context 'for group context' do + let_it_be(:group) { create(:group) } + let_it_be(:context) { { project: nil, group: group } } + let_it_be(:work_item_url) { item_url(work_item) } + + it 'links to a valid reference for url cross-namespace' do + reference = "#{work_item_url}#note_123" + + doc = reference_filter("See #{reference}", context) + + link = doc.css('a').first + expect(link.attr('href')).to eq("#{work_item_url}#note_123") + expect(link.text).to include("#{project.full_path}##{work_item.iid}") + end + + it 'links to a valid reference for cross-namespace in link href' do + reference = "#{work_item_url}#note_123" + reference_link = %(<a href="#{reference}">Reference</a>) + + doc = reference_filter("See #{reference_link}", context) + + link = doc.css('a').first + expect(link.attr('href')).to eq("#{work_item_url}#note_123") + expect(link.text).to include('Reference') + end + end + + describe 'performance' do + let(:another_work_item) { create(:work_item, project: project) } + + it 'does not have a N+1 query problem' do + single_reference = "Work item #{work_item.to_reference}" + multiple_references = "Work items #{work_item.to_reference} and #{another_work_item.to_reference}" + + control_count = ActiveRecord::QueryRecorder.new { reference_filter(single_reference).to_html }.count + + expect { reference_filter(multiple_references).to_html }.not_to exceed_query_limit(control_count) + end + end +end diff --git a/spec/lib/banzai/filter/repository_link_filter_spec.rb b/spec/lib/banzai/filter/repository_link_filter_spec.rb index b2162ea2756..b6966709f5c 100644 --- a/spec/lib/banzai/filter/repository_link_filter_spec.rb +++ b/spec/lib/banzai/filter/repository_link_filter_spec.rb @@ -369,7 +369,18 @@ RSpec.describe Banzai::Filter::RepositoryLinkFilter, feature_category: :team_pla end end - context 'with a valid commit' do + context 'when public project repo with a valid commit' do + include_examples 'valid repository' + end + + context 'when private project repo with a valid commit' do + let_it_be(:project) { create(:project, :repository, :private) } + + before do + # user must have `read_code` ability + project.add_developer(user) + end + include_examples 'valid repository' end diff --git a/spec/lib/banzai/filter/suggestion_filter_spec.rb b/spec/lib/banzai/filter/suggestion_filter_spec.rb index e65a9214e76..12cd411a613 100644 --- a/spec/lib/banzai/filter/suggestion_filter_spec.rb +++ b/spec/lib/banzai/filter/suggestion_filter_spec.rb @@ -25,7 +25,7 @@ RSpec.describe Banzai::Filter::SuggestionFilter, feature_category: :team_plannin end context 'multi-line suggestions' do - let(:data_attr) { Banzai::Filter::SyntaxHighlightFilter::LANG_PARAMS_ATTR } + let(:data_attr) { Banzai::Filter::CodeLanguageFilter::LANG_PARAMS_ATTR } let(:input) { %(<pre class="code highlight js-syntax-highlight language-suggestion" #{data_attr}="-3+2"><code>foo\n</code></pre>) } it 'element has correct data-lang-params' do diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb index 0d7f322d08f..4aacebe6024 100644 --- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb +++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter, feature_category: :team_pl it "escapes HTML tags" do # This is how a script tag inside a code block is presented to this filter # after Markdown rendering. - result = filter(%{<pre lang="#{lang}"><code><script>alert(1)</script></code></pre>}) + result = filter(%{<pre data-canonical-lang="#{lang}"><code><script>alert(1)</script></code></pre>}) # `(1)` symbols are wrapped by lexer tags. expect(result.to_html).not_to match(%r{<script>alert.*</script>}) @@ -23,7 +23,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter, feature_category: :team_pl it "highlights as plaintext" do result = filter('<pre><code>def fun end</code></pre>') - expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre><copy-code></copy-code></div>') + expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre><copy-code></copy-code></div>') end include_examples "XSS prevention", "" @@ -31,9 +31,9 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter, feature_category: :team_pl context "when contains mermaid diagrams" do it "ignores mermaid blocks" do - result = filter('<pre data-mermaid-style="display" lang="mermaid"><code class="js-render-mermaid">mermaid code</code></pre>') + result = filter('<pre data-mermaid-style="display" data-canonical-lang="mermaid"><code class="js-render-mermaid">mermaid code</code></pre>') - expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre data-mermaid-style="display" lang="mermaid" class="code highlight js-syntax-highlight language-mermaid" v-pre="true"><code class="js-render-mermaid"><span id="LC1" class="line" lang="mermaid">mermaid code</span></code></pre><copy-code></copy-code></div>') + expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre data-mermaid-style="display" data-canonical-lang="mermaid" class="code highlight js-syntax-highlight language-mermaid" lang="mermaid" v-pre="true"><code class="js-render-mermaid"><span id="LC1" class="line" lang="mermaid">mermaid code</span></code></pre><copy-code></copy-code></div>') end end @@ -62,15 +62,15 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter, feature_category: :team_pl text = "<div>\n<pre><code>\nsomething\n<pre><code>else\n</code></pre></code></pre>\n</div>" result = filter(text) - expect(result.to_html.delete("\n")).to eq('<div><div class="gl-relative markdown-code-block js-markdown-code"><pre lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext"></span><span id="LC2" class="line" lang="plaintext">something</span><span id="LC3" class="line" lang="plaintext">else</span></code></pre><copy-code></copy-code></div></div>') + expect(result.to_html.delete("\n")).to eq('<div><div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext"></span><span id="LC2" class="line" lang="plaintext">something</span><span id="LC3" class="line" lang="plaintext">else</span></code></pre><copy-code></copy-code></div></div>') end end context "when a valid language is specified" do it "highlights as that language" do - result = filter('<pre lang="ruby"><code>def fun end</code></pre>') + result = filter('<pre data-canonical-lang="ruby"><code>def fun end</code></pre>') - expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre lang="ruby" class="code highlight js-syntax-highlight language-ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre><copy-code></copy-code></div>') + expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre data-canonical-lang="ruby" class="code highlight js-syntax-highlight language-ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre><copy-code></copy-code></div>') end include_examples "XSS prevention", "ruby" @@ -78,88 +78,40 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter, feature_category: :team_pl context "when an invalid language is specified" do it "highlights as plaintext" do - result = filter('<pre lang="gnuplot"><code>This is a test</code></pre>') + result = filter('<pre data-canonical-lang="gnuplot"><code>This is a test</code></pre>') - expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="gnuplot" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre><copy-code></copy-code></div>') + expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre data-canonical-lang="gnuplot" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre><copy-code></copy-code></div>') end include_examples "XSS prevention", "gnuplot" end context "languages that should be passed through" do - let(:delimiter) { described_class::LANG_PARAMS_DELIMITER } - let(:data_attr) { described_class::LANG_PARAMS_ATTR } - %w(math mermaid plantuml suggestion).each do |lang| context "when #{lang} is specified" do it "highlights as plaintext but with the correct language attribute and class" do - result = filter(%{<pre lang="#{lang}"><code>This is a test</code></pre>}) - copy_code_btn = '<copy-code></copy-code>' unless lang == 'suggestion' - - expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre lang="#{lang}" class="code highlight js-syntax-highlight language-#{lang}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>#{copy_code_btn}</div>}) - end - - include_examples "XSS prevention", lang - end - - context "when #{lang} has extra params" do - let(:lang_params) { 'foo-bar-kux' } - let(:xss_lang) { "#{lang} data-meta=\"foo-bar-kux\"<script>alert(1)</script>" } - - it "includes data-lang-params tag with extra information" do - result = filter(%{<pre lang="#{lang}" data-meta="#{lang_params}"><code>This is a test</code></pre>}) + result = filter(%{<pre data-canonical-lang="#{lang}"><code>This is a test</code></pre>}) copy_code_btn = '<copy-code></copy-code>' unless lang == 'suggestion' - expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre lang="#{lang}" class="code highlight js-syntax-highlight language-#{lang}" #{data_attr}="#{lang_params}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>#{copy_code_btn}</div>}) + expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre data-canonical-lang="#{lang}" class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>#{copy_code_btn}</div>}) end include_examples "XSS prevention", lang - - include_examples "XSS prevention", - "#{lang} data-meta=\"foo-bar-kux\"<script>alert(1)</script>" - - include_examples "XSS prevention", - "#{lang} data-meta=\"foo-bar-kux\"<script>alert(1)</script>" - end - end - - context 'when multiple param delimiters are used' do - let(:lang) { 'suggestion' } - let(:lang_params) { '-1+10' } - - let(:expected_result) do - %{<div class="gl-relative markdown-code-block js-markdown-code"><pre lang="#{lang}" class="code highlight js-syntax-highlight language-#{lang}" #{data_attr}="#{lang_params} more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre></div>} - end - - context 'when delimiter is space' do - it 'delimits on the first appearance' do - result = filter(%{<pre lang="#{lang}" data-meta="#{lang_params} more-things"><code>This is a test</code></pre>}) - - expect(result.to_html.delete("\n")).to eq(expected_result) - end - end - - context 'when delimiter is colon' do - it 'delimits on the first appearance' do - result = filter(%{<pre lang="#{lang}#{delimiter}#{lang_params} more-things"><code>This is a test</code></pre>}) - - expect(result.to_html.delete("\n")).to eq(expected_result) - end end end end context "when sourcepos metadata is available" do it "includes it in the highlighted code block" do - result = filter('<pre data-sourcepos="1:1-3:3"><code lang="plaintext">This is a test</code></pre>') + result = filter('<pre data-sourcepos="1:1-3:3" data-canonical-lang="plaintext"><code>This is a test</code></pre>') - expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre data-sourcepos="1:1-3:3" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code lang="plaintext"><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre><copy-code></copy-code></div>') + expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre data-sourcepos="1:1-3:3" data-canonical-lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre><copy-code></copy-code></div>') end it "escape sourcepos metadata to prevent XSS" do result = filter('<pre data-sourcepos=""%22 href="x"></pre><base href=http://unsafe-website.com/><pre x=""><code></code></pre>') - expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre data-sourcepos=\'"%22 href="x"></pre><base href=http://unsafe-website.com/><pre x="\' lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code></code></pre><copy-code></copy-code></div>') + expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre data-sourcepos=\'"%22 href="x"></pre><base href=http://unsafe-website.com/><pre x="\' class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code></code></pre><copy-code></copy-code></div>') end end @@ -171,9 +123,9 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter, feature_category: :team_pl end it "highlights as plaintext" do - result = filter('<pre lang="ruby"><code>This is a test</code></pre>') + result = filter('<pre data-canonical-lang="ruby"><code>This is a test</code></pre>') - expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre lang="" class="code highlight js-syntax-highlight" data-canonical-lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="">This is a test</span></code></pre><copy-code></copy-code></div>') + expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre data-canonical-lang="ruby" class="code highlight js-syntax-highlight" lang="" v-pre="true"><code><span id="LC1" class="line" lang="">This is a test</span></code></pre><copy-code></copy-code></div>') end include_examples "XSS prevention", "ruby" @@ -195,7 +147,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter, feature_category: :team_pl include_examples "XSS prevention", "ruby" end - it_behaves_like "filter timeout" do - let(:text) { '<pre lang="ruby"><code>def fun end</code></pre>' } + it_behaves_like "html filter timeout" do + let(:text) { '<pre data-canonical-lang="ruby"><code>def fun end</code></pre>' } end end diff --git a/spec/lib/banzai/filter/timeout_html_pipeline_filter_spec.rb b/spec/lib/banzai/filter/timeout_html_pipeline_filter_spec.rb index 066f59758f0..52e4b70cfe5 100644 --- a/spec/lib/banzai/filter/timeout_html_pipeline_filter_spec.rb +++ b/spec/lib/banzai/filter/timeout_html_pipeline_filter_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Banzai::Filter::TimeoutHtmlPipelineFilter, feature_category: :team_planning do include FilterSpecHelper - it_behaves_like 'filter timeout' do + it_behaves_like 'html filter timeout' do let(:text) { '<p>some text</p>' } end diff --git a/spec/lib/banzai/filter/timeout_text_pipeline_filter_spec.rb b/spec/lib/banzai/filter/timeout_text_pipeline_filter_spec.rb new file mode 100644 index 00000000000..4e22594389b --- /dev/null +++ b/spec/lib/banzai/filter/timeout_text_pipeline_filter_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::Filter::TimeoutTextPipelineFilter, feature_category: :team_planning do + include FilterSpecHelper + + it_behaves_like 'text filter timeout' do + let(:text) { '<p>some text</p>' } + end + + it 'raises NotImplementedError' do + expect { filter('test') }.to raise_error NotImplementedError + end +end diff --git a/spec/lib/banzai/issuable_extractor_spec.rb b/spec/lib/banzai/issuable_extractor_spec.rb index b2c869bd066..5bbd98592e7 100644 --- a/spec/lib/banzai/issuable_extractor_spec.rb +++ b/spec/lib/banzai/issuable_extractor_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Banzai::IssuableExtractor, feature_category: :team_planning do let(:user) { create(:user) } let(:extractor) { described_class.new(Banzai::RenderContext.new(project, user)) } let(:issue) { create(:issue, project: project) } + let(:work_item) { create(:work_item, project: project) } let(:merge_request) { create(:merge_request, source_project: project) } let(:issue_link) do html_to_node( @@ -14,6 +15,12 @@ RSpec.describe Banzai::IssuableExtractor, feature_category: :team_planning do ) end + let(:work_item_link) do + html_to_node( + "<a href='' data-work-item='#{work_item.id}' data-reference-type='work_item' class='gfm'>text</a>" + ) + end + let(:merge_request_link) do html_to_node( "<a href='' data-merge-request='#{merge_request.id}' data-reference-type='merge_request' class='gfm'>text</a>" @@ -27,17 +34,17 @@ RSpec.describe Banzai::IssuableExtractor, feature_category: :team_planning do end it 'returns instances of issuables for nodes with references' do - result = extractor.extract([issue_link, merge_request_link]) + result = extractor.extract([issue_link, work_item_link, merge_request_link]) - expect(result).to eq(issue_link => issue, merge_request_link => merge_request) + expect(result).to eq(issue_link => issue, work_item_link => work_item, merge_request_link => merge_request) end describe 'caching', :request_store do it 'saves records to cache' do - extractor.extract([issue_link, merge_request_link]) + extractor.extract([issue_link, work_item_link, merge_request_link]) second_call_queries = ActiveRecord::QueryRecorder.new do - extractor.extract([issue_link, merge_request_link]) + extractor.extract([issue_link, work_item_link, merge_request_link]) end.count expect(second_call_queries).to eq 0 diff --git a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb index d0b85a1d043..58d6b9b9a2c 100644 --- a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Banzai::Pipeline::GfmPipeline, feature_category: :team_planning do describe 'integration between parsing regular and external issue references' do - let(:project) { create(:project, :with_redmine_integration, :public) } + let_it_be(:project) { create(:project, :with_redmine_integration, :public) } context 'when internal issue tracker is enabled' do context 'when shorthand pattern #ISSUE_ID is used' do diff --git a/spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb b/spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb index 8d15dbc8f2f..12a6be6bc18 100644 --- a/spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb @@ -78,7 +78,7 @@ RSpec.describe Banzai::Pipeline::IncidentManagement::TimelineEventPipeline do it 'replaces existing label to a link' do # rubocop:disable Layout/LineLength is_expected.to match( - %r(<p>.+<a href="[\w/]+-/issues\?label_name=#{label.name}".+style="background-color: #\d{6}".*>#{label.name}</span></a></span> ~unknown</p>) + %r{<p>.+<a href="[\w\-/]+-/issues\?label_name=#{label.name}".+style="background-color: #\d{6}".*>#{label.name}</span></a></span> ~unknown</p>} ) # rubocop:enable Layout/LineLength end @@ -95,7 +95,7 @@ RSpec.describe Banzai::Pipeline::IncidentManagement::TimelineEventPipeline do let(:markdown) { "issue ##{issue.iid}" } it 'contains a link to the issue' do - is_expected.to match(%r(<p>issue <a href="[\w/]+-/issues/#{issue.iid}".*>##{issue.iid}</a></p>)) + is_expected.to match(%r{<p>issue <a href="[\w\-/]+-/issues/#{issue.iid}".*>##{issue.iid}</a></p>}) end end @@ -104,7 +104,7 @@ RSpec.describe Banzai::Pipeline::IncidentManagement::TimelineEventPipeline do let(:markdown) { "MR !#{mr.iid}" } it 'contains a link to the merge request' do - is_expected.to match(%r(<p>MR <a href="[\w/]+-/merge_requests/#{mr.iid}".*>!#{mr.iid}</a></p>)) + is_expected.to match(%r{<p>MR <a href="[\w\-/]+-/merge_requests/#{mr.iid}".*>!#{mr.iid}</a></p>}) 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 index e7c15ed9cf6..b8d2b6f7d7e 100644 --- a/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb @@ -80,7 +80,7 @@ RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline, feature_category: :team_ let(:markdown) { %Q(``` foo\\@bar\nfoo\n```) } it 'renders correct html' do - correct_html_included(markdown, %Q(<pre data-sourcepos="1:1-3:3" lang="foo@bar"><code>foo\n</code></pre>)) + correct_html_included(markdown, %Q(<pre lang="foo@bar"><code>foo\n</code></pre>)) end where(:markdown, :expected) do @@ -95,7 +95,7 @@ RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline, feature_category: :team_ end def correct_html_included(markdown, expected) - result = described_class.call(markdown, {}) + result = described_class.call(markdown, { no_sourcepos: true }) expect(result[:output].to_html).to include(expected) diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb index 837ea2d7bc0..81406eae0c9 100644 --- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' RSpec.describe Banzai::Pipeline::WikiPipeline, feature_category: :wiki do - let_it_be(:namespace) { create(:namespace, name: "wiki_link_ns") } - let_it_be(:project) { create(:project, :public, name: "wiki_link_project", namespace: namespace) } + let_it_be(:namespace) { create(:namespace) } + let_it_be(:project) { create(:project, :public, namespace: namespace) } let_it_be(:wiki) { ProjectWiki.new(project, nil) } let_it_be(:page) { build(:wiki_page, wiki: wiki, title: 'nested/twice/start-page') } @@ -85,14 +85,14 @@ RSpec.describe Banzai::Pipeline::WikiPipeline, feature_category: :wiki do markdown = "[Page](./page)" output = described_class.to_html(markdown, project: project, wiki: wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/page\"") + expect(output).to include("href=\"#{relative_url_root}/#{project.full_path}/-/wikis/nested/twice/page\"") end it "rewrites file links to be at the scope of the current directory" do markdown = "[Link to Page](./page.md)" output = described_class.to_html(markdown, project: project, wiki: wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/page.md\"") + expect(output).to include("href=\"#{relative_url_root}/#{project.full_path}/-/wikis/nested/twice/page.md\"") end end @@ -101,14 +101,14 @@ RSpec.describe Banzai::Pipeline::WikiPipeline, feature_category: :wiki do markdown = "[Link to Page](../page)" output = described_class.to_html(markdown, project: project, wiki: wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/page\"") + expect(output).to include("href=\"#{relative_url_root}/#{project.full_path}/-/wikis/nested/page\"") end it "rewrites file links to be at the scope of the parent directory" do markdown = "[Link to Page](../page.md)" output = described_class.to_html(markdown, project: project, wiki: wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/page.md\"") + expect(output).to include("href=\"#{relative_url_root}/#{project.full_path}/-/wikis/nested/page.md\"") end end @@ -117,14 +117,14 @@ RSpec.describe Banzai::Pipeline::WikiPipeline, feature_category: :wiki do markdown = "[Link to Page](./subdirectory/page)" output = described_class.to_html(markdown, project: project, wiki: wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/subdirectory/page\"") + expect(output).to include("href=\"#{relative_url_root}/#{project.full_path}/-/wikis/nested/twice/subdirectory/page\"") end it "rewrites file links to be at the scope of the sub-directory" do markdown = "[Link to Page](./subdirectory/page.md)" output = described_class.to_html(markdown, project: project, wiki: wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/subdirectory/page.md\"") + expect(output).to include("href=\"#{relative_url_root}/#{project.full_path}/-/wikis/nested/twice/subdirectory/page.md\"") end end @@ -133,35 +133,35 @@ RSpec.describe Banzai::Pipeline::WikiPipeline, feature_category: :wiki do markdown = "[Link to Page](page)" output = described_class.to_html(markdown, project: project, wiki: wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/page\"") + expect(output).to include("href=\"#{relative_url_root}/#{project.full_path}/-/wikis/page\"") end it 'rewrites non-file links (with spaces) to be at the scope of the wiki root' do markdown = "[Link to Page](page slug)" output = described_class.to_html(markdown, project: project, wiki: wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/page%20slug\"") + expect(output).to include("href=\"#{relative_url_root}/#{project.full_path}/-/wikis/page%20slug\"") end it "rewrites file links to be at the scope of the current directory" do markdown = "[Link to Page](page.md)" output = described_class.to_html(markdown, project: project, wiki: wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/page.md\"") + expect(output).to include("href=\"#{relative_url_root}/#{project.full_path}/-/wikis/nested/twice/page.md\"") end it 'rewrites links with anchor' do markdown = '[Link to Header](start-page#title)' output = described_class.to_html(markdown, project: project, wiki: wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/start-page#title\"") + expect(output).to include("href=\"#{relative_url_root}/#{project.full_path}/-/wikis/start-page#title\"") end it 'rewrites links (with spaces) with anchor' do markdown = '[Link to Header](start page#title)' output = described_class.to_html(markdown, project: project, wiki: wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/start%20page#title\"") + expect(output).to include("href=\"#{relative_url_root}/#{project.full_path}/-/wikis/start%20page#title\"") end end @@ -170,14 +170,14 @@ RSpec.describe Banzai::Pipeline::WikiPipeline, feature_category: :wiki do markdown = "[Link to Page](/page)" output = described_class.to_html(markdown, project: project, wiki: wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/page\"") + expect(output).to include("href=\"#{relative_url_root}/#{project.full_path}/-/wikis/page\"") end it 'rewrites file links to be at the scope of the wiki root' do markdown = "[Link to Page](/page.md)" output = described_class.to_html(markdown, project: project, wiki: wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/page.md\"") + expect(output).to include("href=\"#{relative_url_root}/#{project.full_path}/-/wikis/page.md\"") end end end @@ -278,28 +278,28 @@ RSpec.describe Banzai::Pipeline::WikiPipeline, feature_category: :wiki do markdown = "![video_file](video_file_name.mp4)" output = described_class.to_html(markdown, project: project, wiki: wiki, page_slug: page.slug) - expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/video_file_name.mp4"') + expect(output).to include(%(<video src="/#{project.full_path}/-/wikis/nested/twice/video_file_name.mp4")) end it 'rewrites and replaces video links names with white spaces to %20' do markdown = "![video file](video file name.mp4)" output = described_class.to_html(markdown, project: project, wiki: wiki, page_slug: page.slug) - expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/video%20file%20name.mp4"') + expect(output).to include(%(<video src="/#{project.full_path}/-/wikis/nested/twice/video%20file%20name.mp4")) end it 'generates audio html structure' do markdown = "![audio_file](audio_file_name.wav)" output = described_class.to_html(markdown, project: project, wiki: wiki, page_slug: page.slug) - expect(output).to include('<audio src="/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/audio_file_name.wav"') + expect(output).to include(%(<audio src="/#{project.full_path}/-/wikis/nested/twice/audio_file_name.wav")) end it 'rewrites and replaces audio links names with white spaces to %20' do markdown = "![audio file](audio file name.wav)" output = described_class.to_html(markdown, project: project, wiki: wiki, page_slug: page.slug) - expect(output).to include('<audio src="/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/audio%20file%20name.wav"') + expect(output).to include(%(<audio src="/#{project.full_path}/-/wikis/nested/twice/audio%20file%20name.wav")) end end @@ -320,7 +320,7 @@ RSpec.describe Banzai::Pipeline::WikiPipeline, feature_category: :wiki do output = described_class.to_html(markdown, project: project, wiki: wiki, page_slug: page.slug) doc = Nokogiri::HTML::DocumentFragment.parse(output) - full_path = "/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/#{wiki_file.path}" + full_path = "/#{project.full_path}/-/wikis/nested/twice/#{wiki_file.path}" expect(doc.css('a')[0].attr('href')).to eq(full_path) expect(doc.css('img')[0].attr('class')).to eq('gfm lazy') expect(doc.css('img')[0].attr('data-src')).to eq(full_path) diff --git a/spec/lib/banzai/reference_parser/commit_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_parser_spec.rb index 081bfa26fb2..7a1eed2e35e 100644 --- a/spec/lib/banzai/reference_parser/commit_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/commit_parser_spec.rb @@ -5,8 +5,9 @@ require 'spec_helper' RSpec.describe Banzai::ReferenceParser::CommitParser, feature_category: :source_code_management do include ReferenceParserHelpers - let(:project) { create(:project, :public) } - let(:user) { create(:user) } + let_it_be(:project) { create(:project, :public, :repository) } + let_it_be(:user) { create(:user) } + subject { described_class.new(Banzai::RenderContext.new(project, user)) } let(:link) { empty_html_link } @@ -130,20 +131,28 @@ RSpec.describe Banzai::ReferenceParser::CommitParser, feature_category: :source_ end describe '#find_commits' do - it 'returns an Array of commit objects' do - commit = double(:commit) + let_it_be(:ids) { project.repository.commits(project.default_branch, limit: 3).map(&:id) } + + it 'is empty when repo is invalid' do + allow(project).to receive(:valid_repo?).and_return(false) - expect(project).to receive(:commit).with('123').and_return(commit) - expect(project).to receive(:valid_repo?).and_return(true) + expect(subject.find_commits(project, ids)).to eq([]) + end - expect(subject.find_commits(project, %w{123})).to eq([commit]) + it 'returns commits by the specified ids' do + expect(subject.find_commits(project, ids).map(&:id)).to eq(%w[ + b83d6e391c22777fca1ed3012fce84f633d7fed0 + 498214de67004b1da3d820901307bed2a68a8ef6 + 1b12f15a11fc6e62177bef08f47bc7b5ce50b141 + ]) end - it 'skips commit IDs for which no commit could be found' do - expect(project).to receive(:commit).with('123').and_return(nil) - expect(project).to receive(:valid_repo?).and_return(true) + it 'is limited' do + stub_const("#{described_class}::COMMITS_LIMIT", 1) - expect(subject.find_commits(project, %w{123})).to eq([]) + expect(subject.find_commits(project, ids).map(&:id)).to eq([ + "b83d6e391c22777fca1ed3012fce84f633d7fed0" + ]) end end diff --git a/spec/lib/banzai/reference_parser/work_item_parser_spec.rb b/spec/lib/banzai/reference_parser/work_item_parser_spec.rb new file mode 100644 index 00000000000..dbde01cc94f --- /dev/null +++ b/spec/lib/banzai/reference_parser/work_item_parser_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::ReferenceParser::WorkItemParser, feature_category: :team_planning do + include ReferenceParserHelpers + + let_it_be(:group) { create(:group, :public) } + let_it_be_with_reload(:project) { create(:project, :public, group: group) } + let_it_be(:user) { create(:user) } + let_it_be(:work_item) { create(:work_item, project: project) } + let_it_be(:link) { empty_html_link } + + subject { described_class.new(Banzai::RenderContext.new(project, user)) } + + describe '#records_for_nodes' do + it 'returns a Hash containing the work items for a list of nodes' do + link['data-work-item'] = work_item.id.to_s + nodes = [link] + + expect(subject.records_for_nodes(nodes)).to eq({ link => work_item }) + end + end + + context 'when checking multiple work items on another project' do + let_it_be(:other_project) { create(:project, :public) } + let_it_be(:other_work_item) { create(:work_item, project: other_project) } + let_it_be(:control_links) do + [work_item_link(other_work_item)] + end + + let_it_be(:actual_links) do + control_links + [work_item_link(create(:work_item, project: other_project))] + end + + def work_item_link(work_item) + Nokogiri::HTML.fragment(%(<a data-work-item="#{work_item.id}"></a>)).children[0] + end + + before do + project.add_developer(user) + end + + it_behaves_like 'no N+1 queries' + end +end diff --git a/spec/lib/banzai/reference_redactor_spec.rb b/spec/lib/banzai/reference_redactor_spec.rb index 8a8f3ce586a..21736903cbf 100644 --- a/spec/lib/banzai/reference_redactor_spec.rb +++ b/spec/lib/banzai/reference_redactor_spec.rb @@ -111,13 +111,16 @@ RSpec.describe Banzai::ReferenceRedactor, feature_category: :team_planning do def create_link(issuable) type = issuable.class.name.underscore.downcase - ActionController::Base.helpers.link_to(issuable.to_reference, '', - class: 'gfm has-tooltip', - title: issuable.title, - data: { - reference_type: type, - "#{type}": issuable.id - }) + ActionController::Base.helpers.link_to( + issuable.to_reference, + '', + class: 'gfm has-tooltip', + title: issuable.title, + data: { + reference_type: type, + "#{type}": issuable.id + } + ) end before do diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb index 8c9d8d51d5f..ef503b8ec52 100644 --- a/spec/lib/banzai/renderer_spec.rb +++ b/spec/lib/banzai/renderer_spec.rb @@ -87,15 +87,11 @@ RSpec.describe Banzai::Renderer, feature_category: :team_planning do describe '#cacheless_render' do context 'without cache' do let(:object) { fake_object(fresh: false) } - let(:histogram) { double('prometheus histogram') } it 'returns cacheless render field' do allow(renderer).to receive(:render_result).and_return(output: 'test') - allow(renderer).to receive(:real_duration_histogram).and_return(histogram) - allow(renderer).to receive(:cpu_duration_histogram).and_return(histogram) expect(renderer).to receive(:render_result).with('test', {}) - expect(histogram).to receive(:observe).twice renderer.cacheless_render('test') end |