Commit b34bc6fe authored by Sean McGivern's avatar Sean McGivern

Merge branch '2001-related-issues' into 'master'

Related issues [BE]

Closes #2001

See merge request !1719
parents e9fcae37 959b31c7
module Projects
class IssueLinksController < ApplicationController
before_action :authorize_admin_issue_link!, only: [:create, :destroy]
def index
render json: issues
end
def create
create_params = params.slice(:issue_references)
result = IssueLinks::CreateService.new(issue, current_user, create_params).execute
render json: { message: result[:message], issues: issues }, status: result[:http_status]
end
def destroy
issue_link = IssueLink.find(params[:id])
return render_403 unless can?(current_user, :admin_issue_link, issue_link.target.project)
IssueLinks::DestroyService.new(issue_link, current_user).execute
render json: { issues: issues }
end
private
def issues
IssueLinks::ListService.new(issue, current_user).execute
end
def authorize_admin_issue_link!
render_403 unless can?(current_user, :admin_issue_link, @project)
end
def issue
@issue ||=
IssuesFinder.new(current_user, project_id: @project.id)
.execute
.find_by!(iid: params[:issue_id])
end
end
end
......@@ -16,6 +16,7 @@
# label_name: string
# sort: string
# non_archived: boolean
# feature_availability_check: boolean (default: true)
# iids: integer[]
#
class IssuableFinder
......@@ -25,11 +26,15 @@ class IssuableFinder
ARRAY_PARAMS = { label_name: [], iids: [] }.freeze
VALID_PARAMS = (SCALAR_PARAMS + [ARRAY_PARAMS]).freeze
DEFAULT_PARAMS = {
feature_availability_check: true
}.freeze
attr_accessor :current_user, :params
def initialize(current_user, params = {})
@current_user = current_user
@params = params
@params = DEFAULT_PARAMS.merge(params).with_indifferent_access
end
def execute
......@@ -126,7 +131,20 @@ class IssuableFinder
ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids(items)).execute
end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
# Querying through feature availability for an user is expensive
# (i.e. https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1719#note_31406525),
# and there are cases which a project level access check should be enough.
# In any case, `feature_availability_check` param should be kept with `true`
# by default.
#
projects =
if params[:feature_availability_check]
projects.with_feature_available_for_user(klass, current_user)
else
projects
end
@projects = projects.reorder(nil)
end
def search
......
class IssueLink < ActiveRecord::Base
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
private
def check_self_relation
return unless source && target
if source == target
errors.add(:source, 'cannot be related to itself')
end
end
end
......@@ -6,11 +6,13 @@ class License < ActiveRecord::Base
GEO_FEATURE = 'GitLab_Geo'.freeze
AUDITOR_USER_FEATURE = 'GitLab_Auditor_User'.freeze
SERVICE_DESK_FEATURE = 'GitLab_ServiceDesk'.freeze
RELATED_ISSUES_FEATURE = 'RelatedIssues'.freeze
FEATURE_CODES = {
geo: GEO_FEATURE,
auditor_user: AUDITOR_USER_FEATURE,
service_desk: SERVICE_DESK_FEATURE,
related_issues: RELATED_ISSUES_FEATURE,
# Features that make sense to Namespace:
deploy_board: DEPLOY_BOARD_FEATURE,
file_lock: FILE_LOCK_FEATURE
......@@ -22,7 +24,7 @@ class License < ActiveRecord::Base
EARLY_ADOPTER_PLAN = 'early_adopter'.freeze
EES_FEATURES = [
# ..
{ RELATED_ISSUES_FEATURE => 1 }
].freeze
EEP_FEATURES = [
......
......@@ -3,7 +3,7 @@ class SystemNoteMetadata < ActiveRecord::Base
commit description merge confidential visible label assignee cross_reference
title time_tracking branch milestone discussion task moved opened closed merged
outdated
approved unapproved
approved unapproved relate unrelate
].freeze
validates :note, presence: true
......
......@@ -22,6 +22,11 @@ module EE
cannot! :create_note
cannot! :read_project
end
unless project.feature_available?(:related_issues)
cannot! :read_issue_link
cannot! :admin_issue_link
end
end
end
end
......@@ -55,6 +55,9 @@ class ProjectPolicy < BasePolicy
can! :read_pipeline_schedule
can! :read_build
end
# EE-only
can! :read_issue_link
end
def reporter_access!
......@@ -79,6 +82,9 @@ class ProjectPolicy < BasePolicy
if project.feature_available?(:deploy_board) || Rails.env.development?
can! :read_deploy_board
end
# EE-only
can! :admin_issue_link
end
# Permissions given when an user is team member of a project
......@@ -321,5 +327,8 @@ class ProjectPolicy < BasePolicy
# NOTE: may be overridden by IssuePolicy
can! :read_issue
# EE-only
can! :read_issue_link
end
end
module IssueLinks
class CreateService < BaseService
def initialize(issue, user, params)
@issue, @current_user, @params = issue, user, params.dup
end
def execute
if referenced_issues.blank?
return error('No Issue found for given reference', 401)
end
create_issue_links
success
end
private
def create_issue_links
referenced_issues.each do |referenced_issue|
create_notes(referenced_issue) if relate_issues(referenced_issue)
end
end
def relate_issues(referenced_issue)
IssueLink.create(source: @issue, target: referenced_issue)
end
def create_notes(referenced_issue)
SystemNoteService.relate_issue(@issue, referenced_issue, current_user)
SystemNoteService.relate_issue(referenced_issue, @issue, current_user)
end
def referenced_issues
@referenced_issues ||= begin
issue_references = params[:issue_references]
text = issue_references.join(' ')
extractor = Gitlab::ReferenceExtractor.new(@issue.project, @current_user)
extractor.analyze(text)
extractor.issues.select do |issue|
can?(current_user, :admin_issue_link, issue)
end
end
end
end
end
module IssueLinks
class DestroyService < BaseService
def initialize(issue_link, user)
@issue_link = issue_link
@current_user = user
@issue = issue_link.source
@referenced_issue = issue_link.target
end
def execute
remove_relation
create_notes
success(message: 'Relation was removed')
end
private
def remove_relation
@issue_link.destroy!
end
def create_notes
SystemNoteService.unrelate_issue(@issue, @referenced_issue, current_user)
SystemNoteService.unrelate_issue(@referenced_issue, @issue, current_user)
end
end
end
module IssueLinks
class ListService
include Gitlab::Routing
def initialize(issue, user)
@issue, @current_user, @project = issue, user, issue.project
end
def execute
issues.map do |referenced_issue|
{
id: referenced_issue.id,
iid: referenced_issue.iid,
title: referenced_issue.title,
state: referenced_issue.state,
project_path: referenced_issue.project.path,
namespace_full_path: referenced_issue.project.namespace.full_path,
path: namespace_project_issue_path(referenced_issue.project.namespace, referenced_issue.project, referenced_issue.iid),
destroy_relation_path: destroy_relation_path(referenced_issue)
}
end
end
private
def issues
related_issues = Issue
.select(['issues.*', 'issue_links.id AS issue_links_id'])
.joins("INNER JOIN issue_links ON
(issue_links.source_id = issues.id AND issue_links.target_id = #{@issue.id})
OR
(issue_links.target_id = issues.id AND issue_links.source_id = #{@issue.id})")
.preload(project: :namespace)
.reorder('issue_links_id')
Ability.issues_readable_by_user(related_issues, @current_user)
end
def destroy_relation_path(issue)
# Make sure the user can admin both the current issue AND the
# referenced issue projects in order to return the removal link.
if can_destroy_issue_link_on_current_project? && can_destroy_issue_link?(issue.project)
namespace_project_issue_link_path(@project.namespace,
@issue.project,
@issue.iid,
issue.issue_links_id)
end
end
def can_destroy_issue_link_on_current_project?
return @can_destroy_on_current_project if defined?(@can_destroy_on_current_project)
@can_destroy_on_current_project = can_destroy_issue_link?(@project)
end
def can_destroy_issue_link?(project)
Ability.allowed?(@current_user, :admin_issue_link, project)
end
end
end
......@@ -552,6 +552,38 @@ module SystemNoteService
create_note(NoteSummary.new(noteable, project, author, body, action: 'moved'))
end
#
# noteable - Noteable object
# noteable_ref - Referenced noteable object
# user - User performing reference
#
# Example Note text:
#
# "marked this issue as related to gitlab-ce#9001"
#
# Returns the created Note object
def relate_issue(noteable, noteable_ref, user)
body = "marked this issue as related to #{noteable_ref.to_reference(noteable.project)}"
create_note(NoteSummary.new(noteable, noteable.project, user, body, action: 'relate'))
end
#
# noteable - Noteable object
# noteable_ref - Referenced noteable object
# user - User performing reference
#
# Example Note text:
#
# "removed the relation with gitlab-ce#9001"
#
# Returns the created Note object
def unrelate_issue(noteable, noteable_ref, user)
body = "removed the relation with #{noteable_ref.to_reference(noteable.project)}"
create_note(NoteSummary.new(noteable, noteable.project, user, body, action: 'unrelate'))
end
# Called when the merge request is approved by user
#
# noteable - Noteable object
......
---
title: Allows manually adding bi-directional relationships between issues in the issue page (EES feature)
merge_request:
author:
......@@ -3,6 +3,11 @@
en:
hello: "Hello world"
activerecord:
attributes:
issue_link:
source: Source issue
target: Target issue
errors:
messages:
label_already_exists_at_group_level: "already exists at group level for %{group}. Please choose another one."
......
......@@ -311,6 +311,8 @@ constraints(ProjectUrlConstrainer.new) do
post :bulk_update
post :export_csv
end
resources :issue_links, only: [:index, :create, :destroy], as: 'links', path: 'links'
end
resources :project_members, except: [:show, :new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do
......
class CreateIssueLinksTable < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
create_table :issue_links do |t|
t.integer :source_id, null: false, index: true
t.integer :target_id, null: false, index: true
t.timestamps null: true
end
add_index :issue_links, [:source_id, :target_id], unique: true
add_concurrent_foreign_key :issue_links, :issues, column: :source_id
add_concurrent_foreign_key :issue_links, :issues, column: :target_id
end
def down
drop_table :issue_links
end
end
......@@ -591,6 +591,17 @@ ActiveRecord::Schema.define(version: 20170602003304) do
add_index "issue_assignees", ["issue_id", "user_id"], name: "index_issue_assignees_on_issue_id_and_user_id", unique: true, using: :btree
add_index "issue_assignees", ["user_id"], name: "index_issue_assignees_on_user_id", using: :btree
create_table "issue_links", force: :cascade do |t|
t.integer "source_id", null: false
t.integer "target_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "issue_links", ["source_id", "target_id"], name: "index_issue_links_on_source_id_and_target_id", unique: true, using: :btree
add_index "issue_links", ["source_id"], name: "index_issue_links_on_source_id", using: :btree
add_index "issue_links", ["target_id"], name: "index_issue_links_on_target_id", using: :btree
create_table "issue_metrics", force: :cascade do |t|
t.integer "issue_id", null: false
t.datetime "first_mentioned_in_commit_at"
......@@ -1708,6 +1719,8 @@ ActiveRecord::Schema.define(version: 20170602003304) do
add_foreign_key "container_repositories", "projects"
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
add_foreign_key "issue_links", "issues", column: "source_id", name: "fk_c900194ff2", on_delete: :cascade
add_foreign_key "issue_links", "issues", column: "target_id", name: "fk_e71bb44f1f", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade
......
......@@ -284,13 +284,18 @@ describe Projects::MergeRequestsController do
context 'number of queries' do
it 'verifies number of queries' do
RequestStore.begin!
# pre-create objects
merge_request
recorded = ActiveRecord::QueryRecorder.new { go(format: :json) }
expect(recorded.count).to be_within(10).of(100)
expect(recorded.count).to be_within(1).of(31)
expect(recorded.cached_count).to eq(0)
RequestStore.end!
RequestStore.clear!
end
end
end
......
FactoryGirl.define do
factory :issue_link do
source factory: :issue
target factory: :issue
end
end
......@@ -288,6 +288,18 @@ describe IssuesFinder do
expect(issues.count).to eq 0
end
it 'returns disabled issues if feature_availability_check param set to false' do
[project1, project2].each do |project|
project.project_feature.update!(issues_access_level: ProjectFeature::DISABLED)
end
issues = described_class
.new(search_user, params.reverse_merge(scope: scope, state: 'opened', feature_availability_check: false))
.execute
expect(issues.count).to eq 3
end
end
end
......
require 'spec_helper'
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 '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
......@@ -10,10 +10,14 @@ describe ProjectPolicy, models: true do
let(:admin) { create(:admin) }
let(:project) { create(:empty_project, :public, namespace: owner.namespace) }
before do
allow_any_instance_of(License).to receive(:feature_available?) { true }
end
let(:guest_permissions) do
%i[
read_project read_board read_list read_wiki read_issue read_label
read_milestone read_project_snippet read_project_member
read_issue_link read_milestone read_project_snippet read_project_member
read_note create_project create_issue create_note
upload_file
]
......@@ -22,7 +26,7 @@ describe ProjectPolicy, models: true do
let(:reporter_permissions) do
%i[
download_code fork_project create_project_snippet update_issue
admin_issue admin_label admin_list read_commit_status read_build
admin_issue admin_label admin_issue_link admin_list read_commit_status read_build
read_container_image read_pipeline read_environment read_deployment
read_merge_request download_wiki_code
]
......@@ -71,7 +75,7 @@ describe ProjectPolicy, models: true do
let(:auditor_permissions) do
%i[
download_code download_wiki_code read_project read_board read_list
read_wiki read_issue read_label read_milestone read_project_snippet
read_wiki read_issue read_label read_issue_link read_milestone read_project_snippet
read_project_member read_note read_cycle_analytics read_pipeline
read_build read_commit_status read_container_image read_environment
read_deployment read_merge_request read_pages
......
require 'rails_helper'
describe Projects::IssueLinksController do
let(:user) { create :user }
let(:project) { create(:project_empty_repo) }
let(:issue) { create :issue, project: project }
before do
allow_any_instance_of(License).to receive(:feature_available?) { false }
allow_any_instance_of(License).to receive(:feature_available?).with(:related_issues) { true }
end
describe 'GET /*namespace_id/:project_id/issues/:issue_id/links' do
let(:issue_b) { create :issue, project: project }
let!(:issue_link) { create :issue_link, source: issue, target: issue_b }
before do
project.team << [user, :guest]
login_as user
end
it 'returns JSON response' do
list_service_response = IssueLinks::ListService.new(issue, user).execute
get namespace_project_issue_links_path(issue_links_params)
expect(response).to have_http_status(200)
expect(json_response).to eq(list_service_response.as_json)
end
end
describe 'POST /*namespace_id/:project_id/issues/:issue_id/links' do
let(:issue_b) { create :issue, project: project }
before do
project.team << [user, user_role]
login_as user
end
context 'with success' do
let(:user_role) { :developer }
let(:issue_references) { [issue_b.to_reference] }
it 'returns success JSON' do
post namespace_project_issue_links_path(issue_links_params(issue_references: issue_references))
list_service_response = IssueLinks::ListService.new(issue, user).execute
expect(response).to have_http_status(200)
expect(json_response).to eq('message' => nil,
'issues' => list_service_response.as_json)
end
end
context 'with failure' do
context 'when unauthorized' do
let(:user_role) { :guest }
let(:issue_references) { [issue_b.to_reference] }
it 'returns 403' do
post namespace_project_issue_links_path(issue_links_params(issue_references: issue_references))
expect(response).to have_http_status(403)
end
end
context 'when failing service result' do
let(:user_role) { :developer }
let(:issue_references) { ['#999'] }
it 'returns failure JSON' do
post namespace_project_issue_links_path(issue_links_params(issue_references: issue_references))
list_service_response = IssueLinks::ListService.new(issue, user).execute
expect(response).to have_http_status(401)
expect(json_response).to eq('message' => 'No Issue found for given reference', 'issues' => list_service_response.as_json)
end
end
end
end
describe 'DELETE /*namespace_id/:project_id/issues/:issue_id/link/:id' do
let(:issue_link) { create :issue_link, target: referenced_issue }
before do
project.team << [user, user_role]
login_as user
end
context 'when unauthorized' do
context 'when no authorization on current project' do
let(:referenced_issue) { create :issue, project: project }
let(:user_role) { :guest }
it 'returns 403' do
delete namespace_project_issue_link_path(issue_links_params(id: issue_link.id))
expect(response).to have_http_status(403)
end
end
context 'when no authorization on the related issue project' do
# unauthorized project issue
let(:referenced_issue) { create :issue }
let(:user_role) { :developer }
it 'returns 403' do
delete namespace_project_issue_link_path(issue_links_params(id: issue_link.id))
expect(response).to have_http_status(403)
end
end
end
context 'when authorized' do
let(:referenced_issue) { create :issue, project: project }
let(:user_role) { :developer }
it 'returns success JSON' do
delete namespace_project_issue_link_path(issue_links_params(id: issue_link.id))
list_service_response = IssueLinks::ListService.new(issue, user).execute
expect(json_response).to eq('issues' => list_service_response.as_json)
end
end
end
def issue_links_params(opts = {})
opts.reverse_merge(namespace_id: issue.project.namespace,
project_id: issue.project,
issue_id: issue,
format: :json)
end
end
require 'spec_helper'
describe IssueLinks::CreateService, service: true do
describe '#execute' do
let(:namespace) { create :namespace }
let(:project) { create :empty_project, namespace: namespace }
let(:issue) { create :issue, project: project }
let(:user) { create :user }
let(:params) do
{}
end
before do
allow_any_instance_of(License).to receive(:feature_available?) { false }
allow_any_instance_of(License).to receive(:feature_available?).with(:related_issues) { true }
project.team << [user, :developer]
end
subject { described_class.new(issue, user, params).execute }
context 'when the reference list is empty' do
let(:params) do
{ issue_references: [] }
end
it 'returns error' do
is_expected.to eq(message: 'No Issue found for given reference', status: :error, http_status: 401)
end
end
context 'when Issue not found' do
let(:params) do
{ issue_references: ['#999'] }
end
it 'returns error' do
is_expected.to eq(message: 'No Issue found for given reference', status: :error, http_status: 401)
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_issue) { create :issue }
let(:params) do
{ issue_references: [target_issue.to_reference(project)] }
end
it 'returns error' do
target_issue.project.add_guest(user)
is_expected.to eq(message: 'No Issue found for given reference', status: :error, http_status: 401)
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 :empty_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
{ issue_references: [issue_a_ref, another_project_issue_ref] }
end
before do
another_project.team << [user, :developer]
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)
expect(IssueLink.find_by!(target: another_project_issue)).to have_attributes(source: issue)
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
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 }
before do
create :issue_link, source: issue, target: issue_a
end
let(:params) do
{ issue_references: [issue_b.to_reference, issue_a.to_reference] }
end
it 'returns success status' do
is_expected.to eq(status: :success)
end
it 'valid relations are created' do
expect { subject }.to change(IssueLink, :count).from(1).to(2)
expect(IssueLink.find_by!(target: issue_b)).to have_attributes(source: issue)
end
end
end
end
require 'spec_helper'
describe IssueLinks::DestroyService, service: true do
describe '#execute' do
let(:user) { create :user }
let!(:issue_link) { create :issue_link }
subject { described_class.new(issue_link, user).execute }
it 'removes related issue' do
expect { subject }.to change(IssueLink, :count).from(1).to(0)
end
it 'creates notes' do
# Two-way notes creation
expect(SystemNoteService).to receive(:unrelate_issue)
.with(issue_link.source, issue_link.target, user)
expect(SystemNoteService).to receive(:unrelate_issue)
.with(issue_link.target, issue_link.source, user)
subject
end
it 'returns success message' do
is_expected.to eq(message: 'Relation was removed', status: :success)
end
end
end
require 'spec_helper'
describe IssueLinks::ListService, service: true do
let(:user) { create :user }
let(:project) { create(:project_empty_repo, :private) }
let(:issue) { create :issue, project: project }
let(:user_role) { :developer }
before do
allow_any_instance_of(License).to receive(:feature_available?) { false }
allow_any_instance_of(License).to receive(:feature_available?).with(:related_issues) { true }
project.team << [user, user_role]
end
describe '#execute' do
subject { described_class.new(issue, user).execute }
context 'user can see all issues' do
let(:issue_b) { create :issue, project: project }
let(:issue_c) { create :issue, project: project }
let(:issue_d) { create :issue, project: project }
let!(:issue_link_c) do
create(:issue_link, source: issue_d,
target: issue)
end
let!(:issue_link_b) do
create(:issue_link, source: issue,
target: issue_c)
end
let!(:issue_link_a) do
create(:issue_link, source: issue,
target: issue_b)
end
it 'ensures no N+1 queries are made' do
control_count = ActiveRecord::QueryRecorder.new { subject }.count
project = create :empty_project, :public
issue_x = create :issue, project: project
issue_y = create :issue, project: project
issue_z = create :issue, project: project
create :issue_link, source: issue_x, target: issue_y
create :issue_link, source: issue_x, target: issue_z
create :issue_link, source: issue_y, target: issue_z
expect { subject }.not_to exceed_query_limit(control_count)
end
it 'returns related issues JSON' do
expect(subject.size).to eq(3)
expect(subject).to include(include(id: issue_b.id,
iid: issue_b.iid,
title: issue_b.title,
state: issue_b.state,
path: "/#{project.full_path}/issues/#{issue_b.iid}",
project_path: issue_b.project.path,
namespace_full_path: issue_b.project.namespace.full_path,
destroy_relation_path: "/#{project.full_path}/issues/#{issue.iid}/links/#{issue_link_a.id}"))
expect(subject).to include(include(id: issue_c.id,
iid: issue_c.iid,
title: issue_c.title,
state: issue_c.state,
path: "/#{project.full_path}/issues/#{issue_c.iid}",
project_path: issue_c.project.path,
namespace_full_path: issue_c.project.namespace.full_path,
destroy_relation_path: "/#{project.full_path}/issues/#{issue.iid}/links/#{issue_link_b.id}"))
expect(subject).to include(include(id: issue_d.id,
iid: issue_d.iid,
title: issue_d.title,
state: issue_d.state,
path: "/#{project.full_path}/issues/#{issue_d.iid}",
project_path: issue_d.project.path,
namespace_full_path: issue_d.project.namespace.full_path,
destroy_relation_path: "/#{project.full_path}/issues/#{issue.iid}/links/#{issue_link_c.id}"))
end
end
context 'referencing a public project issue' do
let(:public_project) { create :empty_project, :public }
let(:issue_b) { create :issue, project: public_project }
let!(:issue_link) do
create(:issue_link, source: issue, target: issue_b)
end
it 'presents issue' do
expect(subject.size).to eq(1)
end
end
context 'referencing issue with removed relationships' do
context 'when referenced a deleted issue' do
let(:issue_b) { create :issue, project: project }
let!(:issue_link) do
create(:issue_link, source: issue, target: issue_b)
end
it 'ignores issue' do
issue_b.destroy!
is_expected.to eq([])
end
end
context 'when referenced an issue with deleted project' do
let(:issue_b) { create :issue, project: project }
let!(:issue_link) do
create(:issue_link, source: issue, target: issue_b)
end
it 'ignores issue' do
project.destroy!
is_expected.to eq([])
end
end
context 'when referenced an issue with deleted namespace' do
let(:issue_b) { create :issue, project: project }
let!(:issue_link) do
create(:issue_link, source: issue, target: issue_b)
end
it 'ignores issue' do
project.namespace.destroy!
is_expected.to eq([])
end
end
end
context 'user cannot see relations' do
context 'when user cannot see the referenced issue' do
let!(:issue_link) do
create(:issue_link, source: issue)
end
it 'returns an empty list' do
is_expected.to eq([])
end
end
context 'when user cannot see the issue that referenced' do
let!(:issue_link) do
create(:issue_link, target: issue)
end
it 'returns an empty list' do
is_expected.to eq([])
end
end
end
context 'remove relations' do
let!(:issue_link) do
create(:issue_link, source: issue, target: referenced_issue)
end
context 'user can admin related issues just on target project' do
let(:user_role) { :guest }
let(:target_project) { create :empty_project }
let(:referenced_issue) { create :issue, project: target_project }
it 'returns no destroy relation path' do
target_project.add_developer(user)
expect(subject.first[:destroy_relation_path]).to be_nil
end
end
context 'user can admin related issues just on source project' do
let(:user_role) { :developer }
let(:target_project) { create :empty_project }
let(:referenced_issue) { create :issue, project: target_project }
it 'returns no destroy relation path' do
target_project.add_guest(user)
expect(subject.first[:destroy_relation_path]).to be_nil
end
end
context 'when user can admin related issues on both projects' do
let(:referenced_issue) { create :issue, project: project }
it 'returns related issue destroy relation path' do
expect(subject.first[:destroy_relation_path])
.to eq("/#{project.full_path}/issues/#{issue.iid}/links/#{issue_link.id}")
end
end
end
end
end
......@@ -899,6 +899,38 @@ describe SystemNoteService, services: true do
end
end
describe '.relate_issue' do
let(:noteable_ref) { create(:issue) }
subject { described_class.relate_issue(noteable, noteable_ref, author) }
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 { described_class.unrelate_issue(noteable, noteable_ref, author) }
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 '.approve_mr' do
let(:noteable) { create(:merge_request, source_project: project) }
subject { described_class.approve_mr(noteable, author) }
......
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