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-11-17 14:33:21 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-11-17 14:33:21 +0300
commit7021455bd1ed7b125c55eb1b33c5a01f2bc55ee0 (patch)
tree5bdc2229f5198d516781f8d24eace62fc7e589e9 /spec/services
parent185b095e93520f96e9cfc31d9c3e69b498cdab7c (diff)
Add latest changes from gitlab-org/gitlab@15-6-stable-eev15.6.0-rc42
Diffstat (limited to 'spec/services')
-rw-r--r--spec/services/admin/set_feature_flag_service_spec.rb9
-rw-r--r--spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb2
-rw-r--r--spec/services/boards/issues/list_service_spec.rb4
-rw-r--r--spec/services/branches/create_service_spec.rb23
-rw-r--r--spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb4
-rw-r--r--spec/services/ci/after_requeue_job_service_spec.rb36
-rw-r--r--spec/services/ci/compare_test_reports_service_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/partitioning_spec.rb5
-rw-r--r--spec/services/ci/create_pipeline_service/rules_spec.rb38
-rw-r--r--spec/services/ci/create_pipeline_service/variables_spec.rb136
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb47
-rw-r--r--spec/services/ci/job_artifacts/create_service_spec.rb6
-rw-r--r--spec/services/ci/job_artifacts/destroy_batch_service_spec.rb81
-rw-r--r--spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb127
-rw-r--r--spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb2
-rw-r--r--spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb23
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_manual_build.yml54
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_pipeline.yml53
-rw-r--r--spec/services/ci/pipeline_schedules/take_ownership_service_spec.rb63
-rw-r--r--spec/services/ci/play_build_service_spec.rb10
-rw-r--r--spec/services/ci/process_build_service_spec.rb189
-rw-r--r--spec/services/ci/register_job_service_spec.rb13
-rw-r--r--spec/services/ci/retry_job_service_spec.rb284
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb23
-rw-r--r--spec/services/ci/runners/bulk_delete_runners_service_spec.rb170
-rw-r--r--spec/services/ci/runners/set_runner_associated_projects_service_spec.rb13
-rw-r--r--spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb44
-rw-r--r--spec/services/clusters/applications/check_installation_progress_service_spec.rb204
-rw-r--r--spec/services/clusters/applications/check_uninstall_progress_service_spec.rb155
-rw-r--r--spec/services/clusters/applications/check_upgrade_progress_service_spec.rb94
-rw-r--r--spec/services/clusters/applications/create_service_spec.rb279
-rw-r--r--spec/services/clusters/applications/patch_service_spec.rb80
-rw-r--r--spec/services/clusters/applications/prometheus_update_service_spec.rb111
-rw-r--r--spec/services/clusters/applications/update_service_spec.rb91
-rw-r--r--spec/services/clusters/gcp/provision_service_spec.rb2
-rw-r--r--spec/services/clusters/gcp/verify_provision_status_service_spec.rb10
-rw-r--r--spec/services/clusters/kubernetes/configure_istio_ingress_service_spec.rb223
-rw-r--r--spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb2
-rw-r--r--spec/services/dependency_proxy/find_cached_manifest_service_spec.rb30
-rw-r--r--spec/services/deployments/create_for_build_service_spec.rb87
-rw-r--r--spec/services/environments/create_for_build_service_spec.rb304
-rw-r--r--spec/services/event_create_service_spec.rb107
-rw-r--r--spec/services/git/base_hooks_service_spec.rb94
-rw-r--r--spec/services/git/tag_push_service_spec.rb1
-rw-r--r--spec/services/google_cloud/generate_pipeline_service_spec.rb11
-rw-r--r--spec/services/groups/create_service_spec.rb29
-rw-r--r--spec/services/groups/destroy_service_spec.rb40
-rw-r--r--spec/services/groups/transfer_service_spec.rb4
-rw-r--r--spec/services/groups/update_service_spec.rb9
-rw-r--r--spec/services/groups/update_shared_runners_service_spec.rb2
-rw-r--r--spec/services/import/fogbugz_service_spec.rb2
-rw-r--r--spec/services/import/gitlab_projects/file_acquisition_strategies/file_upload_spec.rb2
-rw-r--r--spec/services/incident_management/timeline_event_tags/create_service_spec.rb71
-rw-r--r--spec/services/incident_management/timeline_events/create_service_spec.rb107
-rw-r--r--spec/services/incident_management/timeline_events/update_service_spec.rb6
-rw-r--r--spec/services/issuable/discussions_list_service_spec.rb27
-rw-r--r--spec/services/issues/close_service_spec.rb21
-rw-r--r--spec/services/issues/create_service_spec.rb61
-rw-r--r--spec/services/issues/export_csv_service_spec.rb2
-rw-r--r--spec/services/issues/move_service_spec.rb48
-rw-r--r--spec/services/issues/relative_position_rebalancing_service_spec.rb12
-rw-r--r--spec/services/issues/reopen_service_spec.rb20
-rw-r--r--spec/services/issues/update_service_spec.rb33
-rw-r--r--spec/services/labels/promote_service_spec.rb2
-rw-r--r--spec/services/loose_foreign_keys/process_deleted_records_service_spec.rb198
-rw-r--r--spec/services/markup/rendering_service_spec.rb163
-rw-r--r--spec/services/members/destroy_service_spec.rb25
-rw-r--r--spec/services/members/invite_service_spec.rb6
-rw-r--r--spec/services/members/update_service_spec.rb356
-rw-r--r--spec/services/merge_requests/approval_service_spec.rb32
-rw-r--r--spec/services/merge_requests/build_service_spec.rb14
-rw-r--r--spec/services/merge_requests/create_service_spec.rb43
-rw-r--r--spec/services/merge_requests/mergeability/run_checks_service_spec.rb22
-rw-r--r--spec/services/merge_requests/mergeability_check_service_spec.rb30
-rw-r--r--spec/services/merge_requests/remove_approval_service_spec.rb16
-rw-r--r--spec/services/merge_requests/squash_service_spec.rb9
-rw-r--r--spec/services/merge_requests/update_service_spec.rb4
-rw-r--r--spec/services/milestones/transfer_service_spec.rb4
-rw-r--r--spec/services/namespaces/statistics_refresher_service_spec.rb19
-rw-r--r--spec/services/notes/build_service_spec.rb8
-rw-r--r--spec/services/notes/update_service_spec.rb2
-rw-r--r--spec/services/notification_service_spec.rb17
-rw-r--r--spec/services/packages/composer/composer_json_service_spec.rb2
-rw-r--r--spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb17
-rw-r--r--spec/services/packages/npm/create_package_service_spec.rb4
-rw-r--r--spec/services/packages/rpm/repository_metadata/base_builder_spec.rb33
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_filelist_xml_service_spec.rb54
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_filelist_xml_spec.rb21
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_other_xml_service_spec.rb27
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_other_xml_spec.rb21
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_primary_xml_service_spec.rb23
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_primary_xml_spec.rb35
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_repomd_xml_service_spec.rb (renamed from spec/services/packages/rpm/repository_metadata/build_repomd_xml_spec.rb)2
-rw-r--r--spec/services/packages/rpm/repository_metadata/update_xml_service_spec.rb177
-rw-r--r--spec/services/projects/hashed_storage/migrate_repository_service_spec.rb2
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_service_spec.rb2
-rw-r--r--spec/services/projects/move_users_star_projects_service_spec.rb5
-rw-r--r--spec/services/projects/prometheus/alerts/notify_service_spec.rb10
-rw-r--r--spec/services/protected_branches/api_service_spec.rb34
-rw-r--r--spec/services/protected_branches/cache_service_spec.rb11
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb15
-rw-r--r--spec/services/resource_access_tokens/revoke_service_spec.rb34
-rw-r--r--spec/services/resource_events/change_milestone_service_spec.rb31
-rw-r--r--spec/services/resource_events/change_state_service_spec.rb10
-rw-r--r--spec/services/search/group_service_spec.rb2
-rw-r--r--spec/services/search_service_spec.rb154
-rw-r--r--spec/services/security/ci_configuration/sast_parser_service_spec.rb8
-rw-r--r--spec/services/security/merge_reports_service_spec.rb4
-rw-r--r--spec/services/system_notes/issuables_service_spec.rb4
-rw-r--r--spec/services/tags/create_service_spec.rb20
-rw-r--r--spec/services/todo_service_spec.rb2
-rw-r--r--spec/services/todos/destroy/entity_leave_service_spec.rb6
-rw-r--r--spec/services/topics/merge_service_spec.rb8
-rw-r--r--spec/services/users/approve_service_spec.rb2
-rw-r--r--spec/services/users/ban_service_spec.rb2
-rw-r--r--spec/services/users/destroy_service_spec.rb620
-rw-r--r--spec/services/users/migrate_records_to_ghost_user_in_batches_service_spec.rb29
-rw-r--r--spec/services/users/migrate_records_to_ghost_user_service_spec.rb92
-rw-r--r--spec/services/users/migrate_to_ghost_user_service_spec.rb97
-rw-r--r--spec/services/users/reject_service_spec.rb31
-rw-r--r--spec/services/users/unban_service_spec.rb2
-rw-r--r--spec/services/web_hook_service_spec.rb43
-rw-r--r--spec/services/work_items/create_service_spec.rb10
-rw-r--r--spec/services/work_items/update_service_spec.rb28
-rw-r--r--spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb22
-rw-r--r--spec/services/work_items/widgets/milestone_service/create_service_spec.rb28
-rw-r--r--spec/services/work_items/widgets/milestone_service/update_service_spec.rb58
-rw-r--r--spec/services/x509_certificate_revoke_service_spec.rb6
128 files changed, 3625 insertions, 3035 deletions
diff --git a/spec/services/admin/set_feature_flag_service_spec.rb b/spec/services/admin/set_feature_flag_service_spec.rb
index 6fa806644c9..9a9c5545e23 100644
--- a/spec/services/admin/set_feature_flag_service_spec.rb
+++ b/spec/services/admin/set_feature_flag_service_spec.rb
@@ -130,6 +130,15 @@ RSpec.describe Admin::SetFeatureFlagService do
end
end
+ context 'when enabling for a repository' do
+ let(:params) { { value: 'true', repository: project.repository.full_path } }
+
+ it 'enables the feature flag' do
+ expect(Feature).to receive(:enable).with(feature_name, project.repository)
+ expect(subject).to be_success
+ end
+ end
+
context 'when enabling for a user actor and a feature group' do
let(:params) { { value: 'true', user: user.username, feature_group: 'perf_team' } }
let(:feature_group) { Feature.group('perf_team') }
diff --git a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
index 73d185283b6..676f55be28a 100644
--- a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
+++ b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
@@ -225,7 +225,7 @@ RSpec.describe AutoMerge::MergeWhenPipelineSucceedsService do
let!(:build) do
create(:ci_build, :created, pipeline: pipeline, ref: ref,
- name: 'build', ci_stage: build_stage )
+ name: 'build', ci_stage: build_stage)
end
let!(:test) do
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index 72027911e51..1959710bb0c 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe Boards::Issues::ListService do
let_it_be(:opened_issue1) { create(:labeled_issue, project: project, milestone: m1, title: 'Issue 1', labels: [bug]) }
let_it_be(:opened_issue2) { create(:labeled_issue, project: project, milestone: m2, title: 'Issue 2', labels: [p2]) }
- let_it_be(:reopened_issue1) { create(:issue, :opened, project: project, title: 'Reopened Issue 1' ) }
+ let_it_be(:reopened_issue1) { create(:issue, :opened, project: project, title: 'Reopened Issue 1') }
let_it_be(:list1_issue1) { create(:labeled_issue, project: project, milestone: m1, labels: [p2, development]) }
let_it_be(:list1_issue2) { create(:labeled_issue, project: project, milestone: m2, labels: [development]) }
@@ -110,7 +110,7 @@ RSpec.describe Boards::Issues::ListService do
let!(:opened_issue1) { create(:labeled_issue, project: project, milestone: m1, title: 'Issue 1', labels: [bug]) }
let!(:opened_issue2) { create(:labeled_issue, project: project, milestone: m2, title: 'Issue 2', labels: [p2, p2_project]) }
let!(:opened_issue3) { create(:labeled_issue, project: project_archived, milestone: m1, title: 'Issue 3', labels: [bug]) }
- let!(:reopened_issue1) { create(:issue, state: 'opened', project: project, title: 'Reopened Issue 1', closed_at: Time.current ) }
+ let!(:reopened_issue1) { create(:issue, state: 'opened', project: project, title: 'Reopened Issue 1', closed_at: Time.current) }
let!(:list1_issue1) { create(:labeled_issue, project: project, milestone: m1, labels: [p2, p2_project, development]) }
let!(:list1_issue2) { create(:labeled_issue, project: project, milestone: m2, labels: [development]) }
diff --git a/spec/services/branches/create_service_spec.rb b/spec/services/branches/create_service_spec.rb
index 26cc1a0665e..19a32aafa38 100644
--- a/spec/services/branches/create_service_spec.rb
+++ b/spec/services/branches/create_service_spec.rb
@@ -56,17 +56,6 @@ RSpec.describe Branches::CreateService, :use_clean_rails_redis_caching do
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' } }
@@ -184,18 +173,6 @@ RSpec.describe Branches::CreateService, :use_clean_rails_redis_caching do
end
end
- context 'when an ambiguous branch name is provided' do
- it 'returns an error that branch could not be created' do
- err_msg = 'Failed to create branch \'feature\': 13:reference is ambiguous.'
-
- service.execute('feature/widget', 'master')
- result = service.execute('feature', 'master')
-
- expect(result[:status]).to eq(:error)
- expect(result[:message]).to eq(err_msg)
- end
- end
-
it 'logs and returns an error if there is a PreReceiveError exception' do
error_message = 'pre receive error'
raw_message = "GitLab: #{error_message}"
diff --git a/spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb b/spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb
index 0de962328c5..5a7852fc32f 100644
--- a/spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb
+++ b/spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb
@@ -77,6 +77,8 @@ RSpec.describe BulkImports::CreatePipelineTrackersService do
message: 'Pipeline skipped as source instance version not compatible with pipeline',
bulk_import_entity_id: entity.id,
bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
importer: 'gitlab_migration',
pipeline_name: 'PipelineClass4',
minimum_source_version: '15.1.0',
@@ -88,6 +90,8 @@ RSpec.describe BulkImports::CreatePipelineTrackersService do
message: 'Pipeline skipped as source instance version not compatible with pipeline',
bulk_import_entity_id: entity.id,
bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
importer: 'gitlab_migration',
pipeline_name: 'PipelineClass5',
minimum_source_version: '16.0.0',
diff --git a/spec/services/ci/after_requeue_job_service_spec.rb b/spec/services/ci/after_requeue_job_service_spec.rb
index 1f692bdb71a..e6f46fb9ebe 100644
--- a/spec/services/ci/after_requeue_job_service_spec.rb
+++ b/spec/services/ci/after_requeue_job_service_spec.rb
@@ -120,26 +120,6 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do
)
end
- context 'when the FF ci_requeue_with_dag_object_hierarchy is disabled' do
- before do
- stub_feature_flags(ci_requeue_with_dag_object_hierarchy: false)
- end
-
- it 'marks subsequent skipped jobs as processable but leaves a3 created' do
- execute_after_requeue_service(a1)
-
- check_jobs_statuses(
- a1: 'pending',
- a2: 'created',
- a3: 'skipped',
- b1: 'success',
- b2: 'created',
- c1: 'created',
- c2: 'created'
- )
- end
- end
-
context 'when executed by a different user than the original owner' do
let(:retryer) { create(:user).tap { |u| project.add_maintainer(u) } }
let(:service) { described_class.new(project, retryer) }
@@ -312,22 +292,6 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do
c: 'created'
)
end
-
- context 'when the FF ci_requeue_with_dag_object_hierarchy is disabled' do
- before do
- stub_feature_flags(ci_requeue_with_dag_object_hierarchy: false)
- end
-
- it 'marks the next subsequent skipped job as processable but leaves c skipped' do
- execute_after_requeue_service(a)
-
- check_jobs_statuses(
- a: 'pending',
- b: 'created',
- c: 'skipped'
- )
- end
- end
end
private
diff --git a/spec/services/ci/compare_test_reports_service_spec.rb b/spec/services/ci/compare_test_reports_service_spec.rb
index 6d3df0f5383..f259072fe87 100644
--- a/spec/services/ci/compare_test_reports_service_spec.rb
+++ b/spec/services/ci/compare_test_reports_service_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe Ci::CompareTestReportsService do
it 'returns a parsed TestReports success status and failure on the individual suite' do
expect(comparison[:status]).to eq(:parsed)
expect(comparison.dig(:data, 'status')).to eq('success')
- expect(comparison.dig(:data, 'suites', 0, 'status') ).to eq('error')
+ expect(comparison.dig(:data, 'suites', 0, 'status')).to eq('error')
end
end
diff --git a/spec/services/ci/create_pipeline_service/partitioning_spec.rb b/spec/services/ci/create_pipeline_service/partitioning_spec.rb
index 43fbb74ede4..f34d103d965 100644
--- a/spec/services/ci/create_pipeline_service/partitioning_spec.rb
+++ b/spec/services/ci/create_pipeline_service/partitioning_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness, :aggregate_failures do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness, :aggregate_failures,
+:ci_partitionable do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
@@ -31,7 +32,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
end
let(:pipeline) { service.execute(:push).payload }
- let(:current_partition_id) { 123 }
+ let(:current_partition_id) { ci_testing_partition_id }
before do
stub_ci_pipeline_yaml_file(config)
diff --git a/spec/services/ci/create_pipeline_service/rules_spec.rb b/spec/services/ci/create_pipeline_service/rules_spec.rb
index c737b8cc329..5fdefb2b306 100644
--- a/spec/services/ci/create_pipeline_service/rules_spec.rb
+++ b/spec/services/ci/create_pipeline_service/rules_spec.rb
@@ -1425,5 +1425,43 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
it_behaves_like 'comparing file changes with workflow rules'
end
end
+
+ context 'workflow name with rules' do
+ let(:ref) { 'refs/heads/feature' }
+
+ let(:variables) do
+ [{ key: 'SOME_VARIABLE', secret_value: 'SOME_VAL' }]
+ end
+
+ let(:pipeline) do
+ execute_service do |pipeline|
+ pipeline.variables.build(variables)
+ end.payload
+ end
+
+ let(:config) do
+ <<-EOY
+ workflow:
+ name: '$PIPELINE_NAME $SOME_VARIABLE'
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ variables:
+ PIPELINE_NAME: 'Name 1'
+ - if: $CI_COMMIT_REF_NAME =~ /feature/
+ variables:
+ PIPELINE_NAME: 'Name 2'
+
+ job:
+ stage: test
+ script: echo 'hello'
+ EOY
+ end
+
+ it 'substitutes variables in pipeline name' do
+ expect(response).not_to be_error
+ expect(pipeline).to be_persisted
+ expect(pipeline.name).to eq('Name 2 SOME_VAL')
+ end
+ end
end
end
diff --git a/spec/services/ci/create_pipeline_service/variables_spec.rb b/spec/services/ci/create_pipeline_service/variables_spec.rb
new file mode 100644
index 00000000000..e9e0cf2c6e0
--- /dev/null
+++ b/spec/services/ci/create_pipeline_service/variables_spec.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { project.first_owner }
+
+ let(:service) { described_class.new(project, user, { ref: 'master' }) }
+ let(:pipeline) { service.execute(:push).payload }
+
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ context 'when using variables' do
+ context 'when variables have expand: true/false' do
+ let(:config) do
+ <<-YAML
+ variables:
+ VAR7:
+ value: "value 7 $CI_PIPELINE_ID"
+ expand: false
+ VAR8:
+ value: "value 8 $CI_PIPELINE_ID"
+ expand: false
+
+ rspec:
+ script: rspec
+ variables:
+ VAR1: "JOBID-$CI_JOB_ID"
+ VAR2: "PIPELINEID-$CI_PIPELINE_ID and $VAR1"
+ VAR3:
+ value: "PIPELINEID-$CI_PIPELINE_ID and $VAR1"
+ expand: false
+ VAR4:
+ value: "JOBID-$CI_JOB_ID"
+ expand: false
+ VAR5: "PIPELINEID-$CI_PIPELINE_ID and $VAR4"
+ VAR6:
+ value: "PIPELINEID-$CI_PIPELINE_ID and $VAR4"
+ expand: false
+ VAR7: "overridden value 7 $CI_PIPELINE_ID"
+ YAML
+ end
+
+ let(:rspec) { find_job('rspec') }
+
+ it 'creates the pipeline with a job that has variable expanded according to "expand"' do
+ expect(pipeline).to be_created_successfully
+
+ expect(Ci::BuildRunnerPresenter.new(rspec).runner_variables).to include(
+ { key: 'VAR1', value: "JOBID-#{rspec.id}", public: true, masked: false },
+ { key: 'VAR2', value: "PIPELINEID-#{pipeline.id} and JOBID-#{rspec.id}", public: true, masked: false },
+ { key: 'VAR3', value: "PIPELINEID-$CI_PIPELINE_ID and $VAR1", public: true, masked: false, raw: true },
+ { key: 'VAR4', value: "JOBID-$CI_JOB_ID", public: true, masked: false, raw: true },
+ { key: 'VAR5', value: "PIPELINEID-#{pipeline.id} and $VAR4", public: true, masked: false },
+ { key: 'VAR6', value: "PIPELINEID-$CI_PIPELINE_ID and $VAR4", public: true, masked: false, raw: true },
+ { key: 'VAR7', value: "overridden value 7 #{pipeline.id}", public: true, masked: false },
+ { key: 'VAR8', value: "value 8 $CI_PIPELINE_ID", public: true, masked: false, raw: true }
+ )
+ end
+
+ context 'when the FF ci_raw_variables_in_yaml_config is disabled' do
+ before do
+ stub_feature_flags(ci_raw_variables_in_yaml_config: false)
+ end
+
+ it 'creates the pipeline with a job that has all variables expanded' do
+ expect(pipeline).to be_created_successfully
+
+ expect(Ci::BuildRunnerPresenter.new(rspec).runner_variables).to include(
+ { key: 'VAR1', value: "JOBID-#{rspec.id}", public: true, masked: false },
+ { key: 'VAR2', value: "PIPELINEID-#{pipeline.id} and JOBID-#{rspec.id}", public: true, masked: false },
+ { key: 'VAR3', value: "PIPELINEID-#{pipeline.id} and JOBID-#{rspec.id}", public: true, masked: false },
+ { key: 'VAR4', value: "JOBID-#{rspec.id}", public: true, masked: false },
+ { key: 'VAR5', value: "PIPELINEID-#{pipeline.id} and JOBID-#{rspec.id}", public: true, masked: false },
+ { key: 'VAR6', value: "PIPELINEID-#{pipeline.id} and JOBID-#{rspec.id}", public: true, masked: false },
+ { key: 'VAR7', value: "overridden value 7 #{pipeline.id}", public: true, masked: false },
+ { key: 'VAR8', value: "value 8 #{pipeline.id}", public: true, masked: false }
+ )
+ end
+ end
+ end
+
+ context 'when trigger variables have expand: true/false' do
+ let(:config) do
+ <<-YAML
+ child:
+ variables:
+ VAR1: "PROJECTID-$CI_PROJECT_ID"
+ VAR2: "PIPELINEID-$CI_PIPELINE_ID and $VAR1"
+ VAR3:
+ value: "PIPELINEID-$CI_PIPELINE_ID and $VAR1"
+ expand: false
+ trigger:
+ include: child.yml
+ YAML
+ end
+
+ let(:child) { find_job('child') }
+
+ it 'creates the pipeline with a trigger job that has downstream_variables expanded according to "expand"' do
+ expect(pipeline).to be_created_successfully
+
+ expect(child.downstream_variables).to include(
+ { key: 'VAR1', value: "PROJECTID-#{project.id}" },
+ { key: 'VAR2', value: "PIPELINEID-#{pipeline.id} and PROJECTID-$CI_PROJECT_ID" },
+ { key: 'VAR3', value: "PIPELINEID-$CI_PIPELINE_ID and $VAR1", raw: true }
+ )
+ end
+
+ context 'when the FF ci_raw_variables_in_yaml_config is disabled' do
+ before do
+ stub_feature_flags(ci_raw_variables_in_yaml_config: false)
+ end
+
+ it 'creates the pipeline with a job that has all variables expanded' do
+ expect(pipeline).to be_created_successfully
+
+ expect(child.downstream_variables).to include(
+ { key: 'VAR1', value: "PROJECTID-#{project.id}" },
+ { key: 'VAR2', value: "PIPELINEID-#{pipeline.id} and PROJECTID-$CI_PROJECT_ID" },
+ { key: 'VAR3', value: "PIPELINEID-#{pipeline.id} and PROJECTID-$CI_PROJECT_ID" }
+ )
+ end
+ end
+ end
+ end
+
+ private
+
+ def find_job(name)
+ pipeline.processables.find { |job| job.name == name }
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 458692ba1c0..67c13649c6f 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -135,7 +135,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
execute_service
expect(histogram).to have_received(:observe)
- .with({ source: 'push' }, 5)
+ .with({ source: 'push', plan: project.actual_plan_name }, 5)
end
it 'tracks included template usage' do
@@ -1867,49 +1867,4 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
end
end
end
-
- describe '#execute!' do
- subject { service.execute!(*args) }
-
- let(:service) { described_class.new(project, user, ref: ref_name) }
- let(:args) { [:push] }
-
- context 'when user has a permission to create a pipeline' do
- let(:user) { create(:user) }
-
- before do
- project.add_developer(user)
- end
-
- it 'does not raise an error' do
- expect { subject }.not_to raise_error
- end
-
- it 'creates a pipeline' do
- expect { subject }.to change { Ci::Pipeline.count }.by(1)
- end
- end
-
- context 'when user does not have a permission to create a pipeline' do
- let(:user) { create(:user) }
-
- it 'raises an error' do
- expect { subject }
- .to raise_error(described_class::CreateError)
- .with_message('Insufficient permissions to create a new pipeline')
- end
- end
-
- context 'when a user with permissions has been blocked' do
- before do
- user.block!
- end
-
- it 'raises an error' do
- expect { subject }
- .to raise_error(described_class::CreateError)
- .with_message('Insufficient permissions to create a new pipeline')
- end
- 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 030ba84951e..5df590a1b78 100644
--- a/spec/services/ci/job_artifacts/create_service_spec.rb
+++ b/spec/services/ci/job_artifacts/create_service_spec.rb
@@ -181,8 +181,8 @@ RSpec.describe Ci::JobArtifacts::CreateService do
end
end
- context 'with job partitioning' do
- let(:pipeline) { create(:ci_pipeline, project: project, partition_id: 123) }
+ context 'with job partitioning', :ci_partitionable do
+ let(:pipeline) { create(:ci_pipeline, project: project, partition_id: ci_testing_partition_id) }
let(:job) { create(:ci_build, pipeline: pipeline) }
it 'sets partition_id on artifacts' do
@@ -190,7 +190,7 @@ RSpec.describe Ci::JobArtifacts::CreateService do
artifacts_partitions = job.job_artifacts.map(&:partition_id).uniq
- expect(artifacts_partitions).to eq([123])
+ expect(artifacts_partitions).to eq([ci_testing_partition_id])
end
end
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 54d1cacc068..79920dcb2c7 100644
--- a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
+++ b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
@@ -60,10 +60,9 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do
execute
end
- it 'preserves trace artifacts and removes any timestamp' do
+ it 'preserves trace artifacts' do
expect { subject }
- .to change { trace_artifact.reload.expire_at }.from(trace_artifact.expire_at).to(nil)
- .and not_change { Ci::JobArtifact.exists?(trace_artifact.id) }
+ .to not_change { Ci::JobArtifact.exists?(trace_artifact.id) }
end
context 'when artifact belongs to a project that is undergoing stats refresh' do
@@ -277,81 +276,5 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do
is_expected.to eq(destroyed_artifacts_count: 0, statistics_updates: {}, status: :success)
end
end
-
- context 'with artifacts that has backfilled expire_at' do
- let!(:created_on_00_30_45_minutes_on_21_22_23) do
- [
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-21 00:00:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-21 01:30:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-22 12:00:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-22 12:30:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-23 23:00:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-23 23:30:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-23 06:45:00.000'))
- ]
- end
-
- let!(:created_close_to_00_or_30_minutes) do
- [
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-21 00:00:00.001')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-21 00:30:00.999'))
- ]
- end
-
- let!(:created_on_00_or_30_minutes_on_other_dates) do
- [
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-01 00:00:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-19 12:00:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-24 23:30:00.000'))
- ]
- end
-
- let!(:created_at_other_times) do
- [
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-19 00:00:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-19 00:30:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-24 00:00:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-24 00:30:00.000'))
- ]
- end
-
- let(:artifacts_to_keep) { created_on_00_30_45_minutes_on_21_22_23 }
- let(:artifacts_to_delete) { created_close_to_00_or_30_minutes + created_on_00_or_30_minutes_on_other_dates + created_at_other_times }
- let(:all_artifacts) { artifacts_to_keep + artifacts_to_delete }
-
- let(:artifacts) { Ci::JobArtifact.where(id: all_artifacts.map(&:id)) }
-
- it 'deletes job artifacts that do not have expire_at on 00, 30 or 45 minute of 21, 22, 23 of the month' do
- expect { subject }.to change { Ci::JobArtifact.count }.by(artifacts_to_delete.size * -1)
- end
-
- it 'keeps job artifacts that have expire_at on 00, 30 or 45 minute of 21, 22, 23 of the month' do
- expect { subject }.not_to change { Ci::JobArtifact.where(id: artifacts_to_keep.map(&:id)).count }
- end
-
- it 'removes expire_at on job artifacts that have expire_at on 00, 30 or 45 minute of 21, 22, 23 of the month' do
- subject
-
- expect(artifacts_to_keep.all? { |artifact| artifact.reload.expire_at.nil? }).to be(true)
- end
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(ci_detect_wrongly_expired_artifacts: false)
- end
-
- it 'deletes all job artifacts' do
- expect { subject }.to change { Ci::JobArtifact.count }.by(all_artifacts.size * -1)
- end
- end
-
- context 'when fix_expire_at is false' do
- let(:service) { described_class.new(artifacts, pick_up_at: Time.current, fix_expire_at: false) }
-
- it 'deletes all job artifacts' do
- expect { subject }.to change { Ci::JobArtifact.count }.by(all_artifacts.size * -1)
- end
- end
- end
end
end
diff --git a/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb b/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb
index 6d9fc4c8e34..d4d56825e1f 100644
--- a/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb
+++ b/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb
@@ -9,7 +9,9 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do
let_it_be(:user1) { create(:user) }
let_it_be(:user2) { create(:user) }
- let(:test_event_name) { 'i_testing_test_report_uploaded' }
+ let(:test_event_name_1) { 'i_testing_test_report_uploaded' }
+ let(:test_event_name_2) { 'i_testing_coverage_report_uploaded' }
+
let(:counter) { Gitlab::UsageDataCounters::HLLRedisCounter }
let(:start_time) { 1.week.ago }
let(:end_time) { 1.week.from_now }
@@ -25,15 +27,15 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do
end
end
- it 'tracks the event using HLLRedisCounter' do
- allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ it 'tracks the test event using HLLRedisCounter' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.to receive(:track_event)
- .with(test_event_name, values: user1.id)
+ .with(test_event_name_1, values: user1.id)
.and_call_original
expect { track_artifact_report }
.to change {
- counter.unique_events(event_names: test_event_name,
+ counter.unique_events(event_names: test_event_name_1,
start_date: start_time,
end_date: end_time)
}
@@ -44,12 +46,20 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do
context 'when pipeline does not have test reports' do
let_it_be(:pipeline) { create(:ci_empty_pipeline) }
- it 'does not track the event' do
+ it 'does not track the test event' do
track_artifact_report
expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.not_to receive(:track_event)
- .with(anything, test_event_name)
+ .with(anything, test_event_name_1)
+ end
+
+ it 'does not track the coverage test event' do
+ track_artifact_report
+
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .not_to receive(:track_event)
+ .with(anything, test_event_name_2)
end
end
@@ -57,15 +67,15 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do
let_it_be(:pipeline1) { create(:ci_pipeline, :with_test_reports, project: project, user: user1) }
let_it_be(:pipeline2) { create(:ci_pipeline, :with_test_reports, project: project, user: user1) }
- it 'tracks all pipelines using HLLRedisCounter by one user_id' do
- allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ it 'tracks all pipelines using HLLRedisCounter by one user_id for the test event' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.to receive(:track_event)
- .with(test_event_name, values: user1.id)
+ .with(test_event_name_1, values: user1.id)
.and_call_original
- allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.to receive(:track_event)
- .with(test_event_name, values: user1.id)
+ .with(test_event_name_1, values: user1.id)
.and_call_original
expect do
@@ -73,7 +83,7 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do
described_class.new.execute(pipeline2)
end
.to change {
- counter.unique_events(event_names: test_event_name,
+ counter.unique_events(event_names: test_event_name_1,
start_date: start_time,
end_date: end_time)
}
@@ -85,25 +95,92 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do
let_it_be(:pipeline1) { create(:ci_pipeline, :with_test_reports, project: project, user: user1) }
let_it_be(:pipeline2) { create(:ci_pipeline, :with_test_reports, project: project, user: user2) }
- it 'tracks all pipelines using HLLRedisCounter by multiple users' do
- allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ it 'tracks all pipelines using HLLRedisCounter by multiple users for test reports' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event)
+ .with(test_event_name_1, values: user1.id)
+ .and_call_original
+
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event)
+ .with(test_event_name_1, values: user2.id)
+ .and_call_original
+
+ expect do
+ described_class.new.execute(pipeline1)
+ described_class.new.execute(pipeline2)
+ end
+ .to change {
+ counter.unique_events(event_names: test_event_name_1,
+ start_date: start_time,
+ end_date: end_time)
+ }
+ .by 2
+ end
+ end
+
+ context 'when pipeline has coverage test reports' do
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user1) }
+
+ before do
+ 2.times do
+ pipeline.builds << build(:ci_build, :coverage_reports, pipeline: pipeline, project: pipeline.project)
+ end
+ end
+
+ it 'tracks the coverage test event using HLLRedisCounter' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.to receive(:track_event)
- .with(test_event_name, values: user1.id)
+ .with(test_event_name_2, values: user1.id)
.and_call_original
- allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ expect { track_artifact_report }
+ .to change {
+ counter.unique_events(event_names: test_event_name_2,
+ start_date: start_time,
+ end_date: end_time)
+ }
+ .by 1
+ end
+ end
+
+ context 'when a single user started multiple pipelines with coverage reports' do
+ let_it_be(:pipeline1) { create(:ci_pipeline, :with_coverage_reports, project: project, user: user1) }
+ let_it_be(:pipeline2) { create(:ci_pipeline, :with_coverage_reports, project: project, user: user1) }
+
+ it 'tracks all pipelines using HLLRedisCounter by one user_id for the coverage test event' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.to receive(:track_event)
- .with(test_event_name, values: user1.id)
+ .with(test_event_name_2, values: user1.id)
+ .twice
.and_call_original
- allow(Gitlab::UsageDataCounters::HLLRedisCounter)
- .to receive(:track_event)
- .with(test_event_name, values: user2.id)
- .and_call_original
+ expect do
+ described_class.new.execute(pipeline1)
+ described_class.new.execute(pipeline2)
+ end
+ .to change {
+ counter.unique_events(event_names: test_event_name_2,
+ start_date: start_time,
+ end_date: end_time)
+ }
+ .by 1
+ end
+ end
+
+ context 'when multiple users started multiple pipelines with coverage test reports' do
+ let_it_be(:pipeline1) { create(:ci_pipeline, :with_coverage_reports, project: project, user: user1) }
+ let_it_be(:pipeline2) { create(:ci_pipeline, :with_coverage_reports, project: project, user: user2) }
+
+ it 'tracks all pipelines using HLLRedisCounter by multiple users for coverage test reports' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event)
+ .with(test_event_name_2, values: user1.id)
+ .and_call_original
- allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.to receive(:track_event)
- .with(test_event_name, values: user2.id)
+ .with(test_event_name_2, values: user2.id)
.and_call_original
expect do
@@ -111,7 +188,7 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do
described_class.new.execute(pipeline2)
end
.to change {
- counter.unique_events(event_names: test_event_name,
+ counter.unique_events(event_names: test_event_name_2,
start_date: start_time,
end_date: end_time)
}
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 7578afa7c50..d0aa1ba4c6c 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
@@ -104,7 +104,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/pipeline_processing/atomic_processing_service_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
index 06bb6d39fe5..1fbefc1fa22 100644
--- a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
+++ b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService do
check_expectation(test_file.dig('init', 'expect'), "init")
test_file['transitions'].each_with_index do |transition, idx|
- event_on_jobs(transition['event'], transition['jobs'])
+ process_events(transition)
Sidekiq::Worker.drain_all # ensure that all async jobs are executed
check_expectation(transition['expect'], "transition:#{idx}")
end
@@ -48,20 +48,37 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService do
}
end
+ def process_events(transition)
+ if transition['jobs']
+ event_on_jobs(transition['event'], transition['jobs'])
+ else
+ event_on_pipeline(transition['event'])
+ end
+ end
+
def event_on_jobs(event, job_names)
statuses = pipeline.latest_statuses.by_name(job_names).to_a
expect(statuses.count).to eq(job_names.count) # ensure that we have the same counts
statuses.each do |status|
- if event == 'play'
+ case event
+ when 'play'
status.play(user)
- elsif event == 'retry'
+ when 'retry'
::Ci::RetryJobService.new(project, user).execute(status)
else
status.public_send("#{event}!")
end
end
end
+
+ def event_on_pipeline(event)
+ if event == 'retry'
+ pipeline.retry_failed(user)
+ else
+ pipeline.public_send("#{event}!")
+ end
+ end
end
end
diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_manual_build.yml b/spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_manual_build.yml
new file mode 100644
index 00000000000..a50fe56f8d4
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_manual_build.yml
@@ -0,0 +1,54 @@
+config:
+ test1:
+ script: exit 0
+
+ test2:
+ when: manual
+ script: exit 1
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ test: pending
+ jobs:
+ test1: pending
+ test2: manual
+
+transitions:
+ - event: success
+ jobs: [test1]
+ expect:
+ pipeline: success
+ stages:
+ test: success
+ jobs:
+ test1: success
+ test2: manual
+ - event: play
+ jobs: [test2]
+ expect:
+ pipeline: running
+ stages:
+ test: running
+ jobs:
+ test1: success
+ test2: pending
+ - event: drop
+ jobs: [test2]
+ expect:
+ pipeline: success
+ stages:
+ test: success
+ jobs:
+ test1: success
+ test2: failed
+ - event: retry
+ jobs: [test2]
+ expect:
+ pipeline: running
+ stages:
+ test: running
+ jobs:
+ test1: success
+ test2: pending
diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_pipeline.yml b/spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_pipeline.yml
new file mode 100644
index 00000000000..a6112a95a12
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_pipeline.yml
@@ -0,0 +1,53 @@
+config:
+ test1:
+ script: exit 0
+
+ test2:
+ when: manual
+ script: exit 1
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ test: pending
+ jobs:
+ test1: pending
+ test2: manual
+
+transitions:
+ - event: success
+ jobs: [test1]
+ expect:
+ pipeline: success
+ stages:
+ test: success
+ jobs:
+ test1: success
+ test2: manual
+ - event: play
+ jobs: [test2]
+ expect:
+ pipeline: running
+ stages:
+ test: running
+ jobs:
+ test1: success
+ test2: pending
+ - event: drop
+ jobs: [test2]
+ expect:
+ pipeline: success
+ stages:
+ test: success
+ jobs:
+ test1: success
+ test2: failed
+ - event: retry
+ expect:
+ pipeline: success
+ stages:
+ test: success
+ jobs:
+ test1: success
+ test2: manual
diff --git a/spec/services/ci/pipeline_schedules/take_ownership_service_spec.rb b/spec/services/ci/pipeline_schedules/take_ownership_service_spec.rb
new file mode 100644
index 00000000000..9a3aad20d89
--- /dev/null
+++ b/spec/services/ci/pipeline_schedules/take_ownership_service_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::PipelineSchedules::TakeOwnershipService do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: owner) }
+
+ before_all do
+ project.add_maintainer(user)
+ project.add_maintainer(owner)
+ project.add_reporter(reporter)
+ end
+
+ describe '#execute' do
+ context 'when user does not have permission' do
+ subject(:service) { described_class.new(pipeline_schedule, reporter) }
+
+ it 'returns ServiceResponse.error' do
+ result = service.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq(_('Failed to change the owner'))
+ end
+ end
+
+ context 'when user has permission' do
+ subject(:service) { described_class.new(pipeline_schedule, user) }
+
+ it 'returns ServiceResponse.success' do
+ result = service.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.success?).to be(true)
+ expect(result.payload).to eq(pipeline_schedule)
+ end
+
+ context 'when schedule update fails' do
+ subject(:service) { described_class.new(pipeline_schedule, owner) }
+
+ before do
+ allow(pipeline_schedule).to receive(:update).and_return(false)
+
+ errors = ActiveModel::Errors.new(pipeline_schedule)
+ errors.add(:base, 'An error occurred')
+ allow(pipeline_schedule).to receive(:errors).and_return(errors)
+ end
+
+ it 'returns ServiceResponse.error' do
+ result = service.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq(['An error occurred'])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/play_build_service_spec.rb b/spec/services/ci/play_build_service_spec.rb
index 85ef8b60af4..fc07801b672 100644
--- a/spec/services/ci/play_build_service_spec.rb
+++ b/spec/services/ci/play_build_service_spec.rb
@@ -87,11 +87,15 @@ RSpec.describe Ci::PlayBuildService, '#execute' do
expect(build.reload.job_variables.map(&:key)).to contain_exactly('first', 'second')
end
- context 'when variables are invalid' do
+ context 'and variables are invalid' do
let(:job_variables) { [{}] }
- it 'raises an error' do
- expect { subject }.to raise_error(ActiveRecord::RecordInvalid)
+ it 'resets the attributes of the build' do
+ build.update!(job_variables_attributes: [{ key: 'old', value: 'old variable' }])
+
+ subject
+
+ expect(build.job_variables.map(&:key)).to contain_exactly('old')
end
end
diff --git a/spec/services/ci/process_build_service_spec.rb b/spec/services/ci/process_build_service_spec.rb
index 2fcb4ce73ff..9301098b083 100644
--- a/spec/services/ci/process_build_service_spec.rb
+++ b/spec/services/ci/process_build_service_spec.rb
@@ -2,147 +2,118 @@
require 'spec_helper'
RSpec.describe Ci::ProcessBuildService, '#execute' do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
+ using RSpec::Parameterized::TableSyntax
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, ref: 'master', project: project) }
subject { described_class.new(project, user).execute(build, current_status) }
- before do
+ before_all do
project.add_maintainer(user)
end
- context 'when build has on_success option' do
- let(:build) { create(:ci_build, :created, when: :on_success, user: user, project: project) }
-
- context 'when current status is success' do
- let(:current_status) { 'success' }
-
- it 'changes the build status' do
- expect { subject }.to change { build.status }.to('pending')
- end
- end
-
- context 'when current status is skipped' do
- let(:current_status) { 'skipped' }
-
- it 'changes the build status' do
- expect { subject }.to change { build.status }.to('pending')
- end
- end
-
- context 'when current status is failed' do
- let(:current_status) { 'failed' }
-
- it 'does not change the build status' do
- expect { subject }.to change { build.status }.to('skipped')
- end
+ shared_context 'with enqueue_immediately set' do
+ before do
+ build.set_enqueue_immediately!
end
end
- context 'when build has on_failure option' do
- let(:build) { create(:ci_build, :created, when: :on_failure, user: user, project: project) }
-
- context 'when current status is success' do
- let(:current_status) { 'success' }
-
- it 'changes the build status' do
- expect { subject }.to change { build.status }.to('skipped')
- end
- end
-
- context 'when current status is failed' do
- let(:current_status) { 'failed' }
-
- it 'does not change the build status' do
- expect { subject }.to change { build.status }.to('pending')
- end
+ shared_context 'with ci_retry_job_fix disabled' do
+ before do
+ stub_feature_flags(ci_retry_job_fix: false)
end
end
- context 'when build has always option' do
- let(:build) { create(:ci_build, :created, when: :always, user: user, project: project) }
-
- context 'when current status is success' do
- let(:current_status) { 'success' }
-
- it 'changes the build status' do
- expect { subject }.to change { build.status }.to('pending')
- end
+ context 'for single build' do
+ let!(:build) { create(:ci_build, *[trait].compact, :created, **conditions, pipeline: pipeline) }
+
+ where(:trait, :conditions, :current_status, :after_status, :retry_after_status, :retry_disabled_after_status) do
+ nil | { when: :on_success } | 'success' | 'pending' | 'pending' | 'pending'
+ nil | { when: :on_success } | 'skipped' | 'pending' | 'pending' | 'pending'
+ nil | { when: :on_success } | 'failed' | 'skipped' | 'skipped' | 'skipped'
+ nil | { when: :on_failure } | 'success' | 'skipped' | 'skipped' | 'skipped'
+ nil | { when: :on_failure } | 'skipped' | 'skipped' | 'skipped' | 'skipped'
+ nil | { when: :on_failure } | 'failed' | 'pending' | 'pending' | 'pending'
+ nil | { when: :always } | 'success' | 'pending' | 'pending' | 'pending'
+ nil | { when: :always } | 'skipped' | 'pending' | 'pending' | 'pending'
+ nil | { when: :always } | 'failed' | 'pending' | 'pending' | 'pending'
+ :actionable | { when: :manual } | 'success' | 'manual' | 'pending' | 'manual'
+ :actionable | { when: :manual } | 'skipped' | 'manual' | 'pending' | 'manual'
+ :actionable | { when: :manual } | 'failed' | 'skipped' | 'skipped' | 'skipped'
+ :schedulable | { when: :delayed } | 'success' | 'scheduled' | 'pending' | 'scheduled'
+ :schedulable | { when: :delayed } | 'skipped' | 'scheduled' | 'pending' | 'scheduled'
+ :schedulable | { when: :delayed } | 'failed' | 'skipped' | 'skipped' | 'skipped'
end
- context 'when current status is failed' do
- let(:current_status) { 'failed' }
-
- it 'does not change the build status' do
- expect { subject }.to change { build.status }.to('pending')
+ with_them do
+ it 'updates the job status to after_status' do
+ expect { subject }.to change { build.status }.to(after_status)
end
- end
- end
-
- context 'when build has manual option' do
- let(:build) { create(:ci_build, :created, :actionable, user: user, project: project) }
- context 'when current status is success' do
- let(:current_status) { 'success' }
+ context 'when build is set to enqueue immediately' do
+ include_context 'with enqueue_immediately set'
- it 'changes the build status' do
- expect { subject }.to change { build.status }.to('manual')
- end
- end
+ it 'updates the job status to retry_after_status' do
+ expect { subject }.to change { build.status }.to(retry_after_status)
+ end
- context 'when current status is failed' do
- let(:current_status) { 'failed' }
+ context 'when feature flag ci_retry_job_fix is disabled' do
+ include_context 'with ci_retry_job_fix disabled'
- it 'does not change the build status' do
- expect { subject }.to change { build.status }.to('skipped')
+ it "updates the job status to retry_disabled_after_status" do
+ expect { subject }.to change { build.status }.to(retry_disabled_after_status)
+ end
+ end
end
end
end
- context 'when build has delayed option' do
- before do
- allow(Ci::BuildScheduleWorker).to receive(:perform_at) {}
+ context 'when build is scheduled with DAG' do
+ let!(:build) do
+ create(
+ :ci_build,
+ *[trait].compact,
+ :dependent,
+ :created,
+ when: build_when,
+ pipeline: pipeline,
+ needed: other_build
+ )
end
- let(:build) { create(:ci_build, :created, :schedulable, user: user, project: project) }
-
- context 'when current status is success' do
- let(:current_status) { 'success' }
+ let!(:other_build) { create(:ci_build, :created, when: :on_success, pipeline: pipeline) }
- it 'changes the build status' do
- expect { subject }.to change { build.status }.to('scheduled')
- end
+ where(:trait, :build_when, :current_status, :after_status, :retry_after_status, :retry_disabled_after_status) do
+ nil | :on_success | 'success' | 'pending' | 'pending' | 'pending'
+ nil | :on_success | 'skipped' | 'skipped' | 'skipped' | 'skipped'
+ nil | :manual | 'success' | 'manual' | 'pending' | 'manual'
+ nil | :manual | 'skipped' | 'skipped' | 'skipped' | 'skipped'
+ nil | :delayed | 'success' | 'manual' | 'pending' | 'manual'
+ nil | :delayed | 'skipped' | 'skipped' | 'skipped' | 'skipped'
+ :schedulable | :delayed | 'success' | 'scheduled' | 'pending' | 'scheduled'
+ :schedulable | :delayed | 'skipped' | 'skipped' | 'skipped' | 'skipped'
end
- context 'when current status is failed' do
- let(:current_status) { 'failed' }
-
- it 'does not change the build status' do
- expect { subject }.to change { build.status }.to('skipped')
+ with_them do
+ it 'updates the job status to after_status' do
+ expect { subject }.to change { build.status }.to(after_status)
end
- end
- end
- context 'when build is scheduled with DAG' do
- using RSpec::Parameterized::TableSyntax
+ context 'when build is set to enqueue immediately' do
+ include_context 'with enqueue_immediately set'
- let(:pipeline) { create(:ci_pipeline, ref: 'master', project: project) }
- let!(:build) { create(:ci_build, :created, when: build_when, pipeline: pipeline, scheduling_type: :dag) }
- let!(:other_build) { create(:ci_build, :created, when: :on_success, pipeline: pipeline) }
- let!(:build_on_other_build) { create(:ci_build_need, build: build, name: other_build.name) }
-
- where(:build_when, :current_status, :after_status) do
- :on_success | 'success' | 'pending'
- :on_success | 'skipped' | 'skipped'
- :manual | 'success' | 'manual'
- :manual | 'skipped' | 'skipped'
- :delayed | 'success' | 'manual'
- :delayed | 'skipped' | 'skipped'
- end
+ it 'updates the job status to retry_after_status' do
+ expect { subject }.to change { build.status }.to(retry_after_status)
+ end
- with_them do
- it 'proceeds the build' do
- expect { subject }.to change { build.status }.to(after_status)
+ context 'when feature flag ci_retry_job_fix is disabled' do
+ include_context 'with ci_retry_job_fix disabled'
+
+ it "updates the job status to retry_disabled_after_status" do
+ expect { subject }.to change { build.status }.to(retry_disabled_after_status)
+ end
+ end
end
end
end
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index e2e760b9812..f40f5cc5a62 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -14,25 +14,29 @@ module Ci
let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
describe '#execute' do
- context 'checks database loadbalancing stickiness' do
- subject { described_class.new(shared_runner).execute }
+ subject { described_class.new(shared_runner).execute }
+ context 'checks database loadbalancing stickiness' do
before do
project.update!(shared_runners_enabled: false)
end
- it 'result is valid if replica did caught-up' do
+ it 'result is valid if replica did caught-up', :aggregate_failures do
expect(ApplicationRecord.sticking).to receive(:all_caught_up?)
.with(:runner, shared_runner.id) { true }
expect(subject).to be_valid
+ expect(subject.build).to be_nil
+ expect(subject.build_json).to be_nil
end
- it 'result is invalid if replica did not caught-up' do
+ it 'result is invalid if replica did not caught-up', :aggregate_failures do
expect(ApplicationRecord.sticking).to receive(:all_caught_up?)
.with(:runner, shared_runner.id) { false }
expect(subject).not_to be_valid
+ expect(subject.build).to be_nil
+ expect(subject.build_json).to be_nil
end
end
@@ -954,6 +958,7 @@ module Ci
expect(result).not_to be_valid
expect(result.build).to be_nil
+ expect(result.build_json).to be_nil
end
end
diff --git a/spec/services/ci/retry_job_service_spec.rb b/spec/services/ci/retry_job_service_spec.rb
index 69f19c5acf2..540e700efa6 100644
--- a/spec/services/ci/retry_job_service_spec.rb
+++ b/spec/services/ci/retry_job_service_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe Ci::RetryJobService do
+ using RSpec::Parameterized::TableSyntax
let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
@@ -11,11 +12,11 @@ RSpec.describe Ci::RetryJobService do
end
let_it_be(:stage) do
- create(:ci_stage, project: project,
- pipeline: pipeline,
- name: 'test')
+ create(:ci_stage, pipeline: pipeline, name: 'test')
end
+ let_it_be(:deploy_stage) { create(:ci_stage, pipeline: pipeline, name: 'deploy', position: stage.position + 1) }
+
let(:job_variables_attributes) { [{ key: 'MANUAL_VAR', value: 'manual test var' }] }
let(:user) { developer }
@@ -26,24 +27,11 @@ RSpec.describe Ci::RetryJobService do
project.add_reporter(reporter)
end
- shared_context 'retryable bridge' do
- let_it_be(:downstream_project) { create(:project, :repository) }
-
- let_it_be_with_refind(:job) do
- create(:ci_bridge, :success,
- pipeline: pipeline, downstream: downstream_project, description: 'a trigger job', ci_stage: stage
- )
- end
-
- let_it_be(:job_to_clone) { job }
-
- before do
- job.update!(retried: false)
+ shared_context 'retryable build' do
+ let_it_be_with_reload(:job) do
+ create(:ci_build, :success, pipeline: pipeline, ci_stage: stage)
end
- end
- shared_context 'retryable build' do
- let_it_be_with_refind(:job) { create(:ci_build, :success, pipeline: pipeline, ci_stage: stage) }
let_it_be(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
let_it_be(:job_to_clone) do
@@ -60,6 +48,12 @@ RSpec.describe Ci::RetryJobService do
end
end
+ shared_context 'with ci_retry_job_fix disabled' do
+ before do
+ stub_feature_flags(ci_retry_job_fix: false)
+ end
+ end
+
shared_examples_for 'clones the job' do
let(:job) { job_to_clone }
@@ -87,8 +81,7 @@ RSpec.describe Ci::RetryJobService do
context 'when the job has needs' do
before do
- create(:ci_build_need, build: job, name: 'build1')
- create(:ci_build_need, build: job, name: 'build2')
+ create_list(:ci_build_need, 2, build: job)
end
it 'bulk inserts all the needs' do
@@ -123,16 +116,12 @@ RSpec.describe Ci::RetryJobService do
end
context 'when there are subsequent processables that are skipped' do
- let_it_be(:stage) { create(:ci_stage, pipeline: pipeline, name: 'deploy') }
-
let!(:subsequent_build) do
- create(:ci_build, :skipped, stage_idx: 2,
- pipeline: pipeline,
- ci_stage: stage)
+ create(:ci_build, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
end
let!(:subsequent_bridge) do
- create(:ci_bridge, :skipped, stage_idx: 2, pipeline: pipeline, ci_stage: stage)
+ create(:ci_bridge, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
end
it 'resumes pipeline processing in the subsequent stage' do
@@ -152,10 +141,9 @@ RSpec.describe Ci::RetryJobService do
end
context 'when the pipeline has other jobs' do
- let!(:stage2) { create(:ci_stage, project: project, pipeline: pipeline, name: 'deploy') }
- let!(:build2) { create(:ci_build, pipeline: pipeline, ci_stage: stage ) }
- let!(:deploy) { create(:ci_build, pipeline: pipeline, ci_stage: stage2) }
- let!(:deploy_needs_build2) { create(:ci_build_need, build: deploy, name: build2.name) }
+ let!(:other_test_build) { create(:ci_build, pipeline: pipeline, ci_stage: stage) }
+ let!(:deploy) { create(:ci_build, pipeline: pipeline, ci_stage: deploy_stage) }
+ let!(:deploy_needs_build2) { create(:ci_build_need, build: deploy, name: other_test_build.name) }
context 'when job has a nil scheduling_type' do
before do
@@ -166,7 +154,7 @@ RSpec.describe Ci::RetryJobService do
it 'populates scheduling_type of processables' do
expect(new_job.scheduling_type).to eq('stage')
expect(job.reload.scheduling_type).to eq('stage')
- expect(build2.reload.scheduling_type).to eq('stage')
+ expect(other_test_build.reload.scheduling_type).to eq('stage')
expect(deploy.reload.scheduling_type).to eq('dag')
end
end
@@ -193,6 +181,13 @@ RSpec.describe Ci::RetryJobService do
end
end
+ shared_examples_for 'checks enqueue_immediately?' do
+ it "returns enqueue_immediately" do
+ subject
+ expect(new_job.enqueue_immediately?).to eq enqueue_immediately
+ end
+ end
+
describe '#clone!' do
let(:new_job) { service.clone!(job) }
@@ -200,20 +195,6 @@ RSpec.describe Ci::RetryJobService do
expect { service.clone!(create(:ci_build).present) }.to raise_error(TypeError)
end
- context 'when the job to be cloned is a bridge' 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
include_context 'retryable build'
@@ -224,7 +205,7 @@ RSpec.describe Ci::RetryJobService do
context 'when a build with a deployment is retried' do
let!(:job) do
create(:ci_build, :with_deployment, :deploy_to_production,
- pipeline: pipeline, ci_stage: stage, project: project)
+ pipeline: pipeline, ci_stage: stage)
end
it 'creates a new deployment' do
@@ -247,7 +228,6 @@ RSpec.describe Ci::RetryJobService do
options: { environment: { name: environment_name } },
pipeline: pipeline,
ci_stage: stage,
- project: project,
user: other_developer)
end
@@ -282,24 +262,44 @@ RSpec.describe Ci::RetryJobService do
end
end
end
- end
- describe '#execute' do
- let(:new_job) { service.execute(job)[:job] }
+ context 'when enqueue_if_actionable is provided' do
+ let!(:job) do
+ create(:ci_build, *[trait].compact, :failed, pipeline: pipeline, ci_stage: stage)
+ end
- context 'when the job to be retried is a bridge' do
- include_context 'retryable bridge'
+ let(:new_job) { subject }
- it_behaves_like 'retries the job'
+ subject { service.clone!(job, enqueue_if_actionable: enqueue_if_actionable) }
- context 'when given variables' do
- let(:new_job) { service.clone!(job, variables: job_variables_attributes) }
+ where(:enqueue_if_actionable, :trait, :enqueue_immediately) do
+ true | nil | false
+ true | :manual | true
+ true | :expired_scheduled | true
+
+ false | nil | false
+ false | :manual | false
+ false | :expired_scheduled | false
+ end
+
+ with_them do
+ it_behaves_like 'checks enqueue_immediately?'
- it 'does not give variables to the new bridge' do
- expect { new_job }.not_to raise_error
+ context 'when feature flag is disabled' do
+ include_context 'with ci_retry_job_fix disabled'
+
+ it_behaves_like 'checks enqueue_immediately?' do
+ let(:enqueue_immediately) { false }
+ end
end
end
end
+ end
+
+ describe '#execute' do
+ let(:new_job) { subject[:job] }
+
+ subject { service.execute(job) }
context 'when the job to be retried is a build' do
include_context 'retryable build'
@@ -307,24 +307,18 @@ RSpec.describe Ci::RetryJobService do
it_behaves_like 'retries the job'
context 'when there are subsequent jobs that are skipped' do
- let_it_be(:stage) { create(:ci_stage, pipeline: pipeline, name: 'deploy') }
-
let!(:subsequent_build) do
- create(:ci_build, :skipped, stage_idx: 2,
- pipeline: pipeline,
- stage_id: stage.id)
+ create(:ci_build, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
end
let!(:subsequent_bridge) do
- create(:ci_bridge, :skipped, stage_idx: 2,
- pipeline: pipeline,
- stage_id: stage.id)
+ create(:ci_bridge, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
end
it 'does not cause an N+1 when updating the job ownership' do
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { service.execute(job) }.count
- create_list(:ci_build, 2, :skipped, stage_idx: job.stage_idx + 1, pipeline: pipeline, stage_id: stage.id)
+ create_list(:ci_build, 2, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
expect { service.execute(job) }.not_to exceed_all_query_limit(control_count)
end
@@ -352,5 +346,161 @@ RSpec.describe Ci::RetryJobService do
end
end
end
+
+ context 'when job being retried has jobs in previous stages' do
+ let!(:job) do
+ create(
+ :ci_build,
+ :failed,
+ name: 'deploy_a',
+ pipeline: pipeline,
+ ci_stage: deploy_stage
+ )
+ end
+
+ before do
+ create(
+ :ci_build,
+ previous_stage_job_status,
+ name: 'test_a',
+ pipeline: pipeline,
+ ci_stage: stage
+ )
+ end
+
+ where(:previous_stage_job_status, :after_status) do
+ :created | 'created'
+ :pending | 'created'
+ :running | 'created'
+ :manual | 'created'
+ :scheduled | 'created'
+ :success | 'pending'
+ :failed | 'skipped'
+ :skipped | 'pending'
+ end
+
+ with_them do
+ it 'updates the new job status to after_status' do
+ expect(subject).to be_success
+ expect(new_job.status).to eq after_status
+ end
+
+ context 'when feature flag is disabled' do
+ include_context 'with ci_retry_job_fix disabled'
+
+ it 'enqueues the new job' do
+ expect(subject).to be_success
+ expect(new_job).to be_pending
+ end
+ end
+ end
+ end
+
+ context 'when job being retried has DAG dependencies' do
+ let!(:job) do
+ create(
+ :ci_build,
+ :failed,
+ :dependent,
+ name: 'deploy_a',
+ pipeline: pipeline,
+ ci_stage: deploy_stage,
+ needed: dependency
+ )
+ end
+
+ let(:dependency) do
+ create(
+ :ci_build,
+ dag_dependency_status,
+ name: 'test_a',
+ pipeline: pipeline,
+ ci_stage: stage
+ )
+ end
+
+ where(:dag_dependency_status, :after_status) do
+ :created | 'created'
+ :pending | 'created'
+ :running | 'created'
+ :manual | 'created'
+ :scheduled | 'created'
+ :success | 'pending'
+ :failed | 'skipped'
+ :skipped | 'skipped'
+ end
+
+ with_them do
+ it 'updates the new job status to after_status' do
+ expect(subject).to be_success
+ expect(new_job.status).to eq after_status
+ end
+
+ context 'when feature flag is disabled' do
+ include_context 'with ci_retry_job_fix disabled'
+
+ it 'enqueues the new job' do
+ expect(subject).to be_success
+ expect(new_job).to be_pending
+ end
+ end
+ end
+ end
+
+ context 'when there are other manual/scheduled jobs' do
+ let_it_be(:test_manual_build) do
+ create(:ci_build, :manual, pipeline: pipeline, ci_stage: stage)
+ end
+
+ let_it_be(:subsequent_manual_build) do
+ create(:ci_build, :manual, pipeline: pipeline, ci_stage: deploy_stage)
+ end
+
+ let_it_be(:test_scheduled_build) do
+ create(:ci_build, :scheduled, pipeline: pipeline, ci_stage: stage)
+ end
+
+ let_it_be(:subsequent_scheduled_build) do
+ create(:ci_build, :scheduled, pipeline: pipeline, ci_stage: deploy_stage)
+ end
+
+ let!(:job) do
+ create(:ci_build, *[trait].compact, :failed, pipeline: pipeline, ci_stage: stage)
+ end
+
+ where(:trait, :enqueue_immediately) do
+ nil | false
+ :manual | true
+ :expired_scheduled | true
+ end
+
+ with_them do
+ it 'retries the given job but not the other manual/scheduled jobs' do
+ expect { subject }
+ .to change { Ci::Build.count }.by(1)
+ .and not_change { test_manual_build.reload.status }
+ .and not_change { subsequent_manual_build.reload.status }
+ .and not_change { test_scheduled_build.reload.status }
+ .and not_change { subsequent_scheduled_build.reload.status }
+
+ expect(new_job).to be_pending
+ end
+
+ it_behaves_like 'checks enqueue_immediately?'
+
+ context 'when feature flag is disabled' do
+ include_context 'with ci_retry_job_fix disabled'
+
+ it 'enqueues the new job' do
+ expect(subject).to be_success
+ expect(new_job).to be_pending
+ end
+
+ it_behaves_like 'checks enqueue_immediately?' do
+ let(:enqueue_immediately) { false }
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index 96437290ae3..77345096537 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -5,14 +5,16 @@ require 'spec_helper'
RSpec.describe Ci::RetryPipelineService, '#execute' do
include ProjectForksHelper
- let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let_it_be_with_refind(:user) { create(:user) }
+ let_it_be_with_refind(:project) { create(:project) }
+
let(:pipeline) { create(:ci_pipeline, project: project) }
- let(:service) { described_class.new(project, user) }
let(:build_stage) { create(:ci_stage, name: 'build', position: 0, pipeline: pipeline) }
let(:test_stage) { create(:ci_stage, name: 'test', position: 1, pipeline: pipeline) }
let(:deploy_stage) { create(:ci_stage, name: 'deploy', position: 2, pipeline: pipeline) }
+ subject(:service) { described_class.new(project, user) }
+
context 'when user has full ability to modify pipeline' do
before do
project.add_developer(user)
@@ -272,6 +274,21 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
expect(pipeline.reload).to be_running
end
end
+
+ context 'when there is a failed manual action' do
+ before do
+ create_build('rspec', :success, build_stage)
+ create_build('manual-rspec', :failed, build_stage, when: :manual, allow_failure: true)
+ end
+
+ it 'processes the manual action' do
+ service.execute(pipeline)
+
+ expect(build('rspec')).to be_success
+ expect(build('manual-rspec')).to be_manual
+ expect(pipeline.reload).to be_success
+ end
+ end
end
it 'closes all todos about failed jobs for pipeline' do
diff --git a/spec/services/ci/runners/bulk_delete_runners_service_spec.rb b/spec/services/ci/runners/bulk_delete_runners_service_spec.rb
index 8e9fc4e3012..fa8af1100df 100644
--- a/spec/services/ci/runners/bulk_delete_runners_service_spec.rb
+++ b/spec/services/ci/runners/bulk_delete_runners_service_spec.rb
@@ -5,78 +5,180 @@ 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_it_be(:admin_user) { create(:user, :admin) }
+ let_it_be_with_refind(:owner_user) { create(:user) } # discard memoized ci_owned_runners
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ let(:user) {}
+ let(:service_args) { { runners: runners_arg, current_user: user } }
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) }
+ let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
+ let!(:project_runner) { create(:ci_runner, :project, projects: [project]) }
shared_examples 'a service deleting runners in bulk' do
+ let!(:expected_deleted_ids) { expected_deleted_runners.map(&:id) }
+
it 'destroys runners', :aggregate_failures do
- expect { subject }.to change { Ci::Runner.count }.by(-2)
+ expect { execute }.to change { Ci::Runner.count }.by(-expected_deleted_ids.count)
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(execute.payload).to eq(
+ {
+ deleted_count: expected_deleted_ids.count,
+ deleted_ids: expected_deleted_ids,
+ errors: []
+ })
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)
+ expected_deleted_runners.each do |deleted_runner|
+ expect(deleted_runner[:errors]).to be_nil
+ expect { deleted_runner.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
end
- context 'with some runners already deleted' do
+ context 'with too many runners specified' do
before do
- instance_runner.destroy!
+ stub_const("#{described_class}::RUNNER_LIMIT", 1)
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)
+ it 'deletes only first RUNNER_LIMIT runners', :aggregate_failures do
+ expect { execute }.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)
+ expect(execute.payload).to eq(
+ {
+ deleted_count: 1,
+ deleted_ids: expected_deleted_ids.take(1),
+ errors: ["Can only delete up to 1 runners per call. Ignored the remaining runner(s)."]
+ })
end
end
+ end
- context 'with too many runners specified' do
+ context 'when the user cannot delete runners' do
+ let(:runners_arg) { Ci::Runner.all }
+
+ context 'when user is not group owner' do
before do
- stub_const("#{described_class}::RUNNER_LIMIT", 1)
+ group.add_developer(user)
end
- it 'deletes only first RUNNER_LIMIT runners' do
- expect { subject }.to change { Ci::Runner.count }.by(-1)
+ let(:user) { create(:user) }
- is_expected.to be_success
- expect(execute.payload).to eq({ deleted_count: 1, deleted_ids: [instance_runner.id] })
+ it 'does not delete any runner and returns error', :aggregate_failures do
+ expect { execute }.not_to change { Ci::Runner.count }
+ expect(execute[:errors]).to match_array("User does not have permission to delete any of the runners")
end
end
- end
- context 'with runners specified as relation' do
- let(:runners_arg) { Ci::Runner.not_group_type }
+ context 'when user is not part of the group' do
+ let(:user) { create(:user) }
- include_examples 'a service deleting runners in bulk'
+ it 'does not delete any runner and returns error', :aggregate_failures do
+ expect { execute }.not_to change { Ci::Runner.count }
+ expect(execute[:errors]).to match_array("User does not have permission to delete any of the runners")
+ end
+ end
end
- context 'with runners specified as array of IDs' do
- let(:runners_arg) { Ci::Runner.not_group_type.ids }
+ context 'when the user can delete runners' do
+ context 'when user is an admin', :enable_admin_mode do
+ include_examples 'a service deleting runners in bulk' do
+ let(:runners_arg) { Ci::Runner.all }
+ let!(:expected_deleted_runners) { [instance_runner, group_runner, project_runner] }
+ let(:user) { admin_user }
+ end
+
+ context 'with a runner already deleted' do
+ before do
+ group_runner.destroy!
+ end
+
+ include_examples 'a service deleting runners in bulk' do
+ let(:runners_arg) { Ci::Runner.all }
+ let!(:expected_deleted_runners) { [instance_runner, project_runner] }
+ let(:user) { admin_user }
+ end
+ end
+
+ context 'when deleting a single runner' do
+ let(:runners_arg) { Ci::Runner.all }
+
+ it 'avoids N+1 cached queries', :use_sql_query_cache, :request_store do
+ # Run this once to establish a baseline
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ execute
+ end
+
+ additional_runners = 1
+
+ create_list(:ci_runner, 1 + additional_runners, :instance)
+ create_list(:ci_runner, 1 + additional_runners, :group, groups: [group])
+ create_list(:ci_runner, 1 + additional_runners, :project, projects: [project])
+
+ service = described_class.new(runners: runners_arg, current_user: user)
+
+ # Base cost per runner is:
+ # - 1 `SELECT * FROM "taggings"` query
+ # - 1 `SAVEPOINT` query
+ # - 1 `DELETE FROM "ci_runners"` query
+ # - 1 `RELEASE SAVEPOINT` query
+ # Project runners have an additional query:
+ # - 1 `DELETE FROM "ci_runner_projects"` query, given the call to `destroy_all`
+ instance_runner_cost = 4
+ group_runner_cost = 4
+ project_runner_cost = 5
+ expect { service.execute }
+ .not_to exceed_all_query_limit(control_count)
+ .with_threshold(additional_runners * (instance_runner_cost + group_runner_cost + project_runner_cost))
+ end
+ end
+ end
- include_examples 'a service deleting runners in bulk'
+ context 'when user is group owner' do
+ before do
+ group.add_owner(user)
+ end
+
+ include_examples 'a service deleting runners in bulk' do
+ let(:runners_arg) { Ci::Runner.not_instance_type }
+ let!(:expected_deleted_runners) { [group_runner, project_runner] }
+ let(:user) { owner_user }
+ end
+
+ context 'with a runner non-authorised to be deleted' do
+ let(:runners_arg) { Ci::Runner.all }
+ let!(:expected_deleted_runners) { [project_runner] }
+ let(:user) { owner_user }
+
+ it 'destroys only authorised runners', :aggregate_failures do
+ allow(Ability).to receive(:allowed?).and_call_original
+ expect(Ability).to receive(:allowed?).with(user, :delete_runner, instance_runner).and_return(false)
+
+ expect { execute }.to change { Ci::Runner.count }.by(-2)
+
+ is_expected.to be_success
+ expect(execute.payload).to eq(
+ {
+ deleted_count: 2,
+ deleted_ids: [group_runner.id, project_runner.id],
+ errors: ["User does not have permission to delete runner(s) ##{instance_runner.id}"]
+ })
+ end
+ end
+ end
end
context 'with no arguments specified' do
let(:runners_arg) { nil }
+ let(:user) { owner_user }
it 'returns 0 deleted runners' do
is_expected.to be_success
- expect(execute.payload).to eq({ deleted_count: 0, deleted_ids: [] })
+ expect(execute.payload).to eq({ deleted_count: 0, deleted_ids: [], errors: [] })
end
end
end
diff --git a/spec/services/ci/runners/set_runner_associated_projects_service_spec.rb b/spec/services/ci/runners/set_runner_associated_projects_service_spec.rb
index 1f44612947b..e5cba80d567 100644
--- a/spec/services/ci/runners/set_runner_associated_projects_service_spec.rb
+++ b/spec/services/ci/runners/set_runner_associated_projects_service_spec.rb
@@ -67,6 +67,19 @@ RSpec.describe ::Ci::Runners::SetRunnerAssociatedProjectsService, '#execute' do
expect(runner.projects.ids).to match_array([owner_project.id] + project_ids)
end
end
+
+ context 'when disassociating all projects' do
+ let(:project_ids) { [] }
+
+ it 'reassigns associated projects and returns success response' do
+ expect(execute).to be_success
+
+ runner.reload
+
+ expect(runner.owner_project).to eq(owner_project)
+ expect(runner.projects.ids).to contain_exactly(owner_project.id)
+ end
+ end
end
context 'with failing assign_to requests' do
diff --git a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb
deleted file mode 100644
index 605d9e67ab6..00000000000
--- a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::CheckIngressIpAddressService do
- include ExclusiveLeaseHelpers
-
- let(:application) { create(:clusters_applications_ingress, :installed) }
- let(:service) { described_class.new(application) }
- let(:kubeclient) { double(::Kubeclient::Client, get_service: kube_service) }
- let(:lease_key) { "check_ingress_ip_address_service:#{application.id}" }
-
- let(:ingress) do
- [
- {
- ip: '111.222.111.222',
- hostname: 'localhost.localdomain'
- }
- ]
- end
-
- let(:kube_service) do
- ::Kubeclient::Resource.new(
- {
- status: {
- loadBalancer: {
- ingress: ingress
- }
- }
- }
- )
- end
-
- subject { service.execute }
-
- before do
- stub_exclusive_lease(lease_key, timeout: 15.seconds.to_i)
- allow(application.cluster).to receive(:kubeclient).and_return(kubeclient)
- end
-
- include_examples 'check ingress ip executions', :clusters_applications_ingress
-
- include_examples 'check ingress ip executions', :clusters_applications_knative
-end
diff --git a/spec/services/clusters/applications/check_installation_progress_service_spec.rb b/spec/services/clusters/applications/check_installation_progress_service_spec.rb
deleted file mode 100644
index 698804ff6af..00000000000
--- a/spec/services/clusters/applications/check_installation_progress_service_spec.rb
+++ /dev/null
@@ -1,204 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
- RESCHEDULE_PHASES = Gitlab::Kubernetes::Pod::PHASES - [Gitlab::Kubernetes::Pod::SUCCEEDED, Gitlab::Kubernetes::Pod::FAILED].freeze
-
- let(:application) { create(:clusters_applications_helm, :installing) }
- let(:service) { described_class.new(application) }
- let(:phase) { Gitlab::Kubernetes::Pod::UNKNOWN }
- let(:errors) { nil }
-
- shared_examples 'a not yet terminated installation' do |a_phase|
- let(:phase) { a_phase }
-
- before do
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- context "when phase is #{a_phase}" do
- context 'when not timed_out' do
- it 'reschedule a new check' do
- expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
- expect(service).not_to receive(:remove_installation_pod)
-
- expect do
- service.execute
-
- application.reload
- end.not_to change(application, :status)
-
- expect(application.status_reason).to be_nil
- end
- end
- end
- end
-
- shared_examples 'error handling' do
- context 'when installation raises a Kubeclient::HttpError' do
- let(:cluster) { create(:cluster, :provided_by_user, :project) }
- let(:logger) { service.send(:logger) }
- let(:error) { Kubeclient::HttpError.new(401, 'Unauthorized', nil) }
-
- before do
- application.update!(cluster: cluster)
-
- expect(service).to receive(:pod_phase).and_raise(error)
- end
-
- include_examples 'logs kubernetes errors' do
- let(:error_name) { 'Kubeclient::HttpError' }
- let(:error_message) { 'Unauthorized' }
- let(:error_code) { 401 }
- end
-
- it 'shows the response code from the error' do
- service.execute
-
- expect(application).to be_errored.or(be_update_errored)
- expect(application.status_reason).to eq('Kubernetes error: 401')
- end
- end
- end
-
- before do
- allow(service).to receive(:installation_errors).and_return(errors)
- allow(service).to receive(:remove_installation_pod).and_return(nil)
- end
-
- context 'when application is updating' do
- let(:application) { create(:clusters_applications_helm, :updating) }
-
- include_examples 'error handling'
-
- RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase }
-
- context 'when installation POD succeeded' do
- let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED }
-
- before do
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- it 'removes the installation POD' do
- expect(service).to receive(:remove_installation_pod).once
-
- service.execute
- end
-
- it 'make the application installed' do
- expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in)
-
- service.execute
-
- expect(application).to be_updated
- expect(application.status_reason).to be_nil
- end
- end
-
- context 'when installation POD failed' do
- let(:phase) { Gitlab::Kubernetes::Pod::FAILED }
- let(:errors) { 'test installation failed' }
-
- before do
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- it 'make the application errored' do
- service.execute
-
- expect(application).to be_update_errored
- expect(application.status_reason).to eq('Operation failed. Check pod logs for install-helm for more details.')
- end
- end
-
- context 'when timed out' do
- let(:application) { create(:clusters_applications_helm, :timed_out, :updating) }
-
- before do
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- it 'make the application errored' do
- expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in)
-
- service.execute
-
- expect(application).to be_update_errored
- expect(application.status_reason).to eq('Operation timed out. Check pod logs for install-helm for more details.')
- end
- end
- end
-
- context 'when application is installing' do
- include_examples 'error handling'
-
- RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase }
-
- context 'when installation POD succeeded' do
- let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED }
-
- before do
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- it 'removes the installation POD' do
- expect_next_instance_of(Gitlab::Kubernetes::Helm::API) do |instance|
- expect(instance).to receive(:delete_pod!).with(kind_of(String)).once
- end
- expect(service).to receive(:remove_installation_pod).and_call_original
-
- service.execute
- end
-
- it 'make the application installed' do
- expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in)
-
- service.execute
-
- expect(application).to be_installed
- expect(application.status_reason).to be_nil
- end
-
- it 'tracks application install', :snowplow do
- service.execute
-
- expect_snowplow_event(category: 'cluster:applications', action: 'cluster_application_helm_installed')
- end
- end
-
- context 'when installation POD failed' do
- let(:phase) { Gitlab::Kubernetes::Pod::FAILED }
- let(:errors) { 'test installation failed' }
-
- before do
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- it 'make the application errored' do
- service.execute
-
- expect(application).to be_errored
- expect(application.status_reason).to eq('Operation failed. Check pod logs for install-helm for more details.')
- end
- end
-
- context 'when timed out' do
- let(:application) { create(:clusters_applications_helm, :timed_out) }
-
- before do
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- it 'make the application errored' do
- expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in)
-
- service.execute
-
- expect(application).to be_errored
- expect(application.status_reason).to eq('Operation timed out. Check pod logs for install-helm for more details.')
- end
- end
- end
-end
diff --git a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb
deleted file mode 100644
index 4b8893429cf..00000000000
--- a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb
+++ /dev/null
@@ -1,155 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::CheckUninstallProgressService do
- reschedule_phases = Gitlab::Kubernetes::Pod::PHASES - [Gitlab::Kubernetes::Pod::SUCCEEDED, Gitlab::Kubernetes::Pod::FAILED].freeze
-
- let(:application) { create(:clusters_applications_prometheus, :uninstalling) }
- let(:service) { described_class.new(application) }
- let(:phase) { Gitlab::Kubernetes::Pod::UNKNOWN }
- let(:errors) { nil }
- let(:worker_class) { Clusters::Applications::WaitForUninstallAppWorker }
-
- before do
- allow(service).to receive(:installation_errors).and_return(errors)
- allow(service).to receive(:remove_installation_pod)
- end
-
- shared_examples 'a not yet terminated installation' do |a_phase|
- let(:phase) { a_phase }
-
- before do
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- context "when phase is #{a_phase}" do
- context 'when not timed_out' do
- it 'reschedule a new check' do
- expect(worker_class).to receive(:perform_in).once
- expect(service).not_to receive(:remove_installation_pod)
-
- expect do
- service.execute
-
- application.reload
- end.not_to change(application, :status)
-
- expect(application.status_reason).to be_nil
- end
- end
- end
- end
-
- context 'when application is uninstalling' do
- reschedule_phases.each { |phase| it_behaves_like 'a not yet terminated installation', phase }
-
- context 'when installation POD succeeded' do
- let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED }
-
- before do
- expect_next_instance_of(Gitlab::Kubernetes::Helm::API) do |instance|
- expect(instance).to receive(:delete_pod!).with(kind_of(String)).once
- end
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- it 'removes the installation POD' do
- expect(service).to receive(:remove_uninstallation_pod).and_call_original
-
- service.execute
- end
-
- it 'runs application post_uninstall' do
- expect(application).to receive(:post_uninstall).and_call_original
-
- service.execute
- end
-
- it 'destroys the application' do
- expect(worker_class).not_to receive(:perform_in)
-
- service.execute
-
- expect(application).to be_destroyed
- end
-
- context 'an error occurs while destroying' do
- before do
- expect(application).to receive(:destroy!).once.and_raise("destroy failed")
- end
-
- it 'still removes the installation POD' do
- expect(service).to receive(:remove_uninstallation_pod).and_call_original
-
- service.execute
- end
-
- it 'makes the application uninstall_errored' do
- service.execute
-
- expect(application).to be_uninstall_errored
- expect(application.status_reason).to eq('Application uninstalled but failed to destroy: destroy failed')
- end
- end
- end
-
- context 'when installation POD failed' do
- let(:phase) { Gitlab::Kubernetes::Pod::FAILED }
- let(:errors) { 'test installation failed' }
-
- before do
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- it 'make the application errored' do
- service.execute
-
- expect(application).to be_uninstall_errored
- expect(application.status_reason).to eq('Operation failed. Check pod logs for uninstall-prometheus for more details.')
- end
- end
-
- context 'when timed out' do
- let(:application) { create(:clusters_applications_prometheus, :timed_out, :uninstalling) }
-
- before do
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- it 'make the application errored' do
- expect(worker_class).not_to receive(:perform_in)
-
- service.execute
-
- expect(application).to be_uninstall_errored
- expect(application.status_reason).to eq('Operation timed out. Check pod logs for uninstall-prometheus for more details.')
- end
- end
-
- context 'when installation raises a Kubeclient::HttpError' do
- let(:cluster) { create(:cluster, :provided_by_user, :project) }
- let(:logger) { service.send(:logger) }
- let(:error) { Kubeclient::HttpError.new(401, 'Unauthorized', nil) }
-
- before do
- application.update!(cluster: cluster)
-
- expect(service).to receive(:pod_phase).and_raise(error)
- end
-
- include_examples 'logs kubernetes errors' do
- let(:error_name) { 'Kubeclient::HttpError' }
- let(:error_message) { 'Unauthorized' }
- let(:error_code) { 401 }
- end
-
- it 'shows the response code from the error' do
- service.execute
-
- expect(application).to be_uninstall_errored
- expect(application.status_reason).to eq('Kubernetes error: 401')
- end
- end
- end
-end
diff --git a/spec/services/clusters/applications/check_upgrade_progress_service_spec.rb b/spec/services/clusters/applications/check_upgrade_progress_service_spec.rb
deleted file mode 100644
index dbde8cec9b9..00000000000
--- a/spec/services/clusters/applications/check_upgrade_progress_service_spec.rb
+++ /dev/null
@@ -1,94 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::CheckUpgradeProgressService do
- reschedule_phashes = ::Gitlab::Kubernetes::Pod::PHASES -
- [::Gitlab::Kubernetes::Pod::SUCCEEDED, ::Gitlab::Kubernetes::Pod::FAILED, ::Gitlab].freeze
-
- let(:application) { create(:clusters_applications_prometheus, :updating) }
- let(:service) { described_class.new(application) }
- let(:phase) { ::Gitlab::Kubernetes::Pod::UNKNOWN }
- let(:errors) { nil }
-
- shared_examples 'a terminated upgrade' do
- it 'removes the POD' do
- expect(service).to receive(:remove_pod).once
-
- service.execute
- end
- end
-
- shared_examples 'a not yet terminated upgrade' do |a_phase|
- let(:phase) { a_phase }
-
- context "when phase is #{a_phase}" do
- context 'when not timed out' do
- it 'reschedule a new check' do
- expect(::ClusterWaitForAppUpdateWorker).to receive(:perform_in).once
- expect(service).not_to receive(:remove_pod)
-
- service.execute
-
- expect(application).to be_updating
- expect(application.status_reason).to be_nil
- end
- end
-
- context 'when timed out' do
- let(:application) { create(:clusters_applications_prometheus, :timed_out, :updating) }
-
- it_behaves_like 'a terminated upgrade'
-
- it 'make the application update errored' do
- expect(::ClusterWaitForAppUpdateWorker).not_to receive(:perform_in)
-
- service.execute
-
- expect(application).to be_update_errored
- expect(application.status_reason).to eq("Update timed out")
- end
- end
- end
- end
-
- before do
- allow(service).to receive(:phase).once.and_return(phase)
-
- allow(service).to receive(:errors).and_return(errors)
- allow(service).to receive(:remove_pod).and_return(nil)
- end
-
- describe '#execute' do
- context 'when upgrade pod succeeded' do
- let(:phase) { ::Gitlab::Kubernetes::Pod::SUCCEEDED }
-
- it_behaves_like 'a terminated upgrade'
-
- it 'make the application upgraded' do
- expect(::ClusterWaitForAppUpdateWorker).not_to receive(:perform_in)
-
- service.execute
-
- expect(application).to be_updated
- expect(application.status_reason).to be_nil
- end
- end
-
- context 'when upgrade pod failed' do
- let(:phase) { ::Gitlab::Kubernetes::Pod::FAILED }
- let(:errors) { 'test installation failed' }
-
- it_behaves_like 'a terminated upgrade'
-
- it 'make the application update errored' do
- service.execute
-
- expect(application).to be_update_errored
- expect(application.status_reason).to eq(errors)
- end
- end
-
- reschedule_phashes.each { |phase| it_behaves_like 'a not yet terminated upgrade', phase }
- end
-end
diff --git a/spec/services/clusters/applications/create_service_spec.rb b/spec/services/clusters/applications/create_service_spec.rb
deleted file mode 100644
index 00a67a9b2ef..00000000000
--- a/spec/services/clusters/applications/create_service_spec.rb
+++ /dev/null
@@ -1,279 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::CreateService do
- include TestRequestHelpers
-
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:user) { create(:user) }
- let(:params) { { application: 'ingress' } }
- let(:service) { described_class.new(cluster, user, params) }
-
- describe '#execute' do
- before do
- allow(ClusterInstallAppWorker).to receive(:perform_async)
- allow(ClusterUpgradeAppWorker).to receive(:perform_async)
- end
-
- subject { service.execute(test_request) }
-
- it 'creates an application' do
- expect do
- subject
-
- cluster.reload
- end.to change(cluster, :application_ingress)
- end
-
- context 'application already installed' do
- let!(:application) { create(:clusters_applications_ingress, :installed, cluster: cluster) }
-
- it 'does not create a new application' do
- expect do
- subject
- end.not_to change(Clusters::Applications::Ingress, :count)
- end
-
- it 'schedules an upgrade for the application' do
- expect(ClusterUpgradeAppWorker).to receive(:perform_async)
-
- subject
- end
- end
-
- context 'known applications' do
- context 'ingress application' do
- let(:params) do
- {
- application: 'ingress'
- }
- end
-
- before do
- expect_any_instance_of(Clusters::Applications::Ingress)
- .to receive(:make_scheduled!)
- .and_call_original
- end
-
- it 'creates the application' do
- expect do
- subject
-
- cluster.reload
- end.to change(cluster, :application_ingress)
- end
- end
-
- context 'cert manager application' do
- let(:params) do
- {
- application: 'cert_manager',
- email: 'test@example.com'
- }
- end
-
- before do
- expect_any_instance_of(Clusters::Applications::CertManager)
- .to receive(:make_scheduled!)
- .and_call_original
- end
-
- it 'creates the application' do
- expect do
- subject
-
- cluster.reload
- end.to change(cluster, :application_cert_manager)
- end
-
- it 'sets the email' do
- expect(subject.email).to eq('test@example.com')
- end
- end
-
- context 'jupyter application' do
- let(:params) do
- {
- application: 'jupyter',
- hostname: 'example.com'
- }
- end
-
- before do
- create(:clusters_applications_ingress, :installed, external_ip: "127.0.0.0", cluster: cluster)
- expect_any_instance_of(Clusters::Applications::Jupyter)
- .to receive(:make_scheduled!)
- .and_call_original
- end
-
- it 'creates the application' do
- expect do
- subject
-
- cluster.reload
- end.to change(cluster, :application_jupyter)
- end
-
- it 'sets the hostname' do
- expect(subject.hostname).to eq('example.com')
- end
-
- it 'sets the oauth_application' do
- expect(subject.oauth_application).to be_present
- end
- end
-
- context 'knative application' do
- let(:params) do
- {
- application: 'knative',
- hostname: 'example.com',
- pages_domain_id: domain.id
- }
- end
-
- let(:domain) { create(:pages_domain, :instance_serverless) }
- let(:associate_domain_service) { double('AssociateDomainService') }
-
- before do
- expect_any_instance_of(Clusters::Applications::Knative)
- .to receive(:make_scheduled!)
- .and_call_original
- end
-
- it 'creates the application' do
- expect do
- subject
-
- cluster.reload
- end.to change(cluster, :application_knative)
- end
-
- it 'sets the hostname' do
- expect(subject.hostname).to eq('example.com')
- end
-
- it 'executes AssociateDomainService' do
- expect(Serverless::AssociateDomainService).to receive(:new) do |knative, args|
- expect(knative).to be_a(Clusters::Applications::Knative)
- expect(args[:pages_domain_id]).to eq(params[:pages_domain_id])
- expect(args[:creator]).to eq(user)
-
- associate_domain_service
- end
-
- expect(associate_domain_service).to receive(:execute)
-
- subject
- end
- end
- end
-
- context 'invalid application' do
- let(:params) { { application: 'non-existent' } }
-
- it 'raises an error' do
- expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError)
- end
- end
-
- context 'group cluster' do
- let(:cluster) { create(:cluster, :provided_by_gcp, :group) }
-
- using RSpec::Parameterized::TableSyntax
-
- where(:application, :association, :allowed, :pre_create_ingress) do
- 'ingress' | :application_ingress | true | false
- 'runner' | :application_runner | true | false
- 'prometheus' | :application_prometheus | true | false
- 'jupyter' | :application_jupyter | true | true
- end
-
- with_them do
- before do
- klass = "Clusters::Applications::#{application.titleize}"
- allow_any_instance_of(klass.constantize).to receive(:make_scheduled!).and_call_original
- create(:clusters_applications_ingress, :installed, cluster: cluster, external_hostname: 'example.com') if pre_create_ingress
- end
-
- let(:params) { { application: application } }
-
- it 'executes for each application' do
- if allowed
- expect do
- subject
-
- cluster.reload
- end.to change(cluster, association)
- else
- expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError)
- end
- end
- end
- end
-
- context 'when application is installable' do
- shared_examples 'installable applications' do
- it 'makes the application scheduled' do
- expect do
- subject
- end.to change { Clusters::Applications::Ingress.with_status(:scheduled).count }.by(1)
- end
-
- it 'schedules an install via worker' do
- expect(ClusterInstallAppWorker)
- .to receive(:perform_async)
- .with(*worker_arguments)
- .once
-
- subject
- end
- end
-
- context 'when application is associated with a cluster' do
- let(:application) { create(:clusters_applications_ingress, :installable, cluster: cluster) }
- let(:worker_arguments) { [application.name, application.id] }
-
- it_behaves_like 'installable applications'
- end
-
- context 'when application is not associated with a cluster' do
- let(:worker_arguments) { [params[:application], kind_of(Numeric)] }
-
- it_behaves_like 'installable applications'
- end
- end
-
- context 'when installation is already in progress' do
- let!(:application) { create(:clusters_applications_ingress, :installing, cluster: cluster) }
-
- it 'raises an exception' do
- expect { subject }
- .to raise_exception(StateMachines::InvalidTransition)
- .and not_change(application.class.with_status(:scheduled), :count)
- end
-
- it 'does not schedule a cluster worker' do
- expect(ClusterInstallAppWorker).not_to receive(:perform_async)
- end
- end
-
- context 'when application is installed' do
- %i(installed updated).each do |status|
- let(:application) { create(:clusters_applications_ingress, status, cluster: cluster) }
-
- it 'schedules an upgrade via worker' do
- expect(ClusterUpgradeAppWorker)
- .to receive(:perform_async)
- .with(application.name, application.id)
- .once
-
- subject
-
- expect(application.reload).to be_scheduled
- end
- end
- end
- end
-end
diff --git a/spec/services/clusters/applications/patch_service_spec.rb b/spec/services/clusters/applications/patch_service_spec.rb
deleted file mode 100644
index 281da62b80b..00000000000
--- a/spec/services/clusters/applications/patch_service_spec.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::PatchService do
- describe '#execute' do
- let(:application) { create(:clusters_applications_knative, :scheduled) }
- let!(:update_command) { application.update_command }
- let(:service) { described_class.new(application) }
- let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::API) }
-
- before do
- allow(service).to receive(:update_command).and_return(update_command)
- allow(service).to receive(:helm_api).and_return(helm_client)
- end
-
- context 'when there are no errors' do
- before do
- expect(helm_client).to receive(:update).with(update_command)
- allow(ClusterWaitForAppInstallationWorker).to receive(:perform_in).and_return(nil)
- end
-
- it 'make the application updating' do
- expect(application.cluster).not_to be_nil
- service.execute
-
- expect(application).to be_updating
- end
-
- it 'schedule async installation status check' do
- expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
-
- service.execute
- end
- end
-
- context 'when kubernetes cluster communication fails' do
- let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) }
-
- before do
- expect(helm_client).to receive(:update).with(update_command).and_raise(error)
- end
-
- include_examples 'logs kubernetes errors' do
- let(:error_name) { 'Kubeclient::HttpError' }
- let(:error_message) { 'system failure' }
- let(:error_code) { 500 }
- end
-
- it 'make the application errored' do
- service.execute
-
- expect(application).to be_update_errored
- expect(application.status_reason).to eq(_('Kubernetes error: %{error_code}') % { error_code: 500 })
- end
- end
-
- context 'a non kubernetes error happens' do
- let(:application) { create(:clusters_applications_knative, :scheduled) }
- let(:error) { StandardError.new('something bad happened') }
-
- include_examples 'logs kubernetes errors' do
- let(:error_name) { 'StandardError' }
- let(:error_message) { 'something bad happened' }
- let(:error_code) { nil }
- end
-
- before do
- expect(helm_client).to receive(:update).with(update_command).and_raise(error)
- end
-
- it 'make the application errored' do
- service.execute
-
- expect(application).to be_update_errored
- expect(application.status_reason).to eq(_('Failed to update.'))
- end
- end
- end
-end
diff --git a/spec/services/clusters/applications/prometheus_update_service_spec.rb b/spec/services/clusters/applications/prometheus_update_service_spec.rb
deleted file mode 100644
index 615bfc44045..00000000000
--- a/spec/services/clusters/applications/prometheus_update_service_spec.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::PrometheusUpdateService do
- describe '#execute' do
- let(:project) { create(:project) }
- let(:environment) { create(:environment, project: project) }
- let(:cluster) { create(:cluster, :provided_by_user, :with_installed_helm, projects: [project]) }
- let(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
- let(:empty_alerts_values_update_yaml) { "---\nalertmanager:\n enabled: false\nserverFiles:\n alerts: {}\n" }
- let(:helm_client) { instance_double(::Gitlab::Kubernetes::Helm::API) }
-
- subject(:service) { described_class.new(application, project) }
-
- context 'when prometheus is a Clusters::Integrations::Prometheus' do
- let(:application) { create(:clusters_integrations_prometheus, cluster: cluster) }
-
- it 'raises NotImplementedError' do
- expect { service.execute }.to raise_error(NotImplementedError)
- end
- end
-
- context 'when prometheus is externally installed' do
- let(:application) { create(:clusters_applications_prometheus, :externally_installed, cluster: cluster) }
-
- it 'raises NotImplementedError' do
- expect { service.execute }.to raise_error(NotImplementedError)
- end
- end
-
- context 'when prometheus is a Clusters::Applications::Prometheus' do
- let!(:patch_command) { application.patch_command(empty_alerts_values_update_yaml) }
-
- before do
- allow(service).to receive(:patch_command).with(empty_alerts_values_update_yaml).and_return(patch_command)
- allow(service).to receive(:helm_api).and_return(helm_client)
- end
-
- context 'when there are no errors' do
- before do
- expect(helm_client).to receive(:update).with(patch_command)
-
- allow(::ClusterWaitForAppUpdateWorker)
- .to receive(:perform_in)
- .and_return(nil)
- end
-
- it 'make the application updating' do
- expect(application.cluster).not_to be_nil
-
- service.execute
-
- expect(application).to be_updating
- end
-
- it 'updates current config' do
- prometheus_config_service = spy(:prometheus_config_service)
-
- expect(Clusters::Applications::PrometheusConfigService)
- .to receive(:new)
- .with(project, cluster, application)
- .and_return(prometheus_config_service)
-
- expect(prometheus_config_service)
- .to receive(:execute)
- .and_return(YAML.safe_load(empty_alerts_values_update_yaml))
-
- service.execute
- end
-
- it 'schedules async update status check' do
- expect(::ClusterWaitForAppUpdateWorker).to receive(:perform_in).once
-
- service.execute
- end
- end
-
- context 'when k8s cluster communication fails' do
- before do
- error = ::Kubeclient::HttpError.new(500, 'system failure', nil)
- allow(helm_client).to receive(:update).and_raise(error)
- end
-
- it 'make the application update errored' do
- service.execute
-
- expect(application).to be_update_errored
- expect(application.status_reason).to match(/kubernetes error:/i)
- end
- end
-
- context 'when application cannot be persisted' do
- let(:application) { build(:clusters_applications_prometheus, :installed) }
-
- before do
- allow(application).to receive(:make_updating!).once
- .and_raise(ActiveRecord::RecordInvalid.new(application))
- end
-
- it 'make the application update errored' do
- expect(helm_client).not_to receive(:update)
-
- service.execute
-
- expect(application).to be_update_errored
- end
- end
- end
- end
-end
diff --git a/spec/services/clusters/applications/update_service_spec.rb b/spec/services/clusters/applications/update_service_spec.rb
deleted file mode 100644
index 4c05a12a4a1..00000000000
--- a/spec/services/clusters/applications/update_service_spec.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::UpdateService do
- include TestRequestHelpers
-
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:user) { create(:user) }
- let(:params) { { application: 'knative', hostname: 'update.example.com', pages_domain_id: domain.id } }
- let(:service) { described_class.new(cluster, user, params) }
- let(:domain) { create(:pages_domain, :instance_serverless) }
-
- subject { service.execute(test_request) }
-
- describe '#execute' do
- before do
- allow(ClusterPatchAppWorker).to receive(:perform_async)
- end
-
- context 'application is not installed' do
- it 'raises Clusters::Applications::BaseService::InvalidApplicationError' do
- expect(ClusterPatchAppWorker).not_to receive(:perform_async)
-
- expect { subject }
- .to raise_exception { Clusters::Applications::BaseService::InvalidApplicationError }
- .and not_change { Clusters::Applications::Knative.count }
- .and not_change { Clusters::Applications::Knative.with_status(:scheduled).count }
- end
- end
-
- context 'application is installed' do
- context 'application is schedulable' do
- let!(:application) do
- create(:clusters_applications_knative, status: 3, cluster: cluster)
- end
-
- it 'updates the application data' do
- expect do
- subject
- end.to change { application.reload.hostname }.to(params[:hostname])
- end
-
- it 'makes application scheduled!' do
- subject
-
- expect(application.reload).to be_scheduled
- end
-
- it 'schedules ClusterPatchAppWorker' do
- expect(ClusterPatchAppWorker).to receive(:perform_async)
-
- subject
- end
-
- context 'knative application' do
- let(:associate_domain_service) { double('AssociateDomainService') }
-
- it 'executes AssociateDomainService' do
- expect(Serverless::AssociateDomainService).to receive(:new) do |knative, args|
- expect(knative.id).to eq(application.id)
- expect(args[:pages_domain_id]).to eq(params[:pages_domain_id])
- expect(args[:creator]).to eq(user)
-
- associate_domain_service
- end
-
- expect(associate_domain_service).to receive(:execute)
-
- subject
- end
- end
- end
-
- context 'application is not schedulable' do
- let!(:application) do
- create(:clusters_applications_knative, status: 4, cluster: cluster)
- end
-
- it 'raises StateMachines::InvalidTransition' do
- expect(ClusterPatchAppWorker).not_to receive(:perform_async)
-
- expect { subject }
- .to raise_exception { StateMachines::InvalidTransition }
- .and not_change { application.reload.hostname }
- .and not_change { Clusters::Applications::Knative.with_status(:scheduled).count }
- end
- end
- end
- end
-end
diff --git a/spec/services/clusters/gcp/provision_service_spec.rb b/spec/services/clusters/gcp/provision_service_spec.rb
index c5778db6001..c8b7f628e5b 100644
--- a/spec/services/clusters/gcp/provision_service_spec.rb
+++ b/spec/services/clusters/gcp/provision_service_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe Clusters::Gcp::ProvisionService do
gcp_project_id, zone,
{
"status": 'unexpected'
- } )
+ })
end
it_behaves_like 'error'
diff --git a/spec/services/clusters/gcp/verify_provision_status_service_spec.rb b/spec/services/clusters/gcp/verify_provision_status_service_spec.rb
index ccb4b3b6c15..ffe4516c02b 100644
--- a/spec/services/clusters/gcp/verify_provision_status_service_spec.rb
+++ b/spec/services/clusters/gcp/verify_provision_status_service_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe Clusters::Gcp::VerifyProvisionStatusService do
{
"status": 'RUNNING',
"startTime": 1.minute.ago.strftime("%FT%TZ")
- } )
+ })
end
it_behaves_like 'continue_creation'
@@ -56,7 +56,7 @@ RSpec.describe Clusters::Gcp::VerifyProvisionStatusService do
{
"status": 'RUNNING',
"startTime": 30.minutes.ago.strftime("%FT%TZ")
- } )
+ })
end
it_behaves_like 'error'
@@ -70,7 +70,7 @@ RSpec.describe Clusters::Gcp::VerifyProvisionStatusService do
{
"status": 'PENDING',
"startTime": 1.minute.ago.strftime("%FT%TZ")
- } )
+ })
end
it_behaves_like 'continue_creation'
@@ -82,7 +82,7 @@ RSpec.describe Clusters::Gcp::VerifyProvisionStatusService do
gcp_project_id, zone, operation_id,
{
"status": 'DONE'
- } )
+ })
end
it_behaves_like 'finalize_creation'
@@ -94,7 +94,7 @@ RSpec.describe Clusters::Gcp::VerifyProvisionStatusService do
gcp_project_id, zone, operation_id,
{
"status": 'unexpected'
- } )
+ })
end
it_behaves_like 'error'
diff --git a/spec/services/clusters/kubernetes/configure_istio_ingress_service_spec.rb b/spec/services/clusters/kubernetes/configure_istio_ingress_service_spec.rb
deleted file mode 100644
index f26177a56d0..00000000000
--- a/spec/services/clusters/kubernetes/configure_istio_ingress_service_spec.rb
+++ /dev/null
@@ -1,223 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Kubernetes::ConfigureIstioIngressService, '#execute' do
- include KubernetesHelpers
-
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:api_url) { 'https://kubernetes.example.com' }
- let(:project) { cluster.project }
- let(:environment) { create(:environment, project: project) }
- let(:cluster_project) { cluster.cluster_project }
- let(:namespace) { "#{project.name}-#{project.id}-#{environment.slug}" }
- let(:kubeclient) { cluster.kubeclient }
-
- subject do
- described_class.new(
- cluster: cluster
- ).execute
- end
-
- before do
- stub_kubeclient_discover_istio(api_url)
- stub_kubeclient_create_secret(api_url, namespace: namespace)
- stub_kubeclient_put_secret(api_url, "#{namespace}-token", namespace: namespace)
-
- stub_kubeclient_get_secret(
- api_url,
- metadata_name: "#{namespace}-token",
- token: Base64.encode64('sample-token'),
- namespace: namespace
- )
-
- stub_kubeclient_get_secret(
- api_url,
- metadata_name: 'istio-ingressgateway-ca-certs',
- namespace: 'istio-system'
- )
-
- stub_kubeclient_get_secret(
- api_url,
- metadata_name: 'istio-ingressgateway-certs',
- namespace: 'istio-system'
- )
-
- stub_kubeclient_put_secret(api_url, 'istio-ingressgateway-ca-certs', namespace: 'istio-system')
- stub_kubeclient_put_secret(api_url, 'istio-ingressgateway-certs', namespace: 'istio-system')
- stub_kubeclient_get_gateway(api_url, 'knative-ingress-gateway', namespace: 'knative-serving')
- stub_kubeclient_put_gateway(api_url, 'knative-ingress-gateway', namespace: 'knative-serving')
- end
-
- context 'without a serverless_domain_cluster' do
- it 'configures gateway to use PASSTHROUGH' do
- subject
-
- expect(WebMock).to have_requested(:put, api_url + '/apis/networking.istio.io/v1alpha3/namespaces/knative-serving/gateways/knative-ingress-gateway').with(
- body: hash_including(
- apiVersion: "networking.istio.io/v1alpha3",
- kind: "Gateway",
- metadata: {
- generation: 1,
- labels: {
- "networking.knative.dev/ingress-provider" => "istio",
- "serving.knative.dev/release" => "v0.7.0"
- },
- name: "knative-ingress-gateway",
- namespace: "knative-serving",
- selfLink: "/apis/networking.istio.io/v1alpha3/namespaces/knative-serving/gateways/knative-ingress-gateway"
- },
- spec: {
- selector: {
- istio: "ingressgateway"
- },
- servers: [
- {
- hosts: ["*"],
- port: {
- name: "http",
- number: 80,
- protocol: "HTTP"
- }
- },
- {
- hosts: ["*"],
- port: {
- name: "https",
- number: 443,
- protocol: "HTTPS"
- },
- tls: {
- mode: "PASSTHROUGH"
- }
- }
- ]
- }
- )
- )
- end
- end
-
- context 'with a serverless_domain_cluster' do
- let(:serverless_domain_cluster) { create(:serverless_domain_cluster) }
- let(:certificate) { OpenSSL::X509::Certificate.new(serverless_domain_cluster.certificate) }
-
- before do
- cluster.application_knative = serverless_domain_cluster.knative
- end
-
- it 'configures certificates' do
- subject
-
- expect(serverless_domain_cluster.reload.key).not_to be_blank
- expect(serverless_domain_cluster.reload.certificate).not_to be_blank
-
- expect(certificate.subject.to_s).to include(serverless_domain_cluster.knative.hostname)
-
- expect(certificate.not_before).to be_within(1.minute).of(Time.current)
- expect(certificate.not_after).to be_within(1.minute).of(Time.current + 1000.years)
-
- expect(WebMock).to have_requested(:put, api_url + '/api/v1/namespaces/istio-system/secrets/istio-ingressgateway-ca-certs').with(
- body: hash_including(
- metadata: {
- name: 'istio-ingressgateway-ca-certs',
- namespace: 'istio-system'
- },
- type: 'Opaque'
- )
- )
-
- expect(WebMock).to have_requested(:put, api_url + '/api/v1/namespaces/istio-system/secrets/istio-ingressgateway-certs').with(
- body: hash_including(
- metadata: {
- name: 'istio-ingressgateway-certs',
- namespace: 'istio-system'
- },
- type: 'kubernetes.io/tls'
- )
- )
- end
-
- it 'configures gateway to use MUTUAL' do
- subject
-
- expect(WebMock).to have_requested(:put, api_url + '/apis/networking.istio.io/v1alpha3/namespaces/knative-serving/gateways/knative-ingress-gateway').with(
- body: {
- apiVersion: "networking.istio.io/v1alpha3",
- kind: "Gateway",
- metadata: {
- generation: 1,
- labels: {
- "networking.knative.dev/ingress-provider" => "istio",
- "serving.knative.dev/release" => "v0.7.0"
- },
- name: "knative-ingress-gateway",
- namespace: "knative-serving",
- selfLink: "/apis/networking.istio.io/v1alpha3/namespaces/knative-serving/gateways/knative-ingress-gateway"
- },
- spec: {
- selector: {
- istio: "ingressgateway"
- },
- servers: [
- {
- hosts: ["*"],
- port: {
- name: "http",
- number: 80,
- protocol: "HTTP"
- }
- },
- {
- hosts: ["*"],
- port: {
- name: "https",
- number: 443,
- protocol: "HTTPS"
- },
- tls: {
- mode: "MUTUAL",
- privateKey: "/etc/istio/ingressgateway-certs/tls.key",
- serverCertificate: "/etc/istio/ingressgateway-certs/tls.crt",
- caCertificates: "/etc/istio/ingressgateway-ca-certs/cert.pem"
- }
- }
- ]
- }
- }
- )
- end
- end
-
- context 'when there is an error' do
- before do
- cluster.application_knative = create(:clusters_applications_knative)
-
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:configure_passthrough).and_raise(error)
- end
- end
-
- context 'Kubeclient::HttpError' do
- let(:error) { Kubeclient::HttpError.new(404, nil, nil) }
-
- it 'puts Knative into an errored state' do
- subject
-
- expect(cluster.application_knative).to be_errored
- expect(cluster.application_knative.status_reason).to eq('Kubernetes error: 404')
- end
- end
-
- context 'StandardError' do
- let(:error) { RuntimeError.new('something went wrong') }
-
- it 'puts Knative into an errored state' do
- subject
-
- expect(cluster.application_knative).to be_errored
- expect(cluster.application_knative.status_reason).to eq('Failed to update.')
- end
- end
- end
-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 064f9e42e96..37478a0bcd9 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
@@ -166,7 +166,7 @@ RSpec.describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do
expect(WebMock).to have_requested(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{role_binding_name}").with(
body: hash_including(
- metadata: { name: "gitlab-#{namespace}", namespace: "#{namespace}" },
+ metadata: { name: "gitlab-#{namespace}", namespace: namespace.to_s },
roleRef: {
apiGroup: 'rbac.authorization.k8s.io',
kind: 'ClusterRole',
diff --git a/spec/services/dependency_proxy/find_cached_manifest_service_spec.rb b/spec/services/dependency_proxy/find_cached_manifest_service_spec.rb
index 607d67d8efe..470c6eb9e03 100644
--- a/spec/services/dependency_proxy/find_cached_manifest_service_spec.rb
+++ b/spec/services/dependency_proxy/find_cached_manifest_service_spec.rb
@@ -39,6 +39,14 @@ RSpec.describe DependencyProxy::FindCachedManifestService do
end
end
+ shared_examples 'returning an error' do
+ it 'returns an error', :aggregate_failures do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:http_status]).to eq(503)
+ expect(subject[:message]).to eq('Failed to download the manifest from the external registry')
+ end
+ end
+
context 'when no manifest exists' do
let_it_be(:image) { 'new-image' }
@@ -101,7 +109,7 @@ RSpec.describe DependencyProxy::FindCachedManifestService do
it_behaves_like 'returning no manifest'
end
- context 'failed connection' do
+ context 'when the connection fails' do
before do
expect(DependencyProxy::HeadManifestService).to receive(:new).and_raise(Net::OpenTimeout)
end
@@ -111,12 +119,24 @@ RSpec.describe DependencyProxy::FindCachedManifestService do
context 'and no manifest is cached' do
let_it_be(:image) { 'new-image' }
- it 'returns an error', :aggregate_failures do
- expect(subject[:status]).to eq(:error)
- expect(subject[:http_status]).to eq(503)
- expect(subject[:message]).to eq('Failed to download the manifest from the external registry')
+ it_behaves_like 'returning an error'
+ end
+ end
+
+ context 'when the connection is successful but with error in result' do
+ before do
+ allow_next_instance_of(DependencyProxy::HeadManifestService) do |service|
+ allow(service).to receive(:execute).and_return(status: :error, http_status: 401, message: "Not found")
end
end
+
+ it_behaves_like 'using the cached manifest'
+
+ context 'and no manifest is cached' do
+ let_it_be(:image) { 'new-image' }
+
+ it_behaves_like 'returning no manifest'
+ end
end
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 a2e1acadcc1..3748df87d99 100644
--- a/spec/services/deployments/create_for_build_service_spec.rb
+++ b/spec/services/deployments/create_for_build_service_spec.rb
@@ -30,7 +30,9 @@ RSpec.describe Deployments::CreateForBuildService do
context 'when creation failure occures' do
before do
- allow(build).to receive(:create_deployment!) { raise ActiveRecord::RecordInvalid }
+ allow_next_instance_of(Deployment) do |deployment|
+ allow(deployment).to receive(:save!) { raise ActiveRecord::RecordInvalid }
+ end
end
it 'trackes the exception' do
@@ -79,5 +81,88 @@ RSpec.describe Deployments::CreateForBuildService do
expect { subject }.not_to change { Deployment.count }
end
end
+
+ context 'when build has environment attribute' do
+ let!(:build) do
+ create(:ci_build, environment: 'production', project: project,
+ options: { environment: { name: 'production', **kubernetes_options } })
+ end
+
+ let!(:environment) { create(:environment, project: project, name: build.expanded_environment_name) }
+
+ let(:kubernetes_options) { {} }
+
+ it 'returns a deployment object with environment' do
+ expect(subject).to be_a(Deployment)
+ expect(subject.iid).to be_present
+ expect(subject.environment.name).to eq('production')
+ expect(subject.cluster).to be_nil
+ expect(subject.deployment_cluster).to be_nil
+ end
+
+ context 'when environment has deployment platform' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project], managed: managed_cluster) }
+ let(:managed_cluster) { true }
+
+ it 'sets the cluster and deployment_cluster' do
+ expect(subject.cluster).to eq(cluster) # until we stop double writing in 12.9: https://gitlab.com/gitlab-org/gitlab/issues/202628
+ expect(subject.deployment_cluster.cluster).to eq(cluster)
+ end
+
+ context 'when a custom namespace is given' do
+ let(:kubernetes_options) { { kubernetes: { namespace: 'the-custom-namespace' } } }
+
+ context 'when cluster is managed' do
+ it 'does not set the custom namespace' do
+ expect(subject.deployment_cluster.kubernetes_namespace).not_to eq('the-custom-namespace')
+ end
+ end
+
+ context 'when cluster is not managed' do
+ let(:managed_cluster) { false }
+
+ it 'sets the custom namespace' do
+ expect(subject.deployment_cluster.kubernetes_namespace).to eq('the-custom-namespace')
+ end
+ end
+ end
+ end
+
+ context 'when build already has deployment' do
+ let!(:build) { create(:ci_build, :with_deployment, project: project, environment: 'production') }
+ let!(:environment) {}
+
+ it 'returns the persisted deployment' do
+ expect { subject }.not_to change { Deployment.count }
+
+ is_expected.to eq(build.deployment)
+ end
+ end
+ end
+
+ context 'when build does not start environment' do
+ where(:action) do
+ %w[stop prepare verify access]
+ end
+
+ with_them do
+ let!(:build) do
+ create(:ci_build, environment: 'production', project: project,
+ options: { environment: { name: 'production', action: action } })
+ end
+
+ it 'returns nothing' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ context 'when build does not have environment attribute' do
+ let!(:build) { create(:ci_build, project: project) }
+
+ it 'returns nothing' do
+ is_expected.to be_nil
+ end
+ end
end
end
diff --git a/spec/services/environments/create_for_build_service_spec.rb b/spec/services/environments/create_for_build_service_spec.rb
new file mode 100644
index 00000000000..721822f355b
--- /dev/null
+++ b/spec/services/environments/create_for_build_service_spec.rb
@@ -0,0 +1,304 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Environments::CreateForBuildService do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let!(:job) { build(:ci_build, project: project, pipeline: pipeline, **attributes) }
+ let(:service) { described_class.new }
+ let(:merge_request) {}
+
+ describe '#execute' do
+ subject { service.execute(job, merge_request: merge_request) }
+
+ shared_examples_for 'returning a correct environment' do
+ let(:expected_auto_stop_in_seconds) do
+ ChronicDuration.parse(expected_auto_stop_in).seconds if expected_auto_stop_in
+ end
+
+ it 'returns a persisted environment object' do
+ freeze_time do
+ expect { subject }.to change { Environment.count }.by(1)
+
+ expect(subject).to be_a(Environment)
+ expect(subject).to be_persisted
+ expect(subject.project).to eq(project)
+ expect(subject.name).to eq(expected_environment_name)
+ expect(subject.auto_stop_in).to eq(expected_auto_stop_in_seconds)
+ end
+ end
+
+ context 'when environment has already existed' do
+ let!(:environment) do
+ create(:environment,
+ project: project,
+ name: expected_environment_name
+ ).tap do |env|
+ env.auto_stop_in = expected_auto_stop_in
+ end
+ end
+
+ it 'returns the existing environment object' do
+ expect { subject }.not_to change { Environment.count }
+ expect { subject }.not_to change { environment.auto_stop_at }
+
+ expect(subject).to be_persisted
+ expect(subject).to eq(environment)
+ end
+ end
+ end
+
+ context 'when job has environment name attribute' do
+ let(:environment_name) { 'production' }
+ let(:expected_environment_name) { 'production' }
+ let(:expected_auto_stop_in) { nil }
+
+ let(:attributes) do
+ {
+ environment: environment_name,
+ options: { environment: { name: environment_name } }
+ }
+ end
+
+ it_behaves_like 'returning a correct environment'
+
+ context 'and job environment also has an auto_stop_in attribute' do
+ let(:environment_auto_stop_in) { '5 minutes' }
+ let(:expected_auto_stop_in) { '5 minutes' }
+
+ let(:attributes) do
+ {
+ environment: environment_name,
+ options: {
+ environment: {
+ name: environment_name,
+ auto_stop_in: environment_auto_stop_in
+ }
+ }
+ }
+ end
+
+ it_behaves_like 'returning a correct environment'
+ end
+
+ context 'and job environment has an auto_stop_in variable attribute' do
+ let(:environment_auto_stop_in) { '10 minutes' }
+ let(:expected_auto_stop_in) { '10 minutes' }
+
+ let(:attributes) do
+ {
+ environment: environment_name,
+ options: {
+ environment: {
+ name: environment_name,
+ auto_stop_in: '$TTL'
+ }
+ },
+ yaml_variables: [
+ { key: "TTL", value: environment_auto_stop_in, public: true }
+ ]
+ }
+ end
+
+ it_behaves_like 'returning a correct environment'
+ end
+ end
+
+ context 'when job has deployment tier attribute' do
+ let(:attributes) do
+ {
+ environment: 'customer-portal',
+ options: {
+ environment: {
+ name: 'customer-portal',
+ deployment_tier: deployment_tier
+ }
+ }
+ }
+ end
+
+ let(:deployment_tier) { 'production' }
+
+ context 'when environment has not been created yet' do
+ it 'sets the specified deployment tier' do
+ is_expected.to be_production
+ end
+
+ context 'when deployment tier is staging' do
+ let(:deployment_tier) { 'staging' }
+
+ it 'sets the specified deployment tier' do
+ is_expected.to be_staging
+ end
+ end
+
+ context 'when deployment tier is unknown' do
+ let(:deployment_tier) { 'unknown' }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(ArgumentError, "'unknown' is not a valid tier")
+ end
+ end
+ end
+
+ context 'when environment has already been created' do
+ before do
+ create(:environment, project: project, name: 'customer-portal', tier: :staging)
+ end
+
+ it 'does not overwrite the specified deployment tier' do
+ # This is to be updated when a deployment succeeded i.e. Deployments::UpdateEnvironmentService.
+ is_expected.to be_staging
+ end
+ end
+ end
+
+ context 'when job starts a review app' do
+ let(:environment_name) { 'review/$CI_COMMIT_REF_NAME' }
+ let(:expected_environment_name) { "review/#{job.ref}" }
+ let(:expected_auto_stop_in) { nil }
+
+ let(:attributes) do
+ {
+ environment: environment_name,
+ options: { environment: { name: environment_name } }
+ }
+ end
+
+ it_behaves_like 'returning a correct environment'
+ end
+
+ context 'when job stops a review app' do
+ let(:environment_name) { 'review/$CI_COMMIT_REF_NAME' }
+ let(:expected_environment_name) { "review/#{job.ref}" }
+ let(:expected_auto_stop_in) { nil }
+
+ let(:attributes) do
+ {
+ environment: environment_name,
+ options: { environment: { name: environment_name, action: 'stop' } }
+ }
+ end
+
+ it_behaves_like 'returning a correct environment'
+ end
+
+ context 'when merge_request is provided' do
+ let(:environment_name) { 'development' }
+ let(:attributes) { { environment: environment_name, options: { environment: { name: environment_name } } } }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:seed) { described_class.new(job, merge_request: merge_request) }
+
+ context 'and environment does not exist' do
+ let(:environment_name) { 'review/$CI_COMMIT_REF_NAME' }
+
+ it 'creates an environment associated with the merge request' do
+ expect { subject }.to change { Environment.count }.by(1)
+
+ expect(subject.merge_request).to eq(merge_request)
+ end
+ end
+
+ context 'and environment already exists' do
+ before do
+ create(:environment, project: project, name: environment_name)
+ end
+
+ it 'does not change the merge request associated with the environment' do
+ expect { subject }.not_to change { Environment.count }
+
+ expect(subject.merge_request).to be_nil
+ end
+ end
+ end
+
+ context 'when a pipeline contains a deployment job' do
+ let!(:job) { build(:ci_build, :start_review_app, project: project) }
+
+ context 'and the environment does not exist' do
+ it 'creates the environment specified by the job' do
+ expect { subject }.to change { Environment.count }.by(1)
+
+ expect(environment).to be_present
+ expect(job.persisted_environment.name).to eq('review/master')
+ expect(job.metadata.expanded_environment_name).to eq('review/master')
+ end
+
+ context 'and the pipeline is for a merge request' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it 'associates the environment with the merge request' do
+ expect { subject }.to change { Environment.count }.by(1)
+
+ expect(environment.merge_request).to eq(merge_request)
+ end
+ end
+ end
+
+ context 'when an environment already exists' do
+ before do
+ create(:environment, project: project, name: 'review/master')
+ end
+
+ it 'ensures environment existence for the job' do
+ expect { subject }.not_to change { Environment.count }
+
+ expect(environment).to be_present
+ expect(job.persisted_environment.name).to eq('review/master')
+ expect(job.metadata.expanded_environment_name).to eq('review/master')
+ end
+
+ context 'and the pipeline is for a merge request' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it 'does not associate the environment with the merge request' do
+ expect { subject }.not_to change { Environment.count }
+
+ expect(environment.merge_request).to be_nil
+ end
+ end
+ end
+
+ context 'when an environment name contains an invalid character' do
+ before do
+ job.pipeline = build(:ci_pipeline, ref: '!!!', project: project)
+ end
+
+ it 'sets the failure status' do
+ expect { subject }.not_to change { Environment.count }
+
+ expect(job).to be_failed
+ expect(job).to be_environment_creation_failure
+ expect(job.persisted_environment).to be_nil
+ end
+ end
+ end
+
+ context 'when a pipeline contains a teardown job' do
+ let!(:job) { build(:ci_build, :stop_review_app, project: project) }
+
+ it 'ensures environment existence for the job' do
+ expect { subject }.to change { Environment.count }.by(1)
+
+ expect(environment).to be_present
+ expect(job.persisted_environment.name).to eq('review/master')
+ expect(job.metadata.expanded_environment_name).to eq('review/master')
+ end
+ end
+
+ context 'when a pipeline does not contain a deployment job' do
+ let!(:job) { build(:ci_build, project: project) }
+
+ it 'does not create any environments' do
+ expect { subject }.not_to change { Environment.count }
+ end
+ end
+
+ def environment
+ project.environments.find_by_name('review/master')
+ end
+ end
+end
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 06f0eb1efbc..c3ae062a4b2 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -20,33 +20,6 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
end
end
- shared_examples 'Snowplow event' do
- let(:label) { nil }
-
- it 'is not emitted if FF is disabled' do
- stub_feature_flags(feature_flag_name => false)
-
- subject
-
- expect_no_snowplow_event
- end
-
- it 'is emitted' do
- params = {
- category: category,
- action: action,
- namespace: namespace,
- user: user,
- project: project,
- label: label
- }.compact
-
- subject
-
- expect_snowplow_event(**params)
- end
- end
-
describe 'Issues' do
describe '#open_issue' do
let(:issue) { create(:issue) }
@@ -95,14 +68,17 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION }
end
- it_behaves_like 'Snowplow event' do
- let(:category) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION.to_s }
- let(:label) { 'merge_requests_users' }
- let(:action) { 'create' }
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:category) { described_class.name }
+ let(:action) { 'created' }
+ let(:label) { 'usage_activity_by_stage_monthly.create.merge_requests_users' }
let(:namespace) { project.namespace }
let(:project) { merge_request.project }
let(:user) { merge_request.author }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: 'merge_requests_users').to_context]
+ end
end
end
@@ -121,14 +97,17 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION }
end
- it_behaves_like 'Snowplow event' do
- let(:category) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION.to_s }
- let(:label) { 'merge_requests_users' }
- let(:action) { 'close' }
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:category) { described_class.name }
+ let(:action) { 'closed' }
+ let(:label) { 'usage_activity_by_stage_monthly.create.merge_requests_users' }
let(:namespace) { project.namespace }
let(:project) { merge_request.project }
let(:user) { merge_request.author }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: 'merge_requests_users').to_context]
+ end
end
end
@@ -147,14 +126,17 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION }
end
- it_behaves_like 'Snowplow event' do
- let(:category) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION.to_s }
- let(:label) { 'merge_requests_users' }
- let(:action) { 'merge' }
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:category) { described_class.name }
+ let(:action) { 'merged' }
+ let(:label) { 'usage_activity_by_stage_monthly.create.merge_requests_users' }
let(:namespace) { project.namespace }
let(:project) { merge_request.project }
let(:user) { merge_request.author }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: 'merge_requests_users').to_context]
+ end
end
end
@@ -330,11 +312,16 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::PUSH_ACTION }
end
- it_behaves_like 'Snowplow event' do
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
let(:category) { described_class.to_s }
- let(:action) { 'action_active_users_project_repo' }
+ let(:action) { :push }
let(:namespace) { project.namespace }
let(:feature_flag_name) { :route_hll_to_snowplow }
+ let(:label) { 'usage_activity_by_stage_monthly.create.action_monthly_active_users_project_repo' }
+ let(:context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll,
+ event: 'action_active_users_project_repo').to_context]
+ end
end
end
@@ -355,11 +342,16 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::PUSH_ACTION }
end
- it_behaves_like 'Snowplow event' do
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
let(:category) { described_class.to_s }
- let(:action) { 'action_active_users_project_repo' }
+ let(:action) { :push }
let(:namespace) { project.namespace }
let(:feature_flag_name) { :route_hll_to_snowplow }
+ let(:label) { 'usage_activity_by_stage_monthly.create.action_monthly_active_users_project_repo' }
+ let(:context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll,
+ event: 'action_active_users_project_repo').to_context]
+ end
end
end
@@ -495,7 +487,7 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
stub_feature_flags(route_hll_to_snowplow_phase2: false)
end
- it 'doesnt emit snowwplow events', :snowplow do
+ it 'doesnt emit snowplow events', :snowplow do
subject
expect_no_snowplow_event
@@ -518,19 +510,22 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
end
context 'when it is a diff note' do
- it_behaves_like "it records the event in the event counter" do
- let(:note) { create(:diff_note_on_merge_request) }
- end
+ let(:note) { create(:diff_note_on_merge_request) }
- it_behaves_like 'Snowplow event' do
+ it_behaves_like "it records the event in the event counter"
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:note) { create(:diff_note_on_merge_request) }
- let(:category) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION.to_s }
- let(:label) { 'merge_requests_users' }
- let(:action) { 'comment' }
- let(:project) { note.project }
+ let(:category) { described_class.name }
+ let(:action) { 'commented' }
+ let(:label) { 'usage_activity_by_stage_monthly.create.merge_requests_users' }
let(:namespace) { project.namespace }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:project) { note.project }
let(:user) { author }
+ let(:context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: 'merge_requests_users').to_context]
+ end
end
end
diff --git a/spec/services/git/base_hooks_service_spec.rb b/spec/services/git/base_hooks_service_spec.rb
index a8d753ff124..5afd7b30ab0 100644
--- a/spec/services/git/base_hooks_service_spec.rb
+++ b/spec/services/git/base_hooks_service_spec.rb
@@ -4,10 +4,10 @@ require 'spec_helper'
RSpec.describe Git::BaseHooksService do
include RepoHelpers
- include GitHelpers
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+
let(:oldrev) { Gitlab::Git::BLANK_SHA }
let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
let(:ref) { 'refs/tags/v1.1.0' }
@@ -150,11 +150,16 @@ RSpec.describe Git::BaseHooksService do
end
shared_examples 'creates pipeline with params and expected variables' do
+ let(:pipeline_service) { double(execute: service_response) }
+ let(:service_response) { double(error?: false, payload: pipeline, message: "Error") }
+ let(:pipeline) { double(persisted?: true) }
+
it 'calls the create pipeline service' do
expect(Ci::CreatePipelineService)
.to receive(:new)
.with(project, user, pipeline_params)
- .and_return(double(execute!: true))
+ .and_return(pipeline_service)
+ expect(subject).not_to receive(:log_pipeline_errors)
subject.execute
end
@@ -239,4 +244,85 @@ RSpec.describe Git::BaseHooksService do
it_behaves_like 'creates pipeline with params and expected variables'
end
end
+
+ describe "Pipeline creation" do
+ let(:pipeline_params) do
+ {
+ after: newrev,
+ before: oldrev,
+ checkout_sha: checkout_sha,
+ push_options: push_options,
+ ref: ref,
+ variables_attributes: variables_attributes
+ }
+ end
+
+ let(:pipeline_service) { double(execute: service_response) }
+ let(:push_options) { {} }
+ let(:variables_attributes) { [] }
+
+ context "when the pipeline is persisted" do
+ let(:pipeline) { double(persisted?: true) }
+
+ context "and there are no errors" do
+ let(:service_response) { double(error?: false, payload: pipeline, message: "Error") }
+
+ it "returns success" do
+ expect(Ci::CreatePipelineService)
+ .to receive(:new)
+ .with(project, user, pipeline_params)
+ .and_return(pipeline_service)
+
+ expect(subject.execute[:status]).to eq(:success)
+ end
+ end
+
+ context "and there are errors" do
+ let(:service_response) { double(error?: true, payload: pipeline, message: "Error") }
+
+ it "does not log errors and returns success" do
+ # This behaviour is due to the save_on_errors: true setting that is the default in the execute method.
+ expect(Ci::CreatePipelineService)
+ .to receive(:new)
+ .with(project, user, pipeline_params)
+ .and_return(pipeline_service)
+ expect(subject).not_to receive(:log_pipeline_errors).with(service_response.message)
+
+ expect(subject.execute[:status]).to eq(:success)
+ end
+ end
+ end
+
+ context "when the pipeline wasn't persisted" do
+ let(:pipeline) { double(persisted?: false) }
+
+ context "and there are no errors" do
+ let(:service_response) { double(error?: false, payload: pipeline, message: nil) }
+
+ it "returns success" do
+ expect(Ci::CreatePipelineService)
+ .to receive(:new)
+ .with(project, user, pipeline_params)
+ .and_return(pipeline_service)
+ expect(subject).to receive(:log_pipeline_errors).with(service_response.message)
+
+ expect(subject.execute[:status]).to eq(:success)
+ end
+ end
+
+ context "and there are errors" do
+ let(:service_response) { double(error?: true, payload: pipeline, message: "Error") }
+
+ it "logs errors and returns success" do
+ expect(Ci::CreatePipelineService)
+ .to receive(:new)
+ .with(project, user, pipeline_params)
+ .and_return(pipeline_service)
+ expect(subject).to receive(:log_pipeline_errors).with(service_response.message)
+
+ expect(subject.execute[:status]).to eq(:success)
+ end
+ end
+ end
+ end
end
diff --git a/spec/services/git/tag_push_service_spec.rb b/spec/services/git/tag_push_service_spec.rb
index 87dbf79a245..597254d46fa 100644
--- a/spec/services/git/tag_push_service_spec.rb
+++ b/spec/services/git/tag_push_service_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe Git::TagPushService do
include RepoHelpers
- include GitHelpers
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
diff --git a/spec/services/google_cloud/generate_pipeline_service_spec.rb b/spec/services/google_cloud/generate_pipeline_service_spec.rb
index 75494f229b5..a78d8ff6661 100644
--- a/spec/services/google_cloud/generate_pipeline_service_spec.rb
+++ b/spec/services/google_cloud/generate_pipeline_service_spec.rb
@@ -67,7 +67,7 @@ RSpec.describe GoogleCloud::GeneratePipelineService do
let_it_be(:service_params) { { action: GoogleCloud::GeneratePipelineService::ACTION_DEPLOY_TO_CLOUD_RUN } }
let_it_be(:service) { described_class.new(project, maintainer, service_params) }
- before do
+ before_all do
project.add_maintainer(maintainer)
file_name = '.gitlab-ci.yml'
@@ -103,6 +103,15 @@ EOF
expect(pipeline[:include]).to be_present
expect(gitlab_ci_yml).to include('https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library/-/raw/main/gcp/cloud-run.gitlab-ci.yml')
end
+
+ it 'stringifies keys from the existing pipelines' do
+ response = service.execute
+
+ branch_name = response[:branch_name]
+ gitlab_ci_yml = project.repository.gitlab_ci_yml_for(branch_name)
+
+ expect(YAML.safe_load(gitlab_ci_yml).keys).to eq(%w[stages build-java test-java include])
+ end
end
describe 'when there is an existing pipeline with `deploy` stage' do
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index 0a8164c9ca3..0425ba3e631 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -271,33 +271,4 @@ RSpec.describe Groups::CreateService, '#execute' do
end
end
end
-
- describe 'logged_out_marketing_header experiment', :experiment do
- let(:service) { described_class.new(user, group_params) }
-
- subject { service.execute }
-
- before do
- stub_experiments(logged_out_marketing_header: :candidate)
- end
-
- it 'tracks signed_up event' do
- expect(experiment(:logged_out_marketing_header)).to track(
- :namespace_created,
- namespace: an_instance_of(Group)
- ).on_next_instance.with_context(actor: user)
-
- subject
- end
-
- context 'when group has not been persisted' do
- let(:service) { described_class.new(user, group_params.merge(name: '<script>alert("Attack!")</script>')) }
-
- it 'does not track signed_up event' do
- expect(experiment(:logged_out_marketing_header)).not_to track(:namespace_created)
-
- subject
- end
- end
- end
end
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index 36e868fa5f1..f2dbb69f855 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -36,37 +36,17 @@ RSpec.describe Groups::DestroyService do
end
context 'bot tokens', :sidekiq_inline do
- context 'when user_destroy_with_limited_execution_time_worker is enabled' do
- it 'initiates group bot removal', :aggregate_failures do
- bot = create(:user, :project_bot)
- group.add_developer(bot)
- create(:personal_access_token, user: bot)
-
- destroy_group(group, user, async)
-
- expect(
- Users::GhostUserMigration.where(user: bot,
- initiator_user: user)
- ).to be_exists
- end
- end
-
- context 'when user_destroy_with_limited_execution_time_worker is disabled' do
- before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
- end
-
- it 'removes group bot', :aggregate_failures do
- bot = create(:user, :project_bot)
- group.add_developer(bot)
- token = create(:personal_access_token, user: bot)
+ it 'initiates group bot removal', :aggregate_failures do
+ bot = create(:user, :project_bot)
+ group.add_developer(bot)
+ create(:personal_access_token, user: bot)
- destroy_group(group, user, async)
+ destroy_group(group, user, async)
- expect(PersonalAccessToken.find_by(id: token.id)).to be_nil
- expect(User.find_by(id: bot.id)).to be_nil
- expect(User.find_by(id: user.id)).not_to be_nil
- end
+ expect(
+ Users::GhostUserMigration.where(user: bot,
+ initiator_user: user)
+ ).to be_exists
end
end
@@ -146,7 +126,7 @@ RSpec.describe Groups::DestroyService do
end
expect { destroy_group(group, user, false) }
- .to raise_error(Groups::DestroyService::DestroyError, "Project #{project.id} can't be deleted" )
+ .to raise_error(Groups::DestroyService::DestroyError, "Project #{project.id} can't be deleted")
end
end
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index b543661e9a0..3cf2c875341 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -200,7 +200,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
let_it_be(:project2) { create(:project, :private, namespace: group) }
it_behaves_like 'project namespace path is in sync with project path' do
- let(:group_full_path) { "#{group.path}" }
+ let(:group_full_path) { group.path.to_s }
let(:projects_with_project_namespace) { [project1, project2] }
end
end
@@ -274,7 +274,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
end
it_behaves_like 'project namespace path is in sync with project path' do
- let(:group_full_path) { "#{new_parent_group.full_path}" }
+ let(:group_full_path) { new_parent_group.full_path.to_s }
let(:projects_with_project_namespace) { [project] }
end
end
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index 5c87b9ac8bb..c758d3d5477 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -101,6 +101,15 @@ RSpec.describe Groups::UpdateService do
expect(public_group.reload.name).to eq('new-name')
end
end
+
+ context 'when the path does not change' do
+ let(:params) { { name: 'new-name', path: public_group.path } }
+
+ it 'allows the update' do
+ expect(subject).to be true
+ expect(public_group.reload.name).to eq('new-name')
+ end
+ end
end
context 'within subgroup' do
diff --git a/spec/services/groups/update_shared_runners_service_spec.rb b/spec/services/groups/update_shared_runners_service_spec.rb
index 6e938984052..98eccedeace 100644
--- a/spec/services/groups/update_shared_runners_service_spec.rb
+++ b/spec/services/groups/update_shared_runners_service_spec.rb
@@ -127,7 +127,7 @@ RSpec.describe Groups::UpdateSharedRunnersService do
end
context 'when parent does not allow' do
- let_it_be(:parent) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false ) }
+ let_it_be(:parent) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false) }
let_it_be(:group) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent: parent) }
it 'results error' do
diff --git a/spec/services/import/fogbugz_service_spec.rb b/spec/services/import/fogbugz_service_spec.rb
index 7b86c5c45b0..027d0240a7a 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/gitlab_projects/file_acquisition_strategies/file_upload_spec.rb b/spec/services/import/gitlab_projects/file_acquisition_strategies/file_upload_spec.rb
index 28af6219812..3c788138157 100644
--- a/spec/services/import/gitlab_projects/file_acquisition_strategies/file_upload_spec.rb
+++ b/spec/services/import/gitlab_projects/file_acquisition_strategies/file_upload_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe ::Import::GitlabProjects::FileAcquisitionStrategies::FileUpload, :aggregate_failures do
- let(:file) { UploadedFile.new( File.join('spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') ) }
+ let(:file) { UploadedFile.new(File.join('spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz')) }
describe 'validation' do
it 'validates presence of file' do
diff --git a/spec/services/incident_management/timeline_event_tags/create_service_spec.rb b/spec/services/incident_management/timeline_event_tags/create_service_spec.rb
new file mode 100644
index 00000000000..c1b993ce3d9
--- /dev/null
+++ b/spec/services/incident_management/timeline_event_tags/create_service_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IncidentManagement::TimelineEventTags::CreateService do
+ let_it_be(:user_with_permissions) { create(:user) }
+ let_it_be(:user_without_permissions) { create(:user) }
+ let_it_be_with_reload(:project) { create(:project) }
+
+ let(:current_user) { user_with_permissions }
+ let(:args) { { 'name': 'Test tag 1', 'project_path': project.full_path } }
+
+ let(:service) { described_class.new(project, current_user, args) }
+
+ before do
+ project.add_maintainer(user_with_permissions)
+ project.add_developer(user_without_permissions)
+ end
+
+ describe '#execute' do
+ shared_examples 'error response' do |message|
+ it 'has an informative message' do
+ expect(execute).to be_error
+ expect(execute.message).to eq(message)
+ end
+ end
+
+ shared_examples 'success response' do
+ it 'has timeline event tag' do
+ expect(execute).to be_success
+
+ result = execute.payload[:timeline_event_tag]
+ expect(result).to be_a(::IncidentManagement::TimelineEventTag)
+ expect(result.name).to eq(args[:name])
+ expect(result.project).to eq(project)
+ end
+ end
+
+ subject(:execute) { service.execute }
+
+ context 'when current user is nil' do
+ let(:current_user) { nil }
+
+ it_behaves_like 'error response',
+ 'You have insufficient permissions to manage timeline event tags for this project'
+ end
+
+ context 'when user does not have permissions to create tags' do
+ let(:current_user) { user_without_permissions }
+
+ it_behaves_like 'error response',
+ 'You have insufficient permissions to manage timeline event tags for this project'
+ end
+
+ context 'when error occurs during creation' do
+ let(:args) { {} }
+
+ it_behaves_like 'error response', "Name can't be blank and Name is invalid"
+ end
+
+ context 'when user has permissions' do
+ it_behaves_like 'success response'
+
+ it 'creates database record' do
+ expect { execute }.to change {
+ ::IncidentManagement::TimelineEventTag.where(project_id: project.id).count
+ }.by(1)
+ end
+ end
+ end
+end
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 a7f448c825f..b10862a78b5 100644
--- a/spec/services/incident_management/timeline_events/create_service_spec.rb
+++ b/spec/services/incident_management/timeline_events/create_service_spec.rb
@@ -8,6 +8,9 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
let_it_be(:project) { create(:project) }
let_it_be_with_refind(:incident) { create(:incident, project: project) }
let_it_be(:comment) { create(:note, project: project, noteable: incident) }
+ let_it_be(:timeline_event_tag) do
+ create(:incident_management_timeline_event_tag, name: 'Test tag 1', project: project)
+ end
let(:args) do
{
@@ -134,6 +137,67 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
end
end
+ context 'when timeline event tag names are passed' do
+ let(:args) do
+ {
+ note: 'note',
+ occurred_at: Time.current,
+ action: 'new comment',
+ promoted_from_note: comment,
+ timeline_event_tag_names: ['Test tag 1']
+ }
+ end
+
+ it_behaves_like 'success response'
+
+ it 'matches the tag name' do
+ result = execute.payload[:timeline_event]
+ expect(result.timeline_event_tags.first).to eq(timeline_event_tag)
+ end
+
+ context 'when predefined tags are passed' do
+ let(:args) do
+ {
+ note: 'note',
+ occurred_at: Time.current,
+ action: 'new comment',
+ promoted_from_note: comment,
+ timeline_event_tag_names: ['start time', 'end time']
+ }
+ end
+
+ it_behaves_like 'success response'
+
+ it 'matches the two tags on the event and creates on project' do
+ result = execute.payload[:timeline_event]
+
+ expect(result.timeline_event_tags.count).to eq(2)
+ expect(result.timeline_event_tags.by_names(['Start time', 'End time']).pluck_names)
+ .to match_array(['Start time', 'End time'])
+ expect(project.incident_management_timeline_event_tags.pluck_names)
+ .to include('Start time', 'End time')
+ end
+ end
+
+ context 'when invalid tag names are passed' do
+ let(:args) do
+ {
+ note: 'note',
+ occurred_at: Time.current,
+ action: 'new comment',
+ promoted_from_note: comment,
+ timeline_event_tag_names: ['some other time']
+ }
+ end
+
+ it_behaves_like 'error response', "Following tags don't exist: [\"some other time\"]"
+
+ it 'does not create timeline event' do
+ expect { execute }.not_to change(IncidentManagement::TimelineEvent, :count)
+ end
+ end
+ end
+
context 'with editable param' do
let(:args) do
{
@@ -161,6 +225,38 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
it 'successfully creates a database record', :aggregate_failures do
expect { execute }.to change { ::IncidentManagement::TimelineEvent.count }.by(1)
end
+
+ context 'when note is more than 280 characters long' do
+ let(:args) do
+ {
+ note: 'a' * 281,
+ occurred_at: Time.current,
+ action: 'new comment',
+ promoted_from_note: comment,
+ auto_created: auto_created
+ }
+ end
+
+ let(:auto_created) { false }
+
+ context 'when was not promoted from note' do
+ let(:comment) { nil }
+
+ context 'when auto_created is true' do
+ let(:auto_created) { true }
+
+ it_behaves_like 'success response'
+ end
+
+ context 'when auto_created is false' do
+ it_behaves_like 'error response', 'Timeline text is too long (maximum is 280 characters)'
+ end
+ end
+
+ context 'when promoted from note' do
+ it_behaves_like 'success response'
+ end
+ end
end
describe 'automatically created timeline events' do
@@ -229,6 +325,17 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
it_behaves_like 'successfully created timeline event'
end
+ describe '.change_severity' do
+ subject(:execute) { described_class.change_severity(incident, current_user) }
+
+ let_it_be(:severity) { create(:issuable_severity, severity: :critical, issue: incident) }
+
+ let(:expected_note) { "@#{current_user.username} changed the incident severity to **Critical - S1**" }
+ let(:expected_action) { 'severity' }
+
+ 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)
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 5d8518cf2ef..2373a73e108 100644
--- a/spec/services/incident_management/timeline_events/update_service_spec.rb
+++ b/spec/services/incident_management/timeline_events/update_service_spec.rb
@@ -87,6 +87,12 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do
it_behaves_like 'error response', "Timeline text can't be blank"
end
+ context 'when note is more than 280 characters long' do
+ let(:params) { { note: 'n' * 281, occurred_at: occurred_at } }
+
+ it_behaves_like 'error response', 'Timeline text is too long (maximum is 280 characters)'
+ end
+
context 'when occurred_at is nil' do
let(:params) { { note: 'Updated note' } }
diff --git a/spec/services/issuable/discussions_list_service_spec.rb b/spec/services/issuable/discussions_list_service_spec.rb
new file mode 100644
index 00000000000..2ce47f42a72
--- /dev/null
+++ b/spec/services/issuable/discussions_list_service_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Issuable::DiscussionsListService do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:project) { create(:project, :repository, :private, group: group) }
+ let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:label) { create(:label, project: project) }
+
+ let(:finder_params_for_issuable) { {} }
+
+ subject(:discussions_service) { described_class.new(current_user, issuable, finder_params_for_issuable) }
+
+ describe 'fetching notes for issue' do
+ let_it_be(:issuable) { create(:issue, project: project) }
+
+ it_behaves_like 'listing issuable discussions', :guest, 1, 7
+ end
+
+ describe 'fetching notes for merge requests' do
+ let_it_be(:issuable) { create(:merge_request, source_project: project, target_project: project) }
+
+ it_behaves_like 'listing issuable discussions', :reporter, 0, 6
+ end
+end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index e88fe1b42f0..ef92b6984d5 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -397,9 +397,26 @@ RSpec.describe Issues::CloseService do
end
context 'when issue is not confidential' do
+ let(:expected_payload) do
+ include(
+ event_type: 'issue',
+ object_kind: 'issue',
+ changes: {
+ closed_at: { current: kind_of(Time), previous: nil },
+ state_id: { current: 2, previous: 1 },
+ updated_at: { current: kind_of(Time), previous: kind_of(Time) }
+ },
+ object_attributes: include(
+ closed_at: kind_of(Time),
+ state: 'closed',
+ action: 'close'
+ )
+ )
+ end
+
it 'executes issue hooks' do
- expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
- expect(project).to receive(:execute_integrations).with(an_instance_of(Hash), :issue_hooks)
+ expect(project).to receive(:execute_hooks).with(expected_payload, :issue_hooks)
+ expect(project).to receive(:execute_integrations).with(expected_payload, :issue_hooks)
described_class.new(project: project, current_user: user).close_issue(issue)
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 5fe4c693451..5ddf91e167e 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -391,22 +391,61 @@ RSpec.describe Issues::CreateService do
end
end
- it 'executes issue hooks when issue is not confidential' do
- opts = { title: 'Title', description: 'Description', confidential: false }
+ describe 'executing hooks' do
+ let(:opts) { { title: 'Title', description: 'Description' } }
+ let(:expected_payload) do
+ include(
+ event_type: 'issue',
+ object_kind: 'issue',
+ changes: {
+ author_id: { current: user.id, previous: nil },
+ created_at: { current: kind_of(Time), previous: nil },
+ description: { current: opts[:description], previous: nil },
+ id: { current: kind_of(Integer), previous: nil },
+ iid: { current: kind_of(Integer), previous: nil },
+ project_id: { current: project.id, previous: nil },
+ title: { current: opts[:title], previous: nil },
+ updated_at: { current: kind_of(Time), previous: nil }
+ },
+ object_attributes: include(
+ opts.merge(
+ author_id: user.id,
+ project_id: project.id
+ )
+ )
+ )
+ end
+
+ it 'executes issue hooks' do
+ expect(project).to receive(:execute_hooks).with(expected_payload, :issue_hooks)
+ expect(project).to receive(:execute_integrations).with(expected_payload, :issue_hooks)
- expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
- expect(project).to receive(:execute_integrations).with(an_instance_of(Hash), :issue_hooks)
+ described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ end
- described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
- end
+ context 'when issue is confidential' do
+ let(:expected_payload) do
+ include(
+ event_type: 'confidential_issue',
+ object_kind: 'issue',
+ changes: include(
+ confidential: { current: true, previous: false }
+ ),
+ object_attributes: include(confidential: true)
+ )
+ end
- it 'executes confidential issue hooks when issue is confidential' do
- opts = { title: 'Title', description: 'Description', confidential: true }
+ before do
+ opts[:confidential] = true
+ end
- expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks)
- expect(project).to receive(:execute_integrations).with(an_instance_of(Hash), :confidential_issue_hooks)
+ it 'executes confidential issue hooks' do
+ expect(project).to receive(:execute_hooks).with(expected_payload, :confidential_issue_hooks)
+ expect(project).to receive(:execute_integrations).with(expected_payload, :confidential_issue_hooks)
- described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ end
+ end
end
context 'after_save callback to store_mentions' do
diff --git a/spec/services/issues/export_csv_service_spec.rb b/spec/services/issues/export_csv_service_spec.rb
index d04480bec18..66d017464bf 100644
--- a/spec/services/issues/export_csv_service_spec.rb
+++ b/spec/services/issues/export_csv_service_spec.rb
@@ -185,7 +185,7 @@ RSpec.describe Issues::ExportCsvService do
labeled_rows = csv.select { |entry| labeled_issues.map(&:iid).include?(entry['Issue ID'].to_i) }
expect(labeled_rows.count).to eq(2)
- expect(labeled_rows.map { |entry| entry['Labels'] }).to all( eq("Feature,Idea") )
+ expect(labeled_rows.map { |entry| entry['Labels'] }).to all(eq("Feature,Idea"))
end
end
end
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index 23180f75eb3..655c5085fdc 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -228,18 +228,48 @@ RSpec.describe Issues::MoveService do
end
context 'project issue hooks' do
- let!(:hook) { create(:project_hook, project: old_project, issues_events: true) }
+ let_it_be(:old_project_hook) { create(:project_hook, project: old_project, issues_events: true) }
+ let_it_be(:new_project_hook) { create(:project_hook, project: new_project, issues_events: true) }
+
+ let(:expected_new_project_hook_payload) do
+ hash_including(
+ event_type: 'issue',
+ object_kind: 'issue',
+ object_attributes: include(
+ project_id: new_project.id,
+ state: 'opened',
+ action: 'open'
+ )
+ )
+ end
+
+ let(:expected_old_project_hook_payload) do
+ hash_including(
+ event_type: 'issue',
+ object_kind: 'issue',
+ changes: {
+ state_id: { current: 2, previous: 1 },
+ closed_at: { current: kind_of(Time), previous: nil },
+ updated_at: { current: kind_of(Time), previous: kind_of(Time) }
+ },
+ object_attributes: include(
+ id: old_issue.id,
+ closed_at: kind_of(Time),
+ state: 'closed',
+ action: 'close'
+ )
+ )
+ end
- it 'executes project issue hooks' do
- allow_next_instance_of(WebHookService) do |instance|
- allow(instance).to receive(:execute)
+ it 'executes project issue hooks for both projects' do
+ expect_next_instance_of(WebHookService, new_project_hook, expected_new_project_hook_payload, 'issue_hooks') do |service|
+ expect(service).to receive(:async_execute).once
+ end
+ expect_next_instance_of(WebHookService, old_project_hook, expected_old_project_hook_payload, 'issue_hooks') do |service|
+ expect(service).to receive(:async_execute).once
end
- # Ideally, we'd test that `WebHookWorker.jobs.size` increased by 1,
- # but since the entire spec run takes place in a transaction, we never
- # actually get to the `after_commit` hook that queues these jobs.
- expect { move_service.execute(old_issue, new_project) }
- .not_to raise_error # Sidekiq::Worker::EnqueueFromTransactionError
+ move_service.execute(old_issue, new_project)
end
end
diff --git a/spec/services/issues/relative_position_rebalancing_service_spec.rb b/spec/services/issues/relative_position_rebalancing_service_spec.rb
index 37a94e1d6a2..27c0394ac8b 100644
--- a/spec/services/issues/relative_position_rebalancing_service_spec.rb
+++ b/spec/services/issues/relative_position_rebalancing_service_spec.rb
@@ -34,10 +34,6 @@ RSpec.describe Issues::RelativePositionRebalancingService, :clean_gitlab_redis_s
end
end
- before do
- stub_feature_flags(issue_rebalancing_with_retry: false)
- end
-
def issues_in_position_order
project.reload.issues.order_by_relative_position.to_a
end
@@ -97,8 +93,12 @@ RSpec.describe Issues::RelativePositionRebalancingService, :clean_gitlab_redis_s
it 'resumes a started rebalance even if there are already too many rebalances running' do
Gitlab::Redis::SharedState.with do |redis|
- redis.sadd("gitlab:issues-position-rebalances:running_rebalances", "#{::Gitlab::Issues::Rebalancing::State::PROJECT}/#{project.id}")
- redis.sadd("gitlab:issues-position-rebalances:running_rebalances", "1/100")
+ redis.sadd("gitlab:issues-position-rebalances:running_rebalances",
+ [
+ "#{::Gitlab::Issues::Rebalancing::State::PROJECT}/#{project.id}",
+ "1/100"
+ ]
+ )
end
caching = service.send(:caching)
diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb
index 477b44f4c2c..6013826f9b1 100644
--- a/spec/services/issues/reopen_service_spec.rb
+++ b/spec/services/issues/reopen_service_spec.rb
@@ -85,9 +85,25 @@ RSpec.describe Issues::ReopenService do
end
context 'when issue is not confidential' do
+ let(:expected_payload) do
+ include(
+ event_type: 'issue',
+ object_kind: 'issue',
+ changes: {
+ closed_at: { current: nil, previous: kind_of(Time) },
+ state_id: { current: 1, previous: 2 },
+ updated_at: { current: kind_of(Time), previous: kind_of(Time) }
+ },
+ object_attributes: include(
+ state: 'opened',
+ action: 'reopen'
+ )
+ )
+ end
+
it 'executes issue hooks' do
- expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
- expect(project).to receive(:execute_integrations).with(an_instance_of(Hash), :issue_hooks)
+ expect(project).to receive(:execute_hooks).with(expected_payload, :issue_hooks)
+ expect(project).to receive(:execute_integrations).with(expected_payload, :issue_hooks)
execute
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 20b1a1f58bb..f1ee62fd589 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -104,10 +104,33 @@ RSpec.describe Issues::UpdateService, :mailer do
expect(issue.issue_customer_relations_contacts.last.contact).to eq contact
end
- it 'updates issue milestone when passing `milestone` param' do
- update_issue(milestone: milestone)
+ context 'when updating milestone' do
+ before do
+ update_issue({ milestone: nil })
+ end
- expect(issue.milestone).to eq milestone
+ it 'updates issue milestone when passing `milestone` param' do
+ expect { update_issue({ milestone: milestone }) }
+ .to change(issue, :milestone).to(milestone).from(nil)
+ end
+
+ it "triggers 'issuableMilestoneUpdated'" do
+ expect(GraphqlTriggers).to receive(:issuable_milestone_updated).with(issue).and_call_original
+
+ update_issue({ milestone: milestone })
+ end
+
+ context 'when milestone remains unchanged' do
+ before do
+ update_issue({ title: 'abc', milestone: milestone })
+ end
+
+ it "does not trigger 'issuableMilestoneUpdated'" do
+ expect(GraphqlTriggers).not_to receive(:issuable_milestone_updated)
+
+ update_issue({ milestone: milestone })
+ end
+ end
end
context 'when sentry identifier is given' do
@@ -520,7 +543,7 @@ RSpec.describe Issues::UpdateService, :mailer do
end
end
- context 'when decription is not changed' do
+ context 'when description is not changed' do
it 'does not trigger GraphQL description updated subscription' do
expect(GraphqlTriggers).not_to receive(:issuable_description_updated)
@@ -1379,7 +1402,7 @@ RSpec.describe Issues::UpdateService, :mailer do
end
end
- include_examples 'issuable update service' do
+ it_behaves_like 'issuable update service' do
let(:open_issuable) { issue }
let(:closed_issuable) { create(:closed_issue, project: project) }
end
diff --git a/spec/services/labels/promote_service_spec.rb b/spec/services/labels/promote_service_spec.rb
index a10aaa14030..3af6cf4c8f4 100644
--- a/spec/services/labels/promote_service_spec.rb
+++ b/spec/services/labels/promote_service_spec.rb
@@ -171,7 +171,7 @@ RSpec.describe Labels::PromoteService do
end
context 'when there is an existing identical group label' do
- let!(:existing_group_label) { create(:group_label, group: group_1, title: project_label_1_1.title ) }
+ let!(:existing_group_label) { create(:group_label, group: group_1, title: project_label_1_1.title) }
it 'uses the existing group label' do
expect { service.execute(project_label_1_1) }
diff --git a/spec/services/loose_foreign_keys/process_deleted_records_service_spec.rb b/spec/services/loose_foreign_keys/process_deleted_records_service_spec.rb
new file mode 100644
index 00000000000..1824f822ba8
--- /dev/null
+++ b/spec/services/loose_foreign_keys/process_deleted_records_service_spec.rb
@@ -0,0 +1,198 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe LooseForeignKeys::ProcessDeletedRecordsService do
+ include MigrationsHelpers
+
+ def create_table_structure
+ migration = ActiveRecord::Migration.new.extend(Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers)
+
+ migration.create_table :_test_loose_fk_parent_table_1
+ migration.create_table :_test_loose_fk_parent_table_2
+
+ migration.create_table :_test_loose_fk_child_table_1_1 do |t|
+ t.bigint :parent_id
+ end
+
+ migration.create_table :_test_loose_fk_child_table_1_2 do |t|
+ t.bigint :parent_id_with_different_column
+ end
+
+ migration.create_table :_test_loose_fk_child_table_2_1 do |t|
+ t.bigint :parent_id
+ end
+
+ migration.track_record_deletions(:_test_loose_fk_parent_table_1)
+ migration.track_record_deletions(:_test_loose_fk_parent_table_2)
+ end
+
+ let(:all_loose_foreign_key_definitions) do
+ {
+ '_test_loose_fk_parent_table_1' => [
+ ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
+ '_test_loose_fk_child_table_1_1',
+ '_test_loose_fk_parent_table_1',
+ {
+ column: 'parent_id',
+ on_delete: :async_delete,
+ gitlab_schema: :gitlab_main
+ }
+ ),
+ ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
+ '_test_loose_fk_child_table_1_2',
+ '_test_loose_fk_parent_table_1',
+ {
+ column: 'parent_id_with_different_column',
+ on_delete: :async_nullify,
+ gitlab_schema: :gitlab_main
+ }
+ )
+ ],
+ '_test_loose_fk_parent_table_2' => [
+ ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
+ '_test_loose_fk_child_table_2_1',
+ '_test_loose_fk_parent_table_2',
+ {
+ column: 'parent_id',
+ on_delete: :async_delete,
+ gitlab_schema: :gitlab_main
+ }
+ )
+ ]
+ }
+ end
+
+ let(:connection) { ::ApplicationRecord.connection }
+
+ let(:loose_fk_parent_table_1) { table(:_test_loose_fk_parent_table_1) }
+ let(:loose_fk_parent_table_2) { table(:_test_loose_fk_parent_table_2) }
+ let(:loose_fk_child_table_1_1) { table(:_test_loose_fk_child_table_1_1) }
+ let(:loose_fk_child_table_1_2) { table(:_test_loose_fk_child_table_1_2) }
+ let(:loose_fk_child_table_2_1) { table(:_test_loose_fk_child_table_2_1) }
+
+ before(:all) do
+ create_table_structure
+ end
+
+ after(:all) do
+ migration = ActiveRecord::Migration.new
+
+ migration.drop_table :_test_loose_fk_parent_table_1
+ migration.drop_table :_test_loose_fk_parent_table_2
+ migration.drop_table :_test_loose_fk_child_table_1_1
+ migration.drop_table :_test_loose_fk_child_table_1_2
+ migration.drop_table :_test_loose_fk_child_table_2_1
+ end
+
+ before do
+ allow(Gitlab::Database::LooseForeignKeys).to receive(:definitions_by_table)
+ .and_return(all_loose_foreign_key_definitions)
+
+ parent_record_1 = loose_fk_parent_table_1.create!
+ loose_fk_child_table_1_1.create!(parent_id: parent_record_1.id)
+ loose_fk_child_table_1_2.create!(parent_id_with_different_column: parent_record_1.id)
+
+ parent_record_2 = loose_fk_parent_table_1.create!
+ 2.times { loose_fk_child_table_1_1.create!(parent_id: parent_record_2.id) }
+ 3.times { loose_fk_child_table_1_2.create!(parent_id_with_different_column: parent_record_2.id) }
+
+ parent_record_3 = loose_fk_parent_table_2.create!
+ 5.times { loose_fk_child_table_2_1.create!(parent_id: parent_record_3.id) }
+
+ loose_fk_parent_table_1.delete_all
+ loose_fk_parent_table_2.delete_all
+ end
+
+ describe '#execute' do
+ def execute
+ ::Gitlab::Database::SharedModel.using_connection(connection) do
+ described_class.new(connection: connection).execute
+ end
+ end
+
+ it 'cleans up all rows' do
+ execute
+
+ expect(loose_fk_child_table_1_1.count).to eq(0)
+ expect(loose_fk_child_table_1_2.where(parent_id_with_different_column: nil).count).to eq(4)
+ expect(loose_fk_child_table_2_1.count).to eq(0)
+ end
+
+ it 'returns stats for records cleaned up' do
+ stats = execute
+
+ expect(stats[:delete_count]).to eq(8)
+ expect(stats[:update_count]).to eq(4)
+ end
+
+ it 'records the Apdex as success: true' do
+ expect(::Gitlab::Metrics::LooseForeignKeysSlis).to receive(:record_apdex)
+ .with(success: true, db_config_name: 'main')
+
+ execute
+ end
+
+ it 'records the error rate as error: false' do
+ expect(::Gitlab::Metrics::LooseForeignKeysSlis).to receive(:record_error_rate)
+ .with(error: false, db_config_name: 'main')
+
+ execute
+ end
+
+ context 'when the amount of records to clean up exceeds BATCH_SIZE' do
+ before do
+ stub_const('LooseForeignKeys::CleanupWorker::BATCH_SIZE', 2)
+ end
+
+ it 'cleans up everything over multiple batches' do
+ expect(LooseForeignKeys::BatchCleanerService).to receive(:new).exactly(:twice).and_call_original
+
+ execute
+
+ expect(loose_fk_child_table_1_1.count).to eq(0)
+ expect(loose_fk_child_table_1_2.where(parent_id_with_different_column: nil).count).to eq(4)
+ expect(loose_fk_child_table_2_1.count).to eq(0)
+ end
+ end
+
+ context 'when the amount of records to clean up exceeds the total MAX_DELETES' do
+ def count_deletable_rows
+ loose_fk_child_table_1_1.count + loose_fk_child_table_2_1.count
+ end
+
+ before do
+ stub_const('LooseForeignKeys::ModificationTracker::MAX_DELETES', 2)
+ stub_const('LooseForeignKeys::CleanerService::DELETE_LIMIT', 1)
+ end
+
+ it 'cleans up MAX_DELETES and leaves the rest for the next run' do
+ expect { execute }.to change { count_deletable_rows }.by(-2)
+ expect(count_deletable_rows).to be > 0
+ end
+
+ it 'records the Apdex as success: false' do
+ expect(::Gitlab::Metrics::LooseForeignKeysSlis).to receive(:record_apdex)
+ .with(success: false, db_config_name: 'main')
+
+ execute
+ end
+ end
+
+ context 'when cleanup raises an error' do
+ before do
+ expect_next_instance_of(::LooseForeignKeys::BatchCleanerService) do |service|
+ allow(service).to receive(:execute).and_raise("Something broke")
+ end
+ end
+
+ it 'records the error rate as error: true and does not increment apdex' do
+ expect(::Gitlab::Metrics::LooseForeignKeysSlis).to receive(:record_error_rate)
+ .with(error: true, db_config_name: 'main')
+ expect(::Gitlab::Metrics::LooseForeignKeysSlis).not_to receive(:record_apdex)
+
+ expect { execute }.to raise_error("Something broke")
+ end
+ end
+ end
+end
diff --git a/spec/services/markup/rendering_service_spec.rb b/spec/services/markup/rendering_service_spec.rb
new file mode 100644
index 00000000000..a5711a8cbc4
--- /dev/null
+++ b/spec/services/markup/rendering_service_spec.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Markup::RenderingService do
+ describe '#execute' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) do
+ user = create(:user, username: 'gfm')
+ project.add_maintainer(user)
+ user
+ end
+
+ let_it_be(:context) { { project: project } }
+ let_it_be(:postprocess_context) { { current_user: user } }
+
+ let(:file_name) { nil }
+ let(:text) { 'Noël' }
+
+ subject do
+ described_class
+ .new(text, file_name: file_name, context: context, postprocess_context: postprocess_context)
+ .execute
+ end
+
+ context 'when text is missing' do
+ let(:text) { nil }
+
+ it 'returns an empty string' do
+ is_expected.to eq('')
+ end
+ end
+
+ context 'when file_name is missing' do
+ it 'returns html (rendered by Banzai)' do
+ expected_html = '<p data-sourcepos="1:1-1:5" dir="auto">Noël</p>'
+
+ expect(Banzai).to receive(:render).with(text, context) { expected_html }
+
+ is_expected.to eq(expected_html)
+ end
+ end
+
+ context 'when postprocess_context is missing' do
+ let(:file_name) { 'foo.txt' }
+ let(:postprocess_context) { nil }
+
+ it 'returns html (rendered by Banzai)' do
+ expected_html = '<pre class="plain-readme">Noël</pre>'
+
+ expect(Banzai).not_to receive(:post_process) { expected_html }
+
+ is_expected.to eq(expected_html)
+ end
+ end
+
+ context 'when rendered context is present' do
+ let(:rendered) { 'rendered text' }
+ let(:file_name) { 'foo.md' }
+
+ it 'returns an empty string' do
+ context[:rendered] = rendered
+
+ is_expected.to eq(rendered)
+ end
+ end
+
+ context 'when file is a markdown file' do
+ let(:file_name) { 'foo.md' }
+
+ it 'returns html (rendered by Banzai)' do
+ expected_html = '<p data-sourcepos="1:1-1:5" dir="auto">Noël</p>'
+
+ expect(Banzai).to receive(:render).with(text, context) { expected_html }
+
+ is_expected.to eq(expected_html)
+ end
+
+ context 'when renderer returns an error' do
+ before do
+ allow(Banzai).to receive(:render).and_raise(StandardError, "An error")
+ end
+
+ it 'returns html (rendered by ActionView:TextHelper)' do
+ is_expected.to eq('<p>Noël</p>')
+ end
+
+ it 'logs the error' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ instance_of(StandardError),
+ project_id: context[:project].id, file_name: 'foo.md'
+ )
+
+ subject
+ end
+ end
+ end
+
+ context 'when file is asciidoc file' do
+ let(:file_name) { 'foo.adoc' }
+
+ it 'returns html (rendered by Gitlab::Asciidoc)' do
+ expected_html = "<div>\n<p>Noël</p>\n</div>"
+
+ expect(Gitlab::Asciidoc).to receive(:render).with(text, context) { expected_html }
+
+ is_expected.to eq(expected_html)
+ end
+ end
+
+ context 'when file is a regular text file' do
+ let(:file_name) { 'foo.txt' }
+
+ it 'returns html (rendered by ActionView::TagHelper)' do
+ is_expected.to eq('<pre class="plain-readme">Noël</pre>')
+ end
+ end
+
+ context 'when file has an unknown type' do
+ let(:file_name) { 'foo.tex' }
+
+ it 'returns html (rendered by Gitlab::OtherMarkup)' do
+ expected_html = 'Noël'
+
+ expect(Gitlab::OtherMarkup).to receive(:render).with(file_name, text, context) { expected_html }
+
+ is_expected.to eq(expected_html)
+ end
+ end
+
+ context 'when rendering takes too long' do
+ let(:file_name) { 'foo.bar' }
+
+ before do
+ stub_const("Markup::RenderingService::RENDER_TIMEOUT", 0.1)
+ allow(Gitlab::OtherMarkup).to receive(:render) do
+ sleep(0.2)
+ text
+ end
+ end
+
+ it 'times out' do
+ expect(Gitlab::RenderTimeout).to receive(:timeout).and_call_original
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ instance_of(Timeout::Error),
+ project_id: context[:project].id, file_name: file_name
+ )
+
+ is_expected.to eq("<p>#{text}</p>")
+ end
+
+ context 'when markup_rendering_timeout is disabled' do
+ it 'waits until the execution completes' do
+ stub_feature_flags(markup_rendering_timeout: false)
+
+ expect(Gitlab::RenderTimeout).not_to receive(:timeout)
+
+ is_expected.to eq(text)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index 8559c02be57..d0f009f1321 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -77,7 +77,7 @@ RSpec.describe Members::DestroyService do
end
end
- shared_examples 'a service destroying an access requester' do
+ shared_examples 'a service destroying an access request of another user' do
it_behaves_like 'a service destroying a member'
it 'calls Member#after_decline_request' do
@@ -85,12 +85,16 @@ RSpec.describe Members::DestroyService do
described_class.new(current_user).execute(member, **opts)
end
+ end
+
+ shared_examples 'a service destroying an access request of self' do
+ it_behaves_like 'a service destroying a member'
context 'when current user is the member' do
it 'does not call Member#after_decline_request' do
expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
- described_class.new(member_user).execute(member, **opts)
+ described_class.new(current_user).execute(member, **opts)
end
end
end
@@ -277,11 +281,24 @@ RSpec.describe Members::DestroyService do
group.add_owner(current_user)
end
- it_behaves_like 'a service destroying an access requester' do
+ it_behaves_like 'a service destroying an access request of another user' do
+ let(:member) { group_project.requesters.find_by(user_id: member_user.id) }
+ end
+
+ it_behaves_like 'a service destroying an access request of another user' do
+ let(:member) { group.requesters.find_by(user_id: member_user.id) }
+ end
+ end
+
+ context 'on withdrawing their own access request' do
+ let(:opts) { { skip_subresources: true } }
+ let(:current_user) { member_user }
+
+ it_behaves_like 'a service destroying an access request of self' do
let(:member) { group_project.requesters.find_by(user_id: member_user.id) }
end
- it_behaves_like 'a service destroying an access requester' do
+ it_behaves_like 'a service destroying an access request of self' do
let(:member) { group.requesters.find_by(user_id: member_user.id) }
end
end
diff --git a/spec/services/members/invite_service_spec.rb b/spec/services/members/invite_service_spec.rb
index 6dbe161ee02..23d4d671afc 100644
--- a/spec/services/members/invite_service_spec.rb
+++ b/spec/services/members/invite_service_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_
let(:params) { {} }
let(:base_params) { { access_level: Gitlab::Access::GUEST, source: project, invite_source: '_invite_source_' } }
- subject(:result) { described_class.new(user, base_params.merge(params) ).execute }
+ subject(:result) { described_class.new(user, base_params.merge(params)).execute }
context 'when there is a valid member invited' do
let(:params) { { email: 'email@example.org' } }
@@ -393,7 +393,7 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_
context 'when email is already a member with a user on the project' do
let!(:existing_member) { create(:project_member, :guest, project: project) }
- let(:params) { { email: "#{existing_member.user.email}", access_level: ProjectMember::MAINTAINER } }
+ let(:params) { { email: existing_member.user.email.to_s, access_level: ProjectMember::MAINTAINER } }
it 'allows re-invite of an already invited email and updates the access_level' do
expect { result }.not_to change(ProjectMember, :count)
@@ -403,7 +403,7 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_
context 'when email belongs to an existing user as a confirmed secondary email' do
let(:secondary_email) { create(:email, :confirmed, email: 'secondary@example.com', user: existing_member.user) }
- let(:params) { { email: "#{secondary_email.email}" } }
+ let(:params) { { email: secondary_email.email.to_s } }
it 'allows re-invite to an already invited email' do
expect_to_create_members(count: 0)
diff --git a/spec/services/members/update_service_spec.rb b/spec/services/members/update_service_spec.rb
index f919d6d1516..eb8fae03c39 100644
--- a/spec/services/members/update_service_spec.rb
+++ b/spec/services/members/update_service_spec.rb
@@ -3,18 +3,34 @@
require 'spec_helper'
RSpec.describe Members::UpdateService do
- let(:project) { create(:project, :public) }
- let(:group) { create(:group, :public) }
- let(:current_user) { create(:user) }
- let(:member_user) { create(:user) }
- let(:permission) { :update }
- let(:member) { source.members_and_requesters.find_by!(user_id: member_user.id) }
- let(:access_level) { Gitlab::Access::MAINTAINER }
- let(:params) do
- { access_level: access_level }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:member_user1) { create(:user) }
+ let_it_be(:member_user2) { create(:user) }
+ let_it_be(:member_users) { [member_user1, member_user2] }
+ let_it_be(:permission) { :update }
+ let_it_be(:access_level) { Gitlab::Access::MAINTAINER }
+ let(:members) { source.members_and_requesters.where(user_id: member_users).to_a }
+ let(:update_service) { described_class.new(current_user, params) }
+ let(:params) { { access_level: access_level } }
+ let(:updated_members) do
+ result = subject
+ Array.wrap(result[:members] || result[:member])
end
- subject { described_class.new(current_user, params).execute(member, permission: permission) }
+ before do
+ member_users.first.tap do |member_user|
+ expires_at = 10.days.from_now
+ project.add_member(member_user, Gitlab::Access::DEVELOPER, expires_at: expires_at)
+ group.add_member(member_user, Gitlab::Access::DEVELOPER, expires_at: expires_at)
+ end
+
+ member_users[1..].each do |member_user|
+ project.add_developer(member_user)
+ group.add_developer(member_user)
+ end
+ end
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
it 'raises Gitlab::Access::AccessDeniedError' do
@@ -23,164 +39,326 @@ RSpec.describe Members::UpdateService do
end
end
- shared_examples 'a service updating a member' do
- it 'updates the member' do
- expect(TodosDestroyer::EntityLeaveWorker).not_to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, source.class.name)
+ shared_examples 'current user cannot update the given members' do
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let_it_be(:source) { project }
+ end
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let_it_be(:source) { group }
+ end
+ end
+
+ shared_examples 'returns error status when params are invalid' do
+ let_it_be(:params) { { expires_at: 2.days.ago } }
- updated_member = subject.fetch(:member)
+ specify do
+ expect(subject[:status]).to eq(:error)
+ end
+ end
- expect(updated_member).to be_valid
- expect(updated_member.access_level).to eq(access_level)
+ shared_examples 'a service updating members' do
+ it 'updates the members' do
+ new_access_levels = updated_members.map(&:access_level)
+
+ expect(updated_members).not_to be_empty
+ expect(updated_members).to all(be_valid)
+ expect(new_access_levels).to all(be access_level)
end
it 'returns success status' do
- result = subject.fetch(:status)
+ expect(subject.fetch(:status)).to eq(:success)
+ end
- expect(result).to eq(:success)
+ it 'invokes after_execute with correct args' do
+ members.each do |member|
+ expect(update_service).to receive(:after_execute).with(
+ action: permission,
+ old_access_level: member.human_access,
+ old_expiry: member.expires_at,
+ member: member
+ )
+ end
+
+ subject
end
- context 'when member is downgraded to guest' do
- shared_examples 'schedules to delete confidential todos' do
- it do
- expect(TodosDestroyer::EntityLeaveWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, source.class.name).once
+ it 'authorization update callback is triggered' do
+ expect(members).to all(receive(:refresh_member_authorized_projects).once)
- updated_member = subject.fetch(:member)
+ subject
+ end
+
+ it 'does not enqueues todos for deletion' do
+ members.each do |member|
+ expect(TodosDestroyer::EntityLeaveWorker)
+ .not_to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, source.class.name)
+ end
- expect(updated_member).to be_valid
- expect(updated_member.access_level).to eq(Gitlab::Access::GUEST)
+ subject
+ end
+
+ context 'when members are downgraded to guest' do
+ shared_examples 'schedules to delete confidential todos' do
+ it do
+ members.each do |member|
+ expect(TodosDestroyer::EntityLeaveWorker)
+ .to receive(:perform_in)
+ .with(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, source.class.name).once
+ end
+
+ new_access_levels = updated_members.map(&:access_level)
+ expect(updated_members).to all(be_valid)
+ expect(new_access_levels).to all(be Gitlab::Access::GUEST)
end
end
context 'with Gitlab::Access::GUEST level as a string' do
- let(:params) { { access_level: Gitlab::Access::GUEST.to_s } }
+ let_it_be(:params) { { access_level: Gitlab::Access::GUEST.to_s } }
it_behaves_like 'schedules to delete confidential todos'
end
context 'with Gitlab::Access::GUEST level as an integer' do
- let(:params) { { access_level: Gitlab::Access::GUEST } }
+ let_it_be(:params) { { access_level: Gitlab::Access::GUEST } }
it_behaves_like 'schedules to delete confidential todos'
end
end
context 'when access_level is invalid' do
- let(:params) { { access_level: 'invalid' } }
+ let_it_be(:params) { { access_level: 'invalid' } }
it 'raises an error' do
- expect { described_class.new(current_user, params) }.to raise_error(ArgumentError, 'invalid value for Integer(): "invalid"')
+ expect { described_class.new(current_user, params) }
+ .to raise_error(ArgumentError, 'invalid value for Integer(): "invalid"')
end
end
- context 'when member is not valid' do
- let(:params) { { expires_at: 2.days.ago } }
+ context 'when members update results in no change' do
+ let(:params) { { access_level: members.first.access_level } }
- it 'returns error status' do
- result = subject
+ it 'does not invoke update! and post_update' do
+ expect(update_service).not_to receive(:save!)
+ expect(update_service).not_to receive(:post_update)
- expect(result[:status]).to eq(:error)
+ expect(subject[:status]).to eq(:success)
end
- end
- end
-
- before do
- project.add_developer(member_user)
- group.add_developer(member_user)
- end
- context 'when current user cannot update the given member' do
- it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:source) { project }
- end
+ it 'authorization update callback is not triggered' do
+ members.each { |member| expect(member).not_to receive(:refresh_member_authorized_projects) }
- it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:source) { group }
+ subject
+ end
end
end
- context 'when current user can update the given member' do
+ shared_examples 'updating a project' do
+ let_it_be(:group_project) { create(:project, group: create(:group)) }
+ let_it_be(:source) { group_project }
+
before do
- project.add_maintainer(current_user)
- group.add_owner(current_user)
+ member_users.each { |member_user| group_project.add_developer(member_user) }
end
- it_behaves_like 'a service updating a member' do
- let(:source) { project }
- end
+ context 'as a project maintainer' do
+ before do
+ group_project.add_maintainer(current_user)
+ end
- it_behaves_like 'a service updating a member' do
- let(:source) { group }
- end
- end
+ it_behaves_like 'a service updating members'
- context 'in a project' do
- let_it_be(:group_project) { create(:project, group: create(:group)) }
+ context 'when member update results in an error' do
+ it_behaves_like 'a service returning an error'
+ end
- let(:source) { group_project }
+ context 'and updating members to OWNER' do
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let_it_be(:access_level) { Gitlab::Access::OWNER }
+ end
+ end
- context 'a project maintainer' do
- before do
- group_project.add_maintainer(current_user)
+ context 'and updating themselves to OWNER' do
+ let(:members) { source.members_and_requesters.find_by!(user_id: current_user.id) }
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let_it_be(:access_level) { Gitlab::Access::OWNER }
+ end
end
- context 'cannot update a member to OWNER' do
+ context 'and downgrading members from OWNER' do
before do
- group_project.add_developer(member_user)
+ member_users.each { |member_user| group_project.add_owner(member_user) }
end
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:access_level) { Gitlab::Access::OWNER }
+ let_it_be(:access_level) { Gitlab::Access::MAINTAINER }
end
end
+ end
- context 'cannot update themselves to OWNER' do
- let(:member) { source.members_and_requesters.find_by!(user_id: current_user.id) }
+ context 'when current_user is considered an owner in the project via inheritance' do
+ before do
+ group_project.group.add_owner(current_user)
+ end
+ context 'and can update members to OWNER' do
before do
- group_project.add_developer(member_user)
+ member_users.each { |member_user| group_project.add_developer(member_user) }
end
- it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:access_level) { Gitlab::Access::OWNER }
+ it_behaves_like 'a service updating members' do
+ let_it_be(:access_level) { Gitlab::Access::OWNER }
end
end
- context 'cannot downgrade a member from OWNER' do
+ context 'and can downgrade members from OWNER' do
before do
- group_project.add_owner(member_user)
+ member_users.each { |member_user| group_project.add_owner(member_user) }
end
- it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:access_level) { Gitlab::Access::MAINTAINER }
+ it_behaves_like 'a service updating members' do
+ let_it_be(:access_level) { Gitlab::Access::MAINTAINER }
end
end
end
+ end
+
+ shared_examples 'updating a group' do
+ let_it_be(:source) { group }
+
+ before do
+ group.add_owner(current_user)
+ end
+
+ it_behaves_like 'a service updating members'
+
+ context 'when member update results in an error' do
+ it_behaves_like 'a service returning an error'
+ end
+
+ context 'when group members expiration date is updated' do
+ let_it_be(:params) { { expires_at: 20.days.from_now } }
+ let(:notification_service) { instance_double(NotificationService) }
- context 'owners' do
before do
- # so that `current_user` is considered an `OWNER` in the project via inheritance.
- group_project.group.add_owner(current_user)
+ allow(NotificationService).to receive(:new).and_return(notification_service)
end
- context 'can update a member to OWNER' do
- before do
- group_project.add_developer(member_user)
+ it 'emails the users that their group membership expiry has changed' do
+ members.each do |member|
+ expect(notification_service).to receive(:updated_group_member_expiration).with(member)
end
- it_behaves_like 'a service updating a member' do
- let(:access_level) { Gitlab::Access::OWNER }
- end
+ subject
end
+ end
+ end
- context 'can downgrade a member from OWNER' do
- before do
- group_project.add_owner(member_user)
+ context 'when :bulk_update_membership_roles feature flag is disabled' do
+ let(:member) { source.members_and_requesters.find_by!(user_id: member_user1.id) }
+ let(:members) { [member] }
+
+ subject { update_service.execute(member, permission: permission) }
+
+ shared_examples 'a service returning an error' do
+ before do
+ allow(member).to receive(:save) do
+ member.errors.add(:user_id)
+ member.errors.add(:access_level)
end
+ .and_return(false)
+ end
+
+ it_behaves_like 'returns error status when params are invalid'
+
+ it 'returns the error' do
+ response = subject
+
+ expect(response[:status]).to eq(:error)
+ expect(response[:message]).to eq('User is invalid and Access level is invalid')
+ end
+ end
+
+ before do
+ stub_feature_flags(bulk_update_membership_roles: false)
+ end
+
+ it_behaves_like 'current user cannot update the given members'
+ it_behaves_like 'updating a project'
+ it_behaves_like 'updating a group'
+ end
+
+ subject { update_service.execute(members, permission: permission) }
+
+ shared_examples 'a service returning an error' do
+ it_behaves_like 'returns error status when params are invalid'
- it_behaves_like 'a service updating a member' do
- let(:access_level) { Gitlab::Access::MAINTAINER }
+ context 'when a member update results in invalid record' do
+ let(:member2) { members.second }
+
+ before do
+ allow(member2).to receive(:save!) do
+ member2.errors.add(:user_id)
+ member2.errors.add(:access_level)
+ end.and_raise(ActiveRecord::RecordInvalid)
+ end
+
+ it 'returns the error' do
+ response = subject
+
+ expect(response[:status]).to eq(:error)
+ expect(response[:message]).to eq('User is invalid and Access level is invalid')
+ end
+
+ it 'rollbacks back the entire update' do
+ old_access_levels = members.pluck(:access_level)
+
+ subject
+
+ expect(members.each(&:reset).pluck(:access_level)).to eq(old_access_levels)
+ end
+ end
+ end
+
+ it_behaves_like 'current user cannot update the given members'
+ it_behaves_like 'updating a project'
+ it_behaves_like 'updating a group'
+
+ context 'with a single member' do
+ let(:member) { create(:group_member, group: group) }
+ let(:members) { member }
+
+ before do
+ group.add_owner(current_user)
+ end
+
+ it 'returns the correct response' do
+ expect(subject[:member]).to eq(member)
+ end
+ end
+
+ context 'when current user is an admin', :enable_admin_mode do
+ let_it_be(:current_user) { create(:admin) }
+ let_it_be(:source) { group }
+
+ context 'when all owners are being downgraded' do
+ before do
+ member_users.each { |member_user| group.add_owner(member_user) }
+ end
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError'
+ end
+
+ context 'when all blocked owners are being downgraded' do
+ before do
+ member_users.each do |member_user|
+ group.add_owner(member_user)
+ member_user.block
end
end
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError'
end
end
end
diff --git a/spec/services/merge_requests/approval_service_spec.rb b/spec/services/merge_requests/approval_service_spec.rb
index 0846ec7f50e..da6492aca95 100644
--- a/spec/services/merge_requests/approval_service_spec.rb
+++ b/spec/services/merge_requests/approval_service_spec.rb
@@ -33,9 +33,17 @@ RSpec.describe MergeRequests::ApprovalService do
service.execute(merge_request)
end
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { service.execute(merge_request) }
+ end
+
it 'does not publish MergeRequests::ApprovedEvent' do
expect { service.execute(merge_request) }.not_to publish_event(MergeRequests::ApprovedEvent)
end
+
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestReviewersUpdated' do
+ let(:action) { service.execute(merge_request) }
+ end
end
context 'with an already approved MR' do
@@ -46,6 +54,14 @@ RSpec.describe MergeRequests::ApprovalService do
it 'does not create an approval' do
expect { service.execute(merge_request) }.not_to change { merge_request.approvals.size }
end
+
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { service.execute(merge_request) }
+ end
+
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestReviewersUpdated' do
+ let(:action) { service.execute(merge_request) }
+ end
end
context 'with valid approval' do
@@ -67,6 +83,14 @@ RSpec.describe MergeRequests::ApprovalService do
.to publish_event(MergeRequests::ApprovedEvent)
.with(current_user_id: user.id, merge_request_id: merge_request.id)
end
+
+ it_behaves_like 'triggers GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { service.execute(merge_request) }
+ end
+
+ it_behaves_like 'triggers GraphQL subscription mergeRequestReviewersUpdated' do
+ let(:action) { service.execute(merge_request) }
+ end
end
context 'user cannot update the merge request' do
@@ -77,6 +101,14 @@ RSpec.describe MergeRequests::ApprovalService do
it 'does not update approvals' do
expect { service.execute(merge_request) }.not_to change { merge_request.approvals.size }
end
+
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { service.execute(merge_request) }
+ end
+
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestReviewersUpdated' do
+ let(:action) { service.execute(merge_request) }
+ end
end
end
end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 6a6f01e6a95..4f27ff30da7 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -93,7 +93,7 @@ RSpec.describe MergeRequests::BuildService do
shared_examples 'with a Default.md template' do
let(:files) { { '.gitlab/merge_request_templates/Default.md' => 'Default template contents' } }
- let(:project) { create(:project, :custom_repo, files: files ) }
+ let(:project) { create(:project, :custom_repo, files: files) }
it 'the template description is preferred' do
expect(merge_request.description).to eq('Default template contents')
@@ -306,7 +306,7 @@ RSpec.describe MergeRequests::BuildService do
context 'a Default.md template is defined' do
let(:files) { { '.gitlab/merge_request_templates/Default.md' => 'Default template contents' } }
- let(:project) { create(:project, :custom_repo, files: files ) }
+ let(:project) { create(:project, :custom_repo, files: files) }
it 'appends the closing description to a Default.md template' do
expected_description = ['Default template contents', closing_message].compact.join("\n\n")
@@ -386,7 +386,7 @@ RSpec.describe MergeRequests::BuildService do
context 'a Default.md template is defined' do
let(:files) { { '.gitlab/merge_request_templates/Default.md' => 'Default template contents' } }
- let(:project) { create(:project, :custom_repo, files: files ) }
+ let(:project) { create(:project, :custom_repo, files: files) }
it 'keeps the description from the initial params' do
expect(merge_request.description).to eq(description)
@@ -425,7 +425,7 @@ RSpec.describe MergeRequests::BuildService do
context 'a Default.md template is defined' do
let(:files) { { '.gitlab/merge_request_templates/Default.md' => 'Default template contents' } }
- let(:project) { create(:project, :custom_repo, files: files ) }
+ let(:project) { create(:project, :custom_repo, files: files) }
it 'appends the closing description to a Default.md template' do
expected_description = ['Default template contents', closing_message].compact.join("\n\n")
@@ -486,7 +486,7 @@ RSpec.describe MergeRequests::BuildService do
context 'a Default.md template is defined' do
let(:files) { { '.gitlab/merge_request_templates/Default.md' => 'Default template contents' } }
- let(:project) { create(:project, :custom_repo, files: files ) }
+ let(:project) { create(:project, :custom_repo, files: files) }
it 'appends the closing description to a Default.md template' do
expected_description = ['Default template contents', closing_message].compact.join("\n\n")
@@ -715,7 +715,7 @@ RSpec.describe MergeRequests::BuildService do
context 'when a Default template is found' do
context 'when its contents cannot be retrieved' do
let(:files) { { '.gitlab/merge_request_templates/OtherTemplate.md' => 'Other template contents' } }
- let(:project) { create(:project, :custom_repo, files: files ) }
+ let(:project) { create(:project, :custom_repo, files: files) }
it 'does not modify the merge request description' do
allow(TemplateFinder).to receive(:all_template_names).and_return({
@@ -732,7 +732,7 @@ RSpec.describe MergeRequests::BuildService do
context 'when its contents can be retrieved' do
let(:files) { { '.gitlab/merge_request_templates/Default.md' => 'Default template contents' } }
- let(:project) { create(:project, :custom_repo, files: files ) }
+ let(:project) { create(:project, :custom_repo, files: files) }
it 'modifies the merge request description' do
merge_request.description = nil
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index 0bc8258af42..da8e8d944d6 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -336,6 +336,12 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do
it_behaves_like 'reviewer_ids filter' do
let(:execute) { service.execute }
end
+
+ context 'when called in a transaction' do
+ it 'does not raise an error' do
+ expect { MergeRequest.transaction { described_class.new(project: project, current_user: user, params: opts).execute } }.not_to raise_error
+ end
+ end
end
it_behaves_like 'issuable record that supports quick actions' do
@@ -495,15 +501,40 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do
project.add_developer(user)
end
- it 'creates the merge request', :sidekiq_might_not_need_inline do
- expect_next_instance_of(MergeRequest) do |instance|
- expect(instance).to receive(:eager_fetch_ref!).and_call_original
+ context 'when async_merge_request_diff_creation is enabled' do
+ before do
+ stub_feature_flags(async_merge_request_diff_creation: true)
end
- merge_request = described_class.new(project: project, current_user: user, params: opts).execute
+ it 'creates the merge request', :sidekiq_inline do
+ expect_next_instance_of(MergeRequest) do |instance|
+ expect(instance).not_to receive(:eager_fetch_ref!)
+ end
- expect(merge_request).to be_persisted
- expect(merge_request.iid).to be > 0
+ merge_request = described_class.new(project: project, current_user: user, params: opts).execute
+
+ expect(merge_request).to be_persisted
+ expect(merge_request.iid).to be > 0
+ expect(merge_request.merge_request_diff).not_to be_empty
+ end
+ end
+
+ context 'when async_merge_request_diff_creation is disabled' do
+ before do
+ stub_feature_flags(async_merge_request_diff_creation: false)
+ end
+
+ it 'creates the merge request' do
+ expect_next_instance_of(MergeRequest) do |instance|
+ expect(instance).to receive(:eager_fetch_ref!).and_call_original
+ end
+
+ merge_request = described_class.new(project: project, current_user: user, params: opts).execute
+
+ expect(merge_request).to be_persisted
+ expect(merge_request.iid).to be > 0
+ expect(merge_request.merge_request_diff).not_to be_empty
+ end
end
it 'does not create the merge request when the target project is archived' do
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 cf34923795e..c56b38bccc1 100644
--- a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::Mergeability::RunChecksService do
+RSpec.describe MergeRequests::Mergeability::RunChecksService, :clean_gitlab_redis_cache do
subject(:run_checks) { described_class.new(merge_request: merge_request, params: {}) }
describe '#execute' do
@@ -104,18 +104,6 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
expect(execute.success?).to eq(true)
end
end
-
- context 'when mergeability_caching is turned off' do
- before do
- stub_feature_flags(mergeability_caching: false)
- end
-
- it 'does not call the results store' do
- expect(Gitlab::MergeRequests::Mergeability::ResultsStore).not_to receive(:new)
-
- expect(execute.success?).to eq(true)
- end
- end
end
end
@@ -161,11 +149,11 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
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
+ before do
+ run_checks.execute
+ end
+
it 'returns nil' do
expect(failure_reason).to eq(nil)
end
diff --git a/spec/services/merge_requests/mergeability_check_service_spec.rb b/spec/services/merge_requests/mergeability_check_service_spec.rb
index c24b83e21a6..ee23238314e 100644
--- a/spec/services/merge_requests/mergeability_check_service_spec.rb
+++ b/spec/services/merge_requests/mergeability_check_service_spec.rb
@@ -190,14 +190,6 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar
target_branch: 'conflict-start')
end
- it 'does not change the merge ref HEAD' do
- expect(merge_request.merge_ref_head).to be_nil
-
- subject
-
- expect(merge_request.reload.merge_ref_head).not_to be_nil
- end
-
it 'returns ServiceResponse.error and keeps merge status as cannot_be_merged' do
result = subject
@@ -351,27 +343,5 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar
end
end
end
-
- context 'merge with conflicts' do
- it 'calls MergeToRefService with true allow_conflicts param' do
- expect(MergeRequests::MergeToRefService).to receive(:new)
- .with(project: project, current_user: merge_request.author, params: { allow_conflicts: true }).and_call_original
-
- subject
- end
-
- context 'when display_merge_conflicts_in_diff is disabled' do
- before do
- stub_feature_flags(display_merge_conflicts_in_diff: false)
- end
-
- it 'calls MergeToRefService with false allow_conflicts param' do
- expect(MergeRequests::MergeToRefService).to receive(:new)
- .with(project: project, current_user: merge_request.author, params: { allow_conflicts: false }).and_call_original
-
- subject
- end
- end
- end
end
end
diff --git a/spec/services/merge_requests/remove_approval_service_spec.rb b/spec/services/merge_requests/remove_approval_service_spec.rb
index 5a319e90a68..7b38f0d1c45 100644
--- a/spec/services/merge_requests/remove_approval_service_spec.rb
+++ b/spec/services/merge_requests/remove_approval_service_spec.rb
@@ -45,6 +45,14 @@ RSpec.describe MergeRequests::RemoveApprovalService do
execute!
end
+
+ it_behaves_like 'triggers GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { execute! }
+ end
+
+ it_behaves_like 'triggers GraphQL subscription mergeRequestReviewersUpdated' do
+ let(:action) { execute! }
+ end
end
context 'with a user who has not approved' do
@@ -61,6 +69,14 @@ RSpec.describe MergeRequests::RemoveApprovalService do
execute!
end
+
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { execute! }
+ end
+
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestReviewersUpdated' do
+ let(:action) { execute! }
+ end
end
end
end
diff --git a/spec/services/merge_requests/squash_service_spec.rb b/spec/services/merge_requests/squash_service_spec.rb
index 9210242a11e..471bb03f18c 100644
--- a/spec/services/merge_requests/squash_service_spec.rb
+++ b/spec/services/merge_requests/squash_service_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe MergeRequests::SquashService do
- include GitHelpers
-
let(:service) { described_class.new(project: project, current_user: user, params: { merge_request: merge_request }) }
let(:user) { project.first_owner }
let(:project) { create(:project, :repository) }
@@ -109,11 +107,10 @@ RSpec.describe MergeRequests::SquashService do
end
it 'has the same diff as the merge request, but a different SHA' do
- rugged = rugged_repo(project.repository)
- mr_diff = rugged.diff(merge_request.diff_base_sha, merge_request.diff_head_sha)
- squash_diff = rugged.diff(merge_request.diff_start_sha, squash_sha)
+ mr_diff = project.repository.diff(merge_request.diff_base_sha, merge_request.diff_head_sha)
+ squash_diff = project.repository.diff(merge_request.diff_start_sha, squash_sha)
- expect(squash_diff.patch.length).to eq(mr_diff.patch.length)
+ expect(squash_diff.size).to eq(mr_diff.size)
expect(squash_commit.sha).not_to eq(merge_request.diff_head_sha)
end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 1d67574b06d..da78f86c7c8 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -794,7 +794,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
end
it "does not try to mark as unchecked if it's already unchecked" do
- expect(merge_request).to receive(:unchecked?).and_return(true)
+ allow(merge_request).to receive(:unchecked?).twice.and_return(true)
expect(merge_request).not_to receive(:mark_as_unchecked)
update_merge_request({ target_branch: "target" })
@@ -1148,7 +1148,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
end
end
- include_examples 'issuable update service' do
+ it_behaves_like 'issuable update service' do
let(:open_issuable) { merge_request }
let(:closed_issuable) { create(:closed_merge_request, source_project: project) }
end
diff --git a/spec/services/milestones/transfer_service_spec.rb b/spec/services/milestones/transfer_service_spec.rb
index b15d90d685c..de02226661c 100644
--- a/spec/services/milestones/transfer_service_spec.rb
+++ b/spec/services/milestones/transfer_service_spec.rb
@@ -11,7 +11,7 @@ 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_milestone) { create(:milestone, :closed, 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) }
@@ -38,6 +38,7 @@ RSpec.describe Milestones::TransferService do
expect(new_milestone).not_to eq(group_milestone)
expect(new_milestone.title).to eq(group_milestone.title)
expect(new_milestone.project_milestone?).to be_truthy
+ expect(new_milestone.state).to eq("closed")
end
context 'when milestone is from an ancestor group' do
@@ -88,6 +89,7 @@ RSpec.describe Milestones::TransferService do
expect(new_milestone).not_to eq(group_milestone)
expect(new_milestone.title).to eq(group_milestone.title)
expect(new_milestone.project_milestone?).to be_truthy
+ expect(new_milestone.state).to eq("closed")
end
it 'does not apply new project milestone to issuables with project milestone' do
diff --git a/spec/services/namespaces/statistics_refresher_service_spec.rb b/spec/services/namespaces/statistics_refresher_service_spec.rb
index d3379e843ec..2d5f9235bd4 100644
--- a/spec/services/namespaces/statistics_refresher_service_spec.rb
+++ b/spec/services/namespaces/statistics_refresher_service_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Namespaces::StatisticsRefresherService, '#execute' do
let(:group) { create(:group) }
+ let(:subgroup) { create(:group, parent: group) }
let(:projects) { create_list(:project, 5, namespace: group) }
let(:service) { described_class.new }
@@ -23,6 +24,14 @@ RSpec.describe Namespaces::StatisticsRefresherService, '#execute' do
service.execute(group)
end
+
+ context 'when given a subgroup' do
+ it 'does not create statistics for the subgroup' do
+ service.execute(subgroup)
+
+ expect(subgroup.reload.root_storage_statistics).not_to be_present
+ end
+ end
end
context 'with a root storage statistics relation', :sidekiq_might_not_need_inline do
@@ -43,6 +52,16 @@ RSpec.describe Namespaces::StatisticsRefresherService, '#execute' do
service.execute(group)
end
+
+ context 'when given a subgroup' do
+ it "recalculates the root namespace's statistics" do
+ expect(Namespace::RootStorageStatistics)
+ .to receive(:safe_find_or_create_by!).with({ namespace_id: group.id })
+ .and_return(group.root_storage_statistics)
+
+ service.execute(subgroup)
+ end
+ end
end
context 'when something goes wrong' do
diff --git a/spec/services/notes/build_service_spec.rb b/spec/services/notes/build_service_spec.rb
index c25895d2efa..67d8b37f809 100644
--- a/spec/services/notes/build_service_spec.rb
+++ b/spec/services/notes/build_service_spec.rb
@@ -189,13 +189,13 @@ RSpec.describe Notes::BuildService do
context 'issuable author' do
let(:user) { noteable_author }
- it_behaves_like 'user allowed to set comment as confidential'
+ it_behaves_like 'user not allowed to set comment as confidential'
end
context 'issuable assignee' do
let(:user) { issuable_assignee }
- it_behaves_like 'user allowed to set comment as confidential'
+ it_behaves_like 'user not allowed to set comment as confidential'
end
context 'admin' do
@@ -265,13 +265,13 @@ RSpec.describe Notes::BuildService do
context 'with noteable author' do
let(:user) { note.noteable.author }
- it_behaves_like 'confidential set to `true`'
+ it_behaves_like 'returns `Discussion to reply to cannot be found` error'
end
context 'with noteable assignee' do
let(:user) { issuable_assignee }
- it_behaves_like 'confidential set to `true`'
+ it_behaves_like 'returns `Discussion to reply to cannot be found` error'
end
context 'with guest access' do
diff --git a/spec/services/notes/update_service_spec.rb b/spec/services/notes/update_service_spec.rb
index 989ca7b8df1..05703ac548d 100644
--- a/spec/services/notes/update_service_spec.rb
+++ b/spec/services/notes/update_service_spec.rb
@@ -245,7 +245,7 @@ RSpec.describe Notes::UpdateService do
context 'for a personal snippet' do
let_it_be(:snippet) { create(:personal_snippet, :public) }
- let(:note) { create(:note, project: nil, noteable: snippet, author: user, note: "Note on a snippet with reference #{issue.to_reference}" ) }
+ let(:note) { create(:note, project: nil, noteable: snippet, author: user, note: "Note on a snippet with reference #{issue.to_reference}") }
it 'does not create todos' do
expect { update_note({ note: "Mentioning user #{user2}" }) }.not_to change { note.todos.count }
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 8fbf023cda0..7857bd2263f 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -211,6 +211,23 @@ RSpec.describe NotificationService, :mailer do
it_behaves_like 'participating by assignee notification'
end
+ describe '.permitted_actions' do
+ it 'includes public methods' do
+ expect(described_class.permitted_actions).to include(:access_token_created)
+ end
+
+ it 'excludes EXCLUDED_ACTIONS' do
+ described_class::EXCLUDED_ACTIONS.each do |action|
+ expect(described_class.permitted_actions).not_to include(action)
+ end
+ end
+
+ it 'excludes protected and private methods' do
+ expect(described_class.permitted_actions).not_to include(:new_resource_email)
+ expect(described_class.permitted_actions).not_to include(:approve_mr_email)
+ end
+ end
+
describe '#async' do
let(:async) { notification.async }
diff --git a/spec/services/packages/composer/composer_json_service_spec.rb b/spec/services/packages/composer/composer_json_service_spec.rb
index 378016a6ffb..d2187688c4c 100644
--- a/spec/services/packages/composer/composer_json_service_spec.rb
+++ b/spec/services/packages/composer/composer_json_service_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Packages::Composer::ComposerJsonService do
subject { described_class.new(project, target).execute }
context 'with an existing file' do
- let(:project) { create(:project, :custom_repo, files: { 'composer.json' => json } ) }
+ let(:project) { create(:project, :custom_repo, files: { 'composer.json' => json }) }
context 'with a valid file' do
let(:json) { '{ "name": "package-name"}' }
diff --git a/spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb b/spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb
index 39c6feb5d12..70c2bbad87a 100644
--- a/spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb
+++ b/spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb
@@ -65,6 +65,23 @@ RSpec.describe ::Packages::Maven::Metadata::CreateVersionsXmlService do
let(:versions_in_database) { versions_in_xml + additional_versions }
it_behaves_like 'returning an xml with versions in the database'
+
+ context 'with an xml without a release version' do
+ let(:version_release) { nil }
+
+ it_behaves_like 'returning an xml with versions in the database'
+
+ it 'logs a warn with the reason' do
+ expect(Gitlab::AppJsonLogger).to receive(:warn).with(
+ message: 'A malformed metadata file has been encountered',
+ reason: 'Missing release tag',
+ project_id: package.project_id,
+ package_id: package.id
+ )
+
+ subject
+ end
+ end
end
end
diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb
index a3e59913918..ef8cdf2e8ab 100644
--- a/spec/services/packages/npm/create_package_service_spec.rb
+++ b/spec/services/packages/npm/create_package_service_spec.rb
@@ -148,7 +148,7 @@ RSpec.describe Packages::Npm::CreatePackageService do
end
context 'when file size is faked by setting the attachment length param to a lower size' do
- let(:params) { super().deep_merge!( { _attachments: { "#{package_name}-#{version}.tgz" => { data: encoded_package_data, length: 1 } } }) }
+ let(:params) { super().deep_merge!({ _attachments: { "#{package_name}-#{version}.tgz" => { data: encoded_package_data, length: 1 } } }) }
# TODO (technical debt): Extract the package size calculation outside the service and add separate specs for it.
# Right now we have several contexts here to test the calculation's different scenarios.
@@ -193,7 +193,7 @@ RSpec.describe Packages::Npm::CreatePackageService do
end
context 'with empty versions' do
- let(:params) { super().merge!({ versions: {} } ) }
+ let(:params) { super().merge!({ versions: {} }) }
it { expect(subject[:http_status]).to eq 400 }
it { expect(subject[:message]).to eq 'Version is empty.' }
diff --git a/spec/services/packages/rpm/repository_metadata/base_builder_spec.rb b/spec/services/packages/rpm/repository_metadata/base_builder_spec.rb
deleted file mode 100644
index 524c224177b..00000000000
--- a/spec/services/packages/rpm/repository_metadata/base_builder_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe Packages::Rpm::RepositoryMetadata::BaseBuilder do
- describe '#execute' do
- subject { described_class.new(xml: xml, data: data).execute }
-
- let(:xml) { nil }
- let(:data) { {} }
-
- before do
- stub_const("#{described_class}::ROOT_TAG", 'test')
- stub_const("#{described_class}::ROOT_ATTRIBUTES", { foo1: 'bar1', foo2: 'bar2' })
- end
-
- it 'generate valid xml' do
- result = Nokogiri::XML::Document.parse(subject)
-
- expect(result.children.count).to eq(1)
- expect(result.children.first.attributes.count).to eq(2)
- expect(result.children.first.attributes['foo1'].value).to eq('bar1')
- expect(result.children.first.attributes['foo2'].value).to eq('bar2')
- end
-
- context 'when call with parameters' do
- let(:xml) { 'test' }
-
- it 'raise NotImplementedError' do
- expect { subject }.to raise_error NotImplementedError
- end
- end
- end
-end
diff --git a/spec/services/packages/rpm/repository_metadata/build_filelist_xml_service_spec.rb b/spec/services/packages/rpm/repository_metadata/build_filelist_xml_service_spec.rb
new file mode 100644
index 00000000000..d93d6ab9fcb
--- /dev/null
+++ b/spec/services/packages/rpm/repository_metadata/build_filelist_xml_service_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Rpm::RepositoryMetadata::BuildFilelistXmlService do
+ describe '#execute' do
+ subject { described_class.new(data).execute }
+
+ include_context 'with rpm package data'
+
+ let(:data) { xml_update_params }
+ let(:file_xpath) { "//package/file" }
+
+ it 'adds all file nodes' do
+ result = subject
+
+ expect(result.xpath(file_xpath).count).to eq(data[:files].count)
+ end
+
+ describe 'setting type attribute' do
+ context 'when all files are directories' do
+ let(:dirs) do
+ 3.times.map { generate_directory } # rubocop:disable Performance/TimesMap
+ end
+
+ let(:files) do
+ 5.times.map { FFaker::Filesystem.file_name(dirs.sample) } # rubocop:disable Performance/TimesMap
+ end
+
+ let(:data) do
+ {
+ directories: dirs.map { "#{_1}/" }, # Add trailing slash as in original package
+ files: dirs + files
+ }
+ end
+
+ it 'set dir type attribute for directories only' do
+ result = subject
+
+ result.xpath(file_xpath).each do |tag|
+ if dirs.include?(tag.content)
+ expect(tag.attributes['type']&.value).to eq('dir')
+ else
+ expect(tag.attributes['type']).to be_nil
+ end
+ end
+ end
+ end
+
+ def generate_directory
+ FFaker::Lorem.words(3).join('/')
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/rpm/repository_metadata/build_filelist_xml_spec.rb b/spec/services/packages/rpm/repository_metadata/build_filelist_xml_spec.rb
deleted file mode 100644
index 2feb44c7c1b..00000000000
--- a/spec/services/packages/rpm/repository_metadata/build_filelist_xml_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe Packages::Rpm::RepositoryMetadata::BuildFilelistXml do
- describe '#execute' do
- subject { described_class.new.execute }
-
- context "when generate empty xml" do
- let(:expected_xml) do
- <<~XML
- <?xml version="1.0" encoding="UTF-8"?>
- <filelists xmlns="http://linux.duke.edu/metadata/filelists" packages="0"/>
- XML
- end
-
- it 'generate expected xml' do
- expect(subject).to eq(expected_xml)
- end
- end
- end
-end
diff --git a/spec/services/packages/rpm/repository_metadata/build_other_xml_service_spec.rb b/spec/services/packages/rpm/repository_metadata/build_other_xml_service_spec.rb
new file mode 100644
index 00000000000..201f9e67ce9
--- /dev/null
+++ b/spec/services/packages/rpm/repository_metadata/build_other_xml_service_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Rpm::RepositoryMetadata::BuildOtherXmlService do
+ describe '#execute' do
+ subject { described_class.new(data).execute }
+
+ include_context 'with rpm package data'
+
+ let(:data) { xml_update_params }
+ let(:changelog_xpath) { "//package/changelog" }
+
+ it 'adds all changelog nodes' do
+ result = subject
+
+ expect(result.xpath(changelog_xpath).count).to eq(data[:changelogs].count)
+ end
+
+ it 'set required date attribute' do
+ result = subject
+
+ data[:changelogs].each do |changelog|
+ expect(result.at("#{changelog_xpath}[@date=\"#{changelog[:changelogtime]}\"]")).not_to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/rpm/repository_metadata/build_other_xml_spec.rb b/spec/services/packages/rpm/repository_metadata/build_other_xml_spec.rb
deleted file mode 100644
index 823aa18808a..00000000000
--- a/spec/services/packages/rpm/repository_metadata/build_other_xml_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe Packages::Rpm::RepositoryMetadata::BuildOtherXml do
- describe '#execute' do
- subject { described_class.new.execute }
-
- context "when generate empty xml" do
- let(:expected_xml) do
- <<~XML
- <?xml version="1.0" encoding="UTF-8"?>
- <otherdata xmlns="http://linux.duke.edu/metadata/other" packages="0"/>
- XML
- end
-
- it 'generate expected xml' do
- expect(subject).to eq(expected_xml)
- end
- end
- end
-end
diff --git a/spec/services/packages/rpm/repository_metadata/build_primary_xml_service_spec.rb b/spec/services/packages/rpm/repository_metadata/build_primary_xml_service_spec.rb
new file mode 100644
index 00000000000..9bbfa5c9863
--- /dev/null
+++ b/spec/services/packages/rpm/repository_metadata/build_primary_xml_service_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Rpm::RepositoryMetadata::BuildPrimaryXmlService do
+ describe '#execute' do
+ subject { described_class.new(data).execute }
+
+ include_context 'with rpm package data'
+
+ let(:data) { xml_update_params }
+ let(:required_text_only_attributes) { %i[description summary arch name] }
+
+ it 'adds node with required_text_only_attributes' do
+ result = subject
+
+ required_text_only_attributes.each do |attribute|
+ expect(
+ result.at("//package/#{attribute}").text
+ ).to eq(data[attribute])
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/rpm/repository_metadata/build_primary_xml_spec.rb b/spec/services/packages/rpm/repository_metadata/build_primary_xml_spec.rb
deleted file mode 100644
index 147d5862a71..00000000000
--- a/spec/services/packages/rpm/repository_metadata/build_primary_xml_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe Packages::Rpm::RepositoryMetadata::BuildPrimaryXml do
- describe '#execute' do
- subject { described_class.new(xml: xml, data: data).execute }
-
- let(:empty_xml) do
- <<~XML
- <?xml version="1.0" encoding="UTF-8"?>
- <metadata xmlns="http://linux.duke.edu/metadata/common" xmlns:rpm="http://linux.duke.edu/metadata/rpm" packages="0"/>
- XML
- end
-
- it_behaves_like 'handling rpm xml file'
-
- context 'when updating existing xml' do
- include_context 'with rpm package data'
-
- let(:xml) { empty_xml }
- let(:data) { xml_update_params }
- let(:required_text_only_attributes) { %i[description summary arch name] }
-
- it 'adds node with required_text_only_attributes' do
- result = Nokogiri::XML::Document.parse(subject).remove_namespaces!
-
- required_text_only_attributes.each do |attribute|
- expect(
- result.at("//#{described_class::ROOT_TAG}/package/#{attribute}").text
- ).to eq(data[attribute])
- end
- end
- end
- end
-end
diff --git a/spec/services/packages/rpm/repository_metadata/build_repomd_xml_spec.rb b/spec/services/packages/rpm/repository_metadata/build_repomd_xml_service_spec.rb
index 0843a983b7e..cf28301fa2c 100644
--- a/spec/services/packages/rpm/repository_metadata/build_repomd_xml_spec.rb
+++ b/spec/services/packages/rpm/repository_metadata/build_repomd_xml_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Rpm::RepositoryMetadata::BuildRepomdXml do
+RSpec.describe Packages::Rpm::RepositoryMetadata::BuildRepomdXmlService do
describe '#execute' do
subject { described_class.new(data).execute }
diff --git a/spec/services/packages/rpm/repository_metadata/update_xml_service_spec.rb b/spec/services/packages/rpm/repository_metadata/update_xml_service_spec.rb
new file mode 100644
index 00000000000..e351392ba1c
--- /dev/null
+++ b/spec/services/packages/rpm/repository_metadata/update_xml_service_spec.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Rpm::RepositoryMetadata::UpdateXmlService do
+ describe '#execute' do
+ subject { described_class.new(filename: filename, xml: xml, data: data).execute }
+
+ let(:xml) { nil }
+ let(:data) { nil }
+
+ shared_examples 'handling not implemented xml filename' do
+ let(:filename) { :not_implemented_yet }
+ let(:empty_xml) { '' }
+
+ it 'raise error' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+
+ shared_context 'with primary xml file data' do
+ let(:filename) { :primary }
+ let(:empty_xml) do
+ <<~XML
+ <?xml version="1.0" encoding="UTF-8"?>
+ <metadata xmlns="http://linux.duke.edu/metadata/common" xmlns:rpm="http://linux.duke.edu/metadata/rpm" packages="0"/>
+ XML
+ end
+ end
+
+ shared_context 'with other xml file data' do
+ let(:filename) { :other }
+ let(:empty_xml) do
+ <<~XML
+ <?xml version="1.0" encoding="UTF-8"?>
+ <otherdata xmlns="http://linux.duke.edu/metadata/other" packages="0"/>
+ XML
+ end
+ end
+
+ shared_context 'with filelist xml file data' do
+ let(:filename) { :filelist }
+ let(:empty_xml) do
+ <<~XML
+ <?xml version="1.0" encoding="UTF-8"?>
+ <filelists xmlns="http://linux.duke.edu/metadata/filelists" packages="0"/>
+ XML
+ end
+ end
+
+ context 'when building empty xml' do
+ shared_examples 'generating empty xml' do
+ it 'generate expected xml' do
+ expect(subject).to eq(empty_xml)
+ end
+ end
+
+ it_behaves_like 'handling not implemented xml filename'
+
+ context "for 'primary' xml file" do
+ include_context 'with primary xml file data'
+
+ it_behaves_like 'generating empty xml'
+ end
+
+ context "for 'other' xml file" do
+ include_context 'with other xml file data'
+
+ it_behaves_like 'generating empty xml'
+ end
+
+ context "for 'filelist' xml file" do
+ include_context 'with filelist xml file data'
+
+ it_behaves_like 'generating empty xml'
+ end
+ end
+
+ context 'when updating xml file' do
+ include_context 'with rpm package data'
+
+ let(:xml) { empty_xml }
+ let(:data) { xml_update_params }
+ let(:builder_class) { described_class::BUILDERS[filename] }
+
+ shared_examples 'updating rpm xml file' do
+ context 'when updating existing xml' do
+ shared_examples 'changing root tag attribute' do
+ it "increment previous 'packages' value by 1" do
+ previous_value = Nokogiri::XML(xml).at(builder_class::ROOT_TAG).attributes["packages"].value.to_i
+ new_value = Nokogiri::XML(subject).at(builder_class::ROOT_TAG).attributes["packages"].value.to_i
+
+ expect(previous_value + 1).to eq(new_value)
+ end
+ end
+
+ it 'generate valid xml add expected xml node to existing xml' do
+ # Have one root attribute
+ result = Nokogiri::XML::Document.parse(subject).remove_namespaces!
+ expect(result.children.count).to eq(1)
+
+ # Root node has 1 child with generated node
+ expect(result.xpath("//#{builder_class::ROOT_TAG}/package").count).to eq(1)
+ end
+
+ context 'when empty xml' do
+ it_behaves_like 'changing root tag attribute'
+ end
+
+ context 'when xml has children' do
+ context "when node with given 'pkgid' does not exist yet" do
+ let(:uniq_node_data) do
+ xml_update_params.tap do |data|
+ data[:pkgid] = SecureRandom.uuid
+ end
+ end
+
+ let(:xml) { build_xml_from(uniq_node_data) }
+
+ it 'has children nodes' do
+ existing_xml = Nokogiri::XML::Document.parse(xml).remove_namespaces!
+ expect(existing_xml.xpath('//package').count).to eq(1)
+ end
+
+ it_behaves_like 'changing root tag attribute'
+ end
+
+ context "when node with given 'pkgid' already exist" do
+ let(:existing_node_data) do
+ existing_data = data.dup
+ existing_data[:name] = FFaker::Lorem.word
+ existing_data
+ end
+
+ let(:xml) { build_xml_from(existing_node_data) }
+
+ it 'has children nodes' do
+ existing_xml = Nokogiri::XML::Document.parse(xml).remove_namespaces!
+ expect(existing_xml.xpath('//package').count).to eq(1)
+ end
+
+ it 'replace existing node with new data' do
+ existing_xml = Nokogiri::XML::Document.parse(xml).remove_namespaces!
+ result = Nokogiri::XML::Document.parse(subject).remove_namespaces!
+ expect(result.xpath('//package').count).to eq(1)
+ expect(result.xpath('//package').first.to_xml).not_to eq(existing_xml.xpath('//package').first.to_xml)
+ end
+ end
+
+ def build_xml_from(data)
+ described_class.new(filename: filename, xml: empty_xml, data: data).execute
+ end
+ end
+ end
+ end
+
+ it_behaves_like 'handling not implemented xml filename'
+
+ context "for 'primary' xml file" do
+ include_context 'with primary xml file data'
+
+ it_behaves_like 'updating rpm xml file'
+ end
+
+ context "for 'other' xml file" do
+ include_context 'with other xml file data'
+
+ it_behaves_like 'updating rpm xml file'
+ end
+
+ context "for 'filelist' xml file" do
+ include_context 'with filelist xml file data'
+
+ it_behaves_like 'updating rpm xml file'
+ end
+ end
+ end
+end
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 65da1976dc2..eb8d94ebfa5 100644
--- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe Projects::HashedStorage::MigrateRepositoryService do
- include GitHelpers
-
let(:gitlab_shell) { Gitlab::Shell.new }
let(:project) { create(:project, :legacy_storage, :repository, :wiki_repo, :design_repo) }
let(:legacy_storage) { Storage::LegacyProject.new(project) }
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 b67b4d64c1d..6c7164c5e06 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
@@ -126,7 +126,7 @@ RSpec.describe Projects::LfsPointers::LfsDownloadService do
let(:redirect_link) { 'http://external-link' }
before do
- stub_full_request(download_link).to_return(status: 301, body: 'You are being redirected', headers: { 'Location' => redirect_link } )
+ stub_full_request(download_link).to_return(status: 301, body: 'You are being redirected', headers: { 'Location' => redirect_link })
stub_full_request(redirect_link).to_return(body: lfs_content)
end
diff --git a/spec/services/projects/move_users_star_projects_service_spec.rb b/spec/services/projects/move_users_star_projects_service_spec.rb
index 0f766ebd0ec..b580d3d8772 100644
--- a/spec/services/projects/move_users_star_projects_service_spec.rb
+++ b/spec/services/projects/move_users_star_projects_service_spec.rb
@@ -15,6 +15,9 @@ RSpec.describe Projects::MoveUsersStarProjectsService do
end
it 'moves the user\'s stars from one project to another' do
+ project_with_stars.reload
+ target_project.reload
+
expect(project_with_stars.users_star_projects.count).to eq 2
expect(project_with_stars.star_count).to eq 2
expect(target_project.users_star_projects.count).to eq 0
@@ -34,6 +37,8 @@ RSpec.describe Projects::MoveUsersStarProjectsService do
allow(subject).to receive(:success).and_raise(StandardError)
expect { subject.execute(project_with_stars) }.to raise_error(StandardError)
+ project_with_stars.reload
+ target_project.reload
expect(project_with_stars.users_star_projects.count).to eq 2
expect(project_with_stars.star_count).to eq 2
diff --git a/spec/services/projects/prometheus/alerts/notify_service_spec.rb b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
index 7bf6dfd0fd8..43d23023d83 100644
--- a/spec/services/projects/prometheus/alerts/notify_service_spec.rb
+++ b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
@@ -244,9 +244,10 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
end
shared_examples 'process truncated alerts' do
- it 'returns 200 but skips processing and logs a warning', :aggregate_failures do
+ it 'returns 201 but skips processing and logs a warning', :aggregate_failures do
expect(subject).to be_success
- expect(subject.payload[:alerts].size).to eq(max_alerts)
+ expect(subject.payload).to eq({})
+ expect(subject.http_status).to eq(:created)
expect(Gitlab::AppLogger)
.to have_received(:warn)
.with(
@@ -260,9 +261,10 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
end
shared_examples 'process all alerts' do
- it 'returns 200 and process alerts without warnings', :aggregate_failures do
+ it 'returns 201 and process alerts without warnings', :aggregate_failures do
expect(subject).to be_success
- expect(subject.payload[:alerts].size).to eq(2)
+ expect(subject.payload).to eq({})
+ expect(subject.http_status).to eq(:created)
expect(Gitlab::AppLogger).not_to have_received(:warn)
end
end
diff --git a/spec/services/protected_branches/api_service_spec.rb b/spec/services/protected_branches/api_service_spec.rb
new file mode 100644
index 00000000000..94484f5a7b9
--- /dev/null
+++ b/spec/services/protected_branches/api_service_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ProtectedBranches::ApiService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user, maintainer_projects: [project]) }
+
+ it 'creates a protected branch with prefilled defaults' do
+ expect(::ProtectedBranches::CreateService).to receive(:new).with(
+ project, user, hash_including(
+ push_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }],
+ merge_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }]
+ )
+ ).and_call_original
+
+ expect(described_class.new(project, user, { name: 'new name' }).create).to be_valid
+ end
+
+ it 'updates a protected branch without prefilled defaults' do
+ protected_branch = create(:protected_branch, project: project, allow_force_push: true)
+
+ expect(::ProtectedBranches::UpdateService).to receive(:new).with(
+ project, user, hash_including(
+ push_access_levels_attributes: [],
+ merge_access_levels_attributes: []
+ )
+ ).and_call_original
+
+ expect do
+ expect(described_class.new(project, user, { name: 'new name' }).update(protected_branch)).to be_valid
+ end.not_to change { protected_branch.reload.allow_force_push }
+ end
+end
diff --git a/spec/services/protected_branches/cache_service_spec.rb b/spec/services/protected_branches/cache_service_spec.rb
index 00d1e8b5457..d7a3258160b 100644
--- a/spec/services/protected_branches/cache_service_spec.rb
+++ b/spec/services/protected_branches/cache_service_spec.rb
@@ -111,5 +111,16 @@ RSpec.describe ProtectedBranches::CacheService, :clean_gitlab_redis_cache do
expect(service.fetch('not-found') { true }).to eq(true)
end
end
+
+ describe 'metrics' do
+ it 'records hit ratio metrics' do
+ expect_next_instance_of(Gitlab::Cache::Metrics) do |metrics|
+ expect(metrics).to receive(:increment_cache_miss).once
+ expect(metrics).to receive(:increment_cache_hit).exactly(4).times
+ end
+
+ 5.times { service.fetch('main') { true } }
+ end
+ end
end
# rubocop:enable Style/RedundantFetchBlock
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index a43f3bc55bf..f9c16c84121 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -1450,6 +1450,11 @@ RSpec.describe QuickActions::InterpretService do
let(:issuable) { issue }
end
+ it_behaves_like 'estimate command' do
+ let(:content) { '/estimate_time 1h' }
+ let(:issuable) { issue }
+ end
+
it_behaves_like 'failed command' do
let(:content) { '/estimate' }
let(:issuable) { issue }
@@ -1470,6 +1475,11 @@ RSpec.describe QuickActions::InterpretService do
let(:issuable) { issue }
end
+ it_behaves_like 'spend command' do
+ let(:content) { '/spend_time 1h' }
+ let(:issuable) { issue }
+ end
+
it_behaves_like 'spend command with negative time' do
let(:content) { '/spend -120m' }
let(:issuable) { issue }
@@ -1537,6 +1547,11 @@ RSpec.describe QuickActions::InterpretService do
let(:issuable) { issue }
end
+ it_behaves_like 'remove_estimate command' do
+ let(:content) { '/remove_time_estimate' }
+ let(:issuable) { issue }
+ end
+
it_behaves_like 'remove_time_spent command' do
let(:content) { '/remove_time_spent' }
let(:issuable) { issue }
diff --git a/spec/services/resource_access_tokens/revoke_service_spec.rb b/spec/services/resource_access_tokens/revoke_service_spec.rb
index 8f89696cc55..28f173f1bc7 100644
--- a/spec/services/resource_access_tokens/revoke_service_spec.rb
+++ b/spec/services/resource_access_tokens/revoke_service_spec.rb
@@ -29,35 +29,13 @@ RSpec.describe ResourceAccessTokens::RevokeService do
expect(resource.reload.users).not_to include(resource_bot)
end
- context 'when user_destroy_with_limited_execution_time_worker is enabled' do
- it 'initiates user removal' do
- subject
-
- expect(
- Users::GhostUserMigration.where(user: resource_bot,
- initiator_user: user)
- ).to be_exists
- end
- end
-
- context 'when user_destroy_with_limited_execution_time_worker is disabled' do
- before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
- end
-
- it 'transfer issuables of bot user to ghost user' do
- issue = create(:issue, author: resource_bot)
-
- subject
-
- expect(issue.reload.author.ghost?).to be true
- end
-
- it 'deletes project bot user' do
- subject
+ it 'initiates user removal' do
+ subject
- expect(User.exists?(resource_bot.id)).to be_falsy
- end
+ expect(
+ Users::GhostUserMigration.where(user: resource_bot,
+ initiator_user: user)
+ ).to be_exists
end
it 'logs the event' do
diff --git a/spec/services/resource_events/change_milestone_service_spec.rb b/spec/services/resource_events/change_milestone_service_spec.rb
index ed234376381..425d5b19907 100644
--- a/spec/services/resource_events/change_milestone_service_spec.rb
+++ b/spec/services/resource_events/change_milestone_service_spec.rb
@@ -14,4 +14,35 @@ RSpec.describe ResourceEvents::ChangeMilestoneService do
let_it_be(:resource) { create(issuable) } # rubocop:disable Rails/SaveBang
end
end
+
+ describe 'events tracking' do
+ let_it_be(:user) { create(:user) }
+
+ let(:resource) { create(resource_type, milestone: timebox, project: timebox.project) }
+
+ subject(:service_instance) { described_class.new(resource, user, old_milestone: nil) }
+
+ context 'when the resource is a work item' do
+ let(:resource_type) { :work_item }
+
+ it 'tracks work item usage data counters' do
+ expect(Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter)
+ .to receive(:track_work_item_milestone_changed_action)
+ .with(author: user)
+
+ service_instance.execute
+ end
+ end
+
+ context 'when the resource is not a work item' do
+ let(:resource_type) { :issue }
+
+ it 'does not track work item usage data counters' do
+ expect(Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter)
+ .not_to receive(:track_work_item_milestone_changed_action)
+
+ service_instance.execute
+ end
+ end
+ end
end
diff --git a/spec/services/resource_events/change_state_service_spec.rb b/spec/services/resource_events/change_state_service_spec.rb
index 255ee9eca57..b679943073c 100644
--- a/spec/services/resource_events/change_state_service_spec.rb
+++ b/spec/services/resource_events/change_state_service_spec.rb
@@ -18,10 +18,11 @@ RSpec.describe ResourceEvents::ChangeStateService do
event = resource.resource_state_events.last
- if resource.is_a?(Issue)
+ case resource
+ when Issue
expect(event.issue).to eq(resource)
expect(event.merge_request).to be_nil
- elsif resource.is_a?(MergeRequest)
+ when MergeRequest
expect(event.issue).to be_nil
expect(event.merge_request).to eq(resource)
end
@@ -91,10 +92,11 @@ RSpec.describe ResourceEvents::ChangeStateService do
end
def expect_event_source(event, source)
- if source.is_a?(MergeRequest)
+ case source
+ when MergeRequest
expect(event.source_commit).to be_nil
expect(event.source_merge_request).to eq(source)
- elsif source.is_a?(Commit)
+ when Commit
expect(event.source_commit).to eq(source.id)
expect(event.source_merge_request).to be_nil
else
diff --git a/spec/services/search/group_service_spec.rb b/spec/services/search/group_service_spec.rb
index 152d0700cc1..c9bfa7cb7b4 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/search_service_spec.rb b/spec/services/search_service_spec.rb
index 5edea13afa4..26def474b88 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -399,159 +399,7 @@ RSpec.describe SearchService do
end
end
- context 'redacting search results' do
- let(:search) { 'anything' }
-
- subject(:result) { search_service.search_objects }
-
- shared_examples "redaction limits N+1 queries" do |limit:|
- it 'does not exceed the query limit' do
- # issuing the query to remove the data loading call
- unredacted_results.to_a
-
- # only the calls from the redaction are left
- query = ActiveRecord::QueryRecorder.new { result }
-
- # these are the project authorization calls, which are not preloaded
- expect(query.count).to be <= limit
- end
- end
-
- def found_blob(project)
- Gitlab::Search::FoundBlob.new(project: project)
- end
-
- def found_wiki_page(project)
- Gitlab::Search::FoundWikiPage.new(found_blob(project))
- end
-
- before do
- expect(search_service)
- .to receive(:search_results)
- .and_return(double('search results', objects: unredacted_results))
- end
-
- def ar_relation(klass, *objects)
- klass.id_in(objects.map(&:id))
- end
-
- def kaminari_array(*objects)
- Kaminari.paginate_array(objects).page(1).per(20)
- end
-
- context 'issues' do
- let(:readable) { create(:issue, project: accessible_project) }
- let(:unreadable) { create(:issue, project: inaccessible_project) }
- let(:unredacted_results) { ar_relation(Issue, readable, unreadable) }
- let(:scope) { 'issues' }
-
- it 'redacts the inaccessible issue' do
- expect(result).to contain_exactly(readable)
- end
- end
-
- context 'notes' do
- let(:readable) { create(:note_on_commit, project: accessible_project) }
- let(:unreadable) { create(:note_on_commit, project: inaccessible_project) }
- let(:unredacted_results) { ar_relation(Note, readable, unreadable) }
- let(:scope) { 'notes' }
-
- it 'redacts the inaccessible note' do
- expect(result).to contain_exactly(readable)
- end
- end
-
- context 'merge_requests' do
- let(:readable) { create(:merge_request, source_project: accessible_project, author: user) }
- let(:unreadable) { create(:merge_request, source_project: inaccessible_project) }
- let(:unredacted_results) { ar_relation(MergeRequest, readable, unreadable) }
- let(:scope) { 'merge_requests' }
-
- it 'redacts the inaccessible merge request' do
- expect(result).to contain_exactly(readable)
- end
-
- context 'with :with_api_entity_associations' do
- let(:unredacted_results) { ar_relation(MergeRequest.with_api_entity_associations, readable, unreadable) }
-
- it_behaves_like "redaction limits N+1 queries", limit: 8
- end
- end
-
- context 'project repository blobs' do
- let(:readable) { found_blob(accessible_project) }
- let(:unreadable) { found_blob(inaccessible_project) }
- let(:unredacted_results) { kaminari_array(readable, unreadable) }
- let(:scope) { 'blobs' }
-
- it 'redacts the inaccessible blob' do
- expect(result).to contain_exactly(readable)
- end
- end
-
- context 'project wiki blobs' do
- let(:readable) { found_wiki_page(accessible_project) }
- let(:unreadable) { found_wiki_page(inaccessible_project) }
- let(:unredacted_results) { kaminari_array(readable, unreadable) }
- let(:scope) { 'wiki_blobs' }
-
- it 'redacts the inaccessible blob' do
- expect(result).to contain_exactly(readable)
- end
- end
-
- context 'project snippets' do
- let(:readable) { create(:project_snippet, project: accessible_project) }
- let(:unreadable) { create(:project_snippet, project: inaccessible_project) }
- let(:unredacted_results) { ar_relation(ProjectSnippet, readable, unreadable) }
- let(:scope) { 'snippet_titles' }
-
- it 'redacts the inaccessible snippet' do
- expect(result).to contain_exactly(readable)
- end
-
- context 'with :with_api_entity_associations' do
- it_behaves_like "redaction limits N+1 queries", limit: 14
- end
- end
-
- context 'personal snippets' do
- let(:readable) { create(:personal_snippet, :private, author: user) }
- let(:unreadable) { create(:personal_snippet, :private) }
- let(:unredacted_results) { ar_relation(PersonalSnippet, readable, unreadable) }
- let(:scope) { 'snippet_titles' }
-
- it 'redacts the inaccessible snippet' do
- expect(result).to contain_exactly(readable)
- end
-
- context 'with :with_api_entity_associations' do
- it_behaves_like "redaction limits N+1 queries", limit: 4
- end
- end
-
- context 'commits' do
- let(:readable) { accessible_project.commit }
- let(:unreadable) { inaccessible_project.commit }
- let(:unredacted_results) { kaminari_array(readable, unreadable) }
- let(:scope) { 'commits' }
-
- it 'redacts the inaccessible commit' do
- expect(result).to contain_exactly(readable)
- end
- end
-
- context 'users' do
- let(:other_user) { create(:user) }
- let(:unredacted_results) { ar_relation(User, user, other_user) }
- let(:scope) { 'users' }
-
- it 'passes the users through' do
- # Users are always visible to everyone
- expect(result).to contain_exactly(user, other_user)
- end
- end
- end
+ it_behaves_like 'a redacted search results'
end
describe '#valid_request?' do
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 7a004e2915c..9211beb76f8 100644
--- a/spec/services/security/ci_configuration/sast_parser_service_spec.rb
+++ b/spec/services/security/ci_configuration/sast_parser_service_spec.rb
@@ -11,9 +11,9 @@ RSpec.describe Security::CiConfiguration::SastParserService do
let(:sast_excluded_paths) { configuration['global'][1] }
let(:sast_pipeline_stage) { configuration['pipeline'][0] }
let(:sast_search_max_depth) { configuration['pipeline'][1] }
- let(:bandit) { configuration['analyzers'][0] }
- let(:brakeman) { configuration['analyzers'][1] }
+ let(:brakeman) { configuration['analyzers'][0] }
let(:sast_brakeman_level) { brakeman['variables'][0] }
+ let(:semgrep) { configuration['analyzers'][1] }
let(:secure_analyzers_prefix) { '$CI_TEMPLATE_REGISTRY_HOST/security-products' }
it 'parses the configuration for SAST' do
@@ -34,7 +34,7 @@ RSpec.describe Security::CiConfiguration::SastParserService do
expect(sast_pipeline_stage['value']).to eql('our_custom_security_stage')
expect(sast_search_max_depth['value']).to eql('8')
expect(brakeman['enabled']).to be(false)
- expect(bandit['enabled']).to be(true)
+ expect(semgrep['enabled']).to be(true)
expect(sast_brakeman_level['value']).to eql('2')
end
@@ -43,7 +43,7 @@ RSpec.describe Security::CiConfiguration::SastParserService do
allow(project.repository).to receive(:blob_data_at).and_return(gitlab_ci_yml_excluded_analyzers_content)
expect(brakeman['enabled']).to be(false)
- expect(bandit['enabled']).to be(true)
+ expect(semgrep['enabled']).to be(true)
end
end
end
diff --git a/spec/services/security/merge_reports_service_spec.rb b/spec/services/security/merge_reports_service_spec.rb
index e61977297c5..8415ed8a22f 100644
--- a/spec/services/security/merge_reports_service_spec.rb
+++ b/spec/services/security/merge_reports_service_spec.rb
@@ -219,10 +219,10 @@ RSpec.describe Security::MergeReportsService, '#execute' do
let(:finding_id_1) { build(:ci_reports_security_finding, identifiers: [identifier_bandit, identifier_cve], scanner: bandit_scanner, report_type: :sast) }
let(:finding_id_2) { build(:ci_reports_security_finding, identifiers: [identifier_cve], scanner: semgrep_scanner, report_type: :sast) }
- let(:finding_id_3) { build(:ci_reports_security_finding, identifiers: [identifier_semgrep], scanner: semgrep_scanner, report_type: :sast ) }
+ let(:finding_id_3) { build(:ci_reports_security_finding, identifiers: [identifier_semgrep], scanner: semgrep_scanner, report_type: :sast) }
let(:bandit_report) do
- build( :ci_reports_security_report,
+ build(:ci_reports_security_report,
type: :sast,
scanners: [bandit_scanner],
findings: [finding_id_1],
diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb
index b2ccd9dba52..3263e410d3c 100644
--- a/spec/services/system_notes/issuables_service_spec.rb
+++ b/spec/services/system_notes/issuables_service_spec.rb
@@ -175,7 +175,7 @@ RSpec.describe ::SystemNotes::IssuablesService do
it 'builds a correct phrase when one reviewer removed from a set' do
expect(build_note([reviewer, reviewer1, reviewer2], [reviewer, reviewer1])).to(
- eq( "removed review request for @#{reviewer2.username}")
+ eq("removed review request for @#{reviewer2.username}")
)
end
@@ -681,7 +681,7 @@ RSpec.describe ::SystemNotes::IssuablesService do
it 'tracks usage' do
expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter)
- .to receive(:track_issue_cloned_action).with(author: author, project: project )
+ .to receive(:track_issue_cloned_action).with(author: author, project: project)
subject
end
diff --git a/spec/services/tags/create_service_spec.rb b/spec/services/tags/create_service_spec.rb
index b1c6623308e..bbf6fe62959 100644
--- a/spec/services/tags/create_service_spec.rb
+++ b/spec/services/tags/create_service_spec.rb
@@ -27,6 +27,26 @@ RSpec.describe Tags::CreateService do
end
end
+ context 'when tag_name is empty' do
+ it 'returns an error' do
+ response = service.execute('', 'foo', 'Foo')
+
+ expect(response[:status]).to eq(:error)
+ expect(response[:http_status]).to eq(400)
+ expect(response[:message]).to eq('Tag name invalid')
+ end
+ end
+
+ context 'when target is empty' do
+ it 'returns an error' do
+ response = service.execute('v1.1.0', '', 'Foo')
+
+ expect(response[:status]).to eq(:error)
+ expect(response[:http_status]).to eq(400)
+ expect(response[:message]).to eq('Target is empty')
+ end
+ end
+
context 'when tag already exists' do
it 'returns an error' do
expect(repository).to receive(:add_tag)
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 45a8268043f..774a6ddcfb3 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -1037,7 +1037,7 @@ RSpec.describe TodoService do
let_it_be(:noteable) { create(:issue, project: project) }
let(:note) { create(:note, project: project, note: mentions, noteable: noteable) }
- let(:addressed_note) { create(:note, project: project, note: "#{directly_addressed}", noteable: noteable) }
+ let(:addressed_note) { create(:note, project: project, note: directly_addressed.to_s, noteable: noteable) }
it 'creates a todo for each valid mentioned user not included in skip_users' do
service.update_note(note, author, skip_users)
diff --git a/spec/services/todos/destroy/entity_leave_service_spec.rb b/spec/services/todos/destroy/entity_leave_service_spec.rb
index 225e7933d79..9d5ed70e9ef 100644
--- a/spec/services/todos/destroy/entity_leave_service_spec.rb
+++ b/spec/services/todos/destroy/entity_leave_service_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
let!(:todo_issue_user) { create(:todo, user: user, target: issue, project: project) }
let!(:todo_issue_c_user) { create(:todo, user: user, target: issue_c, project: project) }
let!(:todo_issue_c_user2) { create(:todo, user: user2, target: issue_c, project: project) }
- let(:internal_note) { create(:note, noteable: issue, project: project, confidential: true ) }
+ let(:internal_note) { create(:note, noteable: issue, project: project, confidential: true) }
let!(:todo_for_internal_note) do
create(:todo, user: user, target: issue, project: project, note: internal_note)
end
@@ -28,7 +28,7 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
set_access(group, user, group_access) if group_access
end
- it "#{params[:method].to_s.humanize(capitalize: false)}" do
+ it params[:method].to_s.humanize(capitalize: false) do
send(method_name)
end
end
@@ -250,7 +250,7 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
let!(:todo_subproject_user2) { create(:todo, user: user2, project: subproject) }
let!(:todo_subpgroup_user2) { create(:todo, user: user2, group: subgroup) }
let!(:todo_parent_group_user) { create(:todo, user: user, group: parent_group) }
- let(:subproject_internal_note) { create(:note, noteable: issue, project: project, confidential: true ) }
+ let(:subproject_internal_note) { create(:note, noteable: issue, project: project, confidential: true) }
let!(:todo_for_internal_subproject_note) do
create(:todo, user: user, target: issue, project: project, note: subproject_internal_note)
end
diff --git a/spec/services/topics/merge_service_spec.rb b/spec/services/topics/merge_service_spec.rb
index eef31817aa8..98247250a61 100644
--- a/spec/services/topics/merge_service_spec.rb
+++ b/spec/services/topics/merge_service_spec.rb
@@ -5,10 +5,10 @@ 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] ) }
+ 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 }
diff --git a/spec/services/users/approve_service_spec.rb b/spec/services/users/approve_service_spec.rb
index 078dde546c9..34eb5b18ff6 100644
--- a/spec/services/users/approve_service_spec.rb
+++ b/spec/services/users/approve_service_spec.rb
@@ -67,7 +67,7 @@ RSpec.describe Users::ApproveService do
subject
- expect(Gitlab::AppLogger).to have_received(:info).with(message: "User instance access request approved", user: "#{user.username}", email: "#{user.email}", approved_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}")
+ expect(Gitlab::AppLogger).to have_received(:info).with(message: "User instance access request approved", user: user.username.to_s, email: user.email.to_s, approved_by: current_user.username.to_s, ip_address: current_user.current_sign_in_ip.to_s)
end
it 'emails the user on approval' do
diff --git a/spec/services/users/ban_service_spec.rb b/spec/services/users/ban_service_spec.rb
index 79f3cbeb46d..3f9c7ebf067 100644
--- a/spec/services/users/ban_service_spec.rb
+++ b/spec/services/users/ban_service_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe Users::BanService do
end
it 'logs ban in application logs' do
- expect(Gitlab::AppLogger).to receive(:info).with(message: "User ban", user: "#{user.username}", email: "#{user.email}", ban_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}")
+ expect(Gitlab::AppLogger).to receive(:info).with(message: "User ban", user: user.username.to_s, email: user.email.to_s, ban_by: current_user.username.to_s, ip_address: current_user.current_sign_in_ip.to_s)
ban_user
end
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 03e1811c8a5..18ad946b289 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -10,546 +10,250 @@ RSpec.describe Users::DestroyService do
let(:service) { described_class.new(admin) }
let(:gitlab_shell) { Gitlab::Shell.new }
- shared_examples 'pre-migrate clean-up' do
- describe "Deletes a user and all their personal projects", :enable_admin_mode do
- context 'no options are given' do
- it 'will delete the personal project' do
- expect_next_instance_of(Projects::DestroyService) do |destroy_service|
- expect(destroy_service).to receive(:execute).once.and_return(true)
- end
-
- service.execute(user)
- end
+ describe "Initiates user deletion and deletes all their personal projects", :enable_admin_mode do
+ context 'no options are given' do
+ it 'creates GhostUserMigration record to handle migration in a worker' do
+ expect { service.execute(user) }
+ .to(
+ change do
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: admin)
+ .exists?
+ end.from(false).to(true))
end
- context 'personal projects in pending_delete' do
- before do
- project.pending_delete = true
- project.save!
+ it 'will delete the personal project' do
+ expect_next_instance_of(Projects::DestroyService) do |destroy_service|
+ expect(destroy_service).to receive(:execute).once.and_return(true)
end
- it 'destroys a personal project in pending_delete' do
- expect_next_instance_of(Projects::DestroyService) do |destroy_service|
- expect(destroy_service).to receive(:execute).once.and_return(true)
- end
-
- service.execute(user)
- end
+ service.execute(user)
end
+ end
- context "solo owned groups present" do
- let(:solo_owned) { create(:group) }
- let(:member) { create(:group_member) }
- let(:user) { member.user }
-
- before do
- solo_owned.group_members = [member]
- end
-
- it 'returns the user with attached errors' do
- expect(service.execute(user)).to be(user)
- expect(user.errors.full_messages).to(
- contain_exactly('You must transfer ownership or delete groups before you can remove user'))
- end
-
- it 'does not delete the user, nor the group' do
- service.execute(user)
-
- expect(User.find(user.id)).to eq user
- expect(Group.find(solo_owned.id)).to eq solo_owned
- end
+ context 'personal projects in pending_delete' do
+ before do
+ project.pending_delete = true
+ project.save!
end
- context "deletions with solo owned groups" do
- let(:solo_owned) { create(:group) }
- let(:member) { create(:group_member) }
- let(:user) { member.user }
-
- before do
- solo_owned.group_members = [member]
- service.execute(user, delete_solo_owned_groups: true)
+ it 'destroys a personal project in pending_delete' do
+ expect_next_instance_of(Projects::DestroyService) do |destroy_service|
+ expect(destroy_service).to receive(:execute).once.and_return(true)
end
- it 'deletes solo owned groups' do
- expect { Group.find(solo_owned.id) }.to raise_error(ActiveRecord::RecordNotFound)
- end
+ service.execute(user)
end
+ end
- context 'deletions with inherited group owners' do
- let(:group) { create(:group, :nested) }
- let(:user) { create(:user) }
- let(:inherited_owner) { create(:user) }
-
- before do
- group.parent.add_owner(inherited_owner)
- group.add_owner(user)
+ context "solo owned groups present" do
+ let(:solo_owned) { create(:group) }
+ let(:member) { create(:group_member) }
+ let(:user) { member.user }
- service.execute(user, delete_solo_owned_groups: true)
- end
-
- it 'does not delete the group' do
- expect(Group.exists?(id: group)).to be_truthy
- end
+ before do
+ solo_owned.group_members = [member]
end
- describe "user personal's repository removal" do
- context 'storages' do
- before do
- perform_enqueued_jobs { service.execute(user) }
- end
-
- context 'legacy storage' do
- let!(:project) { create(:project, :empty_repo, :legacy_storage, namespace: user.namespace) }
-
- it 'removes repository' do
- expect(
- gitlab_shell.repository_exists?(project.repository_storage,
- "#{project.disk_path}.git")
- ).to be_falsey
- end
- end
-
- context 'hashed storage' do
- let!(:project) { create(:project, :empty_repo, namespace: user.namespace) }
-
- it 'removes repository' do
- expect(
- gitlab_shell.repository_exists?(project.repository_storage,
- "#{project.disk_path}.git")
- ).to be_falsey
- end
- end
- end
-
- context 'repository removal status is taken into account' do
- it 'raises exception' do
- expect_next_instance_of(::Projects::DestroyService) do |destroy_service|
- expect(destroy_service).to receive(:execute).and_return(false)
- end
-
- expect { service.execute(user) }
- .to raise_error(Users::DestroyService::DestroyError,
- "Project #{project.id} can't be deleted" )
- end
- end
+ it 'returns the user with attached errors' do
+ expect(service.execute(user)).to be(user)
+ expect(user.errors.full_messages).to(
+ contain_exactly('You must transfer ownership or delete groups before you can remove user'))
end
- describe "calls the before/after callbacks" do
- it 'of project_members' do
- expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:find).once
- expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:initialize).once
- expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:destroy).once
-
- service.execute(user)
- end
-
- it 'of group_members' do
- group_member = create(:group_member)
- group_member.group.group_members.create!(user: user, access_level: 40)
+ it 'does not delete the user, nor the group' do
+ service.execute(user)
- expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:find).once
- expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:initialize).once
- expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:destroy).once
-
- service.execute(user)
- end
+ expect(User.find(user.id)).to eq user
+ expect(Group.find(solo_owned.id)).to eq solo_owned
end
end
- end
-
- context 'when user_destroy_with_limited_execution_time_worker is disabled' do
- before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
- end
-
- include_examples 'pre-migrate clean-up'
-
- describe "Deletes a user and all their personal projects", :enable_admin_mode do
- context 'no options are given' do
- it 'deletes the user' do
- user_data = service.execute(user)
- expect(user_data['email']).to eq(user.email)
- expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
- expect { Namespace.find(namespace.id) }.to raise_error(ActiveRecord::RecordNotFound)
- end
-
- it 'deletes user associations in batches' do
- expect(user).to receive(:destroy_dependent_associations_in_batches)
+ context "deletions with solo owned groups" do
+ let(:solo_owned) { create(:group) }
+ let(:member) { create(:group_member) }
+ let(:user) { member.user }
- service.execute(user)
- end
-
- it 'does not include snippets when deleting in batches' do
- expect(user).to receive(:destroy_dependent_associations_in_batches).with({ exclude: [:snippets] })
-
- service.execute(user)
- end
-
- it 'calls the bulk snippet destroy service for the user personal snippets' do
- repo1 = create(:personal_snippet, :repository, author: user).snippet_repository
- repo2 = create(:project_snippet, :repository, project: project, author: user).snippet_repository
-
- aggregate_failures do
- expect(gitlab_shell.repository_exists?(repo1.shard_name, repo1.disk_path + '.git')).to be_truthy
- expect(gitlab_shell.repository_exists?(repo2.shard_name, repo2.disk_path + '.git')).to be_truthy
- end
-
- # Call made when destroying user personal projects
- expect(Snippets::BulkDestroyService).to receive(:new)
- .with(admin, project.snippets).and_call_original
-
- # Call to remove user personal snippets and for
- # project snippets where projects are not user personal
- # ones
- expect(Snippets::BulkDestroyService).to receive(:new)
- .with(admin, user.snippets.only_personal_snippets).and_call_original
-
- service.execute(user)
-
- aggregate_failures do
- expect(gitlab_shell.repository_exists?(repo1.shard_name, repo1.disk_path + '.git')).to be_falsey
- expect(gitlab_shell.repository_exists?(repo2.shard_name, repo2.disk_path + '.git')).to be_falsey
- end
- end
-
- it 'calls the bulk snippet destroy service with hard delete option if it is present' do
- # this avoids getting into Projects::DestroyService as it would
- # call Snippets::BulkDestroyService first!
- allow(user).to receive(:personal_projects).and_return([])
-
- expect_next_instance_of(Snippets::BulkDestroyService) do |bulk_destroy_service|
- expect(bulk_destroy_service).to receive(:execute).with({ skip_authorization: true }).and_call_original
- end
-
- service.execute(user, { hard_delete: true })
- end
-
- it 'does not delete project snippets that the user is the author of' do
- repo = create(:project_snippet, :repository, author: user).snippet_repository
- service.execute(user)
- expect(gitlab_shell.repository_exists?(repo.shard_name, repo.disk_path + '.git')).to be_truthy
- expect(User.ghost.snippets).to include(repo.snippet)
- end
-
- context 'when an error is raised deleting snippets' do
- it 'does not delete user' do
- snippet = create(:personal_snippet, :repository, author: user)
-
- bulk_service = double
- allow(Snippets::BulkDestroyService).to receive(:new).and_call_original
- allow(Snippets::BulkDestroyService).to receive(:new).with(admin, user.snippets).and_return(bulk_service)
- allow(bulk_service).to receive(:execute).and_return(ServiceResponse.error(message: 'foo'))
-
- aggregate_failures do
- expect { service.execute(user) }
- .to raise_error(Users::DestroyService::DestroyError, 'foo' )
- expect(snippet.reload).not_to be_nil
- expect(
- gitlab_shell.repository_exists?(snippet.repository_storage,
- snippet.disk_path + '.git')
- ).to be_truthy
- end
- end
- end
+ before do
+ solo_owned.group_members = [member]
+ service.execute(user, delete_solo_owned_groups: true)
end
- context 'projects in pending_delete' do
- before do
- project.pending_delete = true
- project.save!
- end
-
- it 'destroys a project in pending_delete' do
- expect_next_instance_of(Projects::DestroyService) do |destroy_service|
- expect(destroy_service).to receive(:execute).once.and_return(true)
- end
-
- service.execute(user)
-
- expect { Project.find(project.id) }.to raise_error(ActiveRecord::RecordNotFound)
- end
+ it 'deletes solo owned groups' do
+ expect { Group.find(solo_owned.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
+ end
- context "a deleted user's issues" do
- let(:project) { create(:project) }
-
- before do
- project.add_developer(user)
- end
-
- context "for an issue the user was assigned to" do
- let!(:issue) { create(:issue, project: project, assignees: [user]) }
-
- before do
- service.execute(user)
- end
+ context 'deletions with inherited group owners' do
+ let(:group) { create(:group, :nested) }
+ let(:user) { create(:user) }
+ let(:inherited_owner) { create(:user) }
- it 'does not delete issues the user is assigned to' do
- expect(Issue.find_by_id(issue.id)).to be_present
- end
+ before do
+ group.parent.add_owner(inherited_owner)
+ group.add_owner(user)
- it 'migrates the issue so that it is "Unassigned"' do
- migrated_issue = Issue.find_by_id(issue.id)
-
- expect(migrated_issue.assignees).to be_empty
- end
- end
+ service.execute(user, delete_solo_owned_groups: true)
end
- context "a deleted user's merge_requests" do
- let(:project) { create(:project, :repository) }
+ it 'does not delete the group' do
+ expect(Group.exists?(id: group)).to be_truthy
+ end
+ end
+ describe "user personal's repository removal" do
+ context 'storages' do
before do
- project.add_developer(user)
+ perform_enqueued_jobs { service.execute(user) }
end
- context "for an merge request the user was assigned to" do
- let!(:merge_request) { create(:merge_request, source_project: project, assignees: [user]) }
+ context 'legacy storage' do
+ let!(:project) { create(:project, :empty_repo, :legacy_storage, namespace: user.namespace) }
- before do
- service.execute(user)
- end
-
- it 'does not delete merge requests the user is assigned to' do
- expect(MergeRequest.find_by_id(merge_request.id)).to be_present
- end
-
- it 'migrates the merge request so that it is "Unassigned"' do
- migrated_merge_request = MergeRequest.find_by_id(merge_request.id)
-
- expect(migrated_merge_request.assignees).to be_empty
+ it 'removes repository' do
+ expect(
+ gitlab_shell.repository_exists?(project.repository_storage,
+ "#{project.disk_path}.git")
+ ).to be_falsey
end
end
- end
-
- context 'migrating associated records' do
- let!(:issue) { create(:issue, author: user) }
- it 'delegates to the `MigrateToGhostUser` service to move associated records to the ghost user' do
- expect_any_instance_of(Users::MigrateToGhostUserService).to receive(:execute).once.and_call_original
+ context 'hashed storage' do
+ let!(:project) { create(:project, :empty_repo, namespace: user.namespace) }
- service.execute(user)
-
- expect(issue.reload.author).to be_ghost
- end
-
- context 'when hard_delete option is given' do
- it 'will not ghost certain records' do
- expect_any_instance_of(Users::MigrateToGhostUserService).to receive(:execute).once.and_call_original
-
- service.execute(user, hard_delete: true)
-
- expect(Issue.exists?(issue.id)).to be_falsy
+ it 'removes repository' do
+ expect(
+ gitlab_shell.repository_exists?(project.repository_storage,
+ "#{project.disk_path}.git")
+ ).to be_falsey
end
end
end
- end
-
- describe "Deletion permission checks" do
- it 'does not delete the user when user is not an admin' do
- other_user = create(:user)
-
- expect { described_class.new(other_user).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
- expect(User.exists?(user.id)).to be(true)
- end
-
- context 'when admin mode is enabled', :enable_admin_mode do
- it 'allows admins to delete anyone' do
- described_class.new(admin).execute(user)
-
- expect(User.exists?(user.id)).to be(false)
- end
- end
- context 'when admin mode is disabled' do
- it 'disallows admins to delete anyone' do
- expect { described_class.new(admin).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ context 'repository removal status is taken into account' do
+ it 'raises exception' do
+ expect_next_instance_of(::Projects::DestroyService) do |destroy_service|
+ expect(destroy_service).to receive(:execute).and_return(false)
+ end
- expect(User.exists?(user.id)).to be(true)
+ expect { service.execute(user) }
+ .to raise_error(Users::DestroyService::DestroyError,
+ "Project #{project.id} can't be deleted")
end
end
-
- it 'allows users to delete their own account' do
- described_class.new(user).execute(user)
-
- expect(User.exists?(user.id)).to be(false)
- end
-
- it 'allows user to be deleted if skip_authorization: true' do
- other_user = create(:user)
-
- described_class.new(user).execute(other_user, skip_authorization: true)
-
- expect(User.exists?(other_user.id)).to be(false)
- end
end
- context 'batched nullify' do
- let(:other_user) { create(:user) }
+ describe "calls the before/after callbacks" do
+ it 'of project_members' do
+ expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:find).once
+ expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:initialize).once
+ expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:destroy).once
- # rubocop:disable Layout/LineLength
- def nullify_in_batches_regexp(table, column, user, batch_size: 100)
- %r{^UPDATE "#{table}" SET "#{column}" = NULL WHERE "#{table}"."id" IN \(SELECT "#{table}"."id" FROM "#{table}" WHERE "#{table}"."#{column}" = #{user.id} LIMIT #{batch_size}\)}
+ service.execute(user)
end
- def delete_in_batches_regexps(table, column, user, items, batch_size: 1000)
- select_query = %r{^SELECT "#{table}".* FROM "#{table}" WHERE "#{table}"."#{column}" = #{user.id}.*ORDER BY "#{table}"."id" ASC LIMIT #{batch_size}}
-
- [select_query] + items.map { |item| %r{^DELETE FROM "#{table}" WHERE "#{table}"."id" = #{item.id}} }
- end
- # rubocop:enable Layout/LineLength
+ it 'of group_members' do
+ group_member = create(:group_member)
+ group_member.group.group_members.create!(user: user, access_level: 40)
- it 'nullifies related associations in batches' do
- expect(other_user).to receive(:nullify_dependent_associations_in_batches).and_call_original
+ expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:find).once
+ expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:initialize).once
+ expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:destroy).once
- described_class.new(user).execute(other_user, skip_authorization: true)
+ service.execute(user)
end
+ end
- it 'nullifies issues and resource associations', :aggregate_failures do
- issue = create(:issue, closed_by: other_user, updated_by: other_user)
- resource_label_event = create(:resource_label_event, user: other_user)
- resource_state_event = create(:resource_state_event, user: other_user)
- todos = create_list(:todo, 2, project: issue.project, user: other_user, author: other_user, target: issue)
- event = create(:event, project: issue.project, author: other_user)
+ describe 'prometheus metrics', :prometheus do
+ context 'scheduled records' do
+ context 'with a single record' do
+ it 'updates the scheduled records gauge' do
+ service.execute(user)
- query_recorder = ActiveRecord::QueryRecorder.new do
- described_class.new(user).execute(other_user, skip_authorization: true)
+ gauge = Gitlab::Metrics.registry.get(:gitlab_ghost_user_migration_scheduled_records_total)
+ expect(gauge.get).to eq(1)
+ end
end
- issue.reload
- resource_label_event.reload
- resource_state_event.reload
-
- expect(issue.closed_by).to be_nil
- expect(issue.updated_by).to be_nil
- expect(resource_label_event.user).to be_nil
- expect(resource_state_event.user).to be_nil
- expect(other_user.authored_todos).to be_empty
- expect(other_user.todos).to be_empty
- expect(other_user.authored_events).to be_empty
-
- expected_queries = [
- nullify_in_batches_regexp(:issues, :updated_by_id, other_user),
- nullify_in_batches_regexp(:issues, :closed_by_id, other_user),
- nullify_in_batches_regexp(:resource_label_events, :user_id, other_user),
- nullify_in_batches_regexp(:resource_state_events, :user_id, other_user)
- ]
-
- expected_queries += delete_in_batches_regexps(:todos, :user_id, other_user, todos)
- expected_queries += delete_in_batches_regexps(:todos, :author_id, other_user, todos)
- expected_queries += delete_in_batches_regexps(:events, :author_id, other_user, [event])
-
- expect(query_recorder.log).to include(*expected_queries)
- end
+ context 'with approximate count due to large number of records' do
+ it 'updates the scheduled records gauge' do
+ allow(Users::GhostUserMigration)
+ .to(receive_message_chain(:limit, :count).and_return(1001))
+ allow(Users::GhostUserMigration).to(receive(:minimum)).and_return(42)
+ allow(Users::GhostUserMigration).to(receive(:maximum)).and_return(9042)
- it 'nullifies merge request associations', :aggregate_failures do
- merge_request = create(:merge_request, source_project: project, target_project: project,
- assignee: other_user, updated_by: other_user, merge_user: other_user)
- merge_request.metrics.update!(merged_by: other_user, latest_closed_by: other_user)
- merge_request.reviewers = [other_user]
- merge_request.assignees = [other_user]
+ service.execute(user)
- query_recorder = ActiveRecord::QueryRecorder.new do
- described_class.new(user).execute(other_user, skip_authorization: true)
+ gauge = Gitlab::Metrics.registry.get(:gitlab_ghost_user_migration_scheduled_records_total)
+ expect(gauge.get).to eq(9000)
+ end
end
-
- merge_request.reload
-
- expect(merge_request.updated_by).to be_nil
- expect(merge_request.assignee).to be_nil
- expect(merge_request.assignee_id).to be_nil
- expect(merge_request.metrics.merged_by).to be_nil
- expect(merge_request.metrics.latest_closed_by).to be_nil
- expect(merge_request.reviewers).to be_empty
- expect(merge_request.assignees).to be_empty
-
- expected_queries = [
- nullify_in_batches_regexp(:merge_requests, :updated_by_id, other_user),
- nullify_in_batches_regexp(:merge_requests, :assignee_id, other_user),
- nullify_in_batches_regexp(:merge_request_metrics, :merged_by_id, other_user),
- nullify_in_batches_regexp(:merge_request_metrics, :latest_closed_by_id, other_user)
- ]
-
- expected_queries += delete_in_batches_regexps(:merge_request_assignees, :user_id, other_user,
- merge_request.assignees)
- expected_queries += delete_in_batches_regexps(:merge_request_reviewers, :user_id, other_user,
- merge_request.reviewers)
-
- expect(query_recorder.log).to include(*expected_queries)
end
- end
- end
- context 'when user_destroy_with_limited_execution_time_worker is enabled' do
- include_examples 'pre-migrate clean-up'
+ context 'lag' do
+ it 'update the lag gauge', :freeze_time do
+ create(:ghost_user_migration, created_at: 10.minutes.ago)
- describe "Deletes a user and all their personal projects", :enable_admin_mode do
- context 'no options are given' do
- it 'creates GhostUserMigration record to handle migration in a worker' do
- expect { service.execute(user) }
- .to(
- change do
- Users::GhostUserMigration.where(user: user,
- initiator_user: admin)
- .exists?
- end.from(false).to(true))
+ service.execute(user)
+
+ gauge = Gitlab::Metrics.registry.get(:gitlab_ghost_user_migration_lag_seconds)
+ expect(gauge.get).to eq(600)
end
end
end
+ end
- describe "Deletion permission checks" do
- it 'does not delete the user when user is not an admin' do
- other_user = create(:user)
-
- expect { described_class.new(other_user).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
-
- expect(Users::GhostUserMigration).not_to be_exists
- end
-
- context 'when admin mode is enabled', :enable_admin_mode do
- it 'allows admins to delete anyone' do
- expect { described_class.new(admin).execute(user) }
- .to(
- change do
- Users::GhostUserMigration.where(user: user,
- initiator_user: admin)
- .exists?
- end.from(false).to(true))
- end
- end
+ describe "Deletion permission checks" do
+ it 'does not delete the user when user is not an admin' do
+ other_user = create(:user)
- context 'when admin mode is disabled' do
- it 'disallows admins to delete anyone' do
- expect { described_class.new(admin).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ expect { described_class.new(other_user).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
- expect(Users::GhostUserMigration).not_to be_exists
- end
- end
+ expect(Users::GhostUserMigration).not_to be_exists
+ end
- it 'allows users to delete their own account' do
- expect { described_class.new(user).execute(user) }
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it 'allows admins to delete anyone' do
+ expect { described_class.new(admin).execute(user) }
.to(
change do
Users::GhostUserMigration.where(user: user,
- initiator_user: user)
+ initiator_user: admin)
.exists?
end.from(false).to(true))
end
+ end
- it 'allows user to be deleted if skip_authorization: true' do
- other_user = create(:user)
+ context 'when admin mode is disabled' do
+ it 'disallows admins to delete anyone' do
+ expect { described_class.new(admin).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+
+ expect(Users::GhostUserMigration).not_to be_exists
+ end
+ end
- expect do
- described_class.new(user)
- .execute(other_user, skip_authorization: true)
- end.to(
+ it 'allows users to delete their own account' do
+ expect { described_class.new(user).execute(user) }
+ .to(
change do
- Users::GhostUserMigration.where(user: other_user,
- initiator_user: user )
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: user)
.exists?
end.from(false).to(true))
- end
+ end
+
+ it 'allows user to be deleted if skip_authorization: true' do
+ other_user = create(:user)
+
+ expect do
+ described_class.new(user)
+ .execute(other_user, skip_authorization: true)
+ end.to(
+ change do
+ Users::GhostUserMigration.where(user: other_user,
+ initiator_user: user )
+ .exists?
+ end.from(false).to(true))
end
end
end
diff --git a/spec/services/users/migrate_records_to_ghost_user_in_batches_service_spec.rb b/spec/services/users/migrate_records_to_ghost_user_in_batches_service_spec.rb
index 7366b1646b9..107ff82016c 100644
--- a/spec/services/users/migrate_records_to_ghost_user_in_batches_service_spec.rb
+++ b/spec/services/users/migrate_records_to_ghost_user_in_batches_service_spec.rb
@@ -27,5 +27,34 @@ RSpec.describe Users::MigrateRecordsToGhostUserInBatchesService do
service.execute
end
+
+ it 'process jobs ordered by the consume_after timestamp' do
+ older_ghost_user_migration = create(:ghost_user_migration, user: create(:user),
+ consume_after: 5.minutes.ago)
+
+ # setup execution tracker to only allow a single job to be processed
+ allow_next_instance_of(::Gitlab::Utils::ExecutionTracker) do |tracker|
+ allow(tracker).to receive(:over_limit?).and_return(false, true)
+ end
+
+ expect(Users::MigrateRecordsToGhostUserService).to(
+ receive(:new).with(older_ghost_user_migration.user,
+ older_ghost_user_migration.initiator_user,
+ any_args)
+ ).and_call_original
+
+ service.execute
+ end
+
+ it 'reschedules job in case of an error', :freeze_time do
+ expect_next_instance_of(Users::MigrateRecordsToGhostUserService) do |service|
+ expect(service).to(receive(:execute)).and_raise(ActiveRecord::QueryCanceled)
+ end
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+
+ expect { service.execute }.to(
+ change { ghost_user_migration.reload.consume_after }
+ .to(30.minutes.from_now))
+ end
end
end
diff --git a/spec/services/users/migrate_records_to_ghost_user_service_spec.rb b/spec/services/users/migrate_records_to_ghost_user_service_spec.rb
index 766be51ae13..6082c7bd10e 100644
--- a/spec/services/users/migrate_records_to_ghost_user_service_spec.rb
+++ b/spec/services/users/migrate_records_to_ghost_user_service_spec.rb
@@ -146,24 +146,106 @@ RSpec.describe Users::MigrateRecordsToGhostUserService do
end
context 'for batched nullify' do
+ # rubocop:disable Layout/LineLength
+ def nullify_in_batches_regexp(table, column, user, batch_size: 100)
+ %r{^UPDATE "#{table}" SET "#{column}" = NULL WHERE "#{table}"."id" IN \(SELECT "#{table}"."id" FROM "#{table}" WHERE "#{table}"."#{column}" = #{user.id} LIMIT #{batch_size}\)}
+ end
+
+ def delete_in_batches_regexps(table, column, user, items, batch_size: 1000)
+ select_query = %r{^SELECT "#{table}".* FROM "#{table}" WHERE "#{table}"."#{column}" = #{user.id}.*ORDER BY "#{table}"."id" ASC LIMIT #{batch_size}}
+
+ [select_query] + items.map { |item| %r{^DELETE FROM "#{table}" WHERE "#{table}"."id" = #{item.id}} }
+ end
+ # rubocop:enable Layout/LineLength
+
it 'nullifies related associations in batches' do
expect(user).to receive(:nullify_dependent_associations_in_batches).and_call_original
service.execute
end
- it 'nullifies last_updated_issues, closed_issues, resource_label_events' do
+ it 'nullifies associations marked as `dependent: :nullify` and'\
+ 'destroys the associations marked as `dependent: :destroy`, in batches', :aggregate_failures do
+ # associations to be nullified
issue = create(:issue, closed_by: user, updated_by: user)
resource_label_event = create(:resource_label_event, user: user)
+ resource_state_event = create(:resource_state_event, user: user)
+ created_project = create(:project, creator: user)
- service.execute
+ # associations to be destroyed
+ todos = create_list(:todo, 2, project: issue.project, user: user, author: user, target: issue)
+ event = create(:event, project: issue.project, author: user)
+
+ query_recorder = ActiveRecord::QueryRecorder.new do
+ service.execute
+ end
issue.reload
resource_label_event.reload
+ resource_state_event.reload
+ created_project.reload
expect(issue.closed_by).to be_nil
- expect(issue.updated_by).to be_nil
- expect(resource_label_event.user).to be_nil
+ expect(issue.updated_by_id).to be_nil
+ expect(resource_label_event.user_id).to be_nil
+ expect(resource_state_event.user_id).to be_nil
+ expect(created_project.creator_id).to be_nil
+ expect(user.authored_todos).to be_empty
+ expect(user.todos).to be_empty
+ expect(user.authored_events).to be_empty
+
+ expected_queries = [
+ nullify_in_batches_regexp(:issues, :updated_by_id, user),
+ nullify_in_batches_regexp(:issues, :closed_by_id, user),
+ nullify_in_batches_regexp(:resource_label_events, :user_id, user),
+ nullify_in_batches_regexp(:resource_state_events, :user_id, user),
+ nullify_in_batches_regexp(:projects, :creator_id, user)
+ ]
+
+ expected_queries += delete_in_batches_regexps(:todos, :user_id, user, todos)
+ expected_queries += delete_in_batches_regexps(:todos, :author_id, user, todos)
+ expected_queries += delete_in_batches_regexps(:events, :author_id, user, [event])
+
+ expect(query_recorder.log).to include(*expected_queries)
+ end
+
+ it 'nullifies merge request associations', :aggregate_failures do
+ merge_request = create(:merge_request, source_project: project,
+ target_project: project,
+ assignee: user,
+ updated_by: user,
+ merge_user: user)
+ merge_request.metrics.update!(merged_by: user, latest_closed_by: user)
+ merge_request.reviewers = [user]
+ merge_request.assignees = [user]
+
+ query_recorder = ActiveRecord::QueryRecorder.new do
+ service.execute
+ end
+
+ merge_request.reload
+
+ expect(merge_request.updated_by).to be_nil
+ expect(merge_request.assignee).to be_nil
+ expect(merge_request.assignee_id).to be_nil
+ expect(merge_request.metrics.merged_by).to be_nil
+ expect(merge_request.metrics.latest_closed_by).to be_nil
+ expect(merge_request.reviewers).to be_empty
+ expect(merge_request.assignees).to be_empty
+
+ expected_queries = [
+ nullify_in_batches_regexp(:merge_requests, :updated_by_id, user),
+ nullify_in_batches_regexp(:merge_requests, :assignee_id, user),
+ nullify_in_batches_regexp(:merge_request_metrics, :merged_by_id, user),
+ nullify_in_batches_regexp(:merge_request_metrics, :latest_closed_by_id, user)
+ ]
+
+ expected_queries += delete_in_batches_regexps(:merge_request_assignees, :user_id, user,
+ merge_request.assignees)
+ expected_queries += delete_in_batches_regexps(:merge_request_reviewers, :user_id, user,
+ merge_request.reviewers)
+
+ expect(query_recorder.log).to include(*expected_queries)
end
end
@@ -235,7 +317,7 @@ RSpec.describe Users::MigrateRecordsToGhostUserService do
aggregate_failures do
expect { service.execute }.to(
- raise_error(Users::MigrateRecordsToGhostUserService::DestroyError, 'foo' ))
+ raise_error(Users::MigrateRecordsToGhostUserService::DestroyError, 'foo'))
expect(snippet.reload).not_to be_nil
expect(
gitlab_shell.repository_exists?(snippet.repository_storage,
diff --git a/spec/services/users/migrate_to_ghost_user_service_spec.rb b/spec/services/users/migrate_to_ghost_user_service_spec.rb
deleted file mode 100644
index 073ebaae5b0..00000000000
--- a/spec/services/users/migrate_to_ghost_user_service_spec.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Users::MigrateToGhostUserService do
- let!(:user) { create(:user) }
- let!(:project) { create(:project, :repository) }
- let(:service) { described_class.new(user) }
- let(:always_ghost) { false }
-
- context "migrating a user's associated records to the ghost user" do
- context 'issues' do
- context 'deleted user is present as both author and edited_user' do
- include_examples "migrating a deleted user's associated records to the ghost user", Issue, [:author, :last_edited_by] do
- let(:created_record) do
- create(:issue, project: project, author: user, last_edited_by: user)
- end
- end
- end
-
- context 'deleted user is present only as edited_user' do
- include_examples "migrating a deleted user's associated records to the ghost user", Issue, [:last_edited_by] do
- let(:created_record) { create(:issue, project: project, author: create(:user), last_edited_by: user) }
- end
- end
- end
-
- context 'merge requests' do
- context 'deleted user is present as both author and merge_user' do
- include_examples "migrating a deleted user's associated records to the ghost user", MergeRequest, [:author, :merge_user] do
- let(:created_record) { create(:merge_request, source_project: project, author: user, merge_user: user, target_branch: "first") }
- end
- end
-
- context 'deleted user is present only as both merge_user' do
- include_examples "migrating a deleted user's associated records to the ghost user", MergeRequest, [:merge_user] do
- let(:created_record) { create(:merge_request, source_project: project, merge_user: user, target_branch: "first") }
- end
- end
- end
-
- context 'notes' do
- include_examples "migrating a deleted user's associated records to the ghost user", Note do
- let(:created_record) { create(:note, project: project, author: user) }
- end
- end
-
- context 'abuse reports' do
- include_examples "migrating a deleted user's associated records to the ghost user", AbuseReport do
- let(:created_record) { create(:abuse_report, reporter: user, user: create(:user)) }
- end
- end
-
- context 'award emoji' do
- include_examples "migrating a deleted user's associated records to the ghost user", AwardEmoji, [:user] do
- let(:created_record) { create(:award_emoji, user: user) }
-
- context "when the awardable already has an award emoji of the same name assigned to the ghost user" do
- let(:awardable) { create(:issue) }
- let!(:existing_award_emoji) { create(:award_emoji, user: User.ghost, name: "thumbsup", awardable: awardable) }
- let!(:award_emoji) { create(:award_emoji, user: user, name: "thumbsup", awardable: awardable) }
-
- it "migrates the award emoji regardless" do
- service.execute
-
- migrated_record = AwardEmoji.find_by_id(award_emoji.id)
-
- expect(migrated_record.user).to eq(User.ghost)
- end
-
- it "does not leave the migrated award emoji in an invalid state" do
- service.execute
-
- migrated_record = AwardEmoji.find_by_id(award_emoji.id)
-
- expect(migrated_record).to be_valid
- end
- end
- end
- end
-
- context 'snippets' do
- include_examples "migrating a deleted user's associated records to the ghost user", Snippet do
- let(:created_record) { create(:snippet, project: project, author: user) }
- end
- end
-
- context 'reviews' do
- let!(:user) { create(:user) }
- let(:service) { described_class.new(user) }
-
- include_examples "migrating a deleted user's associated records to the ghost user", Review, [:author] do
- let(:created_record) { create(:review, author: user) }
- end
- end
- end
-end
diff --git a/spec/services/users/reject_service_spec.rb b/spec/services/users/reject_service_spec.rb
index abff6b1e023..37d003c5dac 100644
--- a/spec/services/users/reject_service_spec.rb
+++ b/spec/services/users/reject_service_spec.rb
@@ -35,29 +35,14 @@ RSpec.describe Users::RejectService do
context 'success' do
context 'when the executor user is an admin in admin mode', :enable_admin_mode do
- context 'when user_destroy_with_limited_execution_time_worker is enabled' do
- it 'initiates user removal', :sidekiq_inline do
- subject
-
- expect(subject[:status]).to eq(:success)
- expect(
- Users::GhostUserMigration.where(user: user,
- initiator_user: current_user)
- ).to be_exists
- end
- end
-
- context 'when user_destroy_with_limited_execution_time_worker is disabled' do
- before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
- end
-
- it 'deletes the user', :sidekiq_inline do
- subject
+ it 'initiates user removal', :sidekiq_inline do
+ subject
- expect(subject[:status]).to eq(:success)
- expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
- end
+ expect(subject[:status]).to eq(:success)
+ expect(
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: current_user)
+ ).to be_exists
end
it 'emails the user on rejection' do
@@ -73,7 +58,7 @@ RSpec.describe Users::RejectService do
subject
- expect(Gitlab::AppLogger).to have_received(:info).with(message: "User instance access request rejected", user: "#{user.username}", email: "#{user.email}", rejected_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}")
+ expect(Gitlab::AppLogger).to have_received(:info).with(message: "User instance access request rejected", user: user.username.to_s, email: user.email.to_s, rejected_by: current_user.username.to_s, ip_address: current_user.current_sign_in_ip.to_s)
end
end
end
diff --git a/spec/services/users/unban_service_spec.rb b/spec/services/users/unban_service_spec.rb
index d536baafdcc..3dcb8450e7b 100644
--- a/spec/services/users/unban_service_spec.rb
+++ b/spec/services/users/unban_service_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe Users::UnbanService do
end
it 'logs unban in application logs' do
- expect(Gitlab::AppLogger).to receive(:info).with(message: "User unban", user: "#{user.username}", email: "#{user.email}", unban_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}")
+ expect(Gitlab::AppLogger).to receive(:info).with(message: "User unban", user: user.username.to_s, email: user.email.to_s, unban_by: current_user.username.to_s, ip_address: current_user.current_sign_in_ip.to_s)
unban_user
end
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 551c3dbcc82..c081b20d95f 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -175,22 +175,6 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
).once
end
- context 'when webhooks_gitlab_instance_header flag is disabled' do
- before do
- stub_feature_flags(webhooks_gitlab_instance_header: false)
- end
-
- it 'excludes the X-Gitlab-Instance header' do
- stub_full_request(project_hook.url, method: :post)
-
- service_instance.execute
-
- expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url)).with(
- headers: headers.except('X-Gitlab-Instance')
- ).once
- end
- end
-
context 'when the data is a Gitlab::DataBuilder::Pipeline' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:data) { ::Gitlab::DataBuilder::Pipeline.new(pipeline) }
@@ -245,7 +229,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
it 'does not execute disabled hooks' do
allow(service_instance).to receive(:disabled?).and_return(true)
- expect(service_instance.execute).to eq({ status: :error, message: 'Hook disabled' })
+ expect(service_instance.execute).to have_attributes(status: :error, message: 'Hook disabled')
end
it 'executes and registers the hook with the recursion detection', :aggregate_failures do
@@ -317,7 +301,8 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
project_hook.enable!
stub_full_request(project_hook.url, method: :post).to_raise(exception)
- expect(service_instance.execute).to eq({ status: :error, message: exception.to_s })
+
+ expect(service_instance.execute).to have_attributes(status: :error, message: exception.to_s)
expect { service_instance.execute }.not_to raise_error
end
end
@@ -326,7 +311,10 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
let_it_be(:project_hook) { create(:project_hook, url: 'http://server.com/my path/') }
it 'handles exceptions' do
- expect(service_instance.execute).to eq(status: :error, message: 'bad URI(is not URI?): "http://server.com/my path/"')
+ expect(service_instance.execute).to have_attributes(
+ status: :error,
+ message: 'bad URI(is not URI?): "http://server.com/my path/"'
+ )
expect { service_instance.execute }.not_to raise_error
end
end
@@ -335,20 +323,31 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
it 'does not perform the request' do
stub_const("#{described_class}::REQUEST_BODY_SIZE_LIMIT", 10.bytes)
- expect(service_instance.execute).to eq({ status: :error, message: "Gitlab::Json::LimitedEncoder::LimitExceeded" })
+ expect(service_instance.execute).to have_attributes(
+ status: :error,
+ message: 'Gitlab::Json::LimitedEncoder::LimitExceeded'
+ )
end
end
it 'handles 200 status code' do
stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: 'Success')
- expect(service_instance.execute).to include({ status: :success, http_status: 200, message: 'Success' })
+ expect(service_instance.execute).to have_attributes(
+ status: :success,
+ payload: { http_status: 200 },
+ message: 'Success'
+ )
end
it 'handles 2xx status codes' do
stub_full_request(project_hook.url, method: :post).to_return(status: 201, body: 'Success')
- expect(service_instance.execute).to include({ status: :success, http_status: 201, message: 'Success' })
+ expect(service_instance.execute).to have_attributes(
+ status: :success,
+ payload: { http_status: 201 },
+ message: 'Success'
+ )
end
context 'execution logging' do
diff --git a/spec/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb
index c0bcf9b606d..1bd7e15db67 100644
--- a/spec/services/work_items/create_service_spec.rb
+++ b/spec/services/work_items/create_service_spec.rb
@@ -179,16 +179,6 @@ RSpec.describe WorkItems::CreateService do
let(:error_message) { 'only Issue and Incident can be parent of Task.' }
end
end
-
- context 'when hierarchy feature flag is disabled' do
- before do
- stub_feature_flags(work_items_hierarchy: false)
- end
-
- it_behaves_like 'fails creating work item and returns errors' do
- let(:error_message) { '`work_items_hierarchy` feature flag disabled for this project' }
- end
- end
end
context 'when user cannot admin parent link' do
diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb
index 1761d1104dd..68efb4c220b 100644
--- a/spec/services/work_items/update_service_spec.rb
+++ b/spec/services/work_items/update_service_spec.rb
@@ -311,6 +311,34 @@ RSpec.describe WorkItems::UpdateService do
end
end
end
+
+ context 'for milestone widget' do
+ let_it_be(:milestone) { create(:milestone, project: project) }
+
+ let(:widget_params) { { milestone_widget: { milestone_id: milestone.id } } }
+
+ context 'when milestone is updated' do
+ it "triggers 'issuableMilestoneUpdated'" do
+ expect(work_item.milestone).to eq(nil)
+ expect(GraphqlTriggers).to receive(:issuable_milestone_updated).with(work_item).and_call_original
+
+ update_work_item
+ end
+ end
+
+ context 'when milestone remains unchanged' do
+ before do
+ update_work_item
+ end
+
+ it "does not trigger 'issuableMilestoneUpdated'" do
+ expect(work_item.milestone).to eq(milestone)
+ expect(GraphqlTriggers).not_to receive(:issuable_milestone_updated)
+
+ update_work_item
+ end
+ end
+ end
end
describe 'label updates' do
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 9a425d5308c..1b8c4c5f15f 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
@@ -42,18 +42,6 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do
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] } }
-
- before do
- stub_feature_flags(work_items_hierarchy: false)
- end
-
- it_behaves_like 'raises a WidgetError' do
- let(:message) { '`work_items_hierarchy` feature flag disabled for this project' }
- end
- end
-
context 'when user has insufficient permissions to link work items' do
let(:params) { { children: [child_work_item4] } }
@@ -105,16 +93,6 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do
let(:params) { { parent: parent_work_item } }
- context 'when work_items_hierarchy feature flag is disabled' do
- before do
- stub_feature_flags(work_items_hierarchy: false)
- end
-
- it_behaves_like 'raises a WidgetError' do
- let(:message) { '`work_items_hierarchy` feature flag disabled for this project' }
- end
- end
-
context 'when user has insufficient permissions to link work items' do
it_behaves_like 'raises a WidgetError' do
let(:message) { not_found_error }
diff --git a/spec/services/work_items/widgets/milestone_service/create_service_spec.rb b/spec/services/work_items/widgets/milestone_service/create_service_spec.rb
new file mode 100644
index 00000000000..3f90784b703
--- /dev/null
+++ b/spec/services/work_items/widgets/milestone_service/create_service_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::Widgets::MilestoneService::CreateService do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :private, group: group) }
+ let_it_be(:project_milestone) { create(:milestone, project: project) }
+ let_it_be(:group_milestone) { create(:milestone, group: group) }
+ let_it_be(:guest) { create(:user) }
+
+ let(:current_user) { guest }
+ let(:work_item) { build(:work_item, project: project, updated_at: 1.day.ago) }
+ let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::Milestone) } }
+ let(:service) { described_class.new(widget: widget, current_user: current_user) }
+
+ before do
+ project.add_guest(guest)
+ end
+
+ describe '#before_create_callback' do
+ it_behaves_like "setting work item's milestone" do
+ subject(:execute_callback) do
+ service.before_create_callback(params: params)
+ end
+ end
+ end
+end
diff --git a/spec/services/work_items/widgets/milestone_service/update_service_spec.rb b/spec/services/work_items/widgets/milestone_service/update_service_spec.rb
new file mode 100644
index 00000000000..f3a7fc156b9
--- /dev/null
+++ b/spec/services/work_items/widgets/milestone_service/update_service_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::Widgets::MilestoneService::UpdateService do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :private, group: group) }
+ let_it_be(:project_milestone) { create(:milestone, project: project) }
+ let_it_be(:group_milestone) { create(:milestone, group: group) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+
+ let(:work_item) { create(:work_item, project: project, updated_at: 1.day.ago) }
+ let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::Milestone) } }
+ let(:service) { described_class.new(widget: widget, current_user: current_user) }
+
+ before do
+ project.add_reporter(reporter)
+ project.add_guest(guest)
+ end
+
+ describe '#before_update_callback' do
+ context 'when current user is not allowed to set work item metadata' do
+ let(:current_user) { guest }
+ let(:params) { { milestone_id: group_milestone.id } }
+
+ it "does not set the work item's milestone" do
+ expect { service.before_update_callback(params: params) }
+ .to not_change(work_item, :milestone)
+ end
+ end
+
+ context "when current user is allowed to set work item metadata" do
+ let(:current_user) { reporter }
+
+ it_behaves_like "setting work item's milestone" do
+ subject(:execute_callback) do
+ service.before_update_callback(params: params)
+ end
+ end
+
+ context 'when unsetting a milestone' do
+ let(:params) { { milestone_id: nil } }
+
+ before do
+ work_item.update!(milestone: project_milestone)
+ end
+
+ it "sets the work item's milestone" do
+ expect { service.before_update_callback(params: params) }
+ .to change(work_item, :milestone)
+ .from(project_milestone)
+ .to(nil)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/x509_certificate_revoke_service_spec.rb b/spec/services/x509_certificate_revoke_service_spec.rb
index adad3281c13..ff5d2dc058b 100644
--- a/spec/services/x509_certificate_revoke_service_spec.rb
+++ b/spec/services/x509_certificate_revoke_service_spec.rb
@@ -5,11 +5,11 @@ require 'spec_helper'
RSpec.describe X509CertificateRevokeService do
describe '#execute' do
let(:service) { described_class.new }
- let!(:x509_signature_1) { create(:x509_commit_signature, x509_certificate: x509_certificate, verification_status: :verified ) }
- let!(:x509_signature_2) { create(:x509_commit_signature, x509_certificate: x509_certificate, verification_status: :verified ) }
+ let!(:x509_signature_1) { create(:x509_commit_signature, x509_certificate: x509_certificate, verification_status: :verified) }
+ let!(:x509_signature_2) { create(:x509_commit_signature, x509_certificate: x509_certificate, verification_status: :verified) }
context 'for revoked certificates' do
- let(:x509_certificate) { create(:x509_certificate, certificate_status: :revoked ) }
+ let(:x509_certificate) { create(:x509_certificate, certificate_status: :revoked) }
it 'update all commit signatures' do
expect do