summaryrefslogtreecommitdiff
path: root/spec/models
diff options
context:
space:
mode:
Diffstat (limited to 'spec/models')
-rw-r--r--spec/models/analytics/cycle_analytics/project_stage_spec.rb1
-rw-r--r--spec/models/analytics/cycle_analytics/project_value_stream_spec.rb39
-rw-r--r--spec/models/appearance_spec.rb4
-rw-r--r--spec/models/application_record_spec.rb14
-rw-r--r--spec/models/application_setting_spec.rb41
-rw-r--r--spec/models/board_group_recent_visit_spec.rb60
-rw-r--r--spec/models/board_project_recent_visit_spec.rb60
-rw-r--r--spec/models/board_spec.rb42
-rw-r--r--spec/models/broadcast_message_spec.rb6
-rw-r--r--spec/models/bulk_imports/entity_spec.rb9
-rw-r--r--spec/models/bulk_imports/export_spec.rb85
-rw-r--r--spec/models/bulk_imports/export_upload_spec.rb23
-rw-r--r--spec/models/bulk_imports/file_transfer/group_config_spec.rb38
-rw-r--r--spec/models/bulk_imports/file_transfer/project_config_spec.rb38
-rw-r--r--spec/models/bulk_imports/file_transfer_spec.rb25
-rw-r--r--spec/models/bulk_imports/stage_spec.rb50
-rw-r--r--spec/models/chat_name_spec.rb6
-rw-r--r--spec/models/ci/build_dependencies_spec.rb22
-rw-r--r--spec/models/ci/build_spec.rb82
-rw-r--r--spec/models/ci/commit_with_pipeline_spec.rb40
-rw-r--r--spec/models/ci/job_artifact_spec.rb28
-rw-r--r--spec/models/ci/pipeline_artifact_spec.rb24
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb30
-rw-r--r--spec/models/ci/pipeline_spec.rb123
-rw-r--r--spec/models/ci/runner_namespace_spec.rb9
-rw-r--r--spec/models/ci/runner_project_spec.rb9
-rw-r--r--spec/models/ci/stage_spec.rb12
-rw-r--r--spec/models/clusters/agent_spec.rb1
-rw-r--r--spec/models/clusters/agent_token_spec.rb13
-rw-r--r--spec/models/clusters/applications/elastic_stack_spec.rb110
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb87
-rw-r--r--spec/models/clusters/integrations/elastic_stack_spec.rb19
-rw-r--r--spec/models/clusters/integrations/prometheus_spec.rb56
-rw-r--r--spec/models/commit_status_spec.rb34
-rw-r--r--spec/models/concerns/bulk_insert_safe_spec.rb36
-rw-r--r--spec/models/concerns/cascading_namespace_setting_attribute_spec.rb36
-rw-r--r--spec/models/concerns/chronic_duration_attribute_spec.rb3
-rw-r--r--spec/models/concerns/ci/maskable_spec.rb2
-rw-r--r--spec/models/concerns/cron_schedulable_spec.rb17
-rw-r--r--spec/models/concerns/has_integrations_spec.rb33
-rw-r--r--spec/models/concerns/has_timelogs_report_spec.rb22
-rw-r--r--spec/models/concerns/noteable_spec.rb2
-rw-r--r--spec/models/concerns/routable_spec.rb74
-rw-r--r--spec/models/concerns/sidebars/container_with_html_options_spec.rb21
-rw-r--r--spec/models/concerns/sidebars/positionable_list_spec.rb59
-rw-r--r--spec/models/container_repository_spec.rb95
-rw-r--r--spec/models/context_commits_diff_spec.rb59
-rw-r--r--spec/models/custom_emoji_spec.rb2
-rw-r--r--spec/models/deployment_spec.rb81
-rw-r--r--spec/models/design_management/design_spec.rb2
-rw-r--r--spec/models/email_spec.rb11
-rw-r--r--spec/models/external_pull_request_spec.rb17
-rw-r--r--spec/models/group_spec.rb502
-rw-r--r--spec/models/hooks/project_hook_spec.rb9
-rw-r--r--spec/models/hooks/service_hook_spec.rb12
-rw-r--r--spec/models/hooks/system_hook_spec.rb8
-rw-r--r--spec/models/hooks/web_hook_log_archived_spec.rb52
-rw-r--r--spec/models/hooks/web_hook_spec.rb198
-rw-r--r--spec/models/instance_metadata/kas_spec.rb33
-rw-r--r--spec/models/instance_metadata_spec.rb3
-rw-r--r--spec/models/integration_spec.rb952
-rw-r--r--spec/models/integrations/asana_spec.rb (renamed from spec/models/project_services/asana_service_spec.rb)18
-rw-r--r--spec/models/integrations/assembla_spec.rb (renamed from spec/models/project_services/assembla_service_spec.rb)2
-rw-r--r--spec/models/integrations/bamboo_spec.rb (renamed from spec/models/project_services/bamboo_service_spec.rb)2
-rw-r--r--spec/models/integrations/campfire_spec.rb (renamed from spec/models/project_services/campfire_service_spec.rb)2
-rw-r--r--spec/models/integrations/chat_message/alert_message_spec.rb (renamed from spec/models/project_services/chat_message/alert_message_spec.rb)2
-rw-r--r--spec/models/integrations/chat_message/base_message_spec.rb (renamed from spec/models/project_services/chat_message/base_message_spec.rb)2
-rw-r--r--spec/models/integrations/chat_message/deployment_message_spec.rb (renamed from spec/models/project_services/chat_message/deployment_message_spec.rb)6
-rw-r--r--spec/models/integrations/chat_message/issue_message_spec.rb (renamed from spec/models/project_services/chat_message/issue_message_spec.rb)2
-rw-r--r--spec/models/integrations/chat_message/merge_message_spec.rb (renamed from spec/models/project_services/chat_message/merge_message_spec.rb)2
-rw-r--r--spec/models/integrations/chat_message/note_message_spec.rb (renamed from spec/models/project_services/chat_message/note_message_spec.rb)2
-rw-r--r--spec/models/integrations/chat_message/pipeline_message_spec.rb (renamed from spec/models/project_services/chat_message/pipeline_message_spec.rb)2
-rw-r--r--spec/models/integrations/chat_message/push_message_spec.rb (renamed from spec/models/project_services/chat_message/push_message_spec.rb)2
-rw-r--r--spec/models/integrations/chat_message/wiki_page_message_spec.rb (renamed from spec/models/project_services/chat_message/wiki_page_message_spec.rb)2
-rw-r--r--spec/models/integrations/confluence_spec.rb (renamed from spec/models/project_services/confluence_service_spec.rb)2
-rw-r--r--spec/models/integrations/datadog_spec.rb (renamed from spec/models/project_services/datadog_service_spec.rb)2
-rw-r--r--spec/models/integrations/emails_on_push_spec.rb (renamed from spec/models/project_services/emails_on_push_service_spec.rb)2
-rw-r--r--spec/models/internal_id_spec.rb9
-rw-r--r--spec/models/issue/metrics_spec.rb15
-rw-r--r--spec/models/issue_spec.rb52
-rw-r--r--spec/models/label_link_spec.rb24
-rw-r--r--spec/models/member_spec.rb64
-rw-r--r--spec/models/members/group_member_spec.rb4
-rw-r--r--spec/models/members/project_member_spec.rb4
-rw-r--r--spec/models/merge_request_diff_spec.rb94
-rw-r--r--spec/models/merge_request_spec.rb20
-rw-r--r--spec/models/milestone_spec.rb16
-rw-r--r--spec/models/namespace/package_setting_spec.rb25
-rw-r--r--spec/models/namespace/traversal_hierarchy_spec.rb30
-rw-r--r--spec/models/namespace_spec.rb111
-rw-r--r--spec/models/note_spec.rb10
-rw-r--r--spec/models/packages/dependency_spec.rb5
-rw-r--r--spec/models/packages/go/module_version_spec.rb11
-rw-r--r--spec/models/packages/helm/file_metadatum_spec.rb60
-rw-r--r--spec/models/packages/package_file_spec.rb47
-rw-r--r--spec/models/packages/package_spec.rb116
-rw-r--r--spec/models/packages/tag_spec.rb1
-rw-r--r--spec/models/pages/lookup_path_spec.rb25
-rw-r--r--spec/models/plan_limits_spec.rb1
-rw-r--r--spec/models/project_auto_devops_spec.rb16
-rw-r--r--spec/models/project_feature_spec.rb2
-rw-r--r--spec/models/project_services/chat_notification_service_spec.rb150
-rw-r--r--spec/models/project_services/data_fields_spec.rb4
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb94
-rw-r--r--spec/models/project_services/issue_tracker_data_spec.rb6
-rw-r--r--spec/models/project_services/jira_service_spec.rb4
-rw-r--r--spec/models/project_services/jira_tracker_data_spec.rb2
-rw-r--r--spec/models/project_services/mattermost_slash_commands_service_spec.rb2
-rw-r--r--spec/models/project_services/microsoft_teams_service_spec.rb6
-rw-r--r--spec/models/project_services/open_project_tracker_data_spec.rb4
-rw-r--r--spec/models/project_services/slack_service_spec.rb10
-rw-r--r--spec/models/project_spec.rb230
-rw-r--r--spec/models/project_team_spec.rb8
-rw-r--r--spec/models/release_highlight_spec.rb50
-rw-r--r--spec/models/release_spec.rb17
-rw-r--r--spec/models/releases/evidence_spec.rb1
-rw-r--r--spec/models/releases/source_spec.rb1
-rw-r--r--spec/models/repository_spec.rb61
-rw-r--r--spec/models/service_spec.rb887
-rw-r--r--spec/models/sidebars/menu_spec.rb67
-rw-r--r--spec/models/sidebars/panel_spec.rb34
-rw-r--r--spec/models/sidebars/projects/context_spec.rb13
-rw-r--r--spec/models/sidebars/projects/menus/learn_gitlab/menu_spec.rb31
-rw-r--r--spec/models/sidebars/projects/menus/project_overview/menu_items/releases_spec.rb38
-rw-r--r--spec/models/sidebars/projects/menus/project_overview/menu_spec.rb18
-rw-r--r--spec/models/sidebars/projects/menus/repository/menu_spec.rb38
-rw-r--r--spec/models/sidebars/projects/panel_spec.rb14
-rw-r--r--spec/models/snippet_spec.rb12
-rw-r--r--spec/models/timelog_spec.rb46
-rw-r--r--spec/models/todo_spec.rb12
-rw-r--r--spec/models/user_preference_spec.rb2
-rw-r--r--spec/models/user_spec.rb221
-rw-r--r--spec/models/user_status_spec.rb2
-rw-r--r--spec/models/users/credit_card_validation_spec.rb7
-rw-r--r--spec/models/wiki_page/meta_spec.rb6
-rw-r--r--spec/models/wiki_page_spec.rb18
136 files changed, 4108 insertions, 2403 deletions
diff --git a/spec/models/analytics/cycle_analytics/project_stage_spec.rb b/spec/models/analytics/cycle_analytics/project_stage_spec.rb
index fce31af619c..9efe90e7d41 100644
--- a/spec/models/analytics/cycle_analytics/project_stage_spec.rb
+++ b/spec/models/analytics/cycle_analytics/project_stage_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe Analytics::CycleAnalytics::ProjectStage do
end
it_behaves_like 'value stream analytics stage' do
+ let(:factory) { :cycle_analytics_project_stage }
let(:parent) { build(:project) }
let(:parent_name) { :project }
end
diff --git a/spec/models/analytics/cycle_analytics/project_value_stream_spec.rb b/spec/models/analytics/cycle_analytics/project_value_stream_spec.rb
new file mode 100644
index 00000000000..d84ecedc634
--- /dev/null
+++ b/spec/models/analytics/cycle_analytics/project_value_stream_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Analytics::CycleAnalytics::ProjectValueStream, type: :model do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_many(:stages) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_length_of(:name).is_at_most(100) }
+
+ it 'validates uniqueness of name' do
+ project = create(:project)
+ create(:cycle_analytics_project_value_stream, name: 'test', project: project)
+
+ value_stream = build(:cycle_analytics_project_value_stream, name: 'test', project: project)
+
+ expect(value_stream).to be_invalid
+ expect(value_stream.errors.messages).to eq(name: [I18n.t('errors.messages.taken')])
+ end
+ end
+
+ it 'is not custom' do
+ expect(described_class.new).not_to be_custom
+ end
+
+ describe '.build_default_value_stream' do
+ it 'builds the default value stream' do
+ project = build(:project)
+
+ value_stream = described_class.build_default_value_stream(project)
+ expect(value_stream.name).to eq('default')
+ end
+ end
+end
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index 37eddf9a22a..2817e177d28 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Appearance do
create(:appearance)
new_row = build(:appearance)
- new_row.save
+ expect { new_row.save! }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Only 1 appearances row can exist')
expect(new_row.valid?).to eq(false)
end
@@ -39,7 +39,7 @@ RSpec.describe Appearance do
end
it 'returns the path when the upload has been orphaned' do
- appearance.send(logo_type).upload.destroy
+ appearance.send(logo_type).upload.destroy!
appearance.reload
expect(appearance.send("#{logo_type}_path")).to eq(expected_path)
diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb
index 7e6ac351e68..24de46cb536 100644
--- a/spec/models/application_record_spec.rb
+++ b/spec/models/application_record_spec.rb
@@ -13,20 +13,24 @@ RSpec.describe ApplicationRecord do
describe '.safe_ensure_unique' do
let(:model) { build(:suggestion) }
+ let_it_be(:note) { create(:diff_note_on_merge_request) }
+
let(:klass) { model.class }
before do
- allow(model).to receive(:save).and_raise(ActiveRecord::RecordNotUnique)
+ allow(model).to receive(:save!).and_raise(ActiveRecord::RecordNotUnique)
end
it 'returns false when ActiveRecord::RecordNotUnique is raised' do
- expect(model).to receive(:save).once
- expect(klass.safe_ensure_unique { model.save }).to be_falsey
+ expect(model).to receive(:save!).once
+ model.note_id = note.id
+ expect(klass.safe_ensure_unique { model.save! }).to be_falsey
end
it 'retries based on retry count specified' do
- expect(model).to receive(:save).exactly(3).times
- expect(klass.safe_ensure_unique(retries: 2) { model.save }).to be_falsey
+ expect(model).to receive(:save!).exactly(3).times
+ model.note_id = note.id
+ expect(klass.safe_ensure_unique(retries: 2) { model.save! }).to be_falsey
end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 808932ce7e4..4b4e7820f7a 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -129,6 +129,11 @@ RSpec.describe ApplicationSetting do
it { is_expected.not_to allow_value(nil).for(:notes_create_limit_allowlist) }
it { is_expected.to allow_value([]).for(:notes_create_limit_allowlist) }
+ it { is_expected.to allow_value('all_tiers').for(:whats_new_variant) }
+ it { is_expected.to allow_value('current_tier').for(:whats_new_variant) }
+ it { is_expected.to allow_value('disabled').for(:whats_new_variant) }
+ it { is_expected.not_to allow_value(nil).for(:whats_new_variant) }
+
context 'help_page_documentation_base_url validations' do
it { is_expected.to allow_value(nil).for(:help_page_documentation_base_url) }
it { is_expected.to allow_value('https://docs.gitlab.com').for(:help_page_documentation_base_url) }
@@ -211,7 +216,8 @@ RSpec.describe ApplicationSetting do
setting.spam_check_endpoint_enabled = true
end
- it { is_expected.to allow_value('https://example.org/spam_check').for(:spam_check_endpoint_url) }
+ it { is_expected.to allow_value('grpc://example.org/spam_check').for(:spam_check_endpoint_url) }
+ it { is_expected.not_to allow_value('https://example.org/spam_check').for(:spam_check_endpoint_url) }
it { is_expected.not_to allow_value('nonsense').for(:spam_check_endpoint_url) }
it { is_expected.not_to allow_value(nil).for(:spam_check_endpoint_url) }
it { is_expected.not_to allow_value('').for(:spam_check_endpoint_url) }
@@ -222,7 +228,8 @@ RSpec.describe ApplicationSetting do
setting.spam_check_endpoint_enabled = false
end
- it { is_expected.to allow_value('https://example.org/spam_check').for(:spam_check_endpoint_url) }
+ it { is_expected.to allow_value('grpc://example.org/spam_check').for(:spam_check_endpoint_url) }
+ it { is_expected.not_to allow_value('https://example.org/spam_check').for(:spam_check_endpoint_url) }
it { is_expected.not_to allow_value('nonsense').for(:spam_check_endpoint_url) }
it { is_expected.to allow_value(nil).for(:spam_check_endpoint_url) }
it { is_expected.to allow_value('').for(:spam_check_endpoint_url) }
@@ -245,7 +252,9 @@ RSpec.describe ApplicationSetting do
context "when user accepted let's encrypt terms of service" do
before do
- setting.update(lets_encrypt_terms_of_service_accepted: true)
+ expect do
+ setting.update!(lets_encrypt_terms_of_service_accepted: true)
+ end.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Lets encrypt notification email can't be blank")
end
it { is_expected.not_to allow_value(nil).for(:lets_encrypt_notification_email) }
@@ -295,26 +304,30 @@ RSpec.describe ApplicationSetting do
describe 'default_artifacts_expire_in' do
it 'sets an error if it cannot parse' do
- setting.update(default_artifacts_expire_in: 'a')
+ expect do
+ setting.update!(default_artifacts_expire_in: 'a')
+ end.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Default artifacts expire in is not a correct duration")
expect_invalid
end
it 'sets an error if it is blank' do
- setting.update(default_artifacts_expire_in: ' ')
+ expect do
+ setting.update!(default_artifacts_expire_in: ' ')
+ end.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Default artifacts expire in can't be blank")
expect_invalid
end
it 'sets the value if it is valid' do
- setting.update(default_artifacts_expire_in: '30 days')
+ setting.update!(default_artifacts_expire_in: '30 days')
expect(setting).to be_valid
expect(setting.default_artifacts_expire_in).to eq('30 days')
end
it 'sets the value if it is 0' do
- setting.update(default_artifacts_expire_in: '0')
+ setting.update!(default_artifacts_expire_in: '0')
expect(setting).to be_valid
expect(setting.default_artifacts_expire_in).to eq('0')
@@ -393,18 +406,18 @@ RSpec.describe ApplicationSetting do
context 'auto_devops_domain setting' do
context 'when auto_devops_enabled? is true' do
before do
- setting.update(auto_devops_enabled: true)
+ setting.update!(auto_devops_enabled: true)
end
it 'can be blank' do
- setting.update(auto_devops_domain: '')
+ setting.update!(auto_devops_domain: '')
expect(setting).to be_valid
end
context 'with a valid value' do
before do
- setting.update(auto_devops_domain: 'domain.com')
+ setting.update!(auto_devops_domain: 'domain.com')
end
it 'is valid' do
@@ -414,7 +427,9 @@ RSpec.describe ApplicationSetting do
context 'with an invalid value' do
before do
- setting.update(auto_devops_domain: 'definitelynotahostname')
+ expect do
+ setting.update!(auto_devops_domain: 'definitelynotahostname')
+ end.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Auto devops domain is not a fully qualified domain name")
end
it 'is invalid' do
@@ -785,6 +800,10 @@ RSpec.describe ApplicationSetting do
throttle_authenticated_api_period_in_seconds
throttle_authenticated_web_requests_per_period
throttle_authenticated_web_period_in_seconds
+ throttle_unauthenticated_packages_api_requests_per_period
+ throttle_unauthenticated_packages_api_period_in_seconds
+ throttle_authenticated_packages_api_requests_per_period
+ throttle_authenticated_packages_api_period_in_seconds
]
end
diff --git a/spec/models/board_group_recent_visit_spec.rb b/spec/models/board_group_recent_visit_spec.rb
index c6fbd263072..d2d287d8e24 100644
--- a/spec/models/board_group_recent_visit_spec.rb
+++ b/spec/models/board_group_recent_visit_spec.rb
@@ -3,9 +3,8 @@
require 'spec_helper'
RSpec.describe BoardGroupRecentVisit do
- let(:user) { create(:user) }
- let(:group) { create(:group) }
- let(:board) { create(:board, group: group) }
+ let_it_be(:board_parent) { create(:group) }
+ let_it_be(:board) { create(:board, group: board_parent) }
describe 'relationships' do
it { is_expected.to belong_to(:user) }
@@ -19,56 +18,9 @@ RSpec.describe BoardGroupRecentVisit do
it { is_expected.to validate_presence_of(:board) }
end
- describe '#visited' do
- it 'creates a visit if one does not exists' do
- expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1)
- end
-
- shared_examples 'was visited previously' do
- let!(:visit) { create :board_group_recent_visit, group: board.group, board: board, user: user, updated_at: 7.days.ago }
-
- it 'updates the timestamp' do
- freeze_time do
- described_class.visited!(user, board)
-
- expect(described_class.count).to eq 1
- expect(described_class.first.updated_at).to be_like_time(Time.zone.now)
- end
- end
- end
-
- it_behaves_like 'was visited previously'
-
- context 'when we try to create a visit that is not unique' do
- before do
- expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique')
- expect(described_class).to receive(:find_or_create_by).and_return(visit)
- end
-
- it_behaves_like 'was visited previously'
- end
- end
-
- describe '#latest' do
- def create_visit(time)
- create :board_group_recent_visit, group: group, user: user, updated_at: time
- end
-
- it 'returns the most recent visited' do
- create_visit(7.days.ago)
- create_visit(5.days.ago)
- recent = create_visit(1.day.ago)
-
- expect(described_class.latest(user, group)).to eq recent
- end
-
- it 'returns last 3 visited boards' do
- create_visit(7.days.ago)
- visit1 = create_visit(3.days.ago)
- visit2 = create_visit(2.days.ago)
- visit3 = create_visit(5.days.ago)
-
- expect(described_class.latest(user, group, count: 3)).to eq([visit2, visit1, visit3])
- end
+ it_behaves_like 'boards recent visit' do
+ let_it_be(:board_relation) { :board }
+ let_it_be(:board_parent_relation) { :group }
+ let_it_be(:visit_relation) { :board_group_recent_visit }
end
end
diff --git a/spec/models/board_project_recent_visit_spec.rb b/spec/models/board_project_recent_visit_spec.rb
index 145a4f5b1a7..262c3a8faaa 100644
--- a/spec/models/board_project_recent_visit_spec.rb
+++ b/spec/models/board_project_recent_visit_spec.rb
@@ -3,9 +3,8 @@
require 'spec_helper'
RSpec.describe BoardProjectRecentVisit do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:board) { create(:board, project: project) }
+ let_it_be(:board_parent) { create(:project) }
+ let_it_be(:board) { create(:board, project: board_parent) }
describe 'relationships' do
it { is_expected.to belong_to(:user) }
@@ -19,56 +18,9 @@ RSpec.describe BoardProjectRecentVisit do
it { is_expected.to validate_presence_of(:board) }
end
- describe '#visited' do
- it 'creates a visit if one does not exists' do
- expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1)
- end
-
- shared_examples 'was visited previously' do
- let!(:visit) { create :board_project_recent_visit, project: board.project, board: board, user: user, updated_at: 7.days.ago }
-
- it 'updates the timestamp' do
- freeze_time do
- described_class.visited!(user, board)
-
- expect(described_class.count).to eq 1
- expect(described_class.first.updated_at).to be_like_time(Time.zone.now)
- end
- end
- end
-
- it_behaves_like 'was visited previously'
-
- context 'when we try to create a visit that is not unique' do
- before do
- expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique')
- expect(described_class).to receive(:find_or_create_by).and_return(visit)
- end
-
- it_behaves_like 'was visited previously'
- end
- end
-
- describe '#latest' do
- def create_visit(time)
- create :board_project_recent_visit, project: project, user: user, updated_at: time
- end
-
- it 'returns the most recent visited' do
- create_visit(7.days.ago)
- create_visit(5.days.ago)
- recent = create_visit(1.day.ago)
-
- expect(described_class.latest(user, project)).to eq recent
- end
-
- it 'returns last 3 visited boards' do
- create_visit(7.days.ago)
- visit1 = create_visit(3.days.ago)
- visit2 = create_visit(2.days.ago)
- visit3 = create_visit(5.days.ago)
-
- expect(described_class.latest(user, project, count: 3)).to eq([visit2, visit1, visit3])
- end
+ it_behaves_like 'boards recent visit' do
+ let_it_be(:board_relation) { :board }
+ let_it_be(:board_parent_relation) { :project }
+ let_it_be(:visit_relation) { :board_project_recent_visit }
end
end
diff --git a/spec/models/board_spec.rb b/spec/models/board_spec.rb
index c8a9504d4fc..0b7c21fd0c3 100644
--- a/spec/models/board_spec.rb
+++ b/spec/models/board_spec.rb
@@ -42,4 +42,46 @@ RSpec.describe Board do
expect { project.boards.first_board.find(board_A.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
+
+ describe '#disabled_for?' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:user) { create(:user) }
+
+ subject { board.disabled_for?(user) }
+
+ shared_examples 'board disabled_for?' do
+ context 'when current user cannot create non backlog issues' do
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when user can create backlog issues' do
+ before do
+ board.resource_parent.add_reporter(user)
+ end
+
+ it { is_expected.to eq(false) }
+
+ context 'when block_issue_repositioning is enabled' do
+ before do
+ stub_feature_flags(block_issue_repositioning: group)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+ end
+ end
+
+ context 'for group board' do
+ let_it_be(:board) { create(:board, group: group) }
+
+ it_behaves_like 'board disabled_for?'
+ end
+
+ context 'for project board' do
+ let_it_be(:board) { create(:board, project: project) }
+
+ it_behaves_like 'board disabled_for?'
+ end
+ end
end
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index c4d17905637..d981189c6f1 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -120,6 +120,12 @@ RSpec.describe BroadcastMessage do
expect(subject.call('/users/name/issues').length).to eq(1)
end
+ it 'returns message if provided a path without a preceding slash' do
+ create(:broadcast_message, target_path: "/users/*/issues", broadcast_type: broadcast_type)
+
+ expect(subject.call('users/name/issues').length).to eq(1)
+ end
+
it 'returns the message for empty target path' do
create(:broadcast_message, target_path: "", broadcast_type: broadcast_type)
diff --git a/spec/models/bulk_imports/entity_spec.rb b/spec/models/bulk_imports/entity_spec.rb
index 652ea431696..d1b7125a6e6 100644
--- a/spec/models/bulk_imports/entity_spec.rb
+++ b/spec/models/bulk_imports/entity_spec.rb
@@ -125,4 +125,13 @@ RSpec.describe BulkImports::Entity, type: :model do
end
end
end
+
+ describe '#encoded_source_full_path' do
+ it 'encodes entity source full path' do
+ expected = 'foo%2Fbar'
+ entity = build(:bulk_import_entity, source_full_path: 'foo/bar')
+
+ expect(entity.encoded_source_full_path).to eq(expected)
+ end
+ end
end
diff --git a/spec/models/bulk_imports/export_spec.rb b/spec/models/bulk_imports/export_spec.rb
new file mode 100644
index 00000000000..d85b77d599b
--- /dev/null
+++ b/spec/models/bulk_imports/export_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Export, type: :model do
+ describe 'associations' do
+ it { is_expected.to belong_to(:group) }
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_one(:upload) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:relation) }
+ it { is_expected.to validate_presence_of(:status) }
+
+ context 'when not associated with a group or project' do
+ it 'is invalid' do
+ export = build(:bulk_import_export, group: nil, project: nil)
+
+ expect(export).not_to be_valid
+ end
+ end
+
+ context 'when associated with a group' do
+ it 'is valid' do
+ export = build(:bulk_import_export, group: build(:group), project: nil)
+
+ expect(export).to be_valid
+ end
+ end
+
+ context 'when associated with a project' do
+ it 'is valid' do
+ export = build(:bulk_import_export, group: nil, project: build(:project))
+
+ expect(export).to be_valid
+ end
+ end
+
+ context 'when relation is invalid' do
+ it 'is invalid' do
+ export = build(:bulk_import_export, relation: 'unsupported')
+
+ expect(export).not_to be_valid
+ expect(export.errors).to include(:relation)
+ end
+ end
+ end
+
+ describe '#portable' do
+ context 'when associated with project' do
+ it 'returns project' do
+ export = create(:bulk_import_export, project: create(:project), group: nil)
+
+ expect(export.portable).to be_instance_of(Project)
+ end
+ end
+
+ context 'when associated with group' do
+ it 'returns group' do
+ export = create(:bulk_import_export)
+
+ expect(export.portable).to be_instance_of(Group)
+ end
+ end
+ end
+
+ describe '#config' do
+ context 'when associated with project' do
+ it 'returns project config' do
+ export = create(:bulk_import_export, project: create(:project), group: nil)
+
+ expect(export.config).to be_instance_of(BulkImports::FileTransfer::ProjectConfig)
+ end
+ end
+
+ context 'when associated with group' do
+ it 'returns group config' do
+ export = create(:bulk_import_export)
+
+ expect(export.config).to be_instance_of(BulkImports::FileTransfer::GroupConfig)
+ end
+ end
+ end
+end
diff --git a/spec/models/bulk_imports/export_upload_spec.rb b/spec/models/bulk_imports/export_upload_spec.rb
new file mode 100644
index 00000000000..641fa4a1b6c
--- /dev/null
+++ b/spec/models/bulk_imports/export_upload_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::ExportUpload do
+ subject { described_class.new(export: create(:bulk_import_export)) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:export) }
+ end
+
+ it 'stores export file' do
+ method = 'export_file'
+ filename = 'labels.ndjson.gz'
+
+ subject.public_send("#{method}=", fixture_file_upload("spec/fixtures/bulk_imports/#{filename}"))
+ subject.save!
+
+ url = "/uploads/-/system/bulk_imports/export_upload/export_file/#{subject.id}/#{filename}"
+
+ expect(subject.public_send(method).url).to eq(url)
+ end
+end
diff --git a/spec/models/bulk_imports/file_transfer/group_config_spec.rb b/spec/models/bulk_imports/file_transfer/group_config_spec.rb
new file mode 100644
index 00000000000..21da71de3c7
--- /dev/null
+++ b/spec/models/bulk_imports/file_transfer/group_config_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::FileTransfer::GroupConfig do
+ let_it_be(:exportable) { create(:group) }
+ let_it_be(:hex) { '123' }
+
+ before do
+ allow(SecureRandom).to receive(:hex).and_return(hex)
+ end
+
+ subject { described_class.new(exportable) }
+
+ describe '#exportable_tree' do
+ it 'returns exportable tree' do
+ expect_next_instance_of(::Gitlab::ImportExport::AttributesFinder) do |finder|
+ expect(finder).to receive(:find_root).with(:group).and_call_original
+ end
+
+ expect(subject.portable_tree).not_to be_empty
+ end
+ end
+
+ describe '#export_path' do
+ it 'returns correct export path' do
+ expect(::Gitlab::ImportExport).to receive(:storage_path).and_return('storage_path')
+
+ expect(subject.export_path).to eq("storage_path/#{exportable.full_path}/#{hex}")
+ end
+ end
+
+ describe '#exportable_relations' do
+ it 'returns a list of top level exportable relations' do
+ expect(subject.portable_relations).to include('milestones', 'badges', 'boards', 'labels')
+ end
+ end
+end
diff --git a/spec/models/bulk_imports/file_transfer/project_config_spec.rb b/spec/models/bulk_imports/file_transfer/project_config_spec.rb
new file mode 100644
index 00000000000..021f96ac2a3
--- /dev/null
+++ b/spec/models/bulk_imports/file_transfer/project_config_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::FileTransfer::ProjectConfig do
+ let_it_be(:exportable) { create(:project) }
+ let_it_be(:hex) { '123' }
+
+ before do
+ allow(SecureRandom).to receive(:hex).and_return(hex)
+ end
+
+ subject { described_class.new(exportable) }
+
+ describe '#exportable_tree' do
+ it 'returns exportable tree' do
+ expect_next_instance_of(::Gitlab::ImportExport::AttributesFinder) do |finder|
+ expect(finder).to receive(:find_root).with(:project).and_call_original
+ end
+
+ expect(subject.portable_tree).not_to be_empty
+ end
+ end
+
+ describe '#export_path' do
+ it 'returns correct export path' do
+ expect(::Gitlab::ImportExport).to receive(:storage_path).and_return('storage_path')
+
+ expect(subject.export_path).to eq("storage_path/#{exportable.disk_path}/#{hex}")
+ end
+ end
+
+ describe '#exportable_relations' do
+ it 'returns a list of top level exportable relations' do
+ expect(subject.portable_relations).to include('issues', 'labels', 'milestones', 'merge_requests')
+ end
+ end
+end
diff --git a/spec/models/bulk_imports/file_transfer_spec.rb b/spec/models/bulk_imports/file_transfer_spec.rb
new file mode 100644
index 00000000000..5a2b303626c
--- /dev/null
+++ b/spec/models/bulk_imports/file_transfer_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::FileTransfer do
+ describe '.config_for' do
+ context 'when portable is group' do
+ it 'returns group config' do
+ expect(described_class.config_for(build(:group))).to be_instance_of(BulkImports::FileTransfer::GroupConfig)
+ end
+ end
+
+ context 'when portable is project' do
+ it 'returns project config' do
+ expect(described_class.config_for(build(:project))).to be_instance_of(BulkImports::FileTransfer::ProjectConfig)
+ end
+ end
+
+ context 'when portable is unsupported' do
+ it 'raises an error' do
+ expect { described_class.config_for(nil) }.to raise_error(BulkImports::FileTransfer::UnsupportedObjectType)
+ end
+ end
+ end
+end
diff --git a/spec/models/bulk_imports/stage_spec.rb b/spec/models/bulk_imports/stage_spec.rb
deleted file mode 100644
index 7765fd4c5c4..00000000000
--- a/spec/models/bulk_imports/stage_spec.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe BulkImports::Stage do
- let(:pipelines) do
- if Gitlab.ee?
- [
- [0, BulkImports::Groups::Pipelines::GroupPipeline],
- [1, BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline],
- [1, BulkImports::Groups::Pipelines::MembersPipeline],
- [1, BulkImports::Groups::Pipelines::LabelsPipeline],
- [1, BulkImports::Groups::Pipelines::MilestonesPipeline],
- [1, BulkImports::Groups::Pipelines::BadgesPipeline],
- [1, 'BulkImports::Groups::Pipelines::IterationsPipeline'.constantize],
- [2, 'BulkImports::Groups::Pipelines::EpicsPipeline'.constantize],
- [3, 'BulkImports::Groups::Pipelines::EpicAwardEmojiPipeline'.constantize],
- [3, 'BulkImports::Groups::Pipelines::EpicEventsPipeline'.constantize],
- [4, BulkImports::Groups::Pipelines::EntityFinisher]
- ]
- else
- [
- [0, BulkImports::Groups::Pipelines::GroupPipeline],
- [1, BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline],
- [1, BulkImports::Groups::Pipelines::MembersPipeline],
- [1, BulkImports::Groups::Pipelines::LabelsPipeline],
- [1, BulkImports::Groups::Pipelines::MilestonesPipeline],
- [1, BulkImports::Groups::Pipelines::BadgesPipeline],
- [2, BulkImports::Groups::Pipelines::EntityFinisher]
- ]
- end
- end
-
- describe '.pipelines' do
- it 'list all the pipelines with their stage number, ordered by stage' do
- expect(described_class.pipelines).to match_array(pipelines)
- end
- end
-
- describe '.pipeline_exists?' do
- it 'returns true when the given pipeline name exists in the pipelines list' do
- expect(described_class.pipeline_exists?(BulkImports::Groups::Pipelines::GroupPipeline)).to eq(true)
- expect(described_class.pipeline_exists?('BulkImports::Groups::Pipelines::GroupPipeline')).to eq(true)
- end
-
- it 'returns false when the given pipeline name exists in the pipelines list' do
- expect(described_class.pipeline_exists?('BulkImports::Groups::Pipelines::InexistentPipeline')).to eq(false)
- end
- end
-end
diff --git a/spec/models/chat_name_spec.rb b/spec/models/chat_name_spec.rb
index 623e55aad21..4d77bd53158 100644
--- a/spec/models/chat_name_spec.rb
+++ b/spec/models/chat_name_spec.rb
@@ -6,11 +6,11 @@ RSpec.describe ChatName do
let_it_be(:chat_name) { create(:chat_name) }
subject { chat_name }
- it { is_expected.to belong_to(:service) }
+ it { is_expected.to belong_to(:integration) }
it { is_expected.to belong_to(:user) }
it { is_expected.to validate_presence_of(:user) }
- it { is_expected.to validate_presence_of(:service) }
+ it { is_expected.to validate_presence_of(:integration) }
it { is_expected.to validate_presence_of(:team_id) }
it { is_expected.to validate_presence_of(:chat_id) }
@@ -18,7 +18,7 @@ RSpec.describe ChatName do
it { is_expected.to validate_uniqueness_of(:chat_id).scoped_to(:service_id, :team_id) }
it 'is removed when the project is deleted' do
- expect { subject.reload.service.project.delete }.to change { ChatName.count }.by(-1)
+ expect { subject.reload.integration.project.delete }.to change { ChatName.count }.by(-1)
expect(ChatName.where(id: subject.id)).not_to exist
end
diff --git a/spec/models/ci/build_dependencies_spec.rb b/spec/models/ci/build_dependencies_spec.rb
index e343ec0e698..d00d88ae397 100644
--- a/spec/models/ci/build_dependencies_spec.rb
+++ b/spec/models/ci/build_dependencies_spec.rb
@@ -18,12 +18,8 @@ RSpec.describe Ci::BuildDependencies do
let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') }
let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') }
- before do
- stub_feature_flags(ci_validate_build_dependencies_override: false)
- end
-
- describe '#local' do
- subject { described_class.new(job).local }
+ context 'for local dependencies' do
+ subject { described_class.new(job).all }
describe 'jobs from previous stages' do
context 'when job is in the first stage' do
@@ -52,7 +48,7 @@ RSpec.describe Ci::BuildDependencies do
project.add_developer(user)
end
- let(:retried_job) { Ci::Build.retry(rspec_test, user) }
+ let!(:retried_job) { Ci::Build.retry(rspec_test, user) }
it 'contains the retried job instead of the original one' do
is_expected.to contain_exactly(build, retried_job, rubocop_test)
@@ -150,7 +146,7 @@ RSpec.describe Ci::BuildDependencies do
end
end
- describe '#cross_pipeline' do
+ context 'for cross_pipeline dependencies' do
let!(:job) do
create(:ci_build,
pipeline: pipeline,
@@ -160,7 +156,7 @@ RSpec.describe Ci::BuildDependencies do
subject { described_class.new(job) }
- let(:cross_pipeline_deps) { subject.cross_pipeline }
+ let(:cross_pipeline_deps) { subject.all }
context 'when dependency specifications are valid' do
context 'when pipeline exists in the hierarchy' do
@@ -378,14 +374,6 @@ RSpec.describe Ci::BuildDependencies do
end
it { is_expected.to eq(false) }
-
- context 'when ci_validate_build_dependencies_override feature flag is enabled' do
- before do
- stub_feature_flags(ci_validate_build_dependencies_override: job.project)
- end
-
- it { is_expected.to eq(true) }
- end
end
end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 339dffa507f..66d2f5f4ee9 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1132,7 +1132,7 @@ RSpec.describe Ci::Build do
it "executes UPDATE query" do
recorded = ActiveRecord::QueryRecorder.new { subject }
- expect(recorded.log.select { |l| l.match?(/UPDATE.*ci_builds/) }.count).to eq(1)
+ expect(recorded.log.count { |l| l.match?(/UPDATE.*ci_builds/) }).to eq(1)
end
end
@@ -1140,7 +1140,7 @@ RSpec.describe Ci::Build do
it 'does not execute UPDATE query' do
recorded = ActiveRecord::QueryRecorder.new { subject }
- expect(recorded.log.select { |l| l.match?(/UPDATE.*ci_builds/) }.count).to eq(0)
+ expect(recorded.log.count { |l| l.match?(/UPDATE.*ci_builds/) }).to eq(0)
end
end
end
@@ -1205,7 +1205,7 @@ RSpec.describe Ci::Build do
before do
allow(Deployments::LinkMergeRequestWorker).to receive(:perform_async)
- allow(Deployments::ExecuteHooksWorker).to receive(:perform_async)
+ allow(Deployments::HooksWorker).to receive(:perform_async)
end
it 'has deployments record with created status' do
@@ -1241,7 +1241,7 @@ RSpec.describe Ci::Build do
before do
allow(Deployments::UpdateEnvironmentWorker).to receive(:perform_async)
- allow(Deployments::ExecuteHooksWorker).to receive(:perform_async)
+ allow(Deployments::HooksWorker).to receive(:perform_async)
end
it_behaves_like 'avoid deadlock'
@@ -3631,46 +3631,29 @@ RSpec.describe Ci::Build do
end
let!(:job) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 1, options: options) }
+ let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) }
- context 'when validates for dependencies is enabled' do
- 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) }
-
- context 'when "dependencies" keyword is not defined' do
- let(:options) { {} }
-
- it { expect(job).to have_valid_build_dependencies }
- end
-
- context 'when "dependencies" keyword is empty' do
- let(:options) { { dependencies: [] } }
+ context 'when "dependencies" keyword is not defined' do
+ let(:options) { {} }
- it { expect(job).to have_valid_build_dependencies }
- end
+ it { expect(job).to have_valid_build_dependencies }
+ end
- context 'when "dependencies" keyword is specified' do
- let(:options) { { dependencies: ['test'] } }
+ context 'when "dependencies" keyword is empty' do
+ let(:options) { { dependencies: [] } }
- it_behaves_like 'validation is active'
- end
+ it { expect(job).to have_valid_build_dependencies }
end
- context 'when validates for dependencies is disabled' do
+ context 'when "dependencies" keyword is specified' do
let(:options) { { dependencies: ['test'] } }
- before do
- stub_feature_flags(ci_validate_build_dependencies_override: true)
- end
-
- it_behaves_like 'validation is not active'
+ it_behaves_like 'validation is active'
end
end
describe 'state transition when build fails' do
- let(:service) { ::MergeRequests::AddTodoWhenBuildFailsService.new(project, user) }
+ let(:service) { ::MergeRequests::AddTodoWhenBuildFailsService.new(project: project, current_user: user) }
before do
allow(::MergeRequests::AddTodoWhenBuildFailsService).to receive(:new).and_return(service)
@@ -4679,25 +4662,30 @@ RSpec.describe Ci::Build do
end
describe '#execute_hooks' do
+ before do
+ build.clear_memoization(:build_data)
+ end
+
context 'with project hooks' do
+ let(:build_data) { double(:BuildData, dup: double(:DupedData)) }
+
before do
create(:project_hook, project: project, job_events: true)
end
- it 'execute hooks' do
- expect_any_instance_of(ProjectHook).to receive(:async_execute)
+ it 'calls project.execute_hooks(build_data, :job_hooks)' do
+ expect(::Gitlab::DataBuilder::Build)
+ .to receive(:build).with(build).and_return(build_data)
+ expect(build.project)
+ .to receive(:execute_hooks).with(build_data.dup, :job_hooks)
build.execute_hooks
end
end
- context 'without relevant project hooks' do
- before do
- create(:project_hook, project: project, job_events: false)
- end
-
- it 'does not execute a hook' do
- expect_any_instance_of(ProjectHook).not_to receive(:async_execute)
+ context 'without project hooks' do
+ it 'does not call project.execute_hooks' do
+ expect(build.project).not_to receive(:execute_hooks)
build.execute_hooks
end
@@ -4708,8 +4696,10 @@ RSpec.describe Ci::Build do
create(:service, active: true, job_events: true, project: project)
end
- it 'execute services' do
- expect_any_instance_of(Service).to receive(:async_execute)
+ it 'executes services' do
+ allow_next_found_instance_of(Integration) do |integration|
+ expect(integration).to receive(:async_execute)
+ end
build.execute_hooks
end
@@ -4720,8 +4710,10 @@ RSpec.describe Ci::Build do
create(:service, active: true, job_events: false, project: project)
end
- it 'execute services' do
- expect_any_instance_of(Service).not_to receive(:async_execute)
+ it 'does not execute services' do
+ allow_next_found_instance_of(Integration) do |integration|
+ expect(integration).not_to receive(:async_execute)
+ end
build.execute_hooks
end
diff --git a/spec/models/ci/commit_with_pipeline_spec.rb b/spec/models/ci/commit_with_pipeline_spec.rb
index 4dd288bde62..320143535e2 100644
--- a/spec/models/ci/commit_with_pipeline_spec.rb
+++ b/spec/models/ci/commit_with_pipeline_spec.rb
@@ -26,15 +26,47 @@ RSpec.describe Ci::CommitWithPipeline do
end
end
+ describe '#lazy_latest_pipeline' do
+ let(:commit_1) do
+ described_class.new(Commit.new(RepoHelpers.sample_commit, project))
+ end
+
+ let(:commit_2) do
+ described_class.new(Commit.new(RepoHelpers.another_sample_commit, project))
+ end
+
+ let!(:commits) { [commit_1, commit_2] }
+
+ it 'executes only 1 SQL query' do
+ recorder = ActiveRecord::QueryRecorder.new do
+ # Running this first ensures we don't run one query for every
+ # commit.
+ commits.each(&:lazy_latest_pipeline)
+
+ # This forces the execution of the SQL queries necessary to load the
+ # data.
+ commits.each { |c| c.latest_pipeline.try(:id) }
+ end
+
+ expect(recorder.count).to eq(1)
+ end
+ end
+
describe '#latest_pipeline' do
let(:pipeline) { double }
shared_examples_for 'fetching latest pipeline' do |ref|
it 'returns the latest pipeline for the project' do
- expect(commit)
- .to receive(:latest_pipeline_for_project)
- .with(ref, project)
- .and_return(pipeline)
+ if ref
+ expect(commit)
+ .to receive(:latest_pipeline_for_project)
+ .with(ref, project)
+ .and_return(pipeline)
+ else
+ expect(commit)
+ .to receive(:lazy_latest_pipeline)
+ .and_return(pipeline)
+ end
expect(result).to eq(pipeline)
end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index cdb123573f1..3c4769764d5 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -602,6 +602,34 @@ RSpec.describe Ci::JobArtifact do
end
end
+ context 'FastDestroyAll' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:job) { create(:ci_build, pipeline: pipeline, project: project) }
+
+ let!(:job_artifact) { create(:ci_job_artifact, :archive, job: job) }
+ let(:subjects) { pipeline.job_artifacts }
+
+ describe '.use_fast_destroy' do
+ it 'performs cascading delete with fast_destroy_all' do
+ expect(Ci::DeletedObject.count).to eq(0)
+ expect(subjects.count).to be > 0
+
+ expect { pipeline.destroy! }.not_to raise_error
+
+ expect(subjects.count).to eq(0)
+ expect(Ci::DeletedObject.count).to be > 0
+ end
+
+ it 'updates project statistics' do
+ expect(ProjectStatistics).to receive(:increment_statistic).once
+ .with(project, :build_artifacts_size, -job_artifact.file.size)
+
+ pipeline.destroy!
+ end
+ end
+ end
+
def file_type_limit_failure_message(type, limit_name)
<<~MSG
The artifact type `#{type}` is missing its counterpart plan limit which is expected to be named `#{limit_name}`.
diff --git a/spec/models/ci/pipeline_artifact_spec.rb b/spec/models/ci/pipeline_artifact_spec.rb
index 3fe09f05cab..f65483d2290 100644
--- a/spec/models/ci/pipeline_artifact_spec.rb
+++ b/spec/models/ci/pipeline_artifact_spec.rb
@@ -50,6 +50,30 @@ RSpec.describe Ci::PipelineArtifact, type: :model do
end
end
+ describe 'scopes' do
+ describe '.unlocked' do
+ subject(:pipeline_artifacts) { described_class.unlocked }
+
+ context 'when pipeline is locked' do
+ it 'returns an empty collection' do
+ expect(pipeline_artifacts).to be_empty
+ end
+ end
+
+ context 'when pipeline is unlocked' do
+ before do
+ create(:ci_pipeline_artifact, :with_coverage_report)
+ end
+
+ it 'returns unlocked artifacts' do
+ codequality_report = create(:ci_pipeline_artifact, :with_codequality_mr_diff_report, :unlocked)
+
+ expect(pipeline_artifacts).to eq([codequality_report])
+ end
+ end
+ end
+ end
+
describe 'file is being stored' do
subject { create(:ci_pipeline_artifact, :with_coverage_report) }
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
index 3e5fbbfe823..d5560edbbfd 100644
--- a/spec/models/ci/pipeline_schedule_spec.rb
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -126,16 +126,6 @@ RSpec.describe Ci::PipelineSchedule do
end
end
- context 'when pipeline schedule runs every minute' do
- let(:pipeline_schedule) { create(:ci_pipeline_schedule, :every_minute) }
-
- it "updates next_run_at to the sidekiq worker's execution time" do
- travel_to(Time.zone.parse("2019-06-01 12:18:00+0000")) do
- expect(pipeline_schedule.next_run_at).to eq(cron_worker_next_run_at)
- end
- end
- end
-
context 'when there are two different pipeline schedules in different time zones' do
let(:pipeline_schedule_1) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'Eastern Time (US & Canada)') }
let(:pipeline_schedule_2) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') }
@@ -144,24 +134,6 @@ RSpec.describe Ci::PipelineSchedule do
expect(pipeline_schedule_1.next_run_at).not_to eq(pipeline_schedule_2.next_run_at)
end
end
-
- context 'when there are two different pipeline schedules in the same time zones' do
- let(:pipeline_schedule_1) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') }
- let(:pipeline_schedule_2) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') }
-
- it 'sets the sames next_run_at' do
- expect(pipeline_schedule_1.next_run_at).to eq(pipeline_schedule_2.next_run_at)
- end
- end
-
- context 'when updates cron of exsisted pipeline schedule' do
- let(:new_cron) { '0 0 1 1 *' }
-
- it 'updates next_run_at automatically' do
- expect { pipeline_schedule.update!(cron: new_cron) }
- .to change { pipeline_schedule.next_run_at }
- end
- end
end
describe '#schedule_next_run!' do
@@ -178,7 +150,7 @@ RSpec.describe Ci::PipelineSchedule do
context 'when record is invalid' do
before do
- allow(pipeline_schedule).to receive(:save!) { raise ActiveRecord::RecordInvalid.new(pipeline_schedule) }
+ allow(pipeline_schedule).to receive(:save!) { raise ActiveRecord::RecordInvalid, pipeline_schedule }
end
it 'nullifies the next run at' do
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index b7f5811e945..b9457055a18 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -68,14 +68,23 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
describe '#downloadable_artifacts' do
- let(:build) { create(:ci_build, pipeline: pipeline) }
+ let_it_be(:build) { create(:ci_build, pipeline: pipeline) }
+ let_it_be(:downloadable_artifact) { create(:ci_job_artifact, :codequality, job: build) }
+ let_it_be(:expired_artifact) { create(:ci_job_artifact, :junit, :expired, job: build) }
+ let_it_be(:undownloadable_artifact) { create(:ci_job_artifact, :trace, job: build) }
+
+ context 'when artifacts are locked' do
+ it 'returns downloadable artifacts including locked artifacts' do
+ expect(pipeline.downloadable_artifacts).to contain_exactly(downloadable_artifact, expired_artifact)
+ end
+ end
- it 'returns downloadable artifacts that have not expired' do
- downloadable_artifact = create(:ci_job_artifact, :codequality, job: build)
- _expired_artifact = create(:ci_job_artifact, :junit, :expired, job: build)
- _undownloadable_artifact = create(:ci_job_artifact, :trace, job: build)
+ context 'when artifacts are unlocked' do
+ it 'returns only downloadable artifacts not expired' do
+ expired_artifact.job.pipeline.unlocked!
- expect(pipeline.downloadable_artifacts).to contain_exactly(downloadable_artifact)
+ expect(pipeline.reload.downloadable_artifacts).to contain_exactly(downloadable_artifact)
+ end
end
end
end
@@ -1939,6 +1948,30 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
expect(pipeline.modified_paths).to match(merge_request.modified_paths)
end
end
+
+ context 'when source is an external pull request' do
+ let(:pipeline) do
+ create(:ci_pipeline, source: :external_pull_request_event, external_pull_request: external_pull_request)
+ end
+
+ let(:external_pull_request) do
+ create(:external_pull_request, project: project, target_sha: '281d3a7', source_sha: '498214d')
+ end
+
+ it 'returns external pull request modified paths' do
+ expect(pipeline.modified_paths).to match(external_pull_request.modified_paths)
+ end
+
+ context 'when the FF ci_modified_paths_of_external_prs is disabled' do
+ before do
+ stub_feature_flags(ci_modified_paths_of_external_prs: false)
+ end
+
+ it 'returns nil' do
+ expect(pipeline.modified_paths).to be_nil
+ end
+ end
+ end
end
describe '#all_worktree_paths' do
@@ -3201,18 +3234,6 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
expect(pipeline.messages.map(&:content)).to contain_exactly('The error message')
end
-
- context 'when feature flag ci_store_pipeline_messages is disabled' do
- before do
- stub_feature_flags(ci_store_pipeline_messages: false)
- end
-
- it 'does not add pipeline error message' do
- pipeline.add_error_message('The error message')
-
- expect(pipeline.messages).to be_empty
- end
- end
end
describe '#has_yaml_errors?' do
@@ -4303,26 +4324,80 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
- describe 'reset_ancestor_bridges!' do
- let_it_be(:pipeline) { create(:ci_pipeline, :created) }
+ describe '#reset_source_bridge!' do
+ let(:pipeline) { create(:ci_pipeline, :created, project: project) }
+
+ subject(:reset_bridge) { pipeline.reset_source_bridge!(project.owner) }
+
+ # This whole block will be removed by https://gitlab.com/gitlab-org/gitlab/-/issues/329194
+ # It contains some duplicate checks.
+ context 'when the FF ci_reset_bridge_with_subsequent_jobs is disabled' do
+ before do
+ stub_feature_flags(ci_reset_bridge_with_subsequent_jobs: false)
+ end
+
+ context 'when the pipeline is a child pipeline and the bridge is depended' do
+ let!(:parent_pipeline) { create(:ci_pipeline) }
+ let!(:bridge) { create_bridge(parent_pipeline, pipeline, true) }
+
+ it 'marks source bridge as pending' do
+ reset_bridge
+
+ expect(bridge.reload).to be_pending
+ end
+
+ context 'when the parent pipeline has subsequent jobs after the bridge' do
+ let!(:after_bridge_job) { create(:ci_build, :skipped, pipeline: parent_pipeline, stage_idx: bridge.stage_idx + 1) }
+
+ it 'does not touch subsequent jobs of the bridge' do
+ reset_bridge
+
+ expect(after_bridge_job.reload).to be_skipped
+ end
+ end
+
+ context 'when the parent pipeline has a dependent upstream pipeline' do
+ let!(:upstream_bridge) do
+ create_bridge(create(:ci_pipeline, project: create(:project)), parent_pipeline, true)
+ end
+
+ it 'marks all source bridges as pending' do
+ reset_bridge
+
+ expect(bridge.reload).to be_pending
+ expect(upstream_bridge.reload).to be_pending
+ end
+ end
+ end
+ end
context 'when the pipeline is a child pipeline and the bridge is depended' do
let!(:parent_pipeline) { create(:ci_pipeline) }
let!(:bridge) { create_bridge(parent_pipeline, pipeline, true) }
it 'marks source bridge as pending' do
- pipeline.reset_ancestor_bridges!
+ reset_bridge
expect(bridge.reload).to be_pending
end
+ context 'when the parent pipeline has subsequent jobs after the bridge' do
+ let!(:after_bridge_job) { create(:ci_build, :skipped, pipeline: parent_pipeline, stage_idx: bridge.stage_idx + 1) }
+
+ it 'marks subsequent jobs of the bridge as processable' do
+ reset_bridge
+
+ expect(after_bridge_job.reload).to be_created
+ end
+ end
+
context 'when the parent pipeline has a dependent upstream pipeline' do
let!(:upstream_bridge) do
create_bridge(create(:ci_pipeline, project: create(:project)), parent_pipeline, true)
end
it 'marks all source bridges as pending' do
- pipeline.reset_ancestor_bridges!
+ reset_bridge
expect(bridge.reload).to be_pending
expect(upstream_bridge.reload).to be_pending
@@ -4335,7 +4410,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
let!(:bridge) { create_bridge(parent_pipeline, pipeline, false) }
it 'does not touch source bridge' do
- pipeline.reset_ancestor_bridges!
+ reset_bridge
expect(bridge.reload).to be_success
end
@@ -4346,7 +4421,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
it 'does not touch any source bridge' do
- pipeline.reset_ancestor_bridges!
+ reset_bridge
expect(bridge.reload).to be_success
expect(upstream_bridge.reload).to be_success
diff --git a/spec/models/ci/runner_namespace_spec.rb b/spec/models/ci/runner_namespace_spec.rb
new file mode 100644
index 00000000000..41d805adb9f
--- /dev/null
+++ b/spec/models/ci/runner_namespace_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::RunnerNamespace do
+ it_behaves_like 'includes Limitable concern' do
+ subject { build(:ci_runner_namespace, group: create(:group, :nested), runner: create(:ci_runner, :group)) }
+ end
+end
diff --git a/spec/models/ci/runner_project_spec.rb b/spec/models/ci/runner_project_spec.rb
new file mode 100644
index 00000000000..13369dba2cf
--- /dev/null
+++ b/spec/models/ci/runner_project_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::RunnerProject do
+ it_behaves_like 'includes Limitable concern' do
+ subject { build(:ci_runner_project, project: create(:project), runner: create(:ci_runner, :project)) }
+ end
+end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index e46d9189c86..5e0fcb4882f 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -286,6 +286,18 @@ RSpec.describe Ci::Stage, :models do
end
end
+ context 'when stage has statuses with nil idx' do
+ before do
+ create(:ci_build, :running, stage_id: stage.id, stage_idx: nil)
+ create(:ci_build, :running, stage_id: stage.id, stage_idx: 10)
+ create(:ci_build, :running, stage_id: stage.id, stage_idx: nil)
+ end
+
+ it 'sets index to a non-empty value' do
+ expect { stage.update_legacy_status }.to change { stage.reload.position }.from(nil).to(10)
+ end
+ end
+
context 'when stage does not have statuses' do
it 'fallbacks to zero' do
expect(stage.reload.position).to be_nil
diff --git a/spec/models/clusters/agent_spec.rb b/spec/models/clusters/agent_spec.rb
index a85a72eba0b..ea7a55480a8 100644
--- a/spec/models/clusters/agent_spec.rb
+++ b/spec/models/clusters/agent_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe Clusters::Agent do
it { is_expected.to belong_to(:created_by_user).class_name('User').optional }
it { is_expected.to belong_to(:project).class_name('::Project') }
it { is_expected.to have_many(:agent_tokens).class_name('Clusters::AgentToken') }
+ it { is_expected.to have_many(:last_used_agent_tokens).class_name('Clusters::AgentToken') }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_length_of(:name).is_at_most(63) }
diff --git a/spec/models/clusters/agent_token_spec.rb b/spec/models/clusters/agent_token_spec.rb
index 680b351d24a..bde4798abec 100644
--- a/spec/models/clusters/agent_token_spec.rb
+++ b/spec/models/clusters/agent_token_spec.rb
@@ -9,6 +9,19 @@ RSpec.describe Clusters::AgentToken do
it { is_expected.to validate_length_of(:name).is_at_most(255) }
it { is_expected.to validate_presence_of(:name) }
+ describe 'scopes' do
+ describe '.order_last_used_at_desc' do
+ let_it_be(:token_1) { create(:cluster_agent_token, last_used_at: 7.days.ago) }
+ let_it_be(:token_2) { create(:cluster_agent_token, last_used_at: nil) }
+ let_it_be(:token_3) { create(:cluster_agent_token, last_used_at: 2.days.ago) }
+
+ it 'sorts by last_used_at descending, with null values at last' do
+ expect(described_class.order_last_used_at_desc)
+ .to eq([token_3, token_1, token_2])
+ end
+ end
+ end
+
describe '#token' do
it 'is generated on save' do
agent_token = build(:cluster_agent_token, token_encrypted: nil)
diff --git a/spec/models/clusters/applications/elastic_stack_spec.rb b/spec/models/clusters/applications/elastic_stack_spec.rb
index 74cacd486b0..af2802d5e47 100644
--- a/spec/models/clusters/applications/elastic_stack_spec.rb
+++ b/spec/models/clusters/applications/elastic_stack_spec.rb
@@ -10,6 +10,41 @@ RSpec.describe Clusters::Applications::ElasticStack do
include_examples 'cluster application version specs', :clusters_applications_elastic_stack
include_examples 'cluster application helm specs', :clusters_applications_elastic_stack
+ describe 'cluster.integration_elastic_stack state synchronization' do
+ let!(:application) { create(:clusters_applications_elastic_stack) }
+ let(:cluster) { application.cluster }
+ let(:integration) { cluster.integration_elastic_stack }
+
+ describe 'after_destroy' do
+ it 'disables the corresponding integration' do
+ application.destroy!
+
+ expect(integration).not_to be_enabled
+ end
+ end
+
+ describe 'on install' do
+ it 'enables the corresponding integration' do
+ application.make_scheduled!
+ application.make_installing!
+ application.make_installed!
+
+ expect(integration).to be_enabled
+ end
+ end
+
+ describe 'on uninstall' do
+ it 'disables the corresponding integration' do
+ application.make_scheduled!
+ application.make_installing!
+ application.make_installed!
+ application.make_externally_uninstalled!
+
+ expect(integration).not_to be_enabled
+ end
+ end
+ end
+
describe '#install_command' do
let!(:elastic_stack) { create(:clusters_applications_elastic_stack) }
@@ -138,78 +173,5 @@ RSpec.describe Clusters::Applications::ElasticStack do
end
end
- describe '#elasticsearch_client' do
- context 'cluster is nil' do
- it 'returns nil' do
- expect(subject.cluster).to be_nil
- expect(subject.elasticsearch_client).to be_nil
- end
- end
-
- context "cluster doesn't have kubeclient" do
- let(:cluster) { create(:cluster) }
-
- subject { create(:clusters_applications_elastic_stack, cluster: cluster) }
-
- it 'returns nil' do
- expect(subject.elasticsearch_client).to be_nil
- end
- end
-
- context 'cluster has kubeclient' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:kubernetes_url) { subject.cluster.platform_kubernetes.api_url }
- let(:kube_client) { subject.cluster.kubeclient.core_client }
-
- subject { create(:clusters_applications_elastic_stack, cluster: cluster) }
-
- before do
- subject.cluster.platform_kubernetes.namespace = 'a-namespace'
- stub_kubeclient_discover(cluster.platform_kubernetes.api_url)
-
- create(:cluster_kubernetes_namespace,
- cluster: cluster,
- cluster_project: cluster.cluster_project,
- project: cluster.cluster_project.project)
- end
-
- it 'creates proxy elasticsearch_client' do
- expect(subject.elasticsearch_client).to be_instance_of(Elasticsearch::Transport::Client)
- end
-
- it 'copies proxy_url, options and headers from kube client to elasticsearch_client' do
- expect(Elasticsearch::Client)
- .to(receive(:new))
- .with(url: a_valid_url)
- .and_call_original
-
- client = subject.elasticsearch_client
- faraday_connection = client.transport.connections.first.connection
-
- expect(faraday_connection.headers["Authorization"]).to eq(kube_client.headers[:Authorization])
- expect(faraday_connection.ssl.cert_store).to be_instance_of(OpenSSL::X509::Store)
- expect(faraday_connection.ssl.verify).to eq(1)
- expect(faraday_connection.options.timeout).to be_nil
- end
-
- context 'when cluster is not reachable' do
- before do
- allow(kube_client).to receive(:proxy_url).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil))
- end
-
- it 'returns nil' do
- expect(subject.elasticsearch_client).to be_nil
- end
- end
-
- context 'when timeout is provided' do
- it 'sets timeout in elasticsearch_client' do
- client = subject.elasticsearch_client(timeout: 123)
- faraday_connection = client.transport.connections.first.connection
-
- expect(faraday_connection.options.timeout).to eq(123)
- end
- end
- end
- end
+ it_behaves_like 'cluster-based #elasticsearch_client', :clusters_applications_elastic_stack
end
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index 5a0ccabd467..549a273e2d7 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -13,16 +13,13 @@ RSpec.describe Clusters::Applications::Prometheus do
include_examples 'cluster application initial status specs'
describe 'after_destroy' do
- context 'cluster type is project' do
- let(:cluster) { create(:cluster, :with_installed_helm) }
- let(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
+ let(:cluster) { create(:cluster, :with_installed_helm) }
+ let(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
- it 'deactivates prometheus_service after destroy' do
- expect(Clusters::Applications::DeactivateServiceWorker)
- .to receive(:perform_async).with(cluster.id, 'prometheus')
+ it 'disables the corresponding integration' do
+ application.destroy!
- application.destroy!
- end
+ expect(cluster.integration_prometheus).not_to be_enabled
end
end
@@ -31,11 +28,10 @@ RSpec.describe Clusters::Applications::Prometheus do
let(:cluster) { create(:cluster, :with_installed_helm) }
let(:application) { create(:clusters_applications_prometheus, :installing, cluster: cluster) }
- it 'schedules post installation job' do
- expect(Clusters::Applications::ActivateServiceWorker)
- .to receive(:perform_async).with(cluster.id, 'prometheus')
-
+ it 'enables the corresponding integration' do
application.make_installed
+
+ expect(cluster.integration_prometheus).to be_enabled
end
end
@@ -44,11 +40,10 @@ RSpec.describe Clusters::Applications::Prometheus do
let(:cluster) { create(:cluster, :with_installed_helm) }
let(:application) { create(:clusters_applications_prometheus, :installing, cluster: cluster) }
- it 'schedules post installation job' do
- expect(Clusters::Applications::ActivateServiceWorker)
- .to receive(:perform_async).with(cluster.id, 'prometheus')
-
+ it 'enables the corresponding integration' do
application.make_externally_installed!
+
+ expect(cluster.integration_prometheus).to be_enabled
end
end
@@ -65,6 +60,26 @@ RSpec.describe Clusters::Applications::Prometheus do
end
end
+ describe '#managed_prometheus?' do
+ subject { prometheus.managed_prometheus? }
+
+ let(:prometheus) { build(:clusters_applications_prometheus) }
+
+ it { is_expected.to be_truthy }
+
+ context 'externally installed' do
+ let(:prometheus) { build(:clusters_applications_prometheus, :externally_installed) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'uninstalled' do
+ let(:prometheus) { build(:clusters_applications_prometheus, :uninstalled) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
describe '#can_uninstall?' do
let(:prometheus) { create(:clusters_applications_prometheus) }
@@ -318,42 +333,10 @@ RSpec.describe Clusters::Applications::Prometheus do
describe 'alert manager token' do
subject { create(:clusters_applications_prometheus) }
- context 'when not set' do
- it 'is empty by default' do
- expect(subject.alert_manager_token).to be_nil
- expect(subject.encrypted_alert_manager_token).to be_nil
- expect(subject.encrypted_alert_manager_token_iv).to be_nil
- end
-
- describe '#generate_alert_manager_token!' do
- it 'generates a token' do
- subject.generate_alert_manager_token!
-
- expect(subject.alert_manager_token).to match(/\A\h{32}\z/)
- end
- end
- end
-
- context 'when set' do
- let(:token) { SecureRandom.hex }
-
- before do
- subject.update!(alert_manager_token: token)
- end
-
- it 'reads the token' do
- expect(subject.alert_manager_token).to eq(token)
- expect(subject.encrypted_alert_manager_token).not_to be_nil
- expect(subject.encrypted_alert_manager_token_iv).not_to be_nil
- end
-
- describe '#generate_alert_manager_token!' do
- it 'does not re-generate the token' do
- subject.generate_alert_manager_token!
-
- expect(subject.alert_manager_token).to eq(token)
- end
- end
+ it 'is autogenerated on creation' do
+ expect(subject.alert_manager_token).to match(/\A\h{32}\z/)
+ expect(subject.encrypted_alert_manager_token).not_to be_nil
+ expect(subject.encrypted_alert_manager_token_iv).not_to be_nil
end
end
end
diff --git a/spec/models/clusters/integrations/elastic_stack_spec.rb b/spec/models/clusters/integrations/elastic_stack_spec.rb
new file mode 100644
index 00000000000..be4d59b52a2
--- /dev/null
+++ b/spec/models/clusters/integrations/elastic_stack_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::Integrations::ElasticStack do
+ include KubernetesHelpers
+ include StubRequests
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:cluster).class_name('Clusters::Cluster') }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:cluster) }
+ it { is_expected.not_to allow_value(nil).for(:enabled) }
+ end
+
+ it_behaves_like 'cluster-based #elasticsearch_client', :clusters_integrations_elastic_stack
+end
diff --git a/spec/models/clusters/integrations/prometheus_spec.rb b/spec/models/clusters/integrations/prometheus_spec.rb
index a7be1673ce2..680786189ad 100644
--- a/spec/models/clusters/integrations/prometheus_spec.rb
+++ b/spec/models/clusters/integrations/prometheus_spec.rb
@@ -15,6 +15,62 @@ RSpec.describe Clusters::Integrations::Prometheus do
it { is_expected.not_to allow_value(nil).for(:enabled) }
end
+ describe 'after_destroy' do
+ subject(:integration) { create(:clusters_integrations_prometheus, cluster: cluster, enabled: true) }
+
+ let(:cluster) { create(:cluster, :with_installed_helm) }
+
+ it 'deactivates prometheus_service' do
+ expect(Clusters::Applications::DeactivateServiceWorker)
+ .to receive(:perform_async).with(cluster.id, 'prometheus')
+
+ integration.destroy!
+ end
+ end
+
+ describe 'after_save' do
+ subject(:integration) { create(:clusters_integrations_prometheus, cluster: cluster, enabled: enabled) }
+
+ let(:cluster) { create(:cluster, :with_installed_helm) }
+ let(:enabled) { true }
+
+ context 'when no change to enabled status' do
+ it 'does not touch project services' do
+ integration # ensure integration exists before we set the expectations
+
+ expect(Clusters::Applications::DeactivateServiceWorker)
+ .not_to receive(:perform_async)
+
+ expect(Clusters::Applications::ActivateServiceWorker)
+ .not_to receive(:perform_async)
+
+ integration.update!(enabled: enabled)
+ end
+ end
+
+ context 'when enabling' do
+ let(:enabled) { false }
+
+ it 'deactivates prometheus_service' do
+ expect(Clusters::Applications::ActivateServiceWorker)
+ .to receive(:perform_async).with(cluster.id, 'prometheus')
+
+ integration.update!(enabled: true)
+ end
+ end
+
+ context 'when disabling' do
+ let(:enabled) { true }
+
+ it 'activates prometheus_service' do
+ expect(Clusters::Applications::DeactivateServiceWorker)
+ .to receive(:perform_async).with(cluster.id, 'prometheus')
+
+ integration.update!(enabled: false)
+ end
+ end
+ end
+
describe '#prometheus_client' do
include_examples '#prometheus_client shared' do
let(:factory) { :clusters_integrations_prometheus }
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index e64dee2d26f..feb2f3630c1 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -259,6 +259,40 @@ RSpec.describe CommitStatus do
end
end
+ describe '#queued_duration' do
+ subject { commit_status.queued_duration }
+
+ around do |example|
+ travel_to(Time.current) { example.run }
+ end
+
+ context 'when created, then enqueued, then started' do
+ before do
+ commit_status.queued_at = 30.seconds.ago
+ commit_status.started_at = 25.seconds.ago
+ end
+
+ it { is_expected.to eq(5.0) }
+ end
+
+ context 'when created but not yet enqueued' do
+ before do
+ commit_status.queued_at = nil
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when enqueued, but not started' do
+ before do
+ commit_status.queued_at = Time.current - 1.minute
+ commit_status.started_at = nil
+ end
+
+ it { is_expected.to eq(1.minute) }
+ end
+ end
+
describe '.latest' do
subject { described_class.latest.order(:id) }
diff --git a/spec/models/concerns/bulk_insert_safe_spec.rb b/spec/models/concerns/bulk_insert_safe_spec.rb
index e40b0cf11ff..ca6df506ee8 100644
--- a/spec/models/concerns/bulk_insert_safe_spec.rb
+++ b/spec/models/concerns/bulk_insert_safe_spec.rb
@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe BulkInsertSafe do
before(:all) do
ActiveRecord::Schema.define do
+ create_table :bulk_insert_parent_items, force: true do |t|
+ t.string :name, null: false
+ end
+
create_table :bulk_insert_items, force: true do |t|
t.string :name, null: true
t.integer :enum_value, null: false
@@ -12,6 +16,7 @@ RSpec.describe BulkInsertSafe do
t.string :encrypted_secret_value_iv, null: false
t.binary :sha_value, null: false, limit: 20
t.jsonb :jsonb_value, null: false
+ t.belongs_to :bulk_insert_parent_item, foreign_key: true, null: true
t.index :name, unique: true
end
@@ -21,9 +26,23 @@ RSpec.describe BulkInsertSafe do
after(:all) do
ActiveRecord::Schema.define do
drop_table :bulk_insert_items, force: true
+ drop_table :bulk_insert_parent_items, force: true
end
end
+ BulkInsertParentItem = Class.new(ActiveRecord::Base) do
+ self.table_name = :bulk_insert_parent_items
+ self.inheritance_column = :_type_disabled
+
+ def self.name
+ table_name.singularize.camelcase
+ end
+ end
+
+ let_it_be(:bulk_insert_parent_item) do
+ BulkInsertParentItem.create!(name: 'parent')
+ end
+
let_it_be(:bulk_insert_item_class) do
Class.new(ActiveRecord::Base) do
self.table_name = 'bulk_insert_items'
@@ -33,6 +52,8 @@ RSpec.describe BulkInsertSafe do
validates :name, :enum_value, :secret_value, :sha_value, :jsonb_value, presence: true
+ belongs_to :bulk_insert_parent_item
+
sha_attribute :sha_value
enum enum_value: { case_1: 1 }
@@ -51,8 +72,8 @@ RSpec.describe BulkInsertSafe do
'BulkInsertItem'
end
- def self.valid_list(count)
- Array.new(count) { |n| new(name: "item-#{n}", secret_value: 'my-secret') }
+ def self.valid_list(count, bulk_insert_parent_item: nil)
+ Array.new(count) { |n| new(name: "item-#{n}", secret_value: 'my-secret', bulk_insert_parent_item: bulk_insert_parent_item) }
end
def self.invalid_list(count)
@@ -117,6 +138,14 @@ RSpec.describe BulkInsertSafe do
bulk_insert_item_class.bulk_insert!(items, batch_size: 5)
end
+ it 'inserts items with belongs_to association' do
+ items = bulk_insert_item_class.valid_list(10, bulk_insert_parent_item: bulk_insert_parent_item)
+
+ bulk_insert_item_class.bulk_insert!(items, batch_size: 5)
+
+ expect(bulk_insert_item_class.last(items.size).map(&:bulk_insert_parent_item)).to eq([bulk_insert_parent_item] * 10)
+ end
+
it 'items can be properly fetched from database' do
items = bulk_insert_item_class.valid_list(10)
@@ -129,8 +158,7 @@ RSpec.describe BulkInsertSafe do
it 'rolls back the transaction when any item is invalid' do
# second batch is bad
- all_items = bulk_insert_item_class.valid_list(10) +
- bulk_insert_item_class.invalid_list(10)
+ all_items = bulk_insert_item_class.valid_list(10) + bulk_insert_item_class.invalid_list(10)
expect do
bulk_insert_item_class.bulk_insert!(all_items, batch_size: 2) rescue nil
diff --git a/spec/models/concerns/cascading_namespace_setting_attribute_spec.rb b/spec/models/concerns/cascading_namespace_setting_attribute_spec.rb
index ddff9ce32b4..02cd8557231 100644
--- a/spec/models/concerns/cascading_namespace_setting_attribute_spec.rb
+++ b/spec/models/concerns/cascading_namespace_setting_attribute_spec.rb
@@ -142,7 +142,7 @@ RSpec.describe NamespaceSetting, 'CascadingNamespaceSettingAttribute' do
end
it 'does not allow the local value to be saved' do
- subgroup_settings.delayed_project_removal = nil
+ subgroup_settings.delayed_project_removal = false
expect { subgroup_settings.save! }
.to raise_error(ActiveRecord::RecordInvalid, /Delayed project removal cannot be changed because it is locked by an ancestor/)
@@ -164,6 +164,19 @@ RSpec.describe NamespaceSetting, 'CascadingNamespaceSettingAttribute' do
end
end
+ describe '#delayed_project_removal=' do
+ before do
+ subgroup_settings.update!(delayed_project_removal: nil)
+ group_settings.update!(delayed_project_removal: true)
+ end
+
+ it 'does not save the value locally when it matches the cascaded value' do
+ subgroup_settings.update!(delayed_project_removal: true)
+
+ expect(subgroup_settings.read_attribute(:delayed_project_removal)).to eq(nil)
+ end
+ end
+
describe '#delayed_project_removal_locked?' do
shared_examples 'not locked' do
it 'is not locked by an ancestor' do
@@ -189,6 +202,20 @@ RSpec.describe NamespaceSetting, 'CascadingNamespaceSettingAttribute' do
it_behaves_like 'not locked'
end
+ context 'when attribute is locked by self' do
+ before do
+ subgroup_settings.update!(lock_delayed_project_removal: true)
+ end
+
+ it 'is not locked by default' do
+ expect(subgroup_settings.delayed_project_removal_locked?).to eq(false)
+ end
+
+ it 'is locked when including self' do
+ expect(subgroup_settings.delayed_project_removal_locked?(include_self: true)).to eq(true)
+ end
+ end
+
context 'when parent does not lock the attribute' do
it_behaves_like 'not locked'
end
@@ -277,6 +304,13 @@ RSpec.describe NamespaceSetting, 'CascadingNamespaceSettingAttribute' do
expect { subgroup_settings.save! }
.to raise_error(ActiveRecord::RecordInvalid, /Delayed project removal cannot be nil when locking the attribute/)
end
+
+ it 'copies the cascaded value when locking the attribute if the local value is nil', :aggregate_failures do
+ subgroup_settings.delayed_project_removal = nil
+ subgroup_settings.lock_delayed_project_removal = true
+
+ expect(subgroup_settings.read_attribute(:delayed_project_removal)).to eq(false)
+ end
end
context 'when application settings locks the attribute' do
diff --git a/spec/models/concerns/chronic_duration_attribute_spec.rb b/spec/models/concerns/chronic_duration_attribute_spec.rb
index e6dbf403b63..00e28e19bd5 100644
--- a/spec/models/concerns/chronic_duration_attribute_spec.rb
+++ b/spec/models/concerns/chronic_duration_attribute_spec.rb
@@ -56,8 +56,7 @@ RSpec.shared_examples 'ChronicDurationAttribute writer' do
subject.send("#{virtual_field}=", '-10m')
expect(subject.valid?).to be_falsey
- expect(subject.errors&.messages)
- .to include(base: ['Maximum job timeout has a value which could not be accepted'])
+ expect(subject.errors.added?(:base, 'Maximum job timeout has a value which could not be accepted')).to be true
end
end
diff --git a/spec/models/concerns/ci/maskable_spec.rb b/spec/models/concerns/ci/maskable_spec.rb
index 840a08b6060..2b13fc21fe8 100644
--- a/spec/models/concerns/ci/maskable_spec.rb
+++ b/spec/models/concerns/ci/maskable_spec.rb
@@ -66,7 +66,7 @@ RSpec.describe Ci::Maskable do
end
it 'matches valid strings' do
- expect(subject.match?('Hello+World_123/@:-.')).to eq(true)
+ expect(subject.match?('Hello+World_123/@:-~.')).to eq(true)
end
end
diff --git a/spec/models/concerns/cron_schedulable_spec.rb b/spec/models/concerns/cron_schedulable_spec.rb
new file mode 100644
index 00000000000..39c3d5e55d3
--- /dev/null
+++ b/spec/models/concerns/cron_schedulable_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe CronSchedulable do
+ let(:ideal_next_run_at) { schedule.send(:ideal_next_run_from, Time.zone.now) }
+ let(:cron_worker_next_run_at) { schedule.send(:cron_worker_next_run_from, Time.zone.now) }
+
+ context 'for ci_pipeline_schedule' do
+ let(:schedule) { create(:ci_pipeline_schedule, :every_minute) }
+ let(:schedule_1) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') }
+ let(:schedule_2) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') }
+ let(:new_cron) { '0 0 1 1 *' }
+
+ it_behaves_like 'handles set_next_run_at'
+ end
+end
diff --git a/spec/models/concerns/has_integrations_spec.rb b/spec/models/concerns/has_integrations_spec.rb
new file mode 100644
index 00000000000..6e55a1c8b01
--- /dev/null
+++ b/spec/models/concerns/has_integrations_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe HasIntegrations do
+ let_it_be(:project_1) { create(:project) }
+ let_it_be(:project_2) { create(:project) }
+ let_it_be(:project_3) { create(:project) }
+ let_it_be(:project_4) { create(:project) }
+ let_it_be(:instance_integration) { create(:jira_service, :instance) }
+
+ before do
+ create(:jira_service, project: project_1, inherit_from_id: instance_integration.id)
+ create(:jira_service, project: project_2, inherit_from_id: nil)
+ create(:jira_service, group: create(:group), project: nil, inherit_from_id: nil)
+ create(:jira_service, project: project_3, inherit_from_id: nil)
+ create(:slack_service, project: project_4, inherit_from_id: nil)
+ end
+
+ describe '.with_custom_integration_for' do
+ it 'returns projects with custom integrations' do
+ # We use pagination to verify that the group is excluded from the query
+ expect(Project.with_custom_integration_for(instance_integration, 0, 2)).to contain_exactly(project_2, project_3)
+ expect(Project.with_custom_integration_for(instance_integration)).to contain_exactly(project_2, project_3)
+ end
+ end
+
+ describe '.without_integration' do
+ it 'returns projects without integration' do
+ expect(Project.without_integration(instance_integration)).to contain_exactly(project_4)
+ end
+ end
+end
diff --git a/spec/models/concerns/has_timelogs_report_spec.rb b/spec/models/concerns/has_timelogs_report_spec.rb
index f694fc350ee..f0dca47fae1 100644
--- a/spec/models/concerns/has_timelogs_report_spec.rb
+++ b/spec/models/concerns/has_timelogs_report_spec.rb
@@ -3,16 +3,20 @@
require 'spec_helper'
RSpec.describe HasTimelogsReport do
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+
let(:group) { create(:group) }
- let(:issue) { create(:issue, project: create(:project, :public, group: group)) }
+ let(:project) { create(:project, :public, group: group) }
+ let(:issue1) { create(:issue, project: project) }
+ let(:merge_request1) { create(:merge_request, source_project: project) }
describe '#timelogs' do
- let!(:timelog1) { create_timelog(15.days.ago) }
- let!(:timelog2) { create_timelog(10.days.ago) }
- let!(:timelog3) { create_timelog(5.days.ago) }
- let(:start_time) { 20.days.ago }
- let(:end_time) { 8.days.ago }
+ let_it_be(:start_time) { 20.days.ago }
+ let_it_be(:end_time) { 8.days.ago }
+
+ let!(:timelog1) { create_timelog(15.days.ago, issue: issue1) }
+ let!(:timelog2) { create_timelog(10.days.ago, merge_request: merge_request1) }
+ let!(:timelog3) { create_timelog(5.days.ago, issue: issue1) }
before do
group.add_developer(user)
@@ -45,7 +49,7 @@ RSpec.describe HasTimelogsReport do
end
end
- def create_timelog(time)
- create(:timelog, issue: issue, user: user, spent_at: time)
+ def create_timelog(time, issue: nil, merge_request: nil)
+ create(:timelog, issue: issue, merge_request: merge_request, user: user, spent_at: time)
end
end
diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb
index a7117af81a2..38766d8decd 100644
--- a/spec/models/concerns/noteable_spec.rb
+++ b/spec/models/concerns/noteable_spec.rb
@@ -288,7 +288,7 @@ RSpec.describe Noteable do
end
before 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)
Discussions::CaptureDiffNotePositionsService.new(merge_request).execute
end
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index 6ab87053258..0a433a8cf4f 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -62,7 +62,7 @@ RSpec.describe Routable do
end
end
-RSpec.describe Group, 'Routable' do
+RSpec.describe Group, 'Routable', :with_clean_rails_cache do
let_it_be_with_reload(:group) { create(:group, name: 'foo') }
let_it_be(:nested_group) { create(:group, parent: group) }
@@ -165,19 +165,63 @@ RSpec.describe Group, 'Routable' do
end
end
+ describe '#parent_loaded?' do
+ before do
+ group.parent = create(:group)
+ group.save!
+
+ group.reload
+ end
+
+ it 'is false when the parent is not loaded' do
+ expect(group.parent_loaded?).to be_falsey
+ end
+
+ it 'is true when the parent is loaded' do
+ group.parent
+
+ expect(group.parent_loaded?).to be_truthy
+ end
+ end
+
+ describe '#route_loaded?' do
+ it 'is false when the route is not loaded' do
+ expect(group.route_loaded?).to be_falsey
+ end
+
+ it 'is true when the route is loaded' do
+ group.route
+
+ expect(group.route_loaded?).to be_truthy
+ end
+ end
+
describe '#full_path' do
it { expect(group.full_path).to eq(group.path) }
it { expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}") }
+
+ it 'hits the cache when not preloaded' do
+ forcibly_hit_cached_lookup(nested_group, :full_path)
+
+ expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}")
+ end
end
describe '#full_name' do
it { expect(group.full_name).to eq(group.name) }
it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") }
+
+ it 'hits the cache when not preloaded' do
+ forcibly_hit_cached_lookup(nested_group, :full_name)
+
+ expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}")
+ end
end
end
-RSpec.describe Project, 'Routable' do
- let_it_be(:project) { create(:project) }
+RSpec.describe Project, 'Routable', :with_clean_rails_cache do
+ let_it_be(:namespace) { create(:namespace) }
+ let_it_be(:project) { create(:project, namespace: namespace) }
it_behaves_like '.find_by_full_path' do
let_it_be(:record) { project }
@@ -192,10 +236,30 @@ RSpec.describe Project, 'Routable' do
end
describe '#full_path' do
- it { expect(project.full_path).to eq "#{project.namespace.full_path}/#{project.path}" }
+ it { expect(project.full_path).to eq "#{namespace.full_path}/#{project.path}" }
+
+ it 'hits the cache when not preloaded' do
+ forcibly_hit_cached_lookup(project, :full_path)
+
+ expect(project.full_path).to eq("#{namespace.full_path}/#{project.path}")
+ end
end
describe '#full_name' do
- it { expect(project.full_name).to eq "#{project.namespace.human_name} / #{project.name}" }
+ it { expect(project.full_name).to eq "#{namespace.human_name} / #{project.name}" }
+
+ it 'hits the cache when not preloaded' do
+ forcibly_hit_cached_lookup(project, :full_name)
+
+ expect(project.full_name).to eq("#{namespace.human_name} / #{project.name}")
+ end
end
end
+
+def forcibly_hit_cached_lookup(record, method)
+ stub_feature_flags(cached_route_lookups: true)
+ expect(record).to receive(:persisted?).and_return(true)
+ expect(record).to receive(:route_loaded?).and_return(false)
+ expect(record).to receive(:parent_loaded?).and_return(false)
+ expect(Gitlab::Cache).to receive(:fetch_once).with([record.cache_key, method]).and_call_original
+end
diff --git a/spec/models/concerns/sidebars/container_with_html_options_spec.rb b/spec/models/concerns/sidebars/container_with_html_options_spec.rb
deleted file mode 100644
index cc83fc84113..00000000000
--- a/spec/models/concerns/sidebars/container_with_html_options_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Sidebars::ContainerWithHtmlOptions do
- subject do
- Class.new do
- include Sidebars::ContainerWithHtmlOptions
-
- def title
- 'Foo'
- end
- end.new
- end
-
- describe '#container_html_options' do
- it 'includes by default aria-label attribute' do
- expect(subject.container_html_options).to eq(aria: { label: 'Foo' })
- end
- end
-end
diff --git a/spec/models/concerns/sidebars/positionable_list_spec.rb b/spec/models/concerns/sidebars/positionable_list_spec.rb
deleted file mode 100644
index 231aa5295dd..00000000000
--- a/spec/models/concerns/sidebars/positionable_list_spec.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Sidebars::PositionableList do
- subject do
- Class.new do
- include Sidebars::PositionableList
- end.new
- end
-
- describe '#add_element' do
- it 'adds the element to the last position of the list' do
- list = [1, 2]
-
- subject.add_element(list, 3)
-
- expect(list).to eq([1, 2, 3])
- end
- end
-
- describe '#insert_element_before' do
- let(:user) { build(:user) }
- let(:list) { [1, user] }
-
- it 'adds element before the specific element class' do
- subject.insert_element_before(list, User, 2)
-
- expect(list).to eq [1, 2, user]
- end
-
- context 'when reference element does not exist' do
- it 'adds the element to the top of the list' do
- subject.insert_element_before(list, Project, 2)
-
- expect(list).to eq [2, 1, user]
- end
- end
- end
-
- describe '#insert_element_after' do
- let(:user) { build(:user) }
- let(:list) { [1, user] }
-
- it 'adds element after the specific element class' do
- subject.insert_element_after(list, Integer, 2)
-
- expect(list).to eq [1, 2, user]
- end
-
- context 'when reference element does not exist' do
- it 'adds the element to the end of the list' do
- subject.insert_element_after(list, Project, 2)
-
- expect(list).to eq [1, user, 2]
- end
- end
- end
-end
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index 0ecefff3a97..abaae5b059a 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe ContainerRepository do
+ using RSpec::Parameterized::TableSyntax
+
let(:group) { create(:group, name: 'group') }
let(:project) { create(:project, path: 'test', group: group) }
@@ -29,18 +31,6 @@ RSpec.describe ContainerRepository do
end
end
- describe '.exists_by_path?' do
- it 'returns true for known container repository paths' do
- path = ContainerRegistry::Path.new("#{project.full_path}/#{repository.name}")
- expect(described_class.exists_by_path?(path)).to be_truthy
- end
-
- it 'returns false for unknown container repository paths' do
- path = ContainerRegistry::Path.new('you/dont/know/me')
- expect(described_class.exists_by_path?(path)).to be_falsey
- end
- end
-
describe '#tag' do
it 'has a test tag' do
expect(repository.tag('test')).not_to be_nil
@@ -359,6 +349,17 @@ RSpec.describe ContainerRepository do
it { is_expected.to contain_exactly(repository) }
end
+ describe '.expiration_policy_started_at_nil_or_before' do
+ let_it_be(:repository1) { create(:container_repository, expiration_policy_started_at: nil) }
+ let_it_be(:repository2) { create(:container_repository, expiration_policy_started_at: 1.day.ago) }
+ let_it_be(:repository3) { create(:container_repository, expiration_policy_started_at: 2.hours.ago) }
+ let_it_be(:repository4) { create(:container_repository, expiration_policy_started_at: 1.week.ago) }
+
+ subject { described_class.expiration_policy_started_at_nil_or_before(3.hours.ago) }
+
+ it { is_expected.to contain_exactly(repository1, repository2, repository4) }
+ end
+
describe '.waiting_for_cleanup' do
let_it_be(:repository_cleanup_scheduled) { create(:container_repository, :cleanup_scheduled) }
let_it_be(:repository_cleanup_unfinished) { create(:container_repository, :cleanup_unfinished) }
@@ -368,4 +369,74 @@ RSpec.describe ContainerRepository do
it { is_expected.to contain_exactly(repository_cleanup_scheduled, repository_cleanup_unfinished) }
end
+
+ describe '.exists_by_path?' do
+ it 'returns true for known container repository paths' do
+ path = ContainerRegistry::Path.new("#{project.full_path}/#{repository.name}")
+ expect(described_class.exists_by_path?(path)).to be_truthy
+ end
+
+ it 'returns false for unknown container repository paths' do
+ path = ContainerRegistry::Path.new('you/dont/know/me')
+ expect(described_class.exists_by_path?(path)).to be_falsey
+ end
+ end
+
+ describe '.with_enabled_policy' do
+ let_it_be(:repository) { create(:container_repository) }
+ let_it_be(:repository2) { create(:container_repository) }
+
+ subject { described_class.with_enabled_policy }
+
+ before do
+ repository.project.container_expiration_policy.update!(enabled: true)
+ end
+
+ it { is_expected.to eq([repository]) }
+ end
+
+ context 'with repositories' do
+ let_it_be_with_reload(:repository) { create(:container_repository, :cleanup_unscheduled) }
+ let_it_be(:other_repository) { create(:container_repository, :cleanup_unscheduled) }
+
+ let(:policy) { repository.project.container_expiration_policy }
+
+ before do
+ ContainerExpirationPolicy.update_all(enabled: true)
+ end
+
+ describe '.requiring_cleanup' do
+ subject { described_class.requiring_cleanup }
+
+ context 'with next_run_at in the future' do
+ before do
+ policy.update_column(:next_run_at, 10.minutes.from_now)
+ end
+
+ it { is_expected.to eq([]) }
+ end
+
+ context 'with next_run_at in the past' do
+ before do
+ policy.update_column(:next_run_at, 10.minutes.ago)
+ end
+
+ it { is_expected.to eq([repository]) }
+ end
+ end
+
+ describe '.with_unfinished_cleanup' do
+ subject { described_class.with_unfinished_cleanup }
+
+ it { is_expected.to eq([]) }
+
+ context 'with an unfinished repository' do
+ before do
+ repository.cleanup_unfinished!
+ end
+
+ it { is_expected.to eq([repository]) }
+ end
+ end
+ end
end
diff --git a/spec/models/context_commits_diff_spec.rb b/spec/models/context_commits_diff_spec.rb
new file mode 100644
index 00000000000..6e03ea2745e
--- /dev/null
+++ b/spec/models/context_commits_diff_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ContextCommitsDiff do
+ let_it_be(:sha1) { "33f3729a45c02fc67d00adb1b8bca394b0e761d9" }
+ let_it_be(:sha2) { "ae73cb07c9eeaf35924a10f713b364d32b2dd34f" }
+ let_it_be(:sha3) { "0b4bc9a49b562e85de7cc9e834518ea6828729b9" }
+ let_it_be(:merge_request) { create(:merge_request) }
+ let_it_be(:project) { merge_request.project }
+ let_it_be(:mrcc1) { create(:merge_request_context_commit, merge_request: merge_request, sha: sha1, committed_date: project.commit_by(oid: sha1).committed_date) }
+ let_it_be(:mrcc2) { create(:merge_request_context_commit, merge_request: merge_request, sha: sha2, committed_date: project.commit_by(oid: sha2).committed_date) }
+ let_it_be(:mrcc3) { create(:merge_request_context_commit, merge_request: merge_request, sha: sha3, committed_date: project.commit_by(oid: sha3).committed_date) }
+
+ subject { merge_request.context_commits_diff }
+
+ describe ".empty?" do
+ it 'checks if empty' do
+ expect(subject.empty?).to be(false)
+ end
+ end
+
+ describe '.commits_count' do
+ it 'reports commits count' do
+ expect(subject.commits_count).to be(3)
+ end
+ end
+
+ describe '.diffs' do
+ it 'returns instance of Gitlab::Diff::FileCollection::Compare' do
+ expect(subject.diffs).to be_a(Gitlab::Diff::FileCollection::Compare)
+ end
+
+ it 'returns all diffs between first and last commits' do
+ expect(subject.diffs.diff_files.size).to be(5)
+ end
+ end
+
+ describe '.raw_diffs' do
+ before do
+ allow(subject).to receive(:paths).and_return(["Gemfile.zip", "files/images/6049019_460s.jpg", "files/ruby/feature.rb"])
+ end
+
+ it 'returns instance of Gitlab::Git::DiffCollection' do
+ expect(subject.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
+ end
+
+ it 'returns only diff for files changed in the context commits' do
+ expect(subject.raw_diffs.size).to be(3)
+ end
+ end
+
+ describe '.diff_refs' do
+ it 'returns correct sha' do
+ expect(subject.diff_refs.head_sha).to eq(sha3)
+ expect(subject.diff_refs.base_sha).to eq("913c66a37b4a45b9769037c55c2d238bd0942d2e")
+ end
+ end
+end
diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb
index e34934d393a..4a8b671bab7 100644
--- a/spec/models/custom_emoji_spec.rb
+++ b/spec/models/custom_emoji_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe CustomEmoji do
new_emoji = build(:custom_emoji, name: old_emoji.name, namespace: old_emoji.namespace, group: group)
expect(new_emoji).not_to be_valid
- expect(new_emoji.errors.messages).to include(name: ["has already been taken"])
+ expect(new_emoji.errors.messages).to eq(creator: ["can't be blank"], name: ["has already been taken"])
end
it 'disallows non http and https file value' do
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index c9544569ad6..bcd237cbd38 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -72,6 +72,35 @@ RSpec.describe Deployment do
end
end
+ describe '.for_environment_name' do
+ subject { described_class.for_environment_name(project, environment_name) }
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:production) { create(:environment, :production, project: project) }
+ let_it_be(:staging) { create(:environment, :staging, project: project) }
+ let_it_be(:other_project) { create(:project, :repository) }
+ let_it_be(:other_production) { create(:environment, :production, project: other_project) }
+ let(:environment_name) { production.name }
+
+ context 'when deployment belongs to the environment' do
+ let!(:deployment) { create(:deployment, project: project, environment: production) }
+
+ it { is_expected.to eq([deployment]) }
+ end
+
+ context 'when deployment belongs to the same project but different environment name' do
+ let!(:deployment) { create(:deployment, project: project, environment: staging) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when deployment belongs to the same environment name but different project' do
+ let!(:deployment) { create(:deployment, project: other_project, environment: other_production) }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
describe '.success' do
subject { described_class.success }
@@ -107,11 +136,13 @@ RSpec.describe Deployment do
end
end
- it 'executes Deployments::ExecuteHooksWorker asynchronously' do
- expect(Deployments::ExecuteHooksWorker)
- .to receive(:perform_async).with(deployment.id)
+ it 'executes Deployments::HooksWorker asynchronously' do
+ freeze_time do
+ expect(Deployments::HooksWorker)
+ .to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current)
- deployment.run!
+ deployment.run!
+ end
end
it 'executes Deployments::DropOlderDeploymentsWorker asynchronously' do
@@ -141,11 +172,13 @@ RSpec.describe Deployment do
deployment.succeed!
end
- it 'executes Deployments::ExecuteHooksWorker asynchronously' do
- expect(Deployments::ExecuteHooksWorker)
- .to receive(:perform_async).with(deployment.id)
+ it 'executes Deployments::HooksWorker asynchronously' do
+ freeze_time do
+ expect(Deployments::HooksWorker)
+ .to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current)
- deployment.succeed!
+ deployment.succeed!
+ end
end
end
@@ -168,11 +201,13 @@ RSpec.describe Deployment do
deployment.drop!
end
- it 'executes Deployments::ExecuteHooksWorker asynchronously' do
- expect(Deployments::ExecuteHooksWorker)
- .to receive(:perform_async).with(deployment.id)
+ it 'executes Deployments::HooksWorker asynchronously' do
+ freeze_time do
+ expect(Deployments::HooksWorker)
+ .to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current)
- deployment.drop!
+ deployment.drop!
+ end
end
end
@@ -195,11 +230,13 @@ RSpec.describe Deployment do
deployment.cancel!
end
- it 'executes Deployments::ExecuteHooksWorker asynchronously' do
- expect(Deployments::ExecuteHooksWorker)
- .to receive(:perform_async).with(deployment.id)
+ it 'executes Deployments::HooksWorker asynchronously' do
+ freeze_time do
+ expect(Deployments::HooksWorker)
+ .to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current)
- deployment.cancel!
+ deployment.cancel!
+ end
end
end
@@ -220,11 +257,13 @@ RSpec.describe Deployment do
deployment.skip!
end
- it 'does not execute Deployments::ExecuteHooksWorker' do
- expect(Deployments::ExecuteHooksWorker)
- .not_to receive(:perform_async).with(deployment.id)
+ it 'does not execute Deployments::HooksWorker' do
+ freeze_time do
+ expect(Deployments::HooksWorker)
+ .not_to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current)
- deployment.skip!
+ deployment.skip!
+ end
end
end
@@ -714,7 +753,7 @@ RSpec.describe Deployment do
it 'schedules workers when finishing a deploy' 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)
deploy.update_status('success')
end
diff --git a/spec/models/design_management/design_spec.rb b/spec/models/design_management/design_spec.rb
index 674d2fc420d..f2ce5e42eaf 100644
--- a/spec/models/design_management/design_spec.rb
+++ b/spec/models/design_management/design_spec.rb
@@ -512,7 +512,7 @@ RSpec.describe DesignManagement::Design do
end
describe '#to_reference' do
- let(:namespace) { build(:namespace, path: 'sample-namespace') }
+ let(:namespace) { build(:namespace, id: non_existing_record_id, path: 'sample-namespace') }
let(:project) { build(:project, name: 'sample-project', namespace: namespace) }
let(:group) { create(:group, name: 'Group', path: 'sample-group') }
let(:issue) { build(:issue, iid: 1, project: project) }
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
index 62f2a53ab3c..cd0938682db 100644
--- a/spec/models/email_spec.rb
+++ b/spec/models/email_spec.rb
@@ -44,12 +44,11 @@ RSpec.describe Email do
end
end
- describe 'delegation' do
- let(:user) { create(:user) }
-
- it 'delegates to :user' do
- expect(build(:email, user: user).username).to eq user.username
- end
+ describe 'delegations' do
+ it { is_expected.to delegate_method(:can?).to(:user) }
+ it { is_expected.to delegate_method(:username).to(:user) }
+ it { is_expected.to delegate_method(:pending_invitations).to(:user) }
+ it { is_expected.to delegate_method(:accept_pending_invitations!).to(:user) }
end
describe 'Devise emails' do
diff --git a/spec/models/external_pull_request_spec.rb b/spec/models/external_pull_request_spec.rb
index e0822fc177a..bac2c369d7d 100644
--- a/spec/models/external_pull_request_spec.rb
+++ b/spec/models/external_pull_request_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe ExternalPullRequest do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project, :repository) }
+
let(:source_branch) { 'the-branch' }
let(:status) { :open }
@@ -217,4 +218,18 @@ RSpec.describe ExternalPullRequest do
expect(pull_request).not_to be_from_fork
end
end
+
+ describe '#modified_paths' do
+ let(:pull_request) do
+ build(:external_pull_request, project: project, target_sha: '281d3a7', source_sha: '498214d')
+ end
+
+ subject(:modified_paths) { pull_request.modified_paths }
+
+ it 'returns modified paths' do
+ expect(modified_paths).to eq ['bar/branch-test.txt',
+ 'files/js/commit.coffee',
+ 'with space/README.md']
+ end
+ end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 2f82d8a0bbe..5cc5c4d86d6 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Group do
it { is_expected.to have_many(:container_repositories) }
it { is_expected.to have_many(:milestones) }
it { is_expected.to have_many(:group_deploy_keys) }
- it { is_expected.to have_many(:services) }
+ it { is_expected.to have_many(:integrations) }
it { is_expected.to have_one(:dependency_proxy_setting) }
it { is_expected.to have_many(:dependency_proxy_blobs) }
it { is_expected.to have_many(:dependency_proxy_manifests) }
@@ -395,18 +395,94 @@ RSpec.describe Group do
end
end
- context 'assigning a new parent' do
- let!(:old_parent) { create(:group) }
- let!(:new_parent) { create(:group) }
+ context 'assign a new parent' do
let!(:group) { create(:group, parent: old_parent) }
+ let(:recorded_queries) { ActiveRecord::QueryRecorder.new }
+
+ subject do
+ recorded_queries.record do
+ group.update(parent: new_parent)
+ end
+ end
before do
- group.update(parent: new_parent)
+ subject
reload_models(old_parent, new_parent, group)
end
- it 'updates traversal_ids' do
- expect(group.traversal_ids).to eq [new_parent.id, group.id]
+ context 'within the same hierarchy' do
+ let!(:root) { create(:group).reload }
+ let!(:old_parent) { create(:group, parent: root) }
+ let!(:new_parent) { create(:group, parent: root) }
+
+ it 'updates traversal_ids' do
+ expect(group.traversal_ids).to eq [root.id, new_parent.id, group.id]
+ end
+
+ it_behaves_like 'hierarchy with traversal_ids'
+ it_behaves_like 'locked row' do
+ let(:row) { root }
+ end
+ end
+
+ context 'to another hierarchy' do
+ let!(:old_parent) { create(:group) }
+ let!(:new_parent) { create(:group) }
+ let!(:group) { create(:group, parent: old_parent) }
+
+ it 'updates traversal_ids' do
+ expect(group.traversal_ids).to eq [new_parent.id, group.id]
+ end
+
+ it_behaves_like 'locked rows' do
+ let(:rows) { [old_parent, new_parent] }
+ end
+
+ context 'old hierarchy' do
+ let(:root) { old_parent.root_ancestor }
+
+ it_behaves_like 'hierarchy with traversal_ids'
+ end
+
+ context 'new hierarchy' do
+ let(:root) { new_parent.root_ancestor }
+
+ it_behaves_like 'hierarchy with traversal_ids'
+ end
+ end
+
+ context 'from being a root ancestor' do
+ let!(:old_parent) { nil }
+ let!(:new_parent) { create(:group) }
+
+ it 'updates traversal_ids' do
+ expect(group.traversal_ids).to eq [new_parent.id, group.id]
+ end
+
+ it_behaves_like 'locked rows' do
+ let(:rows) { [group, new_parent] }
+ end
+
+ it_behaves_like 'hierarchy with traversal_ids' do
+ let(:root) { new_parent }
+ end
+ end
+
+ context 'to being a root ancestor' do
+ let!(:old_parent) { create(:group) }
+ let!(:new_parent) { nil }
+
+ it 'updates traversal_ids' do
+ expect(group.traversal_ids).to eq [group.id]
+ end
+
+ it_behaves_like 'locked rows' do
+ let(:rows) { [old_parent, group] }
+ end
+
+ it_behaves_like 'hierarchy with traversal_ids' do
+ let(:root) { group }
+ end
end
end
@@ -427,6 +503,58 @@ RSpec.describe Group do
end
end
+ context 'traversal queries' do
+ let_it_be(:group, reload: true) { create(:group, :nested) }
+
+ context 'recursive' do
+ before do
+ stub_feature_flags(use_traversal_ids: false)
+ end
+
+ it_behaves_like 'namespace traversal'
+
+ describe '#self_and_descendants' do
+ it { expect(group.self_and_descendants.to_sql).not_to include 'traversal_ids @>' }
+ end
+
+ describe '#descendants' do
+ it { expect(group.descendants.to_sql).not_to include 'traversal_ids @>' }
+ end
+
+ describe '#ancestors' do
+ it { expect(group.ancestors.to_sql).not_to include 'traversal_ids <@' }
+ end
+ end
+
+ context 'linear' do
+ it_behaves_like 'namespace traversal'
+
+ describe '#self_and_descendants' do
+ it { expect(group.self_and_descendants.to_sql).to include 'traversal_ids @>' }
+ end
+
+ describe '#descendants' do
+ it { expect(group.descendants.to_sql).to include 'traversal_ids @>' }
+ end
+
+ describe '#ancestors' do
+ it { expect(group.ancestors.to_sql).to include "\"namespaces\".\"id\" = #{group.parent_id}" }
+
+ it 'hierarchy order' do
+ expect(group.ancestors(hierarchy_order: :asc).to_sql).to include 'ORDER BY "depth" ASC'
+ end
+
+ context 'ancestor linear queries feature flag disabled' do
+ before do
+ stub_feature_flags(use_traversal_ids_for_ancestors: false)
+ end
+
+ it { expect(group.ancestors.to_sql).not_to include 'traversal_ids <@' }
+ end
+ end
+ end
+ end
+
describe '.without_integration' do
let(:another_group) { create(:group) }
let(:instance_integration) { build(:jira_service, :instance) }
@@ -504,6 +632,16 @@ RSpec.describe Group do
it { is_expected.to match_array([private_group, internal_group]) }
end
+ describe 'with_onboarding_progress' do
+ subject { described_class.with_onboarding_progress }
+
+ it 'joins onboarding_progress' do
+ create(:onboarding_progress, namespace: group)
+
+ expect(subject).to eq([group])
+ end
+ end
+
describe 'for_authorized_group_members' do
let_it_be(:group_member1) { create(:group_member, source: private_group, user_id: user1.id, access_level: Gitlab::Access::OWNER) }
@@ -579,7 +717,9 @@ RSpec.describe Group do
it "is false if avatar is html page" do
group.update_attribute(:avatar, 'uploads/avatar.html')
- expect(group.avatar_type).to eq(["file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp"])
+ group.avatar_type
+
+ expect(group.errors.added?(:avatar, "file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp")).to be true
end
end
@@ -953,140 +1093,167 @@ RSpec.describe Group do
it { expect(subject.parent).to be_kind_of(described_class) }
end
- describe '#max_member_access_for_user' do
- context 'group shared with another group' do
- let(:parent_group_user) { create(:user) }
- let(:group_user) { create(:user) }
- let(:child_group_user) { create(:user) }
-
- let_it_be(:group_parent) { create(:group, :private) }
- let_it_be(:group) { create(:group, :private, parent: group_parent) }
- let_it_be(:group_child) { create(:group, :private, parent: group) }
-
- let_it_be(:shared_group_parent) { create(:group, :private) }
- let_it_be(:shared_group) { create(:group, :private, parent: shared_group_parent) }
- let_it_be(:shared_group_child) { create(:group, :private, parent: shared_group) }
-
- before do
- group_parent.add_owner(parent_group_user)
- group.add_owner(group_user)
- group_child.add_owner(child_group_user)
-
- create(:group_group_link, { shared_with_group: group,
- shared_group: shared_group,
- group_access: GroupMember::DEVELOPER })
- end
+ context "with member access" do
+ let_it_be(:group_user) { create(:user) }
+ describe '#max_member_access_for_user' do
context 'with user in the group' do
- let(:user) { group_user }
+ before do
+ group.add_owner(group_user)
+ end
it 'returns correct access level' do
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER)
+ expect(group.max_member_access_for_user(group_user)).to eq(Gitlab::Access::OWNER)
end
+ end
- context 'with lower group access level than max access level for share' do
- let(:user) { create(:user) }
+ context 'when user is nil' do
+ it 'returns NO_ACCESS' do
+ expect(group.max_member_access_for_user(nil)).to eq(Gitlab::Access::NO_ACCESS)
+ end
+ end
- it 'returns correct access level' do
- group.add_reporter(user)
+ context 'evaluating admin access level' do
+ let_it_be(:admin) { create(:admin) }
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it 'returns OWNER by default' do
+ expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::OWNER)
end
end
- end
- context 'with user in the parent group' do
- let(:user) { parent_group_user }
+ context 'when admin mode is disabled' do
+ it 'returns NO_ACCESS' do
+ expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::NO_ACCESS)
+ end
+ end
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ it 'returns NO_ACCESS when only concrete membership should be considered' do
+ expect(group.max_member_access_for_user(admin, only_concrete_membership: true))
+ .to eq(Gitlab::Access::NO_ACCESS)
end
end
- context 'with user in the child group' do
- let(:user) { child_group_user }
+ context 'when max_access_for_group is set' do
+ let(:max_member_access) { 111 }
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ before do
+ group_user.max_access_for_group[group.id] = max_member_access
end
- end
-
- context 'unrelated project owner' do
- let(:common_id) { [Project.maximum(:id).to_i, Namespace.maximum(:id).to_i].max + 999 }
- let!(:group) { create(:group, id: common_id) }
- let!(:unrelated_project) { create(:project, id: common_id) }
- let(:user) { unrelated_project.owner }
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ it 'uses the cached value' do
+ expect(group.max_member_access_for_user(group_user)).to eq(max_member_access)
end
end
+ end
- context 'user without accepted access request' do
- let!(:user) { create(:user) }
+ describe '#max_member_access' do
+ context 'group shared with another group' do
+ let_it_be(:parent_group_user) { create(:user) }
+ let_it_be(:child_group_user) { create(:user) }
+
+ let_it_be(:group_parent) { create(:group, :private) }
+ let_it_be(:group) { create(:group, :private, parent: group_parent) }
+ let_it_be(:group_child) { create(:group, :private, parent: group) }
+
+ let_it_be(:shared_group_parent) { create(:group, :private) }
+ let_it_be(:shared_group) { create(:group, :private, parent: shared_group_parent) }
+ let_it_be(:shared_group_child) { create(:group, :private, parent: shared_group) }
before do
- create(:group_member, :developer, :access_request, user: user, group: group)
+ group_parent.add_owner(parent_group_user)
+ group.add_owner(group_user)
+ group_child.add_owner(child_group_user)
+
+ create(:group_group_link, { shared_with_group: group,
+ shared_group: shared_group,
+ group_access: GroupMember::DEVELOPER })
end
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ context 'with user in the group' do
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access(group_user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access(group_user)).to eq(Gitlab::Access::DEVELOPER)
+ expect(shared_group_child.max_member_access(group_user)).to eq(Gitlab::Access::DEVELOPER)
+ end
+
+ context 'with lower group access level than max access level for share' do
+ let(:user) { create(:user) }
+
+ it 'returns correct access level' do
+ group.add_reporter(user)
+
+ expect(shared_group_parent.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access(user)).to eq(Gitlab::Access::REPORTER)
+ expect(shared_group_child.max_member_access(user)).to eq(Gitlab::Access::REPORTER)
+ end
+ end
end
- end
- end
- context 'multiple groups shared with group' do
- let(:user) { create(:user) }
- let(:group) { create(:group, :private) }
- let(:shared_group_parent) { create(:group, :private) }
- let(:shared_group) { create(:group, :private, parent: shared_group_parent) }
+ context 'with user in the parent group' do
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS)
+ end
+ end
- before do
- group.add_owner(user)
+ context 'with user in the child group' do
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access(child_group_user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access(child_group_user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access(child_group_user)).to eq(Gitlab::Access::NO_ACCESS)
+ end
+ end
- create(:group_group_link, { shared_with_group: group,
- shared_group: shared_group,
- group_access: GroupMember::DEVELOPER })
- create(:group_group_link, { shared_with_group: group,
- shared_group: shared_group_parent,
- group_access: GroupMember::MAINTAINER })
- end
+ context 'unrelated project owner' do
+ let(:common_id) { [Project.maximum(:id).to_i, Namespace.maximum(:id).to_i].max + 999 }
+ let!(:group) { create(:group, id: common_id) }
+ let!(:unrelated_project) { create(:project, id: common_id) }
+ let(:user) { unrelated_project.owner }
- it 'returns correct access level' do
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::MAINTAINER)
- end
- end
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS)
+ end
+ end
+
+ context 'user without accepted access request' do
+ let!(:user) { create(:user) }
- context 'evaluating admin access level' do
- let_it_be(:admin) { create(:admin) }
+ before do
+ create(:group_member, :developer, :access_request, user: user, group: group)
+ end
- context 'when admin mode is enabled', :enable_admin_mode do
- it 'returns OWNER by default' do
- expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::OWNER)
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS)
+ end
end
end
- context 'when admin mode is disabled' do
- it 'returns NO_ACCESS' do
- expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::NO_ACCESS)
+ context 'multiple groups shared with group' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group, :private) }
+ let(:shared_group_parent) { create(:group, :private) }
+ let(:shared_group) { create(:group, :private, parent: shared_group_parent) }
+
+ before do
+ group.add_owner(user)
+
+ create(:group_group_link, { shared_with_group: group,
+ shared_group: shared_group,
+ group_access: GroupMember::DEVELOPER })
+ create(:group_group_link, { shared_with_group: group,
+ shared_group: shared_group_parent,
+ group_access: GroupMember::MAINTAINER })
end
- end
- it 'returns NO_ACCESS when only concrete membership should be considered' do
- expect(group.max_member_access_for_user(admin, only_concrete_membership: true))
- .to eq(Gitlab::Access::NO_ACCESS)
+ it 'returns correct access level' do
+ expect(shared_group.max_member_access(user)).to eq(Gitlab::Access::MAINTAINER)
+ end
end
end
end
@@ -1118,7 +1285,7 @@ RSpec.describe Group do
end
end
- describe '#members_with_parents' do
+ shared_examples_for 'members_with_parents' do
let!(:group) { create(:group, :nested) }
let!(:maintainer) { group.parent.add_user(create(:user), GroupMember::MAINTAINER) }
let!(:developer) { group.add_user(create(:user), GroupMember::DEVELOPER) }
@@ -1142,6 +1309,50 @@ RSpec.describe Group do
end
end
+ describe '#members_with_parents' do
+ it_behaves_like 'members_with_parents'
+ end
+
+ describe '#authorizable_members_with_parents' do
+ let(:group) { create(:group) }
+
+ it_behaves_like 'members_with_parents'
+
+ context 'members with associated user but also having invite_token' do
+ let!(:member) { create(:group_member, :developer, :invited, user: create(:user), group: group) }
+
+ it 'includes such members in the result' do
+ expect(group.authorizable_members_with_parents).to include(member)
+ end
+ end
+
+ context 'invited members' do
+ let!(:member) { create(:group_member, :developer, :invited, group: group) }
+
+ it 'does not include such members in the result' do
+ expect(group.authorizable_members_with_parents).not_to include(member)
+ end
+ end
+
+ context 'members from group shares' do
+ let(:shared_group) { group }
+ let(:shared_with_group) { create(:group) }
+
+ before do
+ create(:group_group_link, shared_group: shared_group, shared_with_group: shared_with_group)
+ end
+
+ context 'an invited member that is part of the shared_with_group' do
+ let!(:member) { create(:group_member, :developer, :invited, group: shared_with_group) }
+
+ it 'does not include such members in the result' do
+ expect(shared_group.authorizable_members_with_parents).not_to(
+ include(member))
+ end
+ end
+ end
+ end
+
describe '#members_from_self_and_ancestors_with_effective_access_level' do
let!(:group_parent) { create(:group, :private) }
let!(:group) { create(:group, :private, parent: group_parent) }
@@ -1769,13 +1980,35 @@ RSpec.describe Group do
allow(project).to receive(:protected_for?).with('ref').and_return(true)
end
- it 'returns all variables belong to the group and parent groups' do
- expected_array1 = [protected_variable, ci_variable]
- expected_array2 = [variable_child, variable_child_2, variable_child_3]
- got_array = group_child_3.ci_variables_for('ref', project).to_a
+ context 'traversal queries' do
+ shared_examples 'correct ancestor order' do
+ it 'returns all variables belong to the group and parent groups' do
+ expected_array1 = [protected_variable, ci_variable]
+ expected_array2 = [variable_child, variable_child_2, variable_child_3]
+ got_array = group_child_3.ci_variables_for('ref', project).to_a
+
+ expect(got_array.shift(2)).to contain_exactly(*expected_array1)
+ expect(got_array).to eq(expected_array2)
+ end
+ end
+
+ context 'recursive' do
+ before do
+ stub_feature_flags(use_traversal_ids: false)
+ end
- expect(got_array.shift(2)).to contain_exactly(*expected_array1)
- expect(got_array).to eq(expected_array2)
+ include_examples 'correct ancestor order'
+ end
+
+ context 'linear' do
+ before do
+ stub_feature_flags(use_traversal_ids: true)
+
+ group_child_3.reload # make sure traversal_ids are reloaded
+ end
+
+ include_examples 'correct ancestor order'
+ end
end
end
end
@@ -2012,22 +2245,31 @@ RSpec.describe Group do
end
describe '#access_request_approvers_to_be_notified' do
- it 'returns a maximum of ten, active, non_requested owners of the group in recent_sign_in descending order' do
- group = create(:group, :public)
+ let_it_be(:group) { create(:group, :public) }
+ it 'returns a maximum of ten owners of the group in recent_sign_in descending order' do
users = create_list(:user, 12, :with_sign_ins)
active_owners = users.map do |user|
create(:group_member, :owner, group: group, user: user)
end
- create(:group_member, :owner, :blocked, group: group)
- create(:group_member, :maintainer, group: group)
- create(:group_member, :access_request, :owner, group: group)
-
- active_owners_in_recent_sign_in_desc_order = group.members_and_requesters.where(id: active_owners).order_recent_sign_in.limit(10)
+ active_owners_in_recent_sign_in_desc_order = group.members_and_requesters
+ .id_in(active_owners)
+ .order_recent_sign_in.limit(10)
expect(group.access_request_approvers_to_be_notified).to eq(active_owners_in_recent_sign_in_desc_order)
end
+
+ it 'returns active, non_invited, non_requested owners of the group' do
+ owner = create(:group_member, :owner, source: group)
+
+ create(:group_member, :maintainer, group: group)
+ create(:group_member, :owner, :invited, group: group)
+ create(:group_member, :owner, :access_request, group: group)
+ create(:group_member, :owner, :blocked, group: group)
+
+ expect(group.access_request_approvers_to_be_notified.to_a).to eq([owner])
+ end
end
describe '.groups_including_descendants_by' do
@@ -2214,17 +2456,17 @@ RSpec.describe Group do
end
describe "#default_branch_name" do
- context "group.namespace_settings does not have a default branch name" do
+ context "when group.namespace_settings does not have a default branch name" do
it "returns nil" do
expect(group.default_branch_name).to be_nil
end
end
- context "group.namespace_settings has a default branch name" do
+ context "when group.namespace_settings has a default branch name" do
let(:example_branch_name) { "example_branch_name" }
before do
- expect(group.namespace_settings)
+ allow(group.namespace_settings)
.to receive(:default_branch_name)
.and_return(example_branch_name)
end
@@ -2361,4 +2603,20 @@ RSpec.describe Group do
it { is_expected.to eq(Set.new([child_1.id])) }
end
+
+ describe '#to_ability_name' do
+ it 'returns group' do
+ group = build(:group)
+
+ expect(group.to_ability_name).to eq('group')
+ end
+ end
+
+ describe '#activity_path' do
+ it 'returns the group activity_path' do
+ expected_path = "/groups/#{group.name}/-/activity"
+
+ expect(group.activity_path).to eq(expected_path)
+ end
+ end
end
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index 69fbc4c3b4f..88149465232 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -30,4 +30,13 @@ RSpec.describe ProjectHook do
expect(described_class.tag_push_hooks).to eq([hook])
end
end
+
+ describe '#rate_limit' do
+ let_it_be(:hook) { create(:project_hook) }
+ let_it_be(:plan_limits) { create(:plan_limits, :default_plan, web_hook_calls: 100) }
+
+ it 'returns the default limit' do
+ expect(hook.rate_limit).to be(100)
+ end
+ end
end
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index f7045d7ac5e..651716c3280 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -4,11 +4,11 @@ require 'spec_helper'
RSpec.describe ServiceHook do
describe 'associations' do
- it { is_expected.to belong_to :service }
+ it { is_expected.to belong_to :integration }
end
describe 'validations' do
- it { is_expected.to validate_presence_of(:service) }
+ it { is_expected.to validate_presence_of(:integration) }
end
describe 'execute' do
@@ -22,4 +22,12 @@ RSpec.describe ServiceHook do
hook.execute(data)
end
end
+
+ describe '#rate_limit' do
+ let(:hook) { build(:service_hook) }
+
+ it 'returns nil' do
+ expect(hook.rate_limit).to be_nil
+ end
+ end
end
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index 02e630cbf27..a72034f1ac5 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -169,4 +169,12 @@ RSpec.describe SystemHook do
hook.async_execute(data, hook_name)
end
end
+
+ describe '#rate_limit' do
+ let(:hook) { build(:system_hook) }
+
+ it 'returns nil' do
+ expect(hook.rate_limit).to be_nil
+ end
+ end
end
diff --git a/spec/models/hooks/web_hook_log_archived_spec.rb b/spec/models/hooks/web_hook_log_archived_spec.rb
new file mode 100644
index 00000000000..ac726dbaf4f
--- /dev/null
+++ b/spec/models/hooks/web_hook_log_archived_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WebHookLogArchived do
+ let(:source_table) { WebHookLog }
+ let(:destination_table) { described_class }
+
+ it 'has the same columns as the source table' do
+ column_names_from_source_table = column_names(source_table)
+ column_names_from_destination_table = column_names(destination_table)
+
+ expect(column_names_from_destination_table).to match_array(column_names_from_source_table)
+ end
+
+ it 'has the same null constraints as the source table' do
+ constraints_from_source_table = null_constraints(source_table)
+ constraints_from_destination_table = null_constraints(destination_table)
+
+ expect(constraints_from_destination_table.to_a).to match_array(constraints_from_source_table.to_a)
+ end
+
+ it 'inserts the same record as the one in the source table', :aggregate_failures do
+ expect { create(:web_hook_log) }.to change { destination_table.count }.by(1)
+
+ event_from_source_table = source_table.connection.select_one(
+ "SELECT * FROM #{source_table.table_name} ORDER BY created_at desc LIMIT 1"
+ )
+ event_from_destination_table = destination_table.connection.select_one(
+ "SELECT * FROM #{destination_table.table_name} ORDER BY created_at desc LIMIT 1"
+ )
+
+ expect(event_from_destination_table).to eq(event_from_source_table)
+ end
+
+ def column_names(table)
+ table.connection.select_all(<<~SQL)
+ SELECT c.column_name
+ FROM information_schema.columns c
+ WHERE c.table_name = '#{table.table_name}'
+ SQL
+ end
+
+ def null_constraints(table)
+ table.connection.select_all(<<~SQL)
+ SELECT c.column_name, c.is_nullable
+ FROM information_schema.columns c
+ WHERE c.table_name = '#{table.table_name}'
+ AND c.column_name != 'created_at'
+ SQL
+ end
+end
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index 413e69fb071..b528dbedd2c 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -3,7 +3,15 @@
require 'spec_helper'
RSpec.describe WebHook do
- let(:hook) { build(:project_hook) }
+ include AfterNextHelpers
+
+ let_it_be(:project) { create(:project) }
+
+ let(:hook) { build(:project_hook, project: project) }
+
+ around do |example|
+ freeze_time { example.run }
+ end
describe 'associations' do
it { is_expected.to have_many(:web_hook_logs) }
@@ -69,18 +77,30 @@ RSpec.describe WebHook do
let(:data) { { key: 'value' } }
let(:hook_name) { 'project hook' }
- before do
- expect(WebHookService).to receive(:new).with(hook, data, hook_name).and_call_original
+ it '#execute' do
+ expect_next(WebHookService).to receive(:execute)
+
+ hook.execute(data, hook_name)
end
- it '#execute' do
- expect_any_instance_of(WebHookService).to receive(:execute)
+ it 'does not execute non-executable hooks' do
+ hook.update!(disabled_until: 1.day.from_now)
+
+ expect(WebHookService).not_to receive(:new)
hook.execute(data, hook_name)
end
it '#async_execute' do
- expect_any_instance_of(WebHookService).to receive(:async_execute)
+ expect_next(WebHookService).to receive(:async_execute)
+
+ hook.async_execute(data, hook_name)
+ end
+
+ it 'does not async execute non-executable hooks' do
+ hook.update!(disabled_until: 1.day.from_now)
+
+ expect(WebHookService).not_to receive(:new)
hook.async_execute(data, hook_name)
end
@@ -94,4 +114,170 @@ RSpec.describe WebHook do
expect { web_hook.destroy! }.to change(web_hook.web_hook_logs, :count).by(-3)
end
end
+
+ describe '.executable' do
+ let(:not_executable) do
+ [
+ [0, Time.current],
+ [0, 1.minute.from_now],
+ [1, 1.minute.from_now],
+ [3, 1.minute.from_now],
+ [4, nil],
+ [4, 1.day.ago],
+ [4, 1.minute.from_now]
+ ].map do |(recent_failures, disabled_until)|
+ create(:project_hook, project: project, recent_failures: recent_failures, disabled_until: disabled_until)
+ end
+ end
+
+ let(:executables) do
+ [
+ [0, nil],
+ [0, 1.day.ago],
+ [1, nil],
+ [1, 1.day.ago],
+ [3, nil],
+ [3, 1.day.ago]
+ ].map do |(recent_failures, disabled_until)|
+ create(:project_hook, project: project, recent_failures: recent_failures, disabled_until: disabled_until)
+ end
+ end
+
+ it 'finds the correct set of project hooks' do
+ expect(described_class.where(project_id: project.id).executable).to match_array executables
+ end
+
+ context 'when the feature flag is not enabled' do
+ before do
+ stub_feature_flags(web_hooks_disable_failed: false)
+ end
+
+ it 'is the same as all' do
+ expect(described_class.where(project_id: project.id).executable).to match_array(executables + not_executable)
+ end
+ end
+ end
+
+ describe '#executable?' do
+ let(:web_hook) { create(:project_hook, project: project) }
+
+ where(:recent_failures, :not_until, :executable) do
+ [
+ [0, :not_set, true],
+ [0, :past, true],
+ [0, :future, false],
+ [0, :now, false],
+ [1, :not_set, true],
+ [1, :past, true],
+ [1, :future, false],
+ [3, :not_set, true],
+ [3, :past, true],
+ [3, :future, false],
+ [4, :not_set, false],
+ [4, :past, false],
+ [4, :future, false]
+ ]
+ end
+
+ with_them do
+ # Phasing means we cannot put these values in the where block,
+ # which is not subject to the frozen time context.
+ let(:disabled_until) do
+ case not_until
+ when :not_set
+ nil
+ when :past
+ 1.minute.ago
+ when :future
+ 1.minute.from_now
+ when :now
+ Time.current
+ end
+ end
+
+ before do
+ web_hook.update!(recent_failures: recent_failures, disabled_until: disabled_until)
+ end
+
+ it 'has the correct state' do
+ expect(web_hook.executable?).to eq(executable)
+ end
+
+ context 'when the feature flag is enabled for a project' do
+ before do
+ stub_feature_flags(web_hooks_disable_failed: project)
+ end
+
+ it 'has the expected value' do
+ expect(web_hook.executable?).to eq(executable)
+ end
+ end
+
+ context 'when the feature flag is not enabled' do
+ before do
+ stub_feature_flags(web_hooks_disable_failed: false)
+ end
+
+ it 'is executable' do
+ expect(web_hook).to be_executable
+ end
+ end
+ end
+ end
+
+ describe '#next_backoff' do
+ context 'when there was no last backoff' do
+ before do
+ hook.backoff_count = 0
+ end
+
+ it 'is 10 minutes' do
+ expect(hook.next_backoff).to eq(described_class::INITIAL_BACKOFF)
+ end
+ end
+
+ context 'when we have backed off once' do
+ before do
+ hook.backoff_count = 1
+ end
+
+ it 'is twice the initial value' do
+ expect(hook.next_backoff).to eq(20.minutes)
+ end
+ end
+
+ context 'when we have backed off 3 times' do
+ before do
+ hook.backoff_count = 3
+ end
+
+ it 'grows exponentially' do
+ expect(hook.next_backoff).to eq(80.minutes)
+ end
+ end
+
+ context 'when the previous backoff was large' do
+ before do
+ hook.backoff_count = 8 # last value before MAX_BACKOFF
+ end
+
+ it 'does not exceed the max backoff value' do
+ expect(hook.next_backoff).to eq(described_class::MAX_BACKOFF)
+ end
+ end
+ end
+
+ describe '#enable!' do
+ it 'makes a hook executable' do
+ hook.recent_failures = 1000
+
+ expect { hook.enable! }.to change(hook, :executable?).from(false).to(true)
+ end
+ end
+
+ describe '#disable!' do
+ it 'disables a hook' do
+ expect { hook.disable! }.to change(hook, :executable?).from(true).to(false)
+ end
+ end
end
diff --git a/spec/models/instance_metadata/kas_spec.rb b/spec/models/instance_metadata/kas_spec.rb
new file mode 100644
index 00000000000..f8cc34fa8d3
--- /dev/null
+++ b/spec/models/instance_metadata/kas_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::InstanceMetadata::Kas do
+ it 'has InstanceMetadataPolicy as declarative policy' do
+ expect(described_class.declarative_policy_class).to eq("InstanceMetadataPolicy")
+ end
+
+ context 'when KAS is enabled' do
+ it 'has the correct properties' do
+ allow(Gitlab::Kas).to receive(:enabled?).and_return(true)
+
+ expect(subject).to have_attributes(
+ enabled: Gitlab::Kas.enabled?,
+ version: Gitlab::Kas.version,
+ external_url: Gitlab::Kas.external_url
+ )
+ end
+ end
+
+ context 'when KAS is disabled' do
+ it 'has the correct properties' do
+ allow(Gitlab::Kas).to receive(:enabled?).and_return(false)
+
+ expect(subject).to have_attributes(
+ enabled: Gitlab::Kas.enabled?,
+ version: nil,
+ external_url: nil
+ )
+ end
+ end
+end
diff --git a/spec/models/instance_metadata_spec.rb b/spec/models/instance_metadata_spec.rb
index 1835dc8a9af..e3a9167620b 100644
--- a/spec/models/instance_metadata_spec.rb
+++ b/spec/models/instance_metadata_spec.rb
@@ -6,7 +6,8 @@ RSpec.describe InstanceMetadata do
it 'has the correct properties' do
expect(subject).to have_attributes(
version: Gitlab::VERSION,
- revision: Gitlab.revision
+ revision: Gitlab.revision,
+ kas: kind_of(::InstanceMetadata::Kas)
)
end
end
diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb
index 781e2aece56..77b3778122a 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/integration_spec.rb
@@ -3,31 +3,945 @@
require 'spec_helper'
RSpec.describe Integration do
- let_it_be(:project_1) { create(:project) }
- let_it_be(:project_2) { create(:project) }
- let_it_be(:project_3) { create(:project) }
- let_it_be(:project_4) { create(:project) }
- let_it_be(:instance_integration) { create(:jira_service, :instance) }
+ using RSpec::Parameterized::TableSyntax
- before do
- create(:jira_service, project: project_1, inherit_from_id: instance_integration.id)
- create(:jira_service, project: project_2, inherit_from_id: nil)
- create(:jira_service, group: create(:group), project: nil, inherit_from_id: nil)
- create(:jira_service, project: project_3, inherit_from_id: nil)
- create(:slack_service, project: project_4, inherit_from_id: nil)
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to belong_to :group }
+ it { is_expected.to have_one :service_hook }
+ it { is_expected.to have_one :jira_tracker_data }
+ it { is_expected.to have_one :issue_tracker_data }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:type) }
+
+ where(:project_id, :group_id, :template, :instance, :valid) do
+ 1 | nil | false | false | true
+ nil | 1 | false | false | true
+ nil | nil | true | false | true
+ nil | nil | false | true | true
+ nil | nil | false | false | false
+ nil | nil | true | true | false
+ 1 | 1 | false | false | false
+ 1 | nil | true | false | false
+ 1 | nil | false | true | false
+ nil | 1 | true | false | false
+ nil | 1 | false | true | false
+ end
+
+ with_them do
+ it 'validates the service' do
+ expect(build(:service, project_id: project_id, group_id: group_id, template: template, instance: instance).valid?).to eq(valid)
+ end
+ end
+
+ context 'with existing services' do
+ before_all do
+ create(:service, :template)
+ create(:service, :instance)
+ create(:service, project: project)
+ create(:service, group: group, project: nil)
+ end
+
+ it 'allows only one service template per type' do
+ expect(build(:service, :template)).to be_invalid
+ end
+
+ it 'allows only one instance service per type' do
+ expect(build(:service, :instance)).to be_invalid
+ end
+
+ it 'allows only one project service per type' do
+ expect(build(:service, project: project)).to be_invalid
+ end
+
+ it 'allows only one group service per type' do
+ expect(build(:service, group: group, project: nil)).to be_invalid
+ end
+ end
+ end
+
+ describe 'Scopes' do
+ describe '.by_type' do
+ let!(:service1) { create(:jira_service) }
+ let!(:service2) { create(:jira_service) }
+ let!(:service3) { create(:redmine_service) }
+
+ subject { described_class.by_type(type) }
+
+ context 'when type is "JiraService"' do
+ let(:type) { 'JiraService' }
+
+ it { is_expected.to match_array([service1, service2]) }
+ end
+
+ context 'when type is "RedmineService"' do
+ let(:type) { 'RedmineService' }
+
+ it { is_expected.to match_array([service3]) }
+ end
+ end
+
+ describe '.for_group' do
+ let!(:service1) { create(:jira_service, project_id: nil, group_id: group.id) }
+ let!(:service2) { create(:jira_service) }
+
+ it 'returns the right group service' do
+ expect(described_class.for_group(group)).to match_array([service1])
+ end
+ end
+
+ describe '.confidential_note_hooks' do
+ it 'includes services where confidential_note_events is true' do
+ create(:service, active: true, confidential_note_events: true)
+
+ expect(described_class.confidential_note_hooks.count).to eq 1
+ end
+
+ it 'excludes services where confidential_note_events is false' do
+ create(:service, active: true, confidential_note_events: false)
+
+ expect(described_class.confidential_note_hooks.count).to eq 0
+ end
+ end
+
+ describe '.alert_hooks' do
+ it 'includes services where alert_events is true' do
+ create(:service, active: true, alert_events: true)
+
+ expect(described_class.alert_hooks.count).to eq 1
+ end
+
+ it 'excludes services where alert_events is false' do
+ create(:service, active: true, alert_events: false)
+
+ expect(described_class.alert_hooks.count).to eq 0
+ end
+ end
+ end
+
+ describe '#operating?' do
+ it 'is false when the service is not active' do
+ expect(build(:service).operating?).to eq(false)
+ end
+
+ it 'is false when the service is not persisted' do
+ expect(build(:service, active: true).operating?).to eq(false)
+ end
+
+ it 'is true when the service is active and persisted' do
+ expect(create(:service, active: true).operating?).to eq(true)
+ end
+ end
+
+ describe "Test Button" do
+ let(:service) { build(:service, project: project) }
+
+ describe '#can_test?' do
+ subject { service.can_test? }
+
+ context 'when repository is not empty' do
+ let(:project) { build(:project, :repository) }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when repository is empty' do
+ let(:project) { build(:project) }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when instance-level service' do
+ Integration.available_services_types.each do |service_type|
+ let(:service) do
+ service_type.constantize.new(instance: true)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when group-level service' do
+ Integration.available_services_types.each do |service_type|
+ let(:service) do
+ service_type.constantize.new(group_id: group.id)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe '#test' do
+ let(:data) { 'test' }
+
+ context 'when repository is not empty' do
+ let(:project) { build(:project, :repository) }
+
+ it 'test runs execute' do
+ expect(service).to receive(:execute).with(data)
+
+ service.test(data)
+ end
+ end
+
+ context 'when repository is empty' do
+ let(:project) { build(:project) }
+
+ it 'test runs execute' do
+ expect(service).to receive(:execute).with(data)
+
+ service.test(data)
+ end
+ end
+ end
+ end
+
+ describe '#project_level?' do
+ it 'is true when service has a project' do
+ expect(build(:service, project: project)).to be_project_level
+ end
+
+ it 'is false when service has no project' do
+ expect(build(:service, project: nil)).not_to be_project_level
+ end
+ end
+
+ describe '#group_level?' do
+ it 'is true when service has a group' do
+ expect(build(:service, group: group)).to be_group_level
+ end
+
+ it 'is false when service has no group' do
+ expect(build(:service, group: nil)).not_to be_group_level
+ end
+ end
+
+ describe '#instance_level?' do
+ it 'is true when service has instance-level integration' do
+ expect(build(:service, :instance)).to be_instance_level
+ end
+
+ it 'is false when service does not have instance-level integration' do
+ expect(build(:service, instance: false)).not_to be_instance_level
+ end
+ end
+
+ describe '.find_or_initialize_non_project_specific_integration' do
+ let!(:service1) { create(:jira_service, project_id: nil, group_id: group.id) }
+ let!(:service2) { create(:jira_service) }
+
+ it 'returns the right service' do
+ expect(Integration.find_or_initialize_non_project_specific_integration('jira', group_id: group)).to eq(service1)
+ end
+
+ it 'does not create a new service' do
+ expect { Integration.find_or_initialize_non_project_specific_integration('redmine', group_id: group) }.not_to change { Integration.count }
+ end
+ end
+
+ describe '.find_or_initialize_all_non_project_specific' do
+ shared_examples 'service instances' do
+ it 'returns the available service instances' do
+ expect(Integration.find_or_initialize_all_non_project_specific(Integration.for_instance).map(&:to_param)).to match_array(Integration.available_services_names(include_project_specific: false))
+ end
+
+ it 'does not create service instances' do
+ expect { Integration.find_or_initialize_all_non_project_specific(Integration.for_instance) }.not_to change { Integration.count }
+ end
+ end
+
+ it_behaves_like 'service instances'
+
+ context 'with all existing instances' do
+ before do
+ Integration.insert_all(
+ Integration.available_services_types(include_project_specific: false).map { |type| { instance: true, type: type } }
+ )
+ end
+
+ it_behaves_like 'service instances'
+
+ context 'with a previous existing service (MockCiService) and a new service (Asana)' do
+ before do
+ Integration.insert({ type: 'MockCiService', instance: true })
+ Integration.delete_by(type: 'AsanaService', instance: true)
+ end
+
+ it_behaves_like 'service instances'
+ end
+ end
+
+ context 'with a few existing instances' do
+ before do
+ create(:jira_service, :instance)
+ end
+
+ it_behaves_like 'service instances'
+ end
+ end
+
+ describe 'template' do
+ shared_examples 'retrieves service templates' do
+ it 'returns the available service templates' do
+ expect(Integration.find_or_create_templates.pluck(:type)).to match_array(Integration.available_services_types(include_project_specific: false))
+ end
+ end
+
+ describe '.find_or_create_templates' do
+ it 'creates service templates' do
+ expect { Integration.find_or_create_templates }.to change { Integration.count }.from(0).to(Integration.available_services_names(include_project_specific: false).size)
+ end
+
+ it_behaves_like 'retrieves service templates'
+
+ context 'with all existing templates' do
+ before do
+ Integration.insert_all(
+ Integration.available_services_types(include_project_specific: false).map { |type| { template: true, type: type } }
+ )
+ end
+
+ it 'does not create service templates' do
+ expect { Integration.find_or_create_templates }.not_to change { Integration.count }
+ end
+
+ it_behaves_like 'retrieves service templates'
+
+ context 'with a previous existing service (Previous) and a new service (Asana)' do
+ before do
+ Integration.insert({ type: 'PreviousService', template: true })
+ Integration.delete_by(type: 'AsanaService', template: true)
+ end
+
+ it_behaves_like 'retrieves service templates'
+ end
+ end
+
+ context 'with a few existing templates' do
+ before do
+ create(:jira_service, :template)
+ end
+
+ it 'creates the rest of the service templates' do
+ expect { Integration.find_or_create_templates }.to change { Integration.count }.from(1).to(Integration.available_services_names(include_project_specific: false).size)
+ end
+
+ it_behaves_like 'retrieves service templates'
+ end
+ end
+
+ describe '.build_from_integration' do
+ context 'when integration is invalid' do
+ let(:integration) do
+ build(:prometheus_service, :template, active: true, properties: {})
+ .tap { |integration| integration.save!(validate: false) }
+ end
+
+ it 'sets service to inactive' do
+ service = described_class.build_from_integration(integration, project_id: project.id)
+
+ expect(service).to be_valid
+ expect(service.active).to be false
+ end
+ end
+
+ context 'when integration is an instance-level integration' do
+ let(:integration) { create(:jira_service, :instance) }
+
+ it 'sets inherit_from_id from integration' do
+ service = described_class.build_from_integration(integration, project_id: project.id)
+
+ expect(service.inherit_from_id).to eq(integration.id)
+ end
+ end
+
+ context 'when integration is a group-level integration' do
+ let(:integration) { create(:jira_service, group: group, project: nil) }
+
+ it 'sets inherit_from_id from integration' do
+ service = described_class.build_from_integration(integration, project_id: project.id)
+
+ expect(service.inherit_from_id).to eq(integration.id)
+ end
+ end
+
+ describe 'build issue tracker from an integration' do
+ let(:url) { 'http://jira.example.com' }
+ let(:api_url) { 'http://api-jira.example.com' }
+ let(:username) { 'jira-username' }
+ let(:password) { 'jira-password' }
+ let(:data_params) do
+ {
+ url: url, api_url: api_url,
+ username: username, password: password
+ }
+ end
+
+ shared_examples 'service creation from an integration' do
+ it 'creates a correct service for a project integration' do
+ service = described_class.build_from_integration(integration, project_id: project.id)
+
+ expect(service).to be_active
+ expect(service.url).to eq(url)
+ expect(service.api_url).to eq(api_url)
+ expect(service.username).to eq(username)
+ expect(service.password).to eq(password)
+ expect(service.template).to eq(false)
+ expect(service.instance).to eq(false)
+ expect(service.project).to eq(project)
+ expect(service.group).to eq(nil)
+ end
+
+ it 'creates a correct service for a group integration' do
+ service = described_class.build_from_integration(integration, group_id: group.id)
+
+ expect(service).to be_active
+ expect(service.url).to eq(url)
+ expect(service.api_url).to eq(api_url)
+ expect(service.username).to eq(username)
+ expect(service.password).to eq(password)
+ expect(service.template).to eq(false)
+ expect(service.instance).to eq(false)
+ expect(service.project).to eq(nil)
+ expect(service.group).to eq(group)
+ end
+ end
+
+ # this will be removed as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
+ context 'when data are stored in properties' do
+ let(:properties) { data_params }
+ let!(:integration) do
+ create(:jira_service, :without_properties_callback, template: true, properties: properties.merge(additional: 'something'))
+ end
+
+ it_behaves_like 'service creation from an integration'
+ end
+
+ context 'when data are stored in separated fields' do
+ let(:integration) do
+ create(:jira_service, :template, data_params.merge(properties: {}))
+ end
+
+ it_behaves_like 'service creation from an integration'
+ end
+
+ context 'when data are stored in both properties and separated fields' do
+ let(:properties) { data_params }
+ let(:integration) do
+ create(:jira_service, :without_properties_callback, active: true, template: true, properties: properties).tap do |integration|
+ create(:jira_tracker_data, data_params.merge(integration: integration))
+ end
+ end
+
+ it_behaves_like 'service creation from an integration'
+ end
+ end
+ end
+
+ describe "for pushover service" do
+ let!(:service_template) do
+ PushoverService.create!(
+ template: true,
+ properties: {
+ device: 'MyDevice',
+ sound: 'mic',
+ priority: 4,
+ api_key: '123456789'
+ })
+ end
+
+ describe 'is prefilled for projects pushover service' do
+ it "has all fields prefilled" do
+ service = project.find_or_initialize_service('pushover')
+
+ expect(service.template).to eq(false)
+ expect(service.device).to eq('MyDevice')
+ expect(service.sound).to eq('mic')
+ expect(service.priority).to eq(4)
+ expect(service.api_key).to eq('123456789')
+ end
+ end
+ end
+ end
+
+ describe '.default_integration' do
+ context 'with an instance-level service' do
+ let_it_be(:instance_service) { create(:jira_service, :instance) }
+
+ it 'returns the instance service' do
+ expect(described_class.default_integration('JiraService', project)).to eq(instance_service)
+ end
+
+ it 'returns nil for nonexistent service type' do
+ expect(described_class.default_integration('HipchatService', project)).to eq(nil)
+ end
+
+ context 'with a group service' do
+ let_it_be(:group_service) { create(:jira_service, group_id: group.id, project_id: nil) }
+
+ it 'returns the group service for a project' do
+ expect(described_class.default_integration('JiraService', project)).to eq(group_service)
+ end
+
+ it 'returns the instance service for a group' do
+ expect(described_class.default_integration('JiraService', group)).to eq(instance_service)
+ end
+
+ context 'with a subgroup' do
+ let_it_be(:subgroup) { create(:group, parent: group) }
+
+ let!(:project) { create(:project, group: subgroup) }
+
+ it 'returns the closest group service for a project' do
+ expect(described_class.default_integration('JiraService', project)).to eq(group_service)
+ end
+
+ it 'returns the closest group service for a subgroup' do
+ expect(described_class.default_integration('JiraService', subgroup)).to eq(group_service)
+ end
+
+ context 'having a service with custom settings' do
+ let!(:subgroup_service) { create(:jira_service, group_id: subgroup.id, project_id: nil) }
+
+ it 'returns the closest group service for a project' do
+ expect(described_class.default_integration('JiraService', project)).to eq(subgroup_service)
+ end
+ end
+
+ context 'having a service inheriting settings' do
+ let!(:subgroup_service) { create(:jira_service, group_id: subgroup.id, project_id: nil, inherit_from_id: group_service.id) }
+
+ it 'returns the closest group service which does not inherit from its parent for a project' do
+ expect(described_class.default_integration('JiraService', project)).to eq(group_service)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe '.create_from_active_default_integrations' do
+ context 'with an active service template' do
+ let_it_be(:template_integration) { create(:prometheus_service, :template, api_url: 'https://prometheus.template.com/') }
+
+ it 'creates a service from the template' do
+ described_class.create_from_active_default_integrations(project, :project_id, with_templates: true)
+
+ expect(project.reload.integrations.size).to eq(1)
+ expect(project.reload.integrations.first.api_url).to eq(template_integration.api_url)
+ expect(project.reload.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
+ described_class.create_from_active_default_integrations(project, :project_id, with_templates: true)
+
+ expect(project.reload.integrations.size).to eq(1)
+ expect(project.reload.integrations.first.api_url).to eq(instance_integration.api_url)
+ expect(project.reload.integrations.first.inherit_from_id).to eq(instance_integration.id)
+ end
+
+ context 'passing a group' do
+ it 'creates a service from the instance-level integration' do
+ described_class.create_from_active_default_integrations(group, :group_id)
+
+ expect(group.reload.integrations.size).to eq(1)
+ expect(group.reload.integrations.first.api_url).to eq(instance_integration.api_url)
+ expect(group.reload.integrations.first.inherit_from_id).to eq(instance_integration.id)
+ end
+ end
+
+ context 'with an active group-level integration' do
+ let!(:group_integration) { create(:prometheus_service, group: group, project: nil, api_url: 'https://prometheus.group.com/') }
+
+ it 'creates a service from the group-level integration' do
+ described_class.create_from_active_default_integrations(project, :project_id, with_templates: true)
+
+ expect(project.reload.integrations.size).to eq(1)
+ expect(project.reload.integrations.first.api_url).to eq(group_integration.api_url)
+ expect(project.reload.integrations.first.inherit_from_id).to eq(group_integration.id)
+ end
+
+ context 'passing a group' do
+ let!(:subgroup) { create(:group, parent: group) }
+
+ it 'creates a service from the group-level integration' do
+ described_class.create_from_active_default_integrations(subgroup, :group_id)
+
+ expect(subgroup.reload.integrations.size).to eq(1)
+ expect(subgroup.reload.integrations.first.api_url).to eq(group_integration.api_url)
+ expect(subgroup.reload.integrations.first.inherit_from_id).to eq(group_integration.id)
+ end
+ end
+
+ context 'with an active subgroup' do
+ let!(:subgroup_integration) { create(:prometheus_service, group: subgroup, project: nil, api_url: 'https://prometheus.subgroup.com/') }
+ let!(:subgroup) { create(:group, parent: group) }
+ let(:project) { create(:project, group: subgroup) }
+
+ it 'creates a service from the subgroup-level integration' do
+ described_class.create_from_active_default_integrations(project, :project_id, with_templates: true)
+
+ expect(project.reload.integrations.size).to eq(1)
+ expect(project.reload.integrations.first.api_url).to eq(subgroup_integration.api_url)
+ expect(project.reload.integrations.first.inherit_from_id).to eq(subgroup_integration.id)
+ end
+
+ context 'passing a group' do
+ let!(:sub_subgroup) { create(:group, parent: subgroup) }
+
+ context 'traversal queries' do
+ shared_examples 'correct ancestor order' do
+ it 'creates a service from the subgroup-level integration' do
+ described_class.create_from_active_default_integrations(sub_subgroup, :group_id)
+
+ sub_subgroup.reload
+
+ expect(sub_subgroup.integrations.size).to eq(1)
+ expect(sub_subgroup.integrations.first.api_url).to eq(subgroup_integration.api_url)
+ expect(sub_subgroup.integrations.first.inherit_from_id).to eq(subgroup_integration.id)
+ end
+
+ context 'having a service inheriting settings' do
+ let!(:subgroup_integration) { create(:prometheus_service, group: subgroup, project: nil, inherit_from_id: group_integration.id, api_url: 'https://prometheus.subgroup.com/') }
+
+ it 'creates a service from the group-level integration' do
+ described_class.create_from_active_default_integrations(sub_subgroup, :group_id)
+
+ sub_subgroup.reload
+
+ expect(sub_subgroup.integrations.size).to eq(1)
+ expect(sub_subgroup.integrations.first.api_url).to eq(group_integration.api_url)
+ expect(sub_subgroup.integrations.first.inherit_from_id).to eq(group_integration.id)
+ end
+ end
+ end
+
+ context 'recursive' do
+ before do
+ stub_feature_flags(use_traversal_ids: false)
+ end
+
+ include_examples 'correct ancestor order'
+ end
+
+ context 'linear' do
+ before do
+ stub_feature_flags(use_traversal_ids: true)
+
+ sub_subgroup.reload # make sure traversal_ids are reloaded
+ end
+
+ include_examples 'correct ancestor order'
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe '.inherited_descendants_from_self_or_ancestors_from' do
+ let_it_be(:subgroup1) { create(:group, parent: group) }
+ let_it_be(:subgroup2) { create(:group, parent: group) }
+ let_it_be(:project1) { create(:project, group: subgroup1) }
+ let_it_be(:project2) { create(:project, group: subgroup2) }
+ let_it_be(:group_integration) { create(:prometheus_service, group: group, project: nil) }
+ let_it_be(:subgroup_integration1) { create(:prometheus_service, group: subgroup1, project: nil, inherit_from_id: group_integration.id) }
+ let_it_be(:subgroup_integration2) { create(:prometheus_service, group: subgroup2, project: nil) }
+ let_it_be(:project_integration1) { create(:prometheus_service, group: nil, project: project1, inherit_from_id: group_integration.id) }
+ let_it_be(:project_integration2) { create(:prometheus_service, group: nil, project: project2, inherit_from_id: subgroup_integration2.id) }
+
+ it 'returns the groups and projects inheriting from integration ancestors', :aggregate_failures do
+ expect(described_class.inherited_descendants_from_self_or_ancestors_from(group_integration)).to eq([subgroup_integration1, project_integration1])
+ expect(described_class.inherited_descendants_from_self_or_ancestors_from(subgroup_integration2)).to eq([project_integration2])
+ end
end
- describe '.with_custom_integration_for' do
- it 'returns projects with custom integrations' do
- # We use pagination to verify that the group is excluded from the query
- expect(Project.with_custom_integration_for(instance_integration, 0, 2)).to contain_exactly(project_2, project_3)
- expect(Project.with_custom_integration_for(instance_integration)).to contain_exactly(project_2, project_3)
+ describe '.service_name_to_model' do
+ it 'returns the model for the given service name', :aggregate_failures do
+ expect(described_class.service_name_to_model('asana')).to eq(Integrations::Asana)
+ # TODO We can remove this test when all models have been namespaced:
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60968#note_570994955
+ expect(described_class.service_name_to_model('youtrack')).to eq(YoutrackService)
end
+
+ it 'raises an error if service name is invalid' do
+ expect { described_class.service_name_to_model('foo') }.to raise_exception(NameError, /uninitialized constant FooService/)
+ end
+ end
+
+ describe "{property}_changed?" do
+ let(:service) do
+ Integrations::Bamboo.create!(
+ project: project,
+ properties: {
+ bamboo_url: 'http://gitlab.com',
+ username: 'mic',
+ password: "password"
+ }
+ )
+ end
+
+ it "returns false when the property has not been assigned a new value" do
+ service.username = "key_changed"
+ expect(service.bamboo_url_changed?).to be_falsy
+ end
+
+ it "returns true when the property has been assigned a different value" do
+ service.bamboo_url = "http://example.com"
+ expect(service.bamboo_url_changed?).to be_truthy
+ end
+
+ it "returns true when the property has been assigned a different value twice" do
+ service.bamboo_url = "http://example.com"
+ service.bamboo_url = "http://example.com"
+ expect(service.bamboo_url_changed?).to be_truthy
+ end
+
+ it "returns false when the property has been re-assigned the same value" do
+ service.bamboo_url = 'http://gitlab.com'
+ expect(service.bamboo_url_changed?).to be_falsy
+ end
+
+ it "returns false when the property has been assigned a new value then saved" do
+ service.bamboo_url = 'http://example.com'
+ service.save!
+ expect(service.bamboo_url_changed?).to be_falsy
+ end
+ end
+
+ describe "{property}_touched?" do
+ let(:service) do
+ Integrations::Bamboo.create!(
+ project: project,
+ properties: {
+ bamboo_url: 'http://gitlab.com',
+ username: 'mic',
+ password: "password"
+ }
+ )
+ end
+
+ it "returns false when the property has not been assigned a new value" do
+ service.username = "key_changed"
+ expect(service.bamboo_url_touched?).to be_falsy
+ end
+
+ it "returns true when the property has been assigned a different value" do
+ service.bamboo_url = "http://example.com"
+ expect(service.bamboo_url_touched?).to be_truthy
+ end
+
+ it "returns true when the property has been assigned a different value twice" do
+ service.bamboo_url = "http://example.com"
+ service.bamboo_url = "http://example.com"
+ expect(service.bamboo_url_touched?).to be_truthy
+ end
+
+ it "returns true when the property has been re-assigned the same value" do
+ service.bamboo_url = 'http://gitlab.com'
+ expect(service.bamboo_url_touched?).to be_truthy
+ end
+
+ it "returns false when the property has been assigned a new value then saved" do
+ service.bamboo_url = 'http://example.com'
+ service.save!
+ expect(service.bamboo_url_changed?).to be_falsy
+ end
+ end
+
+ describe "{property}_was" do
+ let(:service) do
+ Integrations::Bamboo.create!(
+ project: project,
+ properties: {
+ bamboo_url: 'http://gitlab.com',
+ username: 'mic',
+ password: "password"
+ }
+ )
+ end
+
+ it "returns nil when the property has not been assigned a new value" do
+ service.username = "key_changed"
+ expect(service.bamboo_url_was).to be_nil
+ end
+
+ it "returns the previous value when the property has been assigned a different value" do
+ service.bamboo_url = "http://example.com"
+ expect(service.bamboo_url_was).to eq('http://gitlab.com')
+ end
+
+ it "returns initial value when the property has been re-assigned the same value" do
+ service.bamboo_url = 'http://gitlab.com'
+ expect(service.bamboo_url_was).to eq('http://gitlab.com')
+ end
+
+ it "returns initial value when the property has been assigned multiple values" do
+ service.bamboo_url = "http://example.com"
+ service.bamboo_url = "http://example2.com"
+ expect(service.bamboo_url_was).to eq('http://gitlab.com')
+ end
+
+ it "returns nil when the property has been assigned a new value then saved" do
+ service.bamboo_url = 'http://example.com'
+ service.save!
+ expect(service.bamboo_url_was).to be_nil
+ end
+ end
+
+ describe 'initialize service with no properties' do
+ let(:service) do
+ BugzillaService.create!(
+ project: project,
+ project_url: 'http://gitlab.example.com'
+ )
+ end
+
+ it 'does not raise error' do
+ expect { service }.not_to raise_error
+ end
+
+ it 'sets data correctly' do
+ expect(service.data_fields.project_url).to eq('http://gitlab.example.com')
+ end
+ end
+
+ describe '#api_field_names' do
+ let(:fake_service) do
+ Class.new(Integration) do
+ def fields
+ [
+ { name: 'token' },
+ { name: 'api_token' },
+ { name: 'key' },
+ { name: 'api_key' },
+ { name: 'password' },
+ { name: 'password_field' },
+ { name: 'safe_field' }
+ ]
+ end
+ end
+ end
+
+ let(:service) do
+ fake_service.new(properties: [
+ { token: 'token-value' },
+ { api_token: 'api_token-value' },
+ { key: 'key-value' },
+ { api_key: 'api_key-value' },
+ { password: 'password-value' },
+ { password_field: 'password_field-value' },
+ { safe_field: 'safe_field-value' }
+ ])
+ end
+
+ it 'filters out sensitive fields' do
+ expect(service.api_field_names).to eq(['safe_field'])
+ end
+ end
+
+ context 'logging' do
+ let(:service) { build(:service, project: project) }
+ let(:test_message) { "test message" }
+ let(:arguments) do
+ {
+ service_class: service.class.name,
+ project_path: project.full_path,
+ project_id: project.id,
+ message: test_message,
+ additional_argument: 'some argument'
+ }
+ end
+
+ it 'logs info messages using json logger' do
+ expect(Gitlab::JsonLogger).to receive(:info).with(arguments)
+
+ service.log_info(test_message, additional_argument: 'some argument')
+ end
+
+ it 'logs error messages using json logger' do
+ expect(Gitlab::JsonLogger).to receive(:error).with(arguments)
+
+ service.log_error(test_message, additional_argument: 'some argument')
+ end
+
+ context 'when project is nil' do
+ let(:project) { nil }
+ let(:arguments) do
+ {
+ service_class: service.class.name,
+ project_path: nil,
+ project_id: nil,
+ message: test_message,
+ additional_argument: 'some argument'
+ }
+ end
+
+ it 'logs info messages using json logger' do
+ expect(Gitlab::JsonLogger).to receive(:info).with(arguments)
+
+ service.log_info(test_message, additional_argument: 'some argument')
+ end
+ end
+ end
+
+ describe '#external_wiki?' do
+ where(:type, :active, :result) do
+ 'ExternalWikiService' | true | true
+ 'ExternalWikiService' | false | false
+ 'SlackService' | true | false
+ end
+
+ with_them do
+ it 'returns the right result' do
+ expect(build(:service, type: type, active: active).external_wiki?).to eq(result)
+ end
+ end
+ end
+
+ describe '.available_services_names' do
+ it 'calls the right methods' do
+ expect(described_class).to receive(:services_names).and_call_original
+ expect(described_class).to receive(:dev_services_names).and_call_original
+ expect(described_class).to receive(:project_specific_services_names).and_call_original
+
+ described_class.available_services_names
+ end
+
+ it 'does not call project_specific_services_names with include_project_specific false' do
+ expect(described_class).to receive(:services_names).and_call_original
+ expect(described_class).to receive(:dev_services_names).and_call_original
+ expect(described_class).not_to receive(:project_specific_services_names)
+
+ described_class.available_services_names(include_project_specific: false)
+ end
+
+ it 'does not call dev_services_names with include_dev false' do
+ expect(described_class).to receive(:services_names).and_call_original
+ expect(described_class).not_to receive(:dev_services_names)
+ expect(described_class).to receive(:project_specific_services_names).and_call_original
+
+ described_class.available_services_names(include_dev: false)
+ end
+
+ it { expect(described_class.available_services_names).to include('jenkins') }
end
- describe '.without_integration' do
- it 'returns projects without integration' do
- expect(Project.without_integration(instance_integration)).to contain_exactly(project_4)
+ describe '.project_specific_services_names' do
+ it do
+ expect(described_class.project_specific_services_names)
+ .to include(*described_class::PROJECT_SPECIFIC_INTEGRATION_NAMES)
end
end
end
diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/integrations/asana_spec.rb
index 7a6fe4b1537..4473478910a 100644
--- a/spec/models/project_services/asana_service_spec.rb
+++ b/spec/models/integrations/asana_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AsanaService do
+RSpec.describe Integrations::Asana do
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -54,7 +54,7 @@ RSpec.describe AsanaService do
d1 = double('Asana::Resources::Task')
expect(d1).to receive(:add_comment).with(text: expected_message)
- expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, gid).once.and_return(d1)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, gid).once.and_return(d1)
@asana.execute(data)
end
@@ -64,7 +64,7 @@ RSpec.describe AsanaService do
d1 = double('Asana::Resources::Task')
expect(d1).to receive(:add_comment)
expect(d1).to receive(:update).with(completed: true)
- expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '456789').once.and_return(d1)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '456789').once.and_return(d1)
@asana.execute(data)
end
@@ -74,7 +74,7 @@ RSpec.describe AsanaService do
d1 = double('Asana::Resources::Task')
expect(d1).to receive(:add_comment)
expect(d1).to receive(:update).with(completed: true)
- expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d1)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d1)
@asana.execute(data)
end
@@ -88,25 +88,25 @@ RSpec.describe AsanaService do
d1 = double('Asana::Resources::Task')
expect(d1).to receive(:add_comment)
expect(d1).to receive(:update).with(completed: true)
- expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '123').once.and_return(d1)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '123').once.and_return(d1)
d2 = double('Asana::Resources::Task')
expect(d2).to receive(:add_comment)
expect(d2).to receive(:update).with(completed: true)
- expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '456').once.and_return(d2)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '456').once.and_return(d2)
d3 = double('Asana::Resources::Task')
expect(d3).to receive(:add_comment)
- expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '789').once.and_return(d3)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '789').once.and_return(d3)
d4 = double('Asana::Resources::Task')
expect(d4).to receive(:add_comment)
- expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d4)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d4)
d5 = double('Asana::Resources::Task')
expect(d5).to receive(:add_comment)
expect(d5).to receive(:update).with(completed: true)
- expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '12').once.and_return(d5)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '12').once.and_return(d5)
@asana.execute(data)
end
diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/integrations/assembla_spec.rb
index 207add6f090..bf9033416e9 100644
--- a/spec/models/project_services/assembla_service_spec.rb
+++ b/spec/models/integrations/assembla_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AssemblaService do
+RSpec.describe Integrations::Assembla do
include StubRequests
describe "Associations" do
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/integrations/bamboo_spec.rb
index 45afbcca96d..0ba1595bbd8 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/integrations/bamboo_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BambooService, :use_clean_rails_memory_store_caching do
+RSpec.describe Integrations::Bamboo, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
include StubRequests
diff --git a/spec/models/project_services/campfire_service_spec.rb b/spec/models/integrations/campfire_spec.rb
index ea3990b339b..b23edf03e8a 100644
--- a/spec/models/project_services/campfire_service_spec.rb
+++ b/spec/models/integrations/campfire_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe CampfireService do
+RSpec.describe Integrations::Campfire do
include StubRequests
describe 'Associations' do
diff --git a/spec/models/project_services/chat_message/alert_message_spec.rb b/spec/models/integrations/chat_message/alert_message_spec.rb
index 4d400990789..9866b2d9185 100644
--- a/spec/models/project_services/chat_message/alert_message_spec.rb
+++ b/spec/models/integrations/chat_message/alert_message_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ChatMessage::AlertMessage do
+RSpec.describe Integrations::ChatMessage::AlertMessage do
subject { described_class.new(args) }
let_it_be(:start_time) { Time.current }
diff --git a/spec/models/project_services/chat_message/base_message_spec.rb b/spec/models/integrations/chat_message/base_message_spec.rb
index a7ddf230758..eada5d1031d 100644
--- a/spec/models/project_services/chat_message/base_message_spec.rb
+++ b/spec/models/integrations/chat_message/base_message_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ChatMessage::BaseMessage do
+RSpec.describe Integrations::ChatMessage::BaseMessage do
let(:base_message) { described_class.new(args) }
let(:args) { { project_url: 'https://gitlab-domain.com' } }
diff --git a/spec/models/project_services/chat_message/deployment_message_spec.rb b/spec/models/integrations/chat_message/deployment_message_spec.rb
index 6bdf2120b36..ff255af11a3 100644
--- a/spec/models/project_services/chat_message/deployment_message_spec.rb
+++ b/spec/models/integrations/chat_message/deployment_message_spec.rb
@@ -2,14 +2,14 @@
require 'spec_helper'
-RSpec.describe ChatMessage::DeploymentMessage do
+RSpec.describe Integrations::ChatMessage::DeploymentMessage do
describe '#pretext' do
it 'returns a message with the data returned by the deployment data builder' do
environment = create(:environment, name: "myenvironment")
project = create(:project, :repository)
commit = project.commit('HEAD')
deployment = create(:deployment, status: :success, environment: environment, project: project, sha: commit.sha)
- data = Gitlab::DataBuilder::Deployment.build(deployment)
+ data = Gitlab::DataBuilder::Deployment.build(deployment, Time.current)
message = described_class.new(data)
@@ -118,7 +118,7 @@ RSpec.describe ChatMessage::DeploymentMessage do
job_url = Gitlab::Routing.url_helpers.project_job_url(project, ci_build)
commit_url = Gitlab::UrlBuilder.build(deployment.commit)
user_url = Gitlab::Routing.url_helpers.user_url(user)
- data = Gitlab::DataBuilder::Deployment.build(deployment)
+ data = Gitlab::DataBuilder::Deployment.build(deployment, Time.current)
message = described_class.new(data)
diff --git a/spec/models/project_services/chat_message/issue_message_spec.rb b/spec/models/integrations/chat_message/issue_message_spec.rb
index 4701ef3e49e..31b80ad3169 100644
--- a/spec/models/project_services/chat_message/issue_message_spec.rb
+++ b/spec/models/integrations/chat_message/issue_message_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ChatMessage::IssueMessage do
+RSpec.describe Integrations::ChatMessage::IssueMessage do
subject { described_class.new(args) }
let(:args) do
diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/integrations/chat_message/merge_message_spec.rb
index 71cfe3ff45b..ed1ad6837e2 100644
--- a/spec/models/project_services/chat_message/merge_message_spec.rb
+++ b/spec/models/integrations/chat_message/merge_message_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ChatMessage::MergeMessage do
+RSpec.describe Integrations::ChatMessage::MergeMessage do
subject { described_class.new(args) }
let(:args) do
diff --git a/spec/models/project_services/chat_message/note_message_spec.rb b/spec/models/integrations/chat_message/note_message_spec.rb
index 6a741365d55..668c0da26ae 100644
--- a/spec/models/project_services/chat_message/note_message_spec.rb
+++ b/spec/models/integrations/chat_message/note_message_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ChatMessage::NoteMessage do
+RSpec.describe Integrations::ChatMessage::NoteMessage do
subject { described_class.new(args) }
let(:color) { '#345' }
diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/integrations/chat_message/pipeline_message_spec.rb
index 4eb2f57315b..a80d13d7f5d 100644
--- a/spec/models/project_services/chat_message/pipeline_message_spec.rb
+++ b/spec/models/integrations/chat_message/pipeline_message_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe ChatMessage::PipelineMessage do
+RSpec.describe Integrations::ChatMessage::PipelineMessage do
subject { described_class.new(args) }
let(:args) do
diff --git a/spec/models/project_services/chat_message/push_message_spec.rb b/spec/models/integrations/chat_message/push_message_spec.rb
index e3ba4c2aefe..167487449c3 100644
--- a/spec/models/project_services/chat_message/push_message_spec.rb
+++ b/spec/models/integrations/chat_message/push_message_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ChatMessage::PushMessage do
+RSpec.describe Integrations::ChatMessage::PushMessage do
subject { described_class.new(args) }
let(:args) do
diff --git a/spec/models/project_services/chat_message/wiki_page_message_spec.rb b/spec/models/integrations/chat_message/wiki_page_message_spec.rb
index 04c9e5934be..e8672a0f9c8 100644
--- a/spec/models/project_services/chat_message/wiki_page_message_spec.rb
+++ b/spec/models/integrations/chat_message/wiki_page_message_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ChatMessage::WikiPageMessage do
+RSpec.describe Integrations::ChatMessage::WikiPageMessage do
subject { described_class.new(args) }
let(:args) do
diff --git a/spec/models/project_services/confluence_service_spec.rb b/spec/models/integrations/confluence_spec.rb
index 6c7ba2c9f32..c217573f48d 100644
--- a/spec/models/project_services/confluence_service_spec.rb
+++ b/spec/models/integrations/confluence_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ConfluenceService do
+RSpec.describe Integrations::Confluence do
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
diff --git a/spec/models/project_services/datadog_service_spec.rb b/spec/models/integrations/datadog_spec.rb
index d15ea1f351b..165b21840e0 100644
--- a/spec/models/project_services/datadog_service_spec.rb
+++ b/spec/models/integrations/datadog_spec.rb
@@ -3,7 +3,7 @@ require 'securerandom'
require 'spec_helper'
-RSpec.describe DatadogService, :model do
+RSpec.describe Integrations::Datadog do
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:build) { create(:ci_build, project: project) }
diff --git a/spec/models/project_services/emails_on_push_service_spec.rb b/spec/models/integrations/emails_on_push_spec.rb
index c5927503eec..ca060f4155e 100644
--- a/spec/models/project_services/emails_on_push_service_spec.rb
+++ b/spec/models/integrations/emails_on_push_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe EmailsOnPushService do
+RSpec.describe Integrations::EmailsOnPush do
let_it_be(:project) { create_default(:project).freeze }
describe 'Validations' do
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb
index 981245627af..390d1552c16 100644
--- a/spec/models/internal_id_spec.rb
+++ b/spec/models/internal_id_spec.rb
@@ -100,7 +100,8 @@ RSpec.describe InternalId do
context 'when executed outside of transaction' do
it 'increments counter with in_transaction: "false"' do
- expect(ActiveRecord::Base.connection).to receive(:transaction_open?) { false }
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false }
+
expect(InternalId::InternalIdGenerator.internal_id_transactions_total).to receive(:increment)
.with(operation: :generate, usage: 'issues', in_transaction: 'false').and_call_original
@@ -158,7 +159,8 @@ RSpec.describe InternalId do
let(:value) { 2 }
it 'increments counter with in_transaction: "false"' do
- expect(ActiveRecord::Base.connection).to receive(:transaction_open?) { false }
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false }
+
expect(InternalId::InternalIdGenerator.internal_id_transactions_total).to receive(:increment)
.with(operation: :reset, usage: 'issues', in_transaction: 'false').and_call_original
@@ -228,7 +230,8 @@ RSpec.describe InternalId do
context 'when executed outside of transaction' do
it 'increments counter with in_transaction: "false"' do
- expect(ActiveRecord::Base.connection).to receive(:transaction_open?) { false }
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false }
+
expect(InternalId::InternalIdGenerator.internal_id_transactions_total).to receive(:increment)
.with(operation: :track_greatest, usage: 'issues', in_transaction: 'false').and_call_original
diff --git a/spec/models/issue/metrics_spec.rb b/spec/models/issue/metrics_spec.rb
index 18b0a46c928..49c891c20da 100644
--- a/spec/models/issue/metrics_spec.rb
+++ b/spec/models/issue/metrics_spec.rb
@@ -80,5 +80,20 @@ RSpec.describe Issue::Metrics do
expect(metrics.first_added_to_board_at).to be_like_time(time)
end
end
+
+ describe "#record!" do
+ it "does not cause an N+1 query" do
+ label = create(:label)
+ subject.update!(label_ids: [label.id])
+
+ control_count = ActiveRecord::QueryRecorder.new { Issue::Metrics.find_by(issue: subject).record! }.count
+
+ additional_labels = create_list(:label, 4)
+
+ subject.update!(label_ids: additional_labels.map(&:id))
+
+ expect { Issue::Metrics.find_by(issue: subject).record! }.not_to exceed_query_limit(control_count)
+ end
+ end
end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 23caf3647c3..884c476932e 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -242,16 +242,36 @@ RSpec.describe Issue do
expect { issue.close }.to change { issue.state_id }.from(open_state).to(closed_state)
end
+
+ context 'when an argument is provided' do
+ context 'and the argument is a User' do
+ it 'changes closed_by to the given user' do
+ expect { issue.close(user) }.to change { issue.closed_by }.from(nil).to(user)
+ end
+ end
+
+ context 'and the argument is a not a User' do
+ it 'does not change closed_by' do
+ expect { issue.close("test") }.not_to change { issue.closed_by }
+ end
+ end
+ end
+
+ context 'when an argument is not provided' do
+ it 'does not change closed_by' do
+ expect { issue.close }.not_to change { issue.closed_by }
+ end
+ end
end
describe '#reopen' do
let(:issue) { create(:issue, project: reusable_project, state: 'closed', closed_at: Time.current, closed_by: user) }
- it 'sets closed_at to nil when an issue is reopend' do
+ it 'sets closed_at to nil when an issue is reopened' do
expect { issue.reopen }.to change { issue.closed_at }.to(nil)
end
- it 'sets closed_by to nil when an issue is reopend' do
+ it 'sets closed_by to nil when an issue is reopened' do
expect { issue.reopen }.to change { issue.closed_by }.from(user).to(nil)
end
@@ -297,7 +317,7 @@ RSpec.describe Issue do
end
context 'when cross-project in different namespace' do
- let(:another_namespace) { build(:namespace, path: 'another-namespace') }
+ let(:another_namespace) { build(:namespace, id: non_existing_record_id, path: 'another-namespace') }
let(:another_namespace_project) { build(:project, path: 'another-project', namespace: another_namespace) }
it 'returns complete path to the issue' do
@@ -1121,11 +1141,37 @@ RSpec.describe Issue do
end
context "relative positioning" do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:issue1) { create(:issue, project: project, relative_position: nil) }
+ let_it_be(:issue2) { create(:issue, project: project, relative_position: nil) }
+
it_behaves_like "a class that supports relative positioning" do
let_it_be(:project) { reusable_project }
let(:factory) { :issue }
let(:default_params) { { project: project } }
end
+
+ it 'is not blocked for repositioning by default' do
+ expect(issue1.blocked_for_repositioning?).to eq(false)
+ end
+
+ context 'when block_issue_repositioning flag is enabled for group' do
+ before do
+ stub_feature_flags(block_issue_repositioning: group)
+ end
+
+ it 'is blocked for repositioning' do
+ expect(issue1.blocked_for_repositioning?).to eq(true)
+ end
+
+ it 'does not move issues with null position' do
+ payload = [issue1, issue2]
+
+ expect { described_class.move_nulls_to_end(payload) }.to raise_error(Gitlab::RelativePositioning::IssuePositioningDisabled)
+ expect { described_class.move_nulls_to_start(payload) }.to raise_error(Gitlab::RelativePositioning::IssuePositioningDisabled)
+ end
+ end
end
it_behaves_like 'versioned description'
diff --git a/spec/models/label_link_spec.rb b/spec/models/label_link_spec.rb
index a95481f3083..e5753b34e72 100644
--- a/spec/models/label_link_spec.rb
+++ b/spec/models/label_link_spec.rb
@@ -12,4 +12,28 @@ RSpec.describe LabelLink do
let(:valid_items_for_bulk_insertion) { build_list(:label_link, 10) }
let(:invalid_items_for_bulk_insertion) { [] } # class does not have any validations defined
end
+
+ describe 'scopes' do
+ describe '.for_target' do
+ it 'returns the label links for a given target' do
+ label_link = create(:label_link, target: create(:merge_request))
+
+ create(:label_link, target: create(:issue))
+
+ expect(described_class.for_target(label_link.target_id, label_link.target_type))
+ .to contain_exactly(label_link)
+ end
+ end
+
+ describe '.with_remove_on_close_labels' do
+ it 'responds with label_links that can be removed when an issue is closed' do
+ issue = create(:issue)
+ removable_label = create(:label, project: issue.project, remove_on_close: true)
+ create(:label_link, target: issue)
+ removable_issue_label_link = create(:label_link, label: removable_label, target: issue)
+
+ expect(described_class.with_remove_on_close_labels).to contain_exactly(removable_issue_label_link)
+ end
+ end
+ end
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 5f3a67b52ba..247be7654d8 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -143,16 +143,10 @@ RSpec.describe Member do
@blocked_maintainer = project.members.find_by(user_id: @blocked_maintainer_user.id, access_level: Gitlab::Access::MAINTAINER)
@blocked_developer = project.members.find_by(user_id: @blocked_developer_user.id, access_level: Gitlab::Access::DEVELOPER)
- @invited_member = create(:project_member, :developer,
- project: project,
- invite_token: '1234',
- invite_email: 'toto1@example.com')
+ @invited_member = create(:project_member, :invited, :developer, project: project)
accepted_invite_user = build(:user, state: :active)
- @accepted_invite_member = create(:project_member, :developer,
- project: project,
- invite_token: '1234',
- invite_email: 'toto2@example.com')
+ @accepted_invite_member = create(:project_member, :invited, :developer, project: project)
.tap { |u| u.accept_invite!(accepted_invite_user) }
requested_user = create(:user).tap { |u| project.request_access(u) }
@@ -325,12 +319,12 @@ RSpec.describe Member do
describe '.search_invite_email' do
it 'returns only members the matching e-mail' do
- create(:group_member, :invited)
+ invited_member = create(:group_member, :invited, invite_email: 'invited@example.com')
- invited = described_class.search_invite_email(@invited_member.invite_email)
+ invited = described_class.search_invite_email(invited_member.invite_email)
expect(invited.count).to eq(1)
- expect(invited.first).to eq(@invited_member)
+ expect(invited.first).to eq(invited_member)
expect(described_class.search_invite_email('bad-email@example.com').count).to eq(0)
end
@@ -414,6 +408,44 @@ RSpec.describe Member do
it { is_expected.not_to include @member_with_minimal_access }
end
+ describe '.connected_to_user' do
+ subject { described_class.connected_to_user.to_a }
+
+ it { is_expected.to include @owner }
+ it { is_expected.to include @maintainer }
+ it { is_expected.to include @accepted_invite_member }
+ it { is_expected.to include @accepted_request_member }
+ it { is_expected.to include @blocked_maintainer }
+ it { is_expected.to include @blocked_developer }
+ it { is_expected.to include @requested_member }
+ it { is_expected.to include @member_with_minimal_access }
+ it { is_expected.not_to include @invited_member }
+ end
+
+ describe '.authorizable' do
+ subject { described_class.authorizable.to_a }
+
+ it 'includes the member who has an associated user record,'\
+ 'but also having an invite_token' do
+ member = create(:project_member,
+ :developer,
+ :invited,
+ user: create(:user))
+
+ expect(subject).to include(member)
+ end
+
+ it { is_expected.to include @owner }
+ it { is_expected.to include @maintainer }
+ it { is_expected.to include @accepted_invite_member }
+ it { is_expected.to include @accepted_request_member }
+ it { is_expected.to include @blocked_maintainer }
+ it { is_expected.to include @blocked_developer }
+ it { is_expected.not_to include @invited_member }
+ it { is_expected.not_to include @requested_member }
+ it { is_expected.not_to include @member_with_minimal_access }
+ end
+
describe '.distinct_on_user_with_max_access_level' do
let_it_be(:other_group) { create(:group) }
let_it_be(:member_with_lower_access_level) { create(:group_member, :developer, group: other_group, user: @owner_user) }
@@ -884,7 +916,7 @@ RSpec.describe Member do
user = create(:user)
member = project.add_reporter(user)
- member.destroy
+ member.destroy!
expect(user.authorized_projects).not_to include(project)
end
@@ -901,7 +933,7 @@ RSpec.describe Member do
with_them do
describe 'create member' do
- let!(:source) { create(source_type) }
+ let!(:source) { create(source_type) } # rubocop:disable Rails/SaveBang
subject { create(member_type, :guest, user: user, source: source) }
@@ -913,20 +945,20 @@ RSpec.describe Member do
describe 'update member' do
context 'when access level was changed' do
- subject { member.update(access_level: Gitlab::Access::GUEST) }
+ subject { member.update!(access_level: Gitlab::Access::GUEST) }
include_examples 'update highest role with exclusive lease'
end
context 'when access level was not changed' do
- subject { member.update(notification_level: NotificationSetting.levels[:disabled]) }
+ subject { member.update!(notification_level: NotificationSetting.levels[:disabled]) }
include_examples 'does not update the highest role'
end
end
describe 'destroy member' do
- subject { member.destroy }
+ subject { member.destroy! }
include_examples 'update highest role with exclusive lease'
end
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 908bb9f91a3..3a2db5d8516 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -85,11 +85,11 @@ RSpec.describe GroupMember do
expect(user).to receive(:update_two_factor_requirement)
- group_member.save
+ group_member.save!
expect(user).to receive(:update_two_factor_requirement)
- group_member.destroy
+ group_member.destroy!
end
end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index ce3e86f964d..fa77e319c2c 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -49,13 +49,13 @@ RSpec.describe ProjectMember do
it "creates an expired event when left due to expiry" do
expired = create(:project_member, project: project, expires_at: 1.day.from_now)
- travel_to(2.days.from_now) { expired.destroy }
+ travel_to(2.days.from_now) { expired.destroy! }
expect(Event.recent.first).to be_expired_action
end
it "creates a left event when left due to leave" do
- maintainer.destroy
+ maintainer.destroy!
expect(Event.recent.first).to be_left_action
end
end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 5b11a7bf079..4075eb96fc2 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe MergeRequestDiff do
expect(subject.valid?).to be false
expect(subject.errors.count).to eq 3
- expect(subject.errors).to all(include('is not a valid SHA'))
+ expect(subject.errors.full_messages).to all(include('is not a valid SHA'))
end
it 'does not validate uniqueness by default' do
@@ -61,7 +61,7 @@ RSpec.describe MergeRequestDiff do
let_it_be(:merge_head) do
MergeRequests::MergeToRefService
- .new(merge_request.project, merge_request.author)
+ .new(project: merge_request.project, current_user: merge_request.author)
.execute(merge_request)
merge_request.create_merge_head_diff
@@ -485,27 +485,6 @@ RSpec.describe MergeRequestDiff do
'files/whitespace'
])
end
-
- context 'when sort_diffs feature flag is disabled' do
- before do
- stub_feature_flags(sort_diffs: false)
- end
-
- it 'does not sort diff files directory first' do
- expect(diff_with_commits.diffs_in_batch(1, 10, diff_options: diff_options).diff_file_paths).to eq([
- '.DS_Store',
- '.gitattributes',
- '.gitignore',
- '.gitmodules',
- 'CHANGELOG',
- 'README',
- 'bar/branch-test.txt',
- 'custom-highlighting/test.gitlab-custom',
- 'encoding/iso8859.txt',
- 'files/.DS_Store'
- ])
- end
- end
end
end
@@ -581,37 +560,6 @@ RSpec.describe MergeRequestDiff do
'gitlab-shell'
])
end
-
- context 'when sort_diffs feature flag is disabled' do
- before do
- stub_feature_flags(sort_diffs: false)
- end
-
- it 'does not sort diff files directory first' do
- expect(diff_with_commits.diffs(diff_options).diff_file_paths).to eq([
- '.DS_Store',
- '.gitattributes',
- '.gitignore',
- '.gitmodules',
- 'CHANGELOG',
- 'README',
- 'bar/branch-test.txt',
- 'custom-highlighting/test.gitlab-custom',
- 'encoding/iso8859.txt',
- 'files/.DS_Store',
- 'files/images/wm.svg',
- 'files/js/commit.coffee',
- 'files/lfs/lfs_object.iso',
- 'files/ruby/popen.rb',
- 'files/ruby/regex.rb',
- 'files/whitespace',
- 'foo/bar/.gitkeep',
- 'gitlab-grack',
- 'gitlab-shell',
- 'with space/README.md'
- ])
- end
- end
end
end
@@ -718,40 +666,6 @@ RSpec.describe MergeRequestDiff do
])
end
- context 'when sort_diffs feature flag is disabled' do
- before do
- stub_feature_flags(sort_diffs: false)
- end
-
- it 'persists diff files unsorted by directory first' do
- mr_diff = create(:merge_request).merge_request_diff
- diff_files_paths = mr_diff.merge_request_diff_files.map { |file| file.new_path.presence || file.old_path }
-
- expect(diff_files_paths).to eq([
- '.DS_Store',
- '.gitattributes',
- '.gitignore',
- '.gitmodules',
- 'CHANGELOG',
- 'README',
- 'bar/branch-test.txt',
- 'custom-highlighting/test.gitlab-custom',
- 'encoding/iso8859.txt',
- 'files/.DS_Store',
- 'files/images/wm.svg',
- 'files/js/commit.coffee',
- 'files/lfs/lfs_object.iso',
- 'files/ruby/popen.rb',
- 'files/ruby/regex.rb',
- 'files/whitespace',
- 'foo/bar/.gitkeep',
- 'gitlab-grack',
- 'gitlab-shell',
- 'with space/README.md'
- ])
- end
- end
-
it 'expands collapsed diffs before saving' do
mr_diff = create(:merge_request, source_branch: 'expand-collapse-lines', target_branch: 'master').merge_request_diff
diff_file = mr_diff.merge_request_diff_files.find_by(new_path: 'expand-collapse/file-5.txt')
@@ -1166,5 +1080,9 @@ RSpec.describe MergeRequestDiff do
it 'loads nothing if the merge request has no diff record' do
expect(described_class.latest_diff_for_merge_requests(merge_request_3)).to be_empty
end
+
+ it 'loads nothing if nil was passed as merge_request' do
+ expect(described_class.latest_diff_for_merge_requests(nil)).to be_empty
+ end
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 4b46c98117f..a77ca1e9a51 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -2255,7 +2255,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
describe '#find_codequality_mr_diff_reports' do
let(:project) { create(:project, :repository) }
- 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(:pipeline) { merge_request.head_pipeline }
subject(:mr_diff_report) { merge_request.find_codequality_mr_diff_reports }
@@ -2628,7 +2628,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
context 'when the MR has been merged' do
before do
MergeRequests::MergeService
- .new(subject.target_project, subject.author, { sha: subject.diff_head_sha })
+ .new(project: subject.target_project, current_user: subject.author, params: { sha: subject.diff_head_sha })
.execute(subject)
end
@@ -3876,6 +3876,20 @@ RSpec.describe MergeRequest, factory_default: :keep do
subject { merge_request.use_merge_base_pipeline_for_comparison?(service_class) }
+ context 'when service class is Ci::CompareMetricsReportsService' do
+ let(:service_class) { 'Ci::CompareMetricsReportsService' }
+
+ it { is_expected.to be_truthy }
+
+ context 'with the metrics report flag disabled' do
+ before do
+ stub_feature_flags(merge_base_pipeline_for_metrics_comparison: false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
context 'when service class is Ci::CompareCodequalityReportsService' do
let(:service_class) { 'Ci::CompareCodequalityReportsService' }
@@ -4806,7 +4820,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
context 'when merge_ref_sha is not present' do
let!(:result) 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/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index e611484f5ee..20dee288052 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -293,21 +293,7 @@ RSpec.describe Milestone do
end
end
- context 'when `optimized_timebox_queries` feature flag is enabled' do
- before do
- stub_feature_flags(optimized_timebox_queries: true)
- end
-
- it_behaves_like '#for_projects_and_groups'
- end
-
- context 'when `optimized_timebox_queries` feature flag is disabled' do
- before do
- stub_feature_flags(optimized_timebox_queries: false)
- end
-
- it_behaves_like '#for_projects_and_groups'
- end
+ it_behaves_like '#for_projects_and_groups'
describe '.upcoming_ids' do
let(:group_1) { create(:group) }
diff --git a/spec/models/namespace/package_setting_spec.rb b/spec/models/namespace/package_setting_spec.rb
index 097cef8ef3b..4308c8c06bc 100644
--- a/spec/models/namespace/package_setting_spec.rb
+++ b/spec/models/namespace/package_setting_spec.rb
@@ -14,9 +14,12 @@ RSpec.describe Namespace::PackageSetting do
it { is_expected.to allow_value(true).for(:maven_duplicates_allowed) }
it { is_expected.to allow_value(false).for(:maven_duplicates_allowed) }
it { is_expected.not_to allow_value(nil).for(:maven_duplicates_allowed) }
+ it { is_expected.to allow_value(true).for(:generic_duplicates_allowed) }
+ it { is_expected.to allow_value(false).for(:generic_duplicates_allowed) }
+ it { is_expected.not_to allow_value(nil).for(:generic_duplicates_allowed) }
end
- describe '#maven_duplicate_exception_regex' do
+ describe 'regex values' do
let_it_be(:package_settings) { create(:namespace_package_setting) }
subject { package_settings }
@@ -24,12 +27,14 @@ RSpec.describe Namespace::PackageSetting do
valid_regexps = %w[SNAPSHOT .* v.+ v10.1.* (?:v.+|SNAPSHOT|TEMP)]
invalid_regexps = ['[', '(?:v.+|SNAPSHOT|TEMP']
- valid_regexps.each do |valid_regexp|
- it { is_expected.to allow_value(valid_regexp).for(:maven_duplicate_exception_regex) }
- end
+ [:maven_duplicate_exception_regex, :generic_duplicate_exception_regex].each do |attribute|
+ valid_regexps.each do |valid_regexp|
+ it { is_expected.to allow_value(valid_regexp).for(attribute) }
+ end
- invalid_regexps.each do |invalid_regexp|
- it { is_expected.not_to allow_value(invalid_regexp).for(:maven_duplicate_exception_regex) }
+ invalid_regexps.each do |invalid_regexp|
+ it { is_expected.not_to allow_value(invalid_regexp).for(attribute) }
+ end
end
end
end
@@ -41,8 +46,8 @@ RSpec.describe Namespace::PackageSetting do
context 'package types with package_settings' do
# As more package types gain settings they will be added to this list
- [:maven_package].each do |format|
- let_it_be(:package) { create(format) } # rubocop:disable Rails/SaveBang
+ [:maven_package, :generic_package].each do |format|
+ let_it_be(:package) { create(format, name: 'foo', version: 'beta') } # rubocop:disable Rails/SaveBang
let_it_be(:package_type) { package.package_type }
let_it_be(:package_setting) { package.project.namespace.package_settings }
@@ -50,6 +55,8 @@ RSpec.describe Namespace::PackageSetting do
true | '' | true
false | '' | false
false | '.*' | true
+ false | 'fo.*' | true
+ false | 'be.*' | true
end
with_them do
@@ -68,7 +75,7 @@ RSpec.describe Namespace::PackageSetting do
end
context 'package types without package_settings' do
- [:npm_package, :conan_package, :nuget_package, :pypi_package, :composer_package, :generic_package, :golang_package, :debian_package].each do |format|
+ [:npm_package, :conan_package, :nuget_package, :pypi_package, :composer_package, :golang_package, :debian_package].each do |format|
let_it_be(:package) { create(format) } # rubocop:disable Rails/SaveBang
let_it_be(:package_setting) { package.project.namespace.package_settings }
diff --git a/spec/models/namespace/traversal_hierarchy_spec.rb b/spec/models/namespace/traversal_hierarchy_spec.rb
index b166d541171..2cd66f42458 100644
--- a/spec/models/namespace/traversal_hierarchy_spec.rb
+++ b/spec/models/namespace/traversal_hierarchy_spec.rb
@@ -43,16 +43,6 @@ RSpec.describe Namespace::TraversalHierarchy, type: :model do
end
end
- shared_examples 'locked update query' do
- it 'locks query with FOR UPDATE' do
- qr = ActiveRecord::QueryRecorder.new do
- subject
- end
- expect(qr.count).to eq 1
- expect(qr.log.first).to match /FOR UPDATE/
- end
- end
-
describe '#incorrect_traversal_ids' do
let!(:hierarchy) { described_class.new(root) }
@@ -63,12 +53,6 @@ RSpec.describe Namespace::TraversalHierarchy, type: :model do
end
it { is_expected.to match_array Namespace.all }
-
- context 'when lock is true' do
- subject { hierarchy.incorrect_traversal_ids(lock: true).load }
-
- it_behaves_like 'locked update query'
- end
end
describe '#sync_traversal_ids!' do
@@ -79,14 +63,18 @@ RSpec.describe Namespace::TraversalHierarchy, type: :model do
it { expect(hierarchy.incorrect_traversal_ids).to be_empty }
it_behaves_like 'hierarchy with traversal_ids'
- it_behaves_like 'locked update query'
+ it_behaves_like 'locked row' do
+ let(:recorded_queries) { ActiveRecord::QueryRecorder.new }
+ let(:row) { root }
- context 'when deadlocked' do
before do
- connection_double = double(:connection)
+ recorded_queries.record { subject }
+ end
+ end
- allow(Namespace).to receive(:connection).and_return(connection_double)
- allow(connection_double).to receive(:exec_query) { raise ActiveRecord::Deadlocked.new }
+ context 'when deadlocked' do
+ before do
+ allow(root).to receive(:lock!) { raise ActiveRecord::Deadlocked }
end
it { expect { subject }.to raise_error(ActiveRecord::Deadlocked) }
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 96ecc9836d4..56afe49e15f 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -141,7 +141,7 @@ RSpec.describe Namespace do
end
it 'allows updating other attributes for existing record' do
- namespace = build(:namespace, path: 'j')
+ namespace = build(:namespace, path: 'j', owner: create(:user))
namespace.save(validate: false)
namespace.reload
@@ -212,6 +212,54 @@ RSpec.describe Namespace do
end
end
+ describe "after_commit :expire_child_caches" do
+ let(:namespace) { create(:group) }
+
+ it "expires the child caches when updated" do
+ child_1 = create(:group, parent: namespace, updated_at: 1.week.ago)
+ child_2 = create(:group, parent: namespace, updated_at: 1.day.ago)
+ grandchild = create(:group, parent: child_1, updated_at: 1.week.ago)
+ project_1 = create(:project, namespace: namespace, updated_at: 2.days.ago)
+ project_2 = create(:project, namespace: child_1, updated_at: 3.days.ago)
+ project_3 = create(:project, namespace: grandchild, updated_at: 4.years.ago)
+
+ freeze_time do
+ namespace.update!(path: "foo")
+
+ [namespace, child_1, child_2, grandchild, project_1, project_2, project_3].each do |record|
+ expect(record.reload.updated_at).to eq(Time.zone.now)
+ end
+ end
+ end
+
+ it "expires on name changes" do
+ expect(namespace).to receive(:expire_child_caches).once
+
+ namespace.update!(name: "Foo")
+ end
+
+ it "expires on path changes" do
+ expect(namespace).to receive(:expire_child_caches).once
+
+ namespace.update!(path: "bar")
+ end
+
+ it "expires on parent changes" do
+ expect(namespace).to receive(:expire_child_caches).once
+
+ namespace.update!(parent: create(:group))
+ end
+
+ it "doesn't expire on other field changes" do
+ expect(namespace).not_to receive(:expire_child_caches)
+
+ namespace.update!(
+ description: "Foo bar",
+ max_artifacts_size: 10
+ )
+ end
+ end
+
describe '#visibility_level_field' do
it { expect(namespace.visibility_level_field).to eq(:visibility_level) }
end
@@ -224,6 +272,41 @@ RSpec.describe Namespace do
it { expect(namespace.human_name).to eq(namespace.owner_name) }
end
+ describe '#any_project_has_container_registry_tags?' do
+ subject { namespace.any_project_has_container_registry_tags? }
+
+ let!(:project_without_registry) { create(:project, namespace: namespace) }
+
+ context 'without tags' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'with tags' do
+ before do
+ repositories = create_list(:container_repository, 3)
+ create(:project, namespace: namespace, container_repositories: repositories)
+
+ stub_container_registry_config(enabled: true)
+ end
+
+ it 'finds tags' do
+ stub_container_registry_tags(repository: :any, tags: ['tag'])
+
+ is_expected.to be_truthy
+ end
+
+ it 'does not cause N+1 query in fetching registries' do
+ stub_container_registry_tags(repository: :any, tags: [])
+ control_count = ActiveRecord::QueryRecorder.new { namespace.any_project_has_container_registry_tags? }.count
+
+ other_repositories = create_list(:container_repository, 2)
+ create(:project, namespace: namespace, container_repositories: other_repositories)
+
+ expect { namespace.any_project_has_container_registry_tags? }.not_to exceed_query_limit(control_count + 1)
+ end
+ end
+ end
+
describe '#first_project_with_container_registry_tags' do
let(:container_repository) { create(:container_repository) }
let!(:project) { create(:project, namespace: namespace, container_repositories: [container_repository]) }
@@ -880,7 +963,7 @@ RSpec.describe Namespace do
end
describe '#use_traversal_ids?' do
- let_it_be(:namespace) { build(:namespace) }
+ let_it_be(:namespace, reload: true) { create(:namespace) }
subject { namespace.use_traversal_ids? }
@@ -901,30 +984,6 @@ RSpec.describe Namespace do
end
end
- context 'when use_traversal_ids feature flag is true' do
- it_behaves_like 'namespace traversal'
-
- describe '#self_and_descendants' do
- subject { namespace.self_and_descendants }
-
- it { expect(subject.to_sql).to include 'traversal_ids @>' }
- end
- end
-
- context 'when use_traversal_ids feature flag is false' do
- before do
- stub_feature_flags(use_traversal_ids: false)
- end
-
- it_behaves_like 'namespace traversal'
-
- describe '#self_and_descendants' do
- subject { namespace.self_and_descendants }
-
- it { expect(subject.to_sql).not_to include 'traversal_ids @>' }
- end
- end
-
describe '#users_with_descendants' do
let(:user_a) { create(:user) }
let(:user_b) { create(:user) }
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 992b2246f01..4eabc266b40 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -1384,6 +1384,16 @@ RSpec.describe Note do
expect(notes.second.id).to eq(note2.id)
end
end
+
+ describe '.with_suggestions' do
+ it 'returns the correct note' do
+ note_with_suggestion = create(:note, suggestions: [create(:suggestion)])
+ note_without_suggestion = create(:note)
+
+ expect(described_class.with_suggestions).to include(note_with_suggestion)
+ expect(described_class.with_suggestions).not_to include(note_without_suggestion)
+ end
+ end
end
describe '#noteable_assignee_or_author?' do
diff --git a/spec/models/packages/dependency_spec.rb b/spec/models/packages/dependency_spec.rb
index 4437cad46cd..1575dec98c9 100644
--- a/spec/models/packages/dependency_spec.rb
+++ b/spec/models/packages/dependency_spec.rb
@@ -18,6 +18,7 @@ RSpec.describe Packages::Dependency, type: :model do
let_it_be(:package_dependency1) { create(:packages_dependency, name: 'foo', version_pattern: '~1.0.0') }
let_it_be(:package_dependency2) { create(:packages_dependency, name: 'bar', version_pattern: '~2.5.0') }
let_it_be(:expected_ids) { [package_dependency1.id, package_dependency2.id] }
+
let(:names_and_version_patterns) { build_names_and_version_patterns(package_dependency1, package_dependency2) }
let(:chunk_size) { 50 }
let(:rows_limit) { 50 }
@@ -40,6 +41,7 @@ RSpec.describe Packages::Dependency, type: :model do
context 'with a name bigger than column size' do
let_it_be(:big_name) { 'a' * (Packages::Dependency::MAX_STRING_LENGTH + 1) }
+
let(:names_and_version_patterns) { build_names_and_version_patterns(package_dependency1, package_dependency2).merge(big_name => '~1.0.0') }
it { is_expected.to match_array(expected_ids) }
@@ -47,6 +49,7 @@ RSpec.describe Packages::Dependency, type: :model do
context 'with a version pattern bigger than column size' do
let_it_be(:big_version_pattern) { 'a' * (Packages::Dependency::MAX_STRING_LENGTH + 1) }
+
let(:names_and_version_patterns) { build_names_and_version_patterns(package_dependency1, package_dependency2).merge('test' => big_version_pattern) }
it { is_expected.to match_array(expected_ids) }
@@ -65,6 +68,7 @@ RSpec.describe Packages::Dependency, type: :model do
let_it_be(:package_dependency5) { create(:packages_dependency, name: 'foo5', version_pattern: '~1.5.5') }
let_it_be(:package_dependency6) { create(:packages_dependency, name: 'foo6', version_pattern: '~1.5.6') }
let_it_be(:package_dependency7) { create(:packages_dependency, name: 'foo7', version_pattern: '~1.5.7') }
+
let(:expected_ids) { [package_dependency1.id, package_dependency2.id, package_dependency3.id, package_dependency4.id, package_dependency5.id, package_dependency6.id, package_dependency7.id] }
let(:names_and_version_patterns) { build_names_and_version_patterns(package_dependency1, package_dependency2, package_dependency3, package_dependency4, package_dependency5, package_dependency6, package_dependency7) }
@@ -86,6 +90,7 @@ RSpec.describe Packages::Dependency, type: :model do
let_it_be(:package_dependency1) { create(:packages_dependency, name: 'foo', version_pattern: '~1.0.0') }
let_it_be(:package_dependency2) { create(:packages_dependency, name: 'bar', version_pattern: '~2.5.0') }
let_it_be(:expected_array) { [package_dependency1, package_dependency2] }
+
let(:names_and_version_patterns) { build_names_and_version_patterns(package_dependency1, package_dependency2) }
subject { Packages::Dependency.for_package_names_and_version_patterns(names_and_version_patterns) }
diff --git a/spec/models/packages/go/module_version_spec.rb b/spec/models/packages/go/module_version_spec.rb
index 7fa416d8537..cace2160878 100644
--- a/spec/models/packages/go/module_version_spec.rb
+++ b/spec/models/packages/go/module_version_spec.rb
@@ -32,16 +32,19 @@ RSpec.describe Packages::Go::ModuleVersion, type: :model do
describe '#name' do
context 'with ref and name specified' do
let_it_be(:version) { create :go_module_version, mod: mod, name: 'foobar', commit: project.repository.head_commit, ref: project.repository.find_tag('v1.0.0') }
+
it('returns that name') { expect(version.name).to eq('foobar') }
end
context 'with ref specified and name unspecified' do
let_it_be(:version) { create :go_module_version, mod: mod, commit: project.repository.head_commit, ref: project.repository.find_tag('v1.0.0') }
+
it('returns the name of the ref') { expect(version.name).to eq('v1.0.0') }
end
context 'with ref and name unspecified' do
let_it_be(:version) { create :go_module_version, mod: mod, commit: project.repository.head_commit }
+
it('returns nil') { expect(version.name).to eq(nil) }
end
end
@@ -49,11 +52,13 @@ RSpec.describe Packages::Go::ModuleVersion, type: :model do
describe '#gomod' do
context 'with go.mod missing' do
let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.0' }
+
it('returns nil') { expect(version.gomod).to eq(nil) }
end
context 'with go.mod present' do
let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.1' }
+
it('returns the contents of go.mod') { expect(version.gomod).to eq("module #{mod.name}\n") }
end
end
@@ -62,6 +67,7 @@ RSpec.describe Packages::Go::ModuleVersion, type: :model do
context 'with a root module' do
context 'with an empty module path' do
let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.2' }
+
it_behaves_like '#files', 'all the files', 'README.md', 'go.mod', 'a.go', 'pkg/b.go'
end
end
@@ -69,12 +75,14 @@ RSpec.describe Packages::Go::ModuleVersion, type: :model do
context 'with a root module and a submodule' do
context 'with an empty module path' do
let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.3' }
+
it_behaves_like '#files', 'files excluding the submodule', 'README.md', 'go.mod', 'a.go', 'pkg/b.go'
end
context 'with the submodule\'s path' do
let_it_be(:mod) { create :go_module, project: project, path: 'mod' }
let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.3' }
+
it_behaves_like '#files', 'the submodule\'s files', 'mod/go.mod', 'mod/a.go'
end
end
@@ -84,6 +92,7 @@ RSpec.describe Packages::Go::ModuleVersion, type: :model do
context 'with a root module' do
context 'with an empty module path' do
let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.2' }
+
it_behaves_like '#archive', 'all the files', 'README.md', 'go.mod', 'a.go', 'pkg/b.go'
end
end
@@ -91,12 +100,14 @@ RSpec.describe Packages::Go::ModuleVersion, type: :model do
context 'with a root module and a submodule' do
context 'with an empty module path' do
let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.3' }
+
it_behaves_like '#archive', 'files excluding the submodule', 'README.md', 'go.mod', 'a.go', 'pkg/b.go'
end
context 'with the submodule\'s path' do
let_it_be(:mod) { create :go_module, project: project, path: 'mod' }
let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.3' }
+
it_behaves_like '#archive', 'the submodule\'s files', 'go.mod', 'a.go'
end
end
diff --git a/spec/models/packages/helm/file_metadatum_spec.rb b/spec/models/packages/helm/file_metadatum_spec.rb
new file mode 100644
index 00000000000..c7c17b157e4
--- /dev/null
+++ b/spec/models/packages/helm/file_metadatum_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Helm::FileMetadatum, type: :model do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:package_file) }
+ end
+
+ describe 'validations' do
+ describe '#package_file' do
+ it { is_expected.to validate_presence_of(:package_file) }
+ end
+
+ describe '#valid_helm_package_type' do
+ let_it_be_with_reload(:helm_package_file) { create(:helm_package_file) }
+
+ let(:helm_file_metadatum) { helm_package_file.helm_file_metadatum }
+
+ before do
+ helm_package_file.package.package_type = :pypi
+ end
+
+ it 'validates package of type helm' do
+ expect(helm_file_metadatum).not_to be_valid
+ expect(helm_file_metadatum.errors.to_a).to contain_exactly('Package file Package type must be Helm')
+ end
+ end
+
+ describe '#channel' do
+ it 'validates #channel', :aggregate_failures do
+ is_expected.to validate_presence_of(:channel)
+
+ is_expected.to allow_value('a' * 63).for(:channel)
+ is_expected.not_to allow_value('a' * 64).for(:channel)
+
+ is_expected.to allow_value('release').for(:channel)
+ is_expected.to allow_value('my-repo').for(:channel)
+ is_expected.to allow_value('my-repo42').for(:channel)
+
+ # Do not allow empty
+ is_expected.not_to allow_value('').for(:channel)
+
+ # Do not allow Unicode
+ is_expected.not_to allow_value('hé').for(:channel)
+ end
+ end
+
+ describe '#metadata' do
+ it 'validates #metadata', :aggregate_failures do
+ is_expected.not_to validate_presence_of(:metadata)
+ is_expected.to allow_value({ 'name': 'foo', 'version': 'v1.0', 'apiVersion': 'v2' }).for(:metadata)
+ is_expected.not_to allow_value({}).for(:metadata)
+ is_expected.not_to allow_value({ 'version': 'v1.0', 'apiVersion': 'v2' }).for(:metadata)
+ is_expected.not_to allow_value({ 'name': 'foo', 'apiVersion': 'v2' }).for(:metadata)
+ is_expected.not_to allow_value({ 'name': 'foo', 'version': 'v1.0' }).for(:metadata)
+ end
+ end
+ end
+end
diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb
index 9cf998a0639..f8ddd59ddc8 100644
--- a/spec/models/packages/package_file_spec.rb
+++ b/spec/models/packages/package_file_spec.rb
@@ -2,12 +2,18 @@
require 'spec_helper'
RSpec.describe Packages::PackageFile, type: :model do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:package_file1) { create(:package_file, :xml, file_name: 'FooBar') }
+ let_it_be(:package_file2) { create(:package_file, :xml, file_name: 'ThisIsATest') }
+ let_it_be(:debian_package) { create(:debian_package, project: project) }
+
describe 'relationships' do
it { is_expected.to belong_to(:package) }
it { is_expected.to have_one(:conan_file_metadatum) }
it { is_expected.to have_many(:package_file_build_infos).inverse_of(:package_file) }
it { is_expected.to have_many(:pipelines).through(:package_file_build_infos) }
it { is_expected.to have_one(:debian_file_metadatum).inverse_of(:package_file).class_name('Packages::Debian::FileMetadatum') }
+ it { is_expected.to have_one(:helm_file_metadatum).inverse_of(:package_file).class_name('Packages::Helm::FileMetadatum') }
end
describe 'validations' do
@@ -15,9 +21,6 @@ RSpec.describe Packages::PackageFile, type: :model do
end
context 'with package filenames' do
- let_it_be(:package_file1) { create(:package_file, :xml, file_name: 'FooBar') }
- let_it_be(:package_file2) { create(:package_file, :xml, file_name: 'ThisIsATest') }
-
describe '.with_file_name' do
let(:filename) { 'FooBar' }
@@ -51,6 +54,13 @@ RSpec.describe Packages::PackageFile, type: :model do
end
end
+ describe '.for_package_ids' do
+ it 'returns matching packages' do
+ expect(described_class.for_package_ids([package_file1.package.id, package_file2.package.id]))
+ .to contain_exactly(package_file1, package_file2)
+ end
+ end
+
describe '.with_conan_package_reference' do
let_it_be(:non_matching_package_file) { create(:package_file, :nuget) }
let_it_be(:metadatum) { create(:conan_file_metadatum, :package_file) }
@@ -63,7 +73,6 @@ RSpec.describe Packages::PackageFile, type: :model do
end
describe '.for_rubygem_with_file_name' do
- let_it_be(:project) { create(:project) }
let_it_be(:non_ruby_package) { create(:nuget_package, project: project, package_type: :nuget) }
let_it_be(:ruby_package) { create(:rubygems_package, project: project, package_type: :rubygems) }
let_it_be(:file_name) { 'other.gem' }
@@ -77,6 +86,36 @@ RSpec.describe Packages::PackageFile, type: :model do
end
end
+ context 'Debian scopes' do
+ let_it_be(:debian_changes) { debian_package.package_files.last }
+ let_it_be(:debian_deb) { create(:debian_package_file, package: debian_package)}
+ let_it_be(:debian_udeb) { create(:debian_package_file, :udeb, package: debian_package)}
+
+ let_it_be(:debian_contrib) do
+ create(:debian_package_file, package: debian_package).tap do |pf|
+ pf.debian_file_metadatum.update!(component: 'contrib')
+ end
+ end
+
+ let_it_be(:debian_mipsel) do
+ create(:debian_package_file, package: debian_package).tap do |pf|
+ pf.debian_file_metadatum.update!(architecture: 'mipsel')
+ end
+ end
+
+ describe '#with_debian_file_type' do
+ it { expect(described_class.with_debian_file_type(:changes)).to contain_exactly(debian_changes) }
+ end
+
+ describe '#with_debian_component_name' do
+ it { expect(described_class.with_debian_component_name('contrib')).to contain_exactly(debian_contrib) }
+ end
+
+ describe '#with_debian_architecture_name' do
+ it { expect(described_class.with_debian_architecture_name('mipsel')).to contain_exactly(debian_mipsel) }
+ end
+ end
+
describe '#update_file_store callback' do
let_it_be(:package_file) { build(:package_file, :nuget, size: nil) }
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index cf52749a186..52ef61e3d44 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -47,6 +47,7 @@ RSpec.describe Packages::Package, type: :model do
describe '.sort_by_attribute' do
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public, namespace: group, name: 'project A') }
+
let!(:package1) { create(:npm_package, project: project, version: '3.1.0', name: "@#{project.root_namespace.path}/foo1") }
let!(:package2) { create(:nuget_package, project: project, version: '2.0.4') }
let(:package3) { create(:maven_package, project: project, version: '1.1.1', name: 'zzz') }
@@ -113,18 +114,6 @@ RSpec.describe Packages::Package, type: :model do
expect(subject).to match_array([package1, package2])
end
-
- context 'with maven_packages_group_level_improvements disabled' do
- before do
- stub_feature_flags(maven_packages_group_level_improvements: false)
- end
-
- it 'returns package1 and package2' do
- expect(projects).to receive(:any?).and_call_original
-
- expect(subject).to match_array([package1, package2])
- end
- end
end
describe 'validations' do
@@ -184,6 +173,15 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.not_to allow_value('!!().for(:name)().for(:name)').for(:name) }
end
+ context 'helm package' do
+ subject { build(:helm_package) }
+
+ it { is_expected.to allow_value('prometheus').for(:name) }
+ it { is_expected.to allow_value('rook-ceph').for(:name) }
+ it { is_expected.not_to allow_value('a+b').for(:name) }
+ it { is_expected.not_to allow_value('Hé').for(:name) }
+ end
+
context 'nuget package' do
subject { build_stubbed(:nuget_package) }
@@ -210,6 +208,19 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.not_to allow_value("@scope%2e%2e%fpackage").for(:name) }
it { is_expected.not_to allow_value("@scope/sub/package").for(:name) }
end
+
+ context 'terraform module package' do
+ subject { build_stubbed(:terraform_module_package) }
+
+ it { is_expected.to allow_value('my-module/my-system').for(:name) }
+ it { is_expected.to allow_value('my/module').for(:name) }
+ it { is_expected.not_to allow_value('my-module').for(:name) }
+ it { is_expected.not_to allow_value('My-Module').for(:name) }
+ it { is_expected.not_to allow_value('my_module').for(:name) }
+ it { is_expected.not_to allow_value('my.module').for(:name) }
+ it { is_expected.not_to allow_value('../../../my-module').for(:name) }
+ it { is_expected.not_to allow_value('%2e%2e%2fmy-module').for(:name) }
+ end
end
describe '#version' do
@@ -387,7 +398,17 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.not_to allow_value(nil).for(:version) }
end
+ context 'helm package' do
+ subject { build_stubbed(:helm_package) }
+
+ it { is_expected.not_to allow_value(nil).for(:version) }
+ it { is_expected.not_to allow_value('').for(:version) }
+ it { is_expected.to allow_value('v1.2.3').for(:version) }
+ it { is_expected.not_to allow_value('1.2.3').for(:version) }
+ end
+
it_behaves_like 'validating version to be SemVer compliant for', :npm_package
+ it_behaves_like 'validating version to be SemVer compliant for', :terraform_module_package
context 'nuget package' do
it_behaves_like 'validating version to be SemVer compliant for', :nuget_package
@@ -485,6 +506,26 @@ RSpec.describe Packages::Package, type: :model do
end
end
+ describe '.with_package_type' do
+ let!(:package1) { create(:terraform_module_package) }
+ let!(:package2) { create(:npm_package) }
+ let(:package_type) { :terraform_module }
+
+ subject { described_class.with_package_type(package_type) }
+
+ it { is_expected.to eq([package1]) }
+ end
+
+ describe '.without_package_type' do
+ let!(:package1) { create(:npm_package) }
+ let!(:package2) { create(:terraform_module_package) }
+ let(:package_type) { :terraform_module }
+
+ subject { described_class.without_package_type(package_type) }
+
+ it { is_expected.to eq([package1]) }
+ end
+
context 'version scopes' do
let!(:package1) { create(:npm_package, version: '1.0.0') }
let!(:package2) { create(:npm_package, version: '1.0.1') }
@@ -565,22 +606,6 @@ RSpec.describe Packages::Package, type: :model do
end
end
- describe '.processed' do
- let!(:package1) { create(:nuget_package) }
- let!(:package2) { create(:npm_package) }
- let!(:package3) { create(:nuget_package) }
-
- subject { described_class.processed }
-
- it { is_expected.to match_array([package1, package2, package3]) }
-
- context 'with temporary packages' do
- let!(:package1) { create(:nuget_package, name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
-
- it { is_expected.to match_array([package2, package3]) }
- end
- end
-
describe '.limit_recent' do
let!(:package1) { create(:nuget_package) }
let!(:package2) { create(:nuget_package) }
@@ -653,27 +678,37 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.to match_array([pypi_package]) }
end
- describe '.displayable' do
+ context 'status scopes' do
let_it_be(:hidden_package) { create(:maven_package, :hidden) }
let_it_be(:processing_package) { create(:maven_package, :processing) }
let_it_be(:error_package) { create(:maven_package, :error) }
- subject { described_class.displayable }
+ describe '.displayable' do
+ subject { described_class.displayable }
- it 'does not include non-displayable packages', :aggregate_failures do
- is_expected.to include(error_package)
- is_expected.not_to include(hidden_package)
- is_expected.not_to include(processing_package)
+ it 'does not include non-displayable packages', :aggregate_failures do
+ is_expected.to include(error_package)
+ is_expected.not_to include(hidden_package)
+ is_expected.not_to include(processing_package)
+ end
end
- end
- describe '.with_status' do
- let_it_be(:hidden_package) { create(:maven_package, :hidden) }
+ describe '.installable' do
+ subject { described_class.installable }
- subject { described_class.with_status(:hidden) }
+ it 'does not include non-displayable packages', :aggregate_failures do
+ is_expected.not_to include(error_package)
+ is_expected.not_to include(hidden_package)
+ is_expected.not_to include(processing_package)
+ end
+ end
+
+ describe '.with_status' do
+ subject { described_class.with_status(:hidden) }
- it 'returns packages with specified status' do
- is_expected.to match_array([hidden_package])
+ it 'returns packages with specified status' do
+ is_expected.to match_array([hidden_package])
+ end
end
end
end
@@ -896,6 +931,7 @@ RSpec.describe Packages::Package, type: :model do
let_it_be(:package_name) { 'composer-package-name' }
let_it_be(:json) { { 'name' => package_name } }
let_it_be(:project) { create(:project, :custom_repo, files: { 'composer.json' => json.to_json } ) }
+
let!(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) }
before do
diff --git a/spec/models/packages/tag_spec.rb b/spec/models/packages/tag_spec.rb
index 18ec99c3d51..842ba7ad518 100644
--- a/spec/models/packages/tag_spec.rb
+++ b/spec/models/packages/tag_spec.rb
@@ -41,6 +41,7 @@ RSpec.describe Packages::Tag, type: :model do
let_it_be(:tag1) { create(:packages_tag, package: package, name: 'tag1') }
let_it_be(:tag2) { create(:packages_tag, package: package, name: 'tag2') }
let_it_be(:tag3) { create(:packages_tag, package: package, name: 'tag3') }
+
let(:name) { 'tag1' }
subject { described_class.with_name(name) }
diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb
index f2659771a49..735f2225c21 100644
--- a/spec/models/pages/lookup_path_spec.rb
+++ b/spec/models/pages/lookup_path_spec.rb
@@ -47,20 +47,13 @@ RSpec.describe Pages::LookupPath do
describe '#source' do
let(:source) { lookup_path.source }
- shared_examples 'uses disk storage' do
- it 'uses disk storage', :aggregate_failures do
- expect(source[:type]).to eq('file')
- expect(source[:path]).to eq(project.full_path + "/public/")
- end
+ it 'uses disk storage', :aggregate_failures do
+ expect(source[:type]).to eq('file')
+ expect(source[:path]).to eq(project.full_path + "/public/")
end
- include_examples 'uses disk storage'
-
- it 'return nil when legacy storage is disabled and there is no deployment' do
- stub_feature_flags(pages_serve_from_legacy_storage: false)
- expect(Gitlab::ErrorTracking).to receive(:track_exception)
- .with(described_class::LegacyStorageDisabledError, project_id: project.id)
- .and_call_original
+ it 'return nil when local storage is disabled and there is no deployment' do
+ allow(Settings.pages.local_store).to receive(:enabled).and_return(false)
expect(source).to eq(nil)
end
@@ -107,14 +100,6 @@ RSpec.describe Pages::LookupPath do
)
end
end
-
- context 'when pages_serve_with_zip_file_protocol feature flag is disabled' do
- before do
- stub_feature_flags(pages_serve_with_zip_file_protocol: false)
- end
-
- include_examples 'uses disk storage'
- end
end
context 'when deployment were created during migration' do
diff --git a/spec/models/plan_limits_spec.rb b/spec/models/plan_limits_spec.rb
index 4259c8b708b..b8c723b3847 100644
--- a/spec/models/plan_limits_spec.rb
+++ b/spec/models/plan_limits_spec.rb
@@ -210,6 +210,7 @@ RSpec.describe PlanLimits do
ci_active_jobs
storage_size_limit
daily_invites
+ web_hook_calls
] + disabled_max_artifact_size_columns
end
diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb
index 8313879114f..d5f0b66b210 100644
--- a/spec/models/project_auto_devops_spec.rb
+++ b/spec/models/project_auto_devops_spec.rb
@@ -70,7 +70,7 @@ RSpec.describe ProjectAutoDevops do
it 'does not create a gitlab deploy token' do
expect do
- auto_devops.save
+ auto_devops.save!
end.not_to change { DeployToken.count }
end
end
@@ -80,7 +80,7 @@ RSpec.describe ProjectAutoDevops do
it 'creates a gitlab deploy token' do
expect do
- auto_devops.save
+ auto_devops.save!
end.to change { DeployToken.count }.by(1)
end
end
@@ -90,7 +90,7 @@ RSpec.describe ProjectAutoDevops do
it 'creates a gitlab deploy token' do
expect do
- auto_devops.save
+ auto_devops.save!
end.to change { DeployToken.count }.by(1)
end
end
@@ -101,7 +101,7 @@ RSpec.describe ProjectAutoDevops do
it 'creates a deploy token' do
expect do
- auto_devops.save
+ auto_devops.save!
end.to change { DeployToken.count }.by(1)
end
end
@@ -114,7 +114,7 @@ RSpec.describe ProjectAutoDevops do
allow(Gitlab::CurrentSettings).to receive(:auto_devops_enabled?).and_return(true)
expect do
- auto_devops.save
+ auto_devops.save!
end.to change { DeployToken.count }.by(1)
end
end
@@ -125,7 +125,7 @@ RSpec.describe ProjectAutoDevops do
it 'does not create a deploy token' do
expect do
- auto_devops.save
+ auto_devops.save!
end.not_to change { DeployToken.count }
end
end
@@ -137,7 +137,7 @@ RSpec.describe ProjectAutoDevops do
it 'does not create a deploy token' do
expect do
- auto_devops.save
+ auto_devops.save!
end.not_to change { DeployToken.count }
end
end
@@ -149,7 +149,7 @@ RSpec.describe ProjectAutoDevops do
it 'does not create a deploy token' do
expect do
- auto_devops.save
+ auto_devops.save!
end.not_to change { DeployToken.count }
end
end
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index a56018f0fee..3fd7e57a5db 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe ProjectFeature do
context 'repository related features' do
before do
- project.project_feature.update(
+ project.project_feature.update!(
merge_requests_access_level: ProjectFeature::DISABLED,
builds_access_level: ProjectFeature::DISABLED,
repository_access_level: ProjectFeature::PRIVATE
diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb
index 476d99364b6..62f97873a06 100644
--- a/spec/models/project_services/chat_notification_service_spec.rb
+++ b/spec/models/project_services/chat_notification_service_spec.rb
@@ -11,6 +11,10 @@ RSpec.describe ChatNotificationService do
it { is_expected.to validate_presence_of :webhook }
end
+ describe 'validations' do
+ it { is_expected.to validate_inclusion_of(:labels_to_be_notified_behavior).in_array(%w[match_any match_all]).allow_blank }
+ end
+
describe '#can_test?' do
context 'with empty repository' do
it 'returns true' do
@@ -32,8 +36,9 @@ RSpec.describe ChatNotificationService do
describe '#execute' do
subject(:chat_service) { described_class.new }
+ let_it_be(:project) { create(:project, :repository) }
+
let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
let(:webhook_url) { 'https://example.gitlab.com/' }
let(:data) { Gitlab::DataBuilder::Push.build_sample(subject.project, user) }
@@ -76,9 +81,12 @@ RSpec.describe ChatNotificationService do
end
context 'when the data object has a label' do
- let(:label) { create(:label, project: project, name: 'Bug')}
- let(:issue) { create(:labeled_issue, project: project, labels: [label]) }
- let(:note) { create(:note, noteable: issue, project: project)}
+ let_it_be(:label) { create(:label, name: 'Bug') }
+ let_it_be(:label_2) { create(:label, name: 'Community contribution') }
+ let_it_be(:label_3) { create(:label, name: 'Backend') }
+ let_it_be(:issue) { create(:labeled_issue, project: project, labels: [label, label_2, label_3]) }
+ let_it_be(:note) { create(:note, noteable: issue, project: project) }
+
let(:data) { Gitlab::DataBuilder::Note.build(note, user) }
it 'notifies the chat service' do
@@ -87,23 +95,139 @@ RSpec.describe ChatNotificationService do
chat_service.execute(data)
end
- context 'and the chat_service has a label filter that does not matches the label' do
- subject(:chat_service) { described_class.new(labels_to_be_notified: '~some random label') }
+ shared_examples 'notifies the chat service' do
+ specify do
+ expect(chat_service).to receive(:notify).with(any_args)
+
+ chat_service.execute(data)
+ end
+ end
- it 'does not notify the chat service' do
- expect(chat_service).not_to receive(:notify)
+ shared_examples 'does not notify the chat service' do
+ specify do
+ expect(chat_service).not_to receive(:notify).with(any_args)
chat_service.execute(data)
end
end
- context 'and the chat_service has a label filter that matches the label' do
- subject(:chat_service) { described_class.new(labels_to_be_notified: '~Backend, ~Bug') }
+ context 'when labels_to_be_notified_behavior is not defined' do
+ subject(:chat_service) { described_class.new(labels_to_be_notified: label_filter) }
- it 'notifies the chat service' do
- expect(chat_service).to receive(:notify).with(any_args)
+ context 'no matching labels' do
+ let(:label_filter) { '~some random label' }
- chat_service.execute(data)
+ it_behaves_like 'does not notify the chat service'
+ end
+
+ context 'only one label matches' do
+ let(:label_filter) { '~some random label, ~Bug' }
+
+ it_behaves_like 'notifies the chat service'
+ end
+ end
+
+ context 'when labels_to_be_notified_behavior is blank' do
+ subject(:chat_service) { described_class.new(labels_to_be_notified: label_filter, labels_to_be_notified_behavior: '') }
+
+ context 'no matching labels' do
+ let(:label_filter) { '~some random label' }
+
+ it_behaves_like 'does not notify the chat service'
+ end
+
+ context 'only one label matches' do
+ let(:label_filter) { '~some random label, ~Bug' }
+
+ it_behaves_like 'notifies the chat service'
+ end
+ end
+
+ context 'when labels_to_be_notified_behavior is match_any' do
+ subject(:chat_service) do
+ described_class.new(
+ labels_to_be_notified: label_filter,
+ labels_to_be_notified_behavior: 'match_any'
+ )
+ end
+
+ context 'no label filter' do
+ let(:label_filter) { nil }
+
+ it_behaves_like 'notifies the chat service'
+ end
+
+ context 'no matching labels' do
+ let(:label_filter) { '~some random label' }
+
+ it_behaves_like 'does not notify the chat service'
+ end
+
+ context 'only one label matches' do
+ let(:label_filter) { '~some random label, ~Bug' }
+
+ it_behaves_like 'notifies the chat service'
+ end
+ end
+
+ context 'when labels_to_be_notified_behavior is match_all' do
+ subject(:chat_service) do
+ described_class.new(
+ labels_to_be_notified: label_filter,
+ labels_to_be_notified_behavior: 'match_all'
+ )
+ end
+
+ context 'no label filter' do
+ let(:label_filter) { nil }
+
+ it_behaves_like 'notifies the chat service'
+ end
+
+ context 'no matching labels' do
+ let(:label_filter) { '~some random label' }
+
+ it_behaves_like 'does not notify the chat service'
+ end
+
+ context 'only one label matches' do
+ let(:label_filter) { '~some random label, ~Bug' }
+
+ it_behaves_like 'does not notify the chat service'
+ end
+
+ context 'labels matches exactly' do
+ let(:label_filter) { '~Bug, ~Backend, ~Community contribution' }
+
+ it_behaves_like 'notifies the chat service'
+ end
+
+ context 'labels matches but object has more' do
+ let(:label_filter) { '~Bug, ~Backend' }
+
+ it_behaves_like 'notifies the chat service'
+ end
+
+ context 'labels are distributed on multiple objects' do
+ let(:label_filter) { '~Bug, ~Backend' }
+ let(:data) do
+ Gitlab::DataBuilder::Note.build(note, user).merge({
+ issue: {
+ labels: [
+ { title: 'Bug' }
+ ]
+ },
+ merge_request: {
+ labels: [
+ {
+ title: 'Backend'
+ }
+ ]
+ }
+ })
+ end
+
+ it_behaves_like 'does not notify the chat service'
end
end
end
diff --git a/spec/models/project_services/data_fields_spec.rb b/spec/models/project_services/data_fields_spec.rb
index 9a3042f9f8d..d3e6afe4978 100644
--- a/spec/models/project_services/data_fields_spec.rb
+++ b/spec/models/project_services/data_fields_spec.rb
@@ -138,8 +138,8 @@ RSpec.describe DataFields do
context 'when data are stored in both properties and data_fields' do
let(:service) do
- create(:jira_service, :without_properties_callback, active: false, properties: properties).tap do |service|
- create(:jira_tracker_data, properties.merge(service: service))
+ create(:jira_service, :without_properties_callback, active: false, properties: properties).tap do |integration|
+ create(:jira_tracker_data, properties.merge(integration: integration))
end
end
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 82a4cde752b..42368c31ba0 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -2,91 +2,35 @@
require 'spec_helper'
+# HipchatService is partially removed and it will be remove completely
+# after the deletion of all the database records.
+# https://gitlab.com/gitlab-org/gitlab/-/issues/27954
RSpec.describe HipchatService do
- describe "Associations" do
- it { is_expected.to belong_to :project }
- it { is_expected.to have_one :service_hook }
- end
+ let_it_be(:project) { create(:project) }
- describe 'Validations' do
- context 'when service is active' do
- before do
- subject.active = true
- end
+ subject(:service) { described_class.new(project: project) }
- it { is_expected.to validate_presence_of(:token) }
- end
+ it { is_expected.to be_valid }
- context 'when service is inactive' do
- before do
- subject.active = false
- end
+ describe '#to_param' do
+ subject { service.to_param }
- it { is_expected.not_to validate_presence_of(:token) }
- end
+ it { is_expected.to eq('hipchat') }
end
- describe "Execute" do
- let(:hipchat) { described_class.new }
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
- let(:api_url) { 'https://hipchat.example.com/v2/room/123456/notification?auth_token=verySecret' }
- let(:project_name) { project.full_name.gsub(/\s/, '') }
- let(:token) { 'verySecret' }
- let(:server_url) { 'https://hipchat.example.com'}
- let(:push_sample_data) do
- Gitlab::DataBuilder::Push.build_sample(project, user)
- end
-
- before do
- allow(hipchat).to receive_messages(
- project_id: project.id,
- project: project,
- room: 123456,
- server: server_url,
- token: token
- )
- WebMock.stub_request(:post, api_url)
- end
-
- it 'does nothing' do
- expect { hipchat.execute(push_sample_data) }.not_to raise_error
- end
+ describe '#supported_events' do
+ subject { service.supported_events }
- describe "#message_options" do
- it "is set to the defaults" do
- expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'yellow' })
- end
-
- it "sets notify to true" do
- allow(hipchat).to receive(:notify).and_return('1')
-
- expect(hipchat.__send__(:message_options)).to eq({ notify: true, color: 'yellow' })
- end
-
- it "sets the color" do
- allow(hipchat).to receive(:color).and_return('red')
-
- expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'red' })
- end
-
- context 'with a successful build' do
- it 'uses the green color' do
- data = { object_kind: 'pipeline',
- object_attributes: { status: 'success' } }
-
- expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'green' })
- end
- end
+ it { is_expected.to be_empty }
+ end
- context 'with a failed build' do
- it 'uses the red color' do
- data = { object_kind: 'pipeline',
- object_attributes: { status: 'failed' } }
+ describe '#save' do
+ it 'prevents records from being created or updated' do
+ expect(service.save).to be_falsey
- expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'red' })
- end
- end
+ expect(service.errors.full_messages).to include(
+ 'HipChat endpoint is deprecated and should not be created or modified.'
+ )
end
end
end
diff --git a/spec/models/project_services/issue_tracker_data_spec.rb b/spec/models/project_services/issue_tracker_data_spec.rb
index 3ddb7d9250f..a229285f09b 100644
--- a/spec/models/project_services/issue_tracker_data_spec.rb
+++ b/spec/models/project_services/issue_tracker_data_spec.rb
@@ -3,9 +3,7 @@
require 'spec_helper'
RSpec.describe IssueTrackerData do
- let(:service) { create(:custom_issue_tracker_service, active: false, properties: {}) }
-
- describe 'Associations' do
- it { is_expected.to belong_to :service }
+ describe 'associations' do
+ it { is_expected.to belong_to :integration }
end
end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index b50fa1edbc3..73e91bf9ea8 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -433,8 +433,8 @@ RSpec.describe JiraService do
context 'when data are stored in both properties and separated fields' do
let(:properties) { data_params }
let(:service) do
- create(:jira_service, :without_properties_callback, active: false, properties: properties).tap do |service|
- create(:jira_tracker_data, data_params.merge(service: service))
+ create(:jira_service, :without_properties_callback, active: false, properties: properties).tap do |integration|
+ create(:jira_tracker_data, data_params.merge(integration: integration))
end
end
diff --git a/spec/models/project_services/jira_tracker_data_spec.rb b/spec/models/project_services/jira_tracker_data_spec.rb
index a698d3fce5f..72bdbe40a74 100644
--- a/spec/models/project_services/jira_tracker_data_spec.rb
+++ b/spec/models/project_services/jira_tracker_data_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe JiraTrackerData do
describe 'associations' do
- it { is_expected.to belong_to(:service) }
+ it { is_expected.to belong_to(:integration) }
end
describe 'deployment_type' do
diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
index 4fff3bc56cc..87befdd4303 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -49,7 +49,7 @@ RSpec.describe MattermostSlashCommandsService do
end
it 'saves the service' do
- expect { subject }.to change { project.services.count }.by(1)
+ expect { subject }.to change { project.integrations.count }.by(1)
end
it 'saves the token' do
diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb
index 53ab63ef030..5f3a94a5b99 100644
--- a/spec/models/project_services/microsoft_teams_service_spec.rb
+++ b/spec/models/project_services/microsoft_teams_service_spec.rb
@@ -73,7 +73,7 @@ RSpec.describe MicrosoftTeamsService do
context 'with issue events' do
let(:opts) { { title: 'Awesome issue', description: 'please fix' } }
let(:issues_sample_data) do
- service = Issues::CreateService.new(project, user, opts)
+ service = Issues::CreateService.new(project: project, current_user: user, params: opts)
issue = service.execute
service.hook_data(issue, 'open')
end
@@ -96,7 +96,7 @@ RSpec.describe MicrosoftTeamsService do
end
let(:merge_sample_data) do
- service = MergeRequests::CreateService.new(project, user, opts)
+ service = MergeRequests::CreateService.new(project: project, current_user: user, params: opts)
merge_request = service.execute
service.hook_data(merge_request, 'open')
end
@@ -240,7 +240,7 @@ RSpec.describe MicrosoftTeamsService do
chat_service.execute(data)
- message = ChatMessage::PipelineMessage.new(data)
+ message = Integrations::ChatMessage::PipelineMessage.new(data)
expect(WebMock).to have_requested(:post, webhook_url)
.with(body: hash_including({ summary: message.summary }))
diff --git a/spec/models/project_services/open_project_tracker_data_spec.rb b/spec/models/project_services/open_project_tracker_data_spec.rb
index e6a3963ba87..1f7f01cfea4 100644
--- a/spec/models/project_services/open_project_tracker_data_spec.rb
+++ b/spec/models/project_services/open_project_tracker_data_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
RSpec.describe OpenProjectTrackerData do
- describe 'Associations' do
- it { is_expected.to belong_to(:service) }
+ describe 'associations' do
+ it { is_expected.to belong_to(:integration) }
end
describe 'closed_status_id' do
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb
index 688a59fcf09..2e2c1c666d9 100644
--- a/spec/models/project_services/slack_service_spec.rb
+++ b/spec/models/project_services/slack_service_spec.rb
@@ -59,16 +59,22 @@ RSpec.describe SlackService do
context 'deployment notification' do
let_it_be(:deployment) { create(:deployment, user: user) }
- let(:data) { Gitlab::DataBuilder::Deployment.build(deployment) }
+ let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, Time.current) }
it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_deployment_notification'
end
context 'wiki_page notification' do
- let_it_be(:wiki_page) { create(:wiki_page, wiki: project.wiki, message: 'user created page: Awesome wiki_page') }
+ let(:wiki_page) { create(:wiki_page, wiki: project.wiki, message: 'user created page: Awesome wiki_page') }
let(:data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, 'create') }
+ before do
+ # Skip this method that is not relevant to this test to prevent having
+ # to update project which is frozen
+ allow(project.wiki).to receive(:after_wiki_activity)
+ end
+
it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_wiki_page_notification'
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 12c17e699e3..c57c2792f87 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to belong_to(:creator).class_name('User') }
it { is_expected.to belong_to(:pool_repository) }
it { is_expected.to have_many(:users) }
- it { is_expected.to have_many(:services) }
+ it { is_expected.to have_many(:integrations) }
it { is_expected.to have_many(:events) }
it { is_expected.to have_many(:merge_requests) }
it { is_expected.to have_many(:merge_request_metrics).class_name('MergeRequest::Metrics') }
@@ -46,13 +46,13 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to have_one(:asana_service) }
it { is_expected.to have_many(:boards) }
it { is_expected.to have_one(:campfire_service) }
+ it { is_expected.to have_one(:datadog_service) }
it { is_expected.to have_one(:discord_service) }
it { is_expected.to have_one(:drone_ci_service) }
it { is_expected.to have_one(:emails_on_push_service) }
it { is_expected.to have_one(:pipelines_email_service) }
it { is_expected.to have_one(:irker_service) }
it { is_expected.to have_one(:pivotaltracker_service) }
- it { is_expected.to have_one(:hipchat_service) }
it { is_expected.to have_one(:flowdock_service) }
it { is_expected.to have_one(:assembla_service) }
it { is_expected.to have_one(:slack_slash_commands_service) }
@@ -114,7 +114,8 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to have_many(:lfs_file_locks) }
it { is_expected.to have_many(:project_deploy_tokens) }
it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) }
- it { is_expected.to have_many(:cycle_analytics_stages) }
+ it { is_expected.to have_many(:cycle_analytics_stages).inverse_of(:project) }
+ it { is_expected.to have_many(:value_streams).inverse_of(:project) }
it { is_expected.to have_many(:external_pull_requests) }
it { is_expected.to have_many(:sourced_pipelines) }
it { is_expected.to have_many(:source_pipelines) }
@@ -131,6 +132,7 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to have_many(:debian_distributions).class_name('Packages::Debian::ProjectDistribution').dependent(:destroy) }
it { is_expected.to have_many(:pipeline_artifacts) }
it { is_expected.to have_many(:terraform_states).class_name('Terraform::State').inverse_of(:project) }
+ it { is_expected.to have_many(:timelogs) }
# GitLab Pages
it { is_expected.to have_many(:pages_domains) }
@@ -215,7 +217,7 @@ RSpec.describe Project, factory_default: :keep do
it 'does not raise an error' do
project = create(:project)
- expect { project.update(ci_cd_settings: nil) }.not_to raise_exception
+ expect { project.update!(ci_cd_settings: nil) }.not_to raise_exception
end
end
@@ -873,13 +875,13 @@ RSpec.describe Project, factory_default: :keep do
end
it 'returns the most recent timestamp' do
- project.update(updated_at: nil,
+ project.update!(updated_at: nil,
last_activity_at: timestamp,
last_repository_updated_at: timestamp - 1.hour)
expect(project.last_activity_date).to be_like_time(timestamp)
- project.update(updated_at: timestamp,
+ project.update!(updated_at: timestamp,
last_activity_at: timestamp - 1.hour,
last_repository_updated_at: nil)
@@ -1076,14 +1078,14 @@ RSpec.describe Project, factory_default: :keep do
it 'returns nil and does not query services when there is no external issue tracker' do
project = create(:project)
- expect(project).not_to receive(:services)
+ expect(project).not_to receive(:integrations)
expect(project.external_issue_tracker).to eq(nil)
end
it 'retrieves external_issue_tracker querying services and cache it when there is external issue tracker' do
project = create(:redmine_project)
- expect(project).to receive(:services).once.and_call_original
+ expect(project).to receive(:integrations).once.and_call_original
2.times { expect(project.external_issue_tracker).to be_a_kind_of(RedmineService) }
end
end
@@ -1116,7 +1118,7 @@ RSpec.describe Project, factory_default: :keep do
it 'becomes false when external issue tracker service is destroyed' do
expect do
- Service.find(service.id).delete
+ Integration.find(service.id).delete
end.to change { subject }.to(false)
end
@@ -1133,7 +1135,7 @@ RSpec.describe Project, factory_default: :keep do
it 'does not become false when external issue tracker service is destroyed' do
expect do
- Service.find(service.id).delete
+ Integration.find(service.id).delete
end.not_to change { subject }
end
@@ -1191,7 +1193,7 @@ RSpec.describe Project, factory_default: :keep do
it 'becomes false if the external wiki service is destroyed' do
expect do
- Service.find(service.id).delete
+ Integration.find(service.id).delete
end.to change { subject }.to(false)
end
@@ -1277,7 +1279,9 @@ RSpec.describe Project, factory_default: :keep do
it 'is false if avatar is html page' do
project.update_attribute(:avatar, 'uploads/avatar.html')
- expect(project.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp'])
+ project.avatar_type
+
+ expect(project.errors.added?(:avatar, "file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp")).to be true
end
end
@@ -2670,7 +2674,7 @@ RSpec.describe Project, factory_default: :keep do
context 'with pending pipeline' do
it 'returns empty relation' do
- pipeline.update(status: 'pending')
+ pipeline.update!(status: 'pending')
pending_build = create_build(pipeline)
expect { project.latest_successful_build_for_ref!(pending_build.name) }
@@ -2813,11 +2817,11 @@ RSpec.describe Project, factory_default: :keep do
end
describe '#remove_import_data' do
- let_it_be(:import_data) { ProjectImportData.new(data: { 'test' => 'some data' }) }
+ let(:import_data) { ProjectImportData.new(data: { 'test' => 'some data' }) }
context 'when jira import' do
- let_it_be(:project, reload: true) { create(:project, import_type: 'jira', import_data: import_data) }
- let_it_be(:jira_import) { create(:jira_import_state, project: project) }
+ let!(:project) { create(:project, import_type: 'jira', import_data: import_data) }
+ let!(:jira_import) { create(:jira_import_state, project: project) }
it 'does remove import data' do
expect(project.mirror?).to be false
@@ -2827,8 +2831,7 @@ RSpec.describe Project, factory_default: :keep do
end
context 'when neither a mirror nor a jira import' do
- let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project, import_type: 'github', import_data: import_data) }
+ let!(:project) { create(:project, import_type: 'github', import_data: import_data) }
it 'removes import data' do
expect(project.mirror?).to be false
@@ -2864,7 +2867,7 @@ RSpec.describe Project, factory_default: :keep do
end
it 'returns false when remote mirror is disabled' do
- project.remote_mirrors.first.update(enabled: false)
+ project.remote_mirrors.first.update!(enabled: false)
is_expected.to be_falsy
end
@@ -2895,7 +2898,7 @@ RSpec.describe Project, factory_default: :keep do
end
it 'does not sync disabled remote mirrors' do
- project.remote_mirrors.first.update(enabled: false)
+ project.remote_mirrors.first.update!(enabled: false)
expect_any_instance_of(RemoteMirror).not_to receive(:sync)
@@ -2933,7 +2936,7 @@ RSpec.describe Project, factory_default: :keep do
it 'fails stuck remote mirrors' do
project = create(:project, :repository, :remote_mirror)
- project.remote_mirrors.first.update(
+ project.remote_mirrors.first.update!(
update_status: :started,
last_update_started_at: 2.days.ago
)
@@ -3191,7 +3194,7 @@ RSpec.describe Project, factory_default: :keep do
end
it 'returns the root of the fork network when the directs source was deleted' do
- forked_project.destroy
+ forked_project.destroy!
expect(second_fork.fork_source).to eq(project)
end
@@ -3435,7 +3438,7 @@ RSpec.describe Project, factory_default: :keep do
let(:environment) { 'foo%bar/test' }
it 'matches literally for _' do
- ci_variable.update(environment_scope: 'foo%bar/*')
+ ci_variable.environment_scope = 'foo%bar/*'
is_expected.to contain_exactly(ci_variable)
end
@@ -3676,7 +3679,7 @@ RSpec.describe Project, factory_default: :keep do
it "updates the namespace_id when changed" do
namespace = create(:namespace)
- project.update(namespace: namespace)
+ project.update!(namespace: namespace)
expect(project.statistics.namespace_id).to eq namespace.id
end
@@ -3969,14 +3972,14 @@ RSpec.describe Project, factory_default: :keep do
expect(project).to receive(:visibility_level_allowed_as_fork).and_call_original
expect(project).to receive(:visibility_level_allowed_by_group).and_call_original
- project.update(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
it 'does not validate the visibility' do
expect(project).not_to receive(:visibility_level_allowed_as_fork).and_call_original
expect(project).not_to receive(:visibility_level_allowed_by_group).and_call_original
- project.update(updated_at: Time.current)
+ project.update!(updated_at: Time.current)
end
end
@@ -4060,7 +4063,7 @@ RSpec.describe Project, factory_default: :keep do
project_2 = create(:project, :public, :merge_requests_disabled)
project_3 = create(:project, :public, :issues_disabled)
project_4 = create(:project, :public)
- project_4.project_feature.update(issues_access_level: ProjectFeature::PRIVATE, merge_requests_access_level: ProjectFeature::PRIVATE )
+ project_4.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE, merge_requests_access_level: ProjectFeature::PRIVATE )
project_ids = described_class.ids_with_issuables_available_for(user).pluck(:id)
@@ -4103,7 +4106,7 @@ RSpec.describe Project, factory_default: :keep do
let(:project) { create(:project, :public) }
it 'returns projects with the project feature access level nil' do
- project.project_feature.update(merge_requests_access_level: nil)
+ project.project_feature.update!(merge_requests_access_level: nil)
is_expected.to include(project)
end
@@ -4391,7 +4394,7 @@ RSpec.describe Project, factory_default: :keep do
it 'is run when the project is destroyed' do
expect(project).to receive(:legacy_remove_pages).and_call_original
- expect { project.destroy }.not_to raise_error
+ expect { project.destroy! }.not_to raise_error
end
end
@@ -4921,7 +4924,7 @@ RSpec.describe Project, factory_default: :keep do
context 'when enabled on group' do
it 'has auto devops implicitly enabled' do
- project.update(namespace: create(:group, :auto_devops_enabled))
+ project.update!(namespace: create(:group, :auto_devops_enabled))
expect(project).to have_auto_devops_implicitly_enabled
end
@@ -4930,7 +4933,7 @@ RSpec.describe Project, factory_default: :keep do
context 'when enabled on parent group' do
it 'has auto devops implicitly enabled' do
subgroup = create(:group, parent: create(:group, :auto_devops_enabled))
- project.update(namespace: subgroup)
+ project.update!(namespace: subgroup)
expect(project).to have_auto_devops_implicitly_enabled
end
@@ -5404,7 +5407,7 @@ RSpec.describe Project, factory_default: :keep do
before do
create_list(:group_badge, 2, group: project_group)
- project_group.update(parent: parent_group)
+ project_group.update!(parent: parent_group)
end
it 'returns the project and the project nested groups badges' do
@@ -5799,16 +5802,16 @@ RSpec.describe Project, factory_default: :keep do
end
it 'avoids N+1 database queries with more available services' do
- allow(Service).to receive(:available_services_names).and_return(%w[pushover])
+ allow(Integration).to receive(:available_services_names).and_return(%w[pushover])
control_count = ActiveRecord::QueryRecorder.new { subject.find_or_initialize_services }
- allow(Service).to receive(:available_services_names).and_call_original
+ allow(Integration).to receive(:available_services_names).and_call_original
expect { subject.find_or_initialize_services }.not_to exceed_query_limit(control_count)
end
context 'with disabled services' do
before do
- allow(Service).to receive(:available_services_names).and_return(%w[prometheus pushover teamcity])
+ allow(Integration).to receive(:available_services_names).and_return(%w[prometheus pushover teamcity])
allow(subject).to receive(:disabled_services).and_return(%w[prometheus])
end
@@ -5843,11 +5846,11 @@ RSpec.describe Project, factory_default: :keep do
describe '#find_or_initialize_service' do
it 'avoids N+1 database queries' do
- allow(Service).to receive(:available_services_names).and_return(%w[prometheus pushover])
+ allow(Integration).to receive(:available_services_names).and_return(%w[prometheus pushover])
control_count = ActiveRecord::QueryRecorder.new { subject.find_or_initialize_service('prometheus') }.count
- allow(Service).to receive(:available_services_names).and_call_original
+ allow(Integration).to receive(:available_services_names).and_call_original
expect { subject.find_or_initialize_service('prometheus') }.not_to exceed_query_limit(control_count)
end
@@ -6301,23 +6304,31 @@ RSpec.describe Project, factory_default: :keep do
end
describe '#access_request_approvers_to_be_notified' do
- it 'returns a maximum of ten, active, non_requested maintainers of the project in recent_sign_in descending order' do
- group = create(:group, :public)
- project = create(:project, group: group)
+ let_it_be(:project) { create(:project, group: create(:group, :public)) }
+ it 'returns a maximum of ten maintainers of the project in recent_sign_in descending order' do
users = create_list(:user, 12, :with_sign_ins)
active_maintainers = users.map do |user|
- create(:project_member, :maintainer, user: user)
+ create(:project_member, :maintainer, user: user, project: project)
end
- create(:project_member, :maintainer, :blocked, project: project)
- create(:project_member, :developer, project: project)
- create(:project_member, :access_request, :maintainer, project: project)
-
- active_maintainers_in_recent_sign_in_desc_order = project.members_and_requesters.where(id: active_maintainers).order_recent_sign_in.limit(10)
+ active_maintainers_in_recent_sign_in_desc_order = project.members_and_requesters
+ .id_in(active_maintainers)
+ .order_recent_sign_in.limit(10)
expect(project.access_request_approvers_to_be_notified).to eq(active_maintainers_in_recent_sign_in_desc_order)
end
+
+ it 'returns active, non_invited, non_requested maintainers of the project' do
+ maintainer = create(:project_member, :maintainer, source: project)
+
+ create(:project_member, :developer, project: project)
+ create(:project_member, :maintainer, :invited, project: project)
+ create(:project_member, :maintainer, :access_request, project: project)
+ create(:project_member, :maintainer, :blocked, project: project)
+
+ expect(project.access_request_approvers_to_be_notified.to_a).to eq([maintainer])
+ end
end
describe '#pages_lookup_path' do
@@ -6478,17 +6489,17 @@ RSpec.describe Project, factory_default: :keep do
end
end
- describe 'with services and chat names' do
+ describe 'with integrations and chat names' do
subject { create(:project) }
- let(:service) { create(:service, project: subject) }
+ let(:integration) { create(:service, project: subject) }
before do
- create_list(:chat_name, 5, service: service)
+ create_list(:chat_name, 5, integration: integration)
end
it 'removes chat names on removal' do
- expect { subject.destroy }.to change { ChatName.count }.by(-5)
+ expect { subject.destroy! }.to change { ChatName.count }.by(-5)
end
end
@@ -6823,6 +6834,26 @@ RSpec.describe Project, factory_default: :keep do
end
end
+ describe '#parent_loaded?' do
+ let_it_be(:project) { create(:project) }
+
+ before do
+ project.namespace = create(:namespace)
+
+ project.reload
+ end
+
+ it 'is false when the parent is not loaded' do
+ expect(project.parent_loaded?).to be_falsey
+ end
+
+ it 'is true when the parent is loaded' do
+ project.parent
+
+ expect(project.parent_loaded?).to be_truthy
+ end
+ end
+
describe '#bots' do
subject { project.bots }
@@ -6889,6 +6920,105 @@ RSpec.describe Project, factory_default: :keep do
end
end
end
+
+ describe '#activity_path' do
+ it 'returns the project activity_path' do
+ expected_path = "/#{project.namespace.path}/#{project.name}/activity"
+
+ expect(project.activity_path).to eq(expected_path)
+ end
+ end
+ end
+
+ describe '#default_branch_or_main' do
+ let(:project) { create(:project, :repository) }
+
+ it 'returns default branch' do
+ expect(project.default_branch_or_main).to eq(project.default_branch)
+ end
+
+ context 'when default branch is nil' do
+ let(:project) { create(:project, :empty_repo) }
+
+ it 'returns Gitlab::DefaultBranch.value' do
+ expect(project.default_branch_or_main).to eq(Gitlab::DefaultBranch.value)
+ end
+ end
+ end
+
+ describe '#increment_statistic_value' do
+ let(:project) { build_stubbed(:project) }
+
+ subject(:increment) do
+ project.increment_statistic_value(:build_artifacts_size, -10)
+ end
+
+ it 'increments the value' do
+ expect(ProjectStatistics)
+ .to receive(:increment_statistic)
+ .with(project, :build_artifacts_size, -10)
+
+ increment
+ end
+
+ context 'when the project is scheduled for removal' do
+ let(:project) { build_stubbed(:project, pending_delete: true) }
+
+ it 'does not increment the value' do
+ expect(ProjectStatistics).not_to receive(:increment_statistic)
+
+ increment
+ end
+ end
+ end
+
+ describe 'topics' do
+ let_it_be(:project) { create(:project, tag_list: 'topic1, topic2, topic3') }
+
+ it 'topic_list returns correct string array' do
+ expect(project.topic_list).to match_array(%w[topic1 topic2 topic3])
+ end
+
+ it 'topics returns correct tag records' do
+ expect(project.topics.first.class.name).to eq('ActsAsTaggableOn::Tag')
+ expect(project.topics.map(&:name)).to match_array(%w[topic1 topic2 topic3])
+ end
+
+ context 'aliases' do
+ it 'tag_list returns correct string array' do
+ expect(project.tag_list).to match_array(%w[topic1 topic2 topic3])
+ end
+
+ it 'tags returns correct tag records' do
+ expect(project.tags.first.class.name).to eq('ActsAsTaggableOn::Tag')
+ expect(project.tags.map(&:name)).to match_array(%w[topic1 topic2 topic3])
+ end
+ end
+
+ context 'intermediate state during background migration' do
+ before do
+ project.taggings.first.update!(context: 'tags')
+ project.instance_variable_set("@tag_list", nil)
+ project.reload
+ end
+
+ it 'tag_list returns string array including old and new topics' do
+ expect(project.tag_list).to match_array(%w[topic1 topic2 topic3])
+ end
+
+ it 'tags returns old and new tag records' do
+ expect(project.tags.first.class.name).to eq('ActsAsTaggableOn::Tag')
+ expect(project.tags.map(&:name)).to match_array(%w[topic1 topic2 topic3])
+ expect(project.taggings.map(&:context)).to match_array(%w[tags topics topics])
+ end
+
+ it 'update tag_list adds new topics and removes old topics' do
+ project.update!(tag_list: 'topic1, topic2, topic3, topic4')
+
+ expect(project.tags.map(&:name)).to match_array(%w[topic1 topic2 topic3 topic4])
+ expect(project.taggings.map(&:context)).to match_array(%w[topics topics topics topics])
+ end
+ end
end
def finish_job(export_job)
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index bbc056889d6..ce75e68de32 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -294,7 +294,7 @@ RSpec.describe ProjectTeam do
context 'when project is shared with group' do
before do
group = create(:group)
- project.project_group_links.create(
+ project.project_group_links.create!(
group: group,
group_access: Gitlab::Access::DEVELOPER)
@@ -309,7 +309,7 @@ RSpec.describe ProjectTeam do
context 'but share_with_group_lock is true' do
before do
- project.namespace.update(share_with_group_lock: true)
+ project.namespace.update!(share_with_group_lock: true)
end
it { expect(project.team.max_member_access(maintainer.id)).to eq(Gitlab::Access::NO_ACCESS) }
@@ -496,7 +496,7 @@ RSpec.describe ProjectTeam do
project.add_guest(promoted_guest)
project.add_guest(guest)
- project.project_group_links.create(
+ project.project_group_links.create!(
group: group,
group_access: Gitlab::Access::DEVELOPER
)
@@ -505,7 +505,7 @@ RSpec.describe ProjectTeam do
group.add_developer(group_developer)
group.add_developer(second_developer)
- project.project_group_links.create(
+ project.project_group_links.create!(
group: second_group,
group_access: Gitlab::Access::MAINTAINER
)
diff --git a/spec/models/release_highlight_spec.rb b/spec/models/release_highlight_spec.rb
index 673451b5e76..b4dff4c33ff 100644
--- a/spec/models/release_highlight_spec.rb
+++ b/spec/models/release_highlight_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do
before do
allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob)
+ Gitlab::CurrentSettings.update!(whats_new_variant: ApplicationSetting.whats_new_variants[:all_tiers])
end
after do
@@ -24,16 +25,16 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do
subject { ReleaseHighlight.paginated(page: page) }
context 'when there is another page of results' do
- let(:page) { 2 }
+ let(:page) { 3 }
it 'responds with paginated results' do
expect(subject[:items].first['title']).to eq('bright')
- expect(subject[:next_page]).to eq(3)
+ expect(subject[:next_page]).to eq(4)
end
end
context 'when there is NOT another page of results' do
- let(:page) { 3 }
+ let(:page) { 4 }
it 'responds with paginated results and no next_page' do
expect(subject[:items].first['title']).to eq("It's gonna be a bright")
@@ -54,8 +55,8 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do
subject { ReleaseHighlight.paginated }
it 'uses multiple levels of cache' do
- expect(Rails.cache).to receive(:fetch).with("release_highlight:items:page-1:#{Gitlab.revision}", { expires_in: described_class::CACHE_DURATION }).and_call_original
- expect(Rails.cache).to receive(:fetch).with("release_highlight:file_paths:#{Gitlab.revision}", { expires_in: described_class::CACHE_DURATION }).and_call_original
+ expect(Rails.cache).to receive(:fetch).with("release_highlight:all_tiers:items:page-1:#{Gitlab.revision}", { expires_in: described_class::CACHE_DURATION }).and_call_original
+ expect(Rails.cache).to receive(:fetch).with("release_highlight:all_tiers:file_paths:#{Gitlab.revision}", { expires_in: described_class::CACHE_DURATION }).and_call_original
subject
end
@@ -101,7 +102,7 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do
subject { ReleaseHighlight.most_recent_item_count }
it 'uses process memory cache' do
- expect(Gitlab::ProcessMemoryCache.cache_backend).to receive(:fetch).with("release_highlight:recent_item_count:#{Gitlab.revision}", expires_in: described_class::CACHE_DURATION)
+ expect(Gitlab::ProcessMemoryCache.cache_backend).to receive(:fetch).with("release_highlight:all_tiers:recent_item_count:#{Gitlab.revision}", expires_in: described_class::CACHE_DURATION)
subject
end
@@ -127,7 +128,7 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do
subject { ReleaseHighlight.most_recent_version_digest }
it 'uses process memory cache' do
- expect(Gitlab::ProcessMemoryCache.cache_backend).to receive(:fetch).with("release_highlight:most_recent_version_digest:#{Gitlab.revision}", expires_in: described_class::CACHE_DURATION)
+ expect(Gitlab::ProcessMemoryCache.cache_backend).to receive(:fetch).with("release_highlight:all_tiers:most_recent_version_digest:#{Gitlab.revision}", expires_in: described_class::CACHE_DURATION)
subject
end
@@ -148,6 +149,33 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do
end
end
+ describe '.load_items' do
+ context 'whats new for all tiers' do
+ before do
+ Gitlab::CurrentSettings.update!(whats_new_variant: ApplicationSetting.whats_new_variants[:all_tiers])
+ end
+
+ it 'returns all items' do
+ items = described_class.load_items(page: 2)
+
+ expect(items.count).to eq(3)
+ end
+ end
+
+ context 'whats new for current tier only' do
+ before do
+ Gitlab::CurrentSettings.update!(whats_new_variant: ApplicationSetting.whats_new_variants[:current_tier])
+ end
+
+ it 'returns items with package=Free' do
+ items = described_class.load_items(page: 2)
+
+ expect(items.count).to eq(1)
+ expect(items.first['title']).to eq("View epics on a board")
+ end
+ end
+ end
+
describe 'QueryResult' do
subject { ReleaseHighlight::QueryResult.new(items: items, next_page: 2) }
@@ -157,4 +185,12 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do
expect(subject.map(&:to_s)).to eq(items.map(&:to_s))
end
end
+
+ describe '.current_package' do
+ subject { described_class.current_package }
+
+ it 'returns Free' do
+ expect(subject).to eq('Free')
+ end
+ end
end
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index 540a8068b20..b88813b3328 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Release do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) }
+
let(:release) { create(:release, project: project, author: user) }
it { expect(release).to be_valid }
@@ -37,6 +38,18 @@ RSpec.describe Release do
end
end
+ context 'when description of a release is longer than the limit' do
+ let(:description) { 'a' * (Gitlab::Database::MAX_TEXT_SIZE_LIMIT + 1) }
+ let(:release) { build(:release, project: project, description: description) }
+
+ it 'creates a validation error' do
+ release.validate
+
+ expect(release.errors.full_messages)
+ .to include("Description is too long (maximum is #{Gitlab::Database::MAX_TEXT_SIZE_LIMIT} characters)")
+ end
+ end
+
context 'when a release is tied to a milestone for another project' do
it 'creates a validation error' do
milestone = build(:milestone, project: create(:project))
@@ -53,7 +66,7 @@ RSpec.describe Release do
end
describe '#assets_count' do
- subject { release.assets_count }
+ subject { Release.find(release.id).assets_count }
it 'returns the number of sources' do
is_expected.to eq(Gitlab::Workhorse::ARCHIVE_FORMATS.count)
@@ -67,7 +80,7 @@ RSpec.describe Release do
end
it "excludes sources count when asked" do
- assets_count = release.assets_count(except: [:sources])
+ assets_count = Release.find(release.id).assets_count(except: [:sources])
expect(assets_count).to eq(1)
end
end
diff --git a/spec/models/releases/evidence_spec.rb b/spec/models/releases/evidence_spec.rb
index ca5d4b67b59..59133b2fa51 100644
--- a/spec/models/releases/evidence_spec.rb
+++ b/spec/models/releases/evidence_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Releases::Evidence do
let_it_be(:project) { create(:project) }
+
let(:release) { create(:release, project: project) }
describe 'associations' do
diff --git a/spec/models/releases/source_spec.rb b/spec/models/releases/source_spec.rb
index d10b2140550..227085951c0 100644
--- a/spec/models/releases/source_spec.rb
+++ b/spec/models/releases/source_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Releases::Source do
let_it_be(:project) { create(:project, :repository, name: 'finance-cal') }
+
let(:tag_name) { 'v1.0' }
describe '.all' do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index a739f523008..7748846f6a5 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1006,19 +1006,58 @@ RSpec.describe Repository do
end
end
- context 'when specifying a path with wildcard' do
- let(:path) { 'files/*/*.png' }
+ context 'when specifying a wildcard path' do
+ let(:path) { '*.md' }
+
+ it 'returns files matching the path in the root folder' do
+ expect(result).to contain_exactly('CONTRIBUTING.md',
+ 'MAINTENANCE.md',
+ 'PROCESS.md',
+ 'README.md')
+ end
+ end
+
+ context 'when specifying a wildcard path for all' do
+ let(:path) { '**.md' }
+
+ it 'returns all matching files in all folders' do
+ expect(result).to contain_exactly('CONTRIBUTING.md',
+ 'MAINTENANCE.md',
+ 'PROCESS.md',
+ 'README.md',
+ 'files/markdown/ruby-style-guide.md',
+ 'with space/README.md')
+ end
+ end
+
+ context 'when specifying a path to subfolders using two asterisks and a slash' do
+ let(:path) { 'files/**/*.md' }
it 'returns all files matching the path' do
- expect(result).to contain_exactly('files/images/logo-black.png',
- 'files/images/logo-white.png')
+ expect(result).to contain_exactly('files/markdown/ruby-style-guide.md')
+ end
+ end
+
+ context 'when specifying a wildcard path to subfolder with just two asterisks' do
+ let(:path) { 'files/**.md' }
+
+ it 'returns all files in the matching path' do
+ expect(result).to contain_exactly('files/markdown/ruby-style-guide.md')
+ end
+ end
+
+ context 'when specifying a wildcard path to subfolder with one asterisk' do
+ let(:path) { 'files/*/*.md' }
+
+ it 'returns all files in the matching path' do
+ expect(result).to contain_exactly('files/markdown/ruby-style-guide.md')
end
end
- context 'when specifying an extension with wildcard' do
- let(:path) { '*.rb' }
+ context 'when specifying a wildcard path for an unknown number of subfolder levels' do
+ let(:path) { '**/*.rb' }
- it 'returns all files matching the extension' do
+ it 'returns all matched files in all subfolders' do
expect(result).to contain_exactly('encoding/russian.rb',
'files/ruby/popen.rb',
'files/ruby/regex.rb',
@@ -1026,6 +1065,14 @@ RSpec.describe Repository do
end
end
+ context 'when specifying a wildcard path to one level of subfolders' do
+ let(:path) { '*/*.rb' }
+
+ it 'returns all matched files in one subfolder' do
+ expect(result).to contain_exactly('encoding/russian.rb')
+ end
+ end
+
context 'when sending regexp' do
let(:path) { '.*\.rb' }
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
deleted file mode 100644
index d8eb4ebc432..00000000000
--- a/spec/models/service_spec.rb
+++ /dev/null
@@ -1,887 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Service do
- using RSpec::Parameterized::TableSyntax
-
- let_it_be(:group) { create(:group) }
- let_it_be(:project) { create(:project, group: group) }
-
- describe "Associations" do
- it { is_expected.to belong_to :project }
- it { is_expected.to belong_to :group }
- it { is_expected.to have_one :service_hook }
- it { is_expected.to have_one :jira_tracker_data }
- it { is_expected.to have_one :issue_tracker_data }
- end
-
- describe 'validations' do
- it { is_expected.to validate_presence_of(:type) }
-
- where(:project_id, :group_id, :template, :instance, :valid) do
- 1 | nil | false | false | true
- nil | 1 | false | false | true
- nil | nil | true | false | true
- nil | nil | false | true | true
- nil | nil | false | false | false
- nil | nil | true | true | false
- 1 | 1 | false | false | false
- 1 | nil | true | false | false
- 1 | nil | false | true | false
- nil | 1 | true | false | false
- nil | 1 | false | true | false
- end
-
- with_them do
- it 'validates the service' do
- expect(build(:service, project_id: project_id, group_id: group_id, template: template, instance: instance).valid?).to eq(valid)
- end
- end
-
- context 'with existing services' do
- before_all do
- create(:service, :template)
- create(:service, :instance)
- create(:service, project: project)
- create(:service, group: group, project: nil)
- end
-
- it 'allows only one service template per type' do
- expect(build(:service, :template)).to be_invalid
- end
-
- it 'allows only one instance service per type' do
- expect(build(:service, :instance)).to be_invalid
- end
-
- it 'allows only one project service per type' do
- expect(build(:service, project: project)).to be_invalid
- end
-
- it 'allows only one group service per type' do
- expect(build(:service, group: group, project: nil)).to be_invalid
- end
- end
- end
-
- describe 'Scopes' do
- describe '.by_type' do
- let!(:service1) { create(:jira_service) }
- let!(:service2) { create(:jira_service) }
- let!(:service3) { create(:redmine_service) }
-
- subject { described_class.by_type(type) }
-
- context 'when type is "JiraService"' do
- let(:type) { 'JiraService' }
-
- it { is_expected.to match_array([service1, service2]) }
- end
-
- context 'when type is "RedmineService"' do
- let(:type) { 'RedmineService' }
-
- it { is_expected.to match_array([service3]) }
- end
- end
-
- describe '.for_group' do
- let!(:service1) { create(:jira_service, project_id: nil, group_id: group.id) }
- let!(:service2) { create(:jira_service) }
-
- it 'returns the right group service' do
- expect(described_class.for_group(group)).to match_array([service1])
- end
- end
-
- describe '.confidential_note_hooks' do
- it 'includes services where confidential_note_events is true' do
- create(:service, active: true, confidential_note_events: true)
-
- expect(described_class.confidential_note_hooks.count).to eq 1
- end
-
- it 'excludes services where confidential_note_events is false' do
- create(:service, active: true, confidential_note_events: false)
-
- expect(described_class.confidential_note_hooks.count).to eq 0
- end
- end
-
- describe '.alert_hooks' do
- it 'includes services where alert_events is true' do
- create(:service, active: true, alert_events: true)
-
- expect(described_class.alert_hooks.count).to eq 1
- end
-
- it 'excludes services where alert_events is false' do
- create(:service, active: true, alert_events: false)
-
- expect(described_class.alert_hooks.count).to eq 0
- end
- end
- end
-
- describe '#operating?' do
- it 'is false when the service is not active' do
- expect(build(:service).operating?).to eq(false)
- end
-
- it 'is false when the service is not persisted' do
- expect(build(:service, active: true).operating?).to eq(false)
- end
-
- it 'is true when the service is active and persisted' do
- expect(create(:service, active: true).operating?).to eq(true)
- end
- end
-
- describe "Test Button" do
- let(:service) { build(:service, project: project) }
-
- describe '#can_test?' do
- subject { service.can_test? }
-
- context 'when repository is not empty' do
- let(:project) { build(:project, :repository) }
-
- it { is_expected.to be true }
- end
-
- context 'when repository is empty' do
- let(:project) { build(:project) }
-
- it { is_expected.to be true }
- end
-
- context 'when instance-level service' do
- Service.available_services_types.each do |service_type|
- let(:service) do
- service_type.constantize.new(instance: true)
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when group-level service' do
- Service.available_services_types.each do |service_type|
- let(:service) do
- service_type.constantize.new(group_id: group.id)
- end
-
- it { is_expected.to be_falsey }
- end
- end
- end
-
- describe '#test' do
- let(:data) { 'test' }
-
- context 'when repository is not empty' do
- let(:project) { build(:project, :repository) }
-
- it 'test runs execute' do
- expect(service).to receive(:execute).with(data)
-
- service.test(data)
- end
- end
-
- context 'when repository is empty' do
- let(:project) { build(:project) }
-
- it 'test runs execute' do
- expect(service).to receive(:execute).with(data)
-
- service.test(data)
- end
- end
- end
- end
-
- describe '#project_level?' do
- it 'is true when service has a project' do
- expect(build(:service, project: project)).to be_project_level
- end
-
- it 'is false when service has no project' do
- expect(build(:service, project: nil)).not_to be_project_level
- end
- end
-
- describe '.find_or_initialize_non_project_specific_integration' do
- let!(:service1) { create(:jira_service, project_id: nil, group_id: group.id) }
- let!(:service2) { create(:jira_service) }
-
- it 'returns the right service' do
- expect(Service.find_or_initialize_non_project_specific_integration('jira', group_id: group)).to eq(service1)
- end
-
- it 'does not create a new service' do
- expect { Service.find_or_initialize_non_project_specific_integration('redmine', group_id: group) }.not_to change { Service.count }
- end
- end
-
- describe '.find_or_initialize_all_non_project_specific' do
- shared_examples 'service instances' do
- it 'returns the available service instances' do
- expect(Service.find_or_initialize_all_non_project_specific(Service.for_instance).pluck(:type)).to match_array(Service.available_services_types(include_project_specific: false))
- end
-
- it 'does not create service instances' do
- expect { Service.find_or_initialize_all_non_project_specific(Service.for_instance) }.not_to change { Service.count }
- end
- end
-
- it_behaves_like 'service instances'
-
- context 'with all existing instances' do
- before do
- Service.insert_all(
- Service.available_services_types(include_project_specific: false).map { |type| { instance: true, type: type } }
- )
- end
-
- it_behaves_like 'service instances'
-
- context 'with a previous existing service (MockCiService) and a new service (Asana)' do
- before do
- Service.insert({ type: 'MockCiService', instance: true })
- Service.delete_by(type: 'AsanaService', instance: true)
- end
-
- it_behaves_like 'service instances'
- end
- end
-
- context 'with a few existing instances' do
- before do
- create(:jira_service, :instance)
- end
-
- it_behaves_like 'service instances'
- end
- end
-
- describe 'template' do
- shared_examples 'retrieves service templates' do
- it 'returns the available service templates' do
- expect(Service.find_or_create_templates.pluck(:type)).to match_array(Service.available_services_types(include_project_specific: false))
- end
- end
-
- describe '.find_or_create_templates' do
- it 'creates service templates' do
- expect { Service.find_or_create_templates }.to change { Service.count }.from(0).to(Service.available_services_names(include_project_specific: false).size)
- end
-
- it_behaves_like 'retrieves service templates'
-
- context 'with all existing templates' do
- before do
- Service.insert_all(
- Service.available_services_types(include_project_specific: false).map { |type| { template: true, type: type } }
- )
- end
-
- it 'does not create service templates' do
- expect { Service.find_or_create_templates }.not_to change { Service.count }
- end
-
- it_behaves_like 'retrieves service templates'
-
- context 'with a previous existing service (Previous) and a new service (Asana)' do
- before do
- Service.insert({ type: 'PreviousService', template: true })
- Service.delete_by(type: 'AsanaService', template: true)
- end
-
- it_behaves_like 'retrieves service templates'
- end
- end
-
- context 'with a few existing templates' do
- before do
- create(:jira_service, :template)
- end
-
- it 'creates the rest of the service templates' do
- expect { Service.find_or_create_templates }.to change { Service.count }.from(1).to(Service.available_services_names(include_project_specific: false).size)
- end
-
- it_behaves_like 'retrieves service templates'
- end
- end
-
- describe '.build_from_integration' do
- context 'when integration is invalid' do
- let(:integration) do
- build(:prometheus_service, :template, active: true, properties: {})
- .tap { |integration| integration.save(validate: false) }
- end
-
- it 'sets service to inactive' do
- service = described_class.build_from_integration(integration, project_id: project.id)
-
- expect(service).to be_valid
- expect(service.active).to be false
- end
- end
-
- context 'when integration is an instance-level integration' do
- let(:integration) { create(:jira_service, :instance) }
-
- it 'sets inherit_from_id from integration' do
- service = described_class.build_from_integration(integration, project_id: project.id)
-
- expect(service.inherit_from_id).to eq(integration.id)
- end
- end
-
- context 'when integration is a group-level integration' do
- let(:integration) { create(:jira_service, group: group, project: nil) }
-
- it 'sets inherit_from_id from integration' do
- service = described_class.build_from_integration(integration, project_id: project.id)
-
- expect(service.inherit_from_id).to eq(integration.id)
- end
- end
-
- describe 'build issue tracker from an integration' do
- let(:url) { 'http://jira.example.com' }
- let(:api_url) { 'http://api-jira.example.com' }
- let(:username) { 'jira-username' }
- let(:password) { 'jira-password' }
- let(:data_params) do
- {
- url: url, api_url: api_url,
- username: username, password: password
- }
- end
-
- shared_examples 'service creation from an integration' do
- it 'creates a correct service for a project integration' do
- service = described_class.build_from_integration(integration, project_id: project.id)
-
- expect(service).to be_active
- expect(service.url).to eq(url)
- expect(service.api_url).to eq(api_url)
- expect(service.username).to eq(username)
- expect(service.password).to eq(password)
- expect(service.template).to eq(false)
- expect(service.instance).to eq(false)
- expect(service.project).to eq(project)
- expect(service.group).to eq(nil)
- end
-
- it 'creates a correct service for a group integration' do
- service = described_class.build_from_integration(integration, group_id: group.id)
-
- expect(service).to be_active
- expect(service.url).to eq(url)
- expect(service.api_url).to eq(api_url)
- expect(service.username).to eq(username)
- expect(service.password).to eq(password)
- expect(service.template).to eq(false)
- expect(service.instance).to eq(false)
- expect(service.project).to eq(nil)
- expect(service.group).to eq(group)
- end
- end
-
- # this will be removed as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
- context 'when data are stored in properties' do
- let(:properties) { data_params }
- let!(:integration) do
- create(:jira_service, :without_properties_callback, template: true, properties: properties.merge(additional: 'something'))
- end
-
- it_behaves_like 'service creation from an integration'
- end
-
- context 'when data are stored in separated fields' do
- let(:integration) do
- create(:jira_service, :template, data_params.merge(properties: {}))
- end
-
- it_behaves_like 'service creation from an integration'
- end
-
- context 'when data are stored in both properties and separated fields' do
- let(:properties) { data_params }
- let(:integration) do
- create(:jira_service, :without_properties_callback, active: true, template: true, properties: properties).tap do |service|
- create(:jira_tracker_data, data_params.merge(service: service))
- end
- end
-
- it_behaves_like 'service creation from an integration'
- end
- end
- end
-
- describe "for pushover service" do
- let!(:service_template) do
- PushoverService.create(
- template: true,
- properties: {
- device: 'MyDevice',
- sound: 'mic',
- priority: 4,
- api_key: '123456789'
- })
- end
-
- describe 'is prefilled for projects pushover service' do
- it "has all fields prefilled" do
- service = project.find_or_initialize_service('pushover')
-
- expect(service.template).to eq(false)
- expect(service.device).to eq('MyDevice')
- expect(service.sound).to eq('mic')
- expect(service.priority).to eq(4)
- expect(service.api_key).to eq('123456789')
- end
- end
- end
- end
-
- describe '.default_integration' do
- context 'with an instance-level service' do
- let_it_be(:instance_service) { create(:jira_service, :instance) }
-
- it 'returns the instance service' do
- expect(described_class.default_integration('JiraService', project)).to eq(instance_service)
- end
-
- it 'returns nil for nonexistent service type' do
- expect(described_class.default_integration('HipchatService', project)).to eq(nil)
- end
-
- context 'with a group service' do
- let_it_be(:group_service) { create(:jira_service, group_id: group.id, project_id: nil) }
-
- it 'returns the group service for a project' do
- expect(described_class.default_integration('JiraService', project)).to eq(group_service)
- end
-
- it 'returns the instance service for a group' do
- expect(described_class.default_integration('JiraService', group)).to eq(instance_service)
- end
-
- context 'with a subgroup' do
- let_it_be(:subgroup) { create(:group, parent: group) }
- let!(:project) { create(:project, group: subgroup) }
-
- it 'returns the closest group service for a project' do
- expect(described_class.default_integration('JiraService', project)).to eq(group_service)
- end
-
- it 'returns the closest group service for a subgroup' do
- expect(described_class.default_integration('JiraService', subgroup)).to eq(group_service)
- end
-
- context 'having a service with custom settings' do
- let!(:subgroup_service) { create(:jira_service, group_id: subgroup.id, project_id: nil) }
-
- it 'returns the closest group service for a project' do
- expect(described_class.default_integration('JiraService', project)).to eq(subgroup_service)
- end
- end
-
- context 'having a service inheriting settings' do
- let!(:subgroup_service) { create(:jira_service, group_id: subgroup.id, project_id: nil, inherit_from_id: group_service.id) }
-
- it 'returns the closest group service which does not inherit from its parent for a project' do
- expect(described_class.default_integration('JiraService', project)).to eq(group_service)
- end
- end
- end
- end
- end
- end
-
- describe '.create_from_active_default_integrations' do
- context 'with an active service template' do
- let_it_be(:template_integration) { create(:prometheus_service, :template, api_url: 'https://prometheus.template.com/') }
-
- it 'creates a service from the template' do
- described_class.create_from_active_default_integrations(project, :project_id, with_templates: true)
-
- expect(project.reload.services.size).to eq(1)
- expect(project.reload.services.first.api_url).to eq(template_integration.api_url)
- expect(project.reload.services.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
- described_class.create_from_active_default_integrations(project, :project_id, with_templates: true)
-
- expect(project.reload.services.size).to eq(1)
- expect(project.reload.services.first.api_url).to eq(instance_integration.api_url)
- expect(project.reload.services.first.inherit_from_id).to eq(instance_integration.id)
- end
-
- context 'passing a group' do
- it 'creates a service from the instance-level integration' do
- described_class.create_from_active_default_integrations(group, :group_id)
-
- expect(group.reload.services.size).to eq(1)
- expect(group.reload.services.first.api_url).to eq(instance_integration.api_url)
- expect(group.reload.services.first.inherit_from_id).to eq(instance_integration.id)
- end
- end
-
- context 'with an active group-level integration' do
- let!(:group_integration) { create(:prometheus_service, group: group, project: nil, api_url: 'https://prometheus.group.com/') }
-
- it 'creates a service from the group-level integration' do
- described_class.create_from_active_default_integrations(project, :project_id, with_templates: true)
-
- expect(project.reload.services.size).to eq(1)
- expect(project.reload.services.first.api_url).to eq(group_integration.api_url)
- expect(project.reload.services.first.inherit_from_id).to eq(group_integration.id)
- end
-
- context 'passing a group' do
- let!(:subgroup) { create(:group, parent: group) }
-
- it 'creates a service from the group-level integration' do
- described_class.create_from_active_default_integrations(subgroup, :group_id)
-
- expect(subgroup.reload.services.size).to eq(1)
- expect(subgroup.reload.services.first.api_url).to eq(group_integration.api_url)
- expect(subgroup.reload.services.first.inherit_from_id).to eq(group_integration.id)
- end
- end
-
- context 'with an active subgroup' do
- let!(:subgroup_integration) { create(:prometheus_service, group: subgroup, project: nil, api_url: 'https://prometheus.subgroup.com/') }
- let!(:subgroup) { create(:group, parent: group) }
- let(:project) { create(:project, group: subgroup) }
-
- it 'creates a service from the subgroup-level integration' do
- described_class.create_from_active_default_integrations(project, :project_id, with_templates: true)
-
- expect(project.reload.services.size).to eq(1)
- expect(project.reload.services.first.api_url).to eq(subgroup_integration.api_url)
- expect(project.reload.services.first.inherit_from_id).to eq(subgroup_integration.id)
- end
-
- context 'passing a group' do
- let!(:sub_subgroup) { create(:group, parent: subgroup) }
-
- it 'creates a service from the subgroup-level integration' do
- described_class.create_from_active_default_integrations(sub_subgroup, :group_id)
-
- expect(sub_subgroup.reload.services.size).to eq(1)
- expect(sub_subgroup.reload.services.first.api_url).to eq(subgroup_integration.api_url)
- expect(sub_subgroup.reload.services.first.inherit_from_id).to eq(subgroup_integration.id)
- end
-
- context 'having a service inheriting settings' do
- let!(:subgroup_integration) { create(:prometheus_service, group: subgroup, project: nil, inherit_from_id: group_integration.id, api_url: 'https://prometheus.subgroup.com/') }
-
- it 'creates a service from the group-level integration' do
- described_class.create_from_active_default_integrations(sub_subgroup, :group_id)
-
- expect(sub_subgroup.reload.services.size).to eq(1)
- expect(sub_subgroup.reload.services.first.api_url).to eq(group_integration.api_url)
- expect(sub_subgroup.reload.services.first.inherit_from_id).to eq(group_integration.id)
- end
- end
- end
- end
- end
- end
- end
- end
-
- describe '.inherited_descendants_from_self_or_ancestors_from' do
- let_it_be(:subgroup1) { create(:group, parent: group) }
- let_it_be(:subgroup2) { create(:group, parent: group) }
- let_it_be(:project1) { create(:project, group: subgroup1) }
- let_it_be(:project2) { create(:project, group: subgroup2) }
- let_it_be(:group_integration) { create(:prometheus_service, group: group, project: nil) }
- let_it_be(:subgroup_integration1) { create(:prometheus_service, group: subgroup1, project: nil, inherit_from_id: group_integration.id) }
- let_it_be(:subgroup_integration2) { create(:prometheus_service, group: subgroup2, project: nil) }
- let_it_be(:project_integration1) { create(:prometheus_service, group: nil, project: project1, inherit_from_id: group_integration.id) }
- let_it_be(:project_integration2) { create(:prometheus_service, group: nil, project: project2, inherit_from_id: subgroup_integration2.id) }
-
- it 'returns the groups and projects inheriting from integration ancestors', :aggregate_failures do
- expect(described_class.inherited_descendants_from_self_or_ancestors_from(group_integration)).to eq([subgroup_integration1, project_integration1])
- expect(described_class.inherited_descendants_from_self_or_ancestors_from(subgroup_integration2)).to eq([project_integration2])
- end
- end
-
- describe "{property}_changed?" do
- let(:service) do
- BambooService.create(
- project: project,
- properties: {
- bamboo_url: 'http://gitlab.com',
- username: 'mic',
- password: "password"
- }
- )
- end
-
- it "returns false when the property has not been assigned a new value" do
- service.username = "key_changed"
- expect(service.bamboo_url_changed?).to be_falsy
- end
-
- it "returns true when the property has been assigned a different value" do
- service.bamboo_url = "http://example.com"
- expect(service.bamboo_url_changed?).to be_truthy
- end
-
- it "returns true when the property has been assigned a different value twice" do
- service.bamboo_url = "http://example.com"
- service.bamboo_url = "http://example.com"
- expect(service.bamboo_url_changed?).to be_truthy
- end
-
- it "returns false when the property has been re-assigned the same value" do
- service.bamboo_url = 'http://gitlab.com'
- expect(service.bamboo_url_changed?).to be_falsy
- end
-
- it "returns false when the property has been assigned a new value then saved" do
- service.bamboo_url = 'http://example.com'
- service.save
- expect(service.bamboo_url_changed?).to be_falsy
- end
- end
-
- describe "{property}_touched?" do
- let(:service) do
- BambooService.create(
- project: project,
- properties: {
- bamboo_url: 'http://gitlab.com',
- username: 'mic',
- password: "password"
- }
- )
- end
-
- it "returns false when the property has not been assigned a new value" do
- service.username = "key_changed"
- expect(service.bamboo_url_touched?).to be_falsy
- end
-
- it "returns true when the property has been assigned a different value" do
- service.bamboo_url = "http://example.com"
- expect(service.bamboo_url_touched?).to be_truthy
- end
-
- it "returns true when the property has been assigned a different value twice" do
- service.bamboo_url = "http://example.com"
- service.bamboo_url = "http://example.com"
- expect(service.bamboo_url_touched?).to be_truthy
- end
-
- it "returns true when the property has been re-assigned the same value" do
- service.bamboo_url = 'http://gitlab.com'
- expect(service.bamboo_url_touched?).to be_truthy
- end
-
- it "returns false when the property has been assigned a new value then saved" do
- service.bamboo_url = 'http://example.com'
- service.save
- expect(service.bamboo_url_changed?).to be_falsy
- end
- end
-
- describe "{property}_was" do
- let(:service) do
- BambooService.create(
- project: project,
- properties: {
- bamboo_url: 'http://gitlab.com',
- username: 'mic',
- password: "password"
- }
- )
- end
-
- it "returns nil when the property has not been assigned a new value" do
- service.username = "key_changed"
- expect(service.bamboo_url_was).to be_nil
- end
-
- it "returns the previous value when the property has been assigned a different value" do
- service.bamboo_url = "http://example.com"
- expect(service.bamboo_url_was).to eq('http://gitlab.com')
- end
-
- it "returns initial value when the property has been re-assigned the same value" do
- service.bamboo_url = 'http://gitlab.com'
- expect(service.bamboo_url_was).to eq('http://gitlab.com')
- end
-
- it "returns initial value when the property has been assigned multiple values" do
- service.bamboo_url = "http://example.com"
- service.bamboo_url = "http://example2.com"
- expect(service.bamboo_url_was).to eq('http://gitlab.com')
- end
-
- it "returns nil when the property has been assigned a new value then saved" do
- service.bamboo_url = 'http://example.com'
- service.save
- expect(service.bamboo_url_was).to be_nil
- end
- end
-
- describe 'initialize service with no properties' do
- let(:service) do
- BugzillaService.create(
- project: project,
- project_url: 'http://gitlab.example.com'
- )
- end
-
- it 'does not raise error' do
- expect { service }.not_to raise_error
- end
-
- it 'sets data correctly' do
- expect(service.data_fields.project_url).to eq('http://gitlab.example.com')
- end
- end
-
- describe '#api_field_names' do
- let(:fake_service) do
- Class.new(Service) do
- def fields
- [
- { name: 'token' },
- { name: 'api_token' },
- { name: 'key' },
- { name: 'api_key' },
- { name: 'password' },
- { name: 'password_field' },
- { name: 'safe_field' }
- ]
- end
- end
- end
-
- let(:service) do
- fake_service.new(properties: [
- { token: 'token-value' },
- { api_token: 'api_token-value' },
- { key: 'key-value' },
- { api_key: 'api_key-value' },
- { password: 'password-value' },
- { password_field: 'password_field-value' },
- { safe_field: 'safe_field-value' }
- ])
- end
-
- it 'filters out sensitive fields' do
- expect(service.api_field_names).to eq(['safe_field'])
- end
- end
-
- context 'logging' do
- let(:service) { build(:service, project: project) }
- let(:test_message) { "test message" }
- let(:arguments) do
- {
- service_class: service.class.name,
- project_path: project.full_path,
- project_id: project.id,
- message: test_message,
- additional_argument: 'some argument'
- }
- end
-
- it 'logs info messages using json logger' do
- expect(Gitlab::JsonLogger).to receive(:info).with(arguments)
-
- service.log_info(test_message, additional_argument: 'some argument')
- end
-
- it 'logs error messages using json logger' do
- expect(Gitlab::JsonLogger).to receive(:error).with(arguments)
-
- service.log_error(test_message, additional_argument: 'some argument')
- end
-
- context 'when project is nil' do
- let(:project) { nil }
- let(:arguments) do
- {
- service_class: service.class.name,
- project_path: nil,
- project_id: nil,
- message: test_message,
- additional_argument: 'some argument'
- }
- end
-
- it 'logs info messages using json logger' do
- expect(Gitlab::JsonLogger).to receive(:info).with(arguments)
-
- service.log_info(test_message, additional_argument: 'some argument')
- end
- end
- end
-
- describe '#external_wiki?' do
- where(:type, :active, :result) do
- 'ExternalWikiService' | true | true
- 'ExternalWikiService' | false | false
- 'SlackService' | true | false
- end
-
- with_them do
- it 'returns the right result' do
- expect(build(:service, type: type, active: active).external_wiki?).to eq(result)
- end
- end
- end
-
- describe '.available_services_names' do
- it 'calls the right methods' do
- expect(described_class).to receive(:services_names).and_call_original
- expect(described_class).to receive(:dev_services_names).and_call_original
- expect(described_class).to receive(:project_specific_services_names).and_call_original
-
- described_class.available_services_names
- end
-
- it 'does not call project_specific_services_names with include_project_specific false' do
- expect(described_class).to receive(:services_names).and_call_original
- expect(described_class).to receive(:dev_services_names).and_call_original
- expect(described_class).not_to receive(:project_specific_services_names)
-
- described_class.available_services_names(include_project_specific: false)
- end
-
- it 'does not call dev_services_names with include_dev false' do
- expect(described_class).to receive(:services_names).and_call_original
- expect(described_class).not_to receive(:dev_services_names)
- expect(described_class).to receive(:project_specific_services_names).and_call_original
-
- described_class.available_services_names(include_dev: false)
- end
-
- it { expect(described_class.available_services_names).to include('jenkins') }
- end
-
- describe '.project_specific_services_names' do
- it do
- expect(described_class.project_specific_services_names)
- .to include(*described_class::PROJECT_SPECIFIC_SERVICE_NAMES)
- end
- end
-end
diff --git a/spec/models/sidebars/menu_spec.rb b/spec/models/sidebars/menu_spec.rb
deleted file mode 100644
index 320f5f1ad1e..00000000000
--- a/spec/models/sidebars/menu_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Sidebars::Menu do
- let(:menu) { described_class.new(context) }
- let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
-
- describe '#all_active_routes' do
- it 'gathers all active routes of items and the current menu' do
- menu_item1 = Sidebars::MenuItem.new(context)
- menu_item2 = Sidebars::MenuItem.new(context)
- menu_item3 = Sidebars::MenuItem.new(context)
- menu.add_item(menu_item1)
- menu.add_item(menu_item2)
- menu.add_item(menu_item3)
-
- allow(menu).to receive(:active_routes).and_return({ path: 'foo' })
- allow(menu_item1).to receive(:active_routes).and_return({ path: %w(bar test) })
- allow(menu_item2).to receive(:active_routes).and_return({ controller: 'fooc' })
- allow(menu_item3).to receive(:active_routes).and_return({ controller: 'barc' })
-
- expect(menu.all_active_routes).to eq({ path: %w(foo bar test), controller: %w(fooc barc) })
- end
-
- it 'does not include routes for non renderable items' do
- menu_item = Sidebars::MenuItem.new(context)
- menu.add_item(menu_item)
-
- allow(menu).to receive(:active_routes).and_return({ path: 'foo' })
- allow(menu_item).to receive(:render?).and_return(false)
- allow(menu_item).to receive(:active_routes).and_return({ controller: 'bar' })
-
- expect(menu.all_active_routes).to eq({ path: ['foo'] })
- end
- end
-
- describe '#render?' do
- context 'when the menus has no items' do
- it 'returns true' do
- expect(menu.render?).to be true
- end
- end
-
- context 'when the menu has items' do
- let(:menu_item) { Sidebars::MenuItem.new(context) }
-
- before do
- menu.add_item(menu_item)
- end
-
- context 'when items are not renderable' do
- it 'returns false' do
- allow(menu_item).to receive(:render?).and_return(false)
-
- expect(menu.render?).to be false
- end
- end
-
- context 'when there are renderable items' do
- it 'returns true' do
- expect(menu.render?).to be true
- end
- end
- end
- end
-end
diff --git a/spec/models/sidebars/panel_spec.rb b/spec/models/sidebars/panel_spec.rb
deleted file mode 100644
index 0e539460810..00000000000
--- a/spec/models/sidebars/panel_spec.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Sidebars::Panel do
- let(:context) { Sidebars::Context.new(current_user: nil, container: nil) }
- let(:panel) { Sidebars::Panel.new(context) }
- let(:menu1) { Sidebars::Menu.new(context) }
- let(:menu2) { Sidebars::Menu.new(context) }
-
- describe '#renderable_menus' do
- it 'returns only renderable menus' do
- panel.add_menu(menu1)
- panel.add_menu(menu2)
-
- allow(menu1).to receive(:render?).and_return(true)
- allow(menu2).to receive(:render?).and_return(false)
-
- expect(panel.renderable_menus).to eq([menu1])
- end
- end
-
- describe '#has_renderable_menus?' do
- it 'returns false when no renderable menus' do
- expect(panel.has_renderable_menus?).to be false
- end
-
- it 'returns true when no renderable menus' do
- panel.add_menu(menu1)
-
- expect(panel.has_renderable_menus?).to be true
- end
- end
-end
diff --git a/spec/models/sidebars/projects/context_spec.rb b/spec/models/sidebars/projects/context_spec.rb
deleted file mode 100644
index 44578ae1583..00000000000
--- a/spec/models/sidebars/projects/context_spec.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Sidebars::Projects::Context do
- let(:project) { build(:project) }
-
- subject { described_class.new(current_user: nil, container: project) }
-
- it 'sets project attribute reader' do
- expect(subject.project).to eq(project)
- end
-end
diff --git a/spec/models/sidebars/projects/menus/learn_gitlab/menu_spec.rb b/spec/models/sidebars/projects/menus/learn_gitlab/menu_spec.rb
deleted file mode 100644
index bc1815558d3..00000000000
--- a/spec/models/sidebars/projects/menus/learn_gitlab/menu_spec.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Sidebars::Projects::Menus::LearnGitlab::Menu do
- let(:project) { build(:project) }
- let(:experiment_enabled) { true }
- let(:context) { Sidebars::Projects::Context.new(current_user: nil, container: project, learn_gitlab_experiment_enabled: experiment_enabled) }
-
- subject { described_class.new(context) }
-
- it 'does not contain any sub menu' do
- expect(subject.instance_variable_get(:@items)).to be_empty
- end
-
- describe '#render?' do
- context 'when learn gitlab experiment is enabled' do
- it 'returns true' do
- expect(subject.render?).to eq true
- end
- end
-
- context 'when learn gitlab experiment is disabled' do
- let(:experiment_enabled) { false }
-
- it 'returns false' do
- expect(subject.render?).to eq false
- end
- end
- end
-end
diff --git a/spec/models/sidebars/projects/menus/project_overview/menu_items/releases_spec.rb b/spec/models/sidebars/projects/menus/project_overview/menu_items/releases_spec.rb
deleted file mode 100644
index db124c2252e..00000000000
--- a/spec/models/sidebars/projects/menus/project_overview/menu_items/releases_spec.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Sidebars::Projects::Menus::ProjectOverview::MenuItems::Releases do
- let_it_be(:project) { create(:project, :repository) }
-
- let(:user) { project.owner }
- let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
-
- subject { described_class.new(context) }
-
- describe '#render?' do
- context 'when project repository is empty' do
- it 'returns false' do
- allow(project).to receive(:empty_repo?).and_return(true)
-
- expect(subject.render?).to eq false
- end
- end
-
- context 'when project repository is not empty' do
- context 'when user can read releases' do
- it 'returns true' do
- expect(subject.render?).to eq true
- end
- end
-
- context 'when user cannot read releases' do
- let(:user) { nil }
-
- it 'returns false' do
- expect(subject.render?).to eq false
- end
- end
- end
- end
-end
diff --git a/spec/models/sidebars/projects/menus/project_overview/menu_spec.rb b/spec/models/sidebars/projects/menus/project_overview/menu_spec.rb
deleted file mode 100644
index 105a28ce953..00000000000
--- a/spec/models/sidebars/projects/menus/project_overview/menu_spec.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Sidebars::Projects::Menus::ProjectOverview::Menu do
- let(:project) { build(:project) }
- let(:context) { Sidebars::Projects::Context.new(current_user: nil, container: project) }
-
- subject { described_class.new(context) }
-
- it 'has the required items' do
- items = subject.instance_variable_get(:@items)
-
- expect(items[0]).to be_a(Sidebars::Projects::Menus::ProjectOverview::MenuItems::Details)
- expect(items[1]).to be_a(Sidebars::Projects::Menus::ProjectOverview::MenuItems::Activity)
- expect(items[2]).to be_a(Sidebars::Projects::Menus::ProjectOverview::MenuItems::Releases)
- end
-end
diff --git a/spec/models/sidebars/projects/menus/repository/menu_spec.rb b/spec/models/sidebars/projects/menus/repository/menu_spec.rb
deleted file mode 100644
index 04eb3357a6f..00000000000
--- a/spec/models/sidebars/projects/menus/repository/menu_spec.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Sidebars::Projects::Menus::Repository::Menu do
- let_it_be(:project) { create(:project, :repository) }
-
- let(:user) { project.owner }
- let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
-
- subject { described_class.new(context) }
-
- describe '#render?' do
- context 'when project repository is empty' do
- it 'returns false' do
- allow(project).to receive(:empty_repo?).and_return(true)
-
- expect(subject.render?).to eq false
- end
- end
-
- context 'when project repository is not empty' do
- context 'when user can download code' do
- it 'returns true' do
- expect(subject.render?).to eq true
- end
- end
-
- context 'when user cannot download code' do
- let(:user) { nil }
-
- it 'returns false' do
- expect(subject.render?).to eq false
- end
- end
- end
- end
-end
diff --git a/spec/models/sidebars/projects/panel_spec.rb b/spec/models/sidebars/projects/panel_spec.rb
deleted file mode 100644
index bad9b17bc83..00000000000
--- a/spec/models/sidebars/projects/panel_spec.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Sidebars::Projects::Panel do
- let(:project) { build(:project) }
- let(:context) { Sidebars::Projects::Context.new(current_user: nil, container: project) }
-
- subject { described_class.new(context) }
-
- it 'has a scope menu' do
- expect(subject.scope_menu).to be_a(Sidebars::Projects::Menus::Scope::Menu)
- end
-end
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 09f9cf8e222..41991821922 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -826,18 +826,6 @@ RSpec.describe Snippet do
allow(Gitlab::CurrentSettings).to receive(:default_branch_name).and_return(default_branch)
end
- context 'when default branch in settings is "master"' do
- let(:default_branch) { 'master' }
-
- it 'does nothing' do
- expect(File.read(head_path).squish).to eq 'ref: refs/heads/master'
-
- expect(snippet.repository.raw_repository).not_to receive(:write_ref)
-
- subject
- end
- end
-
context 'when default branch in settings is different from "master"' do
let(:default_branch) { 'main' }
diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb
index 6a252b444f9..c3432907112 100644
--- a/spec/models/timelog_spec.rb
+++ b/spec/models/timelog_spec.rb
@@ -3,11 +3,12 @@
require 'spec_helper'
RSpec.describe Timelog do
- subject { build(:timelog) }
+ subject { create(:timelog) }
- let(:issue) { create(:issue) }
- let(:merge_request) { create(:merge_request) }
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:merge_request) { create(:merge_request) }
+ it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:issue).touch(true) }
it { is_expected.to belong_to(:merge_request).touch(true) }
@@ -16,6 +17,8 @@ RSpec.describe Timelog do
it { is_expected.to validate_presence_of(:time_spent) }
it { is_expected.to validate_presence_of(:user) }
+ it { expect(subject.project_id).not_to be_nil }
+
describe 'Issuable validation' do
it 'is invalid if issue_id and merge_request_id are missing' do
subject.attributes = { issue: nil, merge_request: nil }
@@ -51,27 +54,34 @@ RSpec.describe Timelog do
end
describe 'scopes' do
- describe 'for_issues_in_group' do
- it 'return timelogs created for group issues' do
- group = create(:group)
- subgroup = create(:group, parent: group)
-
- create(:issue_timelog)
- timelog1 = create(:issue_timelog, issue: create(:issue, project: create(:project, group: group)))
- timelog2 = create(:issue_timelog, issue: create(:issue, project: create(:project, group: subgroup)))
-
- expect(described_class.for_issues_in_group(group)).to contain_exactly(timelog1, timelog2)
+ let_it_be(:group) { create(:group) }
+ let_it_be(:group_project) { create(:project, :empty_repo, group: group) }
+ let_it_be(:group_issue) { create(:issue, project: group_project) }
+ let_it_be(:group_merge_request) { create(:merge_request, source_project: group_project) }
+
+ let_it_be(:subgroup) { create(:group, parent: group) }
+ let_it_be(:subgroup_project) { create(:project, :empty_repo, group: subgroup) }
+ let_it_be(:subgroup_issue) { create(:issue, project: subgroup_project) }
+ let_it_be(:subgroup_merge_request) { create(:merge_request, source_project: subgroup_project) }
+
+ let_it_be(:timelog) { create(:issue_timelog, spent_at: 65.days.ago) }
+ let_it_be(:timelog1) { create(:issue_timelog, spent_at: 15.days.ago, issue: group_issue) }
+ let_it_be(:timelog2) { create(:issue_timelog, spent_at: 5.days.ago, issue: subgroup_issue) }
+ let_it_be(:timelog3) { create(:merge_request_timelog, spent_at: 65.days.ago) }
+ let_it_be(:timelog4) { create(:merge_request_timelog, spent_at: 15.days.ago, merge_request: group_merge_request) }
+ let_it_be(:timelog5) { create(:merge_request_timelog, spent_at: 5.days.ago, merge_request: subgroup_merge_request) }
+
+ describe 'in_group' do
+ it 'return timelogs created for group issues and merge requests' do
+ expect(described_class.in_group(group)).to contain_exactly(timelog1, timelog2, timelog4, timelog5)
end
end
describe 'between_times' do
it 'returns collection of timelogs within given times' do
- create(:issue_timelog, spent_at: 65.days.ago)
- timelog1 = create(:issue_timelog, spent_at: 15.days.ago)
- timelog2 = create(:issue_timelog, spent_at: 5.days.ago)
- timelogs = described_class.between_times(20.days.ago, 1.day.ago)
+ timelogs = described_class.between_times(20.days.ago, 10.days.ago)
- expect(timelogs).to contain_exactly(timelog1, timelog2)
+ expect(timelogs).to contain_exactly(timelog1, timelog4)
end
end
end
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index c4146b347d7..caa0a886abf 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -452,11 +452,15 @@ RSpec.describe Todo do
end
end
- describe '.pluck_user_id' do
- subject { described_class.pluck_user_id }
+ describe '.distinct_user_ids' do
+ subject { described_class.distinct_user_ids }
- let_it_be(:todo) { create(:todo) }
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:todo) { create(:todo, user: user1) }
+ let_it_be(:todo) { create(:todo, user: user1) }
+ let_it_be(:todo) { create(:todo, user: user2) }
- it { is_expected.to eq([todo.user_id]) }
+ it { is_expected.to contain_exactly(user1.id, user2.id) }
end
end
diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb
index 27ddaea763d..5806f123871 100644
--- a/spec/models/user_preference_spec.rb
+++ b/spec/models/user_preference_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe UserPreference do
describe 'sort_by preferences' do
shared_examples_for 'a sort_by preference' do
it 'allows nil sort fields' do
- user_preference.update(attribute => nil)
+ user_preference.update!(attribute => nil)
expect(user_preference).to be_valid
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 3abf2a651a0..cb34917f073 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -20,6 +20,10 @@ RSpec.describe User do
it { is_expected.to include_module(AsyncDeviseEmail) }
end
+ describe 'constants' do
+ it { expect(described_class::COUNT_CACHE_VALIDITY_PERIOD).to be_a(Integer) }
+ end
+
describe 'delegations' do
it { is_expected.to delegate_method(:path).to(:namespace).with_prefix }
@@ -79,6 +83,7 @@ RSpec.describe User do
it { is_expected.to have_one(:user_detail) }
it { is_expected.to have_one(:atlassian_identity) }
it { is_expected.to have_one(:user_highest_role) }
+ it { is_expected.to have_one(:credit_card_validation) }
it { is_expected.to have_many(:snippets).dependent(:destroy) }
it { is_expected.to have_many(:members) }
it { is_expected.to have_many(:project_members) }
@@ -132,7 +137,7 @@ RSpec.describe User do
it 'creates `user_detail` when `bio` is first updated' do
user = create(:user)
- expect { user.update(bio: 'my bio') }.to change { user.user_detail.persisted? }.from(false).to(true)
+ expect { user.update!(bio: 'my bio') }.to change { user.user_detail.persisted? }.from(false).to(true)
end
end
@@ -655,9 +660,10 @@ RSpec.describe User do
it 'does not accept not verified emails' do
email = create(:email)
user = email.user
- user.update(notification_email: email.email)
+ user.notification_email = email.email
expect(user).to be_invalid
+ expect(user.errors[:notification_email]).to include('is not an email you own')
end
end
@@ -665,7 +671,7 @@ RSpec.describe User do
it 'accepts verified emails' do
email = create(:email, :confirmed, email: 'test@test.com')
user = email.user
- user.update(public_email: email.email)
+ user.notification_email = email.email
expect(user).to be_valid
end
@@ -673,9 +679,10 @@ RSpec.describe User do
it 'does not accept not verified emails' do
email = create(:email)
user = email.user
- user.update(public_email: email.email)
+ user.public_email = email.email
expect(user).to be_invalid
+ expect(user.errors[:public_email]).to include('is not an email you own')
end
end
@@ -721,6 +728,7 @@ RSpec.describe User do
let_it_be(:blocked_user) { create(:user, :blocked) }
let_it_be(:ldap_blocked_user) { create(:omniauth_user, :ldap_blocked) }
let_it_be(:blocked_pending_approval_user) { create(:user, :blocked_pending_approval) }
+ let_it_be(:banned_user) { create(:user, :banned) }
describe '.blocked' do
subject { described_class.blocked }
@@ -731,7 +739,7 @@ RSpec.describe User do
ldap_blocked_user
)
- expect(subject).not_to include(active_user, blocked_pending_approval_user)
+ expect(subject).not_to include(active_user, blocked_pending_approval_user, banned_user)
end
end
@@ -742,6 +750,14 @@ RSpec.describe User do
expect(subject).to contain_exactly(blocked_pending_approval_user)
end
end
+
+ describe '.banned' do
+ subject { described_class.banned }
+
+ it 'returns only banned users' do
+ expect(subject).to contain_exactly(banned_user)
+ end
+ end
end
describe ".with_two_factor" do
@@ -1056,6 +1072,21 @@ RSpec.describe User do
.to contain_exactly(user)
end
end
+
+ describe '.for_todos' do
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:issue) { create(:issue) }
+
+ let_it_be(:todo1) { create(:todo, target: issue, author: user1, user: user1) }
+ let_it_be(:todo2) { create(:todo, target: issue, author: user1, user: user1) }
+ let_it_be(:todo3) { create(:todo, target: issue, author: user2, user: user2) }
+
+ it 'returns users for the given todos' do
+ expect(described_class.for_todos(issue.todos))
+ .to contain_exactly(user1, user2)
+ end
+ end
end
describe "Respond to" do
@@ -1274,7 +1305,7 @@ RSpec.describe User do
let(:secondary) { create(:email, :confirmed, email: 'secondary@example.com', user: user) }
before do
- user.emails.create(email_attrs)
+ user.emails.create!(email_attrs)
user.tap { |u| u.update!(notification_email: email_attrs[:email]) }.reload
end
@@ -1366,6 +1397,26 @@ RSpec.describe User do
end
end
+ describe '#credit_card_validated_at' do
+ let_it_be(:user) { create(:user) }
+
+ context 'when credit_card_validation does not exist' do
+ it 'returns nil' do
+ expect(user.credit_card_validated_at).to be nil
+ end
+ end
+
+ context 'when credit_card_validation exists' do
+ it 'returns the credit card validated time' do
+ credit_card_validated_time = Time.current - 1.day
+
+ create(:credit_card_validation, credit_card_validated_at: credit_card_validated_time, user: user)
+
+ expect(user.credit_card_validated_at).to eq(credit_card_validated_time)
+ end
+ end
+ end
+
describe '#update_tracked_fields!', :clean_gitlab_redis_shared_state do
let(:request) { OpenStruct.new(remote_ip: "127.0.0.1") }
let(:user) { create(:user) }
@@ -1428,7 +1479,7 @@ RSpec.describe User do
let!(:accessible_deploy_keys_project) { create(:deploy_keys_project, project: project) }
before do
- public_deploy_keys_project.deploy_key.update(public: true)
+ public_deploy_keys_project.deploy_key.update!(public: true)
project.add_developer(user)
end
@@ -1518,13 +1569,13 @@ RSpec.describe User do
it 'receives callback when external changes' do
expect(user).to receive(:ensure_user_rights_and_limits)
- user.update(external: false)
+ user.update!(external: false)
end
it 'ensures correct rights and limits for user' do
stub_config_setting(default_can_create_group: true)
- expect { user.update(external: false) }.to change { user.can_create_group }.to(true)
+ expect { user.update!(external: false) }.to change { user.can_create_group }.to(true)
.and change { user.projects_limit }.to(Gitlab::CurrentSettings.default_projects_limit)
end
end
@@ -1535,11 +1586,11 @@ RSpec.describe User do
it 'receives callback when external changes' do
expect(user).to receive(:ensure_user_rights_and_limits)
- user.update(external: true)
+ user.update!(external: true)
end
it 'ensures correct rights and limits for user' do
- expect { user.update(external: true) }.to change { user.can_create_group }.to(false)
+ expect { user.update!(external: true) }.to change { user.can_create_group }.to(false)
.and change { user.projects_limit }.to(0)
end
end
@@ -1892,6 +1943,12 @@ RSpec.describe User do
expect(described_class.filter_items('blocked')).to include user
end
+ it 'filters by banned' do
+ expect(described_class).to receive(:banned).and_return([user])
+
+ expect(described_class.filter_items('banned')).to include user
+ end
+
it 'filters by blocked pending approval' do
expect(described_class).to receive(:blocked_pending_approval).and_return([user])
@@ -2435,7 +2492,7 @@ RSpec.describe User do
end
context 'with a redirect route matching the given path' do
- let!(:redirect_route) { user.namespace.redirect_routes.create(path: 'foo') }
+ let!(:redirect_route) { user.namespace.redirect_routes.create!(path: 'foo') }
context 'without the follow_redirects option' do
it 'returns nil' do
@@ -2511,8 +2568,9 @@ RSpec.describe User do
it 'is false if avatar is html page' do
user.update_attribute(:avatar, 'uploads/avatar.html')
+ user.avatar_type
- expect(user.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp'])
+ expect(user.errors.added?(:avatar, "file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp")).to be true
end
end
@@ -2535,7 +2593,7 @@ RSpec.describe User do
expect(Gitlab::AvatarCache).to receive(:delete_by_email).with(*user.verified_emails)
- user.update(avatar: fixture_file_upload('spec/fixtures/dk.png'))
+ user.update!(avatar: fixture_file_upload('spec/fixtures/dk.png'))
end
end
@@ -2849,12 +2907,12 @@ RSpec.describe User do
expect(user.starred?(project1)).to be_truthy
expect(user.starred?(project2)).to be_truthy
- star1.destroy
+ star1.destroy!
expect(user.starred?(project1)).to be_falsey
expect(user.starred?(project2)).to be_truthy
- star2.destroy
+ star2.destroy!
expect(user.starred?(project1)).to be_falsey
expect(user.starred?(project2)).to be_falsey
@@ -3404,7 +3462,7 @@ RSpec.describe User do
expect(user.authorized_projects).to include(project)
- member.destroy
+ member.destroy!
expect(user.authorized_projects).not_to include(project)
end
@@ -3429,7 +3487,7 @@ RSpec.describe User do
expect(user2.authorized_projects).to include(project)
- project.destroy
+ project.destroy!
expect(user2.authorized_projects).not_to include(project)
end
@@ -3443,7 +3501,7 @@ RSpec.describe User do
expect(user.authorized_projects).to include(project)
- group.destroy
+ group.destroy!
expect(user.authorized_projects).not_to include(project)
end
@@ -4200,16 +4258,45 @@ RSpec.describe User do
end
describe '#invalidate_issue_cache_counts' do
- let(:user) { build_stubbed(:user) }
+ let_it_be(:user) { create(:user) }
- it 'invalidates cache for issue counter' do
- cache_mock = double
+ subject do
+ user.invalidate_issue_cache_counts
+ user.save!
+ end
- expect(cache_mock).to receive(:delete).with(['users', user.id, 'assigned_open_issues_count'])
+ shared_examples 'invalidates the cached value' do
+ it 'invalidates cache for issue counter' do
+ expect(Rails.cache).to receive(:delete).with(['users', user.id, 'assigned_open_issues_count'])
- allow(Rails).to receive(:cache).and_return(cache_mock)
+ subject
+ end
+ end
- user.invalidate_issue_cache_counts
+ it_behaves_like 'invalidates the cached value'
+
+ context 'if feature flag assigned_open_issues_cache is enabled' do
+ it 'calls the recalculate worker' do
+ expect(Users::UpdateOpenIssueCountWorker).to receive(:perform_async).with(user.id)
+
+ subject
+ end
+
+ it_behaves_like 'invalidates the cached value'
+ end
+
+ context 'if feature flag assigned_open_issues_cache is disabled' do
+ before do
+ stub_feature_flags(assigned_open_issues_cache: false)
+ end
+
+ it 'does not call the recalculate worker' do
+ expect(Users::UpdateOpenIssueCountWorker).not_to receive(:perform_async).with(user.id)
+
+ subject
+ end
+
+ it_behaves_like 'invalidates the cached value'
end
end
@@ -4414,9 +4501,10 @@ RSpec.describe User do
end
it 'adds the namespace errors to the user' do
- user.update(username: new_username)
+ user.username = new_username
- expect(user.errors.full_messages.first).to eq('A user, alias, or group already exists with that username.')
+ expect(user).to be_invalid
+ expect(user.errors[:base]).to include('A user, alias, or group already exists with that username.')
end
end
end
@@ -5238,6 +5326,26 @@ RSpec.describe User do
end
end
+ describe 'user credit card validation' do
+ context 'when user is initialized' do
+ let(:user) { build(:user) }
+
+ it { expect(user.credit_card_validation).not_to be_present }
+ end
+
+ context 'when create user without credit card validation' do
+ let(:user) { create(:user) }
+
+ it { expect(user.credit_card_validation).not_to be_present }
+ end
+
+ context 'when user credit card validation exists' do
+ let(:user) { create(:user, :with_credit_card_validation) }
+
+ it { expect(user.credit_card_validation).to be_persisted }
+ end
+ end
+
describe 'user detail' do
context 'when user is initialized' do
let(:user) { build(:user) }
@@ -5307,21 +5415,21 @@ RSpec.describe User do
with_them do
context 'when state was changed' do
- subject { user.update(attributes) }
+ subject { user.update!(attributes) }
include_examples 'update highest role with exclusive lease'
end
end
context 'when state was not changed' do
- subject { user.update(email: 'newmail@example.com') }
+ subject { user.update!(email: 'newmail@example.com') }
include_examples 'does not update the highest role'
end
end
describe 'destroy user' do
- subject { user.destroy }
+ subject { user.destroy! }
include_examples 'does not update the highest role'
end
@@ -5343,7 +5451,7 @@ RSpec.describe User do
context 'when user is a ghost user' do
before do
- user.update(user_type: :ghost)
+ user.update!(user_type: :ghost)
end
it { is_expected.to be false }
@@ -5361,7 +5469,7 @@ RSpec.describe User do
with_them do
before do
- user.update(user_type: user_type)
+ user.update!(user_type: user_type)
end
it { is_expected.to be expected_result }
@@ -5384,7 +5492,7 @@ RSpec.describe User do
context 'when user is an internal user' do
before do
- user.update(user_type: :ghost)
+ user.update!(user_type: :ghost)
end
it { is_expected.to be :forbidden }
@@ -5418,7 +5526,7 @@ RSpec.describe User do
context 'when user is an internal user' do
before do
- user.update(user_type: 'alert_bot')
+ user.update!(user_type: 'alert_bot')
end
it_behaves_like 'does not require password to be present'
@@ -5426,7 +5534,7 @@ RSpec.describe User do
context 'when user is a project bot user' do
before do
- user.update(user_type: 'project_bot')
+ user.update!(user_type: 'project_bot')
end
it_behaves_like 'does not require password to be present'
@@ -5600,4 +5708,47 @@ RSpec.describe User do
end
end
end
+
+ describe '.dormant' do
+ it 'returns dormant users' do
+ freeze_time do
+ not_that_long_ago = (described_class::MINIMUM_INACTIVE_DAYS - 1).days.ago.to_date
+ too_long_ago = described_class::MINIMUM_INACTIVE_DAYS.days.ago.to_date
+
+ create(:user, :deactivated, last_activity_on: too_long_ago)
+
+ User::INTERNAL_USER_TYPES.map do |user_type|
+ create(:user, state: :active, user_type: user_type, last_activity_on: too_long_ago)
+ end
+
+ create(:user, last_activity_on: not_that_long_ago)
+
+ dormant_user = create(:user, last_activity_on: too_long_ago)
+
+ expect(described_class.dormant).to contain_exactly(dormant_user)
+ end
+ end
+ end
+
+ describe '.with_no_activity' do
+ it 'returns users with no activity' do
+ freeze_time do
+ not_that_long_ago = (described_class::MINIMUM_INACTIVE_DAYS - 1).days.ago.to_date
+ too_long_ago = described_class::MINIMUM_INACTIVE_DAYS.days.ago.to_date
+
+ create(:user, :deactivated, last_activity_on: nil)
+
+ User::INTERNAL_USER_TYPES.map do |user_type|
+ create(:user, state: :active, user_type: user_type, last_activity_on: nil)
+ end
+
+ create(:user, last_activity_on: not_that_long_ago)
+ create(:user, last_activity_on: too_long_ago)
+
+ user_with_no_activity = create(:user, last_activity_on: nil)
+
+ expect(described_class.with_no_activity).to contain_exactly(user_with_no_activity)
+ end
+ end
+ end
end
diff --git a/spec/models/user_status_spec.rb b/spec/models/user_status_spec.rb
index 51dd91149cc..87d1fa14aca 100644
--- a/spec/models/user_status_spec.rb
+++ b/spec/models/user_status_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe UserStatus do
it 'is expected to be deleted when the user is deleted' do
status = create(:user_status)
- expect { status.user.destroy }.to change { described_class.count }.from(1).to(0)
+ expect { status.user.destroy! }.to change { described_class.count }.from(1).to(0)
end
describe '#clear_status_after=' do
diff --git a/spec/models/users/credit_card_validation_spec.rb b/spec/models/users/credit_card_validation_spec.rb
new file mode 100644
index 00000000000..fb9f6e35038
--- /dev/null
+++ b/spec/models/users/credit_card_validation_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Users::CreditCardValidation do
+ it { is_expected.to belong_to(:user) }
+end
diff --git a/spec/models/wiki_page/meta_spec.rb b/spec/models/wiki_page/meta_spec.rb
index 24906d4fb79..37a282657d9 100644
--- a/spec/models/wiki_page/meta_spec.rb
+++ b/spec/models/wiki_page/meta_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe WikiPage::Meta do
subject { described_class.find(meta.id) }
let_it_be(:meta) do
- described_class.create(title: generate(:wiki_page_title), project: project)
+ described_class.create!(title: generate(:wiki_page_title), project: project)
end
context 'there are no slugs' do
@@ -124,6 +124,7 @@ RSpec.describe WikiPage::Meta do
context 'the slug is already in the DB (but not canonical)' do
let_it_be(:slug_record) { create(:wiki_page_slug, wiki_page_meta: meta) }
+
let(:slug) { slug_record.slug }
let(:query_limit) { 4 }
@@ -132,6 +133,7 @@ RSpec.describe WikiPage::Meta do
context 'the slug is already in the DB (and canonical)' do
let_it_be(:slug_record) { create(:wiki_page_slug, :canonical, wiki_page_meta: meta) }
+
let(:slug) { slug_record.slug }
let(:query_limit) { 4 }
@@ -181,7 +183,7 @@ RSpec.describe WikiPage::Meta do
# an old slug that = canonical_slug
different_slug = generate(:sluggified_title)
create(:wiki_page_meta, project: project, canonical_slug: different_slug)
- .slugs.create(slug: wiki_page.slug)
+ .slugs.create!(slug: wiki_page.slug)
end
shared_examples 'metadata examples' do
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index be94eca550c..579a9e664cf 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -620,16 +620,12 @@ RSpec.describe WikiPage do
end
describe "#versions" do
- include_context 'subject is persisted page'
+ let(:subject) { create_wiki_page }
it "returns an array of all commits for the page" do
- 3.times { |i| subject.update(content: "content #{i}") }
-
- expect(subject.versions.count).to eq(4)
- end
-
- it 'returns instances of WikiPageVersion' do
- expect(subject.versions).to all( be_a(Gitlab::Git::WikiPageVersion) )
+ expect do
+ 3.times { |i| subject.update(content: "content #{i}") }
+ end.to change { subject.versions.count }.by(3)
end
end
@@ -640,6 +636,7 @@ RSpec.describe WikiPage do
let_it_be(:existing_page) { create_wiki_page(title: 'test page') }
let_it_be(:directory_page) { create_wiki_page(title: 'parent directory/child page') }
let_it_be(:page_with_special_characters) { create_wiki_page(title: 'test+page') }
+
let(:untitled_page) { described_class.new(wiki) }
where(:page, :title, :changed) do
@@ -776,8 +773,11 @@ RSpec.describe WikiPage do
end
describe '#historical?' do
- include_context 'subject is persisted page'
+ let!(:container) { create(:project) }
+
+ subject { create_wiki_page }
+ let(:wiki) { subject.wiki }
let(:old_version) { subject.versions.last.id }
let(:old_page) { wiki.find_page(subject.title, old_version) }
let(:latest_version) { subject.versions.first.id }