diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 15:44:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 15:44:42 +0000 |
commit | 4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch) | |
tree | 5423a1c7516cffe36384133ade12572cf709398d /spec/services | |
parent | e570267f2f6b326480d284e0164a6464ba4081bc (diff) | |
download | gitlab-ce-4555e1b21c365ed8303ffb7a3325d773c9b8bf31.tar.gz |
Add latest changes from gitlab-org/gitlab@13-12-stable-eev13.12.0-rc42
Diffstat (limited to 'spec/services')
170 files changed, 3804 insertions, 1799 deletions
diff --git a/spec/services/admin/propagate_service_template_spec.rb b/spec/services/admin/propagate_service_template_spec.rb index d95d31ceaea..406da790a66 100644 --- a/spec/services/admin/propagate_service_template_spec.rb +++ b/spec/services/admin/propagate_service_template_spec.rb @@ -29,7 +29,7 @@ RSpec.describe Admin::PropagateServiceTemplate do context 'with a project that has another service' do before do - BambooService.create!( + Integrations::Bamboo.create!( active: true, project: project, properties: { @@ -50,10 +50,10 @@ RSpec.describe Admin::PropagateServiceTemplate do end it 'does not create the service if it exists already' do - Service.build_from_integration(service_template, project_id: project.id).save! + Integration.build_from_integration(service_template, project_id: project.id).save! expect { described_class.propagate(service_template) } - .not_to change { Service.count } + .not_to change { Integration.count } end end end diff --git a/spec/services/alert_management/process_prometheus_alert_service_spec.rb b/spec/services/alert_management/process_prometheus_alert_service_spec.rb index 9bd71ea6f64..86a6cdee52d 100644 --- a/spec/services/alert_management/process_prometheus_alert_service_spec.rb +++ b/spec/services/alert_management/process_prometheus_alert_service_spec.rb @@ -5,38 +5,27 @@ require 'spec_helper' RSpec.describe AlertManagement::ProcessPrometheusAlertService do let_it_be(:project, reload: true) { create(:project, :repository) } - before do - allow(ProjectServiceWorker).to receive(:perform_async) - end + let(:service) { described_class.new(project, payload) } describe '#execute' do - let(:service) { described_class.new(project, payload) } - let(:source) { 'Prometheus' } - let(:auto_close_incident) { true } - let(:create_issue) { true } - let(:send_email) { true } - let(:incident_management_setting) do - double( - auto_close_incident?: auto_close_incident, - create_issue?: create_issue, - send_email?: send_email - ) - end + include_context 'incident management settings enabled' + + subject(:execute) { service.execute } before do - allow(service) - .to receive(:incident_management_setting) - .and_return(incident_management_setting) + stub_licensed_features(oncall_schedules: false, generic_alert_fingerprinting: false) end - subject(:execute) { service.execute } - context 'when alert payload is valid' do - let(:parsed_payload) { Gitlab::AlertManagement::Payload.parse(project, payload, monitoring_tool: source) } - let(:fingerprint) { parsed_payload.gitlab_fingerprint } + let_it_be(:starts_at) { '2020-04-27T10:10:22.265949279Z' } + let_it_be(:title) { 'Alert title' } + let_it_be(:fingerprint) { [starts_at, title, 'vector(1)'].join('/') } + let_it_be(:source) { 'Prometheus' } + + let(:prometheus_status) { 'firing' } let(:payload) do { - 'status' => status, + 'status' => prometheus_status, 'labels' => { 'alertname' => 'GitalyFileServerDown', 'channel' => 'gitaly', @@ -46,196 +35,32 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do 'annotations' => { 'description' => 'Alert description', 'runbook' => 'troubleshooting/gitaly-down.md', - 'title' => 'Alert title' + 'title' => title }, - 'startsAt' => '2020-04-27T10:10:22.265949279Z', + 'startsAt' => starts_at, 'endsAt' => '2020-04-27T10:20:22.265949279Z', - 'generatorURL' => 'http://8d467bd4607a:9090/graph?g0.expr=vector%281%29&g0.tab=1', - 'fingerprint' => 'b6ac4d42057c43c1' + 'generatorURL' => 'http://8d467bd4607a:9090/graph?g0.expr=vector%281%29&g0.tab=1' } end - let(:status) { 'firing' } - - context 'when Prometheus alert status is firing' do - context 'when alert with the same fingerprint already exists' do - let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint) } - - it_behaves_like 'adds an alert management alert event' - it_behaves_like 'processes incident issues' - it_behaves_like 'Alert Notification Service sends notification email' - - context 'existing alert is resolved' do - let!(:alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: fingerprint) } - - it_behaves_like 'creates an alert management alert' - it_behaves_like 'Alert Notification Service sends notification email' - end - - context 'existing alert is ignored' do - let!(:alert) { create(:alert_management_alert, :ignored, project: project, fingerprint: fingerprint) } - - it_behaves_like 'adds an alert management alert event' - it_behaves_like 'Alert Notification Service sends no notifications' - end - - context 'existing alert is acknowledged' do - let!(:alert) { create(:alert_management_alert, :acknowledged, project: project, fingerprint: fingerprint) } - - it_behaves_like 'adds an alert management alert event' - it_behaves_like 'Alert Notification Service sends no notifications' - end - - context 'two existing alerts, one resolved one open' do - let!(:resolved_alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: fingerprint) } - let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint) } - - it_behaves_like 'adds an alert management alert event' - it_behaves_like 'Alert Notification Service sends notification email' - end - - context 'when auto-creation of issues is disabled' do - let(:create_issue) { false } - - it_behaves_like 'does not process incident issues' - end - - context 'when emails are disabled' do - let(:send_email) { false } - - it_behaves_like 'Alert Notification Service sends no notifications' - end - end - - context 'when alert does not exist' do - context 'when alert can be created' do - it_behaves_like 'creates an alert management alert' - it_behaves_like 'Alert Notification Service sends notification email' - it_behaves_like 'processes incident issues' - - it_behaves_like 'creates single system note based on the source of the alert' - - context 'when auto-alert creation is disabled' do - let(:create_issue) { false } - - it_behaves_like 'does not process incident issues' - end - - context 'when emails are disabled' do - let(:send_email) { false } - - it_behaves_like 'Alert Notification Service sends no notifications' - end - end - - context 'when alert cannot be created' do - let(:errors) { double(messages: { hosts: ['hosts array is over 255 chars'] })} - - before do - allow(service).to receive(:alert).and_call_original - allow(service).to receive_message_chain(:alert, :save).and_return(false) - allow(service).to receive_message_chain(:alert, :errors).and_return(errors) - end - - it_behaves_like 'Alert Notification Service sends no notifications', http_status: :bad_request - it_behaves_like 'does not process incident issues due to error', http_status: :bad_request - - it 'writes a warning to the log' do - expect(Gitlab::AppLogger).to receive(:warn).with( - message: 'Unable to create AlertManagement::Alert from Prometheus', - project_id: project.id, - alert_errors: { hosts: ['hosts array is over 255 chars'] } - ) - - execute - end - end - - it { is_expected.to be_success } - end - end - - context 'when Prometheus alert status is resolved' do - let(:status) { 'resolved' } - let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint, monitoring_tool: source) } - - context 'when auto_resolve_incident set to true' do - context 'when status can be changed' do - it_behaves_like 'Alert Notification Service sends notification email' - it_behaves_like 'does not process incident issues' - - it 'resolves an existing alert without error' do - expect(Gitlab::AppLogger).not_to receive(:warn) - expect { execute }.to change { alert.reload.resolved? }.to(true) - end - - it_behaves_like 'creates status-change system note for an auto-resolved alert' - - context 'existing issue' do - let!(:alert) { create(:alert_management_alert, :with_issue, project: project, fingerprint: fingerprint) } - - it 'closes the issue' do - issue = alert.issue - - expect { execute } - .to change { issue.reload.state } - .from('opened') - .to('closed') - end - - it 'creates a resource state event' do - expect { execute }.to change(ResourceStateEvent, :count).by(1) - end - end - end - - context 'when status change did not succeed' do - before do - allow(AlertManagement::Alert).to receive(:for_fingerprint).and_return([alert]) - allow(alert).to receive(:resolve).and_return(false) - end - - it 'writes a warning to the log' do - expect(Gitlab::AppLogger).to receive(:warn).with( - message: 'Unable to update AlertManagement::Alert status to resolved', - project_id: project.id, - alert_id: alert.id - ) - - execute - end - - it_behaves_like 'Alert Notification Service sends notification email' - end - - it { is_expected.to be_success } - end + it_behaves_like 'processes new firing alert' - context 'when auto_resolve_incident set to false' do - let(:auto_close_incident) { false } + context 'with resolving payload' do + let(:prometheus_status) { 'resolved' } - it 'does not resolve an existing alert' do - expect { execute }.not_to change { alert.reload.resolved? } - end - - it_behaves_like 'creates single system note based on the source of the alert' - end - - context 'when emails are disabled' do - let(:send_email) { false } - - it_behaves_like 'Alert Notification Service sends no notifications' - end + it_behaves_like 'processes recovery alert' end context 'environment given' do let(:environment) { create(:environment, project: project) } + let(:alert) { project.alert_management_alerts.last } - it 'sets the environment' do + before do payload['labels']['gitlab_environment_name'] = environment.name - execute + end - alert = project.alert_management_alerts.last + it 'sets the environment' do + execute expect(alert.environment).to eq(environment) end @@ -243,12 +68,14 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do context 'prometheus alert given' do let(:prometheus_alert) { create(:prometheus_alert, project: project) } + let(:alert) { project.alert_management_alerts.last } - it 'sets the prometheus alert and environment' do + before do payload['labels']['gitlab_alert_id'] = prometheus_alert.prometheus_metric_id - execute + end - alert = project.alert_management_alerts.last + it 'sets the prometheus alert and environment' do + execute expect(alert.prometheus_alert).to eq(prometheus_alert) expect(alert.environment).to eq(prometheus_alert.environment) @@ -259,10 +86,7 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do context 'when alert payload is invalid' do let(:payload) { {} } - it 'responds with bad_request' do - expect(execute).to be_error - expect(execute.http_status).to eq(:bad_request) - end + it_behaves_like 'alerts service responds with an error and takes no actions', :bad_request end end end diff --git a/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb b/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb new file mode 100644 index 00000000000..24f0123ed3b --- /dev/null +++ b/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Analytics::CycleAnalytics::Stages::ListService do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:value_stream) { Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(project) } + let(:stages) { subject.payload[:stages] } + + subject { described_class.new(parent: project, current_user: user).execute } + + before_all do + project.add_reporter(user) + end + + it 'returns only the default stages' do + expect(stages.size).to eq(Gitlab::Analytics::CycleAnalytics::DefaultStages.all.size) + end + + it 'provides the default stages as non-persisted objects' do + expect(stages.map(&:id)).to all(be_nil) + end +end diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb index 258b3d25aee..56c1284927d 100644 --- a/spec/services/application_settings/update_service_spec.rb +++ b/spec/services/application_settings/update_service_spec.rb @@ -336,6 +336,32 @@ RSpec.describe ApplicationSettings::UpdateService do end end + context 'when package registry rate limits are passed' do + let(:params) do + { + throttle_unauthenticated_packages_api_enabled: 1, + throttle_unauthenticated_packages_api_period_in_seconds: 500, + throttle_unauthenticated_packages_api_requests_per_period: 20, + throttle_authenticated_packages_api_enabled: 1, + throttle_authenticated_packages_api_period_in_seconds: 600, + throttle_authenticated_packages_api_requests_per_period: 10 + } + end + + it 'updates package registry throttle settings' do + subject.execute + + application_settings.reload + + expect(application_settings.throttle_unauthenticated_packages_api_enabled).to be_truthy + expect(application_settings.throttle_unauthenticated_packages_api_period_in_seconds).to eq(500) + expect(application_settings.throttle_unauthenticated_packages_api_requests_per_period).to eq(20) + expect(application_settings.throttle_authenticated_packages_api_enabled).to be_truthy + expect(application_settings.throttle_authenticated_packages_api_period_in_seconds).to eq(600) + expect(application_settings.throttle_authenticated_packages_api_requests_per_period).to eq(10) + end + end + context 'when issues_create_limit is passed' do let(:params) do { diff --git a/spec/services/auto_merge/base_service_spec.rb b/spec/services/auto_merge/base_service_spec.rb index 1d33dc15838..3f535b83788 100644 --- a/spec/services/auto_merge/base_service_spec.rb +++ b/spec/services/auto_merge/base_service_spec.rb @@ -84,7 +84,7 @@ RSpec.describe AutoMerge::BaseService do context 'when failed to save merge request' do before do - allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid.new } + allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid } end it 'does not yield block' do @@ -195,7 +195,7 @@ RSpec.describe AutoMerge::BaseService do context 'when failed to save' do before do - allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid.new } + allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid } end it 'does not yield block' do @@ -213,7 +213,7 @@ RSpec.describe AutoMerge::BaseService do context 'when failed to save merge request' do before do - allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid.new } + allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid } end it 'returns error status' do @@ -260,7 +260,7 @@ RSpec.describe AutoMerge::BaseService do context 'when failed to save' do before do - allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid.new } + allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid } end it 'returns error status' do diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb index 4c512b96065..d5358bcc1e1 100644 --- a/spec/services/boards/lists/destroy_service_spec.rb +++ b/spec/services/boards/lists/destroy_service_spec.rb @@ -3,11 +3,20 @@ require 'spec_helper' RSpec.describe Boards::Lists::DestroyService do + let_it_be(:user) { create(:user) } + + let(:list_type) { :list } + describe '#execute' do context 'when board parent is a project' do - let(:project) { create(:project) } - let(:board) { create(:board, project: project) } - let(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:board) { create(:board, project: project) } + let_it_be(:list) { create(:list, board: board) } + let_it_be(:closed_list) { board.lists.closed.first } + + let(:params) do + { board: board } + end let(:parent) { project } @@ -15,9 +24,14 @@ RSpec.describe Boards::Lists::DestroyService do end context 'when board parent is a group' do - let(:group) { create(:group) } - let(:board) { create(:board, group: group) } - let(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:board) { create(:board, group: group) } + let_it_be(:list) { create(:list, board: board) } + let_it_be(:closed_list) { board.lists.closed.first } + + let(:params) do + { board: board } + end let(:parent) { group } diff --git a/spec/services/boards/lists/update_service_spec.rb b/spec/services/boards/lists/update_service_spec.rb index 10fed9b7aac..21216e1b945 100644 --- a/spec/services/boards/lists/update_service_spec.rb +++ b/spec/services/boards/lists/update_service_spec.rb @@ -3,8 +3,10 @@ require 'spec_helper' RSpec.describe Boards::Lists::UpdateService do - let(:user) { create(:user) } + let_it_be(:user) { create(:user) } + let!(:list) { create(:list, board: board, position: 0) } + let!(:list2) { create(:list, board: board, position: 1) } describe '#execute' do let(:service) { described_class.new(board.resource_parent, user, params) } diff --git a/spec/services/boards/visits/create_service_spec.rb b/spec/services/boards/visits/create_service_spec.rb index a9a8754825b..8910345d170 100644 --- a/spec/services/boards/visits/create_service_spec.rb +++ b/spec/services/boards/visits/create_service_spec.rb @@ -7,47 +7,20 @@ RSpec.describe Boards::Visits::CreateService do let(:user) { create(:user) } context 'when a project board' do - let(:project) { create(:project) } - let(:project_board) { create(:board, project: project) } + let_it_be(:project) { create(:project) } + let_it_be(:board) { create(:board, project: project) } - subject(:service) { described_class.new(project_board.resource_parent, user) } + let_it_be(:model) { BoardProjectRecentVisit } - it 'returns nil when there is no user' do - service.current_user = nil - - expect(service.execute(project_board)).to eq nil - end - - it 'returns nil when database is read only' do - allow(Gitlab::Database).to receive(:read_only?) { true } - - expect(service.execute(project_board)).to eq nil - end - - it 'records the visit' do - expect(BoardProjectRecentVisit).to receive(:visited!).once - - service.execute(project_board) - end + it_behaves_like 'boards recent visit create service' end context 'when a group board' do - let(:group) { create(:group) } - let(:group_board) { create(:board, group: group) } - - subject(:service) { described_class.new(group_board.resource_parent, user) } - - it 'returns nil when there is no user' do - service.current_user = nil - - expect(service.execute(group_board)).to eq nil - end - - it 'records the visit' do - expect(BoardGroupRecentVisit).to receive(:visited!).once + let_it_be(:group) { create(:group) } + let_it_be(:board) { create(:board, group: group) } + let_it_be(:model) { BoardGroupRecentVisit } - service.execute(group_board) - end + it_behaves_like 'boards recent visit create service' end end end diff --git a/spec/services/branches/delete_service_spec.rb b/spec/services/branches/delete_service_spec.rb index 291431c1723..727cadc5a50 100644 --- a/spec/services/branches/delete_service_spec.rb +++ b/spec/services/branches/delete_service_spec.rb @@ -41,7 +41,7 @@ RSpec.describe Branches::DeleteService do context 'when Gitlab::Git::CommandError is raised' do before do allow(repository).to receive(:rm_branch) do - raise Gitlab::Git::CommandError.new('Could not update patch') + raise Gitlab::Git::CommandError, 'Could not update patch' end end diff --git a/spec/services/bulk_create_integration_service_spec.rb b/spec/services/bulk_create_integration_service_spec.rb index 479309572a5..8369eb48088 100644 --- a/spec/services/bulk_create_integration_service_spec.rb +++ b/spec/services/bulk_create_integration_service_spec.rb @@ -59,7 +59,7 @@ RSpec.describe BulkCreateIntegrationService do context 'with a group association' do let!(:group) { create(:group) } - let(:created_integration) { Service.find_by(group: group) } + let(:created_integration) { Integration.find_by(group: group) } let(:batch) { Group.where(id: group.id) } let(:association) { 'group' } @@ -86,7 +86,7 @@ RSpec.describe BulkCreateIntegrationService do context 'with a group association' do let!(:subgroup) { create(:group, parent: group) } let(:integration) { create(:jira_service, group: group, project: nil, inherit_from_id: instance_integration.id) } - let(:created_integration) { Service.find_by(group: subgroup) } + let(:created_integration) { Integration.find_by(group: subgroup) } let(:batch) { Group.where(id: subgroup.id) } let(:association) { 'group' } let(:inherit_from_id) { instance_integration.id } diff --git a/spec/services/bulk_imports/export_service_spec.rb b/spec/services/bulk_imports/export_service_spec.rb new file mode 100644 index 00000000000..2414f7c5ca7 --- /dev/null +++ b/spec/services/bulk_imports/export_service_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::ExportService do + let_it_be(:group) { create(:group) } + let_it_be(:user) { create(:user) } + + before do + group.add_owner(user) + end + + subject { described_class.new(portable: group, user: user) } + + describe '#execute' do + it 'schedules RelationExportWorker for each top level relation' do + expect(subject).to receive(:execute).and_return(ServiceResponse.success).and_call_original + top_level_relations = BulkImports::FileTransfer.config_for(group).portable_relations + + top_level_relations.each do |relation| + expect(BulkImports::RelationExportWorker) + .to receive(:perform_async) + .with(user.id, group.id, group.class.name, relation) + end + + subject.execute + end + + context 'when exception occurs' do + it 'does not schedule RelationExportWorker' do + service = described_class.new(portable: nil, user: user) + + expect(service) + .to receive(:execute) + .and_return(ServiceResponse.error(message: 'Gitlab::ImportExport::Error', http_status: :unprocessible_entity)) + .and_call_original + expect(BulkImports::RelationExportWorker).not_to receive(:perform_async) + + service.execute + end + end + end +end diff --git a/spec/services/bulk_imports/relation_export_service_spec.rb b/spec/services/bulk_imports/relation_export_service_spec.rb new file mode 100644 index 00000000000..bf286998df2 --- /dev/null +++ b/spec/services/bulk_imports/relation_export_service_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::RelationExportService do + let_it_be(:jid) { 'jid' } + let_it_be(:relation) { 'labels' } + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:label) { create(:group_label, group: group) } + let_it_be(:export_path) { "#{Dir.tmpdir}/relation_export_service_spec/tree" } + let_it_be_with_reload(:export) { create(:bulk_import_export, group: group, relation: relation) } + + before do + group.add_owner(user) + + allow(export).to receive(:export_path).and_return(export_path) + end + + after :all do + FileUtils.rm_rf(export_path) + end + + subject { described_class.new(user, group, relation, jid) } + + describe '#execute' do + it 'exports specified relation and marks export as finished' do + subject.execute + + expect(export.reload.upload.export_file).to be_present + expect(export.finished?).to eq(true) + end + + it 'removes temp export files' do + subject.execute + + expect(Dir.exist?(export_path)).to eq(false) + end + + it 'exports specified relation and marks export as finished' do + subject.execute + + expect(export.upload.export_file).to be_present + end + + context 'when export record does not exist' do + let(:another_group) { create(:group) } + + subject { described_class.new(user, another_group, relation, jid) } + + it 'creates export record' do + another_group.add_owner(user) + + expect { subject.execute } + .to change { another_group.bulk_import_exports.count } + .from(0) + .to(1) + end + end + + context 'when there is existing export present' do + let(:upload) { create(:bulk_import_export_upload, export: export) } + + it 'removes existing export before exporting' do + upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/labels.ndjson.gz')) + + expect_any_instance_of(BulkImports::ExportUpload) do |upload| + expect(upload).to receive(:remove_export_file!) + end + + subject.execute + end + end + + context 'when exception occurs during export' do + shared_examples 'tracks exception' do |exception_class| + it 'tracks exception' do + expect(Gitlab::ErrorTracking) + .to receive(:track_exception) + .with(exception_class, portable_id: group.id, portable_type: group.class.name) + .and_call_original + + subject.execute + end + end + + before do + allow_next_instance_of(BulkImports::ExportUpload) do |upload| + allow(upload).to receive(:save!).and_raise(StandardError) + end + end + + it 'marks export as failed' do + subject.execute + + expect(export.reload.failed?).to eq(true) + end + + include_examples 'tracks exception', StandardError + + context 'when passed relation is not supported' do + let(:relation) { 'unsupported' } + + include_examples 'tracks exception', ActiveRecord::RecordInvalid + end + + context 'when user is not allowed to perform export' do + let(:another_user) { create(:user) } + + subject { described_class.new(another_user, group, relation, jid) } + + include_examples 'tracks exception', Gitlab::ImportExport::Error + end + end + end +end diff --git a/spec/services/bulk_update_integration_service_spec.rb b/spec/services/bulk_update_integration_service_spec.rb index e20bcd44923..cd50a2a5708 100644 --- a/spec/services/bulk_update_integration_service_spec.rb +++ b/spec/services/bulk_update_integration_service_spec.rb @@ -11,7 +11,7 @@ RSpec.describe BulkUpdateIntegrationService do let(:excluded_attributes) { %w[id project_id group_id inherit_from_id instance template created_at updated_at] } let(:batch) do - Service.inherited_descendants_from_self_or_ancestors_from(subgroup_integration).where(id: group_integration.id..integration.id) + Integration.inherited_descendants_from_self_or_ancestors_from(subgroup_integration).where(id: group_integration.id..integration.id) end let_it_be(:group) { create(:group) } diff --git a/spec/services/chat_names/find_user_service_spec.rb b/spec/services/chat_names/find_user_service_spec.rb index a29b243ad2c..9bbad09cd0d 100644 --- a/spec/services/chat_names/find_user_service_spec.rb +++ b/spec/services/chat_names/find_user_service_spec.rb @@ -4,13 +4,13 @@ require 'spec_helper' RSpec.describe ChatNames::FindUserService, :clean_gitlab_redis_shared_state do describe '#execute' do - let(:service) { create(:service) } + let(:integration) { create(:service) } - subject { described_class.new(service, params).execute } + subject { described_class.new(integration, params).execute } context 'find user mapping' do let(:user) { create(:user) } - let!(:chat_name) { create(:chat_name, user: user, service: service) } + let!(:chat_name) { create(:chat_name, user: user, integration: integration) } context 'when existing user is requested' do let(:params) { { team_id: chat_name.team_id, user_id: chat_name.chat_id } } @@ -28,7 +28,7 @@ RSpec.describe ChatNames::FindUserService, :clean_gitlab_redis_shared_state do end it 'only updates an existing timestamp once within a certain time frame' do - service = described_class.new(service, params) + service = described_class.new(integration, params) expect(chat_name.last_used_at).to be_nil diff --git a/spec/services/ci/change_variable_service_spec.rb b/spec/services/ci/change_variable_service_spec.rb index 7acdd4e834f..f86a87132b1 100644 --- a/spec/services/ci/change_variable_service_spec.rb +++ b/spec/services/ci/change_variable_service_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Ci::ChangeVariableService do let(:service) { described_class.new(container: group, current_user: user, params: params) } let_it_be(:user) { create(:user) } + let(:group) { create(:group) } describe '#execute' do diff --git a/spec/services/ci/change_variables_service_spec.rb b/spec/services/ci/change_variables_service_spec.rb index 5f1207eaf58..b710ca78554 100644 --- a/spec/services/ci/change_variables_service_spec.rb +++ b/spec/services/ci/change_variables_service_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Ci::ChangeVariablesService do let(:service) { described_class.new(container: group, current_user: user, params: params) } let_it_be(:user) { create(:user) } + let(:group) { spy(:group, variables: []) } let(:params) { { variables_attributes: [{ key: 'new_variable', value: 'variable_value' }] } } diff --git a/spec/services/ci/create_downstream_pipeline_service_spec.rb b/spec/services/ci/create_downstream_pipeline_service_spec.rb index dd10fb017aa..8bab7856375 100644 --- a/spec/services/ci/create_downstream_pipeline_service_spec.rb +++ b/spec/services/ci/create_downstream_pipeline_service_spec.rb @@ -3,9 +3,11 @@ require 'spec_helper' RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do + include Ci::SourcePipelineHelpers + let_it_be(:user) { create(:user) } let(:upstream_project) { create(:project, :repository) } - let_it_be(:downstream_project) { create(:project, :repository) } + let_it_be(:downstream_project, refind: true) { create(:project, :repository) } let!(:upstream_pipeline) do create(:ci_pipeline, :running, project: upstream_project) @@ -394,6 +396,47 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do end end + context 'when relationship between pipelines is cyclical' do + before do + pipeline_a = create(:ci_pipeline, project: upstream_project) + pipeline_b = create(:ci_pipeline, project: downstream_project) + pipeline_c = create(:ci_pipeline, project: upstream_project) + + create_source_pipeline(pipeline_a, pipeline_b) + create_source_pipeline(pipeline_b, pipeline_c) + create_source_pipeline(pipeline_c, upstream_pipeline) + end + + it 'does not create a new pipeline' do + expect { service.execute(bridge) } + .not_to change { Ci::Pipeline.count } + end + + it 'changes status of the bridge build' do + service.execute(bridge) + + expect(bridge.reload).to be_failed + expect(bridge.failure_reason).to eq 'pipeline_loop_detected' + end + + context 'when ci_drop_cyclical_triggered_pipelines is not enabled' do + before do + stub_feature_flags(ci_drop_cyclical_triggered_pipelines: false) + end + + it 'creates a new pipeline' do + expect { service.execute(bridge) } + .to change { Ci::Pipeline.count } + end + + it 'expect bridge build not to be failed' do + service.execute(bridge) + + expect(bridge.reload).not_to be_failed + end + end + end + context 'when downstream pipeline creation errors out' do let(:stub_config) { false } diff --git a/spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb b/spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb index d4e9946ac46..b3b8e34dd8e 100644 --- a/spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb +++ b/spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Ci::CreatePipelineService, '#execute' do let_it_be(:group) { create(:group, name: 'my-organization') } + let(:upstream_project) { create(:project, :repository, name: 'upstream', group: group) } let(:downstram_project) { create(:project, :repository, name: 'downstream', group: group) } let(:user) { create(:user) } diff --git a/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb b/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb index 6320a16d646..42c3f52541b 100644 --- a/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb +++ b/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Ci::CreatePipelineService do let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { project.owner } + let(:ref) { 'refs/heads/master' } let(:service) { described_class.new(project, user, { ref: ref }) } diff --git a/spec/services/ci/create_pipeline_service/dry_run_spec.rb b/spec/services/ci/create_pipeline_service/dry_run_spec.rb index c21a4ef0917..0fb500f5729 100644 --- a/spec/services/ci/create_pipeline_service/dry_run_spec.rb +++ b/spec/services/ci/create_pipeline_service/dry_run_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Ci::CreatePipelineService do let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { project.owner } + let(:ref) { 'refs/heads/master' } let(:service) { described_class.new(project, user, { ref: ref }) } diff --git a/spec/services/ci/create_pipeline_service/environment_spec.rb b/spec/services/ci/create_pipeline_service/environment_spec.rb index 0ed63012325..e77591298ad 100644 --- a/spec/services/ci/create_pipeline_service/environment_spec.rb +++ b/spec/services/ci/create_pipeline_service/environment_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Ci::CreatePipelineService do let_it_be(:project) { create(:project, :repository) } let_it_be(:developer) { create(:user) } + let(:service) { described_class.new(project, user, ref: 'master') } let(:user) { developer } diff --git a/spec/services/ci/create_pipeline_service/parameter_content_spec.rb b/spec/services/ci/create_pipeline_service/parameter_content_spec.rb index 90b8baa23a7..94500a550c6 100644 --- a/spec/services/ci/create_pipeline_service/parameter_content_spec.rb +++ b/spec/services/ci/create_pipeline_service/parameter_content_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Ci::CreatePipelineService do let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { project.owner } + let(:service) { described_class.new(project, user, { ref: 'refs/heads/master' }) } let(:content) do <<~EOY diff --git a/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb b/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb index 5ea75c2253b..512cf546e6a 100644 --- a/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb +++ b/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Ci::CreatePipelineService, '#execute' do let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } + let(:ref_name) { 'master' } let(:service) do diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 98c85234fe7..9fdce1ae926 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Ci::CreatePipelineService do let_it_be(:project, reload: true) { create(:project, :repository) } let_it_be(:user, reload: true) { project.owner } + let(:ref_name) { 'refs/heads/master' } before do @@ -101,14 +102,6 @@ RSpec.describe Ci::CreatePipelineService do execute_service end - describe 'recording a conversion event' do - it 'schedules a record conversion event worker' do - expect(Experiments::RecordConversionEventWorker).to receive(:perform_async).with(:ci_syntax_templates_b, user.id) - - pipeline - end - end - context 'when merge requests already exist for this source branch' do let(:merge_request_1) do create(:merge_request, source_branch: 'feature', target_branch: "master", source_project: project) @@ -539,7 +532,7 @@ RSpec.describe Ci::CreatePipelineService do it 'pull it from Auto-DevOps' do pipeline = execute_service expect(pipeline).to be_auto_devops_source - expect(pipeline.builds.map(&:name)).to match_array(%w[brakeman-sast build code_quality eslint-sast secret_detection_default_branch test]) + expect(pipeline.builds.map(&:name)).to match_array(%w[brakeman-sast build code_quality eslint-sast secret_detection_default_branch semgrep-sast test]) end end diff --git a/spec/services/ci/create_web_ide_terminal_service_spec.rb b/spec/services/ci/create_web_ide_terminal_service_spec.rb index c1acf8fd60c..0804773442d 100644 --- a/spec/services/ci/create_web_ide_terminal_service_spec.rb +++ b/spec/services/ci/create_web_ide_terminal_service_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Ci::CreateWebIdeTerminalService do let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } + let(:ref) { 'master' } describe '#execute' do @@ -20,6 +21,13 @@ RSpec.describe Ci::CreateWebIdeTerminalService do expect(subject[:pipeline].stages.count).to eq(1) expect(subject[:pipeline].builds.count).to eq(1) end + + it 'calls ensure_project_iid explicitly' do + expect_next_instance_of(Ci::Pipeline) do |instance| + expect(instance).to receive(:ensure_project_iid!).twice + end + subject + end end before do diff --git a/spec/services/ci/delete_unit_tests_service_spec.rb b/spec/services/ci/delete_unit_tests_service_spec.rb new file mode 100644 index 00000000000..4c63c513d48 --- /dev/null +++ b/spec/services/ci/delete_unit_tests_service_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::DeleteUnitTestsService do + describe '#execute' do + let!(:unit_test_1) { create(:ci_unit_test) } + let!(:unit_test_2) { create(:ci_unit_test) } + let!(:unit_test_3) { create(:ci_unit_test) } + let!(:unit_test_4) { create(:ci_unit_test) } + let!(:unit_test_1_recent_failure) { create(:ci_unit_test_failure, unit_test: unit_test_1) } + let!(:unit_test_1_old_failure) { create(:ci_unit_test_failure, unit_test: unit_test_1, failed_at: 15.days.ago) } + let!(:unit_test_2_old_failure) { create(:ci_unit_test_failure, unit_test: unit_test_2, failed_at: 15.days.ago) } + let!(:unit_test_3_old_failure) { create(:ci_unit_test_failure, unit_test: unit_test_3, failed_at: 15.days.ago) } + let!(:unit_test_4_old_failure) { create(:ci_unit_test_failure, unit_test: unit_test_4, failed_at: 15.days.ago) } + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 2) + + described_class.new.execute + end + + it 'does not delete unit test failures not older than 14 days' do + expect(unit_test_1_recent_failure.reload).to be_persisted + end + + it 'deletes unit test failures older than 14 days' do + ids = [ + unit_test_1_old_failure, + unit_test_2_old_failure, + unit_test_3_old_failure, + unit_test_4_old_failure + ].map(&:id) + + result = Ci::UnitTestFailure.where(id: ids) + + expect(result).to be_empty + end + + it 'deletes unit tests that have no more associated unit test failures' do + ids = [ + unit_test_2, + unit_test_3, + unit_test_4 + ].map(&:id) + + result = Ci::UnitTest.where(id: ids) + + expect(result).to be_empty + end + end +end diff --git a/spec/services/ci/destroy_pipeline_service_spec.rb b/spec/services/ci/destroy_pipeline_service_spec.rb index 6977c99e335..302233cea5a 100644 --- a/spec/services/ci/destroy_pipeline_service_spec.rb +++ b/spec/services/ci/destroy_pipeline_service_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' RSpec.describe ::Ci::DestroyPipelineService do - let(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } + let!(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.id) } subject { described_class.new(project, user).execute(pipeline) } @@ -17,13 +18,16 @@ RSpec.describe ::Ci::DestroyPipelineService do expect { pipeline.reload }.to raise_error(ActiveRecord::RecordNotFound) end - it 'clears the cache', :use_clean_rails_memory_store_caching do + it 'clears the cache', :use_clean_rails_redis_caching do create(:commit_status, :success, pipeline: pipeline, ref: pipeline.ref) expect(project.pipeline_status.has_status?).to be_truthy subject + # We need to reset lazy_latest_pipeline cache to simulate a new request + BatchLoader::Executor.clear_current + # Need to use find to avoid memoization expect(Project.find(project.id).pipeline_status.has_status?).to be_falsey end @@ -57,6 +61,10 @@ RSpec.describe ::Ci::DestroyPipelineService do expect { artifact.reload }.to raise_error(ActiveRecord::RecordNotFound) end + + it 'inserts deleted objects for object storage files' do + expect { subject }.to change { Ci::DeletedObject.count } + end end end end diff --git a/spec/services/ci/expire_pipeline_cache_service_spec.rb b/spec/services/ci/expire_pipeline_cache_service_spec.rb index 3dbf2dbb8f1..613bbe45e68 100644 --- a/spec/services/ci/expire_pipeline_cache_service_spec.rb +++ b/spec/services/ci/expire_pipeline_cache_service_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Ci::ExpirePipelineCacheService do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project) } let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + subject { described_class.new } describe '#execute' do @@ -14,12 +15,14 @@ RSpec.describe Ci::ExpirePipelineCacheService do new_mr_pipelines_path = "/#{project.full_path}/-/merge_requests/new.json" pipeline_path = "/#{project.full_path}/-/pipelines/#{pipeline.id}.json" graphql_pipeline_path = "/api/graphql:pipelines/id/#{pipeline.id}" + graphql_pipeline_sha_path = "/api/graphql:pipelines/sha/#{pipeline.sha}" expect_next_instance_of(Gitlab::EtagCaching::Store) do |store| expect(store).to receive(:touch).with(pipelines_path) expect(store).to receive(:touch).with(new_mr_pipelines_path) expect(store).to receive(:touch).with(pipeline_path) expect(store).to receive(:touch).with(graphql_pipeline_path) + expect(store).to receive(:touch).with(graphql_pipeline_sha_path) end subject.execute(pipeline) @@ -49,7 +52,7 @@ RSpec.describe Ci::ExpirePipelineCacheService do let(:project_with_repo) { create(:project, :repository) } let!(:pipeline_with_commit) { create(:ci_pipeline, :success, project: project_with_repo, sha: project_with_repo.commit.id) } - it 'clears the cache', :use_clean_rails_memory_store_caching do + it 'clears the cache', :use_clean_rails_redis_caching do create(:commit_status, :success, pipeline: pipeline_with_commit, ref: pipeline_with_commit.ref) # Sanity check @@ -59,6 +62,9 @@ RSpec.describe Ci::ExpirePipelineCacheService do pipeline_with_commit.destroy! + # We need to reset lazy_latest_pipeline cache to simulate a new request + BatchLoader::Executor.clear_current + # Need to use find to avoid memoization expect(Project.find(project_with_repo.id).pipeline_status.has_status?).to be_falsey end diff --git a/spec/services/ci/external_pull_requests/create_pipeline_service_spec.rb b/spec/services/ci/external_pull_requests/create_pipeline_service_spec.rb index 0cbeaa5446b..e25dd351bb3 100644 --- a/spec/services/ci/external_pull_requests/create_pipeline_service_spec.rb +++ b/spec/services/ci/external_pull_requests/create_pipeline_service_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Ci::ExternalPullRequests::CreatePipelineService do describe '#execute' do let_it_be(:project) { create(:project, :auto_devops, :repository) } let_it_be(:user) { create(:user) } + let(:pull_request) { create(:external_pull_request, project: project) } before do diff --git a/spec/services/ci/find_exposed_artifacts_service_spec.rb b/spec/services/ci/find_exposed_artifacts_service_spec.rb index 287f5c4b929..32d96471f16 100644 --- a/spec/services/ci/find_exposed_artifacts_service_spec.rb +++ b/spec/services/ci/find_exposed_artifacts_service_spec.rb @@ -15,6 +15,7 @@ RSpec.describe Ci::FindExposedArtifactsService do end let_it_be(:project) { create(:project) } + let(:user) { nil } after do diff --git a/spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb b/spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb index 5d747a09f2a..63bc7a1caf8 100644 --- a/spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb +++ b/spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Ci::GenerateCodequalityMrDiffReportService do subject { service.execute(base_pipeline, head_pipeline) } context 'when head pipeline has codequality mr diff report' do - let!(:merge_request) { create(:merge_request, :with_codequality_mr_diff_reports, source_project: project) } + let!(:merge_request) { create(:merge_request, :with_codequality_mr_diff_reports, source_project: project, id: 123456789) } let!(:service) { described_class.new(project, nil, id: merge_request.id) } let!(:head_pipeline) { merge_request.head_pipeline } let!(:base_pipeline) { nil } @@ -18,7 +18,7 @@ RSpec.describe Ci::GenerateCodequalityMrDiffReportService do it 'returns status and data', :aggregate_failures do expect_any_instance_of(Ci::PipelineArtifact) do |instance| expect(instance).to receive(:present) - expect(instance).to receive(:for_files).with(merge_request.new_paths).and_call_original + expect(instance).to receive(:for_files).with(merge_request).and_call_original end expect(subject[:status]).to eq(:parsed) diff --git a/spec/services/ci/job_artifacts/create_service_spec.rb b/spec/services/ci/job_artifacts/create_service_spec.rb index 22aa9e62c6f..97c65dc005e 100644 --- a/spec/services/ci/job_artifacts/create_service_spec.rb +++ b/spec/services/ci/job_artifacts/create_service_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Ci::JobArtifacts::CreateService do let_it_be(:project) { create(:project) } + let(:service) { described_class.new(job) } let(:job) { create(:ci_build, project: project) } let(:artifacts_sha256) { '0' * 64 } diff --git a/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb b/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb new file mode 100644 index 00000000000..b1a4741851b --- /dev/null +++ b/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::JobArtifacts::DestroyAssociationsService do + let(:artifacts) { Ci::JobArtifact.all } + let(:service) { described_class.new(artifacts) } + + let_it_be(:artifact, refind: true) do + create(:ci_job_artifact) + end + + before do + artifact.file = fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip') + artifact.save! + end + + describe '#destroy_records' do + it 'removes artifacts without updating statistics' do + expect(ProjectStatistics).not_to receive(:increment_statistic) + + expect { service.destroy_records }.to change { Ci::JobArtifact.count } + end + + context 'when there are no artifacts' do + let(:artifacts) { Ci::JobArtifact.none } + + it 'does not raise error' do + expect { service.destroy_records }.not_to raise_error + end + end + end + + describe '#update_statistics' do + before do + service.destroy_records + end + + it 'updates project statistics' do + expect(ProjectStatistics).to receive(:increment_statistic).once + .with(artifact.project, :build_artifacts_size, -artifact.file.size) + + service.update_statistics + end + + context 'when there are no artifacts' do + let(:artifacts) { Ci::JobArtifact.none } + + it 'does not raise error' do + expect { service.update_statistics }.not_to raise_error + end + end + end +end diff --git a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb index 52aaf73d67e..2cedbf93d74 100644 --- a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb +++ b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb @@ -3,8 +3,6 @@ require 'spec_helper' RSpec.describe Ci::JobArtifacts::DestroyBatchService do - include ExclusiveLeaseHelpers - let(:artifacts) { Ci::JobArtifact.all } let(:service) { described_class.new(artifacts, pick_up_at: Time.current) } @@ -25,14 +23,6 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do expect { subject }.to change { Ci::DeletedObject.count }.by(1) end - it 'resets project statistics' do - expect(ProjectStatistics).to receive(:increment_statistic).once - .with(artifact.project, :build_artifacts_size, -artifact.file.size) - .and_call_original - - execute - end - it 'does not remove the files' do expect { execute }.not_to change { artifact.file.exists? } end @@ -44,6 +34,29 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do execute end + + context 'ProjectStatistics' do + it 'resets project statistics' do + expect(ProjectStatistics).to receive(:increment_statistic).once + .with(artifact.project, :build_artifacts_size, -artifact.file.size) + .and_call_original + + execute + end + + context 'with update_stats: false' do + it 'does not update project statistics' do + expect(ProjectStatistics).not_to receive(:increment_statistic) + + service.execute(update_stats: false) + end + + it 'returns size statistics' do + expect(service.execute(update_stats: false)).to match( + a_hash_including(statistics_updates: { artifact.project => -artifact.file.size })) + end + end + end end context 'when failed to destroy artifact' do @@ -65,16 +78,12 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do context 'when there are no artifacts' do let(:artifacts) { Ci::JobArtifact.none } - before do - artifact.destroy! - end - it 'does not raise error' do expect { execute }.not_to raise_error end it 'reports the number of destroyed artifacts' do - is_expected.to eq(destroyed_artifacts_count: 0, status: :success) + is_expected.to eq(destroyed_artifacts_count: 0, statistics_updates: {}, status: :success) end end end diff --git a/spec/services/ci/parse_dotenv_artifact_service_spec.rb b/spec/services/ci/parse_dotenv_artifact_service_spec.rb index 91b81af9fd1..7536e04f2de 100644 --- a/spec/services/ci/parse_dotenv_artifact_service_spec.rb +++ b/spec/services/ci/parse_dotenv_artifact_service_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Ci::ParseDotenvArtifactService do let_it_be(:project) { create(:project) } let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline, project: project) } let(:service) { described_class.new(project, nil) } @@ -24,7 +25,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do context 'when parse error happens' do before do - allow(service).to receive(:scan_line!) { raise described_class::ParserError.new('Invalid Format') } + allow(service).to receive(:scan_line!) { raise described_class::ParserError, 'Invalid Format' } end it 'returns error' do diff --git a/spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb b/spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb index 0c48f15d726..5568052e346 100644 --- a/spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb +++ b/spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb @@ -4,58 +4,76 @@ require 'spec_helper' RSpec.describe ::Ci::PipelineArtifacts::CreateCodeQualityMrDiffReportService do describe '#execute' do - subject(:pipeline_artifact) { described_class.new.execute(pipeline) } + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.project } + let(:head_pipeline) { create(:ci_pipeline, :success, :with_codequality_reports, project: project, merge_requests_as_head_pipeline: [merge_request]) } + let(:base_pipeline) { create(:ci_pipeline, :success, project: project, ref: merge_request.target_branch, sha: merge_request.diff_base_sha) } - context 'when pipeline has codequality reports' do - let(:project) { create(:project, :repository) } + subject { described_class.new(head_pipeline).execute } - describe 'pipeline completed status' do - using RSpec::Parameterized::TableSyntax + context 'when there are codequality reports' do + context 'when pipeline passes' do + context 'when degradations are present' do + context 'when degradations already present in target branch pipeline' do + before do + create(:ci_build, :success, :codequality_reports, name: 'codequality', pipeline: base_pipeline, project: project) + end - where(:status, :result) do - :success | 1 - :failed | 1 - :canceled | 1 - :skipped | 1 - end + it "does not persist a pipeline artifact" do + expect { subject }.not_to change { Ci::PipelineArtifact.count } + end + end + + context 'when degradation is not present in target branch pipeline' do + before do + create(:ci_build, :success, :codequality_reports_without_degradation, name: 'codequality', pipeline: base_pipeline, project: project) + end - with_them do - let(:pipeline) { create(:ci_pipeline, :with_codequality_reports, status: status, project: project) } + it 'persists a pipeline artifact' do + expect { subject }.to change { Ci::PipelineArtifact.count }.by(1) + end - it 'creates a pipeline artifact' do - expect { pipeline_artifact }.to change(Ci::PipelineArtifact, :count).by(result) - end + it 'persists the default file name' do + subject - it 'persists the default file name' do - expect(pipeline_artifact.file.filename).to eq('code_quality_mr_diff.json') - end + pipeline_artifact = Ci::PipelineArtifact.first - it 'sets expire_at to 1 week' do - freeze_time do - expect(pipeline_artifact.expire_at).to eq(1.week.from_now) + expect(pipeline_artifact.file.filename).to eq('code_quality_mr_diff.json') end - end - end - end - context 'when pipeline artifact has already been created' do - let(:pipeline) { create(:ci_pipeline, :with_codequality_reports, project: project) } + it 'sets expire_at to 1 week' do + freeze_time do + subject + + pipeline_artifact = Ci::PipelineArtifact.first + + expect(pipeline_artifact.expire_at).to eq(1.week.from_now) + end + end - it 'does not persist the same artifact twice' do - 2.times { described_class.new.execute(pipeline) } + it 'does not persist the same artifact twice' do + 2.times { described_class.new(head_pipeline).execute } - expect(Ci::PipelineArtifact.count).to eq(1) + expect { subject }.not_to change { Ci::PipelineArtifact.count } + end + end end end end - context 'when pipeline is not completed and codequality report does not exist' do - let(:pipeline) { create(:ci_pipeline, :running) } + context 'when there are no codequality reports for head pipeline' do + let(:head_pipeline) { create(:ci_pipeline, :success, project: project, merge_requests_as_head_pipeline: [merge_request]) } + + it "does not persist a pipeline artifact" do + expect { subject }.not_to change { Ci::PipelineArtifact.count } + end + end - it 'does not persist data' do - pipeline_artifact + context 'when there are no codequality reports for base pipeline' do + let(:head_pipeline) { create(:ci_pipeline, :success, project: project, merge_requests_as_head_pipeline: [merge_request]) } - expect(Ci::PipelineArtifact.count).to eq(0) + it "does not persist a pipeline artifact" do + expect { subject }.not_to change { Ci::PipelineArtifact.count } end end end diff --git a/spec/services/ci/pipeline_artifacts/destroy_all_expired_service_spec.rb b/spec/services/ci/pipeline_artifacts/destroy_all_expired_service_spec.rb index 3dc4f35df22..eb664043567 100644 --- a/spec/services/ci/pipeline_artifacts/destroy_all_expired_service_spec.rb +++ b/spec/services/ci/pipeline_artifacts/destroy_all_expired_service_spec.rb @@ -30,7 +30,7 @@ RSpec.describe Ci::PipelineArtifacts::DestroyAllExpiredService do stub_const('::Ci::PipelineArtifacts::DestroyAllExpiredService::LOOP_LIMIT', 1) stub_const('::Ci::PipelineArtifacts::DestroyAllExpiredService::BATCH_SIZE', 1) - create_list(:ci_pipeline_artifact, 2, expire_at: 1.week.ago) + create_list(:ci_pipeline_artifact, 2, :unlocked, expire_at: 1.week.ago) end it 'destroys one artifact' do @@ -46,7 +46,7 @@ RSpec.describe Ci::PipelineArtifacts::DestroyAllExpiredService do before do stub_const('Ci::PipelineArtifacts::DestroyAllExpiredService::BATCH_SIZE', 1) - create_list(:ci_pipeline_artifact, 2, expire_at: 1.week.ago) + create_list(:ci_pipeline_artifact, 2, :unlocked, expire_at: 1.week.ago) end it 'destroys all expired artifacts' do @@ -60,7 +60,21 @@ RSpec.describe Ci::PipelineArtifacts::DestroyAllExpiredService do context 'when artifacts are not expired' do before do - create(:ci_pipeline_artifact, expire_at: 2.days.from_now) + create(:ci_pipeline_artifact, :unlocked, expire_at: 2.days.from_now) + end + + it 'does not destroy pipeline artifacts' do + expect { subject }.not_to change { Ci::PipelineArtifact.count } + end + + it 'reports the number of destroyed artifacts' do + is_expected.to eq(0) + end + end + + context 'when pipeline is locked' do + before do + create(:ci_pipeline_artifact, expire_at: 2.weeks.ago) end it 'does not destroy pipeline artifacts' do diff --git a/spec/services/ci/pipeline_bridge_status_service_spec.rb b/spec/services/ci/pipeline_bridge_status_service_spec.rb index 584b23bb3aa..1346f68c952 100644 --- a/spec/services/ci/pipeline_bridge_status_service_spec.rb +++ b/spec/services/ci/pipeline_bridge_status_service_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Ci::PipelineBridgeStatusService do let(:user) { build(:user) } let_it_be(:project) { create(:project) } + let(:pipeline) { build(:ci_pipeline, project: project) } describe '#execute' do diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb index bc8b6b2d113..a66d3898c5c 100644 --- a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb +++ b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true require 'spec_helper' -require_relative 'shared_processing_service.rb' -require_relative 'shared_processing_service_tests_with_yaml.rb' +require_relative 'shared_processing_service' +require_relative 'shared_processing_service_tests_with_yaml' RSpec.describe Ci::PipelineProcessing::AtomicProcessingService do it_behaves_like 'Pipeline Processing Service' diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_post_test_needs_deploy_is_stage.yml b/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_post_test_needs_deploy_is_stage.yml new file mode 100644 index 00000000000..03d5781395d --- /dev/null +++ b/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_post_test_needs_deploy_is_stage.yml @@ -0,0 +1,50 @@ +config: + stages: [build, test, post_test, deploy] + + build: + stage: build + script: exit 0 + + test: + stage: test + script: exit 0 + when: manual + + post_test: + stage: post_test + script: exit 0 + needs: [test] + + deploy: + stage: deploy + script: exit 0 + +init: + expect: + pipeline: pending + stages: + build: pending + test: created + post_test: created + deploy: created + jobs: + build: pending + test: created + post_test: created + deploy: created + +transitions: + - event: success + jobs: [build] + expect: + pipeline: running + stages: + build: success + test: skipped + post_test: skipped + deploy: pending + jobs: + build: success + test: manual + post_test: skipped + deploy: pending diff --git a/spec/services/ci/pipeline_trigger_service_spec.rb b/spec/services/ci/pipeline_trigger_service_spec.rb index 36055779a2e..080ca1cf0cd 100644 --- a/spec/services/ci/pipeline_trigger_service_spec.rb +++ b/spec/services/ci/pipeline_trigger_service_spec.rb @@ -13,12 +13,35 @@ RSpec.describe Ci::PipelineTriggerService do describe '#execute' do let_it_be(:user) { create(:user) } + let(:result) { described_class.new(project, user, params).execute } before do project.add_developer(user) end + shared_examples 'detecting an unprocessable pipeline trigger' do + context 'when the pipeline was not created successfully' do + let(:fail_pipeline) do + receive(:execute).and_wrap_original do |original, *args| + pipeline = original.call(*args) + pipeline.update!(failure_reason: 'unknown_failure') + pipeline + end + end + + before do + allow_next(Ci::CreatePipelineService).to fail_pipeline + end + + it 'has the correct status code' do + expect { result }.to change { Ci::Pipeline.count } + expect(result).to be_error + expect(result.http_status).to eq(:unprocessable_entity) + end + end + end + context 'with a trigger token' do let(:trigger) { create(:ci_trigger, project: project, owner: user) } @@ -62,7 +85,7 @@ RSpec.describe Ci::PipelineTriggerService do it 'ignores [ci skip] and create as general' do expect { result }.to change { Ci::Pipeline.count }.by(1) - expect(result[:status]).to eq(:success) + expect(result).to be_success end end @@ -77,19 +100,22 @@ RSpec.describe Ci::PipelineTriggerService do expect(result[:pipeline].trigger_requests.last.variables).to be_nil end end + + it_behaves_like 'detecting an unprocessable pipeline trigger' end - context 'when params have a non-existsed ref' do + context 'when params have a non-existant ref' do let(:params) { { token: trigger.token, ref: 'invalid-ref', variables: nil } } it 'does not trigger a pipeline' do expect { result }.not_to change { Ci::Pipeline.count } - expect(result[:http_status]).to eq(400) + expect(result).to be_error + expect(result.http_status).to eq(:bad_request) end end end - context 'when params have a non-existsed trigger token' do + context 'when params have a non-existant trigger token' do let(:params) { { token: 'invalid-token', ref: nil, variables: nil } } it 'does not trigger a pipeline' do @@ -172,14 +198,17 @@ RSpec.describe Ci::PipelineTriggerService do expect(job.sourced_pipelines.last.pipeline_id).to eq(result[:pipeline].id) end end + + it_behaves_like 'detecting an unprocessable pipeline trigger' end - context 'when params have a non-existsed ref' do + context 'when params have a non-existant ref' do let(:params) { { token: job.token, ref: 'invalid-ref', variables: nil } } - it 'does not job a pipeline' do + it 'does not trigger a job in the pipeline' do expect { result }.not_to change { Ci::Pipeline.count } - expect(result[:http_status]).to eq(400) + expect(result).to be_error + expect(result.http_status).to eq(:bad_request) end end end diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index 254bd19c808..b5bf0adadaf 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -3,8 +3,7 @@ require 'spec_helper' RSpec.describe Ci::ProcessPipelineService do - let(:user) { create(:user) } - let(:project) { create(:project) } + let_it_be(:project) { create(:project) } let(:pipeline) do create(:ci_empty_pipeline, ref: 'master', project: project) @@ -24,8 +23,6 @@ RSpec.describe Ci::ProcessPipelineService do stub_ci_pipeline_to_return_yaml_file stub_not_protect_default_branch - project.add_developer(user) - allow(subject).to receive(:metrics).and_return(metrics) end @@ -69,6 +66,14 @@ RSpec.describe Ci::ProcessPipelineService do subject.execute end + it 'logs the project and pipeline id' do + expect(Gitlab::AppJsonLogger).to receive(:info).with(event: 'update_retried_is_used', + project_id: project.id, + pipeline_id: pipeline.id) + + subject.execute + end + context 'when the previous build has already retried column true' do before do build_retried.update_columns(retried: true) diff --git a/spec/services/ci/prometheus_metrics/observe_histograms_service_spec.rb b/spec/services/ci/prometheus_metrics/observe_histograms_service_spec.rb index 2eef852b0f4..0b100af5902 100644 --- a/spec/services/ci/prometheus_metrics/observe_histograms_service_spec.rb +++ b/spec/services/ci/prometheus_metrics/observe_histograms_service_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Ci::PrometheusMetrics::ObserveHistogramsService do let_it_be(:project) { create(:project) } + let(:params) { {} } subject(:execute) { described_class.new(project, params).execute } @@ -54,32 +55,6 @@ RSpec.describe Ci::PrometheusMetrics::ObserveHistogramsService do end end - context 'with feature flag disabled' do - before do - stub_feature_flags(ci_accept_frontend_prometheus_metrics: false) - end - - let(:params) do - { - histograms: [ - { name: 'pipeline_graph_link_calculation_duration_seconds', value: '4' } - ] - } - end - - it 'does not register the metrics' do - execute - - expect(histogram_data).to be_nil - end - - it 'returns an empty body and status code' do - is_expected.to be_success - expect(subject.http_status).to eq(:accepted) - expect(subject.payload).to eq({}) - end - end - def histogram_data(name = :pipeline_graph_link_calculation_duration_seconds) Gitlab::Metrics.registry.get(name)&.get({}) end diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb index 02b48e8ba06..839a3c53f07 100644 --- a/spec/services/ci/register_job_service_spec.rb +++ b/spec/services/ci/register_job_service_spec.rb @@ -7,6 +7,7 @@ module Ci let_it_be(:group) { create(:group) } let_it_be(:project, reload: true) { create(:project, group: group, shared_runners_enabled: false, group_runners_enabled: false) } let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let!(:shared_runner) { create(:ci_runner, :instance) } let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) } let!(:group_runner) { create(:ci_runner, :group, groups: [group]) } @@ -81,31 +82,69 @@ module Ci let!(:build2_project2) { FactoryBot.create :ci_build, pipeline: pipeline2 } let!(:build1_project3) { FactoryBot.create :ci_build, pipeline: pipeline3 } - 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) + 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 + end - # in the end the third build - expect(execute(shared_runner)).to eq(build3_project1) + 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) + + 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 + end end - 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) + context 'when using DEFCON mode that disables fair scheduling' do + before do + stub_feature_flags(ci_queueing_disaster_recovery: 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) + end + end - 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) + 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) + + 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) + end + end end end @@ -477,10 +516,6 @@ module Ci end end - before do - stub_feature_flags(ci_validate_build_dependencies_override: false) - end - let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) } let!(:pending_job) do @@ -491,37 +526,7 @@ module Ci subject { execute(specific_runner) } - context 'when validates for dependencies is enabled' do - before do - stub_feature_flags(ci_validate_build_dependencies_override: false) - end - - it_behaves_like 'validation is active' - - context 'when the main feature flag is enabled for a specific project' do - before do - stub_feature_flags(ci_validate_build_dependencies: pipeline.project) - end - - it_behaves_like 'validation is active' - end - - context 'when the main feature flag is enabled for a different project' do - before do - stub_feature_flags(ci_validate_build_dependencies: create(:project)) - end - - it_behaves_like 'validation is not active' - end - end - - context 'when validates for dependencies is disabled' do - before do - stub_feature_flags(ci_validate_build_dependencies_override: true) - end - - it_behaves_like 'validation is not active' - end + it_behaves_like 'validation is active' end context 'when build is degenerated' do diff --git a/spec/services/ci/resource_groups/assign_resource_from_resource_group_service_spec.rb b/spec/services/ci/resource_groups/assign_resource_from_resource_group_service_spec.rb index 6c69a7f3b11..a741e3b49e7 100644 --- a/spec/services/ci/resource_groups/assign_resource_from_resource_group_service_spec.rb +++ b/spec/services/ci/resource_groups/assign_resource_from_resource_group_service_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Ci::ResourceGroups::AssignResourceFromResourceGroupService do let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user) } + let(:service) { described_class.new(project, user) } describe '#execute' do diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index 7dd3d963e56..86bda868625 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -18,6 +18,7 @@ RSpec.describe Ci::RetryBuildService do end let_it_be_with_refind(:build) { create(:ci_build, pipeline: pipeline, stage_id: stage.id) } + let(:user) { developer } let(:service) do diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb index 5a0b7f23556..d5ef67c871c 100644 --- a/spec/services/ci/stop_environments_service_spec.rb +++ b/spec/services/ci/stop_environments_service_spec.rb @@ -188,6 +188,7 @@ RSpec.describe Ci::StopEnvironmentsService do let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } + let(:environments) { Environment.available } before_all do diff --git a/spec/services/clusters/applications/prometheus_update_service_spec.rb b/spec/services/clusters/applications/prometheus_update_service_spec.rb index 076ff0210c9..615bfc44045 100644 --- a/spec/services/clusters/applications/prometheus_update_service_spec.rb +++ b/spec/services/clusters/applications/prometheus_update_service_spec.rb @@ -9,83 +9,102 @@ RSpec.describe Clusters::Applications::PrometheusUpdateService do let(:cluster) { create(:cluster, :provided_by_user, :with_installed_helm, projects: [project]) } let(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) } let(:empty_alerts_values_update_yaml) { "---\nalertmanager:\n enabled: false\nserverFiles:\n alerts: {}\n" } - let!(:patch_command) { application.patch_command(empty_alerts_values_update_yaml) } let(:helm_client) { instance_double(::Gitlab::Kubernetes::Helm::API) } subject(:service) { described_class.new(application, project) } - before do - allow(service).to receive(:patch_command).with(empty_alerts_values_update_yaml).and_return(patch_command) - allow(service).to receive(:helm_api).and_return(helm_client) + context 'when prometheus is a Clusters::Integrations::Prometheus' do + let(:application) { create(:clusters_integrations_prometheus, cluster: cluster) } + + it 'raises NotImplementedError' do + expect { service.execute }.to raise_error(NotImplementedError) + end end - context 'when there are no errors' do - before do - expect(helm_client).to receive(:update).with(patch_command) + context 'when prometheus is externally installed' do + let(:application) { create(:clusters_applications_prometheus, :externally_installed, cluster: cluster) } - allow(::ClusterWaitForAppUpdateWorker) - .to receive(:perform_in) - .and_return(nil) + it 'raises NotImplementedError' do + expect { service.execute }.to raise_error(NotImplementedError) end + end - it 'make the application updating' do - expect(application.cluster).not_to be_nil - - service.execute + context 'when prometheus is a Clusters::Applications::Prometheus' do + let!(:patch_command) { application.patch_command(empty_alerts_values_update_yaml) } - expect(application).to be_updating + before do + allow(service).to receive(:patch_command).with(empty_alerts_values_update_yaml).and_return(patch_command) + allow(service).to receive(:helm_api).and_return(helm_client) end - it 'updates current config' do - prometheus_config_service = spy(:prometheus_config_service) + context 'when there are no errors' do + before do + expect(helm_client).to receive(:update).with(patch_command) - expect(Clusters::Applications::PrometheusConfigService) - .to receive(:new) - .with(project, cluster, application) - .and_return(prometheus_config_service) + allow(::ClusterWaitForAppUpdateWorker) + .to receive(:perform_in) + .and_return(nil) + end - expect(prometheus_config_service) - .to receive(:execute) - .and_return(YAML.safe_load(empty_alerts_values_update_yaml)) + it 'make the application updating' do + expect(application.cluster).not_to be_nil - service.execute - end + service.execute - it 'schedules async update status check' do - expect(::ClusterWaitForAppUpdateWorker).to receive(:perform_in).once + expect(application).to be_updating + end - service.execute - end - end + it 'updates current config' do + prometheus_config_service = spy(:prometheus_config_service) - context 'when k8s cluster communication fails' do - before do - error = ::Kubeclient::HttpError.new(500, 'system failure', nil) - allow(helm_client).to receive(:update).and_raise(error) - end + expect(Clusters::Applications::PrometheusConfigService) + .to receive(:new) + .with(project, cluster, application) + .and_return(prometheus_config_service) + + expect(prometheus_config_service) + .to receive(:execute) + .and_return(YAML.safe_load(empty_alerts_values_update_yaml)) - it 'make the application update errored' do - service.execute + service.execute + end - expect(application).to be_update_errored - expect(application.status_reason).to match(/kubernetes error:/i) + it 'schedules async update status check' do + expect(::ClusterWaitForAppUpdateWorker).to receive(:perform_in).once + + service.execute + end end - end - context 'when application cannot be persisted' do - let(:application) { build(:clusters_applications_prometheus, :installed) } + context 'when k8s cluster communication fails' do + before do + error = ::Kubeclient::HttpError.new(500, 'system failure', nil) + allow(helm_client).to receive(:update).and_raise(error) + end - before do - allow(application).to receive(:make_updating!).once - .and_raise(ActiveRecord::RecordInvalid.new(application)) + it 'make the application update errored' do + service.execute + + expect(application).to be_update_errored + expect(application.status_reason).to match(/kubernetes error:/i) + end end - it 'make the application update errored' do - expect(helm_client).not_to receive(:update) + context 'when application cannot be persisted' do + let(:application) { build(:clusters_applications_prometheus, :installed) } + + before do + allow(application).to receive(:make_updating!).once + .and_raise(ActiveRecord::RecordInvalid.new(application)) + end + + it 'make the application update errored' do + expect(helm_client).not_to receive(:update) - service.execute + service.execute - expect(application).to be_update_errored + expect(application).to be_update_errored + end end end end diff --git a/spec/services/clusters/applications/schedule_update_service_spec.rb b/spec/services/clusters/applications/schedule_update_service_spec.rb index 01a75a334e6..2cbcb861938 100644 --- a/spec/services/clusters/applications/schedule_update_service_spec.rb +++ b/spec/services/clusters/applications/schedule_update_service_spec.rb @@ -10,6 +10,32 @@ RSpec.describe Clusters::Applications::ScheduleUpdateService do freeze_time { example.run } end + context 'when the application is a Clusters::Integrations::Prometheus' do + let(:application) { create(:clusters_integrations_prometheus) } + + it 'does nothing' do + service = described_class.new(application, project) + + expect(::ClusterUpdateAppWorker).not_to receive(:perform_in) + expect(::ClusterUpdateAppWorker).not_to receive(:perform_async) + + service.execute + end + end + + context 'when the application is externally installed' do + let(:application) { create(:clusters_applications_prometheus, :externally_installed) } + + it 'does nothing' do + service = described_class.new(application, project) + + expect(::ClusterUpdateAppWorker).not_to receive(:perform_in) + expect(::ClusterUpdateAppWorker).not_to receive(:perform_async) + + service.execute + end + end + context 'when application is able to be updated' do context 'when the application was recently scheduled' do it 'schedules worker with a backoff delay' do diff --git a/spec/services/clusters/integrations/create_service_spec.rb b/spec/services/clusters/integrations/create_service_spec.rb index cfc0943b6ad..14653236ab1 100644 --- a/spec/services/clusters/integrations/create_service_spec.rb +++ b/spec/services/clusters/integrations/create_service_spec.rb @@ -6,79 +6,64 @@ RSpec.describe Clusters::Integrations::CreateService, '#execute' do let_it_be(:project) { create(:project) } let_it_be_with_reload(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } - let(:params) do - { application_type: 'prometheus', enabled: true } - end - let(:service) do described_class.new(container: project, cluster: cluster, current_user: project.owner, params: params) end - it 'creates a new Prometheus instance' do - expect(service.execute).to be_success - - expect(cluster.integration_prometheus).to be_present - expect(cluster.integration_prometheus).to be_persisted - expect(cluster.integration_prometheus).to be_enabled - end - - context 'enabled param is false' do - let(:params) do - { application_type: 'prometheus', enabled: false } - end - - it 'creates a new uninstalled Prometheus instance' do - expect(service.execute).to be_success + shared_examples_for 'a cluster integration' do |application_type| + let(:integration) { cluster.public_send("integration_#{application_type}") } - expect(cluster.integration_prometheus).to be_present - expect(cluster.integration_prometheus).to be_persisted - expect(cluster.integration_prometheus).not_to be_enabled - end - end + context 'when enabled param is true' do + let(:params) do + { application_type: application_type, enabled: true } + end - context 'unauthorized user' do - let(:service) do - unauthorized_user = create(:user) + it 'creates a new enabled integration' do + expect(service.execute).to be_success - described_class.new(container: project, cluster: cluster, current_user: unauthorized_user, params: params) + expect(integration).to be_present + expect(integration).to be_persisted + expect(integration).to be_enabled + end end - it 'does not create a new Prometheus instance' do - expect(service.execute).to be_error + context 'when enabled param is false' do + let(:params) do + { application_type: application_type, enabled: false } + end - expect(cluster.integration_prometheus).to be_nil - end - end + it 'creates a new disabled integration' do + expect(service.execute).to be_success - context 'prometheus record exists' do - before do - create(:clusters_integrations_prometheus, cluster: cluster) + expect(integration).to be_present + expect(integration).to be_persisted + expect(integration).not_to be_enabled + end end - it 'updates the Prometheus instance' do - expect(service.execute).to be_success - - expect(cluster.integration_prometheus).to be_present - expect(cluster.integration_prometheus).to be_persisted - expect(cluster.integration_prometheus).to be_enabled - end + context 'when integration already exists' do + before do + create(:"clusters_integrations_#{application_type}", cluster: cluster, enabled: false) + end - context 'enabled param is false' do let(:params) do - { application_type: 'prometheus', enabled: false } + { application_type: application_type, enabled: true } end - it 'updates the Prometheus instance as uninstalled' do + it 'updates the integration' do + expect(integration).not_to be_enabled + expect(service.execute).to be_success - expect(cluster.integration_prometheus).to be_present - expect(cluster.integration_prometheus).to be_persisted - expect(cluster.integration_prometheus).not_to be_enabled + expect(integration.reload).to be_enabled end end end - context 'for an un-supported application type' do + it_behaves_like 'a cluster integration', 'prometheus' + it_behaves_like 'a cluster integration', 'elastic_stack' + + context 'when application_type is invalid' do let(:params) do { application_type: 'something_else', enabled: true } end @@ -87,4 +72,22 @@ RSpec.describe Clusters::Integrations::CreateService, '#execute' do expect { service.execute}.to raise_error(ArgumentError) end end + + context 'when user is unauthorized' do + let(:params) do + { application_type: 'prometheus', enabled: true } + end + + let(:service) do + unauthorized_user = create(:user) + + described_class.new(container: project, cluster: cluster, current_user: unauthorized_user, params: params) + end + + it 'returns error and does not create a new integration record' do + expect(service.execute).to be_error + + expect(cluster.integration_prometheus).to be_nil + end + end end diff --git a/spec/services/clusters/management/create_project_service_spec.rb b/spec/services/clusters/management/create_project_service_spec.rb deleted file mode 100644 index 5d8cc71faa4..00000000000 --- a/spec/services/clusters/management/create_project_service_spec.rb +++ /dev/null @@ -1,126 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Clusters::Management::CreateProjectService do - let(:cluster) { create(:cluster, :project) } - let(:current_user) { create(:user) } - - subject { described_class.new(cluster, current_user: current_user).execute } - - shared_examples 'management project is not required' do - it 'does not create a project' do - expect { subject }.not_to change(cluster, :management_project) - end - end - - context ':auto_create_cluster_management_project feature flag is disabled' do - before do - stub_feature_flags(auto_create_cluster_management_project: false) - end - - include_examples 'management project is not required' - end - - context 'cluster already has a management project' do - let(:cluster) { create(:cluster, :management_project) } - - include_examples 'management project is not required' - end - - shared_examples 'creates a management project' do - let(:project_params) do - { - name: "#{cluster.name} Cluster Management", - description: 'This project is automatically generated and will be used to manage your Kubernetes cluster. [More information](/help/user/clusters/management_project)', - namespace_id: namespace&.id, - visibility_level: Gitlab::VisibilityLevel::PRIVATE - } - end - - it 'creates a management project' do - expect(Projects::CreateService).to receive(:new) - .with(current_user, project_params) - .and_call_original - - subject - - management_project = cluster.management_project - - expect(management_project).to be_present - expect(management_project).to be_private - expect(management_project.name).to eq "#{cluster.name} Cluster Management" - expect(management_project.namespace).to eq namespace - end - end - - context 'project cluster' do - let(:cluster) { create(:cluster, projects: [project]) } - let(:project) { create(:project, namespace: current_user.namespace) } - let(:namespace) { project.namespace } - - include_examples 'creates a management project' - end - - context 'group cluster' do - let(:cluster) { create(:cluster, :group, user: current_user) } - let(:namespace) { cluster.group } - - before do - namespace.add_user(current_user, Gitlab::Access::MAINTAINER) - end - - include_examples 'creates a management project' - end - - context 'instance cluster' do - let(:cluster) { create(:cluster, :instance, user: current_user) } - let(:namespace) { create(:group) } - - before do - stub_application_setting(instance_administrators_group: namespace) - - namespace.add_user(current_user, Gitlab::Access::MAINTAINER) - end - - include_examples 'creates a management project' - end - - describe 'error handling' do - let(:project) { cluster.project } - - before do - allow(Projects::CreateService).to receive(:new) - .and_return(double(execute: project)) - end - - context 'project is invalid' do - let(:errors) { double(full_messages: ["Error message"]) } - let(:project) { instance_double(Project, errors: errors) } - - it { expect { subject }.to raise_error(described_class::CreateError, /Failed to create project/) } - end - - context 'instance administrators group is missing' do - let(:cluster) { create(:cluster, :instance) } - - it { expect { subject }.to raise_error(described_class::CreateError, /Instance administrators group not found/) } - end - - context 'cluster is invalid' do - before do - allow(cluster).to receive(:update).and_return(false) - end - - it { expect { subject }.to raise_error(described_class::CreateError, /Failed to update cluster/) } - end - - context 'unknown cluster type' do - before do - allow(cluster).to receive(:cluster_type).and_return("unknown_type") - end - - it { expect { subject }.to raise_error(NotImplementedError) } - end - end -end diff --git a/spec/services/container_expiration_policies/cleanup_service_spec.rb b/spec/services/container_expiration_policies/cleanup_service_spec.rb index 746e3464427..c6faae7449d 100644 --- a/spec/services/container_expiration_policies/cleanup_service_spec.rb +++ b/spec/services/container_expiration_policies/cleanup_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe ContainerExpirationPolicies::CleanupService do - let_it_be(:repository, reload: true) { create(:container_repository) } + let_it_be(:repository, reload: true) { create(:container_repository, expiration_policy_started_at: 30.minutes.ago) } let_it_be(:project) { repository.project } let(:service) { described_class.new(repository) } @@ -11,59 +11,35 @@ RSpec.describe ContainerExpirationPolicies::CleanupService do describe '#execute' do subject { service.execute } - context 'with a successful cleanup tags service execution' do - let(:cleanup_tags_service_params) { project.container_expiration_policy.policy_params.merge('container_expiration_policy' => true) } - let(:cleanup_tags_service) { instance_double(Projects::ContainerRepository::CleanupTagsService) } + shared_examples 'cleaning up a container repository' do + context 'with a successful cleanup tags service execution' do + let(:cleanup_tags_service_params) { project.container_expiration_policy.policy_params.merge('container_expiration_policy' => true) } + let(:cleanup_tags_service) { instance_double(Projects::ContainerRepository::CleanupTagsService) } - it 'completely clean up the repository' do - expect(Projects::ContainerRepository::CleanupTagsService) - .to receive(:new).with(project, nil, cleanup_tags_service_params).and_return(cleanup_tags_service) - expect(cleanup_tags_service).to receive(:execute).with(repository).and_return(status: :success) + it 'completely clean up the repository' do + expect(Projects::ContainerRepository::CleanupTagsService) + .to receive(:new).with(project, nil, cleanup_tags_service_params).and_return(cleanup_tags_service) + expect(cleanup_tags_service).to receive(:execute).with(repository).and_return(status: :success) - response = subject + response = subject - aggregate_failures "checking the response and container repositories" do - expect(response.success?).to eq(true) - expect(response.payload).to include(cleanup_status: :finished, container_repository_id: repository.id) - expect(ContainerRepository.waiting_for_cleanup.count).to eq(0) - expect(repository.reload.cleanup_unscheduled?).to be_truthy - expect(repository.expiration_policy_started_at).to eq(nil) - expect(repository.expiration_policy_completed_at).not_to eq(nil) + aggregate_failures "checking the response and container repositories" do + expect(response.success?).to eq(true) + expect(response.payload).to include(cleanup_status: :finished, container_repository_id: repository.id) + expect(ContainerRepository.waiting_for_cleanup.count).to eq(0) + expect(repository.reload.cleanup_unscheduled?).to be_truthy + expect(repository.expiration_policy_completed_at).not_to eq(nil) + expect(repository.expiration_policy_started_at).not_to eq(nil) + end end end - end - - context 'without a successful cleanup tags service execution' do - let(:cleanup_tags_service_response) { { status: :error, message: 'timeout' } } - - before do - expect(Projects::ContainerRepository::CleanupTagsService) - .to receive(:new).and_return(double(execute: cleanup_tags_service_response)) - end - it 'partially clean up the repository' do - response = subject + context 'without a successful cleanup tags service execution' do + let(:cleanup_tags_service_response) { { status: :error, message: 'timeout' } } - aggregate_failures "checking the response and container repositories" do - expect(response.success?).to eq(true) - expect(response.payload).to include(cleanup_status: :unfinished, container_repository_id: repository.id) - expect(ContainerRepository.waiting_for_cleanup.count).to eq(1) - expect(repository.reload.cleanup_unfinished?).to be_truthy - expect(repository.expiration_policy_started_at).not_to eq(nil) - expect(repository.expiration_policy_completed_at).to eq(nil) - end - end - - context 'with a truncated cleanup tags service response' do - let(:cleanup_tags_service_response) do - { - status: :error, - original_size: 1000, - before_truncate_size: 800, - after_truncate_size: 200, - before_delete_size: 100, - deleted_size: 100 - } + before do + expect(Projects::ContainerRepository::CleanupTagsService) + .to receive(:new).and_return(double(execute: cleanup_tags_service_response)) end it 'partially clean up the repository' do @@ -71,49 +47,179 @@ RSpec.describe ContainerExpirationPolicies::CleanupService do aggregate_failures "checking the response and container repositories" do expect(response.success?).to eq(true) - expect(response.payload) - .to include( - cleanup_status: :unfinished, - container_repository_id: repository.id, - cleanup_tags_service_original_size: 1000, - cleanup_tags_service_before_truncate_size: 800, - cleanup_tags_service_after_truncate_size: 200, - cleanup_tags_service_before_delete_size: 100, - cleanup_tags_service_deleted_size: 100 - ) + expect(response.payload).to include(cleanup_status: :unfinished, container_repository_id: repository.id) expect(ContainerRepository.waiting_for_cleanup.count).to eq(1) expect(repository.reload.cleanup_unfinished?).to be_truthy expect(repository.expiration_policy_started_at).not_to eq(nil) expect(repository.expiration_policy_completed_at).to eq(nil) end end + + context 'with a truncated cleanup tags service response' do + let(:cleanup_tags_service_response) do + { + status: :error, + original_size: 1000, + before_truncate_size: 800, + after_truncate_size: 200, + before_delete_size: 100, + deleted_size: 100 + } + end + + it 'partially clean up the repository' do + response = subject + + aggregate_failures "checking the response and container repositories" do + expect(response.success?).to eq(true) + expect(response.payload) + .to include( + cleanup_status: :unfinished, + container_repository_id: repository.id, + cleanup_tags_service_original_size: 1000, + cleanup_tags_service_before_truncate_size: 800, + cleanup_tags_service_after_truncate_size: 200, + cleanup_tags_service_before_delete_size: 100, + cleanup_tags_service_deleted_size: 100 + ) + expect(ContainerRepository.waiting_for_cleanup.count).to eq(1) + expect(repository.reload.cleanup_unfinished?).to be_truthy + expect(repository.expiration_policy_started_at).not_to eq(nil) + expect(repository.expiration_policy_completed_at).to eq(nil) + end + end + end end - end - context 'with no repository' do - let(:service) { described_class.new(nil) } + context 'with no repository' do + let(:service) { described_class.new(nil) } + + it 'returns an error response' do + expect(subject.success?).to eq(false) + expect(subject.message).to eq('no repository') + end + end - it 'returns an error response' do - response = subject + context 'with an invalid policy' do + let(:policy) { repository.project.container_expiration_policy } - expect(response.success?).to eq(false) + before do + policy.name_regex = nil + policy.enabled = true + repository.expiration_policy_cleanup_status = :cleanup_ongoing + end + + it 'returns an error response' do + expect { subject }.to change { repository.expiration_policy_cleanup_status }.from('cleanup_ongoing').to('cleanup_unscheduled') + expect(subject.success?).to eq(false) + expect(subject.message).to eq('invalid policy') + expect(policy).not_to be_enabled + end + end + + context 'with a network error' do + before do + expect(Projects::ContainerRepository::CleanupTagsService) + .to receive(:new).and_raise(Faraday::TimeoutError) + end + + it 'raises an error' do + expect { subject }.to raise_error(Faraday::TimeoutError) + + expect(ContainerRepository.waiting_for_cleanup.count).to eq(1) + expect(repository.reload.cleanup_unfinished?).to be_truthy + expect(repository.expiration_policy_started_at).not_to eq(nil) + expect(repository.expiration_policy_completed_at).to eq(nil) + end end end - context 'with a network error' do + context 'with loopless enabled' do + let(:policy) { repository.project.container_expiration_policy } + before do - expect(Projects::ContainerRepository::CleanupTagsService) - .to receive(:new).and_raise(Faraday::TimeoutError) + policy.update!(enabled: true) + policy.update_column(:next_run_at, 5.minutes.ago) end - it 'raises an error' do - expect { subject }.to raise_error(Faraday::TimeoutError) + it_behaves_like 'cleaning up a container repository' + + context 'next run scheduling' do + let_it_be_with_reload(:repository2) { create(:container_repository, project: project) } + let_it_be_with_reload(:repository3) { create(:container_repository, project: project) } + + before do + cleanup_tags_service = instance_double(Projects::ContainerRepository::CleanupTagsService) + allow(Projects::ContainerRepository::CleanupTagsService) + .to receive(:new).and_return(cleanup_tags_service) + allow(cleanup_tags_service).to receive(:execute).and_return(status: :success) + end + + shared_examples 'not scheduling the next run' do + it 'does not scheduled the next run' do + expect(policy).not_to receive(:schedule_next_run!) + + expect { subject }.not_to change { policy.reload.next_run_at } + end + end + + shared_examples 'scheduling the next run' do + it 'schedules the next run' do + expect(policy).to receive(:schedule_next_run!).and_call_original + + expect { subject }.to change { policy.reload.next_run_at } + end + end + + context 'with cleanups started_at before policy next_run_at' do + before do + ContainerRepository.update_all(expiration_policy_started_at: 10.minutes.ago) + end + + it_behaves_like 'not scheduling the next run' + end + + context 'with cleanups started_at around policy next_run_at' do + before do + repository3.update!(expiration_policy_started_at: policy.next_run_at + 10.minutes.ago) + end - expect(ContainerRepository.waiting_for_cleanup.count).to eq(1) - expect(repository.reload.cleanup_unfinished?).to be_truthy - expect(repository.expiration_policy_started_at).not_to eq(nil) - expect(repository.expiration_policy_completed_at).to eq(nil) + it_behaves_like 'not scheduling the next run' + end + + context 'with only the current repository started_at before the policy next_run_at' do + before do + repository2.update!(expiration_policy_started_at: policy.next_run_at + 10.minutes) + repository3.update!(expiration_policy_started_at: policy.next_run_at + 12.minutes) + end + + it_behaves_like 'scheduling the next run' + end + + context 'with cleanups started_at after policy next_run_at' do + before do + ContainerRepository.update_all(expiration_policy_started_at: policy.next_run_at + 10.minutes) + end + + it_behaves_like 'scheduling the next run' + end + + context 'with a future policy next_run_at' do + before do + policy.update_column(:next_run_at, 5.minutes.from_now) + end + + it_behaves_like 'not scheduling the next run' + end end end + + context 'with loopless disabled' do + before do + stub_feature_flags(container_registry_expiration_policies_loopless: false) + end + + it_behaves_like 'cleaning up a container repository' + end end end diff --git a/spec/services/deployments/create_service_spec.rb b/spec/services/deployments/create_service_spec.rb index 0bb5949ddb1..0f2a6ce32e1 100644 --- a/spec/services/deployments/create_service_spec.rb +++ b/spec/services/deployments/create_service_spec.rb @@ -21,7 +21,7 @@ RSpec.describe Deployments::CreateService do expect(Deployments::UpdateEnvironmentWorker).to receive(:perform_async) expect(Deployments::LinkMergeRequestWorker).to receive(:perform_async) - expect(Deployments::ExecuteHooksWorker).to receive(:perform_async) + expect(Deployments::HooksWorker).to receive(:perform_async) expect(service.execute).to be_persisted end @@ -37,7 +37,7 @@ RSpec.describe Deployments::CreateService do expect(Deployments::UpdateEnvironmentWorker).not_to receive(:perform_async) expect(Deployments::LinkMergeRequestWorker).not_to receive(:perform_async) - expect(Deployments::ExecuteHooksWorker).not_to receive(:perform_async) + expect(Deployments::HooksWorker).not_to receive(:perform_async) expect(service.execute).to be_persisted end @@ -57,7 +57,7 @@ RSpec.describe Deployments::CreateService do expect(Deployments::UpdateEnvironmentWorker).not_to receive(:perform_async) expect(Deployments::LinkMergeRequestWorker).not_to receive(:perform_async) - expect(Deployments::ExecuteHooksWorker).not_to receive(:perform_async) + expect(Deployments::HooksWorker).not_to receive(:perform_async) described_class.new(environment.reload, user, params).execute end diff --git a/spec/services/deployments/update_environment_service_spec.rb b/spec/services/deployments/update_environment_service_spec.rb index 372805cc0fd..4d15258a186 100644 --- a/spec/services/deployments/update_environment_service_spec.rb +++ b/spec/services/deployments/update_environment_service_spec.rb @@ -33,7 +33,7 @@ RSpec.describe Deployments::UpdateEnvironmentService do before do allow(Deployments::LinkMergeRequestWorker).to receive(:perform_async) - allow(Deployments::ExecuteHooksWorker).to receive(:perform_async) + allow(Deployments::HooksWorker).to receive(:perform_async) job.success! # Create/Succeed deployment end @@ -161,6 +161,7 @@ RSpec.describe Deployments::UpdateEnvironmentService do context 'when deployment was created by an external CD system' do before do deployment.update_column(:deployable_id, nil) + deployment.reload end it 'guesses the deployment tier' do diff --git a/spec/services/discussions/capture_diff_note_positions_service_spec.rb b/spec/services/discussions/capture_diff_note_positions_service_spec.rb index be53b02a4c1..25e5f549bee 100644 --- a/spec/services/discussions/capture_diff_note_positions_service_spec.rb +++ b/spec/services/discussions/capture_diff_note_positions_service_spec.rb @@ -55,7 +55,7 @@ RSpec.describe Discussions::CaptureDiffNotePositionsService do context 'and position of the discussion changed on target branch head' do it 'diff positions are created for the first notes of the discussions' do - MergeRequests::MergeToRefService.new(project, merge_request.author).execute(merge_request) + MergeRequests::MergeToRefService.new(project: project, current_user: merge_request.author).execute(merge_request) service.execute verify_diff_note_position!(first_discussion_note, new_line: first_new_line) diff --git a/spec/services/draft_notes/publish_service_spec.rb b/spec/services/draft_notes/publish_service_spec.rb index f93622dc25a..2e1de367da3 100644 --- a/spec/services/draft_notes/publish_service_spec.rb +++ b/spec/services/draft_notes/publish_service_spec.rb @@ -202,7 +202,7 @@ RSpec.describe DraftNotes::PublishService do expect(newrev).to be_present # Generates new MR revision at DB level - refresh = MergeRequests::RefreshService.new(project, user) + refresh = MergeRequests::RefreshService.new(project: project, current_user: user) refresh.execute(oldrev, newrev, merge_request.source_branch_ref) expect { publish(draft: draft) }.to change { Suggestion.count }.by(1) diff --git a/spec/services/feature_flags/create_service_spec.rb b/spec/services/feature_flags/create_service_spec.rb index 128fab114fe..2e0c162ebc1 100644 --- a/spec/services/feature_flags/create_service_spec.rb +++ b/spec/services/feature_flags/create_service_spec.rb @@ -6,6 +6,7 @@ RSpec.describe FeatureFlags::CreateService do let_it_be(:project) { create(:project) } let_it_be(:developer) { create(:user) } let_it_be(:reporter) { create(:user) } + let(:user) { developer } before_all do diff --git a/spec/services/feature_flags/destroy_service_spec.rb b/spec/services/feature_flags/destroy_service_spec.rb index b35de02c628..ee30474873c 100644 --- a/spec/services/feature_flags/destroy_service_spec.rb +++ b/spec/services/feature_flags/destroy_service_spec.rb @@ -8,6 +8,7 @@ RSpec.describe FeatureFlags::DestroyService do let_it_be(:project) { create(:project) } let_it_be(:developer) { create(:user) } let_it_be(:reporter) { create(:user) } + let(:user) { developer } let!(:feature_flag) { create(:operations_feature_flag, project: project) } diff --git a/spec/services/feature_flags/disable_service_spec.rb b/spec/services/feature_flags/disable_service_spec.rb index de0f70bf552..4b2137be35c 100644 --- a/spec/services/feature_flags/disable_service_spec.rb +++ b/spec/services/feature_flags/disable_service_spec.rb @@ -7,6 +7,7 @@ RSpec.describe FeatureFlags::DisableService do let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user) } + let(:params) { {} } let(:service) { described_class.new(project, user, params) } diff --git a/spec/services/feature_flags/enable_service_spec.rb b/spec/services/feature_flags/enable_service_spec.rb index 88c8028f6c5..c0008b1933f 100644 --- a/spec/services/feature_flags/enable_service_spec.rb +++ b/spec/services/feature_flags/enable_service_spec.rb @@ -7,6 +7,7 @@ RSpec.describe FeatureFlags::EnableService do let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user) } + let(:params) { {} } let(:service) { described_class.new(project, user, params) } diff --git a/spec/services/feature_flags/update_service_spec.rb b/spec/services/feature_flags/update_service_spec.rb index 9639cf3081d..1a127a0d472 100644 --- a/spec/services/feature_flags/update_service_spec.rb +++ b/spec/services/feature_flags/update_service_spec.rb @@ -6,6 +6,7 @@ RSpec.describe FeatureFlags::UpdateService do let_it_be(:project) { create(:project) } let_it_be(:developer) { create(:user) } let_it_be(:reporter) { create(:user) } + let(:user) { developer } let(:feature_flag) { create(:operations_feature_flag, project: project, active: true) } diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb index 52df21897b9..19694a0a354 100644 --- a/spec/services/git/branch_hooks_service_spec.rb +++ b/spec/services/git/branch_hooks_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Git::BranchHooksService do +RSpec.describe Git::BranchHooksService, :clean_gitlab_redis_shared_state do include RepoHelpers include ProjectForksHelper @@ -116,8 +116,6 @@ RSpec.describe Git::BranchHooksService do allow_next_instance_of(Gitlab::Git::Diff) do |diff| allow(diff).to receive(:new_path).and_return('.gitlab-ci.yml') end - - allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event) end let!(:commit_author) { create(:user, email: sample_commit.author_email) } @@ -127,23 +125,11 @@ RSpec.describe Git::BranchHooksService do end it 'tracks the event' do - execute_service - - expect(Gitlab::UsageDataCounters::HLLRedisCounter) - .to have_received(:track_event).with(*tracking_params) - end - - context 'when the FF usage_data_unique_users_committing_ciconfigfile is disabled' do - before do - stub_feature_flags(usage_data_unique_users_committing_ciconfigfile: false) - end + time = Time.zone.now - it 'does not track the event' do - execute_service + execute_service - expect(Gitlab::UsageDataCounters::HLLRedisCounter) - .not_to have_received(:track_event).with(*tracking_params) - end + expect(Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: 'o_pipeline_authoring_unique_users_committing_ciconfigfile', start_date: time, end_date: time + 7.days)).to eq(1) end context 'when usage ping is disabled' do @@ -155,7 +141,7 @@ RSpec.describe Git::BranchHooksService do execute_service expect(Gitlab::UsageDataCounters::HLLRedisCounter) - .not_to have_received(:track_event).with(*tracking_params) + .not_to receive(:track_event).with(*tracking_params) end end @@ -166,7 +152,7 @@ RSpec.describe Git::BranchHooksService do execute_service expect(Gitlab::UsageDataCounters::HLLRedisCounter) - .not_to have_received(:track_event).with(*tracking_params) + .not_to receive(:track_event).with(*tracking_params) end end @@ -179,7 +165,7 @@ RSpec.describe Git::BranchHooksService do execute_service expect(Gitlab::UsageDataCounters::HLLRedisCounter) - .not_to have_received(:track_event).with(*tracking_params) + .not_to receive(:track_event).with(*tracking_params) end end end diff --git a/spec/services/git/wiki_push_service_spec.rb b/spec/services/git/wiki_push_service_spec.rb index df9a48d7b1c..151c2a1d014 100644 --- a/spec/services/git/wiki_push_service_spec.rb +++ b/spec/services/git/wiki_push_service_spec.rb @@ -51,7 +51,7 @@ RSpec.describe Git::WikiPushService, services: true do process_changes do write_new_page update_page(wiki_page_a.title) - delete_page(wiki_page_b.page.path) + delete_page(wiki_page_b.page) end end @@ -198,7 +198,7 @@ RSpec.describe Git::WikiPushService, services: true do context 'when a page we do not know about has been deleted' do def run_service wiki_page = create(:wiki_page, wiki: wiki) - process_changes { delete_page(wiki_page.page.path) } + process_changes { delete_page(wiki_page.page) } end it 'create a new meta-data record' do @@ -350,8 +350,8 @@ RSpec.describe Git::WikiPushService, services: true do git_wiki.update_page(page.path, title, 'markdown', 'Hey', commit_details) end - def delete_page(path) - git_wiki.delete_page(path, commit_details) + def delete_page(page) + wiki.delete_page(page, 'commit message') end def commit_details diff --git a/spec/services/groups/autocomplete_service_spec.rb b/spec/services/groups/autocomplete_service_spec.rb new file mode 100644 index 00000000000..00d0ad3b347 --- /dev/null +++ b/spec/services/groups/autocomplete_service_spec.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Groups::AutocompleteService do + let_it_be(:group, refind: true) { create(:group, :nested, :private, avatar: fixture_file_upload('spec/fixtures/dk.png')) } + let_it_be(:sub_group) { create(:group, :private, parent: group) } + + let(:user) { create(:user) } + + subject { described_class.new(group, user) } + + before do + group.add_developer(user) + end + + def expect_labels_to_equal(labels, expected_labels) + extract_title = lambda { |label| label['title'] } + expect(labels.map(&extract_title)).to match_array(expected_labels.map(&extract_title)) + end + + describe '#labels_as_hash' do + let!(:label1) { create(:group_label, group: group) } + let!(:label2) { create(:group_label, group: group) } + let!(:sub_group_label) { create(:group_label, group: sub_group) } + let!(:parent_group_label) { create(:group_label, group: group.parent) } + + it 'returns labels from own group and ancestor groups' do + results = subject.labels_as_hash(nil) + + expected_labels = [label1, label2, parent_group_label] + + expect_labels_to_equal(results, expected_labels) + end + end + + describe '#issues' do + let(:project) { create(:project, group: group) } + let(:sub_group_project) { create(:project, group: sub_group) } + + let!(:project_issue) { create(:issue, project: project) } + let!(:sub_group_project_issue) { create(:issue, confidential: true, project: sub_group_project) } + + it 'returns issues in group and subgroups' do + issues = subject.issues + + expect(issues.map(&:iid)).to contain_exactly(project_issue.iid, sub_group_project_issue.iid) + expect(issues.map(&:title)).to contain_exactly(project_issue.title, sub_group_project_issue.title) + end + + it 'returns only confidential issues if confidential_only is true' do + issues = subject.issues(confidential_only: true) + + expect(issues.map(&:iid)).to contain_exactly(sub_group_project_issue.iid) + expect(issues.map(&:title)).to contain_exactly(sub_group_project_issue.title) + end + end + + describe '#merge_requests' do + let(:project) { create(:project, :repository, group: group) } + let(:sub_group_project) { create(:project, :repository, group: sub_group) } + + let!(:project_mr) { create(:merge_request, source_project: project) } + let!(:sub_group_project_mr) { create(:merge_request, source_project: sub_group_project) } + + it 'returns merge requests in group and subgroups' do + expect(subject.merge_requests.map(&:iid)).to contain_exactly(project_mr.iid, sub_group_project_mr.iid) + expect(subject.merge_requests.map(&:title)).to contain_exactly(project_mr.title, sub_group_project_mr.title) + end + end + + describe '#milestones' do + let!(:group_milestone) { create(:milestone, group: group) } + let!(:subgroup_milestone) { create(:milestone, group: sub_group) } + + before do + sub_group.add_maintainer(user) + end + + context 'when group is public' do + let(:public_group) { create(:group, :public) } + let(:public_subgroup) { create(:group, :public, parent: public_group) } + + before do + group_milestone.update!(group: public_group) + subgroup_milestone.update!(group: public_subgroup) + end + + it 'returns milestones from groups and subgroups' do + subject = described_class.new(public_subgroup, user) + + expect(subject.milestones.map(&:iid)).to contain_exactly(group_milestone.iid, subgroup_milestone.iid) + expect(subject.milestones.map(&:title)).to contain_exactly(group_milestone.title, subgroup_milestone.title) + end + end + + it 'returns milestones from group' do + expect(subject.milestones.map(&:iid)).to contain_exactly(group_milestone.iid) + expect(subject.milestones.map(&:title)).to contain_exactly(group_milestone.title) + end + + it 'returns milestones from groups and subgroups' do + milestones = described_class.new(sub_group, user).milestones + + expect(milestones.map(&:iid)).to contain_exactly(group_milestone.iid, subgroup_milestone.iid) + expect(milestones.map(&:title)).to contain_exactly(group_milestone.title, subgroup_milestone.title) + end + + it 'returns only milestones that user can read' do + user = create(:user) + sub_group.add_guest(user) + + milestones = described_class.new(sub_group, user).milestones + + expect(milestones.map(&:iid)).to contain_exactly(subgroup_milestone.iid) + expect(milestones.map(&:title)).to contain_exactly(subgroup_milestone.title) + end + end +end diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index f0cd42c1948..dca5497de06 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -164,9 +164,9 @@ RSpec.describe Groups::CreateService, '#execute' do let!(:instance_integration) { create(:prometheus_service, :instance, api_url: 'https://prometheus.instance.com/') } it 'creates a service from the instance-level integration' do - expect(created_group.services.count).to eq(1) - expect(created_group.services.first.api_url).to eq(instance_integration.api_url) - expect(created_group.services.first.inherit_from_id).to eq(instance_integration.id) + expect(created_group.integrations.count).to eq(1) + expect(created_group.integrations.first.api_url).to eq(instance_integration.api_url) + expect(created_group.integrations.first.inherit_from_id).to eq(instance_integration.id) end context 'with an active group-level integration' do @@ -179,9 +179,9 @@ RSpec.describe Groups::CreateService, '#execute' do end it 'creates a service from the group-level integration' do - expect(created_group.services.count).to eq(1) - expect(created_group.services.first.api_url).to eq(group_integration.api_url) - expect(created_group.services.first.inherit_from_id).to eq(group_integration.id) + expect(created_group.integrations.count).to eq(1) + expect(created_group.integrations.first.api_url).to eq(group_integration.api_url) + expect(created_group.integrations.first.inherit_from_id).to eq(group_integration.id) end context 'with an active subgroup' do @@ -194,9 +194,9 @@ RSpec.describe Groups::CreateService, '#execute' do end it 'creates a service from the subgroup-level integration' do - expect(created_group.services.count).to eq(1) - expect(created_group.services.first.api_url).to eq(subgroup_integration.api_url) - expect(created_group.services.first.inherit_from_id).to eq(subgroup_integration.id) + expect(created_group.integrations.count).to eq(1) + expect(created_group.integrations.first.api_url).to eq(subgroup_integration.api_url) + expect(created_group.integrations.first.inherit_from_id).to eq(subgroup_integration.id) end end end diff --git a/spec/services/groups/open_issues_count_service_spec.rb b/spec/services/groups/open_issues_count_service_spec.rb index 740e9846119..fca09bfdebe 100644 --- a/spec/services/groups/open_issues_count_service_spec.rb +++ b/spec/services/groups/open_issues_count_service_spec.rb @@ -57,4 +57,15 @@ RSpec.describe Groups::OpenIssuesCountService, :use_clean_rails_memory_store_cac it_behaves_like 'a counter caching service with threshold' end end + + describe '#clear_all_cache_keys' do + it 'calls `Rails.cache.delete` with the correct keys' do + expect(Rails.cache).to receive(:delete) + .with(['groups', 'open_issues_count_service', 1, group.id, described_class::PUBLIC_COUNT_KEY]) + expect(Rails.cache).to receive(:delete) + .with(['groups', 'open_issues_count_service', 1, group.id, described_class::TOTAL_COUNT_KEY]) + + subject.clear_all_cache_keys + end + end end diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb index 3a1197970f4..2fbd5eeef5f 100644 --- a/spec/services/groups/transfer_service_spec.rb +++ b/spec/services/groups/transfer_service_spec.rb @@ -240,6 +240,7 @@ RSpec.describe Groups::TransferService do end context 'when the group is allowed to be transferred' do + let_it_be(:new_parent_group, reload: true) { create(:group, :public) } let_it_be(:new_parent_group_integration) { create(:slack_service, group: new_parent_group, project: nil, webhook: 'http://new-group.slack.com') } before do @@ -273,17 +274,16 @@ RSpec.describe Groups::TransferService do end context 'with a group integration' do - let_it_be(:instance_integration) { create(:slack_service, :instance, webhook: 'http://project.slack.com') } - - let(:new_created_integration) { Service.find_by(group: group) } + let(:new_created_integration) { Integration.find_by(group: group) } context 'with an inherited integration' do + let_it_be(:instance_integration) { create(:slack_service, :instance, webhook: 'http://project.slack.com') } let_it_be(:group_integration) { create(:slack_service, group: group, project: nil, webhook: 'http://group.slack.com', inherit_from_id: instance_integration.id) } it 'replaces inherited integrations', :aggregate_failures do expect(new_created_integration.webhook).to eq(new_parent_group_integration.webhook) expect(PropagateIntegrationWorker).to have_received(:perform_async).with(new_created_integration.id) - expect(Service.count).to eq(3) + expect(Integration.count).to eq(3) end end @@ -603,6 +603,7 @@ RSpec.describe Groups::TransferService do create(:group_member, :owner, group: new_parent_group, user: user) create(:group, :private, parent: group, require_two_factor_authentication: true) group.update!(require_two_factor_authentication: true) + new_parent_group.reload # make sure traversal_ids are reloaded end it 'does not update group two factor authentication setting' do diff --git a/spec/services/import/gitlab_projects/create_project_from_remote_file_service_spec.rb b/spec/services/import/gitlab_projects/create_project_from_remote_file_service_spec.rb new file mode 100644 index 00000000000..3c461c91ff0 --- /dev/null +++ b/spec/services/import/gitlab_projects/create_project_from_remote_file_service_spec.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Import::GitlabProjects::CreateProjectFromRemoteFileService do + let(:remote_url) { 'https://external.file.path/file' } + + let(:params) do + { + path: 'path', + namespace: user.namespace, + name: 'name', + remote_import_url: remote_url + } + end + + let_it_be(:user) { create(:user) } + + subject { described_class.new(user, params) } + + it 'creates a project and returns a successful response' do + stub_headers_for(remote_url, { + 'content-type' => 'application/gzip', + 'content-length' => '10' + }) + + response = nil + expect { response = subject.execute } + .to change(Project, :count).by(1) + + expect(response).to be_success + expect(response.http_status).to eq(:ok) + expect(response.payload).to be_instance_of(Project) + expect(response.payload.name).to eq('name') + expect(response.payload.path).to eq('path') + expect(response.payload.namespace).to eq(user.namespace) + end + + context 'when the file url is invalid' do + it 'returns an erred response with the reason of the failure' do + stub_application_setting(allow_local_requests_from_web_hooks_and_services: false) + + params[:remote_import_url] = 'https://localhost/file' + + response = nil + expect { response = subject.execute } + .not_to change(Project, :count) + + expect(response).not_to be_success + expect(response.http_status).to eq(:bad_request) + expect(response.message).to eq('Requests to localhost are not allowed') + end + end + + context 'validate file type' do + it 'returns erred response when the file type is not informed' do + stub_headers_for(remote_url, { 'content-length' => '10' }) + + response = nil + expect { response = subject.execute } + .not_to change(Project, :count) + + expect(response).not_to be_success + expect(response.http_status).to eq(:bad_request) + expect(response.message) + .to eq("Missing 'ContentType' header") + end + + it 'returns erred response when the file type is not allowed' do + stub_headers_for(remote_url, { + 'content-type' => 'application/js', + 'content-length' => '10' + }) + + response = nil + expect { response = subject.execute } + .not_to change(Project, :count) + + expect(response).not_to be_success + expect(response.http_status).to eq(:bad_request) + expect(response.message) + .to eq("Remote file content type 'application/js' not allowed. (Allowed content types: application/gzip)") + end + end + + context 'validate content type' do + it 'returns erred response when the file size is not informed' do + stub_headers_for(remote_url, { 'content-type' => 'application/gzip' }) + + response = nil + expect { response = subject.execute } + .not_to change(Project, :count) + + expect(response).not_to be_success + expect(response.http_status).to eq(:bad_request) + expect(response.message) + .to eq("Missing 'ContentLength' header") + end + + it 'returns error response when the file size is a text' do + stub_headers_for(remote_url, { + 'content-type' => 'application/gzip', + 'content-length' => 'some text' + }) + + response = nil + expect { response = subject.execute } + .not_to change(Project, :count) + + expect(response).not_to be_success + expect(response.http_status).to eq(:bad_request) + expect(response.message) + .to eq("Missing 'ContentLength' header") + end + + it 'returns erred response when the file is larger then allowed' do + stub_headers_for(remote_url, { + 'content-type' => 'application/gzip', + 'content-length' => 11.gigabytes.to_s + }) + + response = nil + expect { response = subject.execute } + .not_to change(Project, :count) + + expect(response).not_to be_success + expect(response.http_status).to eq(:bad_request) + expect(response.message) + .to eq('Remote file larger than limit. (limit 10 GB)') + end + end + + context 'when required parameters are not provided' do + let(:params) { {} } + + it 'returns an erred response with the reason of the failure' do + stub_application_setting(allow_local_requests_from_web_hooks_and_services: false) + + response = nil + expect { response = subject.execute } + .not_to change(Project, :count) + + expect(response).not_to be_success + expect(response.http_status).to eq(:bad_request) + expect(response.message).to eq("Parameter 'path' is required") + + expect(subject.errors.full_messages).to match_array([ + "Missing 'ContentLength' header", + "Missing 'ContentType' header", + "Parameter 'namespace' is required", + "Parameter 'path' is required", + "Parameter 'remote_import_url' is required" + ]) + end + end + + context 'when the project is invalid' do + it 'returns an erred response with the reason of the failure' do + create(:project, namespace: user.namespace, path: 'path') + + stub_headers_for(remote_url, { + 'content-type' => 'application/gzip', + 'content-length' => '10' + }) + + response = nil + expect { response = subject.execute } + .not_to change(Project, :count) + + expect(response).not_to be_success + expect(response.http_status).to eq(:bad_request) + expect(response.message).to eq('Path has already been taken') + end + end + + def stub_headers_for(url, headers = {}) + allow(Gitlab::HTTP) + .to receive(:head) + .with(url) + .and_return(double(headers: headers)) + end +end diff --git a/spec/services/import/gitlab_projects/create_project_from_uploaded_file_service_spec.rb b/spec/services/import/gitlab_projects/create_project_from_uploaded_file_service_spec.rb new file mode 100644 index 00000000000..a0e04a9a696 --- /dev/null +++ b/spec/services/import/gitlab_projects/create_project_from_uploaded_file_service_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Import::GitlabProjects::CreateProjectFromUploadedFileService do + let(:file_upload) do + fixture_file_upload('spec/features/projects/import_export/test_project_export.tar.gz') + end + + let(:params) do + { + path: 'path', + namespace: user.namespace, + name: 'name', + file: file_upload + } + end + + let_it_be(:user) { create(:user) } + + subject { described_class.new(user, params) } + + it 'creates a project and returns a successful response' do + response = nil + expect { response = subject.execute } + .to change(Project, :count).by(1) + + expect(response).to be_success + expect(response.http_status).to eq(:ok) + expect(response.payload).to be_instance_of(Project) + expect(response.payload.name).to eq('name') + expect(response.payload.path).to eq('path') + expect(response.payload.namespace).to eq(user.namespace) + end + + context 'when required parameters are not provided' do + let(:params) { {} } + + it 'returns an erred response with the reason of the failure' do + stub_application_setting(allow_local_requests_from_web_hooks_and_services: false) + + response = nil + expect { response = subject.execute } + .not_to change(Project, :count) + + expect(response).not_to be_success + expect(response.http_status).to eq(:bad_request) + expect(response.message).to eq("Parameter 'path' is required") + + expect(subject.errors.full_messages).to match_array([ + "Parameter 'namespace' is required", + "Parameter 'path' is required", + "Parameter 'file' is required" + ]) + end + end + + context 'when the project is invalid' do + it 'returns an erred response with the reason of the failure' do + create(:project, namespace: user.namespace, path: 'path') + + response = nil + expect { response = subject.execute } + .not_to change(Project, :count) + + expect(response).not_to be_success + expect(response.http_status).to eq(:bad_request) + expect(response.message).to eq('Path has already been taken') + end + end +end diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index c749f282cd3..dfdfb57111c 100644 --- a/spec/services/issuable/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -101,6 +101,22 @@ RSpec.describe Issuable::BulkUpdateService do end end + shared_examples 'scheduling cached group count clear' do + it 'schedules worker' do + expect(Issuables::ClearGroupsIssueCounterWorker).to receive(:perform_async) + + bulk_update(issuables, params) + end + end + + shared_examples 'not scheduling cached group count clear' do + it 'does not schedule worker' do + expect(Issuables::ClearGroupsIssueCounterWorker).not_to receive(:perform_async) + + bulk_update(issuables, params) + end + end + context 'with issuables at a project level' do let(:parent) { project } @@ -131,6 +147,11 @@ RSpec.describe Issuable::BulkUpdateService do expect(project.issues.opened).to be_empty expect(project.issues.closed).not_to be_empty end + + it_behaves_like 'scheduling cached group count clear' do + let(:issuables) { issues } + let(:params) { { state_event: 'close' } } + end end describe 'reopen issues' do @@ -149,6 +170,11 @@ RSpec.describe Issuable::BulkUpdateService do 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(:params) { { state_event: 'reopen' } } + end end describe 'updating merge request assignee' do @@ -231,6 +257,10 @@ RSpec.describe Issuable::BulkUpdateService do let(:milestone) { create(:milestone, project: project) } it_behaves_like 'updates milestones' + + it_behaves_like 'not scheduling cached group count clear' do + let(:params) { { milestone_id: milestone.id } } + end end describe 'updating labels' do diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb index a988ab81754..1426ef2a1f6 100644 --- a/spec/services/issuable/common_system_notes_service_spec.rb +++ b/spec/services/issuable/common_system_notes_service_spec.rb @@ -23,7 +23,7 @@ RSpec.describe Issuable::CommonSystemNotesService do end it 'creates a resource label event' do - described_class.new(project, user).execute(issuable, old_labels: []) + described_class.new(project: project, current_user: user).execute(issuable, old_labels: []) event = issuable.reload.resource_label_events.last expect(event).not_to be_nil @@ -66,7 +66,7 @@ RSpec.describe Issuable::CommonSystemNotesService do context 'on issuable create' do let(:issuable) { build(:issue, project: project) } - subject { described_class.new(project, user).execute(issuable, old_labels: [], is_update: false) } + subject { described_class.new(project: project, current_user: user).execute(issuable, old_labels: [], is_update: false) } it 'does not create system note for title and description' do issuable.save! diff --git a/spec/services/issuable/destroy_label_links_service_spec.rb b/spec/services/issuable/destroy_label_links_service_spec.rb new file mode 100644 index 00000000000..bbc69e266c9 --- /dev/null +++ b/spec/services/issuable/destroy_label_links_service_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Issuable::DestroyLabelLinksService do + describe '#execute' do + context 'when target is an Issue' do + let_it_be(:target) { create(:issue) } + + it_behaves_like 'service deleting label links of an issuable' + end + + context 'when target is a MergeRequest' do + let_it_be(:target) { create(:merge_request) } + + it_behaves_like 'service deleting label links of an issuable' + end + end +end diff --git a/spec/services/issuable/destroy_service_spec.rb b/spec/services/issuable/destroy_service_spec.rb index fa4902e5237..c72d48d5b77 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, user) } + subject(:service) { described_class.new(project: project, current_user: user) } describe '#execute' do context 'when issuable is an issue' do @@ -31,6 +31,10 @@ RSpec.describe Issuable::DestroyService do it_behaves_like 'service deleting todos' do let(:issuable) { issue } end + + it_behaves_like 'service deleting label links' do + let(:issuable) { issue } + end end context 'when issuable is a merge request' do @@ -54,6 +58,10 @@ RSpec.describe Issuable::DestroyService do it_behaves_like 'service deleting todos' do let(:issuable) { merge_request } end + + it_behaves_like 'service deleting label links' do + let(:issuable) { merge_request } + end end end end diff --git a/spec/services/issue_rebalancing_service_spec.rb b/spec/services/issue_rebalancing_service_spec.rb index 7b3d4213b24..1c7f74264b7 100644 --- a/spec/services/issue_rebalancing_service_spec.rb +++ b/spec/services/issue_rebalancing_service_spec.rb @@ -3,31 +3,35 @@ require 'spec_helper' RSpec.describe IssueRebalancingService do - let_it_be(:project) { create(:project) } + let_it_be(:project, reload: true) { create(:project) } let_it_be(:user) { project.creator } let_it_be(:start) { RelativePositioning::START_POSITION } let_it_be(:max_pos) { RelativePositioning::MAX_POSITION } let_it_be(:min_pos) { RelativePositioning::MIN_POSITION } let_it_be(:clump_size) { 300 } - let_it_be(:unclumped) do - (0..clump_size).to_a.map do |i| + let_it_be(:unclumped, reload: true) do + (1..clump_size).to_a.map do |i| create(:issue, project: project, author: user, relative_position: start + (1024 * i)) end end - let_it_be(:end_clump) do - (0..clump_size).to_a.map do |i| + let_it_be(:end_clump, reload: true) do + (1..clump_size).to_a.map do |i| create(:issue, project: project, author: user, relative_position: max_pos - i) end end - let_it_be(:start_clump) do - (0..clump_size).to_a.map do |i| + let_it_be(:start_clump, reload: true) do + (1..clump_size).to_a.map do |i| create(:issue, project: project, author: user, relative_position: min_pos + i) end end + before do + stub_feature_flags(issue_rebalancing_with_retry: false) + end + def issues_in_position_order project.reload.issues.reorder(relative_position: :asc).to_a end @@ -101,19 +105,70 @@ RSpec.describe IssueRebalancingService do end end + shared_examples 'rebalancing is retried on statement timeout exceptions' do + subject { described_class.new(project.issues.first) } + + it 'retries update statement' do + call_count = 0 + allow(subject).to receive(:run_update_query) do + call_count += 1 + if call_count < 13 + raise(ActiveRecord::QueryCanceled) + else + call_count = 0 if call_count == 13 + 16 # 16 = 17 sub-batches - 1 call that succeeded as part of 5th batch + true + end + end + + # call math: + # batches start at 100 and are split in half after every 3 retries if ActiveRecord::StatementTimeout exception is raised. + # We raise ActiveRecord::StatementTimeout exception for 13 calls: + # 1. 100 => 3 calls + # 2. 100/2=50 => 3 calls + 3 above = 6 calls, raise ActiveRecord::StatementTimeout + # 3. 50/2=25 => 3 calls + 6 above = 9 calls, raise ActiveRecord::StatementTimeout + # 4. 25/2=12 => 3 calls + 9 above = 12 calls, raise ActiveRecord::StatementTimeout + # 5. 12/2=6 => 1 call + 12 above = 13 calls, run successfully + # + # so out of 100 elements we created batches of 6 items => 100/6 = 17 sub-batches of 6 or less elements + # + # project.issues.count: 900 issues, so 9 batches of 100 => 9 * (13+16) = 261 + expect(subject).to receive(:update_positions).exactly(261).times.and_call_original + + subject.execute + end + end + context 'when issue_rebalancing_optimization feature flag is on' do before do stub_feature_flags(issue_rebalancing_optimization: true) end it_behaves_like 'IssueRebalancingService shared examples' + + context 'when issue_rebalancing_with_retry feature flag is on' do + before do + stub_feature_flags(issue_rebalancing_with_retry: true) + end + + it_behaves_like 'IssueRebalancingService shared examples' + it_behaves_like 'rebalancing is retried on statement timeout exceptions' + end end - context 'when issue_rebalancing_optimization feature flag is on' do + context 'when issue_rebalancing_optimization feature flag is off' do before do stub_feature_flags(issue_rebalancing_optimization: false) end it_behaves_like 'IssueRebalancingService shared examples' + + context 'when issue_rebalancing_with_retry feature flag is on' do + before do + stub_feature_flags(issue_rebalancing_with_retry: true) + end + + it_behaves_like 'IssueRebalancingService shared examples' + it_behaves_like 'rebalancing is retried on statement timeout exceptions' + end end end diff --git a/spec/services/issues/after_create_service_spec.rb b/spec/services/issues/after_create_service_spec.rb index bc9be3211d3..6b720d6e687 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, current_user) } + subject(:after_create_service) { described_class.new(project: 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 80fe2474ecd..3f506ec58b0 100644 --- a/spec/services/issues/build_service_spec.rb +++ b/spec/services/issues/build_service_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper.rb' +require 'spec_helper' RSpec.describe Issues::BuildService do let_it_be(:project) { create(:project, :repository) } @@ -15,7 +15,7 @@ RSpec.describe Issues::BuildService do end def build_issue(issue_params = {}) - described_class.new(project, user, issue_params).execute + described_class.new(project: project, current_user: user, params: issue_params).execute end context 'for a single discussion' do @@ -41,7 +41,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, user, merge_request_to_resolve_discussions_of: merge_request.iid) + service = described_class.new(project: project, current_user: user, params: { merge_request_to_resolve_discussions_of: merge_request.iid }) service.execute @@ -50,7 +50,7 @@ RSpec.describe Issues::BuildService do end describe '#item_for_discussion' do - let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) } + let(:service) { described_class.new(project: 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 @@ -184,9 +184,9 @@ RSpec.describe Issues::BuildService do end it 'cannot set invalid type' do - expect do - build_issue(issue_type: 'invalid type') - end.to raise_error(ArgumentError, "'invalid type' is not a valid issue_type") + issue = build_issue(issue_type: 'invalid type') + + expect(issue).to be_issue end end end diff --git a/spec/services/issues/clone_service_spec.rb b/spec/services/issues/clone_service_spec.rb index 44180a322ca..abbcb1c1d48 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(old_project, user) + described_class.new(project: 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 3cf45143594..8950bdd465f 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -3,24 +3,52 @@ require 'spec_helper' RSpec.describe Issues::CloseService do - let(:project) { create(:project, :repository) } - let(:user) { create(:user, email: "user@example.com") } - let(:user2) { create(:user, email: "user2@example.com") } - let(:guest) { create(:user) } - let(:issue) { create(:issue, title: "My issue", project: project, assignees: [user2], author: create(:user)) } + subject(:close_issue) { described_class.new(project: project, current_user: user).close_issue(issue) } + + let_it_be(:project, refind: true) { create(:project, :repository) } + let_it_be(:label1) { create(:label, project: project) } + let_it_be(:label2) { create(:label, project: project, remove_on_close: true) } + let_it_be(:author) { create(:user) } + let_it_be(:user) { create(:user, email: "user@example.com") } + let_it_be(:user2) { create(:user, email: "user2@example.com") } + let_it_be(:guest) { create(:user) } + let_it_be(:closing_merge_request) { create(:merge_request, source_project: project) } + let(:external_issue) { ExternalIssue.new('JIRA-123', project) } - let(:closing_merge_request) { create(:merge_request, source_project: project) } - let(:closing_commit) { create(:commit, project: project) } - let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) } + let!(:issue) { create(:issue, title: "My issue", project: project, assignees: [user2], author: author) } - before do + before_all do project.add_maintainer(user) project.add_developer(user2) project.add_guest(guest) end + shared_examples 'removes labels marked for removal from issue when closed' do + before do + issue.update!(label_ids: [label1.id, label2.id]) + end + + it 'removes labels marked for removal' do + expect do + close_issue + end.to change { issue.reload.label_ids }.from(containing_exactly(label1.id, label2.id)).to(containing_exactly(label1.id)) + end + + it 'creates system notes for the removed labels' do + expect do + close_issue + end.to change(ResourceLabelEvent, :count).by(1) + + expect(ResourceLabelEvent.last.slice(:action, :issue_id, :label_id)).to eq( + 'action' => 'remove', + 'issue_id' => issue.id, + 'label_id' => label2.id + ) + end + end + describe '#execute' do - let(:service) { described_class.new(project, user) } + let(:service) { described_class.new(project: project, current_user: user) } it 'checks if the user is authorized to update the issue' do expect(service).to receive(:can?).with(user, :update_issue, issue) @@ -87,18 +115,18 @@ RSpec.describe Issues::CloseService do project.reload expect(project.external_issue_tracker).to receive(:close_issue) - described_class.new(project, user).close_issue(external_issue) + described_class.new(project: project, current_user: user).close_issue(external_issue) end end - context 'with innactive external issue tracker supporting close_issue' do + context 'with inactive external issue tracker supporting close_issue' do let!(:external_issue_tracker) { create(:jira_service, project: project, active: false) } it 'does not close the issue on the external issue tracker' do project.reload expect(project.external_issue_tracker).not_to receive(:close_issue) - described_class.new(project, user).close_issue(external_issue) + described_class.new(project: project, current_user: user).close_issue(external_issue) end end @@ -109,7 +137,7 @@ RSpec.describe Issues::CloseService do project.reload expect(project.external_issue_tracker).not_to receive(:close_issue) - described_class.new(project, user).close_issue(external_issue) + described_class.new(project: project, current_user: user).close_issue(external_issue) end end end @@ -117,10 +145,12 @@ 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, user).close_issue(issue, closed_via: closing_merge_request) + described_class.new(project: project, current_user: user).close_issue(issue, closed_via: closing_merge_request) end end + it_behaves_like 'removes labels marked for removal from issue when closed' + it 'mentions closure via a merge request' do close_issue @@ -184,10 +214,18 @@ RSpec.describe Issues::CloseService do end context "closed by a commit", :sidekiq_might_not_need_inline do - it 'mentions closure via a commit' do + subject(:close_issue) do perform_enqueued_jobs do - described_class.new(project, user).close_issue(issue, closed_via: closing_commit) + described_class.new(project: project, current_user: user).close_issue(issue, closed_via: closing_commit) end + end + + let(:closing_commit) { create(:commit, project: project) } + + it_behaves_like 'removes labels marked for removal from issue when closed' + + it 'mentions closure via a commit' do + close_issue email = ActionMailer::Base.deliveries.last @@ -199,9 +237,8 @@ RSpec.describe Issues::CloseService do context 'when user cannot read the commit' 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, user).close_issue(issue, closed_via: closing_commit) - end + + close_issue email = ActionMailer::Base.deliveries.last body_text = email.body.parts.map(&:body).join(" ") @@ -216,10 +253,20 @@ RSpec.describe Issues::CloseService do context "valid params" do subject(:close_issue) do perform_enqueued_jobs do - described_class.new(project, user).close_issue(issue) + described_class.new(project: project, current_user: user).close_issue(issue) end end + it 'verifies the number of queries' do + recorded = ActiveRecord::QueryRecorder.new { close_issue } + expected_queries = 32 + + expect(recorded.count).to be <= expected_queries + expect(recorded.cached_count).to eq(0) + end + + it_behaves_like 'removes labels marked for removal from issue when closed' + it 'closes the issue' do close_issue @@ -230,7 +277,7 @@ RSpec.describe Issues::CloseService do it 'records closed user' do close_issue - expect(issue.closed_by_id).to be(user.id) + expect(issue.reload.closed_by_id).to be(user.id) end it 'sends email to user2 about assign of new issue', :sidekiq_might_not_need_inline do @@ -249,11 +296,23 @@ RSpec.describe Issues::CloseService do end it 'marks todos as done' do + todo = create(:todo, :assigned, user: user, project: project, target: issue, author: user2) + close_issue expect(todo.reload).to be_done end + context 'when closing the issue fails' do + it 'does not assign a closed_by value for the issue' do + allow(issue).to receive(:close).and_return(false) + + close_issue + + expect(issue.closed_by_id).to be_nil + end + end + context 'when there is an associated Alert Management Alert' do context 'when alert can be resolved' do let!(:alert) { create(:alert_management_alert, issue: issue, project: project) } @@ -303,26 +362,32 @@ RSpec.describe Issues::CloseService do end context 'when issue is not confidential' do + it_behaves_like 'removes labels marked for removal from issue when closed' + it 'executes issue hooks' do expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks) expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks) - described_class.new(project, user).close_issue(issue) + close_issue end end context 'when issue is confidential' do - it 'executes confidential issue hooks' do - issue = create(:issue, :confidential, project: project) + let(:issue) { create(:issue, :confidential, project: project) } + + it_behaves_like 'removes labels marked for removal from issue when closed' + it 'executes confidential issue hooks' do expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks) expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks) - described_class.new(project, user).close_issue(issue) + close_issue end end context 'internal issues disabled' do + let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) } + before do project.issues_enabled = false project.save! diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 83c6373c335..9c84242d8ae 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -12,7 +12,7 @@ RSpec.describe Issues::CreateService do let_it_be(:assignee) { create(:user) } let_it_be(:milestone) { create(:milestone, project: project) } - let(:issue) { described_class.new(project, user, opts).execute } + let(:issue) { described_class.new(project: project, current_user: user, params: opts).execute } context 'when params are valid' do let_it_be(:labels) { create_pair(:label, project: project) } @@ -44,7 +44,7 @@ RSpec.describe Issues::CreateService do end context 'when skip_system_notes is true' do - let(:issue) { described_class.new(project, user, opts).execute(skip_system_notes: true) } + let(:issue) { described_class.new(project: project, current_user: user, params: opts).execute(skip_system_notes: true) } it 'does not call Issuable::CommonSystemNotesService' do expect(Issuable::CommonSystemNotesService).not_to receive(:new) @@ -96,7 +96,7 @@ RSpec.describe Issues::CreateService do end it 'filters out params that cannot be set without the :admin_issue permission' do - issue = described_class.new(project, guest, opts).execute + issue = described_class.new(project: project, current_user: guest, params: opts).execute expect(issue).to be_persisted expect(issue.title).to eq('Awesome issue') @@ -108,7 +108,7 @@ RSpec.describe Issues::CreateService do end it 'creates confidential issues' do - issue = described_class.new(project, guest, confidential: true).execute + issue = described_class.new(project: project, current_user: guest, params: { confidential: true }).execute expect(issue.confidential).to be_truthy end @@ -117,7 +117,7 @@ RSpec.describe Issues::CreateService do it 'moves the issue to the end, in an asynchronous worker' do expect(IssuePlacementWorker).to receive(:perform_async).with(be_nil, Integer) - described_class.new(project, user, opts).execute + described_class.new(project: project, current_user: user, params: opts).execute end context 'when label belongs to project group' do @@ -204,7 +204,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, user, opts).execute + described_class.new(project: project, current_user: user, params: opts).execute expect(assignee.assigned_open_issues_count).to eq 1 end @@ -230,7 +230,7 @@ RSpec.describe Issues::CreateService do expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks) expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks) - described_class.new(project, user, opts).execute + described_class.new(project: project, current_user: user, params: opts).execute end it 'executes confidential issue hooks when issue is confidential' do @@ -239,7 +239,7 @@ RSpec.describe Issues::CreateService do expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks) expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks) - described_class.new(project, user, opts).execute + described_class.new(project: project, current_user: user, params: opts).execute end context 'after_save callback to store_mentions' do @@ -283,7 +283,7 @@ RSpec.describe Issues::CreateService do it 'removes assignee when user id is invalid' do opts = { title: 'Title', description: 'Description', assignee_ids: [-1] } - issue = described_class.new(project, user, opts).execute + issue = described_class.new(project: project, current_user: user, params: opts).execute expect(issue.assignees).to be_empty end @@ -291,7 +291,7 @@ RSpec.describe Issues::CreateService do it 'removes assignee when user id is 0' do opts = { title: 'Title', description: 'Description', assignee_ids: [0] } - issue = described_class.new(project, user, opts).execute + issue = described_class.new(project: project, current_user: user, params: opts).execute expect(issue.assignees).to be_empty end @@ -300,7 +300,7 @@ RSpec.describe Issues::CreateService do project.add_maintainer(assignee) opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] } - issue = described_class.new(project, user, opts).execute + issue = described_class.new(project: project, current_user: user, params: opts).execute expect(issue.assignees).to eq([assignee]) end @@ -318,7 +318,7 @@ RSpec.describe Issues::CreateService do project.update!(visibility_level: level) opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] } - issue = described_class.new(project, user, opts).execute + issue = described_class.new(project: project, current_user: user, params: opts).execute expect(issue.assignees).to be_empty end @@ -328,7 +328,7 @@ RSpec.describe Issues::CreateService do end it_behaves_like 'issuable record that supports quick actions' do - let(:issuable) { described_class.new(project, user, params).execute } + let(:issuable) { described_class.new(project: project, current_user: user, params: params).execute } end context 'Quick actions' do @@ -368,14 +368,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, user, opts).execute + described_class.new(project: project, current_user: user, params: opts).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, user, opts).execute + described_class.new(project: project, current_user: user, params: opts).execute reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first @@ -383,17 +383,19 @@ RSpec.describe Issues::CreateService do end it 'assigns the title and description for the issue' do - issue = described_class.new(project, user, opts).execute + issue = described_class.new(project: project, current_user: user, params: opts).execute expect(issue.title).not_to be_nil expect(issue.description).not_to be_nil end it 'can set nil explicitly to the title and description' do - issue = described_class.new(project, user, - merge_request_to_resolve_discussions_of: merge_request, - description: nil, - title: nil).execute + issue = described_class.new(project: project, current_user: user, + params: { + merge_request_to_resolve_discussions_of: merge_request, + description: nil, + title: nil + }).execute expect(issue.description).to be_nil expect(issue.title).to be_nil @@ -404,14 +406,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, user, opts).execute + described_class.new(project: project, current_user: user, params: opts).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, user, opts).execute + described_class.new(project: project, current_user: user, params: opts).execute reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first @@ -419,17 +421,19 @@ RSpec.describe Issues::CreateService do end it 'assigns the title and description for the issue' do - issue = described_class.new(project, user, opts).execute + issue = described_class.new(project: project, current_user: user, params: opts).execute expect(issue.title).not_to be_nil expect(issue.description).not_to be_nil end it 'can set nil explicitly to the title and description' do - issue = described_class.new(project, user, - merge_request_to_resolve_discussions_of: merge_request, - description: nil, - title: nil).execute + issue = described_class.new(project: project, current_user: user, + params: { + merge_request_to_resolve_discussions_of: merge_request, + description: nil, + title: nil + }).execute expect(issue.description).to be_nil expect(issue.title).to be_nil @@ -454,7 +458,7 @@ RSpec.describe Issues::CreateService do end subject do - described_class.new(project, user, params) + described_class.new(project: project, current_user: user, params: params) end before do diff --git a/spec/services/issues/duplicate_service_spec.rb b/spec/services/issues/duplicate_service_spec.rb index 0b5bc3f32ef..0eb0bbb1480 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(duplicate_project, user, {}) } + subject { described_class.new(project: duplicate_project, current_user: user) } describe '#execute' do context 'when the issues passed are the same' do diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 2f29a2e2022..76588860957 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -20,7 +20,7 @@ RSpec.describe Issues::MoveService do end subject(:move_service) do - described_class.new(old_project, user) + described_class.new(project: 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 bf7a4c97e48..dc55ba8ebea 100644 --- a/spec/services/issues/referenced_merge_requests_service_spec.rb +++ b/spec/services/issues/referenced_merge_requests_service_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper.rb' +require 'spec_helper' RSpec.describe Issues::ReferencedMergeRequestsService do def create_referencing_mr(attributes = {}) @@ -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, user) } + let(:service) { described_class.new(project: 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 c9c029bca4f..7a4bae7f852 100644 --- a/spec/services/issues/related_branches_service_spec.rb +++ b/spec/services/issues/related_branches_service_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Issues::RelatedBranchesService do let(:user) { developer } - subject { described_class.new(issue.project, user) } + subject { described_class.new(project: issue.project, current_user: user) } before do issue.project.add_developer(developer) @@ -95,7 +95,7 @@ RSpec.describe Issues::RelatedBranchesService do merge_request.create_cross_references!(user) referenced_merge_requests = Issues::ReferencedMergeRequestsService - .new(issue.project, user) + .new(project: 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 ffe74cca9cf..746a9105531 100644 --- a/spec/services/issues/reopen_service_spec.rb +++ b/spec/services/issues/reopen_service_spec.rb @@ -13,7 +13,7 @@ RSpec.describe Issues::ReopenService do project.add_guest(guest) perform_enqueued_jobs do - described_class.new(project, guest).execute(issue) + described_class.new(project: project, current_user: guest).execute(issue) end end @@ -33,11 +33,11 @@ RSpec.describe Issues::ReopenService do issue.assignees << user expect_any_instance_of(User).to receive(:invalidate_issue_cache_counts) - described_class.new(project, user).execute(issue) + described_class.new(project: project, current_user: user).execute(issue) end it 'refreshes the number of opened issues' do - service = described_class.new(project, user) + service = described_class.new(project: project, current_user: user) expect { service.execute(issue) } .to change { project.open_issues_count }.from(0).to(1) @@ -50,14 +50,14 @@ RSpec.describe Issues::ReopenService do expect(service).to receive(:delete_cache).and_call_original end - described_class.new(project, user).execute(issue) + described_class.new(project: project, current_user: user).execute(issue) end context 'issue is incident type' do let(:issue) { create(:incident, :closed, project: project) } let(:current_user) { user } - subject { described_class.new(project, user).execute(issue) } + subject { described_class.new(project: project, current_user: user).execute(issue) } it_behaves_like 'an incident management tracked event', :incident_management_incident_reopened end @@ -67,7 +67,7 @@ RSpec.describe Issues::ReopenService do expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks) expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks) - described_class.new(project, user).execute(issue) + described_class.new(project: project, current_user: user).execute(issue) end end @@ -78,7 +78,7 @@ RSpec.describe Issues::ReopenService do expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks) expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks) - described_class.new(project, user).execute(issue) + described_class.new(project: project, current_user: user).execute(issue) end end end diff --git a/spec/services/issues/reorder_service_spec.rb b/spec/services/issues/reorder_service_spec.rb index 78b937a1caf..15668a3aa23 100644 --- a/spec/services/issues/reorder_service_spec.rb +++ b/spec/services/issues/reorder_service_spec.rb @@ -75,7 +75,7 @@ RSpec.describe Issues::ReorderService do match_params = { move_between_ids: [issue2.id, issue3.id], board_group_id: group.id } expect(Issues::UpdateService) - .to receive(:new).with(project, user, match_params) + .to receive(:new).with(project: project, current_user: user, params: match_params) .and_return(double(execute: build(:issue))) subject.execute(issue1) @@ -95,6 +95,6 @@ RSpec.describe Issues::ReorderService do end def service(params) - described_class.new(project, user, params) + described_class.new(project: project, current_user: user, params: params) end end diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb index 9fbc9cbcca6..1ac71b966bc 100644 --- a/spec/services/issues/resolve_discussions_spec.rb +++ b/spec/services/issues/resolve_discussions_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper.rb' +require 'spec_helper' RSpec.describe Issues::ResolveDiscussions do let(:project) { create(:project, :repository) } @@ -11,7 +11,7 @@ RSpec.describe Issues::ResolveDiscussions do DummyService.class_eval do include ::Issues::ResolveDiscussions - def initialize(*args) + def initialize(project:, current_user: nil, params: {}) super filter_resolve_discussion_params end @@ -26,7 +26,7 @@ RSpec.describe Issues::ResolveDiscussions do let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: "fix") } describe "#merge_request_for_resolving_discussion" do - let(:service) { DummyService.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) } + let(:service) { DummyService.new(project: project, current_user: user, params: { merge_request_to_resolve_discussions_of: merge_request.iid }) } it "finds the merge request" do expect(service.merge_request_to_resolve_discussions_of).to eq(merge_request) @@ -45,10 +45,12 @@ RSpec.describe Issues::ResolveDiscussions do describe "#discussions_to_resolve" do it "contains a single discussion when matching merge request and discussion are passed" do service = DummyService.new( - project, - user, - discussion_to_resolve: discussion.id, - merge_request_to_resolve_discussions_of: merge_request.iid + project: project, + current_user: user, + params: { + discussion_to_resolve: discussion.id, + merge_request_to_resolve_discussions_of: merge_request.iid + } ) # We need to compare discussion id's because the Discussion-objects are rebuilt # which causes the object-id's not to be different. @@ -63,9 +65,9 @@ RSpec.describe Issues::ResolveDiscussions do project: merge_request.target_project, line_number: 15)]) service = DummyService.new( - project, - user, - merge_request_to_resolve_discussions_of: merge_request.iid + project: project, + current_user: user, + params: { merge_request_to_resolve_discussions_of: merge_request.iid } ) # We need to compare discussion id's because the Discussion-objects are rebuilt # which causes the object-id's not to be different. @@ -81,9 +83,9 @@ RSpec.describe Issues::ResolveDiscussions do line_number: 15 )]) service = DummyService.new( - project, - user, - merge_request_to_resolve_discussions_of: merge_request.iid + project: project, + current_user: user, + params: { merge_request_to_resolve_discussions_of: merge_request.iid } ) # We need to compare discussion id's because the Discussion-objects are rebuilt # which causes the object-id's not to be different. @@ -94,10 +96,12 @@ RSpec.describe Issues::ResolveDiscussions do it "is empty when a discussion and another merge request are passed" do service = DummyService.new( - project, - user, - discussion_to_resolve: discussion.id, - merge_request_to_resolve_discussions_of: other_merge_request.iid + project: project, + current_user: user, + params: { + discussion_to_resolve: discussion.id, + merge_request_to_resolve_discussions_of: other_merge_request.iid + } ) expect(service.discussions_to_resolve).to be_empty diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index fd42a84e405..8c97dd95ced 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -41,7 +41,7 @@ RSpec.describe Issues::UpdateService, :mailer do end def update_issue(opts) - described_class.new(project, user, opts).execute(issue) + described_class.new(project: project, current_user: user, params: opts).execute(issue) end context 'valid params' do @@ -165,20 +165,38 @@ RSpec.describe Issues::UpdateService, :mailer do expect(user2.assigned_open_issues_count).to eq 1 end - it 'sorts issues as specified by parameters' do - issue1 = create(:issue, project: project, assignees: [user3]) - issue2 = create(:issue, project: project, assignees: [user3]) + context 'when changing relative position' do + let(:issue1) { create(:issue, project: project, assignees: [user3]) } + let(:issue2) { create(:issue, project: project, assignees: [user3]) } - [issue, issue1, issue2].each do |issue| - issue.move_to_end - issue.save! + before do + [issue, issue1, issue2].each do |issue| + issue.move_to_end + issue.save! + end end - opts[:move_between_ids] = [issue1.id, issue2.id] + it 'sorts issues as specified by parameters' do + opts[:move_between_ids] = [issue1.id, issue2.id] - update_issue(opts) + update_issue(opts) - expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position) + expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position) + end + + context 'when block_issue_positioning flag is enabled' do + before do + stub_feature_flags(block_issue_repositioning: true) + end + + it 'raises error' do + old_position = issue.relative_position + opts[:move_between_ids] = [issue1.id, issue2.id] + + expect { update_issue(opts) }.to raise_error(::Gitlab::RelativePositioning::IssuePositioningDisabled) + expect(issue.reload.relative_position).to eq(old_position) + end + end end it 'does not rebalance even if needed if the flag is disabled' do @@ -269,7 +287,7 @@ RSpec.describe Issues::UpdateService, :mailer do opts[:move_between_ids] = [issue_1.id, issue_2.id] opts[:board_group_id] = group.id - described_class.new(issue_3.project, user, opts).execute(issue_3) + described_class.new(project: 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 @@ -282,7 +300,12 @@ RSpec.describe Issues::UpdateService, :mailer do end it 'filters out params that cannot be set without the :admin_issue permission' do - described_class.new(project, guest, opts.merge(confidential: true)).execute(issue) + described_class.new( + project: project, current_user: guest, params: opts.merge( + confidential: true, + issue_type: 'test_case' + ) + ).execute(issue) expect(issue).to be_valid expect(issue.title).to eq 'New title' @@ -293,6 +316,7 @@ RSpec.describe Issues::UpdateService, :mailer do expect(issue.due_date).to be_nil expect(issue.discussion_locked).to be_falsey expect(issue.confidential).to be_falsey + expect(issue.issue_type).to eql('issue') end end @@ -650,7 +674,7 @@ RSpec.describe Issues::UpdateService, :mailer do opts = { label_ids: [label.id] } perform_enqueued_jobs do - @issue = described_class.new(project, user, opts).execute(issue) + @issue = described_class.new(project: project, current_user: user, params: opts).execute(issue) end should_email(subscriber) @@ -666,7 +690,7 @@ RSpec.describe Issues::UpdateService, :mailer do opts = { label_ids: [label.id, label2.id] } perform_enqueued_jobs do - @issue = described_class.new(project, user, opts).execute(issue) + @issue = described_class.new(project: project, current_user: user, params: opts).execute(issue) end should_not_email(subscriber) @@ -677,7 +701,7 @@ RSpec.describe Issues::UpdateService, :mailer do opts = { label_ids: [label2.id] } perform_enqueued_jobs do - @issue = described_class.new(project, user, opts).execute(issue) + @issue = described_class.new(project: project, current_user: user, params: opts).execute(issue) end should_not_email(subscriber) @@ -709,7 +733,7 @@ RSpec.describe Issues::UpdateService, :mailer do line_number: 1 } } - service = described_class.new(project, user, params) + service = described_class.new(project: project, current_user: user, params: params) expect(Spam::SpamActionService).not_to receive(:new) @@ -785,7 +809,7 @@ RSpec.describe Issues::UpdateService, :mailer do context 'updating labels' do let(:label3) { create(:label, project: project) } - let(:result) { described_class.new(project, user, params).execute(issue).reload } + let(:result) { described_class.new(project: 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] } } @@ -983,14 +1007,14 @@ RSpec.describe Issues::UpdateService, :mailer do it 'raises an error for invalid move ids within a project' do opts = { move_between_ids: [9000, non_existing_record_id] } - expect { described_class.new(issue.project, user, opts).execute(issue) } + expect { described_class.new(project: issue.project, current_user: user, params: opts).execute(issue) } .to raise_error(ActiveRecord::RecordNotFound) end it 'raises an error for invalid move ids within a group' do opts = { move_between_ids: [9000, non_existing_record_id], board_group_id: create(:group).id } - expect { described_class.new(issue.project, user, opts).execute(issue) } + expect { described_class.new(project: issue.project, current_user: user, params: opts).execute(issue) } .to raise_error(ActiveRecord::RecordNotFound) end end @@ -1014,13 +1038,13 @@ RSpec.describe Issues::UpdateService, :mailer do with_them do it 'broadcasts to the issues channel based on ActionCable and feature flag values' do - expect(Gitlab::ActionCable::Config).to receive(:in_app?).and_return(action_cable_in_app_enabled) + allow(Gitlab::ActionCable::Config).to receive(:in_app?).and_return(action_cable_in_app_enabled) stub_feature_flags(broadcast_issue_updates: feature_flag_enabled) if should_broadcast - expect(IssuesChannel).to receive(:broadcast_to).with(issue, event: 'updated') + expect(GraphqlTriggers).to receive(:issuable_assignees_updated).with(issue) else - expect(IssuesChannel).not_to receive(:broadcast_to) + expect(GraphqlTriggers).not_to receive(:issuable_assignees_updated).with(issue) end update_issue(update_params) @@ -1030,7 +1054,7 @@ 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, user, params).execute(existing_issue) } + let(:issuable) { described_class.new(project: project, current_user: user, params: params).execute(existing_issue) } end end end diff --git a/spec/services/issues/zoom_link_service_spec.rb b/spec/services/issues/zoom_link_service_spec.rb index 8e8adc516cf..19db892fcae 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(issue, user) } + let(:service) { described_class.new(project: project, current_user: user, params: { issue: issue }) } let(:zoom_link) { 'https://zoom.us/j/123456789' } before do diff --git a/spec/services/labels/available_labels_service_spec.rb b/spec/services/labels/available_labels_service_spec.rb index 9ee0b80edcd..355dbd0c712 100644 --- a/spec/services/labels/available_labels_service_spec.rb +++ b/spec/services/labels/available_labels_service_spec.rb @@ -36,6 +36,15 @@ RSpec.describe Labels::AvailableLabelsService do expect(result).to include(project_label, group_label) expect(result).not_to include(other_project_label, other_group_label) end + + it 'do not cause additional query for finding labels' do + label_titles = [project_label.title] + control_count = ActiveRecord::QueryRecorder.new { described_class.new(user, project, labels: label_titles).find_or_create_by_titles } + + new_label = create(:label, project: project) + label_titles = [project_label.title, new_label.title] + expect { described_class.new(user, project, labels: label_titles).find_or_create_by_titles }.not_to exceed_query_limit(control_count) + end end end diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb index aa9eb0e6a0d..3ea2727dc60 100644 --- a/spec/services/labels/find_or_create_service_spec.rb +++ b/spec/services/labels/find_or_create_service_spec.rb @@ -25,6 +25,35 @@ RSpec.describe Labels::FindOrCreateService do project.add_developer(user) end + context 'when existing_labels_by_title is provided' do + let(:preloaded_label) { build(:label, title: 'Security') } + + before do + params.merge!( + existing_labels_by_title: { + 'Security' => preloaded_label + }) + end + + context 'when label exists' do + it 'returns preloaded label' do + expect(service.execute).to eq preloaded_label + end + end + + context 'when label does not exists' do + before do + params[:title] = 'Audit' + end + + it 'does not generates additional label search' do + service.execute + + expect(LabelsFinder).not_to receive(:new) + end + end + end + context 'when label does not exist at group level' do it 'creates a new label at project level' do expect { service.execute }.to change(project.labels, :count).by(1) diff --git a/spec/services/lfs/push_service_spec.rb b/spec/services/lfs/push_service_spec.rb index f67284ff48d..58fb2f3fb9b 100644 --- a/spec/services/lfs/push_service_spec.rb +++ b/spec/services/lfs/push_service_spec.rb @@ -63,6 +63,7 @@ RSpec.describe Lfs::PushService do it 'returns a failure when submitting a batch fails' do expect(lfs_client).to receive(:batch!) { raise 'failed' } + expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original expect(service.execute).to eq(status: :error, message: 'failed') end @@ -70,6 +71,7 @@ RSpec.describe Lfs::PushService do stub_lfs_batch(lfs_object) expect(lfs_client).to receive(:upload!) { raise 'failed' } + expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original expect(service.execute).to eq(status: :error, message: 'failed') end diff --git a/spec/services/merge_requests/add_context_service_spec.rb b/spec/services/merge_requests/add_context_service_spec.rb index 27b46a9023c..448be27efe8 100644 --- a/spec/services/merge_requests/add_context_service_spec.rb +++ b/spec/services/merge_requests/add_context_service_spec.rb @@ -9,7 +9,7 @@ RSpec.describe MergeRequests::AddContextService do let(:commits) { ["874797c3a73b60d2187ed6e2fcabd289ff75171e"] } let(:raw_repository) { project.repository.raw } - subject(:service) { described_class.new(project, admin, merge_request: merge_request, commits: commits) } + subject(:service) { described_class.new(project: project, current_user: admin, params: { merge_request: merge_request, commits: commits }) } describe "#execute" do context "when admin mode is enabled", :enable_admin_mode do @@ -32,7 +32,7 @@ RSpec.describe MergeRequests::AddContextService do let(:user) { create(:user) } let(:merge_request1) { create(:merge_request, source_project: project, author: user) } - subject(:service) { described_class.new(project, user, merge_request: merge_request, commits: commits) } + subject(:service) { described_class.new(project: project, current_user: user, params: { merge_request: merge_request, commits: commits }) } it "doesn't add context commit" do subject.execute @@ -42,7 +42,7 @@ RSpec.describe MergeRequests::AddContextService do end context "when the commits array is empty" do - subject(:service) { described_class.new(project, admin, merge_request: merge_request, commits: []) } + subject(:service) { described_class.new(project: project, current_user: admin, params: { merge_request: merge_request, commits: [] }) } it "doesn't add context commit" do subject.execute diff --git a/spec/services/merge_requests/add_spent_time_service_spec.rb b/spec/services/merge_requests/add_spent_time_service_spec.rb new file mode 100644 index 00000000000..db3380e9582 --- /dev/null +++ b/spec/services/merge_requests/add_spent_time_service_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequests::AddSpentTimeService do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public, :repository) } + let_it_be_with_reload(:merge_request) { create(:merge_request, :simple, :unique_branches, source_project: project) } + + let(:duration) { 1500 } + let(:params) { { spend_time: { duration: duration, user_id: user.id } } } + let(:service) { described_class.new(project: project, current_user: user, params: params) } + + describe '#execute' do + before do + project.add_developer(user) + end + + it 'creates a new timelog with the specified duration' do + expect { service.execute(merge_request) }.to change { Timelog.count }.from(0).to(1) + + timelog = merge_request.timelogs.last + + expect(timelog).not_to be_nil + expect(timelog.time_spent).to eq(1500) + end + + it 'creates a system note with the time added' do + expect { service.execute(merge_request) }.to change { Note.count }.from(0).to(1) + + system_note = merge_request.notes.last + + expect(system_note).not_to be_nil + expect(system_note.note_html).to include('added 25m of time spent') + end + + it 'saves usage data' do + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) + .to receive(:track_time_spent_changed_action).once.with(user: user) + + service.execute(merge_request) + end + + it 'is more efficient than using the full update-service' do + other_mr = create(:merge_request, :simple, :unique_branches, source_project: project) + + update_service = ::MergeRequests::UpdateService.new(project: project, current_user: user, params: params) + other_mr.reload + + expect { service.execute(merge_request) } + .to issue_fewer_queries_than { update_service.execute(other_mr) } + end + + context 'when duration is nil' do + let(:duration) { nil } + + it 'does not create a timelog with the specified duration' do + expect { service.execute(merge_request) }.not_to change { Timelog.count } + expect(merge_request).not_to be_valid + end + end + end +end diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb index 6edaa91b8b2..8d1abe5ea89 100644 --- a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb +++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb @@ -9,7 +9,7 @@ RSpec.describe ::MergeRequests::AddTodoWhenBuildFailsService do let(:ref) { merge_request.source_branch } let(:service) do - described_class.new(project, user, commit_message: 'Awesome message') + described_class.new(project: project, current_user: user, params: { commit_message: 'Awesome message' }) end let(:todo_service) { spy('todo service') } diff --git a/spec/services/merge_requests/after_create_service_spec.rb b/spec/services/merge_requests/after_create_service_spec.rb index e1f28e32164..cbbd193a411 100644 --- a/spec/services/merge_requests/after_create_service_spec.rb +++ b/spec/services/merge_requests/after_create_service_spec.rb @@ -6,7 +6,7 @@ RSpec.describe MergeRequests::AfterCreateService do let_it_be(:merge_request) { create(:merge_request) } subject(:after_create_service) do - described_class.new(merge_request.target_project, merge_request.author) + described_class.new(project: merge_request.target_project, current_user: merge_request.author) end describe '#execute' do @@ -191,7 +191,7 @@ RSpec.describe MergeRequests::AfterCreateService do it 'calls MergeRequests::LinkLfsObjectsService#execute' do service = instance_spy(MergeRequests::LinkLfsObjectsService) - allow(MergeRequests::LinkLfsObjectsService).to receive(:new).with(merge_request.target_project).and_return(service) + allow(MergeRequests::LinkLfsObjectsService).to receive(:new).with(project: merge_request.target_project).and_return(service) execute_service diff --git a/spec/services/merge_requests/approval_service_spec.rb b/spec/services/merge_requests/approval_service_spec.rb index df9a98c5540..d30b2721a36 100644 --- a/spec/services/merge_requests/approval_service_spec.rb +++ b/spec/services/merge_requests/approval_service_spec.rb @@ -9,7 +9,7 @@ RSpec.describe MergeRequests::ApprovalService do let(:project) { merge_request.project } let!(:todo) { create(:todo, user: user, project: project, target: merge_request) } - subject(:service) { described_class.new(project, user) } + subject(:service) { described_class.new(project: project, current_user: user) } before do project.add_developer(user) diff --git a/spec/services/merge_requests/assign_issues_service_spec.rb b/spec/services/merge_requests/assign_issues_service_spec.rb index 6398e8c533e..b857f26c052 100644 --- a/spec/services/merge_requests/assign_issues_service_spec.rb +++ b/spec/services/merge_requests/assign_issues_service_spec.rb @@ -7,7 +7,7 @@ RSpec.describe MergeRequests::AssignIssuesService do let(:project) { create(:project, :public, :repository) } let(:issue) { create(:issue, project: project) } let(:merge_request) { create(:merge_request, :simple, source_project: project, author: user, description: "fixes #{issue.to_reference}") } - let(:service) { described_class.new(project, user, merge_request: merge_request) } + let(:service) { described_class.new(project: project, current_user: user, params: { merge_request: merge_request }) } before do project.add_developer(user) @@ -37,10 +37,12 @@ RSpec.describe MergeRequests::AssignIssuesService do it 'accepts precomputed data for closes_issues' do issue2 = create(:issue, project: project) - service2 = described_class.new(project, - user, - merge_request: merge_request, - closes_issues: [issue, issue2]) + service2 = described_class.new(project: project, + current_user: user, + params: { + merge_request: merge_request, + closes_issues: [issue, issue2] + }) expect(service2.assignable_issues.count).to eq 2 end @@ -52,10 +54,12 @@ RSpec.describe MergeRequests::AssignIssuesService do it 'ignores external issues' do external_issue = ExternalIssue.new('JIRA-123', project) service = described_class.new( - project, - user, - merge_request: merge_request, - closes_issues: [external_issue] + project: project, + current_user: user, + params: { + merge_request: merge_request, + closes_issues: [external_issue] + } ) expect(service.assignable_issues.count).to eq 0 diff --git a/spec/services/merge_requests/base_service_spec.rb b/spec/services/merge_requests/base_service_spec.rb index d8ba2bc43fb..7911392ef19 100644 --- a/spec/services/merge_requests/base_service_spec.rb +++ b/spec/services/merge_requests/base_service_spec.rb @@ -17,7 +17,7 @@ RSpec.describe MergeRequests::BaseService do } end - subject { MergeRequests::CreateService.new(project, project.owner, params) } + subject { MergeRequests::CreateService.new(project: project, current_user: project.owner, params: params) } describe '#execute_hooks' do shared_examples 'enqueues Jira sync worker' do diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 8adf6d69f73..5a6a9df3f44 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -49,7 +49,7 @@ RSpec.describe MergeRequests::BuildService do end let(:service) do - described_class.new(project, user, params) + described_class.new(project: project, current_user: user, params: params) end before do @@ -100,7 +100,7 @@ RSpec.describe MergeRequests::BuildService do context 'with force_remove_source_branch parameter when the user is authorized' do let(:mr_params) { params.merge(force_remove_source_branch: '1') } let(:source_project) { fork_project(project, user) } - let(:merge_request) { described_class.new(project, user, mr_params).execute } + let(:merge_request) { described_class.new(project: project, current_user: user, params: mr_params).execute } before do project.add_reporter(user) diff --git a/spec/services/merge_requests/cleanup_refs_service_spec.rb b/spec/services/merge_requests/cleanup_refs_service_spec.rb index a1822a4d5ba..e8690ae5bf2 100644 --- a/spec/services/merge_requests/cleanup_refs_service_spec.rb +++ b/spec/services/merge_requests/cleanup_refs_service_spec.rb @@ -54,7 +54,7 @@ RSpec.describe MergeRequests::CleanupRefsService do context 'when merge request has merge ref' do before do MergeRequests::MergeToRefService - .new(merge_request.project, merge_request.author) + .new(project: merge_request.project, current_user: merge_request.author) .execute(merge_request) end diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index 48f56b3ec68..f6336a85a25 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -21,7 +21,7 @@ RSpec.describe MergeRequests::CloseService do it_behaves_like 'merge request reviewers cache counters invalidator' context 'valid params' do - let(:service) { described_class.new(project, user, {}) } + let(:service) { described_class.new(project: project, current_user: user) } before do allow(service).to receive(:execute_hooks) @@ -73,7 +73,7 @@ RSpec.describe MergeRequests::CloseService do expect(metrics_service).to receive(:close) - described_class.new(project, user, {}).execute(merge_request) + described_class.new(project: project, current_user: user).execute(merge_request) end it 'calls the merge request activity counter' do @@ -81,11 +81,11 @@ RSpec.describe MergeRequests::CloseService do .to receive(:track_close_mr_action) .with(user: user) - described_class.new(project, user, {}).execute(merge_request) + described_class.new(project: project, current_user: user).execute(merge_request) end it 'refreshes the number of open merge requests for a valid MR', :use_clean_rails_memory_store_caching do - service = described_class.new(project, user, {}) + service = described_class.new(project: project, current_user: user) expect { service.execute(merge_request) } .to change { project.open_merge_requests_count }.from(1).to(0) @@ -96,19 +96,19 @@ RSpec.describe MergeRequests::CloseService do expect(service).to receive(:execute_for_merge_request).with(merge_request) end - described_class.new(project, user).execute(merge_request) + described_class.new(project: project, current_user: user).execute(merge_request) end it 'schedules CleanupRefsService' do expect(MergeRequests::CleanupRefsService).to receive(:schedule).with(merge_request) - described_class.new(project, user).execute(merge_request) + described_class.new(project: project, current_user: user).execute(merge_request) end context 'current user is not authorized to close merge request' do before do perform_enqueued_jobs do - @merge_request = described_class.new(project, guest).execute(merge_request) + @merge_request = described_class.new(project: project, current_user: guest).execute(merge_request) end end 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 6528edfc8b7..749b30bff5f 100644 --- a/spec/services/merge_requests/create_from_issue_service_spec.rb +++ b/spec/services/merge_requests/create_from_issue_service_spec.rb @@ -11,8 +11,8 @@ RSpec.describe MergeRequests::CreateFromIssueService do let(:milestone_id) { create(:milestone, project: project).id } let(:issue) { create(:issue, project: project, milestone_id: milestone_id) } let(:custom_source_branch) { 'custom-source-branch' } - let(:service) { described_class.new(project, user, service_params) } - let(:service_with_custom_source_branch) { described_class.new(project, user, branch_name: custom_source_branch, **service_params) } + let(:service) { described_class.new(project: project, current_user: user, mr_params: service_params) } + let(:service_with_custom_source_branch) { described_class.new(project: project, current_user: user, mr_params: { branch_name: custom_source_branch, **service_params }) } before do project.add_developer(user) @@ -21,14 +21,14 @@ RSpec.describe MergeRequests::CreateFromIssueService do describe '#execute' do shared_examples_for 'a service that creates a merge request from an issue' do it 'returns an error when user can not create merge request on target project' do - result = described_class.new(project, create(:user), service_params).execute + result = described_class.new(project: project, current_user: create(:user), mr_params: service_params).execute expect(result[:status]).to eq(:error) expect(result[:message]).to eq('Not allowed to create merge request') end it 'returns an error with invalid issue iid' do - result = described_class.new(project, user, issue_iid: -1).execute + result = described_class.new(project: project, current_user: user, mr_params: { issue_iid: -1 }).execute expect(result[:status]).to eq(:error) expect(result[:message]).to eq('Invalid issue iid') @@ -123,7 +123,7 @@ RSpec.describe MergeRequests::CreateFromIssueService do end context 'when ref branch is set', :sidekiq_might_not_need_inline do - subject { described_class.new(project, user, ref: 'feature', **service_params).execute } + subject { described_class.new(project: project, current_user: user, mr_params: { ref: 'feature', **service_params }).execute } it 'sets the merge request source branch to the new issue branch' do expect(subject[:merge_request].source_branch).to eq(issue.to_branch_name) @@ -134,7 +134,7 @@ RSpec.describe MergeRequests::CreateFromIssueService do end context 'when the ref is a tag' do - subject { described_class.new(project, user, ref: 'v1.0.0', **service_params).execute } + subject { described_class.new(project: project, current_user: user, mr_params: { ref: 'v1.0.0', **service_params }).execute } it 'sets the merge request source branch to the new issue branch' do expect(subject[:merge_request].source_branch).to eq(issue.to_branch_name) @@ -150,7 +150,7 @@ RSpec.describe MergeRequests::CreateFromIssueService do end context 'when ref branch does not exist' do - subject { described_class.new(project, user, ref: 'no-such-branch', **service_params).execute } + subject { described_class.new(project: project, current_user: user, mr_params: { ref: 'no-such-branch', **service_params }).execute } it 'creates a merge request' do expect { subject }.to change(target_project.merge_requests, :count).by(1) diff --git a/spec/services/merge_requests/create_pipeline_service_spec.rb b/spec/services/merge_requests/create_pipeline_service_spec.rb index 3e2e940dc24..a0ac168f3d7 100644 --- a/spec/services/merge_requests/create_pipeline_service_spec.rb +++ b/spec/services/merge_requests/create_pipeline_service_spec.rb @@ -8,7 +8,7 @@ RSpec.describe MergeRequests::CreatePipelineService do let_it_be(:project, reload: true) { create(:project, :repository) } let_it_be(:user) { create(:user) } - let(:service) { described_class.new(project, actor, params) } + let(:service) { described_class.new(project: project, current_user: actor, params: params) } let(:actor) { user } let(:params) { {} } diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index f2bc55103f0..b2351ab53bd 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -21,7 +21,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do } end - let(:service) { described_class.new(project, user, opts) } + let(:service) { described_class.new(project: project, current_user: user, params: opts) } let(:merge_request) { service.execute } before do @@ -347,12 +347,12 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do } end - let(:issuable) { described_class.new(project, user, params).execute } + let(:issuable) { described_class.new(project: project, current_user: user, params: params).execute } end context 'Quick actions' do context 'with assignee and milestone in params and command' do - let(:merge_request) { described_class.new(project, user, opts).execute } + let(:merge_request) { described_class.new(project: project, current_user: user, params: opts).execute } let(:milestone) { create(:milestone, project: project) } let(:opts) do @@ -390,7 +390,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do it 'removes assignee_id when user id is invalid' do opts = { title: 'Title', description: 'Description', assignee_ids: [-1] } - merge_request = described_class.new(project, user, opts).execute + merge_request = described_class.new(project: project, current_user: user, params: opts).execute expect(merge_request.assignee_ids).to be_empty end @@ -398,7 +398,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do it 'removes assignee_id when user id is 0' do opts = { title: 'Title', description: 'Description', assignee_ids: [0] } - merge_request = described_class.new(project, user, opts).execute + merge_request = described_class.new(project: project, current_user: user, params: opts).execute expect(merge_request.assignee_ids).to be_empty end @@ -407,7 +407,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do project.add_maintainer(user2) opts = { title: 'Title', description: 'Description', assignee_ids: [user2.id] } - merge_request = described_class.new(project, user, opts).execute + merge_request = described_class.new(project: project, current_user: user, params: opts).execute expect(merge_request.assignees).to eq([user2]) end @@ -426,7 +426,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do it 'invalidates open merge request counter for assignees when merge request is assigned' do project.add_maintainer(user2) - described_class.new(project, user, opts).execute + described_class.new(project: project, current_user: user, params: opts).execute expect(user2.assigned_open_merge_requests_count).to eq 1 end @@ -445,7 +445,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do project.update!(visibility_level: level) opts = { title: 'Title', description: 'Description', assignee_ids: [user2.id] } - merge_request = described_class.new(project, user, opts).execute + merge_request = described_class.new(project: project, current_user: user, params: opts).execute expect(merge_request.assignee_id).to be_nil end @@ -473,7 +473,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do end it 'raises an error' do - expect { described_class.new(project, user, opts).execute } + expect { described_class.new(project: project, current_user: user, params: opts).execute } .to raise_error Gitlab::Access::AccessDeniedError end end @@ -485,7 +485,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do end it 'raises an error' do - expect { described_class.new(project, user, opts).execute } + expect { described_class.new(project: project, current_user: user, params: opts).execute } .to raise_error Gitlab::Access::AccessDeniedError end end @@ -497,7 +497,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do end it 'creates the merge request', :sidekiq_might_not_need_inline do - merge_request = described_class.new(project, user, opts).execute + merge_request = described_class.new(project: project, current_user: user, params: opts).execute expect(merge_request).to be_persisted end @@ -505,7 +505,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do it 'does not create the merge request when the target project is archived' do target_project.update!(archived: true) - expect { described_class.new(project, user, opts).execute } + expect { described_class.new(project: project, current_user: user, params: opts).execute } .to raise_error Gitlab::Access::AccessDeniedError end end @@ -529,7 +529,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do end it 'ignores source_project_id' do - merge_request = described_class.new(project, user, opts).execute + merge_request = described_class.new(project: project, current_user: user, params: opts).execute expect(merge_request.source_project_id).to eq(project.id) end diff --git a/spec/services/merge_requests/ff_merge_service_spec.rb b/spec/services/merge_requests/ff_merge_service_spec.rb index aec5a3b3fa3..24a1a8b3113 100644 --- a/spec/services/merge_requests/ff_merge_service_spec.rb +++ b/spec/services/merge_requests/ff_merge_service_spec.rb @@ -23,7 +23,7 @@ RSpec.describe MergeRequests::FfMergeService do describe '#execute' do context 'valid params' do - let(:service) { described_class.new(project, user, valid_merge_params) } + let(:service) { described_class.new(project: project, current_user: user, params: valid_merge_params) } def execute_ff_merge perform_enqueued_jobs do @@ -92,7 +92,7 @@ RSpec.describe MergeRequests::FfMergeService do end context 'error handling' do - let(:service) { described_class.new(project, user, valid_merge_params.merge(commit_message: 'Awesome message')) } + let(:service) { described_class.new(project: project, current_user: user, params: valid_merge_params.merge(commit_message: 'Awesome message')) } before do allow(Gitlab::AppLogger).to receive(:error) diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb index 053752626dc..5f81e1728fa 100644 --- a/spec/services/merge_requests/get_urls_service_spec.rb +++ b/spec/services/merge_requests/get_urls_service_spec.rb @@ -6,7 +6,7 @@ RSpec.describe MergeRequests::GetUrlsService do include ProjectForksHelper let(:project) { create(:project, :public, :repository) } - let(:service) { described_class.new(project) } + let(:service) { described_class.new(project: project) } let(:source_branch) { "merge-test" } let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.full_path}/-/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" } let(:show_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.full_path}/-/merge_requests/#{merge_request.iid}" } @@ -106,7 +106,7 @@ RSpec.describe MergeRequests::GetUrlsService do let!(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project, source_branch: source_branch) } let(:changes) { existing_branch_changes } # Source project is now the forked one - let(:service) { described_class.new(forked_project) } + let(:service) { described_class.new(project: forked_project) } before do allow(forked_project).to receive(:empty_repo?).and_return(false) diff --git a/spec/services/merge_requests/handle_assignees_change_service_spec.rb b/spec/services/merge_requests/handle_assignees_change_service_spec.rb index cc595aab04b..0bf18f16abb 100644 --- a/spec/services/merge_requests/handle_assignees_change_service_spec.rb +++ b/spec/services/merge_requests/handle_assignees_change_service_spec.rb @@ -10,7 +10,7 @@ RSpec.describe MergeRequests::HandleAssigneesChangeService do let_it_be(:old_assignees) { create_list(:user, 3) } let(:options) { {} } - let(:service) { described_class.new(project, user) } + let(:service) { described_class.new(project: project, current_user: user) } before_all do project.add_maintainer(user) @@ -38,18 +38,6 @@ RSpec.describe MergeRequests::HandleAssigneesChangeService do async_execute end - - context 'when async_handle_merge_request_assignees_change feature is disabled' do - before do - stub_feature_flags(async_handle_merge_request_assignees_change: false) - end - - it 'calls #execute' do - expect(service).to receive(:execute).with(merge_request, old_assignees, options) - - async_execute - end - end end describe '#execute' do 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 c1765e3a2ab..2fb6bbaf02f 100644 --- a/spec/services/merge_requests/link_lfs_objects_service_spec.rb +++ b/spec/services/merge_requests/link_lfs_objects_service_spec.rb @@ -18,7 +18,7 @@ RSpec.describe MergeRequests::LinkLfsObjectsService, :sidekiq_inline do ) end - subject { described_class.new(target_project) } + subject { described_class.new(project: target_project) } shared_examples_for 'linking LFS objects' do context 'when source project is the same as target project' do diff --git a/spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb b/spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb index 1075f6f9034..4d7bd3d8800 100644 --- a/spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb +++ b/spec/services/merge_requests/mark_reviewer_reviewed_service_spec.rb @@ -7,7 +7,7 @@ RSpec.describe MergeRequests::MarkReviewerReviewedService do let(:merge_request) { create(:merge_request, reviewers: [current_user]) } let(:reviewer) { merge_request.merge_request_reviewers.find_by(user_id: current_user.id) } let(:project) { merge_request.project } - let(:service) { described_class.new(project, current_user) } + let(:service) { described_class.new(project: project, current_user: current_user) } let(:result) { service.execute(merge_request) } before do @@ -16,7 +16,7 @@ RSpec.describe MergeRequests::MarkReviewerReviewedService do describe '#execute' do describe 'invalid permissions' do - let(:service) { described_class.new(project, create(:user)) } + let(:service) { described_class.new(project: project, current_user: create(:user)) } it 'returns an error' do expect(result[:status]).to eq :error @@ -24,7 +24,7 @@ RSpec.describe MergeRequests::MarkReviewerReviewedService do end describe 'reviewer does not exist' do - let(:service) { described_class.new(project, create(:user)) } + let(:service) { described_class.new(project: project, current_user: create(:user)) } it 'returns an error' do expect(result[:status]).to eq :error diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index c73cbad9d2f..ac39fb59c62 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe MergeRequests::MergeService do + include ExclusiveLeaseHelpers + let_it_be(:user) { create(:user) } let_it_be(:user2) { create(:user) } @@ -15,11 +17,14 @@ RSpec.describe MergeRequests::MergeService do end describe '#execute' do - let(:service) { described_class.new(project, user, merge_params) } + let(:service) { described_class.new(project: project, current_user: user, params: merge_params) } let(:merge_params) do { commit_message: 'Awesome message', sha: merge_request.diff_head_sha } end + let(:lease_key) { "merge_requests_merge_service:#{merge_request.id}" } + let!(:lease) { stub_exclusive_lease(lease_key) } + context 'valid params' do before do allow(service).to receive(:execute_hooks) @@ -90,6 +95,20 @@ RSpec.describe MergeRequests::MergeService do end end + context 'running the service multiple time' do + it 'is idempotent' do + 2.times { service.execute(merge_request) } + + expect(merge_request.merge_error).to be_falsey + expect(merge_request).to be_valid + expect(merge_request).to be_merged + + commit_messages = project.repository.commits('master', limit: 2).map(&:message) + expect(commit_messages.uniq.size).to eq(2) + expect(merge_request.in_progress_merge_commit_sha).to be_nil + end + end + context 'when an invalid sha is passed' do let(:merge_request) do create(:merge_request, :simple, @@ -209,7 +228,7 @@ RSpec.describe MergeRequests::MergeService do context 'source branch removal' do context 'when the source branch is protected' do let(:service) do - described_class.new(project, user, merge_params.merge('should_remove_source_branch' => true)) + described_class.new(project: project, current_user: user, params: merge_params.merge('should_remove_source_branch' => true)) end before do @@ -225,7 +244,7 @@ RSpec.describe MergeRequests::MergeService do context 'when the source branch is the default branch' do let(:service) do - described_class.new(project, user, merge_params.merge('should_remove_source_branch' => true)) + described_class.new(project: project, current_user: user, params: merge_params.merge('should_remove_source_branch' => true)) end before do @@ -251,7 +270,7 @@ RSpec.describe MergeRequests::MergeService do end context 'when the merger set the source branch not to be removed' do - let(:service) { described_class.new(project, user, merge_params.merge('should_remove_source_branch' => false)) } + let(:service) { described_class.new(project: project, current_user: user, params: merge_params.merge('should_remove_source_branch' => false)) } it 'does not delete the source branch' do expect(::MergeRequests::DeleteSourceBranchWorker).not_to receive(:perform_async) @@ -263,7 +282,7 @@ RSpec.describe MergeRequests::MergeService do context 'when MR merger set the source branch to be removed' do let(:service) do - described_class.new(project, user, merge_params.merge('should_remove_source_branch' => true)) + described_class.new(project: project, current_user: user, params: merge_params.merge('should_remove_source_branch' => true)) end it 'removes the source branch using the current user' do @@ -306,10 +325,12 @@ RSpec.describe MergeRequests::MergeService do end it 'logs and saves error if user is not authorized' do + stub_exclusive_lease + unauthorized_user = create(:user) project.add_reporter(unauthorized_user) - service = described_class.new(project, unauthorized_user) + service = described_class.new(project: project, current_user: unauthorized_user) service.execute(merge_request) @@ -423,6 +444,7 @@ RSpec.describe MergeRequests::MergeService do merge_request.project.update!(merge_method: merge_method) error_message = 'Only fast-forward merge is allowed for your project. Please update your source branch' allow(service).to receive(:execute_hooks) + expect(lease).to receive(:cancel) service.execute(merge_request) @@ -473,5 +495,17 @@ RSpec.describe MergeRequests::MergeService do end end end + + context 'when the other sidekiq worker has already been running' do + before do + stub_exclusive_lease_taken(lease_key) + end + + it 'does not execute service' do + expect(service).not_to receive(:commit) + + service.execute(merge_request) + end + end end end diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb index 938165a807c..bb764ff5672 100644 --- a/spec/services/merge_requests/merge_to_ref_service_spec.rb +++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb @@ -74,7 +74,7 @@ RSpec.describe MergeRequests::MergeToRefService do describe '#execute' do let(:service) do - described_class.new(project, user, **params) + described_class.new(project: project, current_user: user, params: params) end let(:params) { { commit_message: 'Awesome message', should_remove_source_branch: true, sha: merge_request.diff_head_sha } } @@ -94,7 +94,7 @@ RSpec.describe MergeRequests::MergeToRefService do it 'returns an error when Gitlab::Git::CommandError is raised during merge' do allow(project.repository).to receive(:merge_to_ref) do - raise Gitlab::Git::CommandError.new('Failed to create merge commit') + raise Gitlab::Git::CommandError, 'Failed to create merge commit' end result = service.execute(merge_request) @@ -111,11 +111,11 @@ RSpec.describe MergeRequests::MergeToRefService do end let(:merge_ref_service) do - described_class.new(project, user, {}) + described_class.new(project: project, current_user: user) end let(:merge_service) do - MergeRequests::MergeService.new(project, user, { sha: merge_request.diff_head_sha }) + MergeRequests::MergeService.new(project: project, current_user: user, params: { sha: merge_request.diff_head_sha }) end context 'when merge commit' do diff --git a/spec/services/merge_requests/mergeability_check_service_spec.rb b/spec/services/merge_requests/mergeability_check_service_spec.rb index e0baf5af8b4..65599b7e046 100644 --- a/spec/services/merge_requests/mergeability_check_service_spec.rb +++ b/spec/services/merge_requests/mergeability_check_service_spec.rb @@ -87,7 +87,7 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar described_class.new(merge_request).async_execute end - context 'when read only DB' do + context 'when read-only DB' do before do allow(Gitlab::Database).to receive(:read_only?) { true } end @@ -232,7 +232,7 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar context 'when MR cannot be merged and has outdated merge ref' do before do - MergeRequests::MergeToRefService.new(project, merge_request.author).execute(merge_request) + MergeRequests::MergeToRefService.new(project: project, current_user: merge_request.author).execute(merge_request) merge_request.mark_as_unmergeable! end @@ -258,7 +258,7 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar end end - context 'when read only DB' do + context 'when read-only DB' do it 'returns ServiceResponse.error' do allow(Gitlab::Database).to receive(:read_only?) { true } @@ -332,7 +332,7 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar context 'when MR is mergeable but merge-ref is already updated' do before do - MergeRequests::MergeToRefService.new(project, merge_request.author).execute(merge_request) + MergeRequests::MergeToRefService.new(project: project, current_user: merge_request.author).execute(merge_request) merge_request.mark_as_mergeable! end @@ -361,7 +361,7 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar context 'merge with conflicts' do it 'calls MergeToRefService with true allow_conflicts param' do expect(MergeRequests::MergeToRefService).to receive(:new) - .with(project, merge_request.author, { allow_conflicts: true }).and_call_original + .with(project: project, current_user: merge_request.author, params: { allow_conflicts: true }).and_call_original subject end @@ -373,7 +373,7 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar it 'calls MergeToRefService with false allow_conflicts param' do expect(MergeRequests::MergeToRefService).to receive(:new) - .with(project, merge_request.author, { allow_conflicts: false }).and_call_original + .with(project: project, current_user: merge_request.author, params: { allow_conflicts: false }).and_call_original subject end diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb index 247b053e729..14804aa33d4 100644 --- a/spec/services/merge_requests/post_merge_service_spec.rb +++ b/spec/services/merge_requests/post_merge_service_spec.rb @@ -9,7 +9,7 @@ RSpec.describe MergeRequests::PostMergeService do let_it_be(:merge_request, reload: true) { create(:merge_request, assignees: [user]) } let_it_be(:project) { merge_request.project } - subject { described_class.new(project, user).execute(merge_request) } + subject { described_class.new(project: project, current_user: user).execute(merge_request) } before do project.add_maintainer(user) @@ -22,7 +22,6 @@ RSpec.describe MergeRequests::PostMergeService do it 'refreshes the number of open merge requests for a valid MR', :use_clean_rails_memory_store_caching do # Cache the counter before the MR changed state. project.open_merge_requests_count - merge_request.update!(state: 'merged') expect { subject }.to change { project.open_merge_requests_count }.from(1).to(0) end diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb index b5086ea3a82..87c3fc6a2d8 100644 --- a/spec/services/merge_requests/push_options_handler_service_spec.rb +++ b/spec/services/merge_requests/push_options_handler_service_spec.rb @@ -11,7 +11,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do let_it_be(:user3) { create(:user, developer_projects: [project]) } let_it_be(:forked_project) { fork_project(project, user1, repository: true) } - let(:service) { described_class.new(project, user1, changes, push_options) } + let(:service) { described_class.new(project: project, current_user: user1, changes: changes, push_options: push_options) } let(:source_branch) { 'fix' } let(:target_branch) { 'feature' } let(:title) { 'my title' } diff --git a/spec/services/merge_requests/pushed_branches_service_spec.rb b/spec/services/merge_requests/pushed_branches_service_spec.rb index cd6af4c275e..59424263ec5 100644 --- a/spec/services/merge_requests/pushed_branches_service_spec.rb +++ b/spec/services/merge_requests/pushed_branches_service_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe MergeRequests::PushedBranchesService do let(:project) { create(:project) } - let!(:service) { described_class.new(project, nil, changes: pushed_branches) } + let!(:service) { described_class.new(project: project, current_user: nil, params: { changes: pushed_branches }) } context 'when branches pushed' do let(:pushed_branches) do diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb index 653fcf12a76..a46f3cf6148 100644 --- a/spec/services/merge_requests/rebase_service_spec.rb +++ b/spec/services/merge_requests/rebase_service_spec.rb @@ -18,7 +18,7 @@ RSpec.describe MergeRequests::RebaseService do let(:repository) { project.repository.raw } let(:skip_ci) { false } - subject(:service) { described_class.new(project, user, {}) } + subject(:service) { described_class.new(project: project, current_user: user) } before do project.add_maintainer(user) diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index f9b76db877b..6e6b4a91e0d 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -64,7 +64,7 @@ RSpec.describe MergeRequests::RefreshService do end context 'push to origin repo source branch' do - let(:refresh_service) { service.new(@project, @user) } + let(:refresh_service) { service.new(project: @project, current_user: @user) } let(:notification_service) { spy('notification_service') } before do @@ -187,7 +187,7 @@ RSpec.describe MergeRequests::RefreshService do context 'when pipeline exists for the source branch' do let!(:pipeline) { create(:ci_empty_pipeline, ref: @merge_request.source_branch, project: @project, sha: @commits.first.sha)} - subject { service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master') } + subject { service.new(project: @project, current_user: @user).execute(@oldrev, @newrev, 'refs/heads/master') } it 'updates the head_pipeline_id for @merge_request', :sidekiq_might_not_need_inline do expect { subject }.to change { @merge_request.reload.head_pipeline_id }.from(nil).to(pipeline.id) @@ -198,12 +198,12 @@ RSpec.describe MergeRequests::RefreshService do end end - shared_examples 'Pipelines for merge requests' do + context 'Pipelines for merge requests', :sidekiq_inline do before do stub_ci_pipeline_yaml_file(config) end - subject { service.new(project, @user).execute(@oldrev, @newrev, ref) } + subject { service.new(project: project, current_user: @user).execute(@oldrev, @newrev, ref) } let(:ref) { 'refs/heads/master' } let(:project) { @project } @@ -291,11 +291,11 @@ RSpec.describe MergeRequests::RefreshService do context "when MergeRequestUpdateWorker is retried by an exception" do it 'does not re-create a duplicate detached merge request pipeline' do expect do - service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master') + service.new(project: @project, current_user: @user).execute(@oldrev, @newrev, 'refs/heads/master') end.to change { @merge_request.pipelines_for_merge_request.count }.by(1) expect do - service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master') + service.new(project: @project, current_user: @user).execute(@oldrev, @newrev, 'refs/heads/master') end.not_to change { @merge_request.pipelines_for_merge_request.count } end end @@ -364,20 +364,8 @@ RSpec.describe MergeRequests::RefreshService do end end - context 'when the code_review_async_pipeline_creation feature flag is on', :sidekiq_inline do - it_behaves_like 'Pipelines for merge requests' - end - - context 'when the code_review_async_pipeline_creation feature flag is off', :sidekiq_inline do - before do - stub_feature_flags(code_review_async_pipeline_creation: false) - end - - it_behaves_like 'Pipelines for merge requests' - end - context 'push to origin repo source branch' do - let(:refresh_service) { service.new(@project, @user) } + let(:refresh_service) { service.new(project: @project, current_user: @user) } let(:notification_service) { spy('notification_service') } before do @@ -409,7 +397,7 @@ RSpec.describe MergeRequests::RefreshService do context 'push to origin repo target branch', :sidekiq_might_not_need_inline do context 'when all MRs to the target branch had diffs' do before do - service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') + service.new(project: @project, current_user: @user).execute(@oldrev, @newrev, 'refs/heads/feature') reload_mrs end @@ -438,7 +426,7 @@ RSpec.describe MergeRequests::RefreshService do # feature all along. empty_fork_merge_request.update_columns(target_branch: 'feature') - service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') + service.new(project: @project, current_user: @user).execute(@oldrev, @newrev, 'refs/heads/feature') reload_mrs empty_fork_merge_request.reload end @@ -461,7 +449,7 @@ RSpec.describe MergeRequests::RefreshService do # Merge master -> feature branch @project.repository.merge(@user, @merge_request.diff_head_sha, @merge_request, 'Test message') commit = @project.repository.commit('feature') - service.new(@project, @user).execute(@oldrev, commit.id, 'refs/heads/feature') + service.new(project: @project, current_user: @user).execute(@oldrev, commit.id, 'refs/heads/feature') reload_mrs end @@ -479,7 +467,7 @@ RSpec.describe MergeRequests::RefreshService do end context 'push to fork repo source branch', :sidekiq_might_not_need_inline do - let(:refresh_service) { service.new(@fork_project, @user) } + let(:refresh_service) { service.new(project: @fork_project, current_user: @user) } def refresh allow(refresh_service).to receive(:execute_hooks) @@ -546,7 +534,7 @@ RSpec.describe MergeRequests::RefreshService do context 'push to fork repo target branch', :sidekiq_might_not_need_inline do describe 'changes to merge requests' do before do - service.new(@fork_project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') + service.new(project: @fork_project, current_user: @user).execute(@oldrev, @newrev, 'refs/heads/feature') reload_mrs end @@ -563,7 +551,7 @@ RSpec.describe MergeRequests::RefreshService do describe 'merge request diff' do it 'does not reload the diff of the merge request made from fork' do expect do - service.new(@fork_project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') + service.new(project: @fork_project, current_user: @user).execute(@oldrev, @newrev, 'refs/heads/feature') end.not_to change { @fork_merge_request.reload.merge_request_diff } end end @@ -594,28 +582,28 @@ RSpec.describe MergeRequests::RefreshService do it 'reloads a new diff for a push to the forked project' do expect do - service.new(@fork_project, @user).execute(@oldrev, first_commit, 'refs/heads/master') + service.new(project: @fork_project, current_user: @user).execute(@oldrev, first_commit, 'refs/heads/master') reload_mrs end.to change { forked_master_mr.merge_request_diffs.count }.by(1) end it 'reloads a new diff for a force push to the source branch' do expect do - service.new(@fork_project, @user).execute(@oldrev, force_push_commit, 'refs/heads/master') + service.new(project: @fork_project, current_user: @user).execute(@oldrev, force_push_commit, 'refs/heads/master') reload_mrs end.to change { forked_master_mr.merge_request_diffs.count }.by(1) end it 'reloads a new diff for a force push to the target branch' do expect do - service.new(@project, @user).execute(@oldrev, force_push_commit, 'refs/heads/master') + service.new(project: @project, current_user: @user).execute(@oldrev, force_push_commit, 'refs/heads/master') reload_mrs end.to change { forked_master_mr.merge_request_diffs.count }.by(1) end it 'reloads a new diff for a push to the target project that contains a commit in the MR' do expect do - service.new(@project, @user).execute(@oldrev, first_commit, 'refs/heads/master') + service.new(project: @project, current_user: @user).execute(@oldrev, first_commit, 'refs/heads/master') reload_mrs end.to change { forked_master_mr.merge_request_diffs.count }.by(1) end @@ -626,7 +614,7 @@ RSpec.describe MergeRequests::RefreshService do branch_name: 'master') expect do - service.new(@project, @user).execute(@newrev, new_commit, 'refs/heads/master') + service.new(project: @project, current_user: @user).execute(@newrev, new_commit, 'refs/heads/master') reload_mrs end.not_to change { forked_master_mr.merge_request_diffs.count } end @@ -635,7 +623,7 @@ RSpec.describe MergeRequests::RefreshService do context 'push to origin repo target branch after fork project was removed' do before do @fork_project.destroy! - service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') + service.new(project: @project, current_user: @user).execute(@oldrev, @newrev, 'refs/heads/feature') reload_mrs end @@ -651,7 +639,7 @@ RSpec.describe MergeRequests::RefreshService do end context 'push new branch that exists in a merge request' do - let(:refresh_service) { service.new(@fork_project, @user) } + let(:refresh_service) { service.new(project: @fork_project, current_user: @user) } it 'refreshes the merge request', :sidekiq_might_not_need_inline do expect(refresh_service).to receive(:execute_hooks) @@ -700,7 +688,7 @@ RSpec.describe MergeRequests::RefreshService do source_branch: 'close-by-commit', source_project: project) - refresh_service = service.new(project, user) + refresh_service = service.new(project: project, current_user: user) allow(refresh_service).to receive(:execute_hooks) refresh_service.execute(@oldrev, @newrev, 'refs/heads/close-by-commit') @@ -723,7 +711,7 @@ RSpec.describe MergeRequests::RefreshService do source_branch: 'close-by-commit', source_project: forked_project) - refresh_service = service.new(forked_project, user) + refresh_service = service.new(project: forked_project, current_user: user) allow(refresh_service).to receive(:execute_hooks) refresh_service.execute(@oldrev, @newrev, 'refs/heads/close-by-commit') @@ -734,7 +722,7 @@ RSpec.describe MergeRequests::RefreshService do end context 'marking the merge request as draft' do - let(:refresh_service) { service.new(@project, @user) } + let(:refresh_service) { service.new(project: @project, current_user: @user) } before do allow(refresh_service).to receive(:execute_hooks) @@ -814,7 +802,7 @@ RSpec.describe MergeRequests::RefreshService do end describe 'updating merge_commit' do - let(:service) { described_class.new(project, user) } + let(:service) { described_class.new(project: project, current_user: user) } let(:user) { create(:user) } let(:project) { create(:project, :repository) } @@ -902,7 +890,7 @@ RSpec.describe MergeRequests::RefreshService do end let(:auto_merge_strategy) { AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS } - let(:refresh_service) { service.new(project, user) } + let(:refresh_service) { service.new(project: project, current_user: user) } before do target_project.merge_method = merge_method diff --git a/spec/services/merge_requests/reload_merge_head_diff_service_spec.rb b/spec/services/merge_requests/reload_merge_head_diff_service_spec.rb index 3152a4e3861..b333d4af6cf 100644 --- a/spec/services/merge_requests/reload_merge_head_diff_service_spec.rb +++ b/spec/services/merge_requests/reload_merge_head_diff_service_spec.rb @@ -10,7 +10,7 @@ RSpec.describe MergeRequests::ReloadMergeHeadDiffService do describe '#execute' do before do MergeRequests::MergeToRefService - .new(merge_request.project, merge_request.author) + .new(project: merge_request.project, current_user: merge_request.author) .execute(merge_request) end diff --git a/spec/services/merge_requests/remove_approval_service_spec.rb b/spec/services/merge_requests/remove_approval_service_spec.rb index 4ef2da290e1..ef6a0ec69bd 100644 --- a/spec/services/merge_requests/remove_approval_service_spec.rb +++ b/spec/services/merge_requests/remove_approval_service_spec.rb @@ -9,7 +9,7 @@ RSpec.describe MergeRequests::RemoveApprovalService do let(:merge_request) { create(:merge_request, source_project: project) } let!(:existing_approval) { create(:approval, merge_request: merge_request) } - subject(:service) { described_class.new(project, user) } + subject(:service) { described_class.new(project: project, current_user: user) } def execute! service.execute(merge_request) diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb index 8541d597581..b9df31b6727 100644 --- a/spec/services/merge_requests/reopen_service_spec.rb +++ b/spec/services/merge_requests/reopen_service_spec.rb @@ -20,7 +20,7 @@ RSpec.describe MergeRequests::ReopenService do it_behaves_like 'merge request reviewers cache counters invalidator' context 'valid params' do - let(:service) { described_class.new(project, user, {}) } + let(:service) { described_class.new(project: project, current_user: user) } before do allow(service).to receive(:execute_hooks) @@ -65,7 +65,7 @@ RSpec.describe MergeRequests::ReopenService do it 'caches merge request closing issues' do expect(merge_request).to receive(:cache_merge_request_closes_issues!) - described_class.new(project, user, {}).execute(merge_request) + described_class.new(project: project, current_user: user).execute(merge_request) end it 'updates metrics' do @@ -78,7 +78,7 @@ RSpec.describe MergeRequests::ReopenService do expect(service).to receive(:reopen) - described_class.new(project, user, {}).execute(merge_request) + described_class.new(project: project, current_user: user).execute(merge_request) end it 'calls the merge request activity counter' do @@ -86,11 +86,11 @@ RSpec.describe MergeRequests::ReopenService do .to receive(:track_reopen_mr_action) .with(user: user) - described_class.new(project, user, {}).execute(merge_request) + described_class.new(project: project, current_user: user).execute(merge_request) end it 'refreshes the number of open merge requests for a valid MR' do - service = described_class.new(project, user, {}) + service = described_class.new(project: project, current_user: user) expect { service.execute(merge_request) } .to change { project.open_merge_requests_count }.from(0).to(1) @@ -99,7 +99,7 @@ RSpec.describe MergeRequests::ReopenService do context 'current user is not authorized to reopen merge request' do before do perform_enqueued_jobs do - @merge_request = described_class.new(project, guest).execute(merge_request) + @merge_request = described_class.new(project: project, current_user: guest).execute(merge_request) end end diff --git a/spec/services/merge_requests/request_review_service_spec.rb b/spec/services/merge_requests/request_review_service_spec.rb index 5cb4120852a..8bc31df605c 100644 --- a/spec/services/merge_requests/request_review_service_spec.rb +++ b/spec/services/merge_requests/request_review_service_spec.rb @@ -8,7 +8,7 @@ RSpec.describe MergeRequests::RequestReviewService do let(:merge_request) { create(:merge_request, reviewers: [user]) } let(:reviewer) { merge_request.find_reviewer(user) } let(:project) { merge_request.project } - let(:service) { described_class.new(project, current_user) } + let(:service) { described_class.new(project: project, current_user: current_user) } let(:result) { service.execute(merge_request, user) } let(:todo_service) { spy('todo service') } let(:notification_service) { spy('notification service') } @@ -26,7 +26,7 @@ RSpec.describe MergeRequests::RequestReviewService do describe '#execute' do describe 'invalid permissions' do - let(:service) { described_class.new(project, create(:user)) } + let(:service) { described_class.new(project: project, current_user: create(:user)) } it 'returns an error' do expect(result[:status]).to eq :error diff --git a/spec/services/merge_requests/resolve_todos_service_spec.rb b/spec/services/merge_requests/resolve_todos_service_spec.rb index 3e6f2ea3f5d..53bd259f0f4 100644 --- a/spec/services/merge_requests/resolve_todos_service_spec.rb +++ b/spec/services/merge_requests/resolve_todos_service_spec.rb @@ -23,18 +23,6 @@ RSpec.describe MergeRequests::ResolveTodosService do async_execute end - - context 'when resolve_merge_request_todos_async feature is disabled' do - before do - stub_feature_flags(resolve_merge_request_todos_async: false) - end - - it 'calls #execute' do - expect(service).to receive(:execute) - - async_execute - end - end end describe '#execute' do diff --git a/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb b/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb index 874cf66659a..74f3a1b06fc 100644 --- a/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb +++ b/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb @@ -7,7 +7,7 @@ RSpec.describe MergeRequests::ResolvedDiscussionNotificationService do let(:user) { create(:user) } let(:project) { merge_request.project } - subject { described_class.new(project, user) } + subject { described_class.new(project: project, current_user: user) } describe "#execute" do context "when not all discussions are resolved" do diff --git a/spec/services/merge_requests/retarget_chain_service_spec.rb b/spec/services/merge_requests/retarget_chain_service_spec.rb index 3937fbe58c3..87bde4a1400 100644 --- a/spec/services/merge_requests/retarget_chain_service_spec.rb +++ b/spec/services/merge_requests/retarget_chain_service_spec.rb @@ -9,7 +9,7 @@ RSpec.describe MergeRequests::RetargetChainService do let_it_be(:merge_request, reload: true) { create(:merge_request, assignees: [user]) } let_it_be(:project) { merge_request.project } - subject { described_class.new(project, user).execute(merge_request) } + subject { described_class.new(project: project, current_user: user).execute(merge_request) } before do project.add_maintainer(user) diff --git a/spec/services/merge_requests/squash_service_spec.rb b/spec/services/merge_requests/squash_service_spec.rb index acbd0a42fcd..149748cdabc 100644 --- a/spec/services/merge_requests/squash_service_spec.rb +++ b/spec/services/merge_requests/squash_service_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe MergeRequests::SquashService do include GitHelpers - let(:service) { described_class.new(project, user, { merge_request: merge_request }) } + let(:service) { described_class.new(project: project, current_user: user, params: { merge_request: merge_request }) } let(:user) { project.owner } let(:project) { create(:project, :repository) } let(:repository) { project.repository.raw } @@ -62,7 +62,7 @@ RSpec.describe MergeRequests::SquashService do end it 'will still perform the squash when a custom squash commit message has been provided' do - service = described_class.new(project, user, { merge_request: merge_request, squash_commit_message: 'A custom commit message' }) + service = described_class.new(project: project, current_user: user, params: { merge_request: merge_request, squash_commit_message: 'A custom commit message' }) expect(merge_request.target_project.repository).to receive(:squash).and_return('sha') @@ -98,7 +98,7 @@ RSpec.describe MergeRequests::SquashService do end context 'if a message was provided' do - let(:service) { described_class.new(project, user, { merge_request: merge_request, squash_commit_message: message }) } + let(:service) { described_class.new(project: project, current_user: user, params: { merge_request: merge_request, squash_commit_message: message }) } let(:message) { 'My custom message' } let(:squash_sha) { service.execute[:squash_sha] } diff --git a/spec/services/merge_requests/update_assignees_service_spec.rb b/spec/services/merge_requests/update_assignees_service_spec.rb index de03aab5418..076161c9029 100644 --- a/spec/services/merge_requests/update_assignees_service_spec.rb +++ b/spec/services/merge_requests/update_assignees_service_spec.rb @@ -26,7 +26,7 @@ RSpec.describe MergeRequests::UpdateAssigneesService do project.add_developer(user3) end - let(:service) { described_class.new(project, user, opts) } + let(:service) { described_class.new(project: project, current_user: user, params: opts) } let(:opts) { { assignee_ids: [user2.id] } } describe 'execute' do @@ -36,8 +36,24 @@ RSpec.describe MergeRequests::UpdateAssigneesService do end context 'when the parameters are valid' do + context 'when using sentinel values' do + let(:opts) { { assignee_ids: [0] } } + + it 'removes all assignees' do + expect { update_merge_request }.to change(merge_request, :assignees).to([]) + end + end + + context 'the assignee_ids parameter is the empty list' do + let(:opts) { { assignee_ids: [] } } + + it 'removes all assignees' do + expect { update_merge_request }.to change(merge_request, :assignees).to([]) + end + end + it 'updates the MR, and queues the more expensive work for later' do - expect_next(MergeRequests::HandleAssigneesChangeService, project, user) do |service| + expect_next(MergeRequests::HandleAssigneesChangeService, project: project, current_user: user) do |service| expect(service) .to receive(:async_execute) .with(merge_request, [user3], execute_hooks: true) @@ -56,7 +72,7 @@ RSpec.describe MergeRequests::UpdateAssigneesService do end it 'is more efficient than using the full update-service' do - allow_next(MergeRequests::HandleAssigneesChangeService, project, user) do |service| + allow_next(MergeRequests::HandleAssigneesChangeService, project: project, current_user: user) do |service| expect(service) .to receive(:async_execute) .with(merge_request, [user3], execute_hooks: true) @@ -69,7 +85,7 @@ RSpec.describe MergeRequests::UpdateAssigneesService do source_project: merge_request.project, author: merge_request.author) - update_service = ::MergeRequests::UpdateService.new(project, user, opts) + update_service = ::MergeRequests::UpdateService.new(project: project, current_user: user, params: opts) expect { service.execute(merge_request) } .to issue_fewer_queries_than { update_service.execute(other_mr) } diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 8c010855eb2..a85fbd77d70 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -43,7 +43,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do end def update_merge_request(opts) - @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + @merge_request = MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request) @merge_request.reload end @@ -64,7 +64,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do } end - let(:service) { described_class.new(project, current_user, opts) } + let(:service) { described_class.new(project: project, current_user: current_user, params: opts) } let(:current_user) { user } before do @@ -99,7 +99,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) .to receive(:track_description_edit_action).once.with(user: user) - MergeRequests::UpdateService.new(project, user, opts).execute(merge_request2) + MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request2) end it 'tracks Draft/WIP marking' do @@ -108,7 +108,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do opts[:title] = "WIP: #{opts[:title]}" - MergeRequests::UpdateService.new(project, user, opts).execute(merge_request2) + MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request2) end it 'tracks Draft/WIP un-marking' do @@ -117,7 +117,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do opts[:title] = "Non-draft/wip title string" - MergeRequests::UpdateService.new(project, user, opts).execute(draft_merge_request) + MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(draft_merge_request) end context 'when MR is locked' do @@ -128,7 +128,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do opts[:discussion_locked] = true - MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request) end end @@ -139,7 +139,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do opts[:discussion_locked] = false - MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request) end end end @@ -154,7 +154,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do opts[:discussion_locked] = false - MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request) end end @@ -165,7 +165,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do opts[:discussion_locked] = true - MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request) end end end @@ -184,7 +184,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do spent_at: Date.parse('2021-02-24') } - MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request) end it 'tracks milestone change' do @@ -193,7 +193,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do opts[:milestone] = milestone - MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request) end it 'track labels change' do @@ -202,7 +202,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do opts[:label_ids] = [label2.id] - MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request) end context 'reviewers' do @@ -213,7 +213,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do opts[:reviewers] = [user2] - MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request) end end @@ -224,7 +224,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do opts[:reviewers] = merge_request.reviewers - MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request) end end end @@ -439,7 +439,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do let(:milestone) { create(:milestone, project: project) } let(:req_opts) { { source_branch: 'feature', target_branch: 'master' } } - subject { MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) } + subject { MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request) } context 'when mentionable attributes change' do let(:opts) { { description: "Description with #{user.to_reference}" }.merge(req_opts) } @@ -486,7 +486,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do } end - let(:service) { described_class.new(project, user, opts) } + let(:service) { described_class.new(project: project, current_user: user, params: opts) } context 'without pipeline' do before do @@ -547,7 +547,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do context 'with a non-authorised user' do let(:visitor) { create(:user) } - let(:service) { described_class.new(project, visitor, opts) } + let(:service) { described_class.new(project: project, current_user: visitor, params: opts) } before do merge_request.update_attribute(:merge_error, 'Error') @@ -805,7 +805,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do opts = { title: 'New title' } perform_enqueued_jobs do - @merge_request = described_class.new(project, user, opts).execute(merge_request) + @merge_request = described_class.new(project: project, current_user: user, params: opts).execute(merge_request) end should_email(subscriber) @@ -818,7 +818,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do opts = { title: 'Draft: New title' } perform_enqueued_jobs do - @merge_request = described_class.new(project, user, opts).execute(merge_request) + @merge_request = described_class.new(project: project, current_user: user, params: opts).execute(merge_request) end should_not_email(subscriber) @@ -840,7 +840,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do opts = { label_ids: [label.id] } perform_enqueued_jobs do - @merge_request = described_class.new(project, user, opts).execute(merge_request) + @merge_request = described_class.new(project: project, current_user: user, params: opts).execute(merge_request) end should_email(subscriber) @@ -856,7 +856,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do opts = { label_ids: [label.id, label2.id] } perform_enqueued_jobs do - @merge_request = described_class.new(project, user, opts).execute(merge_request) + @merge_request = described_class.new(project: project, current_user: user, params: opts).execute(merge_request) end should_not_email(subscriber) @@ -867,7 +867,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do opts = { label_ids: [label2.id] } perform_enqueued_jobs do - @merge_request = described_class.new(project, user, opts).execute(merge_request) + @merge_request = described_class.new(project: project, current_user: user, params: opts).execute(merge_request) end should_not_email(subscriber) @@ -933,7 +933,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do it 'creates a `MergeRequestsClosingIssues` record for each issue' do issue_closing_opts = { description: "Closes #{first_issue.to_reference} and #{second_issue.to_reference}" } - service = described_class.new(project, user, issue_closing_opts) + service = described_class.new(project: project, current_user: user, params: issue_closing_opts) allow(service).to receive(:execute_hooks) service.execute(merge_request) @@ -945,7 +945,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do create(:merge_requests_closing_issues, issue: first_issue, merge_request: merge_request) create(:merge_requests_closing_issues, issue: second_issue, merge_request: merge_request) - service = described_class.new(project, user, description: "not closing any issues") + service = described_class.new(project: project, current_user: user, params: { description: "not closing any issues" }) allow(service).to receive(:execute_hooks) service.execute(merge_request.reload) @@ -1002,7 +1002,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do it 'unassigns assignee when user id is 0' do merge_request.update!(assignee_ids: [user.id]) - expect_next_instance_of(MergeRequests::HandleAssigneesChangeService, project, user) do |service| + expect_next_instance_of(MergeRequests::HandleAssigneesChangeService, project: project, current_user: user) do |service| expect(service) .to receive(:async_execute) .with(merge_request, [user]) @@ -1014,7 +1014,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do end it 'saves assignee when user id is valid' do - expect_next_instance_of(MergeRequests::HandleAssigneesChangeService, project, user) do |service| + expect_next_instance_of(MergeRequests::HandleAssigneesChangeService, project: project, current_user: user) do |service| expect(service) .to receive(:async_execute) .with(merge_request, [user3]) @@ -1052,6 +1052,35 @@ RSpec.describe MergeRequests::UpdateService, :mailer do end end + context 'when adding time spent' do + let(:spend_time) { { duration: 1800, user_id: user3.id } } + + context ':use_specialized_service' do + context 'when true' do + it 'passes the update action to ::MergeRequests::AddSpentTimeService' do + expect(::MergeRequests::AddSpentTimeService) + .to receive(:new).and_call_original + + update_merge_request(spend_time: spend_time, use_specialized_service: true) + end + end + + context 'when false or nil' do + before do + expect(::MergeRequests::AddSpentTimeService).not_to receive(:new) + end + + it 'does not pass the update action to ::MergeRequests::UpdateAssigneesService when false' do + update_merge_request(spend_time: spend_time, use_specialized_service: false) + end + + it 'does not pass the update action to ::MergeRequests::UpdateAssigneesService when nil' do + update_merge_request(spend_time: spend_time, use_specialized_service: nil) + end + end + end + end + include_examples 'issuable update service' do let(:open_issuable) { merge_request } let(:closed_issuable) { create(:closed_merge_request, source_project: project) } @@ -1145,7 +1174,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do it_behaves_like 'issuable record that supports quick actions' do let(:existing_merge_request) { create(:merge_request, source_project: project) } - let(:issuable) { described_class.new(project, user, params).execute(existing_merge_request) } + let(:issuable) { described_class.new(project: project, current_user: user, params: params).execute(existing_merge_request) } end end end diff --git a/spec/services/namespaces/package_settings/update_service_spec.rb b/spec/services/namespaces/package_settings/update_service_spec.rb index fa0c58e4c9b..030bc03038e 100644 --- a/spec/services/namespaces/package_settings/update_service_spec.rb +++ b/spec/services/namespaces/package_settings/update_service_spec.rb @@ -32,7 +32,9 @@ RSpec.describe ::Namespaces::PackageSettings::UpdateService do end shared_examples 'updating the namespace package setting' do - it_behaves_like 'updating the namespace package setting attributes', from: { maven_duplicates_allowed: true, maven_duplicate_exception_regex: 'SNAPSHOT' }, to: { maven_duplicates_allowed: false, maven_duplicate_exception_regex: 'RELEASE' } + it_behaves_like 'updating the namespace package setting attributes', + from: { maven_duplicates_allowed: true, maven_duplicate_exception_regex: 'SNAPSHOT', generic_duplicates_allowed: true, generic_duplicate_exception_regex: 'foo' }, + to: { maven_duplicates_allowed: false, maven_duplicate_exception_regex: 'RELEASE', generic_duplicates_allowed: false, generic_duplicate_exception_regex: 'bar' } it_behaves_like 'returning a success' @@ -60,7 +62,12 @@ RSpec.describe ::Namespaces::PackageSettings::UpdateService do context 'with existing namespace package setting' do let_it_be(:package_settings) { create(:namespace_package_setting, namespace: namespace) } - let_it_be(:params) { { maven_duplicates_allowed: false, maven_duplicate_exception_regex: 'RELEASE' } } + let_it_be(:params) do + { maven_duplicates_allowed: false, + maven_duplicate_exception_regex: 'RELEASE', + generic_duplicates_allowed: false, + generic_duplicate_exception_regex: 'bar' } + end where(:user_role, :shared_examples_name) do :maintainer | 'updating the namespace package setting' diff --git a/spec/services/notes/build_service_spec.rb b/spec/services/notes/build_service_spec.rb index deeab66c4e9..b7b08390dcd 100644 --- a/spec/services/notes/build_service_spec.rb +++ b/spec/services/notes/build_service_spec.rb @@ -173,7 +173,7 @@ RSpec.describe Notes::BuildService do let(:user) { create(:user) } it 'returns `Discussion to reply to cannot be found` error' do - expect(new_note.errors.first).to include("Discussion to reply to cannot be found") + expect(new_note.errors.added?(:base, "Discussion to reply to cannot be found")).to be true end end end diff --git a/spec/services/notes/copy_service_spec.rb b/spec/services/notes/copy_service_spec.rb index fd44aa7cf40..d9b6bafd7ff 100644 --- a/spec/services/notes/copy_service_spec.rb +++ b/spec/services/notes/copy_service_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Notes::CopyService do let_it_be(:noteable) { create(:issue) } it 'validates that we cannot copy notes to the same Noteable' do - expect { described_class.new(noteable, noteable) }.to raise_error(ArgumentError) + expect { described_class.new(nil, noteable, noteable) }.to raise_error(ArgumentError) end end diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index d28cb118529..31263feb947 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -176,7 +176,7 @@ RSpec.describe Notes::CreateService do end it 'note is associated with a note diff file' do - MergeRequests::MergeToRefService.new(merge_request.project, merge_request.author).execute(merge_request) + MergeRequests::MergeToRefService.new(project: merge_request.project, current_user: merge_request.author).execute(merge_request) note = described_class.new(project_with_repo, user, new_opts).execute diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb index c098500b78a..9692bb08379 100644 --- a/spec/services/notes/quick_actions_service_spec.rb +++ b/spec/services/notes/quick_actions_service_spec.rb @@ -103,6 +103,30 @@ RSpec.describe Notes::QuickActionsService do expect(Timelog.last.note_id).to eq(note.id) end end + + context 'adds a system note' do + context 'when not specifying a date' do + let(:note_text) { "/spend 1h" } + + it 'does not include the date' do + _, update_params = service.execute(note) + service.apply_updates(update_params, note) + + expect(Note.last.note).to eq('added 1h of time spent') + end + end + + context 'when specifying a date' do + let(:note_text) { "/spend 1h 2020-01-01" } + + it 'does include the date' do + _, update_params = service.execute(note) + service.apply_updates(update_params, note) + + expect(Note.last.note).to eq('added 1h of time spent at 2020-01-01') + end + end + end end end @@ -214,25 +238,25 @@ RSpec.describe Notes::QuickActionsService do end end - describe '.noteable_update_service' do + describe '.noteable_update_service_class' do include_context 'note on noteable' it 'returns Issues::UpdateService for a note on an issue' do note = create(:note_on_issue, project: project) - expect(described_class.noteable_update_service(note)).to eq(Issues::UpdateService) + expect(described_class.noteable_update_service_class(note)).to eq(Issues::UpdateService) end it 'returns MergeRequests::UpdateService for a note on a merge request' do note = create(:note_on_merge_request, project: project) - expect(described_class.noteable_update_service(note)).to eq(MergeRequests::UpdateService) + expect(described_class.noteable_update_service_class(note)).to eq(MergeRequests::UpdateService) end it 'returns Commits::TagService for a note on a commit' do note = create(:note_on_commit, project: project) - expect(described_class.noteable_update_service(note)).to eq(Commits::TagService) + expect(described_class.noteable_update_service_class(note)).to eq(Commits::TagService) end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 6eff768eac2..c3a0766cb17 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -412,7 +412,7 @@ RSpec.describe NotificationService, :mailer do it_should_not_email! end - context 'do exist' do + context 'do exist and note not confidential' do let!(:issue_email_participant) { issue.issue_email_participants.create!(email: 'service.desk@example.com') } before do @@ -422,6 +422,18 @@ RSpec.describe NotificationService, :mailer do it_should_email! end + + context 'do exist and note is confidential' do + let(:note) { create(:note, noteable: issue, project: project, confidential: true) } + let!(:issue_email_participant) { issue.issue_email_participants.create!(email: 'service.desk@example.com') } + + before do + issue.update!(external_author: 'service.desk@example.com') + project.update!(service_desk_enabled: true) + end + + it_should_not_email! + end end describe '#new_note' 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 new file mode 100644 index 00000000000..b31830c2d3b --- /dev/null +++ b/spec/services/packages/debian/generate_distribution_key_service_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Debian::GenerateDistributionKeyService do + let_it_be(:user) { create(:user) } + + let(:params) { {} } + + subject { described_class.new(current_user: user, params: params) } + + let(:response) { subject.execute } + + context 'with a user' do + it 'returns an Hash', :aggregate_failures do + expect(GPGME::Ctx).to receive(:new).with(armor: true, offline: true).and_call_original + expect(User).to receive(:random_password).with(no_args).and_call_original + + expect(response).to be_a Hash + expect(response.keys).to contain_exactly(:private_key, :public_key, :fingerprint, :passphrase) + expect(response[:private_key]).to start_with('-----BEGIN PGP PRIVATE KEY BLOCK-----') + expect(response[:public_key]).to start_with('-----BEGIN PGP PUBLIC KEY BLOCK-----') + expect(response[:fingerprint].length).to eq(40) + expect(response[:passphrase].length).to be > 10 + end + end + + context 'without a user' do + let(:user) { nil } + + it 'raises an ArgumentError' do + expect { response }.to raise_error(ArgumentError, 'Please provide a user') + end + end +end diff --git a/spec/services/packages/debian/generate_distribution_service_spec.rb b/spec/services/packages/debian/generate_distribution_service_spec.rb new file mode 100644 index 00000000000..0547d18c8bc --- /dev/null +++ b/spec/services/packages/debian/generate_distribution_service_spec.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Debian::GenerateDistributionService do + let_it_be(:group) { create(:group, :public) } + let_it_be(:project) { create(:project, :public, group: group) } + let_it_be(:project_distribution) { create("debian_project_distribution", container: project, codename: 'unstable', valid_time_duration_seconds: 48.hours.to_i) } + + let_it_be(:incoming) { create(:debian_incoming, project: project) } + + before_all do + ::Packages::Debian::ProcessChangesService.new(incoming.package_files.last, nil).execute + end + + let(:service) { described_class.new(distribution) } + + describe '#execute' do + subject { service.execute } + + shared_examples 'Generate Distribution' do |container_type| + context "for #{container_type}" do + if container_type == :group + let_it_be(:container) { group } + let_it_be(:distribution, reload: true) { create('debian_group_distribution', container: group, codename: 'unstable', valid_time_duration_seconds: 48.hours.to_i) } + else + let_it_be(:container) { project } + let_it_be(:distribution, reload: true) { project_distribution } + end + + context 'with components and architectures' do + let_it_be(:component_main ) { create("debian_#{container_type}_component", distribution: distribution, name: 'main') } + let_it_be(:component_contrib) { create("debian_#{container_type}_component", distribution: distribution, name: 'contrib') } + + let_it_be(:architecture_all ) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'all') } + let_it_be(:architecture_amd64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'amd64') } + let_it_be(:architecture_arm64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'arm64') } + + let_it_be(:component_file1) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_all, created_at: '2020-01-24T09:00:00.000Z') } # destroyed + let_it_be(:component_file2) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_amd64, created_at: '2020-01-24T10:29:59.000Z') } # destroyed + let_it_be(:component_file3) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, created_at: '2020-01-24T10:30:00.000Z') } # kept + let_it_be(:component_file4) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, created_at: '2020-01-24T11:30:00.000Z') } # kept + + def check_component_file(component_name, component_file_type, architecture_name, expected_content) + component_file = distribution + .component_files + .with_component_name(component_name) + .with_file_type(component_file_type) + .with_architecture_name(architecture_name) + .last + + expect(component_file).not_to be_nil + expect(component_file.file.exists?).to eq(!expected_content.nil?) + + unless expected_content.nil? + component_file.file.use_file do |file_path| + expect(File.read(file_path)).to eq(expected_content) + end + end + end + + it 'updates distribution and component files', :aggregate_failures do + travel_to(Time.utc(2020, 01, 25, 15, 17, 18, 123456)) do + expect(Gitlab::ErrorTracking).not_to receive(:log_exception) + + expect { subject } + .to not_change { Packages::Package.count } + .and not_change { Packages::PackageFile.count } + .and change { distribution.component_files.count }.from(4).to(2 + 6) + + expected_main_amd64_content = <<~EOF + Package: libsample0 + Source: sample + Version: 1.2.3~alpha2 + Installed-Size: 7 + Maintainer: John Doe <john.doe@example.com> + Architecture: amd64 + Description: Some mostly empty lib + Used in GitLab tests. + . + Testing another paragraph. + Multi-Arch: same + Homepage: https://gitlab.com/ + Section: libs + Priority: optional + Filename: pool/unstable/#{project.id}/s/sample/libsample0_1.2.3~alpha2_amd64.deb + Size: 409600 + MD5sum: fb0842b21adc44207996296fe14439dd + SHA256: 1c383a525bfcba619c7305ccd106d61db501a6bbaf0003bf8d0c429fbdb7fcc1 + + Package: sample-dev + Source: sample (1.2.3~alpha2) + Version: 1.2.3~binary + Installed-Size: 7 + Maintainer: John Doe <john.doe@example.com> + Architecture: amd64 + Depends: libsample0 (= 1.2.3~binary) + Description: Some mostly empty developpement files + Used in GitLab tests. + . + Testing another paragraph. + Multi-Arch: same + Homepage: https://gitlab.com/ + Section: libdevel + Priority: optional + Filename: pool/unstable/#{project.id}/s/sample/sample-dev_1.2.3~binary_amd64.deb + Size: 409600 + MD5sum: d2afbd28e4d74430d22f9504e18bfdf5 + SHA256: 9fbeee2191ce4dab5288fad5ecac1bd369f58fef9a992a880eadf0caf25f086d + EOF + + check_component_file('main', :packages, 'all', nil) + check_component_file('main', :packages, 'amd64', expected_main_amd64_content) + check_component_file('main', :packages, 'arm64', nil) + + check_component_file('contrib', :packages, 'all', nil) + check_component_file('contrib', :packages, 'amd64', nil) + check_component_file('contrib', :packages, 'arm64', nil) + + size = expected_main_amd64_content.length + md5sum = Digest::MD5.hexdigest(expected_main_amd64_content) + sha256 = Digest::SHA256.hexdigest(expected_main_amd64_content) + + expected_release_content = <<~EOF + Codename: unstable + Date: Sat, 25 Jan 2020 15:17:18 +0000 + Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000 + Architectures: all amd64 arm64 + Components: contrib main + MD5Sum: + d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-all/Packages + d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-amd64/Packages + d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-arm64/Packages + d41d8cd98f00b204e9800998ecf8427e 0 main/binary-all/Packages + #{md5sum} #{size} main/binary-amd64/Packages + d41d8cd98f00b204e9800998ecf8427e 0 main/binary-arm64/Packages + SHA256: + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-all/Packages + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-amd64/Packages + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-arm64/Packages + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-all/Packages + #{sha256} #{size} main/binary-amd64/Packages + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-arm64/Packages + EOF + + distribution.file.use_file do |file_path| + expect(File.read(file_path)).to eq(expected_release_content) + end + end + end + end + + context 'without components and architectures' do + it 'updates distribution and component files', :aggregate_failures do + travel_to(Time.utc(2020, 01, 25, 15, 17, 18, 123456)) do + expect(Gitlab::ErrorTracking).not_to receive(:log_exception) + + expect { subject } + .to not_change { Packages::Package.count } + .and not_change { Packages::PackageFile.count } + .and not_change { distribution.component_files.count } + + expected_release_content = <<~EOF + Codename: unstable + Date: Sat, 25 Jan 2020 15:17:18 +0000 + Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000 + MD5Sum: + SHA256: + EOF + + distribution.file.use_file do |file_path| + expect(File.read(file_path)).to eq(expected_release_content) + end + end + end + end + end + end + + it_behaves_like 'Generate Distribution', :project + it_behaves_like 'Generate Distribution', :group + end +end diff --git a/spec/services/packages/debian/process_changes_service_spec.rb b/spec/services/packages/debian/process_changes_service_spec.rb index 98b531bde10..f23471659bc 100644 --- a/spec/services/packages/debian/process_changes_service_spec.rb +++ b/spec/services/packages/debian/process_changes_service_spec.rb @@ -17,6 +17,7 @@ RSpec.describe Packages::Debian::ProcessChangesService do .to change { Packages::Package.count }.from(1).to(2) .and not_change { Packages::PackageFile.count } .and change { incoming.package_files.count }.from(7).to(0) + .and change { package_file.debian_file_metadatum&.reload&.file_type }.from('unknown').to('changes') created_package = Packages::Package.last expect(created_package.name).to eq 'sample' diff --git a/spec/services/packages/generic/create_package_file_service_spec.rb b/spec/services/packages/generic/create_package_file_service_spec.rb index 10c54369f26..1c9eb53cfc7 100644 --- a/spec/services/packages/generic/create_package_file_service_spec.rb +++ b/spec/services/packages/generic/create_package_file_service_spec.rb @@ -6,13 +6,16 @@ RSpec.describe Packages::Generic::CreatePackageFileService do let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user) } let_it_be(:pipeline) { create(:ci_pipeline, user: user) } + let_it_be(:file_name) { 'myfile.tar.gz.1' } + let(:build) { double('build', pipeline: pipeline) } describe '#execute' do + let_it_be(:package) { create(:generic_package, project: project) } + let(:sha256) { '440e5e148a25331bbd7991575f7d54933c0ebf6cc735a18ee5066ac1381bb590' } let(:temp_file) { Tempfile.new("test") } let(:file) { UploadedFile.new(temp_file.path, sha256: sha256) } - let(:package) { create(:generic_package, project: project) } let(:package_service) { double } let(:params) do @@ -20,7 +23,7 @@ RSpec.describe Packages::Generic::CreatePackageFileService do package_name: 'mypackage', package_version: '0.0.1', file: file, - file_name: 'myfile.tar.gz.1', + file_name: file_name, build: build } end @@ -34,7 +37,7 @@ RSpec.describe Packages::Generic::CreatePackageFileService do } end - subject { described_class.new(project, user, params).execute } + subject(:execute_service) { described_class.new(project, user, params).execute } before do FileUtils.touch(temp_file) @@ -47,14 +50,14 @@ RSpec.describe Packages::Generic::CreatePackageFileService do end it 'creates package file', :aggregate_failures do - expect { subject }.to change { package.package_files.count }.by(1) + expect { execute_service }.to change { package.package_files.count }.by(1) .and change { Packages::PackageFileBuildInfo.count }.by(1) package_file = package.package_files.last aggregate_failures do expect(package_file.package.status).to eq('default') expect(package_file.package).to eq(package) - expect(package_file.file_name).to eq('myfile.tar.gz.1') + expect(package_file.file_name).to eq(file_name) expect(package_file.size).to eq(file.size) expect(package_file.file_sha256).to eq(sha256) end @@ -65,7 +68,7 @@ RSpec.describe Packages::Generic::CreatePackageFileService do let(:package_params) { super().merge(status: 'hidden') } it 'updates an existing packages status' do - expect { subject }.to change { package.package_files.count }.by(1) + expect { execute_service }.to change { package.package_files.count }.by(1) .and change { Packages::PackageFileBuildInfo.count }.by(1) package_file = package.package_files.last @@ -76,5 +79,32 @@ RSpec.describe Packages::Generic::CreatePackageFileService do end it_behaves_like 'assigns build to package file' + + context 'with existing package' do + before do + create(:package_file, package: package, file_name: file_name) + end + + it { expect { execute_service }.to change { project.package_files.count }.by(1) } + + context 'when duplicates are not allowed' do + before do + package.project.namespace.package_settings.update!(generic_duplicates_allowed: false) + end + + it 'does not allow duplicates' do + expect { execute_service }.to raise_error(::Packages::DuplicatePackageError) + .and change { project.package_files.count }.by(0) + end + + context 'when the package name matches the exception regex' do + before do + package.project.namespace.package_settings.update!(generic_duplicate_exception_regex: '.*') + end + + it { expect { execute_service }.to change { project.package_files.count }.by(1) } + end + end + end end end diff --git a/spec/services/packages/maven/find_or_create_package_service_spec.rb b/spec/services/packages/maven/find_or_create_package_service_spec.rb index 2543ab0c669..803371af4bf 100644 --- a/spec/services/packages/maven/find_or_create_package_service_spec.rb +++ b/spec/services/packages/maven/find_or_create_package_service_spec.rb @@ -130,7 +130,15 @@ RSpec.describe Packages::Maven::FindOrCreatePackageService do context 'when the package name matches the exception regex' do before do - package_settings.update!(maven_duplicate_exception_regex: '.*') + package_settings.update!(maven_duplicate_exception_regex: existing_package.name) + end + + it_behaves_like 'reuse existing package' + end + + context 'when the package version matches the exception regex' do + before do + package_settings.update!(maven_duplicate_exception_regex: existing_package.version) end it_behaves_like 'reuse existing package' diff --git a/spec/services/packages/nuget/search_service_spec.rb b/spec/services/packages/nuget/search_service_spec.rb index db758dc6672..1838065c5be 100644 --- a/spec/services/packages/nuget/search_service_spec.rb +++ b/spec/services/packages/nuget/search_service_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Packages::Nuget::SearchService do let_it_be(:group) { create(:group) } let_it_be(:subgroup) { create(:group, parent: group) } let_it_be(:project) { create(:project, namespace: subgroup) } - let_it_be(:package_a) { create(:nuget_package, project: project, name: 'DummyPackageA') } + let_it_be_with_refind(:package_a) { create(:nuget_package, project: project, name: 'DummyPackageA') } let_it_be(:packages_b) { create_list(:nuget_package, 5, project: project, name: 'DummyPackageB') } let_it_be(:packages_c) { create_list(:nuget_package, 5, project: project, name: 'DummyPackageC') } let_it_be(:package_d) { create(:nuget_package, project: project, name: 'FooBarD') } @@ -79,6 +79,16 @@ RSpec.describe Packages::Nuget::SearchService do it { expect_search_results 4, package_a, packages_b, packages_c, package_d } end + context 'with non-displayable packages' do + let(:search_term) { '' } + + before do + package_a.update_column(:status, 1) + end + + it { expect_search_results 3, packages_b, packages_c, package_d } + end + context 'with prefix search term' do let(:search_term) { 'dummy' } diff --git a/spec/services/packages/rubygems/dependency_resolver_service_spec.rb b/spec/services/packages/rubygems/dependency_resolver_service_spec.rb index 206bffe53f8..78abfc96ed5 100644 --- a/spec/services/packages/rubygems/dependency_resolver_service_spec.rb +++ b/spec/services/packages/rubygems/dependency_resolver_service_spec.rb @@ -92,7 +92,7 @@ RSpec.describe Packages::Rubygems::DependencyResolverService do ] }] - expect(subject.payload).to eq(expected_result) + expect(subject.payload).to match_array(expected_result) end end end diff --git a/spec/services/packages/rubygems/process_gem_service_spec.rb b/spec/services/packages/rubygems/process_gem_service_spec.rb index 83e868d9579..64deb39c6d8 100644 --- a/spec/services/packages/rubygems/process_gem_service_spec.rb +++ b/spec/services/packages/rubygems/process_gem_service_spec.rb @@ -16,12 +16,11 @@ RSpec.describe Packages::Rubygems::ProcessGemService do describe '#execute' do subject { service.execute } - context 'no gem file', :aggregate_failures do + context 'no gem file' do let(:package_file) { nil } it 'returns an error' do - expect(subject.error?).to be(true) - expect(subject.message).to eq('Gem was not processed') + expect { subject }.to raise_error(::Packages::Rubygems::ProcessGemService::ExtractionError, 'Gem was not processed - package_file is not set') end end diff --git a/spec/services/packages/terraform_module/create_package_service_spec.rb b/spec/services/packages/terraform_module/create_package_service_spec.rb new file mode 100644 index 00000000000..f911bb5b82c --- /dev/null +++ b/spec/services/packages/terraform_module/create_package_service_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::TerraformModule::CreatePackageService do + let_it_be(:namespace) { create(:namespace) } + let_it_be(:project) { create(:project, namespace: namespace) } + let_it_be(:user) { create(:user) } + let_it_be(:sha256) { '440e5e148a25331bbd7991575f7d54933c0ebf6cc735a18ee5066ac1381bb590' } + let_it_be(:temp_file) { Tempfile.new('test') } + let_it_be(:file) { UploadedFile.new(temp_file.path, sha256: sha256) } + + let(:overrides) { {} } + + let(:params) do + { + module_name: 'foo', + module_system: 'bar', + module_version: '1.0.1', + file: file, + file_name: 'foo-bar-1.0.1.tgz' + }.merge(overrides) + end + + subject { described_class.new(project, user, params).execute } + + describe '#execute' do + context 'valid package' do + it 'creates a package' do + expect { subject } + .to change { ::Packages::Package.count }.by(1) + .and change { ::Packages::Package.terraform_module.count }.by(1) + end + end + + context 'package already exists elsewhere' do + let(:project2) { create(:project, namespace: namespace) } + let!(:existing_package) { create(:terraform_module_package, project: project2, name: 'foo/bar', version: '1.0.0') } + + it { expect(subject[:http_status]).to eq 403 } + it { expect(subject[:message]).to be 'Package already exists.' } + end + + context 'version already exists' do + let!(:existing_version) { create(:terraform_module_package, project: project, name: 'foo/bar', version: '1.0.1') } + + it { expect(subject[:http_status]).to eq 403 } + it { expect(subject[:message]).to be 'Package version already exists.' } + end + + context 'with empty version' do + let(:overrides) { { module_version: '' } } + + it { expect(subject[:http_status]).to eq 400 } + it { expect(subject[:message]).to eq 'Version is empty.' } + end + end +end diff --git a/spec/services/post_receive_service_spec.rb b/spec/services/post_receive_service_spec.rb index 033194972c7..2a78dc454c7 100644 --- a/spec/services/post_receive_service_spec.rb +++ b/spec/services/post_receive_service_spec.rb @@ -264,7 +264,7 @@ RSpec.describe PostReceiveService do context "project path matches" do before do - allow(project).to receive(:full_path).and_return("/company/sekrit-project") + allow(project).to receive(:full_path).and_return("company/sekrit-project") end it "does output the latest scoped broadcast message" do diff --git a/spec/services/projects/alerting/notify_service_spec.rb b/spec/services/projects/alerting/notify_service_spec.rb index c272ce13132..feae8f3967c 100644 --- a/spec/services/projects/alerting/notify_service_spec.rb +++ b/spec/services/projects/alerting/notify_service_spec.rb @@ -3,77 +3,49 @@ require 'spec_helper' RSpec.describe Projects::Alerting::NotifyService do - let_it_be_with_reload(:project) { create(:project, :repository) } + let_it_be_with_reload(:project) { create(:project) } + + let(:payload) { ActionController::Parameters.new(payload_raw).permit! } + let(:payload_raw) { {} } + + let(:service) { described_class.new(project, payload) } before do - allow(ProjectServiceWorker).to receive(:perform_async) + stub_licensed_features(oncall_schedules: false, generic_alert_fingerprinting: false) end describe '#execute' do - let(:token) { 'invalid-token' } - let(:starts_at) { Time.current.change(usec: 0) } - let(:fingerprint) { 'testing' } - let(:service) { described_class.new(project, payload) } - let_it_be(:environment) { create(:environment, project: project) } - let(:environment) { create(:environment, project: project) } - let(:ended_at) { nil } - let(:payload_raw) do - { - title: 'alert title', - start_time: starts_at.rfc3339, - end_time: ended_at&.rfc3339, - severity: 'low', - monitoring_tool: 'GitLab RSpec', - service: 'GitLab Test Suite', - description: 'Very detailed description', - hosts: ['1.1.1.1', '2.2.2.2'], - fingerprint: fingerprint, - gitlab_environment_name: environment.name - }.with_indifferent_access - end + include_context 'incident management settings enabled' - let(:payload) { ActionController::Parameters.new(payload_raw).permit! } + subject { service.execute(token, integration) } - subject { service.execute(token, nil) } + context 'with HTTP integration' do + let_it_be_with_reload(:integration) { create(:alert_management_http_integration, project: project) } - shared_examples 'notifications are handled correctly' do context 'with valid token' do let(:token) { integration.token } - let(:incident_management_setting) { double(send_email?: email_enabled, create_issue?: issue_enabled, auto_close_incident?: auto_close_enabled) } - let(:email_enabled) { false } - let(:issue_enabled) { false } - let(:auto_close_enabled) { false } - - before do - allow(service) - .to receive(:incident_management_setting) - .and_return(incident_management_setting) - end context 'with valid payload' do - shared_examples 'assigns the alert properties' do - it 'ensure that created alert has all data properly assigned' do - subject - expect(last_alert_attributes).to match( - project_id: project.id, - title: payload_raw.fetch(:title), - started_at: Time.zone.parse(payload_raw.fetch(:start_time)), - severity: payload_raw.fetch(:severity), - status: AlertManagement::Alert.status_value(:triggered), - events: 1, - domain: 'operations', - hosts: payload_raw.fetch(:hosts), - payload: payload_raw.with_indifferent_access, - issue_id: nil, - description: payload_raw.fetch(:description), - monitoring_tool: payload_raw.fetch(:monitoring_tool), - service: payload_raw.fetch(:service), - fingerprint: Digest::SHA1.hexdigest(fingerprint), - environment_id: environment.id, - ended_at: nil, - prometheus_alert_id: nil - ) - end + let_it_be(:environment) { create(:environment, project: project) } + let_it_be(:fingerprint) { 'testing' } + let_it_be(:source) { 'GitLab RSpec' } + let_it_be(:starts_at) { Time.current.change(usec: 0) } + + let(:ended_at) { nil } + let(:domain) { 'operations' } + let(:payload_raw) do + { + title: 'alert title', + start_time: starts_at.rfc3339, + end_time: ended_at&.rfc3339, + severity: 'low', + monitoring_tool: source, + service: 'GitLab Test Suite', + description: 'Very detailed description', + hosts: ['1.1.1.1', '2.2.2.2'], + fingerprint: fingerprint, + gitlab_environment_name: environment.name + }.with_indifferent_access end let(:last_alert_attributes) do @@ -82,8 +54,8 @@ RSpec.describe Projects::Alerting::NotifyService do .with_indifferent_access end - it_behaves_like 'creates an alert management alert' - it_behaves_like 'assigns the alert properties' + it_behaves_like 'processes new firing alert' + it_behaves_like 'properly assigns the alert properties' it 'passes the integration to alert processing' do expect(Gitlab::AlertManagement::Payload) @@ -94,101 +66,18 @@ RSpec.describe Projects::Alerting::NotifyService do subject end - it 'creates a system note corresponding to alert creation' do - expect { subject }.to change(Note, :count).by(1) - expect(Note.last.note).to include(payload_raw.fetch(:monitoring_tool)) - end - - context 'existing alert with same fingerprint' do - let(:fingerprint_sha) { Digest::SHA1.hexdigest(fingerprint) } - let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint_sha) } - - it_behaves_like 'adds an alert management alert event' - - context 'end time given' do - let(:ended_at) { Time.current.change(nsec: 0) } - - it 'does not resolve the alert' do - expect { subject }.not_to change { alert.reload.status } - end - - it 'does not set the ended at' do - subject - - expect(alert.reload.ended_at).to be_nil - end - - it_behaves_like 'does not an create alert management alert' - it_behaves_like 'creates single system note based on the source of the alert' - - context 'auto_close_enabled setting enabled' do - let(:auto_close_enabled) { true } - - it 'resolves the alert and sets the end time', :aggregate_failures do - subject - alert.reload - - expect(alert.resolved?).to eq(true) - expect(alert.ended_at).to eql(ended_at) - end - - it_behaves_like 'creates status-change system note for an auto-resolved alert' - - context 'related issue exists' do - let(:alert) { create(:alert_management_alert, :with_issue, project: project, fingerprint: fingerprint_sha) } - let(:issue) { alert.issue } - - it { expect { subject }.to change { issue.reload.state }.from('opened').to('closed') } - it { expect { subject }.to change(ResourceStateEvent, :count).by(1) } - end - - context 'with issue enabled' do - let(:issue_enabled) { true } - - it_behaves_like 'does not process incident issues' - end - end - end - - context 'existing alert is resolved' do - let!(:alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: fingerprint_sha) } - - it_behaves_like 'creates an alert management alert' - it_behaves_like 'assigns the alert properties' - end - - context 'existing alert is ignored' do - let!(:alert) { create(:alert_management_alert, :ignored, project: project, fingerprint: fingerprint_sha) } - - it_behaves_like 'adds an alert management alert event' - end - - context 'two existing alerts, one resolved one open' do - let!(:resolved_existing_alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: fingerprint_sha) } - let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint_sha) } - - it_behaves_like 'adds an alert management alert event' - end - end - - context 'end time given' do - let(:ended_at) { Time.current } - - it_behaves_like 'creates an alert management alert' - it_behaves_like 'assigns the alert properties' - end - - context 'with a minimal payload' do - let(:payload_raw) do + context 'with partial payload' do + let_it_be(:source) { integration.name } + let_it_be(:payload_raw) do { title: 'alert title', start_time: starts_at.rfc3339 } end - it_behaves_like 'creates an alert management alert' + include_examples 'processes never-before-seen alert' - it 'created alert has all data properly assigned' do + it 'assigns the alert properties' do subject expect(last_alert_attributes).to match( @@ -212,7 +101,19 @@ RSpec.describe Projects::Alerting::NotifyService do ) end - it_behaves_like 'creates single system note based on the source of the alert' + context 'with existing alert with matching payload' do + let_it_be(:fingerprint) { payload_raw.except(:start_time).stringify_keys } + let_it_be(:gitlab_fingerprint) { Gitlab::AlertManagement::Fingerprint.generate(fingerprint) } + let_it_be(:alert) { create(:alert_management_alert, project: project, fingerprint: gitlab_fingerprint) } + + include_examples 'processes never-before-seen alert' + end + end + + context 'with resolving payload' do + let(:ended_at) { Time.current.change(usec: 0) } + + it_behaves_like 'processes recovery alert' end end @@ -223,63 +124,30 @@ RSpec.describe Projects::Alerting::NotifyService do allow(Gitlab::Utils::DeepSize).to receive(:new).and_return(deep_size_object) end - it_behaves_like 'does not process incident issues due to error', http_status: :bad_request - it_behaves_like 'does not an create alert management alert' + it_behaves_like 'alerts service responds with an error and takes no actions', :bad_request end - it_behaves_like 'does not process incident issues' - - context 'issue enabled' do - let(:issue_enabled) { true } - - it_behaves_like 'processes incident issues' - - context 'when alert already exists' do - let(:fingerprint_sha) { Digest::SHA1.hexdigest(fingerprint) } - let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint_sha) } - - context 'when existing alert does not have an associated issue' do - it_behaves_like 'processes incident issues' - end - - context 'when existing alert has an associated issue' do - let!(:alert) { create(:alert_management_alert, :with_issue, project: project, fingerprint: fingerprint_sha) } - - it_behaves_like 'does not process incident issues' - end + context 'with inactive integration' do + before do + integration.update!(active: false) end - end - context 'with emails turned on' do - let(:email_enabled) { true } - - it_behaves_like 'Alert Notification Service sends notification email' + it_behaves_like 'alerts service responds with an error and takes no actions', :forbidden end end context 'with invalid token' do - it_behaves_like 'does not process incident issues due to error', http_status: :unauthorized - it_behaves_like 'does not an create alert management alert' - end - end - - context 'with an HTTP Integration' do - let_it_be_with_reload(:integration) { create(:alert_management_http_integration, project: project) } + let(:token) { 'invalid-token' } - subject { service.execute(token, integration) } - - it_behaves_like 'notifications are handled correctly' do - let(:source) { integration.name } + it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized end + end - context 'with deactivated HTTP Integration' do - before do - integration.update!(active: false) - end + context 'without HTTP integration' do + let(:integration) { nil } + let(:token) { nil } - it_behaves_like 'does not process incident issues due to error', http_status: :forbidden - it_behaves_like 'does not an create alert management alert' - end + it_behaves_like 'alerts service responds with an error and takes no actions', :forbidden end end end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index e0d6b9afcff..cd659bf5e60 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -273,16 +273,6 @@ RSpec.describe Projects::CreateService, '#execute' do opts[:default_branch] = 'master' expect(create_project(user, opts)).to eq(nil) end - - it 'sets invalid service as inactive' do - create(:service, type: 'JiraService', project: nil, template: true, active: true) - - project = create_project(user, opts) - service = project.services.first - - expect(project).to be_persisted - expect(service.active).to be false - end end context 'wiki_enabled creates repository directory' do @@ -574,18 +564,18 @@ RSpec.describe Projects::CreateService, '#execute' do let!(:template_integration) { create(:prometheus_service, :template, api_url: 'https://prometheus.template.com/') } it 'creates a service from the template' do - expect(project.services.count).to eq(1) - expect(project.services.first.api_url).to eq(template_integration.api_url) - expect(project.services.first.inherit_from_id).to be_nil + expect(project.integrations.count).to eq(1) + expect(project.integrations.first.api_url).to eq(template_integration.api_url) + expect(project.integrations.first.inherit_from_id).to be_nil end context 'with an active instance-level integration' do let!(:instance_integration) { create(:prometheus_service, :instance, api_url: 'https://prometheus.instance.com/') } it 'creates a service from the instance-level integration' do - expect(project.services.count).to eq(1) - expect(project.services.first.api_url).to eq(instance_integration.api_url) - expect(project.services.first.inherit_from_id).to eq(instance_integration.id) + expect(project.integrations.count).to eq(1) + expect(project.integrations.first.api_url).to eq(instance_integration.api_url) + expect(project.integrations.first.inherit_from_id).to eq(instance_integration.id) end context 'with an active group-level integration' do @@ -604,9 +594,9 @@ RSpec.describe Projects::CreateService, '#execute' do end it 'creates a service from the group-level integration' do - expect(project.services.count).to eq(1) - expect(project.services.first.api_url).to eq(group_integration.api_url) - expect(project.services.first.inherit_from_id).to eq(group_integration.id) + expect(project.integrations.count).to eq(1) + expect(project.integrations.first.api_url).to eq(group_integration.api_url) + expect(project.integrations.first.inherit_from_id).to eq(group_integration.id) end context 'with an active subgroup' do @@ -625,25 +615,14 @@ RSpec.describe Projects::CreateService, '#execute' do end it 'creates a service from the subgroup-level integration' do - expect(project.services.count).to eq(1) - expect(project.services.first.api_url).to eq(subgroup_integration.api_url) - expect(project.services.first.inherit_from_id).to eq(subgroup_integration.id) + expect(project.integrations.count).to eq(1) + expect(project.integrations.first.api_url).to eq(subgroup_integration.api_url) + expect(project.integrations.first.inherit_from_id).to eq(subgroup_integration.id) end end end end end - - context 'when there is an invalid integration' do - before do - create(:service, :template, type: 'DroneCiService', active: true) - end - - it 'creates an inactive service' do - expect(project).to be_persisted - expect(project.services.first.active).to be false - end - end end context 'when skip_disk_validation is used' do diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index b2a68bbd0aa..ff582279d71 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -418,6 +418,54 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do end end + context 'when project has webhooks' do + let!(:web_hook1) { create(:project_hook, project: project) } + let!(:web_hook2) { create(:project_hook, project: project) } + let!(:another_project_web_hook) { create(:project_hook) } + let!(:web_hook_log) { create(:web_hook_log, web_hook: web_hook1) } + + it 'deletes webhooks and logs related to project' do + expect_next_instance_of(WebHooks::DestroyService, user) do |instance| + expect(instance).to receive(:sync_destroy).with(web_hook1).and_call_original + end + expect_next_instance_of(WebHooks::DestroyService, user) do |instance| + expect(instance).to receive(:sync_destroy).with(web_hook2).and_call_original + end + + expect do + destroy_project(project, user) + end.to change(WebHook, :count).by(-2) + .and change(WebHookLog, :count).by(-1) + end + + context 'when an error is raised deleting webhooks' do + before do + allow_next_instance_of(WebHooks::DestroyService) do |instance| + allow(instance).to receive(:sync_destroy).and_return(message: 'foo', status: :error) + end + end + + it_behaves_like 'handles errors thrown during async destroy', "Failed to remove webhooks" + end + + context 'when "destroy_webhooks_before_the_project" flag is disabled' do + before do + stub_feature_flags(destroy_webhooks_before_the_project: false) + end + + it 'does not call WebHooks::DestroyService' do + expect(WebHooks::DestroyService).not_to receive(:new) + + expect do + destroy_project(project, user) + end.to change(WebHook, :count).by(-2) + .and change(WebHookLog, :count).by(-1) + + expect(another_project_web_hook.reload).to be + end + end + end + context 'error while destroying', :sidekiq_inline do let!(:pipeline) { create(:ci_pipeline, project: project) } let!(:builds) { create_list(:ci_build, 2, :artifacts, pipeline: pipeline) } diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb deleted file mode 100644 index 0aa4a1cd312..00000000000 --- a/spec/services/projects/housekeeping_service_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -# This is a compatibility class to avoid calling a non-existent -# class from sidekiq during deployment. -# -# We're deploying the name of the referenced class in 13.9. Nevertheless, -# we cannot remove the class entirely because there can be jobs -# referencing it. We still need this specs to ensure that the old -# class still has the old behavior. -# -# We can get rid of this class in 13.10 -# https://gitlab.com/gitlab-org/gitlab/-/issues/297580 -# -RSpec.describe Projects::HousekeepingService do - it_behaves_like 'housekeeps repository' do - let_it_be(:resource) { create(:project, :repository) } - end -end diff --git a/spec/services/projects/prometheus/alerts/notify_service_spec.rb b/spec/services/projects/prometheus/alerts/notify_service_spec.rb index e196220eabe..bfc8225b654 100644 --- a/spec/services/projects/prometheus/alerts/notify_service_spec.rb +++ b/spec/services/projects/prometheus/alerts/notify_service_spec.rb @@ -6,25 +6,26 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do include PrometheusHelpers using RSpec::Parameterized::TableSyntax - let_it_be(:project, reload: true) { create(:project) } + let_it_be_with_reload(:project) { create(:project) } + let_it_be_with_refind(:setting) do + create(:project_incident_management_setting, project: project, send_email: true, create_issue: true) + end let(:service) { described_class.new(project, payload) } let(:token_input) { 'token' } - let!(:setting) do - create(:project_incident_management_setting, project: project, send_email: true, create_issue: true) - end - - let(:subject) { service.execute(token_input) } + subject { service.execute(token_input) } context 'with valid payload' do let_it_be(:alert_firing) { create(:prometheus_alert, project: project) } let_it_be(:alert_resolved) { create(:prometheus_alert, project: project) } - let_it_be(:cluster) { create(:cluster, :provided_by_user, projects: [project]) } + let_it_be(:cluster, reload: true) { create(:cluster, :provided_by_user, projects: [project]) } + let(:payload_raw) { prometheus_alert_payload(firing: [alert_firing], resolved: [alert_resolved]) } let(:payload) { ActionController::Parameters.new(payload_raw).permit! } let(:payload_alert_firing) { payload_raw['alerts'].first } let(:token) { 'token' } + let(:source) { 'Prometheus' } context 'with environment specific clusters' do let(:prd_cluster) do @@ -53,15 +54,15 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do context 'without token' do let(:token_input) { nil } - it_behaves_like 'Alert Notification Service sends notification email' + include_examples 'processes one firing and one resolved prometheus alerts' end context 'with token' do - it_behaves_like 'Alert Notification Service sends no notifications', http_status: :unauthorized + it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized end end - context 'with project specific cluster' do + context 'with project specific cluster using prometheus application' do where(:cluster_enabled, :status, :configured_token, :token_input, :result) do true | :installed | token | token | :success true | :installed | nil | nil | :success @@ -87,9 +88,43 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do case result = params[:result] when :success - it_behaves_like 'Alert Notification Service sends notification email' + include_examples 'processes one firing and one resolved prometheus alerts' + when :failure + it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized + else + raise "invalid result: #{result.inspect}" + end + end + end + + context 'with project specific cluster using prometheus integration' do + where(:cluster_enabled, :integration_enabled, :configured_token, :token_input, :result) do + true | true | token | token | :success + true | true | nil | nil | :success + true | true | token | 'x' | :failure + true | true | token | nil | :failure + true | false | token | token | :failure + false | true | token | token | :failure + false | nil | nil | token | :failure + end + + with_them do + before do + cluster.update!(enabled: cluster_enabled) + + unless integration_enabled.nil? + create(:clusters_integrations_prometheus, + cluster: cluster, + enabled: integration_enabled, + alert_manager_token: configured_token) + end + end + + case result = params[:result] + when :success + include_examples 'processes one firing and one resolved prometheus alerts' when :failure - it_behaves_like 'Alert Notification Service sends no notifications', http_status: :unauthorized + it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized else raise "invalid result: #{result.inspect}" end @@ -97,9 +132,9 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do end context 'without project specific cluster' do - let!(:cluster) { create(:cluster, enabled: true) } + let_it_be(:cluster) { create(:cluster, enabled: true) } - it_behaves_like 'Alert Notification Service sends no notifications', http_status: :unauthorized + it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized end context 'with manual prometheus installation' do @@ -126,9 +161,9 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do case result = params[:result] when :success - it_behaves_like 'Alert Notification Service sends notification email' + it_behaves_like 'processes one firing and one resolved prometheus alerts' when :failure - it_behaves_like 'Alert Notification Service sends no notifications', http_status: :unauthorized + it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized else raise "invalid result: #{result.inspect}" end @@ -150,50 +185,53 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do let(:token_input) { public_send(token) if token } let(:integration) { create(:alert_management_http_integration, active, project: project) if active } - let(:subject) { service.execute(token_input, integration) } + subject { service.execute(token_input, integration) } case result = params[:result] when :success - it_behaves_like 'Alert Notification Service sends notification email' + it_behaves_like 'processes one firing and one resolved prometheus alerts' when :failure - it_behaves_like 'Alert Notification Service sends no notifications', http_status: :unauthorized + it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized else raise "invalid result: #{result.inspect}" end end end - context 'alert emails' do + context 'incident settings' do before do create(:prometheus_service, project: project) create(:project_alerting_setting, project: project, token: token) end - context 'when incident_management_setting does not exist' do - let!(:setting) { nil } + it_behaves_like 'processes one firing and one resolved prometheus alerts' - it 'does not send notification email', :sidekiq_might_not_need_inline do - expect_any_instance_of(NotificationService) - .not_to receive(:async) - - expect(subject).to be_success + context 'when incident_management_setting does not exist' do + before do + setting.destroy! end - end - context 'when incident_management_setting.send_email is true' do - it_behaves_like 'Alert Notification Service sends notification email' + it { is_expected.to be_success } + include_examples 'does not send alert notification emails' + include_examples 'does not process incident issues' end context 'incident_management_setting.send_email is false' do - let!(:setting) do - create(:project_incident_management_setting, send_email: false, project: project) + before do + setting.update!(send_email: false) end - it 'does not send notification' do - expect(NotificationService).not_to receive(:new) + it { is_expected.to be_success } + include_examples 'does not send alert notification emails' + end - expect(subject).to be_success + context 'incident_management_setting.create_issue is false' do + before do + setting.update!(create_issue: false) end + + it { is_expected.to be_success } + include_examples 'does not process incident issues' end end @@ -233,7 +271,7 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do .and_return(false) end - it_behaves_like 'Alert Notification Service sends no notifications', http_status: :unprocessable_entity + it_behaves_like 'alerts service responds with an error and takes no actions', :unprocessable_entity end context 'when the payload is too big' do @@ -244,14 +282,7 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do allow(Gitlab::Utils::DeepSize).to receive(:new).and_return(deep_size_object) end - it_behaves_like 'Alert Notification Service sends no notifications', http_status: :bad_request - - it 'does not process Prometheus alerts' do - expect(AlertManagement::ProcessPrometheusAlertService) - .not_to receive(:new) - - subject - end + it_behaves_like 'alerts service responds with an error and takes no actions', :bad_request end end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 5f41ec1d610..8498b752610 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -130,7 +130,7 @@ RSpec.describe Projects::TransferService do execute_transfer expect(project.slack_service.webhook).to eq(group_integration.webhook) - expect(Service.count).to eq(3) + expect(Integration.count).to eq(3) end end diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb index 90def365fca..d939a79b7e9 100644 --- a/spec/services/projects/unlink_fork_service_spec.rb +++ b/spec/services/projects/unlink_fork_service_spec.rb @@ -16,11 +16,11 @@ RSpec.describe Projects::UnlinkForkService, :use_clean_rails_memory_store_cachin let(:merge_request2) { create(:merge_request, source_project: forked_project, target_project: fork_project(project)) } let(:merge_request_in_fork) { create(:merge_request, source_project: forked_project, target_project: forked_project) } - let(:mr_close_service) { MergeRequests::CloseService.new(forked_project, user) } + let(:mr_close_service) { MergeRequests::CloseService.new(project: forked_project, current_user: user) } before do allow(MergeRequests::CloseService).to receive(:new) - .with(forked_project, user) + .with(project: forked_project, current_user: user) .and_return(mr_close_service) end @@ -79,11 +79,11 @@ RSpec.describe Projects::UnlinkForkService, :use_clean_rails_memory_store_cachin let!(:merge_request2) { create(:merge_request, source_project: project, target_project: fork_project(project)) } let!(:merge_request_in_fork) { create(:merge_request, source_project: forked_project, target_project: forked_project) } - let(:mr_close_service) { MergeRequests::CloseService.new(project, user) } + let(:mr_close_service) { MergeRequests::CloseService.new(project: project, current_user: user) } before do allow(MergeRequests::CloseService).to receive(:new) - .with(project, user) + .with(project: project, current_user: user) .and_return(mr_close_service) end @@ -142,11 +142,11 @@ RSpec.describe Projects::UnlinkForkService, :use_clean_rails_memory_store_cachin let!(:mr_from_child) { create(:merge_request, source_project: fork_of_fork, target_project: forked_project) } let!(:merge_request_in_fork) { create(:merge_request, source_project: forked_project, target_project: forked_project) } - let(:mr_close_service) { MergeRequests::CloseService.new(forked_project, user) } + let(:mr_close_service) { MergeRequests::CloseService.new(project: forked_project, current_user: user) } before do allow(MergeRequests::CloseService).to receive(:new) - .with(forked_project, user) + .with(project: forked_project, current_user: user) .and_return(mr_close_service) end diff --git a/spec/services/projects/update_repository_storage_service_spec.rb b/spec/services/projects/update_repository_storage_service_spec.rb index 828667fdfc2..5b15b7d5f34 100644 --- a/spec/services/projects/update_repository_storage_service_spec.rb +++ b/spec/services/projects/update_repository_storage_service_spec.rb @@ -36,7 +36,7 @@ RSpec.describe Projects::UpdateRepositoryStorageService do end context 'when the move succeeds' do - it 'moves the repository to the new storage and unmarks the repository as read only' do + it 'moves the repository to the new storage and unmarks the repository as read-only' do old_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do project.repository.path_to_repo end diff --git a/spec/services/projects/update_statistics_service_spec.rb b/spec/services/projects/update_statistics_service_spec.rb index 92e97186be3..6987185b549 100644 --- a/spec/services/projects/update_statistics_service_spec.rb +++ b/spec/services/projects/update_statistics_service_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Projects::UpdateStatisticsService do + using RSpec::Parameterized::TableSyntax + let(:service) { described_class.new(project, nil, statistics: statistics)} let(:statistics) { %w(repository_size) } @@ -18,12 +20,46 @@ RSpec.describe Projects::UpdateStatisticsService do end context 'with an existing project' do - let(:project) { create(:project) } + let_it_be(:project) { create(:project) } + + where(:statistics, :method_caches) do + [] | %i(size commit_count) + ['repository_size'] | [:size] + [:repository_size] | [:size] + [:lfs_objects_size] | nil + [:commit_count] | [:commit_count] # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands + [:repository_size, :commit_count] | %i(size commit_count) + [:repository_size, :commit_count, :lfs_objects_size] | %i(size commit_count) + end + + with_them do + it 'refreshes the project statistics' do + expect(project.statistics).to receive(:refresh!).with(only: statistics.map(&:to_sym)).and_call_original + + service.execute + end + + it 'invalidates the method caches after a refresh' do + expect(project.wiki.repository).not_to receive(:expire_method_caches) + + if method_caches.present? + expect(project.repository).to receive(:expire_method_caches).with(method_caches).and_call_original + else + expect(project.repository).not_to receive(:expire_method_caches) + end + + service.execute + end + end + end + + context 'with an existing project with a Wiki' do + let(:project) { create(:project, :repository, :wiki_enabled) } + let(:statistics) { [:wiki_size] } - it 'refreshes the project statistics' do - expect_any_instance_of(ProjectStatistics).to receive(:refresh!) - .with(only: statistics.map(&:to_sym)) - .and_call_original + it 'invalidates and refreshes Wiki size' do + expect(project.statistics).to receive(:refresh!).with(only: statistics).and_call_original + expect(project.wiki.repository).to receive(:expire_method_caches).with(%i(size)).and_call_original service.execute end diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index 9df238c6dac..f3ad69bae13 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -360,25 +360,29 @@ RSpec.describe QuickActions::InterpretService do shared_examples 'spend command' do it 'populates spend_time: 3600 if content contains /spend 1h' do - _, updates, _ = service.execute(content, issuable) + freeze_time do + _, updates, _ = service.execute(content, issuable) - expect(updates).to eq(spend_time: { - duration: 3600, - user_id: developer.id, - spent_at: DateTime.current.to_date - }) + expect(updates).to eq(spend_time: { + duration: 3600, + user_id: developer.id, + spent_at: DateTime.current + }) + end end end shared_examples 'spend command with negative time' do it 'populates spend_time: -7200 if content contains -120m' do - _, updates, _ = service.execute(content, issuable) + freeze_time do + _, updates, _ = service.execute(content, issuable) - expect(updates).to eq(spend_time: { - duration: -7200, - user_id: developer.id, - spent_at: DateTime.current.to_date - }) + expect(updates).to eq(spend_time: { + duration: -7200, + user_id: developer.id, + spent_at: DateTime.current + }) + end end it 'returns the spend_time message including the formatted duration and verb' 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 ff7ab614e08..44f8f07a5be 100644 --- a/spec/services/security/ci_configuration/sast_create_service_spec.rb +++ b/spec/services/security/ci_configuration/sast_create_service_spec.rb @@ -3,67 +3,24 @@ require 'spec_helper' RSpec.describe Security::CiConfiguration::SastCreateService, :snowplow do - describe '#execute' do - let_it_be(:project) { create(:project, :repository) } - let_it_be(:user) { create(:user) } - let(:params) { {} } + subject(:result) { described_class.new(project, user, params).execute } - subject(:result) { described_class.new(project, user, params).execute } + let(:branch_name) { 'set-sast-config-1' } - context 'user does not belong to project' do - it 'returns an error status' do - expect(result[:status]).to eq(:error) - expect(result[:success_path]).to be_nil - end - - it 'does not track a snowplow event' do - subject - - expect_no_snowplow_event - end - end - - context 'user belongs to project' do - before do - project.add_developer(user) - end - - it 'does track the snowplow event' do - subject - - expect_snowplow_event( - category: 'Security::CiConfiguration::SastCreateService', - action: 'create', - label: 'false' - ) - end - - it 'raises exception if the user does not have permission to create a new branch' do - allow(project).to receive(:repository).and_raise(Gitlab::Git::PreReceiveError, "You are not allowed to create protected branches on this project.") - - expect { subject }.to raise_error(Gitlab::Git::PreReceiveError) - end - - context 'with no parameters' do - it 'returns the path to create a new merge request' do - expect(result[:status]).to eq(:success) - expect(result[:success_path]).to match(/#{Gitlab::Routing.url_helpers.project_new_merge_request_url(project, {})}(.*)description(.*)source_branch/) - end - end - - context 'with parameters' do - let(:params) do - { 'stage' => 'security', - 'SEARCH_MAX_DEPTH' => 1, - 'SECURE_ANALYZERS_PREFIX' => 'new_registry', - 'SAST_EXCLUDED_PATHS' => 'spec,docs' } - end + let(:non_empty_params) do + { 'stage' => 'security', + 'SEARCH_MAX_DEPTH' => 1, + 'SECURE_ANALYZERS_PREFIX' => 'new_registry', + 'SAST_EXCLUDED_PATHS' => 'spec,docs' } + end - it 'returns the path to create a new merge request' do - expect(result[:status]).to eq(:success) - expect(result[:success_path]).to match(/#{Gitlab::Routing.url_helpers.project_new_merge_request_url(project, {})}(.*)description(.*)source_branch/) - end - end - end + let(:snowplow_event) do + { + category: 'Security::CiConfiguration::SastCreateService', + action: 'create', + label: 'false' + } end + + include_examples 'services security ci configuration create service' end diff --git a/spec/services/security/ci_configuration/secret_detection_create_service_spec.rb b/spec/services/security/ci_configuration/secret_detection_create_service_spec.rb new file mode 100644 index 00000000000..c1df3ebdca5 --- /dev/null +++ b/spec/services/security/ci_configuration/secret_detection_create_service_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Security::CiConfiguration::SecretDetectionCreateService, :snowplow do + subject(:result) { described_class.new(project, user).execute } + + let(:branch_name) { 'set-secret-detection-config-1' } + + let(:snowplow_event) do + { + category: 'Security::CiConfiguration::SecretDetectionCreateService', + action: 'create', + label: '' + } + end + + include_examples 'services security ci configuration create service', true +end diff --git a/spec/services/snippets/update_repository_storage_service_spec.rb b/spec/services/snippets/update_repository_storage_service_spec.rb index 6ba09a9dca9..50b28a5a125 100644 --- a/spec/services/snippets/update_repository_storage_service_spec.rb +++ b/spec/services/snippets/update_repository_storage_service_spec.rb @@ -31,7 +31,7 @@ RSpec.describe Snippets::UpdateRepositoryStorageService do end context 'when the move succeeds' do - it 'moves the repository to the new storage and unmarks the repository as read only' do + it 'moves the repository to the new storage and unmarks the repository as read-only' do old_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do snippet.repository.path_to_repo end diff --git a/spec/services/spam/spam_action_service_spec.rb b/spec/services/spam/spam_action_service_spec.rb index e8ac826df1c..9ca52b92267 100644 --- a/spec/services/spam/spam_action_service_spec.rb +++ b/spec/services/spam/spam_action_service_spec.rb @@ -9,11 +9,11 @@ RSpec.describe Spam::SpamActionService do let(:issue) { create(:issue, project: project, author: user) } let(:fake_ip) { '1.2.3.4' } let(:fake_user_agent) { 'fake-user-agent' } - let(:fake_referrer) { 'fake-http-referrer' } + let(:fake_referer) { 'fake-http-referer' } let(:env) do { 'action_dispatch.remote_ip' => fake_ip, 'HTTP_USER_AGENT' => fake_user_agent, - 'HTTP_REFERRER' => fake_referrer } + 'HTTP_REFERER' => fake_referer } end let_it_be(:project) { create(:project, :public) } @@ -80,7 +80,7 @@ RSpec.describe Spam::SpamActionService do { ip_address: fake_ip, user_agent: fake_user_agent, - referrer: fake_referrer + referer: fake_referer } end @@ -222,6 +222,38 @@ RSpec.describe Spam::SpamActionService do end end + context 'spam verdict service advises to block the user' do + before do + allow(fake_verdict_service).to receive(:execute).and_return(BLOCK_USER) + end + + context 'when allow_possible_spam feature flag is false' do + before do + stub_feature_flags(allow_possible_spam: false) + end + + it_behaves_like 'only checks for spam if a request is provided' + + it 'marks as spam' do + response = subject + + expect(response.message).to match(expected_service_check_response_message) + expect(issue).to be_spam + end + end + + context 'when allow_possible_spam feature flag is true' do + it_behaves_like 'only checks for spam if a request is provided' + + it 'does not mark as spam' do + response = subject + + expect(response.message).to match(expected_service_check_response_message) + expect(issue).not_to be_spam + end + end + end + context 'when spam verdict service conditionally allows' do before do allow(fake_verdict_service).to receive(:execute).and_return(CONDITIONAL_ALLOW) @@ -281,6 +313,22 @@ RSpec.describe Spam::SpamActionService do end end + context 'when spam verdict service returns noop' do + before do + allow(fake_verdict_service).to receive(:execute).and_return(NOOP) + end + + it 'does not create a spam log' do + expect { subject }.not_to change(SpamLog, :count) + end + + it 'clears spam flags' do + expect(issue).to receive(:clear_spam_flags!) + + subject + end + end + context 'with spam verdict service options' do before do allow(fake_verdict_service).to receive(:execute).and_return(ALLOW) diff --git a/spec/services/spam/spam_verdict_service_spec.rb b/spec/services/spam/spam_verdict_service_spec.rb index 14b788e3a86..215df81de63 100644 --- a/spec/services/spam/spam_verdict_service_spec.rb +++ b/spec/services/spam/spam_verdict_service_spec.rb @@ -7,28 +7,33 @@ RSpec.describe Spam::SpamVerdictService do let(:fake_ip) { '1.2.3.4' } let(:fake_user_agent) { 'fake-user-agent' } - let(:fake_referrer) { 'fake-http-referrer' } + let(:fake_referer) { 'fake-http-referer' } let(:env) do { 'action_dispatch.remote_ip' => fake_ip, 'HTTP_USER_AGENT' => fake_user_agent, - 'HTTP_REFERRER' => fake_referrer } + 'HTTP_REFERER' => fake_referer } end let(:request) { double(:request, env: env) } let(:check_for_spam) { true } let_it_be(:user) { create(:user) } - let(:issue) { build(:issue, author: user) } + let_it_be(:issue) { create(:issue, author: user) } let(:service) do described_class.new(user: user, target: issue, request: request, options: {}) end + let(:attribs) do + extra_attributes = { "monitorMode" => "false" } + extra_attributes + end + describe '#execute' do subject { service.execute } before do allow(service).to receive(:akismet_verdict).and_return(nil) - allow(service).to receive(:external_verdict).and_return(nil) + allow(service).to receive(:spamcheck_verdict).and_return([nil, attribs]) end context 'if all services return nil' do @@ -63,7 +68,7 @@ RSpec.describe Spam::SpamVerdictService do context 'and they are supported' do before do allow(service).to receive(:akismet_verdict).and_return(DISALLOW) - allow(service).to receive(:external_verdict).and_return(BLOCK_USER) + allow(service).to receive(:spamcheck_verdict).and_return([BLOCK_USER, attribs]) end it 'renders the more restrictive verdict' do @@ -74,7 +79,7 @@ RSpec.describe Spam::SpamVerdictService do context 'and one is supported' do before do allow(service).to receive(:akismet_verdict).and_return('nonsense') - allow(service).to receive(:external_verdict).and_return(BLOCK_USER) + allow(service).to receive(:spamcheck_verdict).and_return([BLOCK_USER, attribs]) end it 'renders the more restrictive verdict' do @@ -85,13 +90,56 @@ RSpec.describe Spam::SpamVerdictService do context 'and none are supported' do before do allow(service).to receive(:akismet_verdict).and_return('nonsense') - allow(service).to receive(:external_verdict).and_return('rubbish') + allow(service).to receive(:spamcheck_verdict).and_return(['rubbish', attribs]) end it 'renders the more restrictive verdict' do expect(subject).to eq ALLOW end end + + context 'and attribs - monitorMode is true' do + let(:attribs) do + extra_attributes = { "monitorMode" => "true" } + extra_attributes + end + + before do + allow(service).to receive(:akismet_verdict).and_return(DISALLOW) + allow(service).to receive(:spamcheck_verdict).and_return([BLOCK_USER, attribs]) + end + + it 'renders the more restrictive verdict' do + expect(subject).to eq(DISALLOW) + end + end + end + + context 'records metrics' do + let(:histogram) { instance_double(Prometheus::Client::Histogram) } + + using RSpec::Parameterized::TableSyntax + + where(:verdict, :error, :label) do + Spam::SpamConstants::ALLOW | false | 'ALLOW' + Spam::SpamConstants::ALLOW | true | 'ERROR' + Spam::SpamConstants::CONDITIONAL_ALLOW | false | 'CONDITIONAL_ALLOW' + Spam::SpamConstants::BLOCK_USER | false | 'BLOCK' + Spam::SpamConstants::DISALLOW | false | 'DISALLOW' + Spam::SpamConstants::NOOP | false | 'NOOP' + end + + with_them do + before do + allow(Gitlab::Metrics).to receive(:histogram).with(:gitlab_spamcheck_request_duration_seconds, anything).and_return(histogram) + allow(service).to receive(:spamcheck_verdict).and_return([verdict, attribs, error]) + end + + it 'records duration with labels' do + expect(histogram).to receive(:observe).with(a_hash_including(result: label), anything) + subject + end + end end end @@ -150,48 +198,113 @@ RSpec.describe Spam::SpamVerdictService do end end - describe '#external_verdict' do - subject { service.send(:external_verdict) } + describe '#spamcheck_verdict' do + subject { service.send(:spamcheck_verdict) } context 'if a Spam Check endpoint enabled and set to a URL' do let(:spam_check_body) { {} } - let(:spam_check_http_status) { nil } + let(:endpoint_url) { "grpc://www.spamcheckurl.com/spam_check" } + + let(:spam_client) do + Gitlab::Spamcheck::Client.new + end before do stub_application_setting(spam_check_endpoint_enabled: true) - stub_application_setting(spam_check_endpoint_url: "http://www.spamcheckurl.com/spam_check") - stub_request(:post, /.*spamcheckurl.com.*/).to_return( body: spam_check_body.to_json, status: spam_check_http_status ) + stub_application_setting(spam_check_endpoint_url: endpoint_url) end context 'if the endpoint is accessible' do - let(:spam_check_http_status) { 200 } - let(:error) { nil } + let(:error) { '' } let(:verdict) { nil } - let(:spam_check_body) do - { verdict: verdict, error: error } + + let(:attribs) do + extra_attributes = { "monitorMode" => "false" } + extra_attributes + end + + before do + allow(service).to receive(:spamcheck_client).and_return(spam_client) + allow(spam_client).to receive(:issue_spam?).and_return([verdict, attribs, error]) + end + + context 'if the result is a NOOP verdict' do + let(:verdict) { NOOP } + + it 'returns the verdict' do + expect(subject).to eq([NOOP, attribs]) + end + end + + context 'if attribs - monitorMode is true' do + let(:attribs) do + extra_attributes = { "monitorMode" => "true" } + extra_attributes + end + + let(:verdict) { ALLOW } + + it 'returns the verdict' do + expect(subject).to eq([ALLOW, attribs]) + end end context 'the result is a valid verdict' do - let(:verdict) { 'allow' } + let(:verdict) { ALLOW } it 'returns the verdict' do - expect(subject).to eq ALLOW + expect(subject).to eq([ALLOW, attribs]) end end - context 'the verdict is an unexpected string' do - let(:verdict) { 'this is fine' } + context 'when recaptcha is enabled' do + before do + allow(Gitlab::Recaptcha).to receive(:enabled?).and_return(true) + end - it 'returns the string' do - expect(subject).to eq verdict + using RSpec::Parameterized::TableSyntax + + # rubocop: disable Lint/BinaryOperatorWithIdenticalOperands + where(:verdict_value, :expected) do + ::Spam::SpamConstants::ALLOW | ::Spam::SpamConstants::ALLOW + ::Spam::SpamConstants::CONDITIONAL_ALLOW | ::Spam::SpamConstants::CONDITIONAL_ALLOW + ::Spam::SpamConstants::DISALLOW | ::Spam::SpamConstants::CONDITIONAL_ALLOW + ::Spam::SpamConstants::BLOCK_USER | ::Spam::SpamConstants::CONDITIONAL_ALLOW + end + # rubocop: enable Lint/BinaryOperatorWithIdenticalOperands + + with_them do + let(:verdict) { verdict_value } + + it "returns expected spam constant" do + expect(subject).to eq([expected, attribs]) + end end end - context 'the JSON is malformed' do - let(:spam_check_body) { 'this is fine' } + context 'when recaptcha is disabled' do + before do + allow(Gitlab::Recaptcha).to receive(:enabled?).and_return(false) + end + + [::Spam::SpamConstants::ALLOW, + ::Spam::SpamConstants::CONDITIONAL_ALLOW, + ::Spam::SpamConstants::DISALLOW, + ::Spam::SpamConstants::BLOCK_USER].each do |verdict_value| + let(:verdict) { verdict_value } + let(:expected) { [verdict_value, attribs] } - it 'returns allow' do - expect(subject).to eq ALLOW + it "returns expected spam constant" do + expect(subject).to eq(expected) + end + end + end + + context 'the verdict is an unexpected value' do + let(:verdict) { :this_is_fine } + + it 'returns the string' do + expect(subject).to eq([verdict, attribs]) end end @@ -199,7 +312,7 @@ RSpec.describe Spam::SpamVerdictService do let(:verdict) { '' } it 'returns nil' do - expect(subject).to eq verdict + expect(subject).to eq([verdict, attribs]) end end @@ -207,7 +320,7 @@ RSpec.describe Spam::SpamVerdictService do let(:verdict) { nil } it 'returns nil' do - expect(subject).to be_nil + expect(subject).to eq([nil, attribs]) end end @@ -215,15 +328,19 @@ RSpec.describe Spam::SpamVerdictService do let(:error) { "Sorry Dave, I can't do that" } it 'returns nil' do - expect(subject).to be_nil + expect(subject).to eq([nil, attribs]) end end - context 'the HTTP status is not 200' do - let(:spam_check_http_status) { 500 } + context 'the requested is aborted' do + let(:attribs) { nil } + + before do + allow(spam_client).to receive(:issue_spam?).and_raise(GRPC::Aborted) + end it 'returns nil' do - expect(subject).to be_nil + expect(subject).to eq([ALLOW, attribs, true]) end end @@ -232,18 +349,20 @@ RSpec.describe Spam::SpamVerdictService do let(:error) { 'oh noes!' } it 'renders the verdict' do - expect(subject).to eq DISALLOW + expect(subject).to eq [DISALLOW, attribs] end end end context 'if the endpoint times out' do + let(:attribs) { nil } + before do - stub_request(:post, /.*spamcheckurl.com.*/).to_timeout + allow(spam_client).to receive(:issue_spam?).and_raise(GRPC::DeadlineExceeded) end it 'returns nil' do - expect(subject).to be_nil + expect(subject).to eq([ALLOW, attribs, true]) end end end diff --git a/spec/services/submit_usage_ping_service_spec.rb b/spec/services/submit_usage_ping_service_spec.rb index 53cc33afcff..a9f1b2c2b2d 100644 --- a/spec/services/submit_usage_ping_service_spec.rb +++ b/spec/services/submit_usage_ping_service_spec.rb @@ -217,7 +217,7 @@ RSpec.describe SubmitUsagePingService do end def stub_response(body:, status: 201) - stub_full_request(SubmitUsagePingService::STAGING_URL, method: :post) + stub_full_request(subject.send(:url), method: :post) .to_return( headers: { 'Content-Type' => 'application/json' }, body: body.to_json, diff --git a/spec/services/suggestions/apply_service_spec.rb b/spec/services/suggestions/apply_service_spec.rb index 77d0e892725..9cf794cde7e 100644 --- a/spec/services/suggestions/apply_service_spec.rb +++ b/spec/services/suggestions/apply_service_spec.rb @@ -67,11 +67,13 @@ RSpec.describe Suggestions::ApplyService do apply(suggestions) commit = project.repository.commit + author = suggestions.first.note.author expect(user.commit_email).not_to eq(user.email) - expect(commit.author_email).to eq(user.commit_email) + expect(commit.author_email).to eq(author.commit_email) expect(commit.committer_email).to eq(user.commit_email) - expect(commit.author_name).to eq(user.name) + expect(commit.author_name).to eq(author.name) + expect(commit.committer_name).to eq(user.name) end it 'tracks apply suggestion event' do @@ -319,6 +321,73 @@ RSpec.describe Suggestions::ApplyService do end end + context 'single suggestion' do + let(:author) { suggestions.first.note.author } + let(:commit) { project.repository.commit } + + context 'author of suggestion applies suggestion' do + before do + suggestion.note.update!(author_id: user.id) + + apply(suggestions) + end + + it 'created commit by same author and committer' do + expect(user.commit_email).to eq(author.commit_email) + expect(author).to eq(user) + expect(commit.author_email).to eq(author.commit_email) + expect(commit.committer_email).to eq(user.commit_email) + expect(commit.author_name).to eq(author.name) + expect(commit.committer_name).to eq(user.name) + end + end + + context 'another user applies suggestion' do + before do + apply(suggestions) + end + + it 'created commit has authors info and commiters info' do + expect(user.commit_email).not_to eq(user.email) + expect(author).not_to eq(user) + expect(commit.author_email).to eq(author.commit_email) + expect(commit.committer_email).to eq(user.commit_email) + expect(commit.author_name).to eq(author.name) + expect(commit.committer_name).to eq(user.name) + end + end + end + + context 'multiple suggestions' do + let(:author_emails) { suggestions.map {|s| s.note.author.commit_email } } + let(:first_author) { suggestion.note.author } + let(:commit) { project.repository.commit } + + context 'when all the same author' do + before do + apply(suggestions) + end + + it 'uses first authors information' do + expect(author_emails).to include(first_author.commit_email).exactly(3) + expect(commit.author_email).to eq(first_author.commit_email) + end + end + + context 'when all different authors' do + before do + suggestion2.note.update!(author_id: create(:user).id) + suggestion3.note.update!(author_id: create(:user).id) + apply(suggestions) + end + + it 'uses committers information' do + expect(commit.author_email).to eq(user.commit_email) + expect(commit.committer_email).to eq(user.commit_email) + end + end + end + context 'multiple suggestions applied sequentially' do def apply_suggestion(suggestion) suggestion.reload @@ -329,7 +398,7 @@ RSpec.describe Suggestions::ApplyService do suggestion.reload expect(result[:status]).to eq(:success) - refresh = MergeRequests::RefreshService.new(project, user) + refresh = MergeRequests::RefreshService.new(project: project, current_user: user) refresh.execute(merge_request.diff_head_sha, suggestion.commit_id, merge_request.source_branch_ref) diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index d8435c72896..5d60b6e0487 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -3,133 +3,68 @@ require 'spec_helper' RSpec.describe SystemHooksService do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:project_member) { create(:project_member) } - let(:key) { create(:key, user: user) } - let(:deploy_key) { create(:key) } - let(:group) { create(:group) } - let(:group_member) { create(:group_member) } - - context 'event data' do - it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username) } - it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username) } - it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } - it { expect(event_data(project, :update)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } - it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } - it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :user_id, :access_level, :project_visibility) } - it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :user_id, :access_level, :project_visibility) } - it { expect(event_data(project_member, :update)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :user_id, :access_level, :project_visibility) } - it { expect(event_data(key, :create)).to include(:username, :key, :id) } - it { expect(event_data(key, :destroy)).to include(:username, :key, :id) } - it { expect(event_data(deploy_key, :create)).to include(:key, :id) } - it { expect(event_data(deploy_key, :destroy)).to include(:key, :id) } - - it do - project.old_path_with_namespace = 'renamed_from_path' - expect(event_data(project, :rename)).to include( - :event_name, :name, :created_at, :updated_at, :path, :project_id, - :owner_name, :owner_email, :project_visibility, - :old_path_with_namespace - ) + describe '#execute_hooks_for' do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project) } + let_it_be(:group_member) { create(:group_member, source: group, user: user) } + let_it_be(:project_member) { create(:project_member, source: project, user: user) } + let_it_be(:key) { create(:key, user: user) } + let_it_be(:deploy_key) { create(:key) } + + let(:event) { :create } + + using RSpec::Parameterized::TableSyntax + + where(:model_name, :builder_class) do + :group_member | Gitlab::HookData::GroupMemberBuilder + :group | Gitlab::HookData::GroupBuilder + :project_member | Gitlab::HookData::ProjectMemberBuilder + :user | Gitlab::HookData::UserBuilder + :project | Gitlab::HookData::ProjectBuilder + :key | Gitlab::HookData::KeyBuilder + :deploy_key | Gitlab::HookData::KeyBuilder end - it do - project.old_path_with_namespace = 'transferred_from_path' - expect(event_data(project, :transfer)).to include( - :event_name, :name, :created_at, :updated_at, :path, :project_id, - :owner_name, :owner_email, :project_visibility, - :old_path_with_namespace - ) - end - - it do - expect(event_data(group, :create)).to include( - :event_name, :name, :created_at, :updated_at, :path, :group_id - ) - end + with_them do + it 'builds the data with the relevant builder class and then calls #execute_hooks with the obtained data' do + data = double + model = public_send(model_name) - it do - expect(event_data(group, :destroy)).to include( - :event_name, :name, :created_at, :updated_at, :path, :group_id - ) - end + expect_next_instance_of(builder_class, model) do |builder| + expect(builder).to receive(:build).with(event).and_return(data) + end - it do - expect(event_data(group_member, :create)).to include( - :event_name, :created_at, :updated_at, :group_name, :group_path, - :group_id, :user_id, :user_username, :user_name, :user_email, :group_access - ) - end + service = described_class.new - it do - expect(event_data(group_member, :destroy)).to include( - :event_name, :created_at, :updated_at, :group_name, :group_path, - :group_id, :user_id, :user_username, :user_name, :user_email, :group_access - ) - end - - it do - expect(event_data(group_member, :update)).to include( - :event_name, :created_at, :updated_at, :group_name, :group_path, - :group_id, :user_id, :user_username, :user_name, :user_email, :group_access - ) - end - - it 'includes the correct project visibility level' do - data = event_data(project, :create) - - expect(data[:project_visibility]).to eq('private') - end + expect_next_instance_of(SystemHooksService) do |system_hook_service| + expect(system_hook_service).to receive(:execute_hooks).with(data) + end - it 'handles nil datetime columns' do - user.update!(created_at: nil, updated_at: nil) - data = event_data(user, :destroy) - - expect(data[:created_at]).to be(nil) - expect(data[:updated_at]).to be(nil) + service.execute_hooks_for(model, event) + end end + end - context 'group_rename' do - it 'contains old and new path' do - allow(group).to receive(:path_before_last_save).and_return('old-path') + describe '#execute_hooks' do + let(:data) { { key: :value } } - data = event_data(group, :rename) + subject { described_class.new.execute_hooks(data) } - expect(data).to include(:event_name, :name, :created_at, :updated_at, :full_path, :path, :group_id, :old_path, :old_full_path) - expect(data[:path]).to eq(group.path) - expect(data[:full_path]).to eq(group.path) - expect(data[:old_path]).to eq(group.path_before_last_save) - expect(data[:old_full_path]).to eq(group.path_before_last_save) - end + it 'executes system hooks with the given data' do + hook = create(:system_hook) - it 'contains old and new full_path for subgroup' do - subgroup = create(:group, parent: group) - allow(subgroup).to receive(:path_before_last_save).and_return('old-path') + allow(SystemHook).to receive_message_chain(:hooks_for, :find_each).and_yield(hook) - data = event_data(subgroup, :rename) + expect(hook).to receive(:async_execute).with(data, 'system_hooks') - expect(data[:full_path]).to eq(subgroup.full_path) - expect(data[:old_path]).to eq('old-path') - end + subject end - end - context 'event names' do - it { expect(event_name(project, :create)).to eq "project_create" } - it { expect(event_name(project, :destroy)).to eq "project_destroy" } - it { expect(event_name(project, :rename)).to eq "project_rename" } - it { expect(event_name(project, :transfer)).to eq "project_transfer" } - it { expect(event_name(project, :update)).to eq "project_update" } - it { expect(event_name(key, :create)).to eq 'key_create' } - it { expect(event_name(key, :destroy)).to eq 'key_destroy' } - end + it 'executes FileHook with the given data' do + expect(Gitlab::FileHook).to receive(:execute_all_async).with(data) - def event_data(*args) - SystemHooksService.new.send :build_event_data, *args - end - - def event_name(*args) - SystemHooksService.new.send :build_event_name, *args + subject + end end end diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 59f936509df..6a8e6dc8970 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -345,17 +345,10 @@ RSpec.describe TodoService do describe '#destroy_target' do it 'refreshes the todos count cache for users with todos on the target' do - create(:todo, state: :pending, target: issue, user: john_doe, author: john_doe, project: issue.project) + create(:todo, state: :pending, target: issue, user: author, author: author, project: issue.project) + create(:todo, state: :done, target: issue, user: assignee, author: assignee, project: issue.project) - expect_next(Users::UpdateTodoCountCacheService, [john_doe]).to receive(:execute) - - service.destroy_target(issue) { issue.destroy! } - end - - it 'does not refresh the todos count cache for users with only done todos on the target' do - create(:todo, :done, target: issue, user: john_doe, author: john_doe, project: issue.project) - - expect(Users::UpdateTodoCountCacheService).not_to receive(:new) + expect_next(Users::UpdateTodoCountCacheService, [author.id, assignee.id]).to receive(:execute) service.destroy_target(issue) { issue.destroy! } end @@ -1101,7 +1094,7 @@ RSpec.describe TodoService do it 'updates cached counts when a todo is created' do issue = create(:issue, project: project, assignees: [john_doe], author: author) - expect_next(Users::UpdateTodoCountCacheService, [john_doe]).to receive(:execute) + expect_next(Users::UpdateTodoCountCacheService, [john_doe.id]).to receive(:execute) service.new_issue(issue, author) end diff --git a/spec/services/users/ban_service_spec.rb b/spec/services/users/ban_service_spec.rb new file mode 100644 index 00000000000..0e6ac615da5 --- /dev/null +++ b/spec/services/users/ban_service_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::BanService do + let(:current_user) { create(:admin) } + + subject(:service) { described_class.new(current_user) } + + describe '#execute' do + subject(:operation) { service.execute(user) } + + context 'when successful' do + let(:user) { create(:user) } + + it { is_expected.to eq(status: :success) } + + it "bans the user" do + expect { operation }.to change { user.state }.to('banned') + end + + it "blocks the user" do + expect { operation }.to change { user.blocked? }.from(false).to(true) + end + + it 'logs ban in application logs' do + allow(Gitlab::AppLogger).to receive(:info) + + operation + + expect(Gitlab::AppLogger).to have_received(:info).with(message: "User banned", user: "#{user.username}", email: "#{user.email}", banned_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}") + end + end + + context 'when failed' do + let(:user) { create(:user, :blocked) } + + it 'returns error result' do + aggregate_failures 'error result' do + expect(operation[:status]).to eq(:error) + expect(operation[:message]).to match(/State cannot transition/) + end + end + + it "does not change the user's state" do + expect { operation }.not_to change { user.state } + end + end + end +end diff --git a/spec/services/users/build_service_spec.rb b/spec/services/users/build_service_spec.rb index b2a7d349ce6..e8786c677d1 100644 --- a/spec/services/users/build_service_spec.rb +++ b/spec/services/users/build_service_spec.rb @@ -6,105 +6,76 @@ RSpec.describe Users::BuildService do using RSpec::Parameterized::TableSyntax describe '#execute' do - let(:params) { build_stubbed(:user).slice(:first_name, :last_name, :username, :email, :password) } - - context 'with an admin user' do - let(:params) { build_stubbed(:user).slice(:name, :username, :email, :password) } + let_it_be(:current_user) { nil } - let(:admin_user) { create(:admin) } - let(:service) { described_class.new(admin_user, ActionController::Parameters.new(params).permit!) } + let(:params) { build_stubbed(:user).slice(:first_name, :last_name, :username, :email, :password) } + let(:service) { described_class.new(current_user, params) } - it 'returns a valid user' do - expect(service.execute).to be_valid - end + shared_examples_for 'common build items' do + it { is_expected.to be_valid } it 'sets the created_by_id' do - expect(service.execute.created_by_id).to eq(admin_user.id) + expect(user.created_by_id).to eq(current_user&.id) end - context 'calls the UpdateCanonicalEmailService' do - specify do - expect(Users::UpdateCanonicalEmailService).to receive(:new).and_call_original + it 'calls UpdateCanonicalEmailService' do + expect(Users::UpdateCanonicalEmailService).to receive(:new).and_call_original - service.execute - end + user end - context 'allowed params' do - let(:params) do - { - access_level: 1, - admin: 1, - avatar: anything, - bio: 1, - can_create_group: 1, - color_scheme_id: 1, - email: 1, - external: 1, - force_random_password: 1, - hide_no_password: 1, - hide_no_ssh_key: 1, - linkedin: 1, - name: 1, - password: 1, - password_automatically_set: 1, - password_expires_at: 1, - projects_limit: 1, - remember_me: 1, - skip_confirmation: 1, - skype: 1, - theme_id: 1, - twitter: 1, - username: 1, - website_url: 1, - private_profile: 1, - organization: 1, - location: 1, - public_email: 1 - } - end + context 'when user_type is provided' do + context 'when project_bot' do + before do + params.merge!({ user_type: :project_bot }) + end - it 'sets all allowed attributes' do - admin_user # call first so the admin gets created before setting `expect` + it { expect(user.project_bot?).to be true } + end - expect(User).to receive(:new).with(hash_including(params)).and_call_original + context 'when not a project_bot' do + before do + params.merge!({ user_type: :alert_bot }) + end - service.execute + it { expect(user).to be_human } end end + end + shared_examples_for 'current user not admin' do context 'with "user_default_external" application setting' do where(:user_default_external, :external, :email, :user_default_internal_regex, :result) do true | nil | 'fl@example.com' | nil | true true | true | 'fl@example.com' | nil | true - true | false | 'fl@example.com' | nil | false + true | false | 'fl@example.com' | nil | true # admin difference true | nil | 'fl@example.com' | '' | true true | true | 'fl@example.com' | '' | true - true | false | 'fl@example.com' | '' | false + true | false | 'fl@example.com' | '' | true # admin difference true | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false - true | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | true + true | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false # admin difference true | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false true | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true true | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true - true | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false + true | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true # admin difference false | nil | 'fl@example.com' | nil | false - false | true | 'fl@example.com' | nil | true + false | true | 'fl@example.com' | nil | false # admin difference false | false | 'fl@example.com' | nil | false false | nil | 'fl@example.com' | '' | false - false | true | 'fl@example.com' | '' | true + false | true | 'fl@example.com' | '' | false # admin difference false | false | 'fl@example.com' | '' | false false | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false - false | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | true + false | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false # admin difference false | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false false | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false - false | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true + false | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false # admin difference false | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false end @@ -116,40 +87,11 @@ RSpec.describe Users::BuildService do params.merge!({ external: external, email: email }.compact) end - subject(:user) { service.execute } - - it 'correctly sets user.external' do + it 'sets the value of Gitlab::CurrentSettings.user_default_external' do expect(user.external).to eq(result) end end end - end - - context 'with non admin user' do - let(:user) { create(:user) } - let(:service) { described_class.new(user, params) } - - it 'raises AccessDeniedError exception' do - expect { service.execute }.to raise_error Gitlab::Access::AccessDeniedError - end - - context 'when authorization is skipped' do - subject(:built_user) { service.execute(skip_authorization: true) } - - it { is_expected.to be_valid } - - it 'sets the created_by_id' do - expect(built_user.created_by_id).to eq(user.id) - end - end - end - - context 'with nil user' do - let(:service) { described_class.new(nil, params) } - - it 'returns a valid user' do - expect(service.execute).to be_valid - end context 'when "send_user_confirmation_email" application setting is true' do before do @@ -157,7 +99,7 @@ RSpec.describe Users::BuildService do end it 'does not confirm the user' do - expect(service.execute).not_to be_confirmed + expect(user).not_to be_confirmed end end @@ -167,27 +109,103 @@ RSpec.describe Users::BuildService do end it 'confirms the user' do - expect(service.execute).to be_confirmed + expect(user).to be_confirmed end end - context 'when user_type is provided' do - subject(:user) { service.execute } + context 'with allowed params' do + let(:params) do + { + email: 1, + name: 1, + password: 1, + password_automatically_set: 1, + username: 1, + user_type: 'project_bot' + } + end - context 'when project_bot' do - before do - params.merge!({ user_type: :project_bot }) - end + it 'sets all allowed attributes' do + expect(User).to receive(:new).with(hash_including(params)).and_call_original - it { expect(user.project_bot?).to be true} + user end + end + end - context 'when not a project_bot' do - before do - params.merge!({ user_type: :alert_bot }) - end + context 'with nil current_user' do + subject(:user) { service.execute } + + it_behaves_like 'common build items' + it_behaves_like 'current user not admin' + end + + context 'with non admin current_user' do + let_it_be(:current_user) { create(:user) } + + let(:service) { described_class.new(current_user, params) } + + subject(:user) { service.execute(skip_authorization: true) } + + it 'raises AccessDeniedError exception when authorization is not skipped' do + expect { service.execute }.to raise_error Gitlab::Access::AccessDeniedError + end - it { expect(user.user_type).to be nil } + it_behaves_like 'common build items' + it_behaves_like 'current user not admin' + end + + context 'with an admin current_user' do + let_it_be(:current_user) { create(:admin) } + + let(:params) { build_stubbed(:user).slice(:name, :username, :email, :password) } + let(:service) { described_class.new(current_user, ActionController::Parameters.new(params).permit!) } + + subject(:user) { service.execute } + + it_behaves_like 'common build items' + + context 'with allowed params' do + let(:params) do + { + access_level: 1, + admin: 1, + avatar: anything, + bio: 1, + can_create_group: 1, + color_scheme_id: 1, + email: 1, + external: 1, + force_random_password: 1, + hide_no_password: 1, + hide_no_ssh_key: 1, + linkedin: 1, + name: 1, + password: 1, + password_automatically_set: 1, + password_expires_at: 1, + projects_limit: 1, + remember_me: 1, + skip_confirmation: 1, + skype: 1, + theme_id: 1, + twitter: 1, + username: 1, + website_url: 1, + private_profile: 1, + organization: 1, + location: 1, + public_email: 1, + user_type: 'project_bot', + note: 1, + view_diffs_file_by_file: 1 + } + end + + it 'sets all allowed attributes' do + expect(User).to receive(:new).with(hash_including(params)).and_call_original + + service.execute end end @@ -195,34 +213,34 @@ RSpec.describe Users::BuildService do where(:user_default_external, :external, :email, :user_default_internal_regex, :result) do true | nil | 'fl@example.com' | nil | true true | true | 'fl@example.com' | nil | true - true | false | 'fl@example.com' | nil | true + true | false | 'fl@example.com' | nil | false # admin difference true | nil | 'fl@example.com' | '' | true true | true | 'fl@example.com' | '' | true - true | false | 'fl@example.com' | '' | true + true | false | 'fl@example.com' | '' | false # admin difference true | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false - true | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false + true | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | true # admin difference true | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false true | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true true | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true - true | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true + true | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false # admin difference false | nil | 'fl@example.com' | nil | false - false | true | 'fl@example.com' | nil | false + false | true | 'fl@example.com' | nil | true # admin difference false | false | 'fl@example.com' | nil | false false | nil | 'fl@example.com' | '' | false - false | true | 'fl@example.com' | '' | false + false | true | 'fl@example.com' | '' | true # admin difference false | false | 'fl@example.com' | '' | false false | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false - false | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false + false | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | true # admin difference false | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false false | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false - false | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false + false | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true # admin difference false | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false end @@ -234,8 +252,6 @@ RSpec.describe Users::BuildService do params.merge!({ external: external, email: email }.compact) end - subject(:user) { service.execute } - it 'sets the value of Gitlab::CurrentSettings.user_default_external' do expect(user.external).to eq(result) end diff --git a/spec/services/users/registrations_build_service_spec.rb b/spec/services/users/registrations_build_service_spec.rb new file mode 100644 index 00000000000..bc3718dbdb2 --- /dev/null +++ b/spec/services/users/registrations_build_service_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::RegistrationsBuildService do + describe '#execute' do + let(:base_params) { build_stubbed(:user).slice(:first_name, :last_name, :username, :email, :password) } + let(:skip_param) { {} } + let(:params) { base_params.merge(skip_param) } + + subject(:service) { described_class.new(nil, params) } + + before do + stub_application_setting(signup_enabled?: true) + end + + context 'when automatic user confirmation is not enabled' do + before do + stub_application_setting(send_user_confirmation_email: true) + end + + context 'when skip_confirmation is true' do + let(:skip_param) { { skip_confirmation: true } } + + it 'confirms the user' do + expect(service.execute).to be_confirmed + end + end + + context 'when skip_confirmation is not set' do + it 'does not confirm the user' do + expect(service.execute).not_to be_confirmed + end + end + + context 'when skip_confirmation is false' do + let(:skip_param) { { skip_confirmation: false } } + + it 'does not confirm the user' do + expect(service.execute).not_to be_confirmed + end + end + end + + context 'when automatic user confirmation is enabled' do + before do + stub_application_setting(send_user_confirmation_email: false) + end + + context 'when skip_confirmation is true' do + let(:skip_param) { { skip_confirmation: true } } + + it 'confirms the user' do + expect(service.execute).to be_confirmed + end + end + + context 'when skip_confirmation is not set the application setting takes precedence' do + it 'confirms the user' do + expect(service.execute).to be_confirmed + end + end + + context 'when skip_confirmation is false the application setting takes precedence' do + let(:skip_param) { { skip_confirmation: false } } + + it 'confirms the user' do + expect(service.execute).to be_confirmed + end + end + end + end +end diff --git a/spec/services/users/update_assigned_open_issue_count_service_spec.rb b/spec/services/users/update_assigned_open_issue_count_service_spec.rb new file mode 100644 index 00000000000..55fc60a7893 --- /dev/null +++ b/spec/services/users/update_assigned_open_issue_count_service_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::UpdateAssignedOpenIssueCountService do + let_it_be(:user) { create(:user) } + + describe '#initialize' do + context 'incorrect arguments provided' do + it 'raises an error if there are no target user' do + expect { described_class.new(target_user: nil) }.to raise_error(ArgumentError, /Please provide a target user/) + expect { described_class.new(target_user: "nonsense") }.to raise_error(ArgumentError, /Please provide a target user/) + end + end + + context 'when correct arguments provided' do + it 'is successful' do + expect { described_class.new(target_user: user) }.not_to raise_error + end + end + end + + describe "#execute", :clean_gitlab_redis_cache do + let(:fake_update_service) { double } + let(:fake_issue_count_service) { double } + let(:provided_value) { nil } + + subject { described_class.new(target_user: user).execute } + + context 'successful' do + it 'returns a success response' do + expect(subject).to be_success + end + + it 'writes the cache with the new value' do + expect(Rails.cache).to receive(:write).with(['users', user.id, 'assigned_open_issues_count'], 0, expires_in: User::COUNT_CACHE_VALIDITY_PERIOD) + + subject + end + + it 'calls the issues finder to get the latest value' do + expect(IssuesFinder).to receive(:new).with(user, assignee_id: user.id, state: 'opened', non_archived: true).and_return(fake_issue_count_service) + expect(fake_issue_count_service).to receive(:execute) + + subject + end + end + end +end diff --git a/spec/services/users/update_todo_count_cache_service_spec.rb b/spec/services/users/update_todo_count_cache_service_spec.rb index 3e3618b1291..3d96af928df 100644 --- a/spec/services/users/update_todo_count_cache_service_spec.rb +++ b/spec/services/users/update_todo_count_cache_service_spec.rb @@ -14,13 +14,21 @@ RSpec.describe Users::UpdateTodoCountCacheService do let_it_be(:todo5) { create(:todo, user: user2, state: :pending) } let_it_be(:todo6) { create(:todo, user: user2, state: :pending) } + def execute_all + described_class.new([user1.id, user2.id]).execute + end + + def execute_single + described_class.new([user1.id]).execute + end + it 'updates the todos_counts for users', :use_clean_rails_memory_store_caching do Rails.cache.write(['users', user1.id, 'todos_done_count'], 0) Rails.cache.write(['users', user1.id, 'todos_pending_count'], 0) Rails.cache.write(['users', user2.id, 'todos_done_count'], 0) Rails.cache.write(['users', user2.id, 'todos_pending_count'], 0) - expect { described_class.new([user1, user2]).execute } + expect { execute_all } .to change(user1, :todos_done_count).from(0).to(2) .and change(user1, :todos_pending_count).from(0).to(1) .and change(user2, :todos_done_count).from(0).to(1) @@ -28,7 +36,7 @@ RSpec.describe Users::UpdateTodoCountCacheService do Todo.delete_all - expect { described_class.new([user1, user2]).execute } + expect { execute_all } .to change(user1, :todos_done_count).from(2).to(0) .and change(user1, :todos_pending_count).from(1).to(0) .and change(user2, :todos_done_count).from(1).to(0) @@ -36,26 +44,24 @@ RSpec.describe Users::UpdateTodoCountCacheService do end it 'avoids N+1 queries' do - control_count = ActiveRecord::QueryRecorder.new { described_class.new([user1]).execute }.count + control_count = ActiveRecord::QueryRecorder.new { execute_single }.count - expect { described_class.new([user1, user2]).execute }.not_to exceed_query_limit(control_count) + expect { execute_all }.not_to exceed_query_limit(control_count) end it 'executes one query per batch of users' do stub_const("#{described_class}::QUERY_BATCH_SIZE", 1) - expect(ActiveRecord::QueryRecorder.new { described_class.new([user1]).execute }.count).to eq(1) - expect(ActiveRecord::QueryRecorder.new { described_class.new([user1, user2]).execute }.count).to eq(2) + expect(ActiveRecord::QueryRecorder.new { execute_single }.count).to eq(1) + expect(ActiveRecord::QueryRecorder.new { execute_all }.count).to eq(2) end - it 'sets the cache expire time to the users count_cache_validity_period' do - allow(user1).to receive(:count_cache_validity_period).and_return(1.minute) - allow(user2).to receive(:count_cache_validity_period).and_return(1.hour) - - expect(Rails.cache).to receive(:write).with(['users', user1.id, anything], anything, expires_in: 1.minute).twice - expect(Rails.cache).to receive(:write).with(['users', user2.id, anything], anything, expires_in: 1.hour).twice + it 'sets the correct cache expire time' do + expect(Rails.cache).to receive(:write) + .with(['users', user1.id, anything], anything, expires_in: User::COUNT_CACHE_VALIDITY_PERIOD) + .twice - described_class.new([user1, user2]).execute + execute_single end end end diff --git a/spec/services/users/upsert_credit_card_validation_service_spec.rb b/spec/services/users/upsert_credit_card_validation_service_spec.rb new file mode 100644 index 00000000000..148638fe5e7 --- /dev/null +++ b/spec/services/users/upsert_credit_card_validation_service_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::UpsertCreditCardValidationService do + let_it_be(:user) { create(:user) } + + let(:user_id) { user.id } + let(:credit_card_validated_time) { Time.utc(2020, 1, 1) } + let(:params) { { user_id: user_id, credit_card_validated_at: credit_card_validated_time } } + + describe '#execute' do + subject(:service) { described_class.new(params) } + + context 'successfully set credit card validation record for the user' do + context 'when user does not have credit card validation record' do + it 'creates the credit card validation and returns a success' do + expect(user.credit_card_validated_at).to be nil + + result = service.execute + + expect(result.status).to eq(:success) + expect(user.reload.credit_card_validated_at).to eq(credit_card_validated_time) + end + end + + context 'when user has credit card validation record' do + let(:old_time) { Time.utc(1999, 2, 2) } + + before do + create(:credit_card_validation, user: user, credit_card_validated_at: old_time) + end + + it 'updates the credit card validation and returns a success' do + expect(user.credit_card_validated_at).to eq(old_time) + + result = service.execute + + expect(result.status).to eq(:success) + expect(user.reload.credit_card_validated_at).to eq(credit_card_validated_time) + end + end + end + + shared_examples 'returns an error without tracking the exception' do + it do + expect(Gitlab::ErrorTracking).not_to receive(:track_exception) + + result = service.execute + + expect(result.status).to eq(:error) + end + end + + context 'when user id does not exist' do + let(:user_id) { non_existing_record_id } + + it_behaves_like 'returns an error without tracking the exception' + end + + context 'when missing credit_card_validated_at' do + let(:params) { { user_id: user_id } } + + it_behaves_like 'returns an error without tracking the exception' + end + + context 'when missing user id' do + let(:params) { { credit_card_validated_at: credit_card_validated_time } } + + it_behaves_like 'returns an error without tracking the exception' + end + + context 'when unexpected exception happen' do + it 'tracks the exception and returns an error' do + expect(::Users::CreditCardValidation).to receive(:upsert).and_raise(e = StandardError.new('My exception!')) + expect(Gitlab::ErrorTracking).to receive(:track_exception).with(e, class: described_class.to_s, params: params) + + result = service.execute + + expect(result.status).to eq(:error) + end + end + end +end diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb index 2fe72ab31c2..b3fd4e33640 100644 --- a/spec/services/web_hook_service_spec.rb +++ b/spec/services/web_hook_service_spec.rb @@ -5,8 +5,9 @@ require 'spec_helper' RSpec.describe WebHookService do include StubRequests - let(:project) { create(:project) } - let(:project_hook) { create(:project_hook) } + let_it_be(:project) { create(:project) } + let_it_be_with_reload(:project_hook) { create(:project_hook, project: project) } + let(:headers) do { 'Content-Type' => 'application/json', @@ -21,6 +22,10 @@ RSpec.describe WebHookService do let(:service_instance) { described_class.new(project_hook, data, :push_hooks) } + around do |example| + travel_to(Time.current) { example.run } + end + describe '#initialize' do before do stub_application_setting(setting_name => setting) @@ -56,12 +61,8 @@ RSpec.describe WebHookService do end describe '#execute' do - before do - project.hooks << [project_hook] - end - context 'when token is defined' do - let(:project_hook) { create(:project_hook, :token) } + let_it_be(:project_hook) { create(:project_hook, :token) } it 'POSTs to the webhook URL' do stub_full_request(project_hook.url, method: :post) @@ -85,8 +86,8 @@ RSpec.describe WebHookService do end context 'when auth credentials are present' do - let(:url) {'https://example.org'} - let(:project_hook) { create(:project_hook, url: 'https://demo:demo@example.org/') } + let_it_be(:url) {'https://example.org'} + let_it_be(:project_hook) { create(:project_hook, url: 'https://demo:demo@example.org/') } it 'uses the credentials' do stub_full_request(url, method: :post) @@ -100,8 +101,8 @@ RSpec.describe WebHookService do end context 'when auth credentials are partial present' do - let(:url) {'https://example.org'} - let(:project_hook) { create(:project_hook, url: 'https://demo@example.org/') } + let_it_be(:url) {'https://example.org'} + let_it_be(:project_hook) { create(:project_hook, url: 'https://demo@example.org/') } it 'uses the credentials anyways' do stub_full_request(url, method: :post) @@ -120,10 +121,21 @@ RSpec.describe WebHookService do expect { service_instance.execute }.to raise_error(StandardError) end + it 'does not execute disabled hooks' do + project_hook.update!(recent_failures: 4) + + expect(service_instance.execute).to eq({ status: :error, message: 'Hook disabled' }) + end + it 'handles exceptions' do - exceptions = [SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout, Gitlab::HTTP::BlockedUrlError, Gitlab::HTTP::RedirectionTooDeep] + exceptions = [ + SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, + Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout, + Gitlab::HTTP::BlockedUrlError, Gitlab::HTTP::RedirectionTooDeep + ] exceptions.each do |exception_class| exception = exception_class.new('Exception message') + project_hook.enable! stub_full_request(project_hook.url, method: :post).to_raise(exception) expect(service_instance.execute).to eq({ status: :error, message: exception.to_s }) @@ -132,7 +144,7 @@ RSpec.describe WebHookService do end context 'when url is not encoded' do - let(:project_hook) { create(:project_hook, url: 'http://server.com/my path/') } + let_it_be(:project_hook) { create(:project_hook, url: 'http://server.com/my path/') } it 'handles exceptions' do expect(service_instance.execute).to eq(status: :error, message: 'bad URI(is not URI?): "http://server.com/my path/"') @@ -166,10 +178,11 @@ RSpec.describe WebHookService do context 'with success' do before do stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: 'Success') - service_instance.execute end it 'log successful execution' do + service_instance.execute + expect(hook_log.trigger).to eq('push_hooks') expect(hook_log.url).to eq(project_hook.url) expect(hook_log.request_headers).to eq(headers) @@ -178,15 +191,81 @@ RSpec.describe WebHookService do expect(hook_log.execution_duration).to be > 0 expect(hook_log.internal_error_message).to be_nil end + + it 'does not increment the failure count' do + expect { service_instance.execute }.not_to change(project_hook, :recent_failures) + end + + it 'does not change the disabled_until attribute' do + expect { service_instance.execute }.not_to change(project_hook, :disabled_until) + end + + context 'when the hook had previously failed' do + before do + project_hook.update!(recent_failures: 2) + end + + it 'resets the failure count' do + expect { service_instance.execute }.to change(project_hook, :recent_failures).to(0) + end + end + end + + context 'with bad request' do + before do + stub_full_request(project_hook.url, method: :post).to_return(status: 400, body: 'Bad request') + end + + it 'logs failed execution' do + service_instance.execute + + expect(hook_log).to have_attributes( + trigger: eq('push_hooks'), + url: eq(project_hook.url), + request_headers: eq(headers), + response_body: eq('Bad request'), + response_status: eq('400'), + execution_duration: be > 0, + internal_error_message: be_nil + ) + end + + it 'increments the failure count' do + expect { service_instance.execute }.to change(project_hook, :recent_failures).by(1) + end + + it 'does not change the disabled_until attribute' do + expect { service_instance.execute }.not_to change(project_hook, :disabled_until) + end + + it 'does not allow the failure count to overflow' do + project_hook.update!(recent_failures: 32767) + + expect { service_instance.execute }.not_to change(project_hook, :recent_failures) + end + + context 'when the web_hooks_disable_failed FF is disabled' do + before do + # Hook will only be executed if the flag is disabled. + stub_feature_flags(web_hooks_disable_failed: false) + end + + it 'does not allow the failure count to overflow' do + project_hook.update!(recent_failures: 32767) + + expect { service_instance.execute }.not_to change(project_hook, :recent_failures) + end + end end context 'with exception' do before do stub_full_request(project_hook.url, method: :post).to_raise(SocketError.new('Some HTTP Post error')) - service_instance.execute end it 'log failed execution' do + service_instance.execute + expect(hook_log.trigger).to eq('push_hooks') expect(hook_log.url).to eq(project_hook.url) expect(hook_log.request_headers).to eq(headers) @@ -195,6 +274,47 @@ RSpec.describe WebHookService do expect(hook_log.execution_duration).to be > 0 expect(hook_log.internal_error_message).to eq('Some HTTP Post error') end + + it 'does not increment the failure count' do + expect { service_instance.execute }.not_to change(project_hook, :recent_failures) + end + + it 'sets the disabled_until attribute' do + expect { service_instance.execute } + .to change(project_hook, :disabled_until).to(project_hook.next_backoff.from_now) + end + + it 'increases the backoff count' do + expect { service_instance.execute }.to change(project_hook, :backoff_count).by(1) + end + + context 'when the previous cool-off was near the maximum' do + before do + project_hook.update!(disabled_until: 5.minutes.ago, backoff_count: 8) + end + + it 'sets the disabled_until attribute' do + expect { service_instance.execute }.to change(project_hook, :disabled_until).to(1.day.from_now) + end + + it 'sets the last_backoff attribute' do + expect { service_instance.execute }.to change(project_hook, :backoff_count).by(1) + end + end + + context 'when we have backed-off many many times' do + before do + project_hook.update!(disabled_until: 5.minutes.ago, backoff_count: 365) + end + + it 'sets the disabled_until attribute' do + expect { service_instance.execute }.to change(project_hook, :disabled_until).to(1.day.from_now) + end + + it 'sets the last_backoff attribute' do + expect { service_instance.execute }.to change(project_hook, :backoff_count).by(1) + end + end end context 'with unsafe response body' do @@ -217,12 +337,98 @@ RSpec.describe WebHookService do end describe '#async_execute' do - let(:system_hook) { create(:system_hook) } + def expect_to_perform_worker(hook) + expect(WebHookWorker).to receive(:perform_async).with(hook.id, data, 'push_hooks') + end + + def expect_to_rate_limit(hook, threshold:, throttled: false) + expect(Gitlab::ApplicationRateLimiter).to receive(:throttled?) + .with(:web_hook_calls, scope: [hook], threshold: threshold) + .and_return(throttled) + end + + context 'when rate limiting is not configured' do + it 'queues a worker without tracking the call' do + expect(Gitlab::ApplicationRateLimiter).not_to receive(:throttled?) + expect_to_perform_worker(project_hook) + + service_instance.async_execute + end + end + + context 'when rate limiting is configured' do + let_it_be(:threshold) { 3 } + let_it_be(:plan_limits) { create(:plan_limits, :default_plan, web_hook_calls: threshold) } + + it 'queues a worker and tracks the call' do + expect_to_rate_limit(project_hook, threshold: threshold) + expect_to_perform_worker(project_hook) + + service_instance.async_execute + end + + context 'when the hook is throttled (via mock)' do + before do + expect_to_rate_limit(project_hook, threshold: threshold, throttled: true) + end + + it 'does not queue a worker and logs an error' do + expect(WebHookWorker).not_to receive(:perform_async) - it 'enqueue WebHookWorker' do - expect(WebHookWorker).to receive(:perform_async).with(project_hook.id, data, 'push_hooks') + payload = { + message: 'Webhook rate limit exceeded', + hook_id: project_hook.id, + hook_type: 'ProjectHook', + hook_name: 'push_hooks' + } - described_class.new(project_hook, data, 'push_hooks').async_execute + expect(Gitlab::AuthLogger).to receive(:error).with(payload) + expect(Gitlab::AppLogger).to receive(:error).with(payload) + + service_instance.async_execute + end + end + + context 'when the hook is throttled (via Redis)', :clean_gitlab_redis_cache do + before do + # Set a high interval to avoid intermittent failures in CI + allow(Gitlab::ApplicationRateLimiter).to receive(:rate_limits).and_return( + web_hook_calls: { interval: 1.day } + ) + + expect_to_perform_worker(project_hook).exactly(threshold).times + + threshold.times { service_instance.async_execute } + end + + it 'stops queueing workers and logs errors' do + expect(Gitlab::AuthLogger).to receive(:error).twice + expect(Gitlab::AppLogger).to receive(:error).twice + + 2.times { service_instance.async_execute } + end + + it 'still queues workers for other hooks' do + other_hook = create(:project_hook) + + expect_to_perform_worker(other_hook) + + described_class.new(other_hook, data, :push_hooks).async_execute + end + end + + context 'when the feature flag is disabled' do + before do + stub_feature_flags(web_hooks_rate_limit: false) + end + + it 'queues a worker without tracking the call' do + expect(Gitlab::ApplicationRateLimiter).not_to receive(:throttled?) + expect_to_perform_worker(project_hook) + + service_instance.async_execute + end + end end end end diff --git a/spec/services/web_hooks/destroy_service_spec.rb b/spec/services/web_hooks/destroy_service_spec.rb index fda40eb01e2..5269fe08ac0 100644 --- a/spec/services/web_hooks/destroy_service_spec.rb +++ b/spec/services/web_hooks/destroy_service_spec.rb @@ -41,15 +41,15 @@ RSpec.describe WebHooks::DestroyService do end context 'with system hook' do - let_it_be(:hook) { create(:system_hook, url: "http://example.com") } - let_it_be(:log) { create_list(:web_hook_log, 3, web_hook: hook) } + let!(:hook) { create(:system_hook, url: "http://example.com") } + let!(:log) { create_list(:web_hook_log, 3, web_hook: hook) } it_behaves_like 'batched destroys' end context 'with project hook' do - let_it_be(:hook) { create(:project_hook) } - let_it_be(:log) { create_list(:web_hook_log, 3, web_hook: hook) } + let!(:hook) { create(:project_hook) } + let!(:log) { create_list(:web_hook_log, 3, web_hook: hook) } it_behaves_like 'batched destroys' end |