Commit 22f8ba4a authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch '353473-add-support-for-relating-epics-service' into 'master'

Add support for relating epics

See merge request gitlab-org/gitlab!82128
parents 2ec6b46e 0a36bbc5
......@@ -36,6 +36,20 @@ module IssuableLinks
success
end
# rubocop: disable CodeReuse/ActiveRecord
def relate_issuables(referenced_issuable)
link = link_class.find_or_initialize_by(source: issuable, target: referenced_issuable)
set_link_type(link)
if link.changed? && link.save
create_notes(referenced_issuable)
end
link
end
# rubocop: enable CodeReuse/ActiveRecord
private
def render_conflict_error?
......@@ -96,6 +110,23 @@ module IssuableLinks
{}
end
def issuables_assigned_message
_('%{issuable}(s) already assigned' % { issuable: target_issuable_type.capitalize })
end
def issuables_not_found_message
_('No matching %{issuable} found. Make sure that you are adding a valid %{issuable} URL.' % { issuable: target_issuable_type })
end
def target_issuable_type
:issue
end
def create_notes(referenced_issuable)
SystemNoteService.relate_issuable(issuable, referenced_issuable, current_user)
SystemNoteService.relate_issuable(referenced_issuable, issuable, current_user)
end
def linkable_issuables(objects)
raise NotImplementedError
end
......@@ -104,16 +135,12 @@ module IssuableLinks
raise NotImplementedError
end
def relate_issuables(referenced_object)
def link_class
raise NotImplementedError
end
def issuables_assigned_message
_("Issue(s) already assigned")
end
def issuables_not_found_message
_("No matching issue found. Make sure that you are adding a valid issue URL.")
def set_link_type(_link)
# no-op
end
end
end
......
......@@ -2,44 +2,25 @@
module IssueLinks
class CreateService < IssuableLinks::CreateService
# rubocop: disable CodeReuse/ActiveRecord
def relate_issuables(referenced_issue)
link = IssueLink.find_or_initialize_by(source: issuable, target: referenced_issue)
set_link_type(link)
if link.changed? && link.save
create_notes(referenced_issue)
end
link
end
# rubocop: enable CodeReuse/ActiveRecord
def linkable_issuables(issues)
@linkable_issuables ||= begin
issues.select { |issue| can?(current_user, :admin_issue_link, issue) }
end
end
def create_notes(referenced_issue)
SystemNoteService.relate_issue(issuable, referenced_issue, current_user)
SystemNoteService.relate_issue(referenced_issue, issuable, current_user)
end
def previous_related_issuables
@related_issues ||= issuable.related_issues(current_user).to_a
end
private
def set_link_type(_link)
# EE only
end
def track_event
track_incident_action(current_user, issuable, :incident_relate)
end
def link_class
IssueLink
end
end
end
......
......@@ -49,8 +49,8 @@ module SystemNoteService
::SystemNotes::IssuablesService.new(noteable: issuable, project: project, author: author).change_issuable_contacts(added_count, removed_count)
end
def relate_issue(noteable, noteable_ref, user)
::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).relate_issue(noteable_ref)
def relate_issuable(noteable, noteable_ref, user)
::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).relate_issuable(noteable_ref)
end
def unrelate_issuable(noteable, noteable_ref, user)
......
......@@ -10,8 +10,9 @@ module SystemNotes
# "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)}"
def relate_issuable(noteable_ref)
issuable_type = noteable.to_ability_name.humanize(capitalize: false)
body = "marked this #{issuable_type} as related to #{noteable_ref.to_reference(noteable.resource_parent)}"
issue_activity_counter.track_issue_related_action(author: author) if noteable.is_a?(Issue)
......
......@@ -10,7 +10,7 @@ class Groups::Epics::RelatedEpicLinksController < Groups::ApplicationController
before_action :check_epics_available!
before_action :check_related_epics_available!
before_action :authorize_related_epic_link_association!, only: [:destroy]
before_action :authorize_admin!, only: [:destroy]
before_action :authorize_admin!, only: [:create, :destroy]
feature_category :portfolio_management
urgency :default
......@@ -37,7 +37,15 @@ class Groups::Epics::RelatedEpicLinksController < Groups::ApplicationController
Epics::RelatedEpicLinks::DestroyService.new(link, current_user)
end
def create_service
Epics::RelatedEpicLinks::CreateService.new(epic, current_user, create_params)
end
def ensure_related_epics_enabled!
render_404 unless Feature.enabled?(:related_epics_widget, epic&.group, default_enabled: :yaml)
end
def create_params
params.permit(:link_type, issuable_references: [])
end
end
......@@ -2,10 +2,17 @@
module Epics
class RelatedEpicEntity < Grape::Entity
include Gitlab::Utils::StrongMemoize
include RequestAwareEntity
expose :id, :confidential, :title, :state, :created_at, :closed_at
expose :relation_path do |related_epic|
if can_admin_related_epic_links?(related_epic)
group_epic_related_epic_link_path(issuable.group, issuable.iid, related_epic.related_epic_link_id)
end
end
expose :reference do |related_epic|
related_epic.to_reference(request.issuable.group)
end
......@@ -17,5 +24,17 @@ module Epics
expose :link_type do |related_epic|
related_epic.epic_link_type
end
private
def can_admin_related_epic_links?(epic)
user = request.current_user
Ability.allowed?(user, :admin_related_epic_link, issuable) && Ability.allowed?(user, :admin_epic, epic)
end
def issuable
request.issuable
end
end
end
......@@ -24,6 +24,19 @@ module EE
def affected_epics(_issues)
[]
end
override :set_link_type
def set_link_type(link)
return unless params[:link_type].present?
# `blocked_by` links are treated as `blocks` links where source and target is swapped.
if params[:link_type] == ::IssuableLink::TYPE_IS_BLOCKED_BY
link.source, link.target = link.target, link.source
link.link_type = ::IssuableLink::TYPE_BLOCKS
else
link.link_type = params[:link_type]
end
end
end
end
end
......@@ -13,18 +13,6 @@ module EE
private
def set_link_type(link)
return unless params[:link_type].present?
# `blocked_by` links are treated as `blocks` links where source and target is swapped.
if params[:link_type] == ::IssueLink::TYPE_IS_BLOCKED_BY
link.source, link.target = link.target, link.source
link.link_type = ::IssueLink::TYPE_BLOCKS
else
link.link_type = params[:link_type]
end
end
def link_type_available?
# `blocked_by` is allowed as a param and handled in set_link_type
return true unless [::IssueLink::TYPE_BLOCKS, ::IssueLink::TYPE_IS_BLOCKED_BY].include?(params[:link_type])
......
# frozen_string_literal: true
module Epics::RelatedEpicLinks
class CreateService < IssuableLinks::CreateService
def linkable_issuables(epics)
@linkable_issuables ||= begin
epics.select { |epic| can?(current_user, :admin_epic, epic) }
end
end
def previous_related_issuables
@related_epics ||= issuable.related_epics(current_user).to_a
end
private
def references(extractor)
extractor.epics
end
def extractor_context
{ group: issuable.group }
end
def target_issuable_type
:epic
end
def link_class
Epic::RelatedEpicLink
end
end
end
......@@ -6,7 +6,7 @@ module Epics
private
def permission_to_remove_relation?
can?(current_user, :admin_related_epic_link, source) && can?(current_user, :admin_related_epic_link, target)
can?(current_user, :admin_related_epic_link, source) && can?(current_user, :admin_epic, target)
end
def track_event
......
......@@ -123,7 +123,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
scope module: :epics do
resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ }
resources :related_epic_links, only: [:index, :destroy]
resources :related_epic_links, only: [:index, :create, :destroy]
end
collection do
......
......@@ -249,7 +249,7 @@ RSpec.describe Projects::IssuesController do
context 'with a related system note' do
let(:confidential_issue) { create(:issue, :confidential, project: project) }
let!(:system_note) { SystemNoteService.relate_issue(issue, confidential_issue, user) }
let!(:system_note) { SystemNoteService.relate_issuable(issue, confidential_issue, user) }
shared_examples 'user can see confidential issue' do |access_level|
context "when a user is a #{access_level}" do
......
{
"type": "object",
"allOf": [
{
"required" : [
"id",
"confidential",
"title",
"state",
"created_at",
"closed_at",
"reference",
"path",
"link_type"
],
"properties" : {
"id": { "type": "integer" },
"confidential": { "type": "boolean" },
"title": { "type": "string" },
"state": { "type": "string" },
"created_at": { "type": "string", "format": "date-time" },
"closed_at": { "type": ["string", "null"], "format": "date-time" },
"reference": { "type": "string" },
"path": { "type": "string" },
"link_type": { "type": "string" }
},
"additionalProperties": false
}
]
"properties" : {
"id": { "type": "integer" },
"confidential": { "type": "boolean" },
"title": { "type": "string" },
"state": { "type": "string" },
"created_at": { "type": "string", "format": "date-time" },
"closed_at": { "type": ["string", "null"], "format": "date-time" },
"relation_path": { "type": ["string", "null"] },
"reference": { "type": "string" },
"path": { "type": "string" },
"link_type": { "type": "string" }
},
"required" : [
"id",
"confidential",
"title",
"state",
"created_at",
"closed_at",
"relation_path",
"reference",
"path",
"link_type"
],
"additionalProperties": false
}
......@@ -6,8 +6,10 @@ RSpec.describe Groups::Epics::RelatedEpicLinksController do
let_it_be(:user) { create(:user) }
let_it_be(:epic) { create(:epic) }
let_it_be(:epic2) { create(:epic, group: epic.group) }
let_it_be(:epic_link1) { create(:related_epic_link, source: epic, target: epic2) }
let_it_be(:epic3) { create(:epic, group: epic.group) }
let_it_be(:epic_link1) { create(:related_epic_link, source: epic, target: epic3) }
let_it_be(:epic_link2) { create(:related_epic_link, source: epic) }
let_it_be(:listing_service) { Epics::RelatedEpicLinks::ListService }
before do
stub_licensed_features(epics: true, related_epics: true)
......@@ -113,4 +115,84 @@ RSpec.describe Groups::Epics::RelatedEpicLinksController do
end
end
end
describe 'POST /groups/*group_id/-/epics/:epic_id/related_epic_links' do
let(:issuable_references) { [epic2.to_reference(full: true)] }
subject(:request) do
post group_epic_related_epic_links_path(related_epics_params(issuable_references: issuable_references))
end
before do
epic.group.add_developer(user)
epic2.group.add_developer(user)
login_as user
end
context 'with success' do
it 'returns JSON response' do
request
list_service_response = listing_service.new(epic, user).execute
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq('message' => nil,
'issuables' => list_service_response.as_json)
end
it 'delegates the creation of the related epic link to Epics::RelatedEpicLinks::CreateService' do
expect_next_instance_of(Epics::RelatedEpicLinks::CreateService) do |service|
expect(service).to receive(:execute).once.and_call_original
end
request
expect(response).to have_gitlab_http_status(:ok)
end
it 'creates a new Epic::RelatedEpicLink record' do
expect { request }.to change { Epic::RelatedEpicLink.count }.by(1)
end
it 'returns correct relation path in response' do
request
related_epic_link = Epic::RelatedEpicLink.find_by(source: epic, target: epic2)
expect(json_response['issuables'].last)
.to include('relation_path' => "/groups/#{epic.group.path}/-/epics/#{epic.iid}/related_epic_links/#{related_epic_link&.id}")
end
end
context 'with failure' do
context 'when unauthorized' do
it 'returns 403' do
epic.group.add_guest(user)
request
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when failing service result' do
let(:issuable_references) { ["##{non_existing_record_iid}"] }
it 'returns failure JSON' do
request
list_service_response = listing_service.new(epic, user).execute
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response).to eq('message' => 'No matching epic found. Make sure that you are adding a valid epic URL.', 'issuables' => list_service_response.as_json)
end
end
it_behaves_like 'a not available action'
end
end
def related_epics_params(opts = {})
opts.reverse_merge(group_id: epic.group,
epic_id: epic.iid,
format: :json)
end
end
......@@ -5,8 +5,9 @@ require 'spec_helper'
RSpec.describe Epics::RelatedEpicEntity do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:group_2) { create(:group) }
let_it_be(:source) { create(:epic, group: group) }
let_it_be(:target) { create(:epic, group: group) }
let_it_be(:target) { create(:epic, group: group_2) }
let_it_be(:epic_link) { create(:related_epic_link, source: source, target: target) }
let(:request) { EntityRequest.new(issuable: epic_link.source, current_user: user) }
......@@ -18,9 +19,35 @@ RSpec.describe Epics::RelatedEpicEntity do
group.add_developer(user)
end
describe '#as_json' do
it 'matches json schema' do
expect(entity.to_json).to match_schema('entities/related_epic', dir: 'ee')
describe '#to_json' do
context 'when user can admin_epic on target epic group' do
before do
group_2.add_reporter(user)
end
it 'matches json schema' do
expect(entity.to_json).to match_schema('entities/related_epic', dir: 'ee')
end
it 'returns relation_path' do
path = Gitlab::Routing.url_helpers.group_epic_related_epic_link_path(source.group, source.iid, epic_link.id)
expect(entity.as_json[:relation_path]).to eq(path)
end
end
context 'when user cannot admin_epic on target epic group' do
before do
group_2.add_guest(user)
end
it 'matches json schema' do
expect(entity.to_json).to match_schema('entities/related_epic', dir: 'ee')
end
it 'returns null relation_path' do
expect(entity.as_json[:relation_path]).to eq(nil)
end
end
end
end
......@@ -50,27 +50,15 @@ RSpec.describe IssueLinks::CreateService do
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: 'blocks')
expect(IssueLink.find_by!(target: another_project_issue)).to have_attributes(source: issue, link_type: 'blocks')
end
it 'returns success status' do
is_expected.to eq(status: :success)
end
context 'when blocked_by relation is used' do
let(:params) do
{ issuable_references: [issue_a_ref], link_type: 'is_blocked_by' }
end
it 'creates creates `blocks` relation with swapped source and target' do
expect { subject }.to change(IssueLink, :count).from(0).to(1)
expect(IssueLink.find_by!(source: issue_a)).to have_attributes(target: issue, link_type: 'blocks')
end
it_behaves_like 'issuable link creation with blocking link_type' do
let(:issuable_link_class) { IssueLink }
let(:issuable) { issue }
let(:issuable2) { issue_a }
let(:issuable3) { another_project_issue }
end
end
......
......@@ -87,6 +87,17 @@ RSpec.describe ::SystemNotes::IssuablesService do
subject
end
end
describe '#relate_issuable' do
let(:noteable) { epic }
let(:target) { create(:epic) }
it 'creates system notes when relating epics' do
result = service.relate_issuable(target)
expect(result.note).to eq("marked this epic as related to #{target.to_reference(target.group, full: true)}")
end
end
end
describe '#unrelate_issuable' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Epics::RelatedEpicLinks::CreateService do
describe '#execute' do
let_it_be(:user) { create :user }
let_it_be(:group) { create :group }
let_it_be(:issuable) { create :epic, group: group }
let_it_be(:issuable2) { create :epic, group: group }
let_it_be(:guest_issuable) { create :epic }
let_it_be(:another_group) { create :group }
let_it_be(:issuable3) { create :epic, group: another_group }
let_it_be(:issuable_a) { create :epic, group: group }
let_it_be(:issuable_b) { create :epic, group: group }
let_it_be(:issuable_link) { create :related_epic_link, source: issuable, target: issuable_b, link_type: IssuableLink::TYPE_RELATES_TO }
let(:issuable_parent) { issuable.group }
let(:issuable_type) { :epic }
let(:issuable_link_class) { Epic::RelatedEpicLink }
let(:params) { {} }
before do
stub_licensed_features(epics: true, related_epics: true)
group.add_developer(user)
guest_issuable.group.add_guest(user)
another_group.add_developer(user)
end
it_behaves_like 'issuable link creation'
it_behaves_like 'issuable link creation with blocking link_type' do
let(:params) do
{ issuable_references: [issuable2.to_reference, issuable3.to_reference(issuable3.group, full: true)] }
end
end
context 'when related_epics is not available for target epic' do
let(:params) do
{ issuable_references: [issuable3.to_reference(issuable3.group, full: true)] }
end
subject { described_class.new(issuable, user, params).execute }
before do
stub_licensed_features(epics: true, related_epics: false)
allow(issuable.group).to receive(:licensed_feature_available?).with(:related_epics).and_return(true)
end
it 'creates relationships' do
expect { subject }.to change(issuable_link_class, :count).by(1)
expect(issuable_link_class.find_by!(target: issuable3)).to have_attributes(source: issuable, link_type: 'relates_to')
end
end
end
end
# frozen_string_literal: true
shared_examples 'issuable link creation with blocking link_type' do
subject { described_class.new(issuable, user, params).execute }
context 'when is_blocked_by relation is used' do
before do
params[:link_type] = 'is_blocked_by'
end
it 'creates `blocks` relation with swapped source and target' do
expect { subject }.to change(issuable_link_class, :count).by(2)
expect(issuable_link_class.find_by!(source: issuable2)).to have_attributes(target: issuable, link_type: 'blocks')
expect(issuable_link_class.find_by!(source: issuable3)).to have_attributes(target: issuable, link_type: 'blocks')
end
end
context 'when blocks relation is used' do
before do
params[:link_type] = 'blocks'
end
it 'creates `blocks` relation' do
expect { subject }.to change(issuable_link_class, :count).by(2)
expect(issuable_link_class.find_by!(target: issuable2)).to have_attributes(source: issuable, link_type: 'blocks')
expect(issuable_link_class.find_by!(target: issuable3)).to have_attributes(source: issuable, link_type: 'blocks')
end
end
end
......@@ -691,6 +691,9 @@ msgstr ""
msgid "%{issuableType} will be removed! Are you sure?"
msgstr ""
msgid "%{issuable}(s) already assigned"
msgstr ""
msgid "%{issueType} actions"
msgstr ""
......@@ -20495,9 +20498,6 @@ msgstr ""
msgid "Issue weight"
msgstr ""
msgid "Issue(s) already assigned"
msgstr ""
msgid "IssueAnalytics|Age"
msgstr ""
......@@ -24859,7 +24859,7 @@ msgstr ""
msgid "No matches found"
msgstr ""
msgid "No matching issue found. Make sure that you are adding a valid issue URL."
msgid "No matching %{issuable} found. Make sure that you are adding a valid %{issuable} URL."
msgstr ""
msgid "No matching labels"
......
......@@ -4,180 +4,42 @@ 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
let_it_be(:user) { create :user }
let_it_be(:namespace) { create :namespace }
let_it_be(:project) { create :project, namespace: namespace }
let_it_be(:issuable) { create :issue, project: project }
let_it_be(:issuable2) { create :issue, project: project }
let_it_be(:guest_issuable) { create :issue }
let_it_be(:another_project) { create :project, namespace: project.namespace }
let_it_be(:issuable3) { create :issue, project: another_project }
let_it_be(:issuable_a) { create :issue, project: project }
let_it_be(:issuable_b) { create :issue, project: project }
let_it_be(:issuable_link) { create :issue_link, source: issuable, target: issuable_b, link_type: IssueLink::TYPE_RELATES_TO }
let(:issuable_parent) { issuable.project }
let(:issuable_type) { :issue }
let(:issuable_link_class) { IssueLink }
let(:params) { {} }
before do
project.add_developer(user)
guest_issuable.project.add_guest(user)
another_project.add_developer(user)
end
subject { described_class.new(issue, user, params).execute }
it_behaves_like 'issuable link creation'
context 'when the reference list is empty' do
let(:params) do
{ issuable_references: [] }
end
context 'when target is an incident' do
let_it_be(:issue) { create(:incident, project: project) }
it 'returns error' do
is_expected.to eq(message: 'No matching issue found. Make sure that you are adding a valid issue URL.', status: :error, http_status: 404)
end
end
context 'when Issue not found' do
let(:params) do
{ issuable_references: ["##{non_existing_record_iid}"] }
end
it 'returns error' do
is_expected.to eq(message: 'No matching issue found. Make sure that you are adding a valid issue URL.', status: :error, http_status: 404)
{ issuable_references: [issuable2.to_reference, issuable3.to_reference(another_project)] }
end
it 'no relationship is created' do
expect { subject }.not_to change(IssueLink, :count)
end
end
context 'when user has no permission to target project Issue' do
let(:target_issuable) { create :issue }
let(:params) do
{ issuable_references: [target_issuable.to_reference(project)] }
end
it 'returns error' do
target_issuable.project.add_guest(user)
is_expected.to eq(message: 'No matching issue found. Make sure that you are adding a valid issue URL.', status: :error, http_status: 404)
end
it 'no relationship is created' do
expect { subject }.not_to change(IssueLink, :count)
end
end
context 'source and target are the same issue' do
let(:params) do
{ issuable_references: [issue.to_reference] }
end
it 'does not create notes' do
expect(SystemNoteService).not_to receive(:relate_issue)
subject
end
it 'no relationship is created' do
expect { subject }.not_to change(IssueLink, :count)
end
end
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] }
end
before do
another_project.add_developer(user)
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: '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
is_expected.to eq(status: :success)
end
it 'creates notes' do
# First two-way relation notes
expect(SystemNoteService).to receive(:relate_issue)
.with(issue, issue_a, user)
expect(SystemNoteService).to receive(:relate_issue)
.with(issue_a, issue, user)
# Second two-way relation notes
expect(SystemNoteService).to receive(:relate_issue)
.with(issue, another_project_issue, user)
expect(SystemNoteService).to receive(:relate_issue)
.with(another_project_issue, issue, user)
subject
end
context 'issue is an incident' do
let(:issue) { create(:incident, project: project) }
it_behaves_like 'an incident management tracked event', :incident_management_incident_relate do
let(:current_user) { user }
end
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_RELATES_TO
end
let(:params) do
{
issuable_references: [
issue_a.to_reference,
issue_b.to_reference,
issue_c.to_reference
],
link_type: IssueLink::TYPE_RELATES_TO
}
end
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).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
end
context 'when there are invalid references' do
let(:issue_a) { create :issue, project: project }
let(:params) do
{ issuable_references: [issue.to_reference, issue_a.to_reference] }
end
it 'creates links only for valid references' do
expect { subject }.to change { IssueLink.count }.by(1)
end
subject { described_class.new(issue, user, params).execute }
it 'returns error status' do
expect(subject).to eq(
status: :error,
http_status: 422,
message: "#{issue.to_reference} cannot be added: cannot be related to itself"
)
it_behaves_like 'an incident management tracked event', :incident_management_incident_relate do
let(:current_user) { user }
end
end
end
......
......@@ -100,7 +100,7 @@ RSpec.describe SystemNoteService do
end
end
describe '.relate_issue' do
describe '.relate_issuable' do
let(:noteable_ref) { double }
let(:noteable) { double }
......@@ -110,10 +110,10 @@ RSpec.describe SystemNoteService do
it 'calls IssuableService' do
expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
expect(service).to receive(:relate_issue).with(noteable_ref)
expect(service).to receive(:relate_issuable).with(noteable_ref)
end
described_class.relate_issue(noteable, noteable_ref, double)
described_class.relate_issuable(noteable, noteable_ref, double)
end
end
......
......@@ -14,10 +14,10 @@ RSpec.describe ::SystemNotes::IssuablesService do
let(:service) { described_class.new(noteable: noteable, project: project, author: author) }
describe '#relate_issue' do
describe '#relate_issuable' do
let(:noteable_ref) { create(:issue) }
subject { service.relate_issue(noteable_ref) }
subject { service.relate_issuable(noteable_ref) }
it_behaves_like 'a system note' do
let(:action) { 'relate' }
......
# frozen_string_literal: true
shared_examples 'issuable link creation' do
describe '#execute' do
subject { described_class.new(issuable, user, params).execute }
context 'when the reference list is empty' do
let(:params) do
{ issuable_references: [] }
end
it 'returns error' do
is_expected.to eq(message: "No matching #{issuable_type} found. Make sure that you are adding a valid #{issuable_type} URL.", status: :error, http_status: 404)
end
end
context 'when Issuable not found' do
let(:params) do
{ issuable_references: ["##{non_existing_record_iid}"] }
end
it 'returns error' do
is_expected.to eq(message: "No matching #{issuable_type} found. Make sure that you are adding a valid #{issuable_type} URL.", status: :error, http_status: 404)
end
it 'no relationship is created' do
expect { subject }.not_to change(issuable_link_class, :count)
end
end
context 'when user has no permission to target issuable' do
let(:params) do
{ issuable_references: [guest_issuable.to_reference(issuable_parent)] }
end
it 'returns error' do
is_expected.to eq(message: "No matching #{issuable_type} found. Make sure that you are adding a valid #{issuable_type} URL.", status: :error, http_status: 404)
end
it 'no relationship is created' do
expect { subject }.not_to change(issuable_link_class, :count)
end
end
context 'source and target are the same issuable' do
let(:params) do
{ issuable_references: [issuable.to_reference] }
end
it 'does not create notes' do
expect(SystemNoteService).not_to receive(:relate_issuable)
subject
end
it 'no relationship is created' do
expect { subject }.not_to change(issuable_link_class, :count)
end
end
context 'when there is an issuable to relate' do
let(:params) do
{ issuable_references: [issuable2.to_reference, issuable3.to_reference(issuable_parent)] }
end
it 'creates relationships' do
expect { subject }.to change(issuable_link_class, :count).by(2)
expect(issuable_link_class.find_by!(target: issuable2)).to have_attributes(source: issuable, link_type: 'relates_to')
expect(issuable_link_class.find_by!(target: issuable3)).to have_attributes(source: issuable, link_type: 'relates_to')
end
it 'returns success status' do
is_expected.to eq(status: :success)
end
it 'creates notes' do
# First two-way relation notes
expect(SystemNoteService).to receive(:relate_issuable)
.with(issuable, issuable2, user)
expect(SystemNoteService).to receive(:relate_issuable)
.with(issuable2, issuable, user)
# Second two-way relation notes
expect(SystemNoteService).to receive(:relate_issuable)
.with(issuable, issuable3, user)
expect(SystemNoteService).to receive(:relate_issuable)
.with(issuable3, issuable, user)
subject
end
end
context 'when reference of any already related issue is present' do
let(:params) do
{
issuable_references: [
issuable_a.to_reference,
issuable_b.to_reference
],
link_type: IssueLink::TYPE_RELATES_TO
}
end
it 'creates notes only for new relations' do
expect(SystemNoteService).to receive(:relate_issuable).with(issuable, issuable_a, anything)
expect(SystemNoteService).to receive(:relate_issuable).with(issuable_a, issuable, anything)
expect(SystemNoteService).not_to receive(:relate_issuable).with(issuable, issuable_b, anything)
expect(SystemNoteService).not_to receive(:relate_issuable).with(issuable_b, issuable, anything)
subject
end
end
context 'when there are invalid references' do
let(:params) do
{ issuable_references: [issuable.to_reference, issuable_a.to_reference] }
end
it 'creates links only for valid references' do
expect { subject }.to change { issuable_link_class.count }.by(1)
end
it 'returns error status' do
expect(subject).to eq(
status: :error,
http_status: 422,
message: "#{issuable.to_reference} cannot be added: cannot be related to itself"
)
end
end
end
end
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