From 5725be9508df7922c77cfab4a6c5501548e5e3ca Mon Sep 17 00:00:00 2001 From: Bastian Blank Date: Sat, 30 Mar 2019 00:50:02 +0100 Subject: Display maximum artifact size from runtime config The maximum artifact size was moved into runtime config some time ago. Update the instance configuration code to read this value. --- spec/models/instance_configuration_spec.rb | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'spec') diff --git a/spec/models/instance_configuration_spec.rb b/spec/models/instance_configuration_spec.rb index e65f97df3c3..43954511858 100644 --- a/spec/models/instance_configuration_spec.rb +++ b/spec/models/instance_configuration_spec.rb @@ -82,6 +82,13 @@ describe InstanceConfiguration do it 'returns the key artifacts_max_size' do expect(gitlab_ci.keys).to include(:artifacts_max_size) end + + it 'returns the key artifacts_max_size with values' do + stub_application_setting(max_artifacts_size: 200) + + expect(gitlab_ci[:artifacts_max_size][:default]).to eq(100.megabytes) + expect(gitlab_ci[:artifacts_max_size][:value]).to eq(200.megabytes) + end end end end -- cgit v1.2.1 From 703d62a549ea18b678edf05d9bbb3bf09d3d7356 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Wed, 3 Apr 2019 16:10:21 -0300 Subject: Extract EE specific files/lines for Discussion spec/services This is part of moving GitLab to single codebase --- spec/services/system_note_service_spec.rb | 49 ++++++++++++++++--------------- 1 file changed, 26 insertions(+), 23 deletions(-) (limited to 'spec') diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index b917de14b2e..ff994f3b922 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -1,6 +1,9 @@ +# frozen_string_literal: true + require 'spec_helper' describe SystemNoteService do + include ProjectForksHelper include Gitlab::Routing include RepoHelpers include AssetsHelpers @@ -619,7 +622,7 @@ describe SystemNoteService do context 'commit with cross-reference from fork' do let(:author2) { create(:project_member, :reporter, user: create(:user), project: project).user } - let(:forked_project) { Projects::ForkService.new(project, author2).execute } + let(:forked_project) { fork_project(project, author2, repository: true) } let(:commit2) { forked_project.commit } before do @@ -896,6 +899,28 @@ describe SystemNoteService do end end + describe '.change_time_estimate' do + subject { described_class.change_time_estimate(noteable, project, author) } + + it_behaves_like 'a system note' do + let(:action) { 'time_tracking' } + end + + context 'with a time estimate' do + it 'sets the note text' do + noteable.update_attribute(:time_estimate, 277200) + + expect(subject.note).to eq "changed time estimate to 1w 4d 5h" + end + end + + context 'without a time estimate' do + it 'sets the note text' do + expect(subject.note).to eq "removed time estimate" + end + end + end + describe '.discussion_continued_in_issue' do let(:discussion) { create(:diff_note_on_merge_request, project: project).to_discussion } let(:merge_request) { discussion.noteable } @@ -922,28 +947,6 @@ describe SystemNoteService do end end - describe '.change_time_estimate' do - subject { described_class.change_time_estimate(noteable, project, author) } - - it_behaves_like 'a system note' do - let(:action) { 'time_tracking' } - end - - context 'with a time estimate' do - it 'sets the note text' do - noteable.update_attribute(:time_estimate, 277200) - - expect(subject.note).to eq "changed time estimate to 1w 4d 5h" - end - end - - context 'without a time estimate' do - it 'sets the note text' do - expect(subject.note).to eq "removed time estimate" - end - end - end - describe '.change_time_spent' do # We need a custom noteable in order to the shared examples to be green. let(:noteable) do -- cgit v1.2.1 From 0f5b735685476ff51b26031e9b74fa8256354868 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 3 Apr 2019 22:00:29 -0700 Subject: Fix stage index migration failing in PostgreSQL 10 As discussed in https://www.postgresql.org/message-id/9922.1353433645%40sss.pgh.pa.us, the PostgreSQL window function last_value may not consider the right rows: Note that first_value, last_value, and nth_value consider only the rows within the "window frame", which by default contains the rows from the start of the partition through the last peer of the current row. This is likely to give unhelpful results for last_value and sometimes also nth_value. You can redefine the frame by adding a suitable frame specification (RANGE or ROWS) to the OVER clause. See Section 4.2.8 for more information about frame specifications. This query could be fixed by adding `RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING`, but that's quite verbose. It's simpler just to use the first_value function. Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/59985 --- spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb b/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb index f8107dd40b9..4db829b1e7b 100644 --- a/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb @@ -30,6 +30,6 @@ describe Gitlab::BackgroundMigration::MigrateStageIndex, :migration, schema: 201 described_class.new.perform(100, 101) - expect(stages.all.pluck(:position)).to eq [2, 3] + expect(stages.all.order(:id).pluck(:position)).to eq [2, 3] end end -- cgit v1.2.1 From 0adedbb4822a8daaa215b33c88ace136c31d042d Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Mon, 1 Apr 2019 15:11:08 +0900 Subject: Fix the bug that the project statistics is not updated --- spec/workers/project_cache_worker_spec.rb | 36 +++++++++------- .../update_project_statistics_worker_spec.rb | 48 ++++++++++++++++++++++ 2 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 spec/workers/update_project_statistics_worker_spec.rb (limited to 'spec') diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb index a7353227043..724947228b7 100644 --- a/spec/workers/project_cache_worker_spec.rb +++ b/spec/workers/project_cache_worker_spec.rb @@ -7,9 +7,9 @@ describe ProjectCacheWorker do let(:worker) { described_class.new } let(:project) { create(:project, :repository) } - let(:statistics) { project.statistics } - let(:lease_key) { "project_cache_worker:#{project.id}:update_statistics" } + let(:lease_key) { (["project_cache_worker", project.id] + statistics.sort).join(":") } let(:lease_timeout) { ProjectCacheWorker::LEASE_TIMEOUT } + let(:statistics) { [] } describe '#perform' do before do @@ -35,14 +35,6 @@ describe ProjectCacheWorker do end context 'with an existing project' do - it 'updates the project statistics' do - expect(worker).to receive(:update_statistics) - .with(kind_of(Project), %i(repository_size)) - .and_call_original - - worker.perform(project.id, [], %w(repository_size)) - end - it 'refreshes the method caches' do expect_any_instance_of(Repository).to receive(:refresh_method_caches) .with(%i(readme)) @@ -51,6 +43,18 @@ describe ProjectCacheWorker do worker.perform(project.id, %w(readme)) end + context 'with statistics' do + let(:statistics) { %w(repository_size) } + + it 'updates the project statistics' do + expect(worker).to receive(:update_statistics) + .with(kind_of(Project), %i(repository_size)) + .and_call_original + + worker.perform(project.id, [], statistics) + end + end + context 'with plain readme' do it 'refreshes the method caches' do allow(MarkupHelper).to receive(:gitlab_markdown?).and_return(false) @@ -66,13 +70,15 @@ describe ProjectCacheWorker do end describe '#update_statistics' do + let(:statistics) { %w(repository_size) } + context 'when a lease could not be obtained' do it 'does not update the repository size' do stub_exclusive_lease_taken(lease_key, timeout: lease_timeout) - expect(statistics).not_to receive(:refresh!) + expect(UpdateProjectStatisticsWorker).not_to receive(:perform_in) - worker.update_statistics(project) + worker.update_statistics(project, statistics.map(&:to_sym)) end end @@ -80,11 +86,11 @@ describe ProjectCacheWorker do it 'updates the project statistics' do stub_exclusive_lease(lease_key, timeout: lease_timeout) - expect(statistics).to receive(:refresh!) - .with(only: %i(repository_size)) + expect(UpdateProjectStatisticsWorker).to receive(:perform_in) + .with(lease_timeout, project.id, statistics.map(&:to_sym)) .and_call_original - worker.update_statistics(project, %i(repository_size)) + worker.update_statistics(project, statistics.map(&:to_sym)) end end end diff --git a/spec/workers/update_project_statistics_worker_spec.rb b/spec/workers/update_project_statistics_worker_spec.rb new file mode 100644 index 00000000000..3411e10da7e --- /dev/null +++ b/spec/workers/update_project_statistics_worker_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe UpdateProjectStatisticsWorker do + let(:worker) { described_class.new } + let(:project) { create(:project, :repository) } + + describe '#perform' do + context 'with a non-existing project' do + it 'does nothing' do + expect_any_instance_of(ProjectStatistics).not_to receive(:refresh!) + + worker.perform(-1) + end + end + + context 'with an existing project without a repository' do + it 'does nothing' do + allow_any_instance_of(Repository).to receive(:exists?).and_return(false) + + expect_any_instance_of(ProjectStatistics).not_to receive(:refresh!) + + worker.perform(project.id) + end + end + + context 'with an existing project' do + it 'refreshes the project statistics' do + expect_any_instance_of(ProjectStatistics).to receive(:refresh!) + .with(only: []) + .and_call_original + + worker.perform(project.id) + end + + context 'with a specific statistics target' do + it 'refreshes the project repository size' do + statistics_target = %w(repository_size) + + expect_any_instance_of(ProjectStatistics).to receive(:refresh!) + .with(only: statistics_target.map(&:to_sym)) + .and_call_original + + worker.perform(project.id, statistics_target) + end + end + end + end +end -- cgit v1.2.1 From e6780501cbfd0cae31fadd15b2964204171091cc Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Fri, 5 Apr 2019 00:17:52 +0900 Subject: Refactor project_cache_worker_key --- spec/workers/project_cache_worker_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb index 724947228b7..7053bfc2bfd 100644 --- a/spec/workers/project_cache_worker_spec.rb +++ b/spec/workers/project_cache_worker_spec.rb @@ -7,7 +7,7 @@ describe ProjectCacheWorker do let(:worker) { described_class.new } let(:project) { create(:project, :repository) } - let(:lease_key) { (["project_cache_worker", project.id] + statistics.sort).join(":") } + let(:lease_key) { ["project_cache_worker", project.id, *statistics.sort].join(":") } let(:lease_timeout) { ProjectCacheWorker::LEASE_TIMEOUT } let(:statistics) { [] } -- cgit v1.2.1 From 074a1797fe581c8eb5cc65bd56af584d5c500688 Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Fri, 5 Apr 2019 00:18:17 +0900 Subject: Update the project statistics immediatelly --- spec/workers/project_cache_worker_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'spec') diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb index 7053bfc2bfd..d2445f420f8 100644 --- a/spec/workers/project_cache_worker_spec.rb +++ b/spec/workers/project_cache_worker_spec.rb @@ -86,6 +86,10 @@ describe ProjectCacheWorker do it 'updates the project statistics' do stub_exclusive_lease(lease_key, timeout: lease_timeout) + expect(project.statistics).to receive(:refresh!) + .with(only: statistics.map(&:to_sym)) + .and_call_original + expect(UpdateProjectStatisticsWorker).to receive(:perform_in) .with(lease_timeout, project.id, statistics.map(&:to_sym)) .and_call_original -- cgit v1.2.1 From 770f721962cd30e930ab7b6e06e9386da6325c3c Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Fri, 5 Apr 2019 22:07:09 +0900 Subject: Refactor: extract duplicate steps to a service class --- .../projects/update_statistics_service_spec.rb | 40 ++++++++++++++++++++ spec/workers/project_cache_worker_spec.rb | 19 ++++++---- .../update_project_statistics_worker_spec.rb | 43 +++------------------- 3 files changed, 57 insertions(+), 45 deletions(-) create mode 100644 spec/services/projects/update_statistics_service_spec.rb (limited to 'spec') diff --git a/spec/services/projects/update_statistics_service_spec.rb b/spec/services/projects/update_statistics_service_spec.rb new file mode 100644 index 00000000000..7e351c9ce54 --- /dev/null +++ b/spec/services/projects/update_statistics_service_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Projects::UpdateStatisticsService do + let(:service) { described_class.new(project, nil, statistics: statistics)} + let(:statistics) { %w(repository_size) } + + describe '#execute' do + context 'with a non-existing project' do + let(:project) { nil } + + it 'does nothing' do + expect_any_instance_of(ProjectStatistics).not_to receive(:refresh!) + + service.execute + end + end + + context 'with an existing project without a repository' do + let(:project) { create(:project) } + + it 'does nothing' do + expect_any_instance_of(ProjectStatistics).not_to receive(:refresh!) + + service.execute + end + end + + context 'with an existing project with a repository' do + let(:project) { create(:project, :repository) } + + it 'refreshes the project statistics' do + expect_any_instance_of(ProjectStatistics).to receive(:refresh!) + .with(only: statistics.map(&:to_sym)) + .and_call_original + + service.execute + end + end + end +end diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb index d2445f420f8..3c40269adc7 100644 --- a/spec/workers/project_cache_worker_spec.rb +++ b/spec/workers/project_cache_worker_spec.rb @@ -48,7 +48,7 @@ describe ProjectCacheWorker do it 'updates the project statistics' do expect(worker).to receive(:update_statistics) - .with(kind_of(Project), %i(repository_size)) + .with(kind_of(Project), statistics) .and_call_original worker.perform(project.id, [], statistics) @@ -73,28 +73,31 @@ describe ProjectCacheWorker do let(:statistics) { %w(repository_size) } context 'when a lease could not be obtained' do - it 'does not update the repository size' do + it 'does not update the project statistics' do stub_exclusive_lease_taken(lease_key, timeout: lease_timeout) + expect(Projects::UpdateStatisticsService).not_to receive(:new) + expect(UpdateProjectStatisticsWorker).not_to receive(:perform_in) - worker.update_statistics(project, statistics.map(&:to_sym)) + worker.update_statistics(project, statistics) end end context 'when a lease could be obtained' do - it 'updates the project statistics' do + it 'updates the project statistics twice' do stub_exclusive_lease(lease_key, timeout: lease_timeout) - expect(project.statistics).to receive(:refresh!) - .with(only: statistics.map(&:to_sym)) + expect(Projects::UpdateStatisticsService).to receive(:new) + .with(project, nil, statistics: statistics) .and_call_original + .twice expect(UpdateProjectStatisticsWorker).to receive(:perform_in) - .with(lease_timeout, project.id, statistics.map(&:to_sym)) + .with(lease_timeout, project.id, statistics) .and_call_original - worker.update_statistics(project, statistics.map(&:to_sym)) + worker.update_statistics(project, statistics) end end end diff --git a/spec/workers/update_project_statistics_worker_spec.rb b/spec/workers/update_project_statistics_worker_spec.rb index 3411e10da7e..a268fd2e4ba 100644 --- a/spec/workers/update_project_statistics_worker_spec.rb +++ b/spec/workers/update_project_statistics_worker_spec.rb @@ -3,46 +3,15 @@ require 'spec_helper' describe UpdateProjectStatisticsWorker do let(:worker) { described_class.new } let(:project) { create(:project, :repository) } + let(:statistics) { %w(repository_size) } describe '#perform' do - context 'with a non-existing project' do - it 'does nothing' do - expect_any_instance_of(ProjectStatistics).not_to receive(:refresh!) + it 'updates the project statistics' do + expect(Projects::UpdateStatisticsService).to receive(:new) + .with(project, nil, statistics: statistics) + .and_call_original - worker.perform(-1) - end - end - - context 'with an existing project without a repository' do - it 'does nothing' do - allow_any_instance_of(Repository).to receive(:exists?).and_return(false) - - expect_any_instance_of(ProjectStatistics).not_to receive(:refresh!) - - worker.perform(project.id) - end - end - - context 'with an existing project' do - it 'refreshes the project statistics' do - expect_any_instance_of(ProjectStatistics).to receive(:refresh!) - .with(only: []) - .and_call_original - - worker.perform(project.id) - end - - context 'with a specific statistics target' do - it 'refreshes the project repository size' do - statistics_target = %w(repository_size) - - expect_any_instance_of(ProjectStatistics).to receive(:refresh!) - .with(only: statistics_target.map(&:to_sym)) - .and_call_original - - worker.perform(project.id, statistics_target) - end - end + worker.perform(project.id, statistics) end end end -- cgit v1.2.1 From e46d4bf4da3ee207043c85524df238475e47d650 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Thu, 28 Mar 2019 14:59:24 +0000 Subject: Extract a Git::{Base,Tag,Branch}HooksService --- spec/services/after_branch_delete_service_spec.rb | 15 - spec/services/git/branch_hooks_service_spec.rb | 339 ++++++++++++++++++++++ spec/services/git/branch_push_service_spec.rb | 258 +++------------- spec/services/git/tag_hooks_service_spec.rb | 144 +++++++++ spec/services/git/tag_push_service_spec.rb | 179 +----------- spec/workers/post_receive_spec.rb | 25 +- 6 files changed, 560 insertions(+), 400 deletions(-) delete mode 100644 spec/services/after_branch_delete_service_spec.rb create mode 100644 spec/services/git/branch_hooks_service_spec.rb create mode 100644 spec/services/git/tag_hooks_service_spec.rb (limited to 'spec') diff --git a/spec/services/after_branch_delete_service_spec.rb b/spec/services/after_branch_delete_service_spec.rb deleted file mode 100644 index bc9747d1413..00000000000 --- a/spec/services/after_branch_delete_service_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'spec_helper' - -describe AfterBranchDeleteService do - let(:project) { create(:project, :repository) } - let(:user) { create(:user) } - let(:service) { described_class.new(project, user) } - - describe '#execute' do - it 'stops environments attached to branch' do - expect(service).to receive(:stop_environments) - - service.execute('feature') - end - end -end diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb new file mode 100644 index 00000000000..bb87267db7d --- /dev/null +++ b/spec/services/git/branch_hooks_service_spec.rb @@ -0,0 +1,339 @@ +require 'spec_helper' + +describe Git::BranchHooksService do + include RepoHelpers + + let(:project) { create(:project, :repository) } + let(:user) { project.creator } + + let(:branch) { project.default_branch } + let(:ref) { "refs/heads/#{branch}" } + let(:commit) { project.commit(sample_commit.id) } + let(:oldrev) { commit.parent_id } + let(:newrev) { commit.id } + + let(:service) do + described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) + end + + describe "Git Push Data" do + subject(:push_data) { service.execute } + + it 'has expected push data attributes' do + is_expected.to match a_hash_including( + object_kind: 'push', + before: oldrev, + after: newrev, + ref: ref, + user_id: user.id, + user_name: user.name, + project_id: project.id + ) + end + + context "with repository data" do + subject { push_data[:repository] } + + it 'has expected attributes' do + is_expected.to match a_hash_including( + name: project.name, + url: project.url_to_repo, + description: project.description, + homepage: project.web_url + ) + end + end + + context "with commits" do + subject { push_data[:commits] } + + it { is_expected.to be_an(Array) } + + it 'has 1 element' do + expect(subject.size).to eq(1) + end + + context "the commit" do + subject { push_data[:commits].first } + + it { expect(subject[:timestamp].in_time_zone).to eq(commit.date.in_time_zone) } + + it 'includes expected commit data' do + is_expected.to match a_hash_including( + id: commit.id, + message: commit.safe_message, + url: [ + Gitlab.config.gitlab.url, + project.namespace.to_param, + project.to_param, + 'commit', + commit.id + ].join('/') + ) + end + + context "with a author" do + subject { push_data[:commits].first[:author] } + + it 'includes expected author data' do + is_expected.to match a_hash_including( + name: commit.author_name, + email: commit.author_email + ) + end + end + end + end + end + + describe 'Push Event' do + let(:event) { Event.find_by_action(Event::PUSHED) } + + before do + service.execute + end + + context "with an existing branch" do + it 'generates a push event with one commit' do + expect(event).to be_an_instance_of(PushEvent) + expect(event.project).to eq(project) + expect(event.action).to eq(Event::PUSHED) + expect(event.push_event_payload).to be_an_instance_of(PushEventPayload) + expect(event.push_event_payload.commit_from).to eq(oldrev) + expect(event.push_event_payload.commit_to).to eq(newrev) + expect(event.push_event_payload.ref).to eq('master') + expect(event.push_event_payload.commit_count).to eq(1) + end + end + + context "with a new branch" do + let(:oldrev) { Gitlab::Git::BLANK_SHA } + + it 'generates a push event with more than one commit' do + expect(event).to be_an_instance_of(PushEvent) + expect(event.project).to eq(project) + expect(event.action).to eq(Event::PUSHED) + expect(event.push_event_payload).to be_an_instance_of(PushEventPayload) + expect(event.push_event_payload.commit_from).to be_nil + expect(event.push_event_payload.commit_to).to eq(newrev) + expect(event.push_event_payload.ref).to eq('master') + expect(event.push_event_payload.commit_count).to be > 1 + end + end + + context 'removing a branch' do + let(:newrev) { Gitlab::Git::BLANK_SHA } + + it 'generates a push event with no commits' do + expect(event).to be_an_instance_of(PushEvent) + expect(event.project).to eq(project) + expect(event.action).to eq(Event::PUSHED) + expect(event.push_event_payload).to be_an_instance_of(PushEventPayload) + expect(event.push_event_payload.commit_from).to eq(oldrev) + expect(event.push_event_payload.commit_to).to be_nil + expect(event.push_event_payload.ref).to eq('master') + expect(event.push_event_payload.commit_count).to eq(0) + end + end + end + + describe 'Invalidating project cache' do + let(:commit_id) do + project.repository.update_file( + user, 'README.md', '', message: 'Update', branch_name: branch + ) + end + + let(:commit) { project.repository.commit(commit_id) } + let(:blank_sha) { Gitlab::Git::BLANK_SHA } + + def clears_cache(extended: []) + expect(ProjectCacheWorker) + .to receive(:perform_async) + .with(project.id, extended, %i[commit_count repository_size]) + + service.execute + end + + def clears_extended_cache + clears_cache(extended: %i[readme]) + end + + context 'on default branch' do + context 'create' do + # FIXME: When creating the default branch,the cache worker runs twice + before do + allow(ProjectCacheWorker).to receive(:perform_async) + end + + let(:oldrev) { blank_sha } + + it { clears_cache } + end + + context 'update' do + it { clears_extended_cache } + end + + context 'remove' do + let(:newrev) { blank_sha } + + # TODO: this case should pass, but we only take account of added files + it { clears_cache } + end + end + + context 'on ordinary branch' do + let(:branch) { 'fix' } + + context 'create' do + let(:oldrev) { blank_sha } + + it { clears_cache } + end + + context 'update' do + it { clears_cache } + end + + context 'remove' do + let(:newrev) { blank_sha } + + it { clears_cache } + end + end + end + + describe 'GPG signatures' do + context 'when the commit has a signature' do + context 'when the signature is already cached' do + before do + create(:gpg_signature, commit_sha: commit.id) + end + + it 'does not queue a CreateGpgSignatureWorker' do + expect(CreateGpgSignatureWorker).not_to receive(:perform_async) + + service.execute + end + end + + context 'when the signature is not yet cached' do + it 'queues a CreateGpgSignatureWorker' do + expect(CreateGpgSignatureWorker).to receive(:perform_async).with([commit.id], project.id) + + service.execute + end + + it 'can queue several commits to create the gpg signature' do + allow(Gitlab::Git::Commit) + .to receive(:shas_with_signatures) + .and_return([sample_commit.id, another_sample_commit.id]) + + expect(CreateGpgSignatureWorker) + .to receive(:perform_async) + .with([sample_commit.id, another_sample_commit.id], project.id) + + service.execute + end + end + end + + context 'when the commit does not have a signature' do + before do + allow(Gitlab::Git::Commit) + .to receive(:shas_with_signatures) + .with(project.repository, [sample_commit.id]) + .and_return([]) + end + + it 'does not queue a CreateGpgSignatureWorker' do + expect(CreateGpgSignatureWorker) + .not_to receive(:perform_async) + .with(sample_commit.id, project.id) + + service.execute + end + end + end + + describe 'Processing commit messages' do + # Create 4 commits, 2 of which have references. Limiting to 2 commits, we + # expect to see one commit message processor enqueued. + let(:commit_ids) do + Array.new(4) do |i| + message = "Issue #{'#' if i.even?}#{i}" + project.repository.update_file( + user, 'README.md', '', message: message, branch_name: branch + ) + end + end + + let(:oldrev) { commit_ids.first } + let(:newrev) { commit_ids.last } + + before do + stub_const("::Git::BaseHooksService::PROCESS_COMMIT_LIMIT", 2) + end + + context 'creating the default branch' do + let(:oldrev) { Gitlab::Git::BLANK_SHA } + + it 'does not process commit messages' do + expect(ProcessCommitWorker).not_to receive(:perform_async) + + service.execute + end + end + + context 'updating the default branch' do + it 'processes a limited number of commit messages' do + expect(ProcessCommitWorker).to receive(:perform_async).once + + service.execute + end + end + + context 'removing the default branch' do + let(:newrev) { Gitlab::Git::BLANK_SHA } + + it 'does not process commit messages' do + expect(ProcessCommitWorker).not_to receive(:perform_async) + + service.execute + end + end + + context 'creating a normal branch' do + let(:branch) { 'fix' } + let(:oldrev) { Gitlab::Git::BLANK_SHA } + + it 'processes a limited number of commit messages' do + expect(ProcessCommitWorker).to receive(:perform_async).once + + service.execute + end + end + + context 'updating a normal branch' do + let(:branch) { 'fix' } + + it 'processes a limited number of commit messages' do + expect(ProcessCommitWorker).to receive(:perform_async).once + + service.execute + end + end + + context 'removing a normal branch' do + let(:branch) { 'fix' } + let(:newrev) { Gitlab::Git::BLANK_SHA } + + it 'does not process commit messages' do + expect(ProcessCommitWorker).not_to receive(:perform_async) + + service.execute + end + end + end +end diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb index d0e2169b4a6..322e40a8112 100644 --- a/spec/services/git/branch_push_service_spec.rb +++ b/spec/services/git/branch_push_service_spec.rb @@ -8,7 +8,8 @@ describe Git::BranchPushService, services: true do let(:blankrev) { Gitlab::Git::BLANK_SHA } let(:oldrev) { sample_commit.parent_id } let(:newrev) { sample_commit.id } - let(:ref) { 'refs/heads/master' } + let(:branch) { 'master' } + let(:ref) { "refs/heads/#{branch}" } before do project.add_maintainer(user) @@ -132,64 +133,6 @@ describe Git::BranchPushService, services: true do end end - describe "Git Push Data" do - let(:commit) { project.commit(newrev) } - - subject { push_data_from_service(project, user, oldrev, newrev, ref) } - - it { is_expected.to include(object_kind: 'push') } - it { is_expected.to include(before: oldrev) } - it { is_expected.to include(after: newrev) } - it { is_expected.to include(ref: ref) } - it { is_expected.to include(user_id: user.id) } - it { is_expected.to include(user_name: user.name) } - it { is_expected.to include(project_id: project.id) } - - context "with repository data" do - subject { push_data_from_service(project, user, oldrev, newrev, ref)[:repository] } - - it { is_expected.to include(name: project.name) } - it { is_expected.to include(url: project.url_to_repo) } - it { is_expected.to include(description: project.description) } - it { is_expected.to include(homepage: project.web_url) } - end - - context "with commits" do - subject { push_data_from_service(project, user, oldrev, newrev, ref)[:commits] } - - it { is_expected.to be_an(Array) } - it 'has 1 element' do - expect(subject.size).to eq(1) - end - - context "the commit" do - subject { push_data_from_service(project, user, oldrev, newrev, ref)[:commits].first } - - it { is_expected.to include(id: commit.id) } - it { is_expected.to include(message: commit.safe_message) } - it { expect(subject[:timestamp].in_time_zone).to eq(commit.date.in_time_zone) } - it do - is_expected.to include( - url: [ - Gitlab.config.gitlab.url, - project.namespace.to_param, - project.to_param, - 'commit', - commit.id - ].join('/') - ) - end - - context "with a author" do - subject { push_data_from_service(project, user, oldrev, newrev, ref)[:commits].first[:author] } - - it { is_expected.to include(name: commit.author_name) } - it { is_expected.to include(email: commit.author_email) } - end - end - end - end - describe "Pipelines" do subject { execute_service(project, user, oldrev, newrev, ref) } @@ -203,59 +146,13 @@ describe Git::BranchPushService, services: true do end end - describe "Push Event" do - context "with an existing branch" do - let!(:push_data) { push_data_from_service(project, user, oldrev, newrev, ref) } - let(:event) { Event.find_by_action(Event::PUSHED) } - - it 'generates a push event with one commit' do - expect(event).to be_an_instance_of(PushEvent) - expect(event.project).to eq(project) - expect(event.action).to eq(Event::PUSHED) - expect(event.push_event_payload).to be_an_instance_of(PushEventPayload) - expect(event.push_event_payload.commit_from).to eq(oldrev) - expect(event.push_event_payload.commit_to).to eq(newrev) - expect(event.push_event_payload.ref).to eq('master') - expect(event.push_event_payload.commit_count).to eq(1) - end - end - - context "with a new branch" do - let!(:new_branch_data) { push_data_from_service(project, user, Gitlab::Git::BLANK_SHA, newrev, ref) } - let(:event) { Event.find_by_action(Event::PUSHED) } - - it 'generates a push event with more than one commit' do - expect(event).to be_an_instance_of(PushEvent) - expect(event.project).to eq(project) - expect(event.action).to eq(Event::PUSHED) - expect(event.push_event_payload).to be_an_instance_of(PushEventPayload) - expect(event.push_event_payload.commit_from).to be_nil - expect(event.push_event_payload.commit_to).to eq(newrev) - expect(event.push_event_payload.ref).to eq('master') - expect(event.push_event_payload.commit_count).to be > 1 - end - end - - context "Updates merge requests" do - it "when pushing a new branch for the first time" do - expect(UpdateMergeRequestsWorker).to receive(:perform_async) - .with(project.id, user.id, blankrev, 'newrev', ref) - execute_service(project, user, blankrev, 'newrev', ref ) - end - end - - describe 'system hooks' do - let!(:push_data) { push_data_from_service(project, user, oldrev, newrev, ref) } - let!(:system_hooks_service) { SystemHooksService.new } + describe "Updates merge requests" do + it "when pushing a new branch for the first time" do + expect(UpdateMergeRequestsWorker) + .to receive(:perform_async) + .with(project.id, user.id, blankrev, 'newrev', ref) - it "sends a system hook after pushing a branch" do - allow(SystemHooksService).to receive(:new).and_return(system_hooks_service) - allow(system_hooks_service).to receive(:execute_hooks) - - execute_service(project, user, oldrev, newrev, ref) - - expect(system_hooks_service).to have_received(:execute_hooks).with(push_data, :push_hooks) - end + execute_service(project, user, blankrev, 'newrev', ref ) end end @@ -700,125 +597,64 @@ describe Git::BranchPushService, services: true do end end - describe '#update_caches' do - let(:service) do - described_class.new(project, - user, - oldrev: oldrev, - newrev: newrev, - ref: ref) - end - - context 'on the default branch' do - before do - allow(service).to receive(:default_branch?).and_return(true) - end - - it 'flushes the caches of any special files that have been changed' do - commit = double(:commit) - diff = double(:diff, new_path: 'README.md') - - expect(commit).to receive(:raw_deltas) - .and_return([diff]) - - service.push_commits = [commit] + describe "CI environments" do + context 'create branch' do + let(:oldrev) { blankrev } - expect(ProjectCacheWorker).to receive(:perform_async) - .with(project.id, %i(readme), %i(commit_count repository_size)) + it 'does nothing' do + expect(::Ci::StopEnvironmentsService).not_to receive(:new) - service.update_caches + execute_service(project, user, oldrev, newrev, ref) end end - context 'on a non-default branch' do - before do - allow(service).to receive(:default_branch?).and_return(false) - end - - it 'does not flush any conditional caches' do - expect(ProjectCacheWorker).to receive(:perform_async) - .with(project.id, [], %i(commit_count repository_size)) - .and_call_original + context 'update branch' do + it 'does nothing' do + expect(::Ci::StopEnvironmentsService).not_to receive(:new) - service.update_caches + execute_service(project, user, oldrev, newrev, ref) end end - end - - describe '#process_commit_messages' do - let(:service) do - described_class.new(project, - user, - oldrev: oldrev, - newrev: newrev, - ref: ref) - end - it 'only schedules a limited number of commits' do - service.push_commits = Array.new(1000, double(:commit, to_hash: {}, matches_cross_reference_regex?: true)) - - expect(ProcessCommitWorker).to receive(:perform_async).exactly(100).times - - service.process_commit_messages - end - - it "skips commits which don't include cross-references" do - service.push_commits = [double(:commit, to_hash: {}, matches_cross_reference_regex?: false)] - - expect(ProcessCommitWorker).not_to receive(:perform_async) - - service.process_commit_messages - end - end - - describe '#update_signatures' do - let(:service) do - described_class.new( - project, - user, - oldrev: oldrev, - newrev: newrev, - ref: 'refs/heads/master' - ) - end + context 'delete branch' do + let(:newrev) { blankrev } - context 'when the commit has a signature' do - context 'when the signature is already cached' do - before do - create(:gpg_signature, commit_sha: sample_commit.id) + it 'stops environments' do + expect_next_instance_of(::Ci::StopEnvironmentsService) do |stop_service| + expect(stop_service.project).to eq(project) + expect(stop_service.current_user).to eq(user) + expect(stop_service).to receive(:execute).with(branch) end - it 'does not queue a CreateGpgSignatureWorker' do - expect(CreateGpgSignatureWorker).not_to receive(:perform_async) - - execute_service(project, user, oldrev, newrev, ref) - end + execute_service(project, user, oldrev, newrev, ref) end + end + end - context 'when the signature is not yet cached' do - it 'queues a CreateGpgSignatureWorker' do - expect(CreateGpgSignatureWorker).to receive(:perform_async).with([sample_commit.id], project.id) + describe 'Hooks' do + context 'run on a branch' do + it 'delegates to Git::BranchHooksService' do + expect_next_instance_of(::Git::BranchHooksService) do |hooks_service| + expect(hooks_service.project).to eq(project) + expect(hooks_service.current_user).to eq(user) + expect(hooks_service.params).to include( + oldrev: oldrev, + newrev: newrev, + ref: ref + ) - execute_service(project, user, oldrev, newrev, ref) + expect(hooks_service).to receive(:execute) end - it 'can queue several commits to create the gpg signature' do - allow(Gitlab::Git::Commit).to receive(:shas_with_signatures).and_return([sample_commit.id, another_sample_commit.id]) - - expect(CreateGpgSignatureWorker).to receive(:perform_async).with([sample_commit.id, another_sample_commit.id], project.id) - - execute_service(project, user, oldrev, newrev, ref) - end + execute_service(project, user, oldrev, newrev, ref) end end - context 'when the commit does not have a signature' do - before do - allow(Gitlab::Git::Commit).to receive(:shas_with_signatures).with(project.repository, [sample_commit.id]).and_return([]) - end + context 'run on a tag' do + let(:ref) { 'refs/tags/v1.1.0' } - it 'does not queue a CreateGpgSignatureWorker' do - expect(CreateGpgSignatureWorker).not_to receive(:perform_async).with(sample_commit.id, project.id) + it 'does nothing' do + expect(::Git::BranchHooksService).not_to receive(:new) execute_service(project, user, oldrev, newrev, ref) end @@ -830,8 +666,4 @@ describe Git::BranchPushService, services: true do service.execute service end - - def push_data_from_service(project, user, oldrev, newrev, ref) - execute_service(project, user, oldrev, newrev, ref).push_data - end end diff --git a/spec/services/git/tag_hooks_service_spec.rb b/spec/services/git/tag_hooks_service_spec.rb new file mode 100644 index 00000000000..8f91ce3b4c5 --- /dev/null +++ b/spec/services/git/tag_hooks_service_spec.rb @@ -0,0 +1,144 @@ +require 'spec_helper' + +describe Git::TagHooksService, :service do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + + let(:oldrev) { Gitlab::Git::BLANK_SHA } + let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0 + let(:ref) { "refs/tags/#{tag_name}" } + let(:tag_name) { 'v1.1.0' } + + let(:tag) { project.repository.find_tag(tag_name) } + let(:commit) { tag.dereferenced_target } + + let(:service) do + described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) + end + + describe 'System hooks' do + it 'Executes system hooks' do + push_data = service.execute + + expect_next_instance_of(SystemHooksService) do |system_hooks_service| + expect(system_hooks_service) + .to receive(:execute_hooks) + .with(push_data, :tag_push_hooks) + end + + service.execute + end + end + + describe "Webhooks" do + it "executes hooks on the project" do + expect(project).to receive(:execute_hooks) + + service.execute + end + end + + describe "Pipelines" do + before do + stub_ci_pipeline_to_return_yaml_file + project.add_developer(user) + end + + it "creates a new pipeline" do + expect { service.execute }.to change { Ci::Pipeline.count } + + expect(Ci::Pipeline.last).to be_push + end + end + + describe 'Push data' do + shared_examples_for 'tag push data expectations' do + subject(:push_data) { service.execute } + it 'has expected push data attributes' do + is_expected.to match a_hash_including( + object_kind: 'tag_push', + ref: ref, + before: oldrev, + after: newrev, + message: tag.message, + user_id: user.id, + user_name: user.name, + project_id: project.id + ) + end + + context "with repository data" do + subject { push_data[:repository] } + + it 'has expected repository attributes' do + is_expected.to match a_hash_including( + name: project.name, + url: project.url_to_repo, + description: project.description, + homepage: project.web_url + ) + end + end + + context "with commits" do + subject { push_data[:commits] } + + it { is_expected.to be_an(Array) } + + it 'has 1 element' do + expect(subject.size).to eq(1) + end + + context "the commit" do + subject { push_data[:commits].first } + + it { is_expected.to include(timestamp: commit.date.xmlschema) } + + it 'has expected commit attributes' do + is_expected.to match a_hash_including( + id: commit.id, + message: commit.safe_message, + url: [ + Gitlab.config.gitlab.url, + project.namespace.to_param, + project.to_param, + 'commit', + commit.id + ].join('/') + ) + end + + context "with an author" do + subject { push_data[:commits].first[:author] } + + it 'has expected author attributes' do + is_expected.to match a_hash_including( + name: commit.author_name, + email: commit.author_email + ) + end + end + end + end + end + + context 'annotated tag' do + include_examples 'tag push data expectations' + end + + context 'lightweight tag' do + let(:tag_name) { 'light-tag' } + let(:newrev) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' } + + before do + # Create the lightweight tag + rugged_repo(project.repository).tags.create(tag_name, newrev) + + # Clear tag list cache + project.repository.expire_tags_cache + end + + include_examples 'tag push data expectations' + end + end +end diff --git a/spec/services/git/tag_push_service_spec.rb b/spec/services/git/tag_push_service_spec.rb index 2d960fc9f08..5e89a912060 100644 --- a/spec/services/git/tag_push_service_spec.rb +++ b/spec/services/git/tag_push_service_spec.rb @@ -31,178 +31,27 @@ describe Git::TagPushService do end end - describe 'System Hooks' do - let!(:push_data) { service.tap(&:execute).push_data } - - it "executes system hooks after pushing a tag" do - expect_next_instance_of(SystemHooksService) do |system_hooks_service| - expect(system_hooks_service) - .to receive(:execute_hooks) - .with(push_data, :tag_push_hooks) - end - - service.execute - end - end - - describe "Pipelines" do - subject { service.execute } - - before do - stub_ci_pipeline_to_return_yaml_file - project.add_developer(user) - end - - it "creates a new pipeline" do - expect { subject }.to change { Ci::Pipeline.count } - expect(Ci::Pipeline.last).to be_push - end - end - - describe "Git Tag Push Data" do - subject { @push_data } - let(:tag) { project.repository.find_tag(tag_name) } - let(:commit) { tag.dereferenced_target } - - context 'annotated tag' do - let(:tag_name) { Gitlab::Git.ref_name(ref) } - - before do - service.execute - @push_data = service.push_data - end - - it { is_expected.to include(object_kind: 'tag_push') } - it { is_expected.to include(ref: ref) } - it { is_expected.to include(before: oldrev) } - it { is_expected.to include(after: newrev) } - it { is_expected.to include(message: tag.message) } - it { is_expected.to include(user_id: user.id) } - it { is_expected.to include(user_name: user.name) } - it { is_expected.to include(project_id: project.id) } - - context "with repository data" do - subject { @push_data[:repository] } - - it { is_expected.to include(name: project.name) } - it { is_expected.to include(url: project.url_to_repo) } - it { is_expected.to include(description: project.description) } - it { is_expected.to include(homepage: project.web_url) } - end - - context "with commits" do - subject { @push_data[:commits] } - - it { is_expected.to be_an(Array) } - it 'has 1 element' do - expect(subject.size).to eq(1) - end - - context "the commit" do - subject { @push_data[:commits].first } - - it { is_expected.to include(id: commit.id) } - it { is_expected.to include(message: commit.safe_message) } - it { is_expected.to include(timestamp: commit.date.xmlschema) } - it do - is_expected.to include( - url: [ - Gitlab.config.gitlab.url, - project.namespace.to_param, - project.to_param, - 'commit', - commit.id - ].join('/') - ) - end - - context "with a author" do - subject { @push_data[:commits].first[:author] } - - it { is_expected.to include(name: commit.author_name) } - it { is_expected.to include(email: commit.author_email) } - end + describe 'Hooks' do + context 'run on a tag' do + it 'delegates to Git::TagHooksService' do + expect_next_instance_of(::Git::TagHooksService) do |hooks_service| + expect(hooks_service.project).to eq(service.project) + expect(hooks_service.current_user).to eq(service.current_user) + expect(hooks_service.params).to eq(service.params) + + expect(hooks_service).to receive(:execute) end - end - end - - context 'lightweight tag' do - let(:tag_name) { 'light-tag' } - let(:newrev) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' } - let(:ref) { "refs/tags/light-tag" } - - before do - # Create the lightweight tag - rugged_repo(project.repository).tags.create(tag_name, newrev) - - # Clear tag list cache - project.repository.expire_tags_cache service.execute - @push_data = service.push_data - end - - it { is_expected.to include(object_kind: 'tag_push') } - it { is_expected.to include(ref: ref) } - it { is_expected.to include(before: oldrev) } - it { is_expected.to include(after: newrev) } - it { is_expected.to include(message: tag.message) } - it { is_expected.to include(user_id: user.id) } - it { is_expected.to include(user_name: user.name) } - it { is_expected.to include(project_id: project.id) } - - context "with repository data" do - subject { @push_data[:repository] } - - it { is_expected.to include(name: project.name) } - it { is_expected.to include(url: project.url_to_repo) } - it { is_expected.to include(description: project.description) } - it { is_expected.to include(homepage: project.web_url) } - end - - context "with commits" do - subject { @push_data[:commits] } - - it { is_expected.to be_an(Array) } - it 'has 1 element' do - expect(subject.size).to eq(1) - end - - context "the commit" do - subject { @push_data[:commits].first } - - it { is_expected.to include(id: commit.id) } - it { is_expected.to include(message: commit.safe_message) } - it { is_expected.to include(timestamp: commit.date.xmlschema) } - it do - is_expected.to include( - url: [ - Gitlab.config.gitlab.url, - project.namespace.to_param, - project.to_param, - 'commit', - commit.id - ].join('/') - ) - end - - context "with a author" do - subject { @push_data[:commits].first[:author] } - - it { is_expected.to include(name: commit.author_name) } - it { is_expected.to include(email: commit.author_email) } - end - end end end - end - describe "Webhooks" do - context "execute webhooks" do - let(:service) { described_class.new(project, user, oldrev: 'oldrev', newrev: 'newrev', ref: 'refs/tags/v1.0.0') } + context 'run on a branch' do + let(:ref) { 'refs/heads/master' } + + it 'does nothing' do + expect(::Git::BranchHooksService).not_to receive(:new) - it "when pushing tags" do - expect(project).to receive(:execute_hooks) service.execute end end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 66958a4c116..a3fe8fa4501 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -63,8 +63,12 @@ describe PostReceive do let(:changes) { "123456 789012 refs/heads/tést" } it "calls Git::BranchPushService" do - expect_any_instance_of(Git::BranchPushService).to receive(:execute).and_return(true) - expect_any_instance_of(Git::TagPushService).not_to receive(:execute) + expect_next_instance_of(Git::BranchPushService) do |service| + expect(service).to receive(:execute).and_return(true) + end + + expect(Git::TagPushService).not_to receive(:new) + described_class.new.perform(gl_repository, key_id, base64_changes) end end @@ -73,8 +77,12 @@ describe PostReceive do let(:changes) { "123456 789012 refs/tags/tag" } it "calls Git::TagPushService" do - expect_any_instance_of(Git::BranchPushService).not_to receive(:execute) - expect_any_instance_of(Git::TagPushService).to receive(:execute).and_return(true) + expect(Git::BranchPushService).not_to receive(:execute) + + expect_next_instance_of(Git::TagPushService) do |service| + expect(service).to receive(:execute).and_return(true) + end + described_class.new.perform(gl_repository, key_id, base64_changes) end end @@ -83,8 +91,9 @@ describe PostReceive do let(:changes) { "123456 789012 refs/merge-requests/123" } it "does not call any of the services" do - expect_any_instance_of(Git::BranchPushService).not_to receive(:execute) - expect_any_instance_of(Git::TagPushService).not_to receive(:execute) + expect(Git::BranchPushService).not_to receive(:new) + expect(Git::TagPushService).not_to receive(:new) + described_class.new.perform(gl_repository, key_id, base64_changes) end end @@ -127,7 +136,9 @@ describe PostReceive do allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data) # silence hooks so we can isolate allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true) - allow_any_instance_of(Git::BranchPushService).to receive(:execute).and_return(true) + expect_next_instance_of(Git::BranchPushService) do |service| + expect(service).to receive(:execute).and_return(true) + end end it 'calls SystemHooksService' do -- cgit v1.2.1 From e82ed3f46bcc5442668e64b7080f7bb4425956c0 Mon Sep 17 00:00:00 2001 From: charlieablett Date: Mon, 8 Apr 2019 16:52:40 +1200 Subject: Abstract out method from spec to support --- spec/features/boards/sidebar_spec.rb | 1 + spec/support/helpers/filtered_search_helpers.rb | 6 ++++++ 2 files changed, 7 insertions(+) (limited to 'spec') diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index dfdb8d589eb..d036699f226 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -129,6 +129,7 @@ describe 'Issue Boards', :js do click_link 'Unassigned' end + close_dropdown_menu_if_visible wait_for_requests expect(page).to have_content('No assignee') diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb index 6569feec39b..03057a102c5 100644 --- a/spec/support/helpers/filtered_search_helpers.rb +++ b/spec/support/helpers/filtered_search_helpers.rb @@ -149,4 +149,10 @@ module FilteredSearchHelpers loop until find('.filtered-search').value.strip == text end end + + def close_dropdown_menu_if_visible + find('.dropdown-menu-toggle', visible: :all).tap do |toggle| + toggle.click if toggle.visible? + end + end end -- cgit v1.2.1 From e0efa97c7f24bc5816ecdcb6a633150e7a6a43f6 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Wed, 3 Apr 2019 18:33:52 +0700 Subject: Prevent triggering pipelines when target branch is updated Currently, pipelines for merge requests are triggered when source or target branch is updated. However, we should create only when source branch is updated, because it runs unexpected pipelines. --- .../merge_requests/refresh_service_spec.rb | 34 +++++++++++++++++----- 1 file changed, 27 insertions(+), 7 deletions(-) (limited to 'spec') diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index bd10523bc94..5ed06df7072 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -146,7 +146,10 @@ describe MergeRequests::RefreshService do stub_ci_pipeline_yaml_file(YAML.dump(config)) end - subject { service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master') } + subject { service.new(project, @user).execute(@oldrev, @newrev, ref) } + + let(:ref) { 'refs/heads/master' } + let(:project) { @project } context "when .gitlab-ci.yml has merge_requests keywords" do let(:config) do @@ -162,14 +165,17 @@ describe MergeRequests::RefreshService do it 'create detached merge request pipeline with commits' do expect { subject } .to change { @merge_request.merge_request_pipelines.count }.by(1) - .and change { @fork_merge_request.merge_request_pipelines.count }.by(1) .and change { @another_merge_request.merge_request_pipelines.count }.by(0) expect(@merge_request.has_commits?).to be_truthy - expect(@fork_merge_request.has_commits?).to be_truthy expect(@another_merge_request.has_commits?).to be_falsy end + it 'does not create detached merge request pipeline for forked project' do + expect { subject } + .not_to change { @fork_merge_request.merge_request_pipelines.count } + end + it 'create detached merge request pipeline for non-fork merge request' do subject @@ -177,11 +183,25 @@ describe MergeRequests::RefreshService do .to be_detached_merge_request_pipeline end - it 'create legacy detached merge request pipeline for fork merge request' do - subject + context 'when service is hooked by target branch' do + let(:ref) { 'refs/heads/feature' } - expect(@fork_merge_request.merge_request_pipelines.first) - .to be_legacy_detached_merge_request_pipeline + it 'does not create detached merge request pipeline' do + expect { subject } + .not_to change { @merge_request.merge_request_pipelines.count } + end + end + + context 'when service runs on forked project' do + let(:project) { @fork_project } + + it 'creates legacy detached merge request pipeline for fork merge request' do + expect { subject } + .to change { @fork_merge_request.merge_request_pipelines.count }.by(1) + + expect(@fork_merge_request.merge_request_pipelines.first) + .to be_legacy_detached_merge_request_pipeline + end end context 'when ci_use_merge_request_ref feature flag is false' do -- cgit v1.2.1 From 4e95aa3de59220d32211f608f66400a1f067d77a Mon Sep 17 00:00:00 2001 From: charlieablett Date: Mon, 8 Apr 2019 19:38:34 +1200 Subject: Add helper reference --- spec/features/boards/sidebar_spec.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'spec') diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index d036699f226..b358c6b9c34 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -2,6 +2,7 @@ require 'rails_helper' describe 'Issue Boards', :js do include BoardHelpers + include FilteredSearchHelpers let(:user) { create(:user) } let(:user2) { create(:user) } -- cgit v1.2.1 From 8fdfd521b27e6332ee8bd55d5beecea2cbae9ce0 Mon Sep 17 00:00:00 2001 From: Alexandru Croitor Date: Wed, 3 Apr 2019 14:23:58 +0300 Subject: Extract due quick action to shared example --- .../issues/user_uses_quick_actions_spec.rb | 29 +--------------- .../issue/due_quick_action_shared_examples.rb | 40 ++++++++++++++-------- 2 files changed, 26 insertions(+), 43 deletions(-) (limited to 'spec') diff --git a/spec/features/issues/user_uses_quick_actions_spec.rb b/spec/features/issues/user_uses_quick_actions_spec.rb index ea474759547..68af8303c2f 100644 --- a/spec/features/issues/user_uses_quick_actions_spec.rb +++ b/spec/features/issues/user_uses_quick_actions_spec.rb @@ -60,34 +60,7 @@ describe 'Issues > User uses quick actions', :js do it_behaves_like 'remove_due_date quick action' it_behaves_like 'duplicate quick action' it_behaves_like 'create_merge_request quick action' - - describe 'adding a due date from note' do - let(:issue) { create(:issue, project: project) } - - it_behaves_like 'due quick action available and date can be added' - - context 'when the current user cannot update the due date' do - let(:guest) { create(:user) } - before do - project.add_guest(guest) - gitlab_sign_out - sign_in(guest) - visit project_issue_path(project, issue) - end - - it_behaves_like 'due quick action not available' - end - end - - describe 'toggling the WIP prefix from the title from note' do - let(:issue) { create(:issue, project: project) } - - it 'does not recognize the command nor create a note' do - add_note("/wip") - - expect(page).not_to have_content '/wip' - end - end + it_behaves_like 'due quick action' describe 'move the issue to another project' do let(:issue) { create(:issue, project: project) } diff --git a/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb index db3ecccc339..ae78cd86cd5 100644 --- a/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb @@ -1,25 +1,35 @@ # frozen_string_literal: true -shared_examples 'due quick action not available' do - it 'does not set the due date' do - add_note('/due 2016-08-28') +shared_examples 'due quick action' do + context 'due quick action available and date can be added' do + it 'sets the due date accordingly' do + add_note('/due 2016-08-28') - expect(page).not_to have_content 'Commands applied' - expect(page).not_to have_content '/due 2016-08-28' - end -end + expect(page).not_to have_content '/due 2016-08-28' + expect(page).to have_content 'Commands applied' + + visit project_issue_path(project, issue) -shared_examples 'due quick action available and date can be added' do - it 'sets the due date accordingly' do - add_note('/due 2016-08-28') + page.within '.due_date' do + expect(page).to have_content 'Aug 28, 2016' + end + end + end - expect(page).not_to have_content '/due 2016-08-28' - expect(page).to have_content 'Commands applied' + context 'due quick action not available' do + let(:guest) { create(:user) } + before do + project.add_guest(guest) + gitlab_sign_out + sign_in(guest) + visit project_issue_path(project, issue) + end - visit project_issue_path(project, issue) + it 'does not set the due date' do + add_note('/due 2016-08-28') - page.within '.due_date' do - expect(page).to have_content 'Aug 28, 2016' + expect(page).not_to have_content 'Commands applied' + expect(page).not_to have_content '/due 2016-08-28' end end end -- cgit v1.2.1 From f3ad51f8a57df96bcc69b0821355ef29c3df2ac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Mon, 8 Apr 2019 14:41:05 +0200 Subject: Improve performance of PR import This removes unneeded `.reload` call which makes AR to load ALL objects, and create its in-memory representation. --- .../gitlab/import/merge_request_helpers_spec.rb | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 spec/lib/gitlab/import/merge_request_helpers_spec.rb (limited to 'spec') diff --git a/spec/lib/gitlab/import/merge_request_helpers_spec.rb b/spec/lib/gitlab/import/merge_request_helpers_spec.rb new file mode 100644 index 00000000000..cc0f2baf905 --- /dev/null +++ b/spec/lib/gitlab/import/merge_request_helpers_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Import::MergeRequestHelpers, type: :helper do + set(:project) { create(:project, :repository) } + set(:user) { create(:user) } + + describe '.create_merge_request_without_hooks' do + let(:iid) { 42 } + + let(:attributes) do + { + iid: iid, + title: 'My Pull Request', + description: 'This is my pull request', + source_project_id: project.id, + target_project_id: project.id, + source_branch: 'master-42', + target_branch: 'master', + state: :merged, + author_id: user.id, + assignee_id: user.id + } + end + + subject { helper.create_merge_request_without_hooks(project, attributes, iid) } + + context 'when merge request does not exist' do + it 'returns a new object' do + expect(subject.first).not_to be_nil + expect(subject.second).to eq(false) + end + + it 'does load all existing objects' do + 5.times do |iid| + MergeRequest.create!( + attributes.merge(iid: iid, source_branch: iid.to_s)) + end + + # does ensure that we only load object twice + # 1. by #insert_and_return_id + # 2. by project.merge_requests.find + expect_any_instance_of(MergeRequest).to receive(:attributes) + .twice.times.and_call_original + + expect(subject.first).not_to be_nil + expect(subject.second).to eq(false) + end + end + + context 'when merge request does exist' do + before do + MergeRequest.create!(attributes) + end + + it 'returns an existing object' do + expect(subject.first).not_to be_nil + expect(subject.second).to eq(true) + end + end + + context 'when project is deleted' do + before do + project.delete + end + + it 'returns an existing object' do + expect(subject.first).to be_nil + end + end + end +end -- cgit v1.2.1 From 4317a2a3a2e39e4c2594b0b28abf7a8cc694eeab Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 8 Apr 2019 15:33:30 +0000 Subject: Fix `updated_at` doesn't apply to `state_event` updates of issues via API --- spec/services/note_summary_spec.rb | 10 ++++++--- spec/services/system_note_service_spec.rb | 34 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) (limited to 'spec') diff --git a/spec/services/note_summary_spec.rb b/spec/services/note_summary_spec.rb index a6cc2251e48..f6ee15f750c 100644 --- a/spec/services/note_summary_spec.rb +++ b/spec/services/note_summary_spec.rb @@ -21,16 +21,20 @@ describe NoteSummary do describe '#note' do it 'returns note hash' do - expect(create_note_summary.note).to eq(noteable: noteable, project: project, author: user, note: 'note') + Timecop.freeze do + expect(create_note_summary.note).to eq(noteable: noteable, project: project, author: user, note: 'note', + created_at: Time.now) + end end context 'when noteable is a commit' do - let(:noteable) { build(:commit) } + let(:noteable) { build(:commit, system_note_timestamp: Time.at(43)) } it 'returns note hash specific to commit' do expect(create_note_summary.note).to eq( noteable: nil, project: project, author: user, note: 'note', - noteable_type: 'Commit', commit_id: noteable.id + noteable_type: 'Commit', commit_id: noteable.id, + created_at: Time.at(43) ) end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index b917de14b2e..8d446d1c9d5 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -11,6 +11,14 @@ describe SystemNoteService do let(:noteable) { create(:issue, project: project) } let(:issue) { noteable } + shared_examples_for 'a note with overridable created_at' do + let(:noteable) { create(:issue, project: project, system_note_timestamp: Time.at(42)) } + + it 'the note has the correct time' do + expect(subject.created_at).to eq Time.at(42) + end + end + shared_examples_for 'a system note' do let(:expected_noteable) { noteable } let(:commit_count) { nil } @@ -137,6 +145,8 @@ describe SystemNoteService do end context 'when assignee added' do + it_behaves_like 'a note with overridable created_at' + it 'sets the note text' do expect(subject.note).to eq "assigned to @#{assignee.username}" end @@ -145,6 +155,8 @@ describe SystemNoteService do context 'when assignee removed' do let(:assignee) { nil } + it_behaves_like 'a note with overridable created_at' + it 'sets the note text' do expect(subject.note).to eq 'removed assignee' end @@ -168,6 +180,8 @@ describe SystemNoteService do described_class.change_issue_assignees(issue, project, author, old_assignees).note end + it_behaves_like 'a note with overridable created_at' + it 'builds a correct phrase when an assignee is added to a non-assigned issue' do expect(build_note([], [assignee1])).to eq "assigned to @#{assignee1.username}" end @@ -213,6 +227,8 @@ describe SystemNoteService do expect(subject.note).to eq "changed milestone to #{reference}" end + + it_behaves_like 'a note with overridable created_at' end context 'when milestone removed' do @@ -221,6 +237,8 @@ describe SystemNoteService do it 'sets the note text' do expect(subject.note).to eq 'removed milestone' end + + it_behaves_like 'a note with overridable created_at' end end @@ -237,6 +255,8 @@ describe SystemNoteService do it 'sets the note text to use the milestone name' do expect(subject.note).to eq "changed milestone to #{milestone.to_reference(format: :name)}" end + + it_behaves_like 'a note with overridable created_at' end context 'when milestone removed' do @@ -245,6 +265,8 @@ describe SystemNoteService do it 'sets the note text' do expect(subject.note).to eq 'removed milestone' end + + it_behaves_like 'a note with overridable created_at' end end end @@ -254,6 +276,8 @@ describe SystemNoteService do let(:due_date) { Date.today } + it_behaves_like 'a note with overridable created_at' + it_behaves_like 'a system note' do let(:action) { 'due_date' } end @@ -280,6 +304,8 @@ describe SystemNoteService do let(:status) { 'reopened' } let(:source) { nil } + it_behaves_like 'a note with overridable created_at' + it_behaves_like 'a system note' do let(:action) { 'opened' } end @@ -289,6 +315,8 @@ describe SystemNoteService do let(:status) { 'opened' } let(:source) { double('commit', gfm_reference: 'commit 123456') } + it_behaves_like 'a note with overridable created_at' + it 'sets the note text' do expect(subject.note).to eq "#{status} via commit 123456" end @@ -338,6 +366,8 @@ describe SystemNoteService do let(:action) { 'title' } end + it_behaves_like 'a note with overridable created_at' + it 'sets the note text' do expect(subject.note) .to eq "changed title from **{-Old title-}** to **{+Lorem ipsum+}**" @@ -353,6 +383,8 @@ describe SystemNoteService do let(:action) { 'description' } end + it_behaves_like 'a note with overridable created_at' + it 'sets the note text' do expect(subject.note).to eq('changed the description') end @@ -478,6 +510,8 @@ describe SystemNoteService do let(:action) { 'cross_reference' } end + it_behaves_like 'a note with overridable created_at' + describe 'note_body' do context 'cross-project' do let(:project2) { create(:project, :repository) } -- cgit v1.2.1 From 26fdcf7b6103aa47943271a5f6358d9779d5a9b3 Mon Sep 17 00:00:00 2001 From: Daniel Wyatt Date: Mon, 8 Apr 2019 10:12:39 -0400 Subject: Fix GitHub project import visibility --- spec/lib/gitlab/legacy_github_import/project_creator_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'spec') diff --git a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb index 3d4240fa4ba..8c56622e0ba 100644 --- a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb @@ -47,12 +47,12 @@ describe Gitlab::LegacyGithubImport::ProjectCreator do end context 'when GitHub project is public' do - it 'sets project visibility to public' do + it 'sets project visibility to namespace visibility level' do repo.private = false project = service.execute - expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + expect(project.visibility_level).to eq(namespace.visibility_level) end end -- cgit v1.2.1 From e86a2e7eb27468de8357f27752a9f86ee926807e Mon Sep 17 00:00:00 2001 From: Brett Walker Date: Fri, 5 Apr 2019 12:30:10 -0500 Subject: Increase GraphQL complexity An IntrospectionQuery required more complexity points. --- spec/fixtures/api/graphql/introspection.graphql | 92 +++++++++++++++++++++++++ spec/requests/api/graphql/gitlab_schema_spec.rb | 16 ++++- 2 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 spec/fixtures/api/graphql/introspection.graphql (limited to 'spec') diff --git a/spec/fixtures/api/graphql/introspection.graphql b/spec/fixtures/api/graphql/introspection.graphql new file mode 100644 index 00000000000..7b712068fcd --- /dev/null +++ b/spec/fixtures/api/graphql/introspection.graphql @@ -0,0 +1,92 @@ +# pulled from GraphiQL query +query IntrospectionQuery { + __schema { + queryType { name } + mutationType { name } + subscriptionType { name } + types { + ...FullType + } + directives { + name + description + locations + args { + ...InputValue + } + } + } +} + +fragment FullType on __Type { + kind + name + description + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } +} + +fragment InputValue on __InputValue { + name + description + type { ...TypeRef } + defaultValue +} + +fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } +} diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb index 708a000532b..f95f460fd14 100644 --- a/spec/requests/api/graphql/gitlab_schema_spec.rb +++ b/spec/requests/api/graphql/gitlab_schema_spec.rb @@ -3,14 +3,24 @@ require 'spec_helper' describe 'GitlabSchema configurations' do include GraphqlHelpers - let(:project) { create(:project, :repository) } - let!(:query) { graphql_query_for('project', 'fullPath' => project.full_path) } + it 'shows an error if complexity is too high' do + project = create(:project, :repository) + query = graphql_query_for('project', { 'fullPath' => project.full_path }, "id\nname\ndescription") - it 'shows an error if complexity it too high' do allow(GitlabSchema).to receive(:max_query_complexity).and_return 1 post_graphql(query, current_user: nil) expect(graphql_errors.first['message']).to include('which exceeds max complexity of 1') end + + context 'when IntrospectionQuery' do + it 'is not too complex' do + query = File.read(Rails.root.join('spec/fixtures/api/graphql/introspection.graphql')) + + post_graphql(query, current_user: nil) + + expect(graphql_errors).to be_nil + end + end end -- cgit v1.2.1 From d3257e2cbc047de5da69cabd210d2b885092f5d2 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 8 Apr 2019 19:51:24 +0000 Subject: Enable ESLint rule jest/no-focused-tests --- spec/frontend/.eslintrc.yml | 2 ++ spec/frontend/serverless/components/functions_spec.js | 14 +++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) (limited to 'spec') diff --git a/spec/frontend/.eslintrc.yml b/spec/frontend/.eslintrc.yml index 054dc27cda6..30b559c2363 100644 --- a/spec/frontend/.eslintrc.yml +++ b/spec/frontend/.eslintrc.yml @@ -12,3 +12,5 @@ globals: loadFixtures: false preloadFixtures: false setFixtures: false +rules: + jest/no-focused-tests: error diff --git a/spec/frontend/serverless/components/functions_spec.js b/spec/frontend/serverless/components/functions_spec.js index 5533de1a70a..7af33ceaadc 100644 --- a/spec/frontend/serverless/components/functions_spec.js +++ b/spec/frontend/serverless/components/functions_spec.js @@ -4,14 +4,21 @@ import axios from '~/lib/utils/axios_utils'; import functionsComponent from '~/serverless/components/functions.vue'; import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createStore } from '~/serverless/store'; +import { TEST_HOST } from 'helpers/test_constants'; import { mockServerlessFunctions } from '../mock_data'; describe('functionsComponent', () => { + const statusPath = `${TEST_HOST}/statusPath`; + let component; let store; let localVue; + let axiosMock; beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + axiosMock.onGet(statusPath).reply(200); + localVue = createLocalVue(); localVue.use(Vuex); @@ -20,6 +27,7 @@ describe('functionsComponent', () => { afterEach(() => { component.vm.$destroy(); + axiosMock.restore(); }); it('should render empty state when Knative is not installed', () => { @@ -80,11 +88,7 @@ describe('functionsComponent', () => { ); }); - fit('should render the functions list', () => { - const statusPath = 'statusPath'; - const axiosMock = new AxiosMockAdapter(axios); - axiosMock.onGet(statusPath).reply(200); - + it('should render the functions list', () => { component = shallowMount(functionsComponent, { localVue, store, -- cgit v1.2.1 From 61d08f575c0ecc0be00ff2db5509c38ae67bb65c Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 8 Apr 2019 19:34:39 +0200 Subject: Enable ESLint rule jest/no-identical-title --- spec/frontend/.eslintrc.yml | 1 + 1 file changed, 1 insertion(+) (limited to 'spec') diff --git a/spec/frontend/.eslintrc.yml b/spec/frontend/.eslintrc.yml index 30b559c2363..b4bba8df833 100644 --- a/spec/frontend/.eslintrc.yml +++ b/spec/frontend/.eslintrc.yml @@ -13,4 +13,5 @@ globals: preloadFixtures: false setFixtures: false rules: + jest/no-identical-title: error jest/no-focused-tests: error -- cgit v1.2.1 From f23737a498e41a9ef2ae84fa45dbbdaaf524a174 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 8 Apr 2019 19:54:15 +0200 Subject: Refactor diff_spec.js to use table-based tests --- spec/frontend/ide/lib/diff/diff_spec.js | 93 ++++++++++++++++----------------- 1 file changed, 45 insertions(+), 48 deletions(-) (limited to 'spec') diff --git a/spec/frontend/ide/lib/diff/diff_spec.js b/spec/frontend/ide/lib/diff/diff_spec.js index 57f3ac3d365..d9b088e2c12 100644 --- a/spec/frontend/ide/lib/diff/diff_spec.js +++ b/spec/frontend/ide/lib/diff/diff_spec.js @@ -9,60 +9,57 @@ describe('Multi-file editor library diff calculator', () => { }); describe('modified', () => { - it('', () => { - const diff = computeDiff('123', '1234')[0]; - - expect(diff.added).toBeTruthy(); - expect(diff.modified).toBeTruthy(); - expect(diff.removed).toBeUndefined(); - }); - - it('', () => { - const diff = computeDiff('123\n123\n123', '123\n1234\n123')[0]; - - expect(diff.added).toBeTruthy(); - expect(diff.modified).toBeTruthy(); - expect(diff.removed).toBeUndefined(); - expect(diff.lineNumber).toBe(2); - }); + it.each` + originalContent | newContent | lineNumber + ${'123'} | ${'1234'} | ${1} + ${'123\n123\n123'} | ${'123\n1234\n123'} | ${2} + `( + 'marks line $lineNumber as added and modified but not removed', + ({ originalContent, newContent, lineNumber }) => { + const diff = computeDiff(originalContent, newContent)[0]; + + expect(diff.added).toBeTruthy(); + expect(diff.modified).toBeTruthy(); + expect(diff.removed).toBeUndefined(); + expect(diff.lineNumber).toBe(lineNumber); + }, + ); }); describe('added', () => { - it('', () => { - const diff = computeDiff('123', '123\n123')[0]; - - expect(diff.added).toBeTruthy(); - expect(diff.modified).toBeUndefined(); - expect(diff.removed).toBeUndefined(); - }); - - it('', () => { - const diff = computeDiff('123\n123\n123', '123\n123\n1234\n123')[0]; - - expect(diff.added).toBeTruthy(); - expect(diff.modified).toBeUndefined(); - expect(diff.removed).toBeUndefined(); - expect(diff.lineNumber).toBe(3); - }); + it.each` + originalContent | newContent | lineNumber + ${'123'} | ${'123\n123'} | ${1} + ${'123\n123\n123'} | ${'123\n123\n1234\n123'} | ${3} + `( + 'marks line $lineNumber as added but not modified and not removed', + ({ originalContent, newContent, lineNumber }) => { + const diff = computeDiff(originalContent, newContent)[0]; + + expect(diff.added).toBeTruthy(); + expect(diff.modified).toBeUndefined(); + expect(diff.removed).toBeUndefined(); + expect(diff.lineNumber).toBe(lineNumber); + }, + ); }); describe('removed', () => { - it('', () => { - const diff = computeDiff('123', '')[0]; - - expect(diff.added).toBeUndefined(); - expect(diff.modified).toBeUndefined(); - expect(diff.removed).toBeTruthy(); - }); - - it('', () => { - const diff = computeDiff('123\n123\n123', '123\n123')[0]; - - expect(diff.added).toBeUndefined(); - expect(diff.modified).toBeTruthy(); - expect(diff.removed).toBeTruthy(); - expect(diff.lineNumber).toBe(2); - }); + it.each` + originalContent | newContent | lineNumber | modified + ${'123'} | ${''} | ${1} | ${undefined} + ${'123\n123\n123'} | ${'123\n123'} | ${2} | ${true} + `( + 'marks line $lineNumber as removed', + ({ originalContent, newContent, lineNumber, modified }) => { + const diff = computeDiff(originalContent, newContent)[0]; + + expect(diff.added).toBeUndefined(); + expect(diff.modified).toBe(modified); + expect(diff.removed).toBeTruthy(); + expect(diff.lineNumber).toBe(lineNumber); + }, + ); }); it('includes line number of change', () => { -- cgit v1.2.1 From aa352a95df665ded5178c1b26d4492433e47714e Mon Sep 17 00:00:00 2001 From: Luke Duncalfe Date: Fri, 29 Mar 2019 14:07:03 +1300 Subject: Support merge request create with push options To create a new merge request: git push -u origin -o merge_request.create To create a new merge request setting target branch: git push -u origin -o merge_request.create \ -o merge_request.target=123 To update an existing merge request with a new target branch: git push -u origin -o merge_request.target=123 A new Gitlab::PushOptions class handles parsing and validating the push options array. This can be the start of the standard of GitLab accepting push options that follow namespacing rules. Rules are discussed in issue https://gitlab.com/gitlab-org/gitlab-ce/issues/43263. E.g. these push options: -o merge_request.create -o merge_request.target=123 Become parsed as: { merge_request: { create: true, target: '123', } } And are fetched with the class via: push_options.get(:merge_request) push_options.get(:merge_request, :create) push_options.get(:merge_request, :target) A new MergeRequests::PushOptionsHandlerService takes the `merge_request` namespaced push options and handles creating and updating merge requests. Any errors encountered are passed to the existing `output` Hash in Api::Internal's `post_receive` endpoint, and passed to gitlab-shell where they're output to the user. Issue https://gitlab.com/gitlab-org/gitlab-ce/issues/43263 --- spec/lib/gitlab/push_options_spec.rb | 91 +++++++ spec/requests/api/internal_spec.rb | 61 +++++ .../push_options_handler_service_spec.rb | 291 +++++++++++++++++++++ 3 files changed, 443 insertions(+) create mode 100644 spec/lib/gitlab/push_options_spec.rb create mode 100644 spec/services/merge_requests/push_options_handler_service_spec.rb (limited to 'spec') diff --git a/spec/lib/gitlab/push_options_spec.rb b/spec/lib/gitlab/push_options_spec.rb new file mode 100644 index 00000000000..68b64863c21 --- /dev/null +++ b/spec/lib/gitlab/push_options_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::PushOptions do + describe 'namespace and key validation' do + it 'ignores unrecognised namespaces' do + options = described_class.new(['invalid.key=value']) + + expect(options.get(:invalid)).to eq(nil) + end + + it 'ignores unrecognised keys' do + options = described_class.new(['merge_request.key=value']) + + expect(options.get(:merge_request)).to eq(nil) + end + + it 'ignores blank keys' do + options = described_class.new(['merge_request']) + + expect(options.get(:merge_request)).to eq(nil) + end + + it 'parses recognised namespace and key pairs' do + options = described_class.new(['merge_request.target=value']) + + expect(options.get(:merge_request)).to include({ + target: 'value' + }) + end + end + + describe '#get' do + it 'can emulate Hash#dig' do + options = described_class.new(['merge_request.target=value']) + + expect(options.get(:merge_request, :target)).to eq('value') + end + end + + it 'can parse multiple push options' do + options = described_class.new([ + 'merge_request.create', + 'merge_request.target=value' + ]) + + expect(options.get(:merge_request)).to include({ + create: true, + target: 'value' + }) + expect(options.get(:merge_request, :create)).to eq(true) + expect(options.get(:merge_request, :target)).to eq('value') + end + + it 'stores options internally as a HashWithIndifferentAccess' do + options = described_class.new([ + 'merge_request.create' + ]) + + expect(options.get('merge_request', 'create')).to eq(true) + expect(options.get(:merge_request, :create)).to eq(true) + end + + it 'selects the last option when options contain duplicate namespace and key pairs' do + options = described_class.new([ + 'merge_request.target=value1', + 'merge_request.target=value2' + ]) + + expect(options.get(:merge_request, :target)).to eq('value2') + end + + it 'defaults values to true' do + options = described_class.new(['merge_request.create']) + + expect(options.get(:merge_request, :create)).to eq(true) + end + + it 'expands aliases' do + options = described_class.new(['mr.target=value']) + + expect(options.get(:merge_request, :target)).to eq('value') + end + + it 'forgives broken push options' do + options = described_class.new(['merge_request . target = value']) + + expect(options.get(:merge_request, :target)).to eq('value') + end +end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 0919540e4ba..62ba4281df5 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -938,6 +938,67 @@ describe API::Internal do expect(json_response['merge_request_urls']).to eq([]) end + it 'does not invoke MergeRequests::PushOptionsHandlerService' do + expect(MergeRequests::PushOptionsHandlerService).not_to receive(:new) + + post api("/internal/post_receive"), params: valid_params + end + + context 'when there are merge_request push options' do + before do + valid_params[:push_options] = ['merge_request.create'] + end + + it 'invokes MergeRequests::PushOptionsHandlerService' do + expect(MergeRequests::PushOptionsHandlerService).to receive(:new) + + post api("/internal/post_receive"), params: valid_params + end + + it 'links to the newly created merge request' do + post api("/internal/post_receive"), params: valid_params + + expect(json_response['merge_request_urls']).to match [{ + "branch_name" => "new_branch", + "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/1", + "new_merge_request" => false + }] + end + + it 'adds errors raised from MergeRequests::PushOptionsHandlerService to warnings' do + expect(MergeRequests::PushOptionsHandlerService).to receive(:new).and_raise( + MergeRequests::PushOptionsHandlerService::Error, 'my warning' + ) + + post api("/internal/post_receive"), params: valid_params + + expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my warning') + end + + it 'adds errors on the service instance to warnings' do + expect_any_instance_of( + MergeRequests::PushOptionsHandlerService + ).to receive(:errors).at_least(:once).and_return(['my error']) + + post api("/internal/post_receive"), params: valid_params + + expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my error') + end + + it 'adds ActiveRecord errors on invalid MergeRequest records to warnings' do + invalid_merge_request = MergeRequest.new + invalid_merge_request.errors.add(:base, 'my error') + + expect_any_instance_of( + MergeRequests::CreateService + ).to receive(:execute).and_return(invalid_merge_request) + + post api("/internal/post_receive"), params: valid_params + + expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my error') + end + end + context 'broadcast message exists' do let!(:broadcast_message) { create(:broadcast_message, starts_at: 1.day.ago, ends_at: 1.day.from_now ) } diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb new file mode 100644 index 00000000000..96becfcae26 --- /dev/null +++ b/spec/services/merge_requests/push_options_handler_service_spec.rb @@ -0,0 +1,291 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MergeRequests::PushOptionsHandlerService do + let(:user) { create(:user) } + let(:project) { create(:project, :public, :repository) } + let(:service) { described_class.new(project, user, changes, push_options) } + let(:source_branch) { 'test' } + let(:target_branch) { 'feature' } + let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } + let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } + let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" } + let(:default_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{project.default_branch}" } + + before do + project.add_developer(user) + end + + shared_examples_for 'a service that can create a merge request' do + subject(:last_mr) { MergeRequest.last } + + it 'creates a merge request' do + expect { service.execute }.to change { MergeRequest.count }.by(1) + end + + it 'sets the correct target branch' do + branch = push_options[:target] || project.default_branch + + service.execute + + expect(last_mr.target_branch).to eq(branch) + end + + it 'assigns the MR to the user' do + service.execute + + expect(last_mr.assignee).to eq(user) + end + + it 'sets the title and description from the first non-merge commit' do + commits = project.repository.commits('master', limit: 5) + + expect(Gitlab::Git::Commit).to receive(:between).at_least(:once).and_return(commits) + + service.execute + + merge_commit = commits.first + non_merge_commit = commits.second + + expect(merge_commit.merge_commit?).to eq(true) + expect(non_merge_commit.merge_commit?).to eq(false) + + expect(last_mr.title).to eq(non_merge_commit.title) + expect(last_mr.description).to eq(non_merge_commit.description) + end + end + + shared_examples_for 'a service that can set the target of a merge request' do + subject(:last_mr) { MergeRequest.last } + + it 'sets the target_branch' do + service.execute + + expect(last_mr.target_branch).to eq(target_branch) + end + end + + shared_examples_for 'a service that does not create a merge request' do + it do + expect { service.execute }.not_to change { MergeRequest.count } + end + end + + shared_examples_for 'a service that does not update a merge request' do + it do + expect { service.execute }.not_to change { MergeRequest.maximum(:updated_at) } + end + end + + shared_examples_for 'a service that does nothing' do + include_examples 'a service that does not create a merge request' + include_examples 'a service that does not update a merge request' + end + + describe '`create` push option' do + let(:push_options) { { create: true } } + + context 'with a new branch' do + let(:changes) { new_branch_changes } + + it_behaves_like 'a service that can create a merge request' + end + + context 'with an existing branch but no open MR' do + let(:changes) { existing_branch_changes } + + it_behaves_like 'a service that can create a merge request' + end + + context 'with an existing branch that has a merge request open' do + let(:changes) { existing_branch_changes } + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)} + + it_behaves_like 'a service that does not create a merge request' + end + + context 'with a deleted branch' do + let(:changes) { deleted_branch_changes } + + it_behaves_like 'a service that does nothing' + end + + context 'with the project default branch' do + let(:changes) { default_branch_changes } + + it_behaves_like 'a service that does nothing' + end + end + + describe '`target` push option' do + let(:push_options) { { target: target_branch } } + + context 'with a new branch' do + let(:changes) { new_branch_changes } + + it_behaves_like 'a service that does not create a merge request' + + it 'adds an error to the service' do + error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}" + + service.execute + + expect(service.errors).to include(error) + end + + context 'when coupled with the `create` push option' do + let(:push_options) { { create: true, target: target_branch } } + + it_behaves_like 'a service that can create a merge request' + it_behaves_like 'a service that can set the target of a merge request' + end + end + + context 'with an existing branch but no open MR' do + let(:changes) { existing_branch_changes } + + it_behaves_like 'a service that does not create a merge request' + + it 'adds an error to the service' do + error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}" + + service.execute + + expect(service.errors).to include(error) + end + + context 'when coupled with the `create` push option' do + let(:push_options) { { create: true, target: target_branch } } + + it_behaves_like 'a service that can create a merge request' + it_behaves_like 'a service that can set the target of a merge request' + end + end + + context 'with an existing branch that has a merge request open' do + let(:changes) { existing_branch_changes } + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)} + + it_behaves_like 'a service that does not create a merge request' + it_behaves_like 'a service that can set the target of a merge request' + end + + context 'with a deleted branch' do + let(:changes) { deleted_branch_changes } + + it_behaves_like 'a service that does nothing' + end + + context 'with the project default branch' do + let(:changes) { default_branch_changes } + + it_behaves_like 'a service that does nothing' + end + end + + describe 'multiple pushed branches' do + let(:push_options) { { create: true } } + let(:changes) do + [ + new_branch_changes, + "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/second-branch" + ] + end + + it 'creates a merge request per branch' do + expect { service.execute }.to change { MergeRequest.count }.by(2) + end + + context 'when there are too many pushed branches' do + let(:limit) { MergeRequests::PushOptionsHandlerService::LIMIT } + let(:changes) do + TestEnv::BRANCH_SHA.to_a[0..limit].map do |x| + "#{Gitlab::Git::BLANK_SHA} #{x.first} refs/heads/#{x.last}" + end + end + + it 'throws an error' do + expect { service.execute }.to raise_error( + MergeRequests::PushOptionsHandlerService::Error, + "Too many branches pushed (#{limit + 1} were pushed, limit is #{limit})" + ) + end + end + end + + describe 'no push options' do + let(:push_options) { {} } + let(:changes) { new_branch_changes } + + it_behaves_like 'a service that does nothing' + end + + describe 'no user' do + let(:user) { nil } + let(:push_options) { { create: true } } + let(:changes) { new_branch_changes } + + it 'throws an error' do + expect { service.execute }.to raise_error( + MergeRequests::PushOptionsHandlerService::Error, + 'User is required' + ) + end + end + + describe 'unauthorized user' do + let(:push_options) { { create: true } } + let(:changes) { new_branch_changes } + + it 'throws an error' do + Members::DestroyService.new(user).execute(ProjectMember.find_by!(user_id: user.id)) + + expect { service.execute }.to raise_error(Gitlab::Access::AccessDeniedError) + end + end + + describe 'when target is not a valid branch name' do + let(:push_options) { { create: true, target: 'my-branch' } } + let(:changes) { new_branch_changes } + + it 'throws an error' do + expect { service.execute }.to raise_error( + MergeRequests::PushOptionsHandlerService::Error, + 'Branch my-branch does not exist' + ) + end + end + + describe 'when MRs are not enabled' do + let(:push_options) { { create: true } } + let(:changes) { new_branch_changes } + + it 'throws an error' do + expect(project).to receive(:merge_requests_enabled?).and_return(false) + + expect { service.execute }.to raise_error( + MergeRequests::PushOptionsHandlerService::Error, + 'Merge requests are not enabled for project' + ) + end + end + + describe 'when MR has ActiveRecord errors' do + let(:push_options) { { create: true } } + let(:changes) { new_branch_changes } + + it 'adds the error to its errors property' do + invalid_merge_request = MergeRequest.new + invalid_merge_request.errors.add(:base, 'my error') + + expect_any_instance_of( + MergeRequests::CreateService + ).to receive(:execute).and_return(invalid_merge_request) + + service.execute + + expect(service.errors).to eq(['my error']) + end + end +end -- cgit v1.2.1 From 867ac4d1f733254de1b4c44f314e585e6b2720e8 Mon Sep 17 00:00:00 2001 From: Luke Duncalfe Date: Tue, 2 Apr 2019 14:30:40 +1300 Subject: Change double quote strings to single quotes --- spec/requests/api/internal_spec.rb | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) (limited to 'spec') diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 62ba4281df5..3cc0cceeab1 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -907,7 +907,7 @@ describe API::Internal do expect(PostReceive).to receive(:perform_async) .with(gl_repository, identifier, changes, push_options) - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params end it 'decreases the reference counter and returns the result' do @@ -915,13 +915,13 @@ describe API::Internal do .and_return(reference_counter) expect(reference_counter).to receive(:decrease).and_return(true) - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(json_response['reference_counter_decreased']).to be(true) end it 'returns link to create new merge request' do - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(json_response['merge_request_urls']).to match [{ "branch_name" => "new_branch", @@ -933,7 +933,7 @@ describe API::Internal do it 'returns empty array if printing_merge_request_link_enabled is false' do project.update!(printing_merge_request_link_enabled: false) - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(json_response['merge_request_urls']).to eq([]) end @@ -941,7 +941,7 @@ describe API::Internal do it 'does not invoke MergeRequests::PushOptionsHandlerService' do expect(MergeRequests::PushOptionsHandlerService).not_to receive(:new) - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params end context 'when there are merge_request push options' do @@ -952,16 +952,16 @@ describe API::Internal do it 'invokes MergeRequests::PushOptionsHandlerService' do expect(MergeRequests::PushOptionsHandlerService).to receive(:new) - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params end it 'links to the newly created merge request' do - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(json_response['merge_request_urls']).to match [{ - "branch_name" => "new_branch", - "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/1", - "new_merge_request" => false + 'branch_name' => 'new_branch', + 'url' => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/1", + 'new_merge_request' => false }] end @@ -970,7 +970,7 @@ describe API::Internal do MergeRequests::PushOptionsHandlerService::Error, 'my warning' ) - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my warning') end @@ -980,7 +980,7 @@ describe API::Internal do MergeRequests::PushOptionsHandlerService ).to receive(:errors).at_least(:once).and_return(['my error']) - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my error') end @@ -993,7 +993,7 @@ describe API::Internal do MergeRequests::CreateService ).to receive(:execute).and_return(invalid_merge_request) - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my error') end @@ -1003,7 +1003,7 @@ describe API::Internal do let!(:broadcast_message) { create(:broadcast_message, starts_at: 1.day.ago, ends_at: 1.day.from_now ) } it 'returns one broadcast message' do - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(response).to have_gitlab_http_status(200) expect(json_response['broadcast_message']).to eq(broadcast_message.message) @@ -1012,7 +1012,7 @@ describe API::Internal do context 'broadcast message does not exist' do it 'returns empty string' do - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(response).to have_gitlab_http_status(200) expect(json_response['broadcast_message']).to eq(nil) @@ -1023,7 +1023,7 @@ describe API::Internal do it 'returns empty string' do allow(BroadcastMessage).to receive(:current).and_return(nil) - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(response).to have_gitlab_http_status(200) expect(json_response['broadcast_message']).to eq(nil) @@ -1035,7 +1035,7 @@ describe API::Internal do project_moved = Gitlab::Checks::ProjectMoved.new(project, user, 'http', 'foo/baz') project_moved.add_message - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(response).to have_gitlab_http_status(200) expect(json_response["redirected_message"]).to be_present @@ -1048,7 +1048,7 @@ describe API::Internal do project_created = Gitlab::Checks::ProjectCreated.new(project, user, 'http') project_created.add_message - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(response).to have_gitlab_http_status(200) expect(json_response["project_created_message"]).to be_present @@ -1060,7 +1060,7 @@ describe API::Internal do it 'does not try to notify that project moved' do allow_any_instance_of(Gitlab::Identifier).to receive(:identify).and_return(nil) - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(response).to have_gitlab_http_status(200) end -- cgit v1.2.1 From ca884980ee8e6fe1269f5abdb803519d51aa09c0 Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Sun, 7 Apr 2019 15:35:16 -0300 Subject: [CE] Support multiple assignees for merge requests Backports https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/10161 (code out of ee/ folder). --- .../projects/merge_requests_controller_spec.rb | 6 +- spec/features/dashboard/issuables_counter_spec.rb | 4 +- spec/features/dashboard/merge_requests_spec.rb | 4 +- spec/features/groups/merge_requests_spec.rb | 2 +- spec/features/issues/form_spec.rb | 4 +- .../user_creates_merge_request_spec.rb | 4 +- .../features/merge_request/user_creates_mr_spec.rb | 15 +- spec/features/merge_request/user_edits_mr_spec.rb | 18 ++- .../user_filters_by_assignees_spec.rb | 2 +- .../user_filters_by_multiple_criteria_spec.rb | 2 +- .../user_lists_merge_requests_spec.rb | 4 +- .../merge_requests/user_mass_updates_spec.rb | 3 +- .../search/user_uses_header_search_field_spec.rb | 4 +- spec/finders/issues_finder_spec.rb | 64 +++------ spec/finders/merge_requests_finder_spec.rb | 49 +++++-- .../api/schemas/entities/merge_request_basic.json | 12 +- .../api/schemas/public_api/v4/merge_request.json | 5 + spec/javascripts/sidebar/assignees_spec.js | 85 +++++++++++ spec/lib/gitlab/hook_data/issuable_builder_spec.rb | 6 +- .../gitlab/hook_data/merge_request_builder_spec.rb | 1 + spec/lib/gitlab/import_export/all_models.yml | 4 + .../gitlab/import_export/safe_model_attributes.yml | 4 + spec/lib/gitlab/issuable_metadata_spec.rb | 4 +- spec/mailers/notify_spec.rb | 18 +-- spec/models/ci/pipeline_spec.rb | 8 +- spec/models/concerns/deprecated_assignee_spec.rb | 160 +++++++++++++++++++++ spec/models/concerns/issuable_spec.rb | 7 +- spec/models/event_spec.rb | 2 +- spec/models/merge_request_spec.rb | 60 ++------ spec/models/user_spec.rb | 6 +- spec/requests/api/events_spec.rb | 4 +- spec/requests/api/merge_requests_spec.rb | 124 +++++++++++++--- spec/services/issuable/bulk_update_service_spec.rb | 16 +-- spec/services/issuable/destroy_service_spec.rb | 2 +- spec/services/members/destroy_service_spec.rb | 4 +- spec/services/merge_requests/close_service_spec.rb | 2 +- .../create_from_issue_service_spec.rb | 2 +- .../services/merge_requests/create_service_spec.rb | 30 ++-- .../merge_requests/ff_merge_service_spec.rb | 2 +- spec/services/merge_requests/merge_service_spec.rb | 4 +- .../merge_requests/merge_to_ref_service_spec.rb | 2 +- .../merge_requests/post_merge_service_spec.rb | 2 +- .../services/merge_requests/reopen_service_spec.rb | 2 +- .../services/merge_requests/update_service_spec.rb | 42 +++--- spec/services/notification_service_spec.rb | 55 +++---- .../quick_actions/interpret_service_spec.rb | 4 +- spec/services/system_note_service_spec.rb | 6 +- spec/services/todo_service_spec.rb | 110 +++++++------- spec/services/users/destroy_service_spec.rb | 4 +- .../merge_requests_finder_shared_contexts.rb | 6 +- .../shared_contexts/merge_request_create.rb | 26 ++++ spec/support/shared_contexts/merge_request_edit.rb | 28 ++++ .../creatable_merge_request_shared_examples.rb | 31 +--- .../editable_merge_request_shared_examples.rb | 28 +--- .../multiple_assignees_mr_shared_examples.rb | 47 ++++++ .../finders/assignees_filter_spec.rb | 49 +++++++ .../projects/merge_requests/edit.html.haml_spec.rb | 6 +- .../projects/merge_requests/show.html.haml_spec.rb | 13 -- 58 files changed, 810 insertions(+), 408 deletions(-) create mode 100644 spec/models/concerns/deprecated_assignee_spec.rb create mode 100644 spec/support/shared_contexts/merge_request_create.rb create mode 100644 spec/support/shared_contexts/merge_request_edit.rb create mode 100644 spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb create mode 100644 spec/support/shared_examples/finders/assignees_filter_spec.rb (limited to 'spec') diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 017162519d8..a125e470522 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -238,11 +238,11 @@ describe Projects::MergeRequestsController do assignee = create(:user) project.add_developer(assignee) - update_merge_request({ assignee_id: assignee.id }, format: :json) + update_merge_request({ assignee_ids: [assignee.id] }, format: :json) + body = JSON.parse(response.body) - expect(body['assignee'].keys) - .to match_array(%w(name username avatar_url id state web_url)) + expect(body['assignees']).to all(include(*%w(name username avatar_url id state web_url))) end end diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb index fbc2e5cc3d3..50b71368e13 100644 --- a/spec/features/dashboard/issuables_counter_spec.rb +++ b/spec/features/dashboard/issuables_counter_spec.rb @@ -8,7 +8,7 @@ describe 'Navigation bar counter', :use_clean_rails_memory_store_caching do before do issue.assignees = [user] - merge_request.update(assignee: user) + merge_request.update(assignees: [user]) sign_in(user) end @@ -33,7 +33,7 @@ describe 'Navigation bar counter', :use_clean_rails_memory_store_caching do expect_counters('merge_requests', '1') - merge_request.update(assignee: nil) + merge_request.update(assignees: []) user.invalidate_cache_counts diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb index 4965770605a..0c6713f623c 100644 --- a/spec/features/dashboard/merge_requests_spec.rb +++ b/spec/features/dashboard/merge_requests_spec.rb @@ -48,14 +48,14 @@ describe 'Dashboard Merge Requests' do let!(:assigned_merge_request) do create(:merge_request, - assignee: current_user, + assignees: [current_user], source_project: project, author: create(:user)) end let!(:assigned_merge_request_from_fork) do create(:merge_request, - source_branch: 'markdown', assignee: current_user, + source_branch: 'markdown', assignees: [current_user], target_project: public_project, source_project: forked_project, author: create(:user)) end diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb index 54a8016c157..e1bc4eca619 100644 --- a/spec/features/groups/merge_requests_spec.rb +++ b/spec/features/groups/merge_requests_spec.rb @@ -38,7 +38,7 @@ describe 'Group merge requests page' do context 'when merge request assignee to user' do before do - issuable.update!(assignee: user) + issuable.update!(assignees: [user]) visit path end diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index 26c781350e5..6fa2ad8711f 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -30,8 +30,8 @@ describe 'New/edit issue', :js do # the original method, resulting in infinite recursion when called. # This is likely a bug with helper modules included into dynamically generated view classes. # To work around this, we have to hold on to and call to the original implementation manually. - original_issue_dropdown_options = FormHelper.instance_method(:issue_assignees_dropdown_options) - allow_any_instance_of(FormHelper).to receive(:issue_assignees_dropdown_options).and_wrap_original do |original, *args| + original_issue_dropdown_options = FormHelper.instance_method(:assignees_dropdown_options) + allow_any_instance_of(FormHelper).to receive(:assignees_dropdown_options).and_wrap_original do |original, *args| options = original_issue_dropdown_options.bind(original.receiver).call(*args) options[:data][:per_page] = 2 diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb index ea2bb1503bb..bcc11217389 100644 --- a/spec/features/merge_request/user_creates_merge_request_spec.rb +++ b/spec/features/merge_request/user_creates_merge_request_spec.rb @@ -68,15 +68,15 @@ describe "User creates a merge request", :js do fill_in("Title", with: title) end - click_button("Assignee") - expect(find(".js-assignee-search")["data-project-id"]).to eq(project.id.to_s) + find('.js-assignee-search').click page.within(".dropdown-menu-user") do expect(page).to have_content("Unassigned") .and have_content(user.name) .and have_content(project.users.first.name) end + find('.js-assignee-search').click click_button("Submit merge request") diff --git a/spec/features/merge_request/user_creates_mr_spec.rb b/spec/features/merge_request/user_creates_mr_spec.rb index c169a68cd1c..c9dedab048a 100644 --- a/spec/features/merge_request/user_creates_mr_spec.rb +++ b/spec/features/merge_request/user_creates_mr_spec.rb @@ -1,11 +1,18 @@ require 'rails_helper' describe 'Merge request > User creates MR' do - it_behaves_like 'a creatable merge request' + include ProjectForksHelper - context 'from a forked project' do - include ProjectForksHelper + before do + stub_licensed_features(multiple_merge_request_assignees: false) + end + context 'non-fork merge request' do + include_context 'merge request create context' + it_behaves_like 'a creatable merge request' + end + + context 'from a forked project' do let(:canonical_project) { create(:project, :public, :repository) } let(:source_project) do @@ -15,6 +22,7 @@ describe 'Merge request > User creates MR' do end context 'to canonical project' do + include_context 'merge request create context' it_behaves_like 'a creatable merge request' end @@ -25,6 +33,7 @@ describe 'Merge request > User creates MR' do namespace: user.namespace) end + include_context 'merge request create context' it_behaves_like 'a creatable merge request' end end diff --git a/spec/features/merge_request/user_edits_mr_spec.rb b/spec/features/merge_request/user_edits_mr_spec.rb index 3152707136c..25979513ead 100644 --- a/spec/features/merge_request/user_edits_mr_spec.rb +++ b/spec/features/merge_request/user_edits_mr_spec.rb @@ -1,13 +1,21 @@ -require 'rails_helper' +require 'spec_helper' describe 'Merge request > User edits MR' do include ProjectForksHelper - it_behaves_like 'an editable merge request' + before do + stub_licensed_features(multiple_merge_request_assignees: false) + end + + context 'non-fork merge request' do + include_context 'merge request edit context' + it_behaves_like 'an editable merge request' + end context 'for a forked project' do - it_behaves_like 'an editable merge request' do - let(:source_project) { fork_project(target_project, nil, repository: true) } - end + let(:source_project) { fork_project(target_project, nil, repository: true) } + + include_context 'merge request edit context' + it_behaves_like 'an editable merge request' end end diff --git a/spec/features/merge_requests/user_filters_by_assignees_spec.rb b/spec/features/merge_requests/user_filters_by_assignees_spec.rb index d6c770c93f1..0cbf1bcae30 100644 --- a/spec/features/merge_requests/user_filters_by_assignees_spec.rb +++ b/spec/features/merge_requests/user_filters_by_assignees_spec.rb @@ -7,7 +7,7 @@ describe 'Merge Requests > User filters by assignees', :js do let(:user) { project.creator } before do - create(:merge_request, assignee: user, title: 'Bugfix1', source_project: project, target_project: project, source_branch: 'bugfix1') + create(:merge_request, assignees: [user], title: 'Bugfix1', source_project: project, target_project: project, source_branch: 'bugfix1') create(:merge_request, title: 'Bugfix2', source_project: project, target_project: project, source_branch: 'bugfix2') sign_in(user) diff --git a/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb b/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb index 1615899a047..4627931f26a 100644 --- a/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb +++ b/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb @@ -10,7 +10,7 @@ describe 'Merge requests > User filters by multiple criteria', :js do before do sign_in(user) - mr = create(:merge_request, title: 'Bugfix2', author: user, assignee: user, source_project: project, target_project: project, milestone: milestone) + mr = create(:merge_request, title: 'Bugfix2', author: user, assignees: [user], source_project: project, target_project: project, milestone: milestone) mr.labels << wontfix visit project_merge_requests_path(project) diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb index c691011b9ca..bd91fae1453 100644 --- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb @@ -12,7 +12,7 @@ describe 'Merge requests > User lists merge requests' do title: 'fix', source_project: project, source_branch: 'fix', - assignee: user, + assignees: [user], milestone: create(:milestone, project: project, due_date: '2013-12-11'), created_at: 1.minute.ago, updated_at: 1.minute.ago) @@ -20,7 +20,7 @@ describe 'Merge requests > User lists merge requests' do title: 'markdown', source_project: project, source_branch: 'markdown', - assignee: user, + assignees: [user], milestone: create(:milestone, project: project, due_date: '2013-12-12'), created_at: 2.minutes.ago, updated_at: 2.minutes.ago) diff --git a/spec/features/merge_requests/user_mass_updates_spec.rb b/spec/features/merge_requests/user_mass_updates_spec.rb index e535c7e5811..c2dd105324d 100644 --- a/spec/features/merge_requests/user_mass_updates_spec.rb +++ b/spec/features/merge_requests/user_mass_updates_spec.rb @@ -54,8 +54,7 @@ describe 'Merge requests > User mass updates', :js do describe 'remove assignee' do before do - merge_request.assignee = user - merge_request.save + merge_request.assignees = [user] visit project_merge_requests_path(project) end diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb index 444de26733f..1cc47cd6bd1 100644 --- a/spec/features/search/user_uses_header_search_field_spec.rb +++ b/spec/features/search/user_uses_header_search_field_spec.rb @@ -36,7 +36,7 @@ describe 'User uses header search field' do end context 'when clicking merge requests' do - let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) } + let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignees: [user]) } it 'shows assigned merge requests' do find('.search-input-container .dropdown-menu').click_link('Merge requests assigned to me') @@ -100,7 +100,7 @@ describe 'User uses header search field' do end context 'when clicking merge requests' do - let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) } + let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignees: [user]) } it 'shows assigned merge requests' do find('.dropdown-menu').click_link('Merge requests assigned to me') diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index fe53fabe54c..6e6dc334354 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -13,60 +13,32 @@ describe IssuesFinder do expect(issues).to contain_exactly(issue1, issue2, issue3, issue4) end - context 'filtering by assignee ID' do - let(:params) { { assignee_id: user.id } } + context 'assignee filtering' do + let(:issuables) { issues } - it 'returns issues assigned to that user' do - expect(issues).to contain_exactly(issue1, issue2) - end - end - - context 'filtering by assignee usernames' do - set(:user3) { create(:user) } - let(:params) { { assignee_username: [user2.username, user3.username] } } - - before do - project2.add_developer(user3) - - issue3.assignees = [user2, user3] - end - - it 'returns issues assigned to those users' do - expect(issues).to contain_exactly(issue3) - end - end - - context 'filtering by no assignee' do - let(:params) { { assignee_id: 'None' } } - - it 'returns issues not assigned to any assignee' do - expect(issues).to contain_exactly(issue4) - end - - it 'returns issues not assigned to any assignee' do - params[:assignee_id] = 0 - - expect(issues).to contain_exactly(issue4) + it_behaves_like 'assignee ID filter' do + let(:params) { { assignee_id: user.id } } + let(:expected_issuables) { [issue1, issue2] } end - it 'returns issues not assigned to any assignee' do - params[:assignee_id] = 'none' + it_behaves_like 'assignee username filter' do + before do + project2.add_developer(user3) + issue3.assignees = [user2, user3] + end - expect(issues).to contain_exactly(issue4) + set(:user3) { create(:user) } + let(:params) { { assignee_username: [user2.username, user3.username] } } + let(:expected_issuables) { [issue3] } end - end - context 'filtering by any assignee' do - let(:params) { { assignee_id: 'Any' } } - - it 'returns issues assigned to any assignee' do - expect(issues).to contain_exactly(issue1, issue2, issue3) + it_behaves_like 'no assignee filter' do + set(:user3) { create(:user) } + let(:expected_issuables) { [issue4] } end - it 'returns issues assigned to any assignee' do - params[:assignee_id] = 'any' - - expect(issues).to contain_exactly(issue1, issue2, issue3) + it_behaves_like 'any assignee filter' do + let(:expected_issuables) { [issue1, issue2, issue3] } end end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index f508b9bdb6f..ef002125635 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -136,21 +136,50 @@ describe MergeRequestsFinder do end end - context 'filtering by group milestone' do - let(:group_milestone) { create(:milestone, group: group) } + context 'assignee filtering' do + let(:issuables) { described_class.new(user, params).execute } - before do - project2.update(namespace: group) - merge_request2.update(milestone: group_milestone) - merge_request3.update(milestone: group_milestone) + it_behaves_like 'assignee ID filter' do + let(:params) { { assignee_id: user.id } } + let(:expected_issuables) { [merge_request1, merge_request2] } end - it 'returns merge requests assigned to that group milestone' do - params = { milestone_title: group_milestone.title } + it_behaves_like 'assignee username filter' do + before do + project2.add_developer(user3) + merge_request3.assignees = [user2, user3] + end - merge_requests = described_class.new(user, params).execute + set(:user3) { create(:user) } + let(:params) { { assignee_username: [user2.username, user3.username] } } + let(:expected_issuables) { [merge_request3] } + end + + it_behaves_like 'no assignee filter' do + set(:user3) { create(:user) } + let(:expected_issuables) { [merge_request4, merge_request5] } + end + + it_behaves_like 'any assignee filter' do + let(:expected_issuables) { [merge_request1, merge_request2, merge_request3] } + end + + context 'filtering by group milestone' do + let(:group_milestone) { create(:milestone, group: group) } + + before do + project2.update(namespace: group) + merge_request2.update(milestone: group_milestone) + merge_request3.update(milestone: group_milestone) + end + + it 'returns merge requests assigned to that group milestone' do + params = { milestone_title: group_milestone.title } - expect(merge_requests).to contain_exactly(merge_request2, merge_request3) + merge_requests = described_class.new(user, params).execute + + expect(merge_requests).to contain_exactly(merge_request2, merge_request3) + end end end diff --git a/spec/fixtures/api/schemas/entities/merge_request_basic.json b/spec/fixtures/api/schemas/entities/merge_request_basic.json index 3006b482d41..88a600398b1 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_basic.json +++ b/spec/fixtures/api/schemas/entities/merge_request_basic.json @@ -6,14 +6,14 @@ "source_branch_exists": { "type": "boolean" }, "merge_error": { "type": ["string", "null"] }, "rebase_in_progress": { "type": "boolean" }, - "assignee_id": { "type": ["integer", "null"] }, "allow_collaboration": { "type": "boolean"}, "allow_maintainer_to_push": { "type": "boolean"}, - "assignee": { - "oneOf": [ - { "type": "null" }, - { "$ref": "user.json" } - ] + "assignees": { + "type": ["array"], + "items": { + "type": "object", + "$ref": "../public_api/v4/user/basic.json" + } }, "milestone": { "type": [ "object", "null" ] diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_request.json b/spec/fixtures/api/schemas/public_api/v4/merge_request.json index 918f2c4b47d..a423bf70b69 100644 --- a/spec/fixtures/api/schemas/public_api/v4/merge_request.json +++ b/spec/fixtures/api/schemas/public_api/v4/merge_request.json @@ -64,6 +64,11 @@ }, "additionalProperties": false }, + "assignees": { + "items": { + "$ref": "./merge_request.json" + } + }, "source_project_id": { "type": "integer" }, "target_project_id": { "type": "integer" }, "labels": { diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js index 57b16b12cb0..47fee5d2b21 100644 --- a/spec/javascripts/sidebar/assignees_spec.js +++ b/spec/javascripts/sidebar/assignees_spec.js @@ -132,9 +132,94 @@ describe('Assignee component', () => { -1, ); }); + + it('has correct "cannot merge" tooltip when user cannot merge', () => { + const user = Object.assign({}, UsersMock.user, { can_merge: false }); + + component = new AssigneeComponent({ + propsData: { + rootPath: 'http://localhost:3000/', + users: [user], + editable: true, + issuableType: 'merge_request', + }, + }).$mount(); + + expect(component.mergeNotAllowedTooltipMessage).toEqual('Cannot merge'); + }); }); describe('Two or more assignees/users', () => { + it('has correct "cannot merge" tooltip when one user can merge', () => { + const users = UsersMockHelper.createNumberRandomUsers(3); + users[0].can_merge = true; + users[1].can_merge = false; + users[2].can_merge = false; + + component = new AssigneeComponent({ + propsData: { + rootPath: 'http://localhost:3000/', + users, + editable: true, + issuableType: 'merge_request', + }, + }).$mount(); + + expect(component.mergeNotAllowedTooltipMessage).toEqual('1/3 can merge'); + }); + + it('has correct "cannot merge" tooltip when no user can merge', () => { + const users = UsersMockHelper.createNumberRandomUsers(2); + users[0].can_merge = false; + users[1].can_merge = false; + + component = new AssigneeComponent({ + propsData: { + rootPath: 'http://localhost:3000/', + users, + editable: true, + issuableType: 'merge_request', + }, + }).$mount(); + + expect(component.mergeNotAllowedTooltipMessage).toEqual('No one can merge'); + }); + + it('has correct "cannot merge" tooltip when more than one user can merge', () => { + const users = UsersMockHelper.createNumberRandomUsers(3); + users[0].can_merge = false; + users[1].can_merge = true; + users[2].can_merge = true; + + component = new AssigneeComponent({ + propsData: { + rootPath: 'http://localhost:3000/', + users, + editable: true, + issuableType: 'merge_request', + }, + }).$mount(); + + expect(component.mergeNotAllowedTooltipMessage).toEqual('2/3 can merge'); + }); + + it('has no "cannot merge" tooltip when every user can merge', () => { + const users = UsersMockHelper.createNumberRandomUsers(2); + users[0].can_merge = true; + users[1].can_merge = true; + + component = new AssigneeComponent({ + propsData: { + rootPath: 'http://localhost:3000/', + users, + editable: true, + issuableType: 'merge_request', + }, + }).$mount(); + + expect(component.mergeNotAllowedTooltipMessage).toEqual(null); + }); + it('displays two assignee icons when collapsed', () => { const users = UsersMockHelper.createNumberRandomUsers(2); component = new AssigneeComponent({ diff --git a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb index 26529c4759d..569d5dcc757 100644 --- a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb @@ -97,13 +97,13 @@ describe Gitlab::HookData::IssuableBuilder do end context 'merge_request is assigned' do - let(:merge_request) { create(:merge_request, assignee: user) } + let(:merge_request) { create(:merge_request, assignees: [user]) } let(:data) { described_class.new(merge_request).build(user: user) } it 'returns correct hook data' do expect(data[:object_attributes]['assignee_id']).to eq(user.id) - expect(data[:assignee]).to eq(user.hook_attrs) - expect(data).not_to have_key(:assignees) + expect(data[:assignees].first).to eq(user.hook_attrs) + expect(data).not_to have_key(:assignee) end end end diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb index 9ce697adbba..39f80f92fa6 100644 --- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb @@ -10,6 +10,7 @@ describe Gitlab::HookData::MergeRequestBuilder do it 'includes safe attribute' do %w[ assignee_id + assignee_ids author_id created_at description diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index e418516569a..ed557ffd4e3 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -102,6 +102,7 @@ merge_requests: - merge_request_pipelines - merge_request_assignees - suggestions +- assignees merge_request_diff: - merge_request - merge_request_diff_commits @@ -336,6 +337,9 @@ push_event_payload: issue_assignees: - issue - assignee +merge_request_assignees: +- merge_request +- assignee lfs_file_locks: - user project_badges: diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index d0ed588f05f..06995604a24 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -621,3 +621,7 @@ Suggestion: - outdated - lines_above - lines_below +MergeRequestAssignee: +- id +- user_id +- merge_request_id diff --git a/spec/lib/gitlab/issuable_metadata_spec.rb b/spec/lib/gitlab/issuable_metadata_spec.rb index 6ec86163233..916f3876a8e 100644 --- a/spec/lib/gitlab/issuable_metadata_spec.rb +++ b/spec/lib/gitlab/issuable_metadata_spec.rb @@ -19,7 +19,7 @@ describe Gitlab::IssuableMetadata do let!(:closed_issue) { create(:issue, state: :closed, author: user, project: project) } let!(:downvote) { create(:award_emoji, :downvote, awardable: closed_issue) } let!(:upvote) { create(:award_emoji, :upvote, awardable: issue) } - let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test") } + let!(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, title: "Test") } let!(:closing_issues) { create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request) } it 'aggregates stats on issues' do @@ -39,7 +39,7 @@ describe Gitlab::IssuableMetadata do end context 'merge requests' do - let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test") } + let!(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, title: "Test") } let!(:merge_request_closed) { create(:merge_request, state: "closed", source_project: project, target_project: project, title: "Closed Test") } let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request) } let!(:upvote) { create(:award_emoji, :upvote, awardable: merge_request) } diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 5fa1369c00a..fee1d701e3a 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -19,7 +19,7 @@ describe Notify do create(:merge_request, source_project: project, target_project: project, author: current_user, - assignee: assignee, + assignees: [assignee], description: 'Awesome description') end @@ -275,7 +275,7 @@ describe Notify do context 'for merge requests' do describe 'that are new' do - subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) } + subject { described_class.new_merge_request_email(merge_request.assignee_ids.first, merge_request.id) } it_behaves_like 'an assignee email' it_behaves_like 'an email starting a new thread with reply-by-email enabled' do @@ -300,7 +300,7 @@ describe Notify do end context 'when sent with a reason' do - subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id, NotificationReason::ASSIGNED) } + subject { described_class.new_merge_request_email(merge_request.assignee_ids.first, merge_request.id, NotificationReason::ASSIGNED) } it_behaves_like 'appearance header and footer enabled' it_behaves_like 'appearance header and footer not enabled' @@ -324,7 +324,7 @@ describe Notify do describe 'that are reassigned' do let(:previous_assignee) { create(:user, name: 'Previous Assignee') } - subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) } + subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id) } it_behaves_like 'a multiple recipients email' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do @@ -351,7 +351,7 @@ describe Notify do end context 'when sent with a reason' do - subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, NotificationReason::ASSIGNED) } + subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, NotificationReason::ASSIGNED) } it_behaves_like 'appearance header and footer enabled' it_behaves_like 'appearance header and footer not enabled' @@ -364,11 +364,11 @@ describe Notify do text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(NotificationReason::ASSIGNED) is_expected.to have_body_text(text) - new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, NotificationReason::MENTIONED) + new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, NotificationReason::MENTIONED) text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(NotificationReason::MENTIONED) expect(new_subject).to have_body_text(text) - new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, nil) + new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, nil) text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(nil) expect(new_subject).to have_body_text(text) end @@ -376,7 +376,7 @@ describe Notify do end describe 'that are new with a description' do - subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) } + subject { described_class.new_merge_request_email(merge_request.assignee_ids.first, merge_request.id) } it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like "an unsubscribeable thread" @@ -476,7 +476,7 @@ describe Notify do source_project: project, target_project: project, author: current_user, - assignee: assignee, + assignees: [assignee], description: 'Awesome description') end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 83b0f172f03..f3e78630c1b 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -684,12 +684,12 @@ describe Ci::Pipeline, :mailer do source_branch: 'feature', target_project: project, target_branch: 'master', - assignee: assignee, + assignees: assignees, milestone: milestone, labels: labels) end - let(:assignee) { create(:user) } + let(:assignees) { create_list(:user, 2) } let(:milestone) { create(:milestone, project: project) } let(:labels) { create_list(:label, 2) } @@ -710,7 +710,7 @@ describe Ci::Pipeline, :mailer do 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s, 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => pipeline.source_sha.to_s, 'CI_MERGE_REQUEST_TITLE' => merge_request.title, - 'CI_MERGE_REQUEST_ASSIGNEES' => assignee.username, + 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list, 'CI_MERGE_REQUEST_MILESTONE' => milestone.title, 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).join(',')) end @@ -730,7 +730,7 @@ describe Ci::Pipeline, :mailer do end context 'without assignee' do - let(:assignee) { nil } + let(:assignees) { [] } it 'does not expose assignee variable' do expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_ASSIGNEES') diff --git a/spec/models/concerns/deprecated_assignee_spec.rb b/spec/models/concerns/deprecated_assignee_spec.rb new file mode 100644 index 00000000000..e394de0aa34 --- /dev/null +++ b/spec/models/concerns/deprecated_assignee_spec.rb @@ -0,0 +1,160 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe DeprecatedAssignee do + let(:user) { create(:user) } + + describe '#assignee_id=' do + it 'creates the merge_request_assignees relation' do + merge_request = create(:merge_request, assignee_id: user.id) + + merge_request.reload + + expect(merge_request.merge_request_assignees.count).to eq(1) + end + + it 'nullifies the assignee_id column' do + merge_request = create(:merge_request, assignee_id: user.id) + + merge_request.reload + + expect(merge_request.read_attribute(:assignee_id)).to be_nil + end + + context 'when relation already exists' do + it 'overwrites existing assignees' do + other_user = create(:user) + merge_request = create(:merge_request, assignee_id: nil) + merge_request.merge_request_assignees.create!(user_id: user.id) + merge_request.merge_request_assignees.create!(user_id: other_user.id) + + expect { merge_request.update!(assignee_id: other_user.id) } + .to change { merge_request.reload.merge_request_assignees.count } + .from(2).to(1) + end + end + end + + describe '#assignee=' do + it 'creates the merge_request_assignees relation' do + merge_request = create(:merge_request, assignee: user) + + merge_request.reload + + expect(merge_request.merge_request_assignees.count).to eq(1) + end + + it 'nullifies the assignee_id column' do + merge_request = create(:merge_request, assignee: user) + + merge_request.reload + + expect(merge_request.read_attribute(:assignee_id)).to be_nil + end + + context 'when relation already exists' do + it 'overwrites existing assignees' do + other_user = create(:user) + merge_request = create(:merge_request, assignee: nil) + merge_request.merge_request_assignees.create!(user_id: user.id) + merge_request.merge_request_assignees.create!(user_id: other_user.id) + + expect { merge_request.update!(assignee: other_user) } + .to change { merge_request.reload.merge_request_assignees.count } + .from(2).to(1) + end + end + end + + describe '#assignee_id' do + it 'returns the first assignee ID' do + other_user = create(:user) + merge_request = create(:merge_request, assignees: [user, other_user]) + + merge_request.reload + + expect(merge_request.assignee_id).to eq(merge_request.assignee_ids.first) + end + end + + describe '#assignees' do + context 'when assignee_id exists and there is no relation' do + it 'creates the relation' do + merge_request = create(:merge_request, assignee_id: nil) + merge_request.update_column(:assignee_id, user.id) + + expect { merge_request.assignees }.to change { merge_request.merge_request_assignees.count }.from(0).to(1) + end + + it 'nullifies the assignee_id' do + merge_request = create(:merge_request, assignee_id: nil) + merge_request.update_column(:assignee_id, user.id) + + expect { merge_request.assignees } + .to change { merge_request.read_attribute(:assignee_id) } + .from(user.id).to(nil) + end + end + + context 'when DB is read-only' do + before do + allow(Gitlab::Database).to receive(:read_only?) { true } + end + + it 'returns a users relation' do + merge_request = create(:merge_request, assignee_id: user.id) + + expect(merge_request.assignees).to be_a(ActiveRecord::Relation) + expect(merge_request.assignees).to eq([user]) + end + + it 'returns an empty relation if no assignee_id is set' do + merge_request = create(:merge_request, assignee_id: nil) + + expect(merge_request.assignees).to be_a(ActiveRecord::Relation) + expect(merge_request.assignees).to eq([]) + end + end + end + + describe '#assignee_ids' do + context 'when assignee_id exists and there is no relation' do + it 'creates the relation' do + merge_request = create(:merge_request, assignee_id: nil) + merge_request.update_column(:assignee_id, user.id) + + expect { merge_request.assignee_ids }.to change { merge_request.merge_request_assignees.count }.from(0).to(1) + end + + it 'nullifies the assignee_id' do + merge_request = create(:merge_request, assignee_id: nil) + merge_request.update_column(:assignee_id, user.id) + + expect { merge_request.assignee_ids } + .to change { merge_request.read_attribute(:assignee_id) } + .from(user.id).to(nil) + end + end + + context 'when DB is read-only' do + before do + allow(Gitlab::Database).to receive(:read_only?) { true } + end + + it 'returns a list of user IDs' do + merge_request = create(:merge_request, assignee_id: user.id) + + expect(merge_request.assignee_ids).to be_a(Array) + expect(merge_request.assignee_ids).to eq([user.id]) + end + + it 'returns an empty relation if no assignee_id is set' do + merge_request = create(:merge_request, assignee_id: nil) + + expect(merge_request.assignee_ids).to be_a(Array) + expect(merge_request.assignee_ids).to eq([]) + end + end + end +end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 27ed298ae08..64f02978d79 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -502,8 +502,8 @@ describe Issuable do let(:user2) { create(:user) } before do - merge_request.update(assignee: user) - merge_request.update(assignee: user2) + merge_request.update(assignees: [user]) + merge_request.update(assignees: [user, user2]) expect(Gitlab::HookData::IssuableBuilder) .to receive(:new).with(merge_request).and_return(builder) end @@ -512,8 +512,7 @@ describe Issuable do expect(builder).to receive(:build).with( user: user, changes: hash_including( - 'assignee_id' => [user.id, user2.id], - 'assignee' => [user.hook_attrs, user2.hook_attrs] + 'assignees' => [[user.hook_attrs], [user.hook_attrs, user2.hook_attrs]] )) merge_request.to_hook_data(user, old_associations: { assignees: [user] }) diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index d192fe70506..e91b5c4c86f 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -263,7 +263,7 @@ describe Event do context 'merge request diff note event' do let(:project) { create(:project, :public) } - let(:merge_request) { create(:merge_request, source_project: project, author: author, assignee: assignee) } + let(:merge_request) { create(:merge_request, source_project: project, author: author, assignees: [assignee]) } let(:note_on_merge_request) { create(:legacy_diff_note_on_merge_request, noteable: merge_request, project: project) } let(:target) { note_on_merge_request } diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 6f34ef9c1bc..f61857ea5ff 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -13,7 +13,7 @@ describe MergeRequest do it { is_expected.to belong_to(:target_project).class_name('Project') } it { is_expected.to belong_to(:source_project).class_name('Project') } it { is_expected.to belong_to(:merge_user).class_name("User") } - it { is_expected.to belong_to(:assignee) } + it { is_expected.to have_many(:assignees).through(:merge_request_assignees) } it { is_expected.to have_many(:merge_request_diffs) } context 'for forks' do @@ -181,31 +181,6 @@ describe MergeRequest do expect(MergeRequest::Metrics.count).to eq(1) end end - - describe '#refresh_merge_request_assignees' do - set(:user) { create(:user) } - - it 'creates merge request assignees relation upon MR creation' do - merge_request = create(:merge_request, assignee: nil) - - expect(merge_request.merge_request_assignees).to be_empty - - expect { merge_request.update!(assignee: user) } - .to change { merge_request.reload.merge_request_assignees.count } - .from(0).to(1) - end - - it 'updates merge request assignees relation upon MR assignee change' do - another_user = create(:user) - merge_request = create(:merge_request, assignee: user) - - expect { merge_request.update!(assignee: another_user) } - .to change { merge_request.reload.merge_request_assignees.first.assignee } - .from(user).to(another_user) - - expect(merge_request.merge_request_assignees.count).to eq(1) - end - end end describe 'respond to' do @@ -337,34 +312,18 @@ describe MergeRequest do describe '#card_attributes' do it 'includes the author name' do allow(subject).to receive(:author).and_return(double(name: 'Robert')) - allow(subject).to receive(:assignee).and_return(nil) + allow(subject).to receive(:assignees).and_return([]) expect(subject.card_attributes) - .to eq({ 'Author' => 'Robert', 'Assignee' => nil }) + .to eq({ 'Author' => 'Robert', 'Assignee' => "" }) end - it 'includes the assignee name' do + it 'includes the assignees name' do allow(subject).to receive(:author).and_return(double(name: 'Robert')) - allow(subject).to receive(:assignee).and_return(double(name: 'Douwe')) + allow(subject).to receive(:assignees).and_return([double(name: 'Douwe'), double(name: 'Robert')]) expect(subject.card_attributes) - .to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' }) - end - end - - describe '#assignee_ids' do - it 'returns an array of the assigned user id' do - subject.assignee_id = 123 - - expect(subject.assignee_ids).to eq([123]) - end - end - - describe '#assignee_ids=' do - it 'sets assignee_id to the last id in the array' do - subject.assignee_ids = [123, 456] - - expect(subject.assignee_id).to eq(456) + .to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe and Robert' }) end end @@ -372,7 +331,7 @@ describe MergeRequest do let(:user) { create(:user) } it 'returns true for a user that is assigned to a merge request' do - subject.assignee = user + subject.assignees = [user] expect(subject.assignee_or_author?(user)).to eq(true) end @@ -1949,15 +1908,14 @@ describe MergeRequest do it 'updates when assignees change' do user1 = create(:user) user2 = create(:user) - mr = create(:merge_request, assignee: user1) + mr = create(:merge_request, assignees: [user1]) mr.project.add_developer(user1) mr.project.add_developer(user2) expect(user1.assigned_open_merge_requests_count).to eq(1) expect(user2.assigned_open_merge_requests_count).to eq(0) - mr.assignee = user2 - mr.save + mr.assignees = [user2] expect(user1.assigned_open_merge_requests_count).to eq(0) expect(user2.assigned_open_merge_requests_count).to eq(1) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a45a2737b13..d1338e34bb8 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2816,9 +2816,9 @@ describe User do project = create(:project, :public) archived_project = create(:project, :public, :archived) - create(:merge_request, source_project: project, author: user, assignee: user) - create(:merge_request, :closed, source_project: project, author: user, assignee: user) - create(:merge_request, source_project: archived_project, author: user, assignee: user) + create(:merge_request, source_project: project, author: user, assignees: [user]) + create(:merge_request, :closed, source_project: project, author: user, assignees: [user]) + create(:merge_request, source_project: archived_project, author: user, assignees: [user]) expect(user.assigned_open_merge_requests_count(force: true)).to eq 1 end diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb index 0ac23505de7..065b16c6221 100644 --- a/spec/requests/api/events_spec.rb +++ b/spec/requests/api/events_spec.rb @@ -270,8 +270,8 @@ describe API::Events do end context 'when exists some events' do - let(:merge_request1) { create(:merge_request, :closed, author: user, assignee: user, source_project: private_project, title: 'Test') } - let(:merge_request2) { create(:merge_request, :closed, author: user, assignee: user, source_project: private_project, title: 'Test') } + let(:merge_request1) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') } + let(:merge_request2) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') } before do create_event(merge_request1) diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 7ffa365c651..45818edbf68 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -5,14 +5,15 @@ describe API::MergeRequests do let(:base_time) { Time.now } set(:user) { create(:user) } + set(:user2) { create(:user) } set(:admin) { create(:user, :admin) } let(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) } let(:milestone) { create(:milestone, title: '1.0.0', project: project) } let(:milestone1) { create(:milestone, title: '0.9', project: project) } - let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) } - let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) } - let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') } - let!(:merge_request_locked) { create(:merge_request, state: "locked", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Locked test", created_at: base_time + 1.second) } + let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Test", created_at: base_time) } + let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) } + let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignees: [user], source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') } + let!(:merge_request_locked) { create(:merge_request, state: "locked", milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Locked test", created_at: base_time + 1.second) } let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } let(:label) { create(:label, title: 'label', color: '#FFAABB', project: project) } @@ -20,6 +21,9 @@ describe API::MergeRequests do before do project.add_reporter(user) + project.add_reporter(user2) + + stub_licensed_features(multiple_merge_request_assignees: false) end shared_context 'with labels' do @@ -45,9 +49,9 @@ describe API::MergeRequests do get api(endpoint_path, user) end - create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time) + create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: 'Test', created_at: base_time) - merge_request = create(:merge_request, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time) + merge_request = create(:merge_request, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: 'Test', created_at: base_time) merge_request.metrics.update!(merged_by: user, latest_closed_by: user, @@ -333,7 +337,7 @@ describe API::MergeRequests do state: 'closed', milestone: milestone1, author: user, - assignee: user, + assignees: [user], source_project: project, target_project: project, title: "Test", @@ -451,7 +455,7 @@ describe API::MergeRequests do context 'when authenticated' do let!(:project2) { create(:project, :public, namespace: user.namespace) } - let!(:merge_request2) { create(:merge_request, :simple, author: user, assignee: user, source_project: project2, target_project: project2) } + let!(:merge_request2) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project2, target_project: project2) } let(:user2) { create(:user) } it 'returns an array of all merge requests except unauthorized ones' do @@ -494,7 +498,7 @@ describe API::MergeRequests do end it 'returns an array of merge requests created by current user if no scope is given' do - merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch') + merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch') get api('/merge_requests', user2) @@ -502,7 +506,7 @@ describe API::MergeRequests do end it 'returns an array of merge requests authored by the given user' do - merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch') + merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch') get api('/merge_requests', user), params: { author_id: user2.id, scope: :all } @@ -510,7 +514,7 @@ describe API::MergeRequests do end it 'returns an array of merge requests assigned to the given user' do - merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch') + merge_request3 = create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, source_branch: 'other-branch') get api('/merge_requests', user), params: { assignee_id: user2.id, scope: :all } @@ -535,7 +539,7 @@ describe API::MergeRequests do end it 'returns an array of merge requests assigned to me' do - merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch') + merge_request3 = create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, source_branch: 'other-branch') get api('/merge_requests', user2), params: { scope: 'assigned_to_me' } @@ -543,7 +547,7 @@ describe API::MergeRequests do end it 'returns an array of merge requests assigned to me (kebab-case)' do - merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch') + merge_request3 = create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, source_branch: 'other-branch') get api('/merge_requests', user2), params: { scope: 'assigned-to-me' } @@ -551,7 +555,7 @@ describe API::MergeRequests do end it 'returns an array of merge requests created by me' do - merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch') + merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch') get api('/merge_requests', user2), params: { scope: 'created_by_me' } @@ -559,7 +563,7 @@ describe API::MergeRequests do end it 'returns an array of merge requests created by me (kebab-case)' do - merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch') + merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch') get api('/merge_requests', user2), params: { scope: 'created-by-me' } @@ -567,7 +571,7 @@ describe API::MergeRequests do end it 'returns merge requests reacted by the authenticated user by the given emoji' do - merge_request3 = create(:merge_request, :simple, author: user, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch') + merge_request3 = create(:merge_request, :simple, author: user, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch') award_emoji = create(:award_emoji, awardable: merge_request3, user: user2, name: 'star') get api('/merge_requests', user2), params: { my_reaction_emoji: award_emoji.name, scope: 'all' } @@ -700,7 +704,7 @@ describe API::MergeRequests do get api("/projects/#{project.id}/merge_requests", user) end.count - create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, created_at: base_time) + create(:merge_request, author: user, assignees: [user], source_project: project, target_project: project, created_at: base_time) expect do get api("/projects/#{project.id}/merge_requests", user) @@ -730,7 +734,7 @@ describe API::MergeRequests do describe "GET /projects/:id/merge_requests/:merge_request_iid" do it 'matches json schema' do - merge_request = create(:merge_request, :with_test_reports, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) + merge_request = create(:merge_request, :with_test_reports, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Test", created_at: base_time) get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) expect(response).to have_gitlab_http_status(200) @@ -851,7 +855,7 @@ describe API::MergeRequests do end context 'Work in Progress' do - let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) } + let!(:merge_request_wip) { create(:merge_request, author: user, assignees: [user], source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) } it "returns merge request" do get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.iid}", user) @@ -867,7 +871,7 @@ describe API::MergeRequests do merge_request_overflow = create(:merge_request, :simple, author: user, - assignee: user, + assignees: [user], source_project: project, source_branch: 'expand-collapse-files', target_project: project, @@ -1005,6 +1009,71 @@ describe API::MergeRequests do end describe 'POST /projects/:id/merge_requests' do + context 'support for deprecated assignee_id' do + let(:params) do + { + title: 'Test merge request', + source_branch: 'feature_conflict', + target_branch: 'master', + author_id: user.id, + assignee_id: user2.id + } + end + + it 'creates a new merge request' do + post api("/projects/#{project.id}/merge_requests", user), params: params + + expect(response).to have_gitlab_http_status(201) + expect(json_response['title']).to eq('Test merge request') + expect(json_response['assignee']['name']).to eq(user2.name) + expect(json_response['assignees'].first['name']).to eq(user2.name) + end + + it 'creates a new merge request when assignee_id is empty' do + params[:assignee_id] = '' + + post api("/projects/#{project.id}/merge_requests", user), params: params + + expect(response).to have_gitlab_http_status(201) + expect(json_response['title']).to eq('Test merge request') + expect(json_response['assignee']).to be_nil + end + + it 'filters assignee_id of unauthorized user' do + private_project = create(:project, :private, :repository) + another_user = create(:user) + private_project.add_maintainer(user) + params[:assignee_id] = another_user.id + + post api("/projects/#{private_project.id}/merge_requests", user), params: params + + expect(response).to have_gitlab_http_status(201) + expect(json_response['assignee']).to be_nil + end + end + + context 'single assignee restrictions' do + let(:params) do + { + title: 'Test merge request', + source_branch: 'feature_conflict', + target_branch: 'master', + author_id: user.id, + assignee_ids: [user.id, user2.id] + } + end + + it 'creates a new project merge request with no more than one assignee' do + post api("/projects/#{project.id}/merge_requests", user), params: params + + expect(response).to have_gitlab_http_status(201) + expect(json_response['title']).to eq('Test merge request') + expect(json_response['assignees'].count).to eq(1) + expect(json_response['assignees'].first['name']).to eq(user.name) + expect(json_response.dig('assignee', 'name')).to eq(user.name) + end + end + context 'between branches projects' do context 'different labels' do let(:params) do @@ -1574,6 +1643,19 @@ describe API::MergeRequests do expect(json_response['force_remove_source_branch']).to be_truthy end + it 'filters assignee_id of unauthorized user' do + private_project = create(:project, :private, :repository) + mr = create(:merge_request, source_project: private_project, target_project: private_project) + another_user = create(:user) + private_project.add_maintainer(user) + params = { assignee_id: another_user.id } + + put api("/projects/#{private_project.id}/merge_requests/#{mr.iid}", user), params: params + + expect(response).to have_gitlab_http_status(200) + expect(json_response['assignee']).to be_nil + end + context 'when updating labels' do it 'allows special label names' do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), @@ -1728,7 +1810,7 @@ describe API::MergeRequests do issue = create(:issue, project: jira_project) description = "Closes #{ext_issue.to_reference(jira_project)}\ncloses #{issue.to_reference}" merge_request = create(:merge_request, - :simple, author: user, assignee: user, source_project: jira_project, description: description) + :simple, author: user, assignees: [user], source_project: jira_project, description: description) get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.iid}/closes_issues", user) diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index ca366cdf1df..363b7266940 100644 --- a/spec/services/issuable/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -76,14 +76,14 @@ describe Issuable::BulkUpdateService do end describe 'updating merge request assignee' do - let(:merge_request) { create(:merge_request, target_project: project, source_project: project, assignee: user) } + let(:merge_request) { create(:merge_request, target_project: project, source_project: project, assignees: [user]) } context 'when the new assignee ID is a valid user' do it 'succeeds' do new_assignee = create(:user) project.add_developer(new_assignee) - result = bulk_update(merge_request, assignee_id: new_assignee.id) + result = bulk_update(merge_request, assignee_ids: [user.id, new_assignee.id]) expect(result[:success]).to be_truthy expect(result[:count]).to eq(1) @@ -93,22 +93,22 @@ describe Issuable::BulkUpdateService do assignee = create(:user) project.add_developer(assignee) - expect { bulk_update(merge_request, assignee_id: assignee.id) } - .to change { merge_request.reload.assignee }.from(user).to(assignee) + expect { bulk_update(merge_request, assignee_ids: [assignee.id]) } + .to change { merge_request.reload.assignee_ids }.from([user.id]).to([assignee.id]) end end context "when the new assignee ID is #{IssuableFinder::NONE}" do it 'unassigns the issues' do - expect { bulk_update(merge_request, assignee_id: IssuableFinder::NONE) } - .to change { merge_request.reload.assignee }.to(nil) + expect { bulk_update(merge_request, assignee_ids: [IssuableFinder::NONE]) } + .to change { merge_request.reload.assignee_ids }.to([]) end end context 'when the new assignee ID is not present' do it 'does not unassign' do - expect { bulk_update(merge_request, assignee_id: nil) } - .not_to change { merge_request.reload.assignee } + expect { bulk_update(merge_request, assignee_ids: []) } + .not_to change { merge_request.reload.assignee_ids } end end end diff --git a/spec/services/issuable/destroy_service_spec.rb b/spec/services/issuable/destroy_service_spec.rb index 8ccbba7fa58..15d1bb73ca3 100644 --- a/spec/services/issuable/destroy_service_spec.rb +++ b/spec/services/issuable/destroy_service_spec.rb @@ -34,7 +34,7 @@ describe Issuable::DestroyService do end context 'when issuable is a merge request' do - let!(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: user, assignee: user) } + let!(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: user, assignees: [user]) } it 'destroys the merge request' do expect { service.execute(merge_request) }.to change { project.merge_requests.count }.by(-1) diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb index d37ca13ebd2..91bf4dccd77 100644 --- a/spec/services/members/destroy_service_spec.rb +++ b/spec/services/members/destroy_service_spec.rb @@ -43,9 +43,9 @@ describe Members::DestroyService do shared_examples 'a service destroying a member with access' do it_behaves_like 'a service destroying a member' - it 'invalidates cached counts for todos and assigned issues and merge requests', :aggregate_failures do + it 'invalidates cached counts for assigned issues and merge requests', :aggregate_failures do create(:issue, project: group_project, assignees: [member_user]) - create(:merge_request, source_project: group_project, assignee: member_user) + create(:merge_request, source_project: group_project, assignees: [member_user]) create(:todo, :pending, project: group_project, user: member_user) create(:todo, :done, project: group_project, user: member_user) diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index 433ffbd97f0..706bcea8199 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -4,7 +4,7 @@ describe MergeRequests::CloseService do let(:user) { create(:user) } let(:user2) { create(:user) } let(:guest) { create(:user) } - let(:merge_request) { create(:merge_request, assignee: user2, author: create(:user)) } + let(:merge_request) { create(:merge_request, assignees: [user2], author: create(:user)) } let(:project) { merge_request.project } let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) } diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb index 393299cce00..20bf1cbb8b6 100644 --- a/spec/services/merge_requests/create_from_issue_service_spec.rb +++ b/spec/services/merge_requests/create_from_issue_service_spec.rb @@ -118,7 +118,7 @@ describe MergeRequests::CreateFromIssueService do result = service.execute - expect(result[:merge_request].assignee).to eq(user) + expect(result[:merge_request].assignees).to eq([user]) end context 'when ref branch is set' do diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index dc5d1cf2f04..30271e04c8e 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -32,7 +32,7 @@ describe MergeRequests::CreateService do expect(merge_request).to be_valid expect(merge_request.work_in_progress?).to be(false) expect(merge_request.title).to eq('Awesome merge_request') - expect(merge_request.assignee).to be_nil + expect(merge_request.assignees).to be_empty expect(merge_request.merge_params['force_remove_source_branch']).to eq('1') end @@ -73,7 +73,7 @@ describe MergeRequests::CreateService do description: "well this is not done yet\n/wip", source_branch: 'feature', target_branch: 'master', - assignee: assignee + assignees: [assignee] } end @@ -89,7 +89,7 @@ describe MergeRequests::CreateService do description: "well this is not done yet\n/wip", source_branch: 'feature', target_branch: 'master', - assignee: assignee + assignees: [assignee] } end @@ -106,11 +106,11 @@ describe MergeRequests::CreateService do description: 'please fix', source_branch: 'feature', target_branch: 'master', - assignee: assignee + assignees: [assignee] } end - it { expect(merge_request.assignee).to eq assignee } + it { expect(merge_request.assignees).to eq([assignee]) } it 'creates a todo for new assignee' do attributes = { @@ -301,7 +301,7 @@ describe MergeRequests::CreateService do let(:opts) do { - assignee_id: create(:user).id, + assignee_ids: create(:user).id, milestone_id: 1, title: 'Title', description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}"), @@ -317,7 +317,7 @@ describe MergeRequests::CreateService do it 'assigns and sets milestone to issuable from command' do expect(merge_request).to be_persisted - expect(merge_request.assignee).to eq(assignee) + expect(merge_request.assignees).to eq([assignee]) expect(merge_request.milestone).to eq(milestone) end end @@ -332,28 +332,28 @@ describe MergeRequests::CreateService do end it 'removes assignee_id when user id is invalid' do - opts = { title: 'Title', description: 'Description', assignee_id: -1 } + opts = { title: 'Title', description: 'Description', assignee_ids: [-1] } merge_request = described_class.new(project, user, opts).execute - expect(merge_request.assignee_id).to be_nil + expect(merge_request.assignee_ids).to be_empty end it 'removes assignee_id when user id is 0' do - opts = { title: 'Title', description: 'Description', assignee_id: 0 } + opts = { title: 'Title', description: 'Description', assignee_ids: [0] } merge_request = described_class.new(project, user, opts).execute - expect(merge_request.assignee_id).to be_nil + expect(merge_request.assignee_ids).to be_empty end it 'saves assignee when user id is valid' do project.add_maintainer(assignee) - opts = { title: 'Title', description: 'Description', assignee_id: assignee.id } + opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] } merge_request = described_class.new(project, user, opts).execute - expect(merge_request.assignee).to eq(assignee) + expect(merge_request.assignees).to eq([assignee]) end context 'when assignee is set' do @@ -361,7 +361,7 @@ describe MergeRequests::CreateService do { title: 'Title', description: 'Description', - assignee_id: assignee.id, + assignee_ids: [assignee.id], source_branch: 'feature', target_branch: 'master' } @@ -387,7 +387,7 @@ describe MergeRequests::CreateService do levels.each do |level| it "removes not authorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do project.update(visibility_level: level) - opts = { title: 'Title', description: 'Description', assignee_id: assignee.id } + opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] } merge_request = described_class.new(project, user, opts).execute diff --git a/spec/services/merge_requests/ff_merge_service_spec.rb b/spec/services/merge_requests/ff_merge_service_spec.rb index 1430e12a07e..a87d8b8752c 100644 --- a/spec/services/merge_requests/ff_merge_service_spec.rb +++ b/spec/services/merge_requests/ff_merge_service_spec.rb @@ -7,7 +7,7 @@ describe MergeRequests::FfMergeService do create(:merge_request, source_branch: 'flatten-dir', target_branch: 'improve/awesome', - assignee: user2, + assignees: [user2], author: create(:user)) end let(:project) { merge_request.project } diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 887ec17171e..b0b3273e3dc 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe MergeRequests::MergeService do set(:user) { create(:user) } set(:user2) { create(:user) } - let(:merge_request) { create(:merge_request, :simple, author: user2, assignee: user2) } + let(:merge_request) { create(:merge_request, :simple, author: user2, assignees: [user2]) } let(:project) { merge_request.project } before do @@ -111,7 +111,7 @@ describe MergeRequests::MergeService do end context 'closes related todos' do - let(:merge_request) { create(:merge_request, assignee: user, author: user) } + let(:merge_request) { create(:merge_request, assignees: [user], author: user) } let(:project) { merge_request.project } let(:service) { described_class.new(project, user, commit_message: 'Awesome message') } let!(:todo) do diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb index a3b48abae26..24d09c1fd00 100644 --- a/spec/services/merge_requests/merge_to_ref_service_spec.rb +++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb @@ -149,7 +149,7 @@ describe MergeRequests::MergeToRefService do end context 'does not close related todos' do - let(:merge_request) { create(:merge_request, assignee: user, author: user) } + let(:merge_request) { create(:merge_request, assignees: [user], author: user) } let(:project) { merge_request.project } let!(:todo) do create(:todo, :assigned, diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb index 5ad6f5528f9..2cebefee5d6 100644 --- a/spec/services/merge_requests/post_merge_service_spec.rb +++ b/spec/services/merge_requests/post_merge_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe MergeRequests::PostMergeService do let(:user) { create(:user) } - let(:merge_request) { create(:merge_request, assignee: user) } + let(:merge_request) { create(:merge_request, assignees: [user]) } let(:project) { merge_request.project } before do diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb index 21e71509ed6..8b6db1ce33e 100644 --- a/spec/services/merge_requests/reopen_service_spec.rb +++ b/spec/services/merge_requests/reopen_service_spec.rb @@ -4,7 +4,7 @@ describe MergeRequests::ReopenService do let(:user) { create(:user) } let(:user2) { create(:user) } let(:guest) { create(:user) } - let(:merge_request) { create(:merge_request, :closed, assignee: user2, author: create(:user)) } + let(:merge_request) { create(:merge_request, :closed, assignees: [user2], author: create(:user)) } let(:project) { merge_request.project } before do diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 8e367db031c..0525899ebfa 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -13,7 +13,7 @@ describe MergeRequests::UpdateService, :mailer do let(:merge_request) do create(:merge_request, :simple, title: 'Old title', description: "FYI #{user2.to_reference}", - assignee_id: user3.id, + assignee_ids: [user3.id], source_project: project, author: create(:user)) end @@ -48,7 +48,7 @@ describe MergeRequests::UpdateService, :mailer do { title: 'New title', description: 'Also please fix', - assignee_id: user2.id, + assignee_ids: [user.id], state_event: 'close', label_ids: [label.id], target_branch: 'target', @@ -71,7 +71,7 @@ describe MergeRequests::UpdateService, :mailer do it 'matches base expectations' do expect(@merge_request).to be_valid expect(@merge_request.title).to eq('New title') - expect(@merge_request.assignee).to eq(user2) + expect(@merge_request.assignees).to match_array([user]) expect(@merge_request).to be_closed expect(@merge_request.labels.count).to eq(1) expect(@merge_request.labels.first.title).to eq(label.name) @@ -106,7 +106,7 @@ describe MergeRequests::UpdateService, :mailer do note = find_note('assigned to') expect(note).not_to be_nil - expect(note.note).to include "assigned to #{user2.to_reference}" + expect(note.note).to include "assigned to #{user.to_reference} and unassigned #{user3.to_reference}" end it 'creates a resource label event' do @@ -293,7 +293,7 @@ describe MergeRequests::UpdateService, :mailer do context 'when is reassigned' do before do - update_merge_request({ assignee: user2 }) + update_merge_request({ assignee_ids: [user2.id] }) end it 'marks previous assignee pending todos as done' do @@ -387,7 +387,7 @@ describe MergeRequests::UpdateService, :mailer do context 'when the assignee changes' do it 'updates open merge request counter for assignees when merge request is reassigned' do - update_merge_request(assignee_id: user2.id) + update_merge_request(assignee_ids: [user2.id]) expect(user3.assigned_open_merge_requests_count).to eq 0 expect(user2.assigned_open_merge_requests_count).to eq 1 @@ -541,36 +541,36 @@ describe MergeRequests::UpdateService, :mailer do end end - context 'updating asssignee_id' do + context 'updating asssignee_ids' do it 'does not update assignee when assignee_id is invalid' do - merge_request.update(assignee_id: user.id) + merge_request.update(assignee_ids: [user.id]) - update_merge_request(assignee_id: -1) + update_merge_request(assignee_ids: [-1]) - expect(merge_request.reload.assignee).to eq(user) + expect(merge_request.reload.assignees).to eq([user]) end it 'unassigns assignee when user id is 0' do - merge_request.update(assignee_id: user.id) + merge_request.update(assignee_ids: [user.id]) - update_merge_request(assignee_id: 0) + update_merge_request(assignee_ids: [0]) - expect(merge_request.assignee_id).to be_nil + expect(merge_request.assignee_ids).to be_empty end it 'saves assignee when user id is valid' do - update_merge_request(assignee_id: user.id) + update_merge_request(assignee_ids: [user.id]) - expect(merge_request.assignee_id).to eq(user.id) + expect(merge_request.assignee_ids).to eq([user.id]) end it 'does not update assignee_id when user cannot read issue' do - non_member = create(:user) - original_assignee = merge_request.assignee + non_member = create(:user) + original_assignees = merge_request.assignees - update_merge_request(assignee_id: non_member.id) + update_merge_request(assignee_ids: [non_member.id]) - expect(merge_request.assignee_id).to eq(original_assignee.id) + expect(merge_request.reload.assignees).to eq(original_assignees) end context "when issuable feature is private" do @@ -583,7 +583,7 @@ describe MergeRequests::UpdateService, :mailer do feature_visibility_attr = :"#{merge_request.model_name.plural}_access_level" project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE) - expect { update_merge_request(assignee_id: assignee) }.not_to change { merge_request.assignee } + expect { update_merge_request(assignee_ids: [assignee]) }.not_to change { merge_request.reload.assignees } end end end @@ -619,7 +619,7 @@ describe MergeRequests::UpdateService, :mailer do end it 'is allowed by a user that can push to the source and can update the merge request' do - merge_request.update!(assignee: user) + merge_request.update!(assignees: [user]) source_project.add_developer(user) update_merge_request(allow_collaboration: true, title: 'Updated title') diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 9ba4a11104a..14c73852e65 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -125,11 +125,7 @@ describe NotificationService, :mailer do shared_examples 'participating by assignee notification' do it 'emails the participant' do - if issuable.is_a?(Issue) - issuable.assignees << participant - else - issuable.update_attribute(:assignee, participant) - end + issuable.assignees << participant notification_trigger @@ -620,13 +616,13 @@ describe NotificationService, :mailer do context "merge request diff note" do let(:project) { create(:project, :repository) } let(:user) { create(:user) } - let(:merge_request) { create(:merge_request, source_project: project, assignee: user, author: create(:user)) } + let(:merge_request) { create(:merge_request, source_project: project, assignees: [user], author: create(:user)) } let(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) } before do build_team(note.project) project.add_maintainer(merge_request.author) - project.add_maintainer(merge_request.assignee) + merge_request.assignees.each { |assignee| project.add_maintainer(assignee) } end describe '#new_note' do @@ -637,7 +633,7 @@ describe NotificationService, :mailer do notification.new_note(note) expect(SentNotification.last(3).map(&:recipient).map(&:id)) - .to contain_exactly(merge_request.assignee.id, merge_request.author.id, @u_watcher.id) + .to contain_exactly(*merge_request.assignees.pluck(:id), merge_request.author.id, @u_watcher.id) expect(SentNotification.last.in_reply_to_discussion_id).to eq(note.discussion_id) end end @@ -1223,11 +1219,12 @@ describe NotificationService, :mailer do let(:group) { create(:group) } let(:project) { create(:project, :public, :repository, namespace: group) } let(:another_project) { create(:project, :public, namespace: group) } - let(:merge_request) { create :merge_request, source_project: project, assignee: create(:user), description: 'cc @participant' } + let(:assignee) { create(:user) } + let(:merge_request) { create :merge_request, source_project: project, assignees: [assignee], description: 'cc @participant' } before do project.add_maintainer(merge_request.author) - project.add_maintainer(merge_request.assignee) + merge_request.assignees.each { |assignee| project.add_maintainer(assignee) } build_team(merge_request.target_project) add_users_with_subscription(merge_request.target_project, merge_request) update_custom_notification(:new_merge_request, @u_guest_custom, resource: project) @@ -1239,7 +1236,7 @@ describe NotificationService, :mailer do it do notification.new_merge_request(merge_request, @u_disabled) - should_email(merge_request.assignee) + merge_request.assignees.each { |assignee| should_email(assignee) } should_email(@u_watcher) should_email(@watcher_and_subscriber) should_email(@u_participant_mentioned) @@ -1254,9 +1251,11 @@ describe NotificationService, :mailer do it 'adds "assigned" reason for assignee, if any' do notification.new_merge_request(merge_request, @u_disabled) - email = find_email_for(merge_request.assignee) + merge_request.assignees.each do |assignee| + email = find_email_for(assignee) - expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED) + expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED) + end end it "emails any mentioned users with the mention level" do @@ -1347,9 +1346,9 @@ describe NotificationService, :mailer do end it do - notification.reassigned_merge_request(merge_request, current_user, merge_request.author) + notification.reassigned_merge_request(merge_request, current_user, [assignee]) - should_email(merge_request.assignee) + merge_request.assignees.each { |assignee| should_email(assignee) } should_email(merge_request.author) should_email(@u_watcher) should_email(@u_participant_mentioned) @@ -1365,17 +1364,19 @@ describe NotificationService, :mailer do end it 'adds "assigned" reason for new assignee' do - notification.reassigned_merge_request(merge_request, current_user, merge_request.author) + notification.reassigned_merge_request(merge_request, current_user, [assignee]) - email = find_email_for(merge_request.assignee) + merge_request.assignees.each do |assignee| + email = find_email_for(assignee) - expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED) + expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED) + end end it_behaves_like 'participating notifications' do let(:participant) { create(:user, username: 'user-participant') } let(:issuable) { merge_request } - let(:notification_trigger) { notification.reassigned_merge_request(merge_request, current_user, merge_request.author) } + let(:notification_trigger) { notification.reassigned_merge_request(merge_request, current_user, [assignee]) } end end @@ -1388,7 +1389,7 @@ describe NotificationService, :mailer do it do notification.push_to_merge_request(merge_request, @u_disabled) - should_email(merge_request.assignee) + merge_request.assignees.each { |assignee| should_email(assignee) } should_email(@u_guest_custom) should_email(@u_custom_global) should_email(@u_participant_mentioned) @@ -1430,7 +1431,7 @@ describe NotificationService, :mailer do should_email(subscriber_1_to_group_label_2) should_email(subscriber_2_to_group_label_2) should_email(subscriber_to_label_2) - should_not_email(merge_request.assignee) + merge_request.assignees.each { |assignee| should_not_email(assignee) } should_not_email(merge_request.author) should_not_email(@u_watcher) should_not_email(@u_participant_mentioned) @@ -1499,7 +1500,7 @@ describe NotificationService, :mailer do it do notification.close_mr(merge_request, @u_disabled) - should_email(merge_request.assignee) + merge_request.assignees.each { |assignee| should_email(assignee) } should_email(@u_watcher) should_email(@u_guest_watcher) should_email(@u_guest_custom) @@ -1529,7 +1530,7 @@ describe NotificationService, :mailer do it do notification.merge_mr(merge_request, @u_disabled) - should_email(merge_request.assignee) + merge_request.assignees.each { |assignee| should_email(assignee) } should_email(@u_watcher) should_email(@u_guest_watcher) should_email(@u_guest_custom) @@ -1581,7 +1582,7 @@ describe NotificationService, :mailer do it do notification.reopen_mr(merge_request, @u_disabled) - should_email(merge_request.assignee) + merge_request.assignees.each { |assignee| should_email(assignee) } should_email(@u_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) @@ -1606,7 +1607,7 @@ describe NotificationService, :mailer do it do notification.resolve_all_discussions(merge_request, @u_disabled) - should_email(merge_request.assignee) + merge_request.assignees.each { |assignee| should_email(assignee) } should_email(@u_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) @@ -1850,8 +1851,8 @@ describe NotificationService, :mailer do let(:guest) { create(:user) } let(:developer) { create(:user) } let(:assignee) { create(:user) } - let(:merge_request) { create(:merge_request, source_project: private_project, assignee: assignee) } - let(:merge_request1) { create(:merge_request, source_project: private_project, assignee: assignee, description: "cc @#{guest.username}") } + let(:merge_request) { create(:merge_request, source_project: private_project, assignees: [assignee]) } + let(:merge_request1) { create(:merge_request, source_project: private_project, assignees: [assignee], description: "cc @#{guest.username}") } let(:note) { create(:note, noteable: merge_request, project: private_project) } before do diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index c7e5cca324f..c450f89c3cb 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -16,7 +16,9 @@ describe QuickActions::InterpretService do let(:service) { described_class.new(project, developer) } before do - stub_licensed_features(multiple_issue_assignees: false) + stub_licensed_features(multiple_issue_assignees: false, + multiple_merge_request_assignees: false) + project.add_developer(developer) end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 13d7d795703..51c5a803dbd 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -166,8 +166,8 @@ describe SystemNoteService do end end - describe '.change_issue_assignees' do - subject { described_class.change_issue_assignees(noteable, project, author, [assignee]) } + describe '.change_issuable_assignees' do + subject { described_class.change_issuable_assignees(noteable, project, author, [assignee]) } let(:assignee) { create(:user) } let(:assignee1) { create(:user) } @@ -180,7 +180,7 @@ describe SystemNoteService do def build_note(old_assignees, new_assignees) issue.assignees = new_assignees - described_class.change_issue_assignees(issue, project, author, old_assignees).note + described_class.change_issuable_assignees(issue, project, author, old_assignees).note end it_behaves_like 'a note with overridable created_at' diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 8631f3f9a33..89411b2e908 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -272,28 +272,6 @@ describe TodoService do end end - describe '#reassigned_issue' do - it 'creates a pending todo for new assignee' do - unassigned_issue.assignees << john_doe - service.reassigned_issue(unassigned_issue, author) - - should_create_todo(user: john_doe, target: unassigned_issue, action: Todo::ASSIGNED) - end - - it 'does not create a todo if unassigned' do - issue.assignees.destroy_all # rubocop: disable DestroyAll - - should_not_create_any_todo { service.reassigned_issue(issue, author) } - end - - it 'creates a todo if new assignee is the current user' do - unassigned_issue.assignees << john_doe - service.reassigned_issue(unassigned_issue, john_doe) - - should_create_todo(user: john_doe, target: unassigned_issue, author: john_doe, action: Todo::ASSIGNED) - end - end - describe '#mark_pending_todos_as_done' do it 'marks related pending todos to the target for the user as done' do first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) @@ -504,10 +482,60 @@ describe TodoService do end end + describe '#reassigned_issuable' do + shared_examples 'reassigned issuable' do + it 'creates a pending todo for new assignee' do + issuable_unassigned.assignees = [john_doe] + service.reassigned_issuable(issuable_unassigned, author) + + should_create_todo(user: john_doe, target: issuable_unassigned, action: Todo::ASSIGNED) + end + + it 'does not create a todo if unassigned' do + issuable_assigned.assignees = [] + + should_not_create_any_todo { service.reassigned_issuable(issuable_assigned, author) } + end + + it 'creates a todo if new assignee is the current user' do + issuable_assigned.assignees = [john_doe] + service.reassigned_issuable(issuable_assigned, john_doe) + + should_create_todo(user: john_doe, target: issuable_assigned, author: john_doe, action: Todo::ASSIGNED) + end + + it 'does not create a todo for guests' do + service.reassigned_issuable(issuable_assigned, author) + should_not_create_todo(user: guest, target: issuable_assigned, action: Todo::MENTIONED) + end + + it 'does not create a directly addressed todo for guests' do + service.reassigned_issuable(addressed_issuable_assigned, author) + should_not_create_todo(user: guest, target: addressed_issuable_assigned, action: Todo::DIRECTLY_ADDRESSED) + end + end + + context 'issuable is a merge request' do + it_behaves_like 'reassigned issuable' do + let(:issuable_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") } + let(:addressed_issuable_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") } + let(:issuable_unassigned) { create(:merge_request, source_project: project, author: author, assignees: []) } + end + end + + context 'issuable is an issue' do + it_behaves_like 'reassigned issuable' do + let(:issuable_assigned) { create(:issue, project: project, author: author, assignees: [john_doe], description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") } + let(:addressed_issuable_assigned) { create(:issue, project: project, author: author, assignees: [john_doe], description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") } + let(:issuable_unassigned) { create(:issue, project: project, author: author, assignees: []) } + end + end + end + describe 'Merge Requests' do - let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") } - let(:addressed_mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") } - let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignee: nil) } + let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") } + let(:addressed_mr_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") } + let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignees: []) } describe '#new_merge_request' do it 'creates a pending todo if assigned' do @@ -659,38 +687,6 @@ describe TodoService do end end - describe '#reassigned_merge_request' do - it 'creates a pending todo for new assignee' do - mr_unassigned.update_attribute(:assignee, john_doe) - service.reassigned_merge_request(mr_unassigned, author) - - should_create_todo(user: john_doe, target: mr_unassigned, action: Todo::ASSIGNED) - end - - it 'does not create a todo if unassigned' do - mr_assigned.update_attribute(:assignee, nil) - - should_not_create_any_todo { service.reassigned_merge_request(mr_assigned, author) } - end - - it 'creates a todo if new assignee is the current user' do - mr_assigned.update_attribute(:assignee, john_doe) - service.reassigned_merge_request(mr_assigned, john_doe) - - should_create_todo(user: john_doe, target: mr_assigned, author: john_doe, action: Todo::ASSIGNED) - end - - it 'does not create a todo for guests' do - service.reassigned_merge_request(mr_assigned, author) - should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED) - end - - it 'does not create a directly addressed todo for guests' do - service.reassigned_merge_request(addressed_mr_assigned, author) - should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) - end - end - describe '#merge_merge_request' do it 'marks related pending todos to the target for the user as done' do first_todo = create(:todo, :assigned, user: john_doe, project: project, target: mr_assigned, author: author) diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb index 83f1495a1c6..450e76d5f58 100644 --- a/spec/services/users/destroy_service_spec.rb +++ b/spec/services/users/destroy_service_spec.rb @@ -78,7 +78,7 @@ describe Users::DestroyService do end context "for an merge request the user was assigned to" do - let!(:merge_request) { create(:merge_request, source_project: project, assignee: user) } + let!(:merge_request) { create(:merge_request, source_project: project, assignees: [user]) } before do service.execute(user) @@ -91,7 +91,7 @@ describe Users::DestroyService do it 'migrates the merge request so that it is "Unassigned"' do migrated_merge_request = MergeRequest.find_by_id(merge_request.id) - expect(migrated_merge_request.assignee).to be_nil + expect(migrated_merge_request.assignees).to be_empty end end end diff --git a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb index 4df80b4168a..ab6687f1d07 100644 --- a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb +++ b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb @@ -46,9 +46,9 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests allow_gitaly_n_plus_1 { create(:project, group: subgroup) } end - let!(:merge_request1) { create(:merge_request, author: user, source_project: project2, target_project: project1, target_branch: 'merged-target') } - let!(:merge_request2) { create(:merge_request, :conflict, author: user, source_project: project2, target_project: project1, state: 'closed') } - let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') } + let!(:merge_request1) { create(:merge_request, assignees: [user], author: user, source_project: project2, target_project: project1, target_branch: 'merged-target') } + let!(:merge_request2) { create(:merge_request, :conflict, assignees: [user], author: user, source_project: project2, target_project: project1, state: 'closed') } + let!(:merge_request3) { create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') } let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3, title: 'WIP thing') } let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4, title: '[WIP]') } diff --git a/spec/support/shared_contexts/merge_request_create.rb b/spec/support/shared_contexts/merge_request_create.rb new file mode 100644 index 00000000000..529f481c2b6 --- /dev/null +++ b/spec/support/shared_contexts/merge_request_create.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +shared_context 'merge request create context' do + let(:user) { create(:user) } + let(:user2) { create(:user) } + let(:target_project) { create(:project, :public, :repository) } + let(:source_project) { target_project } + let!(:milestone) { create(:milestone, project: target_project) } + let!(:label) { create(:label, project: target_project) } + let!(:label2) { create(:label, project: target_project) } + + before do + source_project.add_maintainer(user) + target_project.add_maintainer(user) + target_project.add_maintainer(user2) + + sign_in(user) + visit project_new_merge_request_path(target_project, + merge_request: { + source_project_id: source_project.id, + target_project_id: target_project.id, + source_branch: 'fix', + target_branch: 'master' + }) + end +end diff --git a/spec/support/shared_contexts/merge_request_edit.rb b/spec/support/shared_contexts/merge_request_edit.rb new file mode 100644 index 00000000000..c84510ff47d --- /dev/null +++ b/spec/support/shared_contexts/merge_request_edit.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' +shared_context 'merge request edit context' do + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:milestone) { create(:milestone, project: target_project) } + let!(:label) { create(:label, project: target_project) } + let!(:label2) { create(:label, project: target_project) } + let(:target_project) { create(:project, :public, :repository) } + let(:source_project) { target_project } + let(:merge_request) do + create(:merge_request, + source_project: source_project, + target_project: target_project, + source_branch: 'fix', + target_branch: 'master') + end + + before do + source_project.add_maintainer(user) + target_project.add_maintainer(user) + target_project.add_maintainer(user2) + + sign_in(user) + visit edit_project_merge_request_path(target_project, merge_request) + end +end diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb index 7038a366144..ec1b1754cf0 100644 --- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb @@ -1,42 +1,17 @@ RSpec.shared_examples 'a creatable merge request' do include WaitForRequests - let(:user) { create(:user) } - let(:user2) { create(:user) } - let(:target_project) { create(:project, :public, :repository) } - let(:source_project) { target_project } - let!(:milestone) { create(:milestone, project: target_project) } - let!(:label) { create(:label, project: target_project) } - let!(:label2) { create(:label, project: target_project) } - - before do - source_project.add_maintainer(user) - target_project.add_maintainer(user) - target_project.add_maintainer(user2) - - sign_in(user) - visit project_new_merge_request_path( - target_project, - merge_request: { - source_project_id: source_project.id, - target_project_id: target_project.id, - source_branch: 'fix', - target_branch: 'master' - }) - end - it 'creates new merge request', :js do - click_button 'Assignee' + find('.js-assignee-search').click page.within '.dropdown-menu-user' do click_link user2.name end - expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user2.id.to_s) + expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user2.id.to_s) page.within '.js-assignee-search' do expect(page).to have_content user2.name end - click_link 'Assign to me' - expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s) + expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user.id.to_s) page.within '.js-assignee-search' do expect(page).to have_content user.name end diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb index eef0327c9a6..a6121fcc50a 100644 --- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb @@ -1,34 +1,10 @@ RSpec.shared_examples 'an editable merge request' do - let(:user) { create(:user) } - let(:user2) { create(:user) } - let!(:milestone) { create(:milestone, project: target_project) } - let!(:label) { create(:label, project: target_project) } - let!(:label2) { create(:label, project: target_project) } - let(:target_project) { create(:project, :public, :repository) } - let(:source_project) { target_project } - let(:merge_request) do - create(:merge_request, - source_project: source_project, - target_project: target_project, - source_branch: 'fix', - target_branch: 'master') - end - - before do - source_project.add_maintainer(user) - target_project.add_maintainer(user) - target_project.add_maintainer(user2) - - sign_in(user) - visit edit_project_merge_request_path(target_project, merge_request) - end - it 'updates merge request', :js do - click_button 'Assignee' + find('.js-assignee-search').click page.within '.dropdown-menu-user' do click_link user.name end - expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s) + expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user.id.to_s) page.within '.js-assignee-search' do expect(page).to have_content user.name end diff --git a/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb new file mode 100644 index 00000000000..bab7963f06f --- /dev/null +++ b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +shared_examples 'multiple assignees merge request' do |action, save_button_title| + it "#{action} a MR with multiple assignees", :js do + find('.js-assignee-search').click + page.within '.dropdown-menu-user' do + click_link user.name + click_link user2.name + end + + # Extra click needed in order to toggle the dropdown + find('.js-assignee-search').click + + expect(all('input[name="merge_request[assignee_ids][]"]', visible: false).map(&:value)) + .to match_array([user.id.to_s, user2.id.to_s]) + + page.within '.js-assignee-search' do + expect(page).to have_content "#{user2.name} + 1 more" + end + + click_button save_button_title + + page.within '.issuable-sidebar' do + page.within '.assignee' do + expect(page).to have_content '2 Assignees' + + click_link 'Edit' + + expect(page).to have_content user.name + expect(page).to have_content user2.name + end + end + + page.within '.dropdown-menu-user' do + click_link user.name + end + + page.within '.issuable-sidebar' do + page.within '.assignee' do + # Closing dropdown to persist + click_link 'Edit' + + expect(page).to have_content user2.name + end + end + end +end diff --git a/spec/support/shared_examples/finders/assignees_filter_spec.rb b/spec/support/shared_examples/finders/assignees_filter_spec.rb new file mode 100644 index 00000000000..782a2d97746 --- /dev/null +++ b/spec/support/shared_examples/finders/assignees_filter_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +shared_examples 'assignee ID filter' do + it 'returns issuables assigned to that user' do + expect(issuables).to contain_exactly(*expected_issuables) + end +end + +shared_examples 'assignee username filter' do + it 'returns issuables assigned to those users' do + expect(issuables).to contain_exactly(*expected_issuables) + end +end + +shared_examples 'no assignee filter' do + let(:params) { { assignee_id: 'None' } } + + it 'returns issuables not assigned to any assignee' do + expect(issuables).to contain_exactly(*expected_issuables) + end + + it 'returns issuables not assigned to any assignee' do + params[:assignee_id] = 0 + + expect(issuables).to contain_exactly(*expected_issuables) + end + + it 'returns issuables not assigned to any assignee' do + params[:assignee_id] = 'none' + + expect(issuables).to contain_exactly(*expected_issuables) + end +end + +shared_examples 'any assignee filter' do + context '' do + let(:params) { { assignee_id: 'Any' } } + + it 'returns issuables assigned to any assignee' do + expect(issuables).to contain_exactly(*expected_issuables) + end + + it 'returns issuables assigned to any assignee' do + params[:assignee_id] = 'any' + + expect(issuables).to contain_exactly(*expected_issuables) + end + end +end diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb index c13eab30054..529afa03f9c 100644 --- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb @@ -17,7 +17,7 @@ describe 'projects/merge_requests/edit.html.haml' do source_project: forked_project, target_project: project, author: user, - assignee: user, + assignees: [user], milestone: milestone) end @@ -40,7 +40,7 @@ describe 'projects/merge_requests/edit.html.haml' do expect(rendered).to have_field('merge_request[title]') expect(rendered).to have_field('merge_request[description]') - expect(rendered).to have_selector('#merge_request_assignee_id', visible: false) + expect(rendered).to have_selector('input[name="merge_request[label_ids][]"]', visible: false) expect(rendered).to have_selector('#merge_request_milestone_id', visible: false) expect(rendered).not_to have_selector('#merge_request_target_branch', visible: false) end @@ -52,7 +52,7 @@ describe 'projects/merge_requests/edit.html.haml' do expect(rendered).to have_field('merge_request[title]') expect(rendered).to have_field('merge_request[description]') - expect(rendered).to have_selector('#merge_request_assignee_id', visible: false) + expect(rendered).to have_selector('input[name="merge_request[label_ids][]"]', visible: false) expect(rendered).to have_selector('#merge_request_milestone_id', visible: false) expect(rendered).to have_selector('#merge_request_target_branch', visible: false) end diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb index d9bda1a3414..23cb319a202 100644 --- a/spec/views/projects/merge_requests/show.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb @@ -53,19 +53,6 @@ describe 'projects/merge_requests/show.html.haml' do expect(rendered).not_to have_css('.cannot-be-merged') end end - - context 'when assignee is not allowed to merge' do - it 'shows a warning icon' do - reporter = create(:user) - project.add_reporter(reporter) - closed_merge_request.update(assignee_id: reporter.id) - assign(:issuable_sidebar, serialize_issuable_sidebar(user, project, closed_merge_request)) - - render - - expect(rendered).to have_css('.cannot-be-merged') - end - end end context 'when the merge request is closed' do -- cgit v1.2.1 From 1883e320eafa02b332a16eec658f65c4a28def83 Mon Sep 17 00:00:00 2001 From: Luke Duncalfe Date: Fri, 5 Apr 2019 17:19:30 +1300 Subject: Use Gitlab::PushOptions for `ci.skip` push option Previously the raw push option Array was sent to Pipeline::Chain::Skip. This commit updates this class (and the chain of classes that pass the push option parameters from the API internal `post_receive` endpoint to that class) to treat push options as a Hash of options parsed by GitLab::PushOptions. The GitLab::PushOptions class takes options like this: -o ci.skip -o merge_request.create -o merge_request.target=branch and turns them into a Hash like this: { ci: { skip: true }, merge_request: { create: true, target: 'branch' } } This now how Pipeline::Chain::Skip is determining if the `ci.skip` push option was used. --- spec/lib/gitlab/push_options_spec.rb | 12 ++++++++++++ spec/requests/api/internal_spec.rb | 2 +- spec/services/ci/create_pipeline_service_spec.rb | 3 +-- 3 files changed, 14 insertions(+), 3 deletions(-) (limited to 'spec') diff --git a/spec/lib/gitlab/push_options_spec.rb b/spec/lib/gitlab/push_options_spec.rb index 68b64863c21..fc9e421bea6 100644 --- a/spec/lib/gitlab/push_options_spec.rb +++ b/spec/lib/gitlab/push_options_spec.rb @@ -39,6 +39,18 @@ describe Gitlab::PushOptions do end end + describe '#as_json' do + it 'returns all options' do + options = described_class.new(['merge_request.target=value']) + + expect(options.as_json).to include( + merge_request: { + target: 'value' + } + ) + end + end + it 'can parse multiple push options' do options = described_class.new([ 'merge_request.create', diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 3cc0cceeab1..a5a6e08e73b 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -905,7 +905,7 @@ describe API::Internal do it 'enqueues a PostReceive worker job' do expect(PostReceive).to receive(:perform_async) - .with(gl_repository, identifier, changes, push_options) + .with(gl_repository, identifier, changes, { ci: { skip: true } }) post api('/internal/post_receive'), params: valid_params end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 866d709d446..101b91e9cd8 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -418,8 +418,7 @@ describe Ci::CreatePipelineService do context 'when push options contain ci.skip' do let(:push_options) do - ['ci.skip', - 'another push option'] + { 'ci' => { 'skip' => true } } end it 'creates a pipline in the skipped state' do -- cgit v1.2.1 From 68f189ad23d7a384f40caa152d263fdf1465b30a Mon Sep 17 00:00:00 2001 From: Luke Duncalfe Date: Fri, 5 Apr 2019 13:22:58 +0000 Subject: Support merge on pipeline success w/ push options MergeRequests::PushOptionsHandlerService has been updated to allow creating and updating merge requests with the `merge_when_pipeline_succeeds` set using git push options. To create a new merge request and set it to merge when the pipeline succeeds: git push -u origin -o merge_request.create \ -o merge_request.merge_when_pipeline_succeeds To update an existing merge request and set it to merge when the pipeline succeeds: git push -u origin -o merge_request.merge_when_pipeline_succeeds Issue https://gitlab.com/gitlab-org/gitlab-ce/issues/53198 --- .../push_options_handler_service_spec.rb | 82 ++++++++++++++++++++++ 1 file changed, 82 insertions(+) (limited to 'spec') diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb index 96becfcae26..eafe7a34617 100644 --- a/spec/services/merge_requests/push_options_handler_service_spec.rb +++ b/spec/services/merge_requests/push_options_handler_service_spec.rb @@ -66,6 +66,22 @@ describe MergeRequests::PushOptionsHandlerService do end end + shared_examples_for 'a service that can set the merge request to merge when pipeline succeeds' do + subject(:last_mr) { MergeRequest.last } + + it 'sets merge_when_pipeline_succeeds' do + service.execute + + expect(last_mr.merge_when_pipeline_succeeds).to eq(true) + end + + it 'sets merge_user to the user' do + service.execute + + expect(last_mr.merge_user).to eq(user) + end + end + shared_examples_for 'a service that does not create a merge request' do it do expect { service.execute }.not_to change { MergeRequest.count } @@ -118,6 +134,72 @@ describe MergeRequests::PushOptionsHandlerService do end end + describe '`merge_when_pipeline_succeeds` push option' do + let(:push_options) { { merge_when_pipeline_succeeds: true } } + + context 'with a new branch' do + let(:changes) { new_branch_changes } + + it_behaves_like 'a service that does not create a merge request' + + it 'adds an error to the service' do + error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}" + + service.execute + + expect(service.errors).to include(error) + end + + context 'when coupled with the `create` push option' do + let(:push_options) { { create: true, merge_when_pipeline_succeeds: true } } + + it_behaves_like 'a service that can create a merge request' + it_behaves_like 'a service that can set the merge request to merge when pipeline succeeds' + end + end + + context 'with an existing branch but no open MR' do + let(:changes) { existing_branch_changes } + + it_behaves_like 'a service that does not create a merge request' + + it 'adds an error to the service' do + error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}" + + service.execute + + expect(service.errors).to include(error) + end + + context 'when coupled with the `create` push option' do + let(:push_options) { { create: true, merge_when_pipeline_succeeds: true } } + + it_behaves_like 'a service that can create a merge request' + it_behaves_like 'a service that can set the merge request to merge when pipeline succeeds' + end + end + + context 'with an existing branch that has a merge request open' do + let(:changes) { existing_branch_changes } + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)} + + it_behaves_like 'a service that does not create a merge request' + it_behaves_like 'a service that can set the merge request to merge when pipeline succeeds' + end + + context 'with a deleted branch' do + let(:changes) { deleted_branch_changes } + + it_behaves_like 'a service that does nothing' + end + + context 'with the project default branch' do + let(:changes) { default_branch_changes } + + it_behaves_like 'a service that does nothing' + end + end + describe '`target` push option' do let(:push_options) { { target: target_branch } } -- cgit v1.2.1 From 8b8b466a7f6c1e24367fcb202239ddc5ea1bbfe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Tue, 9 Apr 2019 00:08:32 +0200 Subject: Use have_gitlab_http_status in runner_spec --- spec/requests/api/runner_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 5fdc7c64030..3585a827838 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -1734,7 +1734,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do end it 'download artifacts' do - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response.headers.to_h).to include download_headers end end -- cgit v1.2.1 From 285fcb4744ed5464aab7d2ff248e7824dd77315c Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Mon, 8 Apr 2019 19:47:23 -0300 Subject: Add methods to check dead and retrying jobs It adds two methods for checking if a background job (for a given class) has dead or retrying jobs. --- spec/lib/gitlab/background_migration_spec.rb | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) (limited to 'spec') diff --git a/spec/lib/gitlab/background_migration_spec.rb b/spec/lib/gitlab/background_migration_spec.rb index 7d3d8a949ef..1d0ffb5e9df 100644 --- a/spec/lib/gitlab/background_migration_spec.rb +++ b/spec/lib/gitlab/background_migration_spec.rb @@ -195,4 +195,44 @@ describe Gitlab::BackgroundMigration do end end end + + describe '.dead_jobs?' do + let(:queue) do + [double(args: ['Foo', [10, 20]], queue: described_class.queue)] + end + + context 'when there are dead jobs present' do + before do + allow(Sidekiq::DeadSet).to receive(:new).and_return(queue) + end + + it 'returns true if specific job exists' do + expect(described_class.dead_jobs?('Foo')).to eq(true) + end + + it 'returns false if specific job does not exist' do + expect(described_class.dead_jobs?('Bar')).to eq(false) + end + end + end + + describe '.retrying_jobs?' do + let(:queue) do + [double(args: ['Foo', [10, 20]], queue: described_class.queue)] + end + + context 'when there are dead jobs present' do + before do + allow(Sidekiq::RetrySet).to receive(:new).and_return(queue) + end + + it 'returns true if specific job exists' do + expect(described_class.retrying_jobs?('Foo')).to eq(true) + end + + it 'returns false if specific job does not exist' do + expect(described_class.retrying_jobs?('Bar')).to eq(false) + end + end + end end -- cgit v1.2.1 From e73f537cb5097e85849110bafe184cb89e3bbc22 Mon Sep 17 00:00:00 2001 From: Luke Duncalfe Date: Mon, 8 Apr 2019 17:07:53 +1200 Subject: Refactor PushOptionsHandlerService from review Exceptions are no longer raised, instead all errors encountered are added to the errors property. MergeRequests::BuildService is used to generate attributes of a new merge request. Code moved from Api::Internal to Api::Helpers::InternalHelpers. --- spec/requests/api/internal_spec.rb | 26 ++++---- .../push_options_handler_service_spec.rb | 73 ++++++++++++---------- 2 files changed, 51 insertions(+), 48 deletions(-) (limited to 'spec') diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index a5a6e08e73b..26645ff0a44 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -888,8 +888,10 @@ describe API::Internal do } end + let(:branch_name) { 'feature' } + let(:changes) do - "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" + "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{branch_name}" end let(:push_options) do @@ -924,8 +926,8 @@ describe API::Internal do post api('/internal/post_receive'), params: valid_params expect(json_response['merge_request_urls']).to match [{ - "branch_name" => "new_branch", - "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch", + "branch_name" => branch_name, + "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{branch_name}", "new_merge_request" => true }] end @@ -955,26 +957,22 @@ describe API::Internal do post api('/internal/post_receive'), params: valid_params end + it 'creates a new merge request' do + expect do + post api('/internal/post_receive'), params: valid_params + end.to change { MergeRequest.count }.by(1) + end + it 'links to the newly created merge request' do post api('/internal/post_receive'), params: valid_params expect(json_response['merge_request_urls']).to match [{ - 'branch_name' => 'new_branch', + 'branch_name' => branch_name, 'url' => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/1", 'new_merge_request' => false }] end - it 'adds errors raised from MergeRequests::PushOptionsHandlerService to warnings' do - expect(MergeRequests::PushOptionsHandlerService).to receive(:new).and_raise( - MergeRequests::PushOptionsHandlerService::Error, 'my warning' - ) - - post api('/internal/post_receive'), params: valid_params - - expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my warning') - end - it 'adds errors on the service instance to warnings' do expect_any_instance_of( MergeRequests::PushOptionsHandlerService diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb index eafe7a34617..af52feef24d 100644 --- a/spec/services/merge_requests/push_options_handler_service_spec.rb +++ b/spec/services/merge_requests/push_options_handler_service_spec.rb @@ -3,10 +3,13 @@ require 'spec_helper' describe MergeRequests::PushOptionsHandlerService do + include ProjectForksHelper + let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } + let(:forked_project) { fork_project(project, user, repository: true) } let(:service) { described_class.new(project, user, changes, push_options) } - let(:source_branch) { 'test' } + let(:source_branch) { 'fix' } let(:target_branch) { 'feature' } let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } @@ -38,21 +41,25 @@ describe MergeRequests::PushOptionsHandlerService do expect(last_mr.assignee).to eq(user) end - it 'sets the title and description from the first non-merge commit' do - commits = project.repository.commits('master', limit: 5) + context 'when project has been forked' do + let(:forked_project) { fork_project(project, user, repository: true) } + let(:service) { described_class.new(forked_project, user, changes, push_options) } - expect(Gitlab::Git::Commit).to receive(:between).at_least(:once).and_return(commits) + before do + allow(forked_project).to receive(:empty_repo?).and_return(false) + end - service.execute + it 'sets the correct source project' do + service.execute - merge_commit = commits.first - non_merge_commit = commits.second + expect(last_mr.source_project).to eq(forked_project) + end - expect(merge_commit.merge_commit?).to eq(true) - expect(non_merge_commit.merge_commit?).to eq(false) + it 'sets the correct target project' do + service.execute - expect(last_mr.title).to eq(non_merge_commit.title) - expect(last_mr.description).to eq(non_merge_commit.description) + expect(last_mr.target_project).to eq(project) + end end end @@ -271,7 +278,7 @@ describe MergeRequests::PushOptionsHandlerService do let(:changes) do [ new_branch_changes, - "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/second-branch" + "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/feature_conflict" ] end @@ -287,11 +294,10 @@ describe MergeRequests::PushOptionsHandlerService do end end - it 'throws an error' do - expect { service.execute }.to raise_error( - MergeRequests::PushOptionsHandlerService::Error, - "Too many branches pushed (#{limit + 1} were pushed, limit is #{limit})" - ) + it 'records an error' do + service.execute + + expect(service.errors).to eq(["Too many branches pushed (#{limit + 1} were pushed, limit is #{limit})"]) end end end @@ -308,11 +314,10 @@ describe MergeRequests::PushOptionsHandlerService do let(:push_options) { { create: true } } let(:changes) { new_branch_changes } - it 'throws an error' do - expect { service.execute }.to raise_error( - MergeRequests::PushOptionsHandlerService::Error, - 'User is required' - ) + it 'records an error' do + service.execute + + expect(service.errors).to eq(['User is required']) end end @@ -320,10 +325,12 @@ describe MergeRequests::PushOptionsHandlerService do let(:push_options) { { create: true } } let(:changes) { new_branch_changes } - it 'throws an error' do + it 'records an error' do Members::DestroyService.new(user).execute(ProjectMember.find_by!(user_id: user.id)) - expect { service.execute }.to raise_error(Gitlab::Access::AccessDeniedError) + service.execute + + expect(service.errors).to eq(['User access was denied']) end end @@ -331,11 +338,10 @@ describe MergeRequests::PushOptionsHandlerService do let(:push_options) { { create: true, target: 'my-branch' } } let(:changes) { new_branch_changes } - it 'throws an error' do - expect { service.execute }.to raise_error( - MergeRequests::PushOptionsHandlerService::Error, - 'Branch my-branch does not exist' - ) + it 'records an error' do + service.execute + + expect(service.errors).to eq(['Branch my-branch does not exist']) end end @@ -343,13 +349,12 @@ describe MergeRequests::PushOptionsHandlerService do let(:push_options) { { create: true } } let(:changes) { new_branch_changes } - it 'throws an error' do + it 'records an error' do expect(project).to receive(:merge_requests_enabled?).and_return(false) - expect { service.execute }.to raise_error( - MergeRequests::PushOptionsHandlerService::Error, - 'Merge requests are not enabled for project' - ) + service.execute + + expect(service.errors).to eq(["Merge requests are not enabled for project #{project.full_path}"]) end end -- cgit v1.2.1 From 3c40c98e263328ceb11a008dbec108362e727dbc Mon Sep 17 00:00:00 2001 From: Luke Duncalfe Date: Mon, 8 Apr 2019 21:59:12 +1200 Subject: Feature flag for merge requestion push options https://gitlab.com/gitlab-org/gitlab-ce/issues/43263 https://gitlab.com/gitlab-org/gitlab-ce/issues/53198 --- spec/requests/api/internal_spec.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'spec') diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 26645ff0a44..1ce8f520962 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -995,6 +995,20 @@ describe API::Internal do expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my error') end + + context 'when the feature is disabled' do + it 'does not invoke MergeRequests::PushOptionsHandlerService' do + Feature.disable(:mr_push_options) + + expect(MergeRequests::PushOptionsHandlerService).to receive(:new) + + expect do + post api('/internal/post_receive'), params: valid_params + end.not_to change { MergeRequest.count } + + Feature.enable(:mr_push_options) + end + end end context 'broadcast message exists' do -- cgit v1.2.1 From b5bcf80c9a7470ac36bdbefcb8056beff67712ae Mon Sep 17 00:00:00 2001 From: Luke Duncalfe Date: Tue, 9 Apr 2019 10:55:07 +1200 Subject: Update service to handle unexpected exceptions This will ensure that now and in the future, PushOptionsHandlerService will not cause the post_receive API endpoint from running other code if something causes an unknown exception. --- .../push_options_handler_service_spec.rb | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'spec') diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb index af52feef24d..686b4b49f24 100644 --- a/spec/services/merge_requests/push_options_handler_service_spec.rb +++ b/spec/services/merge_requests/push_options_handler_service_spec.rb @@ -334,6 +334,32 @@ describe MergeRequests::PushOptionsHandlerService do end end + describe 'handling unexpected exceptions' do + let(:push_options) { { create: true } } + let(:changes) { new_branch_changes } + let(:exception) { StandardError.new('My standard error') } + + def run_service_with_exception + allow_any_instance_of( + MergeRequests::BuildService + ).to receive(:execute).and_raise(exception) + + service.execute + end + + it 'records an error' do + run_service_with_exception + + expect(service.errors).to eq(['An unknown error occurred']) + end + + it 'writes to Gitlab::AppLogger' do + expect(Gitlab::AppLogger).to receive(:error).with(exception) + + run_service_with_exception + end + end + describe 'when target is not a valid branch name' do let(:push_options) { { create: true, target: 'my-branch' } } let(:changes) { new_branch_changes } -- cgit v1.2.1 From 5b7003282b6b3ce1bfc313b3271bd6827a230c34 Mon Sep 17 00:00:00 2001 From: Jason Goodman Date: Tue, 9 Apr 2019 06:52:15 +0000 Subject: Set release name when adding release notes to an existing tag Also set the release sha and author --- .../projects/tags/releases_controller_spec.rb | 61 +++++++++++++++++++--- .../legacy_github_import/release_formatter_spec.rb | 1 + spec/models/release_spec.rb | 16 ++++++ spec/services/releases/create_service_spec.rb | 10 ++++ 4 files changed, 80 insertions(+), 8 deletions(-) (limited to 'spec') diff --git a/spec/controllers/projects/tags/releases_controller_spec.rb b/spec/controllers/projects/tags/releases_controller_spec.rb index 29f206c574b..66eff4844c2 100644 --- a/spec/controllers/projects/tags/releases_controller_spec.rb +++ b/spec/controllers/projects/tags/releases_controller_spec.rb @@ -18,40 +18,85 @@ describe Projects::Tags::ReleasesController do tag_id = release.tag project.releases.destroy_all # rubocop: disable DestroyAll - get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: tag_id } + response = get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: tag_id } release = assigns(:release) expect(release).not_to be_nil expect(release).not_to be_persisted + expect(response).to have_http_status(:ok) end it 'retrieves an existing release' do - get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: release.tag } + response = get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: release.tag } release = assigns(:release) expect(release).not_to be_nil expect(release).to be_persisted + expect(response).to have_http_status(:ok) end end describe 'PUT #update' do it 'updates release note description' do - update_release('description updated') + response = update_release(release.tag, "description updated") - release = project.releases.find_by_tag(tag) + release = project.releases.find_by(tag: tag) expect(release.description).to eq("description updated") + expect(response).to have_http_status(:found) end - it 'deletes release note when description is null' do - expect { update_release('') }.to change(project.releases, :count).by(-1) + it 'creates a release if one does not exist' do + tag_without_release = create_new_tag + + expect do + update_release(tag_without_release.name, "a new release") + end.to change { project.releases.count }.by(1) + + expect(response).to have_http_status(:found) + end + + it 'sets the release name, sha, and author for a new release' do + tag_without_release = create_new_tag + + response = update_release(tag_without_release.name, "a new release") + + release = project.releases.find_by(tag: tag_without_release.name) + expect(release.name).to eq(tag_without_release.name) + expect(release.sha).to eq(tag_without_release.target_commit.sha) + expect(release.author.id).to eq(user.id) + expect(response).to have_http_status(:found) + end + + it 'deletes release when description is empty' do + initial_releases_count = project.releases.count + + response = update_release(release.tag, "") + + expect(initial_releases_count).to eq(1) + expect(project.releases.count).to eq(0) + expect(response).to have_http_status(:found) + end + + it 'does nothing when description is empty and the tag does not have a release' do + tag_without_release = create_new_tag + + expect do + update_release(tag_without_release.name, "") + end.not_to change { project.releases.count } + + expect(response).to have_http_status(:found) end end - def update_release(description) + def create_new_tag + project.repository.add_tag(user, 'mytag', 'master') + end + + def update_release(tag_id, description) put :update, params: { namespace_id: project.namespace.to_param, project_id: project, - tag_id: release.tag, + tag_id: tag_id, release: { description: description } } end diff --git a/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb index 082e3b36dd0..c57b96fb00d 100644 --- a/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb @@ -25,6 +25,7 @@ describe Gitlab::LegacyGithubImport::ReleaseFormatter do expected = { project: project, tag: 'v1.0.0', + name: 'First release', description: 'Release v1.0.0', created_at: created_at, updated_at: created_at diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb index b4b32c95dee..0b19a4f8efc 100644 --- a/spec/models/release_spec.rb +++ b/spec/models/release_spec.rb @@ -18,6 +18,22 @@ RSpec.describe Release do describe 'validation' do it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:description) } + it { is_expected.to validate_presence_of(:name) } + + context 'when a release exists in the database without a name' do + it 'does not require name' do + existing_release_without_name = build(:release, project: project, author: user, name: nil) + existing_release_without_name.save(validate: false) + + existing_release_without_name.description = "change" + existing_release_without_name.save + existing_release_without_name.reload + + expect(existing_release_without_name).to be_valid + expect(existing_release_without_name.description).to eq("change") + expect(existing_release_without_name.name).to be_nil + end + end end describe '#assets_count' do diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb index 612e9f152e7..0efe37f1167 100644 --- a/spec/services/releases/create_service_spec.rb +++ b/spec/services/releases/create_service_spec.rb @@ -19,6 +19,8 @@ describe Releases::CreateService do shared_examples 'a successful release creation' do it 'creates a new release' do result = service.execute + + expect(project.releases.count).to eq(1) expect(result[:status]).to eq(:success) expect(result[:tag]).not_to be_nil expect(result[:release]).not_to be_nil @@ -69,4 +71,12 @@ describe Releases::CreateService do end end end + + describe '#find_or_build_release' do + it 'does not save the built release' do + service.find_or_build_release + + expect(project.releases.count).to eq(0) + end + end end -- cgit v1.2.1 From b28ba16a1eeb3f6437f282eafc15ea963c82bdf3 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 8 Apr 2019 19:19:54 +0200 Subject: Enable ESLint rule jest/no-jasmine-globals --- spec/frontend/.eslintrc.yml | 1 + 1 file changed, 1 insertion(+) (limited to 'spec') diff --git a/spec/frontend/.eslintrc.yml b/spec/frontend/.eslintrc.yml index 30b559c2363..63004b96ad6 100644 --- a/spec/frontend/.eslintrc.yml +++ b/spec/frontend/.eslintrc.yml @@ -14,3 +14,4 @@ globals: setFixtures: false rules: jest/no-focused-tests: error + jest/no-jasmine-globals: error -- cgit v1.2.1 From cb892da622820505637bafc39c0c0688300a6dc7 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 8 Apr 2019 19:20:40 +0200 Subject: Run yarn eslint --fix --- .../services/recent_searches_service_error_spec.js | 2 +- spec/frontend/ide/lib/editor_options_spec.js | 2 +- .../frontend/pages/admin/abuse_reports/abuse_reports_spec.js | 4 ++-- spec/frontend/pages/profiles/show/emoji_menu_spec.js | 12 ++++++------ 4 files changed, 10 insertions(+), 10 deletions(-) (limited to 'spec') diff --git a/spec/frontend/filtered_search/services/recent_searches_service_error_spec.js b/spec/frontend/filtered_search/services/recent_searches_service_error_spec.js index ea7c146fa4f..0e62bc94517 100644 --- a/spec/frontend/filtered_search/services/recent_searches_service_error_spec.js +++ b/spec/frontend/filtered_search/services/recent_searches_service_error_spec.js @@ -8,7 +8,7 @@ describe('RecentSearchesServiceError', () => { }); it('instantiates an instance of RecentSearchesServiceError and not an Error', () => { - expect(recentSearchesServiceError).toEqual(jasmine.any(RecentSearchesServiceError)); + expect(recentSearchesServiceError).toEqual(expect.any(RecentSearchesServiceError)); expect(recentSearchesServiceError.name).toBe('RecentSearchesServiceError'); }); diff --git a/spec/frontend/ide/lib/editor_options_spec.js b/spec/frontend/ide/lib/editor_options_spec.js index d149a883166..b07a583b7c8 100644 --- a/spec/frontend/ide/lib/editor_options_spec.js +++ b/spec/frontend/ide/lib/editor_options_spec.js @@ -2,7 +2,7 @@ import editorOptions from '~/ide/lib/editor_options'; describe('Multi-file editor library editor options', () => { it('returns an array', () => { - expect(editorOptions).toEqual(jasmine.any(Array)); + expect(editorOptions).toEqual(expect.any(Array)); }); it('contains readOnly option', () => { diff --git a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js index 1e0bc708c31..7e9aec84016 100644 --- a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js +++ b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js @@ -25,14 +25,14 @@ describe('Abuse Reports', () => { it('should truncate long messages', () => { const $longMessage = findMessage('LONG MESSAGE'); - expect($longMessage.data('originalMessage')).toEqual(jasmine.anything()); + expect($longMessage.data('originalMessage')).toEqual(expect.anything()); assertMaxLength($longMessage); }); it('should not truncate short messages', () => { const $shortMessage = findMessage('SHORT MESSAGE'); - expect($shortMessage.data('originalMessage')).not.toEqual(jasmine.anything()); + expect($shortMessage.data('originalMessage')).not.toEqual(expect.anything()); }); it('should allow clicking a truncated message to expand and collapse the full message', () => { diff --git a/spec/frontend/pages/profiles/show/emoji_menu_spec.js b/spec/frontend/pages/profiles/show/emoji_menu_spec.js index efc338b36eb..b8020a2035e 100644 --- a/spec/frontend/pages/profiles/show/emoji_menu_spec.js +++ b/spec/frontend/pages/profiles/show/emoji_menu_spec.js @@ -75,19 +75,19 @@ describe('EmojiMenu', () => { expect(emojiMenu.registerEventListener).toHaveBeenCalledWith( 'one', - jasmine.anything(), + expect.anything(), 'mouseenter focus', dummyToggleButtonSelector, 'mouseenter focus', - jasmine.anything(), + expect.anything(), ); expect(emojiMenu.registerEventListener).toHaveBeenCalledWith( 'on', - jasmine.anything(), + expect.anything(), 'click', dummyToggleButtonSelector, - jasmine.anything(), + expect.anything(), ); }); @@ -96,10 +96,10 @@ describe('EmojiMenu', () => { expect(emojiMenu.registerEventListener).toHaveBeenCalledWith( 'on', - jasmine.anything(), + expect.anything(), 'click', `.js-awards-block .js-emoji-btn, .${dummyMenuClass} .js-emoji-btn`, - jasmine.anything(), + expect.anything(), ); }); }); -- cgit v1.2.1 From e658b33792707a3dc190355ddc3d6c8d3ddb0cd8 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 8 Apr 2019 19:24:00 +0200 Subject: Replace jasmine.createSpy() with jest.fn() --- spec/frontend/error_tracking/components/error_tracking_list_spec.js | 2 +- spec/frontend/ide/lib/common/disposable_spec.js | 2 +- spec/frontend/pages/profiles/show/emoji_menu_spec.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'spec') diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js index 503af3920a8..67e5dc399ac 100644 --- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js +++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js @@ -31,7 +31,7 @@ describe('ErrorTrackingList', () => { actions = { getErrorList: () => {}, startPolling: () => {}, - restartPolling: jasmine.createSpy('restartPolling'), + restartPolling: jest.fn().mockName('restartPolling'), }; const state = { diff --git a/spec/frontend/ide/lib/common/disposable_spec.js b/spec/frontend/ide/lib/common/disposable_spec.js index af12ca15369..8596642eb7a 100644 --- a/spec/frontend/ide/lib/common/disposable_spec.js +++ b/spec/frontend/ide/lib/common/disposable_spec.js @@ -8,7 +8,7 @@ describe('Multi-file editor library disposable class', () => { instance = new Disposable(); disposableClass = { - dispose: jasmine.createSpy('dispose'), + dispose: jest.fn().mockName('dispose'), }; }); diff --git a/spec/frontend/pages/profiles/show/emoji_menu_spec.js b/spec/frontend/pages/profiles/show/emoji_menu_spec.js index b8020a2035e..6ac1e83829f 100644 --- a/spec/frontend/pages/profiles/show/emoji_menu_spec.js +++ b/spec/frontend/pages/profiles/show/emoji_menu_spec.js @@ -13,7 +13,7 @@ describe('EmojiMenu', () => { let dummyEmojiList; beforeEach(() => { - dummySelectEmojiCallback = jasmine.createSpy('dummySelectEmojiCallback'); + dummySelectEmojiCallback = jest.fn().mockName('dummySelectEmojiCallback'); dummyEmojiList = { glEmojiTag() { return dummyEmojiTag; -- cgit v1.2.1 From 724f19ba0a051bbe8e9dd89f208261abe0f8133a Mon Sep 17 00:00:00 2001 From: Krasimir Angelov Date: Tue, 9 Apr 2019 09:16:57 +0000 Subject: Add new API endpoint to expose single environment This is resolving https://gitlab.com/gitlab-org/gitlab-ce/issues/30157. Implement new API endpoint `/projects/:id/environments/:environment_id` to expose single environment. Include information for environment's last deployment if there is one. --- .../api/schemas/public_api/v4/artifact.json | 16 ++++++ .../api/schemas/public_api/v4/artifact_file.json | 12 +++++ .../api/schemas/public_api/v4/deployment.json | 32 +++++++++++ .../api/schemas/public_api/v4/environment.json | 23 ++++++++ spec/fixtures/api/schemas/public_api/v4/job.json | 63 ++++++++++++++++++++++ .../fixtures/api/schemas/public_api/v4/runner.json | 24 +++++++++ spec/requests/api/environments_spec.rb | 22 ++++++++ 7 files changed, 192 insertions(+) create mode 100644 spec/fixtures/api/schemas/public_api/v4/artifact.json create mode 100644 spec/fixtures/api/schemas/public_api/v4/artifact_file.json create mode 100644 spec/fixtures/api/schemas/public_api/v4/deployment.json create mode 100644 spec/fixtures/api/schemas/public_api/v4/environment.json create mode 100644 spec/fixtures/api/schemas/public_api/v4/job.json create mode 100644 spec/fixtures/api/schemas/public_api/v4/runner.json (limited to 'spec') diff --git a/spec/fixtures/api/schemas/public_api/v4/artifact.json b/spec/fixtures/api/schemas/public_api/v4/artifact.json new file mode 100644 index 00000000000..9df957b1498 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/artifact.json @@ -0,0 +1,16 @@ +{ + "type": "object", + "required": [ + "file_type", + "size", + "filename", + "file_format" + ], + "properties": { + "file_type": { "type": "string"}, + "size": { "type": "integer"}, + "filename": { "type": "string"}, + "file_format": { "type": "string"} + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/artifact_file.json b/spec/fixtures/api/schemas/public_api/v4/artifact_file.json new file mode 100644 index 00000000000..4017e6bdabc --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/artifact_file.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "required": [ + "filename", + "size" + ], + "properties": { + "filename": { "type": "string"}, + "size": { "type": "integer"} + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/deployment.json b/spec/fixtures/api/schemas/public_api/v4/deployment.json new file mode 100644 index 00000000000..3af2dc27d55 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/deployment.json @@ -0,0 +1,32 @@ +{ + "type": "object", + "required": [ + "id", + "iid", + "ref", + "sha", + "created_at", + "user", + "deployable" + ], + "properties": { + "id": { "type": "integer" }, + "iid": { "type": "integer" }, + "ref": { "type": "string" }, + "sha": { "type": "string" }, + "created_at": { "type": "string" }, + "user": { + "oneOf": [ + { "type": "null" }, + { "$ref": "user/basic.json" } + ] + }, + "deployable": { + "oneOf": [ + { "type": "null" }, + { "$ref": "job.json" } + ] + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/environment.json b/spec/fixtures/api/schemas/public_api/v4/environment.json new file mode 100644 index 00000000000..242e90fb7ac --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/environment.json @@ -0,0 +1,23 @@ +{ + "type": "object", + "required": [ + "id", + "name", + "slug", + "external_url", + "last_deployment" + ], + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "slug": { "type": "string" }, + "external_url": { "$ref": "../../types/nullable_string.json" }, + "last_deployment": { + "oneOf": [ + { "type": "null" }, + { "$ref": "deployment.json" } + ] + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/job.json b/spec/fixtures/api/schemas/public_api/v4/job.json new file mode 100644 index 00000000000..454935422a0 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/job.json @@ -0,0 +1,63 @@ +{ + "type": "object", + "required": [ + "id", + "status", + "stage", + "name", + "ref", + "tag", + "coverage", + "created_at", + "started_at", + "finished_at", + "duration", + "user", + "commit", + "pipeline", + "web_url", + "artifacts", + "artifacts_expire_at", + "runner" + ], + "properties": { + "id": { "type": "integer" }, + "status": { "type": "string" }, + "stage": { "type": "string" }, + "name": { "type": "string" }, + "ref": { "type": "string" }, + "tag": { "type": "boolean" }, + "coverage": { "type": ["number", "null"] }, + "created_at": { "type": "string" }, + "started_at": { "type": ["null", "string"] }, + "finished_at": { "type": ["null", "string"] }, + "duration": { "type": ["null", "number"] }, + "user": { "$ref": "user/basic.json" }, + "commit": { + "oneOf": [ + { "type": "null" }, + { "$ref": "commit/basic.json" } + ] + }, + "pipeline": { "$ref": "pipeline/basic.json" }, + "web_url": { "type": "string" }, + "artifacts": { + "type": "array", + "items": { "$ref": "artifact.json" } + }, + "artifacts_file": { + "oneOf": [ + { "type": "null" }, + { "$ref": "artifact_file.json" } + ] + }, + "artifacts_expire_at": { "type": ["null", "string"] }, + "runner": { + "oneOf": [ + { "type": "null" }, + { "$ref": "runner.json" } + ] + } + }, + "additionalProperties":false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/runner.json b/spec/fixtures/api/schemas/public_api/v4/runner.json new file mode 100644 index 00000000000..d97d74a93f2 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/runner.json @@ -0,0 +1,24 @@ +{ + "type": "object", + "required": [ + "id", + "description", + "ip_address", + "active", + "is_shared", + "name", + "online", + "status" + ], + "properties": { + "id": { "type": "integer" }, + "description": { "type": "string" }, + "ip_address": { "type": "string" }, + "active": { "type": "boolean" }, + "is_shared": { "type": "boolean" }, + "name": { "type": ["null", "string"] }, + "online": { "type": "boolean" }, + "status": { "type": "string" } + }, + "additionalProperties": false +} diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 493d3642255..8fc7fdc8632 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -32,6 +32,7 @@ describe API::Environments do expect(json_response.first['name']).to eq(environment.name) expect(json_response.first['external_url']).to eq(environment.external_url) expect(json_response.first['project'].keys).to contain_exactly(*project_data_keys) + expect(json_response.first).not_to have_key("last_deployment") end end @@ -188,4 +189,25 @@ describe API::Environments do end end end + + describe 'GET /projects/:id/environments/:environment_id' do + context 'as member of the project' do + it 'returns project environments' do + create(:deployment, :success, project: project, environment: environment) + + get api("/projects/#{project.id}/environments/#{environment.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/environment') + end + end + + context 'as non member' do + it 'returns a 404 status code' do + get api("/projects/#{project.id}/environments/#{environment.id}", non_member) + + expect(response).to have_gitlab_http_status(404) + end + end + end end -- cgit v1.2.1 From 0a4f44de83fac88f39d9f4507c2fa4d935703e1b Mon Sep 17 00:00:00 2001 From: Wolphin Date: Tue, 9 Apr 2019 09:28:55 +0000 Subject: Add environment url validation --- .../lib/gitlab/ci/config/entry/environment_spec.rb | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) (limited to 'spec') diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb index 3c0007f4d57..0bc9e8bd3cd 100644 --- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb @@ -100,6 +100,26 @@ describe Gitlab::Ci::Config::Entry::Environment do end end + context 'when wrong action type is used' do + let(:config) do + { name: 'production', + action: ['stop'] } + end + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + + describe '#errors' do + it 'contains error about wrong action type' do + expect(entry.errors) + .to include 'environment action should be a string' + end + end + end + context 'when invalid action is used' do let(:config) do { name: 'production', @@ -151,6 +171,26 @@ describe Gitlab::Ci::Config::Entry::Environment do end end + context 'when wrong url type is used' do + let(:config) do + { name: 'production', + url: ['https://meow.meow'] } + end + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + + describe '#errors' do + it 'contains error about wrong url type' do + expect(entry.errors) + .to include 'environment url should be a string' + end + end + end + context 'when variables are used for environment' do let(:config) do { name: 'review/$CI_COMMIT_REF_NAME', -- cgit v1.2.1 From 193e6602914a45974e5bd6e2ab12a63f91c62143 Mon Sep 17 00:00:00 2001 From: Sarah Yasonik Date: Tue, 9 Apr 2019 09:54:17 +0000 Subject: Resolve Environments#additional_metrics TypeError, ensure unix format --- .../projects/environments_controller_spec.rb | 11 ++++++++ spec/javascripts/monitoring/utils_spec.js | 29 ++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 spec/javascripts/monitoring/utils_spec.js (limited to 'spec') diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index 43639875265..168c0168bba 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -419,6 +419,17 @@ describe Projects::EnvironmentsController do expect(json_response['data']).to eq({}) expect(json_response['last_update']).to eq(42) end + + context 'when time params are provided' do + it 'returns a metrics JSON document' do + additional_metrics(start: '1554702993.5398998', end: '1554717396.996232') + + expect(response).to be_ok + expect(json_response['success']).to be(true) + expect(json_response['data']).to eq({}) + expect(json_response['last_update']).to eq(42) + end + end end context 'when only one time param is provided' do diff --git a/spec/javascripts/monitoring/utils_spec.js b/spec/javascripts/monitoring/utils_spec.js new file mode 100644 index 00000000000..e3c455d1686 --- /dev/null +++ b/spec/javascripts/monitoring/utils_spec.js @@ -0,0 +1,29 @@ +import { getTimeDiff } from '~/monitoring/utils'; +import { timeWindows } from '~/monitoring/constants'; + +describe('getTimeDiff', () => { + it('defaults to an 8 hour (28800s) difference', () => { + const params = getTimeDiff(); + + expect(params.end - params.start).toEqual(28800); + }); + + it('accepts time window as an argument', () => { + const params = getTimeDiff(timeWindows.thirtyMinutes); + + expect(params.end - params.start).not.toEqual(28800); + }); + + it('returns a value for every defined time window', () => { + const nonDefaultWindows = Object.keys(timeWindows).filter(window => window !== 'eightHours'); + + nonDefaultWindows.forEach(window => { + const params = getTimeDiff(timeWindows[window]); + const diff = params.end - params.start; + + // Ensure we're not returning the default, 28800 (the # of seconds in 8 hrs) + expect(diff).not.toEqual(28800); + expect(typeof diff).toEqual('number'); + }); + }); +}); -- cgit v1.2.1 From 0991dc8c823cdc04ec1e298651b45f7a9cf8f82a Mon Sep 17 00:00:00 2001 From: Sanad Liaquat Date: Tue, 9 Apr 2019 10:04:03 +0000 Subject: Reduce number of rspec retries In both e2e QA tests and unit tests, reduce the number of retires to 2 (i.e., 1 initial and one retry) --- spec/features/issuables/shortcuts_issuable_spec.rb | 2 +- spec/features/profiles/user_visits_profile_preferences_page_spec.rb | 2 +- spec/lib/gitlab/ci/yaml_processor_spec.rb | 2 +- spec/spec_helper.rb | 4 ++-- spec/support/shared_examples/wiki_file_attachments_examples.rb | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) (limited to 'spec') diff --git a/spec/features/issuables/shortcuts_issuable_spec.rb b/spec/features/issuables/shortcuts_issuable_spec.rb index a0ae6720a9f..a19101366a0 100644 --- a/spec/features/issuables/shortcuts_issuable_spec.rb +++ b/spec/features/issuables/shortcuts_issuable_spec.rb @@ -13,7 +13,7 @@ describe 'Blob shortcuts', :js do end shared_examples "quotes the selected text" do - it "quotes the selected text" do + it "quotes the selected text", :quarantine do select_element('.note-text') find('body').native.send_key('r') diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb index 9909bfb5904..1b3718968b9 100644 --- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb +++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb @@ -63,7 +63,7 @@ describe 'User visits the profile preferences page' do end describe 'User changes their language', :js do - it 'creates a flash message' do + it 'creates a flash message', :quarantine do select2('en', from: '#user_preferred_language') click_button 'Save' diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 8b39c4e4dd0..b7b30e60d44 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -615,7 +615,7 @@ module Gitlab subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config), opts) } context "when validating a ci config file with no project context" do - context "when a single string is provided" do + context "when a single string is provided", :quarantine do let(:include_content) { "/local.gitlab-ci.yml" } it "does not return any error" do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 60db3e1bc46..953cf1519bb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -100,8 +100,8 @@ RSpec.configure do |config| config.include PolicyHelpers, type: :policy if ENV['CI'] - # This includes the first try, i.e. tests will be run 4 times before failing. - config.default_retry_count = 4 + # This includes the first try, i.e. tests will be run 2 times before failing. + config.default_retry_count = 2 config.reporter.register_listener( RspecFlaky::Listener.new, :example_passed, diff --git a/spec/support/shared_examples/wiki_file_attachments_examples.rb b/spec/support/shared_examples/wiki_file_attachments_examples.rb index b6fb2a66b0e..22fbfb48928 100644 --- a/spec/support/shared_examples/wiki_file_attachments_examples.rb +++ b/spec/support/shared_examples/wiki_file_attachments_examples.rb @@ -42,7 +42,7 @@ shared_examples 'wiki file attachments' do end end - context 'uploading is complete' do + context 'uploading is complete', :quarantine do it 'shows "Attach a file" button on uploading complete' do attach_with_dropzone wait_for_requests -- cgit v1.2.1 From cf804218c95ce79d56bc6470135e14a505b3f9ab Mon Sep 17 00:00:00 2001 From: Daniel Wyatt Date: Tue, 9 Apr 2019 05:10:15 -0400 Subject: Add test for github project import to user namespace. --- spec/lib/gitlab/legacy_github_import/project_creator_spec.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'spec') diff --git a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb index 8c56622e0ba..3aba744458f 100644 --- a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb @@ -54,6 +54,18 @@ describe Gitlab::LegacyGithubImport::ProjectCreator do expect(project.visibility_level).to eq(namespace.visibility_level) end + + context 'when importing into a user namespace' do + subject(:service) { described_class.new(repo, repo.name, user.namespace, user, github_access_token: 'asdffg') } + + it 'sets project visibility to user namespace visibility level' do + repo.private = false + + project = service.execute + + expect(project.visibility_level).to eq(user.namespace.visibility_level) + end + end end context 'when visibility level is restricted' do -- cgit v1.2.1 From ba4c32c033135b5a2339afa6b85d0eff2ce0dfd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 9 Apr 2019 13:57:40 +0200 Subject: Quarantine flaky specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Related to: - https://gitlab.com/gitlab-org/gitlab-ce/issues/60270 - https://gitlab.com/gitlab-org/gitlab-ce/issues/60271 - https://gitlab.com/gitlab-org/gitlab-ce/issues/60272 - https://gitlab.com/gitlab-org/gitlab-ce/issues/60273 Signed-off-by: Rémy Coutable --- spec/features/issues/filtered_search/dropdown_milestone_spec.rb | 2 +- spec/features/labels_hierarchy_spec.rb | 2 +- spec/features/merge_request/user_creates_image_diff_notes_spec.rb | 2 +- spec/features/projects/wiki/user_creates_wiki_page_spec.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) (limited to 'spec') diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb index 7584339ccc0..7a6f76cb382 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -139,7 +139,7 @@ describe 'Dropdown milestone', :js do expect_filtered_search_input_empty end - it 'fills in the milestone name when the milestone is partially filled' do + it 'fills in the milestone name when the milestone is partially filled', :quarantine do filtered_search.send_keys('v') click_milestone(milestone.title) diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb index bac297de4a6..489651fea15 100644 --- a/spec/features/labels_hierarchy_spec.rb +++ b/spec/features/labels_hierarchy_spec.rb @@ -21,7 +21,7 @@ describe 'Labels Hierarchy', :js, :nested_groups do end shared_examples 'assigning labels from sidebar' do - it 'can assign all ancestors labels' do + it 'can assign all ancestors labels', :quarantine do [grandparent_group_label, parent_group_label, project_label_1].each do |label| page.within('.block.labels') do find('.edit-link').click diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb index c837a6752f9..65de0afae0c 100644 --- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb +++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb @@ -113,7 +113,7 @@ describe 'Merge request > User creates image diff notes', :js do create_image_diff_note end - it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do + it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes', :quarantine do indicator = find('.js-image-badge', match: :first) badge = find('.user-avatar-link .badge', match: :first) diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index efb7b01f5ad..bcbba6f14da 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -43,7 +43,7 @@ describe "User creates wiki page" do expect(page).to have_content("Create Page") end - it "shows non-escaped link in the pages list", :js do + it "shows non-escaped link in the pages list", :js, :quarantine do fill_in(:wiki_title, with: "one/two/three-test") page.within(".wiki-form") do -- cgit v1.2.1 From 20093f9de0b34da88a8b01ca94ee773685b16308 Mon Sep 17 00:00:00 2001 From: Agustin Henze Date: Tue, 9 Apr 2019 14:53:44 +0000 Subject: Add new permission model `read-pipeline-variable` Used to get the variables via the API endpoint `/projects/:id/pipelines/:pipeline_id/variables` Signed-off-by: Agustin Henze --- spec/policies/ci/pipeline_policy_spec.rb | 46 ++++++++++++++++++++++ spec/requests/api/pipelines_spec.rb | 66 ++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) (limited to 'spec') diff --git a/spec/policies/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb index 844d96017de..126d44d1860 100644 --- a/spec/policies/ci/pipeline_policy_spec.rb +++ b/spec/policies/ci/pipeline_policy_spec.rb @@ -100,5 +100,51 @@ describe Ci::PipelinePolicy, :models do end end end + + describe 'read_pipeline_variable' do + let(:project) { create(:project, :public) } + + context 'when user has owner access' do + let(:user) { project.owner } + + it 'is enabled' do + expect(policy).to be_allowed :read_pipeline_variable + end + end + + context 'when user is developer and the creator of the pipeline' do + let(:pipeline) { create(:ci_empty_pipeline, project: project, user: user) } + + before do + project.add_developer(user) + create(:protected_branch, :developers_can_merge, + name: pipeline.ref, project: project) + end + + it 'is enabled' do + expect(policy).to be_allowed :read_pipeline_variable + end + end + + context 'when user is developer and it is not the creator of the pipeline' do + let(:pipeline) { create(:ci_empty_pipeline, project: project, user: project.owner) } + + before do + project.add_developer(user) + create(:protected_branch, :developers_can_merge, + name: pipeline.ref, project: project) + end + + it 'is disabled' do + expect(policy).to be_disallowed :read_pipeline_variable + end + end + + context 'when user is not owner nor developer' do + it 'is disabled' do + expect(policy).not_to be_allowed :read_pipeline_variable + end + end + end end end diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index 9fed07cae82..0d46463312b 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -445,6 +445,72 @@ describe API::Pipelines do end end + describe 'GET /projects/:id/pipelines/:pipeline_id/variables' do + subject { get api("/projects/#{project.id}/pipelines/#{pipeline.id}/variables", api_user) } + + let(:api_user) { user } + + context 'user is a mantainer' do + it 'returns pipeline variables empty' do + subject + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to be_empty + end + + context 'with variables' do + let!(:variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'foo', value: 'bar') } + + it 'returns pipeline variables' do + subject + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to contain_exactly({ "key" => "foo", "value" => "bar" }) + end + end + end + + context 'user is a developer' do + let(:pipeline_owner_user) { create(:user) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, user: pipeline_owner_user) } + + before do + project.add_developer(api_user) + end + + context 'pipeline created by the developer user' do + let(:api_user) { pipeline_owner_user } + let!(:variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'foo', value: 'bar') } + + it 'returns pipeline variables' do + subject + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to contain_exactly({ "key" => "foo", "value" => "bar" }) + end + end + + context 'pipeline created is not created by the developer user' do + let(:api_user) { create(:user) } + + it 'should not return pipeline variables' do + subject + + expect(response).to have_gitlab_http_status(403) + end + end + end + + context 'user is not a project member' do + it 'should not return pipeline variables' do + get api("/projects/#{project.id}/pipelines/#{pipeline.id}/variables", non_member) + + expect(response).to have_gitlab_http_status(404) + expect(json_response['message']).to eq '404 Project Not Found' + end + end + end + describe 'DELETE /projects/:id/pipelines/:pipeline_id' do context 'authorized user' do let(:owner) { project.owner } -- cgit v1.2.1 From 326f6ce40743dc4d44293063637d35b9a2edc693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 9 Apr 2019 17:04:31 +0200 Subject: Quarantine a flaky spec in spec/features/issues/user_interacts_with_awards_spec.rb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- spec/features/issues/user_interacts_with_awards_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/features/issues/user_interacts_with_awards_spec.rb b/spec/features/issues/user_interacts_with_awards_spec.rb index afa425c2cec..eaeaa04f50d 100644 --- a/spec/features/issues/user_interacts_with_awards_spec.rb +++ b/spec/features/issues/user_interacts_with_awards_spec.rb @@ -14,7 +14,7 @@ describe 'User interacts with awards' do visit(project_issue_path(project, issue)) end - it 'toggles the thumbsup award emoji' do + it 'toggles the thumbsup award emoji', :quarantine do page.within('.awards') do thumbsup = page.first('.award-control') thumbsup.click -- cgit v1.2.1 From 9bc5ed14fe97fe63cd5be30c013c6af978715621 Mon Sep 17 00:00:00 2001 From: Imre Farkas Date: Tue, 9 Apr 2019 15:38:58 +0000 Subject: Move Contribution Analytics related spec in spec/features/groups/group_page_with_external_authorization_service_spec to EE --- .../admin/application_settings_controller_spec.rb | 22 ++++ spec/controllers/boards/issues_controller_spec.rb | 26 ++++ .../concerns/project_unauthorized_spec.rb | 51 ++++++++ .../dashboard/groups_controller_spec.rb | 50 +++++--- .../dashboard/labels_controller_spec.rb | 6 +- .../dashboard/milestones_controller_spec.rb | 6 + .../dashboard/projects_controller_spec.rb | 24 +++- .../controllers/dashboard/todos_controller_spec.rb | 6 + spec/controllers/groups/avatars_controller_spec.rb | 10 ++ spec/controllers/groups/boards_controller_spec.rb | 8 ++ .../controllers/groups/children_controller_spec.rb | 12 ++ .../groups/group_members_controller_spec.rb | 86 +++++++++++++ spec/controllers/groups/labels_controller_spec.rb | 6 + .../groups/milestones_controller_spec.rb | 6 + .../groups/settings/ci_cd_controller_spec.rb | 15 +++ .../groups/variables_controller_spec.rb | 34 +++++ spec/controllers/groups_controller_spec.rb | 96 ++++++++++++++ .../controllers/projects/boards_controller_spec.rb | 4 + .../controllers/projects/issues_controller_spec.rb | 11 ++ spec/controllers/projects_controller_spec.rb | 32 +++++ spec/controllers/search_controller_spec.rb | 39 ++++++ spec/controllers/users_controller_spec.rb | 12 ++ ...ard_with_external_authorization_service_spec.rb | 38 ++++++ ...age_with_external_authorization_service_spec.rb | 58 +++++++++ .../classification_label_on_project_pages_spec.rb | 22 ++++ spec/features/projects/forks/fork_list_spec.rb | 35 +++++ ...ues_with_external_authorization_enabled_spec.rb | 128 +++++++++++++++++++ ...external_authorization_service_settings_spec.rb | 21 +++ spec/features/users/show_spec.rb | 22 ++++ spec/finders/issues_finder_spec.rb | 7 + spec/finders/labels_finder_spec.rb | 7 + spec/finders/merge_requests_finder_spec.rb | 7 + spec/finders/snippets_finder_spec.rb | 32 +++++ spec/finders/todos_finder_spec.rb | 7 + spec/fixtures/passphrase_x509_certificate.crt | 27 ++++ spec/fixtures/passphrase_x509_certificate_pk.key | 54 ++++++++ spec/fixtures/x509_certificate.crt | 27 ++++ spec/fixtures/x509_certificate_pk.key | 51 ++++++++ .../gitlab/external_authorization/access_spec.rb | 142 +++++++++++++++++++++ .../gitlab/external_authorization/cache_spec.rb | 48 +++++++ .../gitlab/external_authorization/client_spec.rb | 97 ++++++++++++++ .../gitlab/external_authorization/logger_spec.rb | 45 +++++++ .../gitlab/external_authorization/response_spec.rb | 52 ++++++++ spec/lib/gitlab/external_authorization_spec.rb | 54 ++++++++ .../gitlab/import_export/safe_model_attributes.yml | 1 + spec/models/application_setting_spec.rb | 48 ++++++- spec/models/concerns/protected_ref_access_spec.rb | 12 ++ spec/models/issue_spec.rb | 45 +++++++ spec/models/project_spec.rb | 20 +++ spec/policies/base_policy_spec.rb | 23 ++++ spec/policies/issue_policy_spec.rb | 19 +++ spec/policies/merge_request_policy_spec.rb | 19 +++ spec/policies/project_policy_spec.rb | 53 ++++++++ spec/requests/api/projects_spec.rb | 49 +++++++ spec/requests/api/settings_spec.rb | 33 +++++ spec/serializers/group_child_entity_spec.rb | 19 +++ .../application_settings/update_service_spec.rb | 35 +++++ spec/services/notification_service_spec.rb | 41 ++++++ spec/services/projects/create_service_spec.rb | 37 ++++++ spec/services/projects/update_service_spec.rb | 41 ++++++ .../external_authorization_service_helpers.rb | 33 +++++ ...ternal_authorization_service_shared_examples.rb | 40 ++++++ .../finder_with_external_authorization_enabled.rb | 30 +++++ .../x509_certificate_credentials_validator_spec.rb | 87 +++++++++++++ 64 files changed, 2206 insertions(+), 22 deletions(-) create mode 100644 spec/controllers/concerns/project_unauthorized_spec.rb create mode 100644 spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb create mode 100644 spec/features/groups/group_page_with_external_authorization_service_spec.rb create mode 100644 spec/features/projects/classification_label_on_project_pages_spec.rb create mode 100644 spec/features/projects/forks/fork_list_spec.rb create mode 100644 spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb create mode 100644 spec/features/projects/settings/external_authorization_service_settings_spec.rb create mode 100644 spec/fixtures/passphrase_x509_certificate.crt create mode 100644 spec/fixtures/passphrase_x509_certificate_pk.key create mode 100644 spec/fixtures/x509_certificate.crt create mode 100644 spec/fixtures/x509_certificate_pk.key create mode 100644 spec/lib/gitlab/external_authorization/access_spec.rb create mode 100644 spec/lib/gitlab/external_authorization/cache_spec.rb create mode 100644 spec/lib/gitlab/external_authorization/client_spec.rb create mode 100644 spec/lib/gitlab/external_authorization/logger_spec.rb create mode 100644 spec/lib/gitlab/external_authorization/response_spec.rb create mode 100644 spec/lib/gitlab/external_authorization_spec.rb create mode 100644 spec/support/external_authorization_service_helpers.rb create mode 100644 spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb create mode 100644 spec/support/shared_examples/finders/finder_with_external_authorization_enabled.rb create mode 100644 spec/validators/x509_certificate_credentials_validator_spec.rb (limited to 'spec') diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb index 1a7be4c9a85..f3450a8289f 100644 --- a/spec/controllers/admin/application_settings_controller_spec.rb +++ b/spec/controllers/admin/application_settings_controller_spec.rb @@ -92,6 +92,28 @@ describe Admin::ApplicationSettingsController do expect(response).to redirect_to(admin_application_settings_path) expect(ApplicationSetting.current.default_project_creation).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS) end + + context 'external policy classification settings' do + let(:settings) do + { + external_authorization_service_enabled: true, + external_authorization_service_url: 'https://custom.service/', + external_authorization_service_default_label: 'default', + external_authorization_service_timeout: 3, + external_auth_client_cert: File.read('spec/fixtures/passphrase_x509_certificate.crt'), + external_auth_client_key: File.read('spec/fixtures/passphrase_x509_certificate_pk.key'), + external_auth_client_key_pass: "5iveL!fe" + } + end + + it 'updates settings when the feature is available' do + put :update, params: { application_setting: settings } + + settings.each do |attribute, value| + expect(ApplicationSetting.current.public_send(attribute)).to eq(value) + end + end + end end describe 'PUT #reset_registration_token' do diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb index 5eb05f01b8d..309cac47928 100644 --- a/spec/controllers/boards/issues_controller_spec.rb +++ b/spec/controllers/boards/issues_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Boards::IssuesController do + include ExternalAuthorizationServiceHelpers + let(:project) { create(:project, :private) } let(:board) { create(:board, project: project) } let(:user) { create(:user) } @@ -136,6 +138,30 @@ describe Boards::IssuesController do end end + context 'with external authorization' do + before do + sign_in(user) + enable_external_authorization_service_check + end + + it 'returns a 403 for group boards' do + group = create(:group) + group_board = create(:board, group: group) + + list_issues(user: user, board: group_board) + + expect(response).to have_gitlab_http_status(403) + end + + it 'is successful for project boards' do + project_board = create(:board, project: project) + + list_issues(user: user, board: project_board) + + expect(response).to have_gitlab_http_status(200) + end + end + def list_issues(user:, board:, list: nil) sign_in(user) diff --git a/spec/controllers/concerns/project_unauthorized_spec.rb b/spec/controllers/concerns/project_unauthorized_spec.rb new file mode 100644 index 00000000000..90b59b027cf --- /dev/null +++ b/spec/controllers/concerns/project_unauthorized_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe ProjectUnauthorized do + include ExternalAuthorizationServiceHelpers + let(:user) { create(:user) } + + before do + sign_in user + end + + render_views + + describe '#project_unauthorized_proc' do + controller(::Projects::ApplicationController) do + def show + head :ok + end + end + + let(:project) { create(:project) } + + before do + project.add_developer(user) + end + + it 'renders a 200 when the service allows access to the project' do + external_service_allow_access(user, project) + + get :show, params: { namespace_id: project.namespace.to_param, id: project.to_param } + + expect(response).to have_gitlab_http_status(200) + end + + it 'renders a 403 when the service denies access to the project' do + external_service_deny_access(user, project) + + get :show, params: { namespace_id: project.namespace.to_param, id: project.to_param } + + expect(response).to have_gitlab_http_status(403) + expect(response.body).to match("External authorization denied access to this project") + end + + it 'renders a 404 when the user cannot see the project at all' do + other_project = create(:project, :private) + + get :show, params: { namespace_id: other_project.namespace.to_param, id: other_project.to_param } + + expect(response).to have_gitlab_http_status(404) + end + end +end diff --git a/spec/controllers/dashboard/groups_controller_spec.rb b/spec/controllers/dashboard/groups_controller_spec.rb index c8d99f79277..775b3ca40b2 100644 --- a/spec/controllers/dashboard/groups_controller_spec.rb +++ b/spec/controllers/dashboard/groups_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Dashboard::GroupsController do + include ExternalAuthorizationServiceHelpers + let(:user) { create(:user) } before do @@ -11,33 +13,43 @@ describe Dashboard::GroupsController do expect(described_class).to include(GroupTree) end - it 'only includes projects the user is a member of' do - member_of_group = create(:group) - member_of_group.add_developer(user) - create(:group, :public) + describe '#index' do + it 'only includes projects the user is a member of' do + member_of_group = create(:group) + member_of_group.add_developer(user) + create(:group, :public) - get :index + get :index - expect(assigns(:groups)).to contain_exactly(member_of_group) - end + expect(assigns(:groups)).to contain_exactly(member_of_group) + end - context 'when rendering an expanded hierarchy with public groups you are not a member of', :nested_groups do - let!(:top_level_result) { create(:group, name: 'chef-top') } - let!(:top_level_a) { create(:group, name: 'top-a') } - let!(:sub_level_result_a) { create(:group, name: 'chef-sub-a', parent: top_level_a) } - let!(:other_group) { create(:group, name: 'other') } + context 'when rendering an expanded hierarchy with public groups you are not a member of', :nested_groups do + let!(:top_level_result) { create(:group, name: 'chef-top') } + let!(:top_level_a) { create(:group, name: 'top-a') } + let!(:sub_level_result_a) { create(:group, name: 'chef-sub-a', parent: top_level_a) } + let!(:other_group) { create(:group, name: 'other') } - before do - top_level_result.add_maintainer(user) - top_level_a.add_maintainer(user) + before do + top_level_result.add_maintainer(user) + top_level_a.add_maintainer(user) + end + + it 'renders only groups the user is a member of when searching hierarchy correctly' do + get :index, params: { filter: 'chef' }, format: :json + + expect(response).to have_gitlab_http_status(200) + all_groups = [top_level_result, top_level_a, sub_level_result_a] + expect(assigns(:groups)).to contain_exactly(*all_groups) + end end - it 'renders only groups the user is a member of when searching hierarchy correctly' do - get :index, params: { filter: 'chef' }, format: :json + it 'works when the external authorization service is enabled' do + enable_external_authorization_service_check + + get :index expect(response).to have_gitlab_http_status(200) - all_groups = [top_level_result, top_level_a, sub_level_result_a] - expect(assigns(:groups)).to contain_exactly(*all_groups) end end end diff --git a/spec/controllers/dashboard/labels_controller_spec.rb b/spec/controllers/dashboard/labels_controller_spec.rb index a3bfb2f3a87..01de896f9f4 100644 --- a/spec/controllers/dashboard/labels_controller_spec.rb +++ b/spec/controllers/dashboard/labels_controller_spec.rb @@ -13,13 +13,17 @@ describe Dashboard::LabelsController do describe "#index" do let!(:unrelated_label) { create(:label, project: create(:project, :public)) } + subject { get :index, format: :json } + it 'returns global labels for projects the user has a relationship with' do - get :index, format: :json + subject expect(json_response).to be_kind_of(Array) expect(json_response.size).to eq(1) expect(json_response[0]["id"]).to be_nil expect(json_response[0]["title"]).to eq(label.title) end + + it_behaves_like 'disabled when using an external authorization service' end end diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb index 828de0e7ca5..1614739db05 100644 --- a/spec/controllers/dashboard/milestones_controller_spec.rb +++ b/spec/controllers/dashboard/milestones_controller_spec.rb @@ -81,5 +81,11 @@ describe Dashboard::MilestonesController do expect(response.body).to include("Open\n2") expect(response.body).to include("Closed\n0") end + + context 'external authorization' do + subject { get :index } + + it_behaves_like 'disabled when using an external authorization service' + end end end diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb index 649441f4917..c17cb49e460 100644 --- a/spec/controllers/dashboard/projects_controller_spec.rb +++ b/spec/controllers/dashboard/projects_controller_spec.rb @@ -1,7 +1,29 @@ require 'spec_helper' describe Dashboard::ProjectsController do - it_behaves_like 'authenticates sessionless user', :index, :atom + include ExternalAuthorizationServiceHelpers + + describe '#index' do + context 'user not logged in' do + it_behaves_like 'authenticates sessionless user', :index, :atom + end + + context 'user logged in' do + before do + sign_in create(:user) + end + + context 'external authorization' do + it 'works when the external authorization service is enabled' do + enable_external_authorization_service_check + + get :index + + expect(response).to have_gitlab_http_status(200) + end + end + end + end context 'json requests' do render_views diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb index d88beaff0e1..abbf0b52306 100644 --- a/spec/controllers/dashboard/todos_controller_spec.rb +++ b/spec/controllers/dashboard/todos_controller_spec.rb @@ -105,6 +105,12 @@ describe Dashboard::TodosController do end end end + + context 'external authorization' do + subject { get :index } + + it_behaves_like 'disabled when using an external authorization service' + end end describe 'PATCH #restore' do diff --git a/spec/controllers/groups/avatars_controller_spec.rb b/spec/controllers/groups/avatars_controller_spec.rb index 772d1d0c1dd..6ececa6f372 100644 --- a/spec/controllers/groups/avatars_controller_spec.rb +++ b/spec/controllers/groups/avatars_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Groups::AvatarsController do + include ExternalAuthorizationServiceHelpers + let(:user) { create(:user) } let(:group) { create(:group, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) } @@ -15,4 +17,12 @@ describe Groups::AvatarsController do expect(@group.avatar.present?).to be_falsey expect(@group).to be_valid end + + it 'works when external authorization service is enabled' do + enable_external_authorization_service_check + + delete :destroy, params: { group_id: group } + + expect(response).to have_gitlab_http_status(302) + end end diff --git a/spec/controllers/groups/boards_controller_spec.rb b/spec/controllers/groups/boards_controller_spec.rb index 27ee37b3817..0ca5ce51750 100644 --- a/spec/controllers/groups/boards_controller_spec.rb +++ b/spec/controllers/groups/boards_controller_spec.rb @@ -82,6 +82,10 @@ describe Groups::BoardsController do end end + it_behaves_like 'disabled when using an external authorization service' do + subject { list_boards } + end + def list_boards(format: :html) get :index, params: { group_id: group }, format: format end @@ -160,6 +164,10 @@ describe Groups::BoardsController do end end + it_behaves_like 'disabled when using an external authorization service' do + subject { read_board board: board } + end + def read_board(board:, format: :html) get :show, params: { group_id: group, diff --git a/spec/controllers/groups/children_controller_spec.rb b/spec/controllers/groups/children_controller_spec.rb index e1b97013408..4085c8f95a9 100644 --- a/spec/controllers/groups/children_controller_spec.rb +++ b/spec/controllers/groups/children_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Groups::ChildrenController do + include ExternalAuthorizationServiceHelpers + let(:group) { create(:group, :public) } let(:user) { create(:user) } let!(:group_member) { create(:group_member, group: group, user: user) } @@ -317,5 +319,15 @@ describe Groups::ChildrenController do end end end + + context 'external authorization' do + it 'works when external authorization service is enabled' do + enable_external_authorization_service_check + + get :index, params: { group_id: group }, format: :json + + expect(response).to have_gitlab_http_status(200) + end + end end end diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb index 3a801fabafc..96a58d6d87c 100644 --- a/spec/controllers/groups/group_members_controller_spec.rb +++ b/spec/controllers/groups/group_members_controller_spec.rb @@ -1,8 +1,11 @@ require 'spec_helper' describe Groups::GroupMembersController do + include ExternalAuthorizationServiceHelpers + let(:user) { create(:user) } let(:group) { create(:group, :public, :access_requestable) } + let(:membership) { create(:group_member, group: group) } describe 'GET index' do it 'renders index with 200 status code' do @@ -263,4 +266,87 @@ describe Groups::GroupMembersController do end end end + + context 'with external authorization enabled' do + before do + enable_external_authorization_service_check + group.add_owner(user) + sign_in(user) + end + + describe 'GET #index' do + it 'is successful' do + get :index, params: { group_id: group } + + expect(response).to have_gitlab_http_status(200) + end + end + + describe 'POST #create' do + it 'is successful' do + post :create, params: { group_id: group, users: user, access_level: Gitlab::Access::GUEST } + + expect(response).to have_gitlab_http_status(302) + end + end + + describe 'PUT #update' do + it 'is successful' do + put :update, + params: { + group_member: { access_level: Gitlab::Access::GUEST }, + group_id: group, + id: membership + }, + format: :js + + expect(response).to have_gitlab_http_status(200) + end + end + + describe 'DELETE #destroy' do + it 'is successful' do + delete :destroy, params: { group_id: group, id: membership } + + expect(response).to have_gitlab_http_status(302) + end + end + + describe 'POST #destroy' do + it 'is successful' do + sign_in(create(:user)) + + post :request_access, params: { group_id: group } + + expect(response).to have_gitlab_http_status(302) + end + end + + describe 'POST #approve_request_access' do + it 'is successful' do + access_request = create(:group_member, :access_request, group: group) + post :approve_access_request, params: { group_id: group, id: access_request } + + expect(response).to have_gitlab_http_status(302) + end + end + + describe 'DELETE #leave' do + it 'is successful' do + group.add_owner(create(:user)) + + delete :leave, params: { group_id: group } + + expect(response).to have_gitlab_http_status(302) + end + end + + describe 'POST #resend_invite' do + it 'is successful' do + post :resend_invite, params: { group_id: group, id: membership } + + expect(response).to have_gitlab_http_status(302) + end + end + end end diff --git a/spec/controllers/groups/labels_controller_spec.rb b/spec/controllers/groups/labels_controller_spec.rb index fa664a29066..9af47114838 100644 --- a/spec/controllers/groups/labels_controller_spec.rb +++ b/spec/controllers/groups/labels_controller_spec.rb @@ -37,6 +37,12 @@ describe Groups::LabelsController do expect(label_ids).to match_array([group_label_1.title, subgroup_label_1.title]) end end + + context 'external authorization' do + subject { get :index, params: { group_id: group.to_param } } + + it_behaves_like 'disabled when using an external authorization service' + end end describe 'POST #toggle_subscription' do diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index 043cf28514b..d70946cbc8f 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -80,6 +80,12 @@ describe Groups::MilestonesController do expect(response.content_type).to eq 'application/json' end end + + context 'external authorization' do + subject { get :index, params: { group_id: group.to_param } } + + it_behaves_like 'disabled when using an external authorization service' + end end describe '#show' do diff --git a/spec/controllers/groups/settings/ci_cd_controller_spec.rb b/spec/controllers/groups/settings/ci_cd_controller_spec.rb index 3290ed8b088..b998f64ef72 100644 --- a/spec/controllers/groups/settings/ci_cd_controller_spec.rb +++ b/spec/controllers/groups/settings/ci_cd_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Groups::Settings::CiCdController do + include ExternalAuthorizationServiceHelpers + let(:group) { create(:group) } let(:user) { create(:user) } @@ -33,6 +35,19 @@ describe Groups::Settings::CiCdController do expect(response).to have_gitlab_http_status(404) end end + + context 'external authorization' do + before do + enable_external_authorization_service_check + group.add_owner(user) + end + + it 'renders show with 200 status code' do + get :show, params: { group_id: group } + + expect(response).to have_gitlab_http_status(200) + end + end end describe 'PUT #reset_registration_token' do diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb index 29ec3588316..40f05167350 100644 --- a/spec/controllers/groups/variables_controller_spec.rb +++ b/spec/controllers/groups/variables_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Groups::VariablesController do + include ExternalAuthorizationServiceHelpers + let(:group) { create(:group) } let(:user) { create(:user) } @@ -34,4 +36,36 @@ describe Groups::VariablesController do include_examples 'PATCH #update updates variables' end + + context 'with external authorization enabled' do + before do + enable_external_authorization_service_check + end + + describe 'GET #show' do + let!(:variable) { create(:ci_group_variable, group: group) } + + it 'is successful' do + get :show, params: { group_id: group }, format: :json + + expect(response).to have_gitlab_http_status(200) + end + end + + describe 'PATCH #update' do + let!(:variable) { create(:ci_group_variable, group: group) } + let(:owner) { group } + + it 'is successful' do + patch :update, + params: { + group_id: group, + variables_attributes: [{ id: variable.id, key: 'hello' }] + }, + format: :json + + expect(response).to have_gitlab_http_status(200) + end + end + end end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 4a28a27da79..431627cf85a 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe GroupsController do + include ExternalAuthorizationServiceHelpers + let(:user) { create(:user) } let(:admin) { create(:admin) } let(:group) { create(:group, :public) } @@ -665,4 +667,98 @@ describe GroupsController do end end end + + describe 'external authorization' do + before do + group.add_owner(user) + sign_in(user) + end + + context 'with external authorization service enabled' do + before do + enable_external_authorization_service_check + end + + describe 'GET #show' do + it 'is successful' do + get :show, params: { id: group.to_param } + + expect(response).to have_gitlab_http_status(200) + end + + it 'does not allow other formats' do + get :show, params: { id: group.to_param }, format: :atom + + expect(response).to have_gitlab_http_status(403) + end + end + + describe 'GET #edit' do + it 'is successful' do + get :edit, params: { id: group.to_param } + + expect(response).to have_gitlab_http_status(200) + end + end + + describe 'GET #new' do + it 'is successful' do + get :new + + expect(response).to have_gitlab_http_status(200) + end + end + + describe 'GET #index' do + it 'is successful' do + get :index + + # Redirects to the dashboard + expect(response).to have_gitlab_http_status(302) + end + end + + describe 'POST #create' do + it 'creates a group' do + expect do + post :create, params: { group: { name: 'a name', path: 'a-name' } } + end.to change { Group.count }.by(1) + end + end + + describe 'PUT #update' do + it 'updates a group' do + expect do + put :update, params: { id: group.to_param, group: { name: 'world' } } + end.to change { group.reload.name } + end + end + + describe 'DELETE #destroy' do + it 'deletes the group' do + delete :destroy, params: { id: group.to_param } + + expect(response).to have_gitlab_http_status(302) + end + end + end + + describe 'GET #activity' do + subject { get :activity, params: { id: group.to_param } } + + it_behaves_like 'disabled when using an external authorization service' + end + + describe 'GET #issues' do + subject { get :issues, params: { id: group.to_param } } + + it_behaves_like 'disabled when using an external authorization service' + end + + describe 'GET #merge_requests' do + subject { get :merge_requests, params: { id: group.to_param } } + + it_behaves_like 'disabled when using an external authorization service' + end + end end diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb index 1eeded06459..b1203fd00b0 100644 --- a/spec/controllers/projects/boards_controller_spec.rb +++ b/spec/controllers/projects/boards_controller_spec.rb @@ -98,6 +98,10 @@ describe Projects::BoardsController do end end + it_behaves_like 'unauthorized when external service denies access' do + subject { list_boards } + end + def list_boards(format: :html) get :index, params: { namespace_id: project.namespace, diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index c34d7c13d57..bfa23af76d5 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -127,6 +127,17 @@ describe Projects::IssuesController do expect(assigns(:issues).size).to eq(2) end end + + context 'external authorization' do + before do + sign_in user + project.add_developer(user) + end + + it_behaves_like 'unauthorized when external service denies access' do + subject { get :index, params: { namespace_id: project.namespace, project_id: project } } + end + end end describe 'GET #new' do diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index af437c5561b..1ce06bc877c 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -1,6 +1,7 @@ require('spec_helper') describe ProjectsController do + include ExternalAuthorizationServiceHelpers include ProjectForksHelper let(:project) { create(:project) } @@ -411,6 +412,37 @@ describe ProjectsController do it_behaves_like 'updating a project' end + + context 'as maintainer' do + before do + project.add_maintainer(user) + sign_in(user) + end + + it_behaves_like 'unauthorized when external service denies access' do + subject do + put :update, + params: { + namespace_id: project.namespace, + id: project, + project: { description: 'Hello world' } + } + project.reload + end + + it 'updates when the service allows access' do + external_service_allow_access(user, project) + + expect { subject }.to change(project, :description) + end + + it 'does not update when the service rejects access' do + external_service_deny_access(user, project) + + expect { subject }.not_to change(project, :description) + end + end + end end describe '#transfer' do diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index 02a0cfe0272..752d6ae55cc 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe SearchController do + include ExternalAuthorizationServiceHelpers + let(:user) { create(:user) } before do @@ -76,4 +78,41 @@ describe SearchController do expect(assigns[:search_objects].count).to eq(0) end end + + context 'with external authorization service enabled' do + let(:project) { create(:project, namespace: user.namespace) } + let(:note) { create(:note_on_issue, project: project) } + + before do + enable_external_authorization_service_check + end + + describe 'GET #show' do + it 'renders a 403 when no project is given' do + get :show, params: { scope: 'notes', search: note.note } + + expect(response).to have_gitlab_http_status(403) + end + + it 'renders a 200 when a project was set' do + get :show, params: { project_id: project.id, scope: 'notes', search: note.note } + + expect(response).to have_gitlab_http_status(200) + end + end + + describe 'GET #autocomplete' do + it 'renders a 403 when no project is given' do + get :autocomplete, params: { term: 'hello' } + + expect(response).to have_gitlab_http_status(403) + end + + it 'renders a 200 when a project was set' do + get :autocomplete, params: { project_id: project.id, term: 'hello' } + + expect(response).to have_gitlab_http_status(200) + end + end + end end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 4f6a6881193..42d28c53d34 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -223,6 +223,12 @@ describe UsersController do end end + context 'external authorization' do + subject { get :calendar_activities, params: { username: user.username } } + + it_behaves_like 'disabled when using an external authorization service' + end + def create_push_event push_data = Gitlab::DataBuilder::Push.build_sample(project, public_user) EventCreateService.new.push(project, public_user, push_data) @@ -286,6 +292,12 @@ describe UsersController do expect(JSON.parse(response.body)).to have_key('html') end end + + context 'external authorization' do + subject { get :snippets, params: { username: user.username } } + + it_behaves_like 'disabled when using an external authorization service' + end end describe 'GET #exists' do diff --git a/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb b/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb new file mode 100644 index 00000000000..4098dd02141 --- /dev/null +++ b/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe 'The group dashboard' do + include ExternalAuthorizationServiceHelpers + + let(:user) { create(:user) } + + before do + sign_in user + end + + describe 'The top navigation' do + it 'has all the expected links' do + visit dashboard_groups_path + + within('.navbar') do + expect(page).to have_button('Projects') + expect(page).to have_button('Groups') + expect(page).to have_link('Activity') + expect(page).to have_link('Milestones') + expect(page).to have_link('Snippets') + end + end + + it 'hides some links when an external authorization service is enabled' do + enable_external_authorization_service_check + visit dashboard_groups_path + + within('.navbar') do + expect(page).to have_button('Projects') + expect(page).to have_button('Groups') + expect(page).not_to have_link('Activity') + expect(page).not_to have_link('Milestones') + expect(page).to have_link('Snippets') + end + end + end +end diff --git a/spec/features/groups/group_page_with_external_authorization_service_spec.rb b/spec/features/groups/group_page_with_external_authorization_service_spec.rb new file mode 100644 index 00000000000..c05c3f4f3d6 --- /dev/null +++ b/spec/features/groups/group_page_with_external_authorization_service_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'The group page' do + include ExternalAuthorizationServiceHelpers + + let(:user) { create(:user) } + let(:group) { create(:group) } + + before do + sign_in user + group.add_owner(user) + end + + def expect_all_sidebar_links + within('.nav-sidebar') do + expect(page).to have_link('Overview') + expect(page).to have_link('Details') + expect(page).to have_link('Activity') + expect(page).to have_link('Issues') + expect(page).to have_link('Merge Requests') + expect(page).to have_link('Members') + end + end + + describe 'The sidebar' do + it 'has all the expected links' do + visit group_path(group) + + expect_all_sidebar_links + end + + it 'shows all project features when policy control is enabled' do + stub_application_setting(external_authorization_service_enabled: true) + + visit group_path(group) + + expect_all_sidebar_links + end + + it 'hides some links when an external authorization service configured with an url' do + enable_external_authorization_service_check + visit group_path(group) + + within('.nav-sidebar') do + expect(page).to have_link('Overview') + expect(page).to have_link('Details') + expect(page).not_to have_link('Activity') + expect(page).not_to have_link('Contribution Analytics') + + expect(page).not_to have_link('Issues') + expect(page).not_to have_link('Merge Requests') + expect(page).to have_link('Members') + end + end + end +end diff --git a/spec/features/projects/classification_label_on_project_pages_spec.rb b/spec/features/projects/classification_label_on_project_pages_spec.rb new file mode 100644 index 00000000000..92f8aa8eb8d --- /dev/null +++ b/spec/features/projects/classification_label_on_project_pages_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Classification label on project pages' do + let(:project) do + create(:project, external_authorization_classification_label: 'authorized label') + end + let(:user) { create(:user) } + + before do + stub_application_setting(external_authorization_service_enabled: true) + project.add_maintainer(user) + sign_in(user) + end + + it 'shows the classification label on the project page' do + visit project_path(project) + + expect(page).to have_content('authorized label') + end +end diff --git a/spec/features/projects/forks/fork_list_spec.rb b/spec/features/projects/forks/fork_list_spec.rb new file mode 100644 index 00000000000..2c41c61a660 --- /dev/null +++ b/spec/features/projects/forks/fork_list_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe 'listing forks of a project' do + include ProjectForksHelper + include ExternalAuthorizationServiceHelpers + + let(:source) { create(:project, :public, :repository) } + let!(:fork) { fork_project(source, nil, repository: true) } + let(:user) { create(:user) } + + before do + source.add_maintainer(user) + sign_in(user) + end + + it 'shows the forked project in the list with commit as description' do + visit project_forks_path(source) + + page.within('li.project-row') do + expect(page).to have_content(fork.full_name) + expect(page).to have_css('a.commit-row-message') + end + end + + it 'does not show the commit message when an external authorization service is used' do + enable_external_authorization_service_check + + visit project_forks_path(source) + + page.within('li.project-row') do + expect(page).to have_content(fork.full_name) + expect(page).not_to have_css('a.commit-row-message') + end + end +end diff --git a/spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb b/spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb new file mode 100644 index 00000000000..a8612d77a5e --- /dev/null +++ b/spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb @@ -0,0 +1,128 @@ +require 'spec_helper' + +describe 'viewing an issue with cross project references' do + include ExternalAuthorizationServiceHelpers + include Gitlab::Routing.url_helpers + + let(:user) { create(:user) } + let(:other_project) do + create(:project, :public, + external_authorization_classification_label: 'other_label') + end + let(:other_issue) do + create(:issue, :closed, + title: 'I am in another project', + project: other_project) + end + let(:other_confidential_issue) do + create(:issue, :confidential, :closed, + title: 'I am in another project and confidential', + project: other_project) + end + let(:other_merge_request) do + create(:merge_request, :closed, + title: 'I am a merge request in another project', + source_project: other_project) + end + let(:description_referencing_other_issue) do + "Referencing: #{other_issue.to_reference(project)}, "\ + "a confidential issue #{confidential_issue.to_reference}, "\ + "a cross project confidential issue #{other_confidential_issue.to_reference(project)}, and "\ + "a cross project merge request #{other_merge_request.to_reference(project)}" + end + let(:project) { create(:project) } + let(:issue) do + create(:issue, + project: project, + description: description_referencing_other_issue ) + end + let(:confidential_issue) do + create(:issue, :confidential, :closed, + title: "I am in the same project and confidential", + project: project) + end + + before do + project.add_developer(user) + sign_in(user) + end + + it 'shows all information related to the cross project reference' do + visit project_issue_path(project, issue) + + expect(page).to have_link("#{other_issue.to_reference(project)} (#{other_issue.state})") + expect(page).to have_xpath("//a[@title='#{other_issue.title}']") + end + + it 'shows a link to the confidential issue in the same project' do + visit project_issue_path(project, issue) + + expect(page).to have_link("#{confidential_issue.to_reference(project)} (#{confidential_issue.state})") + expect(page).to have_xpath("//a[@title='#{confidential_issue.title}']") + end + + it 'does not show the link to a cross project confidential issue when the user does not have access' do + visit project_issue_path(project, issue) + + expect(page).not_to have_link("#{other_confidential_issue.to_reference(project)} (#{other_confidential_issue.state})") + expect(page).not_to have_xpath("//a[@title='#{other_confidential_issue.title}']") + end + + it 'shows the link to a cross project confidential issue when the user has access' do + other_project.add_developer(user) + + visit project_issue_path(project, issue) + + expect(page).to have_link("#{other_confidential_issue.to_reference(project)} (#{other_confidential_issue.state})") + expect(page).to have_xpath("//a[@title='#{other_confidential_issue.title}']") + end + + context 'when an external authorization service is enabled' do + before do + enable_external_authorization_service_check + end + + it 'only hits the external service for the project the user is viewing' do + expect(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(user, 'default_label', any_args).at_least(1).and_return(true) + expect(::Gitlab::ExternalAuthorization) + .not_to receive(:access_allowed?).with(user, 'other_label', any_args) + + visit project_issue_path(project, issue) + end + + it 'shows only the link to the cross project references' do + visit project_issue_path(project, issue) + + expect(page).to have_link("#{other_issue.to_reference(project)}") + expect(page).to have_link("#{other_merge_request.to_reference(project)}") + expect(page).not_to have_content("#{other_issue.to_reference(project)} (#{other_issue.state})") + expect(page).not_to have_xpath("//a[@title='#{other_issue.title}']") + expect(page).not_to have_content("#{other_merge_request.to_reference(project)} (#{other_merge_request.state})") + expect(page).not_to have_xpath("//a[@title='#{other_merge_request.title}']") + end + + it 'does not link a cross project confidential issue if the user does not have access' do + visit project_issue_path(project, issue) + + expect(page).not_to have_link("#{other_confidential_issue.to_reference(project)}") + expect(page).not_to have_xpath("//a[@title='#{other_confidential_issue.title}']") + end + + it 'links a cross project confidential issue without exposing information when the user has access' do + other_project.add_developer(user) + + visit project_issue_path(project, issue) + + expect(page).to have_link("#{other_confidential_issue.to_reference(project)}") + expect(page).not_to have_xpath("//a[@title='#{other_confidential_issue.title}']") + end + + it 'shows a link to the confidential issue in the same project' do + visit project_issue_path(project, issue) + + expect(page).to have_link("#{confidential_issue.to_reference(project)} (#{confidential_issue.state})") + expect(page).to have_xpath("//a[@title='#{confidential_issue.title}']") + end + end +end diff --git a/spec/features/projects/settings/external_authorization_service_settings_spec.rb b/spec/features/projects/settings/external_authorization_service_settings_spec.rb new file mode 100644 index 00000000000..31b2892cf6f --- /dev/null +++ b/spec/features/projects/settings/external_authorization_service_settings_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Projects > Settings > External Authorization Classification Label setting' do + let(:user) { create(:user) } + let(:project) { create(:project_empty_repo) } + + before do + project.add_maintainer(user) + sign_in(user) + end + + it 'shows the field to set a classification label' do + stub_application_setting(external_authorization_service_enabled: true) + + visit edit_project_path(project) + + expect(page).to have_selector('#project_external_authorization_classification_label') + end +end diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb index 86379164cf0..351750c0179 100644 --- a/spec/features/users/show_spec.rb +++ b/spec/features/users/show_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe 'User page' do + include ExternalAuthorizationServiceHelpers + let(:user) { create(:user) } context 'with public profile' do @@ -86,4 +88,24 @@ describe 'User page' do end end end + + context 'most recent activity' do + it 'shows the most recent activity' do + visit(user_path(user)) + + expect(page).to have_content('Most Recent Activity') + end + + context 'when external authorization is enabled' do + before do + enable_external_authorization_service_check + end + + it 'hides the most recent activity' do + visit(user_path(user)) + + expect(page).not_to have_content('Most Recent Activity') + end + end + end end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 6e6dc334354..6a47cd013f8 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -531,6 +531,13 @@ describe IssuesFinder do expect(issues.count).to eq 0 end end + + context 'external authorization' do + it_behaves_like 'a finder with external authorization service' do + let!(:subject) { create(:issue, project: project) } + let(:project_params) { { project_id: project.id } } + end + end end describe '#row_count', :request_store do diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb index 3f060ba0553..98b4933fef6 100644 --- a/spec/finders/labels_finder_spec.rb +++ b/spec/finders/labels_finder_spec.rb @@ -226,5 +226,12 @@ describe LabelsFinder do expect(finder.execute).to eq [project_label_1] end end + + context 'external authorization' do + it_behaves_like 'a finder with external authorization service' do + let!(:subject) { create(:label, project: project) } + let(:project_params) { { project_id: project.id } } + end + end end end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index ef002125635..117f4a03735 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -282,6 +282,13 @@ describe MergeRequestsFinder do expect(finder.row_count).to eq(1) end end + + context 'external authorization' do + it_behaves_like 'a finder with external authorization service' do + let!(:subject) { create(:merge_request, source_project: project) } + let(:project_params) { { project_id: project.id } } + end + end end context 'when projects require different access levels for merge requests' do diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index 93287f3e9b8..d367f9015c7 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe SnippetsFinder do + include ExternalAuthorizationServiceHelpers include Gitlab::Allowable describe '#initialize' do @@ -164,4 +165,35 @@ describe SnippetsFinder do end it_behaves_like 'snippet visibility' + + context 'external authorization' do + let(:user) { create(:user) } + let(:project) { create(:project) } + let!(:snippet) { create(:project_snippet, :public, project: project) } + + before do + project.add_maintainer(user) + end + + it_behaves_like 'a finder with external authorization service' do + let!(:subject) { create(:project_snippet, project: project) } + let(:project_params) { { project: project } } + end + + it 'includes the result if the external service allows access' do + external_service_allow_access(user, project) + + results = described_class.new(user, project: project).execute + + expect(results).to contain_exactly(snippet) + end + + it 'does not include any results if the external service denies access' do + external_service_deny_access(user, project) + + results = described_class.new(user, project: project).execute + + expect(results).to be_empty + end + end end diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb index d4ed41d54f0..22318a9946a 100644 --- a/spec/finders/todos_finder_spec.rb +++ b/spec/finders/todos_finder_spec.rb @@ -47,6 +47,13 @@ describe TodosFinder do end end end + + context 'external authorization' do + it_behaves_like 'a finder with external authorization service' do + let!(:subject) { create(:todo, project: project, user: user) } + let(:project_params) { { project_id: project.id } } + end + end end describe '#sort' do diff --git a/spec/fixtures/passphrase_x509_certificate.crt b/spec/fixtures/passphrase_x509_certificate.crt new file mode 100644 index 00000000000..6973163b79e --- /dev/null +++ b/spec/fixtures/passphrase_x509_certificate.crt @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEpTCCAo0CAQEwDQYJKoZIhvcNAQEFBQAwFDESMBAGA1UEAwwJYXV0aG9yaXR5 +MB4XDTE4MDMyMzE0MDIwOFoXDTE5MDMyMzE0MDIwOFowHTEbMBkGA1UEAwwSZ2l0 +bGFiLXBhc3NwaHJhc2VkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +zpsWHOewP/khfDsLUWxaRCinrBzVJm2C01bVahKVR3g/JD4vEH901Wod9Pvbh/9e +PEfE+YZmgSUUopbL3JUheMnyW416F43HKE/fPW4+QeuIEceuhCXg20eOXmvnWWNM +0hXZh4hq69rwvMPREC/LkZy/QkTDKhJNLNAqAQu2AJ3C7Yga8hFQYEhx1hpfGtwD +z/Nf3efat9WN/d6yW9hfJ98NCmImTm5l9Pc0YPNWCAf96vsqsNHBrTkFy6CQwkhH +K1ynVYuqnHYxSc4FPCT5SAleD9gR/xFBAHb7pPy4yGxMSEmiWaMjjZCVPsghj1jM +Ej77MTDL3U9LeDfiILhvZ+EeQxqPiFwwG2eaIn3ZEs2Ujvw7Z2VpG9VMcPTnB4jK +ot6qPM1YXnkGWQ6iT0DTPS3h7zg1xIJXI5N2sI6GXuKrXXwZ1wPqzFLKPv+xBjp8 +P6dih+EImfReFi9zIO1LqGMY+XmRcqodsb6jzsmBimJkqBtatJM7FuUUUN56wiaj +q9+BWbm+ZdQ2lvqndMljjUjTh6pNERfGAJgkNuLn3X9hXVE0TSpmn0nOgaL5izP3 +7FWUt0PTyGgK2zq9SEhZmK2TKckLkKMk/ZBBBVM/nrnjs72IlbsqdcVoTnApytZr +xVYTj1hV7QlAfaU3w/M534qXDiy8+HfX5ksWQMtSklECAwEAATANBgkqhkiG9w0B +AQUFAAOCAgEAMMhzSRq9PqCpui74nwjhmn8Dm2ky7A+MmoXNtk70cS/HWrjzaacb +B/rxsAUp7f0pj4QMMM0ETMFpbNs8+NPd2FRY0PfWE4yyDpvZO2Oj1HZKLHX72Gjn +K5KB9DYlVsXhGPfuFWXpxGWF2Az9hDWnj58M3DOAps+6tHuAtudQUuwf5ENQZWwE +ySpr7yoHm1ykgl0Tsb9ZHi9qLrWRRMNYXRT+gvwP1bba8j9jOtjO/xYiIskwMPLM +W8SFmQxbg0Cvi8Q89PB6zoTNOhPQyoyeSlw9meeZJHAMK2zxeglEm8C4EQ+I9Y6/ +yylM5/Sc55TjWAvRFgbsq+OozgMvffk/Q2fzcGF44J9DEQ7nrhmJxJ+X4enLknR5 +Hw4+WhdYA+bwjx3YZBNTh9/YMgNPYwQhf5gtcZGTd6X4j6qZfJ6CXBmhkC1Cbfyl +yM7B7i4JAqPWMeDP50pXCgyKlwgw1JuFW+xkbkYQAj7wtggQ6z1Vjb5W8R8kYn9q +LXClVtThEeSV5KkVwNX21aFcUs8qeQ+zsgKqpEyM5oILQQ1gDSxLTtrr2KuN+WJN +wM0acwD45X7gA/aZYpCGkIgHIBq0zIDP1s6IqeebFJjW8lWofhRxOEWomWdRweJG +N7qQ1WCTQxAPGAkDI8QPjaspvnAhFKmpBG/mR5IXLFKDbttu7WNdYDo= +-----END CERTIFICATE----- diff --git a/spec/fixtures/passphrase_x509_certificate_pk.key b/spec/fixtures/passphrase_x509_certificate_pk.key new file mode 100644 index 00000000000..f9760dfe70e --- /dev/null +++ b/spec/fixtures/passphrase_x509_certificate_pk.key @@ -0,0 +1,54 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,79CCB506B0FD42A6F1BAE6D72E1CB20C + +EuZQOfgaO6LVCNytTHNJmbiq1rbum9xg6ohfBTVt7Cw4+8yLezWva/3sJQtnEk2P +M2yEQYWIiCX+clPkRiRL8WLjRfLTNcYS6QxxuJdpOrowPrBYr4Aig8jBUUBI4VQf +w1ZEUQd0mxQGnyzkKpsudFOntCtZbvbrBsIAQUNLcrKEFk3XW/BqE1Q/ja6WfWqX +b6EKg6DoXi92V90O6sLDfpmTKZq3ThvVDFuWeJ2K/GVp2cs+MkBIBJ8XX+NT1nWg +g+Ok+yaSI/N9ILX4XDgXunJGwcooI8PhHSjkDWRusi8vbo7RFqIKiSF+h6tIwktF +Uss3JESKgXZCQ7upCnHSzK/aWFtwHtXxqOi7esqEZd+1sB0LY+XMnbaxweCMx2Kj +czktKYvoXUs69Whln+yyXULtl5XhJ8lbvlbIG2FbZ9y+/hHOyBqZyeUyCnXDzv8/ +0U0iZwreP3XPVMsy578pIdcdL27q+r05j4yjrJfbX3T9xp2u3F9uVubCa4euEBwV +yrFdsxJLKON8pFeDS49m5gHNsHmeZ0sUeTPZVGNXdabVetkOA0eAAGK4zAoqG79L +hEN7cDenz+E4XHp8gMzwwMiVyU4FuAb6SXkfSodctmSTWVbzNBja0FBek3UXy+pn +9qq7cIpe7NY5gzcbyoy9lSkyYVkAm8j6BIYtY1ZUAmtCklC2ADWARTjd7dI7aEbO +QbXxNIq2+O/zMOXfougSPoDP8SLyLuE1p6SwfWV7Dwf119hn+mjWlGzAZDxxHhsR +yYUQCUe0NIKzuUp3WYIx8xIb7/WFwit/JaFaxurjBnhkkEviBn+TgXiuFBO3tv/d +URpZ39rH0mrDsR61pCiIcoNVkQkynHcAFPd5VtaeSJPvZP280uOCPPS31cr6/0LB +1JX3lZoWWCuA+JQjxtZDaDTcvEUbfOQ2rexQQo4uylNkBF9F5WOdQBkKG/AfqBq8 +S/TdubYzvpcKhFAlXsI67JdbxGlU4HCsxOLwWzSUYclN4W3l7s7KZ5zxt+MU03Uf +vara9uuZHiKUjZohjXeqcXTc+UyC8VH1dF19M3Cj9RNrwl2xEDUMtIiALBjbGp1E +pu2nPj9NhWf9Vw5MtSszutesxXba2nPmvvGvvZ7N3h/k4NsKL7JdENF7XqkI0D2K +jpO1t6d3cazS1VpMWLZS45kWaM3Y07tVR3V+4Iv9Vo1e9H2u/Z5U4YeJ44sgMsct +dBOAhHdUAI5+P+ocLXiCKo+EcS0cKvz+CC4ux0vvcF3JrTqZJN1U/JxRka2EyJ1B +2Xtu3DF36XpBJcs+MJHjJ+kUn6DHYoYxZa+bB8LX6+FQ+G7ue+Dx/RsGlP7if1nq +DAaM6kZg7/FbFzOZyl5xhwAJMxfgNNU7nSbk9lrvQ4mdwgFjvgGu3jlER4+TcleE +4svXInxp1zK6ES44tI9fXkhPaFkafxAL7eUSyjjEwMC06h+FtqK3mmoKLo5NrGJE +zVl69r2WdoSQEylVN1Kbp+U4YbfncInLJqBq2q5w9ASL/8Rhe8b52q6PuVX/bjoz +0pkSu+At4jVbAhRpER5NGlzG884IaqqvBvMYR5zFJeRroIijyUyH0KslK37/sXRk +ty0yKrkm31De9gDa3+XlgAVDAgbEQmGVwVVcV0IYYJbjIf36lUdGh4+3krwxolr/ +vZct5Z7QxfJlBtdOstjz5U9o05yOhjoNrPZJXuKMmWOQjSwr7rRSdqmAABF9IrBf +Pa/ChF1y5j3gJESAFMyiea3kvLq1EbZRaKoybsQE2ctBQ8EQjzUz+OOxVO6GJ4W9 +XHyfcviFrpsVcJEpXQlEtGtKdfKLp48cytob1Fu1JOYPDCrafUQINCZP4H3Nt892 +zZiTmdwux7pbgf4KbONImN5XkpvdCGjQHSkYMmm5ETRK8s7Fmvt2aBPtlyXxJDOq +iJUqwDV5HZXOnQVE/v/yESKgo2Cb8BWqPZ4/8Ubgu/OADYyv/dtjQel8QQ2FMhO4 +2tnwWbBBJk8VpR/vjFHkGSnj+JJfW/vUVQ+06D3wHYhNp7mh4M+37AngwzGCp7k+ +9aFwb2FBGghArB03E4lIO/959T0cX95WZ6tZtLLEsf3+ug7PPOSswCqsoPsXzFJH +MgXVGKFXccNSsWol7VvrX/uja7LC1OE+pZNXxCRzSs4aljJBpvQ6Mty0lk2yBC0R +MdujMoZH9PG9U6stwFd+P17tlGrQdRD3H2uimn82Ck+j2l0z0pzN0JB2WBYEyK0O +1MC36wLICWjgIPLPOxDEEBeZPbc24DCcYfs/F/hSCHv/XTJzVVILCX11ShGPSXlI +FL9qyq6jTNh/pVz6NiN/WhUPBFfOSzLRDyU0MRsSHM8b/HPpf3NOI3Ywmmj65c2k +2kle1F2M5ZTL+XvLS61qLJ/8AgXWvDHP3xWuKGG/pM40CRTUkRW6NAokMr2/pEFw +IHTE2+84dOKnUIEczzMY3aqzNmYDCmhOY0jD/Ieb4hy9tN+1lbQ/msYMIJ1w7CFR +38yB/UbDD90NcuDhjrMbzVUv1At2rW7GM9lSbxGOlYDmtMNEL63md1pQ724v4gSE +mzoFcMkqdh+hjFvv11o4H32lF3mPYcXuL+po76tqxGOiUrLKe/ZqkT5XAclYV/7H +k3Me++PCh4ZqXBRPvR8Xr90NETtiFCkBQXLdhNWXrRe2v0EbSX+cYAWk68FQKCHa +HKTz9T7wAvB6QWBXFhH9iCP8rnQLCEhLEhdrt+4v2KFkIVzBgOlMoHsZsMp0sBeq +c5ZVbJdiKik3P/8ZQTn4jmOnQXCEyWx+LU4acks8Aho4lqq9yKq2DZpwbIRED47E +r7R/NUevhqqzEHZ2SGD6EDqRN+bHJEi64vq0ryaEielusYXZqlnFXDHJcfLCmR5X +3bj5pCwQF4ScTukrGQB/c4henG4vlF4CaD0CIIK3W6tH+AoDohYJts6YK49LGxmK +yXiyKNak8zHYBBoRvd2avRHyGuR5yC9KrN8cbC/kZqMDvAyM65pIK+U7exJwYJhv +ezCcbiH3bK3anpiRpdeNOot2ba/Y+/ks+DRC+xs4QDIhrmSEBCsLv1JbcWjtHSaG +lm+1DSVduUk/kN+fBnlfif+TQV9AP3/wb8ekk8jjKXsL7H1tJKHsLLIIvrgrpxjw +-----END RSA PRIVATE KEY----- diff --git a/spec/fixtures/x509_certificate.crt b/spec/fixtures/x509_certificate.crt new file mode 100644 index 00000000000..8a84890b928 --- /dev/null +++ b/spec/fixtures/x509_certificate.crt @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEnDCCAoQCAQEwDQYJKoZIhvcNAQEFBQAwFDESMBAGA1UEAwwJYXV0aG9yaXR5 +MB4XDTE4MDMxOTE1MjYzMloXDTE5MDMxOTE1MjYzMlowFDESMBAGA1UEAwwJbG9j +YWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+tcM7iphsLlR +ccUph2ixabRYnw1HeLCiA4O9a4O31oVUBuzAn/eVU4jyVWkaBym6MHa8CiDOro9H +OXodITMw+3G1sG/yQZ8Y/5dsOP2hEoSfs63/2FAgFWzrB2HnYSShiN8tBeeDI5cJ +ii4JVMfpfi9cvXZUXFR8+P0XR1HDxx6or6UTK37k2kbDQZ41rv1ng2w0AUZt0LRA +NWVE48zvUWIU0y+2JLP1yhrKj85RRjQc5cMK88zzWSZBcSjDGGeJ4C8B5Zh2gFlQ ++1aJkyyklORR3v/RyYO9prTeXPqQ3x/nNsNkI+cyv0Gle6tk+CkOfE1m0CvNWlNg +b8LdQ0XZsOYLZvxfpHk3gHA5GrHXvn5StkM5xMXpdUCsh22CZZHe/4SeFE64amkf +1/LuqY0LYc5UdG2SeJ0SDauPRAIuAr4OV7+Q/nLdY8haMC6KOtpbAWvKX/Jqq0z1 +nUXzQn1JWCNw1QMdq9Uz8wiWOjLTr2D/mIVrVef0pb2mfdtzjzUrYCP0PtnQExPB +rocP6BDXN7Ragcdis5/IfLuCOD6pAkmzy6o8RSvAoEUs9VbPiUfN7WAyU1K1rTYH +KV+zPfWF254nZ2SBeReN9CMKbMJE+TX2chRlq07Q5LDz33h9KXw1LZT8MWRinVJf +RePsQiyHpRBWRG0AhbD+YpiGKHzsat0CAwEAATANBgkqhkiG9w0BAQUFAAOCAgEA +Skp0tbvVsg3RG2pX0GP25j0ix+f78zG0+BJ6LiKGMoCIBtGKitfUjBg83ru/ILpa +fpgrQpNQVUnGQ9tmpnqV605ZBBRUC1CRDsvUnyN6p7+yQAq6Fl+2ZKONHpPk+Bl4 +CIewgdkHjTwTpvIM/1DFVCz4R1FxNjY3uqOVcNDczMYEk2Pn2GZNNN35hUHHxWh4 +89ZvI+XKuRFZq3cDPA60PySeJJpCRScWGgnkdEX1gTtWH3WUlq9llxIvRexyNyzZ +Yqvcfx5UT75/Pp+JPh9lpUCcKLHeUiadjkiLxu3IcrYa4gYx4lA8jgm7adNEahd0 +oMAHoO9DU6XMo7o6tnQH3xQv9RAbQanjuyJR9N7mwmc59bQ6mW+pxCk843GwT73F +slseJ1nE1fQQQD7mn/KGjmeWtxY2ElUjTay9ff9/AgJeQYRW+oH0cSdo8WCpc2+G ++LZtLWfBgFLHseRlmarSe2pP8KmbaTd3q7Bu0GekVQOxYcNX59Pj4muQZDVLh8aX +mSQ+Ifts/ljT649MISHn2AZMR4+BUx63tFcatQhbAGGH5LeFdbaGcaVdsUVyZ9a2 +HBmFWNsgEPtcC+WmNzCXbv7jQsLAJXufKG5MnurJgNf/n5uKCmpGsEJDT/KF1k/3 +x9YnqM7zTyV6un+LS3HjEJvwQmqPWe+vFAeXWGCoWxE= +-----END CERTIFICATE----- diff --git a/spec/fixtures/x509_certificate_pk.key b/spec/fixtures/x509_certificate_pk.key new file mode 100644 index 00000000000..c02a3cf6189 --- /dev/null +++ b/spec/fixtures/x509_certificate_pk.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKAIBAAKCAgEA+tcM7iphsLlRccUph2ixabRYnw1HeLCiA4O9a4O31oVUBuzA +n/eVU4jyVWkaBym6MHa8CiDOro9HOXodITMw+3G1sG/yQZ8Y/5dsOP2hEoSfs63/ +2FAgFWzrB2HnYSShiN8tBeeDI5cJii4JVMfpfi9cvXZUXFR8+P0XR1HDxx6or6UT +K37k2kbDQZ41rv1ng2w0AUZt0LRANWVE48zvUWIU0y+2JLP1yhrKj85RRjQc5cMK +88zzWSZBcSjDGGeJ4C8B5Zh2gFlQ+1aJkyyklORR3v/RyYO9prTeXPqQ3x/nNsNk +I+cyv0Gle6tk+CkOfE1m0CvNWlNgb8LdQ0XZsOYLZvxfpHk3gHA5GrHXvn5StkM5 +xMXpdUCsh22CZZHe/4SeFE64amkf1/LuqY0LYc5UdG2SeJ0SDauPRAIuAr4OV7+Q +/nLdY8haMC6KOtpbAWvKX/Jqq0z1nUXzQn1JWCNw1QMdq9Uz8wiWOjLTr2D/mIVr +Vef0pb2mfdtzjzUrYCP0PtnQExPBrocP6BDXN7Ragcdis5/IfLuCOD6pAkmzy6o8 +RSvAoEUs9VbPiUfN7WAyU1K1rTYHKV+zPfWF254nZ2SBeReN9CMKbMJE+TX2chRl +q07Q5LDz33h9KXw1LZT8MWRinVJfRePsQiyHpRBWRG0AhbD+YpiGKHzsat0CAwEA +AQKCAgBf1urJ1Meeji/gGETVx9qBWLbDjn9QTayZSyyEd78155tDShIPDLmxQRHW +MGIReo/5FGSkOgS+DWBZRZ77oGOGrtuMnjkheXhDr8dZvw5b1PBv5ntqWrLnfMYP +/Ag7xZMyiJLbPqmMX5j1gsFt8zPzUoVMnnl9DYryV0Edrs/utHgfJCM+6yzleUQB +PkGkqo1yWVVFZ3Nt2nDt9dNsdlC594+dYQ1m2JuArNvYNiw3dpHT98GnhRc1aLh4 +U+q22FiFn3BKGQat43JdlaLa6KO5f8MIQRYWuI8tss2DGPlhRv9AnUcVsLBjAuIH +bmUVrBosxCYUQ6giatjd2sZPfdC+VIDCbIWRthxkXJ9I/Ap8R98xx/7qIcPFc+XA +hcK1xOM7zIq2xgAOFeeh8O8Wq9cH8NmUhMCgzIE0WT32Zo0JAW6l0kZc82Y/Yofz +U+TJKo0NOFZe687HOhanOHbbQSG29XOqxMYTABZ7Ixf+4RZPD5+yQgZWP1BhLluy +PxZhsLl67xvbfB2i9VVorMN7PbFx5hbni3C7/p63Z0rG5q4/uJBbX3Uuh6KdhIo+ +Zh9UC6u29adIthdxz+ZV5wBccTOgaeHB9wRL9Hbp6ZxyqesQB4RTsFtPNXxZ7K43 +fmJgHZvHhF5gSbeB8JAeBf0cy3pytJM49ZxplifeGVzUJP2gAQKCAQEA/1T9quz5 +sOD03FxV//oRWD1kqfunq3v56sIBG4ZMVZKUqc6wLjTmeklLYKq85AWX8gnCHi0g +nmG/xDh/rt1/IngMWP98WVuD67hFbrj87g7A7YGIiwZ2gi6hqhqmALN+5JjCSTPp +XOiPvNnXP0XM4gIHBXV8diHq5rF9NsSh4vx3OExr8KQqVzWoDcnnWNfnDlrFB8cq +ViII+UqdovXp59hAVOsc+pYAe+8JeQDX17H3U/NMkUw4gU2aWUCvUVjxi9oBG/CW +ncIdYuW8zne4qXbX7YLC0QUUIDVOWzhLauAUBduTqRTldJo0KAxu887tf+uStXs8 +RACLGIaBQw7BXQKCAQEA+38NFnpflKquU92xRtmqWAVaW7rm865ZO6EIaS4JII/N +/Ebu1YZrAhT0ruGJQaolYj8w79BEZRF2CYDPZxKFv/ye0O7rWCAGtCdWQ0BXcrIU +7SdlsdfTNXO1R3WbwCyVxyjg6YF7FjbTaaOAoTiosTjDs2ZOgkbdh/sMeWkSN5HB +aQz4c8rqq0kkYucLqp4nWYSWSJn88bL8ctwEwW77MheJiSpo1ohNRP3ExHnbCbYw +RIj7ATSz74ebpd9NMauB5clvMMh4jRG0EQyt7KCoOyfPRFc3fddvTr03LlgFfX/n +qoxd2nejgAS3NnG1XMxdcUa7cPannt46Sef1uZo3gQKCAQB454zquCYQDKXGBu8u +NAKsjv2wxBqESENyV4VgvDo/NxawRdAFQUV12GkaEB87ti5aDSbfVS0h8lV1G+/S +JM5DyybFqcz/Hyebofk20d/q9g+DJ5g5hMjvIhepTc8Xe+d1ZaRyN2Oke/c8TMbx +DiNTTfR3MEfMRIlPzfHl0jx6GGR3wzBFleb6vsyiIt4qoqmlkXPFGBlDCgDH0v5M +ITgucacczuw8+HSoOut4Yd7TI1FjbkzubHJBQDb7VnbuBTjzqTpnOYiIkVeK8hBy +kBxgGodqz0Vi5o2+Jp/A8Co+JHc2wt/r65ovmali4WhUiMLLlQg2aXGDHeK/rUle +MIl9AoIBAQCPKCYSCnyHypRK5uG3W8VsLzfdCUnXogHnQGXiQTMu1szA8ruWzdnx +qG4TcgxIVYrMHv5DNAEKquLOzATDPjbmLu1ULvvGAQzv1Yhz5ZchkZ7507g+gIUY +YxHoaFjNDlP/txQ3tt2SqoizFD/vBap4nsA/SVgdLiuB8PSL07Rr70rx+lEe0H2+ +HHda2Pu6FiZ9/Uvybb0e8+xhkT4fwYW5YM6IRpzAqXuabv1nfZmiMJPPH04JxK88 +BKwjwjVVtbPOUlg5o5ODcXVXUylZjaXVbna8Bw1uU4hngKt9dNtDMeB0I0x1RC7M +e2Ky2g0LksUJ6uJdjfmiJAt38FLeYJuBAoIBAC2oqaqr86Dug5v8xHpgFoC5u7z7 +BRhaiHpVrUr+wnaNJEXfAEmyKf4xF5xDJqldnYG3c9ETG/7bLcg1dcrMPzXx94Si +MI3ykwiPeI/sVWYmUlq4U8zCIC7MY6sWzWt3oCBNoCN/EeYx9e7+eLNBB+fADAXq +v9RMGlUIy7beX0uac8Bs771dsxIb/RrYw58wz+jrwGlzuDmcPWiu+ARu7hnBqCAV +AITlCV/tsEk7u08oBuv47+rVGCh1Qb19pNswyTtTZARAGErJO0Q+39BNuu0M2TIn +G3M8eNmGHC+mNsZTVgKRuyk9Ye0s4Bo0KcqSndiPFGHjcrF7/t+RqEOXr/E= +-----END RSA PRIVATE KEY----- diff --git a/spec/lib/gitlab/external_authorization/access_spec.rb b/spec/lib/gitlab/external_authorization/access_spec.rb new file mode 100644 index 00000000000..5dc2521b310 --- /dev/null +++ b/spec/lib/gitlab/external_authorization/access_spec.rb @@ -0,0 +1,142 @@ +require 'spec_helper' + +describe Gitlab::ExternalAuthorization::Access, :clean_gitlab_redis_cache do + subject(:access) { described_class.new(build(:user), 'dummy_label') } + + describe '#loaded?' do + it 'is `true` when it was loaded recently' do + Timecop.freeze do + allow(access).to receive(:loaded_at).and_return(5.minutes.ago) + + expect(access).to be_loaded + end + end + + it 'is `false` when there is no loading time' do + expect(access).not_to be_loaded + end + + it 'is `false` when there the result was loaded a long time ago' do + Timecop.freeze do + allow(access).to receive(:loaded_at).and_return(2.weeks.ago) + + expect(access).not_to be_loaded + end + end + end + + describe 'load!' do + let(:fake_client) { double('ExternalAuthorization::Client') } + let(:fake_response) do + double( + 'Response', + 'successful?' => true, + 'valid?' => true, + 'reason' => nil + ) + end + + before do + allow(access).to receive(:load_from_cache) + allow(fake_client).to receive(:request_access).and_return(fake_response) + allow(Gitlab::ExternalAuthorization::Client).to receive(:new) { fake_client } + end + + context 'when loading from the webservice' do + it 'loads from the webservice it the cache was empty' do + expect(access).to receive(:load_from_cache) + expect(access).to receive(:load_from_service).and_call_original + + access.load! + + expect(access).to be_loaded + end + + it 'assigns the accessibility, reason and loaded_at' do + allow(fake_response).to receive(:successful?).and_return(false) + allow(fake_response).to receive(:reason).and_return('Inaccessible label') + + access.load! + + expect(access.reason).to eq('Inaccessible label') + expect(access).not_to have_access + expect(access.loaded_at).not_to be_nil + end + + it 'returns itself' do + expect(access.load!).to eq(access) + end + + it 'stores the result in redis' do + Timecop.freeze do + fake_cache = double + expect(fake_cache).to receive(:store).with(true, nil, Time.now) + expect(access).to receive(:cache).and_return(fake_cache) + + access.load! + end + end + + context 'when the request fails' do + before do + allow(fake_client).to receive(:request_access) do + raise ::Gitlab::ExternalAuthorization::RequestFailed.new('Service unavailable') + end + end + + it 'is loaded' do + access.load! + + expect(access).to be_loaded + end + + it 'assigns the correct accessibility, reason and loaded_at' do + access.load! + + expect(access.reason).to eq('Service unavailable') + expect(access).not_to have_access + expect(access.loaded_at).not_to be_nil + end + + it 'does not store the result in redis' do + fake_cache = double + expect(fake_cache).not_to receive(:store) + allow(access).to receive(:cache).and_return(fake_cache) + + access.load! + end + end + end + + context 'When loading from cache' do + let(:fake_cache) { double('ExternalAuthorization::Cache') } + + before do + allow(access).to receive(:cache).and_return(fake_cache) + end + + it 'does not load from the webservice' do + Timecop.freeze do + expect(fake_cache).to receive(:load).and_return([true, nil, Time.now]) + + expect(access).to receive(:load_from_cache).and_call_original + expect(access).not_to receive(:load_from_service) + + access.load! + end + end + + it 'loads from the webservice when the cached result was too old' do + Timecop.freeze do + expect(fake_cache).to receive(:load).and_return([true, nil, 2.days.ago]) + + expect(access).to receive(:load_from_cache).and_call_original + expect(access).to receive(:load_from_service).and_call_original + allow(fake_cache).to receive(:store) + + access.load! + end + end + end + end +end diff --git a/spec/lib/gitlab/external_authorization/cache_spec.rb b/spec/lib/gitlab/external_authorization/cache_spec.rb new file mode 100644 index 00000000000..58e7d626707 --- /dev/null +++ b/spec/lib/gitlab/external_authorization/cache_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Gitlab::ExternalAuthorization::Cache, :clean_gitlab_redis_cache do + let(:user) { build_stubbed(:user) } + let(:cache_key) { "external_authorization:user-#{user.id}:label-dummy_label" } + + subject(:cache) { described_class.new(user, 'dummy_label') } + + def read_from_redis(key) + Gitlab::Redis::Cache.with do |redis| + redis.hget(cache_key, key) + end + end + + def set_in_redis(key, value) + Gitlab::Redis::Cache.with do |redis| + redis.hmset(cache_key, key, value) + end + end + + describe '#load' do + it 'reads stored info from redis' do + Timecop.freeze do + set_in_redis(:access, false) + set_in_redis(:reason, 'Access denied for now') + set_in_redis(:refreshed_at, Time.now) + + access, reason, refreshed_at = cache.load + + expect(access).to eq(false) + expect(reason).to eq('Access denied for now') + expect(refreshed_at).to be_within(1.second).of(Time.now) + end + end + end + + describe '#store' do + it 'sets the values in redis' do + Timecop.freeze do + cache.store(true, 'the reason', Time.now) + + expect(read_from_redis(:access)).to eq('true') + expect(read_from_redis(:reason)).to eq('the reason') + expect(read_from_redis(:refreshed_at)).to eq(Time.now.to_s) + end + end + end +end diff --git a/spec/lib/gitlab/external_authorization/client_spec.rb b/spec/lib/gitlab/external_authorization/client_spec.rb new file mode 100644 index 00000000000..fa18c1e56e8 --- /dev/null +++ b/spec/lib/gitlab/external_authorization/client_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' + +describe Gitlab::ExternalAuthorization::Client do + let(:user) { build(:user, email: 'dummy_user@example.com') } + let(:dummy_url) { 'https://dummy.net/' } + subject(:client) { described_class.new(user, 'dummy_label') } + + before do + stub_application_setting(external_authorization_service_url: dummy_url) + end + + describe '#request_access' do + it 'performs requests to the configured endpoint' do + expect(Excon).to receive(:post).with(dummy_url, any_args) + + client.request_access + end + + it 'adds the correct params for the user to the body of the request' do + expected_body = { + user_identifier: 'dummy_user@example.com', + project_classification_label: 'dummy_label' + }.to_json + expect(Excon).to receive(:post) + .with(dummy_url, hash_including(body: expected_body)) + + client.request_access + end + + it 'respects the the timeout' do + stub_application_setting( + external_authorization_service_timeout: 3 + ) + + expect(Excon).to receive(:post).with(dummy_url, + hash_including( + connect_timeout: 3, + read_timeout: 3, + write_timeout: 3 + )) + + client.request_access + end + + it 'adds the mutual tls params when they are present' do + stub_application_setting( + external_auth_client_cert: 'the certificate data', + external_auth_client_key: 'the key data', + external_auth_client_key_pass: 'open sesame' + ) + expected_params = { + client_cert_data: 'the certificate data', + client_key_data: 'the key data', + client_key_pass: 'open sesame' + } + + expect(Excon).to receive(:post).with(dummy_url, hash_including(expected_params)) + + client.request_access + end + + it 'returns an expected response' do + expect(Excon).to receive(:post) + + expect(client.request_access) + .to be_kind_of(::Gitlab::ExternalAuthorization::Response) + end + + it 'wraps exceptions if the request fails' do + expect(Excon).to receive(:post) { raise Excon::Error.new('the request broke') } + + expect { client.request_access } + .to raise_error(::Gitlab::ExternalAuthorization::RequestFailed) + end + + describe 'for ldap users' do + let(:user) do + create(:omniauth_user, + email: 'dummy_user@example.com', + extern_uid: 'external id', + provider: 'ldapprovider') + end + + it 'includes the ldap dn for ldap users' do + expected_body = { + user_identifier: 'dummy_user@example.com', + project_classification_label: 'dummy_label', + user_ldap_dn: 'external id' + }.to_json + expect(Excon).to receive(:post) + .with(dummy_url, hash_including(body: expected_body)) + + client.request_access + end + end + end +end diff --git a/spec/lib/gitlab/external_authorization/logger_spec.rb b/spec/lib/gitlab/external_authorization/logger_spec.rb new file mode 100644 index 00000000000..81f1b2390e6 --- /dev/null +++ b/spec/lib/gitlab/external_authorization/logger_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe Gitlab::ExternalAuthorization::Logger do + let(:request_time) { Time.parse('2018-03-26 20:22:15') } + + def fake_access(has_access, user, load_type = :request) + access = double('access') + allow(access).to receive_messages(user: user, + has_access?: has_access, + loaded_at: request_time, + label: 'dummy_label', + load_type: load_type) + + access + end + + describe '.log_access' do + it 'logs a nice message for an access request' do + expected_message = "GRANTED admin@example.com access to 'dummy_label' (the/project/path)" + fake_access = fake_access(true, build(:user, email: 'admin@example.com')) + + expect(described_class).to receive(:info).with(expected_message) + + described_class.log_access(fake_access, 'the/project/path') + end + + it 'does not trip without a project path' do + expected_message = "DENIED admin@example.com access to 'dummy_label'" + fake_access = fake_access(false, build(:user, email: 'admin@example.com')) + + expect(described_class).to receive(:info).with(expected_message) + + described_class.log_access(fake_access, nil) + end + + it 'adds the load time for cached accesses' do + expected_message = "DENIED admin@example.com access to 'dummy_label' - cache #{request_time}" + fake_access = fake_access(false, build(:user, email: 'admin@example.com'), :cache) + + expect(described_class).to receive(:info).with(expected_message) + + described_class.log_access(fake_access, nil) + end + end +end diff --git a/spec/lib/gitlab/external_authorization/response_spec.rb b/spec/lib/gitlab/external_authorization/response_spec.rb new file mode 100644 index 00000000000..43211043eca --- /dev/null +++ b/spec/lib/gitlab/external_authorization/response_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe Gitlab::ExternalAuthorization::Response do + let(:excon_response) { double } + subject(:response) { described_class.new(excon_response) } + + describe '#valid?' do + it 'is valid for 200, 401, and 403 responses' do + [200, 401, 403].each do |status| + allow(excon_response).to receive(:status).and_return(status) + + expect(response).to be_valid + end + end + + it "is invalid for other statuses" do + expect(excon_response).to receive(:status).and_return(500) + + expect(response).not_to be_valid + end + end + + describe '#reason' do + it 'returns a reason if it was included in the response body' do + expect(excon_response).to receive(:body).and_return({ reason: 'Not authorized' }.to_json) + + expect(response.reason).to eq('Not authorized') + end + + it 'returns nil when there was no body' do + expect(excon_response).to receive(:body).and_return('') + + expect(response.reason).to eq(nil) + end + end + + describe '#successful?' do + it 'is `true` if the status is 200' do + allow(excon_response).to receive(:status).and_return(200) + + expect(response).to be_successful + end + + it 'is `false` if the status is 401 or 403' do + [401, 403].each do |status| + allow(excon_response).to receive(:status).and_return(status) + + expect(response).not_to be_successful + end + end + end +end diff --git a/spec/lib/gitlab/external_authorization_spec.rb b/spec/lib/gitlab/external_authorization_spec.rb new file mode 100644 index 00000000000..7394fbfe0ce --- /dev/null +++ b/spec/lib/gitlab/external_authorization_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Gitlab::ExternalAuthorization, :request_store do + include ExternalAuthorizationServiceHelpers + + let(:user) { build(:user) } + let(:label) { 'dummy_label' } + + describe '#access_allowed?' do + it 'is always true when the feature is disabled' do + # Not using `stub_application_setting` because the method is prepended in + # `EE::ApplicationSetting` which breaks when using `any_instance` + # https://gitlab.com/gitlab-org/gitlab-ce/issues/33587 + expect(::Gitlab::CurrentSettings.current_application_settings) + .to receive(:external_authorization_service_enabled) { false } + + expect(described_class).not_to receive(:access_for_user_to_label) + + expect(described_class.access_allowed?(user, label)).to be_truthy + end + end + + describe '#rejection_reason' do + it 'is always nil when the feature is disabled' do + expect(::Gitlab::CurrentSettings.current_application_settings) + .to receive(:external_authorization_service_enabled) { false } + + expect(described_class).not_to receive(:access_for_user_to_label) + + expect(described_class.rejection_reason(user, label)).to be_nil + end + end + + describe '#access_for_user_to_label' do + it 'only loads the access once per request' do + enable_external_authorization_service_check + + expect(::Gitlab::ExternalAuthorization::Access) + .to receive(:new).with(user, label).once.and_call_original + + 2.times { described_class.access_for_user_to_label(user, label, nil) } + end + + it 'logs the access request once per request' do + expect(::Gitlab::ExternalAuthorization::Logger) + .to receive(:log_access) + .with(an_instance_of(::Gitlab::ExternalAuthorization::Access), + 'the/project/path') + .once + + 2.times { described_class.access_for_user_to_label(user, label, 'the/project/path') } + end + end +end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 06995604a24..ebb62124cb1 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -496,6 +496,7 @@ Project: - merge_requests_ff_only_enabled - merge_requests_rebase_enabled - jobs_cache_index +- external_authorization_classification_label - pages_https_only Author: - name diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index c81572d739e..c7d7dbac736 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe ApplicationSetting do - let(:setting) { described_class.create_from_defaults } + subject(:setting) { described_class.create_from_defaults } it { include(CacheableAttributes) } it { include(ApplicationSettingImplementation) } @@ -284,6 +284,52 @@ describe ApplicationSetting do expect(subject).to be_valid end end + + describe 'when external authorization service is enabled' do + before do + setting.external_authorization_service_enabled = true + end + + it { is_expected.not_to allow_value('not a URL').for(:external_authorization_service_url) } + it { is_expected.to allow_value('https://example.com').for(:external_authorization_service_url) } + it { is_expected.to allow_value('').for(:external_authorization_service_url) } + it { is_expected.not_to allow_value(nil).for(:external_authorization_service_default_label) } + it { is_expected.not_to allow_value(11).for(:external_authorization_service_timeout) } + it { is_expected.not_to allow_value(0).for(:external_authorization_service_timeout) } + it { is_expected.not_to allow_value('not a certificate').for(:external_auth_client_cert) } + it { is_expected.to allow_value('').for(:external_auth_client_cert) } + it { is_expected.to allow_value('').for(:external_auth_client_key) } + + context 'when setting a valid client certificate for external authorization' do + let(:certificate_data) { File.read('spec/fixtures/passphrase_x509_certificate.crt') } + + before do + setting.external_auth_client_cert = certificate_data + end + + it 'requires a valid client key when a certificate is set' do + expect(setting).not_to allow_value('fefefe').for(:external_auth_client_key) + end + + it 'requires a matching certificate' do + other_private_key = File.read('spec/fixtures/x509_certificate_pk.key') + + expect(setting).not_to allow_value(other_private_key).for(:external_auth_client_key) + end + + it 'the credentials are valid when the private key can be read and matches the certificate' do + tls_attributes = [:external_auth_client_key_pass, + :external_auth_client_key, + :external_auth_client_cert] + setting.external_auth_client_key = File.read('spec/fixtures/passphrase_x509_certificate_pk.key') + setting.external_auth_client_key_pass = '5iveL!fe' + + setting.validate + + expect(setting.errors).not_to include(*tls_attributes) + end + end + end end context 'restrict creating duplicates' do diff --git a/spec/models/concerns/protected_ref_access_spec.rb b/spec/models/concerns/protected_ref_access_spec.rb index 94798f0590d..f63ad958ed3 100644 --- a/spec/models/concerns/protected_ref_access_spec.rb +++ b/spec/models/concerns/protected_ref_access_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe ProtectedRefAccess do + include ExternalAuthorizationServiceHelpers + subject(:protected_ref_access) do create(:protected_branch, :maintainers_can_push).push_access_levels.first end @@ -29,5 +31,15 @@ describe ProtectedRefAccess do expect(protected_ref_access.check_access(developer)).to be_falsy end + + context 'external authorization' do + it 'is false if external authorization denies access' do + maintainer = create(:user) + project.add_maintainer(maintainer) + external_service_deny_access(maintainer, project) + + expect(protected_ref_access.check_access(maintainer)).to be_falsey + end + end end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 892dd053e39..0cd69cb4817 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe Issue do + include ExternalAuthorizationServiceHelpers + describe "Associations" do it { is_expected.to belong_to(:milestone) } it { is_expected.to have_many(:assignees) } @@ -779,4 +781,47 @@ describe Issue do it_behaves_like 'throttled touch' do subject { create(:issue, updated_at: 1.hour.ago) } end + + context 'when an external authentication service' do + before do + enable_external_authorization_service_check + end + + describe '#visible_to_user?' do + it 'is `false` when an external authorization service is enabled' do + issue = build(:issue, project: build(:project, :public)) + + expect(issue).not_to be_visible_to_user + end + + it 'checks the external service to determine if an issue is readable by a user' do + project = build(:project, :public, + external_authorization_classification_label: 'a-label') + issue = build(:issue, project: project) + user = build(:user) + + expect(::Gitlab::ExternalAuthorization).to receive(:access_allowed?).with(user, 'a-label') { false } + expect(issue.visible_to_user?(user)).to be_falsy + end + + it 'does not check the external service if a user does not have access to the project' do + project = build(:project, :private, + external_authorization_classification_label: 'a-label') + issue = build(:issue, project: project) + user = build(:user) + + expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?) + expect(issue.visible_to_user?(user)).to be_falsy + end + + it 'does not check the external webservice for admins' do + issue = build(:issue) + user = build(:admin) + + expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?) + + issue.visible_to_user?(user) + end + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 5eb31430ccd..7222580e115 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' describe Project do include ProjectForksHelper include GitHelpers + include ExternalAuthorizationServiceHelpers it_behaves_like 'having unique enum values' @@ -4417,6 +4418,25 @@ describe Project do end end + describe '#external_authorization_classification_label' do + it 'falls back to the default when none is configured' do + enable_external_authorization_service_check + + expect(build(:project).external_authorization_classification_label) + .to eq('default_label') + end + + it 'returns the classification label if it was configured on the project' do + enable_external_authorization_service_check + + project = build(:project, + external_authorization_classification_label: 'hello') + + expect(project.external_authorization_classification_label) + .to eq('hello') + end + end + describe "#pages_https_only?" do subject { build(:project) } diff --git a/spec/policies/base_policy_spec.rb b/spec/policies/base_policy_spec.rb index c03d95b34db..09be831dcd5 100644 --- a/spec/policies/base_policy_spec.rb +++ b/spec/policies/base_policy_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe BasePolicy do + include ExternalAuthorizationServiceHelpers + describe '.class_for' do it 'detects policy class based on the subject ancestors' do expect(DeclarativePolicy.class_for(GenericCommitStatus.new)).to eq(CommitStatusPolicy) @@ -16,4 +18,25 @@ describe BasePolicy do expect(DeclarativePolicy.class_for(:global)).to eq(GlobalPolicy) end end + + describe 'read cross project' do + let(:current_user) { create(:user) } + let(:user) { create(:user) } + + subject { described_class.new(current_user, [user]) } + + it { is_expected.to be_allowed(:read_cross_project) } + + context 'when an external authorization service is enabled' do + before do + enable_external_authorization_service_check + end + + it { is_expected.not_to be_allowed(:read_cross_project) } + + it 'allows admins' do + expect(described_class.new(build(:admin), nil)).to be_allowed(:read_cross_project) + end + end + end end diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb index 008d118b557..b149dbcf871 100644 --- a/spec/policies/issue_policy_spec.rb +++ b/spec/policies/issue_policy_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe IssuePolicy do + include ExternalAuthorizationServiceHelpers + let(:guest) { create(:user) } let(:author) { create(:user) } let(:assignee) { create(:user) } @@ -204,4 +206,21 @@ describe IssuePolicy do end end end + + context 'with external authorization enabled' do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project) } + let(:policies) { described_class.new(user, issue) } + + before do + enable_external_authorization_service_check + end + + it 'can read the issue iid without accessing the external service' do + expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?) + + expect(policies).to be_allowed(:read_issue_iid) + end + end end diff --git a/spec/policies/merge_request_policy_spec.rb b/spec/policies/merge_request_policy_spec.rb index 1efa70addc2..81279225d61 100644 --- a/spec/policies/merge_request_policy_spec.rb +++ b/spec/policies/merge_request_policy_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe MergeRequestPolicy do + include ExternalAuthorizationServiceHelpers + let(:guest) { create(:user) } let(:author) { create(:user) } let(:developer) { create(:user) } @@ -47,4 +49,21 @@ describe MergeRequestPolicy do expect(permissions(guest, merge_request_locked)).to be_disallowed(:reopen_merge_request) end end + + context 'with external authorization enabled' do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:policies) { described_class.new(user, merge_request) } + + before do + enable_external_authorization_service_check + end + + it 'can read the issue iid without accessing the external service' do + expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?) + + expect(policies).to be_allowed(:read_merge_request_iid) + end + end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 125ed818bc6..42f8bf3137b 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe ProjectPolicy do + include ExternalAuthorizationServiceHelpers include_context 'ProjectPolicy context' set(:guest) { create(:user) } set(:reporter) { create(:user) } @@ -292,4 +293,56 @@ describe ProjectPolicy do projects: [clusterable]) end end + + context 'reading a project' do + it 'allows access when a user has read access to the repo' do + expect(described_class.new(owner, project)).to be_allowed(:read_project) + expect(described_class.new(developer, project)).to be_allowed(:read_project) + expect(described_class.new(admin, project)).to be_allowed(:read_project) + end + + it 'never checks the external service' do + expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?) + + expect(described_class.new(owner, project)).to be_allowed(:read_project) + end + + context 'with an external authorization service' do + before do + enable_external_authorization_service_check + end + + it 'allows access when the external service allows it' do + external_service_allow_access(owner, project) + external_service_allow_access(developer, project) + + expect(described_class.new(owner, project)).to be_allowed(:read_project) + expect(described_class.new(developer, project)).to be_allowed(:read_project) + end + + it 'does not check the external service for admins and allows access' do + expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?) + + expect(described_class.new(admin, project)).to be_allowed(:read_project) + end + + it 'prevents all but seeing a public project in a list when access is denied' do + [developer, owner, build(:user), nil].each do |user| + external_service_deny_access(user, project) + policy = described_class.new(user, project) + + expect(policy).not_to be_allowed(:read_project) + expect(policy).not_to be_allowed(:owner_access) + expect(policy).not_to be_allowed(:change_namespace) + end + end + + it 'passes the full path to external authorization for logging purposes' do + expect(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(owner, 'default_label', project.full_path).and_call_original + + described_class.new(owner, project).allowed?(:read_project) + end + end + end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 2bfb17d9c9a..352ea448c00 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -46,6 +46,8 @@ shared_examples 'languages and percentages JSON response' do end describe API::Projects do + include ExternalAuthorizationServiceHelpers + let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } @@ -1336,6 +1338,39 @@ describe API::Projects do end end end + + context 'with external authorization' do + let(:project) do + create(:project, + namespace: user.namespace, + external_authorization_classification_label: 'the-label') + end + + context 'when the user has access to the project' do + before do + external_service_allow_access(user, project) + end + + it 'includes the label in the response' do + get api("/projects/#{project.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['external_authorization_classification_label']).to eq('the-label') + end + end + + context 'when the external service denies access' do + before do + external_service_deny_access(user, project) + end + + it 'returns a 404' do + get api("/projects/#{project.id}", user) + + expect(response).to have_gitlab_http_status(404) + end + end + end end describe 'GET /projects/:id/users' do @@ -1890,6 +1925,20 @@ describe API::Projects do expect(response).to have_gitlab_http_status(403) end end + + context 'when updating external classification' do + before do + enable_external_authorization_service_check + end + + it 'updates the classification label' do + put(api("/projects/#{project.id}", user), params: { external_authorization_classification_label: 'new label' }) + + expect(response).to have_gitlab_http_status(200) + + expect(project.reload.external_authorization_classification_label).to eq('new label') + end + end end describe 'POST /projects/:id/archive' do diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index f869325e892..527ab1cfb66 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -116,6 +116,39 @@ describe API::Settings, 'Settings' do expect(json_response['performance_bar_allowed_group_id']).to be_nil end + context 'external policy classification settings' do + let(:settings) do + { + external_authorization_service_enabled: true, + external_authorization_service_url: 'https://custom.service/', + external_authorization_service_default_label: 'default', + external_authorization_service_timeout: 9.99, + external_auth_client_cert: File.read('spec/fixtures/passphrase_x509_certificate.crt'), + external_auth_client_key: File.read('spec/fixtures/passphrase_x509_certificate_pk.key'), + external_auth_client_key_pass: "5iveL!fe" + } + end + let(:attribute_names) { settings.keys.map(&:to_s) } + + it 'includes the attributes in the API' do + get api("/application/settings", admin) + + expect(response).to have_gitlab_http_status(200) + attribute_names.each do |attribute| + expect(json_response.keys).to include(attribute) + end + end + + it 'allows updating the settings' do + put api("/application/settings", admin), params: settings + + expect(response).to have_gitlab_http_status(200) + settings.each do |attribute, value| + expect(ApplicationSetting.current.public_send(attribute)).to eq(value) + end + end + end + context "missing plantuml_url value when plantuml_enabled is true" do it "returns a blank parameter error message" do put api("/application/settings", admin), params: { plantuml_enabled: true } diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb index d02b4c554b1..b58d95ccb43 100644 --- a/spec/serializers/group_child_entity_spec.rb +++ b/spec/serializers/group_child_entity_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe GroupChildEntity do + include ExternalAuthorizationServiceHelpers include Gitlab::Routing.url_helpers let(:user) { create(:user) } @@ -109,4 +110,22 @@ describe GroupChildEntity do it_behaves_like 'group child json' end + + describe 'for a project with external authorization enabled' do + let(:object) do + create(:project, :with_avatar, + description: 'Awesomeness') + end + + before do + enable_external_authorization_service_check + object.add_maintainer(user) + end + + it 'does not hit the external authorization service' do + expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?) + + expect(json[:can_edit]).to eq(false) + end + end end diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb index a4a733eff77..258e5635113 100644 --- a/spec/services/application_settings/update_service_spec.rb +++ b/spec/services/application_settings/update_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe ApplicationSettings::UpdateService do + include ExternalAuthorizationServiceHelpers + let(:application_settings) { create(:application_setting) } let(:admin) { create(:user, :admin) } let(:params) { {} } @@ -143,4 +145,37 @@ describe ApplicationSettings::UpdateService do end end end + + context 'when external authorization is enabled' do + before do + enable_external_authorization_service_check + end + + it 'does not save the settings with an error if the service denies access' do + expect(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(admin, 'new-label') { false } + + described_class.new(application_settings, admin, { external_authorization_service_default_label: 'new-label' }).execute + + expect(application_settings.errors[:external_authorization_service_default_label]).to be_present + end + + it 'saves the setting when the user has access to the label' do + expect(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(admin, 'new-label') { true } + + described_class.new(application_settings, admin, { external_authorization_service_default_label: 'new-label' }).execute + + # Read the attribute directly to avoid the stub from + # `enable_external_authorization_service_check` + expect(application_settings[:external_authorization_service_default_label]).to eq('new-label') + end + + it 'does not validate the label if it was not passed' do + expect(::Gitlab::ExternalAuthorization) + .not_to receive(:access_allowed?) + + described_class.new(application_settings, admin, { home_page_url: 'http://foo.bar' }).execute + end + end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 14c73852e65..ac4aabf3fbd 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe NotificationService, :mailer do include EmailSpec::Matchers + include ExternalAuthorizationServiceHelpers include NotificationHelpers let(:notification) { described_class.new } @@ -2218,6 +2219,46 @@ describe NotificationService, :mailer do end end + context 'with external authorization service' do + let(:issue) { create(:issue) } + let(:project) { issue.project } + let(:note) { create(:note, noteable: issue, project: project) } + let(:member) { create(:user) } + + subject { NotificationService.new } + + before do + project.add_maintainer(member) + member.global_notification_setting.update!(level: :watch) + end + + it 'sends email when the service is not enabled' do + expect(Notify).to receive(:new_issue_email).at_least(:once).with(member.id, issue.id, nil).and_call_original + + subject.new_issue(issue, member) + end + + context 'when the service is enabled' do + before do + enable_external_authorization_service_check + end + + it 'does not send an email' do + expect(Notify).not_to receive(:new_issue_email) + + subject.new_issue(issue, member) + end + + it 'still delivers email to admins' do + member.update!(admin: true) + + expect(Notify).to receive(:new_issue_email).at_least(:once).with(member.id, issue.id, nil).and_call_original + + subject.new_issue(issue, member) + end + end + end + def build_team(project) @u_watcher = create_global_setting_for(create(:user), :watch) @u_participating = create_global_setting_for(create(:user), :participating) diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index e8418b09dc2..e1ec932918e 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe Projects::CreateService, '#execute' do + include ExternalAuthorizationServiceHelpers include GitHelpers let(:gitlab_shell) { Gitlab::Shell.new } @@ -344,6 +345,42 @@ describe Projects::CreateService, '#execute' do expect(rugged.config['gitlab.fullpath']).to eq project.full_path end + context 'with external authorization enabled' do + before do + enable_external_authorization_service_check + end + + it 'does not save the project with an error if the service denies access' do + expect(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(user, 'new-label', any_args) { false } + + project = create_project(user, opts.merge({ external_authorization_classification_label: 'new-label' })) + + expect(project.errors[:external_authorization_classification_label]).to be_present + expect(project).not_to be_persisted + end + + it 'saves the project when the user has access to the label' do + expect(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(user, 'new-label', any_args) { true } + + project = create_project(user, opts.merge({ external_authorization_classification_label: 'new-label' })) + + expect(project).to be_persisted + expect(project.external_authorization_classification_label).to eq('new-label') + end + + it 'does not save the project when the user has no access to the default label and no label is provided' do + expect(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(user, 'default_label', any_args) { false } + + project = create_project(user, opts) + + expect(project.errors[:external_authorization_classification_label]).to be_present + expect(project).not_to be_persisted + end + end + def create_project(user, opts) Projects::CreateService.new(user, opts).execute end diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 90eaea9c872..95eb17b5e3a 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe Projects::UpdateService do + include ExternalAuthorizationServiceHelpers include ProjectForksHelper let(:user) { create(:user) } @@ -361,6 +362,46 @@ describe Projects::UpdateService do call_service end end + + context 'with external authorization enabled' do + before do + enable_external_authorization_service_check + end + + it 'does not save the project with an error if the service denies access' do + expect(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(user, 'new-label') { false } + + result = update_project(project, user, { external_authorization_classification_label: 'new-label' }) + + expect(result[:message]).to be_present + expect(result[:status]).to eq(:error) + end + + it 'saves the new label if the service allows access' do + expect(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(user, 'new-label') { true } + + result = update_project(project, user, { external_authorization_classification_label: 'new-label' }) + + expect(result[:status]).to eq(:success) + expect(project.reload.external_authorization_classification_label).to eq('new-label') + end + + it 'checks the default label when the classification label was cleared' do + expect(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(user, 'default_label') { true } + + update_project(project, user, { external_authorization_classification_label: '' }) + end + + it 'does not check the label when it does not change' do + expect(::Gitlab::ExternalAuthorization) + .not_to receive(:access_allowed?) + + update_project(project, user, { name: 'New name' }) + end + end end describe '#run_auto_devops_pipeline?' do diff --git a/spec/support/external_authorization_service_helpers.rb b/spec/support/external_authorization_service_helpers.rb new file mode 100644 index 00000000000..79dd9a3d58e --- /dev/null +++ b/spec/support/external_authorization_service_helpers.rb @@ -0,0 +1,33 @@ +module ExternalAuthorizationServiceHelpers + def enable_external_authorization_service_check + stub_application_setting(external_authorization_service_enabled: true) + + stub_application_setting(external_authorization_service_url: 'https://authorize.me') + stub_application_setting(external_authorization_service_default_label: 'default_label') + stub_request(:post, "https://authorize.me").to_return(status: 200) + end + + def external_service_set_access(allowed, user, project) + enable_external_authorization_service_check + classification_label = ::Gitlab::CurrentSettings.current_application_settings + .external_authorization_service_default_label + + # Reload the project so cached licensed features are reloaded + if project + classification_label = Project.find(project.id).external_authorization_classification_label + end + + allow(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?) + .with(user, classification_label, any_args) + .and_return(allowed) + end + + def external_service_allow_access(user, project = nil) + external_service_set_access(true, user, project) + end + + def external_service_deny_access(user, project = nil) + external_service_set_access(false, user, project) + end +end diff --git a/spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb b/spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb new file mode 100644 index 00000000000..8dd78fd0a25 --- /dev/null +++ b/spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +shared_examples 'disabled when using an external authorization service' do + include ExternalAuthorizationServiceHelpers + + it 'works when the feature is not enabled' do + subject + + expect(response).to be_success + end + + it 'renders a 404 with a message when the feature is enabled' do + enable_external_authorization_service_check + + subject + + expect(response).to have_gitlab_http_status(403) + end +end + +shared_examples 'unauthorized when external service denies access' do + include ExternalAuthorizationServiceHelpers + + it 'allows access when the authorization service allows it' do + external_service_allow_access(user, project) + + subject + + # Account for redirects after updates + expect(response.status).to be_between(200, 302) + end + + it 'allows access when the authorization service denies it' do + external_service_deny_access(user, project) + + subject + + expect(response).to have_gitlab_http_status(403) + end +end diff --git a/spec/support/shared_examples/finders/finder_with_external_authorization_enabled.rb b/spec/support/shared_examples/finders/finder_with_external_authorization_enabled.rb new file mode 100644 index 00000000000..d7e17cc0b70 --- /dev/null +++ b/spec/support/shared_examples/finders/finder_with_external_authorization_enabled.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +shared_examples 'a finder with external authorization service' do + include ExternalAuthorizationServiceHelpers + + let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + project.add_maintainer(user) + end + + it 'finds the subject' do + expect(described_class.new(user).execute).to include(subject) + end + + context 'with an external authorization service' do + before do + enable_external_authorization_service_check + end + + it 'does not include the subject when no project was given' do + expect(described_class.new(user).execute).not_to include(subject) + end + + it 'includes the subject when a project id was given' do + expect(described_class.new(user, project_params).execute).to include(subject) + end + end +end diff --git a/spec/validators/x509_certificate_credentials_validator_spec.rb b/spec/validators/x509_certificate_credentials_validator_spec.rb new file mode 100644 index 00000000000..24ef68c1fab --- /dev/null +++ b/spec/validators/x509_certificate_credentials_validator_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe X509CertificateCredentialsValidator do + let(:certificate_data) { File.read('spec/fixtures/x509_certificate.crt') } + let(:pkey_data) { File.read('spec/fixtures/x509_certificate_pk.key') } + + let(:validatable) do + Class.new do + include ActiveModel::Validations + + attr_accessor :certificate, :private_key, :passphrase + + def initialize(certificate, private_key, passphrase = nil) + @certificate, @private_key, @passphrase = certificate, private_key, passphrase + end + end + end + + subject(:validator) do + described_class.new(certificate: :certificate, pkey: :private_key) + end + + it 'is not valid when the certificate is not valid' do + record = validatable.new('not a certificate', nil) + + validator.validate(record) + + expect(record.errors[:certificate]).to include('is not a valid X509 certificate.') + end + + it 'is not valid without a certificate' do + record = validatable.new(nil, nil) + + validator.validate(record) + + expect(record.errors[:certificate]).not_to be_empty + end + + context 'when a valid certificate is passed' do + let(:record) { validatable.new(certificate_data, nil) } + + it 'does not track an error for the certificate' do + validator.validate(record) + + expect(record.errors[:certificate]).to be_empty + end + + it 'adds an error when not passing a correct private key' do + validator.validate(record) + + expect(record.errors[:private_key]).to include('could not read private key, is the passphrase correct?') + end + + it 'has no error when the private key is correct' do + record.private_key = pkey_data + + validator.validate(record) + + expect(record.errors).to be_empty + end + end + + context 'when using a passphrase' do + let(:passphrase_certificate_data) { File.read('spec/fixtures/passphrase_x509_certificate.crt') } + let(:passphrase_pkey_data) { File.read('spec/fixtures/passphrase_x509_certificate_pk.key') } + + let(:record) { validatable.new(passphrase_certificate_data, passphrase_pkey_data, '5iveL!fe') } + + subject(:validator) do + described_class.new(certificate: :certificate, pkey: :private_key, pass: :passphrase) + end + + it 'is valid with the correct data' do + validator.validate(record) + + expect(record.errors).to be_empty + end + + it 'adds an error when the passphrase is wrong' do + record.passphrase = 'wrong' + + validator.validate(record) + + expect(record.errors[:private_key]).not_to be_empty + end + end +end -- cgit v1.2.1 From 15e2b5ad0b2bdd0ccc0e5ebd47b78c0cd064dbea Mon Sep 17 00:00:00 2001 From: Daniel Wyatt Date: Tue, 9 Apr 2019 11:51:01 -0400 Subject: Address style review comment --- spec/lib/gitlab/legacy_github_import/project_creator_spec.rb | 2 -- 1 file changed, 2 deletions(-) (limited to 'spec') diff --git a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb index 3aba744458f..8675d8691c8 100644 --- a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb @@ -49,7 +49,6 @@ describe Gitlab::LegacyGithubImport::ProjectCreator do context 'when GitHub project is public' do it 'sets project visibility to namespace visibility level' do repo.private = false - project = service.execute expect(project.visibility_level).to eq(namespace.visibility_level) @@ -60,7 +59,6 @@ describe Gitlab::LegacyGithubImport::ProjectCreator do it 'sets project visibility to user namespace visibility level' do repo.private = false - project = service.execute expect(project.visibility_level).to eq(user.namespace.visibility_level) -- cgit v1.2.1 From d69d29011cf9fe06e50a2c7d65b1ea88ea2d41d5 Mon Sep 17 00:00:00 2001 From: Vladimir Shushlin Date: Tue, 9 Apr 2019 17:46:29 +0000 Subject: Mark unverified pages domains for removal Set pages_domain.remove_at when disabling it Add specs for marking pages domain for removal Notify user that domain is being removed Add documentation --- spec/factories/pages_domains.rb | 4 + spec/mailers/emails/pages_domains_spec.rb | 24 ++++++ spec/services/verify_pages_domain_service_spec.rb | 89 +++++++++++++++++------ 3 files changed, 96 insertions(+), 21 deletions(-) (limited to 'spec') diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb index 20671da016e..b74f72f2bd3 100644 --- a/spec/factories/pages_domains.rb +++ b/spec/factories/pages_domains.rb @@ -41,6 +41,10 @@ nNp/xedE1YxutQ== enabled_until nil end + trait :scheduled_for_removal do + remove_at { 1.day.from_now } + end + trait :unverified do verified_at nil end diff --git a/spec/mailers/emails/pages_domains_spec.rb b/spec/mailers/emails/pages_domains_spec.rb index c74fd66ad22..050af587061 100644 --- a/spec/mailers/emails/pages_domains_spec.rb +++ b/spec/mailers/emails/pages_domains_spec.rb @@ -26,6 +26,26 @@ describe Emails::PagesDomains do end end + shared_examples 'notification about upcoming domain removal' do + context 'when domain is not scheduled for removal' do + it 'asks user to remove it' do + is_expected.to have_body_text 'please remove it' + end + end + + context 'when domain is scheduled for removal' do + before do + domain.update!(remove_at: 1.week.from_now) + end + it 'notifies user that domain will be removed automatically' do + aggregate_failures do + is_expected.to have_body_text domain.remove_at.strftime('%F %T') + is_expected.to have_body_text "it will be removed from your GitLab project" + end + end + end + end + describe '#pages_domain_enabled_email' do let(:email_subject) { "#{project.path} | GitLab Pages domain '#{domain.domain}' has been enabled" } @@ -43,6 +63,8 @@ describe Emails::PagesDomains do it_behaves_like 'a pages domain email' + it_behaves_like 'notification about upcoming domain removal' + it { is_expected.to have_body_text 'has been disabled' } end @@ -63,6 +85,8 @@ describe Emails::PagesDomains do it_behaves_like 'a pages domain email' + it_behaves_like 'notification about upcoming domain removal' + it 'says verification has failed and when the domain is enabled until' do is_expected.to have_body_text 'Verification has failed' is_expected.to have_body_text domain.enabled_until.strftime('%F %T') diff --git a/spec/services/verify_pages_domain_service_spec.rb b/spec/services/verify_pages_domain_service_spec.rb index ddf9d2b4917..b60b13bb3fc 100644 --- a/spec/services/verify_pages_domain_service_spec.rb +++ b/spec/services/verify_pages_domain_service_spec.rb @@ -27,6 +27,7 @@ describe VerifyPagesDomainService do expect(domain).to be_verified expect(domain).to be_enabled + expect(domain.remove_at).to be_nil end end @@ -48,18 +49,32 @@ describe VerifyPagesDomainService do end end + shared_examples 'unverifies and disables domain' do + it 'unverifies domain' do + expect(service.execute).to eq(error_status) + expect(domain).not_to be_verified + end + + it 'disables domain and shedules it for removal' do + Timecop.freeze do + service.execute + expect(domain).not_to be_enabled + expect(domain.remove_at).to be_within(1.second).of(1.week.from_now) + end + end + end + context 'when domain is disabled(or new)' do let(:domain) { create(:pages_domain, :disabled) } include_examples 'successful enablement and verification' - shared_examples 'unverifies and disables domain' do - it 'unverifies and disables domain' do - expect(service.execute).to eq(error_status) - - expect(domain).not_to be_verified - expect(domain).not_to be_enabled + context 'when txt record does not contain verification code' do + before do + stub_resolver(domain_name => 'something else') end + + include_examples 'unverifies and disables domain' end context 'when txt record does not contain verification code' do @@ -84,16 +99,25 @@ describe VerifyPagesDomainService do include_examples 'successful enablement and verification' - context 'when txt record does not contain verification code' do - before do - stub_resolver(domain_name => 'something else') - end - + shared_examples 'unverifing domain' do it 'unverifies but does not disable domain' do expect(service.execute).to eq(error_status) expect(domain).not_to be_verified expect(domain).to be_enabled end + + it 'does not schedule domain for removal' do + service.execute + expect(domain.remove_at).to be_nil + end + end + + context 'when txt record does not contain verification code' do + before do + stub_resolver(domain_name => 'something else') + end + + include_examples 'unverifing domain' end context 'when no txt records are present' do @@ -101,11 +125,7 @@ describe VerifyPagesDomainService do stub_resolver end - it 'unverifies but does not disable domain' do - expect(service.execute).to eq(error_status) - expect(domain).not_to be_verified - expect(domain).to be_enabled - end + include_examples 'unverifing domain' end end @@ -125,13 +145,40 @@ describe VerifyPagesDomainService do stub_resolver end - it 'disables domain' do - error_status[:message] += '. It is now disabled.' + let(:error_status) { { status: :error, message: "Couldn't verify #{domain.domain}. It is now disabled." } } - expect(service.execute).to eq(error_status) + include_examples 'unverifies and disables domain' + end + end - expect(domain).not_to be_verified - expect(domain).not_to be_enabled + context 'when domain is disabled and scheduled for removal' do + let(:domain) { create(:pages_domain, :disabled, :scheduled_for_removal) } + + context 'when the right code is present' do + before do + stub_resolver(domain.domain => domain.keyed_verification_code) + end + + it 'verifies and enables domain' do + expect(service.execute).to eq(status: :success) + + expect(domain).to be_verified + expect(domain).to be_enabled + end + + it 'prevent domain from being removed' do + expect { service.execute }.to change { domain.remove_at }.to(nil) + end + end + + context 'when the right code is not present' do + before do + stub_resolver + end + + it 'keeps domain scheduled for removal but does not change removal time' do + expect { service.execute }.not_to change { domain.remove_at } + expect(domain.remove_at).to be_present end end end -- cgit v1.2.1 From 8fc2347fa96f2594f04a9482fbdca34eab349f15 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Tue, 9 Apr 2019 11:04:26 -0700 Subject: Quarantine flaky test --- spec/support/features/discussion_comments_shared_example.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb index 5b79c40f27b..542f533d590 100644 --- a/spec/support/features/discussion_comments_shared_example.rb +++ b/spec/support/features/discussion_comments_shared_example.rb @@ -7,7 +7,7 @@ shared_examples 'discussion comments' do |resource_name| let(:close_selector) { "#{form_selector} .btn-comment-and-close" } let(:comments_selector) { '.timeline > .note.timeline-entry' } - it 'clicking "Comment" will post a comment' do + it 'clicking "Comment" will post a comment', :quarantine do expect(page).to have_selector toggle_selector find("#{form_selector} .note-textarea").send_keys('a') -- cgit v1.2.1 From f9b4d5efbde0bf3b6b1822177d47199ad2f02fbd Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Tue, 9 Apr 2019 20:30:52 +0100 Subject: Fix MR assignees for push options With multiple MR assignees, `merge_request.assignee_id` is always nil, and `merge_request.assignee_ids` isn't in `merge_request.attributes`. So the existing code doesn't set assignees in the created MR. This fix gets all the tests passing, but we should also check that no other associations in the MergeRequest need similar fixups. --- spec/services/merge_requests/push_options_handler_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb index 686b4b49f24..f7a39bb42d5 100644 --- a/spec/services/merge_requests/push_options_handler_service_spec.rb +++ b/spec/services/merge_requests/push_options_handler_service_spec.rb @@ -38,7 +38,7 @@ describe MergeRequests::PushOptionsHandlerService do it 'assigns the MR to the user' do service.execute - expect(last_mr.assignee).to eq(user) + expect(last_mr.assignees).to contain_exactly(user) end context 'when project has been forked' do -- cgit v1.2.1 From bcfd04a2210c88997332c069493fb008e5d39d90 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 10 Apr 2019 03:36:09 +0000 Subject: Revert "Merge branch 'sh-optimize-projects-api' into 'master'" This reverts merge request !26481 --- spec/models/project_spec.rb | 49 +-------------------------------------------- 1 file changed, 1 insertion(+), 48 deletions(-) (limited to 'spec') diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 7222580e115..7f8d2ff91fd 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2722,7 +2722,7 @@ describe Project do end describe '#any_lfs_file_locks?', :request_store do - let!(:project) { create(:project) } + set(:project) { create(:project) } it 'returns false when there are no LFS file locks' do expect(project.any_lfs_file_locks?).to be_falsey @@ -3160,53 +3160,6 @@ describe Project do expect(projects).to eq([public_project]) end end - - context 'with requested visibility levels' do - set(:internal_project) { create(:project, :internal, :repository) } - set(:private_project_2) { create(:project, :private) } - - context 'with admin user' do - set(:admin) { create(:admin) } - - it 'returns all projects' do - projects = described_class.all.public_or_visible_to_user(admin, []) - - expect(projects).to match_array([public_project, private_project, private_project_2, internal_project]) - end - - it 'returns all public and private projects' do - projects = described_class.all.public_or_visible_to_user(admin, [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::PRIVATE]) - - expect(projects).to match_array([public_project, private_project, private_project_2]) - end - - it 'returns all private projects' do - projects = described_class.all.public_or_visible_to_user(admin, [Gitlab::VisibilityLevel::PRIVATE]) - - expect(projects).to match_array([private_project, private_project_2]) - end - end - - context 'with regular user' do - it 'returns authorized projects' do - projects = described_class.all.public_or_visible_to_user(user, []) - - expect(projects).to match_array([public_project, private_project, internal_project]) - end - - it "returns user's public and private projects" do - projects = described_class.all.public_or_visible_to_user(user, [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::PRIVATE]) - - expect(projects).to match_array([public_project, private_project]) - end - - it 'returns one private project' do - projects = described_class.all.public_or_visible_to_user(user, [Gitlab::VisibilityLevel::PRIVATE]) - - expect(projects).to eq([private_project]) - end - end - end end describe '.with_feature_available_for_user' do -- cgit v1.2.1 From c239bfcb1750794ec1bf8172dfa380dea64fe4c1 Mon Sep 17 00:00:00 2001 From: Dylan Griffith Date: Wed, 10 Apr 2019 06:38:27 +0000 Subject: Add more info logging to cluster apps Log events so that it's easy to see when different requests are starting. --- spec/lib/gitlab/kubernetes/namespace_spec.rb | 27 ++++++++++++++++++++++ .../services/base_helm_service_shared_examples.rb | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/lib/gitlab/kubernetes/namespace_spec.rb b/spec/lib/gitlab/kubernetes/namespace_spec.rb index e1c35c355f4..e91a755aa03 100644 --- a/spec/lib/gitlab/kubernetes/namespace_spec.rb +++ b/spec/lib/gitlab/kubernetes/namespace_spec.rb @@ -62,5 +62,32 @@ describe Gitlab::Kubernetes::Namespace do subject.ensure_exists! end + + context 'when client errors' do + let(:exception) { Kubeclient::HttpError.new(500, 'system failure', nil) } + + before do + allow(client).to receive(:get_namespace).with(name).once.and_raise(exception) + end + + it 'raises the exception' do + expect { subject.ensure_exists! }.to raise_error(exception) + end + + it 'logs the error' do + expect(subject.send(:logger)).to receive(:error).with( + hash_including( + exception: 'Kubeclient::HttpError', + status_code: 500, + namespace: 'a_namespace', + class_name: 'Gitlab::Kubernetes::Namespace', + event: :failed_to_create_namespace, + message: 'system failure' + ) + ) + + expect { subject.ensure_exists! }.to raise_error(exception) + end + end end end diff --git a/spec/support/shared_examples/services/base_helm_service_shared_examples.rb b/spec/support/shared_examples/services/base_helm_service_shared_examples.rb index 78a8e49fd76..fa76b95f768 100644 --- a/spec/support/shared_examples/services/base_helm_service_shared_examples.rb +++ b/spec/support/shared_examples/services/base_helm_service_shared_examples.rb @@ -20,7 +20,7 @@ shared_examples 'logs kubernetes errors' do end it 'logs into kubernetes.log and Sentry' do - expect(service.send(:logger)).to receive(:error).with(logger_hash) + expect(service.send(:logger)).to receive(:error).with(hash_including(logger_hash)) expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with( error, -- cgit v1.2.1 From 6d7876c1287a105e447aeea727e43431e10f9f53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 10 Apr 2019 09:35:24 +0200 Subject: Quarantine a flaky spec in spec/features/issues/user_creates_branch_and_merge_request_spec.rb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- spec/features/issues/user_creates_branch_and_merge_request_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb index 0a006011c89..cf665cff262 100644 --- a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb +++ b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb @@ -139,7 +139,7 @@ describe 'User creates branch and merge request on issue page', :js do visit project_issue_path(project, issue) end - it 'disables the create branch button' do + it 'disables the create branch button', :quarantine do expect(page).to have_css('.create-mr-dropdown-wrap .unavailable:not(.hidden)') expect(page).to have_css('.create-mr-dropdown-wrap .available.hidden', visible: false) expect(page).to have_content /Related merge requests/ -- cgit v1.2.1 From 919e22e05bdcd0e1edb642e0e04be02321387cea Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 10 Apr 2019 17:19:42 +0800 Subject: Double retries for rspec-retries It was 2*4 => 8 times before, so 4 times is still half retries --- spec/spec_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'spec') diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 953cf1519bb..60db3e1bc46 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -100,8 +100,8 @@ RSpec.configure do |config| config.include PolicyHelpers, type: :policy if ENV['CI'] - # This includes the first try, i.e. tests will be run 2 times before failing. - config.default_retry_count = 2 + # This includes the first try, i.e. tests will be run 4 times before failing. + config.default_retry_count = 4 config.reporter.register_listener( RspecFlaky::Listener.new, :example_passed, -- cgit v1.2.1 From e116c45619899d668fd3bae65007cb5398c5bc80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Wed, 10 Apr 2019 12:28:01 +0200 Subject: Add optional IGNORE_ACCESSORS in RetryBuildService --- spec/services/ci/retry_build_service_spec.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'spec') diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index 87185891470..17e2b17a499 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -35,7 +35,7 @@ describe Ci::RetryBuildService do commit_id deployment erased_by_id project_id runner_id tag_taggings taggings tags trigger_request_id user_id auto_canceled_by_id retried failure_reason - artifacts_file_store artifacts_metadata_store + sourced_pipelines artifacts_file_store artifacts_metadata_store metadata runner_session trace_chunks].freeze shared_examples 'build duplication' do @@ -95,7 +95,8 @@ describe Ci::RetryBuildService do end it 'has correct number of known attributes' do - known_accessors = CLONE_ACCESSORS + REJECT_ACCESSORS + IGNORE_ACCESSORS + processed_accessors = CLONE_ACCESSORS + REJECT_ACCESSORS + known_accessors = processed_accessors + IGNORE_ACCESSORS # :tag_list is a special case, this accessor does not exist # in reflected associations, comes from `act_as_taggable` and @@ -108,7 +109,8 @@ describe Ci::RetryBuildService do current_accessors.uniq! - expect(known_accessors).to contain_exactly(*current_accessors) + expect(current_accessors).to include(*processed_accessors) + expect(known_accessors).to include(*current_accessors) end end -- cgit v1.2.1 From c2c8189b10037bdc3e9e31fa73242d169e1ca46b Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 8 Apr 2019 20:04:49 +0200 Subject: Enable ESLint rule jest/valid-describe --- spec/frontend/.eslintrc.yml | 1 + 1 file changed, 1 insertion(+) (limited to 'spec') diff --git a/spec/frontend/.eslintrc.yml b/spec/frontend/.eslintrc.yml index 0e6ca11c1f8..ff18f0e4a2d 100644 --- a/spec/frontend/.eslintrc.yml +++ b/spec/frontend/.eslintrc.yml @@ -15,4 +15,5 @@ globals: rules: jest/no-identical-title: error jest/no-focused-tests: error + jest/valid-describe: error jest/no-jasmine-globals: error -- cgit v1.2.1 From 44d3590e36e8fb96f5cf4f97768ea4dfcb0450b9 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 8 Apr 2019 20:06:14 +0200 Subject: Wrap file template mutation names in template strings --- .../ide/stores/modules/file_templates/mutations_spec.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'spec') diff --git a/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js b/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js index 8e0e3ae99a1..8e8b7f06ca2 100644 --- a/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js +++ b/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js @@ -9,7 +9,7 @@ describe('IDE file templates mutations', () => { state = createState(); }); - describe(types.REQUEST_TEMPLATE_TYPES, () => { + describe(`${types.REQUEST_TEMPLATE_TYPES}`, () => { it('sets isLoading', () => { mutations[types.REQUEST_TEMPLATE_TYPES](state); @@ -17,7 +17,7 @@ describe('IDE file templates mutations', () => { }); }); - describe(types.RECEIVE_TEMPLATE_TYPES_ERROR, () => { + describe(`${types.RECEIVE_TEMPLATE_TYPES_ERROR}`, () => { it('sets isLoading', () => { state.isLoading = true; @@ -27,7 +27,7 @@ describe('IDE file templates mutations', () => { }); }); - describe(types.RECEIVE_TEMPLATE_TYPES_SUCCESS, () => { + describe(`${types.RECEIVE_TEMPLATE_TYPES_SUCCESS}`, () => { it('sets isLoading to false', () => { state.isLoading = true; @@ -43,7 +43,7 @@ describe('IDE file templates mutations', () => { }); }); - describe(types.SET_SELECTED_TEMPLATE_TYPE, () => { + describe(`${types.SET_SELECTED_TEMPLATE_TYPE}`, () => { it('sets selectedTemplateType', () => { mutations[types.SET_SELECTED_TEMPLATE_TYPE](state, 'type'); @@ -59,7 +59,7 @@ describe('IDE file templates mutations', () => { }); }); - describe(types.SET_UPDATE_SUCCESS, () => { + describe(`${types.SET_UPDATE_SUCCESS}`, () => { it('sets updateSuccess', () => { mutations[types.SET_UPDATE_SUCCESS](state, true); -- cgit v1.2.1 From 84792c29ef6e65deae83c7d9dc97465ee9212981 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 8 Apr 2019 20:07:20 +0200 Subject: Wrap import project mutation names in template strings --- spec/frontend/import_projects/store/mutations_spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'spec') diff --git a/spec/frontend/import_projects/store/mutations_spec.js b/spec/frontend/import_projects/store/mutations_spec.js index 8db8e9819ba..505545f7aa5 100644 --- a/spec/frontend/import_projects/store/mutations_spec.js +++ b/spec/frontend/import_projects/store/mutations_spec.js @@ -2,7 +2,7 @@ import * as types from '~/import_projects/store/mutation_types'; import mutations from '~/import_projects/store/mutations'; describe('import_projects store mutations', () => { - describe(types.RECEIVE_IMPORT_SUCCESS, () => { + describe(`${types.RECEIVE_IMPORT_SUCCESS}`, () => { it('removes repoId from reposBeingImported and providerRepos, adds to importedProjects', () => { const repoId = 1; const state = { @@ -20,7 +20,7 @@ describe('import_projects store mutations', () => { }); }); - describe(types.RECEIVE_JOBS_SUCCESS, () => { + describe(`${types.RECEIVE_JOBS_SUCCESS}`, () => { it('updates importStatus of existing importedProjects', () => { const repoId = 1; const state = { importedProjects: [{ id: repoId, importStatus: 'started' }] }; -- cgit v1.2.1 From 41c5733f557c2db74556b868e53b2742239880be Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 8 Apr 2019 20:09:37 +0200 Subject: Hard code test name in suggestion_diff_row_spec.js --- .../frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js index 866d6eb05c6..c8deac1c086 100644 --- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js +++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js @@ -23,7 +23,7 @@ const newLine = { type: 'new', }; -describe(SuggestionDiffRow.name, () => { +describe('SuggestionDiffRow', () => { let wrapper; const factory = (options = {}) => { -- cgit v1.2.1 From 2704c4a588892e2d1928e0d772610cdaf0f8e760 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 8 Apr 2019 20:16:46 +0200 Subject: Hard code test name in timeline_entry_item_spec.js --- spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'spec') diff --git a/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js b/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js index c15635f2105..be6c58f0683 100644 --- a/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js +++ b/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js @@ -1,7 +1,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; -describe(TimelineEntryItem.name, () => { +describe(`TimelineEntryItem`, () => { let wrapper; const factory = (options = {}) => { -- cgit v1.2.1 From 7a255af451a1755a9cc3fc2902ba414d9b5a5f07 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 8 Apr 2019 20:17:53 +0200 Subject: Wrap modal mutation names in template strings --- spec/frontend/vuex_shared/modules/modal/mutations_spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'spec') diff --git a/spec/frontend/vuex_shared/modules/modal/mutations_spec.js b/spec/frontend/vuex_shared/modules/modal/mutations_spec.js index d07f8ba1e65..eaaf196d1ec 100644 --- a/spec/frontend/vuex_shared/modules/modal/mutations_spec.js +++ b/spec/frontend/vuex_shared/modules/modal/mutations_spec.js @@ -2,7 +2,7 @@ import mutations from '~/vuex_shared/modules/modal/mutations'; import * as types from '~/vuex_shared/modules/modal/mutation_types'; describe('Vuex ModalModule mutations', () => { - describe(types.SHOW, () => { + describe(`${types.SHOW}`, () => { it('sets isVisible to true', () => { const state = { isVisible: false, @@ -16,7 +16,7 @@ describe('Vuex ModalModule mutations', () => { }); }); - describe(types.HIDE, () => { + describe(`${types.HIDE}`, () => { it('sets isVisible to false', () => { const state = { isVisible: true, @@ -30,7 +30,7 @@ describe('Vuex ModalModule mutations', () => { }); }); - describe(types.OPEN, () => { + describe(`${types.OPEN}`, () => { it('sets data and sets isVisible to true', () => { const data = { id: 7 }; const state = { -- cgit v1.2.1 From a2d34fbfbe0ba1772359d4c62b93cb90a4d752b4 Mon Sep 17 00:00:00 2001 From: pataar Date: Wed, 10 Apr 2019 11:00:41 +0000 Subject: Add time tracking to issue board entities --- spec/fixtures/api/schemas/entities/issue_board.json | 3 +++ 1 file changed, 3 insertions(+) (limited to 'spec') diff --git a/spec/fixtures/api/schemas/entities/issue_board.json b/spec/fixtures/api/schemas/entities/issue_board.json index f7b270ffa8d..7cb65e1f2f5 100644 --- a/spec/fixtures/api/schemas/entities/issue_board.json +++ b/spec/fixtures/api/schemas/entities/issue_board.json @@ -9,6 +9,9 @@ "project_id": { "type": "integer" }, "relative_position": { "type": ["integer", "null"] }, "time_estimate": { "type": "integer" }, + "total_time_spent": { "type": "integer" }, + "human_time_estimate": { "type": ["string", "null"] }, + "human_total_time_spent": { "type": ["string", "null"] }, "weight": { "type": ["integer", "null"] }, "project": { "type": "object", -- cgit v1.2.1 From 60f0e3c9933c9080e173be04471fe11eb3cab74f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Wed, 10 Apr 2019 13:18:31 +0200 Subject: Use different query count expectations in EE --- spec/serializers/pipeline_serializer_spec.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'spec') diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index 0fdd675aa01..d9023036534 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -157,7 +157,8 @@ describe PipelineSerializer do it 'verifies number of queries', :request_store do recorded = ActiveRecord::QueryRecorder.new { subject } - expect(recorded.count).to be_within(2).of(31) + expected_queries = Gitlab.ee? ? 38 : 31 + expect(recorded.count).to be_within(2).of(expected_queries) expect(recorded.cached_count).to eq(0) end end @@ -176,7 +177,8 @@ describe PipelineSerializer do # pipeline. With the same ref this check is cached but if refs are # different then there is an extra query per ref # https://gitlab.com/gitlab-org/gitlab-ce/issues/46368 - expect(recorded.count).to be_within(2).of(38) + expected_queries = Gitlab.ee? ? 44 : 38 + expect(recorded.count).to be_within(2).of(expected_queries) expect(recorded.cached_count).to eq(0) end end -- cgit v1.2.1 From 177bc25e3fc5c8bebc7073b545bf9ebf3b1035c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 10 Apr 2019 16:20:29 +0200 Subject: Quarantine flaky specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- spec/features/issues/issue_sidebar_spec.rb | 2 +- spec/features/issues/user_creates_branch_and_merge_request_spec.rb | 2 +- spec/features/issues/user_interacts_with_awards_spec.rb | 2 +- spec/services/quick_actions/interpret_service_spec.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) (limited to 'spec') diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index 3050f23c130..321da8f44d7 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -130,7 +130,7 @@ describe 'Issue Sidebar' do end end - context 'creating a project label', :js do + context 'creating a project label', :js, :quarantine do before do page.within('.block.labels') do click_link 'Create project' diff --git a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb index cf665cff262..b69fba0db00 100644 --- a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb +++ b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb @@ -29,7 +29,7 @@ describe 'User creates branch and merge request on issue page', :js do end # In order to improve tests performance, all UI checks are placed in this test. - it 'shows elements' do + it 'shows elements', :quarantine do button_create_merge_request = find('.js-create-merge-request') button_toggle_dropdown = find('.create-mr-dropdown-wrap .dropdown-toggle') diff --git a/spec/features/issues/user_interacts_with_awards_spec.rb b/spec/features/issues/user_interacts_with_awards_spec.rb index eaeaa04f50d..d117620a2b1 100644 --- a/spec/features/issues/user_interacts_with_awards_spec.rb +++ b/spec/features/issues/user_interacts_with_awards_spec.rb @@ -75,7 +75,7 @@ describe 'User interacts with awards' do end end - it 'shows the list of award emoji categories' do + it 'shows the list of award emoji categories', :quarantine do page.within('.awards') do page.find('.js-add-award').click end diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index c450f89c3cb..95a131e8c86 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -529,7 +529,7 @@ describe QuickActions::InterpretService do let(:issuable) { issue } end - it_behaves_like 'assign command' do + it_behaves_like 'assign command', :quarantine do let(:content) { "/assign @#{developer.username} @#{developer2.username}" } let(:issuable) { merge_request } end -- cgit v1.2.1