Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-08-21 03:10:44 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-08-21 03:10:44 +0300
commitc7a46b04196859929e8e4c04fbcbf8490f228edf (patch)
treed378b8cdd9f49903ed6f61810f61fb61217b6e3e /spec
parent5c42c9355afa2bd5f95000b294ae6053f1d9219f (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/factories/issue_links.rb8
-rw-r--r--spec/finders/fork_targets_finder_spec.rb8
-rw-r--r--spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap2
-rw-r--r--spec/graphql/mutations/discussions/toggle_resolve_spec.rb2
-rw-r--r--spec/lib/gitlab/config_checker/external_database_checker_spec.rb81
-rw-r--r--spec/lib/gitlab/database_spec.rb40
-rw-r--r--spec/models/issue_link_spec.rb53
-rw-r--r--spec/models/issue_spec.rb44
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/todos/mark_done_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/todos/restore_spec.rb2
-rw-r--r--spec/serializers/fork_namespace_entity_spec.rb9
-rw-r--r--spec/serializers/linked_project_issue_entity_spec.rb23
-rw-r--r--spec/services/issue_links/create_service_spec.rb176
-rw-r--r--spec/services/issue_links/destroy_service_spec.rb61
-rw-r--r--spec/services/issue_links/list_service_spec.rb194
-rw-r--r--spec/services/issues/duplicate_service_spec.rb11
-rw-r--r--spec/services/issues/move_service_spec.rb39
-rw-r--r--spec/services/notes/quick_actions_service_spec.rb36
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb97
-rw-r--r--spec/services/system_note_service_spec.rb34
-rw-r--r--spec/services/system_notes/issuables_service_spec.rb32
23 files changed, 842 insertions, 118 deletions
diff --git a/spec/factories/issue_links.rb b/spec/factories/issue_links.rb
new file mode 100644
index 00000000000..884e4dfac08
--- /dev/null
+++ b/spec/factories/issue_links.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :issue_link do
+ source factory: :issue
+ target factory: :issue
+ end
+end
diff --git a/spec/finders/fork_targets_finder_spec.rb b/spec/finders/fork_targets_finder_spec.rb
index 7208f46cfff..12f01227af8 100644
--- a/spec/finders/fork_targets_finder_spec.rb
+++ b/spec/finders/fork_targets_finder_spec.rb
@@ -35,5 +35,13 @@ RSpec.describe ForkTargetsFinder do
it 'returns all user manageable namespaces' do
expect(finder.execute).to match_array([user.namespace, maintained_group, owned_group, project.namespace])
end
+
+ it 'returns only groups when only_groups option is passed' do
+ expect(finder.execute(only_groups: true)).to match_array([maintained_group, owned_group, project.namespace])
+ end
+
+ it 'returns groups relation when only_groups option is passed' do
+ expect(finder.execute(only_groups: true)).to include(a_kind_of(Group))
+ end
end
end
diff --git a/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap b/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
index 5fad0d07f97..30dc58f0f28 100644
--- a/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
+++ b/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
@@ -47,4 +47,4 @@ exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = `
</gl-tab-stub>
</gl-tabs-stub>
</gl-modal-stub>
-`;
+`; \ No newline at end of file
diff --git a/spec/graphql/mutations/discussions/toggle_resolve_spec.rb b/spec/graphql/mutations/discussions/toggle_resolve_spec.rb
index 9ac4d6ab165..d779a2227c1 100644
--- a/spec/graphql/mutations/discussions/toggle_resolve_spec.rb
+++ b/spec/graphql/mutations/discussions/toggle_resolve_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe Mutations::Discussions::ToggleResolve do
it 'raises an error' do
expect { subject }.to raise_error(
Gitlab::Graphql::Errors::ArgumentError,
- "#{discussion.to_global_id} is not a valid id for Discussion."
+ "#{discussion.to_global_id} is not a valid ID for Discussion."
)
end
end
diff --git a/spec/lib/gitlab/config_checker/external_database_checker_spec.rb b/spec/lib/gitlab/config_checker/external_database_checker_spec.rb
index 712903e020a..85bafc77553 100644
--- a/spec/lib/gitlab/config_checker/external_database_checker_spec.rb
+++ b/spec/lib/gitlab/config_checker/external_database_checker_spec.rb
@@ -6,84 +6,35 @@ RSpec.describe Gitlab::ConfigChecker::ExternalDatabaseChecker do
describe '#check' do
subject { described_class.check }
- let_it_be(:deprecation_warning) { "Please upgrade" }
- let_it_be(:upcoming_deprecation_warning) { "Please consider upgrading" }
-
- context 'when database meets minimum version and there is no upcoming deprecation' do
+ context 'when database meets minimum supported version' do
before do
allow(Gitlab::Database).to receive(:postgresql_minimum_supported_version?).and_return(true)
- allow(Gitlab::Database).to receive(:postgresql_upcoming_deprecation?).and_return(false)
end
it { is_expected.to be_empty }
end
- context 'when database does not meet minimum version and there is no upcoming deprecation' do
+ context 'when database does not meet minimum supported version' do
before do
allow(Gitlab::Database).to receive(:postgresql_minimum_supported_version?).and_return(false)
- allow(Gitlab::Database).to receive(:postgresql_upcoming_deprecation?).and_return(false)
- end
-
- it 'only returns notice about deprecated database version' do
- is_expected.to include(a_hash_including(message: include(deprecation_warning)))
- is_expected.not_to include(a_hash_including(message: include(upcoming_deprecation_warning)))
end
- end
- context 'when database meets minimum version and there is an upcoming deprecation' do
- before do
- allow(Gitlab::Database).to receive(:postgresql_minimum_supported_version?).and_return(true)
- allow(Gitlab::Database).to receive(:postgresql_upcoming_deprecation?).and_return(true)
+ let(:notice_deprecated_database) do
+ {
+ type: 'warning',
+ message: _('You are using PostgreSQL %{pg_version_current}, but PostgreSQL ' \
+ '%{pg_version_minimum} is required for this version of GitLab. ' \
+ 'Please upgrade your environment to a supported PostgreSQL version, ' \
+ 'see %{pg_requirements_url} for details.') % {
+ pg_version_current: Gitlab::Database.version,
+ pg_version_minimum: Gitlab::Database::MINIMUM_POSTGRES_VERSION,
+ pg_requirements_url: '<a href="https://docs.gitlab.com/ee/install/requirements.html#database">database requirements</a>'
+ }
+ }
end
- context 'inside the deprecation notice window' do
- before do
- allow(Gitlab::Database).to receive(:within_deprecation_notice_window?).and_return(true)
- end
-
- it 'only returns notice about an upcoming deprecation' do
- is_expected.to include(a_hash_including(message: include(upcoming_deprecation_warning)))
- is_expected.not_to include(a_hash_including(message: include(deprecation_warning)))
- end
- end
-
- context 'outside the deprecation notice window' do
- before do
- allow(Gitlab::Database).to receive(:within_deprecation_notice_window?).and_return(false)
- end
-
- it { is_expected.to be_empty }
- end
- end
-
- context 'when database does not meet minimum version and there is an upcoming deprecation' do
- before do
- allow(Gitlab::Database).to receive(:postgresql_minimum_supported_version?).and_return(false)
- allow(Gitlab::Database).to receive(:postgresql_upcoming_deprecation?).and_return(true)
- end
-
- context 'inside the deprecation notice window' do
- before do
- allow(Gitlab::Database).to receive(:within_deprecation_notice_window?).and_return(true)
- end
-
- it 'returns notice about deprecated database version and an upcoming deprecation' do
- is_expected.to include(
- a_hash_including(message: include(deprecation_warning)),
- a_hash_including(message: include(upcoming_deprecation_warning))
- )
- end
- end
-
- context 'outside the deprecation notice window' do
- before do
- allow(Gitlab::Database).to receive(:within_deprecation_notice_window?).and_return(false)
- end
-
- it 'only returns notice about deprecated database version' do
- is_expected.to include(a_hash_including(message: include(deprecation_warning)))
- is_expected.not_to include(a_hash_including(message: include(upcoming_deprecation_warning)))
- end
+ it 'reports deprecated database notice' do
+ is_expected.to contain_exactly(notice_deprecated_database)
end
end
end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 47d2cb05240..420aa0a8df6 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -109,46 +109,6 @@ RSpec.describe Gitlab::Database do
end
end
- describe '.postgresql_upcoming_deprecation?' do
- it 'returns true when database version is lower than the upcoming minimum' do
- allow(described_class).to receive(:version).and_return('11')
-
- expect(described_class.postgresql_upcoming_deprecation?).to eq(true)
- end
-
- it 'returns false when database version equals the upcoming minimum' do
- allow(described_class).to receive(:version).and_return('12')
-
- expect(described_class.postgresql_upcoming_deprecation?).to eq(false)
- end
-
- it 'returns false when database version is greater the upcoming minimum' do
- allow(described_class).to receive(:version).and_return('13')
-
- expect(described_class.postgresql_upcoming_deprecation?).to eq(false)
- end
- end
-
- describe '.within_deprecation_notice_window?' do
- using RSpec::Parameterized::TableSyntax
-
- where(:case_name, :days, :result) do
- 'outside window' | Gitlab::Database::DEPRECATION_WINDOW_DAYS + 1 | false
- 'equal to window' | Gitlab::Database::DEPRECATION_WINDOW_DAYS | true
- 'within window' | Gitlab::Database::DEPRECATION_WINDOW_DAYS - 1 | true
- end
-
- with_them do
- it "returns #{params[:result]} when #{params[:case_name]}" do
- allow(Date)
- .to receive(:today)
- .and_return Date.parse(Gitlab::Database::UPCOMING_POSTGRES_VERSION_DETAILS[:gl_version_date]) - days
-
- expect(described_class.within_deprecation_notice_window?).to eq(result)
- end
- end
- end
-
describe '.check_postgres_version_and_print_warning' do
subject { described_class.check_postgres_version_and_print_warning }
diff --git a/spec/models/issue_link_spec.rb b/spec/models/issue_link_spec.rb
new file mode 100644
index 00000000000..00791d4a48b
--- /dev/null
+++ b/spec/models/issue_link_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IssueLink do
+ describe 'Associations' do
+ it { is_expected.to belong_to(:source).class_name('Issue') }
+ it { is_expected.to belong_to(:target).class_name('Issue') }
+ end
+
+ describe 'link_type' do
+ it { is_expected.to define_enum_for(:link_type).with_values(relates_to: 0, blocks: 1, is_blocked_by: 2) }
+
+ it 'provides the "related" as default link_type' do
+ expect(create(:issue_link).link_type).to eq 'relates_to'
+ end
+ end
+
+ describe 'Validation' do
+ subject { create :issue_link }
+
+ it { is_expected.to validate_presence_of(:source) }
+ it { is_expected.to validate_presence_of(:target) }
+ it do
+ is_expected.to validate_uniqueness_of(:source)
+ .scoped_to(:target_id)
+ .with_message(/already related/)
+ end
+
+ context 'self relation' do
+ let(:issue) { create :issue }
+
+ context 'cannot be validated' do
+ it 'does not invalidate object with self relation error' do
+ issue_link = build :issue_link, source: issue, target: nil
+
+ issue_link.valid?
+
+ expect(issue_link.errors[:source]).to be_empty
+ end
+ end
+
+ context 'can be invalidated' do
+ it 'invalidates object' do
+ issue_link = build :issue_link, source: issue, target: issue
+
+ expect(issue_link).to be_invalid
+ expect(issue_link.errors[:source]).to include('cannot be related to itself')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 59634524e74..af0348b972a 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -311,6 +311,50 @@ RSpec.describe Issue do
end
end
+ describe '#related_issues' do
+ let(:user) { create(:user) }
+ let(:authorized_project) { create(:project) }
+ let(:authorized_project2) { create(:project) }
+ let(:unauthorized_project) { create(:project) }
+
+ let(:authorized_issue_a) { create(:issue, project: authorized_project) }
+ let(:authorized_issue_b) { create(:issue, project: authorized_project) }
+ let(:authorized_issue_c) { create(:issue, project: authorized_project2) }
+
+ let(:unauthorized_issue) { create(:issue, project: unauthorized_project) }
+
+ let!(:issue_link_a) { create(:issue_link, source: authorized_issue_a, target: authorized_issue_b) }
+ let!(:issue_link_b) { create(:issue_link, source: authorized_issue_a, target: unauthorized_issue) }
+ let!(:issue_link_c) { create(:issue_link, source: authorized_issue_a, target: authorized_issue_c) }
+
+ before do
+ authorized_project.add_developer(user)
+ authorized_project2.add_developer(user)
+ end
+
+ it 'returns only authorized related issues for given user' do
+ expect(authorized_issue_a.related_issues(user))
+ .to contain_exactly(authorized_issue_b, authorized_issue_c)
+ end
+
+ it 'returns issues with valid issue_link_type' do
+ link_types = authorized_issue_a.related_issues(user).map(&:issue_link_type)
+
+ expect(link_types).not_to be_empty
+ expect(link_types).not_to include(nil)
+ end
+
+ describe 'when a user cannot read cross project' do
+ it 'only returns issues within the same project' do
+ expect(Ability).to receive(:allowed?).with(user, :read_all_resources, :global).at_least(:once).and_call_original
+ expect(Ability).to receive(:allowed?).with(user, :read_cross_project).and_return(false)
+
+ expect(authorized_issue_a.related_issues(user))
+ .to contain_exactly(authorized_issue_b)
+ end
+ end
+ end
+
describe '#can_move?' do
let(:issue) { create(:issue) }
diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
index 0e2da94f0f9..10ca2cf1cf8 100644
--- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
@@ -101,7 +101,7 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do
graphql_mutation(:create_annotation, variables)
end
- it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab id.']
+ it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab ID.']
end
end
end
@@ -188,7 +188,7 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do
graphql_mutation(:create_annotation, variables)
end
- it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab id.']
+ it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab ID.']
end
end
diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
index 2459a6f3828..7357f3e1e35 100644
--- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete do
graphql_mutation(:delete_annotation, variables)
end
- it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab id.']
+ it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab ID.']
end
context 'when the delete fails' do
diff --git a/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb
index 9c4733f6769..c1232500d79 100644
--- a/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb
+++ b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb
@@ -80,7 +80,7 @@ RSpec.describe 'Marking todos done' do
context 'when using an invalid gid' do
let(:input) { { id: 'invalid_gid' } }
- let(:invalid_gid_error) { 'invalid_gid is not a valid GitLab id.' }
+ let(:invalid_gid_error) { 'invalid_gid is not a valid GitLab ID.' }
it 'contains the expected error' do
post_graphql_mutation(mutation, current_user: current_user)
diff --git a/spec/requests/api/graphql/mutations/todos/restore_spec.rb b/spec/requests/api/graphql/mutations/todos/restore_spec.rb
index 6dedde56e13..0797961f65f 100644
--- a/spec/requests/api/graphql/mutations/todos/restore_spec.rb
+++ b/spec/requests/api/graphql/mutations/todos/restore_spec.rb
@@ -80,7 +80,7 @@ RSpec.describe 'Restoring Todos' do
context 'when using an invalid gid' do
let(:input) { { id: 'invalid_gid' } }
- let(:invalid_gid_error) { 'invalid_gid is not a valid GitLab id.' }
+ let(:invalid_gid_error) { 'invalid_gid is not a valid GitLab ID.' }
it 'contains the expected error' do
post_graphql_mutation(mutation, current_user: current_user)
diff --git a/spec/serializers/fork_namespace_entity_spec.rb b/spec/serializers/fork_namespace_entity_spec.rb
index 7ce6b77da44..7740ed77540 100644
--- a/spec/serializers/fork_namespace_entity_spec.rb
+++ b/spec/serializers/fork_namespace_entity_spec.rb
@@ -8,13 +8,17 @@ RSpec.describe ForkNamespaceEntity do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
+ let_it_be(:namespace) { create(:group, :with_avatar, description: 'test') }
+ let(:memberships) do
+ user.members.index_by(&:source_id)
+ end
- let(:namespace) { create(:group, :with_avatar, description: 'test') }
- let(:entity) { described_class.new(namespace, current_user: user, project: project) }
+ let(:entity) { described_class.new(namespace, current_user: user, project: project, memberships: memberships) }
subject(:json) { entity.as_json }
before do
+ namespace.add_developer(user)
project.add_maintainer(user)
end
@@ -52,7 +56,6 @@ RSpec.describe ForkNamespaceEntity do
end
it 'exposes human readable permission level' do
- namespace.add_developer(user)
expect(json[:permission]).to eql 'Developer'
end
diff --git a/spec/serializers/linked_project_issue_entity_spec.rb b/spec/serializers/linked_project_issue_entity_spec.rb
new file mode 100644
index 00000000000..864b5c45599
--- /dev/null
+++ b/spec/serializers/linked_project_issue_entity_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe LinkedProjectIssueEntity do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue_link) { create(:issue_link) }
+
+ let(:request) { double('request') }
+ let(:related_issue) { issue_link.source.related_issues(user).first }
+ let(:entity) { described_class.new(related_issue, request: request, current_user: user) }
+
+ before do
+ allow(request).to receive(:current_user).and_return(user)
+ allow(request).to receive(:issuable).and_return(issue_link.source)
+ issue_link.target.project.add_developer(user)
+ end
+
+ describe 'issue_link_type' do
+ it { expect(entity.as_json).to include(link_type: 'relates_to') }
+ end
+end
diff --git a/spec/services/issue_links/create_service_spec.rb b/spec/services/issue_links/create_service_spec.rb
new file mode 100644
index 00000000000..a60950e64cb
--- /dev/null
+++ b/spec/services/issue_links/create_service_spec.rb
@@ -0,0 +1,176 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IssueLinks::CreateService do
+ describe '#execute' do
+ let(:namespace) { create :namespace }
+ let(:project) { create :project, namespace: namespace }
+ let(:issue) { create :issue, project: project }
+ let(:user) { create :user }
+ let(:params) do
+ {}
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ subject { described_class.new(issue, 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 Issue found for given params', 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 Issue found for given params', status: :error, http_status: 404)
+ 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 Issue found for given params', 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
+ 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
+ expect(subject).to eq(
+ status: :error,
+ http_status: 422,
+ message: "#{issue.to_reference} cannot be added: cannot be related to itself"
+ )
+ end
+ end
+ end
+end
diff --git a/spec/services/issue_links/destroy_service_spec.rb b/spec/services/issue_links/destroy_service_spec.rb
new file mode 100644
index 00000000000..3f5b7ccf9ba
--- /dev/null
+++ b/spec/services/issue_links/destroy_service_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IssueLinks::DestroyService do
+ describe '#execute' do
+ let(:project) { create(:project_empty_repo) }
+ let(:user) { create(:user) }
+
+ subject { described_class.new(issue_link, user).execute }
+
+ context 'when successfully removes an issue link' do
+ let(:issue_a) { create(:issue, project: project) }
+ let(:issue_b) { create(:issue, project: project) }
+
+ let!(:issue_link) { create(:issue_link, source: issue_a, target: issue_b) }
+
+ before do
+ project.add_reporter(user)
+ end
+
+ 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
+
+ context 'when failing to remove an issue link' do
+ let(:unauthorized_project) { create(:project) }
+ let(:issue_a) { create(:issue, project: project) }
+ let(:issue_b) { create(:issue, project: unauthorized_project) }
+
+ let!(:issue_link) { create(:issue_link, source: issue_a, target: issue_b) }
+
+ it 'does not remove relation' do
+ expect { subject }.not_to change(IssueLink, :count).from(1)
+ end
+
+ it 'does not create notes' do
+ expect(SystemNoteService).not_to receive(:unrelate_issue)
+ end
+
+ it 'returns error message' do
+ is_expected.to eq(message: 'No Issue Link found', status: :error, http_status: 404)
+ end
+ end
+ end
+end
diff --git a/spec/services/issue_links/list_service_spec.rb b/spec/services/issue_links/list_service_spec.rb
new file mode 100644
index 00000000000..7a3ba845c7c
--- /dev/null
+++ b/spec/services/issue_links/list_service_spec.rb
@@ -0,0 +1,194 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IssueLinks::ListService do
+ let(:user) { create :user }
+ let(:project) { create(:project_empty_repo, :private) }
+ let(:issue) { create :issue, project: project }
+ let(:user_role) { :developer }
+
+ before do
+ project.add_role(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 :project, :public
+ milestone = create :milestone, project: project
+ issue_x = create :issue, project: project, milestone: milestone
+ issue_y = create :issue, project: project, assignees: [user]
+ 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,
+ title: issue_b.title,
+ state: issue_b.state,
+ reference: issue_b.to_reference(project),
+ path: "/#{project.full_path}/-/issues/#{issue_b.iid}",
+ relation_path: "/#{project.full_path}/-/issues/#{issue.iid}/links/#{issue_link_a.id}"))
+
+ expect(subject).to include(include(id: issue_c.id,
+ title: issue_c.title,
+ state: issue_c.state,
+ reference: issue_c.to_reference(project),
+ path: "/#{project.full_path}/-/issues/#{issue_c.iid}",
+ relation_path: "/#{project.full_path}/-/issues/#{issue.iid}/links/#{issue_link_b.id}"))
+
+ expect(subject).to include(include(id: issue_d.id,
+ title: issue_d.title,
+ state: issue_d.state,
+ reference: issue_d.to_reference(project),
+ path: "/#{project.full_path}/-/issues/#{issue_d.iid}",
+ 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 :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 :project }
+ let(:referenced_issue) { create :issue, project: target_project }
+
+ it 'returns no destroy relation path' do
+ target_project.add_developer(user)
+
+ expect(subject.first[: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 :project }
+ let(:referenced_issue) { create :issue, project: target_project }
+
+ it 'returns no destroy relation path' do
+ target_project.add_guest(user)
+
+ expect(subject.first[: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[:relation_path])
+ .to eq("/#{project.full_path}/-/issues/#{issue.iid}/links/#{issue_link.id}")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/issues/duplicate_service_spec.rb b/spec/services/issues/duplicate_service_spec.rb
index 78e030e6ac7..0b5bc3f32ef 100644
--- a/spec/services/issues/duplicate_service_spec.rb
+++ b/spec/services/issues/duplicate_service_spec.rb
@@ -83,6 +83,17 @@ RSpec.describe Issues::DuplicateService do
expect(duplicate_issue.reload.duplicated_to).to eq(canonical_issue)
end
+
+ it 'relates the duplicate issues' do
+ canonical_project.add_reporter(user)
+ duplicate_project.add_reporter(user)
+
+ subject.execute(duplicate_issue, canonical_issue)
+
+ issue_link = IssueLink.last
+ expect(issue_link.source).to eq(duplicate_issue)
+ expect(issue_link.target).to eq(canonical_issue)
+ end
end
end
end
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index 5f944d1213b..5d032b28399 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -223,6 +223,45 @@ RSpec.describe Issues::MoveService do
end
end
+ describe '#rewrite_related_issues' do
+ include_context 'user can move issue'
+
+ let(:admin) { create(:admin) }
+ let(:authorized_project) { create(:project) }
+ let(:authorized_project2) { create(:project) }
+ let(:unauthorized_project) { create(:project) }
+
+ let(:authorized_issue_b) { create(:issue, project: authorized_project) }
+ let(:authorized_issue_c) { create(:issue, project: authorized_project2) }
+ let(:authorized_issue_d) { create(:issue, project: authorized_project2) }
+ let(:unauthorized_issue) { create(:issue, project: unauthorized_project) }
+
+ let!(:issue_link_a) { create(:issue_link, source: old_issue, target: authorized_issue_b) }
+ let!(:issue_link_b) { create(:issue_link, source: old_issue, target: unauthorized_issue) }
+ let!(:issue_link_c) { create(:issue_link, source: old_issue, target: authorized_issue_c) }
+ let!(:issue_link_d) { create(:issue_link, source: authorized_issue_d, target: old_issue) }
+
+ before do
+ authorized_project.add_developer(user)
+ authorized_project2.add_developer(user)
+ end
+
+ context 'multiple related issues' do
+ it 'moves all related issues and retains permissions' do
+ new_issue = move_service.execute(old_issue, new_project)
+
+ expect(new_issue.related_issues(admin))
+ .to match_array([authorized_issue_b, authorized_issue_c, authorized_issue_d, unauthorized_issue])
+
+ expect(new_issue.related_issues(user))
+ .to match_array([authorized_issue_b, authorized_issue_c, authorized_issue_d])
+
+ expect(authorized_issue_d.related_issues(user))
+ .to match_array([new_issue])
+ end
+ end
+ end
+
context 'updating sent notifications' do
let!(:old_issue_notification_1) { create(:sent_notification, project: old_issue.project, noteable: old_issue) }
let!(:old_issue_notification_2) { create(:sent_notification, project: old_issue.project, noteable: old_issue) }
diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb
index e9decd44730..794491fc50d 100644
--- a/spec/services/notes/quick_actions_service_spec.rb
+++ b/spec/services/notes/quick_actions_service_spec.rb
@@ -4,9 +4,9 @@ require 'spec_helper'
RSpec.describe Notes::QuickActionsService do
shared_context 'note on noteable' do
- let(:project) { create(:project, :repository) }
- let(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } }
- let(:assignee) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } }
+ let_it_be(:assignee) { create(:user) }
before do
project.add_maintainer(assignee)
@@ -41,6 +41,36 @@ RSpec.describe Notes::QuickActionsService do
end
end
+ context '/relate' do
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:other_issue) { create(:issue, project: project) }
+ let(:note_text) { "/relate #{other_issue.to_reference}" }
+ let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) }
+
+ context 'user cannot relate issues' do
+ before do
+ project.team.find_member(maintainer.id).destroy!
+ project.update!(visibility: Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'does not create issue relation' do
+ expect do
+ _, update_params = service.execute(note)
+ service.apply_updates(update_params, note)
+ end.not_to change { IssueLink.count }
+ end
+ end
+
+ context 'user is allowed to relate issues' do
+ it 'creates issue relation' do
+ expect do
+ _, update_params = service.execute(note)
+ service.apply_updates(update_params, note)
+ end.to change { IssueLink.count }.by(1)
+ end
+ end
+ end
+
describe '/reopen' do
before do
note.noteable.close!
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 57e32b1aea9..b970a48051f 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -1644,6 +1644,103 @@ RSpec.describe QuickActions::InterpretService do
end
end
end
+
+ context 'relate command' do
+ let_it_be_with_refind(:group) { create(:group) }
+
+ shared_examples 'relate command' do
+ it 'relates issues' do
+ service.execute(content, issue)
+
+ expect(IssueLink.where(source: issue).map(&:target)).to match_array(issues_related)
+ end
+ end
+
+ context 'user is member of group' do
+ before do
+ group.add_developer(developer)
+ end
+
+ context 'relate a single issue' do
+ let(:other_issue) { create(:issue, project: project) }
+ let(:issues_related) { [other_issue] }
+ let(:content) { "/relate #{other_issue.to_reference}" }
+
+ it_behaves_like 'relate command'
+ end
+
+ context 'relate multiple issues at once' do
+ let(:second_issue) { create(:issue, project: project) }
+ let(:third_issue) { create(:issue, project: project) }
+ let(:issues_related) { [second_issue, third_issue] }
+ let(:content) { "/relate #{second_issue.to_reference} #{third_issue.to_reference}" }
+
+ it_behaves_like 'relate command'
+ end
+
+ context 'empty relate command' do
+ let(:issues_related) { [] }
+ let(:content) { '/relate' }
+
+ it_behaves_like 'relate command'
+ end
+
+ context 'already having related issues' do
+ let(:second_issue) { create(:issue, project: project) }
+ let(:third_issue) { create(:issue, project: project) }
+ let(:issues_related) { [second_issue, third_issue] }
+ let(:content) { "/relate #{third_issue.to_reference(project)}" }
+
+ before do
+ create(:issue_link, source: issue, target: second_issue)
+ end
+
+ it_behaves_like 'relate command'
+ end
+
+ context 'cross project' do
+ let(:another_group) { create(:group, :public) }
+ let(:other_project) { create(:project, group: another_group) }
+
+ before do
+ another_group.add_developer(developer)
+ end
+
+ context 'relate a cross project issue' do
+ let(:other_issue) { create(:issue, project: other_project) }
+ let(:issues_related) { [other_issue] }
+ let(:content) { "/relate #{other_issue.to_reference(project)}" }
+
+ it_behaves_like 'relate command'
+ end
+
+ context 'relate multiple cross projects issues at once' do
+ let(:second_issue) { create(:issue, project: other_project) }
+ let(:third_issue) { create(:issue, project: other_project) }
+ let(:issues_related) { [second_issue, third_issue] }
+ let(:content) { "/relate #{second_issue.to_reference(project)} #{third_issue.to_reference(project)}" }
+
+ it_behaves_like 'relate command'
+ end
+
+ context 'relate a non-existing issue' do
+ let(:issues_related) { [] }
+ let(:content) { "/relate imaginary##{non_existing_record_iid}" }
+
+ it_behaves_like 'relate command'
+ end
+
+ context 'relate a private issue' do
+ let(:private_project) { create(:project, :private) }
+ let(:other_issue) { create(:issue, project: private_project) }
+ let(:issues_related) { [] }
+ let(:content) { "/relate #{other_issue.to_reference(project)}" }
+
+ it_behaves_like 'relate command'
+ end
+ end
+ end
+ end
end
describe '#explain' do
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 969e5955609..0f5558738ac 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -86,6 +86,40 @@ RSpec.describe SystemNoteService do
end
end
+ describe '.relate_issue' do
+ let(:noteable_ref) { double }
+ let(:noteable) { double }
+
+ before do
+ allow(noteable).to receive(:project).and_return(double)
+ end
+
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:relate_issue).with(noteable_ref)
+ end
+
+ described_class.relate_issue(noteable, noteable_ref, double)
+ end
+ end
+
+ describe '.unrelate_issue' do
+ let(:noteable_ref) { double }
+ let(:noteable) { double }
+
+ before do
+ allow(noteable).to receive(:project).and_return(double)
+ end
+
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:unrelate_issue).with(noteable_ref)
+ end
+
+ described_class.unrelate_issue(noteable, noteable_ref, double)
+ end
+ end
+
describe '.change_due_date' do
let(:due_date) { double }
diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb
index 1b5b26d90da..536e9792534 100644
--- a/spec/services/system_notes/issuables_service_spec.rb
+++ b/spec/services/system_notes/issuables_service_spec.rb
@@ -13,6 +13,38 @@ RSpec.describe ::SystemNotes::IssuablesService do
let(:service) { described_class.new(noteable: noteable, project: project, author: author) }
+ describe '#relate_issue' do
+ let(:noteable_ref) { create(:issue) }
+
+ subject { service.relate_issue(noteable_ref) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'relate' }
+ end
+
+ context 'when issue marks another as related' do
+ it 'sets the note text' do
+ expect(subject.note).to eq "marked this issue as related to #{noteable_ref.to_reference(project)}"
+ end
+ end
+ end
+
+ describe '#unrelate_issue' do
+ let(:noteable_ref) { create(:issue) }
+
+ subject { service.unrelate_issue(noteable_ref) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'unrelate' }
+ end
+
+ context 'when issue relation is removed' do
+ it 'sets the note text' do
+ expect(subject.note).to eq "removed the relation with #{noteable_ref.to_reference(project)}"
+ end
+ end
+ end
+
describe '#change_assignee' do
subject { service.change_assignee(assignee) }