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>2023-02-20 16:49:51 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-20 16:49:51 +0300
commit71786ddc8e28fbd3cb3fcc4b3ff15e5962a1c82e (patch)
tree6a2d93ef3fb2d353bb7739e4b57e6541f51cdd71 /spec/services
parenta7253423e3403b8c08f8a161e5937e1488f5f407 (diff)
Add latest changes from gitlab-org/gitlab@15-9-stable-eev15.9.0-rc42
Diffstat (limited to 'spec/services')
-rw-r--r--spec/services/achievements/create_service_spec.rb2
-rw-r--r--spec/services/analytics/cycle_analytics/stages/list_service_spec.rb7
-rw-r--r--spec/services/audit_event_service_spec.rb54
-rw-r--r--spec/services/authorized_project_update/project_access_changed_service_spec.rb11
-rw-r--r--spec/services/auto_merge_service_spec.rb2
-rw-r--r--spec/services/bulk_imports/create_service_spec.rb227
-rw-r--r--spec/services/chat_names/authorize_user_service_spec.rb2
-rw-r--r--spec/services/ci/archive_trace_service_spec.rb67
-rw-r--r--spec/services/ci/components/fetch_service_spec.rb141
-rw-r--r--spec/services/ci/create_downstream_pipeline_service_spec.rb17
-rw-r--r--spec/services/ci/job_artifacts/create_service_spec.rb8
-rw-r--r--spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb3
-rw-r--r--spec/services/ci/job_token_scope/add_project_service_spec.rb24
-rw-r--r--spec/services/ci/job_token_scope/remove_project_service_spec.rb4
-rw-r--r--spec/services/ci/list_config_variables_service_spec.rb14
-rw-r--r--spec/services/ci/parse_dotenv_artifact_service_spec.rb14
-rw-r--r--spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb250
-rw-r--r--spec/services/ci/pipeline_schedule_service_spec.rb2
-rw-r--r--spec/services/ci/pipeline_schedules/update_service_spec.rb78
-rw-r--r--spec/services/ci/register_job_service_spec.rb1165
-rw-r--r--spec/services/ci/retry_job_service_spec.rb66
-rw-r--r--spec/services/ci/runners/create_runner_service_spec.rb135
-rw-r--r--spec/services/ci/runners/process_runner_version_update_service_spec.rb4
-rw-r--r--spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb32
-rw-r--r--spec/services/ci/runners/register_runner_service_spec.rb1
-rw-r--r--spec/services/ci/runners/stale_machines_cleanup_service_spec.rb45
-rw-r--r--spec/services/ci/update_build_queue_service_spec.rb2
-rw-r--r--spec/services/ci/update_build_state_service_spec.rb2
-rw-r--r--spec/services/clusters/agents/refresh_authorization_service_spec.rb12
-rw-r--r--spec/services/concerns/rate_limited_service_spec.rb4
-rw-r--r--spec/services/event_create_service_spec.rb2
-rw-r--r--spec/services/export_csv/base_service_spec.rb31
-rw-r--r--spec/services/export_csv/map_export_fields_service_spec.rb55
-rw-r--r--spec/services/git/wiki_push_service_spec.rb2
-rw-r--r--spec/services/google_cloud/fetch_google_ip_list_service_spec.rb2
-rw-r--r--spec/services/groups/create_service_spec.rb10
-rw-r--r--spec/services/groups/destroy_service_spec.rb2
-rw-r--r--spec/services/groups/group_links/destroy_service_spec.rb4
-rw-r--r--spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_spec.rb86
-rw-r--r--spec/services/import_csv/base_service_spec.rb64
-rw-r--r--spec/services/incident_management/timeline_events/create_service_spec.rb10
-rw-r--r--spec/services/incident_management/timeline_events/update_service_spec.rb8
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb81
-rw-r--r--spec/services/issuable/destroy_service_spec.rb2
-rw-r--r--spec/services/issuable/discussions_list_service_spec.rb4
-rw-r--r--spec/services/issues/after_create_service_spec.rb2
-rw-r--r--spec/services/issues/build_service_spec.rb6
-rw-r--r--spec/services/issues/clone_service_spec.rb2
-rw-r--r--spec/services/issues/close_service_spec.rb22
-rw-r--r--spec/services/issues/create_service_spec.rb76
-rw-r--r--spec/services/issues/duplicate_service_spec.rb2
-rw-r--r--spec/services/issues/export_csv_service_spec.rb214
-rw-r--r--spec/services/issues/import_csv_service_spec.rb2
-rw-r--r--spec/services/issues/move_service_spec.rb4
-rw-r--r--spec/services/issues/referenced_merge_requests_service_spec.rb2
-rw-r--r--spec/services/issues/related_branches_service_spec.rb4
-rw-r--r--spec/services/issues/reopen_service_spec.rb6
-rw-r--r--spec/services/issues/reorder_service_spec.rb2
-rw-r--r--spec/services/issues/update_service_spec.rb68
-rw-r--r--spec/services/issues/zoom_link_service_spec.rb2
-rw-r--r--spec/services/jira_connect_installations/update_service_spec.rb18
-rw-r--r--spec/services/keys/revoke_service_spec.rb48
-rw-r--r--spec/services/lfs/file_transformer_spec.rb2
-rw-r--r--spec/services/members/approve_access_request_service_spec.rb8
-rw-r--r--spec/services/members/base_service_spec.rb19
-rw-r--r--spec/services/members/destroy_service_spec.rb95
-rw-r--r--spec/services/members/projects/creator_service_spec.rb2
-rw-r--r--spec/services/merge_requests/after_create_service_spec.rb13
-rw-r--r--spec/services/merge_requests/build_service_spec.rb2
-rw-r--r--spec/services/merge_requests/close_service_spec.rb2
-rw-r--r--spec/services/merge_requests/create_from_issue_service_spec.rb2
-rw-r--r--spec/services/merge_requests/create_pipeline_service_spec.rb2
-rw-r--r--spec/services/merge_requests/create_service_spec.rb40
-rw-r--r--spec/services/merge_requests/export_csv_service_spec.rb18
-rw-r--r--spec/services/merge_requests/link_lfs_objects_service_spec.rb2
-rw-r--r--spec/services/merge_requests/pushed_branches_service_spec.rb2
-rw-r--r--spec/services/merge_requests/rebase_service_spec.rb2
-rw-r--r--spec/services/merge_requests/remove_approval_service_spec.rb2
-rw-r--r--spec/services/merge_requests/retarget_chain_service_spec.rb2
-rw-r--r--spec/services/merge_requests/update_service_spec.rb32
-rw-r--r--spec/services/notes/create_service_spec.rb37
-rw-r--r--spec/services/notes/destroy_service_spec.rb8
-rw-r--r--spec/services/notification_service_spec.rb474
-rw-r--r--spec/services/packages/debian/create_distribution_service_spec.rb2
-rw-r--r--spec/services/packages/debian/create_package_file_service_spec.rb31
-rw-r--r--spec/services/packages/debian/extract_changes_metadata_service_spec.rb11
-rw-r--r--spec/services/packages/debian/extract_deb_metadata_service_spec.rb2
-rw-r--r--spec/services/packages/debian/extract_metadata_service_spec.rb2
-rw-r--r--spec/services/packages/debian/find_or_create_incoming_service_spec.rb2
-rw-r--r--spec/services/packages/debian/find_or_create_package_service_spec.rb69
-rw-r--r--spec/services/packages/debian/generate_distribution_key_service_spec.rb2
-rw-r--r--spec/services/packages/debian/generate_distribution_service_spec.rb8
-rw-r--r--spec/services/packages/debian/parse_debian822_service_spec.rb2
-rw-r--r--spec/services/packages/debian/process_changes_service_spec.rb40
-rw-r--r--spec/services/packages/debian/process_package_file_service_spec.rb167
-rw-r--r--spec/services/packages/debian/sign_distribution_service_spec.rb2
-rw-r--r--spec/services/packages/debian/update_distribution_service_spec.rb2
-rw-r--r--spec/services/pages/destroy_deployments_service_spec.rb22
-rw-r--r--spec/services/pages/migrate_from_legacy_storage_service_spec.rb2
-rw-r--r--spec/services/preview_markdown_service_spec.rb17
-rw-r--r--spec/services/projects/container_repository/destroy_service_spec.rb143
-rw-r--r--spec/services/projects/create_service_spec.rb4
-rw-r--r--spec/services/projects/destroy_service_spec.rb69
-rw-r--r--spec/services/projects/group_links/create_service_spec.rb2
-rw-r--r--spec/services/projects/group_links/destroy_service_spec.rb2
-rw-r--r--spec/services/projects/group_links/update_service_spec.rb2
-rw-r--r--spec/services/projects/import_export/export_service_spec.rb21
-rw-r--r--spec/services/projects/import_service_spec.rb2
-rw-r--r--spec/services/projects/protect_default_branch_service_spec.rb32
-rw-r--r--spec/services/projects/transfer_service_spec.rb19
-rw-r--r--spec/services/protected_branches/create_service_spec.rb3
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb2
-rw-r--r--spec/services/quick_actions/target_service_spec.rb40
-rw-r--r--spec/services/releases/create_service_spec.rb71
-rw-r--r--spec/services/releases/update_service_spec.rb77
-rw-r--r--spec/services/resource_events/change_labels_service_spec.rb23
-rw-r--r--spec/services/security/ci_configuration/sast_create_service_spec.rb51
-rw-r--r--spec/services/serverless/associate_domain_service_spec.rb51
-rw-r--r--spec/services/spam/spam_verdict_service_spec.rb8
-rw-r--r--spec/services/system_note_service_spec.rb2
-rw-r--r--spec/services/tasks_to_be_done/base_service_spec.rb6
-rw-r--r--spec/services/todo_service_spec.rb18
-rw-r--r--spec/services/todos/destroy/entity_leave_service_spec.rb2
-rw-r--r--spec/services/todos/destroy/group_private_service_spec.rb2
-rw-r--r--spec/services/user_project_access_changed_service_spec.rb54
-rw-r--r--spec/services/users/activity_service_spec.rb50
-rw-r--r--spec/services/users/assigned_issues_count_service_spec.rb2
-rw-r--r--spec/services/web_hook_service_spec.rb3
-rw-r--r--spec/services/work_items/build_service_spec.rb2
-rw-r--r--spec/services/work_items/create_service_spec.rb6
-rw-r--r--spec/services/work_items/delete_service_spec.rb2
-rw-r--r--spec/services/work_items/export_csv_service_spec.rb77
-rw-r--r--spec/services/work_items/update_service_spec.rb6
133 files changed, 3595 insertions, 1646 deletions
diff --git a/spec/services/achievements/create_service_spec.rb b/spec/services/achievements/create_service_spec.rb
index f62a45deb50..ac28a88572b 100644
--- a/spec/services/achievements/create_service_spec.rb
+++ b/spec/services/achievements/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Achievements::CreateService, feature_category: :users do
+RSpec.describe Achievements::CreateService, feature_category: :user_profile do
describe '#execute' do
let_it_be(:user) { create(:user) }
diff --git a/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb b/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb
index 24f0123ed3b..7bfae0cd9fc 100644
--- a/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb
+++ b/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb
@@ -5,11 +5,14 @@ require 'spec_helper'
RSpec.describe Analytics::CycleAnalytics::Stages::ListService do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
+ let_it_be(:project_namespace) { project.project_namespace.reload }
- let(:value_stream) { Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(project) }
+ let(:value_stream) { Analytics::CycleAnalytics::ValueStream.build_default_value_stream(project_namespace) }
let(:stages) { subject.payload[:stages] }
- subject { described_class.new(parent: project, current_user: user).execute }
+ subject do
+ described_class.new(parent: project_namespace, current_user: user, params: { value_stream: value_stream }).execute
+ end
before_all do
project.add_reporter(user)
diff --git a/spec/services/audit_event_service_spec.rb b/spec/services/audit_event_service_spec.rb
index 1d079adc0be..4f8b90fcb4a 100644
--- a/spec/services/audit_event_service_spec.rb
+++ b/spec/services/audit_event_service_spec.rb
@@ -13,12 +13,12 @@ RSpec.describe AuditEventService, :with_license do
describe '#security_event' do
it 'creates an event and logs to a file' do
expect(service).to receive(:file_logger).and_return(logger)
- expect(logger).to receive(:info).with(author_id: user.id,
- author_name: user.name,
- entity_id: project.id,
- entity_type: "Project",
- action: :destroy,
- created_at: anything)
+ expect(logger).to receive(:info).with({ author_id: user.id,
+ author_name: user.name,
+ entity_id: project.id,
+ entity_type: "Project",
+ action: :destroy,
+ created_at: anything })
expect { service.security_event }.to change(AuditEvent, :count).by(1)
end
@@ -33,15 +33,15 @@ RSpec.describe AuditEventService, :with_license do
target_id: 1
})
expect(service).to receive(:file_logger).and_return(logger)
- expect(logger).to receive(:info).with(author_id: user.id,
- author_name: user.name,
- entity_type: 'Project',
- entity_id: project.id,
- from: 'true',
- to: 'false',
- action: :create,
- target_id: 1,
- created_at: anything)
+ expect(logger).to receive(:info).with({ author_id: user.id,
+ author_name: user.name,
+ entity_type: 'Project',
+ entity_id: project.id,
+ from: 'true',
+ to: 'false',
+ action: :create,
+ target_id: 1,
+ created_at: anything })
expect { service.security_event }.to change(AuditEvent, :count).by(1)
@@ -58,12 +58,12 @@ RSpec.describe AuditEventService, :with_license do
it 'is overridden successfully' do
freeze_time do
expect(service).to receive(:file_logger).and_return(logger)
- expect(logger).to receive(:info).with(author_id: user.id,
- author_name: user.name,
- entity_id: project.id,
- entity_type: "Project",
- action: :destroy,
- created_at: 3.weeks.ago)
+ expect(logger).to receive(:info).with({ author_id: user.id,
+ author_name: user.name,
+ entity_id: project.id,
+ entity_type: "Project",
+ action: :destroy,
+ created_at: 3.weeks.ago })
expect { service.security_event }.to change(AuditEvent, :count).by(1)
expect(AuditEvent.last.created_at).to eq(3.weeks.ago)
@@ -129,12 +129,12 @@ RSpec.describe AuditEventService, :with_license do
describe '#log_security_event_to_file' do
it 'logs security event to file' do
expect(service).to receive(:file_logger).and_return(logger)
- expect(logger).to receive(:info).with(author_id: user.id,
- author_name: user.name,
- entity_type: 'Project',
- entity_id: project.id,
- action: :destroy,
- created_at: anything)
+ expect(logger).to receive(:info).with({ author_id: user.id,
+ author_name: user.name,
+ entity_type: 'Project',
+ entity_id: project.id,
+ action: :destroy,
+ created_at: anything })
service.log_security_event_to_file
end
diff --git a/spec/services/authorized_project_update/project_access_changed_service_spec.rb b/spec/services/authorized_project_update/project_access_changed_service_spec.rb
index 11621055a47..da428bece20 100644
--- a/spec/services/authorized_project_update/project_access_changed_service_spec.rb
+++ b/spec/services/authorized_project_update/project_access_changed_service_spec.rb
@@ -4,18 +4,11 @@ require 'spec_helper'
RSpec.describe AuthorizedProjectUpdate::ProjectAccessChangedService do
describe '#execute' do
- it 'schedules the project IDs' do
- expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_and_wait)
- .with([[1], [2]])
-
- described_class.new([1, 2]).execute
- end
-
- it 'permits non-blocking operation' do
+ it 'executes projects_authorizations refresh' do
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_async)
.with([[1], [2]])
- described_class.new([1, 2]).execute(blocking: false)
+ described_class.new([1, 2]).execute
end
end
end
diff --git a/spec/services/auto_merge_service_spec.rb b/spec/services/auto_merge_service_spec.rb
index 043b413acff..7584e44152e 100644
--- a/spec/services/auto_merge_service_spec.rb
+++ b/spec/services/auto_merge_service_spec.rb
@@ -131,7 +131,7 @@ RSpec.describe AutoMergeService do
subject
end
- context 'when the head piipeline succeeded' do
+ context 'when the head pipeline succeeded' do
let(:pipeline_status) { :success }
it 'returns failed' do
diff --git a/spec/services/bulk_imports/create_service_spec.rb b/spec/services/bulk_imports/create_service_spec.rb
index 75f88e3989c..7f892cfe722 100644
--- a/spec/services/bulk_imports/create_service_spec.rb
+++ b/spec/services/bulk_imports/create_service_spec.rb
@@ -8,27 +8,27 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
let(:destination_group) { create(:group, path: 'destination1') }
let(:migrate_projects) { true }
let_it_be(:parent_group) { create(:group, path: 'parent-group') }
+ # note: destination_name and destination_slug are currently interchangable so we need to test for both possibilities
let(:params) do
[
{
source_type: 'group_entity',
source_full_path: 'full/path/to/group1',
- destination_slug: 'destination group 1',
+ destination_slug: 'destination-group-1',
destination_namespace: 'parent-group',
migrate_projects: migrate_projects
-
},
{
source_type: 'group_entity',
source_full_path: 'full/path/to/group2',
- destination_slug: 'destination group 2',
+ destination_name: 'destination-group-2',
destination_namespace: 'parent-group',
migrate_projects: migrate_projects
},
{
source_type: 'project_entity',
source_full_path: 'full/path/to/project1',
- destination_slug: 'destination project 1',
+ destination_slug: 'destination-project-1',
destination_namespace: 'parent-group',
migrate_projects: migrate_projects
}
@@ -226,7 +226,12 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
expect(result).to be_a(ServiceResponse)
expect(result).to be_error
- expect(result.message).to eq("Validation failed: Source full path can't be blank")
+ expect(result.message).to eq("Validation failed: Source full path can't be blank, " \
+ "Source full path cannot start with a non-alphanumeric character except " \
+ "for periods or underscores, can contain only alphanumeric characters, " \
+ "forward slashes, periods, and underscores, cannot end with " \
+ "a period or forward slash, and has a relative path structure " \
+ "with no http protocol chars or leading or trailing forward slashes")
end
describe '#user-role' do
@@ -267,56 +272,188 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
extra: { user_role: 'Not a member', import_type: 'bulk_import_group' }
)
end
- end
- context 'when there is a destination_namespace but no parent_namespace' do
- let(:params) do
- [
- {
- source_type: 'group_entity',
- source_full_path: 'full/path/to/group1',
- destination_slug: 'destination-group-1',
- destination_namespace: 'destination1'
- }
- ]
+ context 'when there is a destination_namespace but no parent_namespace' do
+ let(:params) do
+ [
+ {
+ source_type: 'group_entity',
+ source_full_path: 'full/path/to/group1',
+ destination_slug: 'destination-group-1',
+ destination_namespace: 'destination1'
+ }
+ ]
+ end
+
+ it 'defines access_level from destination_namespace' do
+ destination_group.add_developer(user)
+ subject.execute
+
+ expect_snowplow_event(
+ category: 'BulkImports::CreateService',
+ action: 'create',
+ label: 'import_access_level',
+ user: user,
+ extra: { user_role: 'Developer', import_type: 'bulk_import_group' }
+ )
+ end
end
- it 'defines access_level from destination_namespace' do
- destination_group.add_developer(user)
- subject.execute
+ context 'when there is no destination_namespace or parent_namespace' do
+ let(:params) do
+ [
+ {
+ source_type: 'group_entity',
+ source_full_path: 'full/path/to/group1',
+ destination_slug: 'destinationational-mcdestiny',
+ destination_namespace: 'destinational-mcdestiny'
+ }
+ ]
+ end
- expect_snowplow_event(
- category: 'BulkImports::CreateService',
- action: 'create',
- label: 'import_access_level',
- user: user,
- extra: { user_role: 'Developer', import_type: 'bulk_import_group' }
- )
+ it 'defines access_level as owner' do
+ subject.execute
+
+ expect_snowplow_event(
+ category: 'BulkImports::CreateService',
+ action: 'create',
+ label: 'import_access_level',
+ user: user,
+ extra: { user_role: 'Owner', import_type: 'bulk_import_group' }
+ )
+ end
end
end
- context 'when there is no destination_namespace or parent_namespace' do
- let(:params) do
- [
- {
- source_type: 'group_entity',
- source_full_path: 'full/path/to/group1',
- destination_slug: 'destinationational mcdestiny',
- destination_namespace: 'destinational-mcdestiny'
- }
- ]
+ describe '.validate_destination_full_path' do
+ context 'when the source_type is a group' do
+ context 'when the provided destination_slug already exists in the destination_namespace' do
+ let_it_be(:existing_subgroup) { create(:group, path: 'existing-subgroup', parent_id: parent_group.id ) }
+ let_it_be(:existing_subgroup_2) { create(:group, path: 'existing-subgroup_2', parent_id: parent_group.id ) }
+ let(:params) do
+ [
+ {
+ source_type: 'group_entity',
+ source_full_path: 'full/path/to/source',
+ destination_slug: existing_subgroup.path,
+ destination_namespace: parent_group.path,
+ migrate_projects: migrate_projects
+ }
+ ]
+ end
+
+ it 'returns ServiceResponse with an error message' do
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result).to be_error
+ expect(result.message)
+ .to eq(
+ "Import aborted as 'parent-group/existing-subgroup' already exists. " \
+ "Change the destination and try again."
+ )
+ end
+ end
+
+ context 'when the destination_slug conflicts with an existing top-level namespace' do
+ let_it_be(:existing_top_level_group) { create(:group, path: 'top-level-group') }
+ let(:params) do
+ [
+ {
+ source_type: 'group_entity',
+ source_full_path: 'full/path/to/source',
+ destination_slug: existing_top_level_group.path,
+ destination_namespace: '',
+ migrate_projects: migrate_projects
+ }
+ ]
+ end
+
+ it 'returns ServiceResponse with an error message' do
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result).to be_error
+ expect(result.message)
+ .to eq(
+ "Import aborted as 'top-level-group' already exists. " \
+ "Change the destination and try again."
+ )
+ end
+ end
+
+ context 'when the destination_slug does not conflict with an existing top-level namespace' do
+ let(:params) do
+ [
+ {
+ source_type: 'group_entity',
+ source_full_path: 'full/path/to/source',
+ destination_slug: 'new-group',
+ destination_namespace: parent_group.path,
+ migrate_projects: migrate_projects
+ }
+ ]
+ end
+
+ it 'returns success ServiceResponse' do
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result).to be_success
+ end
+ end
end
- it 'defines access_level as owner' do
- subject.execute
+ context 'when the source_type is a project' do
+ context 'when the provided destination_slug already exists in the destination_namespace' do
+ let_it_be(:existing_group) { create(:group, path: 'existing-group' ) }
+ let_it_be(:existing_project) { create(:project, path: 'existing-project', parent_id: existing_group.id ) }
+ let(:params) do
+ [
+ {
+ source_type: 'project_entity',
+ source_full_path: 'full/path/to/source',
+ destination_slug: existing_project.path,
+ destination_namespace: existing_group.path,
+ migrate_projects: migrate_projects
+ }
+ ]
+ end
- expect_snowplow_event(
- category: 'BulkImports::CreateService',
- action: 'create',
- label: 'import_access_level',
- user: user,
- extra: { user_role: 'Owner', import_type: 'bulk_import_group' }
- )
+ it 'returns ServiceResponse with an error message' do
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result).to be_error
+ expect(result.message)
+ .to eq(
+ "Import aborted as 'existing-group/existing-project' already exists. " \
+ "Change the destination and try again."
+ )
+ end
+ end
+
+ context 'when the destination_slug does not conflict with an existing project' do
+ let_it_be(:existing_group) { create(:group, path: 'existing-group' ) }
+ let(:params) do
+ [
+ {
+ source_type: 'project_entity',
+ source_full_path: 'full/path/to/source',
+ destination_slug: 'new-project',
+ destination_namespace: 'existing-group',
+ migrate_projects: migrate_projects
+ }
+ ]
+ end
+
+ it 'returns success ServiceResponse' do
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result).to be_success
+ end
+ end
end
end
end
diff --git a/spec/services/chat_names/authorize_user_service_spec.rb b/spec/services/chat_names/authorize_user_service_spec.rb
index 4c261ece504..c9b4439202a 100644
--- a/spec/services/chat_names/authorize_user_service_spec.rb
+++ b/spec/services/chat_names/authorize_user_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ChatNames::AuthorizeUserService, feature_category: :users do
+RSpec.describe ChatNames::AuthorizeUserService, feature_category: :user_profile do
describe '#execute' do
let(:result) { subject.execute }
diff --git a/spec/services/ci/archive_trace_service_spec.rb b/spec/services/ci/archive_trace_service_spec.rb
index 359ea0699e4..3fb9d092ae7 100644
--- a/spec/services/ci/archive_trace_service_spec.rb
+++ b/spec/services/ci/archive_trace_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::ArchiveTraceService, '#execute' do
+RSpec.describe Ci::ArchiveTraceService, '#execute', feature_category: :continuous_integration do
subject { described_class.new.execute(job, worker_name: Ci::ArchiveTraceWorker.name) }
context 'when job is finished' do
@@ -192,4 +192,69 @@ RSpec.describe Ci::ArchiveTraceService, '#execute' do
expect(job.trace_metadata.archival_attempts).to eq(1)
end
end
+
+ describe '#batch_execute' do
+ subject { described_class.new.batch_execute(worker_name: Ci::ArchiveTraceWorker.name) }
+
+ let_it_be_with_reload(:job) { create(:ci_build, :success, :trace_live, finished_at: 1.day.ago) }
+ let_it_be_with_reload(:job2) { create(:ci_build, :success, :trace_live, finished_at: 1.day.ago) }
+
+ it 'archives multiple traces' do
+ expect { subject }.not_to raise_error
+
+ expect(job.reload.job_artifacts_trace).to be_exist
+ expect(job2.reload.job_artifacts_trace).to be_exist
+ end
+
+ it 'processes traces independently' do
+ allow_next_instance_of(Gitlab::Ci::Trace) do |instance|
+ orig_method = instance.method(:archive!)
+ allow(instance).to receive(:archive!) do
+ raise('Unexpected error') if instance.job.id == job.id
+
+ orig_method.call
+ end
+ end
+
+ expect { subject }.not_to raise_error
+
+ expect(job.reload.job_artifacts_trace).to be_nil
+ expect(job2.reload.job_artifacts_trace).to be_exist
+ end
+
+ context 'when timeout is reached' do
+ before do
+ stub_const("#{described_class}::LOOP_TIMEOUT", 0.seconds)
+ end
+
+ it 'stops executing traces' do
+ expect { subject }.not_to raise_error
+
+ expect(job.reload.job_artifacts_trace).to be_nil
+ end
+ end
+
+ context 'when loop limit is reached' do
+ before do
+ stub_const("#{described_class}::LOOP_LIMIT", -1)
+ end
+
+ it 'skips archiving' do
+ expect(job.trace).not_to receive(:archive!)
+
+ subject
+ end
+
+ it 'stops executing traces' do
+ expect(Sidekiq.logger).to receive(:warn).with(
+ class: Ci::ArchiveTraceWorker.name,
+ message: "Loop limit reached.",
+ job_id: job.id)
+
+ expect { subject }.not_to raise_error
+
+ expect(job.reload.job_artifacts_trace).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/services/ci/components/fetch_service_spec.rb b/spec/services/ci/components/fetch_service_spec.rb
new file mode 100644
index 00000000000..f2eaa8d31b4
--- /dev/null
+++ b/spec/services/ci/components/fetch_service_spec.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Components::FetchService, feature_category: :pipeline_authoring do
+ let_it_be(:project) { create(:project, :repository, create_tag: 'v1.0') }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:current_user) { user }
+ let_it_be(:current_host) { Gitlab.config.gitlab.host }
+
+ let(:service) do
+ described_class.new(address: address, current_user: current_user)
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ describe '#execute', :aggregate_failures do
+ subject(:result) { service.execute }
+
+ shared_examples 'an external component' do
+ shared_examples 'component address' do
+ context 'when content exists' do
+ let(:sha) { project.commit(version).id }
+
+ let(:content) do
+ <<~COMPONENT
+ job:
+ script: echo
+ COMPONENT
+ end
+
+ before do
+ stub_project_blob(sha, component_yaml_path, content)
+ end
+
+ it 'returns the content' do
+ expect(result).to be_success
+ expect(result.payload[:content]).to eq(content)
+ end
+ end
+
+ context 'when content does not exist' do
+ it 'returns an error' do
+ expect(result).to be_error
+ expect(result.reason).to eq(:content_not_found)
+ end
+ end
+ end
+
+ context 'when user does not have permissions to read the code' do
+ let(:version) { 'master' }
+ let(:current_user) { create(:user) }
+
+ it 'returns an error' do
+ expect(result).to be_error
+ expect(result.reason).to eq(:not_allowed)
+ end
+ end
+
+ context 'when version is a branch name' do
+ it_behaves_like 'component address' do
+ let(:version) { project.default_branch }
+ end
+ end
+
+ context 'when version is a tag name' do
+ it_behaves_like 'component address' do
+ let(:version) { project.repository.tags.first.name }
+ end
+ end
+
+ context 'when version is a commit sha' do
+ it_behaves_like 'component address' do
+ let(:version) { project.repository.tags.first.id }
+ end
+ end
+
+ context 'when version is not provided' do
+ let(:version) { nil }
+
+ it 'returns an error' do
+ expect(result).to be_error
+ expect(result.reason).to eq(:content_not_found)
+ end
+ end
+
+ context 'when project does not exist' do
+ let(:component_path) { 'unknown/component' }
+ let(:version) { '1.0' }
+
+ it 'returns an error' do
+ expect(result).to be_error
+ expect(result.reason).to eq(:content_not_found)
+ end
+ end
+
+ context 'when host is different than the current instance host' do
+ let(:current_host) { 'another-host.com' }
+ let(:version) { '1.0' }
+
+ it 'returns an error' do
+ expect(result).to be_error
+ expect(result.reason).to eq(:unsupported_path)
+ end
+ end
+ end
+
+ context 'when address points to an external component' do
+ let(:address) { "#{current_host}/#{component_path}@#{version}" }
+
+ context 'when component path is the full path to a project' do
+ let(:component_path) { project.full_path }
+ let(:component_yaml_path) { 'template.yml' }
+
+ it_behaves_like 'an external component'
+ end
+
+ context 'when component path points to a directory in a project' do
+ let(:component_path) { "#{project.full_path}/my-component" }
+ let(:component_yaml_path) { 'my-component/template.yml' }
+
+ it_behaves_like 'an external component'
+ end
+
+ context 'when component path points to a nested directory in a project' do
+ let(:component_path) { "#{project.full_path}/my-dir/my-component" }
+ let(:component_yaml_path) { 'my-dir/my-component/template.yml' }
+
+ it_behaves_like 'an external component'
+ end
+ end
+ end
+
+ def stub_project_blob(ref, path, content)
+ allow_next_instance_of(Repository) do |instance|
+ allow(instance).to receive(:blob_data_at).with(ref, path).and_return(content)
+ end
+ end
+end
diff --git a/spec/services/ci/create_downstream_pipeline_service_spec.rb b/spec/services/ci/create_downstream_pipeline_service_spec.rb
index fd978bffacb..7b576339c61 100644
--- a/spec/services/ci/create_downstream_pipeline_service_spec.rb
+++ b/spec/services/ci/create_downstream_pipeline_service_spec.rb
@@ -890,23 +890,6 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute', feature_category
end
end
end
-
- context 'with :ci_limit_complete_hierarchy_size disabled' do
- before do
- stub_feature_flags(ci_limit_complete_hierarchy_size: false)
- end
-
- it 'creates a new pipeline' do
- expect { subject }.to change { Ci::Pipeline.count }.by(1)
- expect(subject).to be_success
- end
-
- it 'marks the bridge job as successful' do
- subject
-
- expect(bridge.reload).to be_success
- end
- 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 711002e28af..47e9e5994ef 100644
--- a/spec/services/ci/job_artifacts/create_service_spec.rb
+++ b/spec/services/ci/job_artifacts/create_service_spec.rb
@@ -132,6 +132,14 @@ RSpec.describe Ci::JobArtifacts::CreateService do
expect(new_artifact).to be_public_accessibility
end
+ it 'logs the created artifact and metadata' do
+ expect(Gitlab::Ci::Artifacts::Logger)
+ .to receive(:log_created)
+ .with(an_instance_of(Ci::JobArtifact)).twice
+
+ subject
+ end
+
context 'when accessibility level passed as private' do
before do
params.merge!('accessibility' => 'private')
diff --git a/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb b/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb
index dd10c0df374..457be67c1ea 100644
--- a/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb
+++ b/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_shared_state do
+RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_shared_state,
+feature_category: :build_artifacts do
include ExclusiveLeaseHelpers
let(:service) { described_class.new }
diff --git a/spec/services/ci/job_token_scope/add_project_service_spec.rb b/spec/services/ci/job_token_scope/add_project_service_spec.rb
index bf7df3a5595..e6674ee384f 100644
--- a/spec/services/ci/job_token_scope/add_project_service_spec.rb
+++ b/spec/services/ci/job_token_scope/add_project_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Ci::JobTokenScope::AddProjectService do
+RSpec.describe Ci::JobTokenScope::AddProjectService, feature_category: :continuous_integration do
let(:service) { described_class.new(project, current_user) }
let_it_be(:project) { create(:project, ci_outbound_job_token_scope_enabled: true).tap(&:save!) }
@@ -21,6 +21,8 @@ RSpec.describe Ci::JobTokenScope::AddProjectService do
it_behaves_like 'editable job token scope' do
context 'when user has permissions on source and target projects' do
+ let(:resulting_direction) { result.payload.fetch(:project_link)&.direction }
+
before do
project.add_maintainer(current_user)
target_project.add_developer(current_user)
@@ -34,6 +36,26 @@ RSpec.describe Ci::JobTokenScope::AddProjectService do
end
it_behaves_like 'adds project'
+
+ it 'creates an outbound link by default' do
+ expect(resulting_direction).to eq('outbound')
+ end
+
+ context 'when direction is specified' do
+ subject(:result) { service.execute(target_project, direction: direction) }
+
+ context 'when the direction is outbound' do
+ let(:direction) { :outbound }
+
+ specify { expect(resulting_direction).to eq('outbound') }
+ end
+
+ context 'when the direction is inbound' do
+ let(:direction) { :inbound }
+
+ specify { expect(resulting_direction).to eq('inbound') }
+ end
+ end
end
end
diff --git a/spec/services/ci/job_token_scope/remove_project_service_spec.rb b/spec/services/ci/job_token_scope/remove_project_service_spec.rb
index c3f9081cbd8..5b39f8908f2 100644
--- a/spec/services/ci/job_token_scope/remove_project_service_spec.rb
+++ b/spec/services/ci/job_token_scope/remove_project_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Ci::JobTokenScope::RemoveProjectService do
+RSpec.describe Ci::JobTokenScope::RemoveProjectService, feature_category: :continuous_integration do
let(:service) { described_class.new(project, current_user) }
let_it_be(:project) { create(:project, ci_outbound_job_token_scope_enabled: true).tap(&:save!) }
@@ -23,7 +23,7 @@ RSpec.describe Ci::JobTokenScope::RemoveProjectService do
end
describe '#execute' do
- subject(:result) { service.execute(target_project) }
+ subject(:result) { service.execute(target_project, :outbound) }
it_behaves_like 'editable job token scope' do
context 'when user has permissions on source and target project' do
diff --git a/spec/services/ci/list_config_variables_service_spec.rb b/spec/services/ci/list_config_variables_service_spec.rb
index 5b865914d1b..e2bbdefef7f 100644
--- a/spec/services/ci/list_config_variables_service_spec.rb
+++ b/spec/services/ci/list_config_variables_service_spec.rb
@@ -2,19 +2,21 @@
require 'spec_helper'
-RSpec.describe Ci::ListConfigVariablesService, :use_clean_rails_memory_store_caching do
+RSpec.describe Ci::ListConfigVariablesService,
+:use_clean_rails_memory_store_caching, feature_category: :pipeline_authoring do
include ReactiveCachingHelpers
let(:ci_config) { {} }
let(:files) { { '.gitlab-ci.yml' => YAML.dump(ci_config) } }
let(:project) { create(:project, :custom_repo, :auto_devops_disabled, files: files) }
let(:user) { project.creator }
- let(:sha) { project.default_branch }
+ let(:ref) { project.default_branch }
+ let(:sha) { project.commit(ref).sha }
let(:service) { described_class.new(project, user) }
- subject(:result) { service.execute(sha) }
+ subject(:result) { service.execute(ref) }
- context 'when sending a valid sha' do
+ context 'when sending a valid ref' do
let(:ci_config) do
{
variables: {
@@ -109,8 +111,8 @@ RSpec.describe Ci::ListConfigVariablesService, :use_clean_rails_memory_store_cac
end
end
- context 'when sending an invalid sha' do
- let(:sha) { 'invalid-sha' }
+ context 'when sending an invalid ref' do
+ let(:ref) { 'invalid-ref' }
let(:ci_config) { nil }
before do
diff --git a/spec/services/ci/parse_dotenv_artifact_service_spec.rb b/spec/services/ci/parse_dotenv_artifact_service_spec.rb
index 7b3af33ac72..f720375f05c 100644
--- a/spec/services/ci/parse_dotenv_artifact_service_spec.rb
+++ b/spec/services/ci/parse_dotenv_artifact_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::ParseDotenvArtifactService do
+RSpec.describe Ci::ParseDotenvArtifactService, feature_category: :build_artifacts do
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
@@ -223,6 +223,18 @@ RSpec.describe Ci::ParseDotenvArtifactService do
end
end
+ context 'when blob is encoded in UTF-16 LE' do
+ let(:blob) { File.read(Rails.root.join('spec/fixtures/build_artifacts/dotenv_utf16_le.txt')) }
+
+ it 'parses the dotenv data' do
+ subject
+
+ expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
+ hash_including('key' => 'MY_ENV_VAR', 'value' => 'true'),
+ hash_including('key' => 'TEST2', 'value' => 'false'))
+ end
+ end
+
context 'when more than limitated variables are specified in dotenv' do
let(:blob) do
StringIO.new.tap do |s|
diff --git a/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb b/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb
new file mode 100644
index 00000000000..402bc2faa81
--- /dev/null
+++ b/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb
@@ -0,0 +1,250 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::PipelineCreation::CancelRedundantPipelinesService, feature_category: :continuous_integration do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:prev_pipeline) { create(:ci_pipeline, project: project) }
+ let!(:new_commit) { create(:commit, project: project) }
+ let(:pipeline) { create(:ci_pipeline, project: project, sha: new_commit.sha) }
+
+ let(:service) { described_class.new(pipeline) }
+
+ before do
+ create(:ci_build, :interruptible, :running, pipeline: prev_pipeline)
+ create(:ci_build, :interruptible, :success, pipeline: prev_pipeline)
+ create(:ci_build, :created, pipeline: prev_pipeline)
+
+ create(:ci_build, :interruptible, pipeline: pipeline)
+ end
+
+ describe '#execute!' do
+ subject(:execute) { service.execute }
+
+ context 'when build statuses are set up correctly' do
+ it 'has builds of all statuses' do
+ expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')
+ expect(build_statuses(pipeline)).to contain_exactly('pending')
+ end
+ end
+
+ context 'when auto-cancel is enabled' do
+ before do
+ project.update!(auto_cancel_pending_pipelines: 'enabled')
+ end
+
+ it 'cancels only previous interruptible builds' do
+ execute
+
+ expect(build_statuses(prev_pipeline)).to contain_exactly('canceled', 'success', 'canceled')
+ expect(build_statuses(pipeline)).to contain_exactly('pending')
+ end
+
+ it 'logs canceled pipelines' do
+ allow(Gitlab::AppLogger).to receive(:info)
+
+ execute
+
+ expect(Gitlab::AppLogger).to have_received(:info).with(
+ class: described_class.name,
+ message: "Pipeline #{pipeline.id} auto-canceling pipeline #{prev_pipeline.id}",
+ canceled_pipeline_id: prev_pipeline.id,
+ canceled_by_pipeline_id: pipeline.id,
+ canceled_by_pipeline_source: pipeline.source
+ )
+ end
+
+ it 'cancels the builds with 2 queries to avoid query timeout' do
+ second_query_regex = /WHERE "ci_pipelines"\."id" = \d+ AND \(NOT EXISTS/
+ recorder = ActiveRecord::QueryRecorder.new { execute }
+ second_query = recorder.occurrences.keys.filter { |occ| occ =~ second_query_regex }
+
+ expect(second_query).to be_one
+ end
+
+ context 'when the previous pipeline has a child pipeline' do
+ let(:child_pipeline) { create(:ci_pipeline, child_of: prev_pipeline) }
+
+ context 'with another nested child pipeline' do
+ let(:another_child_pipeline) { create(:ci_pipeline, child_of: child_pipeline) }
+
+ before do
+ create(:ci_build, :interruptible, :running, pipeline: another_child_pipeline)
+ create(:ci_build, :interruptible, :running, pipeline: another_child_pipeline)
+ end
+
+ it 'cancels all nested child pipeline builds' do
+ expect(build_statuses(another_child_pipeline)).to contain_exactly('running', 'running')
+
+ execute
+
+ expect(build_statuses(another_child_pipeline)).to contain_exactly('canceled', 'canceled')
+ end
+ end
+
+ context 'when started after pipeline was finished' do
+ before do
+ create(:ci_build, :interruptible, :running, pipeline: child_pipeline)
+ prev_pipeline.update!(status: "success")
+ end
+
+ it 'cancels child pipeline builds' do
+ expect(build_statuses(child_pipeline)).to contain_exactly('running')
+
+ execute
+
+ expect(build_statuses(child_pipeline)).to contain_exactly('canceled')
+ end
+ end
+
+ context 'when the child pipeline has interruptible running jobs' do
+ before do
+ create(:ci_build, :interruptible, :running, pipeline: child_pipeline)
+ create(:ci_build, :interruptible, :running, pipeline: child_pipeline)
+ end
+
+ it 'cancels all child pipeline builds' do
+ expect(build_statuses(child_pipeline)).to contain_exactly('running', 'running')
+
+ execute
+
+ expect(build_statuses(child_pipeline)).to contain_exactly('canceled', 'canceled')
+ end
+
+ context 'when the child pipeline includes completed interruptible jobs' do
+ before do
+ create(:ci_build, :interruptible, :failed, pipeline: child_pipeline)
+ create(:ci_build, :interruptible, :success, pipeline: child_pipeline)
+ end
+
+ it 'cancels all child pipeline builds with a cancelable_status' do
+ expect(build_statuses(child_pipeline)).to contain_exactly('running', 'running', 'failed', 'success')
+
+ execute
+
+ expect(build_statuses(child_pipeline)).to contain_exactly('canceled', 'canceled', 'failed', 'success')
+ end
+ end
+ end
+
+ context 'when the child pipeline has started non-interruptible job' do
+ before do
+ create(:ci_build, :interruptible, :running, pipeline: child_pipeline)
+ # non-interruptible started
+ create(:ci_build, :success, pipeline: child_pipeline)
+ end
+
+ it 'does not cancel any child pipeline builds' do
+ expect(build_statuses(child_pipeline)).to contain_exactly('running', 'success')
+
+ execute
+
+ expect(build_statuses(child_pipeline)).to contain_exactly('running', 'success')
+ end
+ end
+
+ context 'when the child pipeline has non-interruptible non-started job' do
+ before do
+ create(:ci_build, :interruptible, :running, pipeline: child_pipeline)
+ end
+
+ not_started_statuses = Ci::HasStatus::AVAILABLE_STATUSES - Ci::HasStatus::STARTED_STATUSES
+ context 'when the jobs are cancelable' do
+ cancelable_not_started_statuses =
+ Set.new(not_started_statuses).intersection(Ci::HasStatus::CANCELABLE_STATUSES)
+ cancelable_not_started_statuses.each do |status|
+ it "cancels all child pipeline builds when build status #{status} included" do
+ # non-interruptible but non-started
+ create(:ci_build, status.to_sym, pipeline: child_pipeline)
+
+ expect(build_statuses(child_pipeline)).to contain_exactly('running', status)
+
+ execute
+
+ expect(build_statuses(child_pipeline)).to contain_exactly('canceled', 'canceled')
+ end
+ end
+ end
+
+ context 'when the jobs are not cancelable' do
+ not_cancelable_not_started_statuses = not_started_statuses - Ci::HasStatus::CANCELABLE_STATUSES
+ not_cancelable_not_started_statuses.each do |status|
+ it "does not cancel child pipeline builds when build status #{status} included" do
+ # non-interruptible but non-started
+ create(:ci_build, status.to_sym, pipeline: child_pipeline)
+
+ expect(build_statuses(child_pipeline)).to contain_exactly('running', status)
+
+ execute
+
+ expect(build_statuses(child_pipeline)).to contain_exactly('canceled', status)
+ end
+ end
+ end
+ end
+ end
+
+ context 'when the pipeline is a child pipeline' do
+ let!(:parent_pipeline) { create(:ci_pipeline, project: project, sha: new_commit.sha) }
+ let(:pipeline) { create(:ci_pipeline, child_of: parent_pipeline) }
+
+ before do
+ create(:ci_build, :interruptible, :running, pipeline: parent_pipeline)
+ create(:ci_build, :interruptible, :running, pipeline: parent_pipeline)
+ end
+
+ it 'does not cancel any builds' do
+ expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')
+ expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running')
+
+ execute
+
+ expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')
+ expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running')
+ end
+ end
+
+ context 'when the previous pipeline source is webide' do
+ let(:prev_pipeline) { create(:ci_pipeline, :webide, project: project) }
+
+ it 'does not cancel builds of the previous pipeline' do
+ execute
+
+ expect(build_statuses(prev_pipeline)).to contain_exactly('created', 'running', 'success')
+ expect(build_statuses(pipeline)).to contain_exactly('pending')
+ end
+ end
+
+ it 'does not cancel future pipelines' do
+ expect(prev_pipeline.id).to be < pipeline.id
+ expect(build_statuses(pipeline)).to contain_exactly('pending')
+ expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')
+
+ described_class.new(prev_pipeline).execute
+
+ expect(build_statuses(pipeline.reload)).to contain_exactly('pending')
+ end
+ end
+
+ context 'when auto-cancel is disabled' do
+ before do
+ project.update!(auto_cancel_pending_pipelines: 'disabled')
+ end
+
+ it 'does not cancel any build' do
+ subject
+
+ expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')
+ expect(build_statuses(pipeline)).to contain_exactly('pending')
+ end
+ end
+ end
+
+ private
+
+ def build_statuses(pipeline)
+ pipeline.builds.pluck(:status)
+ end
+end
diff --git a/spec/services/ci/pipeline_schedule_service_spec.rb b/spec/services/ci/pipeline_schedule_service_spec.rb
index 2f094583f1a..8896d8ace30 100644
--- a/spec/services/ci/pipeline_schedule_service_spec.rb
+++ b/spec/services/ci/pipeline_schedule_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PipelineScheduleService do
+RSpec.describe Ci::PipelineScheduleService, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
diff --git a/spec/services/ci/pipeline_schedules/update_service_spec.rb b/spec/services/ci/pipeline_schedules/update_service_spec.rb
new file mode 100644
index 00000000000..838f49f6dea
--- /dev/null
+++ b/spec/services/ci/pipeline_schedules/update_service_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuous_integration do
+ let_it_be(:user) { 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: user) }
+
+ before_all do
+ project.add_maintainer(user)
+ 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(_('The current user is not authorized to update the pipeline schedule'))
+ end
+ end
+
+ context 'when user has permission' do
+ let(:params) do
+ {
+ description: 'updated_desc',
+ ref: 'patch-x',
+ active: false,
+ cron: '*/1 * * * *'
+ }
+ end
+
+ subject(:service) { described_class.new(pipeline_schedule, user, params) }
+
+ it 'updates database values with passed params' do
+ expect { service.execute }
+ .to change { pipeline_schedule.description }.from('pipeline schedule').to('updated_desc')
+ .and change { pipeline_schedule.ref }.from('master').to('patch-x')
+ .and change { pipeline_schedule.active }.from(true).to(false)
+ .and change { pipeline_schedule.cron }.from('0 1 * * *').to('*/1 * * * *')
+ end
+
+ it 'returns ServiceResponse.success' do
+ result = service.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.success?).to be(true)
+ expect(result.payload.description).to eq('updated_desc')
+ end
+
+ context 'when schedule update fails' do
+ subject(:service) { described_class.new(pipeline_schedule, user, {}) }
+
+ 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 match_array(['An error occurred'])
+ 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 f40f5cc5a62..9183df359b4 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -3,795 +3,830 @@
require 'spec_helper'
module Ci
- RSpec.describe RegisterJobService do
+ RSpec.describe RegisterJobService, feature_category: :continuous_integration do
let_it_be(:group) { create(:group) }
let_it_be_with_reload(:project) { create(:project, group: group, shared_runners_enabled: false, group_runners_enabled: false) }
let_it_be_with_reload(:pipeline) { create(:ci_pipeline, project: project) }
- let!(:shared_runner) { create(:ci_runner, :instance) }
- let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
+ let_it_be(:shared_runner) { create(:ci_runner, :instance) }
+ let!(:project_runner) { create(:ci_runner, :project, projects: [project]) }
let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
describe '#execute' do
- subject { described_class.new(shared_runner).execute }
+ subject(:execute) { described_class.new(runner, runner_machine).execute }
+
+ context 'with runner_machine specified' do
+ let(:runner) { project_runner }
+ let!(:runner_machine) { create(:ci_runner_machine, runner: project_runner) }
- context 'checks database loadbalancing stickiness' do
before do
- project.update!(shared_runners_enabled: false)
+ pending_job.update!(tag_list: ["linux"])
+ pending_job.reload
+ pending_job.create_queuing_entry!
+ project_runner.update!(tag_list: ["linux"])
end
- 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 }
+ it 'sets runner_machine on job' do
+ expect { execute }.to change { pending_job.reload.runner_machine }.from(nil).to(runner_machine)
- expect(subject).to be_valid
- expect(subject.build).to be_nil
- expect(subject.build_json).to be_nil
+ expect(execute.build).to eq(pending_job)
end
+ end
- 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 }
+ context 'with no runner machine' do
+ let(:runner_machine) { nil }
- expect(subject).not_to be_valid
- expect(subject.build).to be_nil
- expect(subject.build_json).to be_nil
- end
- end
+ context 'checks database loadbalancing stickiness' do
+ let(:runner) { shared_runner }
- shared_examples 'handles runner assignment' do
- context 'runner follow tag list' do
- it "picks build with the same tag" do
- pending_job.update!(tag_list: ["linux"])
- pending_job.reload
- pending_job.create_queuing_entry!
- specific_runner.update!(tag_list: ["linux"])
- expect(execute(specific_runner)).to eq(pending_job)
+ before do
+ project.update!(shared_runners_enabled: false)
end
- it "does not pick build with different tag" do
- pending_job.update!(tag_list: ["linux"])
- pending_job.reload
- pending_job.create_queuing_entry!
- specific_runner.update!(tag_list: ["win32"])
- expect(execute(specific_runner)).to be_falsey
- end
+ it 'result is valid if replica did caught-up', :aggregate_failures do
+ expect(ApplicationRecord.sticking).to receive(:all_caught_up?).with(:runner, runner.id) { true }
- it "picks build without tag" do
- expect(execute(specific_runner)).to eq(pending_job)
+ expect(execute).to be_valid
+ expect(execute.build).to be_nil
+ expect(execute.build_json).to be_nil
end
- it "does not pick build with tag" do
- pending_job.update!(tag_list: ["linux"])
- pending_job.reload
- pending_job.create_queuing_entry!
- expect(execute(specific_runner)).to be_falsey
- end
+ 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 }
- it "pick build without tag" do
- specific_runner.update!(tag_list: ["win32"])
- expect(execute(specific_runner)).to eq(pending_job)
+ expect(subject).not_to be_valid
+ expect(subject.build).to be_nil
+ expect(subject.build_json).to be_nil
end
end
- context 'deleted projects' do
- before do
- project.update!(pending_delete: true)
- end
+ shared_examples 'handles runner assignment' do
+ context 'runner follow tag list' do
+ it "picks build with the same tag" do
+ pending_job.update!(tag_list: ["linux"])
+ pending_job.reload
+ pending_job.create_queuing_entry!
+ project_runner.update!(tag_list: ["linux"])
+ expect(build_on(project_runner)).to eq(pending_job)
+ end
- context 'for shared runners' do
- before do
- project.update!(shared_runners_enabled: true)
+ it "does not pick build with different tag" do
+ pending_job.update!(tag_list: ["linux"])
+ pending_job.reload
+ pending_job.create_queuing_entry!
+ project_runner.update!(tag_list: ["win32"])
+ expect(build_on(project_runner)).to be_falsey
end
- it 'does not pick a build' do
- expect(execute(shared_runner)).to be_nil
+ it "picks build without tag" do
+ expect(build_on(project_runner)).to eq(pending_job)
end
- end
- context 'for specific runner' do
- it 'does not pick a build' do
- expect(execute(specific_runner)).to be_nil
- expect(pending_job.reload).to be_failed
- expect(pending_job.queuing_entry).to be_nil
+ it "does not pick build with tag" do
+ pending_job.update!(tag_list: ["linux"])
+ pending_job.reload
+ pending_job.create_queuing_entry!
+ expect(build_on(project_runner)).to be_falsey
end
- end
- end
- context 'allow shared runners' do
- before do
- project.update!(shared_runners_enabled: true)
- pipeline.reload
- pending_job.reload
- pending_job.create_queuing_entry!
+ it "pick build without tag" do
+ project_runner.update!(tag_list: ["win32"])
+ expect(build_on(project_runner)).to eq(pending_job)
+ end
end
- context 'when build owner has been blocked' do
- let(:user) { create(:user, :blocked) }
-
+ context 'deleted projects' do
before do
- pending_job.update!(user: user)
+ project.update!(pending_delete: true)
end
- it 'does not pick the build and drops the build' do
- expect(execute(shared_runner)).to be_falsey
+ context 'for shared runners' do
+ before do
+ project.update!(shared_runners_enabled: true)
+ end
- expect(pending_job.reload).to be_user_blocked
+ it 'does not pick a build' do
+ expect(build_on(shared_runner)).to be_nil
+ end
+ end
+
+ context 'for project runner' do
+ it 'does not pick a build' do
+ expect(build_on(project_runner)).to be_nil
+ expect(pending_job.reload).to be_failed
+ expect(pending_job.queuing_entry).to be_nil
+ end
end
end
- context 'for multiple builds' do
- let!(:project2) { create :project, shared_runners_enabled: true }
- let!(:pipeline2) { create :ci_pipeline, project: project2 }
- let!(:project3) { create :project, shared_runners_enabled: true }
- let!(:pipeline3) { create :ci_pipeline, project: project3 }
- let!(:build1_project1) { pending_job }
- let!(:build2_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- let!(:build3_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- let!(:build1_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) }
- let!(:build2_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) }
- let!(:build1_project3) { create(:ci_build, :pending, :queued, pipeline: pipeline3) }
-
- it 'picks builds one-by-one' do
- expect(Ci::Build).to receive(:find).with(pending_job.id).and_call_original
-
- expect(execute(shared_runner)).to eq(build1_project1)
- end
-
- context 'when using fair scheduling' do
- context 'when all builds are pending' do
- it 'prefers projects without builds first' do
- # it gets for one build from each of the projects
- expect(execute(shared_runner)).to eq(build1_project1)
- expect(execute(shared_runner)).to eq(build1_project2)
- expect(execute(shared_runner)).to eq(build1_project3)
-
- # then it gets a second build from each of the projects
- expect(execute(shared_runner)).to eq(build2_project1)
- expect(execute(shared_runner)).to eq(build2_project2)
-
- # in the end the third build
- expect(execute(shared_runner)).to eq(build3_project1)
- end
+ context 'allow shared runners' do
+ before do
+ project.update!(shared_runners_enabled: true)
+ pipeline.reload
+ pending_job.reload
+ pending_job.create_queuing_entry!
+ end
+
+ context 'when build owner has been blocked' do
+ let(:user) { create(:user, :blocked) }
+
+ before do
+ pending_job.update!(user: user)
end
- context 'when some builds transition to success' do
- it 'equalises number of running builds' do
- # after finishing the first build for project 1, get a second build from the same project
- expect(execute(shared_runner)).to eq(build1_project1)
- build1_project1.reload.success
- expect(execute(shared_runner)).to eq(build2_project1)
+ it 'does not pick the build and drops the build' do
+ expect(build_on(shared_runner)).to be_falsey
- expect(execute(shared_runner)).to eq(build1_project2)
- build1_project2.reload.success
- expect(execute(shared_runner)).to eq(build2_project2)
- expect(execute(shared_runner)).to eq(build1_project3)
- expect(execute(shared_runner)).to eq(build3_project1)
- end
+ expect(pending_job.reload).to be_user_blocked
end
end
- context 'when using DEFCON mode that disables fair scheduling' do
- before do
- stub_feature_flags(ci_queueing_disaster_recovery_disable_fair_scheduling: true)
- end
-
- context 'when all builds are pending' do
- it 'returns builds in order of creation (FIFO)' do
- # it gets for one build from each of the projects
- expect(execute(shared_runner)).to eq(build1_project1)
- expect(execute(shared_runner)).to eq(build2_project1)
- expect(execute(shared_runner)).to eq(build3_project1)
- expect(execute(shared_runner)).to eq(build1_project2)
- expect(execute(shared_runner)).to eq(build2_project2)
- expect(execute(shared_runner)).to eq(build1_project3)
+ context 'for multiple builds' do
+ let!(:project2) { create :project, shared_runners_enabled: true }
+ let!(:pipeline2) { create :ci_pipeline, project: project2 }
+ let!(:project3) { create :project, shared_runners_enabled: true }
+ let!(:pipeline3) { create :ci_pipeline, project: project3 }
+ let!(:build1_project1) { pending_job }
+ let!(:build2_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ let!(:build3_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ let!(:build1_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) }
+ let!(:build2_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) }
+ let!(:build1_project3) { create(:ci_build, :pending, :queued, pipeline: pipeline3) }
+
+ it 'picks builds one-by-one' do
+ expect(Ci::Build).to receive(:find).with(pending_job.id).and_call_original
+
+ expect(build_on(shared_runner)).to eq(build1_project1)
+ end
+
+ context 'when using fair scheduling' do
+ context 'when all builds are pending' do
+ it 'prefers projects without builds first' do
+ # it gets for one build from each of the projects
+ expect(build_on(shared_runner)).to eq(build1_project1)
+ expect(build_on(shared_runner)).to eq(build1_project2)
+ expect(build_on(shared_runner)).to eq(build1_project3)
+
+ # then it gets a second build from each of the projects
+ expect(build_on(shared_runner)).to eq(build2_project1)
+ expect(build_on(shared_runner)).to eq(build2_project2)
+
+ # in the end the third build
+ expect(build_on(shared_runner)).to eq(build3_project1)
+ end
+ end
+
+ context 'when some builds transition to success' do
+ it 'equalises number of running builds' do
+ # after finishing the first build for project 1, get a second build from the same project
+ expect(build_on(shared_runner)).to eq(build1_project1)
+ build1_project1.reload.success
+ expect(build_on(shared_runner)).to eq(build2_project1)
+
+ expect(build_on(shared_runner)).to eq(build1_project2)
+ build1_project2.reload.success
+ expect(build_on(shared_runner)).to eq(build2_project2)
+ expect(build_on(shared_runner)).to eq(build1_project3)
+ expect(build_on(shared_runner)).to eq(build3_project1)
+ end
end
end
- context 'when some builds transition to success' do
- it 'returns builds in order of creation (FIFO)' do
- expect(execute(shared_runner)).to eq(build1_project1)
- build1_project1.reload.success
- expect(execute(shared_runner)).to eq(build2_project1)
+ context 'when using DEFCON mode that disables fair scheduling' do
+ before do
+ stub_feature_flags(ci_queueing_disaster_recovery_disable_fair_scheduling: true)
+ end
+
+ context 'when all builds are pending' do
+ it 'returns builds in order of creation (FIFO)' do
+ # it gets for one build from each of the projects
+ expect(build_on(shared_runner)).to eq(build1_project1)
+ expect(build_on(shared_runner)).to eq(build2_project1)
+ expect(build_on(shared_runner)).to eq(build3_project1)
+ expect(build_on(shared_runner)).to eq(build1_project2)
+ expect(build_on(shared_runner)).to eq(build2_project2)
+ expect(build_on(shared_runner)).to eq(build1_project3)
+ end
+ end
- expect(execute(shared_runner)).to eq(build3_project1)
- build2_project1.reload.success
- expect(execute(shared_runner)).to eq(build1_project2)
- expect(execute(shared_runner)).to eq(build2_project2)
- expect(execute(shared_runner)).to eq(build1_project3)
+ context 'when some builds transition to success' do
+ it 'returns builds in order of creation (FIFO)' do
+ expect(build_on(shared_runner)).to eq(build1_project1)
+ build1_project1.reload.success
+ expect(build_on(shared_runner)).to eq(build2_project1)
+
+ expect(build_on(shared_runner)).to eq(build3_project1)
+ build2_project1.reload.success
+ expect(build_on(shared_runner)).to eq(build1_project2)
+ expect(build_on(shared_runner)).to eq(build2_project2)
+ expect(build_on(shared_runner)).to eq(build1_project3)
+ end
end
end
end
- end
- context 'shared runner' do
- let(:response) { described_class.new(shared_runner).execute }
- let(:build) { response.build }
+ context 'shared runner' do
+ let(:response) { described_class.new(shared_runner, nil).execute }
+ let(:build) { response.build }
- it { expect(build).to be_kind_of(Build) }
- it { expect(build).to be_valid }
- it { expect(build).to be_running }
- it { expect(build.runner).to eq(shared_runner) }
- it { expect(Gitlab::Json.parse(response.build_json)['id']).to eq(build.id) }
- end
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(shared_runner) }
+ it { expect(Gitlab::Json.parse(response.build_json)['id']).to eq(build.id) }
+ end
- context 'specific runner' do
- let(:build) { execute(specific_runner) }
+ context 'project runner' do
+ let(:build) { build_on(project_runner) }
- it { expect(build).to be_kind_of(Build) }
- it { expect(build).to be_valid }
- it { expect(build).to be_running }
- it { expect(build.runner).to eq(specific_runner) }
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(project_runner) }
+ end
end
- end
- context 'disallow shared runners' do
- before do
- project.update!(shared_runners_enabled: false)
- end
+ context 'disallow shared runners' do
+ before do
+ project.update!(shared_runners_enabled: false)
+ end
- context 'shared runner' do
- let(:build) { execute(shared_runner) }
+ context 'shared runner' do
+ let(:build) { build_on(shared_runner) }
- it { expect(build).to be_nil }
- end
+ it { expect(build).to be_nil }
+ end
- context 'specific runner' do
- let(:build) { execute(specific_runner) }
+ context 'project runner' do
+ let(:build) { build_on(project_runner) }
- it { expect(build).to be_kind_of(Build) }
- it { expect(build).to be_valid }
- it { expect(build).to be_running }
- it { expect(build.runner).to eq(specific_runner) }
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(project_runner) }
+ end
end
- end
- context 'disallow when builds are disabled' do
- before do
- project.update!(shared_runners_enabled: true, group_runners_enabled: true)
- project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
+ context 'disallow when builds are disabled' do
+ before do
+ project.update!(shared_runners_enabled: true, group_runners_enabled: true)
+ project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
- pending_job.reload.create_queuing_entry!
- end
+ pending_job.reload.create_queuing_entry!
+ end
- context 'and uses shared runner' do
- let(:build) { execute(shared_runner) }
+ context 'and uses shared runner' do
+ let(:build) { build_on(shared_runner) }
- it { expect(build).to be_nil }
- end
+ it { expect(build).to be_nil }
+ end
- context 'and uses group runner' do
- let(:build) { execute(group_runner) }
+ context 'and uses group runner' do
+ let(:build) { build_on(group_runner) }
- it { expect(build).to be_nil }
- end
+ it { expect(build).to be_nil }
+ end
- context 'and uses project runner' do
- let(:build) { execute(specific_runner) }
+ context 'and uses project runner' do
+ let(:build) { build_on(project_runner) }
- it 'does not pick a build' do
- expect(build).to be_nil
- expect(pending_job.reload).to be_failed
- expect(pending_job.queuing_entry).to be_nil
+ it 'does not pick a build' do
+ expect(build).to be_nil
+ expect(pending_job.reload).to be_failed
+ expect(pending_job.queuing_entry).to be_nil
+ end
end
end
- end
- context 'allow group runners' do
- before do
- project.update!(group_runners_enabled: true)
- end
+ context 'allow group runners' do
+ before do
+ project.update!(group_runners_enabled: true)
+ end
- context 'for multiple builds' do
- let!(:project2) { create(:project, group_runners_enabled: true, group: group) }
- let!(:pipeline2) { create(:ci_pipeline, project: project2) }
- let!(:project3) { create(:project, group_runners_enabled: true, group: group) }
- let!(:pipeline3) { create(:ci_pipeline, project: project3) }
+ context 'for multiple builds' do
+ let!(:project2) { create(:project, group_runners_enabled: true, group: group) }
+ let!(:pipeline2) { create(:ci_pipeline, project: project2) }
+ let!(:project3) { create(:project, group_runners_enabled: true, group: group) }
+ let!(:pipeline3) { create(:ci_pipeline, project: project3) }
- let!(:build1_project1) { pending_job }
- let!(:build2_project1) { create(:ci_build, :queued, pipeline: pipeline) }
- let!(:build3_project1) { create(:ci_build, :queued, pipeline: pipeline) }
- let!(:build1_project2) { create(:ci_build, :queued, pipeline: pipeline2) }
- let!(:build2_project2) { create(:ci_build, :queued, pipeline: pipeline2) }
- let!(:build1_project3) { create(:ci_build, :queued, pipeline: pipeline3) }
+ let!(:build1_project1) { pending_job }
+ let!(:build2_project1) { create(:ci_build, :queued, pipeline: pipeline) }
+ let!(:build3_project1) { create(:ci_build, :queued, pipeline: pipeline) }
+ let!(:build1_project2) { create(:ci_build, :queued, pipeline: pipeline2) }
+ let!(:build2_project2) { create(:ci_build, :queued, pipeline: pipeline2) }
+ let!(:build1_project3) { create(:ci_build, :queued, pipeline: pipeline3) }
- # these shouldn't influence the scheduling
- let!(:unrelated_group) { create(:group) }
- let!(:unrelated_project) { create(:project, group_runners_enabled: true, group: unrelated_group) }
- let!(:unrelated_pipeline) { create(:ci_pipeline, project: unrelated_project) }
- let!(:build1_unrelated_project) { create(:ci_build, :pending, :queued, pipeline: unrelated_pipeline) }
- let!(:unrelated_group_runner) { create(:ci_runner, :group, groups: [unrelated_group]) }
+ # these shouldn't influence the scheduling
+ let!(:unrelated_group) { create(:group) }
+ let!(:unrelated_project) { create(:project, group_runners_enabled: true, group: unrelated_group) }
+ let!(:unrelated_pipeline) { create(:ci_pipeline, project: unrelated_project) }
+ let!(:build1_unrelated_project) { create(:ci_build, :pending, :queued, pipeline: unrelated_pipeline) }
+ let!(:unrelated_group_runner) { create(:ci_runner, :group, groups: [unrelated_group]) }
- it 'does not consider builds from other group runners' do
- queue = ::Ci::Queue::BuildQueueService.new(group_runner)
+ it 'does not consider builds from other group runners' do
+ queue = ::Ci::Queue::BuildQueueService.new(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 6
- execute(group_runner)
+ expect(queue.builds_for_group_runner.size).to eq 6
+ build_on(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 5
- execute(group_runner)
+ expect(queue.builds_for_group_runner.size).to eq 5
+ build_on(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 4
- execute(group_runner)
+ expect(queue.builds_for_group_runner.size).to eq 4
+ build_on(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 3
- execute(group_runner)
+ expect(queue.builds_for_group_runner.size).to eq 3
+ build_on(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 2
- execute(group_runner)
+ expect(queue.builds_for_group_runner.size).to eq 2
+ build_on(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 1
- execute(group_runner)
+ expect(queue.builds_for_group_runner.size).to eq 1
+ build_on(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 0
- expect(execute(group_runner)).to be_nil
+ expect(queue.builds_for_group_runner.size).to eq 0
+ expect(build_on(group_runner)).to be_nil
+ end
end
- end
- context 'group runner' do
- let(:build) { execute(group_runner) }
+ context 'group runner' do
+ let(:build) { build_on(group_runner) }
- it { expect(build).to be_kind_of(Build) }
- it { expect(build).to be_valid }
- it { expect(build).to be_running }
- it { expect(build.runner).to eq(group_runner) }
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(group_runner) }
+ end
end
- end
- context 'disallow group runners' do
- before do
- project.update!(group_runners_enabled: false)
+ context 'disallow group runners' do
+ before do
+ project.update!(group_runners_enabled: false)
- pending_job.reload.create_queuing_entry!
- end
+ pending_job.reload.create_queuing_entry!
+ end
- context 'group runner' do
- let(:build) { execute(group_runner) }
+ context 'group runner' do
+ let(:build) { build_on(group_runner) }
- it { expect(build).to be_nil }
+ it { expect(build).to be_nil }
+ end
end
- end
- context 'when first build is stalled' do
- before do
- allow_any_instance_of(Ci::RegisterJobService).to receive(:assign_runner!).and_call_original
- allow_any_instance_of(Ci::RegisterJobService).to receive(:assign_runner!)
- .with(pending_job, anything).and_raise(ActiveRecord::StaleObjectError)
- end
+ context 'when first build is stalled' do
+ before do
+ allow_any_instance_of(Ci::RegisterJobService).to receive(:assign_runner!).and_call_original
+ allow_any_instance_of(Ci::RegisterJobService).to receive(:assign_runner!)
+ .with(pending_job, anything).and_raise(ActiveRecord::StaleObjectError)
+ end
- subject { described_class.new(specific_runner).execute }
+ subject { described_class.new(project_runner, nil).execute }
- context 'with multiple builds are in queue' do
- let!(:other_build) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ context 'with multiple builds are in queue' do
+ let!(:other_build) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- before do
- allow_any_instance_of(::Ci::Queue::BuildQueueService)
- .to receive(:execute)
- .and_return(Ci::Build.where(id: [pending_job, other_build]).pluck(:id))
- end
+ before do
+ allow_any_instance_of(::Ci::Queue::BuildQueueService)
+ .to receive(:execute)
+ .and_return(Ci::Build.where(id: [pending_job, other_build]).pluck(:id))
+ end
- it "receives second build from the queue" do
- expect(subject).to be_valid
- expect(subject.build).to eq(other_build)
+ it "receives second build from the queue" do
+ expect(subject).to be_valid
+ expect(subject.build).to eq(other_build)
+ end
end
- end
- context 'when single build is in queue' do
- before do
- allow_any_instance_of(::Ci::Queue::BuildQueueService)
- .to receive(:execute)
- .and_return(Ci::Build.where(id: pending_job).pluck(:id))
- end
+ context 'when single build is in queue' do
+ before do
+ allow_any_instance_of(::Ci::Queue::BuildQueueService)
+ .to receive(:execute)
+ .and_return(Ci::Build.where(id: pending_job).pluck(:id))
+ end
- it "does not receive any valid result" do
- expect(subject).not_to be_valid
+ it "does not receive any valid result" do
+ expect(subject).not_to be_valid
+ end
end
- end
- context 'when there is no build in queue' do
- before do
- allow_any_instance_of(::Ci::Queue::BuildQueueService)
- .to receive(:execute)
- .and_return([])
- end
+ context 'when there is no build in queue' do
+ before do
+ allow_any_instance_of(::Ci::Queue::BuildQueueService)
+ .to receive(:execute)
+ .and_return([])
+ end
- it "does not receive builds but result is valid" do
- expect(subject).to be_valid
- expect(subject.build).to be_nil
+ it "does not receive builds but result is valid" do
+ expect(subject).to be_valid
+ expect(subject.build).to be_nil
+ end
end
end
- end
- context 'when access_level of runner is not_protected' do
- let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
+ context 'when access_level of runner is not_protected' do
+ let!(:project_runner) { create(:ci_runner, :project, projects: [project]) }
- context 'when a job is protected' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) }
+ context 'when a job is protected' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) }
- it 'picks the job' do
- expect(execute(specific_runner)).to eq(pending_job)
+ it 'picks the job' do
+ expect(build_on(project_runner)).to eq(pending_job)
+ end
end
- end
- context 'when a job is unprotected' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ context 'when a job is unprotected' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- it 'picks the job' do
- expect(execute(specific_runner)).to eq(pending_job)
+ it 'picks the job' do
+ expect(build_on(project_runner)).to eq(pending_job)
+ end
end
- end
- context 'when protected attribute of a job is nil' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ context 'when protected attribute of a job is nil' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- before do
- pending_job.update_attribute(:protected, nil)
- end
+ before do
+ pending_job.update_attribute(:protected, nil)
+ end
- it 'picks the job' do
- expect(execute(specific_runner)).to eq(pending_job)
+ it 'picks the job' do
+ expect(build_on(project_runner)).to eq(pending_job)
+ end
end
end
- end
- context 'when access_level of runner is ref_protected' do
- let!(:specific_runner) { create(:ci_runner, :project, :ref_protected, projects: [project]) }
+ context 'when access_level of runner is ref_protected' do
+ let!(:project_runner) { create(:ci_runner, :project, :ref_protected, projects: [project]) }
- context 'when a job is protected' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) }
+ context 'when a job is protected' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) }
- it 'picks the job' do
- expect(execute(specific_runner)).to eq(pending_job)
+ it 'picks the job' do
+ expect(build_on(project_runner)).to eq(pending_job)
+ end
end
- end
- context 'when a job is unprotected' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ context 'when a job is unprotected' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- it 'does not pick the job' do
- expect(execute(specific_runner)).to be_nil
+ it 'does not pick the job' do
+ expect(build_on(project_runner)).to be_nil
+ end
end
- end
- context 'when protected attribute of a job is nil' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ context 'when protected attribute of a job is nil' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- before do
- pending_job.update_attribute(:protected, nil)
- end
+ before do
+ pending_job.update_attribute(:protected, nil)
+ end
- it 'does not pick the job' do
- expect(execute(specific_runner)).to be_nil
+ it 'does not pick the job' do
+ expect(build_on(project_runner)).to be_nil
+ end
end
end
- end
- context 'runner feature set is verified' do
- let(:options) { { artifacts: { reports: { junit: "junit.xml" } } } }
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) }
+ context 'runner feature set is verified' do
+ let(:options) { { artifacts: { reports: { junit: "junit.xml" } } } }
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) }
- subject { execute(specific_runner, params) }
+ subject { build_on(project_runner, params: params) }
- context 'when feature is missing by runner' do
- let(:params) { {} }
+ context 'when feature is missing by runner' do
+ let(:params) { {} }
- it 'does not pick the build and drops the build' do
- expect(subject).to be_nil
- expect(pending_job.reload).to be_failed
- expect(pending_job).to be_runner_unsupported
+ it 'does not pick the build and drops the build' do
+ expect(subject).to be_nil
+ expect(pending_job.reload).to be_failed
+ expect(pending_job).to be_runner_unsupported
+ end
end
- end
- context 'when feature is supported by runner' do
- let(:params) do
- { info: { features: { upload_multiple_artifacts: true } } }
- end
+ context 'when feature is supported by runner' do
+ let(:params) do
+ { info: { features: { upload_multiple_artifacts: true } } }
+ end
- it 'does pick job' do
- expect(subject).not_to be_nil
+ it 'does pick job' do
+ expect(subject).not_to be_nil
+ end
end
end
- end
-
- context 'when "dependencies" keyword is specified' do
- let!(:pre_stage_job) do
- create(:ci_build, :success, :artifacts, pipeline: pipeline, name: 'test', stage_idx: 0)
- end
- let!(:pending_job) do
- create(:ci_build, :pending, :queued,
- pipeline: pipeline, stage_idx: 1,
- options: { script: ["bash"], dependencies: dependencies })
- end
+ context 'when "dependencies" keyword is specified' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, :artifacts, pipeline: pipeline, name: 'test', stage_idx: 0)
+ end
- let(:dependencies) { %w[test] }
+ let!(:pending_job) do
+ create(:ci_build, :pending, :queued,
+ pipeline: pipeline, stage_idx: 1,
+ options: { script: ["bash"], dependencies: dependencies })
+ end
- subject { execute(specific_runner) }
+ let(:dependencies) { %w[test] }
- it 'picks a build with a dependency' do
- picked_build = execute(specific_runner)
+ subject { build_on(project_runner) }
- expect(picked_build).to be_present
- end
+ it 'picks a build with a dependency' do
+ picked_build = build_on(project_runner)
- context 'when there are multiple dependencies with artifacts' do
- let!(:pre_stage_job_second) do
- create(:ci_build, :success, :artifacts, pipeline: pipeline, name: 'deploy', stage_idx: 0)
+ expect(picked_build).to be_present
end
- let(:dependencies) { %w[test deploy] }
-
- it 'logs build artifacts size' do
- execute(specific_runner)
-
- artifacts_size = [pre_stage_job, pre_stage_job_second].sum do |job|
- job.job_artifacts_archive.size
+ context 'when there are multiple dependencies with artifacts' do
+ let!(:pre_stage_job_second) do
+ create(:ci_build, :success, :artifacts, pipeline: pipeline, name: 'deploy', stage_idx: 0)
end
- expect(artifacts_size).to eq 107464 * 2
- expect(Gitlab::ApplicationContext.current).to include({
- 'meta.artifacts_dependencies_size' => artifacts_size,
- 'meta.artifacts_dependencies_count' => 2
- })
- end
- end
+ let(:dependencies) { %w[test deploy] }
- shared_examples 'not pick' do
- it 'does not pick the build and drops the build' do
- expect(subject).to be_nil
- expect(pending_job.reload).to be_failed
- expect(pending_job).to be_missing_dependency_failure
- end
- end
+ it 'logs build artifacts size' do
+ build_on(project_runner)
- shared_examples 'validation is active' do
- context 'when depended job has not been completed yet' do
- let!(:pre_stage_job) { create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
+ artifacts_size = [pre_stage_job, pre_stage_job_second].sum do |job|
+ job.job_artifacts_archive.size
+ end
- it { is_expected.to eq(pending_job) }
+ expect(artifacts_size).to eq 107464 * 2
+ expect(Gitlab::ApplicationContext.current).to include({
+ 'meta.artifacts_dependencies_size' => artifacts_size,
+ 'meta.artifacts_dependencies_count' => 2
+ })
+ end
end
- context 'when artifacts of depended job has been expired' do
- let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
+ shared_examples 'not pick' do
+ it 'does not pick the build and drops the build' do
+ expect(subject).to be_nil
+ expect(pending_job.reload).to be_failed
+ expect(pending_job).to be_missing_dependency_failure
+ end
+ end
- context 'when the pipeline is locked' do
- before do
- pipeline.artifacts_locked!
+ shared_examples 'validation is active' do
+ context 'when depended job has not been completed yet' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0)
end
it { is_expected.to eq(pending_job) }
end
- context 'when the pipeline is unlocked' do
- before do
- pipeline.unlocked!
+ context 'when artifacts of depended job has been expired' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0)
end
- it_behaves_like 'not pick'
+ context 'when the pipeline is locked' do
+ before do
+ pipeline.artifacts_locked!
+ end
+
+ it { is_expected.to eq(pending_job) }
+ end
+
+ context 'when the pipeline is unlocked' do
+ before do
+ pipeline.unlocked!
+ end
+
+ it_behaves_like 'not pick'
+ end
end
- end
- context 'when artifacts of depended job has been erased' do
- let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
+ context 'when artifacts of depended job has been erased' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago)
+ end
- it_behaves_like 'not pick'
- end
+ it_behaves_like 'not pick'
+ end
- context 'when job object is staled' do
- let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
+ context 'when job object is staled' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0)
+ end
- before do
- pipeline.unlocked!
+ before do
+ pipeline.unlocked!
- allow_next_instance_of(Ci::Build) do |build|
- expect(build).to receive(:drop!)
- .and_raise(ActiveRecord::StaleObjectError.new(pending_job, :drop!))
+ allow_next_instance_of(Ci::Build) do |build|
+ expect(build).to receive(:drop!)
+ .and_raise(ActiveRecord::StaleObjectError.new(pending_job, :drop!))
+ end
end
- end
- it 'does not drop nor pick' do
- expect(subject).to be_nil
+ it 'does not drop nor pick' do
+ expect(subject).to be_nil
+ end
end
end
- end
- shared_examples 'validation is not active' do
- context 'when depended job has not been completed yet' do
- let!(:pre_stage_job) { create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
+ shared_examples 'validation is not active' do
+ context 'when depended job has not been completed yet' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0)
+ end
- it { expect(subject).to eq(pending_job) }
- end
+ it { expect(subject).to eq(pending_job) }
+ end
- context 'when artifacts of depended job has been expired' do
- let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
+ context 'when artifacts of depended job has been expired' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0)
+ end
- it { expect(subject).to eq(pending_job) }
- end
+ it { expect(subject).to eq(pending_job) }
+ end
- context 'when artifacts of depended job has been erased' do
- let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
+ context 'when artifacts of depended job has been erased' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago)
+ end
- it { expect(subject).to eq(pending_job) }
+ it { expect(subject).to eq(pending_job) }
+ end
end
- end
- it_behaves_like 'validation is active'
- end
+ it_behaves_like 'validation is active'
+ end
- context 'when build is degenerated' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) }
+ context 'when build is degenerated' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) }
- subject { execute(specific_runner, {}) }
+ subject { build_on(project_runner) }
- it 'does not pick the build and drops the build' do
- expect(subject).to be_nil
+ it 'does not pick the build and drops the build' do
+ expect(subject).to be_nil
- pending_job.reload
- expect(pending_job).to be_failed
- expect(pending_job).to be_archived_failure
+ pending_job.reload
+ expect(pending_job).to be_failed
+ expect(pending_job).to be_archived_failure
+ end
end
- end
- context 'when build has data integrity problem' do
- let!(:pending_job) do
- create(:ci_build, :pending, :queued, pipeline: pipeline)
- end
+ context 'when build has data integrity problem' do
+ let!(:pending_job) do
+ create(:ci_build, :pending, :queued, pipeline: pipeline)
+ end
- before do
- pending_job.update_columns(options: "string")
- end
+ before do
+ pending_job.update_columns(options: "string")
+ end
- subject { execute(specific_runner, {}) }
+ subject { build_on(project_runner) }
- it 'does drop the build and logs both failures' do
- expect(Gitlab::ErrorTracking).to receive(:track_exception)
- .with(anything, a_hash_including(build_id: pending_job.id))
- .twice
- .and_call_original
+ it 'does drop the build and logs both failures' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(anything, a_hash_including(build_id: pending_job.id))
+ .twice
+ .and_call_original
- expect(subject).to be_nil
+ expect(subject).to be_nil
- pending_job.reload
- expect(pending_job).to be_failed
- expect(pending_job).to be_data_integrity_failure
+ pending_job.reload
+ expect(pending_job).to be_failed
+ expect(pending_job).to be_data_integrity_failure
+ end
end
- end
- context 'when build fails to be run!' do
- let!(:pending_job) do
- create(:ci_build, :pending, :queued, pipeline: pipeline)
- end
+ context 'when build fails to be run!' do
+ let!(:pending_job) do
+ create(:ci_build, :pending, :queued, pipeline: pipeline)
+ end
- before do
- expect_any_instance_of(Ci::Build).to receive(:run!)
- .and_raise(RuntimeError, 'scheduler error')
- end
+ before do
+ expect_any_instance_of(Ci::Build).to receive(:run!)
+ .and_raise(RuntimeError, 'scheduler error')
+ end
- subject { execute(specific_runner, {}) }
+ subject { build_on(project_runner) }
- it 'does drop the build and logs failure' do
- expect(Gitlab::ErrorTracking).to receive(:track_exception)
- .with(anything, a_hash_including(build_id: pending_job.id))
- .once
- .and_call_original
+ it 'does drop the build and logs failure' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(anything, a_hash_including(build_id: pending_job.id))
+ .once
+ .and_call_original
- expect(subject).to be_nil
+ expect(subject).to be_nil
- pending_job.reload
- expect(pending_job).to be_failed
- expect(pending_job).to be_scheduler_failure
+ pending_job.reload
+ expect(pending_job).to be_failed
+ expect(pending_job).to be_scheduler_failure
+ end
end
- end
- context 'when an exception is raised during a persistent ref creation' do
- before do
- allow_any_instance_of(Ci::PersistentRef).to receive(:exist?) { false }
- allow_any_instance_of(Ci::PersistentRef).to receive(:create_ref) { raise ArgumentError }
- end
+ context 'when an exception is raised during a persistent ref creation' do
+ before do
+ allow_any_instance_of(Ci::PersistentRef).to receive(:exist?) { false }
+ allow_any_instance_of(Ci::PersistentRef).to receive(:create_ref) { raise ArgumentError }
+ end
- subject { execute(specific_runner, {}) }
+ subject { build_on(project_runner) }
- it 'picks the build' do
- expect(subject).to eq(pending_job)
+ it 'picks the build' do
+ expect(subject).to eq(pending_job)
- pending_job.reload
- expect(pending_job).to be_running
- end
- end
-
- context 'when only some builds can be matched by runner' do
- let!(:specific_runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[matching]) }
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[matching]) }
-
- before do
- # create additional matching and non-matching jobs
- create_list(:ci_build, 2, :pending, :queued, pipeline: pipeline, tag_list: %w[matching])
- create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[non-matching])
+ pending_job.reload
+ expect(pending_job).to be_running
+ end
end
- it 'observes queue size of only matching jobs' do
- # pending_job + 2 x matching ones
- expect(Gitlab::Ci::Queue::Metrics.queue_size_total).to receive(:observe)
- .with({ runner_type: specific_runner.runner_type }, 3)
+ context 'when only some builds can be matched by runner' do
+ let!(:project_runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[matching]) }
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[matching]) }
- expect(execute(specific_runner)).to eq(pending_job)
- end
+ before do
+ # create additional matching and non-matching jobs
+ create_list(:ci_build, 2, :pending, :queued, pipeline: pipeline, tag_list: %w[matching])
+ create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[non-matching])
+ end
- it 'observes queue processing time by the runner type' do
- expect(Gitlab::Ci::Queue::Metrics.queue_iteration_duration_seconds)
- .to receive(:observe)
- .with({ runner_type: specific_runner.runner_type }, anything)
+ it 'observes queue size of only matching jobs' do
+ # pending_job + 2 x matching ones
+ expect(Gitlab::Ci::Queue::Metrics.queue_size_total).to receive(:observe)
+ .with({ runner_type: project_runner.runner_type }, 3)
- expect(Gitlab::Ci::Queue::Metrics.queue_retrieval_duration_seconds)
- .to receive(:observe)
- .with({ runner_type: specific_runner.runner_type }, anything)
+ expect(build_on(project_runner)).to eq(pending_job)
+ end
- expect(execute(specific_runner)).to eq(pending_job)
- end
- end
+ it 'observes queue processing time by the runner type' do
+ expect(Gitlab::Ci::Queue::Metrics.queue_iteration_duration_seconds)
+ .to receive(:observe)
+ .with({ runner_type: project_runner.runner_type }, anything)
- context 'when ci_register_job_temporary_lock is enabled' do
- before do
- stub_feature_flags(ci_register_job_temporary_lock: true)
+ expect(Gitlab::Ci::Queue::Metrics.queue_retrieval_duration_seconds)
+ .to receive(:observe)
+ .with({ runner_type: project_runner.runner_type }, anything)
- allow(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
+ expect(build_on(project_runner)).to eq(pending_job)
+ end
end
- context 'when a build is temporarily locked' do
- let(:service) { described_class.new(specific_runner) }
-
+ context 'when ci_register_job_temporary_lock is enabled' do
before do
- service.send(:acquire_temporary_lock, pending_job.id)
- end
-
- it 'skips this build and marks queue as invalid' do
- expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
- .with(operation: :queue_iteration)
- expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
- .with(operation: :build_temporary_locked)
+ stub_feature_flags(ci_register_job_temporary_lock: true)
- expect(service.execute).not_to be_valid
+ allow(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
end
- context 'when there is another build in queue' do
- let!(:next_pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ context 'when a build is temporarily locked' do
+ let(:service) { described_class.new(project_runner, nil) }
- it 'skips this build and picks another build' do
+ before do
+ service.send(:acquire_temporary_lock, pending_job.id)
+ end
+
+ it 'skips this build and marks queue as invalid' do
expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
- .with(operation: :queue_iteration).twice
+ .with(operation: :queue_iteration)
expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
.with(operation: :build_temporary_locked)
- result = service.execute
+ expect(service.execute).not_to be_valid
+ end
+
+ context 'when there is another build in queue' do
+ let!(:next_pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+
+ it 'skips this build and picks another build' do
+ expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
+ .with(operation: :queue_iteration).twice
+ expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
+ .with(operation: :build_temporary_locked)
- expect(result.build).to eq(next_pending_job)
- expect(result).to be_valid
+ result = service.execute
+
+ expect(result.build).to eq(next_pending_job)
+ expect(result).to be_valid
+ end
end
end
end
end
- end
-
- context 'when using pending builds table' do
- include_examples 'handles runner assignment'
- context 'when a conflicting data is stored in denormalized table' do
- let!(:specific_runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[conflict]) }
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[conflict]) }
+ context 'when using pending builds table' do
+ include_examples 'handles runner assignment'
- before do
- pending_job.update_column(:status, :running)
- end
+ context 'when a conflicting data is stored in denormalized table' do
+ let!(:runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[conflict]) }
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[conflict]) }
- it 'removes queuing entry upon build assignment attempt' do
- expect(pending_job.reload).to be_running
- expect(pending_job.queuing_entry).to be_present
+ before do
+ pending_job.update_column(:status, :running)
+ end
- result = described_class.new(specific_runner).execute
+ it 'removes queuing entry upon build assignment attempt' do
+ expect(pending_job.reload).to be_running
+ expect(pending_job.queuing_entry).to be_present
- expect(result).not_to be_valid
- expect(pending_job.reload.queuing_entry).not_to be_present
+ expect(execute).not_to be_valid
+ expect(pending_job.reload.queuing_entry).not_to be_present
+ end
end
end
end
@@ -807,11 +842,11 @@ module Ci
# Stub tested metrics
allow(Gitlab::Ci::Queue::Metrics)
.to receive(:attempt_counter)
- .and_return(attempt_counter)
+ .and_return(attempt_counter)
allow(Gitlab::Ci::Queue::Metrics)
.to receive(:job_queue_duration_seconds)
- .and_return(job_queue_duration_seconds)
+ .and_return(job_queue_duration_seconds)
project.update!(shared_runners_enabled: true)
pending_job.update!(created_at: current_time - 3600, queued_at: current_time - 1800)
@@ -822,7 +857,7 @@ module Ci
allow(job_queue_duration_seconds).to receive(:observe)
expect(attempt_counter).to receive(:increment)
- execute(runner)
+ build_on(runner)
end
end
@@ -834,7 +869,7 @@ module Ci
jobs_running_for_project: expected_jobs_running_for_project_first_job,
shard: expected_shard }, 1800)
- execute(runner)
+ build_on(runner)
end
context 'when project already has running jobs' do
@@ -854,7 +889,7 @@ module Ci
jobs_running_for_project: expected_jobs_running_for_project_third_job,
shard: expected_shard }, 1800)
- execute(runner)
+ build_on(runner)
end
end
end
@@ -913,12 +948,12 @@ module Ci
allow(attempt_counter).to receive(:increment)
expect(job_queue_duration_seconds).not_to receive(:observe)
- execute(runner)
+ build_on(runner)
end
end
end
- context 'when specific runner is used' do
+ context 'when project runner is used' do
let(:runner) { create(:ci_runner, :project, projects: [project], tag_list: %w(tag1 metrics_shard::shard_tag tag2)) }
let(:expected_shared_runner) { false }
let(:expected_shard) { ::Gitlab::Ci::Queue::Metrics::DEFAULT_METRICS_SHARD }
@@ -933,12 +968,12 @@ module Ci
it 'present sets runner session configuration in the build' do
runner_session_params = { session: { 'url' => 'https://example.com' } }
- expect(execute(specific_runner, runner_session_params).runner_session.attributes)
+ expect(build_on(project_runner, params: runner_session_params).runner_session.attributes)
.to include(runner_session_params[:session])
end
it 'not present it does not configure the runner session' do
- expect(execute(specific_runner).runner_session).to be_nil
+ expect(build_on(project_runner).runner_session).to be_nil
end
end
@@ -954,7 +989,7 @@ module Ci
it 'returns 409 conflict' do
expect(Ci::Build.pending.unstarted.count).to eq 3
- result = described_class.new(specific_runner).execute
+ result = described_class.new(project_runner, nil).execute
expect(result).not_to be_valid
expect(result.build).to be_nil
@@ -962,8 +997,8 @@ module Ci
end
end
- def execute(runner, params = {})
- described_class.new(runner).execute(params).build
+ def build_on(runner, runner_machine: nil, params: {})
+ described_class.new(runner, runner_machine).execute(params).build
end
end
end
diff --git a/spec/services/ci/retry_job_service_spec.rb b/spec/services/ci/retry_job_service_spec.rb
index c3d80f2cb56..10acf032b1a 100644
--- a/spec/services/ci/retry_job_service_spec.rb
+++ b/spec/services/ci/retry_job_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::RetryJobService do
+RSpec.describe Ci::RetryJobService, feature_category: :continuous_integration do
using RSpec::Parameterized::TableSyntax
let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
@@ -27,6 +27,22 @@ 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)
+ end
+ end
+
shared_context 'retryable build' do
let_it_be_with_reload(:job) do
create(:ci_build, :success, pipeline: pipeline, ci_stage: stage)
@@ -102,6 +118,14 @@ RSpec.describe Ci::RetryJobService do
end
end
+ shared_examples_for 'does not retry the job' do
+ it 'returns :not_retryable and :unprocessable_entity' do
+ expect(subject.message).to be('Job cannot be retried')
+ expect(subject.payload[:reason]).to eq(:not_retryable)
+ expect(subject.payload[:job]).to eq(job)
+ end
+ end
+
shared_examples_for 'retries the job' do
it_behaves_like 'clones the job'
@@ -189,6 +213,20 @@ 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'
@@ -287,7 +325,33 @@ RSpec.describe Ci::RetryJobService do
subject { service.execute(job) }
+ context 'when the job to be retried is a bridge' do
+ context 'and it is not retryable' do
+ let_it_be(:job) { create(:ci_bridge, :failed, :reached_max_descendant_pipelines_depth) }
+
+ it_behaves_like 'does not retry the job'
+ end
+
+ include_context 'retryable bridge'
+
+ it_behaves_like 'retries the job'
+
+ context 'when given variables' do
+ let(:new_job) { service.clone!(job, variables: job_variables_attributes) }
+
+ it 'does not give variables to the new bridge' do
+ expect { new_job }.not_to raise_error
+ end
+ end
+ end
+
context 'when the job to be retried is a build' do
+ context 'and it is not retryable' do
+ let_it_be(:job) { create(:ci_build, :deployment_rejected, pipeline: pipeline) }
+
+ it_behaves_like 'does not retry the job'
+ end
+
include_context 'retryable build'
it_behaves_like 'retries the job'
diff --git a/spec/services/ci/runners/create_runner_service_spec.rb b/spec/services/ci/runners/create_runner_service_spec.rb
new file mode 100644
index 00000000000..673bf3ef90e
--- /dev/null
+++ b/spec/services/ci/runners/create_runner_service_spec.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ci::Runners::CreateRunnerService, "#execute", feature_category: :runner_fleet do
+ subject(:execute) { described_class.new(user: current_user, type: type, params: params).execute }
+
+ let(:runner) { execute.payload[:runner] }
+
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:non_admin_user) { create(:user) }
+ let_it_be(:anonymous) { nil }
+
+ shared_context 'when admin user' do
+ let(:current_user) { admin }
+
+ before do
+ allow(current_user).to receive(:can?).with(:create_instance_runners).and_return true
+ end
+ end
+
+ shared_examples 'it can create a runner' do
+ it 'creates a runner of the specified type' do
+ expect(runner.runner_type).to eq expected_type
+ end
+
+ context 'with default params provided' do
+ let(:args) do
+ {}
+ end
+
+ before do
+ params.merge!(args)
+ end
+
+ it { is_expected.to be_success }
+
+ it 'uses default values when none are provided' do
+ expect(runner).to be_an_instance_of(::Ci::Runner)
+ expect(runner.persisted?).to be_truthy
+ expect(runner.run_untagged).to be true
+ expect(runner.active).to be true
+ expect(runner.creator).to be current_user
+ expect(runner.authenticated_user_registration_type?).to be_truthy
+ expect(runner.runner_type).to eq 'instance_type'
+ end
+ end
+
+ context 'with non-default params provided' do
+ let(:args) do
+ {
+ description: 'some description',
+ maintenance_note: 'a note',
+ paused: true,
+ tag_list: %w[tag1 tag2],
+ access_level: 'ref_protected',
+ locked: true,
+ maximum_timeout: 600,
+ run_untagged: false
+ }
+ end
+
+ before do
+ params.merge!(args)
+ end
+
+ it { is_expected.to be_success }
+
+ it 'creates runner with specified values', :aggregate_failures do
+ expect(runner).to be_an_instance_of(::Ci::Runner)
+ expect(runner.description).to eq 'some description'
+ expect(runner.maintenance_note).to eq 'a note'
+ expect(runner.active).to eq !args[:paused]
+ expect(runner.locked).to eq args[:locked]
+ expect(runner.run_untagged).to eq args[:run_untagged]
+ expect(runner.tags).to contain_exactly(
+ an_object_having_attributes(name: 'tag1'),
+ an_object_having_attributes(name: 'tag2')
+ )
+ expect(runner.access_level).to eq args[:access_level]
+ expect(runner.maximum_timeout).to eq args[:maximum_timeout]
+
+ expect(runner.authenticated_user_registration_type?).to be_truthy
+ expect(runner.runner_type).to eq 'instance_type'
+ end
+ end
+ end
+
+ shared_examples 'it cannot create a runner' do
+ it 'runner payload is nil' do
+ expect(runner).to be nil
+ end
+
+ it { is_expected.to be_error }
+ end
+
+ shared_examples 'it can return an error' do
+ let(:group) { create(:group) }
+ let(:runner_double) { Ci::Runner.new }
+
+ context 'when the runner fails to save' do
+ before do
+ allow(Ci::Runner).to receive(:new).and_return runner_double
+ end
+
+ it_behaves_like 'it cannot create a runner'
+
+ it 'returns error message' do
+ expect(execute.errors).not_to be_empty
+ end
+ end
+ end
+
+ context 'with type param set to nil' do
+ let(:expected_type) { 'instance_type' }
+ let(:type) { nil }
+ let(:params) { {} }
+
+ it_behaves_like 'it cannot create a runner' do
+ let(:current_user) { anonymous }
+ end
+
+ it_behaves_like 'it cannot create a runner' do
+ let(:current_user) { non_admin_user }
+ end
+
+ it_behaves_like 'it can create a runner' do
+ include_context 'when admin user'
+ end
+
+ it_behaves_like 'it can return an error' do
+ include_context 'when admin user'
+ end
+ end
+end
diff --git a/spec/services/ci/runners/process_runner_version_update_service_spec.rb b/spec/services/ci/runners/process_runner_version_update_service_spec.rb
index d2a7e87b2d5..e62cb1ec3e3 100644
--- a/spec/services/ci/runners/process_runner_version_update_service_spec.rb
+++ b/spec/services/ci/runners/process_runner_version_update_service_spec.rb
@@ -53,14 +53,14 @@ RSpec.describe Ci::Runners::ProcessRunnerVersionUpdateService, feature_category:
end
context 'with existing ci_runner_version record' do
- let!(:runner_version) { create(:ci_runner_version, version: '1.0.0', status: :not_available) }
+ let!(:runner_version) { create(:ci_runner_version, version: '1.0.0', status: :unavailable) }
it 'updates ci_runner_versions record', :aggregate_failures do
expect do
expect(execute).to be_success
expect(execute.http_status).to eq :ok
expect(execute.payload).to eq({ upgrade_status: 'recommended' })
- end.to change { runner_version.reload.status }.from('not_available').to('recommended')
+ end.to change { runner_version.reload.status }.from('unavailable').to('recommended')
end
end
diff --git a/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb b/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb
index 39082b5c0f4..8d7e97e5ea8 100644
--- a/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb
+++ b/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
let_it_be(:runner_14_0_1) { create(:ci_runner, version: '14.0.1') }
let_it_be(:runner_version_14_0_1) do
- create(:ci_runner_version, version: '14.0.1', status: :not_available)
+ create(:ci_runner_version, version: '14.0.1', status: :unavailable)
end
context 'with RunnerUpgradeCheck recommending 14.0.2' do
@@ -23,15 +23,17 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
context 'with runner with new version' do
let!(:runner_14_0_2) { create(:ci_runner, version: '14.0.2') }
- let!(:runner_version_14_0_0) { create(:ci_runner_version, version: '14.0.0', status: :not_available) }
let!(:runner_14_0_0) { create(:ci_runner, version: '14.0.0') }
+ let!(:runner_version_14_0_0) do
+ create(:ci_runner_version, version: '14.0.0', status: :unavailable)
+ end
before do
allow(upgrade_check).to receive(:check_runner_upgrade_suggestion)
.and_return([::Gitlab::VersionInfo.new(14, 0, 2), :recommended])
allow(upgrade_check).to receive(:check_runner_upgrade_suggestion)
.with('14.0.2')
- .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :not_available])
+ .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :unavailable])
.once
end
@@ -43,9 +45,9 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
.and_call_original
expect { execute }
- .to change { runner_version_14_0_0.reload.status }.from('not_available').to('recommended')
- .and change { runner_version_14_0_1.reload.status }.from('not_available').to('recommended')
- .and change { ::Ci::RunnerVersion.find_by(version: '14.0.2')&.status }.from(nil).to('not_available')
+ .to change { runner_version_14_0_0.reload.status }.from('unavailable').to('recommended')
+ .and change { runner_version_14_0_1.reload.status }.from('unavailable').to('recommended')
+ .and change { ::Ci::RunnerVersion.find_by(version: '14.0.2')&.status }.from(nil).to('unavailable')
expect(execute).to be_success
expect(execute.payload).to eq({
@@ -57,17 +59,19 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
end
context 'with orphan ci_runner_version' do
- let!(:runner_version_14_0_2) { create(:ci_runner_version, version: '14.0.2', status: :not_available) }
+ let!(:runner_version_14_0_2) do
+ create(:ci_runner_version, version: '14.0.2', status: :unavailable)
+ end
before do
allow(upgrade_check).to receive(:check_runner_upgrade_suggestion)
- .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :not_available])
+ .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :unavailable])
end
it 'deletes orphan ci_runner_versions entry', :aggregate_failures do
expect { execute }
- .to change { ::Ci::RunnerVersion.find_by_version('14.0.2')&.status }.from('not_available').to(nil)
- .and not_change { runner_version_14_0_1.reload.status }.from('not_available')
+ .to change { ::Ci::RunnerVersion.find_by_version('14.0.2')&.status }.from('unavailable').to(nil)
+ .and not_change { runner_version_14_0_1.reload.status }.from('unavailable')
expect(execute).to be_success
expect(execute.payload).to eq({
@@ -81,11 +85,11 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
context 'with no runner version changes' do
before do
allow(upgrade_check).to receive(:check_runner_upgrade_suggestion)
- .and_return([::Gitlab::VersionInfo.new(14, 0, 1), :not_available])
+ .and_return([::Gitlab::VersionInfo.new(14, 0, 1), :unavailable])
end
it 'does not modify ci_runner_versions entries', :aggregate_failures do
- expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available')
+ expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('unavailable')
expect(execute).to be_success
expect(execute.payload).to eq({
@@ -103,7 +107,7 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
end
it 'makes no changes to ci_runner_versions', :aggregate_failures do
- expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available')
+ expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('unavailable')
expect(execute).to be_success
expect(execute.payload).to eq({
@@ -121,7 +125,7 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
end
it 'does not modify ci_runner_versions entries', :aggregate_failures do
- expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available')
+ expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('unavailable')
expect(execute).to be_success
expect(execute.payload).to eq({
diff --git a/spec/services/ci/runners/register_runner_service_spec.rb b/spec/services/ci/runners/register_runner_service_spec.rb
index 47d399cb19a..c67040e45eb 100644
--- a/spec/services/ci/runners/register_runner_service_spec.rb
+++ b/spec/services/ci/runners/register_runner_service_spec.rb
@@ -47,6 +47,7 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute', feature_categor
expect(runner.run_untagged).to be true
expect(runner.active).to be true
expect(runner.token).not_to eq(registration_token)
+ expect(runner.token).not_to start_with(::Ci::Runner::CREATED_RUNNER_TOKEN_PREFIX)
expect(runner).to be_instance_type
end
diff --git a/spec/services/ci/runners/stale_machines_cleanup_service_spec.rb b/spec/services/ci/runners/stale_machines_cleanup_service_spec.rb
new file mode 100644
index 00000000000..456dbcebb84
--- /dev/null
+++ b/spec/services/ci/runners/stale_machines_cleanup_service_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Runners::StaleMachinesCleanupService, feature_category: :runner_fleet do
+ let(:service) { described_class.new }
+ let!(:runner_machine3) { create(:ci_runner_machine, created_at: 6.months.ago, contacted_at: Time.current) }
+
+ subject(:response) { service.execute }
+
+ context 'with no stale runner machines' do
+ it 'does not clean any runner machines and returns :success status' do
+ expect do
+ expect(response).to be_success
+ expect(response.payload).to match({ deleted_machines: false })
+ end.not_to change { Ci::RunnerMachine.count }.from(1)
+ end
+ end
+
+ context 'with some stale runner machines' do
+ before do
+ create(:ci_runner_machine, :stale)
+ create(:ci_runner_machine, :stale, contacted_at: nil)
+ end
+
+ it 'only leaves non-stale runners' do
+ expect(response).to be_success
+ expect(response.payload).to match({ deleted_machines: true })
+ expect(Ci::RunnerMachine.all).to contain_exactly(runner_machine3)
+ end
+
+ context 'with more stale runners than MAX_DELETIONS' do
+ before do
+ stub_const("#{described_class}::MAX_DELETIONS", 1)
+ end
+
+ it 'only leaves non-stale runners' do
+ expect do
+ expect(response).to be_success
+ expect(response.payload).to match({ deleted_machines: true })
+ end.to change { Ci::RunnerMachine.count }.by(-Ci::Runners::StaleMachinesCleanupService::MAX_DELETIONS)
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/update_build_queue_service_spec.rb b/spec/services/ci/update_build_queue_service_spec.rb
index d3f537a1aa0..dd26339831c 100644
--- a/spec/services/ci/update_build_queue_service_spec.rb
+++ b/spec/services/ci/update_build_queue_service_spec.rb
@@ -277,7 +277,7 @@ RSpec.describe Ci::UpdateBuildQueueService do
end
end
- context 'when updating specific runners' do
+ context 'when updating project runners' do
let(:runner) { create(:ci_runner, :project, projects: [project]) }
it_behaves_like 'matching build'
diff --git a/spec/services/ci/update_build_state_service_spec.rb b/spec/services/ci/update_build_state_service_spec.rb
index 90a86e7ae59..f8ecff20728 100644
--- a/spec/services/ci/update_build_state_service_spec.rb
+++ b/spec/services/ci/update_build_state_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::UpdateBuildStateService do
+RSpec.describe Ci::UpdateBuildStateService, feature_category: :continuous_integration do
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
diff --git a/spec/services/clusters/agents/refresh_authorization_service_spec.rb b/spec/services/clusters/agents/refresh_authorization_service_spec.rb
index fa38bc202e7..51c054ddc98 100644
--- a/spec/services/clusters/agents/refresh_authorization_service_spec.rb
+++ b/spec/services/clusters/agents/refresh_authorization_service_spec.rb
@@ -2,17 +2,17 @@
require 'spec_helper'
-RSpec.describe Clusters::Agents::RefreshAuthorizationService do
+RSpec.describe Clusters::Agents::RefreshAuthorizationService, feature_category: :kubernetes_management do
describe '#execute' do
let_it_be(:root_ancestor) { create(:group) }
let_it_be(:removed_group) { create(:group, parent: root_ancestor) }
let_it_be(:modified_group) { create(:group, parent: root_ancestor) }
- let_it_be(:added_group) { create(:group, parent: root_ancestor) }
+ let_it_be(:added_group) { create(:group, path: 'group-path-with-UPPERCASE', parent: root_ancestor) }
let_it_be(:removed_project) { create(:project, namespace: root_ancestor) }
let_it_be(:modified_project) { create(:project, namespace: root_ancestor) }
- let_it_be(:added_project) { create(:project, namespace: root_ancestor) }
+ let_it_be(:added_project) { create(:project, path: 'project-path-with-UPPERCASE', namespace: root_ancestor) }
let(:project) { create(:project, namespace: root_ancestor) }
let(:agent) { create(:cluster_agent, project: project) }
@@ -22,11 +22,13 @@ RSpec.describe Clusters::Agents::RefreshAuthorizationService do
ci_access: {
groups: [
{ id: added_group.full_path, default_namespace: 'default' },
- { id: modified_group.full_path, default_namespace: 'new-namespace' }
+ # Uppercase path verifies case-insensitive matching.
+ { id: modified_group.full_path.upcase, default_namespace: 'new-namespace' }
],
projects: [
{ id: added_project.full_path, default_namespace: 'default' },
- { id: modified_project.full_path, default_namespace: 'new-namespace' }
+ # Uppercase path verifies case-insensitive matching.
+ { id: modified_project.full_path.upcase, default_namespace: 'new-namespace' }
]
}
}.deep_stringify_keys
diff --git a/spec/services/concerns/rate_limited_service_spec.rb b/spec/services/concerns/rate_limited_service_spec.rb
index 04007e8e75a..d913cd17067 100644
--- a/spec/services/concerns/rate_limited_service_spec.rb
+++ b/spec/services/concerns/rate_limited_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe RateLimitedService do
let(:key) { :issues_create }
- let(:scope) { [:project, :current_user] }
+ let(:scope) { [:container, :current_user] }
let(:opts) { { scope: scope, users_allowlist: -> { [User.support_bot.username] } } }
let(:rate_limiter) { ::Gitlab::ApplicationRateLimiter }
@@ -39,7 +39,7 @@ RSpec.describe RateLimitedService do
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
- let(:service) { instance_double(Issues::CreateService, project: project, current_user: current_user) }
+ let(:service) { instance_double(Issues::CreateService, container: project, current_user: current_user) }
let(:evaluated_scope) { [project, current_user] }
let(:evaluated_opts) { { scope: evaluated_scope, users_allowlist: %w[support-bot] } }
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index e60954a19ed..b969bd76053 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -319,7 +319,6 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:category) { described_class.to_s }
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(:property) { 'project_action' }
end
@@ -346,7 +345,6 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:category) { described_class.to_s }
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(:property) { 'project_action' }
end
diff --git a/spec/services/export_csv/base_service_spec.rb b/spec/services/export_csv/base_service_spec.rb
new file mode 100644
index 00000000000..e2b4d4829af
--- /dev/null
+++ b/spec/services/export_csv/base_service_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ExportCsv::BaseService, feature_category: :importers do
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:relation) { Issue.all }
+ let_it_be(:resource_parent) { issue.project }
+
+ subject { described_class.new(relation, resource_parent) }
+
+ describe '#email' do
+ it 'raises NotImplementedError' do
+ user = create(:user)
+
+ expect { subject.email(user) }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#header_to_value_hash' do
+ it 'raises NotImplementedError' do
+ expect { subject.send(:header_to_value_hash) }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#associations_to_preload' do
+ it 'return []' do
+ expect(subject.send(:associations_to_preload)).to eq([])
+ end
+ end
+end
diff --git a/spec/services/export_csv/map_export_fields_service_spec.rb b/spec/services/export_csv/map_export_fields_service_spec.rb
new file mode 100644
index 00000000000..0060baf30e4
--- /dev/null
+++ b/spec/services/export_csv/map_export_fields_service_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ExportCsv::MapExportFieldsService, feature_category: :team_planning do
+ let(:selected_fields) { ['Title', 'Author username', 'state'] }
+ let(:invalid_fields) { ['Title', 'Author Username', 'State', 'Invalid Field', 'Other Field'] }
+ let(:data) do
+ {
+ 'Requirement ID' => '1',
+ 'Title' => 'foo',
+ 'Description' => 'bar',
+ 'Author' => 'root',
+ 'Author Username' => 'admin',
+ 'Created At (UTC)' => '2023-02-01 15:16:35',
+ 'State' => 'opened',
+ 'State Updated At (UTC)' => '2023-02-01 15:16:35'
+ }
+ end
+
+ describe '#execute' do
+ it 'returns a hash with selected fields only' do
+ result = described_class.new(selected_fields, data).execute
+
+ expect(result).to be_a(Hash)
+ expect(result.keys).to match_array(selected_fields.map(&:titleize))
+ end
+
+ context 'when the fields collection is empty' do
+ it 'returns a hash with all fields' do
+ result = described_class.new([], data).execute
+
+ expect(result).to be_a(Hash)
+ expect(result.keys).to match_array(data.keys)
+ end
+ end
+
+ context 'when fields collection includes invalid fields' do
+ it 'returns a hash with valid selected fields only' do
+ result = described_class.new(invalid_fields, data).execute
+
+ expect(result).to be_a(Hash)
+ expect(result.keys).to eq(selected_fields.map(&:titleize))
+ end
+ end
+ end
+
+ describe '#invalid_fields' do
+ it 'returns an array containing invalid fields' do
+ result = described_class.new(invalid_fields, data).invalid_fields
+
+ expect(result).to match_array(['Invalid Field', 'Other Field'])
+ end
+ end
+end
diff --git a/spec/services/git/wiki_push_service_spec.rb b/spec/services/git/wiki_push_service_spec.rb
index 878a5c4ccf0..b076b2d51ef 100644
--- a/spec/services/git/wiki_push_service_spec.rb
+++ b/spec/services/git/wiki_push_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Git::WikiPushService, services: true do
+RSpec.describe Git::WikiPushService, services: true, feature_category: :wiki do
include RepoHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/services/google_cloud/fetch_google_ip_list_service_spec.rb b/spec/services/google_cloud/fetch_google_ip_list_service_spec.rb
index ef77958fa60..e5f06824b9f 100644
--- a/spec/services/google_cloud/fetch_google_ip_list_service_spec.rb
+++ b/spec/services/google_cloud/fetch_google_ip_list_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GoogleCloud::FetchGoogleIpListService, :use_clean_rails_memory_store_caching,
-:clean_gitlab_redis_rate_limiting, feature_category: :continuous_integration do
+:clean_gitlab_redis_rate_limiting, feature_category: :build_artifacts do
include StubRequests
let(:google_cloud_ips) { File.read(Rails.root.join('spec/fixtures/cdn/google_cloud.json')) }
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index 0425ba3e631..84794b5f6f8 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::CreateService, '#execute' do
+RSpec.describe Groups::CreateService, '#execute', feature_category: :subgroups do
let!(:user) { create(:user) }
let!(:group_params) { { path: "group_path", visibility_level: Gitlab::VisibilityLevel::PUBLIC } }
@@ -78,10 +78,6 @@ RSpec.describe Groups::CreateService, '#execute' do
it { is_expected.to be_persisted }
- it 'adds an onboarding progress record' do
- expect { subject }.to change(Onboarding::Progress, :count).from(0).to(1)
- end
-
context 'with before_commit callback' do
it_behaves_like 'has sync-ed traversal_ids'
end
@@ -107,10 +103,6 @@ RSpec.describe Groups::CreateService, '#execute' do
it { is_expected.to be_persisted }
- it 'does not add an onboarding progress record' do
- expect { subject }.not_to change(Onboarding::Progress, :count).from(0)
- end
-
it_behaves_like 'has sync-ed traversal_ids'
end
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index 2791203f395..7c3710aeeb2 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::DestroyService do
+RSpec.describe Groups::DestroyService, feature_category: :subgroups do
let!(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
diff --git a/spec/services/groups/group_links/destroy_service_spec.rb b/spec/services/groups/group_links/destroy_service_spec.rb
index 03de7175edd..a570c28cf8b 100644
--- a/spec/services/groups/group_links/destroy_service_spec.rb
+++ b/spec/services/groups/group_links/destroy_service_spec.rb
@@ -70,8 +70,8 @@ RSpec.describe Groups::GroupLinks::DestroyService, '#execute' do
it 'updates project authorization once per group' do
expect(GroupGroupLink).to receive(:delete).and_call_original
- expect(group).to receive(:refresh_members_authorized_projects).with(direct_members_only: true, blocking: false).once
- expect(another_group).to receive(:refresh_members_authorized_projects).with(direct_members_only: true, blocking: false).once
+ expect(group).to receive(:refresh_members_authorized_projects).with(direct_members_only: true).once
+ expect(another_group).to receive(:refresh_members_authorized_projects).with(direct_members_only: true).once
subject.execute(links)
end
diff --git a/spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_spec.rb b/spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_spec.rb
index 8565299b9b7..a28a552746f 100644
--- a/spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_spec.rb
+++ b/spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Import::GitlabProjects::FileAcquisitionStrategies::RemoteFile, :aggregate_failures do
+RSpec.describe ::Import::GitlabProjects::FileAcquisitionStrategies::RemoteFile, :aggregate_failures, feature_category: :importers do
let(:remote_url) { 'https://external.file.path/file.tar.gz' }
let(:params) { { remote_import_url: remote_url } }
@@ -40,23 +40,19 @@ RSpec.describe ::Import::GitlabProjects::FileAcquisitionStrategies::RemoteFile,
end
end
- context 'when import_project_from_remote_file_s3 is enabled' do
- before do
- stub_feature_flags(import_project_from_remote_file_s3: true)
- end
-
- context 'when the HTTP request fail to recover the headers' do
- it 'adds the error message' do
- expect(Gitlab::HTTP)
- .to receive(:head)
- .and_raise(StandardError, 'request invalid')
+ context 'when the HTTP request fails to recover the headers' do
+ it 'adds the error message' do
+ expect(Gitlab::HTTP)
+ .to receive(:head)
+ .and_raise(StandardError, 'request invalid')
- expect(subject).not_to be_valid
- expect(subject.errors.full_messages)
- .to include('Failed to retrive headers: request invalid')
- end
+ expect(subject).not_to be_valid
+ expect(subject.errors.full_messages)
+ .to include('Failed to retrive headers: request invalid')
end
+ end
+ context 'when request is not from an S3 server' do
it 'validates the remote content-length' do
stub_headers_for(remote_url, { 'content-length' => 11.gigabytes })
@@ -72,57 +68,19 @@ RSpec.describe ::Import::GitlabProjects::FileAcquisitionStrategies::RemoteFile,
expect(subject.errors.full_messages)
.to include("Content type 'unknown' not allowed. (Allowed: application/gzip, application/x-tar, application/x-gzip)")
end
-
- context 'when trying to import from AWS S3' do
- it 'adds an error suggesting to use `projects/remote-import-s3`' do
- stub_headers_for(
- remote_url,
- 'Server' => 'AmazonS3',
- 'x-amz-request-id' => 'some-id'
- )
-
- expect(subject).not_to be_valid
- expect(subject.errors.full_messages)
- .to include('To import from AWS S3 use `projects/remote-import-s3`')
- end
- end
end
- context 'when import_project_from_remote_file_s3 is disabled' do
- before do
- stub_feature_flags(import_project_from_remote_file_s3: false)
- end
-
- context 'when trying to import from AWS S3' do
- it 'does not validate the remote content-length or content-type' do
- stub_headers_for(
- remote_url,
- 'Server' => 'AmazonS3',
- 'x-amz-request-id' => 'some-id',
- 'content-length' => 11.gigabytes,
- 'content-type' => 'unknown'
- )
-
- expect(subject).to be_valid
- end
- end
-
- context 'when NOT trying to import from AWS S3' do
- it 'validates content-length and content-type' do
- stub_headers_for(
- remote_url,
- 'Server' => 'NOT AWS S3',
- 'content-length' => 11.gigabytes,
- 'content-type' => 'unknown'
- )
-
- expect(subject).not_to be_valid
-
- expect(subject.errors.full_messages)
- .to include("Content type 'unknown' not allowed. (Allowed: application/gzip, application/x-tar, application/x-gzip)")
- expect(subject.errors.full_messages)
- .to include('Content length is too big (should be at most 10 GB)')
- end
+ context 'when request is from an S3 server' do
+ it 'does not validate the remote content-length or content-type' do
+ stub_headers_for(
+ remote_url,
+ 'Server' => 'AmazonS3',
+ 'x-amz-request-id' => 'some-id',
+ 'content-length' => 11.gigabytes,
+ 'content-type' => 'unknown'
+ )
+
+ expect(subject).to be_valid
end
end
end
diff --git a/spec/services/import_csv/base_service_spec.rb b/spec/services/import_csv/base_service_spec.rb
new file mode 100644
index 00000000000..0c0ed40ff4d
--- /dev/null
+++ b/spec/services/import_csv/base_service_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ImportCsv::BaseService, feature_category: :importers do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:csv_io) { double }
+
+ subject { described_class.new(user, project, csv_io) }
+
+ shared_examples 'abstract method' do |method, args|
+ it "raises NotImplemented error when #{method} is called" do
+ if args
+ expect { subject.send(method, args) }.to raise_error(NotImplementedError)
+ else
+ expect { subject.send(method) }.to raise_error(NotImplementedError)
+ end
+ end
+ end
+
+ it_behaves_like 'abstract method', :email_results_to_user
+ it_behaves_like 'abstract method', :attributes_for, "any"
+ it_behaves_like 'abstract method', :validate_headers_presence!, "any"
+ it_behaves_like 'abstract method', :create_object_class
+
+ describe '#detect_col_sep' do
+ context 'when header contains invalid separators' do
+ it 'raises error' do
+ header = 'Name&email'
+
+ expect { subject.send(:detect_col_sep, header) }.to raise_error(CSV::MalformedCSVError)
+ end
+ end
+
+ context 'when header is valid' do
+ shared_examples 'header with valid separators' do
+ let(:header) { "Name#{separator}email" }
+
+ it 'returns separator value' do
+ expect(subject.send(:detect_col_sep, header)).to eq(separator)
+ end
+ end
+
+ context 'with ; as separator' do
+ let(:separator) { ';' }
+
+ it_behaves_like 'header with valid separators'
+ end
+
+ context 'with \t as separator' do
+ let(:separator) { "\t" }
+
+ it_behaves_like 'header with valid separators'
+ end
+
+ context 'with , as separator' do
+ let(:separator) { ',' }
+
+ it_behaves_like 'header with valid separators'
+ 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 a3810879c65..fa5f4c64a43 100644
--- a/spec/services/incident_management/timeline_events/create_service_spec.rb
+++ b/spec/services/incident_management/timeline_events/create_service_spec.rb
@@ -171,7 +171,7 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
occurred_at: Time.current,
action: 'new comment',
promoted_from_note: comment,
- timeline_event_tag_names: ['start time', 'end time']
+ timeline_event_tag_names: ['start time', 'end time', 'Impact mitigated']
}
end
@@ -180,11 +180,11 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
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(result.timeline_event_tags.count).to eq(3)
+ expect(result.timeline_event_tags.by_names(['Start time', 'End time', 'Impact mitigated']).pluck_names)
+ .to match_array(['Start time', 'End time', 'Impact mitigated'])
expect(project.incident_management_timeline_event_tags.pluck_names)
- .to include('Start time', 'End time')
+ .to include('Start time', 'End time', 'Impact mitigated')
end
end
diff --git a/spec/services/incident_management/timeline_events/update_service_spec.rb b/spec/services/incident_management/timeline_events/update_service_spec.rb
index ff802109715..ebaa4dde7a2 100644
--- a/spec/services/incident_management/timeline_events/update_service_spec.rb
+++ b/spec/services/incident_management/timeline_events/update_service_spec.rb
@@ -201,20 +201,22 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService, feature_catego
{
note: 'Updated note',
occurred_at: occurred_at,
- timeline_event_tag_names: ['start time', 'end time']
+ timeline_event_tag_names: ['start time', 'end time', 'response initiated']
}
end
it 'returns the new tag in response' do
timeline_event = execute.payload[:timeline_event]
- expect(timeline_event.timeline_event_tags.pluck_names).to contain_exactly('Start time', 'End time')
+ expect(timeline_event.timeline_event_tags.pluck_names).to contain_exactly(
+ 'Start time', 'End time', 'Response initiated')
end
it 'creates the predefined tags on the project' do
execute
- expect(project.incident_management_timeline_event_tags.pluck_names).to include('Start time', 'End time')
+ expect(project.incident_management_timeline_event_tags.pluck_names).to include(
+ 'Start time', 'End time', 'Response initiated')
end
end
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index dc72cf04776..7ba349ceeae 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -117,11 +117,37 @@ RSpec.describe Issuable::BulkUpdateService do
end
end
+ shared_examples 'bulk update service' do
+ it 'result count only includes authorized issuables' do
+ all_issues = issues + [create(:issue, project: create(:project, :private))]
+ result = bulk_update(all_issues, { assignee_ids: [user.id] })
+
+ expect(result[:count]).to eq(issues.count)
+ end
+
+ context 'when issuable_ids are passed as an array' do
+ it 'updates assignees' do
+ expect do
+ described_class.new(
+ parent,
+ user,
+ { issuable_ids: issues.map(&:id), assignee_ids: [user.id] }
+ ).execute('issue')
+
+ issues.each(&:reset)
+ end.to change { issues.flat_map(&:assignee_ids) }.from([]).to([user.id] * 2)
+ end
+ end
+ end
+
context 'with issuables at a project level' do
+ let_it_be_with_reload(:issues) { create_list(:issue, 2, project: project) }
+
let(:parent) { project }
+ it_behaves_like 'bulk update service'
+
context 'with unpermitted attributes' do
- let(:issues) { create_list(:issue, 2, project: project) }
let(:label) { create(:label, project: project) }
it 'does not update the issues' do
@@ -131,9 +157,23 @@ RSpec.describe Issuable::BulkUpdateService do
end
end
- describe 'close issues' do
- let(:issues) { create_list(:issue, 2, project: project) }
+ context 'when issuable update service raises an ArgumentError' do
+ before do
+ allow_next_instance_of(Issues::UpdateService) do |update_service|
+ allow(update_service).to receive(:execute).and_raise(ArgumentError, 'update error')
+ end
+ end
+
+ it 'returns an error response' do
+ result = bulk_update(issues, add_label_ids: [])
+ expect(result).to be_error
+ expect(result.errors).to contain_exactly('update error')
+ expect(result.http_status).to eq(422)
+ end
+ end
+
+ describe 'close issues' do
it 'succeeds and returns the correct number of issues updated' do
result = bulk_update(issues, state_event: 'close')
@@ -155,24 +195,24 @@ RSpec.describe Issuable::BulkUpdateService do
end
describe 'reopen issues' do
- let(:issues) { create_list(:closed_issue, 2, project: project) }
+ let_it_be_with_reload(:closed_issues) { create_list(:closed_issue, 2, project: project) }
it 'succeeds and returns the correct number of issues updated' do
- result = bulk_update(issues, state_event: 'reopen')
+ result = bulk_update(closed_issues, state_event: 'reopen')
expect(result.success?).to be_truthy
- expect(result.payload[:count]).to eq(issues.count)
+ expect(result.payload[:count]).to eq(closed_issues.count)
end
it 'reopens all the issues passed' do
- bulk_update(issues, state_event: 'reopen')
+ bulk_update(closed_issues, state_event: 'reopen')
expect(project.issues.closed).to be_empty
expect(project.issues.opened).not_to be_empty
end
it_behaves_like 'scheduling cached group count clear' do
- let(:issuables) { issues }
+ let(:issuables) { closed_issues }
let(:params) { { state_event: 'reopen' } }
end
end
@@ -207,10 +247,10 @@ RSpec.describe Issuable::BulkUpdateService do
end
end
- context 'when the new assignee ID is not present' do
- it 'does not unassign' do
+ context 'when the new assignee IDs array is empty' do
+ it 'removes all assignees' do
expect { bulk_update(merge_request, assignee_ids: []) }
- .not_to change { merge_request.reload.assignee_ids }
+ .to change(merge_request.assignees, :count).by(-1)
end
end
end
@@ -244,10 +284,10 @@ RSpec.describe Issuable::BulkUpdateService do
end
end
- context 'when the new assignee ID is not present' do
- it 'does not unassign' do
+ context 'when the new assignee IDs array is empty' do
+ it 'removes all assignees' do
expect { bulk_update(issue, assignee_ids: []) }
- .not_to change(issue.assignees, :count)
+ .to change(issue.assignees, :count).by(-1)
end
end
end
@@ -321,6 +361,10 @@ RSpec.describe Issuable::BulkUpdateService do
group.add_reporter(user)
end
+ it_behaves_like 'bulk update service' do
+ let_it_be_with_reload(:issues) { create_list(:issue, 2, project: create(:project, group: group)) }
+ end
+
describe 'updating milestones' do
let(:milestone) { create(:milestone, group: group) }
let(:project) { create(:project, :repository, group: group) }
@@ -372,4 +416,13 @@ RSpec.describe Issuable::BulkUpdateService do
end
end
end
+
+ context 'when no parent is provided' do
+ it 'returns an unscoped update error' do
+ result = described_class.new(nil, user, { assignee_ids: [user.id], issuable_ids: [] }).execute('issue')
+
+ expect(result).to be_error
+ expect(result.errors).to contain_exactly(_('A parent must be provided when bulk updating issuables'))
+ end
+ end
end
diff --git a/spec/services/issuable/destroy_service_spec.rb b/spec/services/issuable/destroy_service_spec.rb
index c72d48d5b77..29f548e1c47 100644
--- a/spec/services/issuable/destroy_service_spec.rb
+++ b/spec/services/issuable/destroy_service_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Issuable::DestroyService do
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public, group: group) }
- subject(:service) { described_class.new(project: project, current_user: user) }
+ subject(:service) { described_class.new(container: project, current_user: user) }
describe '#execute' do
context 'when issuable is an issue' do
diff --git a/spec/services/issuable/discussions_list_service_spec.rb b/spec/services/issuable/discussions_list_service_spec.rb
index ecdd8d031c9..a6f57088ad1 100644
--- a/spec/services/issuable/discussions_list_service_spec.rb
+++ b/spec/services/issuable/discussions_list_service_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe Issuable::DiscussionsListService do
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_it_be(:label_2) { create(:label, project: project) }
let(:finder_params_for_issuable) { {} }
@@ -22,8 +23,7 @@ RSpec.describe Issuable::DiscussionsListService do
let_it_be(:issuable) { create(:work_item, :issue, project: project) }
before do
- stub_const('WorkItems::Type::BASE_TYPES', { issue: { name: 'NoNotesWidget', enum_value: 0 } })
- stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] })
+ WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
end
it "returns no notes" do
diff --git a/spec/services/issues/after_create_service_spec.rb b/spec/services/issues/after_create_service_spec.rb
index 6b720d6e687..39a6799dbad 100644
--- a/spec/services/issues/after_create_service_spec.rb
+++ b/spec/services/issues/after_create_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Issues::AfterCreateService do
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:issue) { create(:issue, project: project, author: current_user, milestone: milestone, assignee_ids: [assignee.id]) }
- subject(:after_create_service) { described_class.new(project: project, current_user: current_user) }
+ subject(:after_create_service) { described_class.new(container: project, current_user: current_user) }
describe '#execute' do
it 'creates a pending todo for new assignee' do
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index 838e0675372..2160c45d079 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Issues::BuildService do
end
def build_issue(issue_params = {})
- described_class.new(project: project, current_user: user, params: issue_params).execute
+ described_class.new(container: project, current_user: user, params: issue_params).execute
end
context 'for a single discussion' do
@@ -45,7 +45,7 @@ RSpec.describe Issues::BuildService do
describe '#items_for_discussions' do
it 'has an item for each discussion' do
create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.source_project, line_number: 13)
- service = described_class.new(project: project, current_user: user, params: { merge_request_to_resolve_discussions_of: merge_request.iid })
+ service = described_class.new(container: project, current_user: user, params: { merge_request_to_resolve_discussions_of: merge_request.iid })
service.execute
@@ -54,7 +54,7 @@ RSpec.describe Issues::BuildService do
end
describe '#item_for_discussion' do
- let(:service) { described_class.new(project: project, current_user: user, params: { merge_request_to_resolve_discussions_of: merge_request.iid }) }
+ let(:service) { described_class.new(container: project, current_user: user, params: { merge_request_to_resolve_discussions_of: merge_request.iid }) }
it 'mentions the author of the note' do
discussion = create(:diff_note_on_merge_request, author: create(:user, username: 'author')).to_discussion
diff --git a/spec/services/issues/clone_service_spec.rb b/spec/services/issues/clone_service_spec.rb
index 67f32b85350..eafaea93015 100644
--- a/spec/services/issues/clone_service_spec.rb
+++ b/spec/services/issues/clone_service_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe Issues::CloneService do
let(:with_notes) { false }
subject(:clone_service) do
- described_class.new(project: old_project, current_user: user)
+ described_class.new(container: old_project, current_user: user)
end
shared_context 'user can clone issue' do
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index ef24d1e940e..803808e667c 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -20,13 +20,13 @@ RSpec.describe Issues::CloseService do
end
describe '#execute' do
- let(:service) { described_class.new(project: project, current_user: user) }
+ let(:service) { described_class.new(container: project, current_user: user) }
context 'when skip_authorization is true' do
it 'does close the issue even if user is not authorized' do
non_authorized_user = create(:user)
- service = described_class.new(project: project, current_user: non_authorized_user)
+ service = described_class.new(container: project, current_user: non_authorized_user)
expect do
service.execute(issue, skip_authorization: true)
@@ -167,7 +167,7 @@ RSpec.describe Issues::CloseService do
project.reload
expect(project.external_issue_tracker).to receive(:close_issue)
- described_class.new(project: project, current_user: user).close_issue(external_issue)
+ described_class.new(container: project, current_user: user).close_issue(external_issue)
end
end
@@ -178,7 +178,7 @@ RSpec.describe Issues::CloseService do
project.reload
expect(project.external_issue_tracker).not_to receive(:close_issue)
- described_class.new(project: project, current_user: user).close_issue(external_issue)
+ described_class.new(container: project, current_user: user).close_issue(external_issue)
end
end
@@ -189,7 +189,7 @@ RSpec.describe Issues::CloseService do
project.reload
expect(project.external_issue_tracker).not_to receive(:close_issue)
- described_class.new(project: project, current_user: user).close_issue(external_issue)
+ described_class.new(container: project, current_user: user).close_issue(external_issue)
end
end
end
@@ -197,7 +197,7 @@ RSpec.describe Issues::CloseService do
context "closed by a merge request", :sidekiq_might_not_need_inline do
subject(:close_issue) do
perform_enqueued_jobs do
- described_class.new(project: project, current_user: user).close_issue(issue, closed_via: closing_merge_request)
+ described_class.new(container: project, current_user: user).close_issue(issue, closed_via: closing_merge_request)
end
end
@@ -266,7 +266,7 @@ RSpec.describe Issues::CloseService do
context "closed by a commit", :sidekiq_might_not_need_inline do
it 'mentions closure via a commit' do
perform_enqueued_jobs do
- described_class.new(project: project, current_user: user).close_issue(issue, closed_via: closing_commit)
+ described_class.new(container: project, current_user: user).close_issue(issue, closed_via: closing_commit)
end
email = ActionMailer::Base.deliveries.last
@@ -280,7 +280,7 @@ RSpec.describe Issues::CloseService do
it 'does not mention the commit id' do
project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
perform_enqueued_jobs do
- described_class.new(project: project, current_user: user).close_issue(issue, closed_via: closing_commit)
+ described_class.new(container: project, current_user: user).close_issue(issue, closed_via: closing_commit)
end
email = ActionMailer::Base.deliveries.last
@@ -296,7 +296,7 @@ RSpec.describe Issues::CloseService do
context "valid params" do
subject(:close_issue) do
perform_enqueued_jobs do
- described_class.new(project: project, current_user: user).close_issue(issue)
+ described_class.new(container: project, current_user: user).close_issue(issue)
end
end
@@ -438,7 +438,7 @@ RSpec.describe Issues::CloseService do
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)
+ described_class.new(container: project, current_user: user).close_issue(issue)
end
end
@@ -449,7 +449,7 @@ RSpec.describe Issues::CloseService do
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)
- described_class.new(project: project, current_user: user).close_issue(issue)
+ described_class.new(container: project, current_user: user).close_issue(issue)
end
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 7ab2046b6be..ada5b300d7a 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Issues::CreateService do
let(:opts) { { title: 'title' } }
let(:spam_params) { double }
- let(:service) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params) }
+ let(:service) { described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params) }
it_behaves_like 'rate limited service' do
let(:key) { :issues_create }
@@ -147,7 +147,7 @@ RSpec.describe Issues::CreateService do
end
context 'when a build_service is provided' do
- let(:result) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params, build_service: build_service).execute }
+ let(:result) { described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params, build_service: build_service).execute }
let(:issue_from_builder) { WorkItem.new(project: project, title: 'Issue from builder') }
let(:build_service) { double(:build_service, execute: issue_from_builder) }
@@ -160,7 +160,7 @@ RSpec.describe Issues::CreateService do
end
context 'when skip_system_notes is true' do
- let(:issue) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute(skip_system_notes: true) }
+ let(:issue) { described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute(skip_system_notes: true) }
it 'does not call Issuable::CommonSystemNotesService' do
expect(Issuable::CommonSystemNotesService).not_to receive(:new)
@@ -256,7 +256,7 @@ RSpec.describe Issues::CreateService do
let_it_be(:non_member) { create(:user) }
it 'filters out params that cannot be set without the :set_issue_metadata permission' do
- result = described_class.new(project: project, current_user: non_member, params: opts, spam_params: spam_params).execute
+ result = described_class.new(container: project, current_user: non_member, params: opts, spam_params: spam_params).execute
issue = result[:issue]
expect(result).to be_success
@@ -270,7 +270,7 @@ RSpec.describe Issues::CreateService do
end
it 'can create confidential issues' do
- result = described_class.new(project: project, current_user: non_member, params: opts.merge(confidential: true), spam_params: spam_params).execute
+ result = described_class.new(container: project, current_user: non_member, params: opts.merge(confidential: true), spam_params: spam_params).execute
issue = result[:issue]
expect(result).to be_success
@@ -281,7 +281,7 @@ RSpec.describe Issues::CreateService do
it 'moves the issue to the end, in an asynchronous worker' do
expect(Issues::PlacementWorker).to receive(:perform_async).with(be_nil, Integer)
- described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
end
context 'when label belongs to project group' do
@@ -368,7 +368,7 @@ RSpec.describe Issues::CreateService do
it 'invalidates open issues counter for assignees when issue is assigned' do
project.add_maintainer(assignee)
- described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
expect(assignee.assigned_open_issues_count).to eq 1
end
@@ -439,7 +439,7 @@ RSpec.describe Issues::CreateService do
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, params: opts, spam_params: spam_params).execute
+ described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
end
context 'when issue is confidential' do
@@ -462,7 +462,7 @@ RSpec.describe Issues::CreateService 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(container: project, current_user: user, params: opts, spam_params: spam_params).execute
end
end
end
@@ -508,7 +508,7 @@ RSpec.describe Issues::CreateService do
it 'removes assignee when user id is invalid' do
opts = { title: 'Title', description: 'Description', assignee_ids: [-1] }
- result = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ result = described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
issue = result[:issue]
expect(result).to be_success
@@ -518,7 +518,7 @@ RSpec.describe Issues::CreateService do
it 'removes assignee when user id is 0' do
opts = { title: 'Title', description: 'Description', assignee_ids: [0] }
- result = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ result = described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
issue = result[:issue]
expect(result).to be_success
@@ -529,7 +529,7 @@ RSpec.describe Issues::CreateService do
project.add_maintainer(assignee)
opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
- result = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ result = described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
issue = result[:issue]
expect(result).to be_success
@@ -549,7 +549,7 @@ RSpec.describe Issues::CreateService do
project.update!(visibility_level: level)
opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
- result = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ result = described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
issue = result[:issue]
expect(result).to be_success
@@ -561,10 +561,40 @@ RSpec.describe Issues::CreateService do
end
it_behaves_like 'issuable record that supports quick actions' do
- let(:issuable) { described_class.new(project: project, current_user: user, params: params, spam_params: spam_params).execute[:issue] }
+ let(:issuable) { described_class.new(container: project, current_user: user, params: params, spam_params: spam_params).execute[:issue] }
end
context 'Quick actions' do
+ context 'as work item' do
+ let(:opts) do
+ {
+ title: "My work item",
+ work_item_type: work_item_type,
+ description: "/shrug"
+ }
+ end
+
+ context 'when work item type is not the default Issue' do
+ let(:work_item_type) { create(:work_item_type, namespace: project.namespace) }
+
+ it 'saves the work item without applying the quick action' do
+ expect(result).to be_success
+ expect(issue).to be_persisted
+ expect(issue.description).to eq("/shrug")
+ end
+ end
+
+ context 'when work item type is the default Issue' do
+ let(:work_item_type) { WorkItems::Type.default_by_type(:issue) }
+
+ it 'saves the work item and applies the quick action' do
+ expect(result).to be_success
+ expect(issue).to be_persisted
+ expect(issue.description).to eq(" ¯\\_(ツ)_/¯")
+ end
+ end
+ end
+
context 'with assignee, milestone, and contact in params and command' do
let_it_be(:contact) { create(:contact, group: group) }
@@ -672,14 +702,14 @@ RSpec.describe Issues::CreateService do
let(:opts) { { discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid } }
it 'resolves the discussion' do
- described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
discussion.first_note.reload
expect(discussion.resolved?).to be(true)
end
it 'added a system note to the discussion' do
- described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first
@@ -688,7 +718,7 @@ RSpec.describe Issues::CreateService do
it 'sets default title and description values if not provided' do
result = described_class.new(
- project: project, current_user: user,
+ container: project, current_user: user,
params: opts,
spam_params: spam_params
).execute
@@ -702,7 +732,7 @@ RSpec.describe Issues::CreateService do
it 'takes params from the request over the default values' do
result = described_class.new(
- project: project,
+ container: project,
current_user: user,
params: opts.merge(
description: 'Custom issue description',
@@ -723,14 +753,14 @@ RSpec.describe Issues::CreateService do
let(:opts) { { merge_request_to_resolve_discussions_of: merge_request.iid } }
it 'resolves the discussion' do
- described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
discussion.first_note.reload
expect(discussion.resolved?).to be(true)
end
it 'added a system note to the discussion' do
- described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first
@@ -739,7 +769,7 @@ RSpec.describe Issues::CreateService do
it 'sets default title and description values if not provided' do
result = described_class.new(
- project: project, current_user: user,
+ container: project, current_user: user,
params: opts,
spam_params: spam_params
).execute
@@ -753,7 +783,7 @@ RSpec.describe Issues::CreateService do
it 'takes params from the request over the default values' do
result = described_class.new(
- project: project,
+ container: project,
current_user: user,
params: opts.merge(
description: 'Custom issue description',
@@ -806,7 +836,7 @@ RSpec.describe Issues::CreateService do
end
subject do
- described_class.new(project: project, current_user: user, params: params, spam_params: spam_params)
+ described_class.new(container: project, current_user: user, params: params, spam_params: spam_params)
end
it 'executes SpamActionService' do
diff --git a/spec/services/issues/duplicate_service_spec.rb b/spec/services/issues/duplicate_service_spec.rb
index 0eb0bbb1480..f49bce70cd0 100644
--- a/spec/services/issues/duplicate_service_spec.rb
+++ b/spec/services/issues/duplicate_service_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Issues::DuplicateService do
let(:canonical_issue) { create(:issue, project: canonical_project) }
let(:duplicate_issue) { create(:issue, project: duplicate_project) }
- subject { described_class.new(project: duplicate_project, current_user: user) }
+ subject { described_class.new(container: duplicate_project, current_user: user) }
describe '#execute' do
context 'when the issues passed are the same' do
diff --git a/spec/services/issues/export_csv_service_spec.rb b/spec/services/issues/export_csv_service_spec.rb
index d3359447fd8..1ac64c0301d 100644
--- a/spec/services/issues/export_csv_service_spec.rb
+++ b/spec/services/issues/export_csv_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::ExportCsvService, :with_license do
+RSpec.describe Issues::ExportCsvService, :with_license, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :public, group: group) }
@@ -57,137 +57,151 @@ RSpec.describe Issues::ExportCsvService, :with_license do
time_estimate: 72000)
end
- it 'includes the columns required for import' do
- expect(csv.headers).to include('Title', 'Description')
- end
-
- it 'returns two issues' do
- expect(csv.count).to eq(2)
- end
+ shared_examples 'exports CSVs for issues' do
+ it 'includes the columns required for import' do
+ expect(csv.headers).to include('Title', 'Description')
+ end
- specify 'iid' do
- expect(csv[0]['Issue ID']).to eq issue.iid.to_s
- end
+ it 'returns two issues' do
+ expect(csv.count).to eq(2)
+ end
- specify 'url' do
- expect(csv[0]['URL']).to match(/http.*#{project.full_path}.*#{issue.iid}/)
- end
+ specify 'iid' do
+ expect(csv[0]['Issue ID']).to eq issue.iid.to_s
+ end
- specify 'title' do
- expect(csv[0]['Title']).to eq issue.title
- end
+ specify 'url' do
+ expect(csv[0]['URL']).to match(/http.*#{project.full_path}.*#{issue.iid}/)
+ end
- specify 'state' do
- expect(csv[0]['State']).to eq 'Open'
- end
+ specify 'title' do
+ expect(csv[0]['Title']).to eq issue.title
+ end
- specify 'description' do
- expect(csv[0]['Description']).to eq issue.description
- expect(csv[1]['Description']).to eq nil
- end
+ specify 'state' do
+ expect(csv[0]['State']).to eq 'Open'
+ end
- specify 'author name' do
- expect(csv[0]['Author']).to eq issue.author_name
- end
+ specify 'description' do
+ expect(csv[0]['Description']).to eq issue.description
+ expect(csv[1]['Description']).to eq nil
+ end
- specify 'author username' do
- expect(csv[0]['Author Username']).to eq issue.author.username
- end
+ specify 'author name' do
+ expect(csv[0]['Author']).to eq issue.author_name
+ end
- specify 'assignee name' do
- expect(csv[0]['Assignee']).to eq user.name
- expect(csv[1]['Assignee']).to eq ''
- end
+ specify 'author username' do
+ expect(csv[0]['Author Username']).to eq issue.author.username
+ end
- specify 'assignee username' do
- expect(csv[0]['Assignee Username']).to eq user.username
- expect(csv[1]['Assignee Username']).to eq ''
- end
+ specify 'assignee name' do
+ expect(csv[0]['Assignee']).to eq user.name
+ expect(csv[1]['Assignee']).to eq ''
+ end
- specify 'confidential' do
- expect(csv[0]['Confidential']).to eq 'No'
- end
+ specify 'assignee username' do
+ expect(csv[0]['Assignee Username']).to eq user.username
+ expect(csv[1]['Assignee Username']).to eq ''
+ end
- specify 'milestone' do
- expect(csv[0]['Milestone']).to eq issue.milestone.title
- expect(csv[1]['Milestone']).to eq nil
- end
+ specify 'confidential' do
+ expect(csv[0]['Confidential']).to eq 'No'
+ end
- specify 'labels' do
- expect(csv[0]['Labels']).to eq 'Feature,Idea'
- expect(csv[1]['Labels']).to eq nil
- end
+ specify 'milestone' do
+ expect(csv[0]['Milestone']).to eq issue.milestone.title
+ expect(csv[1]['Milestone']).to eq nil
+ end
- specify 'due_date' do
- expect(csv[0]['Due Date']).to eq '2014-03-02'
- expect(csv[1]['Due Date']).to eq nil
- end
+ specify 'labels' do
+ expect(csv[0]['Labels']).to eq 'Feature,Idea'
+ expect(csv[1]['Labels']).to eq nil
+ end
- specify 'created_at' do
- expect(csv[0]['Created At (UTC)']).to eq '2015-04-03 02:01:00'
- end
+ specify 'due_date' do
+ expect(csv[0]['Due Date']).to eq '2014-03-02'
+ expect(csv[1]['Due Date']).to eq nil
+ end
- specify 'updated_at' do
- expect(csv[0]['Updated At (UTC)']).to eq '2016-05-04 03:02:01'
- end
+ specify 'created_at' do
+ expect(csv[0]['Created At (UTC)']).to eq '2015-04-03 02:01:00'
+ end
- specify 'closed_at' do
- expect(csv[0]['Closed At (UTC)']).to eq '2017-06-05 04:03:02'
- expect(csv[1]['Closed At (UTC)']).to eq nil
- end
+ specify 'updated_at' do
+ expect(csv[0]['Updated At (UTC)']).to eq '2016-05-04 03:02:01'
+ end
- specify 'discussion_locked' do
- expect(csv[0]['Locked']).to eq 'Yes'
- end
+ specify 'closed_at' do
+ expect(csv[0]['Closed At (UTC)']).to eq '2017-06-05 04:03:02'
+ expect(csv[1]['Closed At (UTC)']).to eq nil
+ end
- specify 'weight' do
- expect(csv[0]['Weight']).to eq '4'
- end
+ specify 'discussion_locked' do
+ expect(csv[0]['Locked']).to eq 'Yes'
+ end
- specify 'time estimate' do
- expect(csv[0]['Time Estimate']).to eq '72000'
- expect(csv[1]['Time Estimate']).to eq '0'
- end
+ specify 'weight' do
+ expect(csv[0]['Weight']).to eq '4'
+ end
- specify 'time spent' do
- expect(csv[0]['Time Spent']).to eq '560'
- expect(csv[1]['Time Spent']).to eq '0'
- end
+ specify 'time estimate' do
+ expect(csv[0]['Time Estimate']).to eq '72000'
+ expect(csv[1]['Time Estimate']).to eq '0'
+ end
- context 'with issues filtered by labels and project' do
- subject do
- described_class.new(
- IssuesFinder.new(user,
- project_id: project.id,
- label_name: %w(Idea Feature)).execute, project)
+ specify 'time spent' do
+ expect(csv[0]['Time Spent']).to eq '560'
+ expect(csv[1]['Time Spent']).to eq '0'
end
- it 'returns only filtered objects' do
- expect(csv.count).to eq(1)
- expect(csv[0]['Issue ID']).to eq issue.iid.to_s
+ context 'with issues filtered by labels and project' do
+ subject do
+ described_class.new(
+ IssuesFinder.new(user,
+ project_id: project.id,
+ label_name: %w(Idea Feature)).execute, project)
+ end
+
+ it 'returns only filtered objects' do
+ expect(csv.count).to eq(1)
+ expect(csv[0]['Issue ID']).to eq issue.iid.to_s
+ end
end
- end
- context 'with label links' do
- let(:labeled_issues) { create_list(:labeled_issue, 2, project: project, author: user, labels: [feature_label, idea_label]) }
+ context 'with label links' do
+ let(:labeled_issues) { create_list(:labeled_issue, 2, project: project, author: user, labels: [feature_label, idea_label]) }
- it 'does not run a query for each label link' do
- control_count = ActiveRecord::QueryRecorder.new { csv }.count
+ it 'does not run a query for each label link' do
+ control_count = ActiveRecord::QueryRecorder.new { csv }.count
- labeled_issues
+ labeled_issues
- expect { csv }.not_to exceed_query_limit(control_count)
- expect(csv.count).to eq(4)
- end
+ expect { csv }.not_to exceed_query_limit(control_count)
+ expect(csv.count).to eq(4)
+ end
- it 'returns the labels in sorted order' do
- labeled_issues
+ it 'returns the labels in sorted order' do
+ labeled_issues
- 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"))
+ 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"))
+ end
end
end
+
+ context 'with export_csv_preload_in_batches feature flag disabled' do
+ before do
+ stub_feature_flags(export_csv_preload_in_batches: false)
+ end
+
+ it_behaves_like 'exports CSVs for issues'
+ end
+
+ context 'with export_csv_preload_in_batches feature flag enabled' do
+ it_behaves_like 'exports CSVs for issues'
+ end
end
context 'with minimal details' do
diff --git a/spec/services/issues/import_csv_service_spec.rb b/spec/services/issues/import_csv_service_spec.rb
index 9ad1d7dba9f..90e360f9cf1 100644
--- a/spec/services/issues/import_csv_service_spec.rb
+++ b/spec/services/issues/import_csv_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::ImportCsvService do
+RSpec.describe Issues::ImportCsvService, feature_category: :team_planning do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:assignee) { create(:user, username: 'csv_assignee') }
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index 324b2aa9fe2..12924df3200 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::MoveService do
+RSpec.describe Issues::MoveService, feature_category: :team_planning do
include DesignManagementTestHelpers
let_it_be(:user) { create(:user) }
@@ -20,7 +20,7 @@ RSpec.describe Issues::MoveService do
end
subject(:move_service) do
- described_class.new(project: old_project, current_user: user)
+ described_class.new(container: old_project, current_user: user)
end
shared_context 'user can move issue' do
diff --git a/spec/services/issues/referenced_merge_requests_service_spec.rb b/spec/services/issues/referenced_merge_requests_service_spec.rb
index 16166c1fa33..aee3583b834 100644
--- a/spec/services/issues/referenced_merge_requests_service_spec.rb
+++ b/spec/services/issues/referenced_merge_requests_service_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe Issues::ReferencedMergeRequestsService do
let_it_be(:referencing_mr) { create_referencing_mr(source_project: project, source_branch: 'csv') }
let_it_be(:referencing_mr_other_project) { create_referencing_mr(source_project: other_project, source_branch: 'csv') }
- let(:service) { described_class.new(project: project, current_user: user) }
+ let(:service) { described_class.new(container: project, current_user: user) }
describe '#execute' do
it 'returns a list of sorted merge requests' do
diff --git a/spec/services/issues/related_branches_service_spec.rb b/spec/services/issues/related_branches_service_spec.rb
index 95d456c1b05..05c61d0abfc 100644
--- a/spec/services/issues/related_branches_service_spec.rb
+++ b/spec/services/issues/related_branches_service_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Issues::RelatedBranchesService do
let(:user) { developer }
- subject { described_class.new(project: project, current_user: user) }
+ subject { described_class.new(container: project, current_user: user) }
before_all do
project.add_developer(developer)
@@ -54,7 +54,7 @@ RSpec.describe Issues::RelatedBranchesService do
merge_request.create_cross_references!(user)
referenced_merge_requests = Issues::ReferencedMergeRequestsService
- .new(project: issue.project, current_user: user)
+ .new(container: issue.project, current_user: user)
.referenced_merge_requests(issue)
expect(referenced_merge_requests).not_to be_empty
diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb
index 529b3ff266b..68015a2327e 100644
--- a/spec/services/issues/reopen_service_spec.rb
+++ b/spec/services/issues/reopen_service_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Issues::ReopenService do
guest = create(:user)
project.add_guest(guest)
- described_class.new(project: project, current_user: guest).execute(issue)
+ described_class.new(container: project, current_user: guest).execute(issue)
expect(issue).to be_closed
end
@@ -21,7 +21,7 @@ RSpec.describe Issues::ReopenService do
it 'does close the issue even if user is not authorized' do
non_authorized_user = create(:user)
- service = described_class.new(project: project, current_user: non_authorized_user)
+ service = described_class.new(container: project, current_user: non_authorized_user)
expect do
service.execute(issue, skip_authorization: true)
@@ -33,7 +33,7 @@ RSpec.describe Issues::ReopenService do
context 'when user is authorized to reopen issue' do
let(:user) { create(:user) }
- subject(:execute) { described_class.new(project: project, current_user: user).execute(issue) }
+ subject(:execute) { described_class.new(container: project, current_user: user).execute(issue) }
before do
project.add_maintainer(user)
diff --git a/spec/services/issues/reorder_service_spec.rb b/spec/services/issues/reorder_service_spec.rb
index 392930c1b9f..430a9e9f526 100644
--- a/spec/services/issues/reorder_service_spec.rb
+++ b/spec/services/issues/reorder_service_spec.rb
@@ -85,6 +85,6 @@ RSpec.describe Issues::ReorderService do
end
def service(params)
- described_class.new(project: project, current_user: user, params: params)
+ described_class.new(container: project, current_user: user, params: params)
end
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 930766c520b..973025bd2e3 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe Issues::UpdateService, :mailer do
end
def update_issue(opts)
- described_class.new(project: project, current_user: user, params: opts).execute(issue)
+ described_class.new(container: project, current_user: user, params: opts).execute(issue)
end
it_behaves_like 'issuable update service updating last_edited_at values' do
@@ -106,29 +106,29 @@ RSpec.describe Issues::UpdateService, :mailer do
context 'when updating milestone' do
before do
- update_issue({ milestone: nil })
+ update_issue({ milestone_id: nil })
end
it 'updates issue milestone when passing `milestone` param' do
- expect { update_issue({ milestone: milestone }) }
+ expect { update_issue({ milestone_id: milestone.id }) }
.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 })
+ update_issue({ milestone_id: milestone.id })
end
context 'when milestone remains unchanged' do
before do
- update_issue({ title: 'abc', milestone: milestone })
+ update_issue({ title: 'abc', milestone_id: milestone.id })
end
it "does not trigger 'issuableMilestoneUpdated'" do
expect(GraphqlTriggers).not_to receive(:issuable_milestone_updated)
- update_issue({ milestone: milestone })
+ update_issue({ milestone_id: milestone.id })
end
end
end
@@ -420,7 +420,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue_1.id, issue_2.id]
- described_class.new(project: issue_3.project, current_user: user, params: opts).execute(issue_3)
+ described_class.new(container: issue_3.project, current_user: user, params: opts).execute(issue_3)
expect(issue_2.relative_position).to be_between(issue_1.relative_position, issue_2.relative_position)
end
end
@@ -428,7 +428,7 @@ RSpec.describe Issues::UpdateService, :mailer do
context 'when current user cannot admin issues in the project' do
it 'filters out params that cannot be set without the :admin_issue permission' do
described_class.new(
- project: project, current_user: guest, params: opts.merge(
+ container: project, current_user: guest, params: opts.merge(
confidential: true,
issue_type: 'test_case'
)
@@ -755,14 +755,14 @@ RSpec.describe Issues::UpdateService, :mailer do
end
it 'marks todos as done' do
- update_issue(milestone: create(:milestone, project: project))
+ update_issue(milestone_id: create(:milestone, project: project).id)
expect(todo.reload.done?).to eq true
end
it 'sends notifications for subscribers of changed milestone', :sidekiq_might_not_need_inline do
perform_enqueued_jobs do
- update_issue(milestone: create(:milestone, project: project))
+ update_issue(milestone_id: create(:milestone, project: project).id)
end
should_email(subscriber)
@@ -779,7 +779,7 @@ RSpec.describe Issues::UpdateService, :mailer do
expect(service).to receive(:delete_cache).and_call_original
end
- update_issue(milestone: milestone)
+ update_issue(milestone_id: milestone.id)
end
end
@@ -803,7 +803,7 @@ RSpec.describe Issues::UpdateService, :mailer do
expect(service).to receive(:delete_cache).and_call_original
end
- update_issue(milestone: new_milestone)
+ update_issue(milestone_id: new_milestone.id)
end
end
@@ -838,7 +838,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts = { label_ids: [label.id] }
perform_enqueued_jobs do
- @issue = described_class.new(project: project, current_user: user, params: opts).execute(issue)
+ @issue = described_class.new(container: project, current_user: user, params: opts).execute(issue)
end
should_email(subscriber)
@@ -854,7 +854,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts = { label_ids: [label.id, label2.id] }
perform_enqueued_jobs do
- @issue = described_class.new(project: project, current_user: user, params: opts).execute(issue)
+ @issue = described_class.new(container: project, current_user: user, params: opts).execute(issue)
end
should_not_email(subscriber)
@@ -865,7 +865,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts = { label_ids: [label2.id] }
perform_enqueued_jobs do
- @issue = described_class.new(project: project, current_user: user, params: opts).execute(issue)
+ @issue = described_class.new(container: project, current_user: user, params: opts).execute(issue)
end
should_not_email(subscriber)
@@ -897,7 +897,7 @@ RSpec.describe Issues::UpdateService, :mailer do
line_number: 1
}
}
- service = described_class.new(project: project, current_user: user, params: params)
+ service = described_class.new(container: project, current_user: user, params: params)
expect(Spam::SpamActionService).not_to receive(:new)
@@ -915,7 +915,7 @@ RSpec.describe Issues::UpdateService, :mailer do
line_number: 1
}
}
- service = described_class.new(project: project, current_user: user, params: params)
+ service = described_class.new(container: project, current_user: user, params: params)
expect(service).to receive(:after_update).with(issue, {})
@@ -991,7 +991,7 @@ RSpec.describe Issues::UpdateService, :mailer do
context 'updating labels' do
let(:label3) { create(:label, project: project) }
- let(:result) { described_class.new(project: project, current_user: user, params: params).execute(issue).reload }
+ let(:result) { described_class.new(container: project, current_user: user, params: params).execute(issue).reload }
context 'when add_label_ids and label_ids are passed' do
let(:params) { { label_ids: [label.id], add_label_ids: [label3.id] } }
@@ -1063,7 +1063,7 @@ RSpec.describe Issues::UpdateService, :mailer do
end
context 'updating dates' do
- subject(:result) { described_class.new(project: project, current_user: user, params: params).execute(issue) }
+ subject(:result) { described_class.new(container: project, current_user: user, params: params).execute(issue) }
let(:updated_date) { 1.week.from_now.to_date }
@@ -1428,7 +1428,7 @@ RSpec.describe Issues::UpdateService, :mailer do
it 'raises an error for invalid move ids' do
opts = { move_between_ids: [9000, non_existing_record_id] }
- expect { described_class.new(project: issue.project, current_user: user, params: opts).execute(issue) }
+ expect { described_class.new(container: issue.project, current_user: user, params: opts).execute(issue) }
.to raise_error(ActiveRecord::RecordNotFound)
end
end
@@ -1473,7 +1473,33 @@ RSpec.describe Issues::UpdateService, :mailer do
it_behaves_like 'issuable record that supports quick actions' do
let(:existing_issue) { create(:issue, project: project) }
- let(:issuable) { described_class.new(project: project, current_user: user, params: params).execute(existing_issue) }
+ let(:issuable) { described_class.new(container: project, current_user: user, params: params).execute(existing_issue) }
+ end
+
+ context 'with quick actions' do
+ context 'as work item' do
+ let(:opts) { { description: "/shrug" } }
+
+ context 'when work item type is not the default Issue' do
+ let(:issue) { create(:work_item, :task, description: "") }
+
+ it 'does not apply the quick action' do
+ expect do
+ update_issue(opts)
+ end.to change(issue, :description).to("/shrug")
+ end
+ end
+
+ context 'when work item type is the default Issue' do
+ let(:issue) { create(:work_item, :issue, description: "") }
+
+ it 'does not apply the quick action' do
+ expect do
+ update_issue(opts)
+ end.to change(issue, :description).to(" ¯\\_(ツ)_/¯")
+ end
+ end
+ end
end
end
end
diff --git a/spec/services/issues/zoom_link_service_spec.rb b/spec/services/issues/zoom_link_service_spec.rb
index ad1f91ab5e6..230e4c1b5e1 100644
--- a/spec/services/issues/zoom_link_service_spec.rb
+++ b/spec/services/issues/zoom_link_service_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Issues::ZoomLinkService do
let_it_be(:issue) { create(:issue) }
let(:project) { issue.project }
- let(:service) { described_class.new(project: project, current_user: user, params: { issue: issue }) }
+ let(:service) { described_class.new(container: project, current_user: user, params: { issue: issue }) }
let(:zoom_link) { 'https://zoom.us/j/123456789' }
before do
diff --git a/spec/services/jira_connect_installations/update_service_spec.rb b/spec/services/jira_connect_installations/update_service_spec.rb
index ec5bb5d6d6a..15f3b485b20 100644
--- a/spec/services/jira_connect_installations/update_service_spec.rb
+++ b/spec/services/jira_connect_installations/update_service_spec.rb
@@ -45,8 +45,9 @@ RSpec.describe JiraConnectInstallations::UpdateService, feature_category: :integ
let_it_be_with_reload(:installation) { create(:jira_connect_installation, instance_url: 'https://other_gitlab.example.com') }
it 'sends an installed event to the instance', :aggregate_failures do
- expect_next_instance_of(JiraConnectInstallations::ProxyLifecycleEventService, installation, :installed,
-'https://other_gitlab.example.com') do |proxy_lifecycle_events_service|
+ expect_next_instance_of(
+ JiraConnectInstallations::ProxyLifecycleEventService, installation, :installed, 'https://other_gitlab.example.com'
+ ) do |proxy_lifecycle_events_service|
expect(proxy_lifecycle_events_service).to receive(:execute).and_return(ServiceResponse.new(status: :success))
end
@@ -62,19 +63,19 @@ RSpec.describe JiraConnectInstallations::UpdateService, feature_category: :integ
stub_request(:post, 'https://other_gitlab.example.com/-/jira_connect/events/uninstalled')
end
- it 'starts an async worker to send an uninstalled event to the previous instance' do
- expect(JiraConnect::SendUninstalledHookWorker).to receive(:perform_async).with(installation.id, 'https://other_gitlab.example.com')
-
+ it 'sends an installed event to the instance and updates instance_url' do
expect(JiraConnectInstallations::ProxyLifecycleEventService)
.to receive(:execute).with(installation, :installed, 'https://gitlab.example.com')
.and_return(ServiceResponse.new(status: :success))
+ expect(JiraConnect::SendUninstalledHookWorker).not_to receive(:perform_async)
+
execute_service
expect(installation.instance_url).to eq(update_params[:instance_url])
end
- context 'and the new instance_url is empty' do
+ context 'and the new instance_url is nil' do
let(:update_params) { { instance_url: nil } }
it 'starts an async worker to send an uninstalled event to the previous instance' do
@@ -98,8 +99,9 @@ RSpec.describe JiraConnectInstallations::UpdateService, feature_category: :integ
let(:update_params) { { instance_url: 'https://gitlab.example.com' } }
it 'sends an installed event to the instance and updates instance_url' do
- expect_next_instance_of(JiraConnectInstallations::ProxyLifecycleEventService, installation, :installed,
-'https://gitlab.example.com') do |proxy_lifecycle_events_service|
+ expect_next_instance_of(
+ JiraConnectInstallations::ProxyLifecycleEventService, installation, :installed, 'https://gitlab.example.com'
+ ) do |proxy_lifecycle_events_service|
expect(proxy_lifecycle_events_service).to receive(:execute).and_return(ServiceResponse.new(status: :success))
end
diff --git a/spec/services/keys/revoke_service_spec.rb b/spec/services/keys/revoke_service_spec.rb
new file mode 100644
index 00000000000..ec07701b4b7
--- /dev/null
+++ b/spec/services/keys/revoke_service_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Keys::RevokeService, feature_category: :source_code_management do
+ let(:user) { create(:user) }
+
+ subject(:service) { described_class.new(user) }
+
+ it 'destroys a key' do
+ key = create(:key)
+
+ expect { service.execute(key) }.to change { key.persisted? }.from(true).to(false)
+ end
+
+ it 'unverifies associated signatures' do
+ key = create(:key)
+ signature = create(:ssh_signature, key: key)
+
+ expect do
+ service.execute(key)
+ end.to change { signature.reload.key }.from(key).to(nil)
+ .and change { signature.reload.verification_status }.from('verified').to('revoked_key')
+ end
+
+ it 'does not unverifies signatures if destroy fails' do
+ key = create(:key)
+ signature = create(:ssh_signature, key: key)
+
+ expect(key).to receive(:destroy).and_return(false)
+
+ expect { service.execute(key) }.not_to change { signature.reload.verification_status }
+ expect(key).to be_persisted
+ end
+
+ context 'when revoke_ssh_signatures disabled' do
+ before do
+ stub_feature_flags(revoke_ssh_signatures: false)
+ end
+
+ it 'does not unverifies signatures' do
+ key = create(:key)
+ signature = create(:ssh_signature, key: key)
+
+ expect { service.execute(key) }.not_to change { signature.reload.verification_status }
+ end
+ end
+end
diff --git a/spec/services/lfs/file_transformer_spec.rb b/spec/services/lfs/file_transformer_spec.rb
index 9d4d8851c2d..c90d7af022f 100644
--- a/spec/services/lfs/file_transformer_spec.rb
+++ b/spec/services/lfs/file_transformer_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Lfs::FileTransformer, feature_category: :git_lfs do
+RSpec.describe Lfs::FileTransformer, feature_category: :source_code_management do
let(:project) { create(:project, :repository, :wiki_repo) }
let(:repository) { project.repository }
let(:file_content) { 'Test file content' }
diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb
index d26bab7bb0a..ca5c052d032 100644
--- a/spec/services/members/approve_access_request_service_spec.rb
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -30,6 +30,14 @@ RSpec.describe Members::ApproveAccessRequestService do
expect(member.requested_at).to be_nil
end
+ it 'calls the method to resolve access request for the approver' do
+ expect_next_instance_of(described_class) do |instance|
+ expect(instance).to receive(:resolve_access_request_todos).with(current_user, access_requester)
+ end
+
+ described_class.new(current_user, params).execute(access_requester, **opts)
+ end
+
context 'with a custom access level' do
let(:params) { { access_level: custom_access_level } }
diff --git a/spec/services/members/base_service_spec.rb b/spec/services/members/base_service_spec.rb
new file mode 100644
index 00000000000..b2db599db9c
--- /dev/null
+++ b/spec/services/members/base_service_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Members::BaseService, feature_category: :projects do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:access_requester) { create(:group_member) }
+
+ describe '#resolve_access_request_todos' do
+ it 'calls the resolve_access_request_todos of todo service' do
+ expect_next_instance_of(TodoService) do |todo_service|
+ expect(todo_service)
+ .to receive(:resolve_access_request_todos).with(current_user, access_requester)
+ end
+
+ described_class.new.send(:resolve_access_request_todos, current_user, access_requester)
+ end
+ end
+end
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index d8a8d5881bf..2b956bec469 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -41,6 +41,14 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do
.not_to change { member_user.notification_settings.count }
end
end
+
+ it 'resolves the access request todos for the owner' do
+ expect_next_instance_of(described_class) do |instance|
+ expect(instance).to receive(:resolve_access_request_todos).with(current_user, member)
+ end
+
+ described_class.new(current_user).execute(member, **opts)
+ end
end
shared_examples 'a service destroying a member with access' do
@@ -111,26 +119,6 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do
subject(:destroy_member) { service_object.execute(member_to_delete, **opts) }
- shared_examples_for 'deletes the member without using a lock' do
- it 'does not try to perform the delete within a lock' do
- # `UpdateHighestRole` concern also uses locks to peform work
- # whenever a Member is committed, so that needs to be accounted for.
- lock_key_for_update_highest_role = "update_highest_role:#{member_to_delete.user_id}"
- expect(Gitlab::ExclusiveLease)
- .to receive(:new).with(lock_key_for_update_highest_role, timeout: 10.minutes.to_i).and_call_original
-
- # We do not use any locks for member deletion process.
- expect(Gitlab::ExclusiveLease)
- .not_to receive(:new).with(lock_key, timeout: timeout)
-
- destroy_member
- end
-
- it 'destroys the membership' do
- expect { destroy_member }.to change { entity.members.count }.by(-1)
- end
- end
-
context 'for group members' do
before do
group.add_owner(current_user)
@@ -171,13 +159,70 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do
context 'deleting group members that are not owners' do
let!(:member_to_delete) { group.add_developer(member_user) }
- it_behaves_like 'deletes the member without using a lock' do
- let(:entity) { group }
+ it 'does not try to perform the deletion of the member within a lock' do
+ # We need to account for other places involved in the Member deletion process that
+ # uses ExclusiveLease.
+
+ # 1. `UpdateHighestRole` concern uses locks to peform work
+ # whenever a Member is committed, so that needs to be accounted for.
+ lock_key_for_update_highest_role = "update_highest_role:#{member_to_delete.user_id}"
+
+ expect(Gitlab::ExclusiveLease)
+ .to receive(:new).with(lock_key_for_update_highest_role, timeout: 10.minutes.to_i).and_call_original
+
+ # 2. `Users::RefreshAuthorizedProjectsService` also uses locks to perform work,
+ # whenever a user's authorizations has to be refreshed, so that needs to be accounted for as well.
+ lock_key_for_authorizations_refresh = "refresh_authorized_projects:#{member_to_delete.user_id}"
+
+ expect(Gitlab::ExclusiveLease)
+ .to receive(:new).with(lock_key_for_authorizations_refresh, timeout: 1.minute.to_i).and_call_original
+
+ # We do not use any locks for the member deletion process, from within this service.
+ expect(Gitlab::ExclusiveLease)
+ .not_to receive(:new).with(lock_key, timeout: timeout)
+
+ destroy_member
+ end
+
+ it 'destroys the membership' do
+ expect { destroy_member }.to change { group.members.count }.by(-1)
end
end
end
context 'for project members' do
+ shared_examples_for 'deletes the project member without using a lock' do
+ it 'does not try to perform the deletion of a project member within a lock' do
+ # We need to account for other places involved in the Member deletion process that
+ # uses ExclusiveLease.
+
+ # 1. `UpdateHighestRole` concern uses locks to peform work
+ # whenever a Member is committed, so that needs to be accounted for.
+ lock_key_for_update_highest_role = "update_highest_role:#{member_to_delete.user_id}"
+
+ expect(Gitlab::ExclusiveLease)
+ .to receive(:new).with(lock_key_for_update_highest_role, timeout: 10.minutes.to_i).and_call_original
+
+ # 2. `AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker` also uses locks to perform work,
+ # whenever a user's authorizations has to be refreshed, so that needs to be accounted for as well.
+ lock_key_for_authorizations_refresh =
+ "authorized_project_update/project_recalculate_worker/projects/#{member_to_delete.project.id}"
+
+ expect(Gitlab::ExclusiveLease)
+ .to receive(:new).with(lock_key_for_authorizations_refresh, timeout: 10.seconds).and_call_original
+
+ # We do not use any locks for the member deletion process, from within this service.
+ expect(Gitlab::ExclusiveLease)
+ .not_to receive(:new).with(lock_key, timeout: timeout)
+
+ destroy_member
+ end
+
+ it 'destroys the membership' do
+ expect { destroy_member }.to change { entity.members.count }.by(-1)
+ end
+ end
+
before do
group_project.add_owner(current_user)
end
@@ -186,16 +231,16 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do
context 'deleting project owners' do
let!(:member_to_delete) { entity.add_owner(member_user) }
- it_behaves_like 'deletes the member without using a lock' do
+ it_behaves_like 'deletes the project member without using a lock' do
let(:entity) { group_project }
end
end
end
- context 'deleting project memebrs that are not owners' do
+ context 'deleting project members that are not owners' do
let!(:member_to_delete) { group_project.add_developer(member_user) }
- it_behaves_like 'deletes the member without using a lock' do
+ it_behaves_like 'deletes the project member without using a lock' do
let(:entity) { group_project }
end
end
diff --git a/spec/services/members/projects/creator_service_spec.rb b/spec/services/members/projects/creator_service_spec.rb
index 8304bee2ffc..5dfba7adf0f 100644
--- a/spec/services/members/projects/creator_service_spec.rb
+++ b/spec/services/members/projects/creator_service_spec.rb
@@ -27,6 +27,8 @@ RSpec.describe Members::Projects::CreatorService do
context 'authorized projects update' do
it 'schedules a single project authorization update job when called multiple times' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to receive(:bulk_perform_in).once
1.upto(3) do
diff --git a/spec/services/merge_requests/after_create_service_spec.rb b/spec/services/merge_requests/after_create_service_spec.rb
index f477b2166d9..f2823b1f0c7 100644
--- a/spec/services/merge_requests/after_create_service_spec.rb
+++ b/spec/services/merge_requests/after_create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::AfterCreateService do
+RSpec.describe MergeRequests::AfterCreateService, feature_category: :code_review_workflow do
let_it_be(:merge_request) { create(:merge_request) }
subject(:after_create_service) do
@@ -126,6 +126,17 @@ RSpec.describe MergeRequests::AfterCreateService do
end
end
+ it 'updates the prepared_at' do
+ # Need to reset the `prepared_at` since it can be already set in preceding tests.
+ merge_request.update!(prepared_at: nil)
+
+ freeze_time do
+ expect { execute_service }.to change { merge_request.prepared_at }
+ .from(nil)
+ .to(Time.current)
+ end
+ end
+
it 'increments the usage data counter of create event' do
counter = Gitlab::UsageDataCounters::MergeRequestCounter
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 79c779678a4..0fcfc16af73 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe MergeRequests::BuildService do
+RSpec.describe MergeRequests::BuildService, feature_category: :code_review_workflow do
using RSpec::Parameterized::TableSyntax
include RepoHelpers
include ProjectForksHelper
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index b3c4ed4c544..2c0817550c6 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::CloseService do
+RSpec.describe MergeRequests::CloseService, feature_category: :code_review_workflow do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:guest) { create(:user) }
diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb
index 0eefbed252b..7bb0dd723a1 100644
--- a/spec/services/merge_requests/create_from_issue_service_spec.rb
+++ b/spec/services/merge_requests/create_from_issue_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::CreateFromIssueService do
+RSpec.describe MergeRequests::CreateFromIssueService, feature_category: :code_review_workflow do
include ProjectForksHelper
let(:project) { create(:project, :repository) }
diff --git a/spec/services/merge_requests/create_pipeline_service_spec.rb b/spec/services/merge_requests/create_pipeline_service_spec.rb
index 7984fff3031..f11e3d0d1df 100644
--- a/spec/services/merge_requests/create_pipeline_service_spec.rb
+++ b/spec/services/merge_requests/create_pipeline_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe MergeRequests::CreatePipelineService, :clean_gitlab_redis_cache do
include ProjectForksHelper
- let_it_be(:project, reload: true) { create(:project, :repository) }
+ let_it_be(:project, refind: true) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let(:service) { described_class.new(project: project, current_user: actor, params: params) }
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index da8e8d944d6..394fc269ac3 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do
+RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state, feature_category: :code_review_workflow do
include ProjectForksHelper
let(:project) { create(:project, :repository) }
@@ -501,40 +501,12 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do
project.add_developer(user)
end
- context 'when async_merge_request_diff_creation is enabled' do
- before do
- stub_feature_flags(async_merge_request_diff_creation: true)
- end
-
- it 'creates the merge request', :sidekiq_inline do
- expect_next_instance_of(MergeRequest) do |instance|
- expect(instance).not_to receive(:eager_fetch_ref!)
- 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
-
- 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
+ it 'creates the merge request', :sidekiq_inline do
+ 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
+ expect(merge_request).to be_persisted
+ expect(merge_request.iid).to be > 0
+ expect(merge_request.merge_request_diff).not_to be_empty
end
it 'does not create the merge request when the target project is archived' do
diff --git a/spec/services/merge_requests/export_csv_service_spec.rb b/spec/services/merge_requests/export_csv_service_spec.rb
index 97217e979a5..2f0036845e7 100644
--- a/spec/services/merge_requests/export_csv_service_spec.rb
+++ b/spec/services/merge_requests/export_csv_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::ExportCsvService do
+RSpec.describe MergeRequests::ExportCsvService, feature_category: :importers do
let_it_be(:merge_request) { create(:merge_request) }
let(:csv) { CSV.parse(subject.csv_data, headers: true).first }
@@ -113,5 +113,21 @@ RSpec.describe MergeRequests::ExportCsvService do
end
end
end
+
+ describe '#email' do
+ let_it_be(:user) { create(:user) }
+
+ it 'emails csv' do
+ expect { subject.email(user) }.to change { ActionMailer::Base.deliveries.count }
+ end
+
+ it 'renders with a target filesize' do
+ expect_next_instance_of(CsvBuilder) do |csv_builder|
+ expect(csv_builder).to receive(:render).with(described_class::TARGET_FILESIZE).once
+ end
+
+ subject.email(user)
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/link_lfs_objects_service_spec.rb b/spec/services/merge_requests/link_lfs_objects_service_spec.rb
index 96cb72baac2..9762b600eab 100644
--- a/spec/services/merge_requests/link_lfs_objects_service_spec.rb
+++ b/spec/services/merge_requests/link_lfs_objects_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::LinkLfsObjectsService, :sidekiq_inline do
+RSpec.describe MergeRequests::LinkLfsObjectsService, :sidekiq_inline, feature_category: :code_review_workflow do
include ProjectForksHelper
include RepoHelpers
diff --git a/spec/services/merge_requests/pushed_branches_service_spec.rb b/spec/services/merge_requests/pushed_branches_service_spec.rb
index 59424263ec5..cb5d0a6bd25 100644
--- a/spec/services/merge_requests/pushed_branches_service_spec.rb
+++ b/spec/services/merge_requests/pushed_branches_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::PushedBranchesService do
+RSpec.describe MergeRequests::PushedBranchesService, feature_category: :source_code_management do
let(:project) { create(:project) }
let!(:service) { described_class.new(project: project, current_user: nil, params: { changes: pushed_branches }) }
diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb
index 316f20d8276..704dc1f9000 100644
--- a/spec/services/merge_requests/rebase_service_spec.rb
+++ b/spec/services/merge_requests/rebase_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::RebaseService do
+RSpec.describe MergeRequests::RebaseService, feature_category: :source_code_management do
include ProjectForksHelper
let(:user) { create(:user) }
diff --git a/spec/services/merge_requests/remove_approval_service_spec.rb b/spec/services/merge_requests/remove_approval_service_spec.rb
index fd8240935e8..e4e54db5013 100644
--- a/spec/services/merge_requests/remove_approval_service_spec.rb
+++ b/spec/services/merge_requests/remove_approval_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::RemoveApprovalService do
+RSpec.describe MergeRequests::RemoveApprovalService, feature_category: :code_review_workflow do
describe '#execute' do
let(:user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/services/merge_requests/retarget_chain_service_spec.rb b/spec/services/merge_requests/retarget_chain_service_spec.rb
index 187dd0cf589..ef8cd0a861e 100644
--- a/spec/services/merge_requests/retarget_chain_service_spec.rb
+++ b/spec/services/merge_requests/retarget_chain_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::RetargetChainService do
+RSpec.describe MergeRequests::RetargetChainService, feature_category: :code_review_workflow do
include ProjectForksHelper
let_it_be(:user) { create(:user) }
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 344d93fc5ca..e20ebf18e7c 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -196,7 +196,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_milestone_changed_action).once.with(user: user)
- opts[:milestone] = milestone
+ opts[:milestone_id] = milestone.id
MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request)
end
@@ -236,27 +236,17 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
end
context 'updating milestone' do
- RSpec.shared_examples 'updates milestone' do
+ context 'with milestone_id param' do
+ let(:opts) { { milestone_id: milestone.id } }
+
it 'sets milestone' do
expect(@merge_request.milestone).to eq milestone
end
end
- context 'when milestone_id param' do
- let(:opts) { { milestone_id: milestone.id } }
-
- it_behaves_like 'updates milestone'
- end
-
- context 'when milestone param' do
- let(:opts) { { milestone: milestone } }
-
- it_behaves_like 'updates milestone'
- end
-
context 'milestone counters cache reset' do
let(:milestone_old) { create(:milestone, project: project) }
- let(:opts) { { milestone: milestone_old } }
+ let(:opts) { { milestone_id: milestone_old.id } }
it 'deletes milestone counters' do
expect_next_instance_of(Milestones::MergeRequestsCountService, milestone_old) do |service|
@@ -267,7 +257,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
expect(service).to receive(:delete_cache).and_call_original
end
- update_merge_request(milestone: milestone)
+ update_merge_request(milestone_id: milestone.id)
end
it 'deletes milestone counters when the milestone is removed' do
@@ -275,17 +265,17 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
expect(service).to receive(:delete_cache).and_call_original
end
- update_merge_request(milestone: nil)
+ update_merge_request(milestone_id: nil)
end
it 'deletes milestone counters when the milestone was not set' do
- update_merge_request(milestone: nil)
+ update_merge_request(milestone_id: nil)
expect_next_instance_of(Milestones::MergeRequestsCountService, milestone) do |service|
expect(service).to receive(:delete_cache).and_call_original
end
- update_merge_request(milestone: milestone)
+ update_merge_request(milestone_id: milestone.id)
end
end
end
@@ -754,12 +744,12 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
expect(service).to receive(:async_execute)
end
- update_merge_request({ milestone: create(:milestone, project: project) })
+ update_merge_request(milestone_id: create(:milestone, project: project).id)
end
it 'sends notifications for subscribers of changed milestone', :sidekiq_might_not_need_inline do
perform_enqueued_jobs do
- update_merge_request(milestone: create(:milestone, project: project))
+ update_merge_request(milestone_id: create(:milestone, project: project).id)
end
should_email(subscriber)
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 22606cc2461..1ee9e51433e 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Notes::CreateService do
+RSpec.describe Notes::CreateService, feature_category: :team_planning do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:user) { create(:user) }
@@ -116,6 +116,35 @@ RSpec.describe Notes::CreateService do
end
end
+ context 'in a commit', :snowplow do
+ let_it_be(:commit) { create(:commit, project: project) }
+ let(:opts) { { note: 'Awesome comment', noteable_type: 'Commit', commit_id: commit.id } }
+
+ let(:counter) { Gitlab::UsageDataCounters::NoteCounter }
+
+ let(:execute_create_service) { described_class.new(project, user, opts).execute }
+
+ before do
+ stub_feature_flags(notes_create_service_tracking: false)
+ end
+
+ it 'tracks commit comment usage data', :clean_gitlab_redis_shared_state do
+ expect(counter).to receive(:count).with(:create, 'Commit').and_call_original
+
+ expect do
+ execute_create_service
+ end.to change { counter.read(:create, 'Commit') }.by(1)
+ end
+
+ it_behaves_like 'Snowplow event tracking with Redis context' do
+ let(:category) { described_class.name }
+ let(:action) { 'create_commit_comment' }
+ let(:label) { 'counts.commit_comment' }
+ let(:namespace) { project.namespace }
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase4 }
+ end
+ end
+
describe 'event tracking', :snowplow do
let(:event) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_COMMENT_ADDED }
let(:execute_create_service) { described_class.new(project, user, opts).execute }
@@ -409,7 +438,7 @@ RSpec.describe Notes::CreateService do
end
end
- context 'for merge requests' do
+ context 'for merge requests', feature_category: :code_review_workflow do
let_it_be(:merge_request) { create(:merge_request, source_project: project, labels: [bug_label]) }
let(:issuable) { merge_request }
@@ -483,7 +512,7 @@ RSpec.describe Notes::CreateService do
end
end
- context 'personal snippet note' do
+ context 'personal snippet note', feature_category: :source_code_management do
subject { described_class.new(nil, user, params).execute }
let(:snippet) { create(:personal_snippet) }
@@ -504,7 +533,7 @@ RSpec.describe Notes::CreateService do
end
end
- context 'design note' do
+ context 'design note', feature_category: :design_management do
subject(:service) { described_class.new(project, user, params) }
let_it_be(:design) { create(:design, :with_file) }
diff --git a/spec/services/notes/destroy_service_spec.rb b/spec/services/notes/destroy_service_spec.rb
index 82caec52aee..744808525f5 100644
--- a/spec/services/notes/destroy_service_spec.rb
+++ b/spec/services/notes/destroy_service_spec.rb
@@ -91,5 +91,13 @@ RSpec.describe Notes::DestroyService do
end
end
end
+
+ it 'tracks design comment removal' do
+ note = create(:note_on_design, project: project)
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_design_comment_removed_action).with(author: note.author,
+ project: project)
+
+ described_class.new(project, user).execute(note)
+ end
end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 1ad9234c939..4161f93cdac 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -99,7 +99,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- shared_examples 'is not able to send notifications' do
+ shared_examples 'is not able to send notifications' do |check_delivery_jobs_queue: false|
it 'does not send any notification' do
user_1 = create(:user)
recipient_1 = NotificationRecipient.new(user_1, :custom, custom_action: :new_release)
@@ -107,12 +107,21 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
expect(Gitlab::AppLogger).to receive(:warn).with(message: 'Skipping sending notifications', user: current_user.id, klass: object.class.to_s, object_id: object.id)
- action
+ if check_delivery_jobs_queue
+ expect do
+ action
+ end.to not_enqueue_mail_with(Notify, notification_method, @u_mentioned, anything, anything)
+ .and(not_enqueue_mail_with(Notify, notification_method, @u_guest_watcher, anything, anything))
+ .and(not_enqueue_mail_with(Notify, notification_method, user_1, anything, anything))
+ .and(not_enqueue_mail_with(Notify, notification_method, current_user, anything, anything))
+ else
+ action
- should_not_email(@u_mentioned)
- should_not_email(@u_guest_watcher)
- should_not_email(user_1)
- should_not_email(current_user)
+ should_not_email(@u_mentioned)
+ should_not_email(@u_guest_watcher)
+ should_not_email(user_1)
+ should_not_email(current_user)
+ end
end
end
@@ -123,13 +132,19 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
# * notification trigger
# * participant
#
- shared_examples 'participating by note notification' do
+ shared_examples 'participating by note notification' do |check_delivery_jobs_queue: false|
it 'emails the participant' do
create(:note_on_issue, noteable: issuable, project_id: project.id, note: 'anything', author: participant)
- notification_trigger
+ if check_delivery_jobs_queue
+ expect do
+ notification_trigger
+ end.to enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(participant))
+ else
+ notification_trigger
- should_email(participant)
+ should_email(participant)
+ end
end
context 'for subgroups' do
@@ -140,14 +155,20 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
it 'emails the participant' do
create(:note_on_issue, noteable: issuable, project_id: project.id, note: 'anything', author: @pg_participant)
- notification_trigger
+ if check_delivery_jobs_queue
+ expect do
+ notification_trigger
+ end.to enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(@pg_participant))
+ else
+ notification_trigger
- should_email_nested_group_user(@pg_participant)
+ should_email_nested_group_user(@pg_participant)
+ end
end
end
end
- shared_examples 'participating by confidential note notification' do
+ shared_examples 'participating by confidential note notification' do |check_delivery_jobs_queue: false|
context 'when user is mentioned on confidential note' do
let_it_be(:guest_1) { create(:user) }
let_it_be(:guest_2) { create(:user) }
@@ -164,34 +185,55 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
note_text = "Mentions #{guest_2.to_reference}"
create(:note_on_issue, noteable: issuable, project_id: project.id, note: confidential_note_text, confidential: true)
create(:note_on_issue, noteable: issuable, project_id: project.id, note: note_text)
- reset_delivered_emails!
- notification_trigger
+ if check_delivery_jobs_queue
+ expect do
+ notification_trigger
+ end.to enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(guest_2))
+ .and(enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(reporter)))
+ .and(not_enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(guest_1)))
+ else
+ reset_delivered_emails!
+
+ notification_trigger
- should_not_email(guest_1)
- should_email(guest_2)
- should_email(reporter)
+ should_not_email(guest_1)
+ should_email(guest_2)
+ should_email(reporter)
+ end
end
end
end
- shared_examples 'participating by assignee notification' do
+ shared_examples 'participating by assignee notification' do |check_delivery_jobs_queue: false|
it 'emails the participant' do
issuable.assignees << participant
- notification_trigger
+ if check_delivery_jobs_queue
+ expect do
+ notification_trigger
+ end.to enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(participant))
+ else
+ notification_trigger
- should_email(participant)
+ should_email(participant)
+ end
end
end
- shared_examples 'participating by author notification' do
+ shared_examples 'participating by author notification' do |check_delivery_jobs_queue: false|
it 'emails the participant' do
issuable.author = participant
- notification_trigger
+ if check_delivery_jobs_queue
+ expect do
+ notification_trigger
+ end.to enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(participant))
+ else
+ notification_trigger
- should_email(participant)
+ should_email(participant)
+ end
end
end
@@ -205,10 +247,10 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- shared_examples_for 'participating notifications' do
- it_behaves_like 'participating by note notification'
- it_behaves_like 'participating by author notification'
- it_behaves_like 'participating by assignee notification'
+ shared_examples_for 'participating notifications' do |check_delivery_jobs_queue: false|
+ it_behaves_like 'participating by note notification', check_delivery_jobs_queue: check_delivery_jobs_queue
+ it_behaves_like 'participating by author notification', check_delivery_jobs_queue: check_delivery_jobs_queue
+ it_behaves_like 'participating by assignee notification', check_delivery_jobs_queue: check_delivery_jobs_queue
end
describe '.permitted_actions' do
@@ -1159,7 +1201,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- describe 'Issues', :deliver_mails_inline do
+ describe 'Issues', :aggregate_failures do
let(:another_project) { create(:project, :public, namespace: group) }
let(:issue) { create :issue, project: project, assignees: [assignee], description: 'cc @participant @unsubscribed_mentioned' }
@@ -1184,79 +1226,77 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
describe '#new_issue' do
it 'notifies the expected users' do
- notification.new_issue(issue, @u_disabled)
-
- should_email(assignee)
- should_email(@u_watcher)
- should_email(@u_guest_watcher)
- should_email(@u_guest_custom)
- should_email(@u_custom_global)
- should_email(@u_participant_mentioned)
- should_email(@g_global_watcher)
- should_email(@g_watcher)
- should_email(@unsubscribed_mentioned)
- should_email_nested_group_user(@pg_watcher)
- should_not_email(@u_mentioned)
- should_not_email(@u_participating)
- should_not_email(@u_disabled)
- should_not_email(@u_lazy_participant)
- should_not_email_nested_group_user(@pg_disabled)
- should_not_email_nested_group_user(@pg_mention)
- end
-
- it do
- create_global_setting_for(issue.assignees.first, :mention)
- notification.new_issue(issue, @u_disabled)
+ expect do
+ notification.new_issue(issue, @u_disabled)
+ end.to enqueue_mail_with(Notify, :new_issue_email, assignee, issue, 'assigned')
+ .and(enqueue_mail_with(Notify, :new_issue_email, @u_watcher, issue, nil))
+ .and(enqueue_mail_with(Notify, :new_issue_email, @u_guest_watcher, issue, nil))
+ .and(enqueue_mail_with(Notify, :new_issue_email, @u_guest_custom, issue, nil))
+ .and(enqueue_mail_with(Notify, :new_issue_email, @u_custom_global, issue, nil))
+ .and(enqueue_mail_with(Notify, :new_issue_email, @u_participant_mentioned, issue, 'mentioned'))
+ .and(enqueue_mail_with(Notify, :new_issue_email, @g_global_watcher.id, issue.id, nil))
+ .and(enqueue_mail_with(Notify, :new_issue_email, @g_watcher, issue, nil))
+ .and(enqueue_mail_with(Notify, :new_issue_email, @unsubscribed_mentioned, issue, 'mentioned'))
+ .and(enqueue_mail_with(Notify, :new_issue_email, @pg_watcher, issue, nil))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, @u_mentioned, anything, anything))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, @u_participating, anything, anything))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, @u_disabled, anything, anything))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, @u_lazy_participant, anything, anything))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, @pg_disabled, anything, anything))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, @pg_mention, anything, anything))
+ end
+
+ context 'when user has an only mention notification setting' do
+ before do
+ create_global_setting_for(issue.assignees.first, :mention)
+ end
- should_not_email(issue.assignees.first)
+ it 'does not send assignee notifications' do
+ expect do
+ notification.new_issue(issue, @u_disabled)
+ end.to not_enqueue_mail_with(Notify, :new_issue_email, issue.assignees.first, anything, anything)
+ end
end
it 'properly prioritizes notification reason' do
# have assignee be both assigned and mentioned
issue.update_attribute(:description, "/cc #{assignee.to_reference} #{@u_mentioned.to_reference}")
- notification.new_issue(issue, @u_disabled)
-
- email = find_email_for(assignee)
- expect(email).to have_header('X-GitLab-NotificationReason', 'assigned')
-
- email = find_email_for(@u_mentioned)
- expect(email).to have_header('X-GitLab-NotificationReason', 'mentioned')
+ expect do
+ notification.new_issue(issue, @u_disabled)
+ end.to enqueue_mail_with(Notify, :new_issue_email, assignee, issue, 'assigned')
+ .and(enqueue_mail_with(Notify, :new_issue_email, @u_mentioned, issue, 'mentioned'))
end
it 'adds "assigned" reason for assignees if any' do
- notification.new_issue(issue, @u_disabled)
-
- email = find_email_for(assignee)
-
- expect(email).to have_header('X-GitLab-NotificationReason', 'assigned')
+ expect do
+ notification.new_issue(issue, @u_disabled)
+ end.to enqueue_mail_with(Notify, :new_issue_email, assignee, issue, 'assigned')
end
it "emails any mentioned users with the mention level" do
issue.description = @u_mentioned.to_reference
- notification.new_issue(issue, @u_disabled)
-
- email = find_email_for(@u_mentioned)
- expect(email).not_to be_nil
- expect(email).to have_header('X-GitLab-NotificationReason', 'mentioned')
+ expect do
+ notification.new_issue(issue, @u_disabled)
+ end.to enqueue_mail_with(Notify, :new_issue_email, @u_mentioned, issue, 'mentioned')
end
it "emails the author if they've opted into notifications about their activity" do
issue.author.notified_of_own_activity = true
- notification.new_issue(issue, issue.author)
-
- should_email(issue.author)
+ expect do
+ notification.new_issue(issue, issue.author)
+ end.to enqueue_mail_with(Notify, :new_issue_email, issue.author, issue, 'own_activity')
end
it "doesn't email the author if they haven't opted into notifications about their activity" do
- notification.new_issue(issue, issue.author)
-
- should_not_email(issue.author)
+ expect do
+ notification.new_issue(issue, issue.author)
+ end.to not_enqueue_mail_with(Notify, :new_issue_email, issue.author, anything, anything)
end
- it "emails subscribers of the issue's labels" do
+ it "emails subscribers of the issue's labels and adds `subscribed` reason" do
user_1 = create(:user)
user_2 = create(:user)
user_3 = create(:user)
@@ -1269,27 +1309,15 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
group_label.toggle_subscription(user_3, another_project)
group_label.toggle_subscription(user_4)
- notification.new_issue(issue, @u_disabled)
-
- should_email(user_1)
- should_email(user_2)
- should_not_email(user_3)
- should_email(user_4)
- end
-
- it 'adds "subscribed" reason to subscriber emails' do
- user_1 = create(:user)
- label = create(:label, project: project, issues: [issue])
- issue.reload
- label.subscribe(user_1)
-
- notification.new_issue(issue, @u_disabled)
-
- email = find_email_for(user_1)
- expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::SUBSCRIBED)
+ expect do
+ notification.new_issue(issue, issue.author)
+ end.to enqueue_mail_with(Notify, :new_issue_email, user_1, issue, NotificationReason::SUBSCRIBED)
+ .and(enqueue_mail_with(Notify, :new_issue_email, user_2, issue, NotificationReason::SUBSCRIBED))
+ .and(enqueue_mail_with(Notify, :new_issue_email, user_4, issue, NotificationReason::SUBSCRIBED))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, user_3, anything, anything))
end
- it_behaves_like 'project emails are disabled' do
+ it_behaves_like 'project emails are disabled', check_delivery_jobs_queue: true do
let(:notification_target) { issue }
let(:notification_trigger) { notification.new_issue(issue, @u_disabled) }
end
@@ -1315,35 +1343,33 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
label.toggle_subscription(guest, project)
label.toggle_subscription(admin, project)
- reset_delivered_emails!
-
- notification.new_issue(confidential_issue, @u_disabled)
-
- should_not_email(@u_guest_watcher)
- should_not_email(non_member)
- should_not_email(author)
- should_not_email(guest)
- should_email(assignee)
- should_email(member)
- should_email(admin)
+ expect do
+ notification.new_issue(confidential_issue, issue.author)
+ end.to enqueue_mail_with(Notify, :new_issue_email, assignee, confidential_issue, NotificationReason::ASSIGNED)
+ .and(enqueue_mail_with(Notify, :new_issue_email, member, confidential_issue, NotificationReason::SUBSCRIBED))
+ .and(enqueue_mail_with(Notify, :new_issue_email, admin, confidential_issue, NotificationReason::SUBSCRIBED))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, @u_guest_watcher, anything, anything))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, non_member, anything, anything))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, author, anything, anything))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, guest, anything, anything))
end
end
context 'when the author is not allowed to trigger notifications' do
- let(:current_user) { nil }
let(:object) { issue }
let(:action) { notification.new_issue(issue, current_user) }
+ let(:notification_method) { :new_issue_email }
context 'because they are blocked' do
let(:current_user) { create(:user, :blocked) }
- include_examples 'is not able to send notifications'
+ include_examples 'is not able to send notifications', check_delivery_jobs_queue: true
end
context 'because they are a ghost' do
let(:current_user) { create(:user, :ghost) }
- include_examples 'is not able to send notifications'
+ include_examples 'is not able to send notifications', check_delivery_jobs_queue: true
end
end
end
@@ -1354,9 +1380,52 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
let(:object) { mentionable }
let(:action) { send_notifications(@u_mentioned, current_user: current_user) }
- include_examples 'notifications for new mentions'
+ it 'sends no emails when no new mentions are present' do
+ send_notifications
- it_behaves_like 'project emails are disabled' do
+ expect_no_delivery_jobs
+ end
+
+ it 'emails new mentions with a watch level higher than mention' do
+ expect do
+ send_notifications(@u_watcher, @u_participant_mentioned, @u_custom_global, @u_mentioned)
+ end.to have_only_enqueued_mail_with_args(
+ Notify,
+ :new_mention_in_issue_email,
+ [@u_watcher.id, mentionable.id, anything, anything],
+ [@u_participant_mentioned.id, mentionable.id, anything, anything],
+ [@u_custom_global.id, mentionable.id, anything, anything],
+ [@u_mentioned.id, mentionable.id, anything, anything]
+ )
+ end
+
+ it 'does not email new mentions with a watch level equal to or less than mention' do
+ send_notifications(@u_disabled)
+
+ expect_no_delivery_jobs
+ end
+
+ it 'emails new mentions despite being unsubscribed' do
+ expect do
+ send_notifications(@unsubscribed_mentioned)
+ end.to have_only_enqueued_mail_with_args(
+ Notify,
+ :new_mention_in_issue_email,
+ [@unsubscribed_mentioned.id, mentionable.id, anything, anything]
+ )
+ end
+
+ it 'sends the proper notification reason header' do
+ expect do
+ send_notifications(@u_watcher)
+ end.to have_only_enqueued_mail_with_args(
+ Notify,
+ :new_mention_in_issue_email,
+ [@u_watcher.id, mentionable.id, anything, NotificationReason::MENTIONED]
+ )
+ end
+
+ it_behaves_like 'project emails are disabled', check_delivery_jobs_queue: true do
let(:notification_target) { issue }
let(:notification_trigger) { send_notifications(@u_watcher, @u_participant_mentioned, @u_custom_global, @u_mentioned) }
end
@@ -1364,117 +1433,130 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
context 'where current_user is blocked' do
let(:current_user) { create(:user, :blocked) }
- include_examples 'is not able to send notifications'
+ include_examples 'is not able to send notifications', check_delivery_jobs_queue: true
end
context 'where current_user is a ghost' do
let(:current_user) { create(:user, :ghost) }
- include_examples 'is not able to send notifications'
+ include_examples 'is not able to send notifications', check_delivery_jobs_queue: true
end
end
describe '#reassigned_issue' do
+ let(:anything_args) { [anything, anything, anything, anything] }
+ let(:mailer_method) { :reassigned_issue_email }
+
before do
update_custom_notification(:reassign_issue, @u_guest_custom, resource: project)
update_custom_notification(:reassign_issue, @u_custom_global)
end
it 'emails new assignee' do
- notification.reassigned_issue(issue, @u_disabled, [assignee])
-
- should_email(issue.assignees.first)
- should_email(@u_watcher)
- should_email(@u_guest_watcher)
- should_email(@u_guest_custom)
- should_email(@u_custom_global)
- should_email(@u_participant_mentioned)
- should_email(@subscriber)
- should_not_email(@unsubscriber)
- should_not_email(@u_participating)
- should_not_email(@u_disabled)
- should_not_email(@u_lazy_participant)
+ expect do
+ notification.reassigned_issue(issue, @u_disabled, [assignee])
+ end.to enqueue_mail_with(Notify, :reassigned_issue_email, issue.assignees.first, *anything_args)
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_watcher, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_watcher, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_custom, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_custom_global, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_participant_mentioned, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @subscriber, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @unsubscriber, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_participating, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_disabled, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_lazy_participant, *anything_args))
end
it 'adds "assigned" reason for new assignee' do
- notification.reassigned_issue(issue, @u_disabled, [assignee])
-
- email = find_email_for(assignee)
-
- expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED)
+ expect do
+ notification.reassigned_issue(issue, @u_disabled, [assignee])
+ end.to enqueue_mail_with(
+ Notify,
+ :reassigned_issue_email,
+ issue.assignees.first,
+ anything,
+ anything,
+ anything,
+ NotificationReason::ASSIGNED
+ )
end
it 'emails previous assignee even if they have the "on mention" notif level' do
issue.assignees = [@u_mentioned]
- notification.reassigned_issue(issue, @u_disabled, [@u_watcher])
- should_email(@u_mentioned)
- should_email(@u_watcher)
- should_email(@u_guest_watcher)
- should_email(@u_guest_custom)
- should_email(@u_participant_mentioned)
- should_email(@subscriber)
- should_email(@u_custom_global)
- should_not_email(@unsubscriber)
- should_not_email(@u_participating)
- should_not_email(@u_disabled)
- should_not_email(@u_lazy_participant)
+ expect do
+ notification.reassigned_issue(issue, @u_disabled, [@u_watcher])
+ end.to enqueue_mail_with(Notify, :reassigned_issue_email, @u_mentioned, *anything_args)
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_watcher, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_watcher, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_custom, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_participant_mentioned, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @subscriber, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_custom_global, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @unsubscriber, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_participating, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_disabled, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_lazy_participant, *anything_args))
end
it 'emails new assignee even if they have the "on mention" notif level' do
issue.assignees = [@u_mentioned]
- notification.reassigned_issue(issue, @u_disabled, [@u_mentioned])
- expect(issue.assignees.first).to be @u_mentioned
- should_email(issue.assignees.first)
- should_email(@u_watcher)
- should_email(@u_guest_watcher)
- should_email(@u_guest_custom)
- should_email(@u_participant_mentioned)
- should_email(@subscriber)
- should_email(@u_custom_global)
- should_not_email(@unsubscriber)
- should_not_email(@u_participating)
- should_not_email(@u_disabled)
- should_not_email(@u_lazy_participant)
+ expect(issue.assignees.first).to eq(@u_mentioned)
+ expect do
+ notification.reassigned_issue(issue, @u_disabled, [@u_mentioned])
+ end.to enqueue_mail_with(Notify, :reassigned_issue_email, issue.assignees.first, *anything_args)
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_watcher, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_watcher, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_custom, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_participant_mentioned, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @subscriber, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_custom_global, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @unsubscriber, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_participating, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_disabled, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_lazy_participant, *anything_args))
end
it 'does not email new assignee if they are the current user' do
issue.assignees = [@u_mentioned]
notification.reassigned_issue(issue, @u_mentioned, [@u_mentioned])
- expect(issue.assignees.first).to be @u_mentioned
- should_email(@u_watcher)
- should_email(@u_guest_watcher)
- should_email(@u_guest_custom)
- should_email(@u_participant_mentioned)
- should_email(@subscriber)
- should_email(@u_custom_global)
- should_not_email(issue.assignees.first)
- should_not_email(@unsubscriber)
- should_not_email(@u_participating)
- should_not_email(@u_disabled)
- should_not_email(@u_lazy_participant)
- end
-
- it_behaves_like 'participating notifications' do
+ expect(issue.assignees.first).to eq(@u_mentioned)
+ expect do
+ notification.reassigned_issue(issue, @u_mentioned, [@u_mentioned])
+ end.to enqueue_mail_with(Notify, :reassigned_issue_email, @u_watcher, *anything_args)
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_watcher, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_custom, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_participant_mentioned, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @subscriber, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_custom_global, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, issue.assignees.first, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @unsubscriber, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_participating, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_disabled, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_lazy_participant, *anything_args))
+ end
+
+ it_behaves_like 'participating notifications', check_delivery_jobs_queue: true do
let(:participant) { create(:user, username: 'user-participant') }
let(:issuable) { issue }
let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled, [assignee]) }
end
- it_behaves_like 'participating by confidential note notification' do
+ it_behaves_like 'participating by confidential note notification', check_delivery_jobs_queue: true do
let(:issuable) { issue }
let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled, [assignee]) }
end
- it_behaves_like 'project emails are disabled' do
+ it_behaves_like 'project emails are disabled', check_delivery_jobs_queue: true do
let(:notification_target) { issue }
let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled, [assignee]) }
end
end
- describe '#relabeled_issue' do
+ describe '#relabeled_issue', :deliver_mails_inline do
let(:group_label_1) { create(:group_label, group: group, title: 'Group Label 1', issues: [issue]) }
let(:group_label_2) { create(:group_label, group: group, title: 'Group Label 2') }
let(:label_1) { create(:label, project: project, title: 'Label 1', issues: [issue]) }
@@ -1571,25 +1653,25 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- describe '#removed_milestone_issue' do
+ describe '#removed_milestone on Issue', :deliver_mails_inline do
context do
let(:milestone) { create(:milestone, project: project, issues: [issue]) }
let!(:subscriber_to_new_milestone) { create(:user) { |u| issue.toggle_subscription(u, project) } }
it_behaves_like 'altered milestone notification on issue' do
before do
- notification.removed_milestone_issue(issue, issue.author)
+ notification.removed_milestone(issue, issue.author)
end
end
it_behaves_like 'project emails are disabled' do
let(:notification_target) { issue }
- let(:notification_trigger) { notification.removed_milestone_issue(issue, issue.author) }
+ let(:notification_trigger) { notification.removed_milestone(issue, issue.author) }
end
it_behaves_like 'participating by confidential note notification' do
let(:issuable) { issue }
- let(:notification_trigger) { notification.removed_milestone_issue(issue, issue.author) }
+ let(:notification_trigger) { notification.removed_milestone(issue, issue.author) }
end
end
@@ -1615,7 +1697,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
reset_delivered_emails!
- notification.removed_milestone_issue(confidential_issue, @u_disabled)
+ notification.removed_milestone(confidential_issue, @u_disabled)
should_not_email(non_member)
should_not_email(guest)
@@ -1627,20 +1709,20 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- describe '#changed_milestone_issue' do
+ describe '#changed_milestone on Issue', :deliver_mails_inline do
context do
let(:new_milestone) { create(:milestone, project: project, issues: [issue]) }
let!(:subscriber_to_new_milestone) { create(:user) { |u| issue.toggle_subscription(u, project) } }
it_behaves_like 'altered milestone notification on issue' do
before do
- notification.changed_milestone_issue(issue, new_milestone, issue.author)
+ notification.changed_milestone(issue, new_milestone, issue.author)
end
end
it_behaves_like 'project emails are disabled' do
let(:notification_target) { issue }
- let(:notification_trigger) { notification.changed_milestone_issue(issue, new_milestone, issue.author) }
+ let(:notification_trigger) { notification.changed_milestone(issue, new_milestone, issue.author) }
end
end
@@ -1666,7 +1748,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
reset_delivered_emails!
- notification.changed_milestone_issue(confidential_issue, new_milestone, @u_disabled)
+ notification.changed_milestone(confidential_issue, new_milestone, @u_disabled)
should_not_email(non_member)
should_not_email(guest)
@@ -1678,7 +1760,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- describe '#close_issue' do
+ describe '#close_issue', :deliver_mails_inline do
before do
update_custom_notification(:close_issue, @u_guest_custom, resource: project)
update_custom_notification(:close_issue, @u_custom_global)
@@ -1730,7 +1812,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- describe '#reopen_issue' do
+ describe '#reopen_issue', :deliver_mails_inline do
before do
update_custom_notification(:reopen_issue, @u_guest_custom, resource: project)
update_custom_notification(:reopen_issue, @u_custom_global)
@@ -1771,7 +1853,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- describe '#issue_moved' do
+ describe '#issue_moved', :deliver_mails_inline do
let(:new_issue) { create(:issue) }
it 'sends email to issue notification recipients' do
@@ -1807,7 +1889,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- describe '#issue_cloned' do
+ describe '#issue_cloned', :deliver_mails_inline do
let(:new_issue) { create(:issue) }
it 'sends email to issue notification recipients' do
@@ -1843,7 +1925,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- describe '#issue_due' do
+ describe '#issue_due', :deliver_mails_inline do
before do
issue.update!(due_date: Date.today)
@@ -2395,35 +2477,35 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- describe '#removed_milestone_merge_request' do
+ describe '#removed_milestone on MergeRequest' do
let(:milestone) { create(:milestone, project: project, merge_requests: [merge_request]) }
let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } }
it_behaves_like 'altered milestone notification on merge request' do
before do
- notification.removed_milestone_merge_request(merge_request, merge_request.author)
+ notification.removed_milestone(merge_request, merge_request.author)
end
end
it_behaves_like 'project emails are disabled' do
let(:notification_target) { merge_request }
- let(:notification_trigger) { notification.removed_milestone_merge_request(merge_request, merge_request.author) }
+ let(:notification_trigger) { notification.removed_milestone(merge_request, merge_request.author) }
end
end
- describe '#changed_milestone_merge_request' do
+ describe '#changed_milestone on MergeRequest' do
let(:new_milestone) { create(:milestone, project: project, merge_requests: [merge_request]) }
let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } }
it_behaves_like 'altered milestone notification on merge request' do
before do
- notification.changed_milestone_merge_request(merge_request, new_milestone, merge_request.author)
+ notification.changed_milestone(merge_request, new_milestone, merge_request.author)
end
end
it_behaves_like 'project emails are disabled' do
let(:notification_target) { merge_request }
- let(:notification_trigger) { notification.changed_milestone_merge_request(merge_request, new_milestone, merge_request.author) }
+ let(:notification_trigger) { notification.changed_milestone(merge_request, new_milestone, merge_request.author) }
end
end
@@ -3920,4 +4002,8 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
# Make the watcher a subscriber to detect dupes
issuable.subscriptions.create!(user: @watcher_and_subscriber, project: project, subscribed: true)
end
+
+ def expectation_args_for_user(user)
+ [user, *anything_args]
+ end
end
diff --git a/spec/services/packages/debian/create_distribution_service_spec.rb b/spec/services/packages/debian/create_distribution_service_spec.rb
index ecf82c6a1db..1c53f75cfb6 100644
--- a/spec/services/packages/debian/create_distribution_service_spec.rb
+++ b/spec/services/packages/debian/create_distribution_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::CreateDistributionService do
+RSpec.describe Packages::Debian::CreateDistributionService, feature_category: :package_registry do
RSpec.shared_examples 'Create Debian Distribution' do |expected_message, expected_components, expected_architectures|
let_it_be(:container) { create(container_type) } # rubocop:disable Rails/SaveBang
diff --git a/spec/services/packages/debian/create_package_file_service_spec.rb b/spec/services/packages/debian/create_package_file_service_spec.rb
index 291f6df991c..43928669eb1 100644
--- a/spec/services/packages/debian/create_package_file_service_spec.rb
+++ b/spec/services/packages/debian/create_package_file_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::CreatePackageFileService do
+RSpec.describe Packages::Debian::CreatePackageFileService, feature_category: :package_registry do
include WorkhorseHelpers
let_it_be(:package) { create(:debian_incoming, without_package_files: true) }
@@ -11,8 +11,9 @@ RSpec.describe Packages::Debian::CreatePackageFileService do
describe '#execute' do
let(:file_name) { 'libsample0_1.2.3~alpha2_amd64.deb' }
let(:fixture_path) { "spec/fixtures/packages/debian/#{file_name}" }
+ let(:params) { default_params }
- let(:params) do
+ let(:default_params) do
{
file: file,
file_name: file_name,
@@ -25,8 +26,15 @@ RSpec.describe Packages::Debian::CreatePackageFileService do
subject(:package_file) { service.execute }
- shared_examples 'a valid deb' do
+ shared_examples 'a valid deb' do |process_package_file_worker|
it 'creates a new package file', :aggregate_failures do
+ if process_package_file_worker
+ expect(::Packages::Debian::ProcessPackageFileWorker)
+ .to receive(:perform_async).with(an_instance_of(Integer), params[:distribution], params[:component])
+ else
+ expect(::Packages::Debian::ProcessPackageFileWorker).not_to receive(:perform_async)
+ end
+
expect(::Packages::Debian::ProcessChangesWorker).not_to receive(:perform_async)
expect(package_file).to be_valid
expect(package_file.file.read).to start_with('!<arch>')
@@ -44,7 +52,8 @@ RSpec.describe Packages::Debian::CreatePackageFileService do
shared_examples 'a valid changes' do
it 'creates a new package file', :aggregate_failures do
- expect(::Packages::Debian::ProcessChangesWorker).to receive(:perform_async)
+ expect(::Packages::Debian::ProcessChangesWorker)
+ .to receive(:perform_async).with(an_instance_of(Integer), current_user.id)
expect(package_file).to be_valid
expect(package_file.file.read).to start_with('Format: 1.8')
@@ -80,6 +89,12 @@ RSpec.describe Packages::Debian::CreatePackageFileService do
it_behaves_like 'a valid changes'
end
+ context 'with distribution' do
+ let(:params) { default_params.merge(distribution: 'unstable', component: 'main') }
+
+ it_behaves_like 'a valid deb', true
+ end
+
context 'when current_user is missing' do
let(:current_user) { nil }
@@ -137,13 +152,5 @@ RSpec.describe Packages::Debian::CreatePackageFileService do
expect { package_file }.to raise_error(ActiveRecord::RecordInvalid)
end
end
-
- context 'when FIPS mode enabled', :fips_mode do
- let(:file) { nil }
-
- it 'raises an error' do
- expect { package_file }.to raise_error(::Packages::FIPS::DisabledError)
- end
- end
end
end
diff --git a/spec/services/packages/debian/extract_changes_metadata_service_spec.rb b/spec/services/packages/debian/extract_changes_metadata_service_spec.rb
index 4765e6c3bd4..4d6acac219b 100644
--- a/spec/services/packages/debian/extract_changes_metadata_service_spec.rb
+++ b/spec/services/packages/debian/extract_changes_metadata_service_spec.rb
@@ -1,10 +1,9 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Debian::ExtractChangesMetadataService do
+RSpec.describe Packages::Debian::ExtractChangesMetadataService, feature_category: :package_registry do
describe '#execute' do
- let_it_be(:distribution) { create(:debian_project_distribution, codename: 'unstable') }
- let_it_be(:incoming) { create(:debian_incoming, project: distribution.project) }
+ let_it_be(:incoming) { create(:debian_incoming) }
let(:source_file) { incoming.package_files.first }
let(:dsc_file) { incoming.package_files.second }
@@ -13,12 +12,6 @@ RSpec.describe Packages::Debian::ExtractChangesMetadataService do
subject { service.execute }
- context 'with FIPS mode enabled', :fips_mode do
- it 'raises an error' do
- expect { subject }.to raise_error(::Packages::FIPS::DisabledError)
- end
- end
-
context 'with valid package file' do
it 'extract metadata', :aggregate_failures do
expected_fields = { 'Architecture' => 'source amd64', 'Binary' => 'libsample0 sample-dev sample-udeb' }
diff --git a/spec/services/packages/debian/extract_deb_metadata_service_spec.rb b/spec/services/packages/debian/extract_deb_metadata_service_spec.rb
index 66a9ca5f9e0..1f5cf2ace5a 100644
--- a/spec/services/packages/debian/extract_deb_metadata_service_spec.rb
+++ b/spec/services/packages/debian/extract_deb_metadata_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Debian::ExtractDebMetadataService do
+RSpec.describe Packages::Debian::ExtractDebMetadataService, feature_category: :package_registry do
subject { described_class.new(file_path) }
let(:file_name) { 'libsample0_1.2.3~alpha2_amd64.deb' }
diff --git a/spec/services/packages/debian/extract_metadata_service_spec.rb b/spec/services/packages/debian/extract_metadata_service_spec.rb
index 02c81ad1644..412f285152b 100644
--- a/spec/services/packages/debian/extract_metadata_service_spec.rb
+++ b/spec/services/packages/debian/extract_metadata_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Debian::ExtractMetadataService do
+RSpec.describe Packages::Debian::ExtractMetadataService, feature_category: :package_registry do
let(:service) { described_class.new(package_file) }
subject { service.execute }
diff --git a/spec/services/packages/debian/find_or_create_incoming_service_spec.rb b/spec/services/packages/debian/find_or_create_incoming_service_spec.rb
index e1393c774b1..27c389b5312 100644
--- a/spec/services/packages/debian/find_or_create_incoming_service_spec.rb
+++ b/spec/services/packages/debian/find_or_create_incoming_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::FindOrCreateIncomingService do
+RSpec.describe Packages::Debian::FindOrCreateIncomingService, feature_category: :package_registry do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/packages/debian/find_or_create_package_service_spec.rb b/spec/services/packages/debian/find_or_create_package_service_spec.rb
index 84a0e1465e8..36f96008582 100644
--- a/spec/services/packages/debian/find_or_create_package_service_spec.rb
+++ b/spec/services/packages/debian/find_or_create_package_service_spec.rb
@@ -2,54 +2,57 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::FindOrCreatePackageService do
- let_it_be(:distribution) { create(:debian_project_distribution) }
+RSpec.describe Packages::Debian::FindOrCreatePackageService, feature_category: :package_registry do
+ let_it_be(:distribution) { create(:debian_project_distribution, :with_suite) }
let_it_be(:project) { distribution.project }
let_it_be(:user) { create(:user) }
- let(:params) { { name: 'foo', version: '1.0+debian', distribution_name: distribution.codename } }
+ let(:service) { described_class.new(project, user, params) }
- subject(:service) { described_class.new(project, user, params) }
+ let(:package) { subject.payload[:package] }
+ let(:package2) { service.execute.payload[:package] }
- describe '#execute' do
- subject { service.execute }
+ shared_examples 'find or create Debian package' do
+ it 'returns the same object' do
+ expect { subject }.to change { ::Packages::Package.count }.by(1)
+ expect(subject).to be_success
+ expect(package).to be_valid
+ expect(package.project_id).to eq(project.id)
+ expect(package.creator_id).to eq(user.id)
+ expect(package.name).to eq('foo')
+ expect(package.version).to eq('1.0+debian')
+ expect(package).to be_debian
+ expect(package.debian_publication.distribution).to eq(distribution)
- let(:package) { subject.payload[:package] }
+ expect { package2 }.not_to change { ::Packages::Package.count }
+ expect(package2.id).to eq(package.id)
+ end
- context 'run once' do
- it 'creates a new package', :aggregate_failures do
+ context 'with package marked as pending_destruction' do
+ it 'creates a new package' do
expect { subject }.to change { ::Packages::Package.count }.by(1)
- expect(subject).to be_success
-
- expect(package).to be_valid
- expect(package.project_id).to eq(project.id)
- expect(package.creator_id).to eq(user.id)
- expect(package.name).to eq('foo')
- expect(package.version).to eq('1.0+debian')
- expect(package).to be_debian
- expect(package.debian_publication.distribution).to eq(distribution)
+
+ package.pending_destruction!
+
+ expect { package2 }.to change { ::Packages::Package.count }.by(1)
+ expect(package2.id).not_to eq(package.id)
end
end
+ end
- context 'run twice' do
- let(:package2) { service.execute.payload[:package] }
+ describe '#execute' do
+ subject { service.execute }
- it 'returns the same object' do
- expect { subject }.to change { ::Packages::Package.count }.by(1)
- expect { package2 }.not_to change { ::Packages::Package.count }
+ context 'with a codename as distribution name' do
+ let(:params) { { name: 'foo', version: '1.0+debian', distribution_name: distribution.codename } }
- expect(package2.id).to eq(package.id)
- end
+ it_behaves_like 'find or create Debian package'
+ end
- context 'with package marked as pending_destruction' do
- it 'creates a new package' do
- expect { subject }.to change { ::Packages::Package.count }.by(1)
- package.pending_destruction!
- expect { package2 }.to change { ::Packages::Package.count }.by(1)
+ context 'with a suite as distribution name' do
+ let(:params) { { name: 'foo', version: '1.0+debian', distribution_name: distribution.suite } }
- expect(package2.id).not_to eq(package.id)
- end
- end
+ it_behaves_like 'find or create Debian package'
end
context 'with non-existing distribution' do
diff --git a/spec/services/packages/debian/generate_distribution_key_service_spec.rb b/spec/services/packages/debian/generate_distribution_key_service_spec.rb
index f82d577f071..bc86a9592d0 100644
--- a/spec/services/packages/debian/generate_distribution_key_service_spec.rb
+++ b/spec/services/packages/debian/generate_distribution_key_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::GenerateDistributionKeyService do
+RSpec.describe Packages::Debian::GenerateDistributionKeyService, feature_category: :package_registry do
let(:params) { {} }
subject { described_class.new(params: params) }
diff --git a/spec/services/packages/debian/generate_distribution_service_spec.rb b/spec/services/packages/debian/generate_distribution_service_spec.rb
index fe5fbfbbe1f..6d179c791a3 100644
--- a/spec/services/packages/debian/generate_distribution_service_spec.rb
+++ b/spec/services/packages/debian/generate_distribution_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::GenerateDistributionService do
+RSpec.describe Packages::Debian::GenerateDistributionService, feature_category: :package_registry do
describe '#execute' do
subject { described_class.new(distribution).execute }
@@ -15,12 +15,6 @@ RSpec.describe Packages::Debian::GenerateDistributionService do
context "for #{container_type}" do
include_context 'with Debian distribution', container_type
- context 'with FIPS mode enabled', :fips_mode do
- it 'raises an error' do
- expect { subject }.to raise_error(::Packages::FIPS::DisabledError)
- end
- end
-
it_behaves_like 'Generate Debian Distribution and component files'
end
end
diff --git a/spec/services/packages/debian/parse_debian822_service_spec.rb b/spec/services/packages/debian/parse_debian822_service_spec.rb
index a2731816459..35b7ead9209 100644
--- a/spec/services/packages/debian/parse_debian822_service_spec.rb
+++ b/spec/services/packages/debian/parse_debian822_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Debian::ParseDebian822Service do
+RSpec.describe Packages::Debian::ParseDebian822Service, feature_category: :package_registry do
subject { described_class.new(input) }
context 'with dpkg-deb --field output' do
diff --git a/spec/services/packages/debian/process_changes_service_spec.rb b/spec/services/packages/debian/process_changes_service_spec.rb
index 27b49a13d52..e3ed744377e 100644
--- a/spec/services/packages/debian/process_changes_service_spec.rb
+++ b/spec/services/packages/debian/process_changes_service_spec.rb
@@ -1,14 +1,14 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Debian::ProcessChangesService do
+RSpec.describe Packages::Debian::ProcessChangesService, feature_category: :package_registry do
describe '#execute' do
let_it_be(:user) { create(:user) }
- let_it_be_with_reload(:distribution) { create(:debian_project_distribution, :with_file, codename: 'unstable') }
+ let_it_be_with_reload(:distribution) { create(:debian_project_distribution, :with_file, suite: 'unstable') }
let!(:incoming) { create(:debian_incoming, project: distribution.project) }
- let(:package_file) { incoming.package_files.last }
+ let(:package_file) { incoming.package_files.with_file_name('sample_1.2.3~alpha2_amd64.changes').first }
subject { described_class.new(package_file, user) }
@@ -27,11 +27,37 @@ RSpec.describe Packages::Debian::ProcessChangesService do
expect(created_package.creator).to eq user
end
- context 'with existing package' do
- let_it_be_with_reload(:existing_package) { create(:debian_package, name: 'sample', version: '1.2.3~alpha2', project: distribution.project) }
-
+ context 'with non-matching distribution' do
before do
- existing_package.update!(debian_distribution: distribution)
+ distribution.update! suite: FFaker::Lorem.word
+ end
+
+ it { expect { subject.execute }.to raise_error(ActiveRecord::RecordNotFound) }
+ end
+
+ context 'with missing field in .changes file' do
+ shared_examples 'raises error with missing field' do |missing_field|
+ before do
+ allow_next_instance_of(::Packages::Debian::ExtractChangesMetadataService) do |extract_changes_metadata_service|
+ expect(extract_changes_metadata_service).to receive(:execute).once.and_wrap_original do |m, *args|
+ metadata = m.call(*args)
+ metadata[:fields].delete(missing_field)
+ metadata
+ end
+ end
+ end
+
+ it { expect { subject.execute }.to raise_error(ArgumentError, "missing #{missing_field} field") }
+ end
+
+ it_behaves_like 'raises error with missing field', 'Source'
+ it_behaves_like 'raises error with missing field', 'Version'
+ it_behaves_like 'raises error with missing field', 'Distribution'
+ end
+
+ context 'with existing package' do
+ let_it_be_with_reload(:existing_package) do
+ create(:debian_package, name: 'sample', version: '1.2.3~alpha2', project: distribution.project, published_in: distribution)
end
it 'does not create a package and assigns the package_file to the existing package' do
diff --git a/spec/services/packages/debian/process_package_file_service_spec.rb b/spec/services/packages/debian/process_package_file_service_spec.rb
index 571861f42cf..caf29cfc4fa 100644
--- a/spec/services/packages/debian/process_package_file_service_spec.rb
+++ b/spec/services/packages/debian/process_package_file_service_spec.rb
@@ -1,20 +1,33 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Debian::ProcessPackageFileService do
+RSpec.describe Packages::Debian::ProcessPackageFileService, feature_category: :package_registry do
describe '#execute' do
- let_it_be(:user) { create(:user) }
- let_it_be_with_reload(:distribution) { create(:debian_project_distribution, :with_file, codename: 'unstable') }
-
- let!(:incoming) { create(:debian_incoming, project: distribution.project) }
+ let_it_be_with_reload(:distribution) { create(:debian_project_distribution, :with_suite, :with_file) }
+ let!(:package) { create(:debian_package, :processing, project: distribution.project, published_in: nil) }
let(:distribution_name) { distribution.codename }
+ let(:component_name) { 'main' }
let(:debian_file_metadatum) { package_file.debian_file_metadatum }
- subject { described_class.new(package_file, user, distribution_name, component_name) }
+ subject { described_class.new(package_file, distribution_name, component_name) }
- RSpec.shared_context 'with Debian package file' do |file_name|
- let(:package_file) { incoming.package_files.with_file_name(file_name).first }
+ shared_examples 'updates package and package file' do
+ it 'updates package and package file', :aggregate_failures do
+ expect(::Packages::Debian::GenerateDistributionWorker)
+ .to receive(:perform_async).with(:project, distribution.id)
+ expect { subject.execute }
+ .to not_change(Packages::Package, :count)
+ .and not_change(Packages::PackageFile, :count)
+ .and change(Packages::Debian::Publication, :count).by(1)
+ .and not_change(package.package_files, :count)
+ .and change { package.reload.name }.to('sample')
+ .and change { package.reload.version }.to('1.2.3~alpha2')
+ .and change { package.reload.status }.from('processing').to('default')
+ .and change { package.reload.debian_publication }.from(nil)
+ .and change(debian_file_metadatum, :file_type).from('unknown').to(expected_file_type)
+ .and change(debian_file_metadatum, :component).from(nil).to(component_name)
+ end
end
using RSpec::Parameterized::TableSyntax
@@ -25,59 +38,68 @@ RSpec.describe Packages::Debian::ProcessPackageFileService do
end
with_them do
- include_context 'with Debian package file', params[:file_name] do
- it 'creates package and updates package file', :aggregate_failures do
- expect(::Packages::Debian::GenerateDistributionWorker)
- .to receive(:perform_async).with(:project, distribution.id)
- expect { subject.execute }
- .to change(Packages::Package, :count).from(1).to(2)
- .and not_change(Packages::PackageFile, :count)
- .and change(incoming.package_files, :count).from(7).to(6)
- .and change(debian_file_metadatum, :file_type).from('unknown').to(expected_file_type)
- .and change(debian_file_metadatum, :component).from(nil).to(component_name)
-
- created_package = Packages::Package.last
- expect(created_package.name).to eq 'sample'
- expect(created_package.version).to eq '1.2.3~alpha2'
- expect(created_package.creator).to eq user
- end
+ context 'with Debian package file' do
+ let(:package_file) { package.package_files.with_file_name(file_name).first }
- context 'with existing package' do
- let_it_be_with_reload(:existing_package) do
- create(:debian_package, name: 'sample', version: '1.2.3~alpha2', project: distribution.project)
+ context 'when there is no matching published package' do
+ it_behaves_like 'updates package and package file'
+
+ context 'with suite as distribution name' do
+ let(:distribution_name) { distribution.suite }
+
+ it_behaves_like 'updates package and package file'
end
+ end
- before do
- existing_package.update!(debian_distribution: distribution)
+ context 'when there is a matching published package' do
+ let!(:matching_package) do
+ create(
+ :debian_package,
+ project: distribution.project,
+ published_in: distribution,
+ name: 'sample',
+ version: '1.2.3~alpha2'
+ )
end
- it 'does not create a package and assigns the package_file to the existing package' do
+ it 'reuses existing package and update package file', :aggregate_failures do
expect(::Packages::Debian::GenerateDistributionWorker)
.to receive(:perform_async).with(:project, distribution.id)
expect { subject.execute }
- .to not_change(Packages::Package, :count)
- .and not_change(Packages::PackageFile, :count)
- .and change(incoming.package_files, :count).from(7).to(6)
- .and change(package_file, :package).from(incoming).to(existing_package)
- .and change(debian_file_metadatum, :file_type).from('unknown').to(expected_file_type.to_s)
+ .to change(Packages::Package, :count).from(2).to(1)
+ .and change(Packages::PackageFile, :count).from(14).to(8)
+ .and not_change(Packages::Debian::Publication, :count)
+ .and change(package.package_files, :count).from(7).to(0)
+ .and change(package_file, :package).from(package).to(matching_package)
+ .and not_change(matching_package, :name)
+ .and not_change(matching_package, :version)
+ .and change(debian_file_metadatum, :file_type).from('unknown').to(expected_file_type)
.and change(debian_file_metadatum, :component).from(nil).to(component_name)
- end
- context 'when marked as pending_destruction' do
- it 'does not re-use the existing package' do
- existing_package.pending_destruction!
+ expect { package.reload }
+ .to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
- expect { subject.execute }
- .to change(Packages::Package, :count).by(1)
- .and not_change(Packages::PackageFile, :count)
- end
+ context 'when there is a matching published package pending destruction' do
+ let!(:matching_package) do
+ create(
+ :debian_package,
+ :pending_destruction,
+ project: distribution.project,
+ published_in: distribution,
+ name: 'sample',
+ version: '1.2.3~alpha2'
+ )
end
+
+ it_behaves_like 'updates package and package file'
end
end
end
context 'without a distribution' do
- let(:package_file) { incoming.package_files.with_file_name('libsample0_1.2.3~alpha2_amd64.deb').first }
+ let(:package_file) { package.package_files.with_file_name('libsample0_1.2.3~alpha2_amd64.deb').first }
let(:component_name) { 'main' }
before do
@@ -89,42 +111,41 @@ RSpec.describe Packages::Debian::ProcessPackageFileService do
expect { subject.execute }
.to not_change(Packages::Package, :count)
.and not_change(Packages::PackageFile, :count)
- .and not_change(incoming.package_files, :count)
+ .and not_change(package.package_files, :count)
.and raise_error(ActiveRecord::RecordNotFound)
end
end
- context 'with package file without Debian metadata' do
+ context 'without distribution name' do
let!(:package_file) { create(:debian_package_file, without_loaded_metadatum: true) }
- let(:component_name) { 'main' }
+ let(:distribution_name) { '' }
it 'raise ArgumentError', :aggregate_failures do
expect(::Packages::Debian::GenerateDistributionWorker).not_to receive(:perform_async)
expect { subject.execute }
.to not_change(Packages::Package, :count)
.and not_change(Packages::PackageFile, :count)
- .and not_change(incoming.package_files, :count)
- .and raise_error(ArgumentError, 'package file without Debian metadata')
+ .and not_change(package.package_files, :count)
+ .and raise_error(ArgumentError, 'missing distribution name')
end
end
- context 'with already processed package file' do
- let_it_be(:package_file) { create(:debian_package_file) }
-
- let(:component_name) { 'main' }
+ context 'without component name' do
+ let!(:package_file) { create(:debian_package_file, without_loaded_metadatum: true) }
+ let(:component_name) { '' }
it 'raise ArgumentError', :aggregate_failures do
expect(::Packages::Debian::GenerateDistributionWorker).not_to receive(:perform_async)
expect { subject.execute }
.to not_change(Packages::Package, :count)
.and not_change(Packages::PackageFile, :count)
- .and not_change(incoming.package_files, :count)
- .and raise_error(ArgumentError, 'already processed package file')
+ .and not_change(package.package_files, :count)
+ .and raise_error(ArgumentError, 'missing component name')
end
end
- context 'with invalid package file type' do
- let(:package_file) { incoming.package_files.with_file_name('sample_1.2.3~alpha2.tar.xz').first }
+ context 'with package file without Debian metadata' do
+ let!(:package_file) { create(:debian_package_file, without_loaded_metadatum: true) }
let(:component_name) { 'main' }
it 'raise ArgumentError', :aggregate_failures do
@@ -132,29 +153,37 @@ RSpec.describe Packages::Debian::ProcessPackageFileService do
expect { subject.execute }
.to not_change(Packages::Package, :count)
.and not_change(Packages::PackageFile, :count)
- .and not_change(incoming.package_files, :count)
- .and raise_error(ArgumentError, 'invalid package file type: source')
+ .and not_change(package.package_files, :count)
+ .and raise_error(ArgumentError, 'package file without Debian metadata')
end
end
- context 'when creating package fails' do
- let(:package_file) { incoming.package_files.with_file_name('libsample0_1.2.3~alpha2_amd64.deb').first }
+ context 'with already processed package file' do
+ let_it_be(:package_file) { create(:debian_package_file) }
+
let(:component_name) { 'main' }
- before do
- allow_next_instance_of(::Packages::Debian::FindOrCreatePackageService) do |find_or_create_package_service|
- allow(find_or_create_package_service)
- .to receive(:execute).and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout')
- end
+ it 'raise ArgumentError', :aggregate_failures do
+ expect(::Packages::Debian::GenerateDistributionWorker).not_to receive(:perform_async)
+ expect { subject.execute }
+ .to not_change(Packages::Package, :count)
+ .and not_change(Packages::PackageFile, :count)
+ .and not_change(package.package_files, :count)
+ .and raise_error(ArgumentError, 'already processed package file')
end
+ end
- it 're-raise error', :aggregate_failures do
+ context 'with invalid package file type' do
+ let(:package_file) { package.package_files.with_file_name('sample_1.2.3~alpha2.tar.xz').first }
+ let(:component_name) { 'main' }
+
+ it 'raise ArgumentError', :aggregate_failures do
expect(::Packages::Debian::GenerateDistributionWorker).not_to receive(:perform_async)
expect { subject.execute }
.to not_change(Packages::Package, :count)
.and not_change(Packages::PackageFile, :count)
- .and not_change(incoming.package_files, :count)
- .and raise_error(ActiveRecord::ConnectionTimeoutError, 'connect timeout')
+ .and not_change(package.package_files, :count)
+ .and raise_error(ArgumentError, 'invalid package file type: source')
end
end
end
diff --git a/spec/services/packages/debian/sign_distribution_service_spec.rb b/spec/services/packages/debian/sign_distribution_service_spec.rb
index fc070b6e45e..50c34443495 100644
--- a/spec/services/packages/debian/sign_distribution_service_spec.rb
+++ b/spec/services/packages/debian/sign_distribution_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::SignDistributionService do
+RSpec.describe Packages::Debian::SignDistributionService, feature_category: :package_registry do
let_it_be(:group) { create(:group, :public) }
let(:content) { FFaker::Lorem.paragraph }
diff --git a/spec/services/packages/debian/update_distribution_service_spec.rb b/spec/services/packages/debian/update_distribution_service_spec.rb
index 3dff2754cec..cfafed5841f 100644
--- a/spec/services/packages/debian/update_distribution_service_spec.rb
+++ b/spec/services/packages/debian/update_distribution_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::UpdateDistributionService do
+RSpec.describe Packages::Debian::UpdateDistributionService, feature_category: :package_registry do
RSpec.shared_examples 'Update Debian Distribution' do |expected_message, expected_components, expected_architectures, component_file_delta = 0|
it 'returns ServiceResponse', :aggregate_failures do
expect(distribution).to receive(:update).with(simple_params).and_call_original if expected_message.nil?
diff --git a/spec/services/pages/destroy_deployments_service_spec.rb b/spec/services/pages/destroy_deployments_service_spec.rb
index 0f8e8b6573e..0ca8cbbb681 100644
--- a/spec/services/pages/destroy_deployments_service_spec.rb
+++ b/spec/services/pages/destroy_deployments_service_spec.rb
@@ -2,28 +2,26 @@
require 'spec_helper'
-RSpec.describe Pages::DestroyDeploymentsService do
- let(:project) { create(:project) }
+RSpec.describe Pages::DestroyDeploymentsService, feature_category: :pages do
+ let_it_be(:project) { create(:project) }
let!(:old_deployments) { create_list(:pages_deployment, 2, project: project) }
let!(:last_deployment) { create(:pages_deployment, project: project) }
let!(:newer_deployment) { create(:pages_deployment, project: project) }
let!(:deployment_from_another_project) { create(:pages_deployment) }
it 'destroys all deployments of the project' do
- expect do
- described_class.new(project).execute
- end.to change { PagesDeployment.count }.by(-4)
+ expect { described_class.new(project).execute }
+ .to change { PagesDeployment.count }.by(-4)
- expect(deployment_from_another_project.reload).to be
+ expect(deployment_from_another_project.reload).to be_persisted
end
it 'destroy only deployments older than last deployment if it is provided' do
- expect do
- described_class.new(project, last_deployment.id).execute
- end.to change { PagesDeployment.count }.by(-2)
+ expect { described_class.new(project, last_deployment.id).execute }
+ .to change { PagesDeployment.count }.by(-2)
- expect(last_deployment.reload).to be
- expect(newer_deployment.reload).to be
- expect(deployment_from_another_project.reload).to be
+ expect(last_deployment.reload).to be_persisted
+ expect(newer_deployment.reload).to be_persisted
+ expect(deployment_from_another_project.reload).to be_persisted
end
end
diff --git a/spec/services/pages/migrate_from_legacy_storage_service_spec.rb b/spec/services/pages/migrate_from_legacy_storage_service_spec.rb
index d058324f3bb..4348ce4a271 100644
--- a/spec/services/pages/migrate_from_legacy_storage_service_spec.rb
+++ b/spec/services/pages/migrate_from_legacy_storage_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Pages::MigrateFromLegacyStorageService do
+RSpec.describe Pages::MigrateFromLegacyStorageService, feature_category: :pages do
let(:batch_size) { 10 }
let(:mark_projects_as_not_deployed) { false }
let(:service) { described_class.new(Rails.logger, ignore_invalid_entries: false, mark_projects_as_not_deployed: mark_projects_as_not_deployed) }
diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb
index fe1ab6b1d58..d1bc10cfd28 100644
--- a/spec/services/preview_markdown_service_spec.rb
+++ b/spec/services/preview_markdown_service_spec.rb
@@ -192,4 +192,21 @@ RSpec.describe PreviewMarkdownService do
"Sets time estimate to 2y.<br>Assigns #{user.to_reference}."
end
end
+
+ context 'work item quick action types' do
+ let(:work_item) { create(:work_item, :task, project: project) }
+ let(:params) do
+ {
+ text: "/title new title",
+ target_type: 'WorkItem',
+ target_id: work_item.iid
+ }
+ end
+
+ let(:result) { described_class.new(project, user, params).execute }
+
+ it 'renders the quick action preview' do
+ expect(result[:commands]).to eq "Changes the title to \"new title\"."
+ end
+ end
end
diff --git a/spec/services/projects/container_repository/destroy_service_spec.rb b/spec/services/projects/container_repository/destroy_service_spec.rb
index 0ec0aecaa04..fed1d13daa5 100644
--- a/spec/services/projects/container_repository/destroy_service_spec.rb
+++ b/spec/services/projects/container_repository/destroy_service_spec.rb
@@ -5,86 +5,131 @@ require 'spec_helper'
RSpec.describe Projects::ContainerRepository::DestroyService do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :private) }
+ let_it_be(:params) { {} }
- subject { described_class.new(project, user) }
+ subject { described_class.new(project, user, params) }
before do
stub_container_registry_config(enabled: true)
end
- context 'when user does not have access to registry' do
- let!(:repository) { create(:container_repository, :root, project: project) }
+ shared_examples 'returning an error status with message' do |error_message|
+ it 'returns an error status' do
+ response = subject.execute(repository)
- it 'does not delete a repository' do
- expect { subject.execute(repository) }.not_to change { ContainerRepository.count }
+ expect(response).to include(status: :error, message: error_message)
end
end
- context 'when user has access to registry' do
+ shared_examples 'executing with permissions' do
+ let_it_be_with_refind(:repository) { create(:container_repository, :root, project: project) }
+
before do
- project.add_developer(user)
+ stub_container_registry_tags(repository: :any, tags: %w[latest stable])
end
- context 'when root container repository exists' do
- let!(:repository) { create(:container_repository, :root, project: project) }
+ it 'deletes the repository' do
+ expect_cleanup_tags_service_with(container_repository: repository, return_status: :success)
+ expect { subject.execute(repository) }.to change { ContainerRepository.count }.by(-1)
+ end
+
+ it 'sends disable_timeout = true as part of the params as default' do
+ expect_cleanup_tags_service_with(container_repository: repository, return_status: :success, disable_timeout: true)
+ expect { subject.execute(repository) }.to change { ContainerRepository.count }.by(-1)
+ end
+
+ it 'sends disable_timeout = false as part of the params if it is set to false' do
+ expect_cleanup_tags_service_with(container_repository: repository, return_status: :success, disable_timeout: false)
+ expect { subject.execute(repository, disable_timeout: false) }.to change { ContainerRepository.count }.by(-1)
+ end
+ context 'when deleting the tags fails' do
before do
- stub_container_registry_tags(repository: :any, tags: %w[latest stable])
+ expect_cleanup_tags_service_with(container_repository: repository, return_status: :error)
+ allow(Gitlab::AppLogger).to receive(:error).and_call_original
end
- it 'deletes the repository' do
- expect_cleanup_tags_service_with(container_repository: repository, return_status: :success)
- expect { subject.execute(repository) }.to change { ContainerRepository.count }.by(-1)
- end
+ it 'sets status as deleted_failed' do
+ subject.execute(repository)
- it 'sends disable_timeout = true as part of the params as default' do
- expect_cleanup_tags_service_with(container_repository: repository, return_status: :success, disable_timeout: true)
- expect { subject.execute(repository) }.to change { ContainerRepository.count }.by(-1)
+ expect(repository).to be_delete_failed
end
- it 'sends disable_timeout = false as part of the params if it is set to false' do
- expect_cleanup_tags_service_with(container_repository: repository, return_status: :success, disable_timeout: false)
- expect { subject.execute(repository, disable_timeout: false) }.to change { ContainerRepository.count }.by(-1)
- end
+ it 'logs the error' do
+ subject.execute(repository)
- context 'when deleting the tags fails' do
- it 'sets status as deleted_failed' do
- expect_cleanup_tags_service_with(container_repository: repository, return_status: :error)
- allow(Gitlab::AppLogger).to receive(:error).and_call_original
+ expect(Gitlab::AppLogger).to have_received(:error)
+ .with("Container repository with ID: #{repository.id} and path: #{repository.path} failed with message: error in deleting tags")
+ end
- subject.execute(repository)
+ it_behaves_like 'returning an error status with message', 'Deletion failed for container repository'
+ end
- expect(repository).to be_delete_failed
- expect(Gitlab::AppLogger).to have_received(:error)
- .with("Container repository with ID: #{repository.id} and path: #{repository.path} failed with message: error in deleting tags")
- end
+ context 'when destroying the repository fails' do
+ before do
+ expect_cleanup_tags_service_with(container_repository: repository, return_status: :success)
+ allow(repository).to receive(:destroy).and_return(false)
+ allow(repository.errors).to receive(:full_messages).and_return(['Error 1', 'Error 2'])
+ allow(Gitlab::AppLogger).to receive(:error).and_call_original
end
- context 'when destroying the repository fails' do
- it 'sets status as deleted_failed' do
- expect_cleanup_tags_service_with(container_repository: repository, return_status: :success)
- allow(repository).to receive(:destroy).and_return(false)
- allow(repository.errors).to receive(:full_messages).and_return(['Error 1', 'Error 2'])
- allow(Gitlab::AppLogger).to receive(:error).and_call_original
+ it 'sets status as deleted_failed' do
+ subject.execute(repository)
+
+ expect(repository).to be_delete_failed
+ end
- subject.execute(repository)
+ it 'logs the error' do
+ subject.execute(repository)
- expect(repository).to be_delete_failed
- expect(Gitlab::AppLogger).to have_received(:error)
- .with("Container repository with ID: #{repository.id} and path: #{repository.path} failed with message: Error 1. Error 2")
- end
+ expect(Gitlab::AppLogger).to have_received(:error)
+ .with("Container repository with ID: #{repository.id} and path: #{repository.path} failed with message: Error 1. Error 2")
end
- def expect_cleanup_tags_service_with(container_repository:, return_status:, disable_timeout: true)
- delete_tags_service = instance_double(Projects::ContainerRepository::CleanupTagsService)
+ it_behaves_like 'returning an error status with message', 'Deletion failed for container repository'
+ end
+ end
+
+ context 'when user has access to registry' do
+ before do
+ project.add_developer(user)
+ end
- expect(Projects::ContainerRepository::CleanupTagsService).to receive(:new).with(
- container_repository: container_repository,
- params: described_class::CLEANUP_TAGS_SERVICE_PARAMS.merge('disable_timeout' => disable_timeout)
- ).and_return(delete_tags_service)
+ it_behaves_like 'executing with permissions'
+ end
- expect(delete_tags_service).to receive(:execute).and_return(status: return_status)
- end
+ context 'when user does not have access to registry' do
+ let_it_be(:repository) { create(:container_repository, :root, project: project) }
+
+ it 'does not delete a repository' do
+ expect { subject.execute(repository) }.not_to change { ContainerRepository.count }
end
+
+ it_behaves_like 'returning an error status with message', 'Unauthorized access'
+ end
+
+ context 'when called during project deletion' do
+ let(:user) { nil }
+ let(:params) { { skip_permission_check: true } }
+
+ it_behaves_like 'executing with permissions'
+ end
+
+ context 'when there is no user' do
+ let(:user) { nil }
+ let(:repository) { create(:container_repository, :root, project: project) }
+
+ it_behaves_like 'returning an error status with message', 'Unauthorized access'
+ end
+
+ def expect_cleanup_tags_service_with(container_repository:, return_status:, disable_timeout: true)
+ delete_tags_service = instance_double(Projects::ContainerRepository::CleanupTagsService)
+
+ expect(Projects::ContainerRepository::CleanupTagsService).to receive(:new).with(
+ container_repository: container_repository,
+ params: described_class::CLEANUP_TAGS_SERVICE_PARAMS.merge('disable_timeout' => disable_timeout)
+ ).and_return(delete_tags_service)
+
+ expect(delete_tags_service).to receive(:execute).and_return(status: return_status)
end
end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index f85a8eda7ee..e435db4efa6 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -163,7 +163,7 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :projects
describe 'after create actions' do
it 'invalidate personal_projects_count caches' do
- expect(user).to receive(:invalidate_personal_projects_count)
+ expect(Rails.cache).to receive(:delete).with(['users', user.id, 'personal_projects_count'])
create_project(user, opts)
end
@@ -947,6 +947,8 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :projects
end
it 'schedules authorization update for users with access to group', :sidekiq_inline do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
expect(AuthorizedProjectsWorker).not_to(
receive(:bulk_perform_async)
)
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index ff2de45661f..0689a65c2f4 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publisher do
+RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publisher, feature_category: :projects do
include ProjectForksHelper
include BatchDestroyDependentAssociationsHelper
@@ -151,10 +151,22 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
it_behaves_like 'deleting the project'
- it 'invalidates personal_project_count cache' do
- expect(user).to receive(:invalidate_personal_projects_count)
+ context 'personal projects count cache' do
+ context 'when the executor is the creator of the project itself' do
+ it 'invalidates personal_project_count cache of the the owner of the personal namespace' do
+ expect(user).to receive(:invalidate_personal_projects_count)
- destroy_project(project, user, {})
+ destroy_project(project, user, {})
+ end
+ end
+
+ context 'when the executor is the instance administrator', :enable_admin_mode do
+ it 'invalidates personal_project_count cache of the the owner of the personal namespace' do
+ expect(user).to receive(:invalidate_personal_projects_count)
+
+ destroy_project(project, create(:admin), {})
+ end
+ end
end
context 'with running pipelines' do
@@ -331,18 +343,30 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
end
context 'when image repository deletion succeeds' do
- it 'removes tags' do
- expect_any_instance_of(Projects::ContainerRepository::CleanupTagsService)
- .to receive(:execute).and_return({ status: :success })
+ it 'returns true' do
+ expect_next_instance_of(Projects::ContainerRepository::CleanupTagsService) do |instance|
+ expect(instance).to receive(:execute).and_return(status: :success)
+ end
- destroy_project(project, user)
+ expect(destroy_project(project, user)).to be true
+ end
+ end
+
+ context 'when image repository deletion raises an error' do
+ it 'returns false' do
+ expect_next_instance_of(Projects::ContainerRepository::CleanupTagsService) do |service|
+ expect(service).to receive(:execute).and_raise(RuntimeError)
+ end
+
+ expect(destroy_project(project, user)).to be false
end
end
context 'when image repository deletion fails' do
- it 'raises an exception' do
- expect_any_instance_of(Projects::ContainerRepository::CleanupTagsService)
- .to receive(:execute).and_raise(RuntimeError)
+ it 'returns false' do
+ expect_next_instance_of(Projects::ContainerRepository::DestroyService) do |service|
+ expect(service).to receive(:execute).and_return({ status: :error })
+ end
expect(destroy_project(project, user)).to be false
end
@@ -369,8 +393,9 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
context 'when image repository tags deletion succeeds' do
it 'removes tags' do
- expect_any_instance_of(ContainerRepository)
- .to receive(:delete_tags!).and_return(true)
+ expect_next_instance_of(Projects::ContainerRepository::DestroyService) do |service|
+ expect(service).to receive(:execute).and_return({ status: :sucess })
+ end
destroy_project(project, user)
end
@@ -378,13 +403,27 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
context 'when image repository tags deletion fails' do
it 'raises an exception' do
- expect_any_instance_of(ContainerRepository)
- .to receive(:delete_tags!).and_return(false)
+ expect_next_instance_of(Projects::ContainerRepository::DestroyService) do |service|
+ expect(service).to receive(:execute).and_return({ status: :error })
+ end
expect(destroy_project(project, user)).to be false
end
end
end
+
+ context 'when there are no tags for legacy root repository' do
+ before do
+ stub_container_registry_tags(repository: project.full_path,
+ tags: [])
+ end
+
+ it 'does not try to destroy the repository' do
+ expect(Projects::ContainerRepository::DestroyService).not_to receive(:new)
+
+ destroy_project(project, user)
+ end
+ end
end
context 'for a forked project with LFS objects' do
diff --git a/spec/services/projects/group_links/create_service_spec.rb b/spec/services/projects/group_links/create_service_spec.rb
index 65d3085a850..eae898b4f68 100644
--- a/spec/services/projects/group_links/create_service_spec.rb
+++ b/spec/services/projects/group_links/create_service_spec.rb
@@ -58,6 +58,8 @@ RSpec.describe Projects::GroupLinks::CreateService, '#execute' do
end
it 'schedules authorization update for users with access to group' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
expect(AuthorizedProjectsWorker).not_to(
receive(:bulk_perform_async)
)
diff --git a/spec/services/projects/group_links/destroy_service_spec.rb b/spec/services/projects/group_links/destroy_service_spec.rb
index 5d07fd52230..89865d6bc3b 100644
--- a/spec/services/projects/group_links/destroy_service_spec.rb
+++ b/spec/services/projects/group_links/destroy_service_spec.rb
@@ -28,6 +28,8 @@ RSpec.describe Projects::GroupLinks::DestroyService, '#execute' do
end
it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
receive(:bulk_perform_in)
.with(1.hour,
diff --git a/spec/services/projects/group_links/update_service_spec.rb b/spec/services/projects/group_links/update_service_spec.rb
index 20616890ebd..1acbb770763 100644
--- a/spec/services/projects/group_links/update_service_spec.rb
+++ b/spec/services/projects/group_links/update_service_spec.rb
@@ -42,6 +42,8 @@ RSpec.describe Projects::GroupLinks::UpdateService, '#execute' do
end
it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
receive(:bulk_perform_in)
.with(1.hour,
diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb
index 2c1ebe27014..be059aec697 100644
--- a/spec/services/projects/import_export/export_service_spec.rb
+++ b/spec/services/projects/import_export/export_service_spec.rb
@@ -2,11 +2,12 @@
require 'spec_helper'
-RSpec.describe Projects::ImportExport::ExportService do
+RSpec.describe Projects::ImportExport::ExportService, feature_category: :importers do
describe '#execute' do
let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be_with_reload(:project) { create(:project, group: group) }
- let(:project) { create(:project) }
let(:shared) { project.import_export_shared }
let!(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new }
@@ -220,5 +221,21 @@ RSpec.describe Projects::ImportExport::ExportService do
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error).with_message(expected_message)
end
end
+
+ it "avoids N+1 when exporting project members" do
+ group.add_owner(user)
+ group.add_maintainer(create(:user))
+ project.add_maintainer(create(:user))
+
+ # warm up
+ service.execute
+
+ control = ActiveRecord::QueryRecorder.new { service.execute }
+
+ group.add_maintainer(create(:user))
+ project.add_maintainer(create(:user))
+
+ expect { service.execute }.not_to exceed_query_limit(control)
+ end
end
end
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index 38ab7b6e2ee..97a3b338069 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ImportService do
+RSpec.describe Projects::ImportService, feature_category: :importers do
let!(:project) { create(:project) }
let(:user) { project.creator }
diff --git a/spec/services/projects/protect_default_branch_service_spec.rb b/spec/services/projects/protect_default_branch_service_spec.rb
index c8aa421cdd4..9f9e89ff8f8 100644
--- a/spec/services/projects/protect_default_branch_service_spec.rb
+++ b/spec/services/projects/protect_default_branch_service_spec.rb
@@ -233,6 +233,38 @@ RSpec.describe Projects::ProtectDefaultBranchService do
end
end
+ describe '#protected_branch_exists?' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ let(:default_branch) { "default-branch" }
+
+ before do
+ allow(project).to receive(:default_branch).and_return(default_branch)
+ create(:protected_branch, project: nil, group: group, name: default_branch)
+ end
+
+ context 'when feature flag `group_protected_branches` disabled' do
+ before do
+ stub_feature_flags(group_protected_branches: false)
+ end
+
+ it 'return false' do
+ expect(service.protected_branch_exists?).to eq(false)
+ end
+ end
+
+ context 'when feature flag `group_protected_branches` enabled' do
+ before do
+ stub_feature_flags(group_protected_branches: true)
+ end
+
+ it 'return true' do
+ expect(service.protected_branch_exists?).to eq(true)
+ end
+ end
+ end
+
describe '#default_branch' do
it 'returns the default branch of the project' do
allow(project)
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 5171836f917..32818535146 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -126,6 +126,12 @@ RSpec.describe Projects::TransferService do
expect(project.namespace).to eq(user.namespace)
end
+ it 'invalidates personal_project_count cache of the the owner of the personal namespace' do
+ expect(user).to receive(:invalidate_personal_projects_count)
+
+ execute_transfer
+ end
+
context 'the owner of the namespace does not have a direct membership in the project residing in the group' do
it 'creates a project membership record for the owner of the namespace, with OWNER access level, after the transfer' do
execute_transfer
@@ -161,6 +167,17 @@ RSpec.describe Projects::TransferService do
end
end
+ context 'personal namespace -> group', :enable_admin_mode do
+ let(:executor) { create(:admin) }
+
+ it 'invalidates personal_project_count cache of the the owner of the personal namespace' \
+ 'that previously held the project' do
+ expect(user).to receive(:invalidate_personal_projects_count)
+
+ execute_transfer
+ end
+ end
+
context 'when transfer succeeds' do
before do
group.add_owner(user)
@@ -645,6 +662,8 @@ RSpec.describe Projects::TransferService do
end
it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
user_ids = [user.id, member_of_old_group.id, member_of_new_group.id].map { |id| [id] }
expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb
index 9c8fe769ed8..625aa4fa377 100644
--- a/spec/services/protected_branches/create_service_spec.rb
+++ b/spec/services/protected_branches/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProtectedBranches::CreateService do
+RSpec.describe ProtectedBranches::CreateService, feature_category: :compliance_management do
shared_examples 'execute with entity' do
let(:params) do
{
@@ -58,6 +58,7 @@ RSpec.describe ProtectedBranches::CreateService do
context 'with entity project' do
let_it_be_with_reload(:entity) { create(:project) }
+
let(:user) { entity.first_owner }
it_behaves_like 'execute with entity'
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 8eccb9e41bb..257e7eb972b 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe QuickActions::InterpretService do
+RSpec.describe QuickActions::InterpretService, feature_category: :team_planning do
include AfterNextHelpers
let_it_be(:group) { create(:group, :crm_enabled) }
diff --git a/spec/services/quick_actions/target_service_spec.rb b/spec/services/quick_actions/target_service_spec.rb
index d960678f809..1b0a5d4ae73 100644
--- a/spec/services/quick_actions/target_service_spec.rb
+++ b/spec/services/quick_actions/target_service_spec.rb
@@ -12,9 +12,9 @@ RSpec.describe QuickActions::TargetService do
end
describe '#execute' do
- shared_examples 'no target' do |type_id:|
+ shared_examples 'no target' do |type_iid:|
it 'returns nil' do
- target = service.execute(type, type_id)
+ target = service.execute(type, type_iid)
expect(target).to be_nil
end
@@ -22,15 +22,15 @@ RSpec.describe QuickActions::TargetService do
shared_examples 'find target' do
it 'returns the target' do
- found_target = service.execute(type, target_id)
+ found_target = service.execute(type, target_iid)
expect(found_target).to eq(target)
end
end
- shared_examples 'build target' do |type_id:|
+ shared_examples 'build target' do |type_iid:|
it 'builds a new target' do
- target = service.execute(type, type_id)
+ target = service.execute(type, type_iid)
expect(target.project).to eq(project)
expect(target).to be_new_record
@@ -39,36 +39,44 @@ RSpec.describe QuickActions::TargetService do
context 'for issue' do
let(:target) { create(:issue, project: project) }
- let(:target_id) { target.iid }
+ let(:target_iid) { target.iid }
let(:type) { 'Issue' }
it_behaves_like 'find target'
- it_behaves_like 'build target', type_id: nil
- it_behaves_like 'build target', type_id: -1
+ it_behaves_like 'build target', type_iid: nil
+ it_behaves_like 'build target', type_iid: -1
+ end
+
+ context 'for work item' do
+ let(:target) { create(:work_item, :task, project: project) }
+ let(:target_iid) { target.iid }
+ let(:type) { 'WorkItem' }
+
+ it_behaves_like 'find target'
end
context 'for merge request' do
let(:target) { create(:merge_request, source_project: project) }
- let(:target_id) { target.iid }
+ let(:target_iid) { target.iid }
let(:type) { 'MergeRequest' }
it_behaves_like 'find target'
- it_behaves_like 'build target', type_id: nil
- it_behaves_like 'build target', type_id: -1
+ it_behaves_like 'build target', type_iid: nil
+ it_behaves_like 'build target', type_iid: -1
end
context 'for commit' do
let(:project) { create(:project, :repository) }
let(:target) { project.commit.parent }
- let(:target_id) { target.sha }
+ let(:target_iid) { target.sha }
let(:type) { 'Commit' }
it_behaves_like 'find target'
- it_behaves_like 'no target', type_id: 'invalid_sha'
+ it_behaves_like 'no target', type_iid: 'invalid_sha'
- context 'with nil target_id' do
+ context 'with nil target_iid' do
let(:target) { project.commit }
- let(:target_id) { nil }
+ let(:target_iid) { nil }
it_behaves_like 'find target'
end
@@ -77,7 +85,7 @@ RSpec.describe QuickActions::TargetService do
context 'for unknown type' do
let(:type) { 'unknown' }
- it_behaves_like 'no target', type_id: :unused
+ it_behaves_like 'no target', type_iid: :unused
end
end
end
diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb
index 5f49eed3e77..9768ceb12e8 100644
--- a/spec/services/releases/create_service_spec.rb
+++ b/spec/services/releases/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Releases::CreateService do
+RSpec.describe Releases::CreateService, feature_category: :continuous_integration do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:tag_name) { project.repository.tag_names.first }
@@ -132,6 +132,15 @@ RSpec.describe Releases::CreateService do
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq("Milestone(s) not found: #{inexistent_milestone_tag}")
end
+
+ it 'raises an error saying the milestone id is inexistent' do
+ inexistent_milestone_id = non_existing_record_id
+ service = described_class.new(project, user, params.merge!({ milestone_ids: [inexistent_milestone_id] }))
+ result = service.execute
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq("Milestone id(s) not found: #{inexistent_milestone_id}")
+ end
end
context 'when existing milestone is passed in' do
@@ -140,15 +149,27 @@ RSpec.describe Releases::CreateService do
let(:params_with_milestone) { params.merge!({ milestones: [title] }) }
let(:service) { described_class.new(milestone.project, user, params_with_milestone) }
- it 'creates a release and ties this milestone to it' do
- result = service.execute
+ shared_examples 'creates release' do
+ it 'creates a release and ties this milestone to it' do
+ result = service.execute
- expect(project.releases.count).to eq(1)
- expect(result[:status]).to eq(:success)
+ expect(project.releases.count).to eq(1)
+ expect(result[:status]).to eq(:success)
+
+ release = project.releases.last
+
+ expect(release.milestones).to match_array([milestone])
+ end
+ end
- release = project.releases.last
+ context 'by title' do
+ it_behaves_like 'creates release'
+ end
+
+ context 'by ids' do
+ let(:params_with_milestone) { params.merge!({ milestone_ids: [milestone.id] }) }
- expect(release.milestones).to match_array([milestone])
+ it_behaves_like 'creates release'
end
context 'when another release was previously created with that same milestone linked' do
@@ -164,18 +185,31 @@ RSpec.describe Releases::CreateService do
end
end
- context 'when multiple existing milestone titles are passed in' do
+ context 'when multiple existing milestones are passed in' do
let(:title_1) { 'v1.0' }
let(:title_2) { 'v1.0-rc' }
let!(:milestone_1) { create(:milestone, :active, project: project, title: title_1) }
let!(:milestone_2) { create(:milestone, :active, project: project, title: title_2) }
- let!(:params_with_milestones) { params.merge!({ milestones: [title_1, title_2] }) }
- it 'creates a release and ties it to these milestones' do
- described_class.new(project, user, params_with_milestones).execute
- release = project.releases.last
+ shared_examples 'creates multiple releases' do
+ it 'creates a release and ties it to these milestones' do
+ described_class.new(project, user, params_with_milestones).execute
+ release = project.releases.last
+
+ expect(release.milestones.map(&:title)).to include(title_1, title_2)
+ end
+ end
+
+ context 'by title' do
+ let!(:params_with_milestones) { params.merge!({ milestones: [title_1, title_2] }) }
+
+ it_behaves_like 'creates multiple releases'
+ end
+
+ context 'by ids' do
+ let!(:params_with_milestones) { params.merge!({ milestone_ids: [milestone_1.id, milestone_2.id] }) }
- expect(release.milestones.map(&:title)).to include(title_1, title_2)
+ it_behaves_like 'creates multiple releases'
end
end
@@ -198,6 +232,17 @@ RSpec.describe Releases::CreateService do
service.execute
end.not_to change(Release, :count)
end
+
+ context 'with milestones as ids' do
+ let!(:params_with_milestones) { params.merge!({ milestone_ids: [milestone.id, non_existing_record_id] }) }
+
+ it 'raises an error' do
+ result = service.execute
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq("Milestone id(s) not found: #{non_existing_record_id}")
+ end
+ end
end
context 'no milestone association behavior' do
diff --git a/spec/services/releases/update_service_spec.rb b/spec/services/releases/update_service_spec.rb
index 7461470a844..6bddea48251 100644
--- a/spec/services/releases/update_service_spec.rb
+++ b/spec/services/releases/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Releases::UpdateService do
+RSpec.describe Releases::UpdateService, feature_category: :continuous_integration do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:new_name) { 'A new name' }
@@ -60,18 +60,22 @@ RSpec.describe Releases::UpdateService do
release.milestones << milestone
end
- context 'a different milestone' do
- let(:new_title) { 'v2.0' }
-
+ shared_examples 'updates milestones' do
it 'updates the related milestone accordingly' do
- result = service.execute
release.reload
+ result = service.execute
expect(release.milestones.first.title).to eq(new_title)
expect(result[:milestones_updated]).to be_truthy
end
end
+ context 'a different milestone' do
+ let(:new_title) { 'v2.0' }
+
+ it_behaves_like 'updates milestones'
+ end
+
context 'an identical milestone' do
let(:new_title) { 'v1.0' }
@@ -79,11 +83,17 @@ RSpec.describe Releases::UpdateService do
expect { service.execute }.to raise_error(ActiveRecord::RecordInvalid)
end
end
+
+ context 'by ids' do
+ let(:new_title) { 'v2.0' }
+ let(:params_with_milestone) { params.merge!({ milestone_ids: [new_milestone.id] }) }
+
+ it_behaves_like 'updates milestones'
+ end
end
context "when an 'empty' milestone is passed in" do
let(:milestone) { create(:milestone, project: project, title: 'v1.0') }
- let(:params_with_empty_milestone) { params.merge!({ milestones: [] }) }
before do
release.milestones << milestone
@@ -91,12 +101,26 @@ RSpec.describe Releases::UpdateService do
service.params = params_with_empty_milestone
end
- it 'removes the old milestone and does not associate any new milestone' do
- result = service.execute
- release.reload
+ shared_examples 'removes milestones' do
+ it 'removes the old milestone and does not associate any new milestone' do
+ result = service.execute
+ release.reload
+
+ expect(release.milestones).not_to be_present
+ expect(result[:milestones_updated]).to be_truthy
+ end
+ end
- expect(release.milestones).not_to be_present
- expect(result[:milestones_updated]).to be_truthy
+ context 'by title' do
+ let(:params_with_empty_milestone) { params.merge!({ milestones: [] }) }
+
+ it_behaves_like 'removes milestones'
+ end
+
+ context 'by id' do
+ let(:params_with_empty_milestone) { params.merge!({ milestone_ids: [] }) }
+
+ it_behaves_like 'removes milestones'
end
end
@@ -104,22 +128,35 @@ RSpec.describe Releases::UpdateService do
let(:new_title_1) { 'v2.0' }
let(:new_title_2) { 'v2.0-rc' }
let(:milestone) { create(:milestone, project: project, title: 'v1.0') }
- let(:params_with_milestones) { params.merge!({ milestones: [new_title_1, new_title_2] }) }
let(:service) { described_class.new(project, user, params_with_milestones) }
+ let!(:new_milestone_1) { create(:milestone, project: project, title: new_title_1) }
+ let!(:new_milestone_2) { create(:milestone, project: project, title: new_title_2) }
before do
- create(:milestone, project: project, title: new_title_1)
- create(:milestone, project: project, title: new_title_2)
release.milestones << milestone
end
- it 'removes the old milestone and update the release with the new ones' do
- result = service.execute
- release.reload
+ shared_examples 'updates multiple milestones' do
+ it 'removes the old milestone and update the release with the new ones' do
+ result = service.execute
+ release.reload
+
+ milestone_titles = release.milestones.map(&:title)
+ expect(milestone_titles).to match_array([new_title_1, new_title_2])
+ expect(result[:milestones_updated]).to be_truthy
+ end
+ end
+
+ context 'by title' do
+ let(:params_with_milestones) { params.merge!({ milestones: [new_title_1, new_title_2] }) }
+
+ it_behaves_like 'updates multiple milestones'
+ end
+
+ context 'by id' do
+ let(:params_with_milestones) { params.merge!({ milestone_ids: [new_milestone_1.id, new_milestone_2.id] }) }
- milestone_titles = release.milestones.map(&:title)
- expect(milestone_titles).to match_array([new_title_1, new_title_2])
- expect(result[:milestones_updated]).to be_truthy
+ it_behaves_like 'updates multiple milestones'
end
end
end
diff --git a/spec/services/resource_events/change_labels_service_spec.rb b/spec/services/resource_events/change_labels_service_spec.rb
index 9b0ca54a394..d94b49de9d7 100644
--- a/spec/services/resource_events/change_labels_service_spec.rb
+++ b/spec/services/resource_events/change_labels_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe ResourceEvents::ChangeLabelsService do
+# feature category is shared among plan(issues, epics), monitor(incidents), create(merge request) stages
+RSpec.describe ResourceEvents::ChangeLabelsService, feature_category: :shared do
let_it_be(:project) { create(:project) }
let_it_be(:author) { create(:user) }
let_it_be(:issue) { create(:issue, project: project) }
@@ -86,12 +87,30 @@ RSpec.describe ResourceEvents::ChangeLabelsService do
let(:added) { [labels[0]] }
let(:removed) { [labels[1]] }
+ it_behaves_like 'creating timeline events'
+
it 'creates all label events in a single query' do
expect(ApplicationRecord).to receive(:legacy_bulk_insert).once.and_call_original
expect { change_labels }.to change { resource.resource_label_events.count }.from(0).to(2)
end
- it_behaves_like 'creating timeline events'
+ context 'when resource is a work item' do
+ it 'triggers note created subscription' do
+ expect(GraphqlTriggers).to receive(:work_item_note_created)
+
+ change_labels
+ end
+ end
+
+ context 'when resource is an MR' do
+ let(:resource) { create(:merge_request, source_project: project) }
+
+ it 'does not trigger note created subscription' do
+ expect(GraphqlTriggers).not_to receive(:work_item_note_created)
+
+ change_labels
+ end
+ end
end
describe 'usage data' do
diff --git a/spec/services/security/ci_configuration/sast_create_service_spec.rb b/spec/services/security/ci_configuration/sast_create_service_spec.rb
index 1e6dc367146..e80fe1a42fa 100644
--- a/spec/services/security/ci_configuration/sast_create_service_spec.rb
+++ b/spec/services/security/ci_configuration/sast_create_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Security::CiConfiguration::SastCreateService, :snowplow, feature_category: :sast do
+RSpec.describe Security::CiConfiguration::SastCreateService, :snowplow,
+ feature_category: :static_application_security_testing do
subject(:result) { described_class.new(project, user, params).execute }
let(:branch_name) { 'set-sast-config-1' }
@@ -24,7 +25,45 @@ RSpec.describe Security::CiConfiguration::SastCreateService, :snowplow, feature_
include_examples 'services security ci configuration create service'
- context "when committing to the default branch", :aggregate_failures do
+ RSpec.shared_examples_for 'commits directly to the default branch' do
+ it 'commits directly to the default branch' do
+ expect(project).to receive(:default_branch).twice.and_return('master')
+
+ expect(result.status).to eq(:success)
+ expect(result.payload[:success_path]).to match(/#{Gitlab::Routing.url_helpers.project_new_merge_request_url(project, {})}(.*)description(.*)source_branch/)
+ expect(result.payload[:branch]).to eq('master')
+ end
+ end
+
+ context 'when the repository is empty' do
+ let_it_be(:project) { create(:project_empty_repo) }
+
+ context 'when initialize_with_sast is false' do
+ before do
+ project.add_developer(user)
+ end
+
+ let(:params) { { initialize_with_sast: false } }
+
+ it 'raises an error' do
+ expect { result }.to raise_error(Gitlab::Graphql::Errors::MutationError)
+ end
+ end
+
+ context 'when initialize_with_sast is true' do
+ let(:params) { { initialize_with_sast: true } }
+
+ subject(:result) { described_class.new(project, user, params, commit_on_default: true).execute }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it_behaves_like 'commits directly to the default branch'
+ end
+ end
+
+ context 'when committing to the default branch', :aggregate_failures do
subject(:result) { described_class.new(project, user, params, commit_on_default: true).execute }
let(:params) { {} }
@@ -33,17 +72,13 @@ RSpec.describe Security::CiConfiguration::SastCreateService, :snowplow, feature_
project.add_developer(user)
end
- it "doesn't try to remove that branch on raised exceptions" do
+ it 'does not try to remove that branch on raised exceptions' do
expect(Files::MultiService).to receive(:new).and_raise(StandardError, '_exception_')
expect(project.repository).not_to receive(:rm_branch)
expect { result }.to raise_error(StandardError, '_exception_')
end
- it "commits directly to the default branch" do
- expect(result.status).to eq(:success)
- expect(result.payload[:success_path]).to match(/#{Gitlab::Routing.url_helpers.project_new_merge_request_url(project, {})}(.*)description(.*)source_branch/)
- expect(result.payload[:branch]).to eq('master')
- end
+ it_behaves_like 'commits directly to the default branch'
end
end
diff --git a/spec/services/serverless/associate_domain_service_spec.rb b/spec/services/serverless/associate_domain_service_spec.rb
index 3b5231989bc..2f45806589e 100644
--- a/spec/services/serverless/associate_domain_service_spec.rb
+++ b/spec/services/serverless/associate_domain_service_spec.rb
@@ -3,13 +3,24 @@
require 'spec_helper'
RSpec.describe Serverless::AssociateDomainService do
- subject { described_class.new(knative, pages_domain_id: pages_domain_id, creator: creator) }
+ let_it_be(:sdc_pages_domain) { create(:pages_domain, :instance_serverless) }
+ let_it_be(:sdc_cluster) { create(:cluster, :with_installed_helm, :provided_by_gcp) }
+ let_it_be(:sdc_knative) { create(:clusters_applications_knative, cluster: sdc_cluster) }
+ let_it_be(:sdc_creator) { create(:user) }
+
+ let(:sdc) do
+ create(:serverless_domain_cluster,
+ knative: sdc_knative,
+ creator: sdc_creator,
+ pages_domain: sdc_pages_domain)
+ end
- let(:sdc) { create(:serverless_domain_cluster, pages_domain: create(:pages_domain, :instance_serverless)) }
let(:knative) { sdc.knative }
let(:creator) { sdc.creator }
let(:pages_domain_id) { sdc.pages_domain_id }
+ subject { described_class.new(knative, pages_domain_id: pages_domain_id, creator: creator) }
+
context 'when the domain is unchanged' do
let(:creator) { create(:user) }
@@ -19,8 +30,8 @@ RSpec.describe Serverless::AssociateDomainService do
end
context 'when domain is changed to nil' do
- let(:pages_domain_id) { nil }
- let(:creator) { create(:user) }
+ let_it_be(:creator) { create(:user) }
+ let_it_be(:pages_domain_id) { nil }
it 'removes the association between knative and the domain' do
expect { subject.execute }.to change { knative.reload.pages_domain }.from(sdc.pages_domain).to(nil)
@@ -32,11 +43,13 @@ RSpec.describe Serverless::AssociateDomainService do
end
context 'when a new domain is associated' do
- let(:pages_domain_id) { create(:pages_domain, :instance_serverless).id }
- let(:creator) { create(:user) }
+ let_it_be(:creator) { create(:user) }
+ let_it_be(:pages_domain_id) { create(:pages_domain, :instance_serverless).id }
it 'creates an association with the domain' do
- expect { subject.execute }.to change { knative.pages_domain.id }.from(sdc.pages_domain.id).to(pages_domain_id)
+ expect { subject.execute }.to change { knative.reload.pages_domain.id }
+ .from(sdc.pages_domain.id)
+ .to(pages_domain_id)
end
it 'updates creator' do
@@ -45,7 +58,7 @@ RSpec.describe Serverless::AssociateDomainService do
end
context 'when knative is not authorized to use the pages domain' do
- let(:pages_domain_id) { create(:pages_domain).id }
+ let_it_be(:pages_domain_id) { create(:pages_domain).id }
before do
expect(knative).to receive(:available_domains).and_return(PagesDomain.none)
@@ -56,19 +69,23 @@ RSpec.describe Serverless::AssociateDomainService do
end
end
- context 'when knative hostname is nil' do
- let(:knative) { build(:clusters_applications_knative, hostname: nil) }
+ describe 'for new knative application' do
+ let_it_be(:cluster) { create(:cluster, :with_installed_helm, :provided_by_gcp) }
- it 'sets hostname to a placeholder value' do
- expect { subject.execute }.to change { knative.hostname }.to('example.com')
+ context 'when knative hostname is nil' do
+ let(:knative) { build(:clusters_applications_knative, cluster: cluster, hostname: nil) }
+
+ it 'sets hostname to a placeholder value' do
+ expect { subject.execute }.to change { knative.hostname }.to('example.com')
+ end
end
- end
- context 'when knative hostname exists' do
- let(:knative) { build(:clusters_applications_knative, hostname: 'hostname.com') }
+ context 'when knative hostname exists' do
+ let(:knative) { build(:clusters_applications_knative, cluster: cluster, hostname: 'hostname.com') }
- it 'does not change hostname' do
- expect { subject.execute }.not_to change { knative.hostname }
+ it 'does not change hostname' do
+ expect { subject.execute }.not_to change { knative.hostname }
+ end
end
end
end
diff --git a/spec/services/spam/spam_verdict_service_spec.rb b/spec/services/spam/spam_verdict_service_spec.rb
index b89c96129c2..dde93aa6b93 100644
--- a/spec/services/spam/spam_verdict_service_spec.rb
+++ b/spec/services/spam/spam_verdict_service_spec.rb
@@ -28,10 +28,6 @@ RSpec.describe Spam::SpamVerdictService do
extra_attributes
end
- before do
- stub_feature_flags(allow_possible_spam: false)
- end
-
shared_examples 'execute spam verdict service' do
subject { service.execute }
@@ -119,9 +115,9 @@ RSpec.describe Spam::SpamVerdictService do
end
end
- context 'if allow_possible_spam flag is true' do
+ context 'if allow_possible_spam application setting is true' do
before do
- stub_feature_flags(allow_possible_spam: true)
+ stub_application_setting(allow_possible_spam: true)
end
context 'and a service returns a verdict that should be overridden' do
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index a192fae27db..38b6943b12a 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SystemNoteService do
+RSpec.describe SystemNoteService, feature_category: :shared do
include Gitlab::Routing
include RepoHelpers
include AssetsHelpers
diff --git a/spec/services/tasks_to_be_done/base_service_spec.rb b/spec/services/tasks_to_be_done/base_service_spec.rb
index bf6be6d46e5..cfeff36cc0d 100644
--- a/spec/services/tasks_to_be_done/base_service_spec.rb
+++ b/spec/services/tasks_to_be_done/base_service_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe TasksToBeDone::BaseService do
subject(:service) do
TasksToBeDone::CreateCiTaskService.new(
- project: project,
+ container: project,
current_user: current_user,
assignee_ids: assignee_ids
)
@@ -35,7 +35,7 @@ RSpec.describe TasksToBeDone::BaseService do
expect(Issues::BuildService)
.to receive(:new)
- .with(project: project, current_user: current_user, params: params)
+ .with(container: project, current_user: current_user, params: params)
.and_call_original
expect { service.execute }.to change(Issue, :count).by(1)
@@ -58,7 +58,7 @@ RSpec.describe TasksToBeDone::BaseService do
expect(Issues::UpdateService)
.to receive(:new)
- .with(project: project, current_user: current_user, params: params)
+ .with(container: project, current_user: current_user, params: params)
.and_call_original
expect { service.execute }.not_to change(Issue, :count)
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 596ca9495ff..f73eae70d3c 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -1224,6 +1224,24 @@ RSpec.describe TodoService do
end
end
+ describe '#resolve_access_request_todos' do
+ let_it_be(:source) { create(:group, :public) }
+ let_it_be(:requester) { create(:group_member, :access_request, group: source, user: assignee) }
+
+ it 'marks the todos for request handler as done' do
+ request_handler_todo = create(:todo,
+ user: member,
+ state: :pending,
+ action: Todo::MEMBER_ACCESS_REQUESTED,
+ author: requester.user,
+ target: source)
+
+ service.resolve_access_request_todos(member, requester)
+
+ expect(request_handler_todo.reload).to be_done
+ end
+ end
+
describe '#restore_todo' do
let!(:todo) { create(:todo, :done, user: john_doe) }
diff --git a/spec/services/todos/destroy/entity_leave_service_spec.rb b/spec/services/todos/destroy/entity_leave_service_spec.rb
index 9d5ed70e9ef..1ced2eda799 100644
--- a/spec/services/todos/destroy/entity_leave_service_spec.rb
+++ b/spec/services/todos/destroy/entity_leave_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Todos::Destroy::EntityLeaveService do
+RSpec.describe Todos::Destroy::EntityLeaveService, feature_category: :team_planning do
let_it_be(:user, reload: true) { create(:user) }
let_it_be(:user2, reload: true) { create(:user) }
let_it_be_with_refind(:group) { create(:group, :private) }
diff --git a/spec/services/todos/destroy/group_private_service_spec.rb b/spec/services/todos/destroy/group_private_service_spec.rb
index 30d02cb7400..be470688084 100644
--- a/spec/services/todos/destroy/group_private_service_spec.rb
+++ b/spec/services/todos/destroy/group_private_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Todos::Destroy::GroupPrivateService do
+RSpec.describe Todos::Destroy::GroupPrivateService, feature_category: :team_planning do
let(:group) { create(:group, :public) }
let(:project) { create(:project, group: group) }
let(:user) { create(:user) }
diff --git a/spec/services/user_project_access_changed_service_spec.rb b/spec/services/user_project_access_changed_service_spec.rb
index be4f205afb5..356675d55f2 100644
--- a/spec/services/user_project_access_changed_service_spec.rb
+++ b/spec/services/user_project_access_changed_service_spec.rb
@@ -2,33 +2,39 @@
require 'spec_helper'
-RSpec.describe UserProjectAccessChangedService do
+RSpec.describe UserProjectAccessChangedService, feature_category: :authentication_and_authorization do
describe '#execute' do
- it 'schedules the user IDs' do
- expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait)
+ it 'permits high-priority operation' do
+ expect(AuthorizedProjectsWorker).to receive(:bulk_perform_async)
.with([[1], [2]])
described_class.new([1, 2]).execute
end
- it 'permits non-blocking operation' do
- expect(AuthorizedProjectsWorker).to receive(:bulk_perform_async)
- .with([[1], [2]])
+ context 'for low priority operation' do
+ context 'when the feature flag `do_not_run_safety_net_auth_refresh_jobs` is disabled' do
+ before do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+ end
+
+ it 'permits low-priority operation' do
+ expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
+ receive(:bulk_perform_in).with(
+ described_class::DELAY,
+ [[1], [2]],
+ { batch_delay: 30.seconds, batch_size: 100 }
+ )
+ )
+
+ described_class.new([1, 2]).execute(priority: described_class::LOW_PRIORITY)
+ end
+ end
- described_class.new([1, 2]).execute(blocking: false)
- end
+ it 'does not perform low-priority operation' do
+ expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).not_to receive(:bulk_perform_in)
- it 'permits low-priority operation' do
- expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
- receive(:bulk_perform_in).with(
- described_class::DELAY,
- [[1], [2]],
- { batch_delay: 30.seconds, batch_size: 100 }
- )
- )
-
- described_class.new([1, 2]).execute(blocking: false,
- priority: described_class::LOW_PRIORITY)
+ described_class.new([1, 2]).execute(priority: described_class::LOW_PRIORITY)
+ end
end
it 'permits medium-priority operation' do
@@ -40,14 +46,12 @@ RSpec.describe UserProjectAccessChangedService do
)
)
- described_class.new([1, 2]).execute(blocking: false,
- priority: described_class::MEDIUM_PRIORITY)
+ described_class.new([1, 2]).execute(priority: described_class::MEDIUM_PRIORITY)
end
it 'sets the current caller_id as related_class in the context of all the enqueued jobs' do
Gitlab::ApplicationContext.with_context(caller_id: 'Foo') do
- described_class.new([1, 2]).execute(blocking: false,
- priority: described_class::LOW_PRIORITY)
+ described_class.new([1, 2]).execute(priority: described_class::LOW_PRIORITY)
end
expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker.jobs).to all(
@@ -60,7 +64,7 @@ RSpec.describe UserProjectAccessChangedService do
let(:service) { UserProjectAccessChangedService.new([1, 2]) }
before do
- expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait)
+ expect(AuthorizedProjectsWorker).to receive(:bulk_perform_async)
.with([[1], [2]])
.and_return(10)
end
@@ -79,7 +83,7 @@ RSpec.describe UserProjectAccessChangedService do
service = UserProjectAccessChangedService.new([1, 2, 3, 4, 5])
- allow(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait)
+ allow(AuthorizedProjectsWorker).to receive(:bulk_perform_async)
.with([[1], [2], [3], [4], [5]])
.and_return(10)
diff --git a/spec/services/users/activity_service_spec.rb b/spec/services/users/activity_service_spec.rb
index 47a4b943d83..6c0d93f568a 100644
--- a/spec/services/users/activity_service_spec.rb
+++ b/spec/services/users/activity_service_spec.rb
@@ -7,9 +7,21 @@ RSpec.describe Users::ActivityService do
let(:user) { create(:user, last_activity_on: last_activity_on) }
- subject { described_class.new(user) }
+ subject { described_class.new(author: user) }
describe '#execute', :clean_gitlab_redis_shared_state do
+ shared_examples 'does not update last_activity_on' do
+ it 'does not update user attribute' do
+ expect { subject.execute }.not_to change(user, :last_activity_on)
+ end
+
+ it 'does not track Snowplow event' do
+ subject.execute
+
+ expect_no_snowplow_event
+ end
+ end
+
context 'when last activity is nil' do
let(:last_activity_on) { nil }
@@ -41,13 +53,29 @@ RSpec.describe Users::ActivityService do
subject.execute
end
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ subject(:record_activity) { described_class.new(author: user, namespace: namespace, project: project).execute }
+
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase3 }
+ let(:category) { described_class.name }
+ let(:action) { 'perform_action' }
+ let(:label) { 'redis_hll_counters.manage.unique_active_users_monthly' }
+ let(:namespace) { build(:group) }
+ let(:project) { build(:project) }
+ let(:context) do
+ payload = Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll,
+ event: 'unique_active_user').to_context
+ [Gitlab::Json.dump(payload)]
+ end
+ end
end
context 'when a bad object is passed' do
let(:fake_object) { double(username: 'hello') }
it 'does not record activity' do
- service = described_class.new(fake_object)
+ service = described_class.new(author: fake_object)
expect(service).not_to receive(:record_activity)
@@ -58,9 +86,7 @@ RSpec.describe Users::ActivityService do
context 'when last activity is today' do
let(:last_activity_on) { Date.today }
- it 'does not update last_activity_on' do
- expect { subject.execute }.not_to change(user, :last_activity_on)
- end
+ it_behaves_like 'does not update last_activity_on'
it 'does not try to obtain ExclusiveLease' do
expect(Gitlab::ExclusiveLease).not_to receive(:new).with("activity_service:#{user.id}", anything)
@@ -76,19 +102,17 @@ RSpec.describe Users::ActivityService do
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
end
- it 'does not update last_activity_on' do
- expect { subject.execute }.not_to change(user, :last_activity_on)
- end
+ it_behaves_like 'does not update last_activity_on'
end
context 'when a lease could not be obtained' do
let(:last_activity_on) { nil }
- it 'does not update last_activity_on' do
+ before do
stub_exclusive_lease_taken("activity_service:#{user.id}", timeout: 1.minute.to_i)
-
- expect { subject.execute }.not_to change(user, :last_activity_on)
end
+
+ it_behaves_like 'does not update last_activity_on'
end
end
@@ -104,7 +128,7 @@ RSpec.describe Users::ActivityService do
end
let(:service) do
- service = described_class.new(user)
+ service = described_class.new(author: user)
::Gitlab::Database::LoadBalancing::Session.clear_session
@@ -123,7 +147,7 @@ RSpec.describe Users::ActivityService do
end
context 'database load balancing is not configured' do
- let(:service) { described_class.new(user) }
+ let(:service) { described_class.new(author: user) }
it 'updates user without error' do
service.execute
diff --git a/spec/services/users/assigned_issues_count_service_spec.rb b/spec/services/users/assigned_issues_count_service_spec.rb
index afa6a0af3dd..2062f68b24b 100644
--- a/spec/services/users/assigned_issues_count_service_spec.rb
+++ b/spec/services/users/assigned_issues_count_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Users::AssignedIssuesCountService, :use_clean_rails_memory_store_caching,
- feature_category: :project_management do
+ feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:max_limit) { 10 }
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 4b925a058e7..5736bf885be 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state do
+RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state, feature_category: :integrations do
include StubRequests
let(:ellipsis) { '…' }
@@ -358,6 +358,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
{
trigger: 'push_hooks',
url: project_hook.url,
+ interpolated_url: project_hook.interpolated_url,
request_headers: headers,
request_data: data,
response_body: 'Success',
diff --git a/spec/services/work_items/build_service_spec.rb b/spec/services/work_items/build_service_spec.rb
index 6b2e2d8819e..405b4414fc2 100644
--- a/spec/services/work_items/build_service_spec.rb
+++ b/spec/services/work_items/build_service_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe WorkItems::BuildService do
end
describe '#execute' do
- subject { described_class.new(project: project, current_user: user, params: {}).execute }
+ subject { described_class.new(container: project, current_user: user, params: {}).execute }
it { is_expected.to be_a(::WorkItem) }
end
diff --git a/spec/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb
index 049c90f20b0..1b134c308f2 100644
--- a/spec/services/work_items/create_service_spec.rb
+++ b/spec/services/work_items/create_service_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe WorkItems::CreateService do
describe '#execute' do
let(:service) do
described_class.new(
- project: project,
+ container: project,
current_user: current_user,
params: opts,
spam_params: spam_params,
@@ -118,7 +118,7 @@ RSpec.describe WorkItems::CreateService do
let(:service) do
described_class.new(
- project: project,
+ container: project,
current_user: current_user,
params: opts,
spam_params: spam_params,
@@ -188,7 +188,7 @@ RSpec.describe WorkItems::CreateService do
{
title: 'Awesome work_item',
description: 'please fix',
- work_item_type: create(:work_item_type, :task)
+ work_item_type: WorkItems::Type.default_by_type(:task)
}
end
diff --git a/spec/services/work_items/delete_service_spec.rb b/spec/services/work_items/delete_service_spec.rb
index 6cca5018852..69ae881a12f 100644
--- a/spec/services/work_items/delete_service_spec.rb
+++ b/spec/services/work_items/delete_service_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe WorkItems::DeleteService do
end
describe '#execute' do
- subject(:result) { described_class.new(project: project, current_user: user).execute(work_item) }
+ subject(:result) { described_class.new(container: project, current_user: user).execute(work_item) }
context 'when user can delete the work item' do
it { is_expected.to be_success }
diff --git a/spec/services/work_items/export_csv_service_spec.rb b/spec/services/work_items/export_csv_service_spec.rb
new file mode 100644
index 00000000000..0718d3b686a
--- /dev/null
+++ b/spec/services/work_items/export_csv_service_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::ExportCsvService, :with_license, feature_category: :team_planning do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :public, group: group) }
+ let_it_be(:work_item_1) { create(:work_item, project: project) }
+ let_it_be(:work_item_2) { create(:work_item, :incident, project: project) }
+
+ subject { described_class.new(WorkItem.all, project) }
+
+ def csv
+ CSV.parse(subject.csv_data, headers: true)
+ end
+
+ context 'when import_export_work_items_csv flag is not enabled' do
+ before do
+ stub_feature_flags(import_export_work_items_csv: false)
+ end
+
+ it 'renders an error' do
+ expect { subject.csv_data }.to raise_error(described_class::NotAvailableError)
+ end
+ end
+
+ it 'renders csv to string' do
+ expect(subject.csv_data).to be_a String
+ end
+
+ describe '#email' do
+ # TODO - will be implemented as part of https://gitlab.com/gitlab-org/gitlab/-/issues/379082
+ xit 'emails csv' do
+ expect { subject.email(user) }.o change { ActionMailer::Base.deliveries.count }.from(0).to(1)
+ end
+ end
+
+ it 'returns two work items' do
+ expect(csv.count).to eq(2)
+ end
+
+ specify 'iid' do
+ expect(csv[0]['Id']).to eq work_item_1.iid.to_s
+ end
+
+ specify 'title' do
+ expect(csv[0]['Title']).to eq work_item_1.title
+ end
+
+ specify 'type' do
+ expect(csv[0]['Type']).to eq('Issue')
+ expect(csv[1]['Type']).to eq('Incident')
+ end
+
+ specify 'author name' do
+ expect(csv[0]['Author']).to eq(work_item_1.author_name)
+ end
+
+ specify 'author username' do
+ expect(csv[0]['Author Username']).to eq(work_item_1.author.username)
+ end
+
+ specify 'created_at' do
+ expect(csv[0]['Created At (UTC)']).to eq(work_item_1.created_at.to_s(:csv))
+ end
+
+ it 'preloads fields to avoid N+1 queries' do
+ control = ActiveRecord::QueryRecorder.new { subject.csv_data }
+
+ create(:work_item, :task, project: project)
+
+ expect { subject.csv_data }.not_to exceed_query_limit(control)
+ end
+
+ it_behaves_like 'a service that returns invalid fields from selection'
+end
diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb
index 87665bcad2c..435995c6570 100644
--- a/spec/services/work_items/update_service_spec.rb
+++ b/spec/services/work_items/update_service_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe WorkItems::UpdateService do
describe '#execute' do
let(:service) do
described_class.new(
- project: project,
+ container: project,
current_user: current_user,
params: opts,
spam_params: spam_params,
@@ -146,7 +146,7 @@ RSpec.describe WorkItems::UpdateService do
let(:service) do
described_class.new(
- project: project,
+ container: project,
current_user: current_user,
params: opts,
spam_params: spam_params,
@@ -362,7 +362,7 @@ RSpec.describe WorkItems::UpdateService do
def update_issuable(update_params)
described_class.new(
- project: project,
+ container: project,
current_user: current_user,
params: update_params,
spam_params: spam_params,