Commit 0a36bbc5 authored by Eugenia Grieff's avatar Eugenia Grieff Committed by Heinrich Lee Yu

Add support for relating epics

Add service to allow creating RelatedEpicLinks
Add create related epic links endpoint
Update pot files
Add specs
Expose relation_path in new entity

Changelog: other
EE: true
parent 5fd785d6
...@@ -36,6 +36,20 @@ module IssuableLinks ...@@ -36,6 +36,20 @@ module IssuableLinks
success success
end 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 private
def render_conflict_error? def render_conflict_error?
...@@ -96,6 +110,23 @@ module IssuableLinks ...@@ -96,6 +110,23 @@ module IssuableLinks
{} {}
end 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) def linkable_issuables(objects)
raise NotImplementedError raise NotImplementedError
end end
...@@ -104,16 +135,12 @@ module IssuableLinks ...@@ -104,16 +135,12 @@ module IssuableLinks
raise NotImplementedError raise NotImplementedError
end end
def relate_issuables(referenced_object) def link_class
raise NotImplementedError raise NotImplementedError
end end
def issuables_assigned_message def set_link_type(_link)
_("Issue(s) already assigned") # no-op
end
def issuables_not_found_message
_("No matching issue found. Make sure that you are adding a valid issue URL.")
end end
end end
end end
......
...@@ -2,44 +2,25 @@ ...@@ -2,44 +2,25 @@
module IssueLinks module IssueLinks
class CreateService < IssuableLinks::CreateService 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) def linkable_issuables(issues)
@linkable_issuables ||= begin @linkable_issuables ||= begin
issues.select { |issue| can?(current_user, :admin_issue_link, issue) } issues.select { |issue| can?(current_user, :admin_issue_link, issue) }
end end
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 def previous_related_issuables
@related_issues ||= issuable.related_issues(current_user).to_a @related_issues ||= issuable.related_issues(current_user).to_a
end end
private private
def set_link_type(_link)
# EE only
end
def track_event def track_event
track_incident_action(current_user, issuable, :incident_relate) track_incident_action(current_user, issuable, :incident_relate)
end end
def link_class
IssueLink
end
end end
end end
......
...@@ -49,8 +49,8 @@ module SystemNoteService ...@@ -49,8 +49,8 @@ module SystemNoteService
::SystemNotes::IssuablesService.new(noteable: issuable, project: project, author: author).change_issuable_contacts(added_count, removed_count) ::SystemNotes::IssuablesService.new(noteable: issuable, project: project, author: author).change_issuable_contacts(added_count, removed_count)
end end
def relate_issue(noteable, noteable_ref, user) def relate_issuable(noteable, noteable_ref, user)
::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).relate_issue(noteable_ref) ::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).relate_issuable(noteable_ref)
end end
def unrelate_issuable(noteable, noteable_ref, user) def unrelate_issuable(noteable, noteable_ref, user)
......
...@@ -10,8 +10,9 @@ module SystemNotes ...@@ -10,8 +10,9 @@ module SystemNotes
# "marked this issue as related to gitlab-foss#9001" # "marked this issue as related to gitlab-foss#9001"
# #
# Returns the created Note object # Returns the created Note object
def relate_issue(noteable_ref) def relate_issuable(noteable_ref)
body = "marked this issue as related to #{noteable_ref.to_reference(noteable.project)}" 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) issue_activity_counter.track_issue_related_action(author: author) if noteable.is_a?(Issue)
......
...@@ -10,7 +10,7 @@ class Groups::Epics::RelatedEpicLinksController < Groups::ApplicationController ...@@ -10,7 +10,7 @@ class Groups::Epics::RelatedEpicLinksController < Groups::ApplicationController
before_action :check_epics_available! before_action :check_epics_available!
before_action :check_related_epics_available! before_action :check_related_epics_available!
before_action :authorize_related_epic_link_association!, only: [:destroy] 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 feature_category :portfolio_management
urgency :default urgency :default
...@@ -37,7 +37,15 @@ class Groups::Epics::RelatedEpicLinksController < Groups::ApplicationController ...@@ -37,7 +37,15 @@ class Groups::Epics::RelatedEpicLinksController < Groups::ApplicationController
Epics::RelatedEpicLinks::DestroyService.new(link, current_user) Epics::RelatedEpicLinks::DestroyService.new(link, current_user)
end end
def create_service
Epics::RelatedEpicLinks::CreateService.new(epic, current_user, create_params)
end
def ensure_related_epics_enabled! def ensure_related_epics_enabled!
render_404 unless Feature.enabled?(:related_epics_widget, epic&.group, default_enabled: :yaml) render_404 unless Feature.enabled?(:related_epics_widget, epic&.group, default_enabled: :yaml)
end end
def create_params
params.permit(:link_type, issuable_references: [])
end
end end
...@@ -2,10 +2,17 @@ ...@@ -2,10 +2,17 @@
module Epics module Epics
class RelatedEpicEntity < Grape::Entity class RelatedEpicEntity < Grape::Entity
include Gitlab::Utils::StrongMemoize
include RequestAwareEntity include RequestAwareEntity
expose :id, :confidential, :title, :state, :created_at, :closed_at 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| expose :reference do |related_epic|
related_epic.to_reference(request.issuable.group) related_epic.to_reference(request.issuable.group)
end end
...@@ -17,5 +24,17 @@ module Epics ...@@ -17,5 +24,17 @@ module Epics
expose :link_type do |related_epic| expose :link_type do |related_epic|
related_epic.epic_link_type related_epic.epic_link_type
end 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
end end
...@@ -24,6 +24,19 @@ module EE ...@@ -24,6 +24,19 @@ module EE
def affected_epics(_issues) def affected_epics(_issues)
[] []
end 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 end
end end
...@@ -13,18 +13,6 @@ module EE ...@@ -13,18 +13,6 @@ module EE
private 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? def link_type_available?
# `blocked_by` is allowed as a param and handled in set_link_type # `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]) 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 ...@@ -6,7 +6,7 @@ module Epics
private private
def permission_to_remove_relation? 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 end
def track_event def track_event
......
...@@ -123,7 +123,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do ...@@ -123,7 +123,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
scope module: :epics do scope module: :epics do
resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } 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 end
collection do collection do
......
...@@ -249,7 +249,7 @@ RSpec.describe Projects::IssuesController do ...@@ -249,7 +249,7 @@ RSpec.describe Projects::IssuesController do
context 'with a related system note' do context 'with a related system note' do
let(:confidential_issue) { create(:issue, :confidential, project: project) } 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| shared_examples 'user can see confidential issue' do |access_level|
context "when a user is a #{access_level}" do context "when a user is a #{access_level}" do
......
{ {
"type": "object", "type": "object",
"allOf": [ "properties" : {
{ "id": { "type": "integer" },
"required" : [ "confidential": { "type": "boolean" },
"id", "title": { "type": "string" },
"confidential", "state": { "type": "string" },
"title", "created_at": { "type": "string", "format": "date-time" },
"state", "closed_at": { "type": ["string", "null"], "format": "date-time" },
"created_at", "relation_path": { "type": ["string", "null"] },
"closed_at", "reference": { "type": "string" },
"reference", "path": { "type": "string" },
"path", "link_type": { "type": "string" }
"link_type" },
], "required" : [
"properties" : { "id",
"id": { "type": "integer" }, "confidential",
"confidential": { "type": "boolean" }, "title",
"title": { "type": "string" }, "state",
"state": { "type": "string" }, "created_at",
"created_at": { "type": "string", "format": "date-time" }, "closed_at",
"closed_at": { "type": ["string", "null"], "format": "date-time" }, "relation_path",
"reference": { "type": "string" }, "reference",
"path": { "type": "string" }, "path",
"link_type": { "type": "string" } "link_type"
}, ],
"additionalProperties": false "additionalProperties": false
}
]
} }
...@@ -6,8 +6,10 @@ RSpec.describe Groups::Epics::RelatedEpicLinksController do ...@@ -6,8 +6,10 @@ RSpec.describe Groups::Epics::RelatedEpicLinksController do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:epic) { create(:epic) } let_it_be(:epic) { create(:epic) }
let_it_be(:epic2) { create(:epic, group: epic.group) } 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(:epic_link2) { create(:related_epic_link, source: epic) }
let_it_be(:listing_service) { Epics::RelatedEpicLinks::ListService }
before do before do
stub_licensed_features(epics: true, related_epics: true) stub_licensed_features(epics: true, related_epics: true)
...@@ -113,4 +115,84 @@ RSpec.describe Groups::Epics::RelatedEpicLinksController do ...@@ -113,4 +115,84 @@ RSpec.describe Groups::Epics::RelatedEpicLinksController do
end end
end 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 end
...@@ -5,8 +5,9 @@ require 'spec_helper' ...@@ -5,8 +5,9 @@ require 'spec_helper'
RSpec.describe Epics::RelatedEpicEntity do RSpec.describe Epics::RelatedEpicEntity do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:group_2) { create(:group) }
let_it_be(:source) { create(:epic, group: 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_it_be(:epic_link) { create(:related_epic_link, source: source, target: target) }
let(:request) { EntityRequest.new(issuable: epic_link.source, current_user: user) } let(:request) { EntityRequest.new(issuable: epic_link.source, current_user: user) }
...@@ -18,9 +19,35 @@ RSpec.describe Epics::RelatedEpicEntity do ...@@ -18,9 +19,35 @@ RSpec.describe Epics::RelatedEpicEntity do
group.add_developer(user) group.add_developer(user)
end end
describe '#as_json' do describe '#to_json' do
it 'matches json schema' do context 'when user can admin_epic on target epic group' do
expect(entity.to_json).to match_schema('entities/related_epic', dir: 'ee') 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 end
end end
...@@ -50,27 +50,15 @@ RSpec.describe IssueLinks::CreateService do ...@@ -50,27 +50,15 @@ RSpec.describe IssueLinks::CreateService do
end end
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 it 'returns success status' do
is_expected.to eq(status: :success) is_expected.to eq(status: :success)
end end
context 'when blocked_by relation is used' do it_behaves_like 'issuable link creation with blocking link_type' do
let(:params) do let(:issuable_link_class) { IssueLink }
{ issuable_references: [issue_a_ref], link_type: 'is_blocked_by' } let(:issuable) { issue }
end let(:issuable2) { issue_a }
let(:issuable3) { another_project_issue }
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
end end
end end
......
...@@ -87,6 +87,17 @@ RSpec.describe ::SystemNotes::IssuablesService do ...@@ -87,6 +87,17 @@ RSpec.describe ::SystemNotes::IssuablesService do
subject subject
end end
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 end
describe '#unrelate_issuable' do 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
...@@ -696,6 +696,9 @@ msgstr "" ...@@ -696,6 +696,9 @@ msgstr ""
msgid "%{issuableType} will be removed! Are you sure?" msgid "%{issuableType} will be removed! Are you sure?"
msgstr "" msgstr ""
msgid "%{issuable}(s) already assigned"
msgstr ""
msgid "%{issueType} actions" msgid "%{issueType} actions"
msgstr "" msgstr ""
...@@ -20467,9 +20470,6 @@ msgstr "" ...@@ -20467,9 +20470,6 @@ msgstr ""
msgid "Issue weight" msgid "Issue weight"
msgstr "" msgstr ""
msgid "Issue(s) already assigned"
msgstr ""
msgid "IssueAnalytics|Age" msgid "IssueAnalytics|Age"
msgstr "" msgstr ""
...@@ -24813,7 +24813,7 @@ msgstr "" ...@@ -24813,7 +24813,7 @@ msgstr ""
msgid "No matches found" msgid "No matches found"
msgstr "" 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 "" msgstr ""
msgid "No matching labels" msgid "No matching labels"
......
...@@ -4,180 +4,42 @@ require 'spec_helper' ...@@ -4,180 +4,42 @@ require 'spec_helper'
RSpec.describe IssueLinks::CreateService do RSpec.describe IssueLinks::CreateService do
describe '#execute' do describe '#execute' do
let(:namespace) { create :namespace } let_it_be(:user) { create :user }
let(:project) { create :project, namespace: namespace } let_it_be(:namespace) { create :namespace }
let(:issue) { create :issue, project: project } let_it_be(:project) { create :project, namespace: namespace }
let(:user) { create :user } let_it_be(:issuable) { create :issue, project: project }
let(:params) do let_it_be(:issuable2) { create :issue, project: project }
{} let_it_be(:guest_issuable) { create :issue }
end 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 before do
project.add_developer(user) project.add_developer(user)
guest_issuable.project.add_guest(user)
another_project.add_developer(user)
end end
subject { described_class.new(issue, user, params).execute } it_behaves_like 'issuable link creation'
context 'when the reference list is empty' do context 'when target is an incident' do
let(:params) do let_it_be(:issue) { create(:incident, project: project) }
{ issuable_references: [] }
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)
end
end
context 'when Issue not found' do
let(:params) do let(:params) do
{ issuable_references: ["##{non_existing_record_iid}"] } { issuable_references: [issuable2.to_reference, issuable3.to_reference(another_project)] }
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)
end end
it 'no relationship is created' do subject { described_class.new(issue, user, params).execute }
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
it 'returns error status' do it_behaves_like 'an incident management tracked event', :incident_management_incident_relate do
expect(subject).to eq( let(:current_user) { user }
status: :error,
http_status: 422,
message: "#{issue.to_reference} cannot be added: cannot be related to itself"
)
end end
end end
end end
......
...@@ -100,7 +100,7 @@ RSpec.describe SystemNoteService do ...@@ -100,7 +100,7 @@ RSpec.describe SystemNoteService do
end end
end end
describe '.relate_issue' do describe '.relate_issuable' do
let(:noteable_ref) { double } let(:noteable_ref) { double }
let(:noteable) { double } let(:noteable) { double }
...@@ -110,10 +110,10 @@ RSpec.describe SystemNoteService do ...@@ -110,10 +110,10 @@ RSpec.describe SystemNoteService do
it 'calls IssuableService' do it 'calls IssuableService' do
expect_next_instance_of(::SystemNotes::IssuablesService) do |service| 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 end
described_class.relate_issue(noteable, noteable_ref, double) described_class.relate_issuable(noteable, noteable_ref, double)
end end
end end
......
...@@ -14,10 +14,10 @@ RSpec.describe ::SystemNotes::IssuablesService do ...@@ -14,10 +14,10 @@ RSpec.describe ::SystemNotes::IssuablesService do
let(:service) { described_class.new(noteable: noteable, project: project, author: author) } let(:service) { described_class.new(noteable: noteable, project: project, author: author) }
describe '#relate_issue' do describe '#relate_issuable' do
let(:noteable_ref) { create(:issue) } let(:noteable_ref) { create(:issue) }
subject { service.relate_issue(noteable_ref) } subject { service.relate_issuable(noteable_ref) }
it_behaves_like 'a system note' do it_behaves_like 'a system note' do
let(:action) { 'relate' } 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