Commit 5ab5e759 authored by Mario de la Ossa's avatar Mario de la Ossa

Move all Related Issues services and models to core

As part of making simple related issues free
parent d795ab07
......@@ -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
......@@ -142,24 +142,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?
......@@ -179,15 +161,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
......@@ -219,7 +192,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
......
......@@ -241,50 +241,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