Commit d4f0c229 authored by Alan (Maciej) Paruszewski's avatar Alan (Maciej) Paruszewski Committed by Dmytro Zaporozhets (DZ)

Enable autocomplete special references for Vulnerabilities

This change adds autocomplete functionality in markdown editor to
reference Vulnerabilities using + special reference.
parent fcd510fa
......@@ -34,7 +34,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
private
def autocomplete_service
@autocomplete_service ||= ::Projects::AutocompleteService.new(@project, current_user)
@autocomplete_service ||= ::Projects::AutocompleteService.new(@project, current_user, params)
end
def target
......
......@@ -159,6 +159,7 @@ module NotesHelper
members: autocomplete,
issues: autocomplete,
mergeRequests: autocomplete,
vulnerabilities: autocomplete,
epics: autocomplete,
milestones: autocomplete,
labels: autocomplete
......
......@@ -22,7 +22,7 @@ module Mentionable
def self.default_pattern
strong_memoize(:default_pattern) do
issue_pattern = Issue.reference_pattern
link_patterns = Regexp.union([Issue, Commit, MergeRequest, Epic].map(&:link_reference_pattern).compact)
link_patterns = Regexp.union([Issue, Commit, MergeRequest, Epic, Vulnerability].map(&:link_reference_pattern).compact)
reference_pattern(link_patterns, issue_pattern)
end
end
......
......@@ -5,6 +5,10 @@
class Vulnerability < ApplicationRecord
include IgnorableColumns
def self.link_reference_pattern
nil
end
def self.reference_prefix
'+'
end
......
......@@ -427,6 +427,7 @@ GFM recognizes the following:
| merge request | `!123` | `namespace/project!123` | `project!123` |
| snippet | `$123` | `namespace/project$123` | `project$123` |
| epic **(ULTIMATE)** | `&123` | `group1/subgroup&123` | |
| vulnerability **(ULTIMATE)** | `+123` | `namespace/project+123` | `project+123` |
| label by ID | `~123` | `namespace/project~123` | `project~123` |
| one-word label by name | `~bug` | `namespace/project~bug` | `project~bug` |
| multi-word label by name | `~"feature request"` | `namespace/project~"feature request"` | `project~"feature request"` |
......
......@@ -10,6 +10,12 @@ module EE
render json: autocomplete_service.epics
end
def vulnerabilities
return render_404 unless project.feature_available?(:security_dashboard)
render json: autocomplete_service.vulnerabilities
end
end
end
end
......@@ -7,6 +7,7 @@ class Groups::AutocompleteSourcesController < Groups::ApplicationController
feature_category :issue_tracking, [:issues, :labels, :milestones, :commands]
feature_category :code_review, [:merge_requests]
feature_category :epics, [:epics]
feature_category :vulnerability_management, [:vulnerabilities]
def members
render json: ::Groups::ParticipantsService.new(@group, current_user).execute(target)
......@@ -31,6 +32,10 @@ class Groups::AutocompleteSourcesController < Groups::ApplicationController
render json: @autocomplete_service.epics(confidential_only: params[:confidential_only])
end
def vulnerabilities
render json: issuable_serializer.represent(@autocomplete_service.vulnerabilities, parent_group: @group)
end
def commands
render json: @autocomplete_service.commands(target)
end
......@@ -42,7 +47,7 @@ class Groups::AutocompleteSourcesController < Groups::ApplicationController
private
def load_autocomplete_service
@autocomplete_service = ::Groups::AutocompleteService.new(@group, current_user)
@autocomplete_service = ::Groups::AutocompleteService.new(@group, current_user, params)
end
def issuable_serializer
......
......@@ -20,10 +20,10 @@ module Autocomplete
DEFAULT_AUTOCOMPLETE_LIMIT = 5
def execute
return [] unless vulnerable.feature_available?(:security_dashboard)
return ::Vulnerability.none unless vulnerable.feature_available?(:security_dashboard)
::Security::VulnerabilitiesFinder # rubocop: disable CodeReuse/Finder
.new(vulnerable, params)
.new(vulnerable)
.execute
.autocomplete_search(params[:search].to_s)
.with_limit(DEFAULT_AUTOCOMPLETE_LIMIT)
......
......@@ -92,13 +92,15 @@ module EE
issues: issues_group_autocomplete_sources_path(object),
mergeRequests: merge_requests_group_autocomplete_sources_path(object),
epics: epics_group_autocomplete_sources_path(object),
vulnerabilities: vulnerabilities_group_autocomplete_sources_path(object),
commands: commands_group_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
milestones: milestones_group_autocomplete_sources_path(object)
}
elsif object.group&.feature_available?(:epics)
{ epics: epics_project_autocomplete_sources_path(object) }.merge(super)
else
super
{
epics: object.group&.feature_available?(:epics) ? epics_project_autocomplete_sources_path(object) : nil,
vulnerabilities: object.feature_available?(:security_dashboard) ? vulnerabilities_project_autocomplete_sources_path(object) : nil
}.compact.merge(super)
end
end
......
......@@ -11,7 +11,8 @@ module EE
override :other_patterns
def other_patterns
super.unshift(
::Epic.reference_pattern
::Epic.reference_pattern,
::Vulnerability.reference_pattern
)
end
end
......
......@@ -75,6 +75,7 @@ module EE
scope :with_created_issue_links_and_issues, -> { includes(created_issue_links: :issue) }
scope :visible_to_user_and_access_level, -> (user, access_level) { where(project_id: ::Project.visible_to_user_and_access_level(user, access_level)) }
scope :for_ids, -> (ids) { where(id: ids) }
scope :for_projects, -> (project_ids) { where(project_id: project_ids) }
scope :with_report_types, -> (report_types) { where(report_type: report_types) }
scope :with_severities, -> (severities) { where(severity: severities) }
......@@ -168,6 +169,29 @@ module EE
end
class_methods do
def reference_pattern
@reference_pattern ||= %r{
(#{::Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<vulnerability>\d+)
}x
end
def link_reference_pattern
%r{
(?<url>
#{Regexp.escape(::Gitlab.config.gitlab.url)}
\/#{::Project.reference_pattern}
(?:\/\-)
\/security\/vulnerabilities
\/(?<vulnerability>\d+)
(?<path>
(\/[a-z0-9_=-]+)*\/*
)?
(?<anchor>\#[a-z0-9_-]+)?
)
}x
end
def parent_class
::Project
end
......
# frozen_string_literal: true
class GroupIssuableAutocompleteEntity < Grape::Entity
expose :iid
expose :iid, if: -> (e, _) { e.respond_to?(:iid) }
expose :id, if: -> (e, _) { !e.respond_to?(:iid) }
expose :title
expose :reference do |issuable, options|
issuable.to_reference(options[:parent_group])
......
......@@ -7,6 +7,13 @@ module EE
.new(current_user, group_id: project.group&.id, state: 'opened')
.execute.select([:iid, :title])
end
def vulnerabilities
::Autocomplete::VulnerabilitiesAutocompleteFinder
.new(current_user, project, params)
.execute
.select([:id, :title])
end
end
end
end
......@@ -37,6 +37,13 @@ module Groups
.select(:iid, :title)
end
def vulnerabilities
::Autocomplete::VulnerabilitiesAutocompleteFinder
.new(current_user, group, params)
.execute
.select([:id, :title, :project_id])
end
# rubocop: disable CodeReuse/ActiveRecord
def milestones
group_ids = group.self_and_ancestors.public_or_visible_to_user(current_user).pluck(:id)
......
---
title: Enable Special References for Vulnerabilities
merge_request: 41983
author:
type: added
......@@ -85,6 +85,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
get 'merge_requests'
get 'labels'
get 'epics'
get 'vulnerabilities'
get 'commands'
get 'milestones'
end
......
......@@ -22,6 +22,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :autocomplete_sources, only: [] do
collection do
get 'epics'
get 'vulnerabilities'
end
end
......
# frozen_string_literal: true
module EE
module Banzai
module Filter
# HTML filter that replaces vulnerability references with links. References to
# vulnerabilities that do not exist are ignored.
#
# This filter supports cross-project/group references.
module VulnerabilityReferenceFilter
extend ActiveSupport::Concern
class_methods do
def references_in(text, pattern = object_class.reference_pattern)
text.gsub(pattern) do |match|
symbol = $~[object_sym]
if object_class.reference_valid?(symbol)
yield match, symbol.to_i, $~[:project], $~[:namespace], $~
else
match
end
end
end
end
def unescape_link(href)
return href if href =~ object_class.reference_pattern
super
end
def url_for_object(vulnerability, project)
urls = ::Gitlab::Routing.url_helpers
urls.project_security_vulnerability_url(project, vulnerability, only_path: context[:only_path])
end
def data_attributes_for(text, project, object, link_content: false, link_reference: false)
{
original: escape_html_entities(text),
link: link_content,
link_reference: link_reference,
project: project.id,
object_sym => object.id
}
end
def parent_records(parent, ids)
return Vulnerabilities.none if ids.blank? || parent.nil?
parent.vulnerabilities.for_ids(ids.to_a)
end
def record_identifier(record)
record.id.to_i
end
private
def parent_type
:project
end
end
end
end
end
......@@ -4,15 +4,20 @@ module EE
module Banzai
module IssuableExtractor
EPIC_REFERENCE_TYPE = '@data-reference-type="epic"'.freeze
VULNERABILITY_REFERENCE_TYPE = '@data-reference-type="vulnerability"'.freeze
private
def reference_types
super.push(EPIC_REFERENCE_TYPE)
super
.push(EPIC_REFERENCE_TYPE)
.push(VULNERABILITY_REFERENCE_TYPE)
end
def parsers
super.push(::Banzai::ReferenceParser::EpicParser.new(context))
super
.push(::Banzai::ReferenceParser::EpicParser.new(context))
.push(::Banzai::ReferenceParser::VulnerabilityParser.new(context))
end
end
end
......
......@@ -18,6 +18,7 @@ module EE
[
::Banzai::Filter::EpicReferenceFilter,
::Banzai::Filter::IterationReferenceFilter,
::Banzai::Filter::VulnerabilityReferenceFilter,
*super
]
end
......
......@@ -11,6 +11,7 @@ module EE
[
::Banzai::Filter::EpicReferenceFilter,
::Banzai::Filter::IterationReferenceFilter,
::Banzai::Filter::VulnerabilityReferenceFilter,
*super
]
end
......
# frozen_string_literal: true
module EE
module Banzai
module ReferenceParser
module VulnerabilityParser
def references_relation
Vulnerability
end
# rubocop: disable CodeReuse/ActiveRecord
def records_for_nodes(nodes)
@vulnerabilities_for_nodes ||= grouped_objects_for_nodes(
nodes,
::Vulnerability.includes(
:author,
:project
),
self.class.data_attribute
)
end
# rubocop: enable CodeReuse/ActiveRecord
def can_read_reference?(user, vulnerability)
can?(user, :read_vulnerability, vulnerability)
end
end
end
end
end
......@@ -8,34 +8,67 @@ RSpec.describe Projects::AutocompleteSourcesController do
let_it_be(:project) { create(:project, :public, group: group) }
let_it_be(:epic) { create(:epic, group: group) }
let_it_be(:epic2) { create(:epic, group: group2) }
let_it_be(:vulnerability) { create(:vulnerability, project: project) }
before do
sign_in(user)
end
context 'when epics feature is disabled' do
it 'returns 404 status' do
get :epics, params: { namespace_id: project.namespace, project_id: project }
describe '#epics' do
context 'when epics feature is disabled' do
it 'returns 404 status' do
get :epics, params: { namespace_id: project.namespace, project_id: project }
expect(response).to have_gitlab_http_status(:not_found)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when epics feature is enabled' do
before do
stub_licensed_features(epics: true)
end
describe '#epics' do
it 'returns the correct response' do
get :epics, params: { namespace_id: project.namespace, project_id: project }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an(Array)
expect(json_response.count).to eq(1)
expect(json_response.first).to include(
'iid' => epic.iid, 'title' => epic.title
)
end
end
end
end
context 'when epics feature is enabled' do
before do
stub_licensed_features(epics: true)
describe '#vulnerabilities' do
context 'when vulnerabilities feature is disabled' do
it 'returns 404 status' do
get :vulnerabilities, params: { namespace_id: project.namespace, project_id: project }
expect(response).to have_gitlab_http_status(:not_found)
end
end
describe '#epics' do
it 'returns the correct response' do
get :epics, params: { namespace_id: project.namespace, project_id: project }
context 'when vulnerabilities feature is enabled' do
before do
stub_licensed_features(security_dashboard: true)
project.add_developer(user)
end
describe '#vulnerabilities' do
it 'returns the correct response', :aggregate_failures do
get :vulnerabilities, params: { namespace_id: project.namespace, project_id: project }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an(Array)
expect(json_response.count).to eq(1)
expect(json_response.first).to include(
'iid' => epic.iid, 'title' => epic.title
)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an(Array)
expect(json_response.count).to eq(1)
expect(json_response.first).to include(
'id' => vulnerability.id, 'title' => vulnerability.title
)
end
end
end
end
......
......@@ -33,6 +33,31 @@ RSpec.describe Groups::AutocompleteSourcesController do
end
end
describe '#vulnerabilities' do
let_it_be_with_reload(:project) { create(:project, :private, group: group) }
let_it_be(:vulnerability) { create(:vulnerability, project: project) }
before do
project.add_developer(user)
stub_licensed_features(security_dashboard: true)
end
it 'returns 200 status' do
get :vulnerabilities, params: { group_id: group }
expect(response).to have_gitlab_http_status(:ok)
end
it 'returns the correct response', :aggregate_failures do
get :vulnerabilities, params: { group_id: group }
expect(json_response).to be_an(Array)
expect(json_response.first).to include(
'id' => vulnerability.id, 'title' => vulnerability.title
)
end
end
describe '#issues' do
using RSpec::Parameterized::TableSyntax
......
......@@ -103,7 +103,7 @@ RSpec.describe ApplicationHelper do
let(:noteable_type) { Epic }
it 'returns paths for autocomplete_sources_controller' do
expect_autocomplete_data_sources(object, noteable_type, [:members, :issues, :mergeRequests, :labels, :epics, :commands, :milestones])
expect_autocomplete_data_sources(object, noteable_type, [:members, :issues, :mergeRequests, :labels, :epics, :vulnerabilities, :commands, :milestones])
end
end
......@@ -127,7 +127,23 @@ RSpec.describe ApplicationHelper do
end
end
context 'when epics are disabled' do
context 'when vulnerabilities are enabled' do
before do
stub_licensed_features(security_dashboard: true)
end
it 'returns paths for autocomplete_sources_controller for personal projects' do
expect_autocomplete_data_sources(object, noteable_type, [:members, :issues, :mergeRequests, :labels, :milestones, :commands, :snippets, :vulnerabilities])
end
it 'returns paths for autocomplete_sources_controller including vulnerabilities for group projects' do
object.update_column(:namespace_id, create(:group).id)
expect_autocomplete_data_sources(object, noteable_type, [:members, :issues, :mergeRequests, :labels, :milestones, :commands, :snippets, :vulnerabilities])
end
end
context 'when epics and vulnerabilities are disabled' do
it 'returns paths for autocomplete_sources_controller' do
expect_autocomplete_data_sources(object, noteable_type, [:members, :issues, :mergeRequests, :labels, :milestones, :commands, :snippets])
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Banzai::Filter::VulnerabilityReferenceFilter do
include FilterSpecHelper
let(:urls) { Gitlab::Routing.url_helpers }
let(:project) { create(:project) }
let(:another_project) { create(:project) }
let(:vulnerability) { create(:vulnerability, project: project) }
let(:full_ref_text) { "Check #{vulnerability.project.full_path}+#{vulnerability.id}" }
def doc(reference = nil)
reference ||= "Check +#{vulnerability.id}"
context = { project: project, group: nil }
reference_filter(reference, context)
end
context 'internal reference' do
let(:reference) { "+#{vulnerability.id}" }
it 'links to a valid reference' do
expect(doc.css('a').first.attr('href')).to eq(urls.project_security_vulnerability_url(project, vulnerability))
end
it 'links with adjacent text' do
expect(doc.text).to eq("Check #{reference}")
end
it 'includes a title attribute' do
expect(doc.css('a').first.attr('title')).to eq(vulnerability.title)
end
it 'escapes the title attribute' do
vulnerability.update_column(:title, %{"></a>whatever<a title="})
expect(doc.text).to eq("Check #{reference}")
end
it 'includes default classes' do
expect(doc.css('a').first.attr('class')).to eq('gfm gfm-vulnerability has-tooltip')
end
it 'includes a data-project attribute' do
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-vulnerability attribute' do
link = doc.css('a').first
expect(link).to have_attribute('data-vulnerability')
expect(link.attr('data-vulnerability')).to eq(vulnerability.id.to_s)
end
it 'includes a data-original attribute' do
link = doc.css('a').first
expect(link).to have_attribute('data-original')
expect(link.attr('data-original')).to eq(CGI.escapeHTML(reference))
end
it 'ignores invalid vulnerability IDs' do
text = "Check +#{non_existing_record_id}"
expect(doc(text).to_s).to eq(ERB::Util.html_escape_once(text))
end
it 'ignores out of range vulnerability IDs' do
text = "Check &1161452270761535925900804973910297"
expect(doc(text).to_s).to eq(ERB::Util.html_escape_once(text))
end
it 'does not process links containing vulnerability numbers followed by text' do
href = "#{reference}st"
link = doc("<a href='#{href}'></a>").css('a').first.attr('href')
expect(link).to eq(href)
end
end
context 'internal escaped reference' do
let(:reference) { "+us;#{vulnerability.id}" }
it 'links to a valid reference' do
expect(doc.css('a').first.attr('href')).to eq(urls.project_security_vulnerability_url(project, vulnerability))
end
it 'includes a title attribute' do
expect(doc.css('a').first.attr('title')).to eq(vulnerability.title)
end
it 'includes default classes' do
expect(doc.css('a').first.attr('class')).to eq('gfm gfm-vulnerability has-tooltip')
end
it 'ignores invalid vulnerability IDs' do
text = "Check +#{non_existing_record_id}"
expect(doc(text).to_s).to eq(ERB::Util.html_escape_once(text))
end
end
context 'cross-reference' do
before do
vulnerability.update_column(:project_id, another_project.id)
end
it 'ignores a shorthand reference from another group' do
text = "Check +#{vulnerability.id}"
expect(doc(text).to_s).to eq(ERB::Util.html_escape_once(text))
end
it 'links to a valid reference for full reference' do
expect(doc(full_ref_text).css('a').first.attr('href')).to eq(urls.project_security_vulnerability_url(another_project, vulnerability))
end
it 'link has valid text' do
expect(doc(full_ref_text).css('a').first.text).to eq("#{vulnerability.project.full_path}+#{vulnerability.id}")
end
it 'includes default classes' do
expect(doc(full_ref_text).css('a').first.attr('class')).to eq('gfm gfm-vulnerability has-tooltip')
end
end
context 'escaped cross-reference' do
before do
vulnerability.update_column(:project_id, another_project.id)
end
it 'ignores a shorthand reference from another group' do
text = "Check +#{vulnerability.id}"
expect(doc(text).to_s).to eq(ERB::Util.html_escape_once(text))
end
it 'links to a valid reference for full reference' do
expect(doc(full_ref_text).css('a').first.attr('href')).to eq(urls.project_security_vulnerability_url(another_project, vulnerability))
end
it 'link has valid text' do
expect(doc(full_ref_text).css('a').first.text).to eq("#{vulnerability.project.full_path}+#{vulnerability.id}")
end
it 'includes default classes' do
expect(doc(full_ref_text).css('a').first.attr('class')).to eq('gfm gfm-vulnerability has-tooltip')
end
end
context 'url reference' do
let(:link) { urls.project_security_vulnerability_url(vulnerability.project, vulnerability) }
let(:text) { "Check #{link}" }
let(:project) { create(:project) }
before do
vulnerability.update_column(:project_id, another_project.id)
end
it 'links to a valid reference' do
expect(doc(text).css('a').first.attr('href')).to eq(urls.project_security_vulnerability_url(another_project, vulnerability))
end
it 'link has valid text' do
expect(doc(text).css('a').first.text).to eq(vulnerability.to_reference(project))
end
it 'includes default classes' do
expect(doc(text).css('a').first.attr('class')).to eq('gfm gfm-vulnerability has-tooltip')
end
it 'matches link reference with trailing slash' do
doc2 = reference_filter("Fixed (#{link}/.)")
expect(doc2).to match(%r{\(#{Regexp.escape(vulnerability.to_reference(project))}\.\)})
end
end
context 'url in a link href' do
let(:link) { urls.project_security_vulnerability_url(vulnerability.project, vulnerability) }
let(:text) do
ref = %{<a href="#{link}">Reference</a>}
"Check #{ref}"
end
before do
vulnerability.update_column(:project_id, another_project.id)
end
it 'links to a valid reference for link href' do
expect(doc(text).css('a').first.attr('href')).to eq(urls.project_security_vulnerability_url(another_project, vulnerability))
end
it 'link has valid text' do
expect(doc(text).css('a').first.text).to eq('Reference')
end
it 'includes default classes' do
expect(doc(text).css('a').first.attr('class')).to eq('gfm gfm-vulnerability has-tooltip')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Banzai::ReferenceParser::VulnerabilityParser do
include ReferenceParserHelpers
def link(vulnerability_id)
link = empty_html_link
link['data-vulnerability'] = vulnerability_id.to_s
link
end
let(:user) { create(:user) }
let(:public_project) { create(:project, :public) }
let(:private_project1) { create(:project, :private) }
let(:private_project2) { create(:project, :private) }
let(:vulnerability) { create(:vulnerability, project: public_project) }
let(:vulnerability1) { create(:vulnerability, project: private_project1) }
let(:vulnerability2) { create(:vulnerability, project: private_project2) }
let(:nodes) do
[link(vulnerability.id), link(vulnerability1.id), link(vulnerability2.id)]
end
subject { described_class.new(Banzai::RenderContext.new(nil, user)) }
describe '#nodes_visible_to_user' do
before do
private_project1.add_developer(user)
end
context 'when the vulnerabilities feature is enabled' do
before do
stub_licensed_features(security_dashboard: true)
end
it 'returns the nodes the user can read for valid vulnerability nodes' do
expected_result = [nodes[1]]
expect(subject.nodes_visible_to_user(user, nodes)).to match_array(expected_result)
end
it 'returns an empty array for nodes without required data-attributes' do
expect(subject.nodes_visible_to_user(user, [empty_html_link])).to be_empty
end
end
context 'when the vulnerabilities feature is disabled' do
it 'returns an empty array' do
expect(subject.nodes_visible_to_user(user, nodes)).to be_empty
end
end
end
describe '#referenced_by' do
context 'when using an existing vulnerabilities IDs' do
it 'returns an Array of vulnerabilities' do
expected_result = [vulnerability, vulnerability1, vulnerability2]
expect(subject.referenced_by(nodes)).to match_array(expected_result)
end
it 'returns an empty Array for empty list of nodes' do
expect(subject.referenced_by([])).to be_empty
end
end
context 'when vulnerability with given ID does not exist' do
it 'returns an empty Array' do
expect(subject.referenced_by([link(non_existing_record_id)])).to be_empty
end
end
end
describe '#records_for_nodes' do
it 'returns a Hash containing the vulnerabilities for a list of nodes' do
expected_hash = {
nodes[0] => vulnerability,
nodes[1] => vulnerability1,
nodes[2] => vulnerability2
}
expect(subject.records_for_nodes(nodes)).to eq(expected_hash)
end
end
end
......@@ -25,4 +25,18 @@ RSpec.describe Gitlab::ReferenceExtractor do
expect(subject.epics).to match_array([@e0, @e1])
end
it 'accesses valid vulnerabilities' do
stub_licensed_features(security_dashboard: true)
vulnerability_0 = create(:vulnerability, project: project)
vulnerability_1 = create(:vulnerability, project: project)
vulnerability_2 = create(:vulnerability, project: create(:project, :private))
text = "#{vulnerability_0.to_reference(project, full: true)}, &#{non_existing_record_iid}, #{vulnerability_1.to_reference(project, full: true)}, #{vulnerability_2.to_reference(project, full: true)}"
subject.analyze(text, { project: project })
expect(subject.vulnerabilities).to match_array([vulnerability_0, vulnerability_1])
end
end
......@@ -157,6 +157,18 @@ RSpec.describe Vulnerability do
end
end
describe '.for_ids' do
let(:project) { create(:project) }
let!(:vulnerability1) { create(:vulnerability, project: project) }
let!(:vulnerability2) { create(:vulnerability, project: project) }
subject { described_class.for_ids([vulnerability1.id, vulnerability2.id]) }
it 'returns vulnerabilities with given IDs' do
is_expected.to contain_exactly(vulnerability1, vulnerability2)
end
end
describe '.for_projects' do
let(:project1) { create(:project) }
let(:project2) { create(:project) }
......@@ -498,6 +510,23 @@ RSpec.describe Vulnerability do
it { is_expected.to eq('+') }
end
describe '.reference_pattern' do
subject { described_class.reference_pattern }
it { is_expected.to match('+123') }
it { is_expected.to match('gitlab-ce+123') }
it { is_expected.to match('gitlab-org/gitlab-ce+123') }
end
describe '.link_reference_pattern' do
subject { described_class.link_reference_pattern }
it { is_expected.to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-foss/-/security/vulnerabilities/123") }
it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-foss/security/vulnerabilities/123") }
it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-foss/issues/123") }
it { is_expected.not_to match("gitlab-org/gitlab-ce/milestones/123") }
end
describe '#to_reference' do
let(:namespace) { build(:namespace, path: 'sample-namespace') }
let(:project) { build(:project, name: 'sample-project', namespace: namespace) }
......
......@@ -6,12 +6,23 @@ RSpec.describe GroupIssuableAutocompleteEntity do
let(:group) { build_stubbed(:group) }
let(:project) { build_stubbed(:project, group: group) }
let(:issue) { build_stubbed(:issue, project: project) }
subject { described_class.new(issue, parent_group: group).as_json }
let(:vulnerability) { build_stubbed(:vulnerability, project: project) }
describe '#represent' do
it 'includes the iid, title, and reference' do
expect(subject).to include(:iid, :title, :reference)
context 'when issuable responds to iid' do
subject { described_class.new(issue, parent_group: group).as_json }
it 'includes the iid, title, and reference' do
expect(subject).to include(:iid, :title, :reference)
end
end
context 'when issuable does not respond to iid' do
subject { described_class.new(vulnerability, parent_group: project).as_json }
it 'includes the id, title, and reference' do
expect(subject).to include(:id, :title, :reference)
end
end
end
end
......@@ -3,8 +3,8 @@
require 'spec_helper'
RSpec.describe Groups::AutocompleteService do
let!(:group) { create(:group, :nested, :private, avatar: fixture_file_upload('spec/fixtures/dk.png')) }
let!(:sub_group) { create(:group, :private, parent: group) }
let_it_be(:group, refind: true) { create(:group, :nested, :private, avatar: fixture_file_upload('spec/fixtures/dk.png')) }
let_it_be(:sub_group) { create(:group, :private, parent: group) }
let(:user) { create(:user) }
let!(:epic) { create(:epic, group: group, author: user) }
......@@ -118,6 +118,28 @@ RSpec.describe Groups::AutocompleteService do
end
end
describe '#vulnerability' do
let_it_be(:project) { create(:project, group: group) }
let_it_be(:vulnerability) { create(:vulnerability, project: project) }
before do
stub_licensed_features(security_dashboard: true)
project.add_developer(user)
end
it 'returns nothing if not allowed' do
guest = create(:user)
vulnerabilities = described_class.new(group, guest).vulnerabilities
expect(vulnerabilities).to be_empty
end
it 'returns vulnerabilities from group' do
expect(subject.vulnerabilities.map(&:id)).to contain_exactly(vulnerability.id)
end
end
describe '#commands' do
context 'when target is an epic' do
let(:parent_epic) { create(:epic, group: group, author: user) }
......
......@@ -119,7 +119,7 @@ module Banzai
# Yields the link's URL and inner HTML whenever the node is a valid <a> tag.
def yield_valid_link(node)
link = CGI.unescape(node.attr('href').to_s)
link = unescape_link(node.attr('href').to_s)
inner_html = node.inner_html
return unless link.force_encoding('UTF-8').valid_encoding?
......@@ -127,6 +127,10 @@ module Banzai
yield link, inner_html
end
def unescape_link(href)
CGI.unescape(href)
end
def replace_text_when_pattern_matches(node, index, pattern)
return unless node.text =~ pattern
......
# frozen_string_literal: true
module Banzai
module Filter
# The actual filter is implemented in the EE mixin
class VulnerabilityReferenceFilter < IssuableReferenceFilter
self.reference_type = :vulnerability
def self.object_class
Vulnerability
end
private
def project
context[:project]
end
end
end
end
Banzai::Filter::VulnerabilityReferenceFilter.prepend_if_ee('EE::Banzai::Filter::VulnerabilityReferenceFilter')
# frozen_string_literal: true
module Banzai
module ReferenceParser
# The actual parser is implemented in the EE mixin
class VulnerabilityParser < IssuableParser
self.reference_type = :vulnerability
def records_for_nodes(_nodes)
{}
end
end
end
end
Banzai::ReferenceParser::VulnerabilityParser.prepend_if_ee('::EE::Banzai::ReferenceParser::VulnerabilityParser')
......@@ -4,7 +4,7 @@ module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor < Banzai::ReferenceExtractor
REFERABLES = %i(user issue label milestone mentioned_user mentioned_group mentioned_project
merge_request snippet commit commit_range directly_addressed_user epic iteration).freeze
merge_request snippet commit commit_range directly_addressed_user epic iteration vulnerability).freeze
attr_accessor :project, :current_user, :author
# This counter is increased by a number of references filtered out by
# banzai reference exctractor. Note that this counter is stateful and
......@@ -38,7 +38,7 @@ module Gitlab
end
REFERABLES.each do |type|
define_method("#{type}s") do
define_method(type.to_s.pluralize) do
@references[type] ||= references(type)
end
end
......
......@@ -296,7 +296,7 @@ RSpec.describe Gitlab::ReferenceExtractor do
end
it 'returns all supported prefixes' do
expect(prefixes.keys.uniq).to match_array(%w(@ # ~ % ! $ & *iteration:))
expect(prefixes.keys.uniq).to match_array(%w(@ # ~ % ! $ & + *iteration:))
end
it 'does not allow one prefix for multiple referables if not allowed specificly' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment