diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-21 03:10:44 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-21 03:10:44 +0300 |
commit | c7a46b04196859929e8e4c04fbcbf8490f228edf (patch) | |
tree | d378b8cdd9f49903ed6f61810f61fb61217b6e3e /spec | |
parent | 5c42c9355afa2bd5f95000b294ae6053f1d9219f (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
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) } |