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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-08-18 11:17:02 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-08-18 11:17:02 +0300
commitb39512ed755239198a9c294b6a45e65c05900235 (patch)
treed234a3efade1de67c46b9e5a38ce813627726aa7 /spec/services
parentd31474cf3b17ece37939d20082b07f6657cc79a9 (diff)
Add latest changes from gitlab-org/gitlab@15-3-stable-eev15.3.0-rc42
Diffstat (limited to 'spec/services')
-rw-r--r--spec/services/alert_management/process_prometheus_alert_service_spec.rb1
-rw-r--r--spec/services/audit_events/build_service_spec.rb154
-rw-r--r--spec/services/auto_merge/base_service_spec.rb2
-rw-r--r--spec/services/auto_merge_service_spec.rb4
-rw-r--r--spec/services/branches/create_service_spec.rb142
-rw-r--r--spec/services/bulk_imports/create_service_spec.rb6
-rw-r--r--spec/services/bulk_imports/file_download_service_spec.rb35
-rw-r--r--spec/services/bulk_update_integration_service_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/rules_spec.rb1028
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb815
-rw-r--r--spec/services/ci/deployments/destroy_service_spec.rb65
-rw-r--r--spec/services/ci/destroy_pipeline_service_spec.rb20
-rw-r--r--spec/services/ci/job_artifacts/create_service_spec.rb8
-rw-r--r--spec/services/ci/job_artifacts/destroy_batch_service_spec.rb9
-rw-r--r--spec/services/ci/list_config_variables_service_spec.rb4
-rw-r--r--spec/services/ci/parse_dotenv_artifact_service_spec.rb2
-rw-r--r--spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb2
-rw-r--r--spec/services/ci/process_build_service_spec.rb2
-rw-r--r--spec/services/ci/register_job_service_spec.rb68
-rw-r--r--spec/services/ci/retry_job_service_spec.rb61
-rw-r--r--spec/services/ci/runners/assign_runner_service_spec.rb18
-rw-r--r--spec/services/ci/runners/bulk_delete_runners_service_spec.rb83
-rw-r--r--spec/services/ci/runners/process_runner_version_update_service_spec.rb80
-rw-r--r--spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb77
-rw-r--r--spec/services/ci/runners/register_runner_service_spec.rb150
-rw-r--r--spec/services/ci/runners/reset_registration_token_service_spec.rb13
-rw-r--r--spec/services/ci/runners/unassign_runner_service_spec.rb28
-rw-r--r--spec/services/ci/runners/unregister_runner_service_spec.rb7
-rw-r--r--spec/services/ci/runners/update_runner_service_spec.rb2
-rw-r--r--spec/services/ci/stuck_builds/drop_pending_service_spec.rb4
-rw-r--r--spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb2
-rw-r--r--spec/services/ci/track_failed_build_service_spec.rb56
-rw-r--r--spec/services/ci/update_build_state_service_spec.rb18
-rw-r--r--spec/services/clusters/integrations/create_service_spec.rb2
-rw-r--r--spec/services/clusters/integrations/prometheus_health_check_service_spec.rb1
-rw-r--r--spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb2
-rw-r--r--spec/services/database/consistency_check_service_spec.rb53
-rw-r--r--spec/services/deployments/create_for_build_service_spec.rb2
-rw-r--r--spec/services/deployments/update_environment_service_spec.rb12
-rw-r--r--spec/services/design_management/delete_designs_service_spec.rb24
-rw-r--r--spec/services/design_management/generate_image_versions_service_spec.rb2
-rw-r--r--spec/services/git/branch_push_service_spec.rb4
-rw-r--r--spec/services/google_cloud/create_cloudsql_instance_service_spec.rb90
-rw-r--r--spec/services/google_cloud/enable_cloudsql_service_spec.rb39
-rw-r--r--spec/services/google_cloud/get_cloudsql_instances_service_spec.rb62
-rw-r--r--spec/services/google_cloud/setup_cloudsql_instance_service_spec.rb68
-rw-r--r--spec/services/groups/create_service_spec.rb9
-rw-r--r--spec/services/groups/destroy_service_spec.rb15
-rw-r--r--spec/services/groups/merge_requests_count_service_spec.rb2
-rw-r--r--spec/services/groups/open_issues_count_service_spec.rb2
-rw-r--r--spec/services/groups/transfer_service_spec.rb37
-rw-r--r--spec/services/groups/update_service_spec.rb40
-rw-r--r--spec/services/groups/update_statistics_service_spec.rb2
-rw-r--r--spec/services/import/fogbugz_service_spec.rb2
-rw-r--r--spec/services/import/prepare_service_spec.rb66
-rw-r--r--spec/services/import/validate_remote_git_endpoint_service_spec.rb2
-rw-r--r--spec/services/incident_management/timeline_events/create_service_spec.rb83
-rw-r--r--spec/services/incident_management/timeline_events/update_service_spec.rb21
-rw-r--r--spec/services/issuable/common_system_notes_service_spec.rb45
-rw-r--r--spec/services/issues/clone_service_spec.rb26
-rw-r--r--spec/services/issues/create_service_spec.rb12
-rw-r--r--spec/services/issues/move_service_spec.rb12
-rw-r--r--spec/services/issues/prepare_import_csv_service_spec.rb51
-rw-r--r--spec/services/issues/referenced_merge_requests_service_spec.rb2
-rw-r--r--spec/services/issues/update_service_spec.rb58
-rw-r--r--spec/services/jira_import/start_import_service_spec.rb2
-rw-r--r--spec/services/lfs/push_service_spec.rb2
-rw-r--r--spec/services/markdown_content_rewriter_service_spec.rb2
-rw-r--r--spec/services/members/groups/creator_service_spec.rb5
-rw-r--r--spec/services/members/invite_service_spec.rb6
-rw-r--r--spec/services/merge_requests/approval_service_spec.rb112
-rw-r--r--spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb49
-rw-r--r--spec/services/merge_requests/close_service_spec.rb4
-rw-r--r--spec/services/merge_requests/create_approval_event_service_spec.rb22
-rw-r--r--spec/services/merge_requests/create_pipeline_service_spec.rb10
-rw-r--r--spec/services/merge_requests/execute_approval_hooks_service_spec.rb33
-rw-r--r--spec/services/merge_requests/handle_assignees_change_service_spec.rb8
-rw-r--r--spec/services/merge_requests/mergeability/check_broken_status_service_spec.rb7
-rw-r--r--spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb7
-rw-r--r--spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb7
-rw-r--r--spec/services/merge_requests/mergeability/check_draft_status_service_spec.rb7
-rw-r--r--spec/services/merge_requests/mergeability/check_open_status_service_spec.rb7
-rw-r--r--spec/services/merge_requests/mergeability/run_checks_service_spec.rb94
-rw-r--r--spec/services/merge_requests/push_options_handler_service_spec.rb22
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb2
-rw-r--r--spec/services/merge_requests/remove_attention_requested_service_spec.rb183
-rw-r--r--spec/services/merge_requests/request_attention_service_spec.rb220
-rw-r--r--spec/services/merge_requests/toggle_attention_requested_service_spec.rb188
-rw-r--r--spec/services/merge_requests/update_reviewers_service_spec.rb162
-rw-r--r--spec/services/merge_requests/update_service_spec.rb10
-rw-r--r--spec/services/milestones/transfer_service_spec.rb8
-rw-r--r--spec/services/notes/build_service_spec.rb10
-rw-r--r--spec/services/notes/copy_service_spec.rb6
-rw-r--r--spec/services/notes/create_service_spec.rb94
-rw-r--r--spec/services/notes/destroy_service_spec.rb26
-rw-r--r--spec/services/notes/update_service_spec.rb34
-rw-r--r--spec/services/notification_service_spec.rb45
-rw-r--r--spec/services/packages/composer/create_package_service_spec.rb2
-rw-r--r--spec/services/packages/create_dependency_service_spec.rb2
-rw-r--r--spec/services/packages/debian/extract_deb_metadata_service_spec.rb2
-rw-r--r--spec/services/packages/debian/extract_metadata_service_spec.rb7
-rw-r--r--spec/services/packages/debian/parse_debian822_service_spec.rb8
-rw-r--r--spec/services/packages/debian/sign_distribution_service_spec.rb2
-rw-r--r--spec/services/packages/helm/process_file_service_spec.rb2
-rw-r--r--spec/services/packages/npm/create_package_service_spec.rb8
-rw-r--r--spec/services/packages/npm/create_tag_service_spec.rb1
-rw-r--r--spec/services/packages/rubygems/dependency_resolver_service_spec.rb14
-rw-r--r--spec/services/pages/delete_service_spec.rb4
-rw-r--r--spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb2
-rw-r--r--spec/services/personal_access_tokens/revoke_service_spec.rb1
-rw-r--r--spec/services/projects/after_rename_service_spec.rb9
-rw-r--r--spec/services/projects/alerting/notify_service_spec.rb5
-rw-r--r--spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb2
-rw-r--r--spec/services/projects/create_service_spec.rb39
-rw-r--r--spec/services/projects/enable_deploy_key_service_spec.rb2
-rw-r--r--spec/services/projects/hashed_storage/migrate_repository_service_spec.rb6
-rw-r--r--spec/services/projects/hashed_storage/rollback_repository_service_spec.rb8
-rw-r--r--spec/services/projects/import_export/export_service_spec.rb23
-rw-r--r--spec/services/projects/import_export/relation_export_service_spec.rb121
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb2
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_service_spec.rb2
-rw-r--r--spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb6
-rw-r--r--spec/services/projects/participants_service_spec.rb6
-rw-r--r--spec/services/projects/prometheus/alerts/notify_service_spec.rb9
-rw-r--r--spec/services/projects/transfer_service_spec.rb43
-rw-r--r--spec/services/projects/update_service_spec.rb12
-rw-r--r--spec/services/projects/update_statistics_service_spec.rb2
-rw-r--r--spec/services/protected_branches/cache_service_spec.rb113
-rw-r--r--spec/services/protected_branches/create_service_spec.rb29
-rw-r--r--spec/services/protected_branches/destroy_service_spec.rb27
-rw-r--r--spec/services/protected_branches/update_service_spec.rb33
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb110
-rw-r--r--spec/services/releases/create_service_spec.rb8
-rw-r--r--spec/services/releases/destroy_service_spec.rb2
-rw-r--r--spec/services/releases/update_service_spec.rb2
-rw-r--r--spec/services/resource_access_tokens/create_service_spec.rb2
-rw-r--r--spec/services/resource_events/change_labels_service_spec.rb60
-rw-r--r--spec/services/search/group_service_spec.rb2
-rw-r--r--spec/services/security/ci_configuration/sast_parser_service_spec.rb1
-rw-r--r--spec/services/snippets/update_service_spec.rb2
-rw-r--r--spec/services/suggestions/apply_service_spec.rb2
-rw-r--r--spec/services/system_note_service_spec.rb86
-rw-r--r--spec/services/system_notes/issuables_service_spec.rb108
-rw-r--r--spec/services/system_notes/merge_requests_service_spec.rb4
-rw-r--r--spec/services/system_notes/time_tracking_service_spec.rb174
-rw-r--r--spec/services/terraform/remote_state_handler_spec.rb2
-rw-r--r--spec/services/timelogs/create_service_spec.rb47
-rw-r--r--spec/services/timelogs/delete_service_spec.rb14
-rw-r--r--spec/services/todo_service_spec.rb15
-rw-r--r--spec/services/todos/destroy/design_service_spec.rb8
-rw-r--r--spec/services/todos/destroy/destroyed_issuable_service_spec.rb53
-rw-r--r--spec/services/topics/merge_service_spec.rb60
-rw-r--r--spec/services/uploads/destroy_service_spec.rb103
-rw-r--r--spec/services/users/create_service_spec.rb15
-rw-r--r--spec/services/users/dismiss_namespace_callout_service_spec.rb24
-rw-r--r--spec/services/users/dismiss_project_callout_service_spec.rb25
-rw-r--r--spec/services/users/update_service_spec.rb2
-rw-r--r--spec/services/web_hook_service_spec.rb4
-rw-r--r--spec/services/web_hooks/destroy_service_spec.rb67
-rw-r--r--spec/services/web_hooks/log_execution_service_spec.rb61
-rw-r--r--spec/services/webauthn/authenticate_service_spec.rb25
-rw-r--r--spec/services/work_items/create_and_link_service_spec.rb40
-rw-r--r--spec/services/work_items/create_from_task_service_spec.rb2
-rw-r--r--spec/services/work_items/create_service_spec.rb10
-rw-r--r--spec/services/work_items/parent_links/create_service_spec.rb28
-rw-r--r--spec/services/work_items/parent_links/destroy_service_spec.rb11
-rw-r--r--spec/services/work_items/update_service_spec.rb119
-rw-r--r--spec/services/work_items/widgets/assignees_service/update_service_spec.rb116
-rw-r--r--spec/services/work_items/widgets/description_service/update_service_spec.rb94
-rw-r--r--spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb26
-rw-r--r--spec/services/work_items/widgets/start_and_due_date_service/update_service_spec.rb62
-rw-r--r--spec/services/work_items/widgets/weight_service/update_service_spec.rb36
174 files changed, 4925 insertions, 2475 deletions
diff --git a/spec/services/alert_management/process_prometheus_alert_service_spec.rb b/spec/services/alert_management/process_prometheus_alert_service_spec.rb
index 86a6cdee52d..ae52a09be48 100644
--- a/spec/services/alert_management/process_prometheus_alert_service_spec.rb
+++ b/spec/services/alert_management/process_prometheus_alert_service_spec.rb
@@ -44,6 +44,7 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do
end
it_behaves_like 'processes new firing alert'
+ include_examples 'handles race condition in alert creation'
context 'with resolving payload' do
let(:prometheus_status) { 'resolved' }
diff --git a/spec/services/audit_events/build_service_spec.rb b/spec/services/audit_events/build_service_spec.rb
new file mode 100644
index 00000000000..caf405a53aa
--- /dev/null
+++ b/spec/services/audit_events/build_service_spec.rb
@@ -0,0 +1,154 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AuditEvents::BuildService do
+ let(:author) { build_stubbed(:author, current_sign_in_ip: '127.0.0.1') }
+ let(:deploy_token) { build_stubbed(:deploy_token, user: author) }
+ let(:scope) { build_stubbed(:group) }
+ let(:target) { build_stubbed(:project) }
+ let(:ip_address) { '192.168.8.8' }
+ let(:message) { 'Added an interesting field from project Gotham' }
+ let(:additional_details) { { action: :custom } }
+
+ subject(:service) do
+ described_class.new(
+ author: author,
+ scope: scope,
+ target: target,
+ message: message,
+ additional_details: additional_details,
+ ip_address: ip_address
+ )
+ end
+
+ describe '#execute', :request_store do
+ subject(:event) { service.execute }
+
+ before do
+ allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(ip_address)
+ end
+
+ it 'sets correct attributes', :aggregate_failures do
+ freeze_time do
+ expect(event).to have_attributes(
+ author_id: author.id,
+ author_name: author.name,
+ entity_id: scope.id,
+ entity_type: scope.class.name)
+
+ expect(event.details).to eq(
+ author_name: author.name,
+ author_class: author.class.name,
+ target_id: target.id,
+ target_type: target.class.name,
+ target_details: target.name,
+ custom_message: message,
+ action: :custom)
+
+ expect(event.ip_address).to be_nil
+ expect(event.created_at).to eq(DateTime.current)
+ end
+ end
+
+ context 'when IP address is not provided' do
+ let(:ip_address) { nil }
+
+ it 'uses author current_sign_in_ip' do
+ expect(event.ip_address).to be_nil
+ end
+ end
+
+ context 'when overriding target details' do
+ subject(:service) do
+ described_class.new(
+ author: author,
+ scope: scope,
+ target: target,
+ message: message,
+ target_details: "This is my target details"
+ )
+ end
+
+ it 'uses correct target details' do
+ expect(event.target_details).to eq("This is my target details")
+ end
+ end
+
+ context 'when deploy token is passed as author' do
+ let(:service) do
+ described_class.new(
+ author: deploy_token,
+ scope: scope,
+ target: target,
+ message: message
+ )
+ end
+
+ it 'expect author to be user' do
+ expect(event.author_id).to eq(-2)
+ expect(event.author_name).to eq(deploy_token.name)
+ end
+ end
+
+ context 'when deploy key is passed as author' do
+ let(:deploy_key) { build_stubbed(:deploy_key, user: author) }
+
+ let(:service) do
+ described_class.new(
+ author: deploy_key,
+ scope: scope,
+ target: target,
+ message: message
+ )
+ end
+
+ it 'expect author to be deploy key' do
+ expect(event.author_id).to eq(-3)
+ expect(event.author_name).to eq(deploy_key.name)
+ end
+ end
+
+ context 'when author is passed as UnauthenticatedAuthor' do
+ let(:service) do
+ described_class.new(
+ author: ::Gitlab::Audit::UnauthenticatedAuthor.new,
+ scope: scope,
+ target: target,
+ message: message
+ )
+ end
+
+ it 'sets author as unauthenticated user' do
+ expect(event.author).to be_an_instance_of(::Gitlab::Audit::UnauthenticatedAuthor)
+ expect(event.author_name).to eq('An unauthenticated user')
+ end
+ end
+
+ context 'when attributes are missing' do
+ context 'when author is missing' do
+ let(:author) { nil }
+
+ it { expect { service }.to raise_error(described_class::MissingAttributeError) }
+ end
+
+ context 'when scope is missing' do
+ let(:scope) { nil }
+
+ it { expect { service }.to raise_error(described_class::MissingAttributeError) }
+ end
+
+ context 'when target is missing' do
+ let(:target) { nil }
+
+ it { expect { service }.to raise_error(described_class::MissingAttributeError) }
+ end
+
+ context 'when message is missing' do
+ let(:message) { nil }
+
+ it { expect { service }.to raise_error(described_class::MissingAttributeError) }
+ end
+ end
+ end
+end
diff --git a/spec/services/auto_merge/base_service_spec.rb b/spec/services/auto_merge/base_service_spec.rb
index 3f535b83788..6c804a14620 100644
--- a/spec/services/auto_merge/base_service_spec.rb
+++ b/spec/services/auto_merge/base_service_spec.rb
@@ -254,7 +254,7 @@ RSpec.describe AutoMerge::BaseService do
subject { service.abort(merge_request, reason) }
let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
- let(:reason) { 'an error'}
+ let(:reason) { 'an error' }
it_behaves_like 'Canceled or Dropped'
diff --git a/spec/services/auto_merge_service_spec.rb b/spec/services/auto_merge_service_spec.rb
index 335c608c206..043b413acff 100644
--- a/spec/services/auto_merge_service_spec.rb
+++ b/spec/services/auto_merge_service_spec.rb
@@ -97,7 +97,7 @@ RSpec.describe AutoMergeService do
end
context 'when strategy is not present' do
- let(:strategy) { }
+ let(:strategy) {}
it 'returns nil' do
is_expected.to be_nil
@@ -140,7 +140,7 @@ RSpec.describe AutoMergeService do
end
context 'when strategy is not specified' do
- let(:strategy) { }
+ let(:strategy) {}
it 'chooses the most preferred strategy' do
is_expected.to eq(:merge_when_pipeline_succeeds)
diff --git a/spec/services/branches/create_service_spec.rb b/spec/services/branches/create_service_spec.rb
index 0d2f5838574..26cc1a0665e 100644
--- a/spec/services/branches/create_service_spec.rb
+++ b/spec/services/branches/create_service_spec.rb
@@ -2,17 +2,155 @@
require 'spec_helper'
-RSpec.describe Branches::CreateService do
+RSpec.describe Branches::CreateService, :use_clean_rails_redis_caching do
subject(:service) { described_class.new(project, user) }
let_it_be(:project) { create(:project_empty_repo) }
let_it_be(:user) { create(:user) }
+ describe '#bulk_create' do
+ subject { service.bulk_create(branches) }
+
+ let_it_be(:project) { create(:project, :custom_repo, files: { 'foo/a.txt' => 'foo' }) }
+
+ let(:branches) { { 'branch' => 'master', 'another_branch' => 'master' } }
+
+ it 'creates two branches' do
+ expect(subject[:status]).to eq(:success)
+ expect(subject[:branches].map(&:name)).to match_array(%w[branch another_branch])
+
+ expect(project.repository.branch_exists?('branch')).to be_truthy
+ expect(project.repository.branch_exists?('another_branch')).to be_truthy
+ end
+
+ context 'when branches are empty' do
+ let(:branches) { {} }
+
+ it 'is successful' do
+ expect(subject[:status]).to eq(:success)
+ expect(subject[:branches]).to eq([])
+ end
+ end
+
+ context 'when incorrect reference is provided' do
+ let(:branches) { { 'new-feature' => 'unknown' } }
+
+ before do
+ allow(project.repository).to receive(:add_branch).and_return(false)
+ end
+
+ it 'returns an error with a reference name' do
+ err_msg = 'Failed to create branch \'new-feature\': invalid reference name \'unknown\''
+
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to match_array([err_msg])
+ end
+ end
+
+ context 'when branch already exists' do
+ let(:branches) { { 'master' => 'master' } }
+
+ it 'returns an error' do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to match_array(['Branch already exists'])
+ end
+ end
+
+ context 'when an ambiguous branch name is provided' do
+ let(:branches) { { 'ambiguous/test' => 'master', 'ambiguous' => 'master' } }
+
+ it 'returns an error that branch could not be created' do
+ err_msg = 'Failed to create branch \'ambiguous\': 13:reference is ambiguous.'
+
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to match_array([err_msg])
+ end
+ end
+
+ context 'when PreReceiveError exception' do
+ let(:branches) { { 'error' => 'master' } }
+
+ it 'logs and returns an error if there is a PreReceiveError exception' do
+ error_message = 'pre receive error'
+ raw_message = "GitLab: #{error_message}"
+ pre_receive_error = Gitlab::Git::PreReceiveError.new(raw_message)
+
+ allow(project.repository).to receive(:add_branch).and_raise(pre_receive_error)
+
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
+ pre_receive_error,
+ pre_receive_message: raw_message,
+ branch_name: 'error',
+ ref: 'master'
+ )
+
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to match_array([error_message])
+ end
+ end
+
+ context 'when multiple errors occur' do
+ let(:branches) { { 'master' => 'master', '' => 'master', 'failed_branch' => 'master' } }
+
+ it 'returns all errors' do
+ allow(project.repository).to receive(:add_branch).with(
+ user,
+ 'failed_branch',
+ 'master',
+ expire_cache: false
+ ).and_return(false)
+
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to match_array(
+ [
+ 'Branch already exists',
+ 'Branch name is invalid',
+ "Failed to create branch 'failed_branch': invalid reference name 'master'"
+ ]
+ )
+ end
+ end
+
+ context 'without N+1 for Redis cache' do
+ let(:branches) { { 'branch1' => 'master', 'branch2' => 'master', 'branch3' => 'master' } }
+
+ it 'does not trigger Redis recreation' do
+ project.repository.expire_branches_cache
+
+ control = RedisCommands::Recorder.new(pattern: ':branch_names:') { subject }
+
+ expect(control.by_command(:sadd).count).to eq(1)
+ end
+ end
+
+ context 'without N+1 branch cache expiration' do
+ let(:branches) { { 'branch_1' => 'master', 'branch_2' => 'master', 'branch_3' => 'master' } }
+
+ it 'triggers branch cache expiration only once' do
+ expect(project.repository).to receive(:expire_branches_cache).once
+
+ subject
+ end
+
+ context 'when branches were not added' do
+ let(:branches) { { 'master' => 'master' } }
+
+ it 'does not trigger branch expiration' do
+ expect(project.repository).not_to receive(:expire_branches_cache)
+
+ subject
+ end
+ end
+ end
+ end
+
describe '#execute' do
context 'when repository is empty' do
it 'creates master branch' do
- service.execute('my-feature', 'master')
+ result = service.execute('my-feature', 'master')
+ expect(result[:status]).to eq(:success)
+ expect(result[:branch].name).to eq('my-feature')
expect(project.repository.branch_exists?('master')).to be_truthy
end
diff --git a/spec/services/bulk_imports/create_service_spec.rb b/spec/services/bulk_imports/create_service_spec.rb
index 67ec6fee1ae..4b655dd5d6d 100644
--- a/spec/services/bulk_imports/create_service_spec.rb
+++ b/spec/services/bulk_imports/create_service_spec.rb
@@ -10,19 +10,19 @@ RSpec.describe BulkImports::CreateService do
{
source_type: 'group_entity',
source_full_path: 'full/path/to/group1',
- destination_name: 'destination group 1',
+ destination_slug: 'destination group 1',
destination_namespace: 'full/path/to/destination1'
},
{
source_type: 'group_entity',
source_full_path: 'full/path/to/group2',
- destination_name: 'destination group 2',
+ destination_slug: 'destination group 2',
destination_namespace: 'full/path/to/destination2'
},
{
source_type: 'project_entity',
source_full_path: 'full/path/to/project1',
- destination_name: 'destination project 1',
+ destination_slug: 'destination project 1',
destination_namespace: 'full/path/to/destination1'
}
]
diff --git a/spec/services/bulk_imports/file_download_service_spec.rb b/spec/services/bulk_imports/file_download_service_spec.rb
index bd664d6e996..81229cc8431 100644
--- a/spec/services/bulk_imports/file_download_service_spec.rb
+++ b/spec/services/bulk_imports/file_download_service_spec.rb
@@ -136,14 +136,45 @@ RSpec.describe BulkImports::FileDownloadService do
end
context 'when chunk code is not 200' do
- let(:chunk_double) { double('chunk', size: 1000, code: 307) }
+ let(:chunk_double) { double('chunk', size: 1000, code: 500) }
it 'raises an error' do
expect { subject.execute }.to raise_error(
described_class::ServiceError,
- 'File download error 307'
+ 'File download error 500'
)
end
+
+ context 'when chunk code is redirection' do
+ let(:chunk_double) { double('redirection', size: 1000, code: 303) }
+
+ it 'does not write a redirection chunk' do
+ expect { subject.execute }.not_to raise_error
+
+ expect(File.read(filepath)).not_to include('redirection')
+ end
+
+ context 'when redirection chunk appears at a later stage of the download' do
+ it 'raises an error' do
+ another_chunk_double = double('another redirection', size: 1000, code: 303)
+ data_chunk = double('data chunk', size: 1000, code: 200)
+
+ allow_next_instance_of(BulkImports::Clients::HTTP) do |client|
+ allow(client).to receive(:head).and_return(response_double)
+ allow(client)
+ .to receive(:stream)
+ .and_yield(chunk_double)
+ .and_yield(data_chunk)
+ .and_yield(another_chunk_double)
+ end
+
+ expect { subject.execute }.to raise_error(
+ described_class::ServiceError,
+ 'File download error 303'
+ )
+ end
+ end
+ end
end
context 'when file is a symlink' do
diff --git a/spec/services/bulk_update_integration_service_spec.rb b/spec/services/bulk_update_integration_service_spec.rb
index e3e38aacaa2..7c5bd1db565 100644
--- a/spec/services/bulk_update_integration_service_spec.rb
+++ b/spec/services/bulk_update_integration_service_spec.rb
@@ -71,7 +71,7 @@ RSpec.describe BulkUpdateIntegrationService do
context 'with integration with data fields' do
let(:excluded_attributes) do
- %w[id service_id created_at updated_at encrypted_properties encrypted_properties_iv]
+ %w[id integration_id created_at updated_at encrypted_properties encrypted_properties_iv]
end
it 'updates the data fields from the integration', :aggregate_failures do
diff --git a/spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb b/spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb
index 9add096d782..7c698242921 100644
--- a/spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb
+++ b/spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Ci::CreatePipelineService do
let_it_be(:group) { create(:group, :private) }
- let_it_be(:group_variable) { create(:ci_group_variable, group: group, key: 'RUNNER_TAG', value: 'group')}
+ let_it_be(:group_variable) { create(:ci_group_variable, group: group, key: 'RUNNER_TAG', value: 'group') }
let_it_be(:project) { create(:project, :repository, group: group) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb b/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb
index 4326fa5533f..cc808b7e61c 100644
--- a/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb
+++ b/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
expect(pipeline.statuses).to match_array [test, bridge]
expect(bridge.options).to eq(expected_bridge_options)
expect(bridge.yaml_variables)
- .to include(key: 'CROSS', value: 'downstream', public: true)
+ .to include(key: 'CROSS', value: 'downstream')
end
end
diff --git a/spec/services/ci/create_pipeline_service/rules_spec.rb b/spec/services/ci/create_pipeline_service/rules_spec.rb
index d0ce1c5aba8..6e48141226d 100644
--- a/spec/services/ci/create_pipeline_service/rules_spec.rb
+++ b/spec/services/ci/create_pipeline_service/rules_spec.rb
@@ -7,10 +7,38 @@ RSpec.describe Ci::CreatePipelineService do
let(:ref) { 'refs/heads/master' }
let(:source) { :push }
let(:service) { described_class.new(project, user, { ref: ref }) }
- let(:pipeline) { service.execute(source).payload }
+ let(:response) { execute_service }
+ let(:pipeline) { response.payload }
let(:build_names) { pipeline.builds.pluck(:name) }
+ def execute_service(before: '00000000', variables_attributes: nil)
+ params = { ref: ref, before: before, after: project.commit(ref).sha, variables_attributes: variables_attributes }
+
+ described_class
+ .new(project, user, params)
+ .execute(source) do |pipeline|
+ yield(pipeline) if block_given?
+ end
+ end
+
context 'job:rules' do
+ let(:regular_job) { find_job('regular-job') }
+ let(:rules_job) { find_job('rules-job') }
+ let(:delayed_job) { find_job('delayed-job') }
+
+ def find_job(name)
+ pipeline.builds.find_by(name: name)
+ end
+
+ shared_examples 'rules jobs are excluded' do
+ it 'only persists the job without rules' do
+ expect(pipeline).to be_persisted
+ expect(regular_job).to be_persisted
+ expect(rules_job).to be_nil
+ expect(delayed_job).to be_nil
+ end
+ end
+
before do
stub_ci_pipeline_yaml_file(config)
allow_next_instance_of(Ci::BuildScheduleWorker) do |instance|
@@ -95,10 +123,6 @@ RSpec.describe Ci::CreatePipelineService do
end
context 'with allow_failure and exit_codes', :aggregate_failures do
- def find_job(name)
- pipeline.builds.find_by(name: name)
- end
-
let(:config) do
<<-EOY
job-1:
@@ -280,6 +304,773 @@ RSpec.describe Ci::CreatePipelineService do
end
end
end
+
+ context 'with simple if: clauses' do
+ let(:config) do
+ <<-EOY
+ regular-job:
+ script: 'echo Hello, World!'
+
+ master-job:
+ script: "echo hello world, $CI_COMMIT_REF_NAME"
+ rules:
+ - if: $CI_COMMIT_REF_NAME == "nonexistant-branch"
+ when: never
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ when: manual
+
+ negligible-job:
+ script: "exit 1"
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ allow_failure: true
+
+ delayed-job:
+ script: "echo See you later, World!"
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ when: delayed
+ start_in: 1 hour
+
+ never-job:
+ script: "echo Goodbye, World!"
+ rules:
+ - if: $CI_COMMIT_REF_NAME
+ when: never
+ EOY
+ end
+
+ context 'with matches' do
+ it 'creates a pipeline with the vanilla and manual jobs' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly(
+ 'regular-job', 'delayed-job', 'master-job', 'negligible-job'
+ )
+ end
+
+ it 'assigns job:when values to the builds' do
+ expect(find_job('regular-job').when).to eq('on_success')
+ expect(find_job('master-job').when).to eq('manual')
+ expect(find_job('negligible-job').when).to eq('on_success')
+ expect(find_job('delayed-job').when).to eq('delayed')
+ end
+
+ it 'assigns job:allow_failure values to the builds' do
+ expect(find_job('regular-job').allow_failure).to eq(false)
+ expect(find_job('master-job').allow_failure).to eq(false)
+ expect(find_job('negligible-job').allow_failure).to eq(true)
+ expect(find_job('delayed-job').allow_failure).to eq(false)
+ end
+
+ it 'assigns start_in for delayed jobs' do
+ expect(delayed_job.options[:start_in]).to eq('1 hour')
+ end
+ end
+
+ context 'with no matches' do
+ let(:ref) { 'refs/heads/feature' }
+
+ it_behaves_like 'rules jobs are excluded'
+ end
+ end
+
+ context 'with complex if: clauses' do
+ let(:config) do
+ <<-EOY
+ regular-job:
+ script: 'echo Hello, World!'
+ rules:
+ - if: $VAR == 'present' && $OTHER || $CI_COMMIT_REF_NAME
+ when: manual
+ allow_failure: true
+ EOY
+ end
+
+ it 'matches the first rule' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('regular-job')
+ expect(regular_job.when).to eq('manual')
+ expect(regular_job.allow_failure).to eq(true)
+ end
+ end
+ end
+
+ context 'changes:' do
+ let(:config) do
+ <<-EOY
+ regular-job:
+ script: 'echo Hello, World!'
+
+ rules-job:
+ script: "echo hello world, $CI_COMMIT_REF_NAME"
+ rules:
+ - changes:
+ - README.md
+ when: manual
+ - changes:
+ - app.rb
+ when: on_success
+
+ delayed-job:
+ script: "echo See you later, World!"
+ rules:
+ - changes:
+ - README.md
+ when: delayed
+ start_in: 4 hours
+
+ negligible-job:
+ script: "can be failed sometimes"
+ rules:
+ - changes:
+ - README.md
+ allow_failure: true
+
+ README:
+ script: "I use variables for changes!"
+ rules:
+ - changes:
+ - $CI_JOB_NAME*
+
+ changes-paths:
+ script: "I am using a new syntax!"
+ rules:
+ - changes:
+ paths: [README.md]
+ EOY
+ end
+
+ context 'and matches' do
+ before do
+ allow_next_instance_of(Ci::Pipeline) do |pipeline|
+ allow(pipeline).to receive(:modified_paths).and_return(%w[README.md])
+ end
+ end
+
+ it 'creates five jobs' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly(
+ 'regular-job', 'rules-job', 'delayed-job', 'negligible-job', 'README', 'changes-paths'
+ )
+ end
+
+ it 'sets when: for all jobs' do
+ expect(regular_job.when).to eq('on_success')
+ expect(rules_job.when).to eq('manual')
+ expect(delayed_job.when).to eq('delayed')
+ expect(delayed_job.options[:start_in]).to eq('4 hours')
+ end
+
+ it 'sets allow_failure: for negligible job' do
+ expect(find_job('negligible-job').allow_failure).to eq(true)
+ end
+ end
+
+ context 'and matches the second rule' do
+ before do
+ allow_next_instance_of(Ci::Pipeline) do |pipeline|
+ allow(pipeline).to receive(:modified_paths).and_return(%w[app.rb])
+ end
+ end
+
+ it 'includes both jobs' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('regular-job', 'rules-job')
+ end
+
+ it 'sets when: for the created rules job based on the second clause' do
+ expect(regular_job.when).to eq('on_success')
+ expect(rules_job.when).to eq('on_success')
+ end
+ end
+
+ context 'and does not match' do
+ before do
+ allow_next_instance_of(Ci::Pipeline) do |pipeline|
+ allow(pipeline).to receive(:modified_paths).and_return(%w[useless_script.rb])
+ end
+ end
+
+ it_behaves_like 'rules jobs are excluded'
+
+ it 'sets when: for the created job' do
+ expect(regular_job.when).to eq('on_success')
+ end
+ end
+
+ context 'with paths and compare_to' do
+ let_it_be(:project) { create(:project, :empty_repo) }
+ let_it_be(:user) { project.first_owner }
+
+ before_all do
+ project.repository.add_branch(user, 'feature_1', 'master')
+
+ project.repository.create_file(
+ user, 'file1.txt', 'file 1', message: 'Create file1.txt', branch_name: 'feature_1'
+ )
+
+ project.repository.add_branch(user, 'feature_2', 'feature_1')
+
+ project.repository.create_file(
+ user, 'file2.txt', 'file 2', message: 'Create file2.txt', branch_name: 'feature_2'
+ )
+ end
+
+ let(:changed_file) { 'file2.txt' }
+ let(:ref) { 'feature_2' }
+
+ let(:response) { execute_service(before: nil) }
+
+ context 'for jobs rules' do
+ let(:config) do
+ <<-EOY
+ job1:
+ script: exit 0
+ rules:
+ - changes:
+ paths: [#{changed_file}]
+ compare_to: #{compare_to}
+
+ job2:
+ script: exit 0
+ EOY
+ end
+
+ context 'when there is no such compare_to ref' do
+ let(:compare_to) { 'invalid-branch' }
+
+ it 'returns an error' do
+ expect(pipeline.errors.full_messages).to eq([
+ 'Failed to parse rule for job1: rules:changes:compare_to is not a valid ref'
+ ])
+ end
+
+ context 'when the FF ci_rules_changes_compare is not enabled' do
+ before do
+ stub_feature_flags(ci_rules_changes_compare: false)
+ end
+
+ it 'ignores compare_to and changes is always true' do
+ expect(build_names).to contain_exactly('job1', 'job2')
+ end
+ end
+ end
+
+ context 'when the compare_to ref exists' do
+ let(:compare_to) { 'feature_1' }
+
+ context 'when the rule matches' do
+ it 'creates job1 and job2' do
+ expect(build_names).to contain_exactly('job1', 'job2')
+ end
+
+ context 'when the FF ci_rules_changes_compare is not enabled' do
+ before do
+ stub_feature_flags(ci_rules_changes_compare: false)
+ end
+
+ it 'ignores compare_to and changes is always true' do
+ expect(build_names).to contain_exactly('job1', 'job2')
+ end
+ end
+ end
+
+ context 'when the rule does not match' do
+ let(:changed_file) { 'file1.txt' }
+
+ it 'does not create job1' do
+ expect(build_names).to contain_exactly('job2')
+ end
+
+ context 'when the FF ci_rules_changes_compare is not enabled' do
+ before do
+ stub_feature_flags(ci_rules_changes_compare: false)
+ end
+
+ it 'ignores compare_to and changes is always true' do
+ expect(build_names).to contain_exactly('job1', 'job2')
+ end
+ end
+ end
+ end
+ end
+
+ context 'for workflow rules' do
+ let(:config) do
+ <<-EOY
+ workflow:
+ rules:
+ - changes:
+ paths: [#{changed_file}]
+ compare_to: #{compare_to}
+
+ job1:
+ script: exit 0
+ EOY
+ end
+
+ let(:compare_to) { 'feature_1' }
+
+ context 'when the rule matches' do
+ it 'creates job1' do
+ expect(pipeline).to be_created_successfully
+ expect(build_names).to contain_exactly('job1')
+ end
+
+ context 'when the FF ci_rules_changes_compare is not enabled' do
+ before do
+ stub_feature_flags(ci_rules_changes_compare: false)
+ end
+
+ it 'ignores compare_to and changes is always true' do
+ expect(pipeline).to be_created_successfully
+ expect(build_names).to contain_exactly('job1')
+ end
+ end
+ end
+
+ context 'when the rule does not match' do
+ let(:changed_file) { 'file1.txt' }
+
+ it 'does not create job1' do
+ expect(pipeline).not_to be_created_successfully
+ expect(build_names).to be_empty
+ end
+ end
+ end
+ end
+ end
+
+ context 'mixed if: and changes: rules' do
+ let(:config) do
+ <<-EOY
+ regular-job:
+ script: 'echo Hello, World!'
+
+ rules-job:
+ script: "echo hello world, $CI_COMMIT_REF_NAME"
+ allow_failure: true
+ rules:
+ - changes:
+ - README.md
+ when: manual
+ - if: $CI_COMMIT_REF_NAME == "master"
+ when: on_success
+ allow_failure: false
+
+ delayed-job:
+ script: "echo See you later, World!"
+ rules:
+ - changes:
+ - README.md
+ when: delayed
+ start_in: 4 hours
+ allow_failure: true
+ - if: $CI_COMMIT_REF_NAME == "master"
+ when: delayed
+ start_in: 1 hour
+ EOY
+ end
+
+ context 'and changes: matches before if' do
+ before do
+ allow_next_instance_of(Ci::Pipeline) do |pipeline|
+ allow(pipeline).to receive(:modified_paths).and_return(%w[README.md])
+ end
+ end
+
+ it 'creates two jobs' do
+ expect(pipeline).to be_persisted
+ expect(build_names)
+ .to contain_exactly('regular-job', 'rules-job', 'delayed-job')
+ end
+
+ it 'sets when: for all jobs' do
+ expect(regular_job.when).to eq('on_success')
+ expect(rules_job.when).to eq('manual')
+ expect(delayed_job.when).to eq('delayed')
+ expect(delayed_job.options[:start_in]).to eq('4 hours')
+ end
+
+ it 'sets allow_failure: for all jobs' do
+ expect(regular_job.allow_failure).to eq(false)
+ expect(rules_job.allow_failure).to eq(true)
+ expect(delayed_job.allow_failure).to eq(true)
+ end
+ end
+
+ context 'and if: matches after changes' do
+ it 'includes both jobs' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('regular-job', 'rules-job', 'delayed-job')
+ end
+
+ it 'sets when: for the created rules job based on the second clause' do
+ expect(regular_job.when).to eq('on_success')
+ expect(rules_job.when).to eq('on_success')
+ expect(delayed_job.when).to eq('delayed')
+ expect(delayed_job.options[:start_in]).to eq('1 hour')
+ end
+ end
+
+ context 'and does not match' do
+ let(:ref) { 'refs/heads/wip' }
+
+ it_behaves_like 'rules jobs are excluded'
+
+ it 'sets when: for the created job' do
+ expect(regular_job.when).to eq('on_success')
+ end
+ end
+ end
+
+ context 'mixed if: and changes: clauses' do
+ let(:config) do
+ <<-EOY
+ regular-job:
+ script: 'echo Hello, World!'
+
+ rules-job:
+ script: "echo hello world, $CI_COMMIT_REF_NAME"
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ changes: [README.md]
+ when: on_success
+ allow_failure: true
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ changes: [app.rb]
+ when: manual
+ EOY
+ end
+
+ context 'with if matches and changes matches' do
+ before do
+ allow_next_instance_of(Ci::Pipeline) do |pipeline|
+ allow(pipeline).to receive(:modified_paths).and_return(%w[app.rb])
+ end
+ end
+
+ it 'persists all jobs' do
+ expect(pipeline).to be_persisted
+ expect(regular_job).to be_persisted
+ expect(rules_job).to be_persisted
+ expect(rules_job.when).to eq('manual')
+ expect(rules_job.allow_failure).to eq(false)
+ end
+ end
+
+ context 'with if matches and no change matches' do
+ it_behaves_like 'rules jobs are excluded'
+ end
+
+ context 'with change matches and no if matches' do
+ let(:ref) { 'refs/heads/feature' }
+
+ before do
+ allow_next_instance_of(Ci::Pipeline) do |pipeline|
+ allow(pipeline).to receive(:modified_paths).and_return(%w[README.md])
+ end
+ end
+
+ it_behaves_like 'rules jobs are excluded'
+ end
+
+ context 'and no matches' do
+ let(:ref) { 'refs/heads/feature' }
+
+ it_behaves_like 'rules jobs are excluded'
+ end
+ end
+
+ context 'complex if: allow_failure usages' do
+ let(:config) do
+ <<-EOY
+ job-1:
+ script: "exit 1"
+ allow_failure: true
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ allow_failure: false
+
+ job-2:
+ script: "exit 1"
+ allow_failure: true
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/
+ allow_failure: false
+
+ job-3:
+ script: "exit 1"
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/
+ allow_failure: true
+
+ job-4:
+ script: "exit 1"
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ allow_failure: false
+
+ job-5:
+ script: "exit 1"
+ allow_failure: false
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ allow_failure: true
+
+ job-6:
+ script: "exit 1"
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/
+ allow_failure: false
+ - allow_failure: true
+ EOY
+ end
+
+ it 'creates a pipeline' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('job-1', 'job-4', 'job-5', 'job-6')
+ end
+
+ it 'assigns job:allow_failure values to the builds' do
+ expect(find_job('job-1').allow_failure).to eq(false)
+ expect(find_job('job-4').allow_failure).to eq(false)
+ expect(find_job('job-5').allow_failure).to eq(true)
+ expect(find_job('job-6').allow_failure).to eq(true)
+ end
+ end
+
+ context 'complex if: allow_failure & when usages' do
+ let(:config) do
+ <<-EOY
+ job-1:
+ script: "exit 1"
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ when: manual
+
+ job-2:
+ script: "exit 1"
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ when: manual
+ allow_failure: true
+
+ job-3:
+ script: "exit 1"
+ allow_failure: true
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ when: manual
+
+ job-4:
+ script: "exit 1"
+ allow_failure: true
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ when: manual
+ allow_failure: false
+
+ job-5:
+ script: "exit 1"
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/
+ when: manual
+ allow_failure: false
+ - when: always
+ allow_failure: true
+
+ job-6:
+ script: "exit 1"
+ allow_failure: false
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ when: manual
+
+ job-7:
+ script: "exit 1"
+ allow_failure: false
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/
+ when: manual
+ - when: :on_failure
+ allow_failure: true
+ EOY
+ end
+
+ it 'creates a pipeline' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly(
+ 'job-1', 'job-2', 'job-3', 'job-4', 'job-5', 'job-6', 'job-7'
+ )
+ end
+
+ it 'assigns job:allow_failure values to the builds' do
+ expect(find_job('job-1').allow_failure).to eq(false)
+ expect(find_job('job-2').allow_failure).to eq(true)
+ expect(find_job('job-3').allow_failure).to eq(true)
+ expect(find_job('job-4').allow_failure).to eq(false)
+ expect(find_job('job-5').allow_failure).to eq(true)
+ expect(find_job('job-6').allow_failure).to eq(false)
+ expect(find_job('job-7').allow_failure).to eq(true)
+ end
+
+ it 'assigns job:when values to the builds' do
+ expect(find_job('job-1').when).to eq('manual')
+ expect(find_job('job-2').when).to eq('manual')
+ expect(find_job('job-3').when).to eq('manual')
+ expect(find_job('job-4').when).to eq('manual')
+ expect(find_job('job-5').when).to eq('always')
+ expect(find_job('job-6').when).to eq('manual')
+ expect(find_job('job-7').when).to eq('on_failure')
+ end
+ end
+
+ context 'deploy freeze period `if:` clause' do
+ # '0 23 * * 5' == "At 23:00 on Friday."", '0 7 * * 1' == "At 07:00 on Monday.""
+ let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: '0 23 * * 5', freeze_end: '0 7 * * 1') }
+
+ context 'with 2 jobs' do
+ let(:config) do
+ <<-EOY
+ stages:
+ - test
+ - deploy
+
+ test-job:
+ script:
+ - echo 'running TEST stage'
+
+ deploy-job:
+ stage: deploy
+ script:
+ - echo 'running DEPLOY stage'
+ rules:
+ - if: $CI_DEPLOY_FREEZE == null
+ EOY
+ end
+
+ context 'when outside freeze period' do
+ it 'creates two jobs' do
+ Timecop.freeze(2020, 4, 10, 22, 59) do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('test-job', 'deploy-job')
+ end
+ end
+ end
+
+ context 'when inside freeze period' do
+ it 'creates one job' do
+ Timecop.freeze(2020, 4, 10, 23, 1) do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('test-job')
+ end
+ end
+ end
+ end
+
+ context 'with 1 job' do
+ let(:config) do
+ <<-EOY
+ stages:
+ - deploy
+
+ deploy-job:
+ stage: deploy
+ script:
+ - echo 'running DEPLOY stage'
+ rules:
+ - if: $CI_DEPLOY_FREEZE == null
+ EOY
+ end
+
+ context 'when outside freeze period' do
+ it 'creates two jobs' do
+ Timecop.freeze(2020, 4, 10, 22, 59) do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('deploy-job')
+ end
+ end
+ end
+
+ context 'when inside freeze period' do
+ it 'does not create the pipeline', :aggregate_failures do
+ Timecop.freeze(2020, 4, 10, 23, 1) do
+ expect(response).to be_error
+ expect(pipeline).not_to be_persisted
+ end
+ end
+ end
+ end
+ end
+
+ context 'with when:manual' do
+ let(:config) do
+ <<-EOY
+ job-with-rules:
+ script: 'echo hey'
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+
+ job-when-with-rules:
+ script: 'echo hey'
+ when: manual
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+
+ job-when-with-rules-when:
+ script: 'echo hey'
+ when: manual
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ when: on_success
+
+ job-with-rules-when:
+ script: 'echo hey'
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ when: manual
+
+ job-without-rules:
+ script: 'echo this is a job with NO rules'
+ EOY
+ end
+
+ let(:job_with_rules) { find_job('job-with-rules') }
+ let(:job_when_with_rules) { find_job('job-when-with-rules') }
+ let(:job_when_with_rules_when) { find_job('job-when-with-rules-when') }
+ let(:job_with_rules_when) { find_job('job-with-rules-when') }
+ let(:job_without_rules) { find_job('job-without-rules') }
+
+ context 'when matching the rules' do
+ let(:ref) { 'refs/heads/master' }
+
+ it 'adds the job-with-rules with a when:manual' do
+ expect(job_with_rules).to be_persisted
+ expect(job_when_with_rules).to be_persisted
+ expect(job_when_with_rules_when).to be_persisted
+ expect(job_with_rules_when).to be_persisted
+ expect(job_without_rules).to be_persisted
+
+ expect(job_with_rules.when).to eq('on_success')
+ expect(job_when_with_rules.when).to eq('manual')
+ expect(job_when_with_rules_when.when).to eq('on_success')
+ expect(job_with_rules_when.when).to eq('manual')
+ expect(job_without_rules.when).to eq('on_success')
+ end
+ end
+
+ context 'when there is no match to the rule' do
+ let(:ref) { 'refs/heads/wip' }
+
+ it 'does not add job_with_rules' do
+ expect(job_with_rules).to be_nil
+ expect(job_when_with_rules).to be_nil
+ expect(job_when_with_rules_when).to be_nil
+ expect(job_with_rules_when).to be_nil
+ expect(job_without_rules).to be_persisted
+ end
+ end
end
end
@@ -447,5 +1238,232 @@ RSpec.describe Ci::CreatePipelineService do
end
end
end
+
+ context 'with persisted variables' do
+ let(:config) do
+ <<-EOY
+ workflow:
+ rules:
+ - if: $CI_COMMIT_REF_NAME == "master"
+
+ regular-job:
+ script: 'echo Hello, World!'
+ EOY
+ end
+
+ context 'with matches' do
+ it 'creates a pipeline' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('regular-job')
+ end
+ end
+
+ context 'with no matches' do
+ let(:ref) { 'refs/heads/feature' }
+
+ it 'does not create a pipeline', :aggregate_failures do
+ expect(response).to be_error
+ expect(pipeline).not_to be_persisted
+ end
+ end
+ end
+
+ context 'with pipeline variables' do
+ let(:pipeline) do
+ execute_service(variables_attributes: variables_attributes).payload
+ end
+
+ let(:config) do
+ <<-EOY
+ workflow:
+ rules:
+ - if: $SOME_VARIABLE
+
+ regular-job:
+ script: 'echo Hello, World!'
+ EOY
+ end
+
+ context 'with matches' do
+ let(:variables_attributes) do
+ [{ key: 'SOME_VARIABLE', secret_value: 'SOME_VAR' }]
+ end
+
+ it 'creates a pipeline' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('regular-job')
+ end
+ end
+
+ context 'with no matches' do
+ let(:variables_attributes) { {} }
+
+ it 'does not create a pipeline', :aggregate_failures do
+ expect(response).to be_error
+ expect(pipeline).not_to be_persisted
+ end
+ end
+ end
+
+ context 'with trigger variables' do
+ let(:pipeline) do
+ execute_service do |pipeline|
+ pipeline.variables.build(variables)
+ end.payload
+ end
+
+ let(:config) do
+ <<-EOY
+ workflow:
+ rules:
+ - if: $SOME_VARIABLE
+
+ regular-job:
+ script: 'echo Hello, World!'
+ EOY
+ end
+
+ context 'with matches' do
+ let(:variables) do
+ [{ key: 'SOME_VARIABLE', secret_value: 'SOME_VAR' }]
+ end
+
+ it 'creates a pipeline' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('regular-job')
+ end
+
+ context 'when a job requires the same variable' do
+ let(:config) do
+ <<-EOY
+ workflow:
+ rules:
+ - if: $SOME_VARIABLE
+
+ build:
+ stage: build
+ script: 'echo build'
+ rules:
+ - if: $SOME_VARIABLE
+
+ test1:
+ stage: test
+ script: 'echo test1'
+ needs: [build]
+
+ test2:
+ stage: test
+ script: 'echo test2'
+ EOY
+ end
+
+ it 'creates a pipeline' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('build', 'test1', 'test2')
+ end
+ end
+ end
+
+ context 'with no matches' do
+ let(:variables) { {} }
+
+ it 'does not create a pipeline', :aggregate_failures do
+ expect(response).to be_error
+ expect(pipeline).not_to be_persisted
+ end
+
+ context 'when a job requires the same variable' do
+ let(:config) do
+ <<-EOY
+ workflow:
+ rules:
+ - if: $SOME_VARIABLE
+
+ build:
+ stage: build
+ script: 'echo build'
+ rules:
+ - if: $SOME_VARIABLE
+
+ test1:
+ stage: test
+ script: 'echo test1'
+ needs: [build]
+
+ test2:
+ stage: test
+ script: 'echo test2'
+ EOY
+ end
+
+ it 'does not create a pipeline', :aggregate_failures do
+ expect(response).to be_error
+ expect(pipeline).not_to be_persisted
+ end
+ end
+ end
+ end
+
+ context 'changes' do
+ shared_examples 'comparing file changes with workflow rules' do
+ context 'when matches' do
+ before do
+ allow_next_instance_of(Ci::Pipeline) do |pipeline|
+ allow(pipeline).to receive(:modified_paths).and_return(%w[file1.md])
+ end
+ end
+
+ it 'creates the pipeline with a job' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('job')
+ end
+ end
+
+ context 'when does not match' do
+ before do
+ allow_next_instance_of(Ci::Pipeline) do |pipeline|
+ allow(pipeline).to receive(:modified_paths).and_return(%w[unknown])
+ end
+ end
+
+ it 'creates the pipeline with a job' do
+ expect(pipeline.errors.full_messages).to eq(['Pipeline filtered out by workflow rules.'])
+ expect(response).to be_error
+ expect(pipeline).not_to be_persisted
+ end
+ end
+ end
+
+ context 'changes is an array' do
+ let(:config) do
+ <<-EOY
+ workflow:
+ rules:
+ - changes: [file1.md]
+
+ job:
+ script: exit 0
+ EOY
+ end
+
+ it_behaves_like 'comparing file changes with workflow rules'
+ end
+
+ context 'changes:paths is an array' do
+ let(:config) do
+ <<-EOY
+ workflow:
+ rules:
+ - changes:
+ paths: [file1.md]
+
+ job:
+ script: exit 0
+ EOY
+ end
+
+ it_behaves_like 'comparing file changes with workflow rules'
+ end
+ end
end
end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 9cef7f7dadb..a9442b0dc68 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -18,6 +18,7 @@ RSpec.describe Ci::CreatePipelineService do
# rubocop:disable Metrics/ParameterLists
def execute_service(
source: :push,
+ before: '00000000',
after: project.commit.id,
ref: ref_name,
trigger_request: nil,
@@ -29,7 +30,7 @@ RSpec.describe Ci::CreatePipelineService do
target_sha: nil,
save_on_errors: true)
params = { ref: ref,
- before: '00000000',
+ before: before,
after: after,
variables_attributes: variables_attributes,
push_options: push_options,
@@ -1865,818 +1866,6 @@ RSpec.describe Ci::CreatePipelineService do
end
end
end
-
- context 'when rules are used' do
- let(:ref_name) { 'refs/heads/master' }
- let(:response) { execute_service }
- let(:pipeline) { response.payload }
- let(:build_names) { pipeline.builds.pluck(:name) }
- let(:regular_job) { find_job('regular-job') }
- let(:rules_job) { find_job('rules-job') }
- let(:delayed_job) { find_job('delayed-job') }
-
- context 'with when:manual' do
- let(:config) do
- <<-EOY
- job-with-rules:
- script: 'echo hey'
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /master/
-
- job-when-with-rules:
- script: 'echo hey'
- when: manual
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /master/
-
- job-when-with-rules-when:
- script: 'echo hey'
- when: manual
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /master/
- when: on_success
-
- job-with-rules-when:
- script: 'echo hey'
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /master/
- when: manual
-
- job-without-rules:
- script: 'echo this is a job with NO rules'
- EOY
- end
-
- let(:job_with_rules) { find_job('job-with-rules') }
- let(:job_when_with_rules) { find_job('job-when-with-rules') }
- let(:job_when_with_rules_when) { find_job('job-when-with-rules-when') }
- let(:job_with_rules_when) { find_job('job-with-rules-when') }
- let(:job_without_rules) { find_job('job-without-rules') }
-
- context 'when matching the rules' do
- let(:ref_name) { 'refs/heads/master' }
-
- it 'adds the job-with-rules with a when:manual' do
- expect(job_with_rules).to be_persisted
- expect(job_when_with_rules).to be_persisted
- expect(job_when_with_rules_when).to be_persisted
- expect(job_with_rules_when).to be_persisted
- expect(job_without_rules).to be_persisted
-
- expect(job_with_rules.when).to eq('on_success')
- expect(job_when_with_rules.when).to eq('manual')
- expect(job_when_with_rules_when.when).to eq('on_success')
- expect(job_with_rules_when.when).to eq('manual')
- expect(job_without_rules.when).to eq('on_success')
- end
- end
-
- context 'when there is no match to the rule' do
- let(:ref_name) { 'refs/heads/wip' }
-
- it 'does not add job_with_rules' do
- expect(job_with_rules).to be_nil
- expect(job_when_with_rules).to be_nil
- expect(job_when_with_rules_when).to be_nil
- expect(job_with_rules_when).to be_nil
- expect(job_without_rules).to be_persisted
- end
- end
- end
-
- shared_examples 'rules jobs are excluded' do
- it 'only persists the job without rules' do
- expect(pipeline).to be_persisted
- expect(regular_job).to be_persisted
- expect(rules_job).to be_nil
- expect(delayed_job).to be_nil
- end
- end
-
- def find_job(name)
- pipeline.builds.find_by(name: name)
- end
-
- before do
- stub_ci_pipeline_yaml_file(config)
- allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
- end
-
- context 'with simple if: clauses' do
- let(:config) do
- <<-EOY
- regular-job:
- script: 'echo Hello, World!'
-
- master-job:
- script: "echo hello world, $CI_COMMIT_REF_NAME"
- rules:
- - if: $CI_COMMIT_REF_NAME == "nonexistant-branch"
- when: never
- - if: $CI_COMMIT_REF_NAME =~ /master/
- when: manual
-
- negligible-job:
- script: "exit 1"
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /master/
- allow_failure: true
-
- delayed-job:
- script: "echo See you later, World!"
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /master/
- when: delayed
- start_in: 1 hour
-
- never-job:
- script: "echo Goodbye, World!"
- rules:
- - if: $CI_COMMIT_REF_NAME
- when: never
- EOY
- end
-
- context 'with matches' do
- it 'creates a pipeline with the vanilla and manual jobs' do
- expect(pipeline).to be_persisted
- expect(build_names).to contain_exactly(
- 'regular-job', 'delayed-job', 'master-job', 'negligible-job'
- )
- end
-
- it 'assigns job:when values to the builds' do
- expect(find_job('regular-job').when).to eq('on_success')
- expect(find_job('master-job').when).to eq('manual')
- expect(find_job('negligible-job').when).to eq('on_success')
- expect(find_job('delayed-job').when).to eq('delayed')
- end
-
- it 'assigns job:allow_failure values to the builds' do
- expect(find_job('regular-job').allow_failure).to eq(false)
- expect(find_job('master-job').allow_failure).to eq(false)
- expect(find_job('negligible-job').allow_failure).to eq(true)
- expect(find_job('delayed-job').allow_failure).to eq(false)
- end
-
- it 'assigns start_in for delayed jobs' do
- expect(delayed_job.options[:start_in]).to eq('1 hour')
- end
- end
-
- context 'with no matches' do
- let(:ref_name) { 'refs/heads/feature' }
-
- it_behaves_like 'rules jobs are excluded'
- end
- end
-
- context 'with complex if: clauses' do
- let(:config) do
- <<-EOY
- regular-job:
- script: 'echo Hello, World!'
- rules:
- - if: $VAR == 'present' && $OTHER || $CI_COMMIT_REF_NAME
- when: manual
- allow_failure: true
- EOY
- end
-
- it 'matches the first rule' do
- expect(pipeline).to be_persisted
- expect(build_names).to contain_exactly('regular-job')
- expect(regular_job.when).to eq('manual')
- expect(regular_job.allow_failure).to eq(true)
- end
- end
-
- context 'with changes:' do
- let(:config) do
- <<-EOY
- regular-job:
- script: 'echo Hello, World!'
-
- rules-job:
- script: "echo hello world, $CI_COMMIT_REF_NAME"
- rules:
- - changes:
- - README.md
- when: manual
- - changes:
- - app.rb
- when: on_success
-
- delayed-job:
- script: "echo See you later, World!"
- rules:
- - changes:
- - README.md
- when: delayed
- start_in: 4 hours
-
- negligible-job:
- script: "can be failed sometimes"
- rules:
- - changes:
- - README.md
- allow_failure: true
-
- README:
- script: "I use variables for changes!"
- rules:
- - changes:
- - $CI_JOB_NAME*
-
- changes-paths:
- script: "I am using a new syntax!"
- rules:
- - changes:
- paths: [README.md]
- EOY
- end
-
- context 'and matches' do
- before do
- allow_any_instance_of(Ci::Pipeline)
- .to receive(:modified_paths).and_return(%w[README.md])
- end
-
- it 'creates five jobs' do
- expect(pipeline).to be_persisted
- expect(build_names).to contain_exactly(
- 'regular-job', 'rules-job', 'delayed-job', 'negligible-job', 'README', 'changes-paths'
- )
- end
-
- it 'sets when: for all jobs' do
- expect(regular_job.when).to eq('on_success')
- expect(rules_job.when).to eq('manual')
- expect(delayed_job.when).to eq('delayed')
- expect(delayed_job.options[:start_in]).to eq('4 hours')
- end
-
- it 'sets allow_failure: for negligible job' do
- expect(find_job('negligible-job').allow_failure).to eq(true)
- end
- end
-
- context 'and matches the second rule' do
- before do
- allow_any_instance_of(Ci::Pipeline)
- .to receive(:modified_paths).and_return(%w[app.rb])
- end
-
- it 'includes both jobs' do
- expect(pipeline).to be_persisted
- expect(build_names).to contain_exactly('regular-job', 'rules-job')
- end
-
- it 'sets when: for the created rules job based on the second clause' do
- expect(regular_job.when).to eq('on_success')
- expect(rules_job.when).to eq('on_success')
- end
- end
-
- context 'and does not match' do
- before do
- allow_any_instance_of(Ci::Pipeline)
- .to receive(:modified_paths).and_return(%w[useless_script.rb])
- end
-
- it_behaves_like 'rules jobs are excluded'
-
- it 'sets when: for the created job' do
- expect(regular_job.when).to eq('on_success')
- end
- end
- end
-
- context 'with mixed if: and changes: rules' do
- let(:config) do
- <<-EOY
- regular-job:
- script: 'echo Hello, World!'
-
- rules-job:
- script: "echo hello world, $CI_COMMIT_REF_NAME"
- allow_failure: true
- rules:
- - changes:
- - README.md
- when: manual
- - if: $CI_COMMIT_REF_NAME == "master"
- when: on_success
- allow_failure: false
-
- delayed-job:
- script: "echo See you later, World!"
- rules:
- - changes:
- - README.md
- when: delayed
- start_in: 4 hours
- allow_failure: true
- - if: $CI_COMMIT_REF_NAME == "master"
- when: delayed
- start_in: 1 hour
- EOY
- end
-
- context 'and changes: matches before if' do
- before do
- allow_any_instance_of(Ci::Pipeline)
- .to receive(:modified_paths).and_return(%w[README.md])
- end
-
- it 'creates two jobs' do
- expect(pipeline).to be_persisted
- expect(build_names)
- .to contain_exactly('regular-job', 'rules-job', 'delayed-job')
- end
-
- it 'sets when: for all jobs' do
- expect(regular_job.when).to eq('on_success')
- expect(rules_job.when).to eq('manual')
- expect(delayed_job.when).to eq('delayed')
- expect(delayed_job.options[:start_in]).to eq('4 hours')
- end
-
- it 'sets allow_failure: for all jobs' do
- expect(regular_job.allow_failure).to eq(false)
- expect(rules_job.allow_failure).to eq(true)
- expect(delayed_job.allow_failure).to eq(true)
- end
- end
-
- context 'and if: matches after changes' do
- it 'includes both jobs' do
- expect(pipeline).to be_persisted
- expect(build_names).to contain_exactly('regular-job', 'rules-job', 'delayed-job')
- end
-
- it 'sets when: for the created rules job based on the second clause' do
- expect(regular_job.when).to eq('on_success')
- expect(rules_job.when).to eq('on_success')
- expect(delayed_job.when).to eq('delayed')
- expect(delayed_job.options[:start_in]).to eq('1 hour')
- end
- end
-
- context 'and does not match' do
- let(:ref_name) { 'refs/heads/wip' }
-
- it_behaves_like 'rules jobs are excluded'
-
- it 'sets when: for the created job' do
- expect(regular_job.when).to eq('on_success')
- end
- end
- end
-
- context 'with mixed if: and changes: clauses' do
- let(:config) do
- <<-EOY
- regular-job:
- script: 'echo Hello, World!'
-
- rules-job:
- script: "echo hello world, $CI_COMMIT_REF_NAME"
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /master/
- changes: [README.md]
- when: on_success
- allow_failure: true
- - if: $CI_COMMIT_REF_NAME =~ /master/
- changes: [app.rb]
- when: manual
- EOY
- end
-
- context 'with if matches and changes matches' do
- before do
- allow_any_instance_of(Ci::Pipeline)
- .to receive(:modified_paths).and_return(%w[app.rb])
- end
-
- it 'persists all jobs' do
- expect(pipeline).to be_persisted
- expect(regular_job).to be_persisted
- expect(rules_job).to be_persisted
- expect(rules_job.when).to eq('manual')
- expect(rules_job.allow_failure).to eq(false)
- end
- end
-
- context 'with if matches and no change matches' do
- it_behaves_like 'rules jobs are excluded'
- end
-
- context 'with change matches and no if matches' do
- let(:ref_name) { 'refs/heads/feature' }
-
- before do
- allow_any_instance_of(Ci::Pipeline)
- .to receive(:modified_paths).and_return(%w[README.md])
- end
-
- it_behaves_like 'rules jobs are excluded'
- end
-
- context 'and no matches' do
- let(:ref_name) { 'refs/heads/feature' }
-
- it_behaves_like 'rules jobs are excluded'
- end
- end
-
- context 'with complex if: allow_failure usages' do
- let(:config) do
- <<-EOY
- job-1:
- script: "exit 1"
- allow_failure: true
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /master/
- allow_failure: false
-
- job-2:
- script: "exit 1"
- allow_failure: true
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/
- allow_failure: false
-
- job-3:
- script: "exit 1"
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/
- allow_failure: true
-
- job-4:
- script: "exit 1"
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /master/
- allow_failure: false
-
- job-5:
- script: "exit 1"
- allow_failure: false
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /master/
- allow_failure: true
-
- job-6:
- script: "exit 1"
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/
- allow_failure: false
- - allow_failure: true
- EOY
- end
-
- it 'creates a pipeline' do
- expect(pipeline).to be_persisted
- expect(build_names).to contain_exactly('job-1', 'job-4', 'job-5', 'job-6')
- end
-
- it 'assigns job:allow_failure values to the builds' do
- expect(find_job('job-1').allow_failure).to eq(false)
- expect(find_job('job-4').allow_failure).to eq(false)
- expect(find_job('job-5').allow_failure).to eq(true)
- expect(find_job('job-6').allow_failure).to eq(true)
- end
- end
-
- context 'with complex if: allow_failure & when usages' do
- let(:config) do
- <<-EOY
- job-1:
- script: "exit 1"
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /master/
- when: manual
-
- job-2:
- script: "exit 1"
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /master/
- when: manual
- allow_failure: true
-
- job-3:
- script: "exit 1"
- allow_failure: true
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /master/
- when: manual
-
- job-4:
- script: "exit 1"
- allow_failure: true
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /master/
- when: manual
- allow_failure: false
-
- job-5:
- script: "exit 1"
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/
- when: manual
- allow_failure: false
- - when: always
- allow_failure: true
-
- job-6:
- script: "exit 1"
- allow_failure: false
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /master/
- when: manual
-
- job-7:
- script: "exit 1"
- allow_failure: false
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/
- when: manual
- - when: :on_failure
- allow_failure: true
- EOY
- end
-
- it 'creates a pipeline' do
- expect(pipeline).to be_persisted
- expect(build_names).to contain_exactly(
- 'job-1', 'job-2', 'job-3', 'job-4', 'job-5', 'job-6', 'job-7'
- )
- end
-
- it 'assigns job:allow_failure values to the builds' do
- expect(find_job('job-1').allow_failure).to eq(false)
- expect(find_job('job-2').allow_failure).to eq(true)
- expect(find_job('job-3').allow_failure).to eq(true)
- expect(find_job('job-4').allow_failure).to eq(false)
- expect(find_job('job-5').allow_failure).to eq(true)
- expect(find_job('job-6').allow_failure).to eq(false)
- expect(find_job('job-7').allow_failure).to eq(true)
- end
-
- it 'assigns job:when values to the builds' do
- expect(find_job('job-1').when).to eq('manual')
- expect(find_job('job-2').when).to eq('manual')
- expect(find_job('job-3').when).to eq('manual')
- expect(find_job('job-4').when).to eq('manual')
- expect(find_job('job-5').when).to eq('always')
- expect(find_job('job-6').when).to eq('manual')
- expect(find_job('job-7').when).to eq('on_failure')
- end
- end
-
- context 'with deploy freeze period `if:` clause' do
- # '0 23 * * 5' == "At 23:00 on Friday."", '0 7 * * 1' == "At 07:00 on Monday.""
- let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: '0 23 * * 5', freeze_end: '0 7 * * 1') }
-
- context 'with 2 jobs' do
- let(:config) do
- <<-EOY
- stages:
- - test
- - deploy
-
- test-job:
- script:
- - echo 'running TEST stage'
-
- deploy-job:
- stage: deploy
- script:
- - echo 'running DEPLOY stage'
- rules:
- - if: $CI_DEPLOY_FREEZE == null
- EOY
- end
-
- context 'when outside freeze period' do
- it 'creates two jobs' do
- Timecop.freeze(2020, 4, 10, 22, 59) do
- expect(pipeline).to be_persisted
- expect(build_names).to contain_exactly('test-job', 'deploy-job')
- end
- end
- end
-
- context 'when inside freeze period' do
- it 'creates one job' do
- Timecop.freeze(2020, 4, 10, 23, 1) do
- expect(pipeline).to be_persisted
- expect(build_names).to contain_exactly('test-job')
- end
- end
- end
- end
-
- context 'with 1 job' do
- let(:config) do
- <<-EOY
- stages:
- - deploy
-
- deploy-job:
- stage: deploy
- script:
- - echo 'running DEPLOY stage'
- rules:
- - if: $CI_DEPLOY_FREEZE == null
- EOY
- end
-
- context 'when outside freeze period' do
- it 'creates two jobs' do
- Timecop.freeze(2020, 4, 10, 22, 59) do
- expect(pipeline).to be_persisted
- expect(build_names).to contain_exactly('deploy-job')
- end
- end
- end
-
- context 'when inside freeze period' do
- it 'does not create the pipeline', :aggregate_failures do
- Timecop.freeze(2020, 4, 10, 23, 1) do
- expect(response).to be_error
- expect(pipeline).not_to be_persisted
- end
- end
- end
- end
- end
-
- context 'with workflow rules with persisted variables' do
- let(:config) do
- <<-EOY
- workflow:
- rules:
- - if: $CI_COMMIT_REF_NAME == "master"
-
- regular-job:
- script: 'echo Hello, World!'
- EOY
- end
-
- context 'with matches' do
- it 'creates a pipeline' do
- expect(pipeline).to be_persisted
- expect(build_names).to contain_exactly('regular-job')
- end
- end
-
- context 'with no matches' do
- let(:ref_name) { 'refs/heads/feature' }
-
- it 'does not create a pipeline', :aggregate_failures do
- expect(response).to be_error
- expect(pipeline).not_to be_persisted
- end
- end
- end
-
- context 'with workflow rules with pipeline variables' do
- let(:pipeline) do
- execute_service(variables_attributes: variables_attributes).payload
- end
-
- let(:config) do
- <<-EOY
- workflow:
- rules:
- - if: $SOME_VARIABLE
-
- regular-job:
- script: 'echo Hello, World!'
- EOY
- end
-
- context 'with matches' do
- let(:variables_attributes) do
- [{ key: 'SOME_VARIABLE', secret_value: 'SOME_VAR' }]
- end
-
- it 'creates a pipeline' do
- expect(pipeline).to be_persisted
- expect(build_names).to contain_exactly('regular-job')
- end
- end
-
- context 'with no matches' do
- let(:variables_attributes) { {} }
-
- it 'does not create a pipeline', :aggregate_failures do
- expect(response).to be_error
- expect(pipeline).not_to be_persisted
- end
- end
- end
-
- context 'with workflow rules with trigger variables' do
- let(:pipeline) do
- execute_service do |pipeline|
- pipeline.variables.build(variables)
- end.payload
- end
-
- let(:config) do
- <<-EOY
- workflow:
- rules:
- - if: $SOME_VARIABLE
-
- regular-job:
- script: 'echo Hello, World!'
- EOY
- end
-
- context 'with matches' do
- let(:variables) do
- [{ key: 'SOME_VARIABLE', secret_value: 'SOME_VAR' }]
- end
-
- it 'creates a pipeline' do
- expect(pipeline).to be_persisted
- expect(build_names).to contain_exactly('regular-job')
- end
-
- context 'when a job requires the same variable' do
- let(:config) do
- <<-EOY
- workflow:
- rules:
- - if: $SOME_VARIABLE
-
- build:
- stage: build
- script: 'echo build'
- rules:
- - if: $SOME_VARIABLE
-
- test1:
- stage: test
- script: 'echo test1'
- needs: [build]
-
- test2:
- stage: test
- script: 'echo test2'
- EOY
- end
-
- it 'creates a pipeline' do
- expect(pipeline).to be_persisted
- expect(build_names).to contain_exactly('build', 'test1', 'test2')
- end
- end
- end
-
- context 'with no matches' do
- let(:variables) { {} }
-
- it 'does not create a pipeline', :aggregate_failures do
- expect(response).to be_error
- expect(pipeline).not_to be_persisted
- end
-
- context 'when a job requires the same variable' do
- let(:config) do
- <<-EOY
- workflow:
- rules:
- - if: $SOME_VARIABLE
-
- build:
- stage: build
- script: 'echo build'
- rules:
- - if: $SOME_VARIABLE
-
- test1:
- stage: test
- script: 'echo test1'
- needs: [build]
-
- test2:
- stage: test
- script: 'echo test2'
- EOY
- end
-
- it 'does not create a pipeline', :aggregate_failures do
- expect(response).to be_error
- expect(pipeline).not_to be_persisted
- end
- end
- end
- end
- end
end
describe '#execute!' do
diff --git a/spec/services/ci/deployments/destroy_service_spec.rb b/spec/services/ci/deployments/destroy_service_spec.rb
new file mode 100644
index 00000000000..60a57c05728
--- /dev/null
+++ b/spec/services/ci/deployments/destroy_service_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ci::Deployments::DestroyService do
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:environment) { create(:environment, project: project) }
+ let(:commits) { project.repository.commits(nil, { limit: 3 }) }
+ let!(:deploy) do
+ create(
+ :deployment,
+ :success,
+ project: project,
+ environment: environment,
+ deployable: nil,
+ sha: commits[2].sha
+ )
+ end
+
+ let!(:running_deploy) do
+ create(
+ :deployment,
+ :running,
+ project: project,
+ environment: environment,
+ deployable: nil,
+ sha: commits[1].sha
+ )
+ end
+
+ let!(:old_deploy) do
+ create(
+ :deployment,
+ :success,
+ project: project,
+ environment: environment,
+ deployable: nil,
+ sha: commits[0].sha,
+ finished_at: 1.year.ago
+ )
+ end
+
+ let(:user) { project.first_owner }
+
+ subject { described_class.new(project, user) }
+
+ context 'when deleting a deployment' do
+ it 'delete is accepted for old deployment' do
+ expect(subject.execute(old_deploy)).to be_success
+ end
+
+ it 'does not delete a running deployment' do
+ response = subject.execute(running_deploy)
+ expect(response).to be_an_error
+ expect(response.message).to eq("Cannot destroy running deployment")
+ end
+
+ it 'does not delete the last deployment' do
+ response = subject.execute(deploy)
+ expect(response).to be_an_error
+ expect(response.message).to eq("Deployment currently deployed to environment")
+ end
+ end
+end
diff --git a/spec/services/ci/destroy_pipeline_service_spec.rb b/spec/services/ci/destroy_pipeline_service_spec.rb
index 045051c7152..6bd7fe7559c 100644
--- a/spec/services/ci/destroy_pipeline_service_spec.rb
+++ b/spec/services/ci/destroy_pipeline_service_spec.rb
@@ -90,15 +90,23 @@ RSpec.describe ::Ci::DestroyPipelineService do
end
end
- context 'when pipeline is in cancelable state' do
- before do
- allow(pipeline).to receive(:cancelable?).and_return(true)
- end
+ context 'when pipeline is in cancelable state', :sidekiq_inline do
+ let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
+ let!(:child_pipeline) { create(:ci_pipeline, :running, child_of: pipeline) }
+ let!(:child_build) { create(:ci_build, :running, pipeline: child_pipeline) }
+
+ it 'cancels the pipelines sync' do
+ # turn off deletion for all instances of pipeline to allow for testing cancellation
+ allow(pipeline).to receive_message_chain(:reset, :destroy!)
+ allow_next_found_instance_of(Ci::Pipeline) { |p| allow(p).to receive_message_chain(:reset, :destroy!) }
- it 'cancels the pipeline' do
- expect(pipeline).to receive(:cancel_running)
+ # ensure cancellation happens sync so we accumulate minutes
+ expect(::Ci::CancelPipelineWorker).not_to receive(:perform)
subject
+
+ expect(build.reload.status).to eq('canceled')
+ expect(child_build.reload.status).to eq('canceled')
end
end
end
diff --git a/spec/services/ci/job_artifacts/create_service_spec.rb b/spec/services/ci/job_artifacts/create_service_spec.rb
index b7a810ce47e..7b3f67b192f 100644
--- a/spec/services/ci/job_artifacts/create_service_spec.rb
+++ b/spec/services/ci/job_artifacts/create_service_spec.rb
@@ -34,6 +34,14 @@ RSpec.describe Ci::JobArtifacts::CreateService do
subject { service.execute(artifacts_file, params, metadata_file: metadata_file) }
context 'when artifacts file is uploaded' do
+ it 'logs the created artifact' do
+ expect(Gitlab::Ci::Artifacts::Logger)
+ .to receive(:log_created)
+ .with(an_instance_of(Ci::JobArtifact))
+
+ subject
+ end
+
it 'returns artifact in the response' do
response = subject
new_artifact = job.job_artifacts.last
diff --git a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
index 05069054483..9ca39d4d32e 100644
--- a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
+++ b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
@@ -40,7 +40,14 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do
expect { execute }.not_to change { artifact_with_file.file.exists? }
end
- it 'deletes the artifact records' do
+ it 'deletes the artifact records and logs them' do
+ expect(Gitlab::Ci::Artifacts::Logger)
+ .to receive(:log_deleted)
+ .with(
+ match_array([artifact_with_file, artifact_without_file]),
+ 'Ci::JobArtifacts::DestroyBatchService#execute'
+ )
+
expect { subject }.to change { Ci::JobArtifact.count }.by(-2)
end
diff --git a/spec/services/ci/list_config_variables_service_spec.rb b/spec/services/ci/list_config_variables_service_spec.rb
index 1735f4cfc97..4953b18bfcc 100644
--- a/spec/services/ci/list_config_variables_service_spec.rb
+++ b/spec/services/ci/list_config_variables_service_spec.rb
@@ -40,8 +40,8 @@ RSpec.describe Ci::ListConfigVariablesService, :use_clean_rails_memory_store_cac
it 'returns variable list' do
expect(subject['KEY1']).to eq({ value: 'val 1', description: 'description 1' })
expect(subject['KEY2']).to eq({ value: 'val 2', description: '' })
- expect(subject['KEY3']).to eq({ value: 'val 3', description: nil })
- expect(subject['KEY4']).to eq({ value: 'val 4', description: nil })
+ expect(subject['KEY3']).to eq({ value: 'val 3' })
+ expect(subject['KEY4']).to eq({ value: 'val 4' })
end
end
diff --git a/spec/services/ci/parse_dotenv_artifact_service_spec.rb b/spec/services/ci/parse_dotenv_artifact_service_spec.rb
index aaab849cd93..7b3af33ac72 100644
--- a/spec/services/ci/parse_dotenv_artifact_service_spec.rb
+++ b/spec/services/ci/parse_dotenv_artifact_service_spec.rb
@@ -292,7 +292,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do
end
context 'when build does not have a dotenv artifact' do
- let!(:artifact) { }
+ let!(:artifact) {}
it 'raises an error' do
expect { subject }.to raise_error(ArgumentError)
diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb
index 7868629d34d..289e004fcce 100644
--- a/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb
+++ b/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb
@@ -87,7 +87,7 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService::StatusCollection
describe '#processing_processables' do
it 'returns processables marked as processing' do
- expect(collection.processing_processables.map { |processable| processable[:id]} )
+ expect(collection.processing_processables.map { |processable| processable[:id] } )
.to contain_exactly(build_a.id, build_b.id, test_a.id, test_b.id, deploy.id)
end
end
diff --git a/spec/services/ci/process_build_service_spec.rb b/spec/services/ci/process_build_service_spec.rb
index b54fc45d36a..2fcb4ce73ff 100644
--- a/spec/services/ci/process_build_service_spec.rb
+++ b/spec/services/ci/process_build_service_spec.rb
@@ -101,7 +101,7 @@ RSpec.describe Ci::ProcessBuildService, '#execute' do
context 'when build has delayed option' do
before do
- allow(Ci::BuildScheduleWorker).to receive(:perform_at) { }
+ allow(Ci::BuildScheduleWorker).to receive(:perform_at) {}
end
let(:build) { create(:ci_build, :created, :schedulable, user: user, project: project) }
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 2316575f164..cabd60a22d1 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -129,6 +129,12 @@ module Ci
let!(:build2_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) }
let!(:build1_project3) { create(:ci_build, :pending, :queued, pipeline: pipeline3) }
+ it 'picks builds one-by-one' do
+ expect(Ci::Build).to receive(:find).with(pending_job.id).and_call_original
+
+ expect(execute(shared_runner)).to eq(build1_project1)
+ end
+
context 'when using fair scheduling' do
context 'when all builds are pending' do
it 'prefers projects without builds first' do
@@ -485,6 +491,48 @@ module Ci
end
context 'when "dependencies" keyword is specified' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, :artifacts, pipeline: pipeline, name: 'test', stage_idx: 0)
+ end
+
+ let!(:pending_job) do
+ create(:ci_build, :pending, :queued,
+ pipeline: pipeline, stage_idx: 1,
+ options: { script: ["bash"], dependencies: dependencies })
+ end
+
+ let(:dependencies) { %w[test] }
+
+ subject { execute(specific_runner) }
+
+ it 'picks a build with a dependency' do
+ picked_build = execute(specific_runner)
+
+ expect(picked_build).to be_present
+ end
+
+ context 'when there are multiple dependencies with artifacts' do
+ let!(:pre_stage_job_second) do
+ create(:ci_build, :success, :artifacts, pipeline: pipeline, name: 'deploy', stage_idx: 0)
+ end
+
+ let(:dependencies) { %w[test deploy] }
+
+ it 'logs build artifacts size' do
+ execute(specific_runner)
+
+ artifacts_size = [pre_stage_job, pre_stage_job_second].sum do |job|
+ job.job_artifacts_archive.size
+ end
+
+ expect(artifacts_size).to eq 107464 * 2
+ expect(Gitlab::ApplicationContext.current).to include({
+ 'meta.artifacts_dependencies_size' => artifacts_size,
+ 'meta.artifacts_dependencies_count' => 2
+ })
+ end
+ end
+
shared_examples 'not pick' do
it 'does not pick the build and drops the build' do
expect(subject).to be_nil
@@ -572,16 +620,6 @@ module Ci
end
end
- let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) }
-
- let!(:pending_job) do
- create(:ci_build, :pending, :queued,
- pipeline: pipeline, stage_idx: 1,
- options: { script: ["bash"], dependencies: ['test'] })
- end
-
- subject { execute(specific_runner) }
-
it_behaves_like 'validation is active'
end
@@ -739,16 +777,6 @@ module Ci
end
end
- context 'when a long queue is created' do
- it 'picks builds one-by-one' do
- expect(Ci::Build).to receive(:find).with(pending_job.id).and_call_original
-
- expect(execute(specific_runner)).to eq(pending_job)
- end
-
- include_examples 'handles runner assignment'
- end
-
context 'when using pending builds table' do
include_examples 'handles runner assignment'
diff --git a/spec/services/ci/retry_job_service_spec.rb b/spec/services/ci/retry_job_service_spec.rb
index f042471bd1f..b14e4187c7a 100644
--- a/spec/services/ci/retry_job_service_spec.rb
+++ b/spec/services/ci/retry_job_service_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe Ci::RetryJobService do
name: 'test')
end
+ let(:job_variables_attributes) { [{ key: 'MANUAL_VAR', value: 'manual test var' }] }
let(:user) { developer }
let(:service) { described_class.new(project, user) }
@@ -206,6 +207,14 @@ RSpec.describe Ci::RetryJobService do
include_context 'retryable bridge'
it_behaves_like 'clones the job'
+
+ context 'when given variables' do
+ let(:new_job) { service.clone!(job, variables: job_variables_attributes) }
+
+ it 'does not give variables to the new bridge' do
+ expect { new_job }.not_to raise_error
+ end
+ end
end
context 'when the job to be cloned is a build' do
@@ -250,6 +259,28 @@ RSpec.describe Ci::RetryJobService do
expect { new_job }.not_to change { Environment.count }
end
end
+
+ context 'when given variables' do
+ let(:new_job) { service.clone!(job, variables: job_variables_attributes) }
+
+ context 'when the build is actionable' do
+ let_it_be_with_refind(:job) { create(:ci_build, :actionable, pipeline: pipeline) }
+
+ it 'gives variables to the new build' do
+ expect(new_job.job_variables.count).to be(1)
+ expect(new_job.job_variables.first.key).to eq('MANUAL_VAR')
+ expect(new_job.job_variables.first.value).to eq('manual test var')
+ end
+ end
+
+ context 'when the build is not actionable' do
+ let_it_be_with_refind(:job) { create(:ci_build, pipeline: pipeline) }
+
+ it 'does not give variables to the new build' do
+ expect(new_job.job_variables.count).to be_zero
+ end
+ end
+ end
end
end
@@ -260,6 +291,14 @@ RSpec.describe Ci::RetryJobService do
include_context 'retryable bridge'
it_behaves_like 'retries the job'
+
+ context 'when given variables' do
+ let(:new_job) { service.clone!(job, variables: job_variables_attributes) }
+
+ it 'does not give variables to the new bridge' do
+ expect { new_job }.not_to raise_error
+ end
+ end
end
context 'when the job to be retried is a build' do
@@ -288,6 +327,28 @@ RSpec.describe Ci::RetryJobService do
expect { service.execute(job) }.not_to exceed_all_query_limit(control_count)
end
end
+
+ context 'when given variables' do
+ let(:new_job) { service.clone!(job, variables: job_variables_attributes) }
+
+ context 'when the build is actionable' do
+ let_it_be_with_refind(:job) { create(:ci_build, :actionable, pipeline: pipeline) }
+
+ it 'gives variables to the new build' do
+ expect(new_job.job_variables.count).to be(1)
+ expect(new_job.job_variables.first.key).to eq('MANUAL_VAR')
+ expect(new_job.job_variables.first.value).to eq('manual test var')
+ end
+ end
+
+ context 'when the build is not actionable' do
+ let_it_be_with_refind(:job) { create(:ci_build, pipeline: pipeline) }
+
+ it 'does not give variables to the new build' do
+ expect(new_job.job_variables.count).to be_zero
+ end
+ end
+ end
end
end
end
diff --git a/spec/services/ci/runners/assign_runner_service_spec.rb b/spec/services/ci/runners/assign_runner_service_spec.rb
index 00b176bb759..08bb99830fb 100644
--- a/spec/services/ci/runners/assign_runner_service_spec.rb
+++ b/spec/services/ci/runners/assign_runner_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute' do
- subject { described_class.new(runner, project, user).execute }
+ subject(:execute) { described_class.new(runner, project, user).execute }
let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) }
let_it_be(:project) { create(:project) }
@@ -11,30 +11,32 @@ RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute' do
context 'without user' do
let(:user) { nil }
- it 'does not call assign_to on runner and returns false' do
+ it 'does not call assign_to on runner and returns error response', :aggregate_failures do
expect(runner).not_to receive(:assign_to)
- is_expected.to eq(false)
+ is_expected.to be_error
+ expect(execute.message).to eq('user not allowed to assign runner')
end
end
context 'with unauthorized user' do
let(:user) { build(:user) }
- it 'does not call assign_to on runner and returns false' do
+ it 'does not call assign_to on runner and returns error message' do
expect(runner).not_to receive(:assign_to)
- is_expected.to eq(false)
+ is_expected.to be_error
+ expect(execute.message).to eq('user not allowed to assign runner')
end
end
context 'with admin user', :enable_admin_mode do
let(:user) { create_default(:user, :admin) }
- it 'calls assign_to on runner and returns value unchanged' do
- expect(runner).to receive(:assign_to).with(project, user).once.and_return('assign_to return value')
+ it 'calls assign_to on runner and returns success response' do
+ expect(runner).to receive(:assign_to).with(project, user).once.and_call_original
- is_expected.to eq('assign_to return value')
+ is_expected.to be_success
end
end
end
diff --git a/spec/services/ci/runners/bulk_delete_runners_service_spec.rb b/spec/services/ci/runners/bulk_delete_runners_service_spec.rb
new file mode 100644
index 00000000000..8e9fc4e3012
--- /dev/null
+++ b/spec/services/ci/runners/bulk_delete_runners_service_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ci::Runners::BulkDeleteRunnersService, '#execute' do
+ subject(:execute) { described_class.new(**service_args).execute }
+
+ let(:service_args) { { runners: runners_arg } }
+ let(:runners_arg) {}
+
+ context 'with runners specified' do
+ let!(:instance_runner) { create(:ci_runner) }
+ let!(:group_runner) { create(:ci_runner, :group) }
+ let!(:project_runner) { create(:ci_runner, :project) }
+
+ shared_examples 'a service deleting runners in bulk' do
+ it 'destroys runners', :aggregate_failures do
+ expect { subject }.to change { Ci::Runner.count }.by(-2)
+
+ is_expected.to be_success
+ expect(execute.payload).to eq({ deleted_count: 2, deleted_ids: [instance_runner.id, project_runner.id] })
+ expect(instance_runner[:errors]).to be_nil
+ expect(project_runner[:errors]).to be_nil
+ expect { project_runner.runner_projects.first.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { group_runner.reload }.not_to raise_error
+ expect { instance_runner.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { project_runner.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ context 'with some runners already deleted' do
+ before do
+ instance_runner.destroy!
+ end
+
+ let(:runners_arg) { [instance_runner.id, project_runner.id] }
+
+ it 'destroys runners and returns only deleted runners', :aggregate_failures do
+ expect { subject }.to change { Ci::Runner.count }.by(-1)
+
+ is_expected.to be_success
+ expect(execute.payload).to eq({ deleted_count: 1, deleted_ids: [project_runner.id] })
+ expect(instance_runner[:errors]).to be_nil
+ expect(project_runner[:errors]).to be_nil
+ expect { project_runner.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'with too many runners specified' do
+ before do
+ stub_const("#{described_class}::RUNNER_LIMIT", 1)
+ end
+
+ it 'deletes only first RUNNER_LIMIT runners' do
+ expect { subject }.to change { Ci::Runner.count }.by(-1)
+
+ is_expected.to be_success
+ expect(execute.payload).to eq({ deleted_count: 1, deleted_ids: [instance_runner.id] })
+ end
+ end
+ end
+
+ context 'with runners specified as relation' do
+ let(:runners_arg) { Ci::Runner.not_group_type }
+
+ include_examples 'a service deleting runners in bulk'
+ end
+
+ context 'with runners specified as array of IDs' do
+ let(:runners_arg) { Ci::Runner.not_group_type.ids }
+
+ include_examples 'a service deleting runners in bulk'
+ end
+
+ context 'with no arguments specified' do
+ let(:runners_arg) { nil }
+
+ it 'returns 0 deleted runners' do
+ is_expected.to be_success
+ expect(execute.payload).to eq({ deleted_count: 0, deleted_ids: [] })
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/runners/process_runner_version_update_service_spec.rb b/spec/services/ci/runners/process_runner_version_update_service_spec.rb
new file mode 100644
index 00000000000..b885138fc7a
--- /dev/null
+++ b/spec/services/ci/runners/process_runner_version_update_service_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Runners::ProcessRunnerVersionUpdateService do
+ subject(:service) { described_class.new(version) }
+
+ let(:version) { '1.0.0' }
+ let(:available_runner_releases) { %w[1.0.0 1.0.1] }
+
+ describe '#execute' do
+ subject(:execute) { service.execute }
+
+ context 'with upgrade check returning error' do
+ let(:service_double) { instance_double(Gitlab::Ci::RunnerUpgradeCheck) }
+
+ before do
+ allow(service_double).to receive(:check_runner_upgrade_suggestion).with(version)
+ .and_return([version, :error])
+ allow(service).to receive(:upgrade_check_service).and_return(service_double)
+ end
+
+ it 'does not update ci_runner_versions records', :aggregate_failures do
+ expect do
+ expect(execute).to be_error
+ expect(execute.message).to eq 'upgrade version check failed'
+ end.not_to change(Ci::RunnerVersion, :count).from(0)
+ expect(service_double).to have_received(:check_runner_upgrade_suggestion).with(version).once
+ end
+ end
+
+ context 'with successful result from upgrade check' do
+ before do
+ url = ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url
+
+ WebMock.stub_request(:get, url).to_return(
+ body: available_runner_releases.map { |v| { name: v } }.to_json,
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+
+ context 'with no existing ci_runner_version record' do
+ it 'creates ci_runner_versions record', :aggregate_failures do
+ expect do
+ expect(execute).to be_success
+ expect(execute.http_status).to eq :ok
+ expect(execute.payload).to eq({ upgrade_status: 'recommended' })
+ end.to change(Ci::RunnerVersion, :all).to contain_exactly(
+ an_object_having_attributes(version: version, status: 'recommended')
+ )
+ end
+ end
+
+ context 'with existing ci_runner_version record' do
+ let!(:runner_version) { create(:ci_runner_version, version: '1.0.0', status: :not_available) }
+
+ it 'updates ci_runner_versions record', :aggregate_failures do
+ expect do
+ expect(execute).to be_success
+ expect(execute.http_status).to eq :ok
+ expect(execute.payload).to eq({ upgrade_status: 'recommended' })
+ end.to change { runner_version.reload.status }.from('not_available').to('recommended')
+ end
+ end
+
+ context 'with up-to-date ci_runner_version record' do
+ let!(:runner_version) { create(:ci_runner_version, version: '1.0.0', status: :recommended) }
+
+ it 'does not update ci_runner_versions record', :aggregate_failures do
+ expect do
+ expect(execute).to be_success
+ expect(execute.http_status).to eq :ok
+ expect(execute.payload).to eq({ upgrade_status: 'recommended' })
+ end.not_to change { runner_version.reload.status }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb b/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb
index f8313eaab90..1690190320a 100644
--- a/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb
+++ b/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' do
+ include RunnerReleasesHelper
+
subject(:execute) { described_class.new.execute }
let_it_be(:runner_14_0_1) { create(:ci_runner, version: '14.0.1') }
@@ -11,12 +13,12 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
end
context 'with RunnerUpgradeCheck recommending 14.0.2' do
+ let(:upgrade_check) { instance_double(::Gitlab::Ci::RunnerUpgradeCheck) }
+
before do
stub_const('Ci::Runners::ReconcileExistingRunnerVersionsService::VERSION_BATCH_SIZE', 1)
- allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
- .to receive(:check_runner_upgrade_status)
- .and_return({ recommended: ::Gitlab::VersionInfo.new(14, 0, 2) })
+ allow(::Gitlab::Ci::RunnerUpgradeCheck).to receive(:new).and_return(upgrade_check).once
end
context 'with runner with new version' do
@@ -25,10 +27,11 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
let!(:runner_14_0_0) { create(:ci_runner, version: '14.0.0') }
before do
- allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
- .to receive(:check_runner_upgrade_status)
+ allow(upgrade_check).to receive(:check_runner_upgrade_suggestion)
+ .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :recommended])
+ allow(upgrade_check).to receive(:check_runner_upgrade_suggestion)
.with('14.0.2')
- .and_return({ not_available: ::Gitlab::VersionInfo.new(14, 0, 2) })
+ .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :not_available])
.once
end
@@ -39,14 +42,13 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
.once
.and_call_original
- result = nil
- expect { result = execute }
+ expect { execute }
.to change { runner_version_14_0_0.reload.status }.from('not_available').to('recommended')
.and change { runner_version_14_0_1.reload.status }.from('not_available').to('recommended')
.and change { ::Ci::RunnerVersion.find_by(version: '14.0.2')&.status }.from(nil).to('not_available')
- expect(result).to eq({
- status: :success,
+ expect(execute).to be_success
+ expect(execute.payload).to eq({
total_inserted: 1, # 14.0.2 is inserted
total_updated: 3, # 14.0.0, 14.0.1 are updated, and newly inserted 14.0.2's status is calculated
total_deleted: 0
@@ -58,19 +60,17 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
let!(:runner_version_14_0_2) { create(:ci_runner_version, version: '14.0.2', status: :not_available) }
before do
- allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
- .to receive(:check_runner_upgrade_status)
- .and_return({ not_available: ::Gitlab::VersionInfo.new(14, 0, 2) })
+ allow(upgrade_check).to receive(:check_runner_upgrade_suggestion)
+ .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :not_available])
end
it 'deletes orphan ci_runner_versions entry', :aggregate_failures do
- result = nil
- expect { result = execute }
+ expect { execute }
.to change { ::Ci::RunnerVersion.find_by_version('14.0.2')&.status }.from('not_available').to(nil)
.and not_change { runner_version_14_0_1.reload.status }.from('not_available')
- expect(result).to eq({
- status: :success,
+ expect(execute).to be_success
+ expect(execute.payload).to eq({
total_inserted: 0,
total_updated: 0,
total_deleted: 1 # 14.0.2 is deleted
@@ -80,17 +80,15 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
context 'with no runner version changes' do
before do
- allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
- .to receive(:check_runner_upgrade_status)
- .and_return({ not_available: ::Gitlab::VersionInfo.new(14, 0, 1) })
+ allow(upgrade_check).to receive(:check_runner_upgrade_suggestion)
+ .and_return([::Gitlab::VersionInfo.new(14, 0, 1), :not_available])
end
it 'does not modify ci_runner_versions entries', :aggregate_failures do
- result = nil
- expect { result = execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available')
+ expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available')
- expect(result).to eq({
- status: :success,
+ expect(execute).to be_success
+ expect(execute.payload).to eq({
total_inserted: 0,
total_updated: 0,
total_deleted: 0
@@ -100,17 +98,15 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
context 'with failing version check' do
before do
- allow(::Gitlab::Ci::RunnerUpgradeCheck.instance)
- .to receive(:check_runner_upgrade_status)
- .and_return({ error: ::Gitlab::VersionInfo.new(14, 0, 1) })
+ allow(upgrade_check).to receive(:check_runner_upgrade_suggestion)
+ .and_return([::Gitlab::VersionInfo.new(14, 0, 1), :error])
end
it 'makes no changes to ci_runner_versions', :aggregate_failures do
- result = nil
- expect { result = execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available')
+ expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available')
- expect(result).to eq({
- status: :success,
+ expect(execute).to be_success
+ expect(execute.payload).to eq({
total_inserted: 0,
total_updated: 0,
total_deleted: 0
@@ -120,26 +116,15 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
end
context 'integration testing with Gitlab::Ci::RunnerUpgradeCheck' do
- let(:available_runner_releases) do
- %w[14.0.0 14.0.1]
- end
-
before do
- url = ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url
-
- WebMock.stub_request(:get, url).to_return(
- body: available_runner_releases.map { |v| { name: v } }.to_json,
- status: 200,
- headers: { 'Content-Type' => 'application/json' }
- )
+ stub_runner_releases(%w[14.0.0 14.0.1])
end
it 'does not modify ci_runner_versions entries', :aggregate_failures do
- result = nil
- expect { result = execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available')
+ expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available')
- expect(result).to eq({
- status: :success,
+ expect(execute).to be_success
+ expect(execute.payload).to eq({
total_inserted: 0,
total_updated: 0,
total_deleted: 0
diff --git a/spec/services/ci/runners/register_runner_service_spec.rb b/spec/services/ci/runners/register_runner_service_spec.rb
index 03dcf851e53..6d7b39de21e 100644
--- a/spec/services/ci/runners/register_runner_service_spec.rb
+++ b/spec/services/ci/runners/register_runner_service_spec.rb
@@ -4,8 +4,9 @@ require 'spec_helper'
RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do
let(:registration_token) { 'abcdefg123456' }
- let(:token) { }
+ let(:token) {}
let(:args) { {} }
+ let(:runner) { execute.payload[:runner] }
before do
stub_feature_flags(runner_registration_control: false)
@@ -13,21 +14,25 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do
stub_application_setting(valid_runner_registrars: ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES)
end
- subject(:runner) { described_class.new.execute(token, args) }
+ subject(:execute) { described_class.new.execute(token, args) }
context 'when no token is provided' do
let(:token) { '' }
- it 'returns nil' do
- is_expected.to be_nil
+ it 'returns error response' do
+ expect(execute).to be_error
+ expect(execute.message).to eq 'invalid token supplied'
+ expect(execute.http_status).to eq :forbidden
end
end
context 'when invalid token is provided' do
let(:token) { 'invalid' }
- it 'returns nil' do
- is_expected.to be_nil
+ it 'returns error response' do
+ expect(execute).to be_error
+ expect(execute.message).to eq 'invalid token supplied'
+ expect(execute.http_status).to eq :forbidden
end
end
@@ -36,12 +41,14 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do
let(:token) { registration_token }
it 'creates runner with default values' do
- is_expected.to be_an_instance_of(::Ci::Runner)
- expect(subject.persisted?).to be_truthy
- expect(subject.run_untagged).to be true
- expect(subject.active).to be true
- expect(subject.token).not_to eq(registration_token)
- expect(subject).to be_instance_type
+ expect(execute).to be_success
+
+ expect(runner).to be_an_instance_of(::Ci::Runner)
+ expect(runner.persisted?).to be_truthy
+ expect(runner.run_untagged).to be true
+ expect(runner.active).to be true
+ expect(runner.token).not_to eq(registration_token)
+ expect(runner).to be_instance_type
end
context 'with non-default arguments' do
@@ -67,25 +74,27 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do
end
it 'creates runner with specified values', :aggregate_failures do
- is_expected.to be_an_instance_of(::Ci::Runner)
- expect(subject.active).to eq args[:active]
- expect(subject.locked).to eq args[:locked]
- expect(subject.run_untagged).to eq args[:run_untagged]
- expect(subject.tags).to contain_exactly(
+ expect(execute).to be_success
+
+ expect(runner).to be_an_instance_of(::Ci::Runner)
+ expect(runner.active).to eq args[:active]
+ expect(runner.locked).to eq args[:locked]
+ expect(runner.run_untagged).to eq args[:run_untagged]
+ expect(runner.tags).to contain_exactly(
an_object_having_attributes(name: 'tag1'),
an_object_having_attributes(name: 'tag2')
)
- expect(subject.access_level).to eq args[:access_level]
- expect(subject.maximum_timeout).to eq args[:maximum_timeout]
- expect(subject.name).to eq args[:name]
- expect(subject.version).to eq args[:version]
- expect(subject.revision).to eq args[:revision]
- expect(subject.platform).to eq args[:platform]
- expect(subject.architecture).to eq args[:architecture]
- expect(subject.ip_address).to eq args[:ip_address]
-
- expect(Ci::Runner.tagged_with('tag1')).to include(subject)
- expect(Ci::Runner.tagged_with('tag2')).to include(subject)
+ expect(runner.access_level).to eq args[:access_level]
+ expect(runner.maximum_timeout).to eq args[:maximum_timeout]
+ expect(runner.name).to eq args[:name]
+ expect(runner.version).to eq args[:version]
+ expect(runner.revision).to eq args[:revision]
+ expect(runner.platform).to eq args[:platform]
+ expect(runner.architecture).to eq args[:architecture]
+ expect(runner.ip_address).to eq args[:ip_address]
+
+ expect(Ci::Runner.tagged_with('tag1')).to include(runner)
+ expect(Ci::Runner.tagged_with('tag2')).to include(runner)
end
end
@@ -95,8 +104,10 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do
end
it 'creates runner with token expiration' do
- is_expected.to be_an_instance_of(::Ci::Runner)
- expect(subject.token_expires_at).to eq(5.days.from_now)
+ expect(execute).to be_success
+
+ expect(runner).to be_an_instance_of(::Ci::Runner)
+ expect(runner.token_expires_at).to eq(5.days.from_now)
end
end
end
@@ -106,12 +117,14 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do
let(:token) { project.runners_token }
it 'creates project runner' do
- is_expected.to be_an_instance_of(::Ci::Runner)
+ expect(execute).to be_success
+
+ expect(runner).to be_an_instance_of(::Ci::Runner)
expect(project.runners.size).to eq(1)
- is_expected.to eq(project.runners.first)
- expect(subject.token).not_to eq(registration_token)
- expect(subject.token).not_to eq(project.runners_token)
- expect(subject).to be_project_type
+ expect(runner).to eq(project.runners.first)
+ expect(runner.token).not_to eq(registration_token)
+ expect(runner.token).not_to eq(project.runners_token)
+ expect(runner).to be_project_type
end
context 'when it exceeds the application limits' do
@@ -121,9 +134,13 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do
end
it 'does not create runner' do
- is_expected.to be_an_instance_of(::Ci::Runner)
- expect(subject.persisted?).to be_falsey
- expect(subject.errors.messages).to eq('runner_projects.base': ['Maximum number of ci registered project runners (1) exceeded'])
+ expect(execute).to be_success
+
+ expect(runner).to be_an_instance_of(::Ci::Runner)
+ expect(runner.persisted?).to be_falsey
+ expect(runner.errors.messages).to eq(
+ 'runner_projects.base': ['Maximum number of ci registered project runners (1) exceeded']
+ )
expect(project.runners.reload.size).to eq(1)
end
end
@@ -135,8 +152,10 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do
end
it 'creates runner' do
- is_expected.to be_an_instance_of(::Ci::Runner)
- expect(subject.errors).to be_empty
+ expect(execute).to be_success
+
+ expect(runner).to be_an_instance_of(::Ci::Runner)
+ expect(runner.errors).to be_empty
expect(project.runners.reload.size).to eq(2)
expect(project.runners.recent.size).to eq(1)
end
@@ -153,15 +172,18 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do
end
it 'returns 403 error' do
- is_expected.to be_nil
+ expect(execute).to be_error
+ expect(execute.http_status).to eq :forbidden
end
end
context 'when feature flag is disabled' do
it 'registers the runner' do
- is_expected.to be_an_instance_of(::Ci::Runner)
- expect(subject.errors).to be_empty
- expect(subject.active).to be true
+ expect(execute).to be_success
+
+ expect(runner).to be_an_instance_of(::Ci::Runner)
+ expect(runner.errors).to be_empty
+ expect(runner.active).to be true
end
end
end
@@ -172,12 +194,14 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do
let(:token) { group.runners_token }
it 'creates a group runner' do
- is_expected.to be_an_instance_of(::Ci::Runner)
- expect(subject.errors).to be_empty
+ expect(execute).to be_success
+
+ expect(runner).to be_an_instance_of(::Ci::Runner)
+ expect(runner.errors).to be_empty
expect(group.runners.reload.size).to eq(1)
- expect(subject.token).not_to eq(registration_token)
- expect(subject.token).not_to eq(group.runners_token)
- expect(subject).to be_group_type
+ expect(runner.token).not_to eq(registration_token)
+ expect(runner.token).not_to eq(group.runners_token)
+ expect(runner).to be_group_type
end
context 'when it exceeds the application limits' do
@@ -187,9 +211,13 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do
end
it 'does not create runner' do
- is_expected.to be_an_instance_of(::Ci::Runner)
- expect(subject.persisted?).to be_falsey
- expect(subject.errors.messages).to eq('runner_namespaces.base': ['Maximum number of ci registered group runners (1) exceeded'])
+ expect(execute).to be_success
+
+ expect(runner).to be_an_instance_of(::Ci::Runner)
+ expect(runner.persisted?).to be_falsey
+ expect(runner.errors.messages).to eq(
+ 'runner_namespaces.base': ['Maximum number of ci registered group runners (1) exceeded']
+ )
expect(group.runners.reload.size).to eq(1)
end
end
@@ -202,8 +230,10 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do
end
it 'creates runner' do
- is_expected.to be_an_instance_of(::Ci::Runner)
- expect(subject.errors).to be_empty
+ expect(execute).to be_success
+
+ expect(runner).to be_an_instance_of(::Ci::Runner)
+ expect(runner.errors).to be_empty
expect(group.runners.reload.size).to eq(3)
expect(group.runners.recent.size).to eq(1)
end
@@ -219,16 +249,18 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do
stub_feature_flags(runner_registration_control: true)
end
- it 'returns nil' do
- is_expected.to be_nil
+ it 'returns error response' do
+ is_expected.to be_error
end
end
context 'when feature flag is disabled' do
it 'registers the runner' do
- is_expected.to be_an_instance_of(::Ci::Runner)
- expect(subject.errors).to be_empty
- expect(subject.active).to be true
+ expect(execute).to be_success
+
+ expect(runner).to be_an_instance_of(::Ci::Runner)
+ expect(runner.errors).to be_empty
+ expect(runner.active).to be true
end
end
end
diff --git a/spec/services/ci/runners/reset_registration_token_service_spec.rb b/spec/services/ci/runners/reset_registration_token_service_spec.rb
index c4bfff51cc8..79059712032 100644
--- a/spec/services/ci/runners/reset_registration_token_service_spec.rb
+++ b/spec/services/ci/runners/reset_registration_token_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe ::Ci::Runners::ResetRegistrationTokenService, '#execute' do
- subject { described_class.new(scope, current_user).execute }
+ subject(:execute) { described_class.new(scope, current_user).execute }
let_it_be(:user) { build(:user) }
let_it_be(:admin_user) { create(:user, :admin) }
@@ -12,20 +12,20 @@ RSpec.describe ::Ci::Runners::ResetRegistrationTokenService, '#execute' do
context 'without user' do
let(:current_user) { nil }
- it 'does not reset registration token and returns nil' do
+ it 'does not reset registration token and returns error response' do
expect(scope).not_to receive(token_reset_method_name)
- is_expected.to be_nil
+ expect(execute).to be_error
end
end
context 'with unauthorized user' do
let(:current_user) { user }
- it 'does not reset registration token and returns nil' do
+ it 'does not reset registration token and returns error response' do
expect(scope).not_to receive(token_reset_method_name)
- is_expected.to be_nil
+ expect(execute).to be_error
end
end
@@ -37,7 +37,8 @@ RSpec.describe ::Ci::Runners::ResetRegistrationTokenService, '#execute' do
expect(scope).to receive(token_method_name).once.and_return("#{token_method_name} return value")
end
- is_expected.to eq("#{token_method_name} return value")
+ expect(execute).to be_success
+ expect(execute.payload[:new_registration_token]).to eq("#{token_method_name} return value")
end
end
end
diff --git a/spec/services/ci/runners/unassign_runner_service_spec.rb b/spec/services/ci/runners/unassign_runner_service_spec.rb
index 3fb6925f4bd..cf710cf6893 100644
--- a/spec/services/ci/runners/unassign_runner_service_spec.rb
+++ b/spec/services/ci/runners/unassign_runner_service_spec.rb
@@ -3,21 +3,21 @@
require 'spec_helper'
RSpec.describe ::Ci::Runners::UnassignRunnerService, '#execute' do
- subject(:service) { described_class.new(runner_project, user).execute }
-
- let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) }
let_it_be(:project) { create(:project) }
+ let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:runner_project) { runner.runner_projects.last }
+ subject(:execute) { described_class.new(runner_project, user).execute }
+
context 'without user' do
let(:user) { nil }
it 'does not destroy runner_project', :aggregate_failures do
expect(runner_project).not_to receive(:destroy)
- expect { service }.not_to change { runner.runner_projects.count }.from(1)
+ expect { execute }.not_to change { runner.runner_projects.count }.from(1)
- is_expected.to eq(false)
+ is_expected.to be_error
end
end
@@ -27,17 +27,27 @@ RSpec.describe ::Ci::Runners::UnassignRunnerService, '#execute' do
it 'does not call destroy on runner_project' do
expect(runner).not_to receive(:destroy)
- service
+ is_expected.to be_error
end
end
context 'with admin user', :enable_admin_mode do
let(:user) { create_default(:user, :admin) }
- it 'destroys runner_project' do
- expect(runner_project).to receive(:destroy).once
+ context 'with destroy returning false' do
+ it 'returns error response' do
+ expect(runner_project).to receive(:destroy).once.and_return(false)
+
+ is_expected.to be_error
+ end
+ end
+
+ context 'with destroy returning true' do
+ it 'returns success response' do
+ expect(runner_project).to receive(:destroy).once.and_return(true)
- service
+ is_expected.to be_success
+ end
end
end
end
diff --git a/spec/services/ci/runners/unregister_runner_service_spec.rb b/spec/services/ci/runners/unregister_runner_service_spec.rb
index df1a0a90067..77fc299e4e1 100644
--- a/spec/services/ci/runners/unregister_runner_service_spec.rb
+++ b/spec/services/ci/runners/unregister_runner_service_spec.rb
@@ -3,13 +3,16 @@
require 'spec_helper'
RSpec.describe ::Ci::Runners::UnregisterRunnerService, '#execute' do
- subject { described_class.new(runner, 'some_token').execute }
+ subject(:execute) { described_class.new(runner, 'some_token').execute }
let(:runner) { create(:ci_runner) }
it 'destroys runner' do
expect(runner).to receive(:destroy).once.and_call_original
- expect { subject }.to change { Ci::Runner.count }.by(-1)
+
+ expect do
+ expect(execute).to be_success
+ end.to change { Ci::Runner.count }.by(-1)
expect(runner[:errors]).to be_nil
end
end
diff --git a/spec/services/ci/runners/update_runner_service_spec.rb b/spec/services/ci/runners/update_runner_service_spec.rb
index b02ea8f58b0..e008fde9982 100644
--- a/spec/services/ci/runners/update_runner_service_spec.rb
+++ b/spec/services/ci/runners/update_runner_service_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe Ci::Runners::UpdateRunnerService do
end
context 'with cost factor params' do
- let(:params) { { public_projects_minutes_cost_factor: 1.1, private_projects_minutes_cost_factor: 2.2 }}
+ let(:params) { { public_projects_minutes_cost_factor: 1.1, private_projects_minutes_cost_factor: 2.2 } }
it 'updates the runner cost factors' do
expect(update).to be_truthy
diff --git a/spec/services/ci/stuck_builds/drop_pending_service_spec.rb b/spec/services/ci/stuck_builds/drop_pending_service_spec.rb
index ebc57af77a0..a452a65829a 100644
--- a/spec/services/ci/stuck_builds/drop_pending_service_spec.rb
+++ b/spec/services/ci/stuck_builds/drop_pending_service_spec.rb
@@ -9,8 +9,8 @@ RSpec.describe Ci::StuckBuilds::DropPendingService do
create(:ci_build, pipeline: pipeline, runner: runner)
end
- let(:created_at) { }
- let(:updated_at) { }
+ let(:created_at) {}
+ let(:updated_at) {}
subject(:service) { described_class.new }
diff --git a/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb b/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb
index 1416fab3d25..a4f9f97fffc 100644
--- a/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb
+++ b/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe Ci::StuckBuilds::DropScheduledService do
end
context 'when there are no stale scheduled builds' do
- let(:job) { }
+ let(:job) {}
it 'does not drop the stale scheduled build yet' do
expect { service.execute }.not_to raise_error
diff --git a/spec/services/ci/track_failed_build_service_spec.rb b/spec/services/ci/track_failed_build_service_spec.rb
new file mode 100644
index 00000000000..d83e56f0669
--- /dev/null
+++ b/spec/services/ci/track_failed_build_service_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::TrackFailedBuildService do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+
+ let_it_be(:exit_code) { 42 }
+ let_it_be(:failure_reason) { "script_failure" }
+
+ describe '#execute' do
+ context 'when a build has failed' do
+ let_it_be(:build) { create(:ci_build, :failed, :sast_report, pipeline: pipeline, user: user) }
+
+ subject { described_class.new(build: build, exit_code: exit_code, failure_reason: failure_reason) }
+
+ it 'tracks the build failed event', :snowplow do
+ response = subject.execute
+
+ expect(response.success?).to be true
+
+ expect_snowplow_event(
+ category: 'ci::build',
+ action: 'failed',
+ context: [{
+ schema: described_class::SCHEMA_URL,
+ data: {
+ build_id: build.id,
+ build_name: build.name,
+ build_artifact_types: ["sast"],
+ exit_code: exit_code,
+ failure_reason: failure_reason
+ }
+ }],
+ user: user,
+ project: project.id)
+ end
+ end
+
+ context 'when a build has not failed' do
+ let_it_be(:build) { create(:ci_build, :success, :sast_report, pipeline: pipeline, user: user) }
+
+ subject { described_class.new(build: build, exit_code: nil, failure_reason: nil) }
+
+ it 'does not track the build failed event', :snowplow do
+ response = subject.execute
+
+ expect(response.error?).to be true
+
+ expect_no_snowplow_event
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/update_build_state_service_spec.rb b/spec/services/ci/update_build_state_service_spec.rb
index 937b19beff5..90a86e7ae59 100644
--- a/spec/services/ci/update_build_state_service_spec.rb
+++ b/spec/services/ci/update_build_state_service_spec.rb
@@ -33,6 +33,24 @@ RSpec.describe Ci::UpdateBuildStateService do
end
end
+ context 'when build has failed' do
+ let(:params) do
+ {
+ output: { checksum: 'crc32:12345678', bytesize: 123 },
+ state: 'failed',
+ failure_reason: 'script_failure',
+ exit_code: 7
+ }
+ end
+
+ it 'sends a build failed event to Snowplow' do
+ expect(::Ci::TrackFailedBuildWorker)
+ .to receive(:perform_async).with(build.id, params[:exit_code], params[:failure_reason])
+
+ subject.execute
+ end
+ end
+
context 'when build does not have checksum' do
context 'when state has changed' do
let(:params) { { state: 'success' } }
diff --git a/spec/services/clusters/integrations/create_service_spec.rb b/spec/services/clusters/integrations/create_service_spec.rb
index 016511a3c01..9104e07504d 100644
--- a/spec/services/clusters/integrations/create_service_spec.rb
+++ b/spec/services/clusters/integrations/create_service_spec.rb
@@ -68,7 +68,7 @@ RSpec.describe Clusters::Integrations::CreateService, '#execute' do
end
it 'errors' do
- expect { service.execute}.to raise_error(ArgumentError)
+ expect { service.execute }.to raise_error(ArgumentError)
end
end
diff --git a/spec/services/clusters/integrations/prometheus_health_check_service_spec.rb b/spec/services/clusters/integrations/prometheus_health_check_service_spec.rb
index 7147f1b9b28..526462931a6 100644
--- a/spec/services/clusters/integrations/prometheus_health_check_service_spec.rb
+++ b/spec/services/clusters/integrations/prometheus_health_check_service_spec.rb
@@ -51,6 +51,7 @@ RSpec.describe Clusters::Integrations::PrometheusHealthCheckService, '#execute'
let(:prometheus_enabled) { false }
it { expect(subject).to eq(nil) }
+
include_examples 'no alert'
end
diff --git a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
index a4f018aec0c..064f9e42e96 100644
--- a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
+++ b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
@@ -136,7 +136,7 @@ RSpec.describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do
context 'With RBAC enabled cluster' do
let(:rbac) { true }
- let(:role_binding_name) { "gitlab-#{namespace}"}
+ let(:role_binding_name) { "gitlab-#{namespace}" }
before do
cluster.platform_kubernetes.rbac!
diff --git a/spec/services/database/consistency_check_service_spec.rb b/spec/services/database/consistency_check_service_spec.rb
index 2e642451432..6695e4b5e9f 100644
--- a/spec/services/database/consistency_check_service_spec.rb
+++ b/spec/services/database/consistency_check_service_spec.rb
@@ -24,9 +24,27 @@ RSpec.describe Database::ConsistencyCheckService do
)
end
- describe '#random_start_id' do
- let(:batch_size) { 5 }
+ describe '#min_id' do
+ before do
+ create_list(:namespace, 3)
+ end
+ it 'returns the id of the first record in the database' do
+ expect(subject.send(:min_id)).to eq(Namespace.first.id)
+ end
+ end
+
+ describe '#max_id' do
+ before do
+ create_list(:namespace, 3)
+ end
+
+ it 'returns the id of the first record in the database' do
+ expect(subject.send(:max_id)).to eq(Namespace.last.id)
+ end
+ end
+
+ describe '#random_start_id' do
before do
create_list(:namespace, 50) # This will also create Ci::NameSpaceMirror objects
end
@@ -58,12 +76,11 @@ RSpec.describe Database::ConsistencyCheckService do
end
context 'no cursor has been saved before' do
- let(:selected_start_id) { Namespace.order(:id).limit(5).pluck(:id).last }
- let(:expected_next_start_id) { selected_start_id + batch_size * max_batches }
+ let(:min_id) { Namespace.first.id }
+ let(:max_id) { Namespace.last.id }
before do
create_list(:namespace, 50) # This will also create Ci::NameSpaceMirror objects
- expect(consistency_check_service).to receive(:random_start_id).and_return(selected_start_id)
end
it 'picks a random start_id' do
@@ -72,17 +89,21 @@ RSpec.describe Database::ConsistencyCheckService do
matches: 10,
mismatches: 0,
mismatches_details: [],
- start_id: selected_start_id,
- next_start_id: expected_next_start_id
+ start_id: be_between(min_id, max_id),
+ next_start_id: be_between(min_id, max_id)
}
- expect(consistency_check_service.execute).to eq(expected_result)
+ expect(consistency_check_service).to receive(:rand).with(min_id..max_id).and_call_original
+ result = consistency_check_service.execute
+ expect(result).to match(expected_result)
end
it 'calls the ConsistencyCheckService with the expected parameters' do
+ expect(consistency_check_service).to receive(:random_start_id).and_return(min_id)
+
allow_next_instance_of(Gitlab::Database::ConsistencyChecker) do |instance|
- expect(instance).to receive(:execute).with(start_id: selected_start_id).and_return({
+ expect(instance).to receive(:execute).with(start_id: min_id).and_return({
batches: 2,
- next_start_id: expected_next_start_id,
+ next_start_id: min_id + batch_size,
matches: 10,
mismatches: 0,
mismatches_details: []
@@ -98,17 +119,19 @@ RSpec.describe Database::ConsistencyCheckService do
expected_result = {
batches: 2,
- start_id: selected_start_id,
- next_start_id: expected_next_start_id,
matches: 10,
mismatches: 0,
- mismatches_details: []
+ mismatches_details: [],
+ start_id: be_between(min_id, max_id),
+ next_start_id: be_between(min_id, max_id)
}
- expect(consistency_check_service.execute).to eq(expected_result)
+ result = consistency_check_service.execute
+ expect(result).to match(expected_result)
end
it 'saves the next_start_id in Redis for he next iteration' do
- expect(consistency_check_service).to receive(:save_next_start_id).with(expected_next_start_id).and_call_original
+ expect(consistency_check_service).to receive(:save_next_start_id)
+ .with(be_between(min_id, max_id)).and_call_original
consistency_check_service.execute
end
end
diff --git a/spec/services/deployments/create_for_build_service_spec.rb b/spec/services/deployments/create_for_build_service_spec.rb
index 38d94580512..a2e1acadcc1 100644
--- a/spec/services/deployments/create_for_build_service_spec.rb
+++ b/spec/services/deployments/create_for_build_service_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe Deployments::CreateForBuildService do
end
context 'when the corresponding environment does not exist' do
- let!(:environment) { }
+ let!(:environment) {}
it 'does not create a deployment record' do
expect { subject }.not_to change { Deployment.count }
diff --git a/spec/services/deployments/update_environment_service_spec.rb b/spec/services/deployments/update_environment_service_spec.rb
index 8ab53a37a33..4485ce585bb 100644
--- a/spec/services/deployments/update_environment_service_spec.rb
+++ b/spec/services/deployments/update_environment_service_spec.rb
@@ -112,7 +112,7 @@ RSpec.describe Deployments::UpdateEnvironmentService do
end
context 'when external URL is invalid' do
- let(:external_url) { 'google.com' }
+ let(:external_url) { 'javascript:alert("hello")' }
it 'fails to update the tier due to validation error' do
expect { subject.execute }.not_to change { environment.tier }
@@ -123,7 +123,7 @@ RSpec.describe Deployments::UpdateEnvironmentService do
.with(an_instance_of(described_class::EnvironmentUpdateFailure),
project_id: project.id,
environment_id: environment.id,
- reason: %q{External url is blocked: Only allowed schemes are http, https})
+ reason: %q{External url javascript scheme is not allowed})
.once
subject.execute
@@ -307,14 +307,6 @@ RSpec.describe Deployments::UpdateEnvironmentService do
end
it { is_expected.to eq('http://appname-master.example.com') }
-
- context 'when the FF ci_expand_environment_name_and_url is disabled' do
- before do
- stub_feature_flags(ci_expand_environment_name_and_url: false)
- end
-
- it { is_expected.to eq('http://${STACK_NAME}.example.com') }
- end
end
context 'when yaml environment does not have url' do
diff --git a/spec/services/design_management/delete_designs_service_spec.rb b/spec/services/design_management/delete_designs_service_spec.rb
index bc7625d7c28..a0e049ea42a 100644
--- a/spec/services/design_management/delete_designs_service_spec.rb
+++ b/spec/services/design_management/delete_designs_service_spec.rb
@@ -59,7 +59,11 @@ RSpec.describe DesignManagement::DeleteDesignsService do
it_behaves_like "a service error"
it 'does not create any events in the activity stream' do
- expect { run_service rescue nil }.not_to change { Event.count }
+ expect do
+ run_service
+ rescue StandardError
+ nil
+ end.not_to change { Event.count }
end
end
@@ -78,7 +82,11 @@ RSpec.describe DesignManagement::DeleteDesignsService do
it 'does not log any events' do
counter = ::Gitlab::UsageDataCounters::DesignsCounter
- expect { run_service rescue nil }
+ expect do
+ run_service
+ rescue StandardError
+ nil
+ end
.not_to change { [counter.totals, Event.count] }
end
@@ -86,10 +94,18 @@ RSpec.describe DesignManagement::DeleteDesignsService do
redis_hll = ::Gitlab::UsageDataCounters::HLLRedisCounter
event = Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_DESIGNS_REMOVED
- expect { run_service rescue nil }
+ expect do
+ run_service
+ rescue StandardError
+ nil
+ end
.not_to change { redis_hll.unique_events(event_names: event, start_date: 1.day.ago, end_date: 1.day.from_now) }
- run_service rescue nil
+ begin
+ run_service
+ rescue StandardError
+ nil
+ end
end
end
diff --git a/spec/services/design_management/generate_image_versions_service_spec.rb b/spec/services/design_management/generate_image_versions_service_spec.rb
index e06b6fbf116..5409ec12016 100644
--- a/spec/services/design_management/generate_image_versions_service_spec.rb
+++ b/spec/services/design_management/generate_image_versions_service_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe DesignManagement::GenerateImageVersionsService do
end
it 'skips generating image versions if the mime type is not whitelisted' do
- stub_const('DesignManagement::DesignV432x230Uploader::MIME_TYPE_WHITELIST', [])
+ stub_const('DesignManagement::DesignV432x230Uploader::MIME_TYPE_ALLOWLIST', [])
described_class.new(version).execute
diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb
index 8d41b20c8a9..6280f1263c3 100644
--- a/spec/services/git/branch_push_service_spec.rb
+++ b/spec/services/git/branch_push_service_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Git::BranchPushService, services: true do
include RepoHelpers
let_it_be(:user) { create(:user) }
- let_it_be(:project, reload: true) { create(:project, :repository) }
+ let_it_be_with_refind(:project) { create(:project, :repository) }
let(:blankrev) { Gitlab::Git::BLANK_SHA }
let(:oldrev) { sample_commit.parent_id }
@@ -573,7 +573,7 @@ RSpec.describe Git::BranchPushService, services: true do
before do
allow(project).to receive(:default_branch).and_return('feature')
- expect(project).to receive(:change_head) { 'feature'}
+ expect(project).to receive(:change_head) { 'feature' }
end
it 'push to first branch updates HEAD' do
diff --git a/spec/services/google_cloud/create_cloudsql_instance_service_spec.rb b/spec/services/google_cloud/create_cloudsql_instance_service_spec.rb
new file mode 100644
index 00000000000..cd0dd75e576
--- /dev/null
+++ b/spec/services/google_cloud/create_cloudsql_instance_service_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GoogleCloud::CreateCloudsqlInstanceService do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:gcp_project_id) { 'gcp_project_120' }
+ let(:environment_name) { 'test_env_42' }
+ let(:database_version) { 'POSTGRES_8000' }
+ let(:tier) { 'REIT_TIER' }
+ let(:service) do
+ described_class.new(project, user, {
+ gcp_project_id: gcp_project_id,
+ environment_name: environment_name,
+ database_version: database_version,
+ tier: tier
+ })
+ end
+
+ describe '#execute' do
+ before do
+ allow_next_instance_of(::Ci::VariablesFinder) do |variable_finder|
+ allow(variable_finder).to receive(:execute).and_return([])
+ end
+ end
+
+ it 'triggers creation of a cloudsql instance' do
+ expect_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
+ expected_instance_name = "gitlab-#{project.id}-postgres-8000-test-env-42"
+ expect(client).to receive(:create_cloudsql_instance)
+ .with(gcp_project_id,
+ expected_instance_name,
+ String,
+ database_version,
+ 'us-east1',
+ tier)
+ end
+
+ result = service.execute
+ expect(result[:status]).to be(:success)
+ end
+
+ it 'triggers worker to manage cloudsql instance creation operation results' do
+ expect_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
+ expect(client).to receive(:create_cloudsql_instance)
+ end
+
+ expect(GoogleCloud::CreateCloudsqlInstanceWorker).to receive(:perform_in)
+
+ result = service.execute
+ expect(result[:status]).to be(:success)
+ end
+
+ context 'when google APIs fail' do
+ it 'returns error' do
+ expect_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
+ expect(client).to receive(:create_cloudsql_instance).and_raise(Google::Apis::Error.new('mock-error'))
+ end
+
+ result = service.execute
+ expect(result[:status]).to eq(:error)
+ end
+ end
+
+ context 'when project has GCP_REGION defined' do
+ let(:gcp_region) { instance_double(::Ci::Variable, key: 'GCP_REGION', value: 'user-defined-region') }
+
+ before do
+ allow_next_instance_of(::Ci::VariablesFinder) do |variable_finder|
+ allow(variable_finder).to receive(:execute).and_return([gcp_region])
+ end
+ end
+
+ it 'uses defined region' do
+ expect_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
+ expect(client).to receive(:create_cloudsql_instance)
+ .with(gcp_project_id,
+ String,
+ String,
+ database_version,
+ 'user-defined-region',
+ tier)
+ end
+
+ service.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/google_cloud/enable_cloudsql_service_spec.rb b/spec/services/google_cloud/enable_cloudsql_service_spec.rb
new file mode 100644
index 00000000000..e54e5a8d446
--- /dev/null
+++ b/spec/services/google_cloud/enable_cloudsql_service_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GoogleCloud::EnableCloudsqlService do
+ let_it_be(:project) { create(:project) }
+
+ subject(:result) { described_class.new(project).execute }
+
+ context 'when a project does not have any GCP_PROJECT_IDs configured' do
+ it 'returns error' do
+ message = 'No GCP projects found. Configure a service account or GCP_PROJECT_ID CI variable.'
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq(message)
+ end
+ end
+
+ context 'when a project has GCP_PROJECT_IDs configured' do
+ before do
+ project.variables.build(environment_scope: 'production', key: 'GCP_PROJECT_ID', value: 'prj-prod')
+ project.variables.build(environment_scope: 'staging', key: 'GCP_PROJECT_ID', value: 'prj-staging')
+ project.save!
+ end
+
+ it 'enables cloudsql, compute and service networking Google APIs', :aggregate_failures do
+ expect_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance|
+ expect(instance).to receive(:enable_cloud_sql_admin).with('prj-prod')
+ expect(instance).to receive(:enable_compute).with('prj-prod')
+ expect(instance).to receive(:enable_service_networking).with('prj-prod')
+ expect(instance).to receive(:enable_cloud_sql_admin).with('prj-staging')
+ expect(instance).to receive(:enable_compute).with('prj-staging')
+ expect(instance).to receive(:enable_service_networking).with('prj-staging')
+ end
+
+ expect(result[:status]).to eq(:success)
+ end
+ end
+end
diff --git a/spec/services/google_cloud/get_cloudsql_instances_service_spec.rb b/spec/services/google_cloud/get_cloudsql_instances_service_spec.rb
new file mode 100644
index 00000000000..4587a5077c0
--- /dev/null
+++ b/spec/services/google_cloud/get_cloudsql_instances_service_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GoogleCloud::GetCloudsqlInstancesService do
+ let(:service) { described_class.new(project) }
+ let(:project) { create(:project) }
+
+ context 'when project has no registered cloud sql instances' do
+ it 'result is empty' do
+ expect(service.execute.length).to eq(0)
+ end
+ end
+
+ context 'when project has registered cloud sql instance' do
+ before do
+ keys = %w[
+ GCP_PROJECT_ID
+ GCP_CLOUDSQL_INSTANCE_NAME
+ GCP_CLOUDSQL_CONNECTION_NAME
+ GCP_CLOUDSQL_PRIMARY_IP_ADDRESS
+ GCP_CLOUDSQL_VERSION
+ GCP_CLOUDSQL_DATABASE_NAME
+ GCP_CLOUDSQL_DATABASE_USER
+ GCP_CLOUDSQL_DATABASE_PASS
+ ]
+
+ envs = %w[
+ *
+ STG
+ PRD
+ ]
+
+ keys.each do |key|
+ envs.each do |env|
+ project.variables.build(protected: false, environment_scope: env, key: key, value: "value-#{key}-#{env}")
+ end
+ end
+ end
+
+ it 'result is grouped by environment', :aggregate_failures do
+ expect(service.execute).to contain_exactly({
+ ref: '*',
+ gcp_project: 'value-GCP_PROJECT_ID-*',
+ instance_name: 'value-GCP_CLOUDSQL_INSTANCE_NAME-*',
+ version: 'value-GCP_CLOUDSQL_VERSION-*'
+ },
+ {
+ ref: 'STG',
+ gcp_project: 'value-GCP_PROJECT_ID-STG',
+ instance_name: 'value-GCP_CLOUDSQL_INSTANCE_NAME-STG',
+ version: 'value-GCP_CLOUDSQL_VERSION-STG'
+ },
+ {
+ ref: 'PRD',
+ gcp_project: 'value-GCP_PROJECT_ID-PRD',
+ instance_name: 'value-GCP_CLOUDSQL_INSTANCE_NAME-PRD',
+ version: 'value-GCP_CLOUDSQL_VERSION-PRD'
+ })
+ end
+ end
+end
diff --git a/spec/services/google_cloud/setup_cloudsql_instance_service_spec.rb b/spec/services/google_cloud/setup_cloudsql_instance_service_spec.rb
index 55553097423..e0a622bfa4a 100644
--- a/spec/services/google_cloud/setup_cloudsql_instance_service_spec.rb
+++ b/spec/services/google_cloud/setup_cloudsql_instance_service_spec.rb
@@ -5,6 +5,21 @@ require 'spec_helper'
RSpec.describe GoogleCloud::SetupCloudsqlInstanceService do
let(:random_user) { create(:user) }
let(:project) { create(:project) }
+ let(:list_databases_empty) { Google::Apis::SqladminV1beta4::ListDatabasesResponse.new(items: []) }
+ let(:list_users_empty) { Google::Apis::SqladminV1beta4::ListUsersResponse.new(items: []) }
+ let(:list_databases) do
+ Google::Apis::SqladminV1beta4::ListDatabasesResponse.new(items: [
+ Google::Apis::SqladminV1beta4::Database.new(name: 'postgres'),
+ Google::Apis::SqladminV1beta4::Database.new(name: 'main_db')
+ ])
+ end
+
+ let(:list_users) do
+ Google::Apis::SqladminV1beta4::ListUsersResponse.new(items: [
+ Google::Apis::SqladminV1beta4::User.new(name: 'postgres'),
+ Google::Apis::SqladminV1beta4::User.new(name: 'main_user')
+ ])
+ end
context 'when unauthorized user triggers worker' do
subject do
@@ -76,6 +91,8 @@ RSpec.describe GoogleCloud::SetupCloudsqlInstanceService do
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |google_api_client|
expect(google_api_client).to receive(:get_cloudsql_instance).and_return(get_instance_response_runnable)
expect(google_api_client).to receive(:create_cloudsql_database).and_return(operation_fail)
+ expect(google_api_client).to receive(:list_cloudsql_databases).and_return(list_databases_empty)
+ expect(google_api_client).to receive(:list_cloudsql_users).and_return(list_users_empty)
end
message = subject[:message]
@@ -92,6 +109,8 @@ RSpec.describe GoogleCloud::SetupCloudsqlInstanceService do
expect(google_api_client).to receive(:get_cloudsql_instance).and_return(get_instance_response_runnable)
expect(google_api_client).to receive(:create_cloudsql_database).and_return(operation_done)
expect(google_api_client).to receive(:create_cloudsql_user).and_return(operation_fail)
+ expect(google_api_client).to receive(:list_cloudsql_databases).and_return(list_databases_empty)
+ expect(google_api_client).to receive(:list_cloudsql_users).and_return(list_users_empty)
end
message = subject[:message]
@@ -102,12 +121,59 @@ RSpec.describe GoogleCloud::SetupCloudsqlInstanceService do
end
end
+ context 'when database and user already exist' do
+ it 'does not try to create a database or user' do
+ allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |google_api_client|
+ expect(google_api_client).to receive(:get_cloudsql_instance).and_return(get_instance_response_runnable)
+ expect(google_api_client).not_to receive(:create_cloudsql_database)
+ expect(google_api_client).not_to receive(:create_cloudsql_user)
+ expect(google_api_client).to receive(:list_cloudsql_databases).and_return(list_databases)
+ expect(google_api_client).to receive(:list_cloudsql_users).and_return(list_users)
+ end
+
+ status = subject[:status]
+ expect(status).to eq(:success)
+ end
+ end
+
+ context 'when database already exists' do
+ it 'does not try to create a database' do
+ allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |google_api_client|
+ expect(google_api_client).to receive(:get_cloudsql_instance).and_return(get_instance_response_runnable)
+ expect(google_api_client).not_to receive(:create_cloudsql_database)
+ expect(google_api_client).to receive(:create_cloudsql_user).and_return(operation_done)
+ expect(google_api_client).to receive(:list_cloudsql_databases).and_return(list_databases)
+ expect(google_api_client).to receive(:list_cloudsql_users).and_return(list_users_empty)
+ end
+
+ status = subject[:status]
+ expect(status).to eq(:success)
+ end
+ end
+
+ context 'when user already exists' do
+ it 'does not try to create a user' do
+ allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |google_api_client|
+ expect(google_api_client).to receive(:get_cloudsql_instance).and_return(get_instance_response_runnable)
+ expect(google_api_client).to receive(:create_cloudsql_database).and_return(operation_done)
+ expect(google_api_client).not_to receive(:create_cloudsql_user)
+ expect(google_api_client).to receive(:list_cloudsql_databases).and_return(list_databases_empty)
+ expect(google_api_client).to receive(:list_cloudsql_users).and_return(list_users)
+ end
+
+ status = subject[:status]
+ expect(status).to eq(:success)
+ end
+ end
+
context 'when database and user creation succeeds' do
it 'stores project CI vars' do
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |google_api_client|
expect(google_api_client).to receive(:get_cloudsql_instance).and_return(get_instance_response_runnable)
expect(google_api_client).to receive(:create_cloudsql_database).and_return(operation_done)
expect(google_api_client).to receive(:create_cloudsql_user).and_return(operation_done)
+ expect(google_api_client).to receive(:list_cloudsql_databases).and_return(list_databases_empty)
+ expect(google_api_client).to receive(:list_cloudsql_users).and_return(list_users_empty)
end
subject
@@ -143,6 +209,8 @@ RSpec.describe GoogleCloud::SetupCloudsqlInstanceService do
expect(google_api_client).to receive(:get_cloudsql_instance).and_return(get_instance_response_runnable)
expect(google_api_client).to receive(:create_cloudsql_database).and_return(operation_done)
expect(google_api_client).to receive(:create_cloudsql_user).and_return(operation_done)
+ expect(google_api_client).to receive(:list_cloudsql_databases).and_return(list_databases_empty)
+ expect(google_api_client).to receive(:list_cloudsql_users).and_return(list_users_empty)
end
subject
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index 6e074f451c4..0cfde9ef434 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -176,6 +176,15 @@ RSpec.describe Groups::CreateService, '#execute' do
end
end
+ describe 'creating a details record' do
+ let(:service) { described_class.new(user, group_params) }
+
+ it 'create the details record connected to the group' do
+ group = subject
+ expect(group.namespace_details).to be_persisted
+ end
+ end
+
describe 'create service for the group' do
let(:service) { described_class.new(user, group_params) }
let(:created_group) { service.execute }
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index f43f64fdf89..0d699dd447b 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Groups::DestroyService do
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
let!(:project) { create(:project, :repository, :legacy_storage, namespace: group) }
- let!(:notification_setting) { create(:notification_setting, source: group)}
+ let!(:notification_setting) { create(:notification_setting, source: group) }
let(:gitlab_shell) { Gitlab::Shell.new }
let(:remove_path) { group.path + "+#{group.id}+deleted" }
@@ -74,6 +74,17 @@ RSpec.describe Groups::DestroyService do
end
end
end
+
+ context 'event store', :sidekiq_might_not_need_inline do
+ it 'publishes a GroupDeletedEvent' do
+ expect { destroy_group(group, user, async) }
+ .to publish_event(Groups::GroupDeletedEvent)
+ .with(
+ group_id: group.id,
+ root_namespace_id: group.root_ancestor.id
+ )
+ end
+ end
end
describe 'asynchronous delete' do
@@ -271,7 +282,7 @@ RSpec.describe Groups::DestroyService do
end
context 'the shared_with group is deleted' do
- let!(:group2_subgroup) { create(:group, :private, parent: group2)}
+ let!(:group2_subgroup) { create(:group, :private, parent: group2) }
let!(:group2_subgroup_project) { create(:project, :private, group: group2_subgroup) }
it 'updates project authorizations so users of both groups lose access', :aggregate_failures do
diff --git a/spec/services/groups/merge_requests_count_service_spec.rb b/spec/services/groups/merge_requests_count_service_spec.rb
index 10c7ba5fca4..8bd350d6f0e 100644
--- a/spec/services/groups/merge_requests_count_service_spec.rb
+++ b/spec/services/groups/merge_requests_count_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Groups::MergeRequestsCountService, :use_clean_rails_memory_store_caching do
let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group, :public)}
+ let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :repository, namespace: group) }
let_it_be(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
diff --git a/spec/services/groups/open_issues_count_service_spec.rb b/spec/services/groups/open_issues_count_service_spec.rb
index fca09bfdebe..923caa6c150 100644
--- a/spec/services/groups/open_issues_count_service_spec.rb
+++ b/spec/services/groups/open_issues_count_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Groups::OpenIssuesCountService, :use_clean_rails_memory_store_caching do
- let_it_be(:group) { create(:group, :public)}
+ let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public, namespace: group) }
let_it_be(:user) { create(:user) }
let_it_be(:issue) { create(:issue, :opened, project: project) }
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index fbcca215282..b543661e9a0 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -22,6 +22,18 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
let(:transfer_service) { described_class.new(group, user) }
+ shared_examples 'publishes a GroupTransferedEvent' do
+ it do
+ expect { transfer_service.execute(target) }
+ .to publish_event(Groups::GroupTransferedEvent)
+ .with(
+ group_id: group.id,
+ old_root_namespace_id: group.root_ancestor.id,
+ new_root_namespace_id: target.root_ancestor.id
+ )
+ end
+ end
+
context 'handling packages' do
let_it_be(:group) { create(:group, :public) }
let_it_be(:new_group) { create(:group, :public) }
@@ -895,6 +907,10 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
expect { transfer_service.execute(root_group) }
.not_to change { CustomerRelations::IssueContact.count }
end
+
+ it_behaves_like 'publishes a GroupTransferedEvent' do
+ let(:target) { root_group }
+ end
end
context 'moving down' do
@@ -904,6 +920,10 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
expect { transfer_service.execute(another_subgroup) }
.not_to change { CustomerRelations::IssueContact.count }
end
+
+ it_behaves_like 'publishes a GroupTransferedEvent' do
+ let(:target) { another_subgroup }
+ end
end
context 'moving sideways' do
@@ -913,6 +933,10 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
expect { transfer_service.execute(another_subgroup) }
.not_to change { CustomerRelations::IssueContact.count }
end
+
+ it_behaves_like 'publishes a GroupTransferedEvent' do
+ let(:target) { another_subgroup }
+ end
end
context 'moving to new root group' do
@@ -932,6 +956,10 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
expect { transfer_service.execute(new_parent_group) }
.not_to change { CustomerRelations::IssueContact.count }
end
+
+ it_behaves_like 'publishes a GroupTransferedEvent' do
+ let(:target) { new_parent_group }
+ end
end
context 'moving to a subgroup within a new root group' do
@@ -953,6 +981,10 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
expect { transfer_service.execute(subgroup_in_new_parent_group) }
.not_to change { CustomerRelations::IssueContact.count }
end
+
+ it_behaves_like 'publishes a GroupTransferedEvent' do
+ let(:target) { subgroup_in_new_parent_group }
+ end
end
context 'with permission on the subgroup' do
@@ -965,6 +997,11 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
expect(transfer_service.error).to eq("Transfer failed: Group contains contacts/organizations and you don't have enough permissions to move them to the new root group.")
end
+
+ it 'does not publish a GroupTransferedEvent' do
+ expect { transfer_service.execute(subgroup_in_new_parent_group) }
+ .not_to publish_event(Groups::GroupTransferedEvent)
+ end
end
end
end
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index 856dd4a2567..5c87b9ac8bb 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -339,8 +339,44 @@ RSpec.describe Groups::UpdateService do
end
end
+ context 'EventStore' do
+ let(:service) { described_class.new(group, user, **params) }
+ let(:root_group) { create(:group, path: 'root') }
+ let(:group) do
+ create(:group, parent: root_group, path: 'old-path').tap { |g| g.add_owner(user) }
+ end
+
+ context 'when changing a group path' do
+ let(:new_path) { SecureRandom.hex }
+ let(:params) { { path: new_path } }
+
+ it 'publishes a GroupPathChangedEvent' do
+ old_path = group.full_path
+
+ expect { service.execute }
+ .to publish_event(Groups::GroupPathChangedEvent)
+ .with(
+ group_id: group.id,
+ root_namespace_id: group.root_ancestor.id,
+ old_path: old_path,
+ new_path: "root/#{new_path}"
+ )
+ end
+ end
+
+ context 'when not changing a group path' do
+ let(:params) { { name: 'very-new-name' } }
+
+ it 'does not publish a GroupPathChangedEvent' do
+ expect { service.execute }
+ .not_to publish_event(Groups::GroupPathChangedEvent)
+ end
+ end
+ end
+
context 'rename group' do
- let!(:service) { described_class.new(internal_group, user, path: SecureRandom.hex) }
+ let(:new_path) { SecureRandom.hex }
+ let!(:service) { described_class.new(internal_group, user, path: new_path) }
before do
internal_group.add_member(user, Gitlab::Access::MAINTAINER)
@@ -371,7 +407,7 @@ RSpec.describe Groups::UpdateService do
end
it "hasn't changed the path" do
- expect { service.execute}.not_to change { internal_group.reload.path}
+ expect { service.execute }.not_to change { internal_group.reload.path }
end
end
end
diff --git a/spec/services/groups/update_statistics_service_spec.rb b/spec/services/groups/update_statistics_service_spec.rb
index 5bef51c2727..84b18b670a7 100644
--- a/spec/services/groups/update_statistics_service_spec.rb
+++ b/spec/services/groups/update_statistics_service_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Groups::UpdateStatisticsService do
let(:statistics) { %w(wiki_size) }
- subject(:service) { described_class.new(group, statistics: statistics)}
+ subject(:service) { described_class.new(group, statistics: statistics) }
describe '#execute', :aggregate_failures do
context 'when group is nil' do
diff --git a/spec/services/import/fogbugz_service_spec.rb b/spec/services/import/fogbugz_service_spec.rb
index c9477dba7a5..7b86c5c45b0 100644
--- a/spec/services/import/fogbugz_service_spec.rb
+++ b/spec/services/import/fogbugz_service_spec.rb
@@ -119,7 +119,7 @@ RSpec.describe Import::FogbugzService do
let(:error_messages_array) { instance_double(Array, join: "something went wrong") }
let(:errors_double) { instance_double(ActiveModel::Errors, full_messages: error_messages_array, :[] => nil) }
let(:project_double) { instance_double(Project, persisted?: false, errors: errors_double) }
- let(:project_creator) { instance_double(Gitlab::FogbugzImport::ProjectCreator, execute: project_double )}
+ let(:project_creator) { instance_double(Gitlab::FogbugzImport::ProjectCreator, execute: project_double ) }
before do
allow(Gitlab::FogbugzImport::ProjectCreator).to receive(:new).and_return(project_creator)
diff --git a/spec/services/import/prepare_service_spec.rb b/spec/services/import/prepare_service_spec.rb
new file mode 100644
index 00000000000..0097198f7a9
--- /dev/null
+++ b/spec/services/import/prepare_service_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Import::PrepareService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:file) { double }
+ let(:upload_service) { double }
+ let(:uploader) { double }
+ let(:upload) { double }
+
+ let(:service) { described_class.new(project, user, file: file) }
+
+ subject { service.execute }
+
+ context 'when file is uploaded correctly' do
+ let(:upload_id) { 99 }
+
+ before do
+ mock_upload
+ end
+
+ it 'raises NotImplemented error for worker' do
+ expect { subject }.to raise_error(NotImplementedError)
+ end
+
+ context 'when a job is enqueued' do
+ before do
+ worker = double
+
+ allow(service).to receive(:worker).and_return(worker)
+ allow(worker).to receive(:perform_async)
+ end
+
+ it 'raises NotImplemented error for success_message when a job is enqueued' do
+ expect { subject }.to raise_error(NotImplementedError)
+ end
+
+ it 'returns a success respnse when a success_message is implemented' do
+ message = 'It works!'
+
+ allow(service).to receive(:success_message).and_return(message)
+
+ result = subject
+
+ expect(result).to be_success
+ expect(result.message).to eq(message)
+ end
+ end
+ end
+
+ context 'when file upload fails' do
+ before do
+ mock_upload(false)
+ end
+
+ it 'returns an error message' do
+ result = subject
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('File upload error.')
+ end
+ end
+end
diff --git a/spec/services/import/validate_remote_git_endpoint_service_spec.rb b/spec/services/import/validate_remote_git_endpoint_service_spec.rb
index 9dc862b6ca3..221ac2cd73a 100644
--- a/spec/services/import/validate_remote_git_endpoint_service_spec.rb
+++ b/spec/services/import/validate_remote_git_endpoint_service_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Import::ValidateRemoteGitEndpointService do
end
context 'when uri is using git:// protocol' do
- subject { described_class.new(url: 'git://demo.host/repo')}
+ subject { described_class.new(url: 'git://demo.host/repo') }
it 'returns success' do
result = subject.execute
diff --git a/spec/services/incident_management/timeline_events/create_service_spec.rb b/spec/services/incident_management/timeline_events/create_service_spec.rb
index a4e928b98f4..b999403e168 100644
--- a/spec/services/incident_management/timeline_events/create_service_spec.rb
+++ b/spec/services/incident_management/timeline_events/create_service_spec.rb
@@ -244,5 +244,88 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
it_behaves_like 'successfully created timeline event'
end
+
+ describe '.change_labels' do
+ subject(:execute) do
+ described_class.change_labels(incident, current_user, added_labels: added, removed_labels: removed)
+ end
+
+ let_it_be(:labels) { create_list(:label, 4, project: project) }
+
+ let(:expected_action) { 'label' }
+
+ context 'when there are neither added nor removed labels' do
+ let(:added) { [] }
+ let(:removed) { [] }
+
+ it 'responds with error', :aggregate_failures do
+ expect(execute).to be_error
+ expect(execute.message).to eq(_('There are no changed labels'))
+ end
+
+ it 'does not create timeline event' do
+ expect { execute }.not_to change { incident.incident_management_timeline_events.count }
+ end
+ end
+
+ context 'when there are only added labels' do
+ let(:added) { [labels[0], labels[1]] }
+ let(:removed) { [] }
+
+ let(:expected_note) { "@#{current_user.username} added #{added.map(&:to_reference).join(' ')} labels" }
+
+ it_behaves_like 'successfully created timeline event'
+ end
+
+ context 'when there are only removed labels' do
+ let(:added) { [] }
+ let(:removed) { [labels[2], labels[3]] }
+
+ let(:expected_note) { "@#{current_user.username} removed #{removed.map(&:to_reference).join(' ')} labels" }
+
+ it_behaves_like 'successfully created timeline event'
+ end
+
+ context 'when there are both added and removed labels' do
+ let(:added) { [labels[0], labels[1]] }
+ let(:removed) { [labels[2], labels[3]] }
+
+ let(:expected_note) do
+ added_note = "added #{added.map(&:to_reference).join(' ')} labels"
+ removed_note = "removed #{removed.map(&:to_reference).join(' ')} labels"
+
+ "@#{current_user.username} #{added_note} and #{removed_note}"
+ end
+
+ it_behaves_like 'successfully created timeline event'
+ end
+
+ context 'when there is a single added and single removed labels' do
+ let(:added) { [labels[0]] }
+ let(:removed) { [labels[3]] }
+
+ let(:expected_note) do
+ added_note = "added #{added.first.to_reference} label"
+ removed_note = "removed #{removed.first.to_reference} label"
+
+ "@#{current_user.username} #{added_note} and #{removed_note}"
+ end
+
+ it_behaves_like 'successfully created timeline event'
+ end
+
+ context 'when feature flag is disabled' do
+ let(:added) { [labels[0], labels[1]] }
+ let(:removed) { [labels[2], labels[3]] }
+
+ before do
+ stub_feature_flags(incident_timeline_events_from_labels: false)
+ end
+
+ it 'does not create timeline event' do
+ expect { execute }.not_to change { incident.incident_management_timeline_events.count }
+ end
+ end
+ end
end
end
diff --git a/spec/services/incident_management/timeline_events/update_service_spec.rb b/spec/services/incident_management/timeline_events/update_service_spec.rb
index 728f2fa3e9d..f612c72e2a8 100644
--- a/spec/services/incident_management/timeline_events/update_service_spec.rb
+++ b/spec/services/incident_management/timeline_events/update_service_spec.rb
@@ -32,6 +32,10 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do
expect(execute.message).to eq(message)
end
+ it 'does not update the note' do
+ expect { execute }.not_to change { timeline_event.reload.note }
+ end
+
it_behaves_like 'does not track incident management event', :incident_management_timeline_event_edited
end
@@ -94,16 +98,7 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do
context 'when note is blank' do
let(:params) { { note: '', occurred_at: occurred_at } }
- it_behaves_like 'successful response'
- it_behaves_like 'passing the correct was_changed value', :occurred_at
-
- it 'does not update the note' do
- expect { execute }.not_to change { timeline_event.reload.note }
- end
-
- it 'updates occurred_at' do
- expect { execute }.to change { timeline_event.occurred_at }.to(params[:occurred_at])
- end
+ it_behaves_like 'error response', "Note can't be blank"
end
context 'when occurred_at is nil' do
@@ -121,6 +116,12 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do
end
end
+ context 'when occurred_at is blank' do
+ let(:params) { { note: 'Updated note', occurred_at: '' } }
+
+ it_behaves_like 'error response', "Occurred at can't be blank"
+ end
+
context 'when both occurred_at and note is nil' do
let(:params) { {} }
diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb
index 1426ef2a1f6..0d2b8a4ac3c 100644
--- a/spec/services/issuable/common_system_notes_service_spec.rb
+++ b/spec/services/issuable/common_system_notes_service_spec.rb
@@ -8,6 +8,37 @@ RSpec.describe Issuable::CommonSystemNotesService do
let(:issuable) { create(:issue, project: project) }
+ shared_examples 'system note for issuable date changes' do
+ it 'creates a system note for due_date set' do
+ issuable.update!(due_date: Date.today)
+
+ expect { subject }.to change(issuable.notes, :count).from(0).to(1)
+ expect(issuable.notes.last.note).to match('changed due date to')
+ end
+
+ it 'creates a system note for start_date set' do
+ issuable.update!(start_date: Date.today)
+
+ expect { subject }.to change(issuable.notes, :count).from(0).to(1)
+ expect(issuable.notes.last.note).to match('changed start date to')
+ end
+
+ it 'creates a note when both start and due date are changed' do
+ issuable.update!(start_date: Date.today, due_date: 1.week.from_now)
+
+ expect { subject }.to change { issuable.notes.count }.from(0).to(1)
+ expect(issuable.notes.last.note).to match(/changed start date to.+and changed due date to/)
+ end
+
+ it 'does not call SystemNoteService if no dates are changed' do
+ issuable.update!(title: 'new title')
+
+ expect(SystemNoteService).not_to receive(:change_start_date_or_due_date)
+
+ subject
+ end
+ end
+
context 'on issuable update' do
it_behaves_like 'system note creation', { title: 'New title' }, 'changed title'
it_behaves_like 'system note creation', { description: 'New description' }, 'changed the description'
@@ -61,6 +92,12 @@ RSpec.describe Issuable::CommonSystemNotesService do
end
end
end
+
+ context 'when changing dates' do
+ it_behaves_like 'system note for issuable date changes' do
+ subject { described_class.new(project: project, current_user: user).execute(issuable) }
+ end
+ end
end
context 'on issuable create' do
@@ -102,12 +139,8 @@ RSpec.describe Issuable::CommonSystemNotesService do
end
end
- it 'creates a system note for due_date set' do
- issuable.due_date = Date.today
- issuable.save!
-
- expect { subject }.to change { issuable.notes.count }.from(0).to(1)
- expect(issuable.notes.last.note).to match('changed due date')
+ context 'when changing dates' do
+ it_behaves_like 'system note for issuable date changes'
end
end
end
diff --git a/spec/services/issues/clone_service_spec.rb b/spec/services/issues/clone_service_spec.rb
index 858dfc4ab3a..435488b7f66 100644
--- a/spec/services/issues/clone_service_spec.rb
+++ b/spec/services/issues/clone_service_spec.rb
@@ -57,8 +57,20 @@ RSpec.describe Issues::CloneService do
expect(old_issue.notes.last.note).to start_with 'cloned to'
end
- it 'adds system note to new issue at the end' do
- expect(new_issue.notes.last.note).to start_with 'cloned from'
+ it 'adds system note to new issue at the start' do
+ # We set an assignee so an assignee system note will be generated and
+ # we can assert that the "cloned from" note is the first one
+ assignee = create(:user)
+ new_project.add_developer(assignee)
+ old_issue.assignees = [assignee]
+
+ new_issue = clone_service.execute(old_issue, new_project)
+
+ expect(new_issue.notes.size).to eq(2)
+
+ cloned_from_note = new_issue.notes.last
+ expect(cloned_from_note.note).to start_with 'cloned from'
+ expect(new_issue.notes.fresh.first).to eq(cloned_from_note)
end
it 'keeps old issue open' do
@@ -128,11 +140,11 @@ RSpec.describe Issues::CloneService do
context 'issue with award emoji' do
let!(:award_emoji) { create(:award_emoji, awardable: old_issue) }
- it 'copies the award emoji' do
+ it 'does not copy the award emoji' do
old_issue.reload
new_issue = clone_service.execute(old_issue, new_project)
- expect(old_issue.award_emoji.first.name).to eq new_issue.reload.award_emoji.first.name
+ expect(new_issue.reload.award_emoji).to be_empty
end
end
@@ -170,19 +182,21 @@ RSpec.describe Issues::CloneService do
context 'issue with due date' do
let(:date) { Date.parse('2020-01-10') }
+ let(:new_date) { date + 1.week }
let(:old_issue) do
create(:issue, title: title, description: description, project: old_project, author: author, due_date: date)
end
before do
- SystemNoteService.change_due_date(old_issue, old_project, author, old_issue.due_date)
+ old_issue.update!(due_date: new_date)
+ SystemNoteService.change_start_date_or_due_date(old_issue, old_project, author, old_issue.previous_changes.slice('due_date'))
end
it 'keeps the same due date' do
new_issue = clone_service.execute(old_issue, new_project)
- expect(new_issue.due_date).to eq(date)
+ expect(new_issue.due_date).to eq(old_issue.due_date)
end
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 0bc8511e3e3..80c455e72b0 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -69,6 +69,12 @@ RSpec.describe Issues::CreateService do
expect(issue.issue_customer_relations_contacts).to be_empty
end
+ it 'calls NewIssueWorker with correct arguments' do
+ expect(NewIssueWorker).to receive(:perform_async).with(Integer, user.id, 'Issue')
+
+ issue
+ end
+
context 'when a build_service is provided' do
let(:issue) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params, build_service: build_service).execute }
@@ -143,6 +149,12 @@ RSpec.describe Issues::CreateService do
issue
end
+ it 'calls NewIssueWorker with correct arguments' do
+ expect(NewIssueWorker).to receive(:perform_async).with(Integer, reporter.id, 'Issue')
+
+ issue
+ end
+
context 'when invalid' do
before do
opts.merge!(title: '')
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index 5a1bb2e8b74..863df810d01 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Issues::MoveService do
let_it_be(:new_project) { create(:project, namespace: sub_group_2) }
let(:old_issue) do
- create(:issue, title: title, description: description, project: old_project, author: author)
+ create(:issue, title: title, description: description, project: old_project, author: author, created_at: 1.day.ago, updated_at: 1.day.ago)
end
subject(:move_service) do
@@ -62,8 +62,11 @@ RSpec.describe Issues::MoveService do
expect(old_issue.notes.last.note).to start_with 'moved to'
end
- it 'adds system note to new issue at the end' do
- expect(new_issue.notes.last.note).to start_with 'moved from'
+ it 'adds system note to new issue at the end', :freeze_time do
+ system_note = new_issue.notes.last
+
+ expect(system_note.note).to start_with 'moved from'
+ expect(system_note.created_at).to be_like_time(Time.current)
end
it 'closes old issue' do
@@ -137,7 +140,8 @@ RSpec.describe Issues::MoveService do
end
before do
- SystemNoteService.change_due_date(old_issue, old_project, author, old_issue.due_date)
+ old_issue.update!(due_date: Date.today)
+ SystemNoteService.change_start_date_or_due_date(old_issue, old_project, author, old_issue.previous_changes.slice('due_date'))
end
it 'does not create extra system notes' do
diff --git a/spec/services/issues/prepare_import_csv_service_spec.rb b/spec/services/issues/prepare_import_csv_service_spec.rb
new file mode 100644
index 00000000000..ded23ee43b9
--- /dev/null
+++ b/spec/services/issues/prepare_import_csv_service_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Issues::PrepareImportCsvService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:file) { double }
+ let(:upload_service) { double }
+ let(:uploader) { double }
+ let(:upload) { double }
+
+ let(:subject) do
+ described_class.new(project, user, file: file).execute
+ end
+
+ context 'when file is uploaded correctly' do
+ let(:upload_id) { 99 }
+
+ before do
+ mock_upload
+ end
+
+ it 'returns a success message' do
+ result = subject
+
+ expect(result[:status]).to eq(:success)
+ expect(result[:message]).to eq("Your issues are being imported. Once finished, you'll get a confirmation email.")
+ end
+
+ it 'enqueues the ImportRequirementsCsvWorker' do
+ expect(ImportIssuesCsvWorker).to receive(:perform_async).with(user.id, project.id, upload_id)
+
+ subject
+ end
+ end
+
+ context 'when file upload fails' do
+ before do
+ mock_upload(false)
+ end
+
+ it 'returns an error message' do
+ result = subject
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('File upload error.')
+ end
+ end
+end
diff --git a/spec/services/issues/referenced_merge_requests_service_spec.rb b/spec/services/issues/referenced_merge_requests_service_spec.rb
index dc55ba8ebea..16166c1fa33 100644
--- a/spec/services/issues/referenced_merge_requests_service_spec.rb
+++ b/spec/services/issues/referenced_merge_requests_service_spec.rb
@@ -106,7 +106,7 @@ RSpec.describe Issues::ReferencedMergeRequestsService do
end
describe '#closed_by_merge_requests' do
- let(:closed_issue) { build(:issue, :closed, project: project)}
+ let(:closed_issue) { build(:issue, :closed, project: project) }
it 'returns the open merge requests that close this issue' do
create_closing_mr(source_project: project, state: 'closed')
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index e2e8828ae89..aef3608831c 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -849,8 +849,8 @@ RSpec.describe Issues::UpdateService, :mailer do
end
it 'creates system note about task status change' do
- note1 = find_note('marked the task **Task 1** as completed')
- note2 = find_note('marked the task **Task 2** as completed')
+ note1 = find_note('marked the checklist item **Task 1** as completed')
+ note2 = find_note('marked the checklist item **Task 2** as completed')
expect(note1).not_to be_nil
expect(note2).not_to be_nil
@@ -867,8 +867,8 @@ RSpec.describe Issues::UpdateService, :mailer do
end
it 'creates system note about task status change' do
- note1 = find_note('marked the task **Task 1** as incomplete')
- note2 = find_note('marked the task **Task 2** as incomplete')
+ note1 = find_note('marked the checklist item **Task 1** as incomplete')
+ note2 = find_note('marked the checklist item **Task 2** as incomplete')
expect(note1).not_to be_nil
expect(note2).not_to be_nil
@@ -885,7 +885,7 @@ RSpec.describe Issues::UpdateService, :mailer do
end
it 'does not create a system note for the task' do
- task_note = find_note('marked the task **Task 2** as incomplete')
+ task_note = find_note('marked the checklist item **Task 2** as incomplete')
description_notes = find_notes('description')
expect(task_note).to be_nil
@@ -900,7 +900,7 @@ RSpec.describe Issues::UpdateService, :mailer do
end
it 'does not create a system note referencing the position the old item' do
- task_note = find_note('marked the task **Two** as incomplete')
+ task_note = find_note('marked the checklist item **Two** as incomplete')
description_notes = find_notes('description')
expect(task_note).to be_nil
@@ -988,6 +988,52 @@ RSpec.describe Issues::UpdateService, :mailer do
end
end
+ context 'updating dates' do
+ subject(:result) { described_class.new(project: project, current_user: user, params: params).execute(issue) }
+
+ let(:updated_date) { 1.week.from_now.to_date }
+
+ shared_examples 'issue update service that triggers date updates' do
+ it 'triggers graphql date updated subscription' do
+ expect(GraphqlTriggers).to receive(:issuable_dates_updated).with(issue).and_call_original
+
+ result
+ end
+ end
+
+ shared_examples 'issue update service that does not trigger date updates' do
+ it 'does not trigger date updated subscriptions' do
+ expect(GraphqlTriggers).not_to receive(:issuable_dates_updated)
+
+ result
+ end
+ end
+
+ context 'when due_date is updated' do
+ let(:params) { { due_date: updated_date } }
+
+ it_behaves_like 'issue update service that triggers date updates'
+ end
+
+ context 'when start_date is updated' do
+ let(:params) { { start_date: updated_date } }
+
+ it_behaves_like 'issue update service that triggers date updates'
+ end
+
+ context 'when no date is updated' do
+ let(:params) { { title: 'should not trigger date updates' } }
+
+ it_behaves_like 'issue update service that does not trigger date updates'
+ end
+
+ context 'when update is not successful but date is provided' do
+ let(:params) { { title: '', due_date: updated_date } }
+
+ it_behaves_like 'issue update service that does not trigger date updates'
+ end
+ end
+
context 'updating asssignee_id' do
it 'does not update assignee when assignee_id is invalid' do
update_issue(assignee_ids: [-1])
diff --git a/spec/services/jira_import/start_import_service_spec.rb b/spec/services/jira_import/start_import_service_spec.rb
index 510f58f0e75..c0db3012a30 100644
--- a/spec/services/jira_import/start_import_service_spec.rb
+++ b/spec/services/jira_import/start_import_service_spec.rb
@@ -136,7 +136,7 @@ RSpec.describe JiraImport::StartImportService do
end
context 'when multiple Jira imports for same Jira project' do
- let!(:jira_imports) { create_list(:jira_import_state, 3, :finished, project: project, jira_project_key: fake_key)}
+ let!(:jira_imports) { create_list(:jira_import_state, 3, :finished, project: project, jira_project_key: fake_key) }
it 'creates Jira label title with correct number' do
jira_import = subject.payload[:import_data]
diff --git a/spec/services/lfs/push_service_spec.rb b/spec/services/lfs/push_service_spec.rb
index e1564ca2359..f52bba94eea 100644
--- a/spec/services/lfs/push_service_spec.rb
+++ b/spec/services/lfs/push_service_spec.rb
@@ -98,7 +98,7 @@ RSpec.describe Lfs::PushService do
end
def batch_spec(*objects, upload: true, verify: false)
- { 'transfer' => 'basic', 'objects' => objects.map {|o| object_spec(o, upload: upload) } }
+ { 'transfer' => 'basic', 'objects' => objects.map { |o| object_spec(o, upload: upload) } }
end
def object_spec(object, upload: true, verify: false)
diff --git a/spec/services/markdown_content_rewriter_service_spec.rb b/spec/services/markdown_content_rewriter_service_spec.rb
index 91a117536ca..d94289856cf 100644
--- a/spec/services/markdown_content_rewriter_service_spec.rb
+++ b/spec/services/markdown_content_rewriter_service_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe MarkdownContentRewriterService do
let_it_be(:target_parent) { create(:project, :public) }
let(:content) { 'My content' }
- let(:issue) { create(:issue, project: source_parent, description: content)}
+ let(:issue) { create(:issue, project: source_parent, description: content) }
describe '#initialize' do
it 'raises an error if source_parent is not a Project' do
diff --git a/spec/services/members/groups/creator_service_spec.rb b/spec/services/members/groups/creator_service_spec.rb
index 4130fbd44fa..fced7195046 100644
--- a/spec/services/members/groups/creator_service_spec.rb
+++ b/spec/services/members/groups/creator_service_spec.rb
@@ -27,7 +27,10 @@ RSpec.describe Members::Groups::CreatorService do
context 'authorized projects update' do
it 'schedules a single project authorization update job when called multiple times' do
- expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait).once
+ # this is inline with the overridden behaviour in stubbed_member.rb
+ worker_instance = AuthorizedProjectsWorker.new
+ expect(AuthorizedProjectsWorker).to receive(:new).once.and_return(worker_instance)
+ expect(worker_instance).to receive(:perform).with(user.id)
1.upto(3) do
described_class.add_member(source, user, :maintainer)
diff --git a/spec/services/members/invite_service_spec.rb b/spec/services/members/invite_service_spec.rb
index d25c8996931..6dbe161ee02 100644
--- a/spec/services/members/invite_service_spec.rb
+++ b/spec/services/members/invite_service_spec.rb
@@ -455,7 +455,7 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_
end
context 'when access_level is lower than inheriting member' do
- let(:params) { { user_id: group_member.user_id, access_level: ProjectMember::GUEST }}
+ let(:params) { { user_id: group_member.user_id, access_level: ProjectMember::GUEST } }
it 'does not add the member and returns an error' do
msg = "Access level should be greater than or equal " \
@@ -467,7 +467,7 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_
end
context 'when access_level is the same as the inheriting member' do
- let(:params) { { user_id: group_member.user_id, access_level: ProjectMember::DEVELOPER }}
+ let(:params) { { user_id: group_member.user_id, access_level: ProjectMember::DEVELOPER } }
it 'adds the member with correct access_level' do
expect_to_create_members(count: 1)
@@ -477,7 +477,7 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_
end
context 'when access_level is greater than the inheriting member' do
- let(:params) { { user_id: group_member.user_id, access_level: ProjectMember::MAINTAINER }}
+ let(:params) { { user_id: group_member.user_id, access_level: ProjectMember::MAINTAINER } }
it 'adds the member with correct access_level' do
expect_to_create_members(count: 1)
diff --git a/spec/services/merge_requests/approval_service_spec.rb b/spec/services/merge_requests/approval_service_spec.rb
index e1fbb945ee3..ab98fad5d73 100644
--- a/spec/services/merge_requests/approval_service_spec.rb
+++ b/spec/services/merge_requests/approval_service_spec.rb
@@ -20,79 +20,111 @@ RSpec.describe MergeRequests::ApprovalService do
allow(merge_request.approvals).to receive(:new).and_return(double(save: false))
end
- it 'does not create an approval note' do
- expect(SystemNoteService).not_to receive(:approve_mr)
+ it 'does not reset approvals' do
+ expect(merge_request.approvals).not_to receive(:reset)
service.execute(merge_request)
end
- it 'does not mark pending todos as done' do
- service.execute(merge_request)
-
- expect(todo.reload).to be_pending
- end
-
it 'does not track merge request approve action' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.not_to receive(:track_approve_mr_action).with(user: user)
service.execute(merge_request)
end
- end
-
- context 'with valid approval' do
- let(:notification_service) { NotificationService.new }
- before do
- allow(service).to receive(:notification_service).and_return(notification_service)
+ it 'does not publish MergeRequests::ApprovedEvent' do
+ expect { service.execute(merge_request) }.not_to publish_event(MergeRequests::ApprovedEvent)
end
- it 'creates an approval note and marks pending todos as done' do
- expect(SystemNoteService).to receive(:approve_mr).with(merge_request, user)
- expect(merge_request.approvals).to receive(:reset)
+ context 'async_after_approval feature flag is disabled' do
+ before do
+ stub_feature_flags(async_after_approval: false)
+ end
- service.execute(merge_request)
+ it 'does not create approve MR event' do
+ expect(EventCreateService).not_to receive(:new)
- expect(todo.reload).to be_done
- end
+ service.execute(merge_request)
+ end
- it 'creates approve MR event' do
- expect_next_instance_of(EventCreateService) do |instance|
- expect(instance).to receive(:approve_mr)
- .with(merge_request, user)
+ it 'does not create an approval note' do
+ expect(SystemNoteService).not_to receive(:approve_mr)
+
+ service.execute(merge_request)
end
- service.execute(merge_request)
+ it 'does not mark pending todos as done' do
+ service.execute(merge_request)
+
+ expect(todo.reload).to be_pending
+ end
end
+ end
- it 'sends a notification when approving' do
- expect(notification_service).to receive_message_chain(:async, :approve_mr)
- .with(merge_request, user)
+ context 'with valid approval' do
+ it 'resets approvals' do
+ expect(merge_request.approvals).to receive(:reset)
service.execute(merge_request)
end
- it 'removes attention requested state' do
- expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new)
- .with(project: project, current_user: user, merge_request: merge_request, user: user)
- .and_call_original
+ it 'tracks merge request approve action' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_approve_mr_action).with(user: user, merge_request: merge_request)
service.execute(merge_request)
end
- context 'with remaining approvals' do
- it 'fires an approval webhook' do
- expect(service).to receive(:execute_hooks).with(merge_request, 'approved')
+ it 'publishes MergeRequests::ApprovedEvent' do
+ expect { service.execute(merge_request) }
+ .to publish_event(MergeRequests::ApprovedEvent)
+ .with(current_user_id: user.id, merge_request_id: merge_request.id)
+ end
+
+ context 'async_after_approval feature flag is disabled' do
+ let(:notification_service) { NotificationService.new }
+
+ before do
+ stub_feature_flags(async_after_approval: false)
+ allow(service).to receive(:notification_service).and_return(notification_service)
+ end
+
+ it 'creates approve MR event' do
+ expect_next_instance_of(EventCreateService) do |instance|
+ expect(instance).to receive(:approve_mr)
+ .with(merge_request, user)
+ end
service.execute(merge_request)
end
- end
- it 'tracks merge request approve action' do
- expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
- .to receive(:track_approve_mr_action).with(user: user, merge_request: merge_request)
+ it 'creates an approval note' do
+ expect(SystemNoteService).to receive(:approve_mr).with(merge_request, user)
- service.execute(merge_request)
+ service.execute(merge_request)
+ end
+
+ it 'marks pending todos as done' do
+ service.execute(merge_request)
+
+ expect(todo.reload).to be_done
+ end
+
+ it 'sends a notification when approving' do
+ expect(notification_service).to receive_message_chain(:async, :approve_mr)
+ .with(merge_request, user)
+
+ service.execute(merge_request)
+ end
+
+ context 'with remaining approvals' do
+ it 'fires an approval webhook' do
+ expect(service).to receive(:execute_hooks).with(merge_request, 'approved')
+
+ service.execute(merge_request)
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb b/spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb
deleted file mode 100644
index b2326a28e63..00000000000
--- a/spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe MergeRequests::BulkRemoveAttentionRequestedService do
- let(:current_user) { create(:user) }
- let(:user) { create(:user) }
- let(:assignee_user) { create(:user) }
- let(:merge_request) { create(:merge_request, reviewers: [user], assignees: [assignee_user]) }
- let(:reviewer) { merge_request.find_reviewer(user) }
- let(:assignee) { merge_request.find_assignee(assignee_user) }
- let(:project) { merge_request.project }
- let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, users: [user, assignee_user]) }
- let(:result) { service.execute }
-
- before do
- project.add_developer(current_user)
- project.add_developer(user)
- end
-
- describe '#execute' do
- context 'invalid permissions' do
- let(:service) { described_class.new(project: project, current_user: create(:user), merge_request: merge_request, users: [user]) }
-
- it 'returns an error' do
- expect(result[:status]).to eq :error
- end
- end
-
- context 'updates reviewers and assignees' do
- it 'returns success' do
- expect(result[:status]).to eq :success
- end
-
- it 'updates reviewers state' do
- service.execute
- reviewer.reload
- assignee.reload
-
- expect(reviewer.state).to eq 'reviewed'
- expect(assignee.state).to eq 'reviewed'
- end
-
- it_behaves_like 'invalidates attention request cache' do
- let(:users) { [assignee_user, user] }
- end
- end
- end
-end
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index cd1c362a19f..8f448184b45 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -54,10 +54,6 @@ RSpec.describe MergeRequests::CloseService do
expect(todo.reload).to be_done
end
- it 'removes attention requested state' do
- expect(merge_request.find_assignee(user2).attention_requested?).to eq(false)
- end
-
context 'when auto merge is enabled' do
let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
diff --git a/spec/services/merge_requests/create_approval_event_service_spec.rb b/spec/services/merge_requests/create_approval_event_service_spec.rb
new file mode 100644
index 00000000000..3d41ace11a7
--- /dev/null
+++ b/spec/services/merge_requests/create_approval_event_service_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::CreateApprovalEventService do
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.project }
+
+ subject(:service) { described_class.new(project: project, current_user: user) }
+
+ describe '#execute' do
+ it 'creates approve MR event' do
+ expect_next_instance_of(EventCreateService) do |instance|
+ expect(instance).to receive(:approve_mr)
+ .with(merge_request, user)
+ end
+
+ service.execute(merge_request)
+ end
+ end
+end
diff --git a/spec/services/merge_requests/create_pipeline_service_spec.rb b/spec/services/merge_requests/create_pipeline_service_spec.rb
index 03a37ea59a3..c443d758a77 100644
--- a/spec/services/merge_requests/create_pipeline_service_spec.rb
+++ b/spec/services/merge_requests/create_pipeline_service_spec.rb
@@ -74,6 +74,16 @@ RSpec.describe MergeRequests::CreatePipelineService do
expect(response.payload.project).to eq(project)
end
+ context 'when the feature is disabled in CI/CD settings' do
+ before do
+ project.update!(ci_allow_fork_pipelines_to_run_in_parent_project: false)
+ end
+
+ it 'creates a pipeline in the source project' do
+ expect(response.payload.project).to eq(source_project)
+ end
+ end
+
context 'when source branch is protected' do
context 'when actor does not have permission to update the protected branch in target project' do
let!(:protected_branch) { create(:protected_branch, name: '*', project: project) }
diff --git a/spec/services/merge_requests/execute_approval_hooks_service_spec.rb b/spec/services/merge_requests/execute_approval_hooks_service_spec.rb
new file mode 100644
index 00000000000..863c47e8191
--- /dev/null
+++ b/spec/services/merge_requests/execute_approval_hooks_service_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::ExecuteApprovalHooksService do
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.project }
+
+ subject(:service) { described_class.new(project: project, current_user: user) }
+
+ describe '#execute' do
+ let(:notification_service) { NotificationService.new }
+
+ before do
+ allow(service).to receive(:notification_service).and_return(notification_service)
+ end
+ it 'sends a notification when approving' do
+ expect(notification_service).to receive_message_chain(:async, :approve_mr)
+ .with(merge_request, user)
+
+ service.execute(merge_request)
+ end
+
+ context 'with remaining approvals' do
+ it 'fires an approval webhook' do
+ expect(service).to receive(:execute_hooks).with(merge_request, 'approved')
+
+ service.execute(merge_request)
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/handle_assignees_change_service_spec.rb b/spec/services/merge_requests/handle_assignees_change_service_spec.rb
index fa3b1614e21..c43f5db6059 100644
--- a/spec/services/merge_requests/handle_assignees_change_service_spec.rb
+++ b/spec/services/merge_requests/handle_assignees_change_service_spec.rb
@@ -87,14 +87,6 @@ RSpec.describe MergeRequests::HandleAssigneesChangeService do
expect(todo).to be_pending
end
- it 'removes attention requested state' do
- expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new)
- .with(project: project, current_user: user, merge_request: merge_request, user: user)
- .and_call_original
-
- execute
- end
-
it 'tracks users assigned event' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_users_assigned_to_mr).once.with(users: [assignee])
diff --git a/spec/services/merge_requests/mergeability/check_broken_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_broken_status_service_spec.rb
index 9e178c121ef..6cc1079c94a 100644
--- a/spec/services/merge_requests/mergeability/check_broken_status_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/check_broken_status_service_spec.rb
@@ -8,6 +8,8 @@ RSpec.describe MergeRequests::Mergeability::CheckBrokenStatusService do
let(:merge_request) { build(:merge_request) }
describe '#execute' do
+ let(:result) { check_broken_status.execute }
+
before do
expect(merge_request).to receive(:broken?).and_return(broken)
end
@@ -16,7 +18,8 @@ RSpec.describe MergeRequests::Mergeability::CheckBrokenStatusService do
let(:broken) { true }
it 'returns a check result with status failed' do
- expect(check_broken_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
+ expect(result.payload[:reason]).to eq(:broken_status)
end
end
@@ -24,7 +27,7 @@ RSpec.describe MergeRequests::Mergeability::CheckBrokenStatusService do
let(:broken) { false }
it 'returns a check result with status success' do
- expect(check_broken_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
end
end
end
diff --git a/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb
index 6fbbecd7c0e..def3cb0ca28 100644
--- a/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb
@@ -10,6 +10,8 @@ RSpec.describe MergeRequests::Mergeability::CheckCiStatusService do
let(:skip_check) { false }
describe '#execute' do
+ let(:result) { check_ci_status.execute }
+
before do
expect(merge_request).to receive(:mergeable_ci_state?).and_return(mergeable)
end
@@ -18,7 +20,7 @@ RSpec.describe MergeRequests::Mergeability::CheckCiStatusService do
let(:mergeable) { true }
it 'returns a check result with status success' do
- expect(check_ci_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
end
end
@@ -26,7 +28,8 @@ RSpec.describe MergeRequests::Mergeability::CheckCiStatusService do
let(:mergeable) { false }
it 'returns a check result with status failed' do
- expect(check_ci_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
+ expect(result.payload[:reason]).to eq :ci_must_pass
end
end
end
diff --git a/spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb
index c24d40967c4..9f107ce046a 100644
--- a/spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb
@@ -10,6 +10,8 @@ RSpec.describe MergeRequests::Mergeability::CheckDiscussionsStatusService do
let(:skip_check) { false }
describe '#execute' do
+ let(:result) { check_discussions_status.execute }
+
before do
expect(merge_request).to receive(:mergeable_discussions_state?).and_return(mergeable)
end
@@ -18,7 +20,7 @@ RSpec.describe MergeRequests::Mergeability::CheckDiscussionsStatusService do
let(:mergeable) { true }
it 'returns a check result with status success' do
- expect(check_discussions_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
end
end
@@ -26,7 +28,8 @@ RSpec.describe MergeRequests::Mergeability::CheckDiscussionsStatusService do
let(:mergeable) { false }
it 'returns a check result with status failed' do
- expect(check_discussions_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
+ expect(result.payload[:reason]).to eq(:discussions_not_resolved)
end
end
end
diff --git a/spec/services/merge_requests/mergeability/check_draft_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_draft_status_service_spec.rb
index 923cff220ef..e9363e5d676 100644
--- a/spec/services/merge_requests/mergeability/check_draft_status_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/check_draft_status_service_spec.rb
@@ -8,6 +8,8 @@ RSpec.describe MergeRequests::Mergeability::CheckDraftStatusService do
let(:merge_request) { build(:merge_request) }
describe '#execute' do
+ let(:result) { check_draft_status.execute }
+
before do
expect(merge_request).to receive(:draft?).and_return(draft)
end
@@ -16,7 +18,8 @@ RSpec.describe MergeRequests::Mergeability::CheckDraftStatusService do
let(:draft) { true }
it 'returns a check result with status failed' do
- expect(check_draft_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
+ expect(result.payload[:reason]).to eq(:draft_status)
end
end
@@ -24,7 +27,7 @@ RSpec.describe MergeRequests::Mergeability::CheckDraftStatusService do
let(:draft) { false }
it 'returns a check result with status success' do
- expect(check_draft_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
end
end
end
diff --git a/spec/services/merge_requests/mergeability/check_open_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_open_status_service_spec.rb
index b1c9a930317..936524b020a 100644
--- a/spec/services/merge_requests/mergeability/check_open_status_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/check_open_status_service_spec.rb
@@ -8,6 +8,8 @@ RSpec.describe MergeRequests::Mergeability::CheckOpenStatusService do
let(:merge_request) { build(:merge_request) }
describe '#execute' do
+ let(:result) { check_open_status.execute }
+
before do
expect(merge_request).to receive(:open?).and_return(open)
end
@@ -16,7 +18,7 @@ RSpec.describe MergeRequests::Mergeability::CheckOpenStatusService do
let(:open) { true }
it 'returns a check result with status success' do
- expect(check_open_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
end
end
@@ -24,7 +26,8 @@ RSpec.describe MergeRequests::Mergeability::CheckOpenStatusService do
let(:open) { false }
it 'returns a check result with status failed' do
- expect(check_open_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
+ expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
+ expect(result.payload[:reason]).to eq(:not_open)
end
end
end
diff --git a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
index 2bb7dc3eef7..afea3e952a1 100644
--- a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
@@ -5,11 +5,11 @@ require 'spec_helper'
RSpec.describe MergeRequests::Mergeability::RunChecksService do
subject(:run_checks) { described_class.new(merge_request: merge_request, params: {}) }
- let_it_be(:merge_request) { create(:merge_request) }
-
describe '#execute' do
subject(:execute) { run_checks.execute }
+ let_it_be(:merge_request) { create(:merge_request) }
+
let(:params) { {} }
let(:success_result) { Gitlab::MergeRequests::Mergeability::CheckResult.success }
@@ -23,7 +23,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
end
it 'is still a success' do
- expect(execute.all?(&:success?)).to eq(true)
+ expect(execute.success?).to eq(true)
end
end
@@ -41,13 +41,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
expect(service).not_to receive(:execute)
end
- # Since we're only marking one check to be skipped, we expect to receive
- # `# of checks - 1` success result objects in return
- #
- check_count = merge_request.mergeability_checks.count - 1
- success_array = (1..check_count).each_with_object([]) { |_, array| array << success_result }
-
- expect(execute).to match_array(success_array)
+ expect(execute.success?).to eq(true)
end
end
@@ -75,7 +69,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
expect(service).to receive(:read).with(merge_check: merge_check).and_return(success_result)
end
- expect(execute).to match_array([success_result])
+ expect(execute.success?).to eq(true)
end
end
@@ -86,7 +80,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
expect(service).to receive(:write).with(merge_check: merge_check, result_hash: success_result.to_hash).and_return(true)
end
- expect(execute).to match_array([success_result])
+ expect(execute.success?).to eq(true)
end
end
end
@@ -97,7 +91,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
it 'does not call the results store' do
expect(Gitlab::MergeRequests::Mergeability::ResultsStore).not_to receive(:new)
- expect(execute).to match_array([success_result])
+ expect(execute.success?).to eq(true)
end
end
@@ -109,9 +103,81 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
it 'does not call the results store' do
expect(Gitlab::MergeRequests::Mergeability::ResultsStore).not_to receive(:new)
- expect(execute).to match_array([success_result])
+ expect(execute.success?).to eq(true)
end
end
end
end
+
+ describe '#success?' do
+ subject(:success) { run_checks.success? }
+
+ let_it_be(:merge_request) { create(:merge_request) }
+
+ context 'when the execute method has been executed' do
+ before do
+ run_checks.execute
+ end
+
+ context 'when all the checks succeed' do
+ it 'returns true' do
+ expect(success).to eq(true)
+ end
+ end
+
+ context 'when one check fails' do
+ before do
+ allow(merge_request).to receive(:open?).and_return(false)
+ run_checks.execute
+ end
+
+ it 'returns false' do
+ expect(success).to eq(false)
+ end
+ end
+ end
+
+ context 'when execute has not been exectued' do
+ it 'raises an error' do
+ expect { subject }
+ .to raise_error(/Execute needs to be called before/)
+ end
+ end
+ end
+
+ describe '#failure_reason' do
+ subject(:failure_reason) { run_checks.failure_reason }
+
+ let_it_be(:merge_request) { create(:merge_request) }
+
+ context 'when the execute method has been executed' do
+ before do
+ run_checks.execute
+ end
+
+ context 'when all the checks succeed' do
+ it 'returns nil' do
+ expect(failure_reason).to eq(nil)
+ end
+ end
+
+ context 'when one check fails' do
+ before do
+ allow(merge_request).to receive(:open?).and_return(false)
+ run_checks.execute
+ end
+
+ it 'returns the open reason' do
+ expect(failure_reason).to eq(:not_open)
+ end
+ end
+ end
+
+ context 'when execute has not been exectued' do
+ it 'raises an error' do
+ expect { subject }
+ .to raise_error(/Execute needs to be called before/)
+ end
+ end
+ end
end
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 338057f23d5..391377ad801 100644
--- a/spec/services/merge_requests/push_options_handler_service_spec.rb
+++ b/spec/services/merge_requests/push_options_handler_service_spec.rb
@@ -179,7 +179,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
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)}
+ 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
@@ -231,7 +231,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
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)}
+ 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'
@@ -284,7 +284,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
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)}
+ 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 remove the source branch when it is merged'
@@ -337,7 +337,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
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)}
+ 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'
@@ -390,7 +390,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
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)}
+ 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 title of a merge request'
@@ -443,7 +443,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
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)}
+ 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 description of a merge request'
@@ -503,7 +503,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
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)}
+ 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 draft of a merge request'
@@ -564,7 +564,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
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)}
+ 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 change labels of a merge request', 2
@@ -617,7 +617,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
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)}
+ 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 change labels of a merge request', 1
@@ -672,7 +672,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
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)}
+ 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 milestone of a merge request'
@@ -713,7 +713,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
shared_examples 'with an existing branch that has a merge request open in foss' do
let(:changes) { existing_branch_changes }
- let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)}
+ 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 change assignees of a merge request', 1
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 4b7dd84474a..09d06b8b2ab 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -185,7 +185,7 @@ RSpec.describe MergeRequests::RefreshService do
end
context 'when pipeline exists for the source branch' do
- let!(:pipeline) { create(:ci_empty_pipeline, ref: @merge_request.source_branch, project: @project, sha: @commits.first.sha)}
+ let!(:pipeline) { create(:ci_empty_pipeline, ref: @merge_request.source_branch, project: @project, sha: @commits.first.sha) }
subject { service.new(project: @project, current_user: @user).execute(@oldrev, @newrev, 'refs/heads/master') }
diff --git a/spec/services/merge_requests/remove_attention_requested_service_spec.rb b/spec/services/merge_requests/remove_attention_requested_service_spec.rb
deleted file mode 100644
index 576049b9f1b..00000000000
--- a/spec/services/merge_requests/remove_attention_requested_service_spec.rb
+++ /dev/null
@@ -1,183 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe MergeRequests::RemoveAttentionRequestedService do
- let_it_be(:current_user) { create(:user) }
- let_it_be(:user) { create(:user) }
- let_it_be(:assignee_user) { create(:user) }
- let_it_be(:merge_request) { create(:merge_request, reviewers: [user], assignees: [assignee_user]) }
-
- let(:reviewer) { merge_request.find_reviewer(user) }
- let(:assignee) { merge_request.find_assignee(assignee_user) }
- let(:project) { merge_request.project }
-
- let(:service) do
- described_class.new(
- project: project,
- current_user: current_user,
- merge_request: merge_request,
- user: user
- )
- end
-
- let(:result) { service.execute }
-
- before do
- allow(SystemNoteService).to receive(:remove_attention_request)
-
- project.add_developer(current_user)
- project.add_developer(user)
- end
-
- describe '#execute' do
- context 'when current user cannot update merge request' do
- let(:service) do
- described_class.new(
- project: project,
- current_user: create(:user),
- merge_request: merge_request,
- user: user
- )
- end
-
- it 'returns an error' do
- expect(result[:status]).to eq :error
- end
- end
-
- context 'when user is not a reviewer nor assignee' do
- let(:service) do
- described_class.new(
- project: project,
- current_user: current_user,
- merge_request: merge_request,
- user: create(:user)
- )
- end
-
- it 'returns an error' do
- expect(result[:status]).to eq :error
- end
- end
-
- context 'when user is a reviewer' do
- before do
- reviewer.update!(state: :attention_requested)
- end
-
- it 'returns success' do
- expect(result[:status]).to eq :success
- end
-
- it 'updates reviewer state' do
- service.execute
- reviewer.reload
-
- expect(reviewer.state).to eq 'reviewed'
- end
-
- it 'creates a remove attention request system note' do
- expect(SystemNoteService)
- .to receive(:remove_attention_request)
- .with(merge_request, merge_request.project, current_user, user)
-
- service.execute
- end
-
- it_behaves_like 'invalidates attention request cache' do
- let(:users) { [user] }
- end
- end
-
- context 'when user is an assignee' do
- let(:service) do
- described_class.new(
- project: project,
- current_user: current_user,
- merge_request: merge_request,
- user: assignee_user
- )
- end
-
- before do
- assignee.update!(state: :attention_requested)
- end
-
- it 'returns success' do
- expect(result[:status]).to eq :success
- end
-
- it 'updates assignee state' do
- service.execute
- assignee.reload
-
- expect(assignee.state).to eq 'reviewed'
- end
-
- it_behaves_like 'invalidates attention request cache' do
- let(:users) { [assignee_user] }
- end
-
- it 'creates a remove attention request system note' do
- expect(SystemNoteService)
- .to receive(:remove_attention_request)
- .with(merge_request, merge_request.project, current_user, assignee_user)
-
- service.execute
- end
- end
-
- context 'when user is an assignee and reviewer at the same time' do
- let_it_be(:merge_request) { create(:merge_request, reviewers: [user], assignees: [user]) }
-
- let(:assignee) { merge_request.find_assignee(user) }
-
- let(:service) do
- described_class.new(
- project: project,
- current_user: current_user,
- merge_request: merge_request,
- user: user
- )
- end
-
- before do
- reviewer.update!(state: :attention_requested)
- assignee.update!(state: :attention_requested)
- end
-
- it 'returns success' do
- expect(result[:status]).to eq :success
- end
-
- it 'updates reviewers and assignees state' do
- service.execute
- reviewer.reload
- assignee.reload
-
- expect(reviewer.state).to eq 'reviewed'
- expect(assignee.state).to eq 'reviewed'
- end
- end
-
- context 'when state is already not attention_requested' do
- before do
- reviewer.update!(state: :reviewed)
- end
-
- it 'does not change state' do
- service.execute
- reviewer.reload
-
- expect(reviewer.state).to eq 'reviewed'
- end
-
- it 'does not create a remove attention request system note' do
- expect(SystemNoteService).not_to receive(:remove_attention_request)
-
- service.execute
- end
- end
- end
-end
diff --git a/spec/services/merge_requests/request_attention_service_spec.rb b/spec/services/merge_requests/request_attention_service_spec.rb
deleted file mode 100644
index 813a8150625..00000000000
--- a/spec/services/merge_requests/request_attention_service_spec.rb
+++ /dev/null
@@ -1,220 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe MergeRequests::RequestAttentionService do
- let_it_be(:current_user) { create(:user) }
- let_it_be(:user) { create(:user) }
- let_it_be(:assignee_user) { create(:user) }
- let_it_be(:merge_request) { create(:merge_request, reviewers: [user], assignees: [assignee_user]) }
-
- let(:reviewer) { merge_request.find_reviewer(user) }
- let(:assignee) { merge_request.find_assignee(assignee_user) }
- let(:project) { merge_request.project }
-
- let(:service) do
- described_class.new(
- project: project,
- current_user: current_user,
- merge_request: merge_request,
- user: user
- )
- end
-
- let(:result) { service.execute }
- let(:todo_svc) { instance_double('TodoService') }
- let(:notification_svc) { instance_double('NotificationService') }
-
- before do
- allow(service).to receive(:todo_service).and_return(todo_svc)
- allow(service).to receive(:notification_service).and_return(notification_svc)
- allow(todo_svc).to receive(:create_attention_requested_todo)
- allow(notification_svc).to receive_message_chain(:async, :attention_requested_of_merge_request)
- allow(SystemNoteService).to receive(:request_attention)
-
- project.add_developer(current_user)
- project.add_developer(user)
- end
-
- describe '#execute' do
- context 'when current user cannot update merge request' do
- let(:service) do
- described_class.new(
- project: project,
- current_user: create(:user),
- merge_request: merge_request,
- user: user
- )
- end
-
- it 'returns an error' do
- expect(result[:status]).to eq :error
- end
- end
-
- context 'when user is not a reviewer nor assignee' do
- let(:service) do
- described_class.new(
- project: project,
- current_user: current_user,
- merge_request: merge_request,
- user: create(:user)
- )
- end
-
- it 'returns an error' do
- expect(result[:status]).to eq :error
- end
- end
-
- context 'when user is a reviewer' do
- before do
- reviewer.update!(state: :reviewed)
- end
-
- it 'returns success' do
- expect(result[:status]).to eq :success
- end
-
- it 'updates reviewers state' do
- service.execute
- reviewer.reload
-
- expect(reviewer.state).to eq 'attention_requested'
- end
-
- it 'adds who toggled attention' do
- service.execute
- reviewer.reload
-
- expect(reviewer.updated_state_by).to eq current_user
- end
-
- it 'creates a new todo for the reviewer' do
- expect(todo_svc).to receive(:create_attention_requested_todo).with(merge_request, current_user, user)
-
- service.execute
- end
-
- it 'sends email to reviewer' do
- expect(notification_svc)
- .to receive_message_chain(:async, :attention_requested_of_merge_request)
- .with(merge_request, current_user, user)
-
- service.execute
- end
-
- it 'removes attention requested state' do
- expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new)
- .with(project: project, current_user: current_user, merge_request: merge_request, user: current_user)
- .and_call_original
-
- service.execute
- end
-
- it_behaves_like 'invalidates attention request cache' do
- let(:users) { [user] }
- end
- end
-
- context 'when user is an assignee' do
- let(:service) do
- described_class.new(
- project: project,
- current_user: current_user,
- merge_request: merge_request,
- user: assignee_user
- )
- end
-
- before do
- assignee.update!(state: :reviewed)
- end
-
- it 'returns success' do
- expect(result[:status]).to eq :success
- end
-
- it 'updates assignees state' do
- service.execute
- assignee.reload
-
- expect(assignee.state).to eq 'attention_requested'
- end
-
- it 'creates a new todo for the reviewer' do
- expect(todo_svc).to receive(:create_attention_requested_todo).with(merge_request, current_user, assignee_user)
-
- service.execute
- end
-
- it 'creates a request attention system note' do
- expect(SystemNoteService)
- .to receive(:request_attention)
- .with(merge_request, merge_request.project, current_user, assignee_user)
-
- service.execute
- end
-
- it 'removes attention requested state' do
- expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new)
- .with(project: project, current_user: current_user, merge_request: merge_request, user: current_user)
- .and_call_original
-
- service.execute
- end
-
- it_behaves_like 'invalidates attention request cache' do
- let(:users) { [assignee_user] }
- end
- end
-
- context 'when user is an assignee and reviewer at the same time' do
- let_it_be(:merge_request) { create(:merge_request, reviewers: [user], assignees: [user]) }
-
- let(:assignee) { merge_request.find_assignee(user) }
-
- let(:service) do
- described_class.new(
- project: project,
- current_user: current_user,
- merge_request: merge_request,
- user: user
- )
- end
-
- before do
- reviewer.update!(state: :reviewed)
- assignee.update!(state: :reviewed)
- end
-
- it 'updates reviewers and assignees state' do
- service.execute
- reviewer.reload
- assignee.reload
-
- expect(reviewer.state).to eq 'attention_requested'
- expect(assignee.state).to eq 'attention_requested'
- end
- end
-
- context 'when state is attention_requested' do
- before do
- reviewer.update!(state: :attention_requested)
- end
-
- it 'does not change state' do
- service.execute
- reviewer.reload
-
- expect(reviewer.state).to eq 'attention_requested'
- end
-
- it 'does not create a new todo for the reviewer' do
- expect(todo_svc).not_to receive(:create_attention_requested_todo).with(merge_request, current_user, user)
-
- service.execute
- end
- end
- end
-end
diff --git a/spec/services/merge_requests/toggle_attention_requested_service_spec.rb b/spec/services/merge_requests/toggle_attention_requested_service_spec.rb
deleted file mode 100644
index 20bc536b21e..00000000000
--- a/spec/services/merge_requests/toggle_attention_requested_service_spec.rb
+++ /dev/null
@@ -1,188 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe MergeRequests::ToggleAttentionRequestedService do
- let(:current_user) { create(:user) }
- let(:user) { create(:user) }
- let(:assignee_user) { create(:user) }
- let(:merge_request) { create(:merge_request, reviewers: [user], assignees: [assignee_user]) }
- let(:reviewer) { merge_request.find_reviewer(user) }
- let(:assignee) { merge_request.find_assignee(assignee_user) }
- let(:project) { merge_request.project }
- let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, user: user) }
- let(:result) { service.execute }
- let(:todo_service) { spy('todo service') }
- let(:notification_service) { spy('notification service') }
-
- before do
- allow(NotificationService).to receive(:new) { notification_service }
- allow(service).to receive(:todo_service).and_return(todo_service)
- allow(service).to receive(:notification_service).and_return(notification_service)
- allow(SystemNoteService).to receive(:request_attention)
- allow(SystemNoteService).to receive(:remove_attention_request)
-
- project.add_developer(current_user)
- project.add_developer(user)
- end
-
- describe '#execute' do
- context 'invalid permissions' do
- let(:service) { described_class.new(project: project, current_user: create(:user), merge_request: merge_request, user: user) }
-
- it 'returns an error' do
- expect(result[:status]).to eq :error
- end
- end
-
- context 'reviewer does not exist' do
- let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, user: create(:user)) }
-
- it 'returns an error' do
- expect(result[:status]).to eq :error
- end
- end
-
- context 'reviewer exists' do
- before do
- reviewer.update!(state: :reviewed)
- end
-
- it 'returns success' do
- expect(result[:status]).to eq :success
- end
-
- it 'updates reviewers state' do
- service.execute
- reviewer.reload
-
- expect(reviewer.state).to eq 'attention_requested'
- end
-
- it 'adds who toggled attention' do
- service.execute
- reviewer.reload
-
- expect(reviewer.updated_state_by).to eq current_user
- end
-
- it 'creates a new todo for the reviewer' do
- expect(todo_service).to receive(:create_attention_requested_todo).with(merge_request, current_user, user)
-
- service.execute
- end
-
- it 'sends email to reviewer' do
- expect(notification_service).to receive_message_chain(:async, :attention_requested_of_merge_request).with(merge_request, current_user, user)
-
- service.execute
- end
-
- it 'removes attention requested state' do
- expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new)
- .with(project: project, current_user: current_user, merge_request: merge_request, user: current_user)
- .and_call_original
-
- service.execute
- end
-
- it 'invalidates cache' do
- cache_mock = double
-
- expect(cache_mock).to receive(:delete).with(['users', user.id, 'attention_requested_open_merge_requests_count'])
-
- allow(Rails).to receive(:cache).and_return(cache_mock)
-
- service.execute
- end
- end
-
- context 'assignee exists' do
- let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, user: assignee_user) }
-
- before do
- assignee.update!(state: :reviewed)
- end
-
- it 'returns success' do
- expect(result[:status]).to eq :success
- end
-
- it 'updates assignees state' do
- service.execute
- assignee.reload
-
- expect(assignee.state).to eq 'attention_requested'
- end
-
- it 'creates a new todo for the reviewer' do
- expect(todo_service).to receive(:create_attention_requested_todo).with(merge_request, current_user, assignee_user)
-
- service.execute
- end
-
- it 'creates a request attention system note' do
- expect(SystemNoteService).to receive(:request_attention).with(merge_request, merge_request.project, current_user, assignee_user)
-
- service.execute
- end
-
- it 'removes attention requested state' do
- expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new)
- .with(project: project, current_user: current_user, merge_request: merge_request, user: current_user)
- .and_call_original
-
- service.execute
- end
-
- it_behaves_like 'invalidates attention request cache' do
- let(:users) { [assignee_user] }
- end
- end
-
- context 'assignee is the same as reviewer' do
- let(:merge_request) { create(:merge_request, reviewers: [user], assignees: [user]) }
- let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, user: user) }
- let(:assignee) { merge_request.find_assignee(user) }
-
- before do
- reviewer.update!(state: :reviewed)
- assignee.update!(state: :reviewed)
- end
-
- it 'updates reviewers and assignees state' do
- service.execute
- reviewer.reload
- assignee.reload
-
- expect(reviewer.state).to eq 'attention_requested'
- expect(assignee.state).to eq 'attention_requested'
- end
- end
-
- context 'state is attention_requested' do
- before do
- reviewer.update!(state: :attention_requested)
- end
-
- it 'toggles state to reviewed' do
- service.execute
- reviewer.reload
-
- expect(reviewer.state).to eq "reviewed"
- end
-
- it 'does not create a new todo for the reviewer' do
- expect(todo_service).not_to receive(:create_attention_requested_todo).with(merge_request, current_user, assignee_user)
-
- service.execute
- end
-
- it 'creates a remove attention request system note' do
- expect(SystemNoteService).to receive(:remove_attention_request).with(merge_request, merge_request.project, current_user, user)
-
- service.execute
- end
- end
- end
-end
diff --git a/spec/services/merge_requests/update_reviewers_service_spec.rb b/spec/services/merge_requests/update_reviewers_service_spec.rb
new file mode 100644
index 00000000000..8920141adbb
--- /dev/null
+++ b/spec/services/merge_requests/update_reviewers_service_spec.rb
@@ -0,0 +1,162 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::UpdateReviewersService do
+ include AfterNextHelpers
+
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:project) { create(:project, :private, :repository, group: group) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:user3) { create(:user) }
+
+ let_it_be_with_reload(:merge_request) do
+ create(:merge_request, :simple, :unique_branches,
+ title: 'Old title',
+ description: "FYI #{user2.to_reference}",
+ reviewer_ids: [user3.id],
+ source_project: project,
+ target_project: project,
+ author: create(:user))
+ end
+
+ before do
+ project.add_maintainer(user)
+ project.add_developer(user2)
+ project.add_developer(user3)
+ merge_request.errors.clear
+ end
+
+ let(:service) { described_class.new(project: project, current_user: user, params: opts) }
+ let(:opts) { { reviewer_ids: [user2.id] } }
+
+ describe 'execute' do
+ def set_reviewers
+ service.execute(merge_request)
+ end
+
+ def find_note(starting_with)
+ merge_request.notes.find do |note|
+ note && note.note.start_with?(starting_with)
+ end
+ end
+
+ shared_examples 'removing all reviewers' do
+ it 'removes all reviewers' do
+ expect(set_reviewers).to have_attributes(reviewers: be_empty, errors: be_none)
+ end
+ end
+
+ context 'when the parameters are valid' do
+ context 'when using sentinel values' do
+ let(:opts) { { reviewer_ids: [0] } }
+
+ it_behaves_like 'removing all reviewers'
+ end
+
+ context 'when the reviewer_ids parameter is the empty list' do
+ let(:opts) { { reviewer_ids: [] } }
+
+ it_behaves_like 'removing all reviewers'
+ end
+
+ it 'updates the MR' do
+ expect { set_reviewers }
+ .to change { merge_request.reload.reviewers }.from([user3]).to([user2])
+ .and change(merge_request, :updated_at)
+ .and change(merge_request, :updated_by).to(user)
+ end
+
+ it 'creates system note about merge_request review request' do
+ set_reviewers
+
+ note = find_note('requested review from')
+
+ expect(note).not_to be_nil
+ expect(note.note).to include "requested review from #{user2.to_reference}"
+ end
+
+ it 'creates a pending todo for new review request' do
+ set_reviewers
+
+ attributes = {
+ project: project,
+ author: user,
+ user: user2,
+ target_id: merge_request.id,
+ target_type: merge_request.class.name,
+ action: Todo::REVIEW_REQUESTED,
+ state: :pending
+ }
+
+ expect(Todo.where(attributes).count).to eq 1
+ end
+
+ it 'sends email reviewer change notifications to old and new reviewers', :sidekiq_inline, :mailer do
+ perform_enqueued_jobs do
+ set_reviewers
+ end
+
+ should_email(user2)
+ should_email(user3)
+ end
+
+ it 'updates open merge request counter for reviewers', :use_clean_rails_memory_store_caching do
+ # Cache them to ensure the cache gets invalidated on update
+ expect(user2.review_requested_open_merge_requests_count).to eq(0)
+ expect(user3.review_requested_open_merge_requests_count).to eq(1)
+
+ set_reviewers
+
+ expect(user2.review_requested_open_merge_requests_count).to eq(1)
+ expect(user3.review_requested_open_merge_requests_count).to eq(0)
+ end
+
+ it 'updates the tracking' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_users_review_requested)
+ .with(users: [user2])
+
+ set_reviewers
+ end
+
+ it 'tracks reviewers changed event' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_reviewers_changed_action).once.with(user: user)
+
+ set_reviewers
+ end
+
+ it 'calls MergeRequest::ResolveTodosService#async_execute' do
+ expect_next_instance_of(MergeRequests::ResolveTodosService, merge_request, user) do |service|
+ expect(service).to receive(:async_execute)
+ end
+
+ set_reviewers
+ end
+
+ it 'executes hooks with update action' do
+ expect(service).to receive(:execute_hooks)
+ .with(
+ merge_request,
+ 'update',
+ old_associations: {
+ reviewers: [user3]
+ }
+ )
+
+ set_reviewers
+ end
+
+ it 'does not update the reviewers if they do not have access' do
+ opts[:reviewer_ids] = [create(:user).id]
+
+ expect(set_reviewers).to have_attributes(
+ reviewers: [user3],
+ errors: be_any
+ )
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 212f75d853f..b7fb48718d8 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -91,7 +91,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
context 'usage counters' do
let(:merge_request2) { create(:merge_request) }
- let(:draft_merge_request) { create(:merge_request, :draft_merge_request)}
+ let(:draft_merge_request) { create(:merge_request, :draft_merge_request) }
it 'update as expected' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
@@ -980,8 +980,8 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
end
it 'creates system note about task status change' do
- note1 = find_note('marked the task **Task 1** as completed')
- note2 = find_note('marked the task **Task 2** as completed')
+ note1 = find_note('marked the checklist item **Task 1** as completed')
+ note2 = find_note('marked the checklist item **Task 2** as completed')
expect(note1).not_to be_nil
expect(note2).not_to be_nil
@@ -998,8 +998,8 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
end
it 'creates system note about task status change' do
- note1 = find_note('marked the task **Task 1** as incomplete')
- note2 = find_note('marked the task **Task 2** as incomplete')
+ note1 = find_note('marked the checklist item **Task 1** as incomplete')
+ note2 = find_note('marked the checklist item **Task 2** as incomplete')
expect(note1).not_to be_nil
expect(note2).not_to be_nil
diff --git a/spec/services/milestones/transfer_service_spec.rb b/spec/services/milestones/transfer_service_spec.rb
index afbc9c7dca2..b15d90d685c 100644
--- a/spec/services/milestones/transfer_service_spec.rb
+++ b/spec/services/milestones/transfer_service_spec.rb
@@ -11,9 +11,9 @@ RSpec.describe Milestones::TransferService do
let(:new_group) { create(:group) }
let(:old_group) { create(:group) }
let(:project) { create(:project, namespace: old_group) }
- let(:group_milestone) { create(:milestone, group: old_group)}
- let(:group_milestone2) { create(:milestone, group: old_group)}
- let(:project_milestone) { create(:milestone, project: project)}
+ let(:group_milestone) { create(:milestone, group: old_group) }
+ let(:group_milestone2) { create(:milestone, group: old_group) }
+ let(:project_milestone) { create(:milestone, project: project) }
let!(:issue_with_group_milestone) { create(:issue, project: project, milestone: group_milestone) }
let!(:issue_with_project_milestone) { create(:issue, project: project, milestone: project_milestone) }
let!(:mr_with_group_milestone) { create(:merge_request, source_project: project, source_branch: 'branch-1', milestone: group_milestone) }
@@ -43,7 +43,7 @@ RSpec.describe Milestones::TransferService do
context 'when milestone is from an ancestor group' do
let(:old_group_ancestor) { create(:group) }
let(:old_group) { create(:group, parent: old_group_ancestor) }
- let(:group_milestone) { create(:milestone, group: old_group_ancestor)}
+ let(:group_milestone) { create(:milestone, group: old_group_ancestor) }
it 'recreates the missing group milestones at project level' do
expect { service.execute }.to change(project.milestones, :count).by(1)
diff --git a/spec/services/notes/build_service_spec.rb b/spec/services/notes/build_service_spec.rb
index 0e2bbcc8c66..c25895d2efa 100644
--- a/spec/services/notes/build_service_spec.rb
+++ b/spec/services/notes/build_service_spec.rb
@@ -170,7 +170,7 @@ RSpec.describe Notes::BuildService do
end
context 'when creating a new confidential comment' do
- let(:params) { { confidential: true, noteable: issue } }
+ let(:params) { { internal: true, noteable: issue } }
shared_examples 'user allowed to set comment as confidential' do
it { expect(new_note.confidential).to be_truthy }
@@ -219,6 +219,14 @@ RSpec.describe Notes::BuildService do
it_behaves_like 'user not allowed to set comment as confidential'
end
+
+ context 'when using the deprecated `confidential` parameter' do
+ let(:params) { { internal: true, noteable: issue } }
+
+ shared_examples 'user allowed to set comment as confidential' do
+ it { expect(new_note.confidential).to be_truthy }
+ end
+ end
end
context 'when replying to a confidential comment' do
diff --git a/spec/services/notes/copy_service_spec.rb b/spec/services/notes/copy_service_spec.rb
index fd8802e6640..f146a49e929 100644
--- a/spec/services/notes/copy_service_spec.rb
+++ b/spec/services/notes/copy_service_spec.rb
@@ -138,7 +138,7 @@ RSpec.describe Notes::CopyService do
context 'notes with upload' do
let(:uploader) { build(:file_uploader, project: from_noteable.project) }
- let(:text) { "Simple text with image: #{uploader.markdown_link} "}
+ let(:text) { "Simple text with image: #{uploader.markdown_link} " }
let!(:note) { create(:note, noteable: from_noteable, note: text, project: from_noteable.project) }
it 'rewrites note content correctly' do
@@ -146,8 +146,8 @@ RSpec.describe Notes::CopyService do
new_note = to_noteable.notes.first
aggregate_failures do
- expect(note.note).to match(/Simple text with image: #{FileUploader::MARKDOWN_PATTERN}/)
- expect(new_note.note).to match(/Simple text with image: #{FileUploader::MARKDOWN_PATTERN}/)
+ expect(note.note).to match(/Simple text with image: #{FileUploader::MARKDOWN_PATTERN}/o)
+ expect(new_note.note).to match(/Simple text with image: #{FileUploader::MARKDOWN_PATTERN}/o)
expect(note.note).not_to eq(new_note.note)
expect(note.note_html).not_to eq(new_note.note_html)
end
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 53b75a3c991..37318d76586 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -7,37 +7,74 @@ RSpec.describe Notes::CreateService do
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:user) { create(:user) }
- let(:opts) do
- { note: 'Awesome comment', noteable_type: 'Issue', noteable_id: issue.id, confidential: true }
- end
+ let(:base_opts) { { note: 'Awesome comment', noteable_type: 'Issue', noteable_id: issue.id } }
+ let(:opts) { base_opts.merge(confidential: true) }
describe '#execute' do
+ subject(:note) { described_class.new(project, user, opts).execute }
+
before do
project.add_maintainer(user)
end
context "valid params" do
it 'returns a valid note' do
- note = described_class.new(project, user, opts).execute
-
expect(note).to be_valid
end
it 'returns a persisted note' do
- note = described_class.new(project, user, opts).execute
-
expect(note).to be_persisted
end
- it 'note has valid content' do
- note = described_class.new(project, user, opts).execute
+ context 'with internal parameter' do
+ context 'when confidential' do
+ let(:opts) { base_opts.merge(internal: true) }
+
+ it 'returns a confidential note' do
+ expect(note).to be_confidential
+ end
+ end
+
+ context 'when not confidential' do
+ let(:opts) { base_opts.merge(internal: false) }
+
+ it 'returns a confidential note' do
+ expect(note).not_to be_confidential
+ end
+ end
+ end
+
+ context 'with confidential parameter' do
+ context 'when confidential' do
+ let(:opts) { base_opts.merge(confidential: true) }
+
+ it 'returns a confidential note' do
+ expect(note).to be_confidential
+ end
+ end
+
+ context 'when not confidential' do
+ let(:opts) { base_opts.merge(confidential: false) }
+ it 'returns a confidential note' do
+ expect(note).not_to be_confidential
+ end
+ end
+ end
+
+ context 'with confidential and internal parameter set' do
+ let(:opts) { base_opts.merge(internal: true, confidential: false) }
+
+ it 'prefers the internal parameter' do
+ expect(note).to be_confidential
+ end
+ end
+
+ it 'note has valid content' do
expect(note.note).to eq(opts[:note])
end
it 'note belongs to the correct project' do
- note = described_class.new(project, user, opts).execute
-
expect(note.project).to eq(project)
end
@@ -60,8 +97,6 @@ RSpec.describe Notes::CreateService do
end
context 'issue is an incident' do
- subject { described_class.new(project, user, opts).execute }
-
let(:issue) { create(:incident, project: project) }
it_behaves_like 'an incident management tracked event', :incident_management_incident_comment do
@@ -69,20 +104,31 @@ RSpec.describe Notes::CreateService do
end
end
- it 'tracks issue comment usage data', :clean_gitlab_redis_shared_state do
- event = Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_COMMENT_ADDED
- counter = Gitlab::UsageDataCounters::HLLRedisCounter
+ describe 'event tracking', :snowplow do
+ let(:event) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_COMMENT_ADDED }
+ let(:execute_create_service) { described_class.new(project, user, opts).execute }
- expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_comment_added_action).with(author: user).and_call_original
- expect do
- described_class.new(project, user, opts).execute
- end.to change { counter.unique_events(event_names: event, start_date: 1.day.ago, end_date: 1.day.from_now) }.by(1)
- end
+ it 'tracks issue comment usage data', :clean_gitlab_redis_shared_state do
+ counter = Gitlab::UsageDataCounters::HLLRedisCounter
+
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_comment_added_action)
+ .with(author: user, project: project)
+ .and_call_original
+ expect do
+ execute_create_service
+ end.to change { counter.unique_events(event_names: event, start_date: 1.day.ago, end_date: 1.day.from_now) }.by(1)
+ end
- it 'does not track merge request usage data' do
- expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter).not_to receive(:track_create_comment_action)
+ it 'does not track merge request usage data' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter).not_to receive(:track_create_comment_action)
- described_class.new(project, user, opts).execute
+ execute_create_service
+ end
+
+ it_behaves_like 'issue_edit snowplow tracking' do
+ let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_COMMENT_ADDED }
+ subject(:service_action) { execute_create_service }
+ end
end
context 'in a merge request' do
diff --git a/spec/services/notes/destroy_service_spec.rb b/spec/services/notes/destroy_service_spec.rb
index 55acdabef82..be95a4bb181 100644
--- a/spec/services/notes/destroy_service_spec.rb
+++ b/spec/services/notes/destroy_service_spec.rb
@@ -25,15 +25,25 @@ RSpec.describe Notes::DestroyService do
.to change { user.todos_pending_count }.from(1).to(0)
end
- it 'tracks issue comment removal usage data', :clean_gitlab_redis_shared_state do
- note = create(:note, project: project, noteable: issue)
- event = Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_COMMENT_REMOVED
- counter = Gitlab::UsageDataCounters::HLLRedisCounter
+ describe 'comment removed event tracking', :snowplow do
+ let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_COMMENT_REMOVED }
+ let(:note) { create(:note, project: project, noteable: issue) }
+ let(:service_action) { described_class.new(project, user).execute(note) }
+
+ it 'tracks issue comment removal usage data', :clean_gitlab_redis_shared_state do
+ counter = Gitlab::UsageDataCounters::HLLRedisCounter
+
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_comment_removed_action)
+ .with(author: user, project: project)
+ .and_call_original
+ expect do
+ service_action
+ end.to change { counter.unique_events(event_names: property, start_date: 1.day.ago, end_date: 1.day.from_now) }.by(1)
+ end
- expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_comment_removed_action).with(author: user).and_call_original
- expect do
- described_class.new(project, user).execute(note)
- end.to change { counter.unique_events(event_names: event, start_date: 1.day.ago, end_date: 1.day.from_now) }.by(1)
+ it_behaves_like 'issue_edit snowplow tracking' do
+ subject(:execute_service_action) { service_action }
+ end
end
it 'tracks merge request usage data' do
diff --git a/spec/services/notes/update_service_spec.rb b/spec/services/notes/update_service_spec.rb
index ae7bea30944..989ca7b8df1 100644
--- a/spec/services/notes/update_service_spec.rb
+++ b/spec/services/notes/update_service_spec.rb
@@ -47,21 +47,31 @@ RSpec.describe Notes::UpdateService do
end
end
- it 'does not track usage data when params is blank' do
- expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_comment_edited_action)
- expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter).not_to receive(:track_edit_comment_action)
+ describe 'event tracking', :snowplow do
+ let(:event) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_COMMENT_EDITED }
- update_note({})
- end
+ it 'does not track usage data when params is blank' do
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_comment_edited_action)
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter).not_to receive(:track_edit_comment_action)
- it 'tracks issue usage data', :clean_gitlab_redis_shared_state do
- event = Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_COMMENT_EDITED
- counter = Gitlab::UsageDataCounters::HLLRedisCounter
+ update_note({})
+ end
- expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_comment_edited_action).with(author: user).and_call_original
- expect do
- update_note(note: 'new text')
- end.to change { counter.unique_events(event_names: event, start_date: 1.day.ago, end_date: 1.day.from_now) }.by(1)
+ it 'tracks issue usage data', :clean_gitlab_redis_shared_state do
+ counter = Gitlab::UsageDataCounters::HLLRedisCounter
+
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_comment_edited_action)
+ .with(author: user, project: project)
+ .and_call_original
+ expect do
+ update_note(note: 'new text')
+ end.to change { counter.unique_events(event_names: event, start_date: 1.day.ago, end_date: 1.day.from_now) }.by(1)
+ end
+
+ it_behaves_like 'issue_edit snowplow tracking' do
+ let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_COMMENT_EDITED }
+ subject(:service_action) { update_note(note: 'new text') }
+ end
end
context 'when note text was changed' do
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 98fe8a40c61..935dcef1011 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -2006,19 +2006,19 @@ RSpec.describe NotificationService, :mailer do
context 'participating' do
it_behaves_like 'participating by assignee notification' do
- let(:participant) { create(:user, username: 'user-participant')}
+ let(:participant) { create(:user, username: 'user-participant') }
let(:issuable) { merge_request }
let(:notification_trigger) { notification.new_merge_request(merge_request, @u_disabled) }
end
it_behaves_like 'participating by note notification' do
- let(:participant) { create(:user, username: 'user-participant')}
+ let(:participant) { create(:user, username: 'user-participant') }
let(:issuable) { merge_request }
let(:notification_trigger) { notification.new_merge_request(merge_request, @u_disabled) }
end
context 'by author' do
- let(:participant) { create(:user, username: 'user-participant')}
+ let(:participant) { create(:user, username: 'user-participant') }
before do
merge_request.author = participant
@@ -2657,45 +2657,6 @@ RSpec.describe NotificationService, :mailer do
let(:notification_trigger) { notification.review_requested_of_merge_request(merge_request, current_user, reviewer) }
end
end
-
- describe '#attention_requested_of_merge_request' do
- let_it_be(:current_user) { create(:user) }
- let_it_be(:reviewer) { create(:user) }
- let_it_be(:merge_request) { create(:merge_request, source_project: project, reviewers: [reviewer]) }
-
- it 'sends email to reviewer', :aggregate_failures do
- notification.attention_requested_of_merge_request(merge_request, current_user, reviewer)
-
- merge_request.reviewers.each { |reviewer| should_email(reviewer) }
- should_not_email(merge_request.author)
- should_not_email(@u_watcher)
- should_not_email(@u_participant_mentioned)
- should_not_email(@subscriber)
- should_not_email(@watcher_and_subscriber)
- should_not_email(@u_guest_watcher)
- should_not_email(@u_guest_custom)
- should_not_email(@u_custom_global)
- should_not_email(@unsubscriber)
- should_not_email(@u_participating)
- should_not_email(@u_disabled)
- should_not_email(@u_lazy_participant)
- end
-
- it 'adds "attention requested" reason' do
- notification.attention_requested_of_merge_request(merge_request, current_user, [reviewer])
-
- merge_request.reviewers.each do |reviewer|
- email = find_email_for(reviewer)
-
- expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ATTENTION_REQUESTED)
- end
- end
-
- it_behaves_like 'project emails are disabled' do
- let(:notification_target) { merge_request }
- let(:notification_trigger) { notification.attention_requested_of_merge_request(merge_request, current_user, reviewer) }
- end
- end
end
describe 'Projects', :deliver_mails_inline do
diff --git a/spec/services/packages/composer/create_package_service_spec.rb b/spec/services/packages/composer/create_package_service_spec.rb
index b04a6c8382f..26429a7b5d9 100644
--- a/spec/services/packages/composer/create_package_service_spec.rb
+++ b/spec/services/packages/composer/create_package_service_spec.rb
@@ -88,7 +88,7 @@ RSpec.describe Packages::Composer::CreatePackageService do
end
context 'belonging to another project' do
- let(:other_project) { create(:project)}
+ let(:other_project) { create(:project) }
let!(:other_package) { create(:composer_package, name: package_name, version: 'dev-master', project: other_project) }
it 'fails with an error' do
diff --git a/spec/services/packages/create_dependency_service_spec.rb b/spec/services/packages/create_dependency_service_spec.rb
index 55414ea68fe..f95e21cd045 100644
--- a/spec/services/packages/create_dependency_service_spec.rb
+++ b/spec/services/packages/create_dependency_service_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
RSpec.describe Packages::CreateDependencyService do
describe '#execute' do
- let_it_be(:namespace) {create(:namespace)}
+ let_it_be(:namespace) { create(:namespace) }
let_it_be(:version) { '1.0.1' }
let_it_be(:package_name) { "@#{namespace.path}/my-app" }
diff --git a/spec/services/packages/debian/extract_deb_metadata_service_spec.rb b/spec/services/packages/debian/extract_deb_metadata_service_spec.rb
index ee3f3d179dc..66a9ca5f9e0 100644
--- a/spec/services/packages/debian/extract_deb_metadata_service_spec.rb
+++ b/spec/services/packages/debian/extract_deb_metadata_service_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Packages::Debian::ExtractDebMetadataService do
let(:file_name) { 'README.md' }
it 'raise error' do
- expect {subject.execute}.to raise_error(described_class::CommandFailedError, /is not a Debian format archive/i)
+ expect { subject.execute }.to raise_error(described_class::CommandFailedError, /is not a Debian format archive/i)
end
end
end
diff --git a/spec/services/packages/debian/extract_metadata_service_spec.rb b/spec/services/packages/debian/extract_metadata_service_spec.rb
index e3911dbbfe0..02c81ad1644 100644
--- a/spec/services/packages/debian/extract_metadata_service_spec.rb
+++ b/spec/services/packages/debian/extract_metadata_service_spec.rb
@@ -11,15 +11,10 @@ RSpec.describe Packages::Debian::ExtractMetadataService do
end
RSpec.shared_examples 'Test Debian ExtractMetadata Service' do |expected_file_type, expected_architecture, expected_fields|
- it "returns file_type #{expected_file_type.inspect}" do
+ it "returns file_type #{expected_file_type.inspect}, architecture #{expected_architecture.inspect} and fields #{expected_fields.nil? ? '' : 'including '}#{expected_fields.inspect}", :aggregate_failures do
expect(subject[:file_type]).to eq(expected_file_type)
- end
-
- it "returns architecture #{expected_architecture.inspect}" do
expect(subject[:architecture]).to eq(expected_architecture)
- end
- it "returns fields #{expected_fields.nil? ? '' : 'including '}#{expected_fields.inspect}" do
if expected_fields.nil?
expect(subject[:fields]).to be_nil
else
diff --git a/spec/services/packages/debian/parse_debian822_service_spec.rb b/spec/services/packages/debian/parse_debian822_service_spec.rb
index cad4e81f350..ff146fda250 100644
--- a/spec/services/packages/debian/parse_debian822_service_spec.rb
+++ b/spec/services/packages/debian/parse_debian822_service_spec.rb
@@ -102,7 +102,7 @@ RSpec.describe Packages::Debian::ParseDebian822Service do
let(:input) { ' continuation' }
it 'raise error' do
- expect {subject.execute}.to raise_error(described_class::InvalidDebian822Error, 'Parse error. Unexpected continuation line')
+ expect { subject.execute }.to raise_error(described_class::InvalidDebian822Error, 'Parse error. Unexpected continuation line')
end
end
@@ -116,7 +116,7 @@ RSpec.describe Packages::Debian::ParseDebian822Service do
end
it 'raise error' do
- expect {subject.execute}.to raise_error(described_class::InvalidDebian822Error, "Duplicate field 'Source' in section 'Package: libsample0'")
+ expect { subject.execute }.to raise_error(described_class::InvalidDebian822Error, "Duplicate field 'Source' in section 'Package: libsample0'")
end
end
@@ -128,7 +128,7 @@ RSpec.describe Packages::Debian::ParseDebian822Service do
end
it 'raise error' do
- expect {subject.execute}.to raise_error(described_class::InvalidDebian822Error, 'Parse error on line Hello')
+ expect { subject.execute }.to raise_error(described_class::InvalidDebian822Error, 'Parse error on line Hello')
end
end
@@ -142,7 +142,7 @@ RSpec.describe Packages::Debian::ParseDebian822Service do
end
it 'raise error' do
- expect {subject.execute}.to raise_error(described_class::InvalidDebian822Error, "Duplicate section 'Package: libsample0'")
+ expect { subject.execute }.to raise_error(described_class::InvalidDebian822Error, "Duplicate section 'Package: libsample0'")
end
end
end
diff --git a/spec/services/packages/debian/sign_distribution_service_spec.rb b/spec/services/packages/debian/sign_distribution_service_spec.rb
index 2aec0e50636..fc070b6e45e 100644
--- a/spec/services/packages/debian/sign_distribution_service_spec.rb
+++ b/spec/services/packages/debian/sign_distribution_service_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe Packages::Debian::SignDistributionService do
end
context 'with an existing key' do
- let!(:key) { create("debian_#{container_type}_distribution_key", distribution: distribution)}
+ let!(:key) { create("debian_#{container_type}_distribution_key", distribution: distribution) }
it 'returns the content signed', :aggregate_failures do
expect(Packages::Debian::GenerateDistributionKeyService).not_to receive(:new)
diff --git a/spec/services/packages/helm/process_file_service_spec.rb b/spec/services/packages/helm/process_file_service_spec.rb
index d22c1de2335..1be0153a4a5 100644
--- a/spec/services/packages/helm/process_file_service_spec.rb
+++ b/spec/services/packages/helm/process_file_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Packages::Helm::ProcessFileService do
- let(:package) { create(:helm_package, without_package_files: true, status: 'processing')}
+ let(:package) { create(:helm_package, without_package_files: true, status: 'processing') }
let!(:package_file) { create(:helm_package_file, without_loaded_metadatum: true, package: package) }
let(:channel) { 'stable' }
let(:service) { described_class.new(channel, package_file) }
diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb
index 5b41055397b..a3e59913918 100644
--- a/spec/services/packages/npm/create_package_service_spec.rb
+++ b/spec/services/packages/npm/create_package_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Packages::Npm::CreatePackageService do
- let(:namespace) {create(:namespace)}
+ let(:namespace) { create(:namespace) }
let(:project) { create(:project, namespace: namespace) }
let(:user) { create(:user) }
let(:version) { '1.0.1' }
@@ -129,7 +129,7 @@ RSpec.describe Packages::Npm::CreatePackageService do
end
describe 'max file size validation' do
- let(:max_file_size) { 5.bytes}
+ let(:max_file_size) { 5.bytes }
shared_examples_for 'max file size validation failure' do
it 'returns a 400 error', :aggregate_failures do
@@ -160,7 +160,7 @@ RSpec.describe Packages::Npm::CreatePackageService do
end
context "when encoded package data is padded with '='" do
- let(:max_file_size) { 4.bytes}
+ let(:max_file_size) { 4.bytes }
# 'Hello' (size = 5 bytes) => 'SGVsbG8='
let(:encoded_package_data) { 'SGVsbG8=' }
@@ -168,7 +168,7 @@ RSpec.describe Packages::Npm::CreatePackageService do
end
context "when encoded package data is padded with '=='" do
- let(:max_file_size) { 3.bytes}
+ let(:max_file_size) { 3.bytes }
# 'Hell' (size = 4 bytes) => 'SGVsbA=='
let(:encoded_package_data) { 'SGVsbA==' }
diff --git a/spec/services/packages/npm/create_tag_service_spec.rb b/spec/services/packages/npm/create_tag_service_spec.rb
index e7a784068fa..a4b07bf97cc 100644
--- a/spec/services/packages/npm/create_tag_service_spec.rb
+++ b/spec/services/packages/npm/create_tag_service_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe Packages::Npm::CreateTagService do
shared_examples 'it creates the tag' do
it { expect { subject }.to change { Packages::Tag.count }.by(1) }
it { expect(subject.name).to eq(tag_name) }
+
it 'adds tag to the package' do
tag = subject
expect(package.reload.tags).to match_array([tag])
diff --git a/spec/services/packages/rubygems/dependency_resolver_service_spec.rb b/spec/services/packages/rubygems/dependency_resolver_service_spec.rb
index f23ed0e5fbc..bb84e0cd361 100644
--- a/spec/services/packages/rubygems/dependency_resolver_service_spec.rb
+++ b/spec/services/packages/rubygems/dependency_resolver_service_spec.rb
@@ -47,9 +47,9 @@ RSpec.describe Packages::Rubygems::DependencyResolverService do
end
context 'package with dependencies' do
- let(:dependency_link) { create(:packages_dependency_link, :rubygems, package: package)}
- let(:dependency_link2) { create(:packages_dependency_link, :rubygems, package: package)}
- let(:dependency_link3) { create(:packages_dependency_link, :rubygems, package: package)}
+ let(:dependency_link) { create(:packages_dependency_link, :rubygems, package: package) }
+ let(:dependency_link2) { create(:packages_dependency_link, :rubygems, package: package) }
+ let(:dependency_link3) { create(:packages_dependency_link, :rubygems, package: package) }
it 'returns a set of dependencies' do
expected_result = [{
@@ -68,11 +68,11 @@ RSpec.describe Packages::Rubygems::DependencyResolverService do
end
context 'package with multiple versions' do
- let(:dependency_link) { create(:packages_dependency_link, :rubygems, package: package)}
- let(:dependency_link2) { create(:packages_dependency_link, :rubygems, package: package)}
- let(:dependency_link3) { create(:packages_dependency_link, :rubygems, package: package)}
+ let(:dependency_link) { create(:packages_dependency_link, :rubygems, package: package) }
+ let(:dependency_link2) { create(:packages_dependency_link, :rubygems, package: package) }
+ let(:dependency_link3) { create(:packages_dependency_link, :rubygems, package: package) }
let(:package2) { create(:package, project: project, name: package.name, version: '9.9.9') }
- let(:dependency_link4) { create(:packages_dependency_link, :rubygems, package: package2)}
+ let(:dependency_link4) { create(:packages_dependency_link, :rubygems, package: package2) }
it 'returns a set of dependencies' do
expected_result = [{
diff --git a/spec/services/pages/delete_service_spec.rb b/spec/services/pages/delete_service_spec.rb
index 29d9a47c72e..8b9e72ac9b1 100644
--- a/spec/services/pages/delete_service_spec.rb
+++ b/spec/services/pages/delete_service_spec.rb
@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe Pages::DeleteService do
let_it_be(:admin) { create(:admin) }
- let(:project) { create(:project, path: "my.project")}
- let(:service) { described_class.new(project, admin)}
+ let(:project) { create(:project, path: "my.project") }
+ let(:service) { described_class.new(project, admin) }
before do
project.mark_pages_as_deployed
diff --git a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
index 79654c9b190..ecb445fa441 100644
--- a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
+++ b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
@@ -135,7 +135,7 @@ RSpec.describe PagesDomains::ObtainLetsEncryptCertificateService do
cert.add_extension ef.create_extension("authorityKeyIdentifier",
"keyid:always,issuer:always")
- cert.sign key, OpenSSL::Digest.new('SHA1')
+ cert.sign key, OpenSSL::Digest.new('SHA256')
cert.to_pem
end
diff --git a/spec/services/personal_access_tokens/revoke_service_spec.rb b/spec/services/personal_access_tokens/revoke_service_spec.rb
index a25484e218e..f16b6f00a0a 100644
--- a/spec/services/personal_access_tokens/revoke_service_spec.rb
+++ b/spec/services/personal_access_tokens/revoke_service_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe PersonalAccessTokens::RevokeService do
shared_examples_for 'a successfully revoked token' do
it { expect(subject.success?).to be true }
it { expect(service.token.revoked?).to be true }
+
it 'logs the event' do
expect(Gitlab::AppLogger).to receive(:info).with(/PAT REVOCATION: revoked_by: '#{current_user.username}', revoked_for: '#{token.user.username}', token_id: '\d+'/)
diff --git a/spec/services/projects/after_rename_service_spec.rb b/spec/services/projects/after_rename_service_spec.rb
index 9dc15131bc5..edf4bbe0f7f 100644
--- a/spec/services/projects/after_rename_service_spec.rb
+++ b/spec/services/projects/after_rename_service_spec.rb
@@ -3,7 +3,6 @@
require 'spec_helper'
RSpec.describe Projects::AfterRenameService do
- let(:rugged_config) { rugged_repo(project.repository).config }
let(:legacy_storage) { Storage::LegacyProject.new(project) }
let(:hashed_storage) { Storage::Hashed.new(project) }
let!(:path_before_rename) { project.path }
@@ -71,10 +70,10 @@ RSpec.describe Projects::AfterRenameService do
end
end
- it 'updates project full path in .git/config' do
+ it 'updates project full path in gitaly' do
service_execute
- expect(rugged_config['gitlab.fullpath']).to eq(project.full_path)
+ expect(project.repository.full_path).to eq(project.full_path)
end
it 'updates storage location' do
@@ -173,10 +172,10 @@ RSpec.describe Projects::AfterRenameService do
end
end
- it 'updates project full path in .git/config' do
+ it 'updates project full path in gitaly' do
service_execute
- expect(rugged_config['gitlab.fullpath']).to eq(project.full_path)
+ expect(project.repository.full_path).to eq(project.full_path)
end
it 'updates storage location' do
diff --git a/spec/services/projects/alerting/notify_service_spec.rb b/spec/services/projects/alerting/notify_service_spec.rb
index feae8f3967c..aa2ef39bf98 100644
--- a/spec/services/projects/alerting/notify_service_spec.rb
+++ b/spec/services/projects/alerting/notify_service_spec.rb
@@ -56,6 +56,7 @@ RSpec.describe Projects::Alerting::NotifyService do
it_behaves_like 'processes new firing alert'
it_behaves_like 'properly assigns the alert properties'
+ include_examples 'handles race condition in alert creation'
it 'passes the integration to alert processing' do
expect(Gitlab::AlertManagement::Payload)
@@ -118,10 +119,10 @@ RSpec.describe Projects::Alerting::NotifyService do
end
context 'with overlong payload' do
- let(:deep_size_object) { instance_double(Gitlab::Utils::DeepSize, valid?: false) }
+ let(:payload_raw) { { 'the-payload-is-too-big' => true } }
before do
- allow(Gitlab::Utils::DeepSize).to receive(:new).and_return(deep_size_object)
+ stub_const('::Gitlab::Utils::DeepSize::DEFAULT_MAX_DEPTH', 0)
end
it_behaves_like 'alerts service responds with an error and takes no actions', :bad_request
diff --git a/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb b/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb
index 22cada7816b..4de36452684 100644
--- a/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb
@@ -58,7 +58,7 @@ RSpec.describe Projects::ContainerRepository::ThirdParty::DeleteTagsService do
stub_put_manifest_request('Ba', 500, {})
end
- it { is_expected.to eq(status: :error, message: "could not delete tags: #{tags.join(', ')}")}
+ it { is_expected.to eq(status: :error, message: "could not delete tags: #{tags.join(', ')}") }
context 'when a large list of tag updates fails' do
let(:tags) { Array.new(1000) { |i| "tag_#{i}" } }
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 59dee209ff9..e112c1e2497 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe Projects::CreateService, '#execute' do
include ExternalAuthorizationServiceHelpers
- include GitHelpers
let(:user) { create :user }
let(:project_name) { 'GitLab' }
@@ -254,6 +253,39 @@ RSpec.describe Projects::CreateService, '#execute' do
end
end
+ context 'user with project limit' do
+ let_it_be(:user_with_projects_limit) { create(:user, projects_limit: 0) }
+
+ let(:params) { opts.merge!(namespace_id: target_namespace.id) }
+
+ subject(:project) { create_project(user_with_projects_limit, params) }
+
+ context 'under personal namespace' do
+ let(:target_namespace) { user_with_projects_limit.namespace }
+
+ it 'cannot create a project' do
+ expect(project.errors.errors.length).to eq 1
+ expect(project.errors.messages[:limit_reached].first).to eq(_('Personal project creation is not allowed. Please contact your administrator with questions'))
+ end
+ end
+
+ context 'under group namespace' do
+ let_it_be(:group) do
+ create(:group).tap do |group|
+ group.add_owner(user_with_projects_limit)
+ end
+ end
+
+ let(:target_namespace) { group }
+
+ it 'can create a project' do
+ expect(project).to be_valid
+ expect(project).to be_saved
+ expect(project.errors.errors.length).to eq 0
+ end
+ end
+ end
+
context 'membership overrides', :sidekiq_inline do
let_it_be(:group) { create(:group, :private) }
let_it_be(:subgroup_for_projects) { create(:group, :private, parent: group) }
@@ -769,11 +801,10 @@ RSpec.describe Projects::CreateService, '#execute' do
create_project(user, opts)
end
- it 'writes project full path to .git/config' do
+ it 'writes project full path to gitaly' do
project = create_project(user, opts)
- rugged = rugged_repo(project.repository)
- expect(rugged.config['gitlab.fullpath']).to eq project.full_path
+ expect(project.repository.full_path).to eq project.full_path
end
it 'triggers PostCreationWorker' do
diff --git a/spec/services/projects/enable_deploy_key_service_spec.rb b/spec/services/projects/enable_deploy_key_service_spec.rb
index f297ec374cf..c0b3992037e 100644
--- a/spec/services/projects/enable_deploy_key_service_spec.rb
+++ b/spec/services/projects/enable_deploy_key_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::EnableDeployKeyService do
let(:deploy_key) { create(:deploy_key, public: true) }
let(:project) { create(:project) }
- let(:user) { project.creator}
+ let(:user) { project.creator }
let!(:params) { { key_id: deploy_key.id } }
it 'enables the key' do
diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
index d0064873972..65da1976dc2 100644
--- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
@@ -68,12 +68,10 @@ RSpec.describe Projects::HashedStorage::MigrateRepositoryService do
service.execute
end
- it 'writes project full path to .git/config' do
+ it 'writes project full path to gitaly' do
service.execute
- rugged_config = rugged_repo(project.repository).config['gitlab.fullpath']
-
- expect(rugged_config).to eq project.full_path
+ expect(project.repository.full_path).to eq project.full_path
end
end
diff --git a/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb b/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb
index 23e776b72bc..385c03e6308 100644
--- a/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab_redis_shared_state do
- include GitHelpers
-
let(:gitlab_shell) { Gitlab::Shell.new }
let(:project) { create(:project, :repository, :wiki_repo, :design_repo, storage_version: ::Project::HASHED_STORAGE_FEATURES[:repository]) }
let(:legacy_storage) { Storage::LegacyProject.new(project) }
@@ -68,12 +66,10 @@ RSpec.describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab
service.execute
end
- it 'writes project full path to .git/config' do
+ it 'writes project full path to gitaly' do
service.execute
- rugged_config = rugged_repo(project.repository).config['gitlab.fullpath']
-
- expect(rugged_config).to eq project.full_path
+ expect(project.repository.full_path).to eq project.full_path
end
end
diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb
index 54abbc04084..285687505e9 100644
--- a/spec/services/projects/import_export/export_service_spec.rb
+++ b/spec/services/projects/import_export/export_service_spec.rb
@@ -89,7 +89,21 @@ RSpec.describe Projects::ImportExport::ExportService do
context 'when all saver services succeed' do
before do
- allow(service).to receive(:save_services).and_return(true)
+ allow(service).to receive(:save_exporters).and_return(true)
+ end
+
+ it 'logs a successful message' do
+ allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true)
+
+ expect(service.instance_variable_get(:@logger)).to receive(:info).ordered.with(
+ hash_including({ message: 'Project export started', project_id: project.id })
+ )
+
+ expect(service.instance_variable_get(:@logger)).to receive(:info).ordered.with(
+ hash_including({ message: 'Project successfully exported', project_id: project.id })
+ )
+
+ service.execute
end
it 'saves the project in the file system' do
@@ -111,6 +125,7 @@ RSpec.describe Projects::ImportExport::ExportService do
end
it 'calls the after export strategy' do
+ allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true)
expect(after_export_strategy).to receive(:execute)
service.execute(after_export_strategy)
@@ -119,7 +134,7 @@ RSpec.describe Projects::ImportExport::ExportService do
context 'when after export strategy fails' do
before do
allow(after_export_strategy).to receive(:execute).and_return(false)
- expect(Gitlab::ImportExport::Saver).to receive(:save).with(exportable: project, shared: shared).and_return(true)
+ allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true)
end
after do
@@ -140,7 +155,9 @@ RSpec.describe Projects::ImportExport::ExportService do
end
it 'notifies logger' do
- expect(service.instance_variable_get(:@logger)).to receive(:error)
+ expect(service.instance_variable_get(:@logger)).to receive(:error).with(
+ hash_including({ message: 'Project export error', project_id: project.id })
+ )
end
end
end
diff --git a/spec/services/projects/import_export/relation_export_service_spec.rb b/spec/services/projects/import_export/relation_export_service_spec.rb
new file mode 100644
index 00000000000..94f5653ee7d
--- /dev/null
+++ b/spec/services/projects/import_export/relation_export_service_spec.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::ImportExport::RelationExportService do
+ using RSpec::Parameterized::TableSyntax
+
+ subject(:service) { described_class.new(relation_export, 'jid') }
+
+ let_it_be(:project_export_job) { create(:project_export_job) }
+ let_it_be(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
+ let_it_be(:archive_path) { "#{Dir.tmpdir}/project_archive_spec" }
+
+ let(:relation_export) { create(:project_relation_export, relation: relation, project_export_job: project_export_job) }
+
+ before do
+ stub_uploads_object_storage(ImportExportUploader, enabled: false)
+
+ allow(project_export_job.project.import_export_shared).to receive(:export_path).and_return(export_path)
+ allow(project_export_job.project.import_export_shared).to receive(:archive_path).and_return(archive_path)
+ allow(FileUtils).to receive(:remove_entry).with(any_args).and_call_original
+ end
+
+ describe '#execute' do
+ let(:relation) { 'labels' }
+
+ it 'removes temporary paths used to export files' do
+ expect(FileUtils).to receive(:remove_entry).with(export_path)
+ expect(FileUtils).to receive(:remove_entry).with(archive_path)
+
+ service.execute
+ end
+
+ context 'when saver fails to export relation' do
+ before do
+ allow_next_instance_of(Gitlab::ImportExport::Project::RelationSaver) do |saver|
+ allow(saver).to receive(:save).and_return(false)
+ end
+ end
+
+ it 'flags export as failed' do
+ service.execute
+
+ expect(relation_export.failed?).to eq(true)
+ end
+
+ it 'logs failed message' do
+ expect_next_instance_of(Gitlab::Export::Logger) do |logger|
+ expect(logger).to receive(:error).with(
+ export_error: '',
+ message: 'Project relation export failed',
+ project_export_job_id: project_export_job.id,
+ project_id: project_export_job.project.id,
+ project_name: project_export_job.project.name
+ )
+ end
+
+ service.execute
+ end
+ end
+
+ context 'when an exception is raised' do
+ before do
+ allow_next_instance_of(Gitlab::ImportExport::Project::RelationSaver) do |saver|
+ allow(saver).to receive(:save).and_raise('Error!')
+ end
+ end
+
+ it 'flags export as failed' do
+ service.execute
+
+ expect(relation_export.failed?).to eq(true)
+ expect(relation_export.export_error).to eq('Error!')
+ end
+
+ it 'logs exception error message' do
+ expect_next_instance_of(Gitlab::Export::Logger) do |logger|
+ expect(logger).to receive(:error).with(
+ export_error: 'Error!',
+ message: 'Project relation export failed',
+ project_export_job_id: project_export_job.id,
+ project_id: project_export_job.project.id,
+ project_name: project_export_job.project.name
+ )
+ end
+
+ service.execute
+ end
+ end
+
+ describe 'relation name and saver class' do
+ where(:relation_name, :saver) do
+ Projects::ImportExport::RelationExport::UPLOADS_RELATION | Gitlab::ImportExport::UploadsSaver
+ Projects::ImportExport::RelationExport::REPOSITORY_RELATION | Gitlab::ImportExport::RepoSaver
+ Projects::ImportExport::RelationExport::WIKI_REPOSITORY_RELATION | Gitlab::ImportExport::WikiRepoSaver
+ Projects::ImportExport::RelationExport::LFS_OBJECTS_RELATION | Gitlab::ImportExport::LfsSaver
+ Projects::ImportExport::RelationExport::SNIPPETS_REPOSITORY_RELATION | Gitlab::ImportExport::SnippetsRepoSaver
+ Projects::ImportExport::RelationExport::DESIGN_REPOSITORY_RELATION | Gitlab::ImportExport::DesignRepoSaver
+ Projects::ImportExport::RelationExport::ROOT_RELATION | Gitlab::ImportExport::Project::RelationSaver
+ 'labels' | Gitlab::ImportExport::Project::RelationSaver
+ end
+
+ with_them do
+ let(:relation) { relation_name }
+
+ it 'exports relation using correct saver' do
+ expect(saver).to receive(:new).and_call_original
+
+ service.execute
+ end
+
+ it 'assigns finished status and relation file' do
+ service.execute
+
+ expect(relation_export.finished?).to eq(true)
+ expect(relation_export.upload.export_file.filename).to eq("#{relation}.tar.gz")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
index 047ebe65dff..d472d6493c3 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Projects::LfsPointers::LfsDownloadLinkListService do
let(:lfs_endpoint) { "#{import_url}/info/lfs/objects/batch" }
let!(:project) { create(:project, import_url: import_url) }
let(:new_oids) { { 'oid1' => 123, 'oid2' => 125 } }
- let(:headers) { { 'X-Some-Header' => '456' }}
+ let(:headers) { { 'X-Some-Header' => '456' } }
let(:remote_uri) { URI.parse(lfs_endpoint) }
let(:request_object) { HTTParty::Request.new(Net::HTTP::Post, '/') }
diff --git a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
index 04c6349bf52..b67b4d64c1d 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
@@ -250,7 +250,7 @@ RSpec.describe Projects::LfsPointers::LfsDownloadService do
end
context 'that is not blocked' do
- let(:redirect_link) { "http://example.com/"}
+ let(:redirect_link) { "http://example.com/" }
before do
stub_full_request(download_link).to_return(status: 301, headers: { 'Location' => redirect_link })
diff --git a/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb
index 981d7027a17..adcc2b85706 100644
--- a/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
RSpec.describe Projects::LfsPointers::LfsObjectDownloadListService do
let(:import_url) { 'http://www.gitlab.com/demo/repo.git' }
- let(:default_endpoint) { "#{import_url}/info/lfs/objects/batch"}
- let(:group) { create(:group, lfs_enabled: true)}
+ let(:default_endpoint) { "#{import_url}/info/lfs/objects/batch" }
+ let(:group) { create(:group, lfs_enabled: true) }
let!(:project) { create(:project, namespace: group, import_url: import_url, lfs_enabled: true) }
let!(:lfs_objects_project) { create_list(:lfs_objects_project, 2, project: project) }
let!(:existing_lfs_objects) { LfsObject.pluck(:oid, :size).to_h }
@@ -75,7 +75,7 @@ RSpec.describe Projects::LfsPointers::LfsObjectDownloadListService do
end
context 'when import url has credentials' do
- let(:import_url) { 'http://user:password@www.gitlab.com/demo/repo.git'}
+ let(:import_url) { 'http://user:password@www.gitlab.com/demo/repo.git' }
it 'adds the credentials to the new endpoint' do
expect(Projects::LfsPointers::LfsDownloadLinkListService)
diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb
index 61edfd23700..fc745cd669f 100644
--- a/spec/services/projects/participants_service_spec.rb
+++ b/spec/services/projects/participants_service_spec.rb
@@ -107,7 +107,7 @@ RSpec.describe Projects::ParticipantsService do
shared_examples 'return project members' do
context 'when there is a project in group namespace' do
let_it_be(:public_group) { create(:group, :public) }
- let_it_be(:public_project) { create(:project, :public, namespace: public_group)}
+ let_it_be(:public_project) { create(:project, :public, namespace: public_group) }
let_it_be(:public_group_owner) { create(:user) }
@@ -125,9 +125,9 @@ RSpec.describe Projects::ParticipantsService do
context 'when there is a private group and a public project' do
let_it_be(:public_group) { create(:group, :public) }
let_it_be(:private_group) { create(:group, :private, :nested) }
- let_it_be(:public_project) { create(:project, :public, namespace: public_group)}
+ let_it_be(:public_project) { create(:project, :public, namespace: public_group) }
- let_it_be(:project_issue) { create(:issue, project: public_project)}
+ let_it_be(:project_issue) { create(:issue, project: public_project) }
let_it_be(:public_group_owner) { create(:user) }
let_it_be(:private_group_member) { create(:user) }
diff --git a/spec/services/projects/prometheus/alerts/notify_service_spec.rb b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
index 6f760e6dbfa..7bf6dfd0fd8 100644
--- a/spec/services/projects/prometheus/alerts/notify_service_spec.rb
+++ b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
@@ -177,6 +177,7 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
end
it { is_expected.to be_success }
+
include_examples 'does not send alert notification emails'
include_examples 'does not process incident issues'
end
@@ -187,6 +188,7 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
end
it { is_expected.to be_success }
+
include_examples 'does not send alert notification emails'
end
@@ -196,6 +198,7 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
end
it { is_expected.to be_success }
+
include_examples 'does not process incident issues'
end
end
@@ -313,11 +316,11 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
end
context 'when the payload is too big' do
- let(:payload) { { 'the-payload-is-too-big' => true } }
- let(:deep_size_object) { instance_double(Gitlab::Utils::DeepSize, valid?: false) }
+ let(:payload_raw) { { 'the-payload-is-too-big' => true } }
+ let(:payload) { ActionController::Parameters.new(payload_raw).permit! }
before do
- allow(Gitlab::Utils::DeepSize).to receive(:new).and_return(deep_size_object)
+ stub_const('::Gitlab::Utils::DeepSize::DEFAULT_MAX_DEPTH', 0)
end
it_behaves_like 'alerts service responds with an error and takes no actions', :bad_request
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index ecf9f92d74f..8f505c31c5a 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe Projects::TransferService do
- include GitHelpers
-
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
let_it_be(:group_integration) { create(:integrations_slack, :group, group: group, webhook: 'http://group.slack.com') }
@@ -64,6 +62,30 @@ RSpec.describe Projects::TransferService do
expect(project.namespace).to eq(group)
end
+ context 'EventStore' do
+ let(:group) do
+ create(:group, :nested).tap { |g| g.add_owner(user) }
+ end
+
+ let(:target) do
+ create(:group, :nested).tap { |g| g.add_owner(user) }
+ end
+
+ let(:project) { create(:project, namespace: group) }
+
+ it 'publishes a ProjectTransferedEvent' do
+ expect { execute_transfer }
+ .to publish_event(Projects::ProjectTransferedEvent)
+ .with(
+ project_id: project.id,
+ old_namespace_id: group.id,
+ old_root_namespace_id: group.root_ancestor.id,
+ new_namespace_id: target.id,
+ new_root_namespace_id: target.root_ancestor.id
+ )
+ end
+ end
+
context 'when project has an associated project namespace' do
it 'keeps project namespace in sync with project' do
transfer_result = execute_transfer
@@ -178,10 +200,10 @@ RSpec.describe Projects::TransferService do
expect(project.disk_path).to start_with(group.path)
end
- it 'updates project full path in .git/config' do
+ it 'updates project full path in gitaly' do
execute_transfer
- expect(rugged_config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}"
+ expect(project.repository.full_path).to eq "#{group.full_path}/#{project.path}"
end
it 'updates storage location' do
@@ -272,10 +294,10 @@ RSpec.describe Projects::TransferService do
expect(original_path).to eq current_path
end
- it 'rolls back project full path in .git/config' do
+ it 'rolls back project full path in gitaly' do
attempt_project_transfer
- expect(rugged_config['gitlab.fullpath']).to eq project.full_path
+ expect(project.repository.full_path).to eq project.full_path
end
it "doesn't send move notifications" do
@@ -299,6 +321,11 @@ RSpec.describe Projects::TransferService do
)
end
+ it 'does not publish a ProjectTransferedEvent' do
+ expect { attempt_project_transfer }
+ .not_to publish_event(Projects::ProjectTransferedEvent)
+ end
+
context 'when project has pending builds', :sidekiq_inline do
let!(:other_project) { create(:project) }
let!(:pending_build) { create(:ci_pending_build, project: project.reload) }
@@ -741,10 +768,6 @@ RSpec.describe Projects::TransferService do
end
end
- def rugged_config
- rugged_repo(project.repository).config
- end
-
def project_namespace_in_sync(group)
project.reload
expect(project.namespace).to eq(group)
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index ca838be0fa8..85d3e99109d 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -348,6 +348,18 @@ RSpec.describe Projects::UpdateService do
end
end
+ context 'when archiving a project' do
+ it 'publishes a ProjectTransferedEvent' do
+ expect { update_project(project, user, archived: true) }
+ .to publish_event(Projects::ProjectArchivedEvent)
+ .with(
+ project_id: project.id,
+ namespace_id: project.namespace_id,
+ root_namespace_id: project.root_namespace.id
+ )
+ end
+ end
+
context 'when changing operations feature visibility' do
let(:feature_params) { { operations_access_level: ProjectFeature::DISABLED } }
diff --git a/spec/services/projects/update_statistics_service_spec.rb b/spec/services/projects/update_statistics_service_spec.rb
index 6987185b549..1cc69e7e2fe 100644
--- a/spec/services/projects/update_statistics_service_spec.rb
+++ b/spec/services/projects/update_statistics_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::UpdateStatisticsService do
using RSpec::Parameterized::TableSyntax
- let(:service) { described_class.new(project, nil, statistics: statistics)}
+ let(:service) { described_class.new(project, nil, statistics: statistics) }
let(:statistics) { %w(repository_size) }
describe '#execute' do
diff --git a/spec/services/protected_branches/cache_service_spec.rb b/spec/services/protected_branches/cache_service_spec.rb
new file mode 100644
index 00000000000..4fa7553c23d
--- /dev/null
+++ b/spec/services/protected_branches/cache_service_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+# rubocop:disable Style/RedundantFetchBlock
+#
+require 'spec_helper'
+
+RSpec.describe ProtectedBranches::CacheService, :clean_gitlab_redis_cache do
+ subject(:service) { described_class.new(project, user) }
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { project.first_owner }
+
+ let(:immediate_expiration) { 0 }
+
+ describe '#fetch' do
+ it 'caches the value' do
+ expect(service.fetch('main') { true }).to eq(true)
+ expect(service.fetch('not-found') { false }).to eq(false)
+
+ # Uses cached values
+ expect(service.fetch('main') { false }).to eq(true)
+ expect(service.fetch('not-found') { true }).to eq(false)
+ end
+
+ it 'sets expiry on the key' do
+ stub_const("#{described_class.name}::CACHE_EXPIRE_IN", immediate_expiration)
+
+ expect(service.fetch('main') { true }).to eq(true)
+ expect(service.fetch('not-found') { false }).to eq(false)
+
+ expect(service.fetch('main') { false }).to eq(false)
+ expect(service.fetch('not-found') { true }).to eq(true)
+ end
+
+ it 'does not set an expiry on the key after the hash is already created' do
+ expect(service.fetch('main') { true }).to eq(true)
+
+ stub_const("#{described_class.name}::CACHE_EXPIRE_IN", immediate_expiration)
+
+ expect(service.fetch('not-found') { false }).to eq(false)
+
+ expect(service.fetch('main') { false }).to eq(true)
+ expect(service.fetch('not-found') { true }).to eq(false)
+ end
+
+ context 'when CACHE_LIMIT is exceeded' do
+ before do
+ stub_const("#{described_class.name}::CACHE_LIMIT", 2)
+ end
+
+ it 'recreates cache' do
+ expect(service.fetch('main') { true }).to eq(true)
+ expect(service.fetch('not-found') { false }).to eq(false)
+
+ # Uses cached values
+ expect(service.fetch('main') { false }).to eq(true)
+ expect(service.fetch('not-found') { true }).to eq(false)
+
+ # Overflow
+ expect(service.fetch('new-branch') { true }).to eq(true)
+
+ # Refreshes values
+ expect(service.fetch('main') { false }).to eq(false)
+ expect(service.fetch('not-found') { true }).to eq(true)
+ end
+ end
+
+ context 'when dry_run is on' do
+ it 'does not use cached value' do
+ expect(service.fetch('main', dry_run: true) { true }).to eq(true)
+ expect(service.fetch('main', dry_run: true) { false }).to eq(false)
+ end
+
+ context 'when cache mismatch' do
+ it 'logs an error' do
+ expect(service.fetch('main', dry_run: true) { true }).to eq(true)
+
+ expect(Gitlab::AppLogger).to receive(:error).with(
+ 'class' => described_class.name,
+ 'message' => /Cache mismatch/,
+ 'project_id' => project.id,
+ 'project_path' => project.full_path
+ )
+
+ expect(service.fetch('main', dry_run: true) { false }).to eq(false)
+ end
+ end
+
+ context 'when cache matches' do
+ it 'does not log an error' do
+ expect(service.fetch('main', dry_run: true) { true }).to eq(true)
+
+ expect(Gitlab::AppLogger).not_to receive(:error)
+
+ expect(service.fetch('main', dry_run: true) { true }).to eq(true)
+ end
+ end
+ end
+ end
+
+ describe '#refresh' do
+ it 'clears cached values' do
+ expect(service.fetch('main') { true }).to eq(true)
+ expect(service.fetch('not-found') { false }).to eq(false)
+
+ service.refresh
+
+ # Recreates cache
+ expect(service.fetch('main') { false }).to eq(false)
+ expect(service.fetch('not-found') { true }).to eq(true)
+ end
+ end
+end
+# rubocop:enable Style/RedundantFetchBlock
diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb
index 3ac42d41377..b42524e761c 100644
--- a/spec/services/protected_branches/create_service_spec.rb
+++ b/spec/services/protected_branches/create_service_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe ProtectedBranches::CreateService do
- let(:project) { create(:project) }
+ let_it_be_with_reload(:project) { create(:project) }
+
let(:user) { project.first_owner }
let(:params) do
{
@@ -13,22 +14,28 @@ RSpec.describe ProtectedBranches::CreateService do
}
end
+ subject(:service) { described_class.new(project, user, params) }
+
describe '#execute' do
let(:name) { 'master' }
- subject(:service) { described_class.new(project, user, params) }
-
it 'creates a new protected branch' do
expect { service.execute }.to change(ProtectedBranch, :count).by(1)
expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER])
expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER])
end
+ it 'refreshes the cache' do
+ expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service|
+ expect(cache_service).to receive(:refresh)
+ end
+
+ service.execute
+ end
+
context 'when protecting a branch with a name that contains HTML tags' do
let(:name) { 'foo<b>bar<\b>' }
- subject(:service) { described_class.new(project, user, params) }
-
it 'creates a new protected branch' do
expect { service.execute }.to change(ProtectedBranch, :count).by(1)
expect(project.protected_branches.last.name).to eq(name)
@@ -52,16 +59,18 @@ RSpec.describe ProtectedBranches::CreateService do
end
context 'when a policy restricts rule creation' do
- before do
- policy = instance_double(ProtectedBranchPolicy, allowed?: false)
- expect(ProtectedBranchPolicy).to receive(:new).and_return(policy)
- end
-
it "prevents creation of the protected branch rule" do
+ disallow(:create_protected_branch, an_instance_of(ProtectedBranch))
+
expect do
service.execute
end.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
end
+
+ def disallow(ability, protected_branch)
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user, ability, protected_branch).and_return(false)
+ end
end
diff --git a/spec/services/protected_branches/destroy_service_spec.rb b/spec/services/protected_branches/destroy_service_spec.rb
index 4e55c72f312..9fa07820148 100644
--- a/spec/services/protected_branches/destroy_service_spec.rb
+++ b/spec/services/protected_branches/destroy_service_spec.rb
@@ -3,30 +3,41 @@
require 'spec_helper'
RSpec.describe ProtectedBranches::DestroyService do
- let(:protected_branch) { create(:protected_branch) }
- let(:project) { protected_branch.project }
+ let_it_be_with_reload(:project) { create(:project) }
+
+ let(:protected_branch) { create(:protected_branch, project: project) }
let(:user) { project.first_owner }
- describe '#execute' do
- subject(:service) { described_class.new(project, user) }
+ subject(:service) { described_class.new(project, user) }
+ describe '#execute' do
it 'destroys a protected branch' do
service.execute(protected_branch)
expect(protected_branch).to be_destroyed
end
- context 'when a policy restricts rule deletion' do
- before do
- policy = instance_double(ProtectedBranchPolicy, allowed?: false)
- expect(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ it 'refreshes the cache' do
+ expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service|
+ expect(cache_service).to receive(:refresh)
end
+ service.execute(protected_branch)
+ end
+
+ context 'when a policy restricts rule deletion' do
it "prevents deletion of the protected branch rule" do
+ disallow(:destroy_protected_branch, protected_branch)
+
expect do
service.execute(protected_branch)
end.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
end
+
+ def disallow(ability, protected_branch)
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user, ability, protected_branch).and_return(false)
+ end
end
diff --git a/spec/services/protected_branches/update_service_spec.rb b/spec/services/protected_branches/update_service_spec.rb
index 4405af35c37..c4fe4d78070 100644
--- a/spec/services/protected_branches/update_service_spec.rb
+++ b/spec/services/protected_branches/update_service_spec.rb
@@ -3,27 +3,34 @@
require 'spec_helper'
RSpec.describe ProtectedBranches::UpdateService do
- let(:protected_branch) { create(:protected_branch) }
- let(:project) { protected_branch.project }
+ let_it_be_with_reload(:project) { create(:project) }
+
+ let(:protected_branch) { create(:protected_branch, project: project) }
let(:user) { project.first_owner }
let(:params) { { name: new_name } }
+ subject(:service) { described_class.new(project, user, params) }
+
describe '#execute' do
let(:new_name) { 'new protected branch name' }
let(:result) { service.execute(protected_branch) }
- subject(:service) { described_class.new(project, user, params) }
-
it 'updates a protected branch' do
expect(result.reload.name).to eq(params[:name])
end
+ it 'refreshes the cache' do
+ expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service|
+ expect(cache_service).to receive(:refresh)
+ end
+
+ result
+ end
+
context 'when updating name of a protected branch to one that contains HTML tags' do
let(:new_name) { 'foo<b>bar<\b>' }
let(:result) { service.execute(protected_branch) }
- subject(:service) { described_class.new(project, user, params) }
-
it 'updates a protected branch' do
expect(result.reload.name).to eq(new_name)
end
@@ -37,15 +44,17 @@ RSpec.describe ProtectedBranches::UpdateService do
end
end
- context 'when a policy restricts rule creation' do
- before do
- policy = instance_double(ProtectedBranchPolicy, allowed?: false)
- expect(ProtectedBranchPolicy).to receive(:new).and_return(policy)
- end
+ context 'when a policy restricts rule update' do
+ it "prevents update of the protected branch rule" do
+ disallow(:update_protected_branch, protected_branch)
- it "prevents creation of the protected branch rule" do
expect { service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
end
+
+ def disallow(ability, protected_branch)
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user, ability, protected_branch).and_return(false)
+ end
end
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 3f11eaa7e93..2d38d968ce4 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -810,38 +810,6 @@ RSpec.describe QuickActions::InterpretService do
end
end
- shared_examples 'attention command' do
- it 'updates reviewers attention status' do
- _, _, message = service.execute(content, issuable)
-
- expect(message).to eq("Requested attention from #{developer.to_reference}.")
-
- reviewer.reload
-
- expect(reviewer).to be_attention_requested
- end
-
- it 'supports attn alias' do
- attn_cmd = content.gsub(/attention/, 'attn')
- _, _, message = service.execute(attn_cmd, issuable)
-
- expect(message).to eq("Requested attention from #{developer.to_reference}.")
-
- reviewer.reload
-
- expect(reviewer).to be_attention_requested
- end
- end
-
- shared_examples 'remove attention command' do
- it 'updates reviewers attention status' do
- _, _, message = service.execute(content, issuable)
-
- expect(message).to eq("Removed attention from #{developer.to_reference}.")
- expect(reviewer).not_to be_attention_requested
- end
- end
-
it_behaves_like 'reopen command' do
let(:content) { '/reopen' }
let(:issuable) { issue }
@@ -1888,7 +1856,7 @@ RSpec.describe QuickActions::InterpretService do
context '/target_branch command' do
let(:non_empty_project) { create(:project, :repository) }
let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) }
- let(:service) { described_class.new(non_empty_project, developer)}
+ let(:service) { described_class.new(non_empty_project, developer) }
it 'updates target_branch if /target_branch command is executed' do
_, updates, _ = service.execute('/target_branch merge-test', merge_request)
@@ -2481,82 +2449,6 @@ RSpec.describe QuickActions::InterpretService do
expect(message).to eq('One or more contacts were successfully removed.')
end
end
-
- describe 'attention command' do
- let(:issuable) { create(:merge_request, reviewers: [developer], source_project: project) }
- let(:reviewer) { issuable.merge_request_reviewers.find_by(user_id: developer.id) }
- let(:content) { "/attention @#{developer.username}" }
-
- context 'with one user' do
- before do
- reviewer.update!(state: :reviewed)
- end
-
- it_behaves_like 'attention command'
- end
-
- context 'with no user' do
- let(:content) { "/attention" }
-
- it_behaves_like 'failed command', 'Failed to request attention because no user was found.'
- end
-
- context 'with incorrect permissions' do
- let(:service) { described_class.new(project, create(:user)) }
-
- it_behaves_like 'failed command', 'Could not apply attention command.'
- end
-
- context 'with feature flag disabled' do
- before do
- stub_feature_flags(mr_attention_requests: false)
- end
-
- it_behaves_like 'failed command', 'Could not apply attention command.'
- end
-
- context 'with an issue instead of a merge request' do
- let(:issuable) { issue }
-
- it_behaves_like 'failed command', 'Could not apply attention command.'
- end
- end
-
- describe 'remove attention command' do
- let(:issuable) { create(:merge_request, reviewers: [developer], source_project: project) }
- let(:reviewer) { issuable.merge_request_reviewers.find_by(user_id: developer.id) }
- let(:content) { "/remove_attention @#{developer.username}" }
-
- context 'with one user' do
- it_behaves_like 'remove attention command'
- end
-
- context 'with no user' do
- let(:content) { "/remove_attention" }
-
- it_behaves_like 'failed command', 'Failed to remove attention because no user was found.'
- end
-
- context 'with incorrect permissions' do
- let(:service) { described_class.new(project, create(:user)) }
-
- it_behaves_like 'failed command', 'Could not apply remove_attention command.'
- end
-
- context 'with feature flag disabled' do
- before do
- stub_feature_flags(mr_attention_requests: false)
- end
-
- it_behaves_like 'failed command', 'Could not apply remove_attention command.'
- end
-
- context 'with an issue instead of a merge request' do
- let(:issuable) { issue }
-
- it_behaves_like 'failed command', 'Could not apply remove_attention command.'
- end
- end
end
describe '#explain' do
diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb
index 566d73a3b75..2421fab0eec 100644
--- a/spec/services/releases/create_service_spec.rb
+++ b/spec/services/releases/create_service_spec.rb
@@ -111,14 +111,6 @@ RSpec.describe Releases::CreateService do
expect(result[:message]).to eq("Milestone(s) not found: #{inexistent_milestone_tag}")
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
context 'when existing milestone is passed in' do
let(:title) { 'v1.0' }
diff --git a/spec/services/releases/destroy_service_spec.rb b/spec/services/releases/destroy_service_spec.rb
index bc5bff0b31d..46550ac5bef 100644
--- a/spec/services/releases/destroy_service_spec.rb
+++ b/spec/services/releases/destroy_service_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe Releases::DestroyService do
end
context 'when release is not found' do
- let!(:release) { }
+ let!(:release) {}
it 'returns an error' do
is_expected.to include(status: :error,
diff --git a/spec/services/releases/update_service_spec.rb b/spec/services/releases/update_service_spec.rb
index 932a7fab5ec..7461470a844 100644
--- a/spec/services/releases/update_service_spec.rb
+++ b/spec/services/releases/update_service_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe Releases::UpdateService do
end
context 'when the release does not exist' do
- let!(:release) { }
+ let!(:release) {}
it_behaves_like 'a failed update'
end
diff --git a/spec/services/resource_access_tokens/create_service_spec.rb b/spec/services/resource_access_tokens/create_service_spec.rb
index 127948549b0..442232920f9 100644
--- a/spec/services/resource_access_tokens/create_service_spec.rb
+++ b/spec/services/resource_access_tokens/create_service_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe ResourceAccessTokens::CreateService do
describe '#execute' do
shared_examples 'token creation fails' do
- let(:resource) { create(:project)}
+ let(:resource) { create(:project) }
it 'does not add the project bot as a member' do
expect { subject }.not_to change { resource.members.count }
diff --git a/spec/services/resource_events/change_labels_service_spec.rb b/spec/services/resource_events/change_labels_service_spec.rb
index c2c0a4c2126..8dc7b07e397 100644
--- a/spec/services/resource_events/change_labels_service_spec.rb
+++ b/spec/services/resource_events/change_labels_service_spec.rb
@@ -5,11 +5,40 @@ require 'spec_helper'
RSpec.describe ResourceEvents::ChangeLabelsService do
let_it_be(:project) { create(:project) }
let_it_be(:author) { create(:user) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:incident) { create(:incident, project: project) }
- let(:resource) { create(:issue, project: project) }
+ let(:resource) { issue }
- describe '.change_labels' do
- subject { described_class.new(resource, author).execute(added_labels: added, removed_labels: removed) }
+ describe '#execute' do
+ shared_examples 'creating timeline events' do
+ context 'when resource is not an incident' do
+ let(:resource) { issue }
+
+ it 'does not call create timeline events service' do
+ expect(IncidentManagement::TimelineEvents::CreateService).not_to receive(:change_labels)
+
+ change_labels
+ end
+ end
+
+ context 'when resource is an incident' do
+ let(:resource) { incident }
+
+ it 'calls create timeline events service with correct attributes' do
+ expect(IncidentManagement::TimelineEvents::CreateService)
+ .to receive(:change_labels)
+ .with(resource, author, added_labels: added, removed_labels: removed)
+ .and_call_original
+
+ change_labels
+ end
+ end
+ end
+
+ subject(:change_labels) do
+ described_class.new(resource, author).execute(added_labels: added, removed_labels: removed)
+ end
let_it_be(:labels) { create_list(:label, 2, project: project) }
@@ -20,9 +49,9 @@ RSpec.describe ResourceEvents::ChangeLabelsService do
end
it 'expires resource note etag cache' do
- expect_any_instance_of(Gitlab::EtagCaching::Store)
- .to receive(:touch)
- .with("/#{resource.project.namespace.to_param}/#{resource.project.to_param}/noteable/issue/#{resource.id}/notes")
+ expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(
+ "/#{resource.project.namespace.to_param}/#{resource.project.to_param}/noteable/issue/#{resource.id}/notes"
+ )
described_class.new(resource, author).execute(added_labels: [labels[0]])
end
@@ -32,10 +61,12 @@ RSpec.describe ResourceEvents::ChangeLabelsService do
let(:removed) { [] }
it 'creates new label event' do
- expect { subject }.to change { resource.resource_label_events.count }.from(0).to(1)
+ expect { change_labels }.to change { resource.resource_label_events.count }.from(0).to(1)
expect_label_event(resource.resource_label_events.first, labels[0], 'add')
end
+
+ it_behaves_like 'creating timeline events'
end
context 'when removing a label' do
@@ -43,10 +74,12 @@ RSpec.describe ResourceEvents::ChangeLabelsService do
let(:removed) { [labels[1]] }
it 'creates new label event' do
- expect { subject }.to change { resource.resource_label_events.count }.from(0).to(1)
+ expect { change_labels }.to change { resource.resource_label_events.count }.from(0).to(1)
expect_label_event(resource.resource_label_events.first, labels[1], 'remove')
end
+
+ it_behaves_like 'creating timeline events'
end
context 'when both adding and removing labels' do
@@ -55,8 +88,10 @@ RSpec.describe ResourceEvents::ChangeLabelsService do
it 'creates all label events in a single query' do
expect(ApplicationRecord).to receive(:legacy_bulk_insert).once.and_call_original
- expect { subject }.to change { resource.resource_label_events.count }.from(0).to(2)
+ expect { change_labels }.to change { resource.resource_label_events.count }.from(0).to(2)
end
+
+ it_behaves_like 'creating timeline events'
end
describe 'usage data' do
@@ -67,7 +102,7 @@ RSpec.describe ResourceEvents::ChangeLabelsService do
it 'tracks changed labels' do
expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_label_changed_action)
- subject
+ change_labels
end
end
@@ -75,9 +110,10 @@ RSpec.describe ResourceEvents::ChangeLabelsService do
let(:resource) { create(:merge_request, source_project: project) }
it 'does not track changed labels' do
- expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_label_changed_action)
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter)
+ .not_to receive(:track_issue_label_changed_action)
- subject
+ change_labels
end
end
end
diff --git a/spec/services/search/group_service_spec.rb b/spec/services/search/group_service_spec.rb
index 7beeec98b23..152d0700cc1 100644
--- a/spec/services/search/group_service_spec.rb
+++ b/spec/services/search/group_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Search::GroupService do
# These projects shouldn't be found
let!(:outside_project) { create(:project, :public, name: "Outside #{term}") }
- let!(:private_project) { create(:project, :private, namespace: nested_group, name: "Private #{term}" )}
+ let!(:private_project) { create(:project, :private, namespace: nested_group, name: "Private #{term}" ) }
let!(:other_project) { create(:project, :public, namespace: nested_group, name: term.reverse) }
# These projects should be found
diff --git a/spec/services/security/ci_configuration/sast_parser_service_spec.rb b/spec/services/security/ci_configuration/sast_parser_service_spec.rb
index 4346d0a9e07..1fd196cdcee 100644
--- a/spec/services/security/ci_configuration/sast_parser_service_spec.rb
+++ b/spec/services/security/ci_configuration/sast_parser_service_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe Security::CiConfiguration::SastParserService do
let(:bandit) { configuration['analyzers'][0] }
let(:brakeman) { configuration['analyzers'][1] }
let(:sast_brakeman_level) { brakeman['variables'][0] }
+ let(:secure_analyzers_prefix) { '$CI_TEMPLATE_REGISTRY_HOST/security-products' }
it 'parses the configuration for SAST' do
expect(secure_analyzers['default_value']).to eql(secure_analyzers_prefix)
diff --git a/spec/services/snippets/update_service_spec.rb b/spec/services/snippets/update_service_spec.rb
index f61d33e2436..67cc258b4b6 100644
--- a/spec/services/snippets/update_service_spec.rb
+++ b/spec/services/snippets/update_service_spec.rb
@@ -140,7 +140,7 @@ RSpec.describe Snippets::UpdateService do
context 'when snippet_actions param is used' do
let(:file_path) { 'CHANGELOG' }
- let(:created_file_path) { 'New file'}
+ let(:created_file_path) { 'New file' }
let(:content) { 'foobar' }
let(:snippet_actions) { [{ action: :move, previous_path: snippet.file_name, file_path: file_path }, { action: :create, file_path: created_file_path, content: content }] }
let(:base_opts) do
diff --git a/spec/services/suggestions/apply_service_spec.rb b/spec/services/suggestions/apply_service_spec.rb
index 6052882813e..e34324d5fe2 100644
--- a/spec/services/suggestions/apply_service_spec.rb
+++ b/spec/services/suggestions/apply_service_spec.rb
@@ -359,7 +359,7 @@ RSpec.describe Suggestions::ApplyService do
end
context 'multiple suggestions' do
- let(:author_emails) { suggestions.map {|s| s.note.author.commit_email_or_default } }
+ let(:author_emails) { suggestions.map { |s| s.note.author.commit_email_or_default } }
let(:first_author) { suggestion.note.author }
let(:commit) { project.repository.commit }
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 741d136b9a0..a192fae27db 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -134,15 +134,15 @@ RSpec.describe SystemNoteService do
end
end
- describe '.change_due_date' do
- let(:due_date) { double }
+ describe '.change_start_date_or_due_date' do
+ let(:changed_dates) { double }
it 'calls TimeTrackingService' do
expect_next_instance_of(::SystemNotes::TimeTrackingService) do |service|
- expect(service).to receive(:change_due_date).with(due_date)
+ expect(service).to receive(:change_start_date_or_due_date).with(changed_dates)
end
- described_class.change_due_date(noteable, project, author, due_date)
+ described_class.change_start_date_or_due_date(noteable, project, author, changed_dates)
end
end
@@ -159,30 +159,6 @@ RSpec.describe SystemNoteService do
end
end
- describe '.request_attention' do
- let(:user) { double }
-
- it 'calls IssuableService' do
- expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
- expect(service).to receive(:request_attention).with(user)
- end
-
- described_class.request_attention(noteable, project, author, user)
- end
- end
-
- describe '.remove_attention_request' do
- let(:user) { double }
-
- it 'calls IssuableService' do
- expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
- expect(service).to receive(:remove_attention_request).with(user)
- end
-
- described_class.remove_attention_request(noteable, project, author, user)
- end
- end
-
describe '.merge_when_pipeline_succeeds' do
it 'calls MergeRequestsService' do
sha = double
@@ -375,13 +351,14 @@ RSpec.describe SystemNoteService do
describe '.noteable_cloned' do
let(:noteable_ref) { double }
let(:direction) { double }
+ let(:created_at) { double }
it 'calls IssuableService' do
expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
- expect(service).to receive(:noteable_cloned).with(noteable_ref, direction)
+ expect(service).to receive(:noteable_cloned).with(noteable_ref, direction, created_at: created_at)
end
- described_class.noteable_cloned(double, double, noteable_ref, double, direction: direction)
+ described_class.noteable_cloned(double, double, noteable_ref, double, direction: direction, created_at: created_at)
end
end
@@ -431,9 +408,22 @@ RSpec.describe SystemNoteService do
end
end
+ describe '.created_timelog' do
+ let(:issue) { create(:issue, project: project) }
+ let(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800) }
+
+ it 'calls TimeTrackingService' do
+ expect_next_instance_of(::SystemNotes::TimeTrackingService) do |service|
+ expect(service).to receive(:created_timelog)
+ end
+
+ described_class.created_timelog(noteable, project, author, timelog)
+ end
+ end
+
describe '.remove_timelog' do
let(:issue) { create(:issue, project: project) }
- let(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800)}
+ let(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800) }
it 'calls TimeTrackingService' do
expect_next_instance_of(::SystemNotes::TimeTrackingService) do |service|
@@ -742,4 +732,38 @@ RSpec.describe SystemNoteService do
described_class.delete_timeline_event(noteable, author)
end
end
+
+ describe '.relate_work_item' do
+ let(:work_item) { double('work_item', issue_type: :task) }
+ let(:noteable) { double }
+
+ before do
+ allow(noteable).to receive(:project).and_return(double)
+ end
+
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:hierarchy_changed).with(work_item, 'relate')
+ end
+
+ described_class.relate_work_item(noteable, work_item, double)
+ end
+ end
+
+ describe '.unrelate_wotk_item' do
+ let(:work_item) { double('work_item', issue_type: :task) }
+ let(:noteable) { double }
+
+ before do
+ allow(noteable).to receive(:project).and_return(double)
+ end
+
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:hierarchy_changed).with(work_item, 'unrelate')
+ end
+
+ described_class.unrelate_work_item(noteable, work_item, double)
+ end
+ end
end
diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb
index 5bc7ea82976..b2ccd9dba52 100644
--- a/spec/services/system_notes/issuables_service_spec.rb
+++ b/spec/services/system_notes/issuables_service_spec.rb
@@ -247,42 +247,6 @@ RSpec.describe ::SystemNotes::IssuablesService do
end
end
- describe '#request_attention' do
- subject { service.request_attention(user) }
-
- let(:user) { create(:user) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'attention_requested' }
- end
-
- context 'when attention requested' do
- it_behaves_like 'a note with overridable created_at'
-
- it 'sets the note text' do
- expect(subject.note).to eq "requested attention from @#{user.username}"
- end
- end
- end
-
- describe '#remove_attention_request' do
- subject { service.remove_attention_request(user) }
-
- let(:user) { create(:user) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'attention_request_removed' }
- end
-
- context 'when attention request is removed' do
- it_behaves_like 'a note with overridable created_at'
-
- it 'sets the note text' do
- expect(subject.note).to eq "removed attention request from @#{user.username}"
- end
- end
- end
-
describe '#change_title' do
let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum') }
@@ -559,8 +523,8 @@ RSpec.describe ::SystemNotes::IssuablesService do
let(:action) { 'task' }
end
- it "posts the 'marked the task as complete' system note" do
- expect(subject.note).to eq("marked the task **task** as completed")
+ it "posts the 'marked the checklist item as complete' system note" do
+ expect(subject.note).to eq("marked the checklist item **task** as completed")
end
end
@@ -625,8 +589,8 @@ RSpec.describe ::SystemNotes::IssuablesService do
end
describe '#noteable_cloned' do
- let(:new_project) { create(:project) }
- let(:new_noteable) { create(:issue, project: new_project) }
+ let_it_be(:new_project) { create(:project) }
+ let_it_be(:new_noteable) { create(:issue, project: new_project) }
subject do
service.noteable_cloned(new_noteable, direction)
@@ -684,6 +648,22 @@ RSpec.describe ::SystemNotes::IssuablesService do
end
end
+ context 'custom created timestamp' do
+ let(:direction) { :from }
+
+ it 'allows setting of custom created_at value' do
+ timestamp = 1.day.ago
+
+ note = service.noteable_cloned(new_noteable, direction, created_at: timestamp)
+
+ expect(note.created_at).to be_like_time(timestamp)
+ end
+
+ it 'defaults to current time when created_at is not given', :freeze_time do
+ expect(subject.created_at).to be_like_time(Time.current)
+ end
+ end
+
context 'metrics' do
context 'cloned from' do
let(:direction) { :from }
@@ -696,15 +676,20 @@ RSpec.describe ::SystemNotes::IssuablesService do
end
end
- context 'cloned to' do
+ context 'cloned to', :snowplow do
let(:direction) { :to }
it 'tracks usage' do
expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter)
- .to receive(:track_issue_cloned_action).with(author: author)
+ .to receive(:track_issue_cloned_action).with(author: author, project: project )
subject
end
+
+ it_behaves_like 'issue_edit snowplow tracking' do
+ let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_CLONED }
+ let(:user) { author }
+ end
end
end
end
@@ -886,4 +871,43 @@ RSpec.describe ::SystemNotes::IssuablesService do
it { expect(subject.note).to eq "changed issue type to incident" }
end
+
+ describe '#hierarchy_changed' do
+ let_it_be_with_reload(:work_item) { create(:work_item, project: project) }
+ let_it_be_with_reload(:task) { create(:work_item, :task, project: project) }
+
+ let(:service) { described_class.new(noteable: work_item, project: project, author: author) }
+
+ subject { service.hierarchy_changed(task, hierarchy_change_action) }
+
+ context 'when task is added as a child' do
+ let(:hierarchy_change_action) { 'relate' }
+
+ it_behaves_like 'a system note' do
+ let(:expected_noteable) { task }
+ let(:action) { 'relate_to_parent' }
+ end
+
+ it 'sets the correct note text' do
+ expect { subject }.to change { Note.system.count }.by(2)
+ expect(work_item.notes.last.note).to eq("added ##{task.iid} as child task")
+ expect(task.notes.last.note).to eq("added ##{work_item.iid} as parent issue")
+ end
+ end
+
+ context 'when child task is removed' do
+ let(:hierarchy_change_action) { 'unrelate' }
+
+ it_behaves_like 'a system note' do
+ let(:expected_noteable) { task }
+ let(:action) { 'unrelate_from_parent' }
+ end
+
+ it 'sets the correct note text' do
+ expect { subject }.to change { Note.system.count }.by(2)
+ expect(work_item.notes.last.note).to eq("removed child task ##{task.iid}")
+ expect(task.notes.last.note).to eq("removed parent issue ##{work_item.iid}")
+ end
+ end
+ end
end
diff --git a/spec/services/system_notes/merge_requests_service_spec.rb b/spec/services/system_notes/merge_requests_service_spec.rb
index 58d2489f878..3e66ccef106 100644
--- a/spec/services/system_notes/merge_requests_service_spec.rb
+++ b/spec/services/system_notes/merge_requests_service_spec.rb
@@ -167,8 +167,8 @@ RSpec.describe ::SystemNotes::MergeRequestsService do
end
describe '.change_branch' do
- let(:old_branch) { 'old_branch'}
- let(:new_branch) { 'new_branch'}
+ let(:old_branch) { 'old_branch' }
+ let(:new_branch) { 'new_branch' }
it_behaves_like 'a system note' do
let(:action) { 'branch' }
diff --git a/spec/services/system_notes/time_tracking_service_spec.rb b/spec/services/system_notes/time_tracking_service_spec.rb
index fdf18f4f29a..33608deaa64 100644
--- a/spec/services/system_notes/time_tracking_service_spec.rb
+++ b/spec/services/system_notes/time_tracking_service_spec.rb
@@ -3,35 +3,112 @@
require 'spec_helper'
RSpec.describe ::SystemNotes::TimeTrackingService do
- let_it_be(:author) { create(:user) }
- let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:author) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
- describe '#change_due_date' do
- subject { described_class.new(noteable: noteable, project: project, author: author).change_due_date(due_date) }
+ describe '#change_start_date_or_due_date' do
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:work_item) { create(:work_item, project: project) }
- let(:due_date) { Date.today }
+ subject(:note) { described_class.new(noteable: noteable, project: project, author: author).change_start_date_or_due_date(changed_dates) }
- context 'when noteable is an issue' do
- let_it_be(:noteable) { create(:issue, project: project) }
+ let(:start_date) { Date.today }
+ let(:due_date) { 1.week.from_now.to_date }
+ let(:changed_dates) { { 'due_date' => [nil, due_date], 'start_date' => [nil, start_date] } }
+ shared_examples 'issuable getting date change notes' do
it_behaves_like 'a note with overridable created_at'
it_behaves_like 'a system note' do
- let(:action) { 'due_date' }
+ let(:action) { 'start_date_or_due_date' }
end
- context 'when due date added' do
- it 'sets the note text' do
- expect(subject.note).to eq "changed due date to #{due_date.to_s(:long)}"
+ context 'when both dates are added' do
+ it 'sets the correct note message' do
+ expect(note.note).to eq("changed start date to #{start_date.to_s(:long)} and changed due date to #{due_date.to_s(:long)}")
end
end
- context 'when due date removed' do
- let(:due_date) { nil }
+ context 'when both dates are removed' do
+ let(:changed_dates) { { 'due_date' => [due_date, nil], 'start_date' => [start_date, nil] } }
- it 'sets the note text' do
- expect(subject.note).to eq 'removed due date'
+ before do
+ noteable.update!(start_date: start_date, due_date: due_date)
+ end
+
+ it 'sets the correct note message' do
+ expect(note.note).to eq('removed start date and removed due date')
+ end
+ end
+
+ context 'when due date is added' do
+ let(:changed_dates) { { 'due_date' => [nil, due_date] } }
+
+ it 'sets the correct note message' do
+ expect(note.note).to eq("changed due date to #{due_date.to_s(:long)}")
+ end
+
+ it 'tracks the issue event in usage ping' do
+ expect(activity_counter_class).to receive(activity_counter_method).with(author: author)
+
+ subject
end
+
+ context 'and start date removed' do
+ let(:changed_dates) { { 'due_date' => [nil, due_date], 'start_date' => [start_date, nil] } }
+
+ it 'sets the correct note message' do
+ expect(note.note).to eq("removed start date and changed due date to #{due_date.to_s(:long)}")
+ end
+ end
+ end
+
+ context 'when start_date is added' do
+ let(:changed_dates) { { 'start_date' => [nil, start_date] } }
+
+ it 'does not track the issue event in usage ping' do
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_due_date_changed_action)
+
+ subject
+ end
+
+ it 'sets the correct note message' do
+ expect(note.note).to eq("changed start date to #{start_date.to_s(:long)}")
+ end
+
+ context 'and due date removed' do
+ let(:changed_dates) { { 'due_date' => [due_date, nil], 'start_date' => [nil, start_date] } }
+
+ it 'sets the correct note message' do
+ expect(note.note).to eq("changed start date to #{start_date.to_s(:long)} and removed due date")
+ end
+ end
+ end
+
+ context 'when no dates are changed' do
+ let(:changed_dates) { {} }
+
+ it 'does not create a note and returns nil' do
+ expect do
+ note
+ end.to not_change(Note, :count)
+
+ expect(note).to be_nil
+ end
+ end
+ end
+
+ context 'when noteable is an issue' do
+ let(:noteable) { issue }
+ let(:activity_counter_class) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter }
+ let(:activity_counter_method) { :track_issue_due_date_changed_action }
+
+ it_behaves_like 'issuable getting date change notes'
+
+ it 'does not track the work item event in usage ping' do
+ expect(Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter).not_to receive(:track_work_item_date_changed_action)
+
+ subject
end
it 'tracks the issue event in usage ping' do
@@ -39,13 +116,48 @@ RSpec.describe ::SystemNotes::TimeTrackingService do
subject
end
+
+ context 'when only start_date is added' do
+ let(:changed_dates) { { 'start_date' => [nil, start_date] } }
+
+ it 'does not track the issue event in usage ping' do
+ expect(activity_counter_class).not_to receive(activity_counter_method)
+
+ subject
+ end
+ end
+ end
+
+ context 'when noteable is a work item' do
+ let(:noteable) { work_item }
+ let(:activity_counter_class) { Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter }
+ let(:activity_counter_method) { :track_work_item_date_changed_action }
+
+ it_behaves_like 'issuable getting date change notes'
+
+ it 'does not track the issue event in usage ping' do
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_due_date_changed_action)
+
+ subject
+ end
+
+ context 'when only start_date is added' do
+ let(:changed_dates) { { 'start_date' => [nil, start_date] } }
+
+ it 'tracks the issue event in usage ping' do
+ expect(activity_counter_class).to receive(activity_counter_method).with(author: author)
+
+ subject
+ end
+ end
end
context 'when noteable is a merge request' do
- let_it_be(:noteable) { create(:merge_request, source_project: project) }
+ let(:noteable) { create(:merge_request, source_project: project) }
it 'does not track the issue event in usage ping' do
- expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_due_date_changed_action).with(author: author)
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_due_date_changed_action)
+ expect(Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter).not_to receive(:track_work_item_date_changed_action)
subject
end
@@ -106,13 +218,37 @@ RSpec.describe ::SystemNotes::TimeTrackingService do
end
end
+ describe '#create_timelog' do
+ subject { described_class.new(noteable: noteable, project: project, author: author).created_timelog(timelog) }
+
+ context 'when the timelog has a positive time spent value' do
+ let_it_be(:noteable, reload: true) { create(:issue, project: project) }
+
+ let(:timelog) { create(:timelog, user: author, issue: noteable, time_spent: 1800, spent_at: '2022-03-30T00:00:00.000Z') }
+
+ it 'sets the note text' do
+ expect(subject.note).to eq "added 30m of time spent at 2022-03-30"
+ end
+ end
+
+ context 'when the timelog has a negative time spent value' do
+ let_it_be(:noteable, reload: true) { create(:issue, project: project) }
+
+ let(:timelog) { create(:timelog, user: author, issue: noteable, time_spent: -1800, spent_at: '2022-03-30T00:00:00.000Z') }
+
+ it 'sets the note text' do
+ expect(subject.note).to eq "subtracted 30m of time spent at 2022-03-30"
+ end
+ end
+ end
+
describe '#remove_timelog' do
subject { described_class.new(noteable: noteable, project: project, author: author).remove_timelog(timelog) }
context 'when the timelog has a positive time spent value' do
let_it_be(:noteable, reload: true) { create(:issue, project: project) }
- let(:timelog) { create(:timelog, user: author, issue: noteable, time_spent: 1800, spent_at: '2022-03-30T00:00:00.000Z')}
+ let(:timelog) { create(:timelog, user: author, issue: noteable, time_spent: 1800, spent_at: '2022-03-30T00:00:00.000Z') }
it 'sets the note text' do
expect(subject.note).to eq "deleted 30m of spent time from 2022-03-30"
@@ -122,7 +258,7 @@ RSpec.describe ::SystemNotes::TimeTrackingService do
context 'when the timelog has a negative time spent value' do
let_it_be(:noteable, reload: true) { create(:issue, project: project) }
- let(:timelog) { create(:timelog, user: author, issue: noteable, time_spent: -1800, spent_at: '2022-03-30T00:00:00.000Z')}
+ let(:timelog) { create(:timelog, user: author, issue: noteable, time_spent: -1800, spent_at: '2022-03-30T00:00:00.000Z') }
it 'sets the note text' do
expect(subject.note).to eq "deleted -30m of spent time from 2022-03-30"
diff --git a/spec/services/terraform/remote_state_handler_spec.rb b/spec/services/terraform/remote_state_handler_spec.rb
index 19c1d4109e9..369309e4d5a 100644
--- a/spec/services/terraform/remote_state_handler_spec.rb
+++ b/spec/services/terraform/remote_state_handler_spec.rb
@@ -171,7 +171,7 @@ RSpec.describe Terraform::RemoteStateHandler do
end
context 'with no lock ID (force-unlock)' do
- let(:lock_id) { }
+ let(:lock_id) {}
it 'unlocks the state' do
state = handler.unlock!
diff --git a/spec/services/timelogs/create_service_spec.rb b/spec/services/timelogs/create_service_spec.rb
new file mode 100644
index 00000000000..b5ed4a005c7
--- /dev/null
+++ b/spec/services/timelogs/create_service_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Timelogs::CreateService do
+ let_it_be(:author) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:time_spent) { 3600 }
+ let_it_be(:spent_at) { "2022-07-08" }
+ let_it_be(:summary) { "Test summary" }
+
+ let(:issuable) { nil }
+ let(:users_container) { project }
+ let(:service) { described_class.new(issuable, time_spent, spent_at, summary, user) }
+
+ describe '#execute' do
+ subject { service.execute }
+
+ context 'when issuable is an Issue' do
+ let_it_be(:issuable) { create(:issue, project: project) }
+ let_it_be(:note_noteable) { create(:issue, project: project) }
+
+ it_behaves_like 'issuable supports timelog creation service'
+ end
+
+ context 'when issuable is a MergeRequest' do
+ let_it_be(:issuable) { create(:merge_request, source_project: project, source_branch: 'branch-1') }
+ let_it_be(:note_noteable) { create(:merge_request, source_project: project, source_branch: 'branch-2') }
+
+ it_behaves_like 'issuable supports timelog creation service'
+ end
+
+ context 'when issuable is a WorkItem' do
+ let_it_be(:issuable) { create(:work_item, project: project, title: 'WorkItem-1') }
+ let_it_be(:note_noteable) { create(:work_item, project: project, title: 'WorkItem-2') }
+
+ it_behaves_like 'issuable supports timelog creation service'
+ end
+
+ context 'when issuable is an Incident' do
+ let_it_be(:issuable) { create(:incident, project: project) }
+ let_it_be(:note_noteable) { create(:incident, project: project) }
+
+ it_behaves_like 'issuable supports timelog creation service'
+ end
+ end
+end
diff --git a/spec/services/timelogs/delete_service_spec.rb b/spec/services/timelogs/delete_service_spec.rb
index c52cebdc5bf..ee1133af6b3 100644
--- a/spec/services/timelogs/delete_service_spec.rb
+++ b/spec/services/timelogs/delete_service_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Timelogs::DeleteService do
let_it_be(:author) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:issue) { create(:issue, project: project) }
- let_it_be(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800)}
+ let_it_be(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800) }
let(:service) { described_class.new(timelog, user) }
@@ -21,8 +21,8 @@ RSpec.describe Timelogs::DeleteService do
end
it 'returns the removed timelog' do
- expect(subject).to be_success
- expect(subject.payload).to eq(timelog)
+ is_expected.to be_success
+ expect(subject.payload[:timelog]).to eq(timelog)
end
end
@@ -31,7 +31,7 @@ RSpec.describe Timelogs::DeleteService do
let!(:timelog) { nil }
it 'returns an error' do
- expect(subject).to be_error
+ is_expected.to be_error
expect(subject.message).to eq('Timelog doesn\'t exist or you don\'t have permission to delete it')
expect(subject.http_status).to eq(404)
end
@@ -41,7 +41,7 @@ RSpec.describe Timelogs::DeleteService do
let(:user) { create(:user) }
it 'returns an error' do
- expect(subject).to be_error
+ is_expected.to be_error
expect(subject.message).to eq('Timelog doesn\'t exist or you don\'t have permission to delete it')
expect(subject.http_status).to eq(404)
end
@@ -49,14 +49,14 @@ RSpec.describe Timelogs::DeleteService do
context 'when the timelog deletion fails' do
let(:user) { author }
- let!(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800)}
+ let!(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800) }
before do
allow(timelog).to receive(:destroy).and_return(false)
end
it 'returns an error' do
- expect(subject).to be_error
+ is_expected.to be_error
expect(subject.message).to eq('Failed to remove timelog')
expect(subject.http_status).to eq(400)
end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 1cb44366457..45a8268043f 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -207,7 +207,7 @@ RSpec.describe TodoService do
end
it_behaves_like 'an incident management tracked event', :incident_management_incident_todo do
- let(:current_user) { john_doe}
+ let(:current_user) { john_doe }
end
end
end
@@ -1139,7 +1139,7 @@ RSpec.describe TodoService do
it 'updates related todos for the user with the new_state' do
method_call
- expect(collection.all? { |todo| todo.reload.state?(new_state)}).to be_truthy
+ expect(collection.all? { |todo| todo.reload.state?(new_state) }).to be_truthy
end
if new_resolved_by
@@ -1250,17 +1250,6 @@ RSpec.describe TodoService do
end
end
- describe '#create_attention_requested_todo' do
- let(:target) { create(:merge_request, author: author, source_project: project) }
- let(:user) { create(:user) }
-
- it 'creates a todo for user' do
- service.create_attention_requested_todo(target, author, user)
-
- should_create_todo(user: user, target: target, action: Todo::ATTENTION_REQUESTED)
- end
- end
-
def should_create_todo(attributes = {})
attributes.reverse_merge!(
project: project,
diff --git a/spec/services/todos/destroy/design_service_spec.rb b/spec/services/todos/destroy/design_service_spec.rb
index 61a6718dc9d..92b25d94dc6 100644
--- a/spec/services/todos/destroy/design_service_spec.rb
+++ b/spec/services/todos/destroy/design_service_spec.rb
@@ -9,8 +9,8 @@ RSpec.describe Todos::Destroy::DesignService do
let_it_be(:design_2) { create(:design) }
let_it_be(:design_3) { create(:design) }
- let_it_be(:create_action) { create(:design_action, design: design)}
- let_it_be(:create_action_2) { create(:design_action, design: design_2)}
+ let_it_be(:create_action) { create(:design_action, design: design) }
+ let_it_be(:create_action_2) { create(:design_action, design: design_2) }
describe '#execute' do
before do
@@ -23,8 +23,8 @@ RSpec.describe Todos::Destroy::DesignService do
subject { described_class.new([design.id, design_2.id, design_3.id]).execute }
context 'when the design has been archived' do
- let_it_be(:archive_action) { create(:design_action, design: design, event: :deletion)}
- let_it_be(:archive_action_2) { create(:design_action, design: design_3, event: :deletion)}
+ let_it_be(:archive_action) { create(:design_action, design: design, event: :deletion) }
+ let_it_be(:archive_action_2) { create(:design_action, design: design_3, event: :deletion) }
it 'removes todos for that design' do
expect { subject }.to change { Todo.count }.from(4).to(1)
diff --git a/spec/services/todos/destroy/destroyed_issuable_service_spec.rb b/spec/services/todos/destroy/destroyed_issuable_service_spec.rb
index 24f74bae7c8..6d6abe06d1c 100644
--- a/spec/services/todos/destroy/destroyed_issuable_service_spec.rb
+++ b/spec/services/todos/destroy/destroyed_issuable_service_spec.rb
@@ -4,31 +4,46 @@ require 'spec_helper'
RSpec.describe Todos::Destroy::DestroyedIssuableService do
describe '#execute' do
- let_it_be(:target) { create(:merge_request) }
- let_it_be(:pending_todo) { create(:todo, :pending, project: target.project, target: target, user: create(:user)) }
- let_it_be(:done_todo) { create(:todo, :done, project: target.project, target: target, user: create(:user)) }
+ let_it_be(:user) { create(:user) }
- def execute
- described_class.new(target.id, target.class.name).execute
- end
+ subject { described_class.new(target.id, target.class.name).execute }
+
+ context 'when target is merge request' do
+ let_it_be(:target) { create(:merge_request) }
+ let_it_be(:pending_todo) { create(:todo, :pending, project: target.project, target: target, user: user) }
+ let_it_be(:done_todo) { create(:todo, :done, project: target.project, target: target, user: user) }
- it 'deletes todos for specified target ID and type' do
- control_count = ActiveRecord::QueryRecorder.new { execute }.count
+ it 'deletes todos for specified target ID and type' do
+ control_count = ActiveRecord::QueryRecorder.new { subject }.count
- # Create more todos for the target
- create(:todo, :pending, project: target.project, target: target, user: create(:user))
- create(:todo, :pending, project: target.project, target: target, user: create(:user))
- create(:todo, :done, project: target.project, target: target, user: create(:user))
- create(:todo, :done, project: target.project, target: target, user: create(:user))
+ # Create more todos for the target
+ create(:todo, :pending, project: target.project, target: target, user: user)
+ create(:todo, :pending, project: target.project, target: target, user: user)
+ create(:todo, :done, project: target.project, target: target, user: user)
+ create(:todo, :done, project: target.project, target: target, user: user)
- expect { execute }.not_to exceed_query_limit(control_count)
- expect(target.reload.todos.count).to eq(0)
+ expect { subject }.not_to exceed_query_limit(control_count)
+ end
+
+ it 'invalidates todos cache counts of todo users', :use_clean_rails_redis_caching do
+ expect { subject }
+ .to change { pending_todo.user.todos_pending_count }.from(1).to(0)
+ .and change { done_todo.user.todos_done_count }.from(1).to(0)
+ end
end
- it 'invalidates todos cache counts of todo users', :use_clean_rails_redis_caching do
- expect { execute }
- .to change { pending_todo.user.todos_pending_count }.from(1).to(0)
- .and change { done_todo.user.todos_done_count }.from(1).to(0)
+ context 'when target is an work item' do
+ let_it_be(:target) { create(:work_item) }
+ let_it_be(:todo1) { create(:todo, :pending, project: target.project, target: target, user: user) }
+ let_it_be(:todo2) { create(:todo, :done, project: target.project, target: target, user: user) }
+ # rubocop: disable Cop/AvoidBecomes
+ let_it_be(:todo3) { create(:todo, :pending, project: target.project, target: target.becomes(Issue), user: user) }
+ let_it_be(:todo4) { create(:todo, :done, project: target.project, target: target.becomes(Issue), user: user) }
+ # rubocop: enable Cop/AvoidBecomes
+
+ it 'deletes todos' do
+ expect { subject }.to change(Todo, :count).by(-4)
+ end
end
end
end
diff --git a/spec/services/topics/merge_service_spec.rb b/spec/services/topics/merge_service_spec.rb
new file mode 100644
index 00000000000..971917eb8e9
--- /dev/null
+++ b/spec/services/topics/merge_service_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Topics::MergeService do
+ let_it_be(:source_topic) { create(:topic, name: 'source_topic') }
+ let_it_be(:target_topic) { create(:topic, name: 'target_topic') }
+ let_it_be(:project_1) { create(:project, :public, topic_list: source_topic.name ) }
+ let_it_be(:project_2) { create(:project, :private, topic_list: source_topic.name ) }
+ let_it_be(:project_3) { create(:project, :public, topic_list: target_topic.name ) }
+ let_it_be(:project_4) { create(:project, :public, topic_list: [source_topic.name, target_topic.name] ) }
+
+ subject { described_class.new(source_topic, target_topic).execute }
+
+ describe '#execute' do
+ it 'merges source topic into target topic' do
+ subject
+
+ expect(target_topic.projects).to contain_exactly(project_1, project_2, project_3, project_4)
+ expect { source_topic.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'refreshes counters of target topic' do
+ expect { subject }
+ .to change { target_topic.reload.total_projects_count }.by(2)
+ .and change { target_topic.reload.non_private_projects_count }.by(1)
+ end
+
+ context 'when source topic fails to delete' do
+ it 'reverts previous changes' do
+ allow(source_topic.reload).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)
+
+ expect { subject }.to raise_error(ActiveRecord::RecordNotDestroyed)
+
+ expect(source_topic.projects).to contain_exactly(project_1, project_2, project_4)
+ expect(target_topic.projects).to contain_exactly(project_3, project_4)
+ end
+ end
+
+ context 'for parameter validation' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { described_class.new(source_topic_parameter, target_topic_parameter).execute }
+
+ where(:source_topic_parameter, :target_topic_parameter, :expected_message) do
+ nil | ref(:target_topic) | 'The source topic is not a topic.'
+ ref(:source_topic) | nil | 'The target topic is not a topic.'
+ ref(:target_topic) | ref(:target_topic) | 'The source topic and the target topic are identical.' # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
+ end
+
+ with_them do
+ it 'raises correct error' do
+ expect { subject }.to raise_error(ArgumentError) do |error|
+ expect(error.message).to eq(expected_message)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/uploads/destroy_service_spec.rb b/spec/services/uploads/destroy_service_spec.rb
new file mode 100644
index 00000000000..bb58da231b6
--- /dev/null
+++ b/spec/services/uploads/destroy_service_spec.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Uploads::DestroyService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be_with_reload(:upload) { create(:upload, :issuable_upload, model: project) }
+
+ let(:filename) { File.basename(upload.path) }
+ let(:secret) { upload.secret }
+ let(:model) { project }
+ let(:service) { described_class.new(model, user) }
+
+ describe '#execute' do
+ subject { service.execute(secret, filename) }
+
+ shared_examples_for 'upload not found' do
+ it 'does not delete any upload' do
+ expect { subject }.not_to change { Upload.count }
+ end
+
+ it 'returns an error' do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to eq("The resource that you are attempting to access does not "\
+ "exist or you don't have permission to perform this action.")
+ end
+ end
+
+ context 'when user is nil' do
+ let(:user) { nil }
+
+ it_behaves_like 'upload not found'
+ end
+
+ context 'when user cannot destroy upload' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'upload not found'
+ end
+
+ context 'when user can destroy upload' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'deletes the upload' do
+ expect { subject }.to change { Upload.count }.by(-1)
+ end
+
+ it 'returns success response' do
+ expect(subject[:status]).to eq(:success)
+ expect(subject[:upload]).to eq(upload)
+ end
+
+ context 'when upload is not found' do
+ let(:filename) { 'not existing filename' }
+
+ it_behaves_like 'upload not found'
+ end
+
+ context 'when upload secret is not found' do
+ let(:secret) { 'aaaaaaaaaa' }
+
+ it_behaves_like 'upload not found'
+ end
+
+ context 'when upload secret has invalid format' do
+ let(:secret) { 'invalid' }
+
+ it_behaves_like 'upload not found'
+ end
+
+ context 'when unknown model is used' do
+ let(:model) { user }
+
+ it 'raises an error' do
+ expect { subject }.to raise_exception(ArgumentError)
+ end
+ end
+
+ context 'when upload belongs to other model' do
+ let_it_be(:upload) { create(:upload, :namespace_upload) }
+
+ it_behaves_like 'upload not found'
+ end
+
+ context 'when upload destroy fails' do
+ before do
+ allow(service).to receive(:find_upload).and_return(upload)
+ allow(upload).to receive(:destroy).and_return(false)
+ end
+
+ it 'returns error' do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to eq('Upload could not be deleted.')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/users/create_service_spec.rb b/spec/services/users/create_service_spec.rb
index 74340bac055..f3c9701c556 100644
--- a/spec/services/users/create_service_spec.rb
+++ b/spec/services/users/create_service_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Users::CreateService do
describe '#execute' do
+ let(:password) { User.random_password }
let(:admin_user) { create(:admin) }
context 'with an admin user' do
@@ -12,7 +13,7 @@ RSpec.describe Users::CreateService do
context 'when required parameters are provided' do
let(:params) do
- { name: 'John Doe', username: 'jduser', email: email, password: 'mydummypass' }
+ { name: 'John Doe', username: 'jduser', email: email, password: password }
end
it 'returns a persisted user' do
@@ -82,13 +83,13 @@ RSpec.describe Users::CreateService do
context 'when force_random_password parameter is true' do
let(:params) do
- { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', force_random_password: true }
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: password, force_random_password: true }
end
it 'generates random password' do
user = service.execute
- expect(user.password).not_to eq 'mydummypass'
+ expect(user.password).not_to eq password
expect(user.password).to be_present
end
end
@@ -99,7 +100,7 @@ RSpec.describe Users::CreateService do
name: 'John Doe',
username: 'jduser',
email: 'jd@example.com',
- password: 'mydummypass',
+ password: password,
password_automatically_set: true
}
end
@@ -121,7 +122,7 @@ RSpec.describe Users::CreateService do
context 'when skip_confirmation parameter is true' do
let(:params) do
- { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', skip_confirmation: true }
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: password, skip_confirmation: true }
end
it 'confirms the user' do
@@ -131,7 +132,7 @@ RSpec.describe Users::CreateService do
context 'when reset_password parameter is true' do
let(:params) do
- { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', reset_password: true }
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: password, reset_password: true }
end
it 'resets password even if a password parameter is given' do
@@ -152,7 +153,7 @@ RSpec.describe Users::CreateService do
context 'with nil user' do
let(:params) do
- { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', skip_confirmation: true }
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: password, skip_confirmation: true }
end
let(:service) { described_class.new(nil, params) }
diff --git a/spec/services/users/dismiss_namespace_callout_service_spec.rb b/spec/services/users/dismiss_namespace_callout_service_spec.rb
new file mode 100644
index 00000000000..fbcdb66c9e8
--- /dev/null
+++ b/spec/services/users/dismiss_namespace_callout_service_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Users::DismissNamespaceCalloutService do
+ describe '#execute' do
+ let_it_be(:user) { create(:user) }
+
+ let(:params) { { feature_name: feature_name, namespace_id: user.namespace.id } }
+ let(:feature_name) { Users::NamespaceCallout.feature_names.each_key.first }
+
+ subject(:execute) do
+ described_class.new(
+ container: nil, current_user: user, params: params
+ ).execute
+ end
+
+ it_behaves_like 'dismissing user callout', Users::NamespaceCallout
+
+ it 'sets the namespace_id' do
+ expect(execute.namespace_id).to eq(user.namespace.id)
+ end
+ end
+end
diff --git a/spec/services/users/dismiss_project_callout_service_spec.rb b/spec/services/users/dismiss_project_callout_service_spec.rb
new file mode 100644
index 00000000000..73e50a4c37d
--- /dev/null
+++ b/spec/services/users/dismiss_project_callout_service_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Users::DismissProjectCalloutService do
+ describe '#execute' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ let(:params) { { feature_name: feature_name, project_id: project.id } }
+ let(:feature_name) { Users::ProjectCallout.feature_names.each_key.first }
+
+ subject(:execute) do
+ described_class.new(
+ container: nil, current_user: user, params: params
+ ).execute
+ end
+
+ it_behaves_like 'dismissing user callout', Users::ProjectCallout
+
+ it 'sets the project_id' do
+ expect(execute.project_id).to eq(project.id)
+ end
+ end
+end
diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb
index 52c7b54ed72..411cd7316d8 100644
--- a/spec/services/users/update_service_spec.rb
+++ b/spec/services/users/update_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Users::UpdateService do
- let(:password) { 'longsecret987!' }
+ let(:password) { User.random_password }
let(:user) { create(:user, password: password, password_confirmation: password) }
describe '#execute' do
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 339ffc44e4d..fed3ae7a543 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -190,7 +190,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
end
context 'when auth credentials are present' do
- let_it_be(:url) {'https://example.org'}
+ let_it_be(:url) { 'https://example.org' }
let_it_be(:project_hook) { create(:project_hook, url: 'https://demo:demo@example.org/') }
it 'uses the credentials' do
@@ -205,7 +205,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
end
context 'when auth credentials are partial present' do
- let_it_be(:url) {'https://example.org'}
+ let_it_be(:url) { 'https://example.org' }
let_it_be(:project_hook) { create(:project_hook, url: 'https://demo@example.org/') }
it 'uses the credentials anyways' do
diff --git a/spec/services/web_hooks/destroy_service_spec.rb b/spec/services/web_hooks/destroy_service_spec.rb
index 4d9bb18e540..ca8cb8a1b75 100644
--- a/spec/services/web_hooks/destroy_service_spec.rb
+++ b/spec/services/web_hooks/destroy_service_spec.rb
@@ -8,43 +8,54 @@ RSpec.describe WebHooks::DestroyService do
subject { described_class.new(user) }
describe '#execute' do
- %i[system_hook project_hook].each do |factory|
- context "deleting a #{factory}" do
- let!(:hook) { create(factory) } # rubocop: disable Rails/SaveBang (false-positive!)
- let!(:log) { create_list(:web_hook_log, 3, web_hook: hook) }
+ # Testing with a project hook only - for permission tests, see policy specs.
+ let!(:hook) { create(:project_hook) }
+ let!(:log) { create_list(:web_hook_log, 3, web_hook: hook) }
+
+ context 'when the user does not have permission' do
+ it 'is an error' do
+ expect(subject.execute(hook))
+ .to be_error
+ .and have_attributes(message: described_class::DENIED)
+ end
+ end
- it 'is successful' do
- expect(subject.execute(hook)).to be_success
- end
+ context 'when the user does have permission' do
+ before do
+ hook.project.add_maintainer(user)
+ end
- it 'destroys the hook' do
- expect { subject.execute(hook) }.to change(WebHook, :count).from(1).to(0)
- end
+ it 'is successful' do
+ expect(subject.execute(hook)).to be_success
+ end
- it 'does not destroy logs' do
- expect { subject.execute(hook) }.not_to change(WebHookLog, :count)
- end
+ it 'destroys the hook' do
+ expect { subject.execute(hook) }.to change(WebHook, :count).from(1).to(0)
+ end
- it 'schedules the destruction of logs' do
- expect(WebHooks::LogDestroyWorker).to receive(:perform_async).with({ 'hook_id' => hook.id })
- expect(Gitlab::AppLogger).to receive(:info).with(match(/scheduled a deletion of logs/))
+ it 'does not destroy logs' do
+ expect { subject.execute(hook) }.not_to change(WebHookLog, :count)
+ end
- subject.execute(hook)
- end
+ it 'schedules the destruction of logs' do
+ expect(WebHooks::LogDestroyWorker).to receive(:perform_async).with({ 'hook_id' => hook.id })
+ expect(Gitlab::AppLogger).to receive(:info).with(match(/scheduled a deletion of logs/))
- context 'when the hook fails to destroy' do
- before do
- allow(hook).to receive(:destroy).and_return(false)
- end
+ subject.execute(hook)
+ end
+
+ context 'when the hook fails to destroy' do
+ before do
+ allow(hook).to receive(:destroy).and_return(false)
+ end
- it 'is not a success' do
- expect(WebHooks::LogDestroyWorker).not_to receive(:perform_async)
+ it 'is not a success' do
+ expect(WebHooks::LogDestroyWorker).not_to receive(:perform_async)
- r = subject.execute(hook)
+ r = subject.execute(hook)
- expect(r).to be_error
- expect(r[:message]).to match %r{Unable to destroy}
- end
+ expect(r).to be_error
+ expect(r[:message]).to match %r{Unable to destroy}
end
end
end
diff --git a/spec/services/web_hooks/log_execution_service_spec.rb b/spec/services/web_hooks/log_execution_service_spec.rb
index 873f6adc8dc..1967a8368fb 100644
--- a/spec/services/web_hooks/log_execution_service_spec.rb
+++ b/spec/services/web_hooks/log_execution_service_spec.rb
@@ -101,27 +101,6 @@ RSpec.describe WebHooks::LogExecutionService do
it 'resets the failure count' do
expect { service.execute }.to change(project_hook, :recent_failures).to(0)
end
-
- it 'sends a message to AuthLogger if the hook as not previously enabled' do
- project_hook.update!(recent_failures: ::WebHook::FAILURE_THRESHOLD + 1)
-
- expect(Gitlab::AuthLogger).to receive(:info).with include(
- message: 'WebHook change active_state',
- # identification
- hook_id: project_hook.id,
- hook_type: project_hook.type,
- project_id: project_hook.project_id,
- group_id: nil,
- # relevant data
- prev_state: :permanently_disabled,
- new_state: :enabled,
- duration: 1.2,
- response_status: '200',
- recent_hook_failures: 0
- )
-
- service.execute
- end
end
end
@@ -158,27 +137,6 @@ RSpec.describe WebHooks::LogExecutionService do
expect { service.execute }.not_to change(project_hook, :recent_failures)
end
end
-
- it 'sends a message to AuthLogger if the state would change' do
- project_hook.update!(recent_failures: ::WebHook::FAILURE_THRESHOLD)
-
- expect(Gitlab::AuthLogger).to receive(:info).with include(
- message: 'WebHook change active_state',
- # identification
- hook_id: project_hook.id,
- hook_type: project_hook.type,
- project_id: project_hook.project_id,
- group_id: nil,
- # relevant data
- prev_state: :enabled,
- new_state: :permanently_disabled,
- duration: (be > 0),
- response_status: data[:response_status],
- recent_hook_failures: ::WebHook::FAILURE_THRESHOLD + 1
- )
-
- service.execute
- end
end
context 'when response_category is :error' do
@@ -200,25 +158,6 @@ RSpec.describe WebHooks::LogExecutionService do
expect { service.execute }.to change(project_hook, :backoff_count).by(1)
end
- it 'sends a message to AuthLogger if the state would change' do
- expect(Gitlab::AuthLogger).to receive(:info).with include(
- message: 'WebHook change active_state',
- # identification
- hook_id: project_hook.id,
- hook_type: project_hook.type,
- project_id: project_hook.project_id,
- group_id: nil,
- # relevant data
- prev_state: :enabled,
- new_state: :temporarily_disabled,
- duration: (be > 0),
- response_status: data[:response_status],
- recent_hook_failures: 0
- )
-
- service.execute
- end
-
context 'when the previous cool-off was near the maximum' do
before do
project_hook.update!(disabled_until: 5.minutes.ago, backoff_count: 8)
diff --git a/spec/services/webauthn/authenticate_service_spec.rb b/spec/services/webauthn/authenticate_service_spec.rb
index 61f64f24f5e..b40f9465b63 100644
--- a/spec/services/webauthn/authenticate_service_spec.rb
+++ b/spec/services/webauthn/authenticate_service_spec.rb
@@ -30,19 +30,28 @@ RSpec.describe Webauthn::AuthenticateService do
get_result['clientExtensionResults'] = {}
service = Webauthn::AuthenticateService.new(user, get_result.to_json, challenge)
- expect(service.execute).to be_truthy
+ expect(service.execute).to eq true
end
- it 'returns false if the response is valid but no matching stored credential is present' do
- other_client = WebAuthn::FakeClient.new(origin)
- other_client.create(challenge: challenge) # rubocop:disable Rails/SaveBang
+ context 'when response is valid but no matching stored credential is present' do
+ it 'returns false' do
+ other_client = WebAuthn::FakeClient.new(origin)
+ other_client.create(challenge: challenge) # rubocop:disable Rails/SaveBang
- get_result = other_client.get(challenge: challenge)
+ get_result = other_client.get(challenge: challenge)
- get_result['clientExtensionResults'] = {}
- service = Webauthn::AuthenticateService.new(user, get_result.to_json, challenge)
+ get_result['clientExtensionResults'] = {}
+ service = Webauthn::AuthenticateService.new(user, get_result.to_json, challenge)
+
+ expect(service.execute).to eq false
+ end
+ end
- expect(service.execute).to be_falsey
+ context 'when device response includes invalid json' do
+ it 'returns false' do
+ service = Webauthn::AuthenticateService.new(user, 'invalid JSON', '')
+ expect(service.execute).to eq false
+ end
end
end
end
diff --git a/spec/services/work_items/create_and_link_service_spec.rb b/spec/services/work_items/create_and_link_service_spec.rb
index 81be15f9e2f..e259a22d388 100644
--- a/spec/services/work_items/create_and_link_service_spec.rb
+++ b/spec/services/work_items/create_and_link_service_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe WorkItems::CreateAndLinkService do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:user) { create(:user) }
- let_it_be(:related_work_item) { create(:work_item, project: project) }
+ let_it_be(:related_work_item, refind: true) { create(:work_item, project: project) }
let_it_be(:invalid_parent) { create(:work_item, :task, project: project) }
let(:spam_params) { double }
@@ -24,6 +24,26 @@ RSpec.describe WorkItems::CreateAndLinkService do
project.add_developer(user)
end
+ shared_examples 'successful work item and link creator' do
+ it 'creates a work item successfully with links' do
+ expect do
+ service_result
+ end.to change(WorkItem, :count).by(1).and(
+ change(WorkItems::ParentLink, :count).by(1)
+ )
+ end
+
+ it 'copies confidential status from the parent' do
+ expect do
+ service_result
+ end.to change(WorkItem, :count).by(1)
+
+ created_task = WorkItem.last
+
+ expect(created_task.confidential).to eq(related_work_item.confidential)
+ end
+ end
+
describe '#execute' do
subject(:service_result) { described_class.new(project: project, current_user: user, params: params, spam_params: spam_params, link_params: link_params).execute }
@@ -42,15 +62,21 @@ RSpec.describe WorkItems::CreateAndLinkService do
)
end
+ it_behaves_like 'title with extra spaces'
+
context 'when link params are valid' do
let(:link_params) { { parent_work_item: related_work_item } }
- it 'creates a work item successfully with links' do
- expect do
- service_result
- end.to change(WorkItem, :count).by(1).and(
- change(WorkItems::ParentLink, :count).by(1)
- )
+ context 'when parent is not confidential' do
+ it_behaves_like 'successful work item and link creator'
+ end
+
+ context 'when parent is confidential' do
+ before do
+ related_work_item.update!(confidential: true)
+ end
+
+ it_behaves_like 'successful work item and link creator'
end
end
diff --git a/spec/services/work_items/create_from_task_service_spec.rb b/spec/services/work_items/create_from_task_service_spec.rb
index 7d2dab228b1..7c5430f038c 100644
--- a/spec/services/work_items/create_from_task_service_spec.rb
+++ b/spec/services/work_items/create_from_task_service_spec.rb
@@ -64,6 +64,8 @@ RSpec.describe WorkItems::CreateFromTaskService do
expect(list_work_item.description).to eq("- [ ] #{created_work_item.to_reference}+")
end
+
+ it_behaves_like 'title with extra spaces'
end
context 'when last operation fails' do
diff --git a/spec/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb
index 4009c85bacd..c0bcf9b606d 100644
--- a/spec/services/work_items/create_service_spec.rb
+++ b/spec/services/work_items/create_service_spec.rb
@@ -65,6 +65,12 @@ RSpec.describe WorkItems::CreateService do
expect(work_item.description).to eq('please fix')
expect(work_item.work_item_type.base_type).to eq('issue')
end
+
+ it 'calls NewIssueWorker with correct arguments' do
+ expect(NewIssueWorker).to receive(:perform_async).with(Integer, current_user.id, 'WorkItem')
+
+ service_result
+ end
end
context 'when params are invalid' do
@@ -170,7 +176,7 @@ RSpec.describe WorkItems::CreateService do
let_it_be(:parent) { create(:work_item, :task, project: project) }
it_behaves_like 'fails creating work item and returns errors' do
- let(:error_message) { 'only Issue and Incident can be parent of Task.'}
+ let(:error_message) { 'only Issue and Incident can be parent of Task.' }
end
end
@@ -197,7 +203,7 @@ RSpec.describe WorkItems::CreateService do
end
it_behaves_like 'fails creating work item and returns errors' do
- let(:error_message) { 'No matching task found. Make sure that you are adding a valid task ID.'}
+ let(:error_message) { 'No matching task found. Make sure that you are adding a valid task ID.' }
end
end
end
diff --git a/spec/services/work_items/parent_links/create_service_spec.rb b/spec/services/work_items/parent_links/create_service_spec.rb
index 85b0ee040cd..0ba41373544 100644
--- a/spec/services/work_items/parent_links/create_service_spec.rb
+++ b/spec/services/work_items/parent_links/create_service_spec.rb
@@ -12,10 +12,10 @@ RSpec.describe WorkItems::ParentLinks::CreateService do
let_it_be(:task1) { create(:work_item, :task, project: project) }
let_it_be(:task2) { create(:work_item, :task, project: project) }
let_it_be(:guest_task) { create(:work_item, :task) }
- let_it_be(:invalid_task) { build_stubbed(:work_item, :task, id: non_existing_record_id)}
+ let_it_be(:invalid_task) { build_stubbed(:work_item, :task, id: non_existing_record_id) }
let_it_be(:another_project) { (create :project) }
let_it_be(:other_project_task) { create(:work_item, :task, iid: 100, project: another_project) }
- let_it_be(:existing_parent_link) { create(:parent_link, work_item: task, work_item_parent: work_item)}
+ let_it_be(:existing_parent_link) { create(:parent_link, work_item: task, work_item_parent: work_item) }
let(:parent_link_class) { WorkItems::ParentLink }
let(:issuable_type) { :task }
@@ -84,13 +84,26 @@ RSpec.describe WorkItems::ParentLinks::CreateService do
expect(subject[:created_references].map(&:work_item_id)).to match_array([task1.id, task2.id])
end
+ it 'creates notes', :aggregate_failures do
+ subject
+
+ work_item_notes = work_item.notes.last(2)
+ expect(work_item_notes.first.note).to eq("added #{task1.to_reference} as child task")
+ expect(work_item_notes.last.note).to eq("added #{task2.to_reference} as child task")
+ expect(task1.notes.last.note).to eq("added #{work_item.to_reference} as parent issue")
+ expect(task2.notes.last.note).to eq("added #{work_item.to_reference} as parent issue")
+ end
+
context 'when task is already assigned' do
let(:params) { { issuable_references: [task, task2] } }
- it 'creates links only for non related tasks' do
+ it 'creates links only for non related tasks', :aggregate_failures do
expect { subject }.to change(parent_link_class, :count).by(1)
expect(subject[:created_references].map(&:work_item_id)).to match_array([task2.id])
+ expect(work_item.notes.last.note).to eq("added #{task2.to_reference} as child task")
+ expect(task2.notes.last.note).to eq("added #{work_item.to_reference} as parent issue")
+ expect(task.notes).to be_empty
end
end
@@ -109,6 +122,15 @@ RSpec.describe WorkItems::ParentLinks::CreateService do
is_expected.to eq(service_error(error, http_status: 422))
end
+
+ it 'creates notes for valid links' do
+ subject
+
+ expect(work_item.notes.last.note).to eq("added #{task1.to_reference} as child task")
+ expect(task1.notes.last.note).to eq("added #{work_item.to_reference} as parent issue")
+ expect(issue.notes).to be_empty
+ expect(other_project_task.notes).to be_empty
+ end
end
context 'when parent type is invalid' do
diff --git a/spec/services/work_items/parent_links/destroy_service_spec.rb b/spec/services/work_items/parent_links/destroy_service_spec.rb
index 574b70af397..654a03ef6f7 100644
--- a/spec/services/work_items/parent_links/destroy_service_spec.rb
+++ b/spec/services/work_items/parent_links/destroy_service_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe WorkItems::ParentLinks::DestroyService do
let_it_be(:project) { create(:project) }
let_it_be(:work_item) { create(:work_item, project: project) }
let_it_be(:task) { create(:work_item, :task, project: project) }
- let_it_be(:parent_link) { create(:parent_link, work_item: task, work_item_parent: work_item)}
+ let_it_be(:parent_link) { create(:parent_link, work_item: task, work_item_parent: work_item) }
let(:parent_link_class) { WorkItems::ParentLink }
@@ -23,8 +23,11 @@ RSpec.describe WorkItems::ParentLinks::DestroyService do
context 'when user has permissions to update work items' do
let(:user) { reporter }
- it 'removes relation' do
+ it 'removes relation and creates notes', :aggregate_failures do
expect { subject }.to change(parent_link_class, :count).by(-1)
+
+ expect(work_item.notes.last.note).to eq("removed child task #{task.to_reference}")
+ expect(task.notes.last.note).to eq("removed parent issue #{work_item.to_reference}")
end
it 'returns success message' do
@@ -35,8 +38,10 @@ RSpec.describe WorkItems::ParentLinks::DestroyService do
context 'when user has insufficient permissions' do
let(:user) { guest }
- it 'does not remove relation' do
+ it 'does not remove relation', :aggregate_failures do
expect { subject }.not_to change(parent_link_class, :count).from(1)
+
+ expect(SystemNoteService).not_to receive(:unrelate_work_item)
end
it 'returns error message' do
diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb
index b17c9ffb4fb..2e0b0051495 100644
--- a/spec/services/work_items/update_service_spec.rb
+++ b/spec/services/work_items/update_service_spec.rb
@@ -4,7 +4,8 @@ require 'spec_helper'
RSpec.describe WorkItems::UpdateService do
let_it_be(:developer) { create(:user) }
- let_it_be(:project) { create(:project).tap { |proj| proj.add_developer(developer) } }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:project) { create(:project) }
let_it_be(:parent) { create(:work_item, project: project) }
let_it_be_with_reload(:work_item) { create(:work_item, project: project, assignees: [developer]) }
@@ -13,21 +14,36 @@ RSpec.describe WorkItems::UpdateService do
let(:opts) { {} }
let(:current_user) { developer }
+ before do
+ project.add_developer(developer)
+ project.add_guest(guest)
+ end
+
describe '#execute' do
- subject(:update_work_item) do
+ let(:service) do
described_class.new(
project: project,
current_user: current_user,
params: opts,
spam_params: spam_params,
widget_params: widget_params
- ).execute(work_item)
+ )
end
+ subject(:update_work_item) { service.execute(work_item) }
+
before do
stub_spam_services
end
+ shared_examples 'update service that triggers graphql dates updated subscription' do
+ it 'triggers graphql subscription issueableDatesUpdated' do
+ expect(GraphqlTriggers).to receive(:issuable_dates_updated).with(work_item).and_call_original
+
+ update_work_item
+ end
+ end
+
context 'when title is changed' do
let(:opts) { { title: 'changed' } }
@@ -50,6 +66,16 @@ RSpec.describe WorkItems::UpdateService do
end
end
+ context 'when dates are changed' do
+ let(:opts) { { start_date: Date.today } }
+
+ it 'tracks users updating work item dates' do
+ expect(Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter).to receive(:track_work_item_date_changed_action).with(author: current_user)
+
+ update_work_item
+ end
+ end
+
context 'when updating state_event' do
context 'when state_event is close' do
let(:opts) { { state_event: 'close' } }
@@ -82,8 +108,7 @@ RSpec.describe WorkItems::UpdateService do
let(:widget_params) do
{
hierarchy_widget: { parent: parent },
- description_widget: { description: 'foo' },
- weight_widget: { weight: 1 }
+ description_widget: { description: 'foo' }
}
end
@@ -101,8 +126,7 @@ RSpec.describe WorkItems::UpdateService do
let(:supported_widgets) do
[
- { klass: WorkItems::Widgets::DescriptionService::UpdateService, callback: :update, params: { description: 'foo' } },
- { klass: WorkItems::Widgets::WeightService::UpdateService, callback: :update, params: { weight: 1 } },
+ { klass: WorkItems::Widgets::DescriptionService::UpdateService, callback: :before_update_callback, params: { description: 'foo' } },
{ klass: WorkItems::Widgets::HierarchyService::UpdateService, callback: :before_update_in_transaction, params: { parent: parent } }
]
end
@@ -126,7 +150,7 @@ RSpec.describe WorkItems::UpdateService do
before do
allow_next_instance_of(widget_service_class) do |instance|
allow(instance)
- .to receive(:update)
+ .to receive(:before_update_callback)
.with(params: { description: 'changed' }).and_return(nil)
end
end
@@ -142,6 +166,69 @@ RSpec.describe WorkItems::UpdateService do
expect(work_item.description).to eq('changed')
end
+
+ context 'with mentions', :mailer, :sidekiq_might_not_need_inline do
+ shared_examples 'creates the todo and sends email' do |attribute|
+ it 'creates a todo and sends email' do
+ expect { perform_enqueued_jobs { update_work_item } }.to change(Todo, :count).by(1)
+ expect(work_item.reload.attributes[attribute.to_s]).to eq("mention #{guest.to_reference}")
+ should_email(guest)
+ end
+ end
+
+ context 'when description contains a user mention' do
+ let(:widget_params) { { description_widget: { description: "mention #{guest.to_reference}" } } }
+
+ it_behaves_like 'creates the todo and sends email', :description
+ end
+
+ context 'when title contains a user mention' do
+ let(:opts) { { title: "mention #{guest.to_reference}" } }
+
+ it_behaves_like 'creates the todo and sends email', :title
+ end
+ end
+
+ context 'when work item validation fails' do
+ let(:opts) { { title: '' } }
+
+ it 'returns validation errors' do
+ expect(update_work_item[:message]).to contain_exactly("Title can't be blank")
+ end
+
+ it 'does not execute after-update widgets', :aggregate_failures do
+ expect(service).to receive(:update).and_call_original
+ expect(service).not_to receive(:execute_widgets).with(callback: :update, widget_params: widget_params)
+
+ expect { update_work_item }.not_to change(work_item, :description)
+ end
+ end
+ end
+
+ context 'for start and due date widget' do
+ let(:updated_date) { 1.week.from_now.to_date }
+
+ context 'when due_date is updated' do
+ let(:widget_params) { { start_and_due_date_widget: { due_date: updated_date } } }
+
+ it_behaves_like 'update service that triggers graphql dates updated subscription'
+ end
+
+ context 'when start_date is updated' do
+ let(:widget_params) { { start_and_due_date_widget: { start_date: updated_date } } }
+
+ it_behaves_like 'update service that triggers graphql dates updated subscription'
+ end
+
+ context 'when no date param is updated' do
+ let(:opts) { { title: 'should not trigger' } }
+
+ it 'does not trigger date updated subscription' do
+ expect(GraphqlTriggers).not_to receive(:issuable_dates_updated)
+
+ update_work_item
+ end
+ end
end
context 'for the hierarchy widget' do
@@ -175,6 +262,22 @@ RSpec.describe WorkItems::UpdateService do
end.to not_change(WorkItems::ParentLink, :count).and(not_change(work_item, :title))
end
end
+
+ context 'when work item validation fails' do
+ let(:opts) { { title: '' } }
+
+ it 'returns validation errors' do
+ expect(update_work_item[:message]).to contain_exactly("Title can't be blank")
+ end
+
+ it 'does not execute after-update widgets', :aggregate_failures do
+ expect(service).to receive(:update).and_call_original
+ expect(service).not_to receive(:execute_widgets).with(callback: :before_update_in_transaction, widget_params: widget_params)
+ expect(work_item.work_item_children).not_to include(child_work_item)
+
+ update_work_item
+ end
+ end
end
end
end
diff --git a/spec/services/work_items/widgets/assignees_service/update_service_spec.rb b/spec/services/work_items/widgets/assignees_service/update_service_spec.rb
new file mode 100644
index 00000000000..0ab2c85f078
--- /dev/null
+++ b/spec/services/work_items/widgets/assignees_service/update_service_spec.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::Widgets::AssigneesService::UpdateService, :freeze_time do
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:new_assignee) { create(:user) }
+
+ let(:work_item) do
+ create(:work_item, project: project, updated_at: 1.day.ago)
+ end
+
+ let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::Assignees) } }
+ let(:current_user) { reporter }
+ let(:params) { { assignee_ids: [new_assignee.id] } }
+
+ before_all do
+ project.add_reporter(reporter)
+ project.add_guest(new_assignee)
+ end
+
+ describe '#before_update_in_transaction' do
+ subject do
+ described_class.new(widget: widget, current_user: current_user)
+ .before_update_in_transaction(params: params)
+ end
+
+ it 'updates the assignees and sets updated_at to the current time' do
+ subject
+
+ expect(work_item.assignee_ids).to contain_exactly(new_assignee.id)
+ expect(work_item.updated_at).to be_like_time(Time.current)
+ end
+
+ context 'when passing an empty array' do
+ let(:params) { { assignee_ids: [] } }
+
+ before do
+ work_item.assignee_ids = [reporter.id]
+ end
+
+ it 'removes existing assignees' do
+ subject
+
+ expect(work_item.assignee_ids).to be_empty
+ expect(work_item.updated_at).to be_like_time(Time.current)
+ end
+ end
+
+ context 'when user does not have access' do
+ let(:current_user) { create(:user) }
+
+ it 'does not update the assignees' do
+ subject
+
+ expect(work_item.assignee_ids).to be_empty
+ expect(work_item.updated_at).to be_like_time(1.day.ago)
+ end
+ end
+
+ context 'when multiple assignees are given' do
+ let(:params) { { assignee_ids: [new_assignee.id, reporter.id] } }
+
+ context 'when work item allows multiple assignees' do
+ before do
+ allow(work_item).to receive(:allows_multiple_assignees?).and_return(true)
+ end
+
+ it 'sets all the given assignees' do
+ subject
+
+ expect(work_item.assignee_ids).to contain_exactly(new_assignee.id, reporter.id)
+ expect(work_item.updated_at).to be_like_time(Time.current)
+ end
+ end
+
+ context 'when work item does not allow multiple assignees' do
+ before do
+ allow(work_item).to receive(:allows_multiple_assignees?).and_return(false)
+ end
+
+ it 'only sets the first assignee' do
+ subject
+
+ expect(work_item.assignee_ids).to contain_exactly(new_assignee.id)
+ expect(work_item.updated_at).to be_like_time(Time.current)
+ end
+ end
+ end
+
+ context 'when assignee does not have access to the work item' do
+ let(:params) { { assignee_ids: [create(:user).id] } }
+
+ it 'does not set the assignee' do
+ subject
+
+ expect(work_item.assignee_ids).to be_empty
+ expect(work_item.updated_at).to be_like_time(1.day.ago)
+ end
+ end
+
+ context 'when assignee ids are the same as the existing ones' do
+ before do
+ work_item.assignee_ids = [new_assignee.id]
+ end
+
+ it 'does not touch updated_at' do
+ subject
+
+ expect(work_item.assignee_ids).to contain_exactly(new_assignee.id)
+ expect(work_item.updated_at).to be_like_time(1.day.ago)
+ end
+ end
+ end
+end
diff --git a/spec/services/work_items/widgets/description_service/update_service_spec.rb b/spec/services/work_items/widgets/description_service/update_service_spec.rb
index a2eceb97f09..582d9dc85f7 100644
--- a/spec/services/work_items/widgets/description_service/update_service_spec.rb
+++ b/spec/services/work_items/widgets/description_service/update_service_spec.rb
@@ -3,32 +3,102 @@
require 'spec_helper'
RSpec.describe WorkItems::Widgets::DescriptionService::UpdateService do
- let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project) }
- let_it_be_with_reload(:work_item) { create(:work_item, project: project, description: 'old description') }
+ let_it_be(:random_user) { create(:user) }
+ let_it_be(:author) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
- let(:widget) { work_item.widgets.find {|widget| widget.is_a?(WorkItems::Widgets::Description) } }
+ let(:params) { { description: 'updated description' } }
+ let(:current_user) { author }
+ let(:work_item) do
+ create(:work_item, author: author, project: project, description: 'old description',
+ last_edited_at: Date.yesterday, last_edited_by: random_user
+ )
+ end
- describe '#update' do
- subject { described_class.new(widget: widget, current_user: user).update(params: params) } # rubocop:disable Rails/SaveBang
+ let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::Description) } }
- context 'when description param is present' do
- let(:params) { { description: 'updated description' } }
+ describe '#update' do
+ subject { described_class.new(widget: widget, current_user: current_user).before_update_callback(params: params) }
+ shared_examples 'sets work item description' do
it 'correctly sets work item description value' do
subject
- expect(work_item.description).to eq('updated description')
+ expect(work_item.description).to eq(params[:description])
+ expect(work_item.last_edited_by).to eq(current_user)
+ expect(work_item.last_edited_at).to be_within(2.seconds).of(Time.current)
end
end
- context 'when description param is not present' do
- let(:params) { {} }
-
+ shared_examples 'does not set work item description' do
it 'does not change work item description value' do
subject
expect(work_item.description).to eq('old description')
+ expect(work_item.last_edited_by).to eq(random_user)
+ expect(work_item.last_edited_at).to eq(Date.yesterday)
+ end
+ end
+
+ context 'when user has permission to update description' do
+ context 'when user is work item author' do
+ let(:current_user) { author }
+
+ it_behaves_like 'sets work item description'
+ end
+
+ context 'when user is a project reporter' do
+ let(:current_user) { reporter }
+
+ before do
+ project.add_reporter(reporter)
+ end
+
+ it_behaves_like 'sets work item description'
+ end
+
+ context 'when description is nil' do
+ let(:current_user) { author }
+ let(:params) { { description: nil } }
+
+ it_behaves_like 'sets work item description'
+ end
+
+ context 'when description is empty' do
+ let(:current_user) { author }
+ let(:params) { { description: '' } }
+
+ it_behaves_like 'sets work item description'
+ end
+
+ context 'when description param is not present' do
+ let(:params) { {} }
+
+ it_behaves_like 'does not set work item description'
+ end
+ end
+
+ context 'when user does not have permission to update description' do
+ context 'when user is a project guest' do
+ let(:current_user) { guest }
+
+ before do
+ project.add_guest(guest)
+ end
+
+ it_behaves_like 'does not set work item description'
+ end
+
+ context 'with private project' do
+ let_it_be(:project) { create(:project) }
+
+ context 'when user is work item author' do
+ let(:current_user) { author }
+
+ it_behaves_like 'does not set work item description'
+ end
end
end
end
diff --git a/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb b/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb
index 4f6ff1b8676..9a425d5308c 100644
--- a/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb
+++ b/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do
let_it_be(:child_work_item) { create(:work_item, :task, project: project) }
let_it_be(:existing_link) { create(:parent_link, work_item: child_work_item, work_item_parent: work_item) }
- let(:widget) { work_item.widgets.find {|widget| widget.is_a?(WorkItems::Widgets::Hierarchy) } }
+ let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::Hierarchy) } }
let(:not_found_error) { 'No matching task found. Make sure that you are adding a valid task ID.' }
shared_examples 'raises a WidgetError' do
@@ -29,13 +29,21 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do
end
end
+ context 'when invalid params are present' do
+ let(:params) { { other_parent: parent_work_item } }
+
+ it_behaves_like 'raises a WidgetError' do
+ let(:message) { 'One or more arguments are invalid: other_parent.' }
+ end
+ end
+
context 'when updating children' do
let_it_be(:child_work_item2) { create(:work_item, :task, project: project) }
let_it_be(:child_work_item3) { create(:work_item, :task, project: project) }
let_it_be(:child_work_item4) { create(:work_item, :task, project: project) }
context 'when work_items_hierarchy feature flag is disabled' do
- let(:params) { { children: [child_work_item4] }}
+ let(:params) { { children: [child_work_item4] } }
before do
stub_feature_flags(work_items_hierarchy: false)
@@ -47,7 +55,7 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do
end
context 'when user has insufficient permissions to link work items' do
- let(:params) { { children: [child_work_item4] }}
+ let(:params) { { children: [child_work_item4] } }
it_behaves_like 'raises a WidgetError' do
let(:message) { not_found_error }
@@ -60,7 +68,7 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do
end
context 'with valid params' do
- let(:params) { { children: [child_work_item2, child_work_item3] }}
+ let(:params) { { children: [child_work_item2, child_work_item3] } }
it 'correctly sets work item parent' do
subject
@@ -71,7 +79,7 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do
end
context 'when child is already assigned' do
- let(:params) { { children: [child_work_item] }}
+ let(:params) { { children: [child_work_item] } }
it_behaves_like 'raises a WidgetError' do
let(:message) { 'Task(s) already assigned' }
@@ -81,7 +89,7 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do
context 'when child type is invalid' do
let_it_be(:child_issue) { create(:work_item, project: project) }
- let(:params) { { children: [child_issue] }}
+ let(:params) { { children: [child_issue] } }
it_behaves_like 'raises a WidgetError' do
let(:message) do
@@ -95,7 +103,7 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do
context 'when updating parent' do
let_it_be(:work_item) { create(:work_item, :task, project: project) }
- let(:params) {{ parent: parent_work_item } }
+ let(:params) { { parent: parent_work_item } }
context 'when work_items_hierarchy feature flag is disabled' do
before do
@@ -144,9 +152,9 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do
end
context 'when type is invalid' do
- let_it_be(:parent_task) { create(:work_item, :task, project: project)}
+ let_it_be(:parent_task) { create(:work_item, :task, project: project) }
- let(:params) {{ parent: parent_task } }
+ let(:params) { { parent: parent_task } }
it_behaves_like 'raises a WidgetError' do
let(:message) do
diff --git a/spec/services/work_items/widgets/start_and_due_date_service/update_service_spec.rb b/spec/services/work_items/widgets/start_and_due_date_service/update_service_spec.rb
new file mode 100644
index 00000000000..d328c541fc7
--- /dev/null
+++ b/spec/services/work_items/widgets/start_and_due_date_service/update_service_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::Widgets::StartAndDueDateService::UpdateService do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be_with_reload(:work_item) { create(:work_item, project: project) }
+
+ let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::StartAndDueDate) } }
+
+ describe '#before_update_callback' do
+ let(:start_date) { Date.today }
+ let(:due_date) { 1.week.from_now.to_date }
+
+ subject(:update_params) do
+ described_class.new(widget: widget, current_user: user).before_update_callback(params: params)
+ end
+
+ context 'when start and due date params are present' do
+ let(:params) { { start_date: Date.today, due_date: 1.week.from_now.to_date } }
+
+ it 'correctly sets date values' do
+ expect do
+ update_params
+ end.to change(work_item, :start_date).from(nil).to(start_date).and(
+ change(work_item, :due_date).from(nil).to(due_date)
+ )
+ end
+ end
+
+ context 'when date params are not present' do
+ let(:params) { {} }
+
+ it 'does not change work item date values' do
+ expect do
+ update_params
+ end.to not_change(work_item, :start_date).from(nil).and(
+ not_change(work_item, :due_date).from(nil)
+ )
+ end
+ end
+
+ context 'when work item had both date values already set' do
+ before do
+ work_item.update!(start_date: start_date, due_date: due_date)
+ end
+
+ context 'when one of the two params is null' do
+ let(:params) { { start_date: nil } }
+
+ it 'sets only one date to null' do
+ expect do
+ update_params
+ end.to change(work_item, :start_date).from(start_date).to(nil).and(
+ not_change(work_item, :due_date).from(due_date)
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/work_items/widgets/weight_service/update_service_spec.rb b/spec/services/work_items/widgets/weight_service/update_service_spec.rb
deleted file mode 100644
index 97e17f1c526..00000000000
--- a/spec/services/work_items/widgets/weight_service/update_service_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe WorkItems::Widgets::WeightService::UpdateService do
- let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project) }
- let_it_be_with_reload(:work_item) { create(:work_item, project: project, weight: 1) }
-
- let(:widget) { work_item.widgets.find {|widget| widget.is_a?(WorkItems::Widgets::Weight) } }
-
- describe '#update' do
- subject { described_class.new(widget: widget, current_user: user).update(params: params) } # rubocop:disable Rails/SaveBang
-
- context 'when weight param is present' do
- let(:params) { { weight: 2 } }
-
- it 'correctly sets work item weight value' do
- subject
-
- expect(work_item.weight).to eq(2)
- end
- end
-
- context 'when weight param is not present' do
- let(:params) { {} }
-
- it 'does not change work item weight value', :aggregate_failures do
- expect { subject }
- .to not_change { work_item.weight }
-
- expect(work_item.weight).to eq(1)
- end
- end
- end
-end