Commit f495925f authored by Dylan Griffith's avatar Dylan Griffith

Merge branch '212329-related-issues-to-core_services' into 'master'

[Related Issues to core] Move all Related Issues services to core RUN AS-IF-FOSS

See merge request gitlab-org/gitlab!39664
parents 791c2f62 5ab5e759
......@@ -305,6 +305,24 @@ class Issue < ApplicationRecord
end
end
def related_issues(current_user, preload: nil)
related_issues = ::Issue
.select(['issues.*', 'issue_links.id AS issue_link_id',
'issue_links.link_type as issue_link_type_value',
'issue_links.target_id as issue_link_source_id'])
.joins("INNER JOIN issue_links ON
(issue_links.source_id = issues.id AND issue_links.target_id = #{id})
OR
(issue_links.target_id = issues.id AND issue_links.source_id = #{id})")
.preload(preload)
.reorder('issue_link_id')
cross_project_filter = -> (issues) { issues.where(project: project) }
Ability.issues_readable_by_user(related_issues,
current_user,
filters: { read_cross_project: cross_project_filter })
end
def can_be_worked_on?
!self.closed? && !self.project.forked?
end
......@@ -378,6 +396,15 @@ class Issue < ApplicationRecord
author.id == User.support_bot.id
end
def issue_link_type
return unless respond_to?(:issue_link_type_value) && respond_to?(:issue_link_source_id)
type = IssueLink.link_types.key(issue_link_type_value) || IssueLink::TYPE_RELATES_TO
return type if issue_link_source_id == id
IssueLink.inverse_link_type(type)
end
private
def ensure_metrics
......
# frozen_string_literal: true
class IssueLink < ApplicationRecord
include FromUnion
belongs_to :source, class_name: 'Issue'
belongs_to :target, class_name: 'Issue'
validates :source, presence: true
validates :target, presence: true
validates :source, uniqueness: { scope: :target_id, message: 'is already related' }
validate :check_self_relation
scope :for_source_issue, ->(issue) { where(source_id: issue.id) }
scope :for_target_issue, ->(issue) { where(target_id: issue.id) }
TYPE_RELATES_TO = 'relates_to'
TYPE_BLOCKS = 'blocks'
TYPE_IS_BLOCKED_BY = 'is_blocked_by'
enum link_type: { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1, TYPE_IS_BLOCKED_BY => 2 }
def self.inverse_link_type(type)
type
end
private
def check_self_relation
return unless source && target
if source == target
errors.add(:source, 'cannot be related to itself')
end
end
end
IssueLink.prepend_if_ee('EE::IssueLink')
......@@ -11,6 +11,7 @@ class SystemNoteMetadata < ApplicationRecord
close duplicate
moved merge
label milestone
relate unrelate
].freeze
ICON_TYPES = %w[
......@@ -19,7 +20,7 @@ class SystemNoteMetadata < ApplicationRecord
title time_tracking branch milestone discussion task moved
opened closed merged duplicate locked unlocked outdated
tag due_date pinned_embed cherry_pick health_status approved unapproved
status alert_issue_added
status alert_issue_added relate unrelate
].freeze
validates :note, presence: true
......
......@@ -42,13 +42,7 @@ module IssuableLinks
def create_links
objects = linkable_issuables(referenced_issuables)
# it is important that this is not called after relate_issuables, as it relinks epic to the issuable
# see EpicLinks::EpicIssues#relate_issuables
affected_epics = affected_epics(objects)
link_issuables(objects)
Epics::UpdateDatesService.new(affected_epics).execute unless affected_epics.blank?
end
def link_issuables(target_issuables)
......@@ -88,10 +82,6 @@ module IssuableLinks
references(extractor)
end
def affected_epics(issues)
[]
end
def references(extractor)
extractor.issues
end
......@@ -121,3 +111,5 @@ module IssuableLinks
end
end
end
IssuableLinks::CreateService.prepend_if_ee('EE::IssuableLinks::CreateService')
......@@ -6,9 +6,7 @@ module IssueLinks
def relate_issuables(referenced_issue)
link = IssueLink.find_or_initialize_by(source: issuable, target: referenced_issue)
if params[:link_type].present?
link.link_type = params[:link_type]
end
set_link_type(link)
if link.changed? && link.save
create_notes(referenced_issue)
......@@ -32,5 +30,13 @@ module IssueLinks
def previous_related_issuables
@related_issues ||= issuable.related_issues(current_user).to_a
end
private
def set_link_type(_link)
# EE only
end
end
end
IssueLinks::CreateService.prepend_if_ee('EE::IssueLinks::CreateService')
......@@ -12,6 +12,8 @@ module Issues
close_service.new(project, current_user, {}).execute(duplicate_issue)
duplicate_issue.update(duplicated_to: canonical_issue)
relate_two_issues(duplicate_issue, canonical_issue)
end
private
......@@ -23,7 +25,10 @@ module Issues
def create_issue_canonical_note(canonical_issue, duplicate_issue)
SystemNoteService.mark_canonical_issue_of_duplicate(canonical_issue, canonical_issue.project, current_user, duplicate_issue)
end
def relate_two_issues(duplicate_issue, canonical_issue)
params = { target_issuable: canonical_issue }
IssueLinks::CreateService.new(duplicate_issue, current_user, params).execute
end
end
end
Issues::DuplicateService.prepend_if_ee('EE::Issues::DuplicateService')
......@@ -38,6 +38,7 @@ module Issues
def update_old_entity
super
rewrite_related_issues
mark_as_moved
end
......@@ -58,6 +59,14 @@ module Issues
original_entity.update(moved_to: new_entity)
end
def rewrite_related_issues
source_issue_links = IssueLink.for_source_issue(original_entity)
source_issue_links.update_all(source_id: new_entity.id)
target_issue_links = IssueLink.for_target_issue(original_entity)
target_issue_links.update_all(target_id: new_entity.id)
end
def notify_participants
notification_service.async.issue_moved(original_entity, new_entity, @current_user)
end
......
......@@ -10,6 +10,7 @@ module QuickActions
include Gitlab::QuickActions::MergeRequestActions
include Gitlab::QuickActions::CommitActions
include Gitlab::QuickActions::CommonActions
include Gitlab::QuickActions::RelateActions
attr_reader :quick_action_target
......
......@@ -45,6 +45,14 @@ module SystemNoteService
::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).change_milestone(milestone)
end
def relate_issue(noteable, noteable_ref, user)
::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).relate_issue(noteable_ref)
end
def unrelate_issue(noteable, noteable_ref, user)
::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).unrelate_issue(noteable_ref)
end
# Called when the due_date of a Noteable is changed
#
# noteable - Noteable object
......
......@@ -2,6 +2,34 @@
module SystemNotes
class IssuablesService < ::SystemNotes::BaseService
#
# noteable_ref - Referenced noteable object
#
# Example Note text:
#
# "marked this issue as related to gitlab-foss#9001"
#
# Returns the created Note object
def relate_issue(noteable_ref)
body = "marked this issue as related to #{noteable_ref.to_reference(noteable.project)}"
create_note(NoteSummary.new(noteable, project, author, body, action: 'relate'))
end
#
# noteable_ref - Referenced noteable object
#
# Example Note text:
#
# "removed the relation with gitlab-foss#9001"
#
# Returns the created Note object
def unrelate_issue(noteable_ref)
body = "removed the relation with #{noteable_ref.to_reference(noteable.project)}"
create_note(NoteSummary.new(noteable, project, author, body, action: 'unrelate'))
end
# Called when the assignee of a Noteable is changed or removed
#
# assignee - User being assigned, or nil
......
......@@ -22,4 +22,6 @@ resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
post :import_csv
post :export_csv
end
resources :issue_links, only: [:index, :create, :destroy], as: 'links', path: 'links'
end
......@@ -143,24 +143,6 @@ module EE
user&.can?(:admin_epic, project.group)
end
def related_issues(current_user, preload: nil)
related_issues = ::Issue
.select(['issues.*', 'issue_links.id AS issue_link_id',
'issue_links.link_type as issue_link_type_value',
'issue_links.target_id as issue_link_source_id'])
.joins("INNER JOIN issue_links ON
(issue_links.source_id = issues.id AND issue_links.target_id = #{id})
OR
(issue_links.target_id = issues.id AND issue_links.source_id = #{id})")
.preload(preload)
.reorder('issue_link_id')
cross_project_filter = -> (issues) { issues.where(project: project) }
Ability.issues_readable_by_user(related_issues,
current_user,
filters: { read_cross_project: cross_project_filter })
end
# Issue position on boards list should be relative to all group projects
def parent_ids
return super unless has_group_boards?
......@@ -180,15 +162,6 @@ module EE
!!promoted_to_epic_id
end
def issue_link_type
return unless respond_to?(:issue_link_type_value) && respond_to?(:issue_link_source_id)
type = IssueLink.link_types.key(issue_link_type_value) || IssueLink::TYPE_RELATES_TO
return type if issue_link_source_id == id
IssueLink.inverse_link_type(type)
end
class_methods do
extend ::Gitlab::Utils::Override
......@@ -220,7 +193,7 @@ module EE
end
def update_blocking_issues_count!
blocking_count = IssueLink.blocking_issues_count_for(self)
blocking_count = ::IssueLink.blocking_issues_count_for(self)
update!(blocking_issues_count: blocking_count)
end
......
# frozen_string_literal: true
module EE
module IssueLink
extend ActiveSupport::Concern
prepended do
after_create :refresh_blocking_issue_cache
after_destroy :refresh_blocking_issue_cache
end
class_methods do
def inverse_link_type(type)
case type
when ::IssueLink::TYPE_BLOCKS
::IssueLink::TYPE_IS_BLOCKED_BY
when ::IssueLink::TYPE_IS_BLOCKED_BY
::IssueLink::TYPE_BLOCKS
else
type
end
end
def blocked_issue_ids(issue_ids)
blocked_and_blocking_issues_union(issue_ids).pluck(:blocked_issue_id)
end
def blocking_issue_ids_for(issue)
blocked_and_blocking_issues_union(issue.id).pluck(:blocking_issue_id)
end
def blocked_and_blocking_issues_union(issue_ids)
from_union([
blocked_or_blocking_issues(issue_ids, ::IssueLink::TYPE_BLOCKS),
blocked_or_blocking_issues(issue_ids, ::IssueLink::TYPE_IS_BLOCKED_BY)
])
end
def blocked_or_blocking_issues(issue_ids, link_type)
if link_type == ::IssueLink::TYPE_BLOCKS
blocked_key = :target_id
blocking_key = :source_id
else
blocked_key = :source_id
blocking_key = :target_id
end
select("#{blocked_key} as blocked_issue_id, #{blocking_key} as blocking_issue_id")
.where(link_type: link_type).where(blocked_key => issue_ids)
.joins("INNER JOIN issues ON issues.id = issue_links.#{blocking_key}")
.where('issues.state_id' => ::Issuable::STATE_ID_MAP[:opened])
end
def blocking_issues_for_collection(issues_ids)
from_union([
select('COUNT(*), issue_links.source_id AS blocking_issue_id')
.joins(:target)
.where(issues: { state_id: ::Issue.available_states[:opened] })
.where(link_type: ::IssueLink::TYPE_BLOCKS)
.where(source_id: issues_ids)
.group(:blocking_issue_id),
select('COUNT(*), issue_links.target_id AS blocking_issue_id')
.joins(:source)
.where(issues: { state_id: ::Issue.available_states[:opened] })
.where(link_type: ::IssueLink::TYPE_IS_BLOCKED_BY)
.where(target_id: issues_ids)
.group(:blocking_issue_id)
], remove_duplicates: false).select('blocking_issue_id, SUM(count) AS count').group('blocking_issue_id')
end
def blocked_issues_for_collection(issues_ids)
from_union([
select('COUNT(*), issue_links.source_id AS blocked_issue_id')
.joins(:target)
.where(issues: { state_id: ::Issue.available_states[:opened] })
.where(link_type: ::IssueLink::TYPE_IS_BLOCKED_BY)
.where(source_id: issues_ids)
.group(:blocked_issue_id),
select('COUNT(*), issue_links.target_id AS blocked_issue_id')
.joins(:source)
.where(issues: { state_id: ::Issue.available_states[:opened] })
.where(link_type: ::IssueLink::TYPE_BLOCKS)
.where(target_id: issues_ids)
.group(:blocked_issue_id)
], remove_duplicates: false).select('blocked_issue_id, SUM(count) AS count').group('blocked_issue_id')
end
def blocking_issues_count_for(issue)
blocking_issues_for_collection(issue.id)[0]&.count.to_i
end
end
private
def blocking_issue
case link_type
when ::IssueLink::TYPE_BLOCKS then source
when ::IssueLink::TYPE_IS_BLOCKED_BY then target
end
end
def refresh_blocking_issue_cache
blocking_issue&.update_blocking_issues_count!
end
end
end
......@@ -5,7 +5,7 @@ module EE
extend ::Gitlab::Utils::Override
EE_ICON_TYPES = %w[
weight relate unrelate published
weight published
epic_issue_added issue_added_to_epic epic_issue_removed issue_removed_from_epic
epic_issue_moved issue_changed_epic epic_date_changed relate_epic unrelate_epic
vulnerability_confirmed vulnerability_dismissed vulnerability_resolved
......@@ -13,7 +13,6 @@ module EE
].freeze
EE_TYPES_WITH_CROSS_REFERENCES = %w[
relate unrelate
epic_issue_added issue_added_to_epic epic_issue_removed issue_removed_from_epic
epic_issue_moved issue_changed_epic relate_epic unrelate_epic
iteration
......
# frozen_string_literal: true
class IssueLink < ApplicationRecord
include FromUnion
belongs_to :source, class_name: 'Issue'
belongs_to :target, class_name: 'Issue'
validates :source, presence: true
validates :target, presence: true
validates :source, uniqueness: { scope: :target_id, message: 'is already related' }
validate :check_self_relation
scope :for_source_issue, ->(issue) { where(source_id: issue.id) }
scope :for_target_issue, ->(issue) { where(target_id: issue.id) }
TYPE_RELATES_TO = 'relates_to'
TYPE_BLOCKS = 'blocks'
TYPE_IS_BLOCKED_BY = 'is_blocked_by'
after_create :refresh_blocking_issue_cache
after_destroy :refresh_blocking_issue_cache
enum link_type: { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1, TYPE_IS_BLOCKED_BY => 2 }
class << self
def inverse_link_type(type)
case type
when TYPE_BLOCKS
TYPE_IS_BLOCKED_BY
when TYPE_IS_BLOCKED_BY
TYPE_BLOCKS
else
type
end
end
def blocked_issue_ids(issue_ids)
blocked_and_blocking_issues_union(issue_ids).pluck(:blocked_issue_id)
end
def blocking_issue_ids_for(issue)
blocked_and_blocking_issues_union(issue.id).pluck(:blocking_issue_id)
end
end
private
def blocking_issue
case link_type
when TYPE_BLOCKS then source
when TYPE_IS_BLOCKED_BY then target
end
end
def refresh_blocking_issue_cache
blocking_issue&.update_blocking_issues_count!
end
class << self
def blocked_and_blocking_issues_union(issue_ids)
from_union([
blocked_or_blocking_issues(issue_ids, IssueLink::TYPE_BLOCKS),
blocked_or_blocking_issues(issue_ids, IssueLink::TYPE_IS_BLOCKED_BY)
])
end
def blocked_or_blocking_issues(issue_ids, link_type)
if link_type == IssueLink::TYPE_BLOCKS
blocked_key = :target_id
blocking_key = :source_id
else
blocked_key = :source_id
blocking_key = :target_id
end
select("#{blocked_key} as blocked_issue_id, #{blocking_key} as blocking_issue_id")
.where(link_type: link_type).where(blocked_key => issue_ids)
.joins("INNER JOIN issues ON issues.id = issue_links.#{blocking_key}")
.where('issues.state_id' => Issuable::STATE_ID_MAP[:opened])
end
def blocking_issues_for_collection(issues_ids)
from_union([
select('COUNT(*), issue_links.source_id AS blocking_issue_id')
.joins(:target)
.where(issues: { state_id: Issue.available_states[:opened] })
.where(link_type: TYPE_BLOCKS)
.where(source_id: issues_ids)
.group(:blocking_issue_id),
select('COUNT(*), issue_links.target_id AS blocking_issue_id')
.joins(:source)
.where(issues: { state_id: Issue.available_states[:opened] })
.where(link_type: TYPE_IS_BLOCKED_BY)
.where(target_id: issues_ids)
.group(:blocking_issue_id)
], remove_duplicates: false).select('blocking_issue_id, SUM(count) AS count').group('blocking_issue_id')
end
def blocked_issues_for_collection(issues_ids)
from_union([
select('COUNT(*), issue_links.source_id AS blocked_issue_id')
.joins(:target)
.where(issues: { state_id: Issue.available_states[:opened] })
.where(link_type: TYPE_IS_BLOCKED_BY)
.where(source_id: issues_ids)
.group(:blocked_issue_id),
select('COUNT(*), issue_links.target_id AS blocked_issue_id')
.joins(:source)
.where(issues: { state_id: Issue.available_states[:opened] })
.where(link_type: TYPE_BLOCKS)
.where(target_id: issues_ids)
.group(:blocked_issue_id)
], remove_duplicates: false).select('blocked_issue_id, SUM(count) AS count').group('blocked_issue_id')
end
def blocking_issues_count_for(issue)
blocking_issues_for_collection(issue.id)[0]&.count.to_i
end
end
def check_self_relation
return unless source && target
if source == target
errors.add(:source, 'cannot be related to itself')
end
end
end
......@@ -6,9 +6,6 @@ module EE
extend ::Gitlab::Utils::Override
prepended do
with_scope :subject
condition(:related_issues_disabled) { !@subject.feature_available?(:blocked_issues) }
with_scope :subject
condition(:repository_mirrors_enabled) { @subject.feature_available?(:repository_mirrors) }
......@@ -190,11 +187,6 @@ module EE
prevent :push_code
end
rule { related_issues_disabled }.policy do
prevent :read_issue_link
prevent :admin_issue_link
end
rule { ~group_timelogs_available }.prevent :read_group_timelogs
rule { can?(:guest_access) & iterations_available }.enable :read_iteration
......
# frozen_string_literal: true
module EE
module IssuableLinks
module CreateService
extend ::Gitlab::Utils::Override
private
override :link_issuables
def link_issuables(objects)
# it is important that this is not called after relate_issuables, as it relinks epic to the issuable
# relate_issuables is called during the `super` portion of this method
# see EpicLinks::EpicIssues#relate_issuables
affected_epics = affected_epics(objects)
super
Epics::UpdateDatesService.new(affected_epics).execute unless affected_epics.blank?
end
def affected_epics(_issues)
[]
end
end
end
end
# frozen_string_literal: true
module EE
module IssueLinks
module CreateService
def execute
if params[:link_type].present?
return error('Blocked issues not available for current license', 403) unless link_type_available?
end
super
end
private
def set_link_type(link)
if params[:link_type].present?
link.link_type = params[:link_type]
end
end
def link_type_available?
return true unless [::IssueLink::TYPE_BLOCKS, ::IssueLink::TYPE_IS_BLOCKED_BY].include?(params[:link_type])
issuable.resource_parent.feature_available?(:blocked_issues)
end
end
end
end
# frozen_string_literal: true
module EE
module Issues
module DuplicateService
extend ::Gitlab::Utils::Override
override :execute
def execute(duplicate_issue, canonical_issue)
super
relate_two_issues(duplicate_issue, canonical_issue)
end
private
def relate_two_issues(duplicate_issue, canonical_issue)
params = { target_issuable: canonical_issue }
IssueLinks::CreateService.new(duplicate_issue, current_user, params).execute
end
end
end
end
......@@ -8,7 +8,6 @@ module EE
override :update_old_entity
def update_old_entity
rewrite_epic_issue
rewrite_related_issues
rewrite_related_vulnerability_issues
super
end
......@@ -23,14 +22,6 @@ module EE
original_entity.reset
end
def rewrite_related_issues
source_issue_links = IssueLink.for_source_issue(original_entity)
source_issue_links.update_all(source_id: new_entity.id)
target_issue_links = IssueLink.for_target_issue(original_entity)
target_issue_links.update_all(target_id: new_entity.id)
end
def rewrite_related_vulnerability_issues
issue_links = Vulnerabilities::IssueLink.for_issue(original_entity)
issue_links.update_all(issue_id: new_entity.id)
......
......@@ -12,7 +12,6 @@ module EE
include EE::Gitlab::QuickActions::IssueActions
include EE::Gitlab::QuickActions::MergeRequestActions
include EE::Gitlab::QuickActions::IssueAndMergeRequestActions
include EE::Gitlab::QuickActions::RelateActions
# rubocop: enable Cop/InjectEnterpriseEditionModule
end
end
......
......@@ -16,14 +16,6 @@ module EE
extend_if_ee('EE::SystemNoteService') # rubocop: disable Cop/InjectEnterpriseEditionModule
end
def relate_issue(noteable, noteable_ref, user)
issuables_service(noteable, noteable.project, user).relate_issue(noteable_ref)
end
def unrelate_issue(noteable, noteable_ref, user)
issuables_service(noteable, noteable.project, user).unrelate_issue(noteable_ref)
end
def epic_issue(epic, issue, user, type)
epics_service(epic, user).epic_issue(issue, type)
end
......
......@@ -2,34 +2,6 @@
module EE
module SystemNotes
module IssuablesService
#
# noteable_ref - Referenced noteable object
#
# Example Note text:
#
# "marked this issue as related to gitlab-foss#9001"
#
# Returns the created Note object
def relate_issue(noteable_ref)
body = "marked this issue as related to #{noteable_ref.to_reference(noteable.project)}"
create_note(NoteSummary.new(noteable, project, author, body, action: 'relate'))
end
#
# noteable_ref - Referenced noteable object
#
# Example Note text:
#
# "removed the relation with gitlab-foss#9001"
#
# Returns the created Note object
def unrelate_issue(noteable_ref)
body = "removed the relation with #{noteable_ref.to_reference(noteable.project)}"
create_note(NoteSummary.new(noteable, project, author, body, action: 'unrelate'))
end
# Called when the weight of a Noteable is changed
#
# Example Note text:
......
......@@ -5,6 +5,4 @@ resources :issues, only: [], constraints: { id: /\d+/ } do
get '/descriptions/:version_id/diff', action: :description_diff, as: :description_diff
delete '/descriptions/:version_id', action: :delete_description_version, as: :delete_description_version
end
resources :issue_links, only: [:index, :create, :destroy], as: 'links', path: 'links'
end
......@@ -21,9 +21,9 @@ module EE
def grouped_blocking_issues_count
strong_memoize(:grouped_blocking_issues_count) do
next IssueLink.none unless collection_type == 'Issue'
next ::IssueLink.none unless collection_type == 'Issue'
IssueLink.blocking_issues_for_collection(issuable_ids)
::IssueLink.blocking_issues_for_collection(issuable_ids)
end
end
end
......
# frozen_string_literal: true
module EE
module Gitlab
module QuickActions
module RelateActions
extend ActiveSupport::Concern
include ::Gitlab::QuickActions::Dsl
included do
desc _('Mark this issue as related to another issue')
explanation do |related_reference|
_('Marks this issue as related to %{issue_ref}.') % { issue_ref: related_reference }
end
execution_message do |related_reference|
_('Marked this issue as related to %{issue_ref}.') % { issue_ref: related_reference }
end
params '#issue'
types Issue
condition do
quick_action_target.persisted? &&
current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target)
end
command :relate do |related_param|
IssueLinks::CreateService.new(quick_action_target, current_user, { issuable_references: [related_param] }).execute
end
end
end
end
end
end
......@@ -72,19 +72,6 @@ RSpec.describe 'Feature flag issue links', :js do
expect(page).not_to have_selector '#related-issues'
end
end
context 'when the related issues feature is unavailable' do
before do
stub_licensed_features(blocked_issues: false, feature_flags: true)
end
it 'does not show the related issues widget' do
visit(edit_project_feature_flag_path(project, feature_flag))
expect(page).to have_text 'Strategies'
expect(page).not_to have_selector '#related-issues'
end
end
end
describe 'unlinking a feature flag from an issue' do
......
......@@ -33,7 +33,9 @@ RSpec.describe Gitlab::Graphql::Aggregations::Issues::LazyBlockAggregate do
end
it 'does not make the query again' do
expect(IssueLink).not_to receive(:blocked_issues_for_collection)
# We cannot directly stub IssueLink, otherwise we get a strange RSpec error
issue_link = class_double('IssueLink').as_stubbed_const
expect(issue_link).not_to receive(:blocked_issues_for_collection)
subject.block_aggregate
end
......@@ -53,7 +55,9 @@ RSpec.describe Gitlab::Graphql::Aggregations::Issues::LazyBlockAggregate do
end
before do
expect(IssueLink).to receive(:blocked_issues_for_collection).and_return(fake_data)
# We cannot directly stub IssueLink, otherwise we get a strange RSpec error
issue_link = class_double('IssueLink').as_stubbed_const
expect(issue_link).to receive(:blocked_issues_for_collection).and_return(fake_data)
end
it 'clears the pending IDs' do
......
......@@ -3,54 +3,6 @@
require 'spec_helper'
RSpec.describe IssueLink do
describe 'Associations' do
it { is_expected.to belong_to(:source).class_name('Issue') }
it { is_expected.to belong_to(:target).class_name('Issue') }
end
describe 'link_type' do
it { is_expected.to define_enum_for(:link_type).with_values(relates_to: 0, blocks: 1, is_blocked_by: 2) }
it 'provides the "related" as default link_type' do
expect(create(:issue_link).link_type).to eq 'relates_to'
end
end
describe 'Validation' do
subject { create :issue_link }
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:target) }
it do
is_expected.to validate_uniqueness_of(:source)
.scoped_to(:target_id)
.with_message(/already related/)
end
context 'self relation' do
let(:issue) { create :issue }
context 'cannot be validated' do
it 'does not invalidate object with self relation error' do
issue_link = build :issue_link, source: issue, target: nil
issue_link.valid?
expect(issue_link.errors[:source]).to be_empty
end
end
context 'can be invalidated' do
it 'invalidates object' do
issue_link = build :issue_link, source: issue, target: issue
expect(issue_link).to be_invalid
expect(issue_link.errors[:source]).to include('cannot be related to itself')
end
end
end
end
context 'callbacks' do
let_it_be(:target) { create(:issue) }
let_it_be(:source) { create(:issue) }
......@@ -61,7 +13,7 @@ RSpec.describe IssueLink do
expect(source).to receive(:update_blocking_issues_count!)
expect(target).not_to receive(:update_blocking_issues_count!)
create(:issue_link, target: target, source: source, link_type: described_class::TYPE_BLOCKS)
create(:issue_link, target: target, source: source, link_type: ::IssueLink::TYPE_BLOCKS)
end
end
......@@ -70,7 +22,7 @@ RSpec.describe IssueLink do
expect(source).not_to receive(:update_blocking_issues_count!)
expect(target).to receive(:update_blocking_issues_count!)
create(:issue_link, target: target, source: source, link_type: described_class::TYPE_IS_BLOCKED_BY)
create(:issue_link, target: target, source: source, link_type: ::IssueLink::TYPE_IS_BLOCKED_BY)
end
end
......@@ -79,7 +31,7 @@ RSpec.describe IssueLink do
expect(source).not_to receive(:update_blocking_issues_count!)
expect(target).not_to receive(:update_blocking_issues_count!)
create(:issue_link, target: target, source: source, link_type: described_class::TYPE_RELATES_TO)
create(:issue_link, target: target, source: source, link_type: ::IssueLink::TYPE_RELATES_TO)
end
end
end
......@@ -87,7 +39,7 @@ RSpec.describe IssueLink do
describe '.after_destroy_commit' do
context 'with TYPE_BLOCKS relation' do
it 'updates blocking issues count' do
link = create(:issue_link, target: target, source: source, link_type: described_class::TYPE_BLOCKS)
link = create(:issue_link, target: target, source: source, link_type: ::IssueLink::TYPE_BLOCKS)
expect(source).to receive(:update_blocking_issues_count!)
expect(target).not_to receive(:update_blocking_issues_count!)
......@@ -98,7 +50,7 @@ RSpec.describe IssueLink do
context 'with TYPE_IS_BLOCKED_BY' do
it 'updates blocking issues count' do
link = create(:issue_link, target: target, source: source, link_type: described_class::TYPE_IS_BLOCKED_BY)
link = create(:issue_link, target: target, source: source, link_type: ::IssueLink::TYPE_IS_BLOCKED_BY)
expect(source).not_to receive(:update_blocking_issues_count!)
expect(target).to receive(:update_blocking_issues_count!)
......@@ -109,7 +61,7 @@ RSpec.describe IssueLink do
context 'with TYPE_RELATES_TO' do
it 'does not update blocking_issues_count' do
link = create(:issue_link, target: target, source: source, link_type: described_class::TYPE_RELATES_TO)
link = create(:issue_link, target: target, source: source, link_type: ::IssueLink::TYPE_RELATES_TO)
expect(source).not_to receive(:update_blocking_issues_count!)
expect(target).not_to receive(:update_blocking_issues_count!)
......@@ -122,10 +74,10 @@ RSpec.describe IssueLink do
describe '.blocked_issue_ids' do
it 'returns only ids of issues which are blocked' do
link1 = create(:issue_link, link_type: described_class::TYPE_BLOCKS)
link2 = create(:issue_link, link_type: described_class::TYPE_IS_BLOCKED_BY)
link3 = create(:issue_link, link_type: described_class::TYPE_RELATES_TO)
link4 = create(:issue_link, source: create(:issue, :closed), link_type: described_class::TYPE_BLOCKS)
link1 = create(:issue_link, link_type: ::IssueLink::TYPE_BLOCKS)
link2 = create(:issue_link, link_type: ::IssueLink::TYPE_IS_BLOCKED_BY)
link3 = create(:issue_link, link_type: ::IssueLink::TYPE_RELATES_TO)
link4 = create(:issue_link, source: create(:issue, :closed), link_type: ::IssueLink::TYPE_BLOCKS)
expect(described_class.blocked_issue_ids([link1.target_id, link2.source_id, link3.source_id, link4.target_id]))
.to match_array([link1.target_id, link2.source_id])
......@@ -137,8 +89,8 @@ RSpec.describe IssueLink do
issue = create(:issue)
blocking_issue = create(:issue, project: issue.project)
blocked_by_issue = create(:issue, project: issue.project)
create(:issue_link, source: blocking_issue, target: issue, link_type: IssueLink::TYPE_BLOCKS)
create(:issue_link, source: issue, target: blocked_by_issue, link_type: IssueLink::TYPE_IS_BLOCKED_BY)
create(:issue_link, source: blocking_issue, target: issue, link_type: ::IssueLink::TYPE_BLOCKS)
create(:issue_link, source: issue, target: blocked_by_issue, link_type: ::IssueLink::TYPE_IS_BLOCKED_BY)
blocking_ids = described_class.blocking_issue_ids_for(issue)
......@@ -163,9 +115,9 @@ RSpec.describe IssueLink do
let_it_be(:blocking_issue_2) { create(:issue, project: project) }
before :all do
create(:issue_link, source: blocking_issue_1, target: blocked_issue_1, link_type: IssueLink::TYPE_BLOCKS)
create(:issue_link, source: blocked_issue_2, target: blocking_issue_1, link_type: IssueLink::TYPE_IS_BLOCKED_BY)
create(:issue_link, source: blocking_issue_2, target: blocked_issue_3, link_type: IssueLink::TYPE_BLOCKS)
create(:issue_link, source: blocking_issue_1, target: blocked_issue_1, link_type: ::IssueLink::TYPE_BLOCKS)
create(:issue_link, source: blocked_issue_2, target: blocking_issue_1, link_type: ::IssueLink::TYPE_IS_BLOCKED_BY)
create(:issue_link, source: blocking_issue_2, target: blocked_issue_3, link_type: ::IssueLink::TYPE_BLOCKS)
end
describe '.blocking_issues_for_collection' do
......
......@@ -242,50 +242,6 @@ RSpec.describe Issue do
let(:set_mentionable_text) { ->(txt) { subject.description = txt } }
end
describe '#related_issues' do
let(:user) { create(:user) }
let(:authorized_project) { create(:project) }
let(:authorized_project2) { create(:project) }
let(:unauthorized_project) { create(:project) }
let(:authorized_issue_a) { create(:issue, project: authorized_project) }
let(:authorized_issue_b) { create(:issue, project: authorized_project) }
let(:authorized_issue_c) { create(:issue, project: authorized_project2) }
let(:unauthorized_issue) { create(:issue, project: unauthorized_project) }
let!(:issue_link_a) { create(:issue_link, source: authorized_issue_a, target: authorized_issue_b) }
let!(:issue_link_b) { create(:issue_link, source: authorized_issue_a, target: unauthorized_issue) }
let!(:issue_link_c) { create(:issue_link, source: authorized_issue_a, target: authorized_issue_c) }
before do
authorized_project.add_developer(user)
authorized_project2.add_developer(user)
end
it 'returns only authorized related issues for given user' do
expect(authorized_issue_a.related_issues(user))
.to contain_exactly(authorized_issue_b, authorized_issue_c)
end
it 'returns issues with valid issue_link_type' do
link_types = authorized_issue_a.related_issues(user).map(&:issue_link_type)
expect(link_types).not_to be_empty
expect(link_types).not_to include(nil)
end
describe 'when a user cannot read cross project' do
it 'only returns issues within the same project' do
expect(Ability).to receive(:allowed?).with(user, :read_all_resources, :global).at_least(:once).and_call_original
expect(Ability).to receive(:allowed?).with(user, :read_cross_project).and_return(false)
expect(authorized_issue_a.related_issues(user))
.to contain_exactly(authorized_issue_b)
end
end
end
describe '#allows_multiple_assignees?' do
it 'does not allow multiple assignees without license' do
stub_licensed_features(multiple_issue_assignees: false)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IssueLinks::CreateService do
describe '#execute' do
let(:namespace) { create :namespace }
let(:project) { create :project, namespace: namespace }
let(:issue) { create :issue, project: project }
let(:user) { create :user }
let(:params) do
{}
end
before do
stub_licensed_features(blocked_issues: true)
project.add_developer(user)
end
subject { described_class.new(issue, user, params).execute }
context 'when there is an issue to relate' do
let(:issue_a) { create :issue, project: project }
let(:another_project) { create :project, namespace: project.namespace }
let(:another_project_issue) { create :issue, project: another_project }
let(:issue_a_ref) { issue_a.to_reference }
let(:another_project_issue_ref) { another_project_issue.to_reference(project) }
let(:params) do
{ issuable_references: [issue_a_ref, another_project_issue_ref], link_type: 'is_blocked_by' }
end
before do
another_project.add_developer(user)
end
context 'when feature is not available' do
before do
stub_licensed_features(blocked_issues: false)
end
it 'returns error' do
is_expected.to eq(message: 'Blocked issues not available for current license', status: :error, http_status: 403)
end
it 'no relationship is created' do
expect { subject }.not_to change(IssueLink, :count)
end
end
it 'creates relationships' do
expect { subject }.to change(IssueLink, :count).from(0).to(2)
expect(IssueLink.find_by!(target: issue_a)).to have_attributes(source: issue, link_type: 'is_blocked_by')
expect(IssueLink.find_by!(target: another_project_issue)).to have_attributes(source: issue, link_type: 'is_blocked_by')
end
it 'returns success status' do
is_expected.to eq(status: :success)
end
end
context 'when reference of any already related issue is present' do
let(:issue_a) { create :issue, project: project }
let(:issue_b) { create :issue, project: project }
let(:issue_c) { create :issue, project: project }
before do
create :issue_link, source: issue, target: issue_b, link_type: IssueLink::TYPE_RELATES_TO
create :issue_link, source: issue, target: issue_c, link_type: IssueLink::TYPE_IS_BLOCKED_BY
end
let(:params) do
{
issuable_references: [
issue_a.to_reference,
issue_b.to_reference,
issue_c.to_reference
],
link_type: IssueLink::TYPE_IS_BLOCKED_BY
}
end
it 'sets the same type of relation for selected references' do
expect(subject).to eq(status: :success)
expect(IssueLink.where(target: [issue_a, issue_b, issue_c]).pluck(:link_type))
.to eq([IssueLink::TYPE_IS_BLOCKED_BY, IssueLink::TYPE_IS_BLOCKED_BY, IssueLink::TYPE_IS_BLOCKED_BY])
end
end
end
end
......@@ -47,45 +47,6 @@ RSpec.describe Issues::MoveService do
end
end
describe '#rewrite_related_issues' do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:authorized_project) { create(:project) }
let(:authorized_project2) { create(:project) }
let(:unauthorized_project) { create(:project) }
let(:authorized_issue_b) { create(:issue, project: authorized_project) }
let(:authorized_issue_c) { create(:issue, project: authorized_project2) }
let(:authorized_issue_d) { create(:issue, project: authorized_project2) }
let(:unauthorized_issue) { create(:issue, project: unauthorized_project) }
let!(:issue_link_a) { create(:issue_link, source: old_issue, target: authorized_issue_b) }
let!(:issue_link_b) { create(:issue_link, source: old_issue, target: unauthorized_issue) }
let!(:issue_link_c) { create(:issue_link, source: old_issue, target: authorized_issue_c) }
let!(:issue_link_d) { create(:issue_link, source: authorized_issue_d, target: old_issue) }
before do
stub_licensed_features(blocked_issues: true)
authorized_project.add_developer(user)
authorized_project2.add_developer(user)
end
context 'multiple related issues' do
it 'moves all related issues and retains permissions' do
new_issue = move_service.execute(old_issue, new_project)
expect(new_issue.related_issues(admin))
.to match_array([authorized_issue_b, authorized_issue_c, authorized_issue_d, unauthorized_issue])
expect(new_issue.related_issues(user))
.to match_array([authorized_issue_b, authorized_issue_c, authorized_issue_d])
expect(authorized_issue_d.related_issues(user))
.to match_array([new_issue])
end
end
end
describe '#rewrite_related_vulnerability_issues' do
let(:user) { create(:user) }
......
......@@ -312,48 +312,6 @@ RSpec.describe Notes::QuickActionsService do
end
end
context '/relate' do
let(:other_issue) { create(:issue, project: project) }
let(:note_text) { "/relate #{other_issue.to_reference}" }
let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) }
context 'user cannot relate issues' do
before do
project.update(visibility: Gitlab::VisibilityLevel::PUBLIC)
end
it 'does not create issue relation' do
expect { execute(note) }.not_to change { IssueLink.count }
end
end
context 'user is allowed to relate issues' do
before do
group.add_developer(user)
end
context 'related issues are not enabled' do
before do
stub_licensed_features(blocked_issues: false)
end
it 'does not create issue relation' do
expect { execute(note) }.not_to change { IssueLink.count }
end
end
context 'related issues are enabled' do
before do
stub_licensed_features(blocked_issues: true)
end
it 'creates issue relation' do
expect { execute(note) }.to change { IssueLink.count }.by(1)
end
end
end
end
context '/promote' do
let(:note_text) { "/promote" }
let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) }
......
......@@ -13,38 +13,6 @@ RSpec.describe ::SystemNotes::IssuablesService do
let(:service) { described_class.new(noteable: noteable, project: project, author: author) }
describe '#relate_issue' do
let(:noteable_ref) { create(:issue) }
subject { service.relate_issue(noteable_ref) }
it_behaves_like 'a system note' do
let(:action) { 'relate' }
end
context 'when issue marks another as related' do
it 'sets the note text' do
expect(subject.note).to eq "marked this issue as related to #{noteable_ref.to_reference(project)}"
end
end
end
describe '#unrelate_issue' do
let(:noteable_ref) { create(:issue) }
subject { service.unrelate_issue(noteable_ref) }
it_behaves_like 'a system note' do
let(:action) { 'unrelate' }
end
context 'when issue relation is removed' do
it 'sets the note text' do
expect(subject.note).to eq "removed the relation with #{noteable_ref.to_reference(project)}"
end
end
end
describe '#change_weight_note' do
context 'when weight changed' do
let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum', weight: 4) }
......
......@@ -879,101 +879,6 @@ RSpec.describe QuickActions::InterpretService do
let(:issuable) { build(:merge_request, source_project: project) }
end
end
context 'relate command' do
shared_examples 'relate command' do
it 'relates issues' do
service.execute(content, issue)
expect(IssueLink.where(source: issue).map(&:target)).to match_array(issues_related)
end
end
context 'user is member of group' do
before do
group.add_developer(user)
end
context 'relate a single issue' do
let(:other_issue) { create(:issue, project: project) }
let(:issues_related) { [other_issue] }
let(:content) { "/relate #{other_issue.to_reference}" }
it_behaves_like 'relate command'
end
context 'relate multiple issues at once' do
let(:second_issue) { create(:issue, project: project) }
let(:third_issue) { create(:issue, project: project) }
let(:issues_related) { [second_issue, third_issue] }
let(:content) { "/relate #{second_issue.to_reference} #{third_issue.to_reference}" }
it_behaves_like 'relate command'
end
context 'empty relate command' do
let(:issues_related) { [] }
let(:content) { '/relate' }
it_behaves_like 'relate command'
end
context 'already having related issues' do
let(:second_issue) { create(:issue, project: project) }
let(:third_issue) { create(:issue, project: project) }
let(:issues_related) { [second_issue, third_issue] }
let(:content) { "/relate #{third_issue.to_reference(project)}" }
before do
create(:issue_link, source: issue, target: second_issue)
end
it_behaves_like 'relate command'
end
context 'cross project' do
let(:another_group) { create(:group, :public) }
let(:other_project) { create(:project, group: another_group) }
before do
another_group.add_developer(current_user)
end
context 'relate a cross project issue' do
let(:other_issue) { create(:issue, project: other_project) }
let(:issues_related) { [other_issue] }
let(:content) { "/relate #{other_issue.to_reference(project)}" }
it_behaves_like 'relate command'
end
context 'relate multiple cross projects issues at once' do
let(:second_issue) { create(:issue, project: other_project) }
let(:third_issue) { create(:issue, project: other_project) }
let(:issues_related) { [second_issue, third_issue] }
let(:content) { "/relate #{second_issue.to_reference(project)} #{third_issue.to_reference(project)}" }
it_behaves_like 'relate command'
end
context 'relate a non-existing issue' do
let(:issues_related) { [] }
let(:content) { "/relate imaginary##{non_existing_record_iid}" }
it_behaves_like 'relate command'
end
context 'relate a private issue' do
let(:private_project) { create(:project, :private) }
let(:other_issue) { create(:issue, project: private_project) }
let(:issues_related) { [] }
let(:content) { "/relate #{other_issue.to_reference(project)}" }
it_behaves_like 'relate command'
end
end
end
end
end
describe '#explain' do
......
......@@ -14,40 +14,6 @@ RSpec.describe SystemNoteService do
let_it_be(:issue) { noteable }
let_it_be(:epic) { create(:epic, group: group) }
describe '.relate_issue' do
let(:noteable_ref) { double }
let(:noteable) { double }
before do
allow(noteable).to receive(:project).and_return(double)
end
it 'calls IssuableService' do
expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
expect(service).to receive(:relate_issue).with(noteable_ref)
end
described_class.relate_issue(noteable, noteable_ref, double)
end
end
describe '.unrelate_issue' do
let(:noteable_ref) { double }
let(:noteable) { double }
before do
allow(noteable).to receive(:project).and_return(double)
end
it 'calls IssuableService' do
expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
expect(service).to receive(:unrelate_issue).with(noteable_ref)
end
described_class.unrelate_issue(noteable, noteable_ref, double)
end
end
describe '.change_weight_note' do
it 'calls IssuableService' do
expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
......
# frozen_string_literal: true
module Gitlab
module QuickActions
module RelateActions
extend ActiveSupport::Concern
include ::Gitlab::QuickActions::Dsl
included do
desc _('Mark this issue as related to another issue')
explanation do |related_reference|
_('Marks this issue as related to %{issue_ref}.') % { issue_ref: related_reference }
end
execution_message do |related_reference|
_('Marked this issue as related to %{issue_ref}.') % { issue_ref: related_reference }
end
params '#issue'
types Issue
condition do
quick_action_target.persisted? &&
current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target)
end
command :relate do |related_param|
IssueLinks::CreateService.new(quick_action_target, current_user, { issuable_references: [related_param] }).execute
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IssueLink do
describe 'Associations' do
it { is_expected.to belong_to(:source).class_name('Issue') }
it { is_expected.to belong_to(:target).class_name('Issue') }
end
describe 'link_type' do
it { is_expected.to define_enum_for(:link_type).with_values(relates_to: 0, blocks: 1, is_blocked_by: 2) }
it 'provides the "related" as default link_type' do
expect(create(:issue_link).link_type).to eq 'relates_to'
end
end
describe 'Validation' do
subject { create :issue_link }
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:target) }
it do
is_expected.to validate_uniqueness_of(:source)
.scoped_to(:target_id)
.with_message(/already related/)
end
context 'self relation' do
let(:issue) { create :issue }
context 'cannot be validated' do
it 'does not invalidate object with self relation error' do
issue_link = build :issue_link, source: issue, target: nil
issue_link.valid?
expect(issue_link.errors[:source]).to be_empty
end
end
context 'can be invalidated' do
it 'invalidates object' do
issue_link = build :issue_link, source: issue, target: issue
expect(issue_link).to be_invalid
expect(issue_link.errors[:source]).to include('cannot be related to itself')
end
end
end
end
end
......@@ -311,6 +311,50 @@ RSpec.describe Issue do
end
end
describe '#related_issues' do
let(:user) { create(:user) }
let(:authorized_project) { create(:project) }
let(:authorized_project2) { create(:project) }
let(:unauthorized_project) { create(:project) }
let(:authorized_issue_a) { create(:issue, project: authorized_project) }
let(:authorized_issue_b) { create(:issue, project: authorized_project) }
let(:authorized_issue_c) { create(:issue, project: authorized_project2) }
let(:unauthorized_issue) { create(:issue, project: unauthorized_project) }
let!(:issue_link_a) { create(:issue_link, source: authorized_issue_a, target: authorized_issue_b) }
let!(:issue_link_b) { create(:issue_link, source: authorized_issue_a, target: unauthorized_issue) }
let!(:issue_link_c) { create(:issue_link, source: authorized_issue_a, target: authorized_issue_c) }
before do
authorized_project.add_developer(user)
authorized_project2.add_developer(user)
end
it 'returns only authorized related issues for given user' do
expect(authorized_issue_a.related_issues(user))
.to contain_exactly(authorized_issue_b, authorized_issue_c)
end
it 'returns issues with valid issue_link_type' do
link_types = authorized_issue_a.related_issues(user).map(&:issue_link_type)
expect(link_types).not_to be_empty
expect(link_types).not_to include(nil)
end
describe 'when a user cannot read cross project' do
it 'only returns issues within the same project' do
expect(Ability).to receive(:allowed?).with(user, :read_all_resources, :global).at_least(:once).and_call_original
expect(Ability).to receive(:allowed?).with(user, :read_cross_project).and_return(false)
expect(authorized_issue_a.related_issues(user))
.to contain_exactly(authorized_issue_b)
end
end
end
describe '#can_move?' do
let(:issue) { create(:issue) }
......
......@@ -13,8 +13,6 @@ RSpec.describe IssueLinks::CreateService do
end
before do
stub_licensed_features(blocked_issues: true)
project.add_developer(user)
end
......@@ -87,7 +85,7 @@ RSpec.describe IssueLinks::CreateService do
let(:another_project_issue_ref) { another_project_issue.to_reference(project) }
let(:params) do
{ issuable_references: [issue_a_ref, another_project_issue_ref], link_type: 'is_blocked_by' }
{ issuable_references: [issue_a_ref, another_project_issue_ref] }
end
before do
......@@ -97,8 +95,8 @@ RSpec.describe IssueLinks::CreateService do
it 'creates relationships' do
expect { subject }.to change(IssueLink, :count).from(0).to(2)
expect(IssueLink.find_by!(target: issue_a)).to have_attributes(source: issue, link_type: 'is_blocked_by')
expect(IssueLink.find_by!(target: another_project_issue)).to have_attributes(source: issue, link_type: 'is_blocked_by')
expect(IssueLink.find_by!(target: issue_a)).to have_attributes(source: issue, link_type: 'relates_to')
expect(IssueLink.find_by!(target: another_project_issue)).to have_attributes(source: issue, link_type: 'relates_to')
end
it 'returns success status' do
......@@ -129,7 +127,7 @@ RSpec.describe IssueLinks::CreateService do
before do
create :issue_link, source: issue, target: issue_b, link_type: IssueLink::TYPE_RELATES_TO
create :issue_link, source: issue, target: issue_c, link_type: IssueLink::TYPE_IS_BLOCKED_BY
create :issue_link, source: issue, target: issue_c, link_type: IssueLink::TYPE_RELATES_TO
end
let(:params) do
......@@ -139,27 +137,20 @@ RSpec.describe IssueLinks::CreateService do
issue_b.to_reference,
issue_c.to_reference
],
link_type: IssueLink::TYPE_IS_BLOCKED_BY
link_type: IssueLink::TYPE_RELATES_TO
}
end
it 'creates notes only for new and changed relations' do
it 'creates notes only for new relations' do
expect(SystemNoteService).to receive(:relate_issue).with(issue, issue_a, anything)
expect(SystemNoteService).to receive(:relate_issue).with(issue_a, issue, anything)
expect(SystemNoteService).to receive(:relate_issue).with(issue, issue_b, anything)
expect(SystemNoteService).to receive(:relate_issue).with(issue_b, issue, anything)
expect(SystemNoteService).not_to receive(:relate_issue).with(issue, issue_b, anything)
expect(SystemNoteService).not_to receive(:relate_issue).with(issue_b, issue, anything)
expect(SystemNoteService).not_to receive(:relate_issue).with(issue, issue_c, anything)
expect(SystemNoteService).not_to receive(:relate_issue).with(issue_c, issue, anything)
subject
end
it 'sets the same type of relation for selected references' do
expect(subject).to eq(status: :success)
expect(IssueLink.where(target: [issue_a, issue_b, issue_c]).pluck(:link_type))
.to eq([IssueLink::TYPE_IS_BLOCKED_BY, IssueLink::TYPE_IS_BLOCKED_BY, IssueLink::TYPE_IS_BLOCKED_BY])
end
end
context 'when there are invalid references' do
......
......@@ -9,8 +9,6 @@ RSpec.describe IssueLinks::ListService do
let(:user_role) { :developer }
before do
stub_licensed_features(blocked_issues: true)
project.add_role(user, user_role)
end
......
......@@ -83,6 +83,17 @@ RSpec.describe Issues::DuplicateService do
expect(duplicate_issue.reload.duplicated_to).to eq(canonical_issue)
end
it 'relates the duplicate issues' do
canonical_project.add_reporter(user)
duplicate_project.add_reporter(user)
subject.execute(duplicate_issue, canonical_issue)
issue_link = IssueLink.last
expect(issue_link.source).to eq(duplicate_issue)
expect(issue_link.target).to eq(canonical_issue)
end
end
end
end
......@@ -223,6 +223,45 @@ RSpec.describe Issues::MoveService do
end
end
describe '#rewrite_related_issues' do
include_context 'user can move issue'
let(:admin) { create(:admin) }
let(:authorized_project) { create(:project) }
let(:authorized_project2) { create(:project) }
let(:unauthorized_project) { create(:project) }
let(:authorized_issue_b) { create(:issue, project: authorized_project) }
let(:authorized_issue_c) { create(:issue, project: authorized_project2) }
let(:authorized_issue_d) { create(:issue, project: authorized_project2) }
let(:unauthorized_issue) { create(:issue, project: unauthorized_project) }
let!(:issue_link_a) { create(:issue_link, source: old_issue, target: authorized_issue_b) }
let!(:issue_link_b) { create(:issue_link, source: old_issue, target: unauthorized_issue) }
let!(:issue_link_c) { create(:issue_link, source: old_issue, target: authorized_issue_c) }
let!(:issue_link_d) { create(:issue_link, source: authorized_issue_d, target: old_issue) }
before do
authorized_project.add_developer(user)
authorized_project2.add_developer(user)
end
context 'multiple related issues' do
it 'moves all related issues and retains permissions' do
new_issue = move_service.execute(old_issue, new_project)
expect(new_issue.related_issues(admin))
.to match_array([authorized_issue_b, authorized_issue_c, authorized_issue_d, unauthorized_issue])
expect(new_issue.related_issues(user))
.to match_array([authorized_issue_b, authorized_issue_c, authorized_issue_d])
expect(authorized_issue_d.related_issues(user))
.to match_array([new_issue])
end
end
end
context 'updating sent notifications' do
let!(:old_issue_notification_1) { create(:sent_notification, project: old_issue.project, noteable: old_issue) }
let!(:old_issue_notification_2) { create(:sent_notification, project: old_issue.project, noteable: old_issue) }
......
......@@ -4,9 +4,9 @@ require 'spec_helper'
RSpec.describe Notes::QuickActionsService do
shared_context 'note on noteable' do
let(:project) { create(:project, :repository) }
let(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } }
let(:assignee) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } }
let_it_be(:assignee) { create(:user) }
before do
project.add_maintainer(assignee)
......@@ -41,6 +41,36 @@ RSpec.describe Notes::QuickActionsService do
end
end
context '/relate' do
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:other_issue) { create(:issue, project: project) }
let(:note_text) { "/relate #{other_issue.to_reference}" }
let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) }
context 'user cannot relate issues' do
before do
project.team.find_member(maintainer.id).destroy!
project.update!(visibility: Gitlab::VisibilityLevel::PUBLIC)
end
it 'does not create issue relation' do
expect do
_, update_params = service.execute(note)
service.apply_updates(update_params, note)
end.not_to change { IssueLink.count }
end
end
context 'user is allowed to relate issues' do
it 'creates issue relation' do
expect do
_, update_params = service.execute(note)
service.apply_updates(update_params, note)
end.to change { IssueLink.count }.by(1)
end
end
end
describe '/reopen' do
before do
note.noteable.close!
......
......@@ -1644,6 +1644,103 @@ RSpec.describe QuickActions::InterpretService do
end
end
end
context 'relate command' do
let_it_be_with_refind(:group) { create(:group) }
shared_examples 'relate command' do
it 'relates issues' do
service.execute(content, issue)
expect(IssueLink.where(source: issue).map(&:target)).to match_array(issues_related)
end
end
context 'user is member of group' do
before do
group.add_developer(developer)
end
context 'relate a single issue' do
let(:other_issue) { create(:issue, project: project) }
let(:issues_related) { [other_issue] }
let(:content) { "/relate #{other_issue.to_reference}" }
it_behaves_like 'relate command'
end
context 'relate multiple issues at once' do
let(:second_issue) { create(:issue, project: project) }
let(:third_issue) { create(:issue, project: project) }
let(:issues_related) { [second_issue, third_issue] }
let(:content) { "/relate #{second_issue.to_reference} #{third_issue.to_reference}" }
it_behaves_like 'relate command'
end
context 'empty relate command' do
let(:issues_related) { [] }
let(:content) { '/relate' }
it_behaves_like 'relate command'
end
context 'already having related issues' do
let(:second_issue) { create(:issue, project: project) }
let(:third_issue) { create(:issue, project: project) }
let(:issues_related) { [second_issue, third_issue] }
let(:content) { "/relate #{third_issue.to_reference(project)}" }
before do
create(:issue_link, source: issue, target: second_issue)
end
it_behaves_like 'relate command'
end
context 'cross project' do
let(:another_group) { create(:group, :public) }
let(:other_project) { create(:project, group: another_group) }
before do
another_group.add_developer(developer)
end
context 'relate a cross project issue' do
let(:other_issue) { create(:issue, project: other_project) }
let(:issues_related) { [other_issue] }
let(:content) { "/relate #{other_issue.to_reference(project)}" }
it_behaves_like 'relate command'
end
context 'relate multiple cross projects issues at once' do
let(:second_issue) { create(:issue, project: other_project) }
let(:third_issue) { create(:issue, project: other_project) }
let(:issues_related) { [second_issue, third_issue] }
let(:content) { "/relate #{second_issue.to_reference(project)} #{third_issue.to_reference(project)}" }
it_behaves_like 'relate command'
end
context 'relate a non-existing issue' do
let(:issues_related) { [] }
let(:content) { "/relate imaginary##{non_existing_record_iid}" }
it_behaves_like 'relate command'
end
context 'relate a private issue' do
let(:private_project) { create(:project, :private) }
let(:other_issue) { create(:issue, project: private_project) }
let(:issues_related) { [] }
let(:content) { "/relate #{other_issue.to_reference(project)}" }
it_behaves_like 'relate command'
end
end
end
end
end
describe '#explain' do
......
......@@ -86,6 +86,40 @@ RSpec.describe SystemNoteService do
end
end
describe '.relate_issue' do
let(:noteable_ref) { double }
let(:noteable) { double }
before do
allow(noteable).to receive(:project).and_return(double)
end
it 'calls IssuableService' do
expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
expect(service).to receive(:relate_issue).with(noteable_ref)
end
described_class.relate_issue(noteable, noteable_ref, double)
end
end
describe '.unrelate_issue' do
let(:noteable_ref) { double }
let(:noteable) { double }
before do
allow(noteable).to receive(:project).and_return(double)
end
it 'calls IssuableService' do
expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
expect(service).to receive(:unrelate_issue).with(noteable_ref)
end
described_class.unrelate_issue(noteable, noteable_ref, double)
end
end
describe '.change_due_date' do
let(:due_date) { double }
......
......@@ -13,6 +13,38 @@ RSpec.describe ::SystemNotes::IssuablesService do
let(:service) { described_class.new(noteable: noteable, project: project, author: author) }
describe '#relate_issue' do
let(:noteable_ref) { create(:issue) }
subject { service.relate_issue(noteable_ref) }
it_behaves_like 'a system note' do
let(:action) { 'relate' }
end
context 'when issue marks another as related' do
it 'sets the note text' do
expect(subject.note).to eq "marked this issue as related to #{noteable_ref.to_reference(project)}"
end
end
end
describe '#unrelate_issue' do
let(:noteable_ref) { create(:issue) }
subject { service.unrelate_issue(noteable_ref) }
it_behaves_like 'a system note' do
let(:action) { 'unrelate' }
end
context 'when issue relation is removed' do
it 'sets the note text' do
expect(subject.note).to eq "removed the relation with #{noteable_ref.to_reference(project)}"
end
end
end
describe '#change_assignee' do
subject { service.change_assignee(assignee) }
......
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